@cosmicdrift/kumiko-bundled-features 0.277.0 → 0.280.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 (62) hide show
  1. package/package.json +9 -9
  2. package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +8 -2
  3. package/src/auth-mfa-user-data/__tests__/hooks.integration.test.ts +6 -6
  4. package/src/auth-mfa-user-data/hooks.ts +4 -6
  5. package/src/cap-counter/__tests__/enforce-cap.test.ts +7 -10
  6. package/src/cap-counter/__tests__/stock-cap-guard.integration.test.ts +123 -6
  7. package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +135 -1
  8. package/src/cap-counter/book-cap-usage.ts +158 -0
  9. package/src/cap-counter/changes.json +16 -1
  10. package/src/cap-counter/enforce-cap.ts +5 -37
  11. package/src/cap-counter/handlers/increment.write.ts +7 -47
  12. package/src/cap-counter/handlers/mark-soft-warned.write.ts +6 -33
  13. package/src/cap-counter/index.ts +8 -0
  14. package/src/cap-counter/stock-cap-guard.ts +14 -19
  15. package/src/cap-counter/with-cap-enforcement.ts +9 -11
  16. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +10 -3
  17. package/src/custom-fields/wire-user-data-rights.ts +40 -24
  18. package/src/data-retention/resolve-for-tenant.ts +18 -6
  19. package/src/delivery/db/queries/preferences.ts +1 -0
  20. package/src/files-tenant-data/hooks.ts +3 -5
  21. package/src/folders/__tests__/folders.integration.test.ts +4 -3
  22. package/src/folders-user-data/__tests__/hooks.integration.test.ts +2 -1
  23. package/src/folders-user-data/hooks.ts +5 -10
  24. package/src/form-draft-user-data/__tests__/hooks-delete-failure.integration.test.ts +7 -1
  25. package/src/form-draft-user-data/__tests__/hooks.integration.test.ts +16 -5
  26. package/src/form-draft-user-data/hooks.ts +3 -6
  27. package/src/notes-history-user-data/__tests__/hooks.integration.test.ts +9 -3
  28. package/src/notes-history-user-data/hooks.ts +3 -4
  29. package/src/personal-access-tokens/handlers/create.write.ts +1 -0
  30. package/src/template-resolver-user-data/hooks.ts +1 -2
  31. package/src/tenant-lifecycle/feature.ts +2 -0
  32. package/src/tenant-lifecycle/run-tenant-destroy.ts +9 -0
  33. package/src/tenant-lifecycle/stages.ts +26 -2
  34. package/src/tier-engine/__tests__/tier-resolver.integration.test.ts +138 -0
  35. package/src/tier-engine/changes.json +7 -0
  36. package/src/tier-engine/tier-resolver.ts +7 -6
  37. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +5 -14
  38. package/src/user-data-rights/__tests__/escape-hatch-audit.integration.test.ts +133 -0
  39. package/src/user-data-rights/__tests__/escape-hatch-declaration.integration.test.ts +198 -0
  40. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +15 -3
  41. package/src/user-data-rights/__tests__/forget-cleanup-search-purge-ordering.integration.test.ts +10 -2
  42. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +3 -1
  43. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +8 -3
  44. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +8 -2
  45. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +8 -4
  46. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +15 -5
  47. package/src/user-data-rights/feature.ts +4 -0
  48. package/src/user-data-rights/run-export-jobs.ts +16 -1
  49. package/src/user-data-rights/run-forget-cleanup.ts +44 -2
  50. package/src/user-data-rights/run-user-export.ts +43 -3
  51. package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +7 -6
  52. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +64 -18
  53. package/src/user-data-rights-defaults/hooks/api-token.userdata-hook.ts +2 -3
  54. package/src/user-data-rights-defaults/hooks/config-value.userdata-hook.ts +4 -6
  55. package/src/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.ts +1 -2
  56. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +5 -11
  57. package/src/user-data-rights-defaults/hooks/in-app-message.userdata-hook.ts +2 -3
  58. package/src/user-data-rights-defaults/hooks/job-run.userdata-hook.ts +1 -2
  59. package/src/user-data-rights-defaults/hooks/notification-preference.userdata-hook.ts +4 -6
  60. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +8 -10
  61. package/src/user-data-rights-defaults/hooks/user-session.userdata-hook.ts +2 -3
  62. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +9 -10
@@ -49,7 +49,11 @@ import { addDurationSpec } from "@cosmicdrift/kumiko-framework/compliance";
49
49
  import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
50
50
  import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
51
51
  import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
52
- import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
52
+ import type {
53
+ EscapeHatchAuditSink,
54
+ Registry,
55
+ TenantId,
56
+ } from "@cosmicdrift/kumiko-framework/engine";
53
57
  import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
54
58
  import {
55
59
  createZipStream,
@@ -155,6 +159,11 @@ export interface RunExportJobsArgs {
155
159
  * a URL fragment (not a query param — see issue #1271) and passes that
156
160
  * Callback durch. Required wenn sendExportReadyEmail gesetzt. */
157
161
  readonly appExportDownloadUrl?: string;
162
+
163
+ // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
164
+ // systemUser.id); threaded into runUserExport's per-hook TenantDb.
165
+ readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
166
+ readonly actor?: string;
158
167
  }
159
168
 
160
169
  export interface ExportJobError {
@@ -219,6 +228,8 @@ export async function runExportJobs(args: RunExportJobsArgs): Promise<RunExportJ
219
228
  buildStorageProvider,
220
229
  now,
221
230
  job,
231
+ escapeHatchAuditSink: args.escapeHatchAuditSink,
232
+ actor: args.actor,
222
233
  });
223
234
  if (outcome.kind === "done") {
224
235
  completedJobIds.push(job.id);
@@ -337,6 +348,8 @@ async function processJob(args: {
337
348
  buildStorageProvider: (tenantId: TenantId) => Promise<FileStorageProvider>;
338
349
  now: Instant;
339
350
  job: JobRow;
351
+ escapeHatchAuditSink?: EscapeHatchAuditSink;
352
+ actor?: string;
340
353
  }): Promise<ProcessOutcome> {
341
354
  const { db, registry, buildStorageProvider, now, job } = args;
342
355
  const executor = createSystemUser(job.requestedFromTenantId);
@@ -381,6 +394,8 @@ async function processJob(args: {
381
394
  registry,
382
395
  userId: job.userId,
383
396
  now,
397
+ escapeHatchAuditSink: args.escapeHatchAuditSink,
398
+ actor: args.actor,
384
399
  });
385
400
 
386
401
  const profile = await resolveProfileForTenant({
@@ -39,14 +39,17 @@ import {
39
39
  subjectIdToKey,
40
40
  } from "@cosmicdrift/kumiko-framework/crypto";
41
41
  import {
42
+ createTenantDb,
42
43
  type DbRunner,
43
44
  entityEventName,
44
45
  executeRawQuery,
45
46
  nullBlindIndexesForSubject,
46
47
  } from "@cosmicdrift/kumiko-framework/db";
47
48
  import {
49
+ type EscapeHatchAuditSink,
48
50
  EXT_USER_DATA,
49
51
  EXT_USER_DATA_ORDER,
52
+ extensionUsageEscapeHatchReason,
50
53
  type Registry,
51
54
  type TenantId,
52
55
  type TenantUserModel,
@@ -54,6 +57,7 @@ import {
54
57
  type UserDataDeleteStrategy,
55
58
  type UserDataStorageProvider,
56
59
  } from "@cosmicdrift/kumiko-framework/engine";
60
+ import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
57
61
  import {
58
62
  purgeSearchDocumentsForSubject,
59
63
  type SearchAdapter,
@@ -140,6 +144,12 @@ export interface RunForgetCleanupArgs {
140
144
  * (no tenant-scoped erasure).
141
145
  */
142
146
  readonly tenantModel?: TenantUserModel;
147
+
148
+ // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
149
+ // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
150
+ // escapeHatch when its delete hook calls ctx.db.unsafeRaw(reason).
151
+ readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
152
+ readonly actor?: string;
143
153
  }
144
154
 
145
155
  export interface ForgetCleanupError {
@@ -184,6 +194,26 @@ interface HookEntry {
184
194
  /** Lower runs first. Owner-column-preserving redaction declares a negative
185
195
  * order so it precedes owner-nulling hooks on the same entity (see sort below). */
186
196
  readonly order: number;
197
+ readonly escapeHatchReason: string | undefined;
198
+ }
199
+
200
+ function buildHookDb(
201
+ db: DbRunner,
202
+ tenantId: TenantId,
203
+ entry: HookEntry,
204
+ args: { readonly escapeHatchAuditSink?: EscapeHatchAuditSink; readonly actor?: string },
205
+ ) {
206
+ const report = createEscapeHatchReporter({
207
+ handler: `${EXT_USER_DATA}:${entry.entityName}`,
208
+ tenantId,
209
+ actor: args.actor ?? "system",
210
+ sink: args.escapeHatchAuditSink,
211
+ });
212
+ return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
213
+ unsafeRaw:
214
+ entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
215
+ report,
216
+ });
187
217
  }
188
218
 
189
219
  // EXT_USER_DATA delete-hooks default here; a hook that redacts data keyed on an
@@ -226,7 +256,12 @@ export async function runForgetCleanup(
226
256
  const opts = (u.options ?? {}) as { delete?: UserDataDeleteHook; order?: number }; // @cast-boundary engine-payload
227
257
  if (!opts.delete) return null;
228
258
  const order = typeof opts.order === "number" ? opts.order : HOOK_ORDER_DEFAULT;
229
- return { entityName: u.entityName, deleteHook: opts.delete, order };
259
+ return {
260
+ entityName: u.entityName,
261
+ deleteHook: opts.delete,
262
+ order,
263
+ escapeHatchReason: extensionUsageEscapeHatchReason(u),
264
+ };
230
265
  })
231
266
  .filter((x): x is HookEntry => x !== null)
232
267
  // Order ascending. Array.sort is ES2019-stable, so equal orders keep
@@ -252,6 +287,8 @@ export async function runForgetCleanup(
252
287
  appTenantModel,
253
288
  kms,
254
289
  searchAdapter: args.searchAdapter,
290
+ escapeHatchAuditSink: args.escapeHatchAuditSink,
291
+ actor: args.actor,
255
292
  });
256
293
  hookCallsAttempted += userResult.hookCallsAttempted;
257
294
  errors.push(...userResult.errors);
@@ -316,6 +353,8 @@ async function processUser(args: {
316
353
  appTenantModel: TenantUserModel;
317
354
  kms?: KmsAdapter;
318
355
  searchAdapter?: SearchAdapter;
356
+ escapeHatchAuditSink?: EscapeHatchAuditSink;
357
+ actor?: string;
319
358
  }): Promise<ProcessUserResult> {
320
359
  const { db, registry, userId, hookEntries, buildStorageProvider, appTenantModel, kms } = args;
321
360
  const errors: ForgetCleanupError[] = [];
@@ -404,7 +443,10 @@ async function processUser(args: {
404
443
  hookCallsAttempted++;
405
444
  const hookResult = await entry.deleteHook(
406
445
  {
407
- db: tx,
446
+ db: buildHookDb(tx, tenantId, entry, {
447
+ escapeHatchAuditSink: args.escapeHatchAuditSink,
448
+ actor: args.actor,
449
+ }),
408
450
  registry,
409
451
  tenantId,
410
452
  userId,
@@ -30,15 +30,19 @@ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
30
30
  import {
31
31
  collectEncryptedFieldNames,
32
32
  configuredEntityFieldEncryption,
33
+ createTenantDb,
33
34
  decryptEntityFieldValues,
34
35
  } from "@cosmicdrift/kumiko-framework/db";
35
36
  import {
37
+ type EscapeHatchAuditSink,
36
38
  EXT_USER_DATA,
39
+ extensionUsageEscapeHatchReason,
37
40
  type Registry,
38
41
  type TenantId,
39
42
  type UserDataExportHook,
40
43
  type UserDataExportSnippet,
41
44
  } from "@cosmicdrift/kumiko-framework/engine";
45
+ import { createEscapeHatchReporter } from "@cosmicdrift/kumiko-framework/pipeline";
42
46
  import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
43
47
  import { tenantMembershipsTable } from "../tenant";
44
48
  import { buildFileRefZipPath } from "./zip-path";
@@ -50,6 +54,11 @@ export interface RunUserExportArgs {
50
54
  readonly registry: Registry;
51
55
  readonly userId: string;
52
56
  readonly now: Instant;
57
+ // fw#2914 — sourced from the owning job's ctx (_escapeHatchAuditSink,
58
+ // systemUser.id); attributes+audits any EXT_USER_DATA usage's declared
59
+ // escapeHatch when its export hook calls ctx.db.unsafeRaw(reason).
60
+ readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
61
+ readonly actor?: string;
53
62
  }
54
63
 
55
64
  export interface UserExportFileRef {
@@ -90,6 +99,26 @@ export interface UserExportBundle {
90
99
  interface HookEntry {
91
100
  readonly entityName: string;
92
101
  readonly exportHook: UserDataExportHook;
102
+ readonly escapeHatchReason: string | undefined;
103
+ }
104
+
105
+ function buildHookDb(
106
+ db: DbRunner,
107
+ tenantId: TenantId,
108
+ entry: HookEntry,
109
+ args: Pick<RunUserExportArgs, "escapeHatchAuditSink" | "actor">,
110
+ ) {
111
+ const report = createEscapeHatchReporter({
112
+ handler: `${EXT_USER_DATA}:${entry.entityName}`,
113
+ tenantId,
114
+ actor: args.actor ?? "system",
115
+ sink: args.escapeHatchAuditSink,
116
+ });
117
+ return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
118
+ unsafeRaw:
119
+ entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
120
+ report,
121
+ });
93
122
  }
94
123
 
95
124
  /**
@@ -137,7 +166,13 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
137
166
  const hookEntries: HookEntry[] = usages
138
167
  .map((u): HookEntry | null => {
139
168
  const opts = (u.options ?? {}) as { export?: UserDataExportHook }; // @cast-boundary engine-payload
140
- return opts.export ? { entityName: u.entityName, exportHook: opts.export } : null;
169
+ return opts.export
170
+ ? {
171
+ entityName: u.entityName,
172
+ exportHook: opts.export,
173
+ escapeHatchReason: extensionUsageEscapeHatchReason(u),
174
+ }
175
+ : null;
141
176
  })
142
177
  .filter((x): x is HookEntry => x !== null);
143
178
 
@@ -147,7 +182,12 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
147
182
  for (const tenantId of tenantList) {
148
183
  const entities: UserDataExportSnippet[] = [];
149
184
  for (const entry of hookEntries) {
150
- const rawSnippet = await entry.exportHook({ db, registry, tenantId, userId });
185
+ const rawSnippet = await entry.exportHook({
186
+ db: buildHookDb(db, tenantId, entry, args),
187
+ registry,
188
+ tenantId,
189
+ userId,
190
+ });
151
191
  if (rawSnippet === null) continue;
152
192
  const snippet = await decryptSnippetFields(registry, entry.entityName, rawSnippet);
153
193
  entities.push(snippet);
@@ -181,7 +221,7 @@ export async function runUserExport(args: RunUserExportArgs): Promise<UserExport
181
221
  const orphanEntities: UserDataExportSnippet[] = [];
182
222
  for (const entry of hookEntries) {
183
223
  const rawSnippet = await entry.exportHook({
184
- db,
224
+ db: buildHookDb(db, SYSTEM_TENANT_ID_FOR_ORPHANS, entry, args),
185
225
  registry,
186
226
  tenantId: SYSTEM_TENANT_ID_FOR_ORPHANS,
187
227
  userId,
@@ -12,6 +12,8 @@
12
12
 
13
13
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
14
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
15
+ import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
16
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
15
17
  import {
16
18
  setupTestStack,
17
19
  type TestStack,
@@ -117,7 +119,7 @@ function uuid(suffix: number): string {
117
119
 
118
120
  function ctx(userId: string, overrides: Record<string, unknown> = {}) {
119
121
  return {
120
- db: full.db,
122
+ db: createTenantDb(full.db, TENANT_A, "tenant"),
121
123
  registry: full.registry,
122
124
  tenantId: TENANT_A,
123
125
  userId,
@@ -132,10 +134,9 @@ async function rawSelect(sql: string, params: unknown[]) {
132
134
  }
133
135
 
134
136
  async function seedUser(id: string): Promise<void> {
135
- const SYSTEM_TENANT = "00000000-0000-4000-8000-000000000001";
136
137
  await seedRow(full.db, userTable, {
137
138
  id,
138
- tenantId: SYSTEM_TENANT,
139
+ tenantId: SYSTEM_TENANT_ID,
139
140
  email: `user-${id}@example.com`,
140
141
  passwordHash: "hashed-password",
141
142
  displayName: `User ${id}`,
@@ -404,11 +405,11 @@ describe("job-run userData-hooks (#799)", () => {
404
405
  `INSERT INTO store_job_runs
405
406
  (id, tenant_id, job_name, bull_job_id, status, payload, attempt, started_at, triggered_by_id)
406
407
  VALUES ($1::uuid, $2, 'app:job:export', $3, 'completed', '{"scope":"mine"}', 1, now(), $4)`,
407
- [id, TENANT_A, `bull-${id}`, triggeredById],
408
+ [id, SYSTEM_TENANT_ID, `bull-${id}`, triggeredById],
408
409
  );
409
410
  }
410
411
 
411
- test("export filters by triggeredById only (system-tenant rows)", async () => {
412
+ test("export filters by triggeredById only (rows live on SYSTEM_TENANT_ID, ctx bound to TENANT_A)", async () => {
412
413
  await seedRun(uuid(70), "jr-user-1");
413
414
  await seedRun(uuid(71), "jr-user-2");
414
415
  await seedRun(uuid(72), null);
@@ -428,7 +429,7 @@ describe("job-run userData-hooks (#799)", () => {
428
429
  describe("Presence-Gating: Source-Feature nicht gemountet → null/no-op statt Crash", () => {
429
430
  test("alle gegateten Hooks no-open auf dem Minimal-Stack", async () => {
430
431
  const minCtx = {
431
- db: minimal.db,
432
+ db: createTenantDb(minimal.db, TENANT_A, "tenant"),
432
433
  registry: minimal.registry,
433
434
  tenantId: TENANT_A,
434
435
  userId: "any-user",
@@ -13,7 +13,9 @@
13
13
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
14
  import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
15
15
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
16
+ import { createTenantDb } from "@cosmicdrift/kumiko-framework/db";
16
17
  import { variantSuffix } from "@cosmicdrift/kumiko-framework/derivatives";
18
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
17
19
  import {
18
20
  createInMemoryFileProvider,
19
21
  deriveKey,
@@ -87,10 +89,9 @@ async function seedUser(id: string, overrides: Record<string, unknown> = {}): Pr
87
89
  // user-Entity ist tenant-agnostisch im Domain-Sinn, aber das DB-
88
90
  // Schema hat tenant_id-Spalte automatisch (Framework-Default).
89
91
  // Pragmatisch: SYSTEM_TENANT_ID fuer User-Rows in Tests.
90
- const SYSTEM_TENANT = "00000000-0000-4000-8000-000000000001";
91
92
  await seedRow(stack.db, userTable, {
92
93
  id,
93
- tenantId: SYSTEM_TENANT,
94
+ tenantId: SYSTEM_TENANT_ID,
94
95
  email: `user-${id}@example.com`,
95
96
  passwordHash: "hashed-password",
96
97
  displayName: `User ${id}`,
@@ -168,7 +169,7 @@ describe("S2.H1 :: userExportHook", () => {
168
169
  await seedUser(uuid(1001), { displayName: "Marc" });
169
170
 
170
171
  const result = await userExportHook({
171
- db: stack.db,
172
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
172
173
  registry: stack.registry,
173
174
  tenantId: TENANT_A,
174
175
  userId: uuid(1001),
@@ -189,7 +190,7 @@ describe("S2.H1 :: userExportHook", () => {
189
190
 
190
191
  test("returns null wenn User nicht existiert", async () => {
191
192
  const result = await userExportHook({
192
- db: stack.db,
193
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
193
194
  registry: stack.registry,
194
195
  tenantId: TENANT_A,
195
196
  userId: uuid(1002),
@@ -203,7 +204,12 @@ describe("S2.H1 :: userDeleteHook", () => {
203
204
  await seedUser(uuid(1003));
204
205
 
205
206
  await userDeleteHook(
206
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: uuid(1003) },
207
+ {
208
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
209
+ registry: stack.registry,
210
+ tenantId: TENANT_A,
211
+ userId: uuid(1003),
212
+ },
207
213
  "delete",
208
214
  );
209
215
 
@@ -222,7 +228,12 @@ describe("S2.H1 :: userDeleteHook", () => {
222
228
  await seedUser(uuid(1004));
223
229
 
224
230
  await userDeleteHook(
225
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: uuid(1004) },
231
+ {
232
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
233
+ registry: stack.registry,
234
+ tenantId: TENANT_A,
235
+ userId: uuid(1004),
236
+ },
226
237
  "anonymize",
227
238
  );
228
239
 
@@ -238,7 +249,12 @@ describe("S2.H1 :: userDeleteHook", () => {
238
249
  await seedUser(uuid(1005));
239
250
 
240
251
  await userDeleteHook(
241
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: uuid(1005) },
252
+ {
253
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
254
+ registry: stack.registry,
255
+ tenantId: TENANT_A,
256
+ userId: uuid(1005),
257
+ },
242
258
  "delete",
243
259
  );
244
260
  const afterFirst = await fetchUser(uuid(1005));
@@ -247,7 +263,12 @@ describe("S2.H1 :: userDeleteHook", () => {
247
263
  // Zweiter Call: kein Crash + State unverändert
248
264
  await expect(
249
265
  userDeleteHook(
250
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: uuid(1005) },
266
+ {
267
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
268
+ registry: stack.registry,
269
+ tenantId: TENANT_A,
270
+ userId: uuid(1005),
271
+ },
251
272
  "delete",
252
273
  ),
253
274
  ).resolves.toBeUndefined();
@@ -269,7 +290,7 @@ describe("S2.H2 :: fileRefExportHook", () => {
269
290
  await seedFileRef(uuid(102), TENANT_A, "user-files-1", "anschreiben.pdf");
270
291
 
271
292
  const result = await fileRefExportHook({
272
- db: stack.db,
293
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
273
294
  registry: stack.registry,
274
295
  tenantId: TENANT_A,
275
296
  userId: "user-files-1",
@@ -284,7 +305,7 @@ describe("S2.H2 :: fileRefExportHook", () => {
284
305
 
285
306
  test("returns null wenn User keine Files hat", async () => {
286
307
  const result = await fileRefExportHook({
287
- db: stack.db,
308
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
288
309
  registry: stack.registry,
289
310
  tenantId: TENANT_A,
290
311
  userId: "ghost-user-no-files",
@@ -299,7 +320,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
299
320
  await seedFileRef(uuid(202), TENANT_A, "user-delete-files", "f2.pdf");
300
321
 
301
322
  await fileRefDeleteHook(
302
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: "user-delete-files" },
323
+ {
324
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
325
+ registry: stack.registry,
326
+ tenantId: TENANT_A,
327
+ userId: "user-delete-files",
328
+ },
303
329
  "delete",
304
330
  );
305
331
 
@@ -311,7 +337,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
311
337
  await seedFileRef(uuid(203), TENANT_A, "user-anon-files", "shared.pdf");
312
338
 
313
339
  await fileRefDeleteHook(
314
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: "user-anon-files" },
340
+ {
341
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
342
+ registry: stack.registry,
343
+ tenantId: TENANT_A,
344
+ userId: "user-anon-files",
345
+ },
315
346
  "anonymize",
316
347
  );
317
348
 
@@ -329,7 +360,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
329
360
 
330
361
  // Tenant A loescht alle Files von "shared-user"
331
362
  await fileRefDeleteHook(
332
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: "shared-user" },
363
+ {
364
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
365
+ registry: stack.registry,
366
+ tenantId: TENANT_A,
367
+ userId: "shared-user",
368
+ },
333
369
  "delete",
334
370
  );
335
371
 
@@ -345,7 +381,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
345
381
  await seedFileRef(uuid(401), TENANT_A, "user-idem-files", "f.pdf");
346
382
 
347
383
  await fileRefDeleteHook(
348
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: "user-idem-files" },
384
+ {
385
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
386
+ registry: stack.registry,
387
+ tenantId: TENANT_A,
388
+ userId: "user-idem-files",
389
+ },
349
390
  "delete",
350
391
  );
351
392
  const afterFirst = await fetchFileRefs(TENANT_A, "user-idem-files");
@@ -354,7 +395,12 @@ describe("S2.H2 :: fileRefDeleteHook", () => {
354
395
  // Zweiter Call: kein Crash + State weiter 0 Files
355
396
  await expect(
356
397
  fileRefDeleteHook(
357
- { db: stack.db, registry: stack.registry, tenantId: TENANT_A, userId: "user-idem-files" },
398
+ {
399
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
400
+ registry: stack.registry,
401
+ tenantId: TENANT_A,
402
+ userId: "user-idem-files",
403
+ },
358
404
  "delete",
359
405
  ),
360
406
  ).resolves.toBeUndefined();
@@ -398,7 +444,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
398
444
 
399
445
  await fileRefDeleteHook(
400
446
  {
401
- db: stack.db,
447
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
402
448
  registry: stack.registry,
403
449
  tenantId: TENANT_A,
404
450
  userId,
@@ -428,7 +474,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
428
474
 
429
475
  await fileRefDeleteHook(
430
476
  {
431
- db: stack.db,
477
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
432
478
  registry: stack.registry,
433
479
  tenantId: TENANT_A,
434
480
  userId,
@@ -459,7 +505,7 @@ describe("S2.H2 :: fileRefDeleteHook — GDPR derivatives survive forget (issue
459
505
  await expect(
460
506
  fileRefDeleteHook(
461
507
  {
462
- db: stack.db,
508
+ db: createTenantDb(stack.db, TENANT_A, "tenant"),
463
509
  registry: stack.registry,
464
510
  tenantId: TENANT_A,
465
511
  userId,
@@ -1,4 +1,3 @@
1
- import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { apiTokenTable } from "../../personal-access-tokens";
4
3
  import { featureMounted } from "./feature-mounted";
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
11
10
 
12
11
  export const apiTokenExportHook: UserDataExportHook = async (ctx) => {
13
12
  if (!featureMounted(ctx, "personal-access-tokens")) return null;
14
- const rows = await selectMany<Record<string, unknown>>(ctx.db, apiTokenTable, {
13
+ const rows = await ctx.db.selectMany(apiTokenTable, {
15
14
  userId: ctx.userId,
16
15
  tenantId: ctx.tenantId,
17
16
  });
@@ -33,5 +32,5 @@ export const apiTokenExportHook: UserDataExportHook = async (ctx) => {
33
32
  export const apiTokenDeleteHook: UserDataDeleteHook = async (ctx) => {
34
33
  // skip: personal-access-tokens not mounted — its table doesn't exist, nothing to erase.
35
34
  if (!featureMounted(ctx, "personal-access-tokens")) return;
36
- await deleteMany(ctx.db, apiTokenTable, { userId: ctx.userId, tenantId: ctx.tenantId });
35
+ await ctx.db.deleteMany(apiTokenTable, { userId: ctx.userId, tenantId: ctx.tenantId });
37
36
  };
@@ -1,5 +1,4 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
1
+ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
2
  import {
4
3
  createSystemUser,
5
4
  type UserDataDeleteHook,
@@ -21,7 +20,7 @@ const crud = createEventStoreExecutor(configValuesTable, configValueEntity, {
21
20
 
22
21
  export const configValueExportHook: UserDataExportHook = async (ctx) => {
23
22
  if (!featureMounted(ctx, "config")) return null;
24
- const rows = await selectMany<Record<string, unknown>>(ctx.db, configValuesTable, {
23
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(configValuesTable, {
25
24
  tenantId: ctx.tenantId,
26
25
  userId: ctx.userId,
27
26
  });
@@ -39,14 +38,13 @@ export const configValueDeleteHook: UserDataDeleteHook = async (ctx) => {
39
38
  // skip: config not mounted — its table doesn't exist, nothing to erase.
40
39
  if (!featureMounted(ctx, "config")) return;
41
40
  const systemUser = createSystemUser(ctx.tenantId);
42
- const tdb = createTenantDb(ctx.db, ctx.tenantId, "system");
43
- const rows = await selectMany<Record<string, unknown>>(ctx.db, configValuesTable, {
41
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(configValuesTable, {
44
42
  tenantId: ctx.tenantId,
45
43
  userId: ctx.userId,
46
44
  });
47
45
  for (const row of rows) {
48
46
  const id = row["id"]; // @cast-boundary db-row
49
47
  if (typeof id !== "string") continue;
50
- assertErased(await crud.forget({ id }, systemUser, tdb), "config-value", id);
48
+ assertErased(await crud.forget({ id }, systemUser, ctx.db), "config-value", id);
51
49
  }
52
50
  };
@@ -1,4 +1,3 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { deliveryAttemptsTable } from "../../delivery";
4
3
  import { featureMounted } from "./feature-mounted";
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
11
10
 
12
11
  export const deliveryAttemptExportHook: UserDataExportHook = async (ctx) => {
13
12
  if (!featureMounted(ctx, "delivery")) return null;
14
- const rows = await selectMany<Record<string, unknown>>(ctx.db, deliveryAttemptsTable, {
13
+ const rows = await ctx.db.selectMany(deliveryAttemptsTable, {
15
14
  tenantId: ctx.tenantId,
16
15
  recipientId: ctx.userId,
17
16
  });
@@ -1,9 +1,4 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import {
3
- createEventStoreExecutor,
4
- createTenantDb,
5
- type TenantDb,
6
- } from "@cosmicdrift/kumiko-framework/db";
1
+ import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
7
2
  import { derivativeListPrefix, isDerivativeKeyOf } from "@cosmicdrift/kumiko-framework/derivatives";
8
3
  import {
9
4
  createSystemUser,
@@ -69,7 +64,7 @@ const crud = createEventStoreExecutor(fileRefsTable, fileRefEntity, { entityName
69
64
  export const fileRefExportHook: UserDataExportHook = async (ctx) => {
70
65
  // isDeleted:false — soft-deleted (trashed) Files gehören nicht ins
71
66
  // Auskunfts-Bundle. Forget (delete-Hook unten) erfasst sie trotzdem.
72
- const rawRows = await selectMany(ctx.db, fileRefsTable, {
67
+ const rawRows = await ctx.db.selectMany(fileRefsTable, {
73
68
  tenantId: ctx.tenantId,
74
69
  insertedById: ctx.userId,
75
70
  isDeleted: false,
@@ -218,8 +213,7 @@ async function severPersonLink(
218
213
 
219
214
  export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
220
215
  const systemUser = createSystemUser(ctx.tenantId);
221
- const tdb = createTenantDb(ctx.db, ctx.tenantId, "system");
222
- const rows = await selectMany<Record<string, unknown>>(ctx.db, fileRefsTable, {
216
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(fileRefsTable, {
223
217
  tenantId: ctx.tenantId,
224
218
  insertedById: ctx.userId,
225
219
  });
@@ -227,7 +221,7 @@ export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
227
221
  if (strategy !== "delete") {
228
222
  // anonymize: insertedById=null, FileRef + binary bleiben. Use-case: shared
229
223
  // chat-Attachment im Multi-User-Channel — Author-ID raus, Datei bleibt sichtbar.
230
- await severPersonLink(tdb, systemUser, rows);
224
+ await severPersonLink(ctx.db, systemUser, rows);
231
225
  // skip: anonymize is complete — the hard-delete path below runs only for strategy "delete".
232
226
  return;
233
227
  }
@@ -259,6 +253,6 @@ export const fileRefDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
259
253
  for (const row of rows) {
260
254
  const id = row["id"]; // @cast-boundary db-row
261
255
  if (typeof id !== "string") continue;
262
- assertErased(await crud.forget({ id }, systemUser, tdb), "fileRef", id);
256
+ assertErased(await crud.forget({ id }, systemUser, ctx.db), "fileRef", id);
263
257
  }
264
258
  };
@@ -1,4 +1,3 @@
1
- import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { inAppMessagesTable } from "../../channel-in-app";
4
3
  import { featureMounted } from "./feature-mounted";
@@ -14,7 +13,7 @@ import { featureMounted } from "./feature-mounted";
14
13
 
15
14
  export const inAppMessageExportHook: UserDataExportHook = async (ctx) => {
16
15
  if (!featureMounted(ctx, "channel-in-app")) return null;
17
- const rows = await selectMany<Record<string, unknown>>(ctx.db, inAppMessagesTable, {
16
+ const rows = await ctx.db.selectMany(inAppMessagesTable, {
18
17
  userId: ctx.userId,
19
18
  tenantId: ctx.tenantId,
20
19
  });
@@ -34,5 +33,5 @@ export const inAppMessageExportHook: UserDataExportHook = async (ctx) => {
34
33
  export const inAppMessageDeleteHook: UserDataDeleteHook = async (ctx) => {
35
34
  // skip: channel-in-app not mounted — its table doesn't exist, nothing to erase.
36
35
  if (!featureMounted(ctx, "channel-in-app")) return;
37
- await deleteMany(ctx.db, inAppMessagesTable, { userId: ctx.userId, tenantId: ctx.tenantId });
36
+ await ctx.db.deleteMany(inAppMessagesTable, { userId: ctx.userId, tenantId: ctx.tenantId });
38
37
  };
@@ -1,4 +1,3 @@
1
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
1
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
3
2
  import { jobRunsTable } from "../../jobs";
4
3
  import { featureMounted } from "./feature-mounted";
@@ -11,7 +10,7 @@ import { featureMounted } from "./feature-mounted";
11
10
 
12
11
  export const jobRunExportHook: UserDataExportHook = async (ctx) => {
13
12
  if (!featureMounted(ctx, "jobs")) return null;
14
- const rows = await selectMany<Record<string, unknown>>(ctx.db, jobRunsTable, {
13
+ const rows = await ctx.db.selectMany<Record<string, unknown>>(jobRunsTable, {
15
14
  triggeredById: ctx.userId,
16
15
  });
17
16
  if (rows.length === 0) return null;