@forklaunch/core 3.0.1 → 3.2.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 +91 -4
- package/bin/gateway-mock/server.mjs +39 -21
- package/lib/http/emailClient.d.ts +46 -0
- package/lib/http/emailClient.d.ts.map +1 -1
- package/lib/http/index.d.ts +2 -0
- package/lib/http/index.d.ts.map +1 -1
- package/lib/http/index.js +393 -91
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +351 -53
- package/lib/http/index.mjs.map +1 -1
- package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
- package/lib/http/paymentsClient.d.ts +19 -8
- package/lib/http/paymentsClient.d.ts.map +1 -1
- package/lib/http/previewModelGatewayClient.d.ts +48 -0
- package/lib/http/previewModelGatewayClient.d.ts.map +1 -0
- package/lib/http/telemetry/auditLogger.d.ts +5 -1
- package/lib/http/telemetry/auditLogger.d.ts.map +1 -1
- package/lib/http/telemetry/requestAudit.d.ts +30 -0
- package/lib/http/telemetry/requestAudit.d.ts.map +1 -0
- package/lib/persistence/complianceChanges.d.ts +30 -0
- package/lib/persistence/complianceChanges.d.ts.map +1 -0
- package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
- package/lib/persistence/compliantField.d.ts +41 -8
- package/lib/persistence/compliantField.d.ts.map +1 -1
- package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
- package/lib/persistence/index.d.ts +2 -1
- package/lib/persistence/index.d.ts.map +1 -1
- package/lib/persistence/index.js +150 -18
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +148 -18
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +34 -34
|
@@ -727,6 +727,22 @@ var LegacyCiphertextError = class extends Error {
|
|
|
727
727
|
var CompliantQueryError = class extends Error {
|
|
728
728
|
name = "CompliantQueryError";
|
|
729
729
|
};
|
|
730
|
+
var TenantMismatchError = class extends Error {
|
|
731
|
+
constructor(path, operation, ownerTenantId, boundTenantId) {
|
|
732
|
+
super(
|
|
733
|
+
`Cannot ${operation} ${path || "a compliant field"} under tenant '${boundTenantId}': it belongs to tenant '${ownerTenantId}', the tenant it was loaded or assigned under. Read it outside any encryption context, or inside the owning tenant's.`
|
|
734
|
+
);
|
|
735
|
+
this.path = path;
|
|
736
|
+
this.operation = operation;
|
|
737
|
+
this.ownerTenantId = ownerTenantId;
|
|
738
|
+
this.boundTenantId = boundTenantId;
|
|
739
|
+
}
|
|
740
|
+
path;
|
|
741
|
+
operation;
|
|
742
|
+
ownerTenantId;
|
|
743
|
+
boundTenantId;
|
|
744
|
+
name = "TenantMismatchError";
|
|
745
|
+
};
|
|
730
746
|
var MINT = /* @__PURE__ */ Symbol.for("forklaunch.compliance.mint");
|
|
731
747
|
var ANON_BRAND = /* @__PURE__ */ Symbol.for("forklaunch.compliance.anon");
|
|
732
748
|
var Anon = class {
|
|
@@ -807,10 +823,11 @@ var CompliantField = class _CompliantField {
|
|
|
807
823
|
Object.freeze(this);
|
|
808
824
|
}
|
|
809
825
|
/**
|
|
810
|
-
* @internal Wrap a plaintext value for `spec
|
|
811
|
-
*
|
|
826
|
+
* @internal Wrap a plaintext value for `spec`, owned by `tenant` (the one
|
|
827
|
+
* bound where it was assigned, if any). Applications assign plain values to
|
|
828
|
+
* the entity property instead.
|
|
812
829
|
*/
|
|
813
|
-
static fromPlain(value, spec) {
|
|
830
|
+
static fromPlain(value, spec, tenant) {
|
|
814
831
|
if (spec.enumValues) {
|
|
815
832
|
const values = spec.isArray && Array.isArray(value) ? value : [value];
|
|
816
833
|
for (const v of values) {
|
|
@@ -823,16 +840,22 @@ var CompliantField = class _CompliantField {
|
|
|
823
840
|
}
|
|
824
841
|
return new _CompliantField({
|
|
825
842
|
spec,
|
|
843
|
+
tenant,
|
|
826
844
|
hasPlain: true,
|
|
827
845
|
plain: value,
|
|
828
846
|
anon: computeAnon(spec.anon, value)
|
|
829
847
|
});
|
|
830
848
|
}
|
|
831
|
-
/**
|
|
832
|
-
|
|
849
|
+
/**
|
|
850
|
+
* @internal Wrap a stored value (any envelope, or legacy plaintext) loaded
|
|
851
|
+
* under `tenant`: the tenant bound during hydration, which is the loading
|
|
852
|
+
* entity manager's.
|
|
853
|
+
*/
|
|
854
|
+
static fromStored(stored, spec, index, tenant) {
|
|
833
855
|
if (!isEncryptedCiphertext(stored)) {
|
|
834
856
|
return new _CompliantField({
|
|
835
857
|
spec,
|
|
858
|
+
tenant,
|
|
836
859
|
hasPlain: true,
|
|
837
860
|
plain: deserializeFromEncryption(
|
|
838
861
|
stored,
|
|
@@ -844,6 +867,7 @@ var CompliantField = class _CompliantField {
|
|
|
844
867
|
}
|
|
845
868
|
return new _CompliantField({
|
|
846
869
|
spec,
|
|
870
|
+
tenant,
|
|
847
871
|
hasPlain: false,
|
|
848
872
|
sealed: stored,
|
|
849
873
|
anon: sealedAnon(stored) ?? void 0,
|
|
@@ -876,11 +900,24 @@ var CompliantField = class _CompliantField {
|
|
|
876
900
|
return readPlaintext(this, true);
|
|
877
901
|
}
|
|
878
902
|
};
|
|
903
|
+
function tenantOf(state, operation) {
|
|
904
|
+
const bound = getBoundTenantId();
|
|
905
|
+
if (state.tenant === void 0) return requireBoundTenantId(operation);
|
|
906
|
+
if (bound !== void 0 && bound !== state.tenant) {
|
|
907
|
+
throw new TenantMismatchError(
|
|
908
|
+
state.spec.path,
|
|
909
|
+
operation,
|
|
910
|
+
state.tenant,
|
|
911
|
+
bound
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return state.tenant;
|
|
915
|
+
}
|
|
879
916
|
function readPlaintext(field, report) {
|
|
880
917
|
const state = stateOf(field);
|
|
881
918
|
let tenantId = "";
|
|
882
919
|
if (!state.hasPlain) {
|
|
883
|
-
tenantId =
|
|
920
|
+
tenantId = tenantOf(state, "read");
|
|
884
921
|
if (state.sealed === void 0) {
|
|
885
922
|
throw new Error(
|
|
886
923
|
`${state.spec.path} was not loaded: its ciphertext column was left out of the selection`
|
|
@@ -899,6 +936,8 @@ function readPlaintext(field, report) {
|
|
|
899
936
|
state.spec.isArray
|
|
900
937
|
);
|
|
901
938
|
state.hasPlain = true;
|
|
939
|
+
} else if (state.tenant !== void 0) {
|
|
940
|
+
tenantId = tenantOf(state, "read");
|
|
902
941
|
}
|
|
903
942
|
if (report) {
|
|
904
943
|
tenantId ||= getBoundTenantId() ?? "";
|
|
@@ -926,10 +965,11 @@ function sealedValueOf(field) {
|
|
|
926
965
|
return state.sealed;
|
|
927
966
|
}
|
|
928
967
|
if (!state.hasPlain) return state.sealed;
|
|
929
|
-
|
|
930
|
-
if (state.sealed !== void 0 && state.sealedTenant === tenantId) {
|
|
968
|
+
if (state.sealed !== void 0 && state.sealedTenant === state.tenant) {
|
|
931
969
|
return state.sealed;
|
|
932
970
|
}
|
|
971
|
+
const tenantId = tenantOf(state, "write");
|
|
972
|
+
state.tenant = tenantId;
|
|
933
973
|
if (state.anon === void 0) {
|
|
934
974
|
state.anon = computeAnon(state.spec.anon, state.plain);
|
|
935
975
|
}
|
|
@@ -948,9 +988,12 @@ function normalizedForIndex(value, spec) {
|
|
|
948
988
|
return normalizeSerializedForIndex(serializeForEncryption(value), spec);
|
|
949
989
|
}
|
|
950
990
|
function blindIndexOfValue(value, spec) {
|
|
991
|
+
return blindIndexUnder(value, spec, requireBoundTenantId("query"));
|
|
992
|
+
}
|
|
993
|
+
function blindIndexUnder(value, spec, tenantId) {
|
|
951
994
|
return requireEncryptor(spec.path).blindIndex(
|
|
952
995
|
normalizedForIndex(value, spec),
|
|
953
|
-
|
|
996
|
+
tenantId
|
|
954
997
|
);
|
|
955
998
|
}
|
|
956
999
|
function blindIndexOf(field) {
|
|
@@ -959,7 +1002,7 @@ function blindIndexOf(field) {
|
|
|
959
1002
|
if (!state.hasPlain && state.sealed === void 0) return state.index ?? null;
|
|
960
1003
|
try {
|
|
961
1004
|
const plain = state.hasPlain ? state.plain : readPlaintext(field, false);
|
|
962
|
-
state.index =
|
|
1005
|
+
state.index = blindIndexUnder(plain, state.spec, tenantOf(state, "index"));
|
|
963
1006
|
} catch {
|
|
964
1007
|
return state.index ?? null;
|
|
965
1008
|
}
|
|
@@ -972,6 +1015,7 @@ function isPendingIndex(value) {
|
|
|
972
1015
|
function pendingIndexValue(value) {
|
|
973
1016
|
return value[PENDING];
|
|
974
1017
|
}
|
|
1018
|
+
var COMPLIANT_RUNTIME_TYPE = "CompliantField";
|
|
975
1019
|
var CompliantTypeBase = class extends Type2 {
|
|
976
1020
|
constructor(spec) {
|
|
977
1021
|
super();
|
|
@@ -982,7 +1026,7 @@ var CompliantTypeBase = class extends Type2 {
|
|
|
982
1026
|
return "text";
|
|
983
1027
|
}
|
|
984
1028
|
get runtimeType() {
|
|
985
|
-
return
|
|
1029
|
+
return COMPLIANT_RUNTIME_TYPE;
|
|
986
1030
|
}
|
|
987
1031
|
ensureComparable() {
|
|
988
1032
|
return false;
|
|
@@ -1001,12 +1045,19 @@ var CompliantType = class extends CompliantTypeBase {
|
|
|
1001
1045
|
if (value === null || value === void 0) return null;
|
|
1002
1046
|
if (isCompliantField(value)) return sealedValueOf(value) ?? null;
|
|
1003
1047
|
if (isEncryptedCiphertext(value)) return value;
|
|
1004
|
-
return sealedValueOf(
|
|
1048
|
+
return sealedValueOf(
|
|
1049
|
+
CompliantField.fromPlain(value, this.spec, getBoundTenantId())
|
|
1050
|
+
) ?? null;
|
|
1005
1051
|
}
|
|
1006
1052
|
convertToJSValue(value) {
|
|
1007
1053
|
if (value === null || value === void 0) return value;
|
|
1008
1054
|
if (isCompliantField(value)) return value;
|
|
1009
|
-
return CompliantField.fromStored(
|
|
1055
|
+
return CompliantField.fromStored(
|
|
1056
|
+
String(value),
|
|
1057
|
+
this.spec,
|
|
1058
|
+
void 0,
|
|
1059
|
+
getBoundTenantId()
|
|
1060
|
+
);
|
|
1010
1061
|
}
|
|
1011
1062
|
};
|
|
1012
1063
|
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$in", "$nin"]);
|
|
@@ -1035,6 +1086,18 @@ function isCompliantType(type) {
|
|
|
1035
1086
|
return type instanceof CompliantType || type instanceof CompliantIndexType;
|
|
1036
1087
|
}
|
|
1037
1088
|
var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
|
|
1089
|
+
var LOADED_TENANTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.loadedTenants")] ??= /* @__PURE__ */ new WeakMap();
|
|
1090
|
+
function loadedTenantOf(entity, binding) {
|
|
1091
|
+
return LOADED_TENANTS.get(entity)?.[binding.property];
|
|
1092
|
+
}
|
|
1093
|
+
function recordLoadedTenant(entity, binding) {
|
|
1094
|
+
let tenants = LOADED_TENANTS.get(entity);
|
|
1095
|
+
if (!tenants) {
|
|
1096
|
+
tenants = {};
|
|
1097
|
+
LOADED_TENANTS.set(entity, tenants);
|
|
1098
|
+
}
|
|
1099
|
+
tenants[binding.property] = getBoundTenantId();
|
|
1100
|
+
}
|
|
1038
1101
|
function slotsOf(entity) {
|
|
1039
1102
|
let slots = SLOTS.get(entity);
|
|
1040
1103
|
if (!slots) {
|
|
@@ -1050,12 +1113,22 @@ function readProperty(entity, binding) {
|
|
|
1050
1113
|
const stored = slots[binding.sibling];
|
|
1051
1114
|
if (isPendingIndex(value)) {
|
|
1052
1115
|
const index = pendingIndexValue(value);
|
|
1053
|
-
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1116
|
+
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1117
|
+
stored,
|
|
1118
|
+
binding.spec,
|
|
1119
|
+
index,
|
|
1120
|
+
loadedTenantOf(entity, binding)
|
|
1121
|
+
) : CompliantField.fromIndexOnly(index, binding.spec);
|
|
1054
1122
|
slots[binding.property] = field;
|
|
1055
1123
|
return field;
|
|
1056
1124
|
}
|
|
1057
1125
|
if (value == null && typeof stored === "string") {
|
|
1058
|
-
const field = CompliantField.fromStored(
|
|
1126
|
+
const field = CompliantField.fromStored(
|
|
1127
|
+
stored,
|
|
1128
|
+
binding.spec,
|
|
1129
|
+
null,
|
|
1130
|
+
loadedTenantOf(entity, binding)
|
|
1131
|
+
);
|
|
1059
1132
|
slots[binding.property] = field;
|
|
1060
1133
|
return field;
|
|
1061
1134
|
}
|
|
@@ -1077,10 +1150,18 @@ function writeProperty(entity, binding, value) {
|
|
|
1077
1150
|
return;
|
|
1078
1151
|
}
|
|
1079
1152
|
if (isCompliantField(value)) {
|
|
1080
|
-
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1153
|
+
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1154
|
+
readPlaintext(value, false),
|
|
1155
|
+
binding.spec,
|
|
1156
|
+
stateOf(value).tenant ?? getBoundTenantId()
|
|
1157
|
+
);
|
|
1081
1158
|
return;
|
|
1082
1159
|
}
|
|
1083
|
-
slots[binding.property] = CompliantField.fromPlain(
|
|
1160
|
+
slots[binding.property] = CompliantField.fromPlain(
|
|
1161
|
+
value,
|
|
1162
|
+
binding.spec,
|
|
1163
|
+
getBoundTenantId()
|
|
1164
|
+
);
|
|
1084
1165
|
}
|
|
1085
1166
|
function installCompliantAccessors(prototype, bindings) {
|
|
1086
1167
|
installInspect(prototype, bindings);
|
|
@@ -1107,6 +1188,7 @@ function installCompliantAccessors(prototype, bindings) {
|
|
|
1107
1188
|
return sealedValueOf(field) ?? stored;
|
|
1108
1189
|
},
|
|
1109
1190
|
set(value) {
|
|
1191
|
+
recordLoadedTenant(this, binding);
|
|
1110
1192
|
slotsOf(this)[sibling] = value;
|
|
1111
1193
|
}
|
|
1112
1194
|
});
|
|
@@ -1215,7 +1297,7 @@ function wrapClassified(builder, level, complianceOptions) {
|
|
|
1215
1297
|
};
|
|
1216
1298
|
options.type = spec.queryable ? new CompliantIndexType(spec) : new CompliantType(spec);
|
|
1217
1299
|
options.columnType = "text";
|
|
1218
|
-
options.runtimeType =
|
|
1300
|
+
options.runtimeType = COMPLIANT_RUNTIME_TYPE;
|
|
1219
1301
|
options.accessor = true;
|
|
1220
1302
|
}
|
|
1221
1303
|
}
|
|
@@ -1323,6 +1405,47 @@ import {
|
|
|
1323
1405
|
p as p2,
|
|
1324
1406
|
UnderscoreNamingStrategy
|
|
1325
1407
|
} from "@mikro-orm/core";
|
|
1408
|
+
|
|
1409
|
+
// src/persistence/complianceChanges.ts
|
|
1410
|
+
var LISTENERS2 = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.changeListeners")] ??= /* @__PURE__ */ new Set();
|
|
1411
|
+
function onComplianceChange(listener) {
|
|
1412
|
+
LISTENERS2.add(listener);
|
|
1413
|
+
return () => LISTENERS2.delete(listener);
|
|
1414
|
+
}
|
|
1415
|
+
function emit(event) {
|
|
1416
|
+
for (const listener of LISTENERS2) {
|
|
1417
|
+
try {
|
|
1418
|
+
listener(event);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
console.error("Compliance change listener failed:", error);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function installComplianceChangeHooks(schema, entityName, levels) {
|
|
1425
|
+
const sensitive = new Set(
|
|
1426
|
+
[...levels].filter(([, level]) => level !== "none").map(([name]) => name)
|
|
1427
|
+
);
|
|
1428
|
+
if (!sensitive.size) return;
|
|
1429
|
+
const report = (action) => (args) => {
|
|
1430
|
+
if (!LISTENERS2.size) return;
|
|
1431
|
+
const entity = args.entity;
|
|
1432
|
+
const keys = args.meta?.primaryKeys ?? ["id"];
|
|
1433
|
+
const fields = action === "update" ? Object.keys(args.changeSet?.payload ?? {}) : action === "create" ? [...levels.keys()].filter((name) => entity[name] !== void 0) : [];
|
|
1434
|
+
emit({
|
|
1435
|
+
entity: entityName,
|
|
1436
|
+
id: keys.map((key) => String(entity[key] ?? "")).join(":"),
|
|
1437
|
+
action,
|
|
1438
|
+
fields,
|
|
1439
|
+
sensitiveFields: action === "delete" ? [...sensitive] : fields.filter((name) => sensitive.has(name)),
|
|
1440
|
+
tenantId: getBoundTenantId() ?? null
|
|
1441
|
+
});
|
|
1442
|
+
};
|
|
1443
|
+
schema.addHook("afterCreate", report("create"));
|
|
1444
|
+
schema.addHook("afterUpdate", report("update"));
|
|
1445
|
+
schema.addHook("afterDelete", report("delete"));
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/persistence/defineComplianceEntity.ts
|
|
1326
1449
|
var naming = new UnderscoreNamingStrategy();
|
|
1327
1450
|
function wireCompliantProperties(entityName, properties) {
|
|
1328
1451
|
const wired = {};
|
|
@@ -1402,6 +1525,11 @@ function defineComplianceEntity(meta) {
|
|
|
1402
1525
|
...meta,
|
|
1403
1526
|
properties: wired.properties
|
|
1404
1527
|
});
|
|
1528
|
+
installComplianceChangeHooks(
|
|
1529
|
+
schema,
|
|
1530
|
+
entityName,
|
|
1531
|
+
complianceFields
|
|
1532
|
+
);
|
|
1405
1533
|
if (wired.bindings.length > 0) {
|
|
1406
1534
|
const entityClass = schema.meta.class;
|
|
1407
1535
|
if (entityClass)
|
|
@@ -1983,6 +2111,7 @@ export {
|
|
|
1983
2111
|
RetentionDuration,
|
|
1984
2112
|
RlsEventSubscriber,
|
|
1985
2113
|
TENANT_FILTER_NAME,
|
|
2114
|
+
TenantMismatchError,
|
|
1986
2115
|
UnboundTenantError,
|
|
1987
2116
|
asEncryptionSafe,
|
|
1988
2117
|
assertBindableTenantId,
|
|
@@ -2011,6 +2140,7 @@ export {
|
|
|
2011
2140
|
isEncryptedCiphertext,
|
|
2012
2141
|
isEncryptedValue,
|
|
2013
2142
|
onComplianceAccess,
|
|
2143
|
+
onComplianceChange,
|
|
2014
2144
|
parseDuration,
|
|
2015
2145
|
parseEncryptionKeyList,
|
|
2016
2146
|
reencryptEncryptedColumns,
|