@forklaunch/core 3.1.0 → 3.2.1
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/lib/http/emailClient.d.ts +48 -1
- package/lib/http/emailClient.d.ts.map +1 -1
- package/lib/http/index.d.ts +1 -0
- package/lib/http/index.d.ts.map +1 -1
- package/lib/http/index.js +307 -96
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +266 -58
- package/lib/http/index.mjs.map +1 -1
- package/lib/http/instanceGateway.d.ts +10 -6
- package/lib/http/instanceGateway.d.ts.map +1 -1
- package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
- package/lib/http/modelGatewayClient.d.ts +13 -12
- package/lib/http/modelGatewayClient.d.ts.map +1 -1
- package/lib/http/smsClient.d.ts +2 -2
- package/lib/http/smsClient.d.ts.map +1 -1
- 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/http/voiceClient.d.ts +4 -4
- package/lib/http/voiceClient.d.ts.map +1 -1
- package/lib/http/whatsappClient.d.ts +5 -5
- 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 +156 -19
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +154 -19
- package/lib/persistence/index.mjs.map +1 -1
- package/package.json +10 -10
|
@@ -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;
|
|
@@ -993,7 +1037,7 @@ var CompliantTypeBase = class extends Type2 {
|
|
|
993
1037
|
};
|
|
994
1038
|
var CompliantType = class extends CompliantTypeBase {
|
|
995
1039
|
convertToDatabaseValue(value, _platform, context) {
|
|
996
|
-
if (context?.fromQuery) {
|
|
1040
|
+
if (context?.fromQuery && context.mode !== "query-data") {
|
|
997
1041
|
throw new CompliantQueryError(
|
|
998
1042
|
`${this.spec.path} cannot be queried: declare it with .compliance('${this.spec.level}', { queryable: true }) to allow equality lookups`
|
|
999
1043
|
);
|
|
@@ -1001,18 +1045,30 @@ 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"]);
|
|
1013
1064
|
var CompliantIndexType = class extends CompliantTypeBase {
|
|
1014
1065
|
convertToDatabaseValue(value, _platform, context) {
|
|
1015
1066
|
if (value === null || value === void 0) return null;
|
|
1067
|
+
if (context?.mode === "query-data") {
|
|
1068
|
+
throw new CompliantQueryError(
|
|
1069
|
+
`${this.spec.path} is queryable, so a bulk update (nativeUpdate, qb.update) cannot set it: its sealed value lives in a second column the update does not write. Load the entity, assign the value and flush.`
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1016
1072
|
if (context?.fromQuery) {
|
|
1017
1073
|
const key = context.key;
|
|
1018
1074
|
if (key !== void 0 && !EQUALITY_OPERATORS.has(String(key)) && !/^\d+$/.test(String(key))) {
|
|
@@ -1035,6 +1091,18 @@ function isCompliantType(type) {
|
|
|
1035
1091
|
return type instanceof CompliantType || type instanceof CompliantIndexType;
|
|
1036
1092
|
}
|
|
1037
1093
|
var SLOTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.slots")] ??= /* @__PURE__ */ new WeakMap();
|
|
1094
|
+
var LOADED_TENANTS = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.loadedTenants")] ??= /* @__PURE__ */ new WeakMap();
|
|
1095
|
+
function loadedTenantOf(entity, binding) {
|
|
1096
|
+
return LOADED_TENANTS.get(entity)?.[binding.property];
|
|
1097
|
+
}
|
|
1098
|
+
function recordLoadedTenant(entity, binding) {
|
|
1099
|
+
let tenants = LOADED_TENANTS.get(entity);
|
|
1100
|
+
if (!tenants) {
|
|
1101
|
+
tenants = {};
|
|
1102
|
+
LOADED_TENANTS.set(entity, tenants);
|
|
1103
|
+
}
|
|
1104
|
+
tenants[binding.property] = getBoundTenantId();
|
|
1105
|
+
}
|
|
1038
1106
|
function slotsOf(entity) {
|
|
1039
1107
|
let slots = SLOTS.get(entity);
|
|
1040
1108
|
if (!slots) {
|
|
@@ -1050,12 +1118,22 @@ function readProperty(entity, binding) {
|
|
|
1050
1118
|
const stored = slots[binding.sibling];
|
|
1051
1119
|
if (isPendingIndex(value)) {
|
|
1052
1120
|
const index = pendingIndexValue(value);
|
|
1053
|
-
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1121
|
+
const field = typeof stored === "string" ? CompliantField.fromStored(
|
|
1122
|
+
stored,
|
|
1123
|
+
binding.spec,
|
|
1124
|
+
index,
|
|
1125
|
+
loadedTenantOf(entity, binding)
|
|
1126
|
+
) : CompliantField.fromIndexOnly(index, binding.spec);
|
|
1054
1127
|
slots[binding.property] = field;
|
|
1055
1128
|
return field;
|
|
1056
1129
|
}
|
|
1057
1130
|
if (value == null && typeof stored === "string") {
|
|
1058
|
-
const field = CompliantField.fromStored(
|
|
1131
|
+
const field = CompliantField.fromStored(
|
|
1132
|
+
stored,
|
|
1133
|
+
binding.spec,
|
|
1134
|
+
null,
|
|
1135
|
+
loadedTenantOf(entity, binding)
|
|
1136
|
+
);
|
|
1059
1137
|
slots[binding.property] = field;
|
|
1060
1138
|
return field;
|
|
1061
1139
|
}
|
|
@@ -1077,10 +1155,18 @@ function writeProperty(entity, binding, value) {
|
|
|
1077
1155
|
return;
|
|
1078
1156
|
}
|
|
1079
1157
|
if (isCompliantField(value)) {
|
|
1080
|
-
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1158
|
+
slots[binding.property] = sameRules(stateOf(value).spec, binding.spec) ? value : CompliantField.fromPlain(
|
|
1159
|
+
readPlaintext(value, false),
|
|
1160
|
+
binding.spec,
|
|
1161
|
+
stateOf(value).tenant ?? getBoundTenantId()
|
|
1162
|
+
);
|
|
1081
1163
|
return;
|
|
1082
1164
|
}
|
|
1083
|
-
slots[binding.property] = CompliantField.fromPlain(
|
|
1165
|
+
slots[binding.property] = CompliantField.fromPlain(
|
|
1166
|
+
value,
|
|
1167
|
+
binding.spec,
|
|
1168
|
+
getBoundTenantId()
|
|
1169
|
+
);
|
|
1084
1170
|
}
|
|
1085
1171
|
function installCompliantAccessors(prototype, bindings) {
|
|
1086
1172
|
installInspect(prototype, bindings);
|
|
@@ -1107,6 +1193,7 @@ function installCompliantAccessors(prototype, bindings) {
|
|
|
1107
1193
|
return sealedValueOf(field) ?? stored;
|
|
1108
1194
|
},
|
|
1109
1195
|
set(value) {
|
|
1196
|
+
recordLoadedTenant(this, binding);
|
|
1110
1197
|
slotsOf(this)[sibling] = value;
|
|
1111
1198
|
}
|
|
1112
1199
|
});
|
|
@@ -1215,7 +1302,7 @@ function wrapClassified(builder, level, complianceOptions) {
|
|
|
1215
1302
|
};
|
|
1216
1303
|
options.type = spec.queryable ? new CompliantIndexType(spec) : new CompliantType(spec);
|
|
1217
1304
|
options.columnType = "text";
|
|
1218
|
-
options.runtimeType =
|
|
1305
|
+
options.runtimeType = COMPLIANT_RUNTIME_TYPE;
|
|
1219
1306
|
options.accessor = true;
|
|
1220
1307
|
}
|
|
1221
1308
|
}
|
|
@@ -1323,6 +1410,47 @@ import {
|
|
|
1323
1410
|
p as p2,
|
|
1324
1411
|
UnderscoreNamingStrategy
|
|
1325
1412
|
} from "@mikro-orm/core";
|
|
1413
|
+
|
|
1414
|
+
// src/persistence/complianceChanges.ts
|
|
1415
|
+
var LISTENERS2 = globalThis[/* @__PURE__ */ Symbol.for("forklaunch.compliance.changeListeners")] ??= /* @__PURE__ */ new Set();
|
|
1416
|
+
function onComplianceChange(listener) {
|
|
1417
|
+
LISTENERS2.add(listener);
|
|
1418
|
+
return () => LISTENERS2.delete(listener);
|
|
1419
|
+
}
|
|
1420
|
+
function emit(event) {
|
|
1421
|
+
for (const listener of LISTENERS2) {
|
|
1422
|
+
try {
|
|
1423
|
+
listener(event);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
console.error("Compliance change listener failed:", error);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function installComplianceChangeHooks(schema, entityName, levels) {
|
|
1430
|
+
const sensitive = new Set(
|
|
1431
|
+
[...levels].filter(([, level]) => level !== "none").map(([name]) => name)
|
|
1432
|
+
);
|
|
1433
|
+
if (!sensitive.size) return;
|
|
1434
|
+
const report = (action) => (args) => {
|
|
1435
|
+
if (!LISTENERS2.size) return;
|
|
1436
|
+
const entity = args.entity;
|
|
1437
|
+
const keys = args.meta?.primaryKeys ?? ["id"];
|
|
1438
|
+
const fields = action === "update" ? Object.keys(args.changeSet?.payload ?? {}) : action === "create" ? [...levels.keys()].filter((name) => entity[name] !== void 0) : [];
|
|
1439
|
+
emit({
|
|
1440
|
+
entity: entityName,
|
|
1441
|
+
id: keys.map((key) => String(entity[key] ?? "")).join(":"),
|
|
1442
|
+
action,
|
|
1443
|
+
fields,
|
|
1444
|
+
sensitiveFields: action === "delete" ? [...sensitive] : fields.filter((name) => sensitive.has(name)),
|
|
1445
|
+
tenantId: getBoundTenantId() ?? null
|
|
1446
|
+
});
|
|
1447
|
+
};
|
|
1448
|
+
schema.addHook("afterCreate", report("create"));
|
|
1449
|
+
schema.addHook("afterUpdate", report("update"));
|
|
1450
|
+
schema.addHook("afterDelete", report("delete"));
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/persistence/defineComplianceEntity.ts
|
|
1326
1454
|
var naming = new UnderscoreNamingStrategy();
|
|
1327
1455
|
function wireCompliantProperties(entityName, properties) {
|
|
1328
1456
|
const wired = {};
|
|
@@ -1402,6 +1530,11 @@ function defineComplianceEntity(meta) {
|
|
|
1402
1530
|
...meta,
|
|
1403
1531
|
properties: wired.properties
|
|
1404
1532
|
});
|
|
1533
|
+
installComplianceChangeHooks(
|
|
1534
|
+
schema,
|
|
1535
|
+
entityName,
|
|
1536
|
+
complianceFields
|
|
1537
|
+
);
|
|
1405
1538
|
if (wired.bindings.length > 0) {
|
|
1406
1539
|
const entityClass = schema.meta.class;
|
|
1407
1540
|
if (entityClass)
|
|
@@ -1983,6 +2116,7 @@ export {
|
|
|
1983
2116
|
RetentionDuration,
|
|
1984
2117
|
RlsEventSubscriber,
|
|
1985
2118
|
TENANT_FILTER_NAME,
|
|
2119
|
+
TenantMismatchError,
|
|
1986
2120
|
UnboundTenantError,
|
|
1987
2121
|
asEncryptionSafe,
|
|
1988
2122
|
assertBindableTenantId,
|
|
@@ -2011,6 +2145,7 @@ export {
|
|
|
2011
2145
|
isEncryptedCiphertext,
|
|
2012
2146
|
isEncryptedValue,
|
|
2013
2147
|
onComplianceAccess,
|
|
2148
|
+
onComplianceChange,
|
|
2014
2149
|
parseDuration,
|
|
2015
2150
|
parseEncryptionKeyList,
|
|
2016
2151
|
reencryptEncryptedColumns,
|