@cosmicdrift/kumiko-framework 0.270.0 → 0.274.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 (111) hide show
  1. package/package.json +3 -3
  2. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.expected.ts +44 -0
  3. package/src/__tests__/fixtures/migrate-cross-tenant/manual-only.input.ts +44 -0
  4. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.expected.ts +41 -0
  5. package/src/__tests__/fixtures/migrate-cross-tenant/rewrite.input.ts +41 -0
  6. package/src/__tests__/migrate-cross-tenant-codemod.test.ts +106 -0
  7. package/src/__tests__/related-list-parent-filter.integration.test.ts +103 -0
  8. package/src/api/__tests__/nested-write-ownership.integration.test.ts +268 -0
  9. package/src/api/server.ts +7 -20
  10. package/src/bun-db/__tests__/query-guards.test.ts +60 -3
  11. package/src/bun-db/query.ts +24 -21
  12. package/src/changes.json +105 -0
  13. package/src/db/__tests__/build-filter-where.test.ts +8 -0
  14. package/src/db/__tests__/event-store-executor-list.integration.test.ts +69 -0
  15. package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +266 -2
  16. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +77 -0
  17. package/src/db/__tests__/event-store-executor.integration.test.ts +1 -1
  18. package/src/db/__tests__/replay-migration-sql.test.ts +17 -0
  19. package/src/db/__tests__/tenant-db-convention-cross-tenant.test.ts +72 -0
  20. package/src/db/__tests__/tenant-db-declared-unsafe-raw.test.ts +104 -0
  21. package/src/db/__tests__/tenant-db-global.test.ts +92 -0
  22. package/src/db/__tests__/tenant-db-no-raw.test.ts +75 -0
  23. package/src/db/__tests__/tenant-db.integration.test.ts +3 -1
  24. package/src/db/__tests__/unchecked-system-db.test.ts +76 -0
  25. package/src/db/apply-entity-event.ts +1 -1
  26. package/src/db/assert-exists-in.ts +1 -1
  27. package/src/db/entity-field-encryption.ts +18 -0
  28. package/src/db/event-store-executor-context.ts +12 -5
  29. package/src/db/event-store-executor-read.ts +125 -37
  30. package/src/db/event-store-executor-write.ts +24 -18
  31. package/src/db/queries/shadow-swap.ts +36 -0
  32. package/src/db/replay-migration-sql.ts +5 -5
  33. package/src/db/tenant-db-runner.ts +23 -0
  34. package/src/db/tenant-db.ts +97 -24
  35. package/src/derivatives/__tests__/derivatives-context.test.ts +0 -1
  36. package/src/engine/__tests__/boot-validator-nav-area.test.ts +116 -0
  37. package/src/engine/__tests__/boot-validator-projection-list.test.ts +91 -0
  38. package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +143 -0
  39. package/src/engine/__tests__/boot-validator.test.ts +71 -1
  40. package/src/engine/__tests__/engine.test.ts +86 -0
  41. package/src/engine/__tests__/entity-create-caller-id.integration.test.ts +87 -0
  42. package/src/engine/__tests__/entity-handlers-escape-hatch.test.ts +91 -0
  43. package/src/engine/__tests__/entity-handlers.test.ts +28 -0
  44. package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +30 -5
  45. package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +206 -47
  46. package/src/engine/__tests__/read-steps-tenant-filter.integration.test.ts +215 -0
  47. package/src/engine/__tests__/required-surface-keys.test.ts +16 -0
  48. package/src/engine/__tests__/screen-filter-ops.test.ts +1 -1
  49. package/src/engine/__tests__/screen.test.ts +150 -0
  50. package/src/engine/__tests__/soft-delete-cleanup.test.ts +0 -1
  51. package/src/engine/__tests__/steps-read.test.ts +204 -8
  52. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +7 -4
  53. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +6 -3
  54. package/src/engine/boot-validator/__tests__/global-tenancy.test.ts +16 -1
  55. package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +45 -0
  56. package/src/engine/boot-validator/entity-handler.ts +21 -0
  57. package/src/engine/boot-validator/global-tenancy.ts +13 -1
  58. package/src/engine/boot-validator/parent-ref.ts +1 -1
  59. package/src/engine/boot-validator/projection-list-screens.ts +68 -3
  60. package/src/engine/boot-validator/query-output-columns.ts +33 -0
  61. package/src/engine/boot-validator/screens.ts +154 -31
  62. package/src/engine/build-config-feature-schema.ts +7 -4
  63. package/src/engine/entity-handlers.ts +74 -22
  64. package/src/engine/feature-entity-handlers.ts +4 -4
  65. package/src/engine/index.ts +5 -0
  66. package/src/engine/registry-validate.ts +36 -6
  67. package/src/engine/schema-builder.ts +6 -0
  68. package/src/engine/screen-filter-ops.ts +10 -2
  69. package/src/engine/screen-helpers.ts +52 -0
  70. package/src/engine/steps/_read-source.ts +12 -0
  71. package/src/engine/steps/read-find-many.ts +8 -2
  72. package/src/engine/steps/read-find-one.ts +11 -8
  73. package/src/engine/steps/unsafe-projection-delete.ts +2 -1
  74. package/src/engine/steps/unsafe-projection-upsert.ts +2 -1
  75. package/src/engine/types/index.ts +8 -1
  76. package/src/errors/i18n/de.yaml +14 -0
  77. package/src/errors/i18n/en.yaml +13 -0
  78. package/src/errors/reasons.ts +4 -0
  79. package/src/i18n/required-surface-keys.ts +6 -2
  80. package/src/jobs/__tests__/job-systemdb.integration.test.ts +5 -4
  81. package/src/jobs/__tests__/jobs.integration.test.ts +41 -0
  82. package/src/jobs/job-runner.ts +35 -5
  83. package/src/pipeline/__tests__/escape-hatch-audit.integration.test.ts +254 -0
  84. package/src/pipeline/__tests__/escape-hatch-report.test.ts +149 -0
  85. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +28 -0
  86. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +94 -0
  87. package/src/pipeline/__tests__/projections-runner-systemscope.integration.test.ts +101 -0
  88. package/src/pipeline/__tests__/query-projection.integration.test.ts +169 -14
  89. package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +118 -0
  90. package/src/pipeline/__tests__/rate-limit-default.test.ts +117 -0
  91. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +5 -2
  92. package/src/pipeline/__tests__/system-identity-switch.test.ts +240 -1
  93. package/src/pipeline/dispatch-query.ts +27 -8
  94. package/src/pipeline/dispatch-shared.ts +141 -57
  95. package/src/pipeline/dispatch-stream.ts +1 -3
  96. package/src/pipeline/dispatch-write.ts +62 -14
  97. package/src/pipeline/dispatcher.ts +20 -0
  98. package/src/pipeline/escape-hatch-report.ts +127 -0
  99. package/src/pipeline/index.ts +8 -0
  100. package/src/pipeline/member-read-only-transaction.ts +75 -0
  101. package/src/pipeline/msp-rebuild.ts +2 -0
  102. package/src/pipeline/projection-rebuild.ts +2 -0
  103. package/src/pipeline/projections-runner.ts +15 -13
  104. package/src/pipeline/system-identity-switch.ts +121 -9
  105. package/src/scripts/codemod/migrate-cross-tenant.ts +335 -0
  106. package/src/scripts/codemod/migrate-db-raw.ts +19 -6
  107. package/src/scripts/codemod/migrate-open-to-all.ts +2 -1
  108. package/src/ui-types/index.ts +3 -0
  109. package/src/utils/__tests__/ids.test.ts +29 -1
  110. package/src/utils/ids.ts +10 -1
  111. package/src/utils/index.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { EntityTableMeta } from "@cosmicdrift/kumiko-types/entity-table-meta-types";
3
+ import type { EscapeHatchReporter, EscapeHatchTarget } from "@cosmicdrift/kumiko-types/handlers";
3
4
  import type { TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
4
5
  import { createEntity, createTextField } from "../../engine";
5
6
  import { AccessDeniedError } from "../../errors";
@@ -69,6 +70,19 @@ const explicitGlobalLiteralTable = buildEntityTable("globalGuardExplicitGlobal",
69
70
 
70
71
  const own = testTenantId(1);
71
72
 
73
+ function recordingReporter(): {
74
+ readonly report: EscapeHatchReporter;
75
+ readonly calls: Array<{ kind: string; reason: string; target: EscapeHatchTarget | undefined }>;
76
+ } {
77
+ const calls: Array<{ kind: string; reason: string; target: EscapeHatchTarget | undefined }> = [];
78
+ return {
79
+ report: (kind, reason, target) => {
80
+ calls.push({ kind, reason, target });
81
+ },
82
+ calls,
83
+ };
84
+ }
85
+
72
86
  // Type-only assertions — never invoked, checked by `tsc --build` only.
73
87
  function typeAssertions(tdb: TenantDb): void {
74
88
  // @ts-expect-error db.global() on a "tenant"-tenancy table (default tenancy) is a type error.
@@ -157,6 +171,41 @@ describe("TenantDb.global()", () => {
157
171
  expect(captured).toHaveLength(1);
158
172
  });
159
173
 
174
+ test("write with an escapeHatch reports global-write once with the grant's reason", async () => {
175
+ const db: DbRunner = {
176
+ unsafe: async () => [{ id: "x" }],
177
+ begin: async () => {
178
+ throw new Error("begin not used in this test");
179
+ },
180
+ } as DbRunner;
181
+ const { report, calls } = recordingReporter();
182
+ const tdb = createTenantDb(db, own, "tenant", undefined, undefined, undefined, {
183
+ globalWrites: { reason: "cross-tenant backfill job" },
184
+ report,
185
+ });
186
+ await tdb.global(globalUnmanagedTable).insertOne({ name: "x" });
187
+ expect(calls).toEqual([
188
+ { kind: "global-write", reason: "cross-tenant backfill job", target: undefined },
189
+ ]);
190
+ });
191
+
192
+ test("denied global write (no grant) reports nothing", async () => {
193
+ const { report, calls } = recordingReporter();
194
+ const tdb = createTenantDb(
195
+ unreachableRunner(),
196
+ own,
197
+ "tenant",
198
+ undefined,
199
+ undefined,
200
+ undefined,
201
+ {
202
+ report,
203
+ },
204
+ );
205
+ await expect(tdb.global(globalUnmanagedTable).insertOne({ name: "x" })).rejects.toThrow();
206
+ expect(calls).toEqual([]);
207
+ });
208
+
160
209
  test("write with an explicit non-SYSTEM tenantId rejects even with a valid escapeHatch", async () => {
161
210
  const tdb = createTenantDb(
162
211
  unreachableRunner(),
@@ -214,6 +263,41 @@ describe("TenantDb.unsafeRaw()", () => {
214
263
  });
215
264
  expect(tdb.unsafeRaw("some reason")).toBe(rawDb);
216
265
  });
266
+
267
+ test("with a valid grant reports unsafe-raw once with the reason", () => {
268
+ const { report, calls } = recordingReporter();
269
+ const tdb = createTenantDb(
270
+ unreachableRunner(),
271
+ own,
272
+ "tenant",
273
+ undefined,
274
+ undefined,
275
+ undefined,
276
+ {
277
+ unsafeRaw: { reason: "granted" },
278
+ report,
279
+ },
280
+ );
281
+ tdb.unsafeRaw("cleanup read");
282
+ expect(calls).toEqual([{ kind: "unsafe-raw", reason: "cleanup read", target: undefined }]);
283
+ });
284
+
285
+ test("without a grant reports nothing", () => {
286
+ const { report, calls } = recordingReporter();
287
+ const tdb = createTenantDb(
288
+ unreachableRunner(),
289
+ own,
290
+ "tenant",
291
+ undefined,
292
+ undefined,
293
+ undefined,
294
+ {
295
+ report,
296
+ },
297
+ );
298
+ expect(() => tdb.unsafeRaw("some reason")).toThrow(AccessDeniedError);
299
+ expect(calls).toEqual([]);
300
+ });
217
301
  });
218
302
 
219
303
  describe("withUnsafeRawGrant", () => {
@@ -257,4 +341,12 @@ describe("UncheckedSystemDb.unsafeRaw", () => {
257
341
  const sysDb = createUncheckedSystemDb(tdb);
258
342
  expect(sysDb.unsafeRaw("cleanup job")).toBe(rawDb);
259
343
  });
344
+
345
+ test("reports unsafe-raw once with the reason", () => {
346
+ const tdb = createTenantDb(unreachableRunner(), own);
347
+ const { report, calls } = recordingReporter();
348
+ const sysDb = createUncheckedSystemDb(tdb, undefined, report);
349
+ sysDb.unsafeRaw("cleanup job");
350
+ expect(calls).toEqual([{ kind: "unsafe-raw", reason: "cleanup job", target: undefined }]);
351
+ });
260
352
  });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
3
+ import { InternalError } from "../../errors";
4
+ import { testTenantId } from "../../stack";
5
+ import type { DbRunner } from "../connection";
6
+ import { createTenantDb, createUncheckedSystemDb, withUnsafeRawGrant } from "../tenant-db";
7
+ import { tenantDbRunner } from "../tenant-db-runner";
8
+
9
+ // TenantDb.raw is gone from the type; framework-internal callers resolve
10
+ // the bound DbRunner through tenant-db-runner.ts's WeakMap instead.
11
+
12
+ const tenantId = testTenantId(1);
13
+
14
+ function fakeRunner(): DbRunner {
15
+ return {
16
+ unsafe: async () => [],
17
+ begin: async () => {
18
+ throw new Error("begin not used in these tests");
19
+ },
20
+ } as unknown as DbRunner;
21
+ }
22
+
23
+ describe("TenantDb has no .raw", () => {
24
+ test("compile-time: .raw is not a property of TenantDb", () => {
25
+ const tdb = createTenantDb(fakeRunner(), tenantId);
26
+ // @ts-expect-error TenantDb no longer exposes `raw`.
27
+ expect(tdb.raw).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("tenantDbRunner", () => {
32
+ test("returns the runner bound at createTenantDb() (identity)", () => {
33
+ const runner = fakeRunner();
34
+ const tdb = createTenantDb(runner, tenantId);
35
+ expect(tenantDbRunner(tdb)).toBe(runner);
36
+ });
37
+
38
+ test("withUnsafeRawGrant rebinds also resolve to the original runner", () => {
39
+ const runner = fakeRunner();
40
+ const tdb = createTenantDb(runner, tenantId);
41
+ const regranted = withUnsafeRawGrant(tdb, { reason: "test grant" });
42
+ expect(regranted).not.toBe(tdb);
43
+ expect(tenantDbRunner(regranted)).toBe(runner);
44
+ });
45
+
46
+ test("throws InternalError for a hand-built object not built by createTenantDb", () => {
47
+ const handBuilt = { tenantId, mode: "tenant" } as unknown as TenantDb;
48
+ expect(() => tenantDbRunner(handBuilt)).toThrow(InternalError);
49
+ });
50
+
51
+ test("throws InternalError for a throwing guard Proxy, without the proxy's get trap firing", () => {
52
+ let getTrapFired = false;
53
+ const guardProxy = new Proxy(
54
+ {},
55
+ {
56
+ get() {
57
+ getTrapFired = true;
58
+ throw new Error("systemScope guard: property access denied");
59
+ },
60
+ },
61
+ ) as unknown as TenantDb;
62
+
63
+ expect(() => tenantDbRunner(guardProxy)).toThrow(InternalError);
64
+ expect(getTrapFired).toBe(false);
65
+ });
66
+ });
67
+
68
+ describe("createUncheckedSystemDb(...).unsafeRaw", () => {
69
+ test("resolves to the runner bound at createTenantDb()", () => {
70
+ const runner = fakeRunner();
71
+ const tdb = createTenantDb(runner, tenantId, "system");
72
+ const systemDb = createUncheckedSystemDb(tdb);
73
+ expect(systemDb.unsafeRaw("test reason")).toBe(runner);
74
+ });
75
+ });
@@ -110,7 +110,9 @@ describe("scoped mode (default)", () => {
110
110
 
111
111
  await tdb.insertOne(table, { name: "ColSelect" });
112
112
 
113
- const rows = await asRawClient(tdb).unsafe<Record<string, unknown>>(
113
+ // This probes a custom column projection, not tenant-scoping, so it goes
114
+ // straight at the underlying runner tdb was built from.
115
+ const rows = await asRawClient(testDb.db).unsafe<Record<string, unknown>>(
114
116
  `SELECT id, name FROM "tenant_db_items" WHERE name = $1 LIMIT 10`,
115
117
  ["ColSelect"],
116
118
  );
@@ -1,9 +1,23 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import type { EscapeHatchReporter, EscapeHatchTarget } from "@cosmicdrift/kumiko-types/handlers";
2
3
  import { SYSTEM_TENANT_ID } from "../../engine";
3
4
  import { testTenantId } from "../../stack";
4
5
  import type { DbRunner } from "../connection";
5
6
  import { createTenantDb, createUncheckedSystemDb, SYSTEM_SCOPE_CHECK_BRAND } from "../tenant-db";
6
7
 
8
+ function recordingReporter(): {
9
+ readonly report: EscapeHatchReporter;
10
+ readonly calls: Array<{ kind: string; reason: string; target: EscapeHatchTarget | undefined }>;
11
+ } {
12
+ const calls: Array<{ kind: string; reason: string; target: EscapeHatchTarget | undefined }> = [];
13
+ return {
14
+ report: (kind, reason, target) => {
15
+ calls.push({ kind, reason, target });
16
+ },
17
+ calls,
18
+ };
19
+ }
20
+
7
21
  // createUncheckedSystemDb wraps a "system"-mode TenantDb (r.systemScope())
8
22
  // so a handler must explicitly clear a self-check before using it — none of
9
23
  // these checks execute a query, so a runner that always throws is enough to
@@ -44,6 +58,16 @@ describe("createUncheckedSystemDb", () => {
44
58
 
45
59
  expect(() => unchecked.assertTenantMatch(foreign)).toThrow(/tenant self-check failed/);
46
60
  });
61
+
62
+ test("does not report — it is a tenant-safe check", () => {
63
+ const systemDb = createTenantDb(unusedRunner(), own, "system");
64
+ const { report, calls } = recordingReporter();
65
+ const unchecked = createUncheckedSystemDb(systemDb, undefined, report);
66
+
67
+ unchecked.assertTenantMatch(own);
68
+ expect(() => unchecked.assertTenantMatch(foreign)).toThrow();
69
+ expect(calls).toEqual([]);
70
+ });
47
71
  });
48
72
 
49
73
  describe("assertRowsTenant", () => {
@@ -88,6 +112,16 @@ describe("createUncheckedSystemDb", () => {
88
112
 
89
113
  expect(unchecked.assertRowsTenant(rows, "tenantId")).toBe(rows);
90
114
  });
115
+
116
+ test("does not report — it is a tenant-safe check", () => {
117
+ const systemDb = createTenantDb(unusedRunner(), own, "system");
118
+ const { report, calls } = recordingReporter();
119
+ const unchecked = createUncheckedSystemDb(systemDb, undefined, report);
120
+ const rows = [{ tenantId: own, name: "a" }];
121
+
122
+ unchecked.assertRowsTenant(rows, "tenantId");
123
+ expect(calls).toEqual([]);
124
+ });
91
125
  });
92
126
 
93
127
  describe("acknowledgeCrossTenant", () => {
@@ -113,6 +147,30 @@ describe("createUncheckedSystemDb", () => {
113
147
 
114
148
  expect(() => unchecked.acknowledgeCrossTenant(" ")).toThrow(/non-empty reason/);
115
149
  });
150
+
151
+ test("reports acknowledge-cross-tenant once with the reason", () => {
152
+ const systemDb = createTenantDb(unusedRunner(), own, "system");
153
+ const { report, calls } = recordingReporter();
154
+ const unchecked = createUncheckedSystemDb(systemDb, undefined, report);
155
+
156
+ unchecked.acknowledgeCrossTenant("user feature is cross-tenant by design");
157
+ expect(calls).toEqual([
158
+ {
159
+ kind: "acknowledge-cross-tenant",
160
+ reason: "user feature is cross-tenant by design",
161
+ target: undefined,
162
+ },
163
+ ]);
164
+ });
165
+
166
+ test("an empty reason throws and does not report", () => {
167
+ const systemDb = createTenantDb(unusedRunner(), own, "system");
168
+ const { report, calls } = recordingReporter();
169
+ const unchecked = createUncheckedSystemDb(systemDb, undefined, report);
170
+
171
+ expect(() => unchecked.acknowledgeCrossTenant("")).toThrow();
172
+ expect(calls).toEqual([]);
173
+ });
116
174
  });
117
175
 
118
176
  describe("outsideTransaction", () => {
@@ -178,6 +236,24 @@ describe("createUncheckedSystemDb", () => {
178
236
  /no outside-transaction database source is configured/,
179
237
  );
180
238
  });
239
+
240
+ test("reports acknowledge-cross-tenant once with the reason", () => {
241
+ const systemDb = createTenantDb(unusedRunner(), own, "system");
242
+ const outsideTxDb = createTenantDb(unusedRunner(), own, "system");
243
+ const { report, calls } = recordingReporter();
244
+ const unchecked = createUncheckedSystemDb(systemDb, outsideTxDb, report);
245
+
246
+ unchecked.outsideTransaction.acknowledgeCrossTenant(
247
+ "durability write is cross-tenant by design",
248
+ );
249
+ expect(calls).toEqual([
250
+ {
251
+ kind: "acknowledge-cross-tenant",
252
+ reason: "durability write is cross-tenant by design",
253
+ target: undefined,
254
+ },
255
+ ]);
256
+ });
181
257
  });
182
258
  });
183
259
  });
@@ -89,7 +89,7 @@ export async function applyEntityEvent(
89
89
  switch (verb) {
90
90
  case "created": {
91
91
  // tenantId-Resolution explizit, nicht via Spread-Reihenfolge:
92
- // Live-Pfad nutzt tx=db.raw (kein TenantDb-Wrapper-Auto-Inject),
92
+ // Live-Pfad nutzt tx=tenantDbRunner(db) (kein TenantDb-Wrapper-Auto-Inject),
93
93
  // beim Replay erst recht keiner. Default = event.tenantId; payload
94
94
  // gewinnt NUR wenn gültig string mit length > 0 (seedTenantMembership-
95
95
  // Pfad: Operator schreibt im Ziel-Tenant, Event im Operator-Tenant).
@@ -5,7 +5,7 @@ import type { DbConnection } from "./connection";
5
5
  import type { TenantDb } from "./tenant-db";
6
6
 
7
7
  function isTenantDb(db: DbConnection | TenantDb): db is TenantDb {
8
- return typeof (db as TenantDb).fetchOne === "function" && "raw" in db;
8
+ return "tenantId" in db && "fetchOne" in db && typeof db.fetchOne === "function";
9
9
  }
10
10
 
11
11
  /**
@@ -4,6 +4,7 @@
4
4
  // single-key ciphertexts (pre-envelope ENCRYPTION_KEY era) readable via
5
5
  // the cipher's legacy fallback until re-encrypted.
6
6
 
7
+ import { collectPiiSubjectFields } from "../crypto";
7
8
  import type { EntityDefinition, TenantId } from "../engine/types";
8
9
  import { createEnvMasterKeyProvider } from "../secrets/env-master-key-provider";
9
10
  import type { EnvelopeCipher } from "../secrets/envelope-cipher";
@@ -19,6 +20,23 @@ export function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySe
19
20
  return names;
20
21
  }
21
22
 
23
+ // A field whose stored value is ciphertext, either always (encrypted) or
24
+ // depending on runtime KMS wiring (PII) — either way it has no plaintext
25
+ // column to compare against directly.
26
+ export function isSensitiveLabelField(entity: EntityDefinition, field: string): boolean {
27
+ return (
28
+ collectEncryptedFieldNames(entity).has(field) || collectPiiSubjectFields(entity).includes(field)
29
+ );
30
+ }
31
+
32
+ // `searchable: true` on a text field means it is indexed plaintext
33
+ // somewhere findable by term — a plain text field's own column, or (for a
34
+ // personal-data field) the derived search index mirroring it.
35
+ export function hasSearchablePlaintext(entity: EntityDefinition, field: string): boolean {
36
+ const def = entity.fields[field];
37
+ return def?.type === "text" && def.searchable === true;
38
+ }
39
+
22
40
  // BYOK hook: thread the row's tenant to the cipher when the row carries one
23
41
  // (system-scope rows may not).
24
42
  function scopeOf(row: Record<string, unknown>): KeyScope | undefined {
@@ -38,6 +38,7 @@ import type { EventStoreExecutorOptions } from "./event-store-executor";
38
38
  import { constraintOf, isUniqueViolation } from "./pg-error";
39
39
  import { toSnakeCase } from "./table-builder";
40
40
  import type { TenantDb } from "./tenant-db";
41
+ import { tenantDbRunner } from "./tenant-db-runner";
41
42
 
42
43
  // Shared context-building for the event-store-executor CRUD verbs (create/
43
44
  // update/delete/forget/restore/list/detail — see event-store-executor-write.ts
@@ -50,11 +51,11 @@ export type Table = TableColumns<any>;
50
51
 
51
52
  // Screen-Filter (Tier 2.7c) — Op-Mapping als Where-Operator. Boot-
52
53
  // Validator pinst field-Existenz + filterable + op-vs-Type-Compat.
53
- // `op` ist auf {eq,ne,lt,gt,in} normalisiert; "in" mit empty-array ist
54
- // explizit no-match.
54
+ // `op` ist auf {eq,ne,lt,gt,lte,gte,in} normalisiert; "in" mit
55
+ // empty-array ist explizit no-match.
55
56
  export function buildFilterWhere(
56
57
  field: string,
57
- op: "eq" | "ne" | "lt" | "gt" | "in",
58
+ op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in",
58
59
  value: unknown,
59
60
  ): WhereObject | null {
60
61
  switch (op) {
@@ -66,6 +67,10 @@ export function buildFilterWhere(
66
67
  return { [field]: { lt: value } };
67
68
  case "gt":
68
69
  return { [field]: { gt: value } };
70
+ case "lte":
71
+ return { [field]: { lte: value } };
72
+ case "gte":
73
+ return { [field]: { gte: value } };
69
74
  case "in":
70
75
  if (Array.isArray(value) && value.length > 0) {
71
76
  return { [field]: value };
@@ -358,7 +363,7 @@ export function buildExecutorContext(
358
363
  id: EntityId,
359
364
  tenantId: TenantId,
360
365
  ): Promise<void> {
361
- if (await isStreamArchived(db.raw, tenantId, String(id))) {
366
+ if (await isStreamArchived(tenantDbRunner(db), tenantId, String(id))) {
362
367
  throw new ArchivedStreamError(tenantId, String(id));
363
368
  }
364
369
  }
@@ -401,7 +406,9 @@ export function buildExecutorContext(
401
406
  whereParts.push(shifted.sqlText);
402
407
  for (const p of shifted.params) params.push(p);
403
408
  const sqlText = `SELECT * FROM "${tableName}" WHERE ${whereParts.join(" AND ")} LIMIT 1`;
404
- return [...(await executeRawQueryRead<Record<string, unknown>>(db.raw, sqlText, params))];
409
+ return [
410
+ ...(await executeRawQueryRead<Record<string, unknown>>(tenantDbRunner(db), sqlText, params)),
411
+ ];
405
412
  }
406
413
 
407
414
  return {