@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
package/lib/persistence/index.js
CHANGED
|
@@ -53,6 +53,7 @@ __export(persistence_exports, {
|
|
|
53
53
|
RetentionDuration: () => RetentionDuration,
|
|
54
54
|
RlsEventSubscriber: () => RlsEventSubscriber,
|
|
55
55
|
TENANT_FILTER_NAME: () => TENANT_FILTER_NAME,
|
|
56
|
+
TenantMismatchError: () => TenantMismatchError,
|
|
56
57
|
UnboundTenantError: () => UnboundTenantError,
|
|
57
58
|
asEncryptionSafe: () => asEncryptionSafe,
|
|
58
59
|
assertBindableTenantId: () => assertBindableTenantId,
|
|
@@ -81,6 +82,7 @@ __export(persistence_exports, {
|
|
|
81
82
|
isEncryptedCiphertext: () => isEncryptedCiphertext,
|
|
82
83
|
isEncryptedValue: () => isEncryptedValue,
|
|
83
84
|
onComplianceAccess: () => onComplianceAccess,
|
|
85
|
+
onComplianceChange: () => onComplianceChange,
|
|
84
86
|
parseDuration: () => parseDuration,
|
|
85
87
|
parseEncryptionKeyList: () => parseEncryptionKeyList,
|
|
86
88
|
reencryptEncryptedColumns: () => reencryptEncryptedColumns,
|
|
@@ -827,6 +829,22 @@ var LegacyCiphertextError = class extends Error {
|
|
|
827
829
|
var CompliantQueryError = class extends Error {
|
|
828
830
|
name = "CompliantQueryError";
|
|
829
831
|
};
|
|
832
|
+
var TenantMismatchError = class extends Error {
|
|
833
|
+
constructor(path, operation, ownerTenantId, boundTenantId) {
|
|
834
|
+
super(
|
|
835
|
+
`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.`
|
|
836
|
+
);
|
|
837
|
+
this.path = path;
|
|
838
|
+
this.operation = operation;
|
|
839
|
+
this.ownerTenantId = ownerTenantId;
|
|
840
|
+
this.boundTenantId = boundTenantId;
|
|
841
|
+
}
|
|
842
|
+
path;
|
|
843
|
+
operation;
|
|
844
|
+
ownerTenantId;
|
|
845
|
+
boundTenantId;
|
|
846
|
+
name = "TenantMismatchError";
|
|
847
|
+
};
|
|
830
848
|
var MINT = /* @__PURE__ */ Symbol.for("forklaunch.compliance.mint");
|
|
831
849
|
var ANON_BRAND = /* @__PURE__ */ Symbol.for("forklaunch.compliance.anon");
|
|
832
850
|
var Anon = class {
|
|
@@ -907,10 +925,11 @@ var CompliantField = class _CompliantField {
|
|
|
907
925
|
Object.freeze(this);
|
|
908
926
|
}
|
|
909
927
|
/**
|
|
910
|
-
* @internal Wrap a plaintext value for `spec
|
|
911
|
-
*
|
|
928
|
+
* @internal Wrap a plaintext value for `spec`, owned by `tenant` (the one
|
|
929
|
+
* bound where it was assigned, if any). Applications assign plain values to
|
|
930
|
+
* the entity property instead.
|
|
912
931
|
*/
|
|
913
|
-
static fromPlain(value, spec) {
|
|
932
|
+
static fromPlain(value, spec, tenant) {
|
|
914
933
|
if (spec.enumValues) {
|
|
915
934
|
const values = spec.isArray && Array.isArray(value) ? value : [value];
|
|
916
935
|
for (const v of values) {
|
|
@@ -923,16 +942,22 @@ var CompliantField = class _CompliantField {
|
|
|
923
942
|
}
|
|
924
943
|
return new _CompliantField({
|
|
925
944
|
spec,
|
|
945
|
+
tenant,
|
|
926
946
|
hasPlain: true,
|
|
927
947
|
plain: value,
|
|
928
948
|
anon: computeAnon(spec.anon, value)
|
|
929
949
|
});
|
|
930
950
|
}
|
|
931
|
-
/**
|
|
932
|
-
|
|
951
|
+
/**
|
|
952
|
+
* @internal Wrap a stored value (any envelope, or legacy plaintext) loaded
|
|
953
|
+
* under `tenant`: the tenant bound during hydration, which is the loading
|
|
954
|
+
* entity manager's.
|
|
955
|
+
*/
|
|
956
|
+
static fromStored(stored, spec, index, tenant) {
|
|
933
957
|
if (!isEncryptedCiphertext(stored)) {
|
|
934
958
|
return new _CompliantField({
|
|
935
959
|
spec,
|
|
960
|
+
tenant,
|
|
936
961
|
hasPlain: true,
|
|
937
962
|
plain: deserializeFromEncryption(
|
|
938
963
|
stored,
|
|
@@ -944,6 +969,7 @@ var CompliantField = class _CompliantField {
|
|
|
944
969
|
}
|
|
945
970
|
return new _CompliantField({
|
|
946
971
|
spec,
|
|
972
|
+
tenant,
|
|
947
973
|
hasPlain: false,
|
|
948
974
|
sealed: stored,
|
|
949
975
|
anon: sealedAnon(stored) ?? void 0,
|
|
@@ -976,11 +1002,24 @@ var CompliantField = class _CompliantField {
|
|
|
976
1002
|
return readPlaintext(this, true);
|
|
977
1003
|
}
|
|
978
1004
|
};
|
|
1005
|
+
function tenantOf(state, operation) {
|
|
1006
|
+
const bound = getBoundTenantId();
|
|
1007
|
+
if (state.tenant === void 0) return requireBoundTenantId(operation);
|
|
1008
|
+
if (bound !== void 0 && bound !== state.tenant) {
|
|
1009
|
+
throw new TenantMismatchError(
|
|
1010
|
+
state.spec.path,
|
|
1011
|
+
operation,
|
|
1012
|
+
state.tenant,
|
|
1013
|
+
bound
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return state.tenant;
|
|
1017
|
+
}
|
|
979
1018
|
function readPlaintext(field, report) {
|
|
980
1019
|
const state = stateOf(field);
|
|
981
1020
|
let tenantId = "";
|
|
982
1021
|
if (!state.hasPlain) {
|
|
983
|
-
tenantId =
|
|
1022
|
+
tenantId = tenantOf(state, "read");
|
|
984
1023
|
if (state.sealed === void 0) {
|
|
985
1024
|
throw new Error(
|
|
986
1025
|
`${state.spec.path} was not loaded: its ciphertext column was left out of the selection`
|
|
@@ -999,6 +1038,8 @@ function readPlaintext(field, report) {
|
|
|
999
1038
|
state.spec.isArray
|
|
1000
1039
|
);
|
|
1001
1040
|
state.hasPlain = true;
|
|
1041
|
+
} else if (state.tenant !== void 0) {
|
|
1042
|
+
tenantId = tenantOf(state, "read");
|
|
1002
1043
|
}
|
|
1003
1044
|
if (report) {
|
|
1004
1045
|
tenantId ||= getBoundTenantId() ?? "";
|
|
@@ -1026,10 +1067,11 @@ function sealedValueOf(field) {
|
|
|
1026
1067
|
return state.sealed;
|
|
1027
1068
|
}
|
|
1028
1069
|
if (!state.hasPlain) return state.sealed;
|
|
1029
|
-
|
|
1030
|
-
if (state.sealed !== void 0 && state.sealedTenant === tenantId) {
|
|
1070
|
+
if (state.sealed !== void 0 && state.sealedTenant === state.tenant) {
|
|
1031
1071
|
return state.sealed;
|
|
1032
1072
|
}
|
|
1073
|
+
const tenantId = tenantOf(state, "write");
|
|
1074
|
+
state.tenant = tenantId;
|
|
1033
1075
|
if (state.anon === void 0) {
|
|
1034
1076
|
state.anon = computeAnon(state.spec.anon, state.plain);
|
|
1035
1077
|
}
|
|
@@ -1048,9 +1090,12 @@ function normalizedForIndex(value, spec) {
|
|
|
1048
1090
|
return normalizeSerializedForIndex(serializeForEncryption(value), spec);
|
|
1049
1091
|
}
|
|
1050
1092
|
function blindIndexOfValue(value, spec) {
|
|
1093
|
+
return blindIndexUnder(value, spec, requireBoundTenantId("query"));
|
|
1094
|
+
}
|
|
1095
|
+
function blindIndexUnder(value, spec, tenantId) {
|
|
1051
1096
|
return requireEncryptor(spec.path).blindIndex(
|
|
1052
1097
|
normalizedForIndex(value, spec),
|
|
1053
|
-
|
|
1098
|
+
tenantId
|
|
1054
1099
|
);
|
|
1055
1100
|
}
|
|
1056
1101
|
function blindIndexOf(field) {
|
|
@@ -1059,7 +1104,7 @@ function blindIndexOf(field) {
|
|
|
1059
1104
|
if (!state.hasPlain && state.sealed === void 0) return state.index ?? null;
|
|
1060
1105
|
try {
|
|
1061
1106
|
const plain = state.hasPlain ? state.plain : readPlaintext(field, false);
|
|
1062
|
-
state.index =
|
|
1107
|
+
state.index = blindIndexUnder(plain, state.spec, tenantOf(state, "index"));
|
|
1063
1108
|
} catch {
|
|
1064
1109
|
return state.index ?? null;
|
|
1065
1110
|
}
|
|
@@ -1072,6 +1117,7 @@ function isPendingIndex(value) {
|
|
|
1072
1117
|
function pendingIndexValue(value) {
|
|
1073
1118
|
return value[PENDING];
|
|
1074
1119
|
}
|
|
1120
|
+
var COMPLIANT_RUNTIME_TYPE = "CompliantField";
|
|
1075
1121
|
var CompliantTypeBase = class extends import_core2.Type {
|
|
1076
1122
|
constructor(spec) {
|
|
1077
1123
|
super();
|
|
@@ -1082,7 +1128,7 @@ var CompliantTypeBase = class extends import_core2.Type {
|
|
|
1082
1128
|
return "text";
|
|
1083
1129
|
}
|
|
1084
1130
|
get runtimeType() {
|
|
1085
|
-
return
|
|
1131
|
+
return COMPLIANT_RUNTIME_TYPE;
|
|
1086
1132
|
}
|
|
1087
1133
|
ensureComparable() {
|
|
1088
1134
|
return false;
|
|
@@ -1101,12 +1147,19 @@ var CompliantType = class extends CompliantTypeBase {
|
|
|
1101
1147
|
if (value === null || value === void 0) return null;
|
|
1102
1148
|
if (isCompliantField(value)) return sealedValueOf(value) ?? null;
|
|
1103
1149
|
if (isEncryptedCiphertext(value)) return value;
|
|
1104
|
-
return sealedValueOf(
|
|
1150
|
+
return sealedValueOf(
|
|
1151
|
+
CompliantField.fromPlain(value, this.spec, getBoundTenantId())
|
|
1152
|
+
) ?? null;
|
|
1105
1153
|
}
|
|
1106
1154
|
convertToJSValue(value) {
|
|
1107
1155
|
if (value === null || value === void 0) return value;
|
|
1108
1156
|
if (isCompliantField(value)) return value;
|
|
1109
|
-
return CompliantField.fromStored(
|
|
1157
|
+
return CompliantField.fromStored(
|
|
1158
|
+
String(value),
|
|
1159
|
+
this.spec,
|
|
1160
|
+
void 0,
|
|
1161
|
+
getBoundTenantId()
|
|
1162
|
+
);
|
|
1110
1163
|
}
|
|
1111
1164
|
};
|
|
1112
1165
|
var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$in", "$nin"]);
|
|
@@ -1135,6 +1188,18 @@ function isCompliantType(type) {
|
|
|
1135
1188
|
return type instanceof CompliantType || type instanceof CompliantIndexType;
|
|
1136
1189
|
}
|
|
1137
1190
|
var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
|
|
1191
|
+
var LOADED_TENANTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.loadedTenants")] ??= /* @__PURE__ */ new WeakMap();
|
|
1192
|
+
function loadedTenantOf(entity, binding) {
|
|
1193
|
+
return LOADED_TENANTS.get(entity)?.[binding.property];
|
|
1194
|
+
}
|
|
1195
|
+
function recordLoadedTenant(entity, binding) {
|
|
1196
|
+
let tenants = LOADED_TENANTS.get(entity);
|
|
1197
|
+
if (!tenants) {
|
|
1198
|
+
tenants = {};
|
|
1199
|
+
LOADED_TENANTS.set(entity, tenants);
|
|
1200
|
+
}
|
|
1201
|
+
tenants[binding.property] = getBoundTenantId();
|
|
1202
|
+
}
|
|
1138
1203
|
function slotsOf(entity) {
|
|
1139
1204
|
let slots = SLOTS.get(entity);
|
|
1140
1205
|
if (!slots) {
|
|
@@ -1150,12 +1215,22 @@ function readProperty(entity, binding) {
|
|
|
1150
1215
|
const stored = slots[binding.sibling];
|
|
1151
1216
|
if (isPendingIndex(value)) {
|
|
1152
1217
|
const index = pendingIndexValue(value);
|
|
1153
|
-
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1218
|
+
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1219
|
+
stored,
|
|
1220
|
+
binding.spec,
|
|
1221
|
+
index,
|
|
1222
|
+
loadedTenantOf(entity, binding)
|
|
1223
|
+
) : CompliantField.fromIndexOnly(index, binding.spec);
|
|
1154
1224
|
slots[binding.property] = field;
|
|
1155
1225
|
return field;
|
|
1156
1226
|
}
|
|
1157
1227
|
if (value == null && typeof stored === "string") {
|
|
1158
|
-
const field = CompliantField.fromStored(
|
|
1228
|
+
const field = CompliantField.fromStored(
|
|
1229
|
+
stored,
|
|
1230
|
+
binding.spec,
|
|
1231
|
+
null,
|
|
1232
|
+
loadedTenantOf(entity, binding)
|
|
1233
|
+
);
|
|
1159
1234
|
slots[binding.property] = field;
|
|
1160
1235
|
return field;
|
|
1161
1236
|
}
|
|
@@ -1177,10 +1252,18 @@ function writeProperty(entity, binding, value) {
|
|
|
1177
1252
|
return;
|
|
1178
1253
|
}
|
|
1179
1254
|
if (isCompliantField(value)) {
|
|
1180
|
-
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1255
|
+
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1256
|
+
readPlaintext(value, false),
|
|
1257
|
+
binding.spec,
|
|
1258
|
+
stateOf(value).tenant ?? getBoundTenantId()
|
|
1259
|
+
);
|
|
1181
1260
|
return;
|
|
1182
1261
|
}
|
|
1183
|
-
slots[binding.property] = CompliantField.fromPlain(
|
|
1262
|
+
slots[binding.property] = CompliantField.fromPlain(
|
|
1263
|
+
value,
|
|
1264
|
+
binding.spec,
|
|
1265
|
+
getBoundTenantId()
|
|
1266
|
+
);
|
|
1184
1267
|
}
|
|
1185
1268
|
function installCompliantAccessors(prototype, bindings) {
|
|
1186
1269
|
installInspect(prototype, bindings);
|
|
@@ -1207,6 +1290,7 @@ function installCompliantAccessors(prototype, bindings) {
|
|
|
1207
1290
|
return sealedValueOf(field) ?? stored;
|
|
1208
1291
|
},
|
|
1209
1292
|
set(value) {
|
|
1293
|
+
recordLoadedTenant(this, binding);
|
|
1210
1294
|
slotsOf(this)[sibling] = value;
|
|
1211
1295
|
}
|
|
1212
1296
|
});
|
|
@@ -1315,7 +1399,7 @@ function wrapClassified(builder, level, complianceOptions) {
|
|
|
1315
1399
|
};
|
|
1316
1400
|
options.type = spec.queryable ? new CompliantIndexType(spec) : new CompliantType(spec);
|
|
1317
1401
|
options.columnType = "text";
|
|
1318
|
-
options.runtimeType =
|
|
1402
|
+
options.runtimeType = COMPLIANT_RUNTIME_TYPE;
|
|
1319
1403
|
options.accessor = true;
|
|
1320
1404
|
}
|
|
1321
1405
|
}
|
|
@@ -1419,6 +1503,47 @@ var fp = new Proxy(import_core3.p, {
|
|
|
1419
1503
|
|
|
1420
1504
|
// src/persistence/defineComplianceEntity.ts
|
|
1421
1505
|
var import_core4 = require("@mikro-orm/core");
|
|
1506
|
+
|
|
1507
|
+
// src/persistence/complianceChanges.ts
|
|
1508
|
+
var LISTENERS2 = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.changeListeners")] ??= /* @__PURE__ */ new Set();
|
|
1509
|
+
function onComplianceChange(listener) {
|
|
1510
|
+
LISTENERS2.add(listener);
|
|
1511
|
+
return () => LISTENERS2.delete(listener);
|
|
1512
|
+
}
|
|
1513
|
+
function emit(event) {
|
|
1514
|
+
for (const listener of LISTENERS2) {
|
|
1515
|
+
try {
|
|
1516
|
+
listener(event);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
console.error("Compliance change listener failed:", error);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function installComplianceChangeHooks(schema, entityName, levels) {
|
|
1523
|
+
const sensitive = new Set(
|
|
1524
|
+
[...levels].filter(([, level]) => level !== "none").map(([name]) => name)
|
|
1525
|
+
);
|
|
1526
|
+
if (!sensitive.size) return;
|
|
1527
|
+
const report = (action) => (args) => {
|
|
1528
|
+
if (!LISTENERS2.size) return;
|
|
1529
|
+
const entity = args.entity;
|
|
1530
|
+
const keys = args.meta?.primaryKeys ?? ["id"];
|
|
1531
|
+
const fields = action === "update" ? Object.keys(args.changeSet?.payload ?? {}) : action === "create" ? [...levels.keys()].filter((name) => entity[name] !== void 0) : [];
|
|
1532
|
+
emit({
|
|
1533
|
+
entity: entityName,
|
|
1534
|
+
id: keys.map((key) => String(entity[key] ?? "")).join(":"),
|
|
1535
|
+
action,
|
|
1536
|
+
fields,
|
|
1537
|
+
sensitiveFields: action === "delete" ? [...sensitive] : fields.filter((name) => sensitive.has(name)),
|
|
1538
|
+
tenantId: getBoundTenantId() ?? null
|
|
1539
|
+
});
|
|
1540
|
+
};
|
|
1541
|
+
schema.addHook("afterCreate", report("create"));
|
|
1542
|
+
schema.addHook("afterUpdate", report("update"));
|
|
1543
|
+
schema.addHook("afterDelete", report("delete"));
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/persistence/defineComplianceEntity.ts
|
|
1422
1547
|
var naming = new import_core4.UnderscoreNamingStrategy();
|
|
1423
1548
|
function wireCompliantProperties(entityName, properties) {
|
|
1424
1549
|
const wired = {};
|
|
@@ -1498,6 +1623,11 @@ function defineComplianceEntity(meta) {
|
|
|
1498
1623
|
...meta,
|
|
1499
1624
|
properties: wired.properties
|
|
1500
1625
|
});
|
|
1626
|
+
installComplianceChangeHooks(
|
|
1627
|
+
schema,
|
|
1628
|
+
entityName,
|
|
1629
|
+
complianceFields
|
|
1630
|
+
);
|
|
1501
1631
|
if (wired.bindings.length > 0) {
|
|
1502
1632
|
const entityClass = schema.meta.class;
|
|
1503
1633
|
if (entityClass)
|
|
@@ -2080,6 +2210,7 @@ function rebind(result, target, proxy) {
|
|
|
2080
2210
|
RetentionDuration,
|
|
2081
2211
|
RlsEventSubscriber,
|
|
2082
2212
|
TENANT_FILTER_NAME,
|
|
2213
|
+
TenantMismatchError,
|
|
2083
2214
|
UnboundTenantError,
|
|
2084
2215
|
asEncryptionSafe,
|
|
2085
2216
|
assertBindableTenantId,
|
|
@@ -2108,6 +2239,7 @@ function rebind(result, target, proxy) {
|
|
|
2108
2239
|
isEncryptedCiphertext,
|
|
2109
2240
|
isEncryptedValue,
|
|
2110
2241
|
onComplianceAccess,
|
|
2242
|
+
onComplianceChange,
|
|
2111
2243
|
parseDuration,
|
|
2112
2244
|
parseEncryptionKeyList,
|
|
2113
2245
|
reencryptEncryptedColumns,
|