@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,355 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/apex-surface-auth. Do not edit. -->
2
+
3
+ # apex-surface-auth
4
+
5
+ Apex-Surface: öffentliche Account-Flows (Login/Register/PW/Löschen) via createPublicSurface + AuthShell, inkl. anonymem email-verifiziertem Deletion-Flow.
6
+
7
+ ## README
8
+
9
+ # apex-surface-auth
10
+
11
+ The **standard pattern for public apex auth**: login, signup, forgot-password,
12
+ and account-deletion on your marketing surface — schema-less, anonymously
13
+ reachable, without leaking admin nav or internal topology.
14
+
15
+ A typical app has two mounts sharing locale and primitives:
16
+
17
+ - **Admin UI** → `createKumikoApp` (loads the full schema after login via
18
+ the authenticated `GET /api/schema`)
19
+ - **Apex** → `createPublicSurface` (no schema, ever)
20
+
21
+ ## What it shows
22
+
23
+ - **Four public routes** — `/login`, `/signup`, `/forgot-password`,
24
+ `/delete-account` (+ confirm) using bundled auth screens.
25
+ - **`createPublicSurface`** — stacks `clientFeatures` providers and i18n only,
26
+ **not** their gates (an `AuthGate` would lock the public surface).
27
+ - **`AuthShellProvider`** — auth card inside marketing chrome instead of
28
+ fullscreen (optional; default fullscreen still works).
29
+ - **`composeApexAccountApp()`** — server feature list for all four flows.
30
+ - **Anonymous deletion** — email-verified magic-link flow when the user cannot
31
+ log in (GDPR Art. 17 lockout case).
32
+ - **`anonymousAccess`** — lets unauthenticated `/api/write` hit anonymous
33
+ handlers on the apex host tenant.
34
+
35
+ ## Client: `createPublicSurface` + `AuthShell`
36
+
37
+ ```tsx illustration
38
+ import {
39
+ ForgotPasswordScreen, SignupScreen, createLoginRoute,
40
+ AuthShellProvider, emailPasswordClient,
41
+ } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
42
+ import {
43
+ RequestAccountDeletionScreen, ConfirmAccountDeletionScreen,
44
+ } from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
45
+ import { createPublicSurface } from "@cosmicdrift/kumiko-renderer-web";
46
+
47
+ // createLoginRoute, not a raw LoginScreen render — it owns the challenge-
48
+ // swap for a second factor, so mounting auth-mfa later doesn't need this
49
+ // route touched again. Pass `mfaVerifyScreen: MfaVerifyScreen` (from
50
+ // `.../auth-mfa/web`) once the app mounts auth-mfa.
51
+ const LoginRoute = createLoginRoute({ loginScreenProps: { signupHref: "/signup" } });
52
+
53
+ createPublicSurface({
54
+ clientFeatures: [emailPasswordClient()], // SessionProvider + i18n
55
+ shell: ({ children }) => (
56
+ <MarketingChrome>
57
+ <AuthShellProvider shell={(card) => <div className="py-12 flex justify-center">{card}</div>}>
58
+ {children}
59
+ </AuthShellProvider>
60
+ </MarketingChrome>
61
+ ),
62
+ routes: [
63
+ { path: "/login", component: <LoginRoute /> },
64
+ { path: "/signup", component: <SignupScreen loginHref="/login" /> },
65
+ { path: "/forgot-password", component: <ForgotPasswordScreen loginHref="/login" /> },
66
+ { path: "/delete-account", component: <RequestAccountDeletionScreen /> },
67
+ { path: "/delete-account/confirm", component: <ConfirmAccountDeletionScreen /> },
68
+ ],
69
+ fallback: <LoginRoute />,
70
+ });
71
+ ```
72
+
73
+ ## Server: composition + `anonymousAccess`
74
+
75
+ `composeApexAccountApp()` (see `src/feature.ts`) mounts features for all four
76
+ flows. Login/signup/password reset come from `auth-email-password`; the
77
+ anonymous **email-verified** deletion flow from `user-data-rights`:
78
+
79
+ - `request-deletion-by-email` (anonymous, enumeration-safe) → magic link
80
+ - `confirm-deletion-by-token` (anonymous) → starts grace period
81
+
82
+ ```ts illustration
83
+ runProdApp({
84
+ features: composeApexAccountApp({
85
+ deletionTokenSecret: process.env.DELETION_TOKEN_SECRET!,
86
+ deletionVerifyUrl: "https://app.example.com/delete-account/confirm",
87
+ sendDeletionVerificationEmail: async ({ email, verifyUrl }) => {
88
+ // Must be non-blocking (enqueue) — synchronous send enables timing oracle.
89
+ await delivery.notify("account.deletion.verify", { email, verifyUrl });
90
+ },
91
+ }),
92
+ anonymousAccess: { defaultTenantId: APEX_TENANT_ID },
93
+ });
94
+ ```
95
+
96
+ ## Why deletion is anonymous
97
+
98
+ GDPR Art. 17 applies when the user **cannot log in** (lockout). The flow is
99
+ email-verified (magic link), not login-gated. The HMAC token carries `userId`
100
+ + expiry (no DB table); purpose `"deletion-request"` blocks replay on other
101
+ token endpoints. Second confirm is idempotent (`user_not_in_active_state`).
102
+
103
+ ## Tests
104
+
105
+ Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
106
+
107
+ ```bash
108
+ bun test src/__tests__/feature.integration.test.ts
109
+ ```
110
+
111
+ Proves end-to-end over real `/api/write` without auth:
112
+
113
+ 1. `request-deletion-by-email` → verification email enqueued
114
+ 2. `confirm-deletion-by-token` → user status `DeletionRequested`
115
+ 3. Unknown email → same 200 response, no mail (enumeration-safe)
116
+
117
+ ## Related samples
118
+
119
+ - [user-profile](/en/samples/recipes-user-profile/) — logged-in self-service
120
+ profile (password, email, deletion while authenticated).
121
+ - [session-revocation](/en/samples/recipes-session-revocation/) — wire
122
+ `createSessionCallbacks()` so logout invalidates JWTs server-side.
123
+
124
+ ## Source files
125
+
126
+ ### src/__tests__/feature.integration.test.ts
127
+
128
+ ```ts
129
+ // Beweist: composeApexAccountApp() bootet (volle Account-Komposition inkl.
130
+ // auth + user-data-rights) UND der anonyme email-verifizierte Deletion-Flow
131
+ // läuft end-to-end durch /api/write OHNE Auth (anonymousAccess).
132
+
133
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
134
+ import {
135
+ tenantComplianceProfileEntity,
136
+ tenantComplianceProfileTable,
137
+ } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
138
+ import { USER_STATUS, userEntity, userTable } from "@cosmicdrift/kumiko-bundled-features/user";
139
+ import type { SendDeletionVerificationEmailFn } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
140
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
141
+ import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
142
+ import {
143
+ createTestUser,
144
+ setupTestStack,
145
+ type TestStack,
146
+ testTenantId,
147
+ unsafeCreateEntityTable,
148
+ } from "@cosmicdrift/kumiko-framework/stack";
149
+ import { resetTestTables, seedRow } from "@cosmicdrift/kumiko-framework/testing";
150
+ import { composeApexAccountApp } from "../feature";
151
+
152
+ const REQUEST_BY_EMAIL = "user-data-rights:write:request-deletion-by-email";
153
+ const CONFIRM_BY_TOKEN = "user-data-rights:write:confirm-deletion-by-token";
154
+ const SECRET = "recipe-deletion-secret-0123456789";
155
+ const VERIFY_URL = "https://app.example.test/delete-account/confirm";
156
+
157
+ const tenant = testTenantId(1);
158
+ const user = createTestUser({ id: 7, tenantId: tenant, roles: ["Member"] });
159
+ const EMAIL = "user@example.com";
160
+
161
+ const verifyCalls: Parameters<SendDeletionVerificationEmailFn>[0][] = [];
162
+ const sendDeletionVerificationEmail: SendDeletionVerificationEmailFn = async (args) => {
163
+ verifyCalls.push(args);
164
+ };
165
+
166
+ let stack: TestStack;
167
+
168
+ beforeAll(async () => {
169
+ stack = await setupTestStack({
170
+ features: composeApexAccountApp({
171
+ deletionTokenSecret: SECRET,
172
+ deletionVerifyUrl: VERIFY_URL,
173
+ sendDeletionVerificationEmail,
174
+ }),
175
+ anonymousAccess: { defaultTenantId: tenant },
176
+ });
177
+ await unsafeCreateEntityTable(stack.db, userEntity);
178
+ await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
179
+ await createEventsTable(stack.db);
180
+ });
181
+
182
+ afterAll(async () => {
183
+ await stack.cleanup();
184
+ });
185
+
186
+ beforeEach(async () => {
187
+ verifyCalls.length = 0;
188
+ await resetTestTables(stack.db, [userTable, tenantComplianceProfileTable, eventsTable]);
189
+ await seedRow(stack.db, userTable, {
190
+ id: user.id,
191
+ tenantId: tenant,
192
+ email: EMAIL,
193
+ passwordHash: "hashed",
194
+ displayName: "User",
195
+ locale: "de",
196
+ emailVerified: true,
197
+ roles: '["Member"]',
198
+ status: USER_STATUS.Active,
199
+ gracePeriodEnd: null,
200
+ });
201
+ });
202
+
203
+ describe("apex-surface-auth recipe", () => {
204
+ test("anonymer Deletion-Flow: request-by-email → confirm-by-token → Grace-Period", async () => {
205
+ const reqRes = await stack.http.raw("POST", "/api/write", {
206
+ type: REQUEST_BY_EMAIL,
207
+ payload: { email: EMAIL },
208
+ });
209
+ expect(reqRes.status).toBe(200);
210
+ expect(verifyCalls).toHaveLength(1);
211
+
212
+ const verifyUrl = new URL(verifyCalls[0]?.verifyUrl ?? "");
213
+ const token = new URLSearchParams(verifyUrl.hash.slice(1)).get("token") ?? "";
214
+ expect(token.length).toBeGreaterThan(0);
215
+
216
+ const confirmRes = await stack.http.raw("POST", "/api/write", {
217
+ type: CONFIRM_BY_TOKEN,
218
+ payload: { token },
219
+ });
220
+ expect(confirmRes.status).toBe(200);
221
+
222
+ const rows = (await selectMany(stack.db, userTable, { id: user.id })) as Array<{
223
+ status: string;
224
+ }>;
225
+ expect(rows[0]?.status).toBe(USER_STATUS.DeletionRequested);
226
+ });
227
+
228
+ test("enumeration-safe: unbekannte Email → success, keine Mail", async () => {
229
+ const res = await stack.http.raw("POST", "/api/write", {
230
+ type: REQUEST_BY_EMAIL,
231
+ payload: { email: "nobody@example.com" },
232
+ });
233
+ expect(res.status).toBe(200);
234
+ expect(verifyCalls).toHaveLength(0);
235
+ });
236
+ });
237
+ ```
238
+
239
+ ### src/feature.ts
240
+
241
+ ```ts
242
+ // apex-surface-auth Recipe — der evidente Weg für öffentlichen Apex-Content.
243
+ //
244
+ // Eine Kumiko-App hat eine öffentliche Apex-Präsenz (Landing/Login) UND eine
245
+ // schema-getriebene Admin-UI. Die Admin-UI mountet via createKumikoApp (volles
246
+ // Schema). Die Apex mountet via createPublicSurface — schema-LOS, anonym
247
+ // erreichbar, kein Admin-Nav/Topologie-Leak. Beide teilen Locale + Primitives.
248
+ //
249
+ // Dieses Recipe zeigt:
250
+ // 1. Server: die Feature-Komposition für die 4 Account-Flows + den anonymen,
251
+ // email-verifizierten Deletion-Flow (Lockout-sicher).
252
+ // 2. Client (Kommentar unten + README): wie createPublicSurface + AuthShell
253
+ // die Screens in der Apex-Chrome mounten.
254
+ //
255
+ // CLIENT-WIRING (apex.tsx der App — renderer-web, hier nur als Referenz, da
256
+ // Recipes keine Browser-Deps ziehen):
257
+ //
258
+ // import {
259
+ // ForgotPasswordScreen, SignupScreen, createLoginRoute,
260
+ // AuthShellProvider, emailPasswordClient,
261
+ // } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
262
+ // import {
263
+ // RequestAccountDeletionScreen, ConfirmAccountDeletionScreen,
264
+ // defaultTranslations as deletionI18n,
265
+ // } from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
266
+ // import { createPublicSurface } from "@cosmicdrift/kumiko-renderer-web";
267
+ //
268
+ // // AuthShell: die Auth-Card rendert in der Marketing-Chrome statt
269
+ // // Fullscreen. Default (ohne Provider) bleibt der Fullscreen-Wrapper.
270
+ // const shell = ({ children }) => (
271
+ // <MarketingChrome>
272
+ // <AuthShellProvider shell={(card) => <div className="py-12 flex justify-center">{card}</div>}>
273
+ // {children}
274
+ // </AuthShellProvider>
275
+ // </MarketingChrome>
276
+ // );
277
+ //
278
+ // // createLoginRoute — NICHT LoginScreen direkt rendern: die Route
279
+ // // braucht die Challenge-Swap-Logik für einen zweiten Faktor. Ohne die
280
+ // // (z.B. raw <LoginScreen />) bleibt ein MFA-enrolter User beim Login
281
+ // // hängen, sobald die App irgendwann auth-mfa mountet. Mountet die App
282
+ // // auth-mfa, `mfaVerifyScreen: MfaVerifyScreen` (aus
283
+ // // "@cosmicdrift/kumiko-bundled-features/auth-mfa/web") ergänzen.
284
+ // const LoginRoute = createLoginRoute({ loginScreenProps: { signupHref: "/signup" } });
285
+ //
286
+ // createPublicSurface({
287
+ // clientFeatures: [emailPasswordClient()], // bringt SessionProvider + i18n
288
+ // shell,
289
+ // routes: [
290
+ // { path: "/login", component: <LoginRoute /> },
291
+ // { path: "/signup", component: <SignupScreen loginHref="/login" /> },
292
+ // { path: "/forgot-password", component: <ForgotPasswordScreen loginHref="/login" /> },
293
+ // { path: "/delete-account", component: <RequestAccountDeletionScreen /> },
294
+ // { path: "/delete-account/confirm", component: <ConfirmAccountDeletionScreen /> },
295
+ // ],
296
+ // fallback: <LoginRoute />,
297
+ // });
298
+ //
299
+ //
300
+ // SERVER-WIRING: die App aktiviert `anonymousAccess` (defaultTenantId = der
301
+ // Apex-Host-Tenant), damit /api/write die anonymen Deletion-Handler erreicht:
302
+ //
303
+ // runProdApp({ features: composeApexAccountApp({...}),
304
+ // anonymousAccess: { defaultTenantId: APEX_TENANT_ID } })
305
+
306
+ import { createAuthEmailPasswordFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
307
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
308
+ import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
309
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
310
+ import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
311
+ import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
312
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
313
+ import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
314
+ import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
315
+ import {
316
+ createUserDataRightsFeature,
317
+ type SendDeletionVerificationEmailFn,
318
+ } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
319
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
320
+
321
+ export type ApexAccountAppOptions = {
322
+ /** HMAC-Secret für das Deletion-Verify-Token. */
323
+ readonly deletionTokenSecret: string;
324
+ /** Apex-Route des Confirm-Screens; der Handler hängt das Token als
325
+ * URL-Fragment an (`#token=`). */
326
+ readonly deletionVerifyUrl: string;
327
+ /** Versand des Verify-Magic-Links. MUSS non-blocking sein (enqueue) — ein
328
+ * synchroner Send würde ein Timing-Oracle für Account-Enumeration öffnen. */
329
+ readonly sendDeletionVerificationEmail: SendDeletionVerificationEmailFn;
330
+ };
331
+
332
+ // Volle Feature-Komposition für die öffentlichen Account-Flows. Die
333
+ // user-data-rights-Options aktivieren den anonymen Deletion-Flow; die übrigen
334
+ // Features liefern Login/Register/PW (auth-email-password) + die
335
+ // Require-Kette (user-data-rights → data-retention + compliance-profiles +
336
+ // sessions).
337
+ export function composeApexAccountApp(opts: ApexAccountAppOptions): FeatureDefinition[] {
338
+ return [
339
+ createConfigFeature(),
340
+ createUserFeature(),
341
+ createTenantFeature(),
342
+ createAuthEmailPasswordFeature(),
343
+ createDataRetentionFeature(),
344
+ createComplianceProfilesFeature(),
345
+ authFoundationFeature,
346
+ createPersonalAccessTokensFeature({ scopes: {} }),
347
+ createSessionsFeature(),
348
+ createUserDataRightsFeature({
349
+ deletionTokenSecret: opts.deletionTokenSecret,
350
+ deletionVerifyUrl: opts.deletionVerifyUrl,
351
+ sendDeletionVerificationEmail: opts.sendDeletionVerificationEmail,
352
+ }),
353
+ ];
354
+ }
355
+ ```