@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.
Files changed (32) hide show
  1. package/bin/gateway-mock/routes/email.mjs +91 -4
  2. package/bin/gateway-mock/server.mjs +39 -21
  3. package/lib/http/emailClient.d.ts +46 -0
  4. package/lib/http/emailClient.d.ts.map +1 -1
  5. package/lib/http/index.d.ts +2 -0
  6. package/lib/http/index.d.ts.map +1 -1
  7. package/lib/http/index.js +393 -91
  8. package/lib/http/index.js.map +1 -1
  9. package/lib/http/index.mjs +351 -53
  10. package/lib/http/index.mjs.map +1 -1
  11. package/lib/http/middleware/request/enrichDetails.middleware.d.ts.map +1 -1
  12. package/lib/http/paymentsClient.d.ts +19 -8
  13. package/lib/http/paymentsClient.d.ts.map +1 -1
  14. package/lib/http/previewModelGatewayClient.d.ts +48 -0
  15. package/lib/http/previewModelGatewayClient.d.ts.map +1 -0
  16. package/lib/http/telemetry/auditLogger.d.ts +5 -1
  17. package/lib/http/telemetry/auditLogger.d.ts.map +1 -1
  18. package/lib/http/telemetry/requestAudit.d.ts +30 -0
  19. package/lib/http/telemetry/requestAudit.d.ts.map +1 -0
  20. package/lib/persistence/complianceChanges.d.ts +30 -0
  21. package/lib/persistence/complianceChanges.d.ts.map +1 -0
  22. package/lib/persistence/compliancePropertyBuilder.d.ts.map +1 -1
  23. package/lib/persistence/compliantField.d.ts +41 -8
  24. package/lib/persistence/compliantField.d.ts.map +1 -1
  25. package/lib/persistence/defineComplianceEntity.d.ts.map +1 -1
  26. package/lib/persistence/index.d.ts +2 -1
  27. package/lib/persistence/index.d.ts.map +1 -1
  28. package/lib/persistence/index.js +150 -18
  29. package/lib/persistence/index.js.map +1 -1
  30. package/lib/persistence/index.mjs +148 -18
  31. package/lib/persistence/index.mjs.map +1 -1
  32. 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`. Applications assign plain
811
- * values to the entity property instead.
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
- /** @internal Wrap a stored value (any envelope, or legacy plaintext). */
832
- static fromStored(stored, spec, index) {
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 = requireBoundTenantId("read");
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
- const tenantId = requireBoundTenantId("write");
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
- requireBoundTenantId("query")
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 = blindIndexOfValue(plain, state.spec);
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 "string";
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(CompliantField.fromPlain(value, this.spec)) ?? null;
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(String(value), this.spec);
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(stored, binding.spec, index) : CompliantField.fromIndexOnly(index, binding.spec);
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(stored, binding.spec, null);
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(readPlaintext(value, false), binding.spec);
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(value, binding.spec);
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 = "string";
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,