@forklaunch/core 1.2.9 → 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.
- package/lib/persistence/index.d.mts +74 -38
- package/lib/persistence/index.d.ts +74 -38
- package/lib/persistence/index.js +152 -109
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +143 -104
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +3 -3
package/lib/persistence/index.js
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
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,14 +387,11 @@ function defineComplianceEntity(meta) {
|
|
|
261
387
|
if (meta.userIdField) {
|
|
262
388
|
registerEntityUserIdField(entityName, meta.userIdField);
|
|
263
389
|
}
|
|
264
|
-
return (0,
|
|
390
|
+
return (0, import_core3.defineEntity)(
|
|
265
391
|
meta
|
|
266
392
|
);
|
|
267
393
|
}
|
|
268
394
|
|
|
269
|
-
// src/persistence/complianceEventSubscriber.ts
|
|
270
|
-
var import_core3 = require("@mikro-orm/core");
|
|
271
|
-
|
|
272
395
|
// src/persistence/fieldEncryptor.ts
|
|
273
396
|
var import_crypto = __toESM(require("crypto"));
|
|
274
397
|
var MissingEncryptionKeyError = class extends Error {
|
|
@@ -311,12 +434,23 @@ var FieldEncryptor = class {
|
|
|
311
434
|
import_crypto.default.hkdfSync(HKDF_HASH, this.masterKey, HKDF_SALT, tenantId, KEY_BYTES)
|
|
312
435
|
);
|
|
313
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
|
+
}
|
|
314
448
|
encrypt(plaintext, tenantId) {
|
|
315
449
|
if (plaintext === null || plaintext === void 0) {
|
|
316
450
|
return null;
|
|
317
451
|
}
|
|
318
452
|
const key = this.deriveKey(tenantId ?? "");
|
|
319
|
-
const iv =
|
|
453
|
+
const iv = this.deriveDeterministicIv(key, plaintext);
|
|
320
454
|
const cipher = import_crypto.default.createCipheriv(ALGORITHM, key, iv);
|
|
321
455
|
const encrypted = Buffer.concat([
|
|
322
456
|
cipher.update(plaintext, "utf8"),
|
|
@@ -324,7 +458,7 @@ var FieldEncryptor = class {
|
|
|
324
458
|
]);
|
|
325
459
|
const authTag = cipher.getAuthTag();
|
|
326
460
|
return [
|
|
327
|
-
"
|
|
461
|
+
"v2",
|
|
328
462
|
iv.toString("base64"),
|
|
329
463
|
authTag.toString("base64"),
|
|
330
464
|
encrypted.toString("base64")
|
|
@@ -335,7 +469,7 @@ var FieldEncryptor = class {
|
|
|
335
469
|
return null;
|
|
336
470
|
}
|
|
337
471
|
const parts = ciphertext.split(":");
|
|
338
|
-
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
472
|
+
if (parts.length !== 4 || parts[0] !== "v1" && parts[0] !== "v2") {
|
|
339
473
|
throw new DecryptionError(
|
|
340
474
|
`Unknown ciphertext version or malformed format`
|
|
341
475
|
);
|
|
@@ -360,102 +494,7 @@ var FieldEncryptor = class {
|
|
|
360
494
|
};
|
|
361
495
|
|
|
362
496
|
// src/persistence/complianceEventSubscriber.ts
|
|
363
|
-
var
|
|
364
|
-
var ENCRYPTED_LEVELS = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
|
|
365
|
-
var ComplianceEventSubscriber = class {
|
|
366
|
-
encryptor;
|
|
367
|
-
constructor(encryptor) {
|
|
368
|
-
this.encryptor = encryptor;
|
|
369
|
-
}
|
|
370
|
-
async beforeCreate(args) {
|
|
371
|
-
this.encryptFields(args);
|
|
372
|
-
}
|
|
373
|
-
async beforeUpdate(args) {
|
|
374
|
-
this.encryptFields(args);
|
|
375
|
-
}
|
|
376
|
-
async onLoad(args) {
|
|
377
|
-
this.decryptFields(args);
|
|
378
|
-
try {
|
|
379
|
-
const wrapped = (0, import_core3.wrap)(args.entity, true);
|
|
380
|
-
const snapshot = wrapped.__originalEntityData;
|
|
381
|
-
if (snapshot) {
|
|
382
|
-
const entity = args.entity;
|
|
383
|
-
const complianceFields = getEntityComplianceFields(args.meta.className);
|
|
384
|
-
if (complianceFields) {
|
|
385
|
-
for (const [fieldName, level] of complianceFields) {
|
|
386
|
-
if (!ENCRYPTED_LEVELS.has(level)) continue;
|
|
387
|
-
if (entity[fieldName] !== void 0) {
|
|
388
|
-
snapshot[fieldName] = entity[fieldName];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
|
-
// Encrypt on persist
|
|
398
|
-
// ---------------------------------------------------------------------------
|
|
399
|
-
encryptFields(args) {
|
|
400
|
-
const entityName = args.meta.className;
|
|
401
|
-
const complianceFields = getEntityComplianceFields(entityName);
|
|
402
|
-
if (!complianceFields) return;
|
|
403
|
-
const tenantId = this.getTenantId(args.em);
|
|
404
|
-
const entity = args.entity;
|
|
405
|
-
for (const [fieldName, level] of complianceFields) {
|
|
406
|
-
if (!ENCRYPTED_LEVELS.has(level)) continue;
|
|
407
|
-
const value = entity[fieldName];
|
|
408
|
-
if (value === null || value === void 0) continue;
|
|
409
|
-
if (typeof value !== "string") continue;
|
|
410
|
-
if (value.startsWith(ENCRYPTED_PREFIX)) continue;
|
|
411
|
-
entity[fieldName] = this.encryptor.encrypt(value, tenantId ?? "");
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
// ---------------------------------------------------------------------------
|
|
415
|
-
// Decrypt on load
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
decryptFields(args) {
|
|
418
|
-
const entityName = args.meta.className;
|
|
419
|
-
const complianceFields = getEntityComplianceFields(entityName);
|
|
420
|
-
if (!complianceFields) return;
|
|
421
|
-
const tenantId = this.getTenantId(args.em);
|
|
422
|
-
const entity = args.entity;
|
|
423
|
-
for (const [fieldName, level] of complianceFields) {
|
|
424
|
-
if (!ENCRYPTED_LEVELS.has(level)) continue;
|
|
425
|
-
const value = entity[fieldName];
|
|
426
|
-
if (value === null || value === void 0) continue;
|
|
427
|
-
if (typeof value !== "string") continue;
|
|
428
|
-
if (value.startsWith(ENCRYPTED_PREFIX)) {
|
|
429
|
-
try {
|
|
430
|
-
entity[fieldName] = this.encryptor.decrypt(value, tenantId ?? "");
|
|
431
|
-
} catch (err) {
|
|
432
|
-
if (err instanceof DecryptionError) {
|
|
433
|
-
throw new DecryptionError(
|
|
434
|
-
`Failed to decrypt ${entityName}.${fieldName}: ${err.message}`
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
throw err;
|
|
438
|
-
}
|
|
439
|
-
} else {
|
|
440
|
-
console.warn(
|
|
441
|
-
`[compliance] ${entityName}.${fieldName} contains unencrypted ${level} data. Run encryption migration to encrypt existing data.`
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
// ---------------------------------------------------------------------------
|
|
447
|
-
// Tenant ID resolution
|
|
448
|
-
// ---------------------------------------------------------------------------
|
|
449
|
-
/**
|
|
450
|
-
* Read the tenant ID from the EntityManager's filter parameters.
|
|
451
|
-
* Returns undefined when no tenant context is set (startup, seeders,
|
|
452
|
-
* better-auth). Callers skip encryption/decryption in that case.
|
|
453
|
-
*/
|
|
454
|
-
getTenantId(em) {
|
|
455
|
-
const filters = em.getFilterParams("tenant");
|
|
456
|
-
return filters?.tenantId;
|
|
457
|
-
}
|
|
458
|
-
};
|
|
497
|
+
var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set(["pii", "phi", "pci"]);
|
|
459
498
|
function wrapEmWithNativeQueryBlocking(em) {
|
|
460
499
|
const BLOCKED_METHODS = [
|
|
461
500
|
"nativeInsert",
|
|
@@ -471,7 +510,7 @@ function wrapEmWithNativeQueryBlocking(em) {
|
|
|
471
510
|
const fields = getEntityComplianceFields(entityName);
|
|
472
511
|
if (fields) {
|
|
473
512
|
for (const [fieldName, level] of fields) {
|
|
474
|
-
if (
|
|
513
|
+
if (ENCRYPTED_LEVELS2.has(level)) {
|
|
475
514
|
throw new EncryptionRequiredError(
|
|
476
515
|
`${prop}() blocked on entity '${entityName}' because field '${fieldName}' has compliance level '${level}'. Use em.create() + em.flush() instead to ensure encryption.`
|
|
477
516
|
);
|
|
@@ -644,9 +683,9 @@ function escapeSqlString(value) {
|
|
|
644
683
|
}
|
|
645
684
|
// Annotate the CommonJS export names for ESM import in node:
|
|
646
685
|
0 && (module.exports = {
|
|
647
|
-
ComplianceEventSubscriber,
|
|
648
686
|
ComplianceLevel,
|
|
649
687
|
DecryptionError,
|
|
688
|
+
EncryptedType,
|
|
650
689
|
EncryptionRequiredError,
|
|
651
690
|
FieldEncryptor,
|
|
652
691
|
MissingEncryptionKeyError,
|
|
@@ -661,14 +700,18 @@ function escapeSqlString(value) {
|
|
|
661
700
|
getAllRetentionPolicies,
|
|
662
701
|
getAllUserIdFields,
|
|
663
702
|
getComplianceMetadata,
|
|
703
|
+
getCurrentTenantId,
|
|
664
704
|
getEntityComplianceFields,
|
|
665
705
|
getEntityRetention,
|
|
666
706
|
getEntityUserIdField,
|
|
667
707
|
getSuperAdminContext,
|
|
668
708
|
parseDuration,
|
|
709
|
+
registerEncryptor,
|
|
710
|
+
setEncryptionTenantId,
|
|
669
711
|
setupRls,
|
|
670
712
|
setupTenantFilter,
|
|
671
713
|
subtractDuration,
|
|
714
|
+
withEncryptionContext,
|
|
672
715
|
wrapEmWithNativeQueryBlocking
|
|
673
716
|
});
|
|
674
717
|
//# sourceMappingURL=index.js.map
|