@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.
- package/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- 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
|
+
```
|