@forklaunch/core 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
1
+ import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, Type, Platform, TransformContext, FilterDef, EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';
2
2
 
3
3
  declare const ComplianceLevel: {
4
4
  readonly pii: "pii";
@@ -101,6 +101,19 @@ declare function defineComplianceEntity<const TName extends string, const TTable
101
101
  userIdField?: string;
102
102
  }): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
103
103
 
104
+ /**
105
+ * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
106
+ * `nativeDelete` on entities that have PII, PHI, or PCI compliance fields.
107
+ *
108
+ * This prevents bypassing the EncryptedType's encryption by using raw
109
+ * queries. Call this in the tenant context middleware when creating the
110
+ * request-scoped EM.
111
+ *
112
+ * @returns A Proxy-wrapped EntityManager that throws on native query
113
+ * operations targeting compliance entities.
114
+ */
115
+ declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
116
+
104
117
  declare class MissingEncryptionKeyError extends Error {
105
118
  readonly name: "MissingEncryptionKeyError";
106
119
  constructor(message?: string);
@@ -122,60 +135,83 @@ declare class FieldEncryptor {
122
135
  */
123
136
  deriveKey(tenantId: string): Buffer;
124
137
  /**
125
- * Encrypt a plaintext string for a specific tenant.
138
+ * Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
139
+ * truncated to IV_BYTES. Same plaintext + same key → same IV → same
140
+ * ciphertext. This enables WHERE clause matching on encrypted columns
141
+ * while maintaining AES-256-GCM authenticated encryption.
142
+ *
143
+ * Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
144
+ */
145
+ private deriveDeterministicIv;
146
+ /**
147
+ * Encrypt a plaintext string.
126
148
  *
127
- * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
149
+ * Uses deterministic encryption (HMAC-derived IV) so the same plaintext
150
+ * always produces the same ciphertext. This enables database WHERE clause
151
+ * matching and UNIQUE constraints on encrypted columns.
152
+ *
153
+ * @returns Format: `v2:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
128
154
  */
129
155
  encrypt(plaintext: string | null): string | null;
130
156
  encrypt(plaintext: string | null, tenantId: string): string | null;
131
157
  /**
132
158
  * Decrypt a ciphertext string produced by {@link encrypt}.
159
+ * Supports both v1 (random IV) and v2 (deterministic IV) formats.
133
160
  */
134
161
  decrypt(ciphertext: string | null): string | null;
135
162
  decrypt(ciphertext: string | null, tenantId: string): string | null;
136
163
  }
137
164
 
138
165
  /**
139
- * MikroORM EventSubscriber that enforces field-level encryption for
140
- * compliance-classified fields (PHI and PCI).
166
+ * Register the FieldEncryptor instance for use by EncryptedType.
167
+ * Call this once at application bootstrap (e.g., in mikro-orm.config.ts).
168
+ */
169
+ declare function registerEncryptor(encryptor: FieldEncryptor): void;
170
+ /**
171
+ * Run a callback with the given tenant ID available to EncryptedType
172
+ * for per-tenant key derivation.
173
+ */
174
+ declare function withEncryptionContext<T>(tenantId: string, fn: () => T): T;
175
+ /**
176
+ * Set the encryption tenant ID for the current async context.
177
+ * Called automatically by the tenant context middleware — users
178
+ * don't need to call this directly.
179
+ */
180
+ declare function setEncryptionTenantId(tenantId: string): void;
181
+ /**
182
+ * Get the current tenant ID. Returns empty string when no context is set
183
+ * (startup, seeders, better-auth, background jobs).
184
+ */
185
+ declare function getCurrentTenantId(): string;
186
+ /**
187
+ * MikroORM custom Type that transparently encrypts/decrypts values.
188
+ *
189
+ * Works with any JS type (string, number, boolean, object/JSON).
190
+ * Non-string values are JSON-serialized before encryption and
191
+ * JSON-parsed after decryption.
141
192
  *
142
- * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before
143
- * database persistence. Throws `EncryptionRequiredError` if the encryption
144
- * key is unavailable.
145
- * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.
146
- * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a
147
- * console warning to support rolling deployments.
193
+ * DB column is always `text` the encrypted ciphertext is a string
194
+ * regardless of the original JS type.
148
195
  *
149
- * The tenant ID for key derivation is read from the EntityManager's filter
150
- * parameters (set by the tenant context middleware).
196
+ * Operates at the data conversion layer before the identity map,
197
+ * during hydration, and during persistence. Entities always hold
198
+ * their original JS values (plaintext).
151
199
  */
152
- declare class ComplianceEventSubscriber implements EventSubscriber {
153
- private readonly encryptor;
154
- constructor(encryptor: FieldEncryptor);
155
- beforeCreate(args: EventArgs<unknown>): Promise<void>;
156
- beforeUpdate(args: EventArgs<unknown>): Promise<void>;
157
- onLoad(args: EventArgs<unknown>): Promise<void>;
158
- private encryptFields;
159
- private decryptFields;
200
+ declare class EncryptedType extends Type<unknown, string | null> {
201
+ private readonly originalType;
160
202
  /**
161
- * Read the tenant ID from the EntityManager's filter parameters.
162
- * Returns undefined when no tenant context is set (startup, seeders,
163
- * better-auth). Callers skip encryption/decryption in that case.
203
+ * @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
204
+ * Used to determine serialization strategy.
164
205
  */
165
- private getTenantId;
206
+ constructor(originalType?: string);
207
+ convertToDatabaseValue(value: unknown, _platform: Platform, _context?: TransformContext): string | null;
208
+ convertToJSValue(value: string | null, _platform: Platform): unknown;
209
+ getColumnType(): string;
210
+ get runtimeType(): string;
211
+ ensureComparable(): boolean;
212
+ private serializeValue;
213
+ private deserializeValue;
166
214
  }
167
- /**
168
- * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
169
- * `nativeDelete` on entities that have PHI or PCI compliance fields.
170
- *
171
- * This prevents bypassing the ComplianceEventSubscriber's encryption
172
- * by using raw queries. Call this in the tenant context middleware when
173
- * creating the request-scoped EM.
174
- *
175
- * @returns A Proxy-wrapped EntityManager that throws on native query
176
- * operations targeting compliance entities.
177
- */
178
- declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
179
215
 
180
216
  /**
181
217
  * The name used to register the tenant isolation filter.
@@ -276,4 +312,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
276
312
  */
277
313
  declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
278
314
 
279
- export { ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, subtractDuration, wrapEmWithNativeQueryBlocking };
315
+ export { ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptedType, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getCurrentTenantId, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, registerEncryptor, setEncryptionTenantId, setupRls, setupTenantFilter, subtractDuration, withEncryptionContext, wrapEmWithNativeQueryBlocking };
@@ -1,4 +1,4 @@
1
- import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, EventSubscriber, EventArgs, FilterDef, EntityManager, MikroORM } from '@mikro-orm/core';
1
+ import { PropertyBuilders, EntityMetadataWithProperties, EntitySchemaWithMeta, InferEntityFromProperties, Type, Platform, TransformContext, FilterDef, EntityManager, EventSubscriber, MikroORM } from '@mikro-orm/core';
2
2
 
3
3
  declare const ComplianceLevel: {
4
4
  readonly pii: "pii";
@@ -101,6 +101,19 @@ declare function defineComplianceEntity<const TName extends string, const TTable
101
101
  userIdField?: string;
102
102
  }): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
103
103
 
104
+ /**
105
+ * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
106
+ * `nativeDelete` on entities that have PII, PHI, or PCI compliance fields.
107
+ *
108
+ * This prevents bypassing the EncryptedType's encryption by using raw
109
+ * queries. Call this in the tenant context middleware when creating the
110
+ * request-scoped EM.
111
+ *
112
+ * @returns A Proxy-wrapped EntityManager that throws on native query
113
+ * operations targeting compliance entities.
114
+ */
115
+ declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
116
+
104
117
  declare class MissingEncryptionKeyError extends Error {
105
118
  readonly name: "MissingEncryptionKeyError";
106
119
  constructor(message?: string);
@@ -122,60 +135,83 @@ declare class FieldEncryptor {
122
135
  */
123
136
  deriveKey(tenantId: string): Buffer;
124
137
  /**
125
- * Encrypt a plaintext string for a specific tenant.
138
+ * Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
139
+ * truncated to IV_BYTES. Same plaintext + same key → same IV → same
140
+ * ciphertext. This enables WHERE clause matching on encrypted columns
141
+ * while maintaining AES-256-GCM authenticated encryption.
142
+ *
143
+ * Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
144
+ */
145
+ private deriveDeterministicIv;
146
+ /**
147
+ * Encrypt a plaintext string.
126
148
  *
127
- * @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
149
+ * Uses deterministic encryption (HMAC-derived IV) so the same plaintext
150
+ * always produces the same ciphertext. This enables database WHERE clause
151
+ * matching and UNIQUE constraints on encrypted columns.
152
+ *
153
+ * @returns Format: `v2:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
128
154
  */
129
155
  encrypt(plaintext: string | null): string | null;
130
156
  encrypt(plaintext: string | null, tenantId: string): string | null;
131
157
  /**
132
158
  * Decrypt a ciphertext string produced by {@link encrypt}.
159
+ * Supports both v1 (random IV) and v2 (deterministic IV) formats.
133
160
  */
134
161
  decrypt(ciphertext: string | null): string | null;
135
162
  decrypt(ciphertext: string | null, tenantId: string): string | null;
136
163
  }
137
164
 
138
165
  /**
139
- * MikroORM EventSubscriber that enforces field-level encryption for
140
- * compliance-classified fields (PHI and PCI).
166
+ * Register the FieldEncryptor instance for use by EncryptedType.
167
+ * Call this once at application bootstrap (e.g., in mikro-orm.config.ts).
168
+ */
169
+ declare function registerEncryptor(encryptor: FieldEncryptor): void;
170
+ /**
171
+ * Run a callback with the given tenant ID available to EncryptedType
172
+ * for per-tenant key derivation.
173
+ */
174
+ declare function withEncryptionContext<T>(tenantId: string, fn: () => T): T;
175
+ /**
176
+ * Set the encryption tenant ID for the current async context.
177
+ * Called automatically by the tenant context middleware — users
178
+ * don't need to call this directly.
179
+ */
180
+ declare function setEncryptionTenantId(tenantId: string): void;
181
+ /**
182
+ * Get the current tenant ID. Returns empty string when no context is set
183
+ * (startup, seeders, better-auth, background jobs).
184
+ */
185
+ declare function getCurrentTenantId(): string;
186
+ /**
187
+ * MikroORM custom Type that transparently encrypts/decrypts values.
188
+ *
189
+ * Works with any JS type (string, number, boolean, object/JSON).
190
+ * Non-string values are JSON-serialized before encryption and
191
+ * JSON-parsed after decryption.
141
192
  *
142
- * - **onBeforeCreate / onBeforeUpdate**: Encrypts PHI/PCI fields before
143
- * database persistence. Throws `EncryptionRequiredError` if the encryption
144
- * key is unavailable.
145
- * - **onLoad**: Decrypts PHI/PCI fields after loading from the database.
146
- * Pre-migration plaintext (no `v1:` prefix) is returned as-is with a
147
- * console warning to support rolling deployments.
193
+ * DB column is always `text` the encrypted ciphertext is a string
194
+ * regardless of the original JS type.
148
195
  *
149
- * The tenant ID for key derivation is read from the EntityManager's filter
150
- * parameters (set by the tenant context middleware).
196
+ * Operates at the data conversion layer before the identity map,
197
+ * during hydration, and during persistence. Entities always hold
198
+ * their original JS values (plaintext).
151
199
  */
152
- declare class ComplianceEventSubscriber implements EventSubscriber {
153
- private readonly encryptor;
154
- constructor(encryptor: FieldEncryptor);
155
- beforeCreate(args: EventArgs<unknown>): Promise<void>;
156
- beforeUpdate(args: EventArgs<unknown>): Promise<void>;
157
- onLoad(args: EventArgs<unknown>): Promise<void>;
158
- private encryptFields;
159
- private decryptFields;
200
+ declare class EncryptedType extends Type<unknown, string | null> {
201
+ private readonly originalType;
160
202
  /**
161
- * Read the tenant ID from the EntityManager's filter parameters.
162
- * Returns undefined when no tenant context is set (startup, seeders,
163
- * better-auth). Callers skip encryption/decryption in that case.
203
+ * @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
204
+ * Used to determine serialization strategy.
164
205
  */
165
- private getTenantId;
206
+ constructor(originalType?: string);
207
+ convertToDatabaseValue(value: unknown, _platform: Platform, _context?: TransformContext): string | null;
208
+ convertToJSValue(value: string | null, _platform: Platform): unknown;
209
+ getColumnType(): string;
210
+ get runtimeType(): string;
211
+ ensureComparable(): boolean;
212
+ private serializeValue;
213
+ private deserializeValue;
166
214
  }
167
- /**
168
- * Wraps an EntityManager to block `nativeInsert`, `nativeUpdate`, and
169
- * `nativeDelete` on entities that have PHI or PCI compliance fields.
170
- *
171
- * This prevents bypassing the ComplianceEventSubscriber's encryption
172
- * by using raw queries. Call this in the tenant context middleware when
173
- * creating the request-scoped EM.
174
- *
175
- * @returns A Proxy-wrapped EntityManager that throws on native query
176
- * operations targeting compliance entities.
177
- */
178
- declare function wrapEmWithNativeQueryBlocking<T extends object>(em: T): T;
179
215
 
180
216
  /**
181
217
  * The name used to register the tenant isolation filter.
@@ -276,4 +312,4 @@ declare class RlsEventSubscriber implements EventSubscriber {
276
312
  */
277
313
  declare function setupRls(orm: MikroORM, config?: RlsConfig): void;
278
314
 
279
- export { ComplianceEventSubscriber, ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, setupRls, setupTenantFilter, subtractDuration, wrapEmWithNativeQueryBlocking };
315
+ export { ComplianceLevel, ComplianceLevel as ComplianceLevelType, DecryptionError, EncryptedType, EncryptionRequiredError, FieldEncryptor, MissingEncryptionKeyError, type ParsedDuration, RetentionAction, RetentionAction as RetentionActionType, RetentionDuration, type RetentionPolicy, type RlsConfig, RlsEventSubscriber, TENANT_FILTER_NAME, createTenantFilterDef, defineComplianceEntity, entityHasEncryptedFields, fp, getAllRetentionPolicies, getAllUserIdFields, getComplianceMetadata, getCurrentTenantId, getEntityComplianceFields, getEntityRetention, getEntityUserIdField, getSuperAdminContext, parseDuration, registerEncryptor, setEncryptionTenantId, setupRls, setupTenantFilter, subtractDuration, withEncryptionContext, wrapEmWithNativeQueryBlocking };
@@ -30,9 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/persistence/index.ts
31
31
  var persistence_exports = {};
32
32
  __export(persistence_exports, {
33
- ComplianceEventSubscriber: () => ComplianceEventSubscriber,
34
33
  ComplianceLevel: () => ComplianceLevel,
35
34
  DecryptionError: () => DecryptionError,
35
+ EncryptedType: () => EncryptedType,
36
36
  EncryptionRequiredError: () => EncryptionRequiredError,
37
37
  FieldEncryptor: () => FieldEncryptor,
38
38
  MissingEncryptionKeyError: () => MissingEncryptionKeyError,
@@ -47,14 +47,18 @@ __export(persistence_exports, {
47
47
  getAllRetentionPolicies: () => getAllRetentionPolicies,
48
48
  getAllUserIdFields: () => getAllUserIdFields,
49
49
  getComplianceMetadata: () => getComplianceMetadata,
50
+ getCurrentTenantId: () => getCurrentTenantId,
50
51
  getEntityComplianceFields: () => getEntityComplianceFields,
51
52
  getEntityRetention: () => getEntityRetention,
52
53
  getEntityUserIdField: () => getEntityUserIdField,
53
54
  getSuperAdminContext: () => getSuperAdminContext,
54
55
  parseDuration: () => parseDuration,
56
+ registerEncryptor: () => registerEncryptor,
57
+ setEncryptionTenantId: () => setEncryptionTenantId,
55
58
  setupRls: () => setupRls,
56
59
  setupTenantFilter: () => setupTenantFilter,
57
60
  subtractDuration: () => subtractDuration,
61
+ withEncryptionContext: () => withEncryptionContext,
58
62
  wrapEmWithNativeQueryBlocking: () => wrapEmWithNativeQueryBlocking
59
63
  });
60
64
  module.exports = __toCommonJS(persistence_exports);
@@ -143,7 +147,102 @@ function getAllRetentionPolicies() {
143
147
  }
144
148
 
145
149
  // src/persistence/compliancePropertyBuilder.ts
150
+ var import_core2 = require("@mikro-orm/core");
151
+
152
+ // src/persistence/encryptedType.ts
153
+ var import_node_async_hooks = require("async_hooks");
146
154
  var import_core = require("@mikro-orm/core");
155
+ var ENCRYPTED_PREFIXES = ["v1:", "v2:"];
156
+ function isEncrypted(value) {
157
+ return ENCRYPTED_PREFIXES.some((p3) => value.startsWith(p3));
158
+ }
159
+ var _encryptor;
160
+ var _tenantContext = new import_node_async_hooks.AsyncLocalStorage();
161
+ function registerEncryptor(encryptor) {
162
+ _encryptor = encryptor;
163
+ }
164
+ function withEncryptionContext(tenantId, fn) {
165
+ return _tenantContext.run({ tenantId }, fn);
166
+ }
167
+ function setEncryptionTenantId(tenantId) {
168
+ _tenantContext.enterWith({ tenantId });
169
+ }
170
+ function getCurrentTenantId() {
171
+ return _tenantContext.getStore()?.tenantId ?? "";
172
+ }
173
+ var EncryptedType = class extends import_core.Type {
174
+ originalType;
175
+ /**
176
+ * @param originalType - The original JS type hint ('string' | 'json' | 'number' | 'boolean').
177
+ * Used to determine serialization strategy.
178
+ */
179
+ constructor(originalType = "string") {
180
+ super();
181
+ this.originalType = originalType;
182
+ }
183
+ convertToDatabaseValue(value, _platform, _context) {
184
+ if (value === null || value === void 0) return null;
185
+ if (typeof value === "string" && value.length === 0) return "";
186
+ if (!_encryptor) {
187
+ return this.serializeValue(value);
188
+ }
189
+ if (typeof value === "string" && isEncrypted(value)) {
190
+ return value;
191
+ }
192
+ const serialized = this.serializeValue(value);
193
+ return _encryptor.encrypt(serialized, getCurrentTenantId()) ?? serialized;
194
+ }
195
+ convertToJSValue(value, _platform) {
196
+ if (value === null || value === void 0) return null;
197
+ if (typeof value !== "string") return value;
198
+ if (!isEncrypted(value)) {
199
+ return this.deserializeValue(value);
200
+ }
201
+ if (!_encryptor) return value;
202
+ try {
203
+ const decrypted = _encryptor.decrypt(value, getCurrentTenantId());
204
+ if (decrypted === null) return null;
205
+ return this.deserializeValue(decrypted);
206
+ } catch {
207
+ return value;
208
+ }
209
+ }
210
+ getColumnType() {
211
+ return "text";
212
+ }
213
+ get runtimeType() {
214
+ return this.originalType === "json" ? "object" : this.originalType;
215
+ }
216
+ ensureComparable() {
217
+ return true;
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Serialization helpers
221
+ // ---------------------------------------------------------------------------
222
+ serializeValue(value) {
223
+ if (typeof value === "string") return value;
224
+ return JSON.stringify(value);
225
+ }
226
+ deserializeValue(value) {
227
+ switch (this.originalType) {
228
+ case "string":
229
+ return value;
230
+ case "number":
231
+ return Number(value);
232
+ case "boolean":
233
+ return value === "true";
234
+ case "json":
235
+ default:
236
+ try {
237
+ return JSON.parse(value);
238
+ } catch {
239
+ return value;
240
+ }
241
+ }
242
+ }
243
+ };
244
+
245
+ // src/persistence/compliancePropertyBuilder.ts
147
246
  function isBuilder(value) {
148
247
  return value != null && typeof value === "object" && "~options" in value;
149
248
  }
@@ -169,7 +268,34 @@ function wrapUnclassified(builder) {
169
268
  }
170
269
  });
171
270
  }
271
+ var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
272
+ function detectOriginalType(options) {
273
+ const type = options.type;
274
+ if (!type) return "string";
275
+ let t;
276
+ if (typeof type === "string") {
277
+ t = type.toLowerCase();
278
+ } else if (typeof type === "object" && type !== null) {
279
+ const rt = type.runtimeType;
280
+ t = rt ?? type.constructor?.name?.toLowerCase() ?? "string";
281
+ } else {
282
+ return "string";
283
+ }
284
+ if (t.includes("json") || t === "object") return "json";
285
+ if (t.includes("int") || t === "number" || t === "double" || t === "float" || t === "decimal" || t === "smallint" || t === "tinyint") return "number";
286
+ if (t === "boolean" || t === "bool") return "boolean";
287
+ return "string";
288
+ }
172
289
  function wrapClassified(builder, level) {
290
+ if (ENCRYPTED_LEVELS.has(level)) {
291
+ const options = builder["~options"];
292
+ if (options) {
293
+ const originalType = detectOriginalType(options);
294
+ options.type = new EncryptedType(originalType);
295
+ options.columnType = "text";
296
+ options.runtimeType = originalType === "json" ? "object" : originalType;
297
+ }
298
+ }
173
299
  return new Proxy(builder, {
174
300
  get(target, prop) {
175
301
  if (prop === COMPLIANCE_KEY) return level;
@@ -201,7 +327,7 @@ var RELATION_METHODS = /* @__PURE__ */ new Set([
201
327
  function isRelationMethod(prop) {
202
328
  return typeof prop === "string" && RELATION_METHODS.has(prop);
203
329
  }
204
- var fp = new Proxy(import_core.p, {
330
+ var fp = new Proxy(import_core2.p, {
205
331
  get(target, prop) {
206
332
  const value = Reflect.get(target, prop, target);
207
333
  if (typeof value !== "function") return value;
@@ -225,7 +351,7 @@ var fp = new Proxy(import_core.p, {
225
351
  });
226
352
 
227
353
  // src/persistence/defineComplianceEntity.ts
228
- var import_core2 = require("@mikro-orm/core");
354
+ var import_core3 = require("@mikro-orm/core");
229
355
  function readComplianceLevel(builder) {
230
356
  if (builder == null || typeof builder !== "object") return void 0;
231
357
  return builder[COMPLIANCE_KEY];
@@ -234,7 +360,7 @@ function defineComplianceEntity(meta) {
234
360
  const entityName = "name" in meta ? meta.name : "Unknown";
235
361
  const complianceFields = /* @__PURE__ */ new Map();
236
362
  const rawProperties = meta.properties;
237
- const resolvedProperties = typeof rawProperties === "function" ? rawProperties(import_core2.p) : rawProperties;
363
+ const resolvedProperties = typeof rawProperties === "function" ? rawProperties(import_core3.p) : rawProperties;
238
364
  for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {
239
365
  if (typeof rawProp === "function") {
240
366
  complianceFields.set(fieldName, "none");
@@ -261,7 +387,7 @@ function defineComplianceEntity(meta) {
261
387
  if (meta.userIdField) {
262
388
  registerEntityUserIdField(entityName, meta.userIdField);
263
389
  }
264
- return (0, import_core2.defineEntity)(
390
+ return (0, import_core3.defineEntity)(
265
391
  meta
266
392
  );
267
393
  }
@@ -308,12 +434,23 @@ var FieldEncryptor = class {
308
434
  import_crypto.default.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)
309
435
  );
310
436
  }
437
+ /**
438
+ * Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
439
+ * truncated to IV_BYTES. Same plaintext + same key → same IV → same
440
+ * ciphertext. This enables WHERE clause matching on encrypted columns
441
+ * while maintaining AES-256-GCM authenticated encryption.
442
+ *
443
+ * Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
444
+ */
445
+ deriveDeterministicIv(key, plaintext) {
446
+ return import_crypto.default.createHmac("sha256", key).update(plaintext).digest().subarray(0, IV_BYTES);
447
+ }
311
448
  encrypt(plaintext, tenantId) {
312
449
  if (plaintext === null || plaintext === void 0) {
313
450
  return null;
314
451
  }
315
452
  const key = this.deriveKey(tenantId ?? "");
316
- const iv = import_crypto.default.randomBytes(IV_BYTES);
453
+ const iv = this.deriveDeterministicIv(key, plaintext);
317
454
  const cipher = import_crypto.default.createCipheriv(ALGORITHM, key, iv);
318
455
  const encrypted = Buffer.concat([
319
456
  cipher.update(plaintext, "utf8"),
@@ -321,7 +458,7 @@ var FieldEncryptor = class {
321
458
  ]);
322
459
  const authTag = cipher.getAuthTag();
323
460
  return [
324
- "v1",
461
+ "v2",
325
462
  iv.toString("base64"),
326
463
  authTag.toString("base64"),
327
464
  encrypted.toString("base64")
@@ -332,7 +469,7 @@ var FieldEncryptor = class {
332
469
  return null;
333
470
  }
334
471
  const parts = ciphertext.split(":");
335
- if (parts.length !== 4 || parts[0] !== "v1") {
472
+ if (parts.length !== 4 || parts[0] !== "v1" && parts[0] !== "v2") {
336
473
  throw new DecryptionError(
337
474
  `Unknown ciphertext version or malformed format`
338
475
  );
@@ -357,85 +494,7 @@ var FieldEncryptor = class {
357
494
  };
358
495
 
359
496
  // src/persistence/complianceEventSubscriber.ts
360
- var ENCRYPTED_PREFIX = "v1:";
361
- var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
362
- var ComplianceEventSubscriber = class {
363
- encryptor;
364
- constructor(encryptor) {
365
- this.encryptor = encryptor;
366
- }
367
- async beforeCreate(args) {
368
- this.encryptFields(args);
369
- }
370
- async beforeUpdate(args) {
371
- this.encryptFields(args);
372
- }
373
- async onLoad(args) {
374
- this.decryptFields(args);
375
- }
376
- // ---------------------------------------------------------------------------
377
- // Encrypt on persist
378
- // ---------------------------------------------------------------------------
379
- encryptFields(args) {
380
- const entityName = args.meta.className;
381
- const complianceFields = getEntityComplianceFields(entityName);
382
- if (!complianceFields) return;
383
- const tenantId = this.getTenantId(args.em);
384
- const entity = args.entity;
385
- for (const [fieldName, level] of complianceFields) {
386
- if (!ENCRYPTED_LEVELS.has(level)) continue;
387
- const value = entity[fieldName];
388
- if (value === null || value === void 0) continue;
389
- if (typeof value !== "string") continue;
390
- if (value.startsWith(ENCRYPTED_PREFIX)) continue;
391
- entity[fieldName] = this.encryptor.encrypt(value, tenantId ?? "");
392
- }
393
- }
394
- // ---------------------------------------------------------------------------
395
- // Decrypt on load
396
- // ---------------------------------------------------------------------------
397
- decryptFields(args) {
398
- const entityName = args.meta.className;
399
- const complianceFields = getEntityComplianceFields(entityName);
400
- if (!complianceFields) return;
401
- const tenantId = this.getTenantId(args.em);
402
- const entity = args.entity;
403
- for (const [fieldName, level] of complianceFields) {
404
- if (!ENCRYPTED_LEVELS.has(level)) continue;
405
- const value = entity[fieldName];
406
- if (value === null || value === void 0) continue;
407
- if (typeof value !== "string") continue;
408
- if (value.startsWith(ENCRYPTED_PREFIX)) {
409
- try {
410
- entity[fieldName] = this.encryptor.decrypt(value, tenantId ?? "");
411
- } catch (err) {
412
- if (err instanceof DecryptionError) {
413
- throw new DecryptionError(
414
- `Failed to decrypt ${entityName}.${fieldName}: ${err.message}`
415
- );
416
- }
417
- throw err;
418
- }
419
- } else {
420
- console.warn(
421
- `[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. Run encryption migration to encrypt existing data.`
422
- );
423
- }
424
- }
425
- }
426
- // ---------------------------------------------------------------------------
427
- // Tenant ID resolution
428
- // ---------------------------------------------------------------------------
429
- /**
430
- * Read the tenant ID from the EntityManager's filter parameters.
431
- * Returns undefined when no tenant context is set (startup, seeders,
432
- * better-auth). Callers skip encryption/decryption in that case.
433
- */
434
- getTenantId(em) {
435
- const filters = em.getFilterParams("tenant");
436
- return filters?.tenantId;
437
- }
438
- };
497
+ var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
439
498
  function wrapEmWithNativeQueryBlocking(em) {
440
499
  const BLOCKED_METHODS = [
441
500
  "nativeInsert",
@@ -451,7 +510,7 @@ function wrapEmWithNativeQueryBlocking(em) {
451
510
  const fields = getEntityComplianceFields(entityName);
452
511
  if (fields) {
453
512
  for (const [fieldName, level] of fields) {
454
- if (ENCRYPTED_LEVELS.has(level)) {
513
+ if (ENCRYPTED_LEVELS2.has(level)) {
455
514
  throw new EncryptionRequiredError(
456
515
  `${prop}() blocked on entity '${entityName}' because field '${fieldName}' has compliance level '${level}'. Use em.create() + em.flush() instead to ensure encryption.`
457
516
  );
@@ -624,9 +683,9 @@ function escapeSqlString(value) {
624
683
  }
625
684
  // Annotate the CommonJS export names for ESM import in node:
626
685
  0 && (module.exports = {
627
- ComplianceEventSubscriber,
628
686
  ComplianceLevel,
629
687
  DecryptionError,
688
+ EncryptedType,
630
689
  EncryptionRequiredError,
631
690
  FieldEncryptor,
632
691
  MissingEncryptionKeyError,
@@ -641,14 +700,18 @@ function escapeSqlString(value) {
641
700
  getAllRetentionPolicies,
642
701
  getAllUserIdFields,
643
702
  getComplianceMetadata,
703
+ getCurrentTenantId,
644
704
  getEntityComplianceFields,
645
705
  getEntityRetention,
646
706
  getEntityUserIdField,
647
707
  getSuperAdminContext,
648
708
  parseDuration,
709
+ registerEncryptor,
710
+ setEncryptionTenantId,
649
711
  setupRls,
650
712
  setupTenantFilter,
651
713
  subtractDuration,
714
+ withEncryptionContext,
652
715
  wrapEmWithNativeQueryBlocking
653
716
  });
654
717
  //# sourceMappingURL=index.js.map