@cosmicdrift/kumiko-bundled-features 0.105.2 → 0.108.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 (115) hide show
  1. package/package.json +8 -6
  2. package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +3 -2
  3. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +5 -9
  4. package/src/auth-email-password/__tests__/invite-flow.integration.test.ts +3 -2
  5. package/src/auth-email-password/__tests__/multi-roles.integration.test.ts +3 -2
  6. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +4 -3
  7. package/src/auth-email-password/handlers/change-password.write.ts +11 -11
  8. package/src/auth-email-password/handlers/confirm-token-flow.ts +33 -104
  9. package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +44 -0
  10. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +14 -1
  11. package/src/auth-email-password/web/auth-gate.tsx +3 -4
  12. package/src/auth-email-password/web/invite-accept-screen.tsx +7 -3
  13. package/src/auth-email-password/web/session.tsx +5 -1
  14. package/src/auth-email-password/web/user-menu.tsx +1 -1
  15. package/src/config/__tests__/deserialize-value.test.ts +3 -6
  16. package/src/custom-fields/__tests__/custom-fields.integration.test.ts +65 -0
  17. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +4 -3
  18. package/src/custom-fields/constants.ts +1 -0
  19. package/src/custom-fields/handlers/delete-system-field.write.ts +2 -1
  20. package/src/custom-fields/handlers/delete-tenant-field.write.ts +2 -1
  21. package/src/custom-fields/wire-for-entity.ts +104 -77
  22. package/src/data-retention/__tests__/parse-override.test.ts +11 -6
  23. package/src/data-retention/resolve-for-tenant.ts +16 -4
  24. package/src/data-retention/run-retention-cleanup.ts +72 -10
  25. package/src/delivery/attempt-log.ts +22 -10
  26. package/src/feature-toggles/__tests__/feature-toggles.integration.test.ts +4 -4
  27. package/src/folders/__tests__/folders.integration.test.ts +26 -0
  28. package/src/folders/feature.ts +4 -2
  29. package/src/folders/handlers/delete-folder.write.ts +40 -0
  30. package/src/folders/web/__tests__/folder-manager.test.tsx +24 -0
  31. package/src/folders/web/folder-manager.tsx +18 -13
  32. package/src/folders/web/i18n.ts +0 -2
  33. package/src/folders-user-data/__tests__/hooks.integration.test.ts +69 -0
  34. package/src/folders-user-data/hooks.ts +31 -8
  35. package/src/folders-user-data/index.ts +4 -4
  36. package/src/jobs/feature.ts +23 -21
  37. package/src/jobs/handlers/projection-rebuild.job.ts +14 -4
  38. package/src/ledger/__tests__/ledger.integration.test.ts +44 -0
  39. package/src/ledger/__tests__/reports.test.ts +20 -0
  40. package/src/ledger/entity.ts +1 -1
  41. package/src/ledger/handlers/confirm-schedule-period.write.ts +29 -13
  42. package/src/ledger/handlers/reverse-transaction.write.ts +44 -5
  43. package/src/ledger/recurring.ts +18 -9
  44. package/src/ledger/schemas.ts +4 -1
  45. package/src/ledger/web/index.ts +3 -2
  46. package/src/managed-pages/__tests__/branding-coerce.test.ts +2 -6
  47. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +204 -0
  48. package/src/personal-access-tokens/constants.ts +33 -0
  49. package/src/personal-access-tokens/feature.ts +78 -0
  50. package/src/personal-access-tokens/handlers/available-scopes.query.ts +15 -0
  51. package/src/personal-access-tokens/handlers/create.write.ts +41 -0
  52. package/src/personal-access-tokens/handlers/list.query.ts +47 -0
  53. package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
  54. package/src/personal-access-tokens/hash.ts +17 -0
  55. package/src/personal-access-tokens/index.ts +45 -0
  56. package/src/personal-access-tokens/resolver.ts +58 -0
  57. package/src/personal-access-tokens/roles.ts +29 -0
  58. package/src/personal-access-tokens/schema/api-token.ts +56 -0
  59. package/src/personal-access-tokens/scopes.ts +22 -0
  60. package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
  61. package/src/personal-access-tokens/web/i18n.ts +45 -0
  62. package/src/personal-access-tokens/web/index.ts +7 -0
  63. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +150 -0
  64. package/src/renderer-simple/__tests__/template-resolver.integration.test.ts +2 -2
  65. package/src/sessions/__tests__/cleanup.integration.test.ts +3 -3
  66. package/src/sessions/__tests__/rebuild-survival.integration.test.ts +5 -9
  67. package/src/sessions/__tests__/sessions.integration.test.ts +14 -9
  68. package/src/sessions/schema/user-session.ts +8 -2
  69. package/src/sessions/session-callbacks.ts +9 -3
  70. package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
  71. package/src/tags/__tests__/tags.integration.test.ts +15 -0
  72. package/src/tags/handlers/assign-tag.write.ts +13 -3
  73. package/src/tags/schemas.ts +10 -4
  74. package/src/template-resolver/__tests__/template-resolver.integration.test.ts +2 -2
  75. package/src/template-resolver/testing.ts +4 -3
  76. package/src/tier-engine/feature.ts +22 -10
  77. package/src/tier-engine/handlers/set-tenant-tier.write.ts +11 -1
  78. package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
  79. package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
  80. package/src/user/index.ts +4 -0
  81. package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +4 -4
  82. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +4 -4
  83. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -4
  84. package/src/user-data-rights/__tests__/download.integration.test.ts +7 -7
  85. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +5 -10
  86. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +84 -0
  87. package/src/user-data-rights/__tests__/file-retention.integration.test.ts +3 -3
  88. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +3 -2
  89. package/src/user-data-rights/__tests__/forget-test-helpers.ts +3 -2
  90. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +3 -3
  91. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +10 -6
  92. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +4 -4
  93. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -4
  94. package/src/user-data-rights/__tests__/request-export.integration.test.ts +4 -9
  95. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +7 -3
  96. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +8 -4
  97. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +12 -17
  98. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +9 -5
  99. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +3 -3
  100. package/src/user-data-rights/handlers/download-attempt-list.query.ts +1 -1
  101. package/src/user-data-rights/handlers/download-by-job.query.ts +4 -6
  102. package/src/user-data-rights/handlers/export-job-detail.query.ts +1 -0
  103. package/src/user-data-rights/handlers/export-job-list.query.ts +1 -0
  104. package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
  105. package/src/user-data-rights/run-forget-cleanup.ts +8 -1
  106. package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
  107. package/src/user-data-rights/web/i18n.ts +0 -10
  108. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +3 -2
  109. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +102 -54
  110. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +53 -15
  111. package/src/user-profile/__tests__/profile-screen.test.tsx +9 -0
  112. package/src/user-profile/handlers/change-email.write.ts +11 -8
  113. package/src/user-profile/web/profile-screen.tsx +10 -8
  114. package/src/auth-email-password/stream-tenant.ts +0 -31
  115. package/src/user-data-rights-defaults/db/queries/user-hook.ts +0 -17
@@ -18,12 +18,7 @@
18
18
  // hasJob=false wenn nichts da
19
19
 
20
20
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
21
- import {
22
- asRawClient,
23
- insertOne,
24
- selectMany,
25
- updateMany,
26
- } from "@cosmicdrift/kumiko-framework/bun-db";
21
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
27
22
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
28
23
  import {
29
24
  createTestUser,
@@ -32,7 +27,7 @@ import {
32
27
  testTenantId,
33
28
  unsafeCreateEntityTable,
34
29
  } from "@cosmicdrift/kumiko-framework/stack";
35
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
30
+ import { resetTestTables, seedRow, updateRows } from "@cosmicdrift/kumiko-framework/testing";
36
31
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
37
32
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
38
33
  import { createDataRetentionFeature } from "../../data-retention";
@@ -136,7 +131,7 @@ describe("request-export :: App-side-Idempotency (primaerer Pfad)", () => {
136
131
  test("Klick nach done-Job ist NEUER Job (Audit-Historie wird nicht blockiert)", async () => {
137
132
  const first = await stack.http.writeOk<RequestExportResponse>(REQUEST_EXPORT, {}, aliceUser);
138
133
  // Worker-Simulation: status auf done flippen (direct-update OK in Test)
139
- await updateMany(
134
+ await updateRows(
140
135
  stack.db,
141
136
  exportJobsTable,
142
137
  { status: EXPORT_JOB_STATUS.Done },
@@ -251,7 +246,7 @@ describe("export-status :: User-Polling", () => {
251
246
  const T = getTemporal();
252
247
  // 1. Job done in der Vergangenheit
253
248
  const oldJobId = "11111111-1111-4111-8111-111111111111";
254
- await insertOne(stack.db, exportJobsTable, {
249
+ await seedRow(stack.db, exportJobsTable, {
255
250
  id: oldJobId,
256
251
  tenantId: tenantA,
257
252
  userId: aliceUser.id,
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
14
14
  import { randomBytes } from "node:crypto";
15
- import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
15
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
16
16
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
17
17
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
18
18
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
@@ -24,7 +24,11 @@ import {
24
24
  unsafeCreateEntityTable,
25
25
  unsafePushTables,
26
26
  } from "@cosmicdrift/kumiko-framework/stack";
27
- import { createLateBoundHolder, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
27
+ import {
28
+ createLateBoundHolder,
29
+ resetTestTables,
30
+ updateRows,
31
+ } from "@cosmicdrift/kumiko-framework/testing";
28
32
  import { AuthErrors, AuthHandlers } from "../../auth-email-password/constants";
29
33
  import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
30
34
  import { hashPassword } from "../../auth-email-password/password-hashing";
@@ -117,7 +121,7 @@ async function seedAliceWithMembership(
117
121
  TestUsers.systemAdmin,
118
122
  );
119
123
  if (status !== USER_STATUS.Active) {
120
- await updateMany(stack.db, userTable, { status }, { id: created.id });
124
+ await updateRows(stack.db, userTable, { status }, { id: created.id });
121
125
  }
122
126
  await seedTenantMembership(stack.db, {
123
127
  userId: created.id,
@@ -10,8 +10,8 @@
10
10
  // jeden Export auf "failed" setzte).
11
11
 
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
- import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
14
- import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
13
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { type JobContext, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
15
15
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
16
  import {
17
17
  setupTestStack,
@@ -19,6 +19,7 @@ import {
19
19
  unsafeCreateEntityTable,
20
20
  unsafePushTables,
21
21
  } from "@cosmicdrift/kumiko-framework/stack";
22
+ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
22
23
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
23
24
  import {
24
25
  createComplianceProfilesFeature,
@@ -104,7 +105,7 @@ beforeEach(async () => {
104
105
  await raw.unsafe("DELETE FROM read_export_jobs");
105
106
  await raw.unsafe("DELETE FROM read_users");
106
107
  await raw.unsafe("DELETE FROM read_tenant_memberships");
107
- await insertOne(stack.db, userTable, {
108
+ await seedRow(stack.db, userTable, {
108
109
  id: USER_ID,
109
110
  tenantId: TENANT,
110
111
  email: "export-cron@example.test",
@@ -146,7 +147,10 @@ describe("run-export-jobs cron-context", () => {
146
147
  now: getTemporal().Now.instant(),
147
148
  };
148
149
  // Vor dem Fix wirft der Wrapper hier "ctx.config is missing".
149
- await job?.handler({}, jobCtx as never);
150
+ // jobCtx is a deliberately minimal prod-context replica (missing
151
+ // systemUser/log/triggeredBy) — double-cast at this test boundary since
152
+ // the handler under test never touches those fields.
153
+ await job?.handler({}, jobCtx as unknown as JobContext);
150
154
 
151
155
  const [row] = (await selectMany(stack.db, exportJobsTable, { id: jobId })) as Array<{
152
156
  status: string;
@@ -17,12 +17,7 @@ import { spawn } from "node:child_process";
17
17
  import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
18
18
  import { tmpdir } from "node:os";
19
19
  import { join } from "node:path";
20
- import {
21
- asRawClient,
22
- insertOne,
23
- selectMany,
24
- updateMany,
25
- } from "@cosmicdrift/kumiko-framework/bun-db";
20
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
26
21
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
27
22
  import {
28
23
  createInMemoryFileProvider,
@@ -35,7 +30,7 @@ import {
35
30
  testTenantId,
36
31
  unsafeCreateEntityTable,
37
32
  } from "@cosmicdrift/kumiko-framework/stack";
38
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
33
+ import { resetTestTables, seedRow, updateRows } from "@cosmicdrift/kumiko-framework/testing";
39
34
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
40
35
  import {
41
36
  createComplianceProfilesFeature,
@@ -112,7 +107,7 @@ beforeEach(async () => {
112
107
 
113
108
  // Atom 5: aliceUser-Row mit email seeden — Worker-Notification-Callback
114
109
  // schaut email via lookupUserEmail an.
115
- await insertOne(stack.db, userTable, {
110
+ await seedRow(stack.db, userTable, {
116
111
  id: String(aliceUser.id),
117
112
  tenantId: tenantA,
118
113
  email: "alice@example.com",
@@ -222,7 +217,7 @@ describe("runExportJobs :: stale-detection", () => {
222
217
  const jobId = await seedPendingJob();
223
218
  const T = getTemporal();
224
219
  const twoHoursAgo = T.Instant.fromEpochMilliseconds(Date.now() - 2 * 60 * 60 * 1000);
225
- await updateMany(
220
+ await updateRows(
226
221
  stack.db,
227
222
  exportJobsTable,
228
223
  { status: EXPORT_JOB_STATUS.Running, startedAt: twoHoursAgo },
@@ -250,7 +245,7 @@ describe("runExportJobs :: stale-detection", () => {
250
245
  const jobId = await seedPendingJob();
251
246
  const T = getTemporal();
252
247
  const justNow = T.Instant.fromEpochMilliseconds(Date.now() - 60 * 1000); // 1min ago
253
- await updateMany(
248
+ await updateRows(
254
249
  stack.db,
255
250
  exportJobsTable,
256
251
  { status: EXPORT_JOB_STATUS.Running, startedAt: justNow },
@@ -285,7 +280,7 @@ describe("runExportJobs :: stale-detection", () => {
285
280
  // gesetzt + ZIP geschrieben. Worker dann gecrashed (kein done-flip).
286
281
  const provider = await buildProvider(tenantA);
287
282
  await provider.write(storageKey, new Uint8Array([1, 2, 3]));
288
- await updateMany(
283
+ await updateRows(
289
284
  stack.db,
290
285
  exportJobsTable,
291
286
  {
@@ -327,7 +322,7 @@ describe("runExportJobs :: storage-cleanup", () => {
327
322
  const provider = await buildProvider(tenantA);
328
323
  await provider.write(storageKey, new Uint8Array([1, 2, 3]));
329
324
 
330
- await updateMany(
325
+ await updateRows(
331
326
  stack.db,
332
327
  exportJobsTable,
333
328
  {
@@ -366,7 +361,7 @@ describe("runExportJobs :: storage-cleanup", () => {
366
361
  const provider = await buildProvider(tenantA);
367
362
  await provider.write(storageKey, new Uint8Array([4, 5, 6]));
368
363
 
369
- await updateMany(
364
+ await updateRows(
370
365
  stack.db,
371
366
  exportJobsTable,
372
367
  {
@@ -470,7 +465,7 @@ describe("runExportJobs :: stale-detection profile-driven cutoff", () => {
470
465
  const jobId = await seedPendingJob();
471
466
  const T = getTemporal();
472
467
  const fortyFiveMinAgo = T.Instant.fromEpochMilliseconds(Date.now() - 45 * 60 * 1000);
473
- await updateMany(
468
+ await updateRows(
474
469
  stack.db,
475
470
  exportJobsTable,
476
471
  { status: EXPORT_JOB_STATUS.Running, startedAt: fortyFiveMinAgo },
@@ -661,7 +656,7 @@ describe("runExportJobs :: Atom 4a download-tokens", () => {
661
656
  // (simuliert orphan-state nach Worker-crash).
662
657
  const provider = await buildProvider(tenantA);
663
658
  await provider.write(storageKey, new Uint8Array([99, 99, 99]));
664
- await updateMany(
659
+ await updateRows(
665
660
  stack.db,
666
661
  exportJobsTable,
667
662
  {
@@ -1046,7 +1041,7 @@ describe("runExportJobs :: Atom 5 notification-callbacks", () => {
1046
1041
  // User-Row mit email=null seeden (override). Worker logged warn,
1047
1042
  // Callback wird NICHT gerufen, Worker-Run bleibt successful.
1048
1043
  await resetTestTables(stack.db, [userTable]);
1049
- await insertOne(stack.db, userTable, {
1044
+ await seedRow(stack.db, userTable, {
1050
1045
  id: String(aliceUser.id),
1051
1046
  tenantId: tenantA,
1052
1047
  email: "" as string, // empty string — lookupUserEmail returnt null
@@ -1092,7 +1087,7 @@ describe("runExportJobs :: Atom 5 notification-callbacks", () => {
1092
1087
  tenantId: tenantA,
1093
1088
  roles: ["Member"],
1094
1089
  });
1095
- await insertOne(stack.db, userTable, {
1090
+ await seedRow(stack.db, userTable, {
1096
1091
  id: String(bobUser.id),
1097
1092
  tenantId: tenantA,
1098
1093
  email: "bob@example.com",
@@ -16,7 +16,8 @@
16
16
  // der end-to-end-Default-Pfad (delete).
17
17
 
18
18
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
19
- import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
19
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
20
+ import type { JobContext } from "@cosmicdrift/kumiko-framework/engine";
20
21
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
21
22
  import {
22
23
  setupTestStack,
@@ -24,7 +25,7 @@ import {
24
25
  unsafeCreateEntityTable,
25
26
  unsafePushTables,
26
27
  } from "@cosmicdrift/kumiko-framework/stack";
27
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
28
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
28
29
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
29
30
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
30
31
  import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
@@ -123,7 +124,7 @@ async function seedUser(
123
124
  displayName?: string;
124
125
  } = {},
125
126
  ): Promise<void> {
126
- await insertOne(stack.db, userTable, {
127
+ await seedRow(stack.db, userTable, {
127
128
  id,
128
129
  tenantId: TENANT_SYSTEM,
129
130
  email: overrides.email ?? `user-${id}@example.com`,
@@ -289,7 +290,10 @@ describe("run-forget-cleanup :: registered cron (autonomous Art.17)", () => {
289
290
  await seedMembership(CRON_USER, TENANT_A);
290
291
 
291
292
  const jobCtx = { db: stack.db, registry: stack.registry };
292
- await job?.handler({}, jobCtx as never);
293
+ // jobCtx is a deliberately minimal prod-context replica (missing
294
+ // systemUser/log/triggeredBy) — double-cast at this test boundary since
295
+ // the handler under test never touches those fields.
296
+ await job?.handler({}, jobCtx as unknown as JobContext);
293
297
 
294
298
  const row = await fetchUser(CRON_USER);
295
299
  expect(row?.status).toBe(USER_STATUS.Deleted);
@@ -616,7 +620,7 @@ describe("runForgetCleanup :: sendDeletionExecutedEmail callback (Atom 5b)", ()
616
620
  // einem vorigen Run schon anonymisiert hat aber status haengen blieb,
617
621
  // oder durch external Migration). Skip schuetzt vor crashing-callback
618
622
  // mit invaliden Args.
619
- await insertOne(stack.db, userTable, {
623
+ await seedRow(stack.db, userTable, {
620
624
  id: ALICE_ID,
621
625
  tenantId: TENANT_SYSTEM,
622
626
  email: "",
@@ -15,7 +15,7 @@
15
15
  // ueber Pseudo-Tenant.
16
16
 
17
17
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
18
- import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
18
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
19
19
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
20
20
  import {
21
21
  setupTestStack,
@@ -23,7 +23,7 @@ import {
23
23
  unsafeCreateEntityTable,
24
24
  unsafePushTables,
25
25
  } from "@cosmicdrift/kumiko-framework/stack";
26
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
26
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
27
27
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
28
28
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
29
29
  import { createDataRetentionFeature } from "../../data-retention";
@@ -99,7 +99,7 @@ async function seedUser(
99
99
  id: string,
100
100
  overrides: { email?: string; displayName?: string; roles?: string } = {},
101
101
  ): Promise<void> {
102
- await insertOne(stack.db, userTable, {
102
+ await seedRow(stack.db, userTable, {
103
103
  id,
104
104
  tenantId: TENANT_SYSTEM,
105
105
  email: overrides.email ?? `user-${id}@example.com`,
@@ -7,5 +7,5 @@ import { downloadAttemptEntity } from "../schema/download-attempt";
7
7
  export const downloadAttemptListQuery = defineEntityListHandler(
8
8
  "download-attempt",
9
9
  downloadAttemptEntity,
10
- { access: { roles: access.systemAdmin } },
10
+ { access: { roles: access.systemAdmin }, crossTenant: true },
11
11
  );
@@ -63,13 +63,11 @@ export const downloadByJobQuery = defineQueryHandler({
63
63
  const userId = query.user.id;
64
64
  const jobId = query.payload.jobId;
65
65
  const tenantId = query.user.tenantId;
66
- // IP aus dem request-scoped Kontext (von requestIdMiddleware aus
67
- // x-forwarded-for befuellt) — server-trusted, anders als ein vom Client
68
- // mitgeschickter Wert. UA steht nicht im RequestContext; der Audit-Row
69
- // laesst sie null (best-effort, via requestId in den Server-Logs
70
- // cross-referenzierbar).
66
+ // IP + UA aus dem request-scoped Kontext (von requestIdMiddleware
67
+ // befuellt) — server-trusted, anders als ein vom Client mitgeschickter
68
+ // Wert (603/2).
71
69
  const auditIp = requestContext.get()?.ip ?? null;
72
- const auditUa: string | null = null;
70
+ const auditUa = requestContext.get()?.userAgent ?? null;
73
71
 
74
72
  // Step 1-2: job-lookup + cross-user-isolation
75
73
  // ctx.db.raw weil tenant-agnostisch — Alice in Tenant B sucht den
@@ -4,4 +4,5 @@ import { exportJobEntity } from "../schema/export-job";
4
4
  // Detail fetch backing the read-only export-job inspector screen.
5
5
  export const exportJobDetailQuery = defineEntityDetailHandler("export-job", exportJobEntity, {
6
6
  access: { roles: access.systemAdmin },
7
+ crossTenant: true,
7
8
  });
@@ -5,4 +5,5 @@ import { exportJobEntity } from "../schema/export-job";
5
5
  // rows are created by the user's request-export flow, never through this handler.
6
6
  export const exportJobListQuery = defineEntityListHandler("export-job", exportJobEntity, {
7
7
  access: { roles: access.systemAdmin },
8
+ crossTenant: true,
8
9
  });
@@ -1,7 +1,7 @@
1
- import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
3
  import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
4
- import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+ import { createSystemUser, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
6
6
  import { USER_STATUS, userEntity, userTable } from "../../user";
7
7
 
@@ -21,16 +21,6 @@ import { USER_STATUS, userEntity, userTable } from "../../user";
21
21
  // kann abweichen).
22
22
  const userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
23
23
 
24
- // Stream-Tenant = die framework-injizierte tenant_id der read_users-Row. Der
25
- // Reducer setzt sie aus `user.created`.tenantId, ein Rebuild rekonstruiert sie
26
- // daraus — sie IST also der Stream-Key des Aggregats. Die Row direkt zu lesen
27
- // (statt das created-Event zu joinen) deckt auch direkt-geseedete Rows ab.
28
- async function streamTenantOf(conn: DbRunner, userId: string): Promise<TenantId | null> {
29
- const row = await fetchOne<{ tenantId?: string }>(conn, userTable, { id: userId });
30
- // @cast-boundary db-row — tenant_id ist eine TenantId-shaped uuid-Spalte.
31
- return row?.tenantId ? (row.tenantId as TenantId) : null;
32
- }
33
-
34
24
  // `conn` ist ctx.db.raw (regulaere Handler) ODER die offene tx (forget-cleanup
35
25
  // Sub-Tx) — so bleibt der Event-Append atomar mit dem umgebenden Write.
36
26
  export async function updateUserLifecycle(
@@ -38,20 +28,14 @@ export async function updateUserLifecycle(
38
28
  userId: string,
39
29
  changes: Record<string, unknown>,
40
30
  ): Promise<void> {
41
- const streamTenantId = await streamTenantOf(conn, userId);
42
- if (!streamTenantId) {
43
- throw new InternalError({
44
- message: `read_users row ${userId} has no tenant_id cannot rescope lifecycle write to its stream tenant`,
45
- });
46
- }
47
-
48
- // Rescope BEIDE Achsen auf den Stream-Tenant: der db-Arg treibt loadById +
49
- // den Stream-Read, der user-Arg die Event-tenantId + Ownership. Nur beide
50
- // zusammen halten created + updated auf einem Stream.
51
- const tenantDb = createTenantDb(conn, streamTenantId, "tenant");
31
+ // user ist systemStream (#497): der Executor-Choke-Point addressiert den
32
+ // Stream immer auf SYSTEM_TENANT_ID — ein Rescope auf die row-tenant_id
33
+ // waere wirkungslos. "system"-Mode, damit loadById auch Legacy-Rows findet,
34
+ // deren tenant_id noch vor dem #762-Backfill-Rebuild steht.
35
+ const tenantDb = createTenantDb(conn, SYSTEM_TENANT_ID, "system");
52
36
  const result = await userExecutor.update(
53
37
  { id: userId, changes },
54
- createSystemUser(streamTenantId),
38
+ createSystemUser(SYSTEM_TENANT_ID),
55
39
  tenantDb,
56
40
  { skipOptimisticLock: true },
57
41
  );
@@ -410,7 +410,14 @@ async function resolveEffectiveTenantModel(
410
410
  appTenantModel: TenantUserModel,
411
411
  ): Promise<TenantUserModel> {
412
412
  if (appTenantModel !== "single-user") return "multi-user";
413
- const members = await selectMany<{ userId: string }>(db, tenantMembershipsTable, { tenantId });
413
+ // Only "exactly one member vs. more than one" matters here — a LIMIT 2
414
+ // read answers that without pulling every historical membership row.
415
+ const members = await selectMany<{ userId: string }>(
416
+ db,
417
+ tenantMembershipsTable,
418
+ { tenantId },
419
+ { limit: 2 },
420
+ );
414
421
  return members.length === 1 ? "single-user" : "multi-user";
415
422
  }
416
423
 
@@ -5,7 +5,7 @@
5
5
  // Provider-Wrapper lokal (Dependency-Richtung renderer-web → bundled-features
6
6
  // verbietet test-utils-Import).
7
7
 
8
- import { describe, expect, test } from "bun:test";
8
+ import { afterEach, describe, expect, test } from "bun:test";
9
9
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
10
10
  import {
11
11
  createStaticLocaleResolver,
@@ -27,6 +27,12 @@ const stubDispatcher = {
27
27
 
28
28
  const ROUTES = { requestPath: "/account/delete", confirmPath: "/account/delete/confirm" };
29
29
 
30
+ // renderGate mutates the shared happy-dom document's history (570/1) — reset
31
+ // so a later, unrelated test file doesn't inherit this suite's last path.
32
+ afterEach(() => {
33
+ window.history.replaceState({}, "", "/");
34
+ });
35
+
30
36
  function renderGate(path: string, gate: ReactNode): ReturnType<typeof within> {
31
37
  window.history.replaceState({}, "", path);
32
38
  const { container } = render(
@@ -74,11 +74,6 @@ export const defaultTranslations: TranslationsByLocale = {
74
74
  "userDataRights.privacyCenter.deletion.dialogTitle": "Konto wirklich löschen?",
75
75
  "userDataRights.privacyCenter.deletion.dialogDescription":
76
76
  "Mit dem Bestätigen startet die Lösch-Frist. Du kannst die Löschung bis zu ihrem Ablauf wieder abbrechen.",
77
-
78
- "userDataRights.privacyCenter.audit.title": "Aktivitätsprotokoll (Art. 15)",
79
- "userDataRights.privacyCenter.audit.intro":
80
- "Die letzten Ereignisse zu deinem Konto über alle Tenants hinweg.",
81
- "userDataRights.privacyCenter.audit.empty": "Noch keine Ereignisse.",
82
77
  },
83
78
  en: {
84
79
  "userDataRights.deletion.request.title": "Request account deletion",
@@ -146,10 +141,5 @@ export const defaultTranslations: TranslationsByLocale = {
146
141
  "userDataRights.privacyCenter.deletion.dialogTitle": "Delete your account?",
147
142
  "userDataRights.privacyCenter.deletion.dialogDescription":
148
143
  "Confirming starts the deletion grace period. You can cancel the deletion until it ends.",
149
-
150
- "userDataRights.privacyCenter.audit.title": "Activity log (Art. 15)",
151
- "userDataRights.privacyCenter.audit.intro":
152
- "The most recent events on your account across all tenants.",
153
- "userDataRights.privacyCenter.audit.empty": "No events yet.",
154
144
  },
155
145
  };
@@ -11,7 +11,7 @@
11
11
  // fileRef-Forget beruehrt Tenant B's Files nicht)
12
12
 
13
13
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
- import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
15
15
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
16
16
  import {
17
17
  setupTestStack,
@@ -19,6 +19,7 @@ import {
19
19
  unsafeCreateEntityTable,
20
20
  unsafePushTables,
21
21
  } from "@cosmicdrift/kumiko-framework/stack";
22
+ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
22
23
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
23
24
  import { createDataRetentionFeature } from "../../data-retention";
24
25
  import { createFilesFeature } from "../../files";
@@ -80,7 +81,7 @@ async function seedUser(id: string, overrides: Record<string, unknown> = {}): Pr
80
81
  // Schema hat tenant_id-Spalte automatisch (Framework-Default).
81
82
  // Pragmatisch: SYSTEM_TENANT_ID fuer User-Rows in Tests.
82
83
  const SYSTEM_TENANT = "00000000-0000-4000-8000-000000000001";
83
- await insertOne(stack.db, userTable, {
84
+ await seedRow(stack.db, userTable, {
84
85
  id,
85
86
  tenantId: SYSTEM_TENANT,
86
87
  email: `user-${id}@example.com`,