@cosmicdrift/kumiko-bundled-features 0.158.2 → 0.159.1

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 (149) hide show
  1. package/package.json +7 -6
  2. package/src/auth-email-password/__tests__/account-unlock.integration.test.ts +283 -0
  3. package/src/auth-email-password/__tests__/auth-user-row.test.ts +36 -0
  4. package/src/auth-email-password/__tests__/auth.integration.test.ts +21 -0
  5. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +1 -1
  6. package/src/auth-email-password/__tests__/password-policy.test.ts +15 -0
  7. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +15 -1
  8. package/src/auth-email-password/__tests__/session-strict-mode.integration.test.ts +7 -14
  9. package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +12 -0
  10. package/src/auth-email-password/constants.ts +15 -0
  11. package/src/auth-email-password/email-templates.ts +32 -0
  12. package/src/auth-email-password/errors.ts +9 -0
  13. package/src/auth-email-password/feature.ts +37 -5
  14. package/src/auth-email-password/handlers/change-password.write.ts +2 -1
  15. package/src/auth-email-password/handlers/confirm-account-unlock.write.ts +59 -0
  16. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +1 -0
  17. package/src/auth-email-password/handlers/invite-accept.write.ts +6 -2
  18. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +1 -0
  19. package/src/auth-email-password/handlers/request-account-unlock.write.ts +38 -0
  20. package/src/auth-email-password/handlers/reset-password.write.ts +2 -1
  21. package/src/auth-email-password/handlers/signup-confirm.write.ts +2 -1
  22. package/src/auth-email-password/i18n.ts +42 -0
  23. package/src/auth-email-password/index.ts +2 -0
  24. package/src/auth-email-password/lockout-store.ts +10 -5
  25. package/src/auth-email-password/password-policy.ts +2108 -0
  26. package/src/auth-email-password/signed-token.ts +1 -0
  27. package/src/auth-email-password/unlock-token.ts +28 -0
  28. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +65 -3
  29. package/src/auth-email-password/web/auth-client.ts +33 -0
  30. package/src/auth-email-password/web/confirm-account-unlock-screen.tsx +95 -0
  31. package/src/auth-email-password/web/index.ts +6 -0
  32. package/src/auth-email-password/web/login-screen.tsx +15 -0
  33. package/src/auth-email-password/web/request-account-unlock-screen.tsx +112 -0
  34. package/src/auth-foundation/__tests__/feature.test.ts +33 -0
  35. package/src/auth-foundation/__tests__/session-store.test.ts +78 -0
  36. package/src/auth-foundation/__tests__/token-verifier.test.ts +117 -0
  37. package/src/auth-foundation/boot-checks.ts +91 -0
  38. package/src/auth-foundation/feature.ts +103 -0
  39. package/src/auth-foundation/index.ts +23 -0
  40. package/src/auth-foundation/types.ts +107 -0
  41. package/src/auth-mfa/__tests__/errors-i18n-parity.test.ts +8 -0
  42. package/src/auth-mfa/__tests__/verify.integration.test.ts +20 -0
  43. package/src/auth-mfa/web/__tests__/mfa-client.test.ts +118 -0
  44. package/src/auth-mfa/web/__tests__/mfa-enable-screen.test.tsx +173 -0
  45. package/src/auth-mfa/web/__tests__/mfa-verify-screen.test.tsx +100 -0
  46. package/src/billing-foundation/__tests__/subscription-provider-contract.ts +69 -0
  47. package/src/channel-email/pii-guard.ts +6 -3
  48. package/src/config/__tests__/config.integration.test.ts +14 -0
  49. package/src/config/__tests__/pii-encrypted.integration.test.ts +3 -2
  50. package/src/config/__tests__/reencrypt-job-concurrent-write-race.integration.test.ts +20 -3
  51. package/src/config/__tests__/reencrypt-job-kek-rotation.integration.test.ts +2 -3
  52. package/src/config/feature.ts +2 -4
  53. package/src/config/handlers/set.write.ts +7 -1
  54. package/src/config/resolver.ts +1 -1
  55. package/src/custom-fields/__tests__/user-data-rights.integration.test.ts +2 -0
  56. package/src/delivery/handlers/log.query.ts +1 -1
  57. package/src/files-provider-s3/__tests__/s3-provider.integration.test.ts +7 -0
  58. package/src/folders/web/__tests__/folder-manager.test.tsx +27 -2
  59. package/src/inbound-mail-foundation/__tests__/account-state.test.ts +30 -0
  60. package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +57 -6
  61. package/src/inbound-mail-foundation/__tests__/inbound-mail-provider-contract.ts +93 -0
  62. package/src/inbound-mail-foundation/handlers/ingest-message.write.ts +60 -50
  63. package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +12 -0
  64. package/src/inbound-provider-inmemory/__tests__/feature.test.ts +48 -0
  65. package/src/inbound-provider-inmemory/feature.ts +2 -2
  66. package/src/jobs/__tests__/reindex-entity-job.integration.test.ts +53 -1
  67. package/src/jobs/handlers/detail.query.ts +1 -1
  68. package/src/jobs/handlers/list.query.ts +1 -1
  69. package/src/mail-foundation/__tests__/mail-transport-contract.ts +42 -0
  70. package/src/mail-transport-inmemory/__tests__/feature.test.ts +19 -0
  71. package/src/mail-transport-smtp/__tests__/feature.test.ts +60 -0
  72. package/src/managed-pages/handlers/branding.query.ts +5 -1
  73. package/src/personal-access-tokens/__tests__/feature.test.ts +18 -0
  74. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +8 -11
  75. package/src/personal-access-tokens/feature.ts +22 -7
  76. package/src/personal-access-tokens/handlers/list.query.ts +1 -1
  77. package/src/personal-access-tokens/index.ts +0 -14
  78. package/src/personal-access-tokens/resolver.ts +2 -2
  79. package/src/secrets/__tests__/rotate.integration.test.ts +31 -0
  80. package/src/sessions/__tests__/cleanup.integration.test.ts +2 -1
  81. package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +5 -3
  82. package/src/sessions/__tests__/rebuild-survival.integration.test.ts +11 -2
  83. package/src/sessions/__tests__/session-store-registration.test.ts +21 -0
  84. package/src/sessions/__tests__/sessions-screens.boot.test.ts +10 -1
  85. package/src/sessions/__tests__/sessions.integration.test.ts +28 -17
  86. package/src/sessions/__tests__/test-helpers.ts +15 -2
  87. package/src/sessions/feature.ts +31 -3
  88. package/src/sessions/handlers/detail.query.ts +2 -2
  89. package/src/sessions/handlers/list.query.ts +4 -2
  90. package/src/sessions/handlers/mine.query.ts +4 -2
  91. package/src/sessions/testing.ts +14 -1
  92. package/src/shared/__tests__/chunked-entity-migration.test.ts +145 -0
  93. package/src/shared/decrypt-stored-pii.ts +12 -5
  94. package/src/subscription-mollie/__tests__/feature.test.ts +87 -0
  95. package/src/subscription-stripe/__tests__/feature.test.ts +120 -1
  96. package/src/tenant/__tests__/members-query-kms.integration.test.ts +98 -0
  97. package/src/tenant/handlers/invitations.query.ts +4 -2
  98. package/src/tenant/handlers/members.query.ts +2 -2
  99. package/src/tenant/seeding.ts +2 -2
  100. package/src/tenant-lifecycle/__tests__/tenant-lifecycle.integration.test.ts +2 -0
  101. package/src/tenant-lifecycle/boot-checks.ts +53 -0
  102. package/src/tenant-lifecycle/feature.ts +7 -0
  103. package/src/tenant-lifecycle/handlers/request-destruction.write.ts +1 -1
  104. package/src/tier-engine/feature.ts +2 -2
  105. package/src/user-data-rights/COMPLIANCE.md +20 -0
  106. package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +2 -0
  107. package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +3 -0
  108. package/src/user-data-rights/__tests__/audit-log.integration.test.ts +2 -0
  109. package/src/user-data-rights/__tests__/boot-checks.test.ts +172 -0
  110. package/src/user-data-rights/__tests__/cross-data-matrix.integration.test.ts +2 -0
  111. package/src/user-data-rights/__tests__/download-by-token-resolver-trust.integration.test.ts +2 -0
  112. package/src/user-data-rights/__tests__/download.integration.test.ts +59 -4
  113. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +2 -0
  114. package/src/user-data-rights/__tests__/export-job-idempotency.integration.test.ts +2 -0
  115. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +2 -0
  116. package/src/user-data-rights/__tests__/file-binary-forget-cleanup.integration.test.ts +2 -0
  117. package/src/user-data-rights/__tests__/file-binary-forget-failure.integration.test.ts +2 -0
  118. package/src/user-data-rights/__tests__/file-retention.integration.test.ts +2 -0
  119. package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +2 -0
  120. package/src/user-data-rights/__tests__/forget-cleanup-hook-ordering.integration.test.ts +2 -0
  121. package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +2 -0
  122. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +10 -1
  123. package/src/user-data-rights/__tests__/mail-default-bridge.integration.test.ts +2 -0
  124. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +15 -7
  125. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.test.ts +2 -0
  126. package/src/user-data-rights/__tests__/request-deletion-callback.integration.test.ts +2 -0
  127. package/src/user-data-rights/__tests__/request-export.integration.test.ts +2 -0
  128. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +48 -15
  129. package/src/user-data-rights/__tests__/run-export-jobs-cron-context.integration.test.ts +2 -0
  130. package/src/user-data-rights/__tests__/run-export-jobs.integration.test.ts +4 -1
  131. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.test.ts +2 -0
  132. package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +2 -0
  133. package/src/user-data-rights/__tests__/tenant-model-erasure.integration.test.ts +2 -0
  134. package/src/user-data-rights/__tests__/user-data-rights.integration.test.ts +9 -1
  135. package/src/user-data-rights/boot-checks.ts +86 -0
  136. package/src/user-data-rights/feature.ts +116 -6
  137. package/src/user-data-rights/handlers/deletion-grace-period.ts +1 -1
  138. package/src/user-data-rights/handlers/download-by-token.query.ts +1 -1
  139. package/src/user-data-rights/handlers/lift-restriction.write.ts +13 -19
  140. package/src/user-data-rights/handlers/restrict-account.write.ts +30 -8
  141. package/src/user-data-rights/run-export-jobs.ts +3 -3
  142. package/src/user-data-rights/run-forget-cleanup.ts +1 -1
  143. package/src/user-data-rights-defaults/__tests__/bundled-entities.userdata-hooks.integration.test.ts +3 -0
  144. package/src/user-data-rights-defaults/__tests__/feature-mounted.test.ts +24 -0
  145. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +2 -0
  146. package/src/user-data-rights-defaults/hooks/tenant-invitation.userdata-hook.ts +1 -1
  147. package/src/user-profile/__tests__/change-email.integration.test.ts +2 -0
  148. package/src/user-profile/handlers/change-email.write.ts +1 -1
  149. package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +0 -179
@@ -62,7 +62,7 @@ import {
62
62
  type TierResolverPlugin,
63
63
  type TrialGate,
64
64
  } from "@cosmicdrift/kumiko-framework/engine";
65
- import { getAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
65
+ import { getUnscopedAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
66
66
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
67
67
  import { z } from "zod";
68
68
  import { tenantTable } from "../tenant";
@@ -365,7 +365,7 @@ export function createTierEngineFeature<
365
365
 
366
366
  // Idempotency: stream-existence-check vor create. Pattern aus
367
367
  // seedTenant.ts. Bei re-replay (rebuild) nicht versionsbumpen.
368
- const streamVersion = await getAggregateStreamMaxVersion(rawDb, aggregateId);
368
+ const streamVersion = await getUnscopedAggregateStreamMaxVersion(rawDb, aggregateId);
369
369
  // skip: idempotent — tier-assignment stream already seeded (rebuild/replay).
370
370
  if (streamVersion > 0) return;
371
371
 
@@ -45,6 +45,26 @@ Dieses Dokument bündelt die **technischen Fakten** zur DSGVO-Pipeline
45
45
  | `read_tenant_retention_overrides` | Per-Tenant Retention-Override pro Entity | nein | unbounded (Konfiguration) | Aufbewahrungspflicht-Edge-Cases |
46
46
  | Storage-Provider (Local / S3) | Export-ZIPs + File-Binaries | indirekt (Inhalt) | Local: per `exportDownloadTtl` Cleanup. S3: lifecycle-Policy (App-Author-Verantwortung) | Daten-Export-Auslieferung |
47
47
 
48
+ ### Bekanntes Residuum: `createdBy` / `metadata.userId` in `kumiko_events`
49
+
50
+ Nach der Forget-Pipeline (Art. 17, Abschnitt 1) bleiben `kumiko_events.created_by`
51
+ und `metadata.userId` (Event-Store-Schema `events-schema.ts`, EventMetadata-Feld)
52
+ als Klartext-UUID stehen. Die Forget-Hooks aus `user-data-rights-defaults` fassen
53
+ `kumiko_events` nicht an — `createdBy`/`metadata.userId` sind keine über
54
+ `defineEvent(..., { piiFields })` deklarierten Payload-PII-Felder (siehe
55
+ `packages/framework/src/crypto/event-pii.ts`) und laufen deshalb nicht durch die
56
+ Payload-Verschlüsselung. Löschung dieser Rows erfolgt ausschließlich über die
57
+ zeitbasierte `data-retention`-Policy (`keepFor`, s. `kumiko_events`-Zeile in Abschnitt
58
+ 2 oben), unabhängig vom User-Forget-Antrag.
59
+
60
+ Technische Basis: Der `read_users`-Row zur betroffenen UUID wird beim Forget-Run
61
+ in-place anonymisiert, nicht hard-gelöscht (Sentinel-Pattern, siehe Abschnitt 4
62
+ „Integrität"). Email, Displayname und Passwort-Hash werden dabei entfernt/ersetzt —
63
+ das ist der Schlüssel, der die Verknüpfung kappt: Sobald diese Felder weg sind, gibt
64
+ es keinen Rückweg von der UUID in `created_by` zu einer realen Identität mehr. Die
65
+ UUID bleibt als stabiler, aber orphaned Pseudonym-Wert bestehen, bis die
66
+ Retention-Policy irgendwann das Event selbst löscht.
67
+
48
68
  ---
49
69
 
50
70
  ## 3. Compliance-Profile
@@ -4,6 +4,7 @@
4
4
  // KLARTEXT schicken (vorher: Ciphertext-Adresse → Mail unzustellbar).
5
5
 
6
6
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
7
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
7
8
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
8
9
  import {
9
10
  configureBlindIndexKey,
@@ -58,6 +59,7 @@ beforeAll(async () => {
58
59
  createUserFeature(),
59
60
  createDataRetentionFeature(),
60
61
  createComplianceProfilesFeature(),
62
+ authFoundationFeature,
61
63
  createSessionsFeature(),
62
64
  createUserDataRightsFeature({
63
65
  deletionTokenSecret: DELETION_SECRET,
@@ -6,6 +6,7 @@
6
6
  // Beweist end-to-end via echte /api/write-Calls OHNE Auth (anonymousAccess).
7
7
 
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
9
10
  import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
10
11
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
11
12
  import {
@@ -53,6 +54,7 @@ beforeAll(async () => {
53
54
  createUserFeature(),
54
55
  createDataRetentionFeature(),
55
56
  createComplianceProfilesFeature(),
57
+ authFoundationFeature,
56
58
  createSessionsFeature(),
57
59
  createUserDataRightsFeature({
58
60
  deletionTokenSecret: DELETION_SECRET,
@@ -331,6 +333,7 @@ describe("anonymous deletion flow — not configured (kein Secret)", () => {
331
333
  createUserFeature(),
332
334
  createDataRetentionFeature(),
333
335
  createComplianceProfilesFeature(),
336
+ authFoundationFeature,
334
337
  createSessionsFeature(),
335
338
  createUserDataRightsFeature({
336
339
  sendDeletionVerificationEmail: async (args) => {
@@ -1,6 +1,7 @@
1
1
  // S2.U7 — my-audit-log + invalid-attempt-audit + list-download-attempts.
2
2
 
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
4
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
4
5
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
5
6
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
6
7
  import {
@@ -39,6 +40,7 @@ beforeAll(async () => {
39
40
  createUserFeature(),
40
41
  createDataRetentionFeature(),
41
42
  createComplianceProfilesFeature(),
43
+ authFoundationFeature,
42
44
  createSessionsFeature(),
43
45
 
44
46
  createUserDataRightsFeature(),
@@ -0,0 +1,172 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { integer, type SchemaTable, table, uuid } from "@cosmicdrift/kumiko-framework/db";
3
+ import {
4
+ createEntity,
5
+ createTextField,
6
+ defineFeature,
7
+ EXT_TENANT_DATA,
8
+ EXT_USER_DATA,
9
+ validateBoot,
10
+ } from "@cosmicdrift/kumiko-framework/engine";
11
+ import { createFilesFeature } from "@cosmicdrift/kumiko-framework/files";
12
+ import { z } from "zod";
13
+ import { EXT_SESSION_STORE } from "../../auth-foundation";
14
+ import { createComplianceProfilesFeature } from "../../compliance-profiles/feature";
15
+ import { createConfigFeature } from "../../config/feature";
16
+ import { createDataRetentionFeature } from "../../data-retention/feature";
17
+ import { createSessionsFeature } from "../../sessions/feature";
18
+ import { createTenantFeature } from "../../tenant/feature";
19
+ import { createTenantLifecycleFeature } from "../../tenant-lifecycle/feature";
20
+ import { createUserFeature } from "../../user/feature";
21
+ import { createUserDataRightsDefaultsFeature } from "../../user-data-rights-defaults/feature";
22
+ import { createUserDataRightsFeature } from "../feature";
23
+
24
+ // GDPR-storage guards V2 (export-without-erase) and V3 (pii-entity-without-
25
+ // hook) moved off the framework-internal boot-validator onto user-data-
26
+ // rights' own `r.bootCheck()` (#1314); V4 (tenantOwned-entity-without-hook)
27
+ // onto tenant-lifecycle's own `r.bootCheck()` — tenant-lifecycle owns
28
+ // EXT_TENANT_DATA, so its own mount preserves the original guard's trigger
29
+ // exactly (gating on a sibling feature would silently narrow coverage).
30
+ // Unlike the old direct-function tests, these only run inside a real
31
+ // validateBoot() pass — exercising the actual `r.bootCheck()` plumbing, not
32
+ // just the guard logic.
33
+ // Stub instead of the real auth-foundation — these boot-checks are about
34
+ // GDPR-storage guards, not auth; only sessions' sessionStore registration
35
+ // needs an owner present.
36
+ const sessionStoreOwnerStub = defineFeature("auth-foundation", (r) => {
37
+ r.extendsRegistrar(EXT_SESSION_STORE, {});
38
+ });
39
+
40
+ function baseFeatures() {
41
+ return [
42
+ createConfigFeature(),
43
+ createUserFeature(),
44
+ sessionStoreOwnerStub,
45
+ createSessionsFeature(),
46
+ createDataRetentionFeature(),
47
+ createComplianceProfilesFeature(),
48
+ createFilesFeature(),
49
+ createUserDataRightsFeature(),
50
+ createUserDataRightsDefaultsFeature(),
51
+ ];
52
+ }
53
+
54
+ // Minimal tenant-lifecycle assembly with NO user-data-rights* mounted at all
55
+ // — proves V4 fires from tenant-lifecycle's own mount, not from a sibling
56
+ // feature accidentally re-gating it.
57
+ function minimalTenantLifecycleFeatures() {
58
+ return [
59
+ createConfigFeature(),
60
+ createTenantFeature(),
61
+ createComplianceProfilesFeature(),
62
+ createTenantLifecycleFeature(),
63
+ ];
64
+ }
65
+
66
+ describe("GDPR-storage boot guards V2-V4 (via r.bootCheck)", () => {
67
+ test("known-good assembly boot-validates (baseline)", () => {
68
+ expect(() => validateBoot(baseFeatures())).not.toThrow();
69
+ });
70
+
71
+ test("V2: export hook without delete hook → Art.17 throw", () => {
72
+ const bad = defineFeature("bad-export", (r) => {
73
+ r.requires("user-data-rights");
74
+ r.useExtension(EXT_USER_DATA, "badEntity", { export: async () => null });
75
+ });
76
+ expect(() => validateBoot([...baseFeatures(), bad])).toThrow(/Art\.17/);
77
+ });
78
+
79
+ test("V3: pii entity without any EXT_USER_DATA hook → throws the guard's own message", () => {
80
+ const bad = defineFeature("crm", (r) => {
81
+ r.entity("contact", createEntity({ fields: { email: createTextField({ pii: true }) } }));
82
+ });
83
+ expect(() => validateBoot([...baseFeatures(), bad])).toThrow(/EXT_USER_DATA hook.*Art\.17 gap/);
84
+ });
85
+
86
+ test("V3: pii field on a projection-only entity (no r.entity) is still caught", () => {
87
+ const projectionTable = table("crm_contact_summary", {
88
+ id: uuid("id").primaryKey(),
89
+ count: integer("count").notNull().default(0),
90
+ }) as unknown as SchemaTable;
91
+ const bad = defineFeature("crm", (r) => {
92
+ const contactCreated = r.defineEvent("contact-created", z.unknown());
93
+ r.projection({
94
+ name: "contact-summary",
95
+ source: "contact",
96
+ table: projectionTable,
97
+ apply: { [contactCreated.name]: async () => {} },
98
+ entity: createEntity({ fields: { email: createTextField({ pii: true }) } }),
99
+ });
100
+ });
101
+ expect(() => validateBoot([...baseFeatures(), bad])).toThrow(/EXT_USER_DATA hook.*Art\.17 gap/);
102
+ });
103
+
104
+ test("V3: projection-only pii entity WITH EXT_USER_DATA hook registered for its entity name → no throw", () => {
105
+ const projectionTable = table("crm_contact_summary_hooked", {
106
+ id: uuid("id").primaryKey(),
107
+ count: integer("count").notNull().default(0),
108
+ }) as unknown as SchemaTable;
109
+ const hooked = defineFeature("crm-hooked", (r) => {
110
+ r.requires("user-data-rights");
111
+ const contactCreated = r.defineEvent("contact-hooked-created", z.unknown());
112
+ r.projection({
113
+ name: "contact-summary",
114
+ source: "contact",
115
+ table: projectionTable,
116
+ apply: { [contactCreated.name]: async () => {} },
117
+ entity: createEntity({ fields: { email: createTextField({ pii: true }) } }),
118
+ });
119
+ r.useExtension(EXT_USER_DATA, "contact-summary", {
120
+ export: async () => null,
121
+ delete: async () => {},
122
+ });
123
+ });
124
+ expect(() => validateBoot([...baseFeatures(), hooked])).not.toThrow();
125
+ });
126
+
127
+ test("V4: tenantOwned entity without EXT_TENANT_DATA hook, tenant-lifecycle mounted → throws the guard's own message", () => {
128
+ const bad = defineFeature("billing", (r) => {
129
+ r.entity(
130
+ "subscription",
131
+ createEntity({ fields: { providerCustomerId: createTextField({ tenantOwned: true }) } }),
132
+ );
133
+ });
134
+ expect(() => validateBoot([...minimalTenantLifecycleFeatures(), bad])).toThrow(
135
+ /EXT_TENANT_DATA destroy hook/,
136
+ );
137
+ });
138
+
139
+ test("V4: fires with no user-data-rights* mounted at all — trigger is tenant-lifecycle's own mount", () => {
140
+ const bad = defineFeature("billing", (r) => {
141
+ r.entity(
142
+ "subscription",
143
+ createEntity({ fields: { providerCustomerId: createTextField({ tenantOwned: true }) } }),
144
+ );
145
+ });
146
+ const features = minimalTenantLifecycleFeatures().concat(bad);
147
+ expect(features.some((f) => f.name.startsWith("user-data-rights"))).toBe(false);
148
+ expect(() => validateBoot(features)).toThrow(/EXT_TENANT_DATA destroy hook/);
149
+ });
150
+
151
+ test("V4: tenant-lifecycle not mounted → tenantOwned entity does not throw", () => {
152
+ const notMounted = defineFeature("billing-standalone", (r) => {
153
+ r.entity(
154
+ "subscription",
155
+ createEntity({ fields: { providerCustomerId: createTextField({ tenantOwned: true }) } }),
156
+ );
157
+ });
158
+ expect(() => validateBoot([...baseFeatures(), notMounted])).not.toThrow();
159
+ });
160
+
161
+ test("V4: tenantOwned entity WITH EXT_TENANT_DATA hook registered → no throw", () => {
162
+ const hooked = defineFeature("billing-hooked", (r) => {
163
+ r.requires("tenant-lifecycle");
164
+ r.entity(
165
+ "subscription",
166
+ createEntity({ fields: { providerCustomerId: createTextField({ tenantOwned: true }) } }),
167
+ );
168
+ r.useExtension(EXT_TENANT_DATA, "subscription", { destroy: async () => {} });
169
+ });
170
+ expect(() => validateBoot([...minimalTenantLifecycleFeatures(), hooked])).not.toThrow();
171
+ });
172
+ });
@@ -13,6 +13,7 @@
13
13
  // Alices Forget; Bobs Daten landen NICHT in Alices Export-Bundle.
14
14
 
15
15
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
16
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
16
17
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
17
18
  import { defineUnmanagedTable } from "@cosmicdrift/kumiko-framework/db";
18
19
  import {
@@ -118,6 +119,7 @@ beforeAll(async () => {
118
119
  createFilesFeature(),
119
120
  createDataRetentionFeature(),
120
121
  createComplianceProfilesFeature(),
122
+ authFoundationFeature,
121
123
  createSessionsFeature(),
122
124
 
123
125
  createUserDataRightsFeature(),
@@ -12,6 +12,7 @@
12
12
  // for `jobRow.requestedFromTenantId` instead.
13
13
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14
14
  import { randomBytes } from "node:crypto";
15
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
15
16
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
16
17
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
17
18
  import {
@@ -76,6 +77,7 @@ beforeAll(async () => {
76
77
  createComplianceProfilesFeature(),
77
78
  fileFoundationFeature,
78
79
  fileProviderInMemoryFeature,
80
+ authFoundationFeature,
79
81
  createSessionsFeature(),
80
82
  createUserDataRightsFeature(),
81
83
  ],
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
11
11
  import { randomBytes } from "node:crypto";
12
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
12
13
  import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
13
14
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
14
15
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
@@ -115,6 +116,7 @@ beforeAll(async () => {
115
116
  fileFoundationFeature,
116
117
  fileProviderInMemoryFeature,
117
118
  noSignedUrlProviderFeature,
119
+ authFoundationFeature,
118
120
  createSessionsFeature(),
119
121
 
120
122
  createUserDataRightsFeature(),
@@ -464,13 +466,25 @@ describe("r.httpRoute :: /user-export/by-token (Magic-Link e2e)", () => {
464
466
  expect(body.error?.i18nKey).toBe("userDataRights.errors.download.notFound");
465
467
  });
466
468
 
467
- test("missing token query-param → 400", async () => {
469
+ test("no query token (fragment-Pfad) → serviert Interstitial-HTML statt 400", async () => {
468
470
  const res = await stack.app.fetch(
469
471
  new Request("http://test/user-export/by-token", { method: "GET" }),
470
472
  );
471
- expect(res.status).toBe(400);
472
- const body = (await res.json()) as { error?: string };
473
- expect(body.error).toBe("missing_token");
473
+ expect(res.status).toBe(200);
474
+ expect(res.headers.get("content-type")).toContain("text/html");
475
+ const body = await res.text();
476
+ expect(body).toContain("/user-export/by-token.js");
477
+ });
478
+
479
+ test("by-token.js → same-origin exchange-script, application/javascript", async () => {
480
+ const res = await stack.app.fetch(
481
+ new Request("http://test/user-export/by-token.js", { method: "GET" }),
482
+ );
483
+ expect(res.status).toBe(200);
484
+ expect(res.headers.get("content-type")).toContain("application/javascript");
485
+ const body = await res.text();
486
+ expect(body).toContain("location.hash");
487
+ expect(body).toContain("/user-export/by-token");
474
488
  });
475
489
 
476
490
  test("Audit-Update: useCount + IP/UA aus httpRoute-Headers (nicht aus payload)", async () => {
@@ -495,6 +509,47 @@ describe("r.httpRoute :: /user-export/by-token (Magic-Link e2e)", () => {
495
509
  expect(row?.lastUsedFromIp).toBe("198.51.100.7");
496
510
  expect(row?.lastUsedUserAgent).toBe("e2e-test/2.0");
497
511
  });
512
+
513
+ test("POST-Exchange (Fragment-Pfad): happy-path + IP/UA-Audit aus httpRoute-Headers", async () => {
514
+ const { jobId, plainToken } = await seedDoneJobWithToken();
515
+
516
+ const res = await stack.app.fetch(
517
+ new Request("http://test/user-export/by-token", {
518
+ method: "POST",
519
+ headers: {
520
+ "content-type": "application/json",
521
+ "user-agent": "e2e-test/3.0",
522
+ "x-forwarded-for": "203.0.113.9, 10.0.0.1",
523
+ },
524
+ body: JSON.stringify({ token: plainToken }),
525
+ }),
526
+ );
527
+ expect(res.status).toBe(200);
528
+ const body = (await res.json()) as { data?: { url?: string } };
529
+ expect(body.data?.url).toBeTruthy();
530
+
531
+ const [row] = (await selectMany(stack.db, exportDownloadTokensTable, { jobId })) as Array<{
532
+ useCount: number;
533
+ lastUsedFromIp: string | null;
534
+ lastUsedUserAgent: string | null;
535
+ }>;
536
+ expect(row?.useCount).toBe(1);
537
+ expect(row?.lastUsedFromIp).toBe("203.0.113.9");
538
+ expect(row?.lastUsedUserAgent).toBe("e2e-test/3.0");
539
+ });
540
+
541
+ test("POST-Exchange: missing token → 400", async () => {
542
+ const res = await stack.app.fetch(
543
+ new Request("http://test/user-export/by-token", {
544
+ method: "POST",
545
+ headers: { "content-type": "application/json" },
546
+ body: JSON.stringify({}),
547
+ }),
548
+ );
549
+ expect(res.status).toBe(400);
550
+ const body = (await res.json()) as { error?: string };
551
+ expect(body.error).toBe("missing_token");
552
+ });
498
553
  });
499
554
 
500
555
  describe("download-by-job :: cross-user + cross-tenant", () => {
@@ -4,6 +4,7 @@
4
4
  // (never the raw ciphertext) without one.
5
5
 
6
6
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
7
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
7
8
  import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
8
9
  import {
9
10
  buildEntityTable,
@@ -74,6 +75,7 @@ beforeAll(async () => {
74
75
  createUserFeature(),
75
76
  createDataRetentionFeature(),
76
77
  createComplianceProfilesFeature(),
78
+ authFoundationFeature,
77
79
  createSessionsFeature(),
78
80
  createUserDataRightsFeature(),
79
81
  vaultFeature,
@@ -10,6 +10,7 @@
10
10
  // reale Postgres + Drizzle-customType-Codec-Path braucht.
11
11
 
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
13
14
  import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
14
15
  import { extractPgError } from "@cosmicdrift/kumiko-framework/db";
15
16
  import {
@@ -43,6 +44,7 @@ beforeAll(async () => {
43
44
  createUserFeature(),
44
45
  createDataRetentionFeature(),
45
46
  createComplianceProfilesFeature(),
47
+ authFoundationFeature,
46
48
  createSessionsFeature(),
47
49
 
48
50
  createUserDataRightsFeature(),
@@ -6,6 +6,7 @@
6
6
  // tenant A saw only tenant A's export jobs, never tenant B's.
7
7
 
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
9
10
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
10
11
  import {
11
12
  createTestUser,
@@ -40,6 +41,7 @@ beforeAll(async () => {
40
41
  createUserFeature(),
41
42
  createDataRetentionFeature(),
42
43
  createComplianceProfilesFeature(),
44
+ authFoundationFeature,
43
45
  createSessionsFeature(),
44
46
  createUserDataRightsFeature(),
45
47
  ],
@@ -6,6 +6,7 @@
6
6
  // Datei ihre Bytes dauerhaft auf Disk (Issue gefunden im Review zu #177).
7
7
 
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
9
10
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
10
11
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
11
12
  import { type JobContext, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
@@ -78,6 +79,7 @@ beforeAll(async () => {
78
79
  createTestFileProviderFeature(provider, "test"),
79
80
  createDataRetentionFeature(),
80
81
  createComplianceProfilesFeature(),
82
+ authFoundationFeature,
81
83
  createSessionsFeature(),
82
84
  createUserDataRightsFeature(),
83
85
  createUserDataRightsDefaultsFeature(),
@@ -10,6 +10,7 @@
10
10
  // still unvollständig).
11
11
 
12
12
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
13
14
  import { asRawClient, fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
15
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
15
16
  import { SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
@@ -86,6 +87,7 @@ beforeAll(async () => {
86
87
  createTestFileProviderFeature(flakyProvider, "test"),
87
88
  createDataRetentionFeature(),
88
89
  createComplianceProfilesFeature(),
90
+ authFoundationFeature,
89
91
  createSessionsFeature(),
90
92
  createUserDataRightsFeature(),
91
93
  createUserDataRightsDefaultsFeature(),
@@ -16,6 +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 { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
19
20
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
20
21
  import {
21
22
  createEventStoreExecutor,
@@ -68,6 +69,7 @@ beforeAll(async () => {
68
69
  createFilesFeature(),
69
70
  createDataRetentionFeature(),
70
71
  createComplianceProfilesFeature(),
72
+ authFoundationFeature,
71
73
  createSessionsFeature(),
72
74
  createUserDataRightsFeature(),
73
75
  createUserDataRightsDefaultsFeature(),
@@ -11,6 +11,7 @@
11
11
  // assertion below go red.
12
12
 
13
13
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
14
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
14
15
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
15
16
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
16
17
  import { type SessionUser, SYSTEM_USER_ID } from "@cosmicdrift/kumiko-framework/engine";
@@ -94,6 +95,7 @@ beforeAll(async () => {
94
95
  createTestFileProviderFeature(provider, "test"),
95
96
  createDataRetentionFeature(),
96
97
  createComplianceProfilesFeature(),
98
+ authFoundationFeature,
97
99
  createSessionsFeature(),
98
100
  createUserDataRightsFeature(),
99
101
  createUserDataRightsDefaultsFeature(),
@@ -14,6 +14,7 @@
14
14
  // 0 rows after the owner column is nulled).
15
15
 
16
16
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
17
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
17
18
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
18
19
  import {
19
20
  createEntity,
@@ -148,6 +149,7 @@ beforeAll(async () => {
148
149
  stack = await setupTestStack({
149
150
  features: [
150
151
  createUserFeature(),
152
+ authFoundationFeature,
151
153
  createSessionsFeature(),
152
154
  createDataRetentionFeature(),
153
155
  createComplianceProfilesFeature(),
@@ -8,6 +8,7 @@
8
8
  // functional registry instance; the end-to-end cascade is covered consumer-side.
9
9
 
10
10
  import { afterAll, beforeAll, beforeEach, expect, test } from "bun:test";
11
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
11
12
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
12
13
  import {
13
14
  createEntity,
@@ -66,6 +67,7 @@ beforeAll(async () => {
66
67
  stack = await setupTestStack({
67
68
  features: [
68
69
  createUserFeature(),
70
+ authFoundationFeature,
69
71
  createSessionsFeature(),
70
72
  createDataRetentionFeature(),
71
73
  createComplianceProfilesFeature(),
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { access, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { access, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { createFilesFeature } from "@cosmicdrift/kumiko-framework/files";
4
+ import { EXT_SESSION_STORE } from "../../auth-foundation";
4
5
  import { createComplianceProfilesFeature } from "../../compliance-profiles/feature";
5
6
  import { createConfigFeature } from "../../config/feature";
6
7
  import { createDataRetentionFeature } from "../../data-retention/feature";
@@ -17,6 +18,13 @@ import { createUserDataRightsFeature } from "../feature";
17
18
  // entities (export-job, download-attempt) are r.entity, not direct-write stores,
18
19
  // so an entityList binding is rebuild-safe (unlike jobs/sessions read-models).
19
20
 
21
+ // Stub instead of the real auth-foundation — this test is about screen
22
+ // wiring, not auth; only sessions' sessionStore registration needs an owner
23
+ // present.
24
+ const sessionStoreOwnerStub = defineFeature("auth-foundation", (r) => {
25
+ r.extendsRegistrar(EXT_SESSION_STORE, {});
26
+ });
27
+
20
28
  describe("user-data-rights read-only inspector screens", () => {
21
29
  // A realistic assembly: mounting user-data-rights without its default hooks
22
30
  // now fails the V3 boot gate (the framework `user` entity holds PII with no
@@ -25,6 +33,7 @@ describe("user-data-rights read-only inspector screens", () => {
25
33
  const features = [
26
34
  createConfigFeature(),
27
35
  createUserFeature(),
36
+ sessionStoreOwnerStub,
28
37
  createSessionsFeature(),
29
38
  createDataRetentionFeature(),
30
39
  createComplianceProfilesFeature(),
@@ -10,6 +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 { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
13
14
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
14
15
  import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
15
16
  import {
@@ -56,6 +57,7 @@ beforeAll(async () => {
56
57
  createFilesFeature(),
57
58
  createDataRetentionFeature(),
58
59
  createComplianceProfilesFeature(),
60
+ authFoundationFeature,
59
61
  createSessionsFeature(),
60
62
  mailFoundationFeature,
61
63
  mailTransportInMemoryFeature,