@cosmicdrift/kumiko-samples 0.404.0 → 0.404.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 (65) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +25 -25
  3. package/packages/bundled-features/src/account-security/README.md +7 -5
  4. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -0
  5. package/packages/bundled-features/src/documents/changes.json +8 -0
  6. package/packages/bundled-features/src/host-routing/README.md +10 -10
  7. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +1 -0
  8. package/packages/bundled-features/src/legal-pages/README.md +21 -13
  9. package/packages/bundled-features/src/renderer-foundation/README.md +6 -4
  10. package/packages/bundled-features/src/renderer-pdf/README.md +5 -1
  11. package/packages/bundled-features/src/template-resolver/README.md +29 -29
  12. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -2
  13. package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +1 -0
  14. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +1 -0
  15. package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +36 -36
  16. package/packages/bundled-features/src/user-data-rights/README.md +33 -33
  17. package/packages/bundled-features/src/user-data-rights/web/public-deletion-gate.tsx +1 -0
  18. package/samples/apps/admin-console/README.md +5 -5
  19. package/samples/apps/admin-console/package.json +3 -3
  20. package/samples/apps/cap-billing-demo/README.md +15 -15
  21. package/samples/apps/config-demo/README.md +7 -7
  22. package/samples/apps/config-demo/package.json +2 -2
  23. package/samples/apps/marketing-demo/README.md +2 -2
  24. package/samples/apps/marketing-demo/package.json +2 -2
  25. package/samples/apps/showcase/package.json +3 -3
  26. package/samples/apps/styleguide/package.json +2 -2
  27. package/samples/apps/ui-walkthrough/README.md +8 -9
  28. package/samples/apps/ui-walkthrough/package.json +2 -2
  29. package/samples/apps/use-all-bundled/README.md +2 -2
  30. package/samples/apps/use-all-bundled/package.json +2 -2
  31. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +18 -1
  32. package/samples/apps/user-data-rights-demo/README.md +12 -12
  33. package/samples/apps/workspaces/package.json +2 -2
  34. package/samples/recipes/apex-landing/README.md +3 -3
  35. package/samples/recipes/apex-landing/package.json +1 -1
  36. package/samples/recipes/apex-surface-auth/README.md +13 -9
  37. package/samples/recipes/auth-mfa/README.md +2 -4
  38. package/samples/recipes/auth-mfa/package.json +2 -2
  39. package/samples/recipes/boot-check/README.md +2 -2
  40. package/samples/recipes/compliance-profiles-demo/README.md +5 -5
  41. package/samples/recipes/compose-stacks/README.md +8 -8
  42. package/samples/recipes/cross-feature-events/README.md +1 -0
  43. package/samples/recipes/crypto-shredding-hr/README.md +3 -3
  44. package/samples/recipes/custom-fields-basic/README.md +1 -1
  45. package/samples/recipes/documents/README.md +25 -0
  46. package/samples/recipes/embedded-entity-form/package.json +2 -2
  47. package/samples/recipes/encrypted-tenant-config/README.md +8 -8
  48. package/samples/recipes/env-composition/README.md +10 -10
  49. package/samples/recipes/error-contract/README.md +9 -9
  50. package/samples/recipes/event-sourcing/README.md +9 -9
  51. package/samples/recipes/ledger-invoicing/README.md +6 -6
  52. package/samples/recipes/legal-pages/README.md +16 -16
  53. package/samples/recipes/managed-config/README.md +15 -11
  54. package/samples/recipes/managed-pages/README.md +7 -7
  55. package/samples/recipes/pdf-audit-export/package.json +1 -1
  56. package/samples/recipes/pipeline-basics/README.md +13 -13
  57. package/samples/recipes/public-share-token/README.md +4 -4
  58. package/samples/recipes/record-detail-layout/package.json +3 -3
  59. package/samples/recipes/security-baseline/README.md +4 -1
  60. package/samples/recipes/session-revocation/README.md +1 -1
  61. package/samples/recipes/tenant-settings/README.md +4 -4
  62. package/samples/recipes/tier-admin/README.md +2 -2
  63. package/samples/recipes/user-data-rights/README.md +21 -19
  64. package/samples/recipes/wizard-form/package.json +3 -3
  65. package/samples/recipes/writeform-section/package.json +3 -3
@@ -18,16 +18,16 @@ defineFeature("tasks", (r) => {
18
18
  r.useExtension(EXT_USER_DATA, "task", {
19
19
  export: async (ctx) => ({
20
20
  entity: "task",
21
- rows: await ctx.db.select().from(tasksTable)
22
- .where(eq(tasksTable.authorId, ctx.userId)),
21
+ rows: await ctx.db.select().from(tasksTable).where(eq(tasksTable.authorId, ctx.userId)),
23
22
  }),
24
23
  delete: async (ctx, strategy) => {
25
24
  if (strategy === "anonymize") {
26
- await ctx.db.update(tasksTable).set({ authorId: null })
25
+ await ctx.db
26
+ .update(tasksTable)
27
+ .set({ authorId: null })
27
28
  .where(eq(tasksTable.authorId, ctx.userId));
28
29
  } else {
29
- await ctx.db.delete(tasksTable)
30
- .where(eq(tasksTable.authorId, ctx.userId));
30
+ await ctx.db.delete(tasksTable).where(eq(tasksTable.authorId, ctx.userId));
31
31
  }
32
32
  },
33
33
  });
@@ -42,15 +42,15 @@ Hook-Signaturen in `framework/src/engine/extensions/user-data.ts`:
42
42
 
43
43
  ## Endpoints
44
44
 
45
- | Article | Handler | Zweck |
46
- |---------|---------|-------|
47
- | Art. 15 | `user-data-rights:query:my-audit-log` | User sieht eigene Framework-Events (account-weit über alle Memberships). Domain-Entities ohne `ctx.events.append` erscheinen NICHT — nur im Export-Bundle. |
48
- | Art. 15+20 | `user-data-rights:write:request-export` | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten + signed Magic-Link |
49
- | Art. 17 | `user-data-rights:write:request-deletion` | Soft-Delete mit Grace-Period, anschließend Cron anonymisiert User + cleant Domain-Entities |
50
- | Art. 17 | `user-data-rights:write:cancel-deletion` | User widerruft seinen Forget-Request während der Grace |
51
- | Art. 18 | `user-data-rights:write:restrict-account` | Auth-Middleware blockt Logins bis Lift |
52
- | Art. 18 | `user-data-rights:write:lift-restriction` | Admin/SystemAdmin hebt Restriction auf |
53
- | Operator | `user-data-rights:query:list-download-attempts` | DPO-Sicht auf invalid Download-Versuche (Brute-Force-Detection, Admin/SystemAdmin only) |
45
+ | Article | Handler | Zweck |
46
+ | ---------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | Art. 15 | `user-data-rights:query:my-audit-log` | User sieht eigene Framework-Events (account-weit über alle Memberships). Domain-Entities ohne `ctx.events.append` erscheinen NICHT — nur im Export-Bundle. |
48
+ | Art. 15+20 | `user-data-rights:write:request-export` | Async Job → ZIP mit user-Profil + fileRefs + alle EXT_USER_DATA-Provider-Daten + signed Magic-Link |
49
+ | Art. 17 | `user-data-rights:write:request-deletion` | Soft-Delete mit Grace-Period, anschließend Cron anonymisiert User + cleant Domain-Entities |
50
+ | Art. 17 | `user-data-rights:write:cancel-deletion` | User widerruft seinen Forget-Request während der Grace |
51
+ | Art. 18 | `user-data-rights:write:restrict-account` | Auth-Middleware blockt Logins bis Lift |
52
+ | Art. 18 | `user-data-rights:write:lift-restriction` | Admin/SystemAdmin hebt Restriction auf |
53
+ | Operator | `user-data-rights:query:list-download-attempts` | DPO-Sicht auf invalid Download-Versuche (Brute-Force-Detection, Admin/SystemAdmin only) |
54
54
 
55
55
  Plus 2 anonyme HTTP-Routes für Export-Download (Magic-Link-Pfad +
56
56
  session-auth-Pfad), siehe `handlers/download-by-{token,job}.query.ts`.
@@ -77,30 +77,30 @@ weglassen wenn er Custom-Hooks registrieren will.
77
77
 
78
78
  ## Audit-Trail
79
79
 
80
- | Tabelle | Zweck | Retention |
81
- |---------|-------|-----------|
82
- | `kumiko_events` | Framework-Event-Store, Quelle für `my-audit-log` | per Domain-Policy |
83
- | `read_export_jobs` | Async Export-Status (queued / done / failed) | per `compliance-profiles` |
84
- | `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit | per `compliance-profiles` (default `exportDownloadTtl`) |
85
- | `read_download_attempts` | Invalid-Download-Versuche für DPO-Brute-Force-Detection | **90d hardDelete** (Entity-Default — schützt vor Disk-Bomb bei aktiven Angriffen) |
80
+ | Tabelle | Zweck | Retention |
81
+ | ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------- |
82
+ | `kumiko_events` | Framework-Event-Store, Quelle für `my-audit-log` | per Domain-Policy |
83
+ | `read_export_jobs` | Async Export-Status (queued / done / failed) | per `compliance-profiles` |
84
+ | `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit | per `compliance-profiles` (default `exportDownloadTtl`) |
85
+ | `read_download_attempts` | Invalid-Download-Versuche für DPO-Brute-Force-Detection | **90d hardDelete** (Entity-Default — schützt vor Disk-Bomb bei aktiven Angriffen) |
86
86
 
87
87
  ## Tests
88
88
 
89
89
  18 Testdateien, 188 Tests, alle grün:
90
90
 
91
- | Datei | Pinst |
92
- |-------|-------|
93
- | `audit-log.integration.test.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
94
- | `cross-data-matrix.integration.test.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
95
- | `download.integration.test.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
96
- | `request-export.integration.test.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
97
- | `request-deletion-callback.integration.test.ts` + `request-cancel-deletion.integration.test.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
98
- | `restriction-flow.integration.test.ts` | Status-Flip + Auth-Middleware-Block + Lift |
99
- | `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts` | Worker-Logic + Idempotency + Email-Callbacks |
100
- | `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
101
- | `user-data-rights.integration.test.ts` | Boot-Smoke + Feature-Meta |
102
- | `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
103
- | `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
91
+ | Datei | Pinst |
92
+ | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `audit-log.integration.test.ts` | Cross-User-Isolation, Account-weite Sicht, eventType-Filter, Admin-only operator-query, download-attempt 90d-retention |
94
+ | `cross-data-matrix.integration.test.ts` | 3-Provider-Pipeline (user + fileRef + custom-domain), Cross-Tenant Forget mit user-anonymize, Other-User-Isolation |
95
+ | `download.integration.test.ts` | HTTP-e2e via `r.httpRoute`: Magic-Link, multi-use, expired, failed-job, storage-cleared, cross-tenant-same-user, malicious-filename |
96
+ | `request-export.integration.test.ts` | Idempotency, active-job-constraint, cross-tenant-anyMember-userId-pattern |
97
+ | `request-deletion-callback.integration.test.ts` + `request-cancel-deletion.integration.test.ts` | Grace + Cancel-Pfad + Email-Callback best-effort |
98
+ | `restriction-flow.integration.test.ts` | Status-Flip + Auth-Middleware-Block + Lift |
99
+ | `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts` | Worker-Logic + Idempotency + Email-Callbacks |
100
+ | `policy-to-strategy.test.ts` | Retention.strategy → UserDataDeleteStrategy mapping |
101
+ | `user-data-rights.integration.test.ts` | Boot-Smoke + Feature-Meta |
102
+ | `token-helpers.test.ts` + `zip-path.test.ts` | Token-Hashing + Path-Traversal-Schutz |
103
+ | `export-job-{idempotency,schema}.test.ts` | Active-job-uniqueness + Schema-Constraints |
104
104
 
105
105
  ## Sample
106
106
 
@@ -32,6 +32,7 @@ export function makePublicDeletionGate(
32
32
  routes: PublicDeletionRoutes,
33
33
  ): ComponentType<{ children: ReactNode }> {
34
34
  const shell = routes.shell ?? centeredShell;
35
+ // biome-ignore lint/correctness/noComponentHookFactories: setup-time factory, called once per userDataRightsClient(); gates only receive children, so routes are bound here
35
36
  function PublicDeletionGate({ children }: { readonly children: ReactNode }): ReactNode {
36
37
  const path = window.location.pathname;
37
38
  if (path === routes.requestPath) return shell(<RequestAccountDeletionScreen />);
@@ -24,11 +24,11 @@ Port 4177 is hardcoded in the `dev` script. The server reads the root
24
24
  All three log into the dev tenant "Admin Console Demo" (tenant key
25
25
  `demo`). Passwords are in `src/app/auth-constants.ts`.
26
26
 
27
- | Email | Role | Sees |
28
- |---|---|---|
29
- | `sysadmin@admin-console.local` | global `SystemAdmin`, tenant `Admin` | tenant-admin and platform workspaces |
30
- | `tenant-admin@admin-console.local` | tenant `TenantAdmin` | tenant-admin workspace only |
31
- | `regular@admin-console.local` | tenant `User` | no admin workspace |
27
+ | Email | Role | Sees |
28
+ | ---------------------------------- | ------------------------------------ | ------------------------------------ |
29
+ | `sysadmin@admin-console.local` | global `SystemAdmin`, tenant `Admin` | tenant-admin and platform workspaces |
30
+ | `tenant-admin@admin-console.local` | tenant `TenantAdmin` | tenant-admin workspace only |
31
+ | `regular@admin-console.local` | tenant `User` | no admin workspace |
32
32
 
33
33
  The SystemAdmin comes from the `auth.admin` bootstrap in
34
34
  `src/app/server.ts`; the other two are created by `seedRoleUsers` in
@@ -11,9 +11,9 @@
11
11
  "@cosmicdrift/kumiko-dev-server": "workspace:*",
12
12
  "@cosmicdrift/kumiko-framework": "workspace:*",
13
13
  "@cosmicdrift/kumiko-renderer": "workspace:*",
14
- "lucide-react": "^1.14.0",
15
- "react": "^19.2.6",
16
- "react-dom": "^19.2.6",
14
+ "lucide-react": "^1.65.0",
15
+ "react": "^19.3.1",
16
+ "react-dom": "^19.3.1",
17
17
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
18
18
  }
19
19
  }
@@ -9,9 +9,9 @@ The sample doubles as **living documentation** for the pattern: read the code to
9
9
  A tiny newsletter app with two tiers:
10
10
 
11
11
  | Tier | Newsletters per month | Soft warning at | Hard block at |
12
- |------|-----------|-------------------|-----------------|
13
- | free | 10 | 11 (110%) | 12 (120%) |
14
- | pro | 100 | 110 (110%) | 120 (120%) |
12
+ | ---- | --------------------- | --------------- | ------------- |
13
+ | free | 10 | 11 (110%) | 12 (120%) |
14
+ | pro | 100 | 110 (110%) | 120 (120%) |
15
15
 
16
16
  Mails land in an **in-memory transport** (`mail-transport-inmemory`).
17
17
  There's no real SMTP server — perfect for the demo, no Mailpit/Mailcrab
@@ -87,12 +87,12 @@ cd samples/apps/cap-billing-demo
87
87
  bun dev # → http://localhost:4290
88
88
  ```
89
89
 
90
- | Login | Value |
91
- |-------|------|
92
- | URL | `http://localhost:4290` |
93
- | Email | `admin@cap-demo.local` |
94
- | Password | `changeme` |
95
- | Tenant | "Cap-Billing-Demo" |
90
+ | Login | Value |
91
+ | -------- | ----------------------- |
92
+ | URL | `http://localhost:4290` |
93
+ | Email | `admin@cap-demo.local` |
94
+ | Password | `changeme` |
95
+ | Tenant | "Cap-Billing-Demo" |
96
96
 
97
97
  In the browser, use the Designer/Admin UI to set the config key
98
98
  `newsletter:config:tier` to `"free"` or `"pro"` and trigger the
@@ -110,13 +110,13 @@ omitted to keep the focus on cap+tier.
110
110
 
111
111
  The sample is intentionally minimal. For a production app, swap:
112
112
 
113
- | Demo component | Production replacement |
114
- |-----------------|-------------------|
115
- | `mail-transport-inmemory` | `mail-transport-smtp` (BYOK) or a custom plugin |
116
- | Hardcoded `DEMO_TIER_MAP` | stays — tier definitions are static, the subscription row only writes the `tier` key |
113
+ | Demo component | Production replacement |
114
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
115
+ | `mail-transport-inmemory` | `mail-transport-smtp` (BYOK) or a custom plugin |
116
+ | Hardcoded `DEMO_TIER_MAP` | stays — tier definitions are static, the subscription row only writes the `tier` key |
117
117
  | Tier switch via webhook (test-only) | mount a real plugin: `createSubscriptionStripeFeature(...)` and/or `createSubscriptionMollieFeature(...)` in the run config |
118
- | 2 tiers (free/pro) | any number, see `samples/apps/platform/src/tier-map.ts` for a 4-tier example |
119
- | Newsletter domain | your own feature with `withCapEnforcement(handler, capResolver)` |
118
+ | 2 tiers (free/pro) | any number, see `samples/apps/platform/src/tier-map.ts` for a 4-tier example |
119
+ | Newsletter domain | your own feature with `withCapEnforcement(handler, capResolver)` |
120
120
 
121
121
  The **plugin API switch** between demo and production is a single
122
122
  config value: `mail-foundation:config:provider` flips from
@@ -26,13 +26,13 @@ The server reads the root `.env` (`--env-file=../../../.env`).
26
26
 
27
27
  `src/features/demo/feature.ts` declares five keys with `r.config()`:
28
28
 
29
- | Key | Scope | Type | Default | Write access |
30
- |---|---|---|---|---|
31
- | `siteName` | tenant | text | `My Site` | all |
32
- | `themeColor` | tenant | text | `#000000` | all |
33
- | `maxUploadSize` | tenant | number, bounds 1 to 1000 | `10` | all |
34
- | `emailNotifications` | user | boolean | `true` | all |
35
- | `autoApprove` | system | boolean | `false` | `access.systemAdmin` |
29
+ | Key | Scope | Type | Default | Write access |
30
+ | -------------------- | ------ | ------------------------ | --------- | -------------------- |
31
+ | `siteName` | tenant | text | `My Site` | all |
32
+ | `themeColor` | tenant | text | `#000000` | all |
33
+ | `maxUploadSize` | tenant | number, bounds 1 to 1000 | `10` | all |
34
+ | `emailNotifications` | user | boolean | `true` | all |
35
+ | `autoApprove` | system | boolean | `false` | `access.systemAdmin` |
36
36
 
37
37
  ### Seeds
38
38
 
@@ -13,8 +13,8 @@
13
13
  "@cosmicdrift/kumiko-dev-server": "workspace:*",
14
14
  "@cosmicdrift/kumiko-renderer": "workspace:*",
15
15
  "@cosmicdrift/kumiko-framework": "workspace:*",
16
- "react": "^19.2.6",
17
- "react-dom": "^19.2.6",
16
+ "react": "^19.3.1",
17
+ "react-dom": "^19.3.1",
18
18
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
19
19
  }
20
20
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  Sample app that produces marketing screenshots for
4
4
  [kumiko.rocks](https://kumiko.rocks). Two small internal tools (asset tracker
5
- + helpdesk) on a single Kumiko instance — a DACH-persona proof that the
6
- same framework stack covers two tools without separate codebases.
5
+ - helpdesk) on a single Kumiko instance — a DACH-persona proof that the
6
+ same framework stack covers two tools without separate codebases.
7
7
 
8
8
  **No auth** — like the showcase, in auto-mint-JWT mode. Straight to
9
9
  the edit/list screen without a login. To exercise auth paths, see
@@ -14,8 +14,8 @@
14
14
  "@cosmicdrift/kumiko-renderer": "workspace:*",
15
15
  "@cosmicdrift/kumiko-framework": "workspace:*",
16
16
  "@cosmicdrift/kumiko-types": "workspace:*",
17
- "react": "^19.2.6",
18
- "react-dom": "^19.2.6",
17
+ "react": "^19.3.1",
18
+ "react-dom": "^19.3.1",
19
19
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
20
20
  }
21
21
  }
@@ -13,9 +13,9 @@
13
13
  "@cosmicdrift/kumiko-dev-server": "workspace:*",
14
14
  "@cosmicdrift/kumiko-renderer": "workspace:*",
15
15
  "@cosmicdrift/kumiko-framework": "workspace:*",
16
- "lucide-react": "^1.14.0",
17
- "react": "^19.2.6",
18
- "react-dom": "^19.2.6",
16
+ "lucide-react": "^1.65.0",
17
+ "react": "^19.3.1",
18
+ "react-dom": "^19.3.1",
19
19
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
20
20
  },
21
21
  "devDependencies": {
@@ -5,8 +5,8 @@
5
5
  "@cosmicdrift/kumiko-dev-server": "workspace:*",
6
6
  "@cosmicdrift/kumiko-renderer": "workspace:*",
7
7
  "@cosmicdrift/kumiko-framework": "workspace:*",
8
- "react": "^19.2.6",
9
- "react-dom": "^19.2.6",
8
+ "react": "^19.3.1",
9
+ "react-dom": "^19.3.1",
10
10
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
11
11
  },
12
12
  "description": "Styleguide-Gallery: rendert die Auto-UI-Bausteine, Screenshot-Runner schießt sie in 3 Themes für die Doku",
@@ -31,14 +31,14 @@ bun kumiko dev
31
31
  cd samples/apps/ui-walkthrough && bun dev # http://localhost:4173, admin@kumiko.dev / kumiko-admin
32
32
  ```
33
33
 
34
- | Mask | Route |
35
- | --- | --- |
36
- | Liste | `/lease-list` (pager check: `/lease-list-short`, 3 rows) |
37
- | Detail | click "Max Nachmieter" (`/lease-detail/00000000-0000-4000-8000-000000003103`), tab "Positionen" |
38
- | Drawer | Positionen tab, button "Miete anpassen" |
39
- | Formular | `/vehicle-list`, click "Octavia" (`/vehicle-edit`) |
40
- | Wizard | `/vehicle-list`, row menu "Schritt für Schritt" (`/vehicle-wizard`), 3x "Weiter" for step 4 |
41
- | Mobile | `/campaign-list` at 390 px width |
34
+ | Mask | Route |
35
+ | -------- | ----------------------------------------------------------------------------------------------- |
36
+ | Liste | `/lease-list` (pager check: `/lease-list-short`, 3 rows) |
37
+ | Detail | click "Max Nachmieter" (`/lease-detail/00000000-0000-4000-8000-000000003103`), tab "Positionen" |
38
+ | Drawer | Positionen tab, button "Miete anpassen" |
39
+ | Formular | `/vehicle-list`, click "Octavia" (`/vehicle-edit`) |
40
+ | Wizard | `/vehicle-list`, row menu "Schritt für Schritt" (`/vehicle-wizard`), 3x "Weiter" for step 4 |
41
+ | Mobile | `/campaign-list` at 390 px width |
42
42
 
43
43
  Screenshots (light 1440x900, mobile 390x844, dark for liste and detail):
44
44
 
@@ -102,4 +102,3 @@ bun kumiko test e2e samples/apps/ui-walkthrough
102
102
 
103
103
  Six Playwright specs: smoke + create flow + update flow + 4 generated
104
104
  specs (from the registry-driven E2E generator).
105
-
@@ -16,8 +16,8 @@
16
16
  "@cosmicdrift/kumiko-renderer-mail-html": "workspace:*",
17
17
  "@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
18
18
  "@cosmicdrift/kumiko-server-runtime": "workspace:*",
19
- "react": "^19.2.6",
20
- "react-dom": "^19.2.6",
19
+ "react": "^19.3.1",
20
+ "react-dom": "^19.3.1",
21
21
  "zod": "^4.6.7",
22
22
  "@cosmicdrift/kumiko-locale-de": "workspace:*",
23
23
  "@cosmicdrift/kumiko-locale-es": "workspace:*"
@@ -12,8 +12,8 @@ from any real app. This sample is the gate.
12
12
 
13
13
  The `use-all-bundled-smoke` job guards `release`. It boots the sample with `KUMIKO_DRY_RUN_ENV=boot` (no DB) so feature-wiring bugs surface before publish.
14
14
 
15
- | Job | What it catches |
16
- |---|---|
15
+ | Job | What it catches |
16
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
17
  | `use-all-bundled-smoke` | feature-wiring bugs: `Object.entries(undefined)`, self-extension, missing-requires, schema-Validators, `composeFeatures` failures. **No DB**. Boot exits after `createRegistry`. |
18
18
 
19
19
  DB-backed boot (schema apply for every bundled feature against real Postgres + Redis, dispatcher round-trip) is gated by `src/__tests__/full-stack-boot.integration.test.ts` in the non-blocking `integration` CI-job. The prod-only paths (`assertSchemaCurrent`, `GET /health`) run in `packages/dev-server`'s `run-prod-app.integration.test.ts` — not against this sample's feature set.
@@ -25,8 +25,8 @@
25
25
  "@cosmicdrift/kumiko-renderer-puppeteer-client": "workspace:*",
26
26
  "@cosmicdrift/kumiko-server-runtime": "workspace:*",
27
27
  "@cosmicdrift/kumiko-types": "workspace:*",
28
- "react": "^19.2.6",
29
- "react-dom": "^19.2.6",
28
+ "react": "^19.3.1",
29
+ "react-dom": "^19.3.1",
30
30
  "zod": "^4.6.7",
31
31
  "@cosmicdrift/kumiko-locale-de": "workspace:*",
32
32
  "@cosmicdrift/kumiko-locale-es": "workspace:*",
@@ -232,6 +232,19 @@ function scanSamples(): Map<string, MutableFeatureRow> {
232
232
  return byFeature;
233
233
  }
234
234
 
235
+ // The docs site builds sample pages from READMEs only; a primarySample without one
236
+ // makes feature-reference link to a page that does not exist.
237
+ export function assertPrimarySampleHasReadme(
238
+ feature: string,
239
+ slug: string,
240
+ readmeExists: boolean,
241
+ ): void {
242
+ if (readmeExists) return;
243
+ throw new Error(
244
+ `sample-index.overrides.json: feature "${feature}" has primarySample "${slug}" but ${slug}/README.md does not exist. Add a README.md to that sample (the docs site only generates sample pages from READMEs) or point primarySample at a sample that has one.`,
245
+ );
246
+ }
247
+
235
248
  export function loadOverrides(): SampleIndexOverrides {
236
249
  if (!existsSync(OVERRIDES_PATH)) return {};
237
250
  return JSON.parse(readFileSync(OVERRIDES_PATH, "utf-8")) as SampleIndexOverrides;
@@ -257,7 +270,11 @@ export function buildSampleIndex(): SampleIndex {
257
270
  const primarySample = override?.primarySample ?? dedicated[0];
258
271
  let readmeSummary: string | undefined;
259
272
  if (primarySample) {
260
- readmeSummary = extractReadmeSummary(join(sampleDirFromSlug(primarySample), "README.md"));
273
+ const readmePath = join(sampleDirFromSlug(primarySample), "README.md");
274
+ if (override?.primarySample) {
275
+ assertPrimarySampleHasReadme(name, primarySample, existsSync(readmePath));
276
+ }
277
+ readmeSummary = extractReadmeSummary(readmePath);
261
278
  }
262
279
 
263
280
  features[name] = {
@@ -14,12 +14,12 @@ the integration test top-to-bottom and you've understood the contract.
14
14
  A tiny todo app where each user has private todos. The demo proves that
15
15
  DSGVO requests work end-to-end:
16
16
 
17
- | Article | Endpoint / Runner | What it does |
18
- |---------|-----------|----------------|
19
- | **Art. 15** | `user-data-rights:query:my-audit-log` | User sees own framework events (auth, deletion-request, restriction). Domain entities like todos appear in the **export bundle** (Art. 20), not the audit-log — only handlers using `ctx.events.append` show up here. |
20
- | **Art. 15+20** | `user-data-rights:write:request-export` | ZIP with user-profile + fileRefs + todos + signed download magic-link |
21
- | **Art. 17** | `user-data-rights:write:request-deletion` | Soft-delete with grace period; cron anonymizes user + deletes todos |
22
- | **Art. 18** | `user-data-rights:write:restrict-account` | Auth-middleware blocks logins until lift-restriction |
17
+ | Article | Endpoint / Runner | What it does |
18
+ | -------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | **Art. 15** | `user-data-rights:query:my-audit-log` | User sees own framework events (auth, deletion-request, restriction). Domain entities like todos appear in the **export bundle** (Art. 20), not the audit-log — only handlers using `ctx.events.append` show up here. |
20
+ | **Art. 15+20** | `user-data-rights:write:request-export` | ZIP with user-profile + fileRefs + todos + signed download magic-link |
21
+ | **Art. 17** | `user-data-rights:write:request-deletion` | Soft-delete with grace period; cron anonymizes user + deletes todos |
22
+ | **Art. 18** | `user-data-rights:write:restrict-account` | Auth-middleware blocks logins until lift-restriction |
23
23
 
24
24
  ## Architecture in 3 layers
25
25
 
@@ -80,12 +80,12 @@ cd samples/apps/user-data-rights-demo
80
80
  bun dev # → http://localhost:4291
81
81
  ```
82
82
 
83
- | Login | Value |
84
- |-------|------|
85
- | URL | `http://localhost:4291` |
86
- | Email | `admin@user-data-rights.local` |
87
- | Password | `changeme` |
88
- | Tenant | "User-Data-Rights Demo" |
83
+ | Login | Value |
84
+ | -------- | ------------------------------ |
85
+ | URL | `http://localhost:4291` |
86
+ | Email | `admin@user-data-rights.local` |
87
+ | Password | `changeme` |
88
+ | Tenant | "User-Data-Rights Demo" |
89
89
 
90
90
  In the browser, use the dispatcher to create a few todos, then call
91
91
  `user-data-rights:write:request-export` — the worker queues a job and
@@ -11,8 +11,8 @@
11
11
  "@cosmicdrift/kumiko-dev-server": "workspace:*",
12
12
  "@cosmicdrift/kumiko-renderer": "workspace:*",
13
13
  "@cosmicdrift/kumiko-framework": "workspace:*",
14
- "react": "^19.2.6",
15
- "react-dom": "^19.2.6",
14
+ "react": "^19.3.1",
15
+ "react-dom": "^19.3.1",
16
16
  "@cosmicdrift/kumiko-locale-de": "workspace:*"
17
17
  },
18
18
  "devDependencies": {
@@ -10,9 +10,9 @@ spec also paints a product-board mock (`hero-app.png`), uses it in the hero
10
10
 
11
11
  `buildLandingPage`'s `head.schemaJson` combines `organizationSchema` +
12
12
  `webPageSchema` (from the `seo` feature) into one `@graph` block — Organization
13
- + WebPage nodes in a single `<script type="application/ld+json">`, which
14
- `renderApexPage` already knows how to emit. This is the seam an app extends
15
- with `faqPageSchema` for an FAQ section, or its own custom schema.org type.
13
+ - WebPage nodes in a single `<script type="application/ld+json">`, which
14
+ `renderApexPage` already knows how to emit. This is the seam an app extends
15
+ with `faqPageSchema` for an FAQ section, or its own custom schema.org type.
16
16
 
17
17
  ## Site discovery: sitemap.xml / llms.txt
18
18
 
@@ -12,6 +12,6 @@
12
12
  },
13
13
  "devDependencies": {
14
14
  "@cosmicdrift/kumiko-testing": "workspace:*",
15
- "@playwright/test": "^1.62.1"
15
+ "@playwright/test": "^1.64.1"
16
16
  }
17
17
  }
@@ -28,11 +28,15 @@ A typical app has two mounts sharing locale and primitives:
28
28
 
29
29
  ```tsx illustration
30
30
  import {
31
- ForgotPasswordScreen, SignupScreen, createLoginRoute,
32
- AuthShellProvider, emailPasswordClient,
31
+ ForgotPasswordScreen,
32
+ SignupScreen,
33
+ createLoginRoute,
34
+ AuthShellProvider,
35
+ emailPasswordClient,
33
36
  } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
34
37
  import {
35
- RequestAccountDeletionScreen, ConfirmAccountDeletionScreen,
38
+ RequestAccountDeletionScreen,
39
+ ConfirmAccountDeletionScreen,
36
40
  } from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
37
41
  import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
38
42
 
@@ -43,7 +47,7 @@ import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
43
47
  const LoginRoute = createLoginRoute({ loginScreenProps: { signupHref: "/signup" } });
44
48
 
45
49
  createPublicSurface({
46
- clientFeatures: [emailPasswordClient()], // SessionProvider + i18n
50
+ clientFeatures: [emailPasswordClient()], // SessionProvider + i18n
47
51
  shell: ({ children }) => (
48
52
  <MarketingChrome>
49
53
  <AuthShellProvider shell={(card) => <div className="py-12 flex justify-center">{card}</div>}>
@@ -52,10 +56,10 @@ createPublicSurface({
52
56
  </MarketingChrome>
53
57
  ),
54
58
  routes: [
55
- { path: "/login", component: <LoginRoute /> },
56
- { path: "/signup", component: <SignupScreen loginHref="/login" /> },
59
+ { path: "/login", component: <LoginRoute /> },
60
+ { path: "/signup", component: <SignupScreen loginHref="/login" /> },
57
61
  { path: "/forgot-password", component: <ForgotPasswordScreen loginHref="/login" /> },
58
- { path: "/delete-account", component: <RequestAccountDeletionScreen /> },
62
+ { path: "/delete-account", component: <RequestAccountDeletionScreen /> },
59
63
  { path: "/delete-account/confirm", component: <ConfirmAccountDeletionScreen /> },
60
64
  ],
61
65
  fallback: <LoginRoute />,
@@ -90,8 +94,8 @@ runProdApp({
90
94
 
91
95
  GDPR Art. 17 applies when the user **cannot log in** (lockout). The flow is
92
96
  email-verified (magic link), not login-gated. The HMAC token carries `userId`
93
- + expiry (no DB table); purpose `"deletion-request"` blocks replay on other
94
- token endpoints. Second confirm is idempotent (`user_not_in_active_state`).
97
+ - expiry (no DB table); purpose `"deletion-request"` blocks replay on other
98
+ token endpoints. Second confirm is idempotent (`user_not_in_active_state`).
95
99
 
96
100
  ## Tests
97
101
 
@@ -22,9 +22,7 @@ const mfaFeature = createAuthMfaFeature({
22
22
  issuer: "Your App",
23
23
  });
24
24
  const sessionCallbacks = createSessionCallbacks({ db });
25
- bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(
26
- sessionCallbacks.sessionRevokeAllOthers,
27
- );
25
+ bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(sessionCallbacks.sessionRevokeAllOthers);
28
26
 
29
27
  const server = buildServer({
30
28
  registry: buildRegistry([
@@ -37,7 +35,7 @@ const server = buildServer({
37
35
  mfaFeature,
38
36
  // ... other features
39
37
  ]),
40
- context: { db, /* ... */ },
38
+ context: { db /* ... */ },
41
39
  jwtSecret: process.env.JWT_SECRET,
42
40
  auth: {
43
41
  membershipQuery: "tenant:query:memberships",
@@ -6,10 +6,10 @@
6
6
  "@cosmicdrift/kumiko-bundled-features": "workspace:*",
7
7
  "@cosmicdrift/kumiko-framework": "workspace:*",
8
8
  "@cosmicdrift/kumiko-renderer": "workspace:*",
9
- "react": "^19.2.6"
9
+ "react": "^19.3.1"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@cosmicdrift/kumiko-testing": "workspace:*",
13
- "@types/react": "^19.2.14"
13
+ "@types/react": "^19.3.0"
14
14
  }
15
15
  }
@@ -12,7 +12,7 @@ never mounted, and nothing caught it — until now.
12
12
  ctx exposing every mounted feature. Throw to fail the boot with a clear,
13
13
  feature-prefixed message.
14
14
  - **Conditional invariants `r.requires` can't express** — the check only
15
- fails when *this* feature's own shape demands it (has a PII field), not
15
+ fails when _this_ feature's own shape demands it (has a PII field), not
16
16
  unconditionally. A plain `r.requires({ features: ["user-data-hook"] })` would fail even
17
17
  for a prompt-store variant with no PII fields at all.
18
18
  - **`validateBoot`** — the framework's boot-time validator, run directly
@@ -38,7 +38,7 @@ user-data-hook → companion feature the check requires when PII is present
38
38
 
39
39
  ## When to reach for it
40
40
 
41
- You're writing a feature whose validity depends on *another* feature being
41
+ You're writing a feature whose validity depends on _another_ feature being
42
42
  mounted, but only under a condition your feature alone can evaluate (e.g.
43
43
  "only if I have PII fields", "only if I expose more than N screens"). If the
44
44
  requirement is unconditional, `r.requires`/`r.optionalRequires` is simpler
@@ -8,11 +8,11 @@ branches in your domain handlers.
8
8
  How a multi-tenant platform uses `compliance-profiles` to deliver different
9
9
  regulatory behaviour per tenant:
10
10
 
11
- | Tenant | Profile | Supervisory authority | Languages | Tenant-destroy grace |
12
- |---|---|---|---|---|
13
- | **A** (DACH) | `eu-dsgvo` | BlnBDI Berlin | de / en | 30 days |
14
- | **B** (Switzerland) | `swiss-dsg` | EDÖB Bern | de / fr / it / en | 30 days |
15
- | **C** (DACH-HR) | `de-hr-dsgvo-hgb` | State DPA | de | 60 days (HR override) |
11
+ | Tenant | Profile | Supervisory authority | Languages | Tenant-destroy grace |
12
+ | ------------------- | ----------------- | --------------------- | ----------------- | --------------------- |
13
+ | **A** (DACH) | `eu-dsgvo` | BlnBDI Berlin | de / en | 30 days |
14
+ | **B** (Switzerland) | `swiss-dsg` | EDÖB Bern | de / fr / it / en | 30 days |
15
+ | **C** (DACH-HR) | `de-hr-dsgvo-hgb` | State DPA | de | 60 days (HR override) |
16
16
 
17
17
  Plus tenant B with an override (`gracePeriod: { days: 90 }`) extending
18
18
  user-rights grace without losing other profile fields — deep-merge on the
@@ -28,14 +28,14 @@ the option.
28
28
 
29
29
  ## Boot requirements
30
30
 
31
- | Stack | Boot option | Also satisfied by |
32
- | --- | --- | --- |
33
- | `composeMailStack` | `mail` | a mounted `channel-email` |
34
- | `composeFileStack` without providers | `fileStorage` | a mounted file provider |
35
- | `composeGdprStack` | `kmsMode` | `kms` or `allowPlaintextPii` |
36
- | `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail` | a mounted `channel-email` |
37
- | `composeSecurityStack` | `kmsMode`, `rateLimit` not `false` | `kms` or `allowPlaintextPii` |
38
- | `composeSaasStack({ billing: "stripe" })` | `billing` (warning only) | a mounted subscription provider |
31
+ | Stack | Boot option | Also satisfied by |
32
+ | ----------------------------------------------------------------- | ---------------------------------- | ------------------------------- |
33
+ | `composeMailStack` | `mail` | a mounted `channel-email` |
34
+ | `composeFileStack` without providers | `fileStorage` | a mounted file provider |
35
+ | `composeGdprStack` | `kmsMode` | `kms` or `allowPlaintextPii` |
36
+ | `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail` | a mounted `channel-email` |
37
+ | `composeSecurityStack` | `kmsMode`, `rateLimit` not `false` | `kms` or `allowPlaintextPii` |
38
+ | `composeSaasStack({ billing: "stripe" })` | `billing` (warning only) | a mounted subscription provider |
39
39
 
40
40
  A missing option fails the boot with:
41
41