72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
// P17 HR-Hub (AK-P17a) — field-level crypto SEAM for sensitive HR values.
|
|
//
|
|
// AD-8 mandates a PER-TENANT DEK / KMS envelope for the password-safe secrets and
|
|
// the employee emergency contact. This module is the seam every HR service goes
|
|
// through, so the underlying envelope provider can be swapped without touching the
|
|
// services.
|
|
//
|
|
// The per-tenant envelope landed with R-P5 (`getTenantFieldCrypto` in
|
|
// @hivegrid/db, AD-8). `getHrFieldCrypto()` now binds to it, which lights up the
|
|
// previously deferred sensitive-field paths (vault entry secrets + emergency
|
|
// contacts). `deferredHrFieldCrypto` / `HrCryptoNotProvisionedError` are retained
|
|
// as a fail-closed scaffold: a future binding may reject with a Klartext (M002)
|
|
// error rather than silently store plaintext if the master key is unconfigured.
|
|
// Service functions still accept an explicit `HrFieldCrypto` override so unit
|
|
// tests can inject a working in-memory provider.
|
|
//
|
|
// IMPORTANT: this is a server-only module (node:crypto-adjacent). Never import it
|
|
// from packages/ui-app or view-models.
|
|
|
|
import { getTenantFieldCrypto } from "@hivegrid/db/crypto/tenant-envelope.server";
|
|
|
|
/**
|
|
* Field-level crypto contract: encrypts/decrypts a single sensitive string for a
|
|
* given tenant. Implementations MUST scope the key material per tenant (AD-8) so
|
|
* a tenant deletion can crypto-shred only that tenant's data (M165).
|
|
*/
|
|
export interface HrFieldCrypto {
|
|
/** Encrypt `plaintext` for `tenantId`; returns a self-describing envelope string. */
|
|
encrypt(tenantId: string, plaintext: string): Promise<string>;
|
|
/** Decrypt an envelope string produced by {@link encrypt} for `tenantId`. */
|
|
decrypt(tenantId: string, ciphertext: string): Promise<string>;
|
|
}
|
|
|
|
/**
|
|
* Domain error raised when a sensitive HR value is read/written before the AD-8
|
|
* per-tenant envelope layer (separate ticket) is wired. Carries an i18n message
|
|
* key (M002 Klartext) so routes surface "this needs the encryption module" — not
|
|
* a bare 500.
|
|
*/
|
|
export class HrCryptoNotProvisionedError extends Error {
|
|
readonly messageKey = "hr.error.cryptoNotProvisioned";
|
|
constructor() {
|
|
super("hr.error.cryptoNotProvisioned");
|
|
this.name = "HrCryptoNotProvisionedError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Production binding until the AD-8 envelope ticket lands: every call defers
|
|
* (throws {@link HrCryptoNotProvisionedError}). Swapping this for the real
|
|
* per-tenant envelope is the ONLY change that follow-up ticket makes here.
|
|
*/
|
|
export const deferredHrFieldCrypto: HrFieldCrypto = {
|
|
encrypt() {
|
|
return Promise.reject(new HrCryptoNotProvisionedError());
|
|
},
|
|
decrypt() {
|
|
return Promise.reject(new HrCryptoNotProvisionedError());
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The crypto binding HR services use by default: the R-P5 per-tenant envelope
|
|
* provider (AD-8). `TenantFieldCrypto` is structurally identical to
|
|
* `HrFieldCrypto`, so it satisfies this contract directly. Service functions
|
|
* accept an explicit `HrFieldCrypto` override so unit tests can inject a working
|
|
* in-memory provider.
|
|
*/
|
|
export function getHrFieldCrypto(): HrFieldCrypto {
|
|
return getTenantFieldCrypto();
|
|
}
|