@cosmicdrift/kumiko-bundled-features 0.105.2 → 0.109.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 (116) 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 +205 -0
  48. package/src/personal-access-tokens/__tests__/scopes.test.ts +42 -0
  49. package/src/personal-access-tokens/constants.ts +33 -0
  50. package/src/personal-access-tokens/feature.ts +78 -0
  51. package/src/personal-access-tokens/handlers/available-scopes.query.ts +21 -0
  52. package/src/personal-access-tokens/handlers/create.write.ts +41 -0
  53. package/src/personal-access-tokens/handlers/list.query.ts +47 -0
  54. package/src/personal-access-tokens/handlers/revoke.write.ts +37 -0
  55. package/src/personal-access-tokens/hash.ts +17 -0
  56. package/src/personal-access-tokens/index.ts +45 -0
  57. package/src/personal-access-tokens/resolver.ts +58 -0
  58. package/src/personal-access-tokens/roles.ts +29 -0
  59. package/src/personal-access-tokens/schema/api-token.ts +56 -0
  60. package/src/personal-access-tokens/scopes.ts +43 -0
  61. package/src/personal-access-tokens/web/client-plugin.tsx +29 -0
  62. package/src/personal-access-tokens/web/i18n.ts +69 -0
  63. package/src/personal-access-tokens/web/index.ts +7 -0
  64. package/src/personal-access-tokens/web/pat-tokens-screen.tsx +251 -0
  65. package/src/renderer-simple/__tests__/template-resolver.integration.test.ts +2 -2
  66. package/src/sessions/__tests__/cleanup.integration.test.ts +3 -3
  67. package/src/sessions/__tests__/rebuild-survival.integration.test.ts +5 -9
  68. package/src/sessions/__tests__/sessions.integration.test.ts +14 -9
  69. package/src/sessions/schema/user-session.ts +8 -2
  70. package/src/sessions/session-callbacks.ts +9 -3
  71. package/src/subscription-stripe/__tests__/runtime.test.ts +14 -24
  72. package/src/tags/__tests__/tags.integration.test.ts +15 -0
  73. package/src/tags/handlers/assign-tag.write.ts +13 -3
  74. package/src/tags/schemas.ts +10 -4
  75. package/src/template-resolver/__tests__/template-resolver.integration.test.ts +2 -2
  76. package/src/template-resolver/testing.ts +4 -3
  77. package/src/tier-engine/feature.ts +22 -10
  78. package/src/tier-engine/handlers/set-tenant-tier.write.ts +11 -1
  79. package/src/user/__tests__/stream-tenant-backfill.integration.test.ts +186 -0
  80. package/src/user/db/queries/stream-tenant-backfill.ts +112 -0
  81. package/src/user/index.ts +4 -0
  82. package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +4 -4
  83. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +4 -4
  84. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +4 -4
  85. package/src/user-data-rights/__tests__/download.integration.test.ts +7 -7
  86. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +5 -10
  87. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +84 -0
  88. package/src/user-data-rights/__tests__/file-retention.integration.test.ts +3 -3
  89. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +3 -2
  90. package/src/user-data-rights/__tests__/forget-test-helpers.ts +3 -2
  91. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +3 -3
  92. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +10 -6
  93. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +4 -4
  94. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +4 -4
  95. package/src/user-data-rights/__tests__/request-export.integration.test.ts +4 -9
  96. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +7 -3
  97. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +8 -4
  98. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +12 -17
  99. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +9 -5
  100. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +3 -3
  101. package/src/user-data-rights/handlers/download-attempt-list.query.ts +1 -1
  102. package/src/user-data-rights/handlers/download-by-job.query.ts +4 -6
  103. package/src/user-data-rights/handlers/export-job-detail.query.ts +1 -0
  104. package/src/user-data-rights/handlers/export-job-list.query.ts +1 -0
  105. package/src/user-data-rights/lib/update-user-lifecycle.ts +8 -24
  106. package/src/user-data-rights/run-forget-cleanup.ts +8 -1
  107. package/src/user-data-rights/web/__tests__/public-deletion-gate.test.tsx +7 -1
  108. package/src/user-data-rights/web/i18n.ts +0 -10
  109. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +3 -2
  110. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +102 -54
  111. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +53 -15
  112. package/src/user-profile/__tests__/profile-screen.test.tsx +9 -0
  113. package/src/user-profile/handlers/change-email.write.ts +11 -8
  114. package/src/user-profile/web/profile-screen.tsx +10 -8
  115. package/src/auth-email-password/stream-tenant.ts +0 -31
  116. package/src/user-data-rights-defaults/db/queries/user-hook.ts +0 -17
@@ -0,0 +1,84 @@
1
+ // export-job-list/detail :: SystemAdmin cross-tenant visibility.
2
+ //
3
+ // Regression: the changeset describes these as "a platform-operator
4
+ // inspector over the GDPR read-models" (i.e. cross-tenant), but the handlers
5
+ // registered no crossTenant/systemScope override — a SystemAdmin acting in
6
+ // tenant A saw only tenant A's export jobs, never tenant B's.
7
+
8
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
10
+ import {
11
+ createTestUser,
12
+ setupTestStack,
13
+ type TestStack,
14
+ testTenantId,
15
+ unsafeCreateEntityTable,
16
+ } from "@cosmicdrift/kumiko-framework/stack";
17
+ import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
18
+ import { createComplianceProfilesFeature } from "../../compliance-profiles";
19
+ import { createDataRetentionFeature } from "../../data-retention";
20
+ import { createSessionsFeature } from "../../sessions";
21
+ import { createUserFeature } from "../../user";
22
+ import { createUserDataRightsFeature } from "../feature";
23
+ import { exportJobEntity, exportJobsTable } from "../schema/export-job";
24
+
25
+ const REQUEST_EXPORT = "user-data-rights:write:request-export";
26
+ const EXPORT_JOB_LIST = "user-data-rights:query:export-job:list";
27
+ const EXPORT_JOB_DETAIL = "user-data-rights:query:export-job:detail";
28
+
29
+ let stack: TestStack;
30
+
31
+ const tenantA = testTenantId(1);
32
+ const tenantB = testTenantId(2);
33
+ const aliceInA = createTestUser({ id: 42, tenantId: tenantA, roles: ["Member"] });
34
+ const bobInB = createTestUser({ id: 43, tenantId: tenantB, roles: ["Member"] });
35
+ const sysadminInA = createTestUser({ id: 1, tenantId: tenantA, roles: ["SystemAdmin"] });
36
+
37
+ beforeAll(async () => {
38
+ stack = await setupTestStack({
39
+ features: [
40
+ createUserFeature(),
41
+ createDataRetentionFeature(),
42
+ createComplianceProfilesFeature(),
43
+ createSessionsFeature(),
44
+ createUserDataRightsFeature(),
45
+ ],
46
+ });
47
+ await unsafeCreateEntityTable(stack.db, exportJobEntity);
48
+ await createEventsTable(stack.db);
49
+ });
50
+
51
+ afterAll(async () => {
52
+ await stack.cleanup();
53
+ });
54
+
55
+ beforeEach(async () => {
56
+ await resetTestTables(stack.db, [exportJobsTable, eventsTable]);
57
+ });
58
+
59
+ describe("export-job list/detail :: SystemAdmin sees every tenant's jobs", () => {
60
+ test("list includes jobs from a tenant the SystemAdmin isn't acting in", async () => {
61
+ const aJob = await stack.http.writeOk<{ jobId: string }>(REQUEST_EXPORT, {}, aliceInA);
62
+ const bJob = await stack.http.writeOk<{ jobId: string }>(REQUEST_EXPORT, {}, bobInB);
63
+
64
+ const result = await stack.http.queryOk<{ rows: Array<{ id: string }> }>(
65
+ EXPORT_JOB_LIST,
66
+ {},
67
+ sysadminInA,
68
+ );
69
+ const ids = result.rows.map((r) => r.id);
70
+ expect(ids).toContain(aJob.jobId);
71
+ expect(ids).toContain(bJob.jobId);
72
+ });
73
+
74
+ test("detail resolves a job that belongs to a different tenant than the caller", async () => {
75
+ const bJob = await stack.http.writeOk<{ jobId: string }>(REQUEST_EXPORT, {}, bobInB);
76
+
77
+ const detail = await stack.http.queryOk<{ id: string } | null>(
78
+ EXPORT_JOB_DETAIL,
79
+ { id: bJob.jobId },
80
+ sysadminInA,
81
+ );
82
+ expect(detail?.id).toBe(bJob.jobId);
83
+ });
84
+ });
@@ -16,7 +16,7 @@
16
16
  // die Policy entscheidet pro Tenant.
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
20
  import {
21
21
  createEventStoreExecutor,
22
22
  createTenantDb,
@@ -30,7 +30,7 @@ import {
30
30
  unsafeCreateEntityTable,
31
31
  unsafePushTables,
32
32
  } from "@cosmicdrift/kumiko-framework/stack";
33
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
33
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
34
34
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
35
35
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
36
36
  import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
@@ -117,7 +117,7 @@ beforeEach(async () => {
117
117
  });
118
118
 
119
119
  async function seedForgetUser(id: string): Promise<void> {
120
- await insertOne(db, userTable, {
120
+ await seedRow(db, userTable, {
121
121
  id,
122
122
  tenantId: TENANT_SYSTEM,
123
123
  email: `user-${id}@example.com`,
@@ -15,7 +15,7 @@
15
15
  // (getExtensionUsages + direct call) and is structurally blind to ordering.
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 { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
20
20
  import {
21
21
  createEntity,
@@ -32,6 +32,7 @@ import {
32
32
  type TestStack,
33
33
  unsafeCreateEntityTable,
34
34
  } from "@cosmicdrift/kumiko-framework/stack";
35
+ import { seedRow as seedProjectionRow } from "@cosmicdrift/kumiko-framework/testing";
35
36
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
36
37
  import { fieldDefinitionEntity } from "../../custom-fields/entity";
37
38
  import { createCustomFieldsFeature } from "../../custom-fields/feature";
@@ -181,7 +182,7 @@ async function defineSensitiveField(entityName: string, fieldKey: string, sensit
181
182
  }
182
183
 
183
184
  async function seedAnonymizeOverride(entityName: string) {
184
- await insertOne(stack.db, tenantRetentionOverrideTable, {
185
+ await seedProjectionRow(stack.db, tenantRetentionOverrideTable, {
185
186
  entityName,
186
187
  config: JSON.stringify({ keepFor: "0d", strategy: "anonymize" }),
187
188
  reason: "test: force anonymize strategy",
@@ -4,10 +4,11 @@
4
4
  // DDL and seed logic in one place so a file_refs/user-schema change updates
5
5
  // both at once instead of drifting.
6
6
 
7
- import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
7
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
8
8
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
9
9
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
11
+ import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
11
12
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
12
13
  import { USER_STATUS, userTable } from "../../user";
13
14
 
@@ -66,7 +67,7 @@ export interface ForgetSeeders {
66
67
  export function createForgetSeeders(db: DbConnection, writer: FileWriter): ForgetSeeders {
67
68
  return {
68
69
  async seedForgetUser(id) {
69
- await insertOne(db, userTable, {
70
+ await seedRow(db, userTable, {
70
71
  id,
71
72
  tenantId: TENANT_SYSTEM,
72
73
  email: `user-${id}@example.com`,
@@ -10,7 +10,7 @@
10
10
  // #624: "App mountet mail-foundation+transport → GDPR-Mails ohne Callback-Code".
11
11
 
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
- import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
13
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
14
14
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
15
15
  import {
16
16
  setupTestStack,
@@ -18,7 +18,7 @@ import {
18
18
  unsafeCreateEntityTable,
19
19
  unsafePushTables,
20
20
  } from "@cosmicdrift/kumiko-framework/stack";
21
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
21
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
22
22
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
23
23
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
24
24
  import { configValuesTable, createConfigFeature, createConfigResolver } from "../../config";
@@ -99,7 +99,7 @@ beforeEach(async () => {
99
99
 
100
100
  describe("C6 default mail bridge :: forget cron sends deletion-executed without app callback", () => {
101
101
  test("registered cron + configResolver(provider=inmemory) → user deleted + mail in inbox", async () => {
102
- await insertOne(stack.db, userTable, {
102
+ await seedRow(stack.db, userTable, {
103
103
  id: USER_ID,
104
104
  tenantId: TENANT_SYSTEM,
105
105
  email: ORIGINAL_EMAIL,
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
15
15
  import { randomBytes } from "node:crypto";
16
- import { selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
16
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
17
17
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
18
18
  import {
19
19
  createRegistry,
@@ -38,7 +38,11 @@ import {
38
38
  unsafeCreateEntityTable,
39
39
  unsafePushTables,
40
40
  } from "@cosmicdrift/kumiko-framework/stack";
41
- import { createLateBoundHolder, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
41
+ import {
42
+ createLateBoundHolder,
43
+ resetTestTables,
44
+ updateRows,
45
+ } from "@cosmicdrift/kumiko-framework/testing";
42
46
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
43
47
  import { AuthHandlers } from "../../auth-email-password/constants";
44
48
  import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
@@ -218,7 +222,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
218
222
  );
219
223
 
220
224
  // Prae-Fix-Zustand simulieren: roher Write OHNE Event.
221
- await updateMany(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: created.id });
225
+ await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: created.id });
222
226
 
223
227
  // Ohne Backfill replayt der Rebuild nur user.created -> Status weggewischt.
224
228
  await rebuildProjection(USER_PROJECTION, { db: stack.db, registry });
@@ -229,7 +233,7 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
229
233
 
230
234
  // Bestand wieder in den divergenten Live-State bringen (der Rebuild hat ihn
231
235
  // auf Active gesetzt) und den Reconcile laufen lassen.
232
- await updateMany(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: created.id });
236
+ await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: created.id });
233
237
  const { backfilled, failed } = await backfillUserLifecycleEvents(stack.db);
234
238
  expect(backfilled).toBeGreaterThanOrEqual(1);
235
239
  expect(failed).toEqual([]);
@@ -254,14 +258,14 @@ describe("#494 :: read_users-Rebuild bewahrt Lifecycle-State", () => {
254
258
  { email: "healthy.rebuild@example.com", passwordHash: hash, displayName: "Healthy" },
255
259
  TestUsers.systemAdmin,
256
260
  );
257
- await updateMany(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: goodRow.id });
261
+ await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: goodRow.id });
258
262
 
259
263
  const badRow = await stack.http.writeOk<{ id: string }>(
260
264
  UserHandlers.create,
261
265
  { email: "corrupt.rebuild@example.com", passwordHash: hash, displayName: "Corrupt" },
262
266
  TestUsers.systemAdmin,
263
267
  );
264
- await updateMany(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: badRow.id });
268
+ await updateRows(stack.db, userTable, { status: USER_STATUS.Restricted }, { id: badRow.id });
265
269
  // userEntity is systemStream:true (schema/user.ts) — its event stream
266
270
  // lives on SYSTEM_TENANT_ID regardless of the creating tenant. Archiving
267
271
  // under any other tenant would be a silent no-op against assertStreamWritable.
@@ -10,7 +10,7 @@
10
10
  // hier; der Frist-Ablauf-Cleanup folgt mit S2.U5b.
11
11
 
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
13
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
14
14
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
15
15
  import {
16
16
  createTestUser,
@@ -20,7 +20,7 @@ import {
20
20
  testUserId,
21
21
  unsafeCreateEntityTable,
22
22
  } from "@cosmicdrift/kumiko-framework/stack";
23
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
23
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
24
24
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
25
25
  import {
26
26
  createComplianceProfilesFeature,
@@ -92,7 +92,7 @@ async function seedAlice(
92
92
  gracePeriodEnd: Instant | null;
93
93
  }> = {},
94
94
  ): Promise<void> {
95
- await insertOne(stack.db, userTable, {
95
+ await seedRow(stack.db, userTable, {
96
96
  id: aliceUser.id,
97
97
  tenantId: tenantA,
98
98
  email: "alice@example.com",
@@ -345,7 +345,7 @@ describe("Cross-User-Isolation", () => {
345
345
  tenantId: tenantA,
346
346
  roles: ["Member"],
347
347
  });
348
- await insertOne(stack.db, userTable, {
348
+ await seedRow(stack.db, userTable, {
349
349
  id: testUserId(43),
350
350
  tenantId: tenantA,
351
351
  email: "bob@example.com",
@@ -8,7 +8,7 @@
8
8
  // Properties — dieser Test verifiziert sie end-to-end.
9
9
 
10
10
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
11
- import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
11
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
12
12
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
13
13
  import {
14
14
  createTestUser,
@@ -17,7 +17,7 @@ import {
17
17
  testTenantId,
18
18
  unsafeCreateEntityTable,
19
19
  } from "@cosmicdrift/kumiko-framework/stack";
20
- import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
20
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
21
21
  import {
22
22
  createComplianceProfilesFeature,
23
23
  tenantComplianceProfileEntity,
@@ -85,7 +85,7 @@ beforeEach(async () => {
85
85
  });
86
86
 
87
87
  async function seedAlice(email: string = "alice@example.com"): Promise<void> {
88
- await insertOne(stack.db, userTable, {
88
+ await seedRow(stack.db, userTable, {
89
89
  id: aliceUser.id,
90
90
  tenantId: tenantA,
91
91
  email,
@@ -153,7 +153,7 @@ describe("request-deletion :: sendDeletionRequestedEmail callback", () => {
153
153
  });
154
154
 
155
155
  test("422 user_not_in_active_state → callback NICHT gefeuert", async () => {
156
- await insertOne(stack.db, userTable, {
156
+ await seedRow(stack.db, userTable, {
157
157
  id: aliceUser.id,
158
158
  tenantId: tenantA,
159
159
  email: "alice@example.com",
@@ -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
  });