@forklaunch/core 2.1.1 → 3.0.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/bin/gateway-mock/routes/email.mjs +233 -0
- package/bin/gateway-mock/routes/models.mjs +192 -0
- package/bin/gateway-mock/routes/payments.mjs +541 -0
- package/bin/gateway-mock/routes/sms.mjs +209 -0
- package/bin/gateway-mock/routes/voice.mjs +237 -0
- package/bin/gateway-mock/routes/whatsapp.mjs +227 -0
- package/bin/gateway-mock/server.mjs +321 -0
- package/bin/migrate-compliant-fields.mjs +312 -0
- package/bin/model-gateway-mock.mjs +13 -0
- package/lib/http/emailClient.d.ts +102 -0
- package/lib/http/emailClient.d.ts.map +1 -0
- package/lib/http/index.d.ts +7 -0
- package/lib/http/index.d.ts.map +1 -1
- package/lib/http/index.js +850 -4
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +809 -1
- package/lib/http/index.mjs.map +1 -1
- package/lib/http/instanceGateway.d.ts +113 -0
- package/lib/http/instanceGateway.d.ts.map +1 -0
- package/lib/http/modelGatewayClient.d.ts +126 -0
- package/lib/http/modelGatewayClient.d.ts.map +1 -0
- package/lib/http/paymentsClient.d.ts +123 -0
- package/lib/http/paymentsClient.d.ts.map +1 -0
- package/lib/http/smsClient.d.ts +123 -0
- package/lib/http/smsClient.d.ts.map +1 -0
- package/lib/http/voiceClient.d.ts +105 -0
- package/lib/http/voiceClient.d.ts.map +1 -0
- package/lib/http/whatsappClient.d.ts +118 -0
- package/lib/http/whatsappClient.d.ts.map +1 -0
- package/lib/objectstore/interfaces/objectstore.interface.d.ts +45 -0
- package/lib/objectstore/interfaces/objectstore.interface.d.ts.map +1 -1
- package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
- package/lib/persistence/complianceTypes.d.ts +10 -1
- package/lib/persistence/complianceTypes.d.ts.map +1 -1
- package/lib/persistence/compliantField.d.ts +203 -0
- package/lib/persistence/compliantField.d.ts.map +1 -0
- package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
- package/lib/persistence/encryptedType.d.ts +8 -1
- package/lib/persistence/encryptedType.d.ts.map +1 -1
- package/lib/persistence/fieldEncryptor.d.ts +47 -6
- package/lib/persistence/fieldEncryptor.d.ts.map +1 -1
- package/lib/persistence/index.d.ts +3 -2
- package/lib/persistence/index.d.ts.map +1 -1
- package/lib/persistence/index.js +987 -259
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +976 -257
- package/lib/persistence/index.mjs.map +1 -1
- package/lib/persistence/keyRotation.d.ts +5 -0
- package/lib/persistence/keyRotation.d.ts.map +1 -1
- package/lib/services/index.js.map +1 -1
- package/lib/services/index.mjs.map +1 -1
- package/package.json +25 -9
|
@@ -159,9 +159,11 @@ function asEncryptionSafe(em) {
|
|
|
159
159
|
return em;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// src/persistence/
|
|
163
|
-
import
|
|
164
|
-
import {
|
|
162
|
+
// src/persistence/compliantField.ts
|
|
163
|
+
import crypto2 from "crypto";
|
|
164
|
+
import {
|
|
165
|
+
Type as Type2
|
|
166
|
+
} from "@mikro-orm/core";
|
|
165
167
|
|
|
166
168
|
// src/persistence/encryptedType.ts
|
|
167
169
|
import { Type } from "@mikro-orm/core";
|
|
@@ -175,6 +177,9 @@ var _tenantContext = new AsyncLocalStorage();
|
|
|
175
177
|
function registerEncryptor(encryptor) {
|
|
176
178
|
_encryptor = encryptor;
|
|
177
179
|
}
|
|
180
|
+
function getRegisteredEncryptor() {
|
|
181
|
+
return _encryptor;
|
|
182
|
+
}
|
|
178
183
|
var EmptyTenantError = class extends Error {
|
|
179
184
|
constructor(api) {
|
|
180
185
|
super(
|
|
@@ -330,255 +335,55 @@ var EncryptedType = class extends Type {
|
|
|
330
335
|
// Serialization helpers
|
|
331
336
|
// ---------------------------------------------------------------------------
|
|
332
337
|
serialize(value) {
|
|
333
|
-
|
|
334
|
-
if (value instanceof Date) return value.toISOString();
|
|
335
|
-
if (typeof value === "bigint") return value.toString();
|
|
336
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
337
|
-
return Buffer.from(value).toString("base64");
|
|
338
|
-
return JSON.stringify(value);
|
|
338
|
+
return serializeForEncryption(value);
|
|
339
339
|
}
|
|
340
340
|
deserialize(value) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
(el) => hydrateValue(el, this._elementRuntimeType)
|
|
347
|
-
);
|
|
348
|
-
} catch {
|
|
349
|
-
return value;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
switch (this._elementRuntimeType) {
|
|
353
|
-
case "string":
|
|
354
|
-
return value;
|
|
355
|
-
case "any":
|
|
356
|
-
try {
|
|
357
|
-
return JSON.parse(value);
|
|
358
|
-
} catch {
|
|
359
|
-
return value;
|
|
360
|
-
}
|
|
361
|
-
// Types serialized via toString/toISOString/base64 — hydrate from string directly
|
|
362
|
-
case "bigint":
|
|
363
|
-
case "Date":
|
|
364
|
-
case "Buffer":
|
|
365
|
-
return hydrateValue(value, this._elementRuntimeType);
|
|
366
|
-
// Types where JSON.parse recovers the native JS value (number, boolean)
|
|
367
|
-
default:
|
|
368
|
-
return hydrateValue(this.tryJsonParse(value), this._elementRuntimeType);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
tryJsonParse(value) {
|
|
372
|
-
try {
|
|
373
|
-
return JSON.parse(value);
|
|
374
|
-
} catch {
|
|
375
|
-
return value;
|
|
376
|
-
}
|
|
341
|
+
return deserializeFromEncryption(
|
|
342
|
+
value,
|
|
343
|
+
this._elementRuntimeType,
|
|
344
|
+
this._isArray
|
|
345
|
+
);
|
|
377
346
|
}
|
|
378
347
|
};
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return
|
|
386
|
-
get(target, prop) {
|
|
387
|
-
if (prop === "compliance") {
|
|
388
|
-
return (level) => wrapClassified(target, level);
|
|
389
|
-
}
|
|
390
|
-
if (prop === "~options") return Reflect.get(target, prop, target);
|
|
391
|
-
if (prop === COMPLIANCE_KEY) return void 0;
|
|
392
|
-
const value = Reflect.get(target, prop, target);
|
|
393
|
-
if (typeof value === "function") {
|
|
394
|
-
return (...args) => {
|
|
395
|
-
const result = value.apply(
|
|
396
|
-
target,
|
|
397
|
-
args
|
|
398
|
-
);
|
|
399
|
-
return isBuilder(result) ? wrapUnclassified(result) : result;
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
return value;
|
|
403
|
-
}
|
|
404
|
-
});
|
|
348
|
+
function serializeForEncryption(value) {
|
|
349
|
+
if (typeof value === "string") return value;
|
|
350
|
+
if (value instanceof Date) return value.toISOString();
|
|
351
|
+
if (typeof value === "bigint") return value.toString();
|
|
352
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
353
|
+
return Buffer.from(value).toString("base64");
|
|
354
|
+
return JSON.stringify(value);
|
|
405
355
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
function resolveEncryptedTypeArgs(options) {
|
|
412
|
-
const type = options.type;
|
|
413
|
-
const isArray = options.array === true;
|
|
414
|
-
const resolved = resolveTypeInstance(type);
|
|
415
|
-
if (resolved) {
|
|
416
|
-
const rt = resolved.runtimeType;
|
|
417
|
-
if (rt.endsWith("[]")) {
|
|
418
|
-
return { elementRuntimeType: rt.slice(0, -2) || "string", isArray: true };
|
|
419
|
-
}
|
|
420
|
-
return { elementRuntimeType: rt, isArray };
|
|
356
|
+
function tryJsonParse(value) {
|
|
357
|
+
try {
|
|
358
|
+
return JSON.parse(value);
|
|
359
|
+
} catch {
|
|
360
|
+
return value;
|
|
421
361
|
}
|
|
422
|
-
return { elementRuntimeType: "string", isArray };
|
|
423
362
|
}
|
|
424
|
-
function
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
enumValues = extractEnumValues(options.items);
|
|
432
|
-
delete options.items;
|
|
433
|
-
delete options.nativeEnumName;
|
|
434
|
-
delete options.enum;
|
|
435
|
-
}
|
|
436
|
-
options.type = new EncryptedType(elementRuntimeType, isArray, enumValues);
|
|
437
|
-
options.columnType = "text";
|
|
438
|
-
options.runtimeType = isArray ? "object" : elementRuntimeType;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return new Proxy(builder, {
|
|
442
|
-
get(target, prop) {
|
|
443
|
-
if (prop === COMPLIANCE_KEY) return level;
|
|
444
|
-
if (prop === "~options") return Reflect.get(target, prop, target);
|
|
445
|
-
const value = Reflect.get(target, prop, target);
|
|
446
|
-
if (typeof value === "function") {
|
|
447
|
-
return (...args) => {
|
|
448
|
-
const result = value.apply(
|
|
449
|
-
target,
|
|
450
|
-
args
|
|
451
|
-
);
|
|
452
|
-
return isBuilder(result) ? wrapClassified(result, level) : result;
|
|
453
|
-
};
|
|
454
|
-
}
|
|
363
|
+
function deserializeFromEncryption(value, elementRuntimeType, isArray) {
|
|
364
|
+
if (isArray) {
|
|
365
|
+
try {
|
|
366
|
+
const arr = JSON.parse(value);
|
|
367
|
+
if (!Array.isArray(arr)) return arr;
|
|
368
|
+
return arr.map((el) => hydrateValue(el, elementRuntimeType));
|
|
369
|
+
} catch {
|
|
455
370
|
return value;
|
|
456
371
|
}
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
function wrapRelation(builder) {
|
|
460
|
-
return wrapClassified(builder, "none");
|
|
461
|
-
}
|
|
462
|
-
var RELATION_METHODS = /* @__PURE__ */ new Set([
|
|
463
|
-
"manyToOne",
|
|
464
|
-
"oneToMany",
|
|
465
|
-
"manyToMany",
|
|
466
|
-
"oneToOne",
|
|
467
|
-
"embedded"
|
|
468
|
-
]);
|
|
469
|
-
function isRelationMethod(prop) {
|
|
470
|
-
return typeof prop === "string" && RELATION_METHODS.has(prop);
|
|
471
|
-
}
|
|
472
|
-
function extractEnumValues(items) {
|
|
473
|
-
if (typeof items === "function") {
|
|
474
|
-
return extractEnumValues(items());
|
|
475
|
-
}
|
|
476
|
-
if (Array.isArray(items)) return items;
|
|
477
|
-
if (items != null && typeof items === "object") {
|
|
478
|
-
const obj = items;
|
|
479
|
-
const allValues = Object.values(obj);
|
|
480
|
-
return allValues.filter((v) => typeof v !== "string" || !(v in obj));
|
|
481
|
-
}
|
|
482
|
-
return [];
|
|
483
|
-
}
|
|
484
|
-
var EnumTextType = class extends Type2 {
|
|
485
|
-
convertToDatabaseValue(value) {
|
|
486
|
-
return safeStringify(value);
|
|
487
|
-
}
|
|
488
|
-
convertToJSValue(value) {
|
|
489
|
-
return safeParse(value);
|
|
490
|
-
}
|
|
491
|
-
getColumnType() {
|
|
492
|
-
return "text";
|
|
493
|
-
}
|
|
494
|
-
get runtimeType() {
|
|
495
|
-
return "string";
|
|
496
|
-
}
|
|
497
|
-
};
|
|
498
|
-
function inferEnumType(items) {
|
|
499
|
-
const values = extractEnumValues(items);
|
|
500
|
-
if (values.length === 0) return "text";
|
|
501
|
-
const allStrings = values.every((v) => typeof v === "string");
|
|
502
|
-
if (allStrings) return "text";
|
|
503
|
-
const allNumbers = values.every((v) => typeof v === "number");
|
|
504
|
-
if (allNumbers) return "integer";
|
|
505
|
-
return new EnumTextType();
|
|
506
|
-
}
|
|
507
|
-
var fp = new Proxy(p, {
|
|
508
|
-
get(target, prop) {
|
|
509
|
-
const value = Reflect.get(target, prop, target);
|
|
510
|
-
if (typeof value !== "function") return value;
|
|
511
|
-
if (isRelationMethod(prop)) {
|
|
512
|
-
return (...args) => {
|
|
513
|
-
const result = value.apply(
|
|
514
|
-
target,
|
|
515
|
-
args
|
|
516
|
-
);
|
|
517
|
-
return isBuilder(result) ? wrapRelation(result) : result;
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
return (...args) => {
|
|
521
|
-
const result = value.apply(
|
|
522
|
-
target,
|
|
523
|
-
args
|
|
524
|
-
);
|
|
525
|
-
if (isBuilder(result)) {
|
|
526
|
-
if (prop === "enum") {
|
|
527
|
-
const options = result["~options"];
|
|
528
|
-
if (options && !options.type) {
|
|
529
|
-
options.type = inferEnumType(options.items);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
return wrapUnclassified(result);
|
|
533
|
-
}
|
|
534
|
-
return result;
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// src/persistence/defineComplianceEntity.ts
|
|
540
|
-
import {
|
|
541
|
-
defineEntity,
|
|
542
|
-
p as p2
|
|
543
|
-
} from "@mikro-orm/core";
|
|
544
|
-
function readComplianceLevel(builder) {
|
|
545
|
-
if (builder == null || typeof builder !== "object") return void 0;
|
|
546
|
-
return builder[COMPLIANCE_KEY];
|
|
547
|
-
}
|
|
548
|
-
function defineComplianceEntity(meta) {
|
|
549
|
-
const entityName = "name" in meta ? meta.name : "Unknown";
|
|
550
|
-
const complianceFields = /* @__PURE__ */ new Map();
|
|
551
|
-
const rawProperties = meta.properties;
|
|
552
|
-
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(p2) : rawProperties;
|
|
553
|
-
for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {
|
|
554
|
-
if (typeof rawProp === "function") {
|
|
555
|
-
complianceFields.set(fieldName, "none");
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
const level = readComplianceLevel(rawProp);
|
|
559
|
-
if (level == null) {
|
|
560
|
-
throw new Error(
|
|
561
|
-
`Field '${entityName}.${fieldName}' is missing compliance classification. Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, or use a relation method (fp.manyToOne, etc.) which is auto-classified.`
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
complianceFields.set(fieldName, level);
|
|
565
|
-
}
|
|
566
|
-
registerEntityCompliance(entityName, complianceFields);
|
|
567
|
-
if (meta.retention) {
|
|
568
|
-
parseDuration(meta.retention.duration);
|
|
569
|
-
if (!resolvedProperties["createdAt"]) {
|
|
570
|
-
throw new Error(
|
|
571
|
-
`Entity '${entityName}' has a retention policy but no 'createdAt' property. Retention requires createdAt to compute expiration.`
|
|
572
|
-
);
|
|
573
|
-
}
|
|
574
|
-
registerEntityRetention(entityName, meta.retention);
|
|
575
372
|
}
|
|
576
|
-
|
|
577
|
-
|
|
373
|
+
switch (elementRuntimeType) {
|
|
374
|
+
case "string":
|
|
375
|
+
return value;
|
|
376
|
+
case "any":
|
|
377
|
+
return tryJsonParse(value);
|
|
378
|
+
// Types serialized via toString/toISOString/base64 — hydrate from string directly
|
|
379
|
+
case "bigint":
|
|
380
|
+
case "Date":
|
|
381
|
+
case "Buffer":
|
|
382
|
+
return hydrateValue(value, elementRuntimeType);
|
|
383
|
+
// Types where JSON.parse recovers the native JS value (number, boolean)
|
|
384
|
+
default:
|
|
385
|
+
return hydrateValue(tryJsonParse(value), elementRuntimeType);
|
|
578
386
|
}
|
|
579
|
-
return defineEntity(
|
|
580
|
-
meta
|
|
581
|
-
);
|
|
582
387
|
}
|
|
583
388
|
|
|
584
389
|
// src/persistence/fieldEncryptor.ts
|
|
@@ -606,9 +411,17 @@ var IV_BYTES = 12;
|
|
|
606
411
|
var KEY_BYTES = 32;
|
|
607
412
|
var HKDF_HASH = "sha256";
|
|
608
413
|
var HKDF_SALT = Buffer.alloc(0);
|
|
414
|
+
var BLIND_INDEX_INFO = "forklaunch:blind-index\0";
|
|
609
415
|
var KEY_ID_HEX_CHARS = 12;
|
|
610
416
|
var DERIVED_KEY_CACHE_LIMIT = 4096;
|
|
611
|
-
var ENCRYPTED_PREFIXES2 = ["v1:", "v2:", "v3:"];
|
|
417
|
+
var ENCRYPTED_PREFIXES2 = ["v1:", "v2:", "v3:", "v4:"];
|
|
418
|
+
var BLIND_INDEX_PREFIX = "bi1:";
|
|
419
|
+
function sealedAnon(ciphertext) {
|
|
420
|
+
if (!ciphertext.startsWith("v4:")) return null;
|
|
421
|
+
const parts = ciphertext.split(":");
|
|
422
|
+
if (parts.length !== 6) return null;
|
|
423
|
+
return Buffer.from(parts[2], "base64url").toString("utf8");
|
|
424
|
+
}
|
|
612
425
|
function isEncryptedCiphertext(value) {
|
|
613
426
|
return typeof value === "string" && ENCRYPTED_PREFIXES2.some((p3) => value.startsWith(p3));
|
|
614
427
|
}
|
|
@@ -630,9 +443,12 @@ function encryptionKeyId(masterKey) {
|
|
|
630
443
|
return crypto.createHash("sha256").update(masterKey).digest("hex").slice(0, KEY_ID_HEX_CHARS);
|
|
631
444
|
}
|
|
632
445
|
function stampedKeyId(ciphertext) {
|
|
633
|
-
if (!ciphertext.startsWith("v3:")) return null;
|
|
634
446
|
const parts = ciphertext.split(":");
|
|
635
|
-
return parts.length === 5 ? parts[1] : null;
|
|
447
|
+
if (ciphertext.startsWith("v3:")) return parts.length === 5 ? parts[1] : null;
|
|
448
|
+
if (ciphertext.startsWith("v4:")) return parts.length === 6 ? parts[1] : null;
|
|
449
|
+
if (ciphertext.startsWith(BLIND_INDEX_PREFIX))
|
|
450
|
+
return parts.length === 3 ? parts[1] : null;
|
|
451
|
+
return null;
|
|
636
452
|
}
|
|
637
453
|
function stampedPrefix(keyId) {
|
|
638
454
|
return `v3:${keyId}:`;
|
|
@@ -765,7 +581,8 @@ var FieldEncryptor = class _FieldEncryptor {
|
|
|
765
581
|
const version = parts[0];
|
|
766
582
|
let candidates;
|
|
767
583
|
let body;
|
|
768
|
-
|
|
584
|
+
let anon;
|
|
585
|
+
if (version === "v3" && parts.length === 5 || version === "v4" && parts.length === 6) {
|
|
769
586
|
const key = this.byId.get(parts[1]);
|
|
770
587
|
if (!key) {
|
|
771
588
|
throw new DecryptionError(
|
|
@@ -773,7 +590,12 @@ var FieldEncryptor = class _FieldEncryptor {
|
|
|
773
590
|
);
|
|
774
591
|
}
|
|
775
592
|
candidates = [key];
|
|
776
|
-
|
|
593
|
+
if (version === "v4") {
|
|
594
|
+
anon = Buffer.from(parts[2], "base64url").toString("utf8");
|
|
595
|
+
body = parts.slice(3);
|
|
596
|
+
} else {
|
|
597
|
+
body = parts.slice(2);
|
|
598
|
+
}
|
|
777
599
|
} else if ((version === "v1" || version === "v2") && parts.length === 4) {
|
|
778
600
|
candidates = this.ring;
|
|
779
601
|
body = parts.slice(1);
|
|
@@ -804,7 +626,8 @@ var FieldEncryptor = class _FieldEncryptor {
|
|
|
804
626
|
keyId: key.id,
|
|
805
627
|
current,
|
|
806
628
|
version,
|
|
807
|
-
|
|
629
|
+
anon,
|
|
630
|
+
stale: version === "v4" ? !current : !current || this.format === "v3" && version !== "v3"
|
|
808
631
|
};
|
|
809
632
|
} catch {
|
|
810
633
|
}
|
|
@@ -830,15 +653,773 @@ var FieldEncryptor = class _FieldEncryptor {
|
|
|
830
653
|
if (ciphertext === null || ciphertext === void 0) return null;
|
|
831
654
|
const opened = this.open(ciphertext, tenantId);
|
|
832
655
|
if (!opened.stale) return ciphertext;
|
|
656
|
+
if (opened.version === "v4") {
|
|
657
|
+
return this.seal(opened.plaintext, tenantId, opened.anon ?? "");
|
|
658
|
+
}
|
|
833
659
|
return this.encrypt(opened.plaintext, tenantId);
|
|
834
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Encrypt for a compliant field: a fresh random IV every time (equal
|
|
663
|
+
* plaintexts produce unrelated ciphertexts), stamped with the current key,
|
|
664
|
+
* and carrying `anon` — the field's de-identified value — in clear so it
|
|
665
|
+
* can be read without decrypting.
|
|
666
|
+
*
|
|
667
|
+
* Equality lookups do not use the ciphertext; they use {@link blindIndex}.
|
|
668
|
+
*
|
|
669
|
+
* @returns `v4:{keyId}:{anon}:{iv}:{tag}:{data}`
|
|
670
|
+
*/
|
|
671
|
+
seal(plaintext, tenantId, anon) {
|
|
672
|
+
const key = this.deriveKey(tenantId);
|
|
673
|
+
const iv = crypto.randomBytes(IV_BYTES);
|
|
674
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
675
|
+
const encrypted = Buffer.concat([
|
|
676
|
+
cipher.update(plaintext, "utf8"),
|
|
677
|
+
cipher.final()
|
|
678
|
+
]);
|
|
679
|
+
return [
|
|
680
|
+
"v4",
|
|
681
|
+
this.ring[0].id,
|
|
682
|
+
Buffer.from(anon, "utf8").toString("base64url"),
|
|
683
|
+
iv.toString("base64"),
|
|
684
|
+
cipher.getAuthTag().toString("base64"),
|
|
685
|
+
encrypted.toString("base64")
|
|
686
|
+
].join(":");
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Keyed HMAC of a plaintext for equality lookups, under a key derived
|
|
690
|
+
* from the current master key and the tenant (separate from the
|
|
691
|
+
* encryption key). Deterministic per tenant and key, one-way, and never
|
|
692
|
+
* decryptable.
|
|
693
|
+
*
|
|
694
|
+
* After a master key rotation, rows still indexed under the previous key
|
|
695
|
+
* do not match until `reencryptEncryptedColumns` re-indexes them; see
|
|
696
|
+
* {@link blindIndexes} for a lookup that spans the ring.
|
|
697
|
+
*
|
|
698
|
+
* @returns `bi1:{keyId}:{hmac base64url}`
|
|
699
|
+
*/
|
|
700
|
+
blindIndex(plaintext, tenantId) {
|
|
701
|
+
return this.blindIndexFor(this.ring[0], plaintext, tenantId);
|
|
702
|
+
}
|
|
703
|
+
/** {@link blindIndex} under every key in the ring, current first. */
|
|
704
|
+
blindIndexes(plaintext, tenantId) {
|
|
705
|
+
return this.ring.map((k) => this.blindIndexFor(k, plaintext, tenantId));
|
|
706
|
+
}
|
|
707
|
+
/** True when a blind index was computed under a previous key. */
|
|
708
|
+
isStaleBlindIndex(index) {
|
|
709
|
+
return stampedKeyId(index) !== this.ring[0].id;
|
|
710
|
+
}
|
|
711
|
+
blindIndexFor(key, plaintext, tenantId) {
|
|
712
|
+
const indexKey = this.deriveKeyFor(key, BLIND_INDEX_INFO + tenantId);
|
|
713
|
+
const mac = crypto.createHmac("sha256", indexKey).update(plaintext, "utf8").digest("base64url");
|
|
714
|
+
return `${BLIND_INDEX_PREFIX}${key.id}:${mac}`;
|
|
715
|
+
}
|
|
835
716
|
};
|
|
836
717
|
|
|
837
|
-
// src/persistence/
|
|
838
|
-
var
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
718
|
+
// src/persistence/compliantField.ts
|
|
719
|
+
var LISTENERS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.listeners")] ??= /* @__PURE__ */ new Set();
|
|
720
|
+
function onComplianceAccess(listener) {
|
|
721
|
+
LISTENERS.add(listener);
|
|
722
|
+
return () => LISTENERS.delete(listener);
|
|
723
|
+
}
|
|
724
|
+
var LegacyCiphertextError = class extends Error {
|
|
725
|
+
name = "LegacyCiphertextError";
|
|
726
|
+
};
|
|
727
|
+
var CompliantQueryError = class extends Error {
|
|
728
|
+
name = "CompliantQueryError";
|
|
729
|
+
};
|
|
730
|
+
var MINT = /* @__PURE__ */ Symbol.for("forklaunch.compliance.mint");
|
|
731
|
+
var ANON_BRAND = /* @__PURE__ */ Symbol.for("forklaunch.compliance.anon");
|
|
732
|
+
var Anon = class {
|
|
733
|
+
value;
|
|
734
|
+
/** @internal */
|
|
735
|
+
constructor(value, key) {
|
|
736
|
+
if (key !== MINT) {
|
|
737
|
+
throw new Error("Anon values can only be produced by a compliant field");
|
|
738
|
+
}
|
|
739
|
+
Object.defineProperty(this, ANON_BRAND, { value: true });
|
|
740
|
+
Object.defineProperty(this, "value", { value, enumerable: true });
|
|
741
|
+
Object.freeze(this);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
function isAnon(value) {
|
|
745
|
+
return value != null && typeof value === "object" && value[ANON_BRAND] === true;
|
|
746
|
+
}
|
|
747
|
+
var RESTRICTED_ZIP3 = /* @__PURE__ */ new Set([
|
|
748
|
+
"036",
|
|
749
|
+
"059",
|
|
750
|
+
"063",
|
|
751
|
+
"102",
|
|
752
|
+
"203",
|
|
753
|
+
"556",
|
|
754
|
+
"692",
|
|
755
|
+
"790",
|
|
756
|
+
"821",
|
|
757
|
+
"823",
|
|
758
|
+
"830",
|
|
759
|
+
"831",
|
|
760
|
+
"878",
|
|
761
|
+
"879",
|
|
762
|
+
"884",
|
|
763
|
+
"890",
|
|
764
|
+
"893"
|
|
765
|
+
]);
|
|
766
|
+
var REDACTED = "[redacted]";
|
|
767
|
+
function computeAnon(strategy, value) {
|
|
768
|
+
switch (strategy) {
|
|
769
|
+
case "token":
|
|
770
|
+
return "tok_" + crypto2.randomBytes(9).toString("base64url");
|
|
771
|
+
case "year": {
|
|
772
|
+
if (value instanceof Date && !isNaN(value.getTime()))
|
|
773
|
+
return String(value.getUTCFullYear());
|
|
774
|
+
const match = /\d{4}/.exec(String(value));
|
|
775
|
+
return match ? match[0] : REDACTED;
|
|
776
|
+
}
|
|
777
|
+
case "zip3": {
|
|
778
|
+
const digits = String(value).replace(/\D/g, "").slice(0, 3);
|
|
779
|
+
if (digits.length < 3 || RESTRICTED_ZIP3.has(digits)) return "000";
|
|
780
|
+
return digits;
|
|
781
|
+
}
|
|
782
|
+
case "last4": {
|
|
783
|
+
const digits = String(value).replace(/\D/g, "");
|
|
784
|
+
return digits.length >= 4 ? `\u2022\u2022\u2022\u2022 ${digits.slice(-4)}` : REDACTED;
|
|
785
|
+
}
|
|
786
|
+
case "redact":
|
|
787
|
+
return REDACTED;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function defaultAnonStrategy(elementRuntimeType, isArray, hasEnum) {
|
|
791
|
+
if (isArray || hasEnum) return "redact";
|
|
792
|
+
if (elementRuntimeType === "string") return "token";
|
|
793
|
+
if (elementRuntimeType === "Date") return "year";
|
|
794
|
+
return "redact";
|
|
795
|
+
}
|
|
796
|
+
var STATE = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.state")] ??= /* @__PURE__ */ new WeakMap();
|
|
797
|
+
var FIELD_BRAND = /* @__PURE__ */ Symbol.for("forklaunch.compliance.field");
|
|
798
|
+
function stateOf(field) {
|
|
799
|
+
const state = STATE.get(field);
|
|
800
|
+
if (!state) throw new Error("Not a compliant field");
|
|
801
|
+
return state;
|
|
802
|
+
}
|
|
803
|
+
var CompliantField = class _CompliantField {
|
|
804
|
+
constructor(state) {
|
|
805
|
+
Object.defineProperty(this, FIELD_BRAND, { value: true });
|
|
806
|
+
STATE.set(this, state);
|
|
807
|
+
Object.freeze(this);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* @internal Wrap a plaintext value for `spec`. Applications assign plain
|
|
811
|
+
* values to the entity property instead.
|
|
812
|
+
*/
|
|
813
|
+
static fromPlain(value, spec) {
|
|
814
|
+
if (spec.enumValues) {
|
|
815
|
+
const values = spec.isArray && Array.isArray(value) ? value : [value];
|
|
816
|
+
for (const v of values) {
|
|
817
|
+
if (!spec.enumValues.includes(v)) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`Invalid enum value${spec.path ? ` for ${spec.path}` : ""}: ${String(v)}. Allowed values: ${spec.enumValues.join(", ")}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return new _CompliantField({
|
|
825
|
+
spec,
|
|
826
|
+
hasPlain: true,
|
|
827
|
+
plain: value,
|
|
828
|
+
anon: computeAnon(spec.anon, value)
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/** @internal Wrap a stored value (any envelope, or legacy plaintext). */
|
|
832
|
+
static fromStored(stored, spec, index) {
|
|
833
|
+
if (!isEncryptedCiphertext(stored)) {
|
|
834
|
+
return new _CompliantField({
|
|
835
|
+
spec,
|
|
836
|
+
hasPlain: true,
|
|
837
|
+
plain: deserializeFromEncryption(
|
|
838
|
+
stored,
|
|
839
|
+
spec.elementRuntimeType,
|
|
840
|
+
spec.isArray
|
|
841
|
+
),
|
|
842
|
+
index
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return new _CompliantField({
|
|
846
|
+
spec,
|
|
847
|
+
hasPlain: false,
|
|
848
|
+
sealed: stored,
|
|
849
|
+
anon: sealedAnon(stored) ?? void 0,
|
|
850
|
+
index
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/** @internal A queryable field whose ciphertext column was not selected. */
|
|
854
|
+
static fromIndexOnly(index, spec) {
|
|
855
|
+
return new _CompliantField({ spec, hasPlain: false, index });
|
|
856
|
+
}
|
|
857
|
+
/** The field's classification. */
|
|
858
|
+
get level() {
|
|
859
|
+
return stateOf(this).spec.level;
|
|
860
|
+
}
|
|
861
|
+
/** The de-identified value. Never decrypts for values in a `v4` envelope. */
|
|
862
|
+
get anon() {
|
|
863
|
+
const state = stateOf(this);
|
|
864
|
+
if (state.anon === void 0) {
|
|
865
|
+
if (state.spec.anon === "token") {
|
|
866
|
+
throw new LegacyCiphertextError(
|
|
867
|
+
`${state.spec.path} is stored in a pre-v4 envelope and has no token yet; run reencryptEncryptedColumns() once to upgrade it`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
state.anon = computeAnon(state.spec.anon, readPlaintext(this, false));
|
|
871
|
+
}
|
|
872
|
+
return new Anon(state.anon, MINT);
|
|
873
|
+
}
|
|
874
|
+
/** The plaintext. Every read is reported to access listeners. */
|
|
875
|
+
get deanon() {
|
|
876
|
+
return readPlaintext(this, true);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
function readPlaintext(field, report) {
|
|
880
|
+
const state = stateOf(field);
|
|
881
|
+
let tenantId = "";
|
|
882
|
+
if (!state.hasPlain) {
|
|
883
|
+
tenantId = requireBoundTenantId("read");
|
|
884
|
+
if (state.sealed === void 0) {
|
|
885
|
+
throw new Error(
|
|
886
|
+
`${state.spec.path} was not loaded: its ciphertext column was left out of the selection`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
const encryptor = getRegisteredEncryptor();
|
|
890
|
+
if (!encryptor) {
|
|
891
|
+
throw new EncryptionRequiredError(
|
|
892
|
+
`Cannot read ${state.spec.path}: no encryptor registered. Call registerEncryptor() at bootstrap.`
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
const opened = encryptor.open(state.sealed, tenantId);
|
|
896
|
+
state.plain = deserializeFromEncryption(
|
|
897
|
+
opened.plaintext,
|
|
898
|
+
state.spec.elementRuntimeType,
|
|
899
|
+
state.spec.isArray
|
|
900
|
+
);
|
|
901
|
+
state.hasPlain = true;
|
|
902
|
+
}
|
|
903
|
+
if (report) {
|
|
904
|
+
tenantId ||= getBoundTenantId() ?? "";
|
|
905
|
+
for (const listener of LISTENERS) {
|
|
906
|
+
listener({ field: state.spec.path, level: state.spec.level, tenantId });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return state.plain;
|
|
910
|
+
}
|
|
911
|
+
function isCompliantField(value) {
|
|
912
|
+
return value != null && typeof value === "object" && value[FIELD_BRAND] === true;
|
|
913
|
+
}
|
|
914
|
+
function requireEncryptor(path) {
|
|
915
|
+
const encryptor = getRegisteredEncryptor();
|
|
916
|
+
if (!encryptor) {
|
|
917
|
+
throw new EncryptionRequiredError(
|
|
918
|
+
`Cannot write ${path}: no encryptor registered. Call registerEncryptor() at bootstrap; compliant fields are never stored in plaintext.`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return encryptor;
|
|
922
|
+
}
|
|
923
|
+
function sealedValueOf(field) {
|
|
924
|
+
const state = stateOf(field);
|
|
925
|
+
if (state.sealed !== void 0 && state.sealedTenant === void 0) {
|
|
926
|
+
return state.sealed;
|
|
927
|
+
}
|
|
928
|
+
if (!state.hasPlain) return state.sealed;
|
|
929
|
+
const tenantId = requireBoundTenantId("write");
|
|
930
|
+
if (state.sealed !== void 0 && state.sealedTenant === tenantId) {
|
|
931
|
+
return state.sealed;
|
|
932
|
+
}
|
|
933
|
+
if (state.anon === void 0) {
|
|
934
|
+
state.anon = computeAnon(state.spec.anon, state.plain);
|
|
935
|
+
}
|
|
936
|
+
state.sealed = requireEncryptor(state.spec.path).seal(
|
|
937
|
+
serializeForEncryption(state.plain),
|
|
938
|
+
tenantId,
|
|
939
|
+
state.anon
|
|
940
|
+
);
|
|
941
|
+
state.sealedTenant = tenantId;
|
|
942
|
+
return state.sealed;
|
|
943
|
+
}
|
|
944
|
+
function normalizeSerializedForIndex(serialized, spec) {
|
|
945
|
+
return spec.normalize === "lowercase" ? serialized.trim().toLowerCase() : serialized;
|
|
946
|
+
}
|
|
947
|
+
function normalizedForIndex(value, spec) {
|
|
948
|
+
return normalizeSerializedForIndex(serializeForEncryption(value), spec);
|
|
949
|
+
}
|
|
950
|
+
function blindIndexOfValue(value, spec) {
|
|
951
|
+
return requireEncryptor(spec.path).blindIndex(
|
|
952
|
+
normalizedForIndex(value, spec),
|
|
953
|
+
requireBoundTenantId("query")
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
function blindIndexOf(field) {
|
|
957
|
+
const state = stateOf(field);
|
|
958
|
+
if (state.index) return state.index;
|
|
959
|
+
if (!state.hasPlain && state.sealed === void 0) return state.index ?? null;
|
|
960
|
+
try {
|
|
961
|
+
const plain = state.hasPlain ? state.plain : readPlaintext(field, false);
|
|
962
|
+
state.index = blindIndexOfValue(plain, state.spec);
|
|
963
|
+
} catch {
|
|
964
|
+
return state.index ?? null;
|
|
965
|
+
}
|
|
966
|
+
return state.index;
|
|
967
|
+
}
|
|
968
|
+
var PENDING = /* @__PURE__ */ Symbol.for("forklaunch.compliance.pending");
|
|
969
|
+
function isPendingIndex(value) {
|
|
970
|
+
return value != null && typeof value === "object" && PENDING in value;
|
|
971
|
+
}
|
|
972
|
+
function pendingIndexValue(value) {
|
|
973
|
+
return value[PENDING];
|
|
974
|
+
}
|
|
975
|
+
var CompliantTypeBase = class extends Type2 {
|
|
976
|
+
constructor(spec) {
|
|
977
|
+
super();
|
|
978
|
+
this.spec = spec;
|
|
979
|
+
}
|
|
980
|
+
spec;
|
|
981
|
+
getColumnType() {
|
|
982
|
+
return "text";
|
|
983
|
+
}
|
|
984
|
+
get runtimeType() {
|
|
985
|
+
return "string";
|
|
986
|
+
}
|
|
987
|
+
ensureComparable() {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
compareAsType() {
|
|
991
|
+
return "string";
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
var CompliantType = class extends CompliantTypeBase {
|
|
995
|
+
convertToDatabaseValue(value, _platform, context) {
|
|
996
|
+
if (context?.fromQuery) {
|
|
997
|
+
throw new CompliantQueryError(
|
|
998
|
+
`${this.spec.path} cannot be queried: declare it with .compliance('${this.spec.level}', { queryable: true }) to allow equality lookups`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
if (value === null || value === void 0) return null;
|
|
1002
|
+
if (isCompliantField(value)) return sealedValueOf(value) ?? null;
|
|
1003
|
+
if (isEncryptedCiphertext(value)) return value;
|
|
1004
|
+
return sealedValueOf(CompliantField.fromPlain(value, this.spec)) ?? null;
|
|
1005
|
+
}
|
|
1006
|
+
convertToJSValue(value) {
|
|
1007
|
+
if (value === null || value === void 0) return value;
|
|
1008
|
+
if (isCompliantField(value)) return value;
|
|
1009
|
+
return CompliantField.fromStored(String(value), this.spec);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$in", "$nin"]);
|
|
1013
|
+
var CompliantIndexType = class extends CompliantTypeBase {
|
|
1014
|
+
convertToDatabaseValue(value, _platform, context) {
|
|
1015
|
+
if (value === null || value === void 0) return null;
|
|
1016
|
+
if (context?.fromQuery) {
|
|
1017
|
+
const key = context.key;
|
|
1018
|
+
if (key !== void 0 && !EQUALITY_OPERATORS.has(String(key)) && !/^\d+$/.test(String(key))) {
|
|
1019
|
+
throw new CompliantQueryError(
|
|
1020
|
+
`${this.spec.path} supports equality lookups only ($eq, $ne, $in, $nin); ${String(key)} would need the plaintext`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (isCompliantField(value)) return blindIndexOf(value);
|
|
1025
|
+
if (isPendingIndex(value)) return pendingIndexValue(value);
|
|
1026
|
+
if (typeof value === "string" && value.startsWith("bi1:")) return value;
|
|
1027
|
+
return blindIndexOfValue(value, this.spec);
|
|
1028
|
+
}
|
|
1029
|
+
convertToJSValue(value) {
|
|
1030
|
+
if (isCompliantField(value) || isPendingIndex(value)) return value;
|
|
1031
|
+
return { [PENDING]: value ?? null };
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function isCompliantType(type) {
|
|
1035
|
+
return type instanceof CompliantType || type instanceof CompliantIndexType;
|
|
1036
|
+
}
|
|
1037
|
+
var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
|
|
1038
|
+
function slotsOf(entity) {
|
|
1039
|
+
let slots = SLOTS.get(entity);
|
|
1040
|
+
if (!slots) {
|
|
1041
|
+
slots = {};
|
|
1042
|
+
SLOTS.set(entity, slots);
|
|
1043
|
+
}
|
|
1044
|
+
return slots;
|
|
1045
|
+
}
|
|
1046
|
+
function readProperty(entity, binding) {
|
|
1047
|
+
const slots = slotsOf(entity);
|
|
1048
|
+
const value = slots[binding.property];
|
|
1049
|
+
if (binding.sibling) {
|
|
1050
|
+
const stored = slots[binding.sibling];
|
|
1051
|
+
if (isPendingIndex(value)) {
|
|
1052
|
+
const index = pendingIndexValue(value);
|
|
1053
|
+
const field = typeof stored === "string" ? CompliantField.fromStored(stored, binding.spec, index) : CompliantField.fromIndexOnly(index, binding.spec);
|
|
1054
|
+
slots[binding.property] = field;
|
|
1055
|
+
return field;
|
|
1056
|
+
}
|
|
1057
|
+
if (value == null && typeof stored === "string") {
|
|
1058
|
+
const field = CompliantField.fromStored(stored, binding.spec, null);
|
|
1059
|
+
slots[binding.property] = field;
|
|
1060
|
+
return field;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return value;
|
|
1064
|
+
}
|
|
1065
|
+
function sameRules(a, b) {
|
|
1066
|
+
return a === b || a.path === b.path && a.anon === b.anon && a.normalize === b.normalize && a.queryable === b.queryable;
|
|
1067
|
+
}
|
|
1068
|
+
function writeProperty(entity, binding, value) {
|
|
1069
|
+
const slots = slotsOf(entity);
|
|
1070
|
+
if (isPendingIndex(value)) {
|
|
1071
|
+
slots[binding.property] = value;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (value === null || value === void 0) {
|
|
1075
|
+
slots[binding.property] = value;
|
|
1076
|
+
if (binding.sibling) slots[binding.sibling] = value;
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (isCompliantField(value)) {
|
|
1080
|
+
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(readPlaintext(value, false), binding.spec);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
slots[binding.property] = CompliantField.fromPlain(value, binding.spec);
|
|
1084
|
+
}
|
|
1085
|
+
function installCompliantAccessors(prototype, bindings) {
|
|
1086
|
+
installInspect(prototype, bindings);
|
|
1087
|
+
for (const binding of bindings) {
|
|
1088
|
+
Object.defineProperty(prototype, binding.property, {
|
|
1089
|
+
configurable: true,
|
|
1090
|
+
enumerable: true,
|
|
1091
|
+
get() {
|
|
1092
|
+
return readProperty(this, binding);
|
|
1093
|
+
},
|
|
1094
|
+
set(value) {
|
|
1095
|
+
writeProperty(this, binding, value);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
if (binding.sibling) {
|
|
1099
|
+
const sibling = binding.sibling;
|
|
1100
|
+
Object.defineProperty(prototype, sibling, {
|
|
1101
|
+
configurable: true,
|
|
1102
|
+
enumerable: false,
|
|
1103
|
+
get() {
|
|
1104
|
+
const field = readProperty(this, binding);
|
|
1105
|
+
const stored = slotsOf(this)[sibling];
|
|
1106
|
+
if (field === null || field === void 0) return stored ?? field;
|
|
1107
|
+
return sealedValueOf(field) ?? stored;
|
|
1108
|
+
},
|
|
1109
|
+
set(value) {
|
|
1110
|
+
slotsOf(this)[sibling] = value;
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
1117
|
+
function installInspect(prototype, bindings) {
|
|
1118
|
+
const hidden = new Set(
|
|
1119
|
+
bindings.map((b) => b.sibling).filter((s) => !!s)
|
|
1120
|
+
);
|
|
1121
|
+
Object.defineProperty(prototype, INSPECT, {
|
|
1122
|
+
configurable: true,
|
|
1123
|
+
writable: true,
|
|
1124
|
+
value(depth, options, inspect) {
|
|
1125
|
+
const shown = {};
|
|
1126
|
+
for (const key of Object.keys(this)) {
|
|
1127
|
+
if (!hidden.has(key)) shown[key] = this[key];
|
|
1128
|
+
}
|
|
1129
|
+
for (const binding of bindings) {
|
|
1130
|
+
const value = this[binding.property];
|
|
1131
|
+
if (value !== void 0) shown[binding.property] = value;
|
|
1132
|
+
}
|
|
1133
|
+
const name = this.constructor?.name ?? "Entity";
|
|
1134
|
+
return `${name} ${inspect(shown, { ...options, depth })}`;
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
function deanon(entity) {
|
|
1139
|
+
const copy = {};
|
|
1140
|
+
for (const key in entity) {
|
|
1141
|
+
const value = entity[key];
|
|
1142
|
+
copy[key] = isCompliantField(value) ? value.deanon : value;
|
|
1143
|
+
}
|
|
1144
|
+
return copy;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/persistence/compliancePropertyBuilder.ts
|
|
1148
|
+
import { safeParse, safeStringify } from "@forklaunch/common";
|
|
1149
|
+
import { p, Type as Type3 } from "@mikro-orm/core";
|
|
1150
|
+
function isBuilder(value) {
|
|
1151
|
+
return value != null && typeof value === "object" && "~options" in value;
|
|
1152
|
+
}
|
|
1153
|
+
function wrapUnclassified(builder) {
|
|
1154
|
+
return new Proxy(builder, {
|
|
1155
|
+
get(target, prop) {
|
|
1156
|
+
if (prop === "compliance") {
|
|
1157
|
+
return (level, options) => wrapClassified(target, level, options);
|
|
1158
|
+
}
|
|
1159
|
+
if (prop === "~options") return Reflect.get(target, prop, target);
|
|
1160
|
+
if (prop === COMPLIANCE_KEY) return void 0;
|
|
1161
|
+
const value = Reflect.get(target, prop, target);
|
|
1162
|
+
if (typeof value === "function") {
|
|
1163
|
+
return (...args) => {
|
|
1164
|
+
const result = value.apply(
|
|
1165
|
+
target,
|
|
1166
|
+
args
|
|
1167
|
+
);
|
|
1168
|
+
return isBuilder(result) ? wrapUnclassified(result) : result;
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return value;
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
var ENCRYPTED_LEVELS2 = /* @__PURE__ */ new Set([
|
|
1176
|
+
"pii",
|
|
1177
|
+
"phi",
|
|
1178
|
+
"pci"
|
|
1179
|
+
]);
|
|
1180
|
+
function resolveEncryptedTypeArgs(options) {
|
|
1181
|
+
const type = options.type;
|
|
1182
|
+
const isArray = options.array === true;
|
|
1183
|
+
const resolved = resolveTypeInstance(type);
|
|
1184
|
+
if (resolved) {
|
|
1185
|
+
const rt = resolved.runtimeType;
|
|
1186
|
+
if (rt.endsWith("[]")) {
|
|
1187
|
+
return { elementRuntimeType: rt.slice(0, -2) || "string", isArray: true };
|
|
1188
|
+
}
|
|
1189
|
+
return { elementRuntimeType: rt, isArray };
|
|
1190
|
+
}
|
|
1191
|
+
return { elementRuntimeType: "string", isArray };
|
|
1192
|
+
}
|
|
1193
|
+
function wrapClassified(builder, level, complianceOptions) {
|
|
1194
|
+
if (ENCRYPTED_LEVELS2.has(level)) {
|
|
1195
|
+
const options = builder["~options"];
|
|
1196
|
+
if (options) {
|
|
1197
|
+
const { elementRuntimeType, isArray } = resolveEncryptedTypeArgs(options);
|
|
1198
|
+
let enumValues;
|
|
1199
|
+
if (options.items) {
|
|
1200
|
+
enumValues = extractEnumValues(options.items);
|
|
1201
|
+
delete options.items;
|
|
1202
|
+
delete options.nativeEnumName;
|
|
1203
|
+
delete options.enum;
|
|
1204
|
+
}
|
|
1205
|
+
const spec = {
|
|
1206
|
+
level,
|
|
1207
|
+
anon: complianceOptions?.anon ?? defaultAnonStrategy(elementRuntimeType, isArray, !!enumValues),
|
|
1208
|
+
queryable: complianceOptions?.queryable === true,
|
|
1209
|
+
normalize: complianceOptions?.normalize ?? "exact",
|
|
1210
|
+
elementRuntimeType,
|
|
1211
|
+
isArray,
|
|
1212
|
+
enumValues,
|
|
1213
|
+
path: ""
|
|
1214
|
+
// set by defineComplianceEntity once the property name is known
|
|
1215
|
+
};
|
|
1216
|
+
options.type = spec.queryable ? new CompliantIndexType(spec) : new CompliantType(spec);
|
|
1217
|
+
options.columnType = "text";
|
|
1218
|
+
options.runtimeType = "string";
|
|
1219
|
+
options.accessor = true;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return new Proxy(builder, {
|
|
1223
|
+
get(target, prop) {
|
|
1224
|
+
if (prop === COMPLIANCE_KEY) return level;
|
|
1225
|
+
if (prop === "~options") return Reflect.get(target, prop, target);
|
|
1226
|
+
const value = Reflect.get(target, prop, target);
|
|
1227
|
+
if (typeof value === "function") {
|
|
1228
|
+
return (...args) => {
|
|
1229
|
+
const result = value.apply(
|
|
1230
|
+
target,
|
|
1231
|
+
args
|
|
1232
|
+
);
|
|
1233
|
+
return isBuilder(result) ? wrapClassified(result, level) : result;
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return value;
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function wrapRelation(builder) {
|
|
1241
|
+
return wrapClassified(builder, "none");
|
|
1242
|
+
}
|
|
1243
|
+
var RELATION_METHODS = /* @__PURE__ */ new Set([
|
|
1244
|
+
"manyToOne",
|
|
1245
|
+
"oneToMany",
|
|
1246
|
+
"manyToMany",
|
|
1247
|
+
"oneToOne",
|
|
1248
|
+
"embedded"
|
|
1249
|
+
]);
|
|
1250
|
+
function isRelationMethod(prop) {
|
|
1251
|
+
return typeof prop === "string" && RELATION_METHODS.has(prop);
|
|
1252
|
+
}
|
|
1253
|
+
function extractEnumValues(items) {
|
|
1254
|
+
if (typeof items === "function") {
|
|
1255
|
+
return extractEnumValues(items());
|
|
1256
|
+
}
|
|
1257
|
+
if (Array.isArray(items)) return items;
|
|
1258
|
+
if (items != null && typeof items === "object") {
|
|
1259
|
+
const obj = items;
|
|
1260
|
+
const allValues = Object.values(obj);
|
|
1261
|
+
return allValues.filter((v) => typeof v !== "string" || !(v in obj));
|
|
1262
|
+
}
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
var EnumTextType = class extends Type3 {
|
|
1266
|
+
convertToDatabaseValue(value) {
|
|
1267
|
+
return safeStringify(value);
|
|
1268
|
+
}
|
|
1269
|
+
convertToJSValue(value) {
|
|
1270
|
+
return safeParse(value);
|
|
1271
|
+
}
|
|
1272
|
+
getColumnType() {
|
|
1273
|
+
return "text";
|
|
1274
|
+
}
|
|
1275
|
+
get runtimeType() {
|
|
1276
|
+
return "string";
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
function inferEnumType(items) {
|
|
1280
|
+
const values = extractEnumValues(items);
|
|
1281
|
+
if (values.length === 0) return "text";
|
|
1282
|
+
const allStrings = values.every((v) => typeof v === "string");
|
|
1283
|
+
if (allStrings) return "text";
|
|
1284
|
+
const allNumbers = values.every((v) => typeof v === "number");
|
|
1285
|
+
if (allNumbers) return "integer";
|
|
1286
|
+
return new EnumTextType();
|
|
1287
|
+
}
|
|
1288
|
+
var fp = new Proxy(p, {
|
|
1289
|
+
get(target, prop) {
|
|
1290
|
+
const value = Reflect.get(target, prop, target);
|
|
1291
|
+
if (typeof value !== "function") return value;
|
|
1292
|
+
if (isRelationMethod(prop)) {
|
|
1293
|
+
return (...args) => {
|
|
1294
|
+
const result = value.apply(
|
|
1295
|
+
target,
|
|
1296
|
+
args
|
|
1297
|
+
);
|
|
1298
|
+
return isBuilder(result) ? wrapRelation(result) : result;
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
return (...args) => {
|
|
1302
|
+
const result = value.apply(
|
|
1303
|
+
target,
|
|
1304
|
+
args
|
|
1305
|
+
);
|
|
1306
|
+
if (isBuilder(result)) {
|
|
1307
|
+
if (prop === "enum") {
|
|
1308
|
+
const options = result["~options"];
|
|
1309
|
+
if (options && !options.type) {
|
|
1310
|
+
options.type = inferEnumType(options.items);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return wrapUnclassified(result);
|
|
1314
|
+
}
|
|
1315
|
+
return result;
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// src/persistence/defineComplianceEntity.ts
|
|
1321
|
+
import {
|
|
1322
|
+
defineEntity,
|
|
1323
|
+
p as p2,
|
|
1324
|
+
UnderscoreNamingStrategy
|
|
1325
|
+
} from "@mikro-orm/core";
|
|
1326
|
+
var naming = new UnderscoreNamingStrategy();
|
|
1327
|
+
function wireCompliantProperties(entityName, properties) {
|
|
1328
|
+
const wired = {};
|
|
1329
|
+
const bindings = [];
|
|
1330
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
1331
|
+
wired[name] = prop;
|
|
1332
|
+
if (typeof prop === "function" || prop == null) continue;
|
|
1333
|
+
const options = prop["~options"] ?? prop;
|
|
1334
|
+
const type = options.type;
|
|
1335
|
+
if (!isCompliantType(type)) continue;
|
|
1336
|
+
type.spec.path = `${entityName}.${name}`;
|
|
1337
|
+
if (!(type instanceof CompliantIndexType)) {
|
|
1338
|
+
bindings.push({ property: name, spec: type.spec });
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const sibling = `${name}Sealed`;
|
|
1342
|
+
if (sibling in properties) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Entity '${entityName}': '${sibling}' is reserved for the encrypted value of queryable field '${name}'`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const explicit = options.fieldName ?? options.fieldNames?.[0];
|
|
1348
|
+
const column = typeof explicit === "string" ? explicit : naming.propertyToColumnName(name);
|
|
1349
|
+
const required = options.nullable !== true;
|
|
1350
|
+
options.fieldName = `${column}_idx`;
|
|
1351
|
+
delete options.fieldNames;
|
|
1352
|
+
options.nullable = true;
|
|
1353
|
+
wired[sibling] = {
|
|
1354
|
+
type: "text",
|
|
1355
|
+
columnType: "text",
|
|
1356
|
+
fieldName: column,
|
|
1357
|
+
nullable: !required,
|
|
1358
|
+
hidden: true,
|
|
1359
|
+
accessor: true
|
|
1360
|
+
};
|
|
1361
|
+
bindings.push({ property: name, spec: type.spec, sibling });
|
|
1362
|
+
}
|
|
1363
|
+
return { properties: wired, bindings };
|
|
1364
|
+
}
|
|
1365
|
+
function readComplianceLevel(builder) {
|
|
1366
|
+
if (builder == null || typeof builder !== "object") return void 0;
|
|
1367
|
+
return builder[COMPLIANCE_KEY];
|
|
1368
|
+
}
|
|
1369
|
+
function defineComplianceEntity(meta) {
|
|
1370
|
+
const entityName = "name" in meta ? meta.name : "Unknown";
|
|
1371
|
+
const complianceFields = /* @__PURE__ */ new Map();
|
|
1372
|
+
const rawProperties = meta.properties;
|
|
1373
|
+
const resolvedProperties = typeof rawProperties === "function" ? rawProperties(p2) : rawProperties;
|
|
1374
|
+
for (const [fieldName, rawProp] of Object.entries(resolvedProperties)) {
|
|
1375
|
+
if (typeof rawProp === "function") {
|
|
1376
|
+
complianceFields.set(fieldName, "none");
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const level = readComplianceLevel(rawProp);
|
|
1380
|
+
if (level == null) {
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`Field '${entityName}.${fieldName}' is missing compliance classification. Call .compliance('pii' | 'phi' | 'pci' | 'none') on this property, or use a relation method (fp.manyToOne, etc.) which is auto-classified.`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
complianceFields.set(fieldName, level);
|
|
1386
|
+
}
|
|
1387
|
+
registerEntityCompliance(entityName, complianceFields);
|
|
1388
|
+
if (meta.retention) {
|
|
1389
|
+
parseDuration(meta.retention.duration);
|
|
1390
|
+
if (!resolvedProperties["createdAt"]) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
`Entity '${entityName}' has a retention policy but no 'createdAt' property. Retention requires createdAt to compute expiration.`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
registerEntityRetention(entityName, meta.retention);
|
|
1396
|
+
}
|
|
1397
|
+
if (meta.userIdField) {
|
|
1398
|
+
registerEntityUserIdField(entityName, meta.userIdField);
|
|
1399
|
+
}
|
|
1400
|
+
const wired = wireCompliantProperties(entityName, resolvedProperties);
|
|
1401
|
+
const schema = defineEntity({
|
|
1402
|
+
...meta,
|
|
1403
|
+
properties: wired.properties
|
|
1404
|
+
});
|
|
1405
|
+
if (wired.bindings.length > 0) {
|
|
1406
|
+
const entityClass = schema.meta.class;
|
|
1407
|
+
if (entityClass)
|
|
1408
|
+
installCompliantAccessors(entityClass.prototype, wired.bindings);
|
|
1409
|
+
const setClass = schema.setClass.bind(schema);
|
|
1410
|
+
schema.setClass = ((cls) => {
|
|
1411
|
+
installCompliantAccessors(cls.prototype, wired.bindings);
|
|
1412
|
+
return setClass(cls);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
return schema;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// src/persistence/complianceEventSubscriber.ts
|
|
1419
|
+
var ENCRYPTED_LEVELS3 = /* @__PURE__ */ new Set([
|
|
1420
|
+
"pii",
|
|
1421
|
+
"phi",
|
|
1422
|
+
"pci"
|
|
842
1423
|
]);
|
|
843
1424
|
function wrapEmWithNativeQueryBlocking(em) {
|
|
844
1425
|
const BLOCKED_METHODS = [
|
|
@@ -974,14 +1555,23 @@ async function reencryptEncryptedColumns(options) {
|
|
|
974
1555
|
const fields = getEntityComplianceFields(meta.className);
|
|
975
1556
|
if (!fields) continue;
|
|
976
1557
|
const props = meta.properties;
|
|
977
|
-
const
|
|
1558
|
+
const compliant = compliantColumnsOf(props);
|
|
1559
|
+
const compliantNames = new Set(compliant.map((c) => c.property));
|
|
1560
|
+
const encryptedProps = [...fields.entries()].filter(
|
|
1561
|
+
([prop, level]) => ENCRYPTED_LEVELS4.has(level) && !compliantNames.has(prop)
|
|
1562
|
+
).map(([prop]) => props[prop]).filter(
|
|
978
1563
|
(p3) => !!p3 && Array.isArray(p3.fieldNames) && p3.fieldNames.length === 1
|
|
979
1564
|
);
|
|
980
|
-
if (encryptedProps.length === 0) continue;
|
|
1565
|
+
if (encryptedProps.length === 0 && compliant.length === 0) continue;
|
|
981
1566
|
const pkProp = props[meta.primaryKeys[0]];
|
|
982
1567
|
if (!pkProp) continue;
|
|
983
1568
|
const pkColumn = pkProp.fieldNames[0];
|
|
984
|
-
const columns =
|
|
1569
|
+
const columns = [
|
|
1570
|
+
...encryptedProps.map((p3) => p3.fieldNames[0]),
|
|
1571
|
+
...compliant.flatMap(
|
|
1572
|
+
(c) => c.indexColumn ? [c.envelopeColumn, c.indexColumn] : [c.envelopeColumn]
|
|
1573
|
+
)
|
|
1574
|
+
];
|
|
985
1575
|
const report = {
|
|
986
1576
|
table: meta.tableName,
|
|
987
1577
|
columns,
|
|
@@ -992,7 +1582,9 @@ async function reencryptEncryptedColumns(options) {
|
|
|
992
1582
|
unreadable: 0,
|
|
993
1583
|
unreadableIds: [],
|
|
994
1584
|
byKeyId: {},
|
|
995
|
-
missingKeyIds: {}
|
|
1585
|
+
missingKeyIds: {},
|
|
1586
|
+
sealed: 0,
|
|
1587
|
+
reindexed: 0
|
|
996
1588
|
};
|
|
997
1589
|
const exists = await execute(`select to_regclass(?) as t`, [
|
|
998
1590
|
quote(meta.tableName)
|
|
@@ -1037,6 +1629,34 @@ async function reencryptEncryptedColumns(options) {
|
|
|
1037
1629
|
report.missingKeyIds[missing2] = (report.missingKeyIds[missing2] ?? 0) + 1;
|
|
1038
1630
|
}
|
|
1039
1631
|
}
|
|
1632
|
+
for (const column of compliant) {
|
|
1633
|
+
const outcome = planCompliantColumn(
|
|
1634
|
+
column,
|
|
1635
|
+
row,
|
|
1636
|
+
encryptor,
|
|
1637
|
+
tenantIds,
|
|
1638
|
+
fallbackTenantIds
|
|
1639
|
+
);
|
|
1640
|
+
if (outcome.kind === "unreadable") {
|
|
1641
|
+
report.unreadable += 1;
|
|
1642
|
+
rowUnreadable = true;
|
|
1643
|
+
const missing2 = stampedKeyId(String(row[column.envelopeColumn]));
|
|
1644
|
+
if (missing2)
|
|
1645
|
+
report.missingKeyIds[missing2] = (report.missingKeyIds[missing2] ?? 0) + 1;
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (outcome.kind === "empty") continue;
|
|
1649
|
+
if (outcome.sealed) {
|
|
1650
|
+
report.sealed = (report.sealed ?? 0) + 1;
|
|
1651
|
+
report.rewritten += 1;
|
|
1652
|
+
if (outcome.fromKeyId)
|
|
1653
|
+
report.byKeyId[outcome.fromKeyId] = (report.byKeyId[outcome.fromKeyId] ?? 0) + 1;
|
|
1654
|
+
} else {
|
|
1655
|
+
report.current += 1;
|
|
1656
|
+
}
|
|
1657
|
+
if (outcome.reindexed) report.reindexed = (report.reindexed ?? 0) + 1;
|
|
1658
|
+
updates.push(...outcome.updates.map((u) => ({ ...u, jsonb: false })));
|
|
1659
|
+
}
|
|
1040
1660
|
if (rowUnreadable) report.unreadableIds.push(String(row[pkColumn]));
|
|
1041
1661
|
if (updates.length && !dryRun) {
|
|
1042
1662
|
const sets = updates.map(
|
|
@@ -1051,7 +1671,7 @@ async function reencryptEncryptedColumns(options) {
|
|
|
1051
1671
|
const byKey = Object.entries(report.byKeyId).map(([k, n]) => `${k}: ${n}`).join(", ");
|
|
1052
1672
|
const missing = Object.entries(report.missingKeyIds).map(([k, n]) => `${k}: ${n}`).join(", ");
|
|
1053
1673
|
log(
|
|
1054
|
-
`[key-rotation] ${meta.tableName}: scanned ${report.scanned} rows, ${report.rewritten} values ${dryRun ? "would be " : ""}rewritten${byKey ? ` (from ${byKey})` : ""}, ${report.current} already current, ${report.plaintext} plaintext, ${report.unreadable} unreadable${missing ? ` (stamped with keys not in the ring: ${missing})` : ""}${report.unreadableIds.length ? ` (rows ${report.unreadableIds.slice(0, 10).join(", ")}${report.unreadableIds.length > 10 ? ", ..." : ""})` : ""}`
|
|
1674
|
+
`[key-rotation] ${meta.tableName}: scanned ${report.scanned} rows, ${report.rewritten} values ${dryRun ? "would be " : ""}rewritten${byKey ? ` (from ${byKey})` : ""}, ${report.current} already current, ${report.plaintext} plaintext, ${report.sealed ?? 0} compliant values sealed, ${report.reindexed ?? 0} blind indexes written, ${report.unreadable} unreadable${missing ? ` (stamped with keys not in the ring: ${missing})` : ""}${report.unreadableIds.length ? ` (rows ${report.unreadableIds.slice(0, 10).join(", ")}${report.unreadableIds.length > 10 ? ", ..." : ""})` : ""}`
|
|
1055
1675
|
);
|
|
1056
1676
|
reports.push(report);
|
|
1057
1677
|
}
|
|
@@ -1082,6 +1702,93 @@ function rotationTotals(reports) {
|
|
|
1082
1702
|
unreadable: sum((r) => r.unreadable)
|
|
1083
1703
|
};
|
|
1084
1704
|
}
|
|
1705
|
+
function compliantColumnsOf(props) {
|
|
1706
|
+
const columns = [];
|
|
1707
|
+
for (const [property, prop] of Object.entries(props)) {
|
|
1708
|
+
const type = prop?.customType;
|
|
1709
|
+
if (!prop || !isCompliantType(type)) continue;
|
|
1710
|
+
if (type instanceof CompliantIndexType) {
|
|
1711
|
+
const sibling = props[`${property}Sealed`];
|
|
1712
|
+
if (!sibling?.fieldNames?.[0]) continue;
|
|
1713
|
+
columns.push({
|
|
1714
|
+
property,
|
|
1715
|
+
spec: type.spec,
|
|
1716
|
+
envelopeColumn: sibling.fieldNames[0],
|
|
1717
|
+
indexColumn: prop.fieldNames[0]
|
|
1718
|
+
});
|
|
1719
|
+
} else {
|
|
1720
|
+
columns.push({
|
|
1721
|
+
property,
|
|
1722
|
+
spec: type.spec,
|
|
1723
|
+
envelopeColumn: prop.fieldNames[0]
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return columns;
|
|
1728
|
+
}
|
|
1729
|
+
function planCompliantColumn(column, row, encryptor, tenantIds, fallbackTenantIds) {
|
|
1730
|
+
const stored = row[column.envelopeColumn];
|
|
1731
|
+
if (stored === null || stored === void 0) {
|
|
1732
|
+
if (column.indexColumn && row[column.indexColumn] != null) {
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "ok",
|
|
1735
|
+
sealed: false,
|
|
1736
|
+
reindexed: true,
|
|
1737
|
+
updates: [{ column: column.indexColumn, next: null }]
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
return { kind: "empty" };
|
|
1741
|
+
}
|
|
1742
|
+
const primary = tenantIds.length ? tenantIds : [""];
|
|
1743
|
+
const candidates = [
|
|
1744
|
+
...primary,
|
|
1745
|
+
...fallbackTenantIds.filter((t) => !primary.includes(t))
|
|
1746
|
+
];
|
|
1747
|
+
for (const tenantId of candidates) {
|
|
1748
|
+
let serialized;
|
|
1749
|
+
let anon;
|
|
1750
|
+
let current = false;
|
|
1751
|
+
let fromKeyId;
|
|
1752
|
+
if (!isEncryptedValue(stored)) {
|
|
1753
|
+
serialized = String(stored);
|
|
1754
|
+
} else {
|
|
1755
|
+
const opened = tryOpen(encryptor, String(stored), tenantId);
|
|
1756
|
+
if (!opened) continue;
|
|
1757
|
+
serialized = opened.plaintext;
|
|
1758
|
+
anon = opened.anon;
|
|
1759
|
+
current = opened.version === "v4" && !opened.stale;
|
|
1760
|
+
fromKeyId = opened.keyId;
|
|
1761
|
+
}
|
|
1762
|
+
const updates = [];
|
|
1763
|
+
if (!current) {
|
|
1764
|
+
anon ??= computeAnon(
|
|
1765
|
+
column.spec.anon,
|
|
1766
|
+
deserializeFromEncryption(
|
|
1767
|
+
serialized,
|
|
1768
|
+
column.spec.elementRuntimeType,
|
|
1769
|
+
column.spec.isArray
|
|
1770
|
+
)
|
|
1771
|
+
);
|
|
1772
|
+
updates.push({
|
|
1773
|
+
column: column.envelopeColumn,
|
|
1774
|
+
next: encryptor.seal(serialized, tenantId, anon)
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
let reindexed = false;
|
|
1778
|
+
if (column.indexColumn) {
|
|
1779
|
+
const index = encryptor.blindIndex(
|
|
1780
|
+
normalizeSerializedForIndex(serialized, column.spec),
|
|
1781
|
+
tenantId
|
|
1782
|
+
);
|
|
1783
|
+
if (row[column.indexColumn] !== index) {
|
|
1784
|
+
updates.push({ column: column.indexColumn, next: index });
|
|
1785
|
+
reindexed = true;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return { kind: "ok", sealed: !current, reindexed, fromKeyId, updates };
|
|
1789
|
+
}
|
|
1790
|
+
return { kind: "unreadable" };
|
|
1791
|
+
}
|
|
1085
1792
|
|
|
1086
1793
|
// src/persistence/tenantFilter.ts
|
|
1087
1794
|
var TENANT_FILTER_NAME = "tenant";
|
|
@@ -1253,7 +1960,13 @@ function rebind(result, target, proxy) {
|
|
|
1253
1960
|
return result === target ? proxy : result;
|
|
1254
1961
|
}
|
|
1255
1962
|
export {
|
|
1963
|
+
Anon,
|
|
1964
|
+
BLIND_INDEX_PREFIX,
|
|
1256
1965
|
ComplianceLevel,
|
|
1966
|
+
CompliantField,
|
|
1967
|
+
CompliantIndexType,
|
|
1968
|
+
CompliantQueryError,
|
|
1969
|
+
CompliantType,
|
|
1257
1970
|
DecryptionError,
|
|
1258
1971
|
ENCRYPTED_PREFIXES2 as ENCRYPTED_PREFIXES,
|
|
1259
1972
|
ENCRYPTION_FORMAT_ENV,
|
|
@@ -1264,6 +1977,7 @@ export {
|
|
|
1264
1977
|
FieldEncryptor,
|
|
1265
1978
|
LEGACY_ENCRYPTION_KEYS_ENV,
|
|
1266
1979
|
LEGACY_ENCRYPTION_KEY_ENV,
|
|
1980
|
+
LegacyCiphertextError,
|
|
1267
1981
|
MissingEncryptionKeyError,
|
|
1268
1982
|
RetentionAction,
|
|
1269
1983
|
RetentionDuration,
|
|
@@ -1276,6 +1990,7 @@ export {
|
|
|
1276
1990
|
collectFallbackTenantIds,
|
|
1277
1991
|
countValuesByKeyId,
|
|
1278
1992
|
createTenantFilterDef,
|
|
1993
|
+
deanon,
|
|
1279
1994
|
defaultTenantIdsFor,
|
|
1280
1995
|
defineComplianceEntity,
|
|
1281
1996
|
encryptionKeyId,
|
|
@@ -1291,13 +2006,17 @@ export {
|
|
|
1291
2006
|
getEntityRetention,
|
|
1292
2007
|
getEntityUserIdField,
|
|
1293
2008
|
getSuperAdminContext,
|
|
2009
|
+
isAnon,
|
|
2010
|
+
isCompliantField,
|
|
1294
2011
|
isEncryptedCiphertext,
|
|
1295
2012
|
isEncryptedValue,
|
|
2013
|
+
onComplianceAccess,
|
|
1296
2014
|
parseDuration,
|
|
1297
2015
|
parseEncryptionKeyList,
|
|
1298
2016
|
reencryptEncryptedColumns,
|
|
1299
2017
|
registerEncryptor,
|
|
1300
2018
|
rotationTotals,
|
|
2019
|
+
sealedAnon,
|
|
1301
2020
|
setEncryptionTenantId,
|
|
1302
2021
|
setupRls,
|
|
1303
2022
|
setupTenantFilter,
|