@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +25 -25
- package/packages/bundled-features/src/account-security/README.md +7 -5
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -0
- package/packages/bundled-features/src/documents/changes.json +8 -0
- package/packages/bundled-features/src/host-routing/README.md +10 -10
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +1 -0
- package/packages/bundled-features/src/legal-pages/README.md +21 -13
- package/packages/bundled-features/src/renderer-foundation/README.md +6 -4
- package/packages/bundled-features/src/renderer-pdf/README.md +5 -1
- package/packages/bundled-features/src/template-resolver/README.md +29 -29
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -2
- package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +1 -0
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +36 -36
- package/packages/bundled-features/src/user-data-rights/README.md +33 -33
- package/packages/bundled-features/src/user-data-rights/web/public-deletion-gate.tsx +1 -0
- package/samples/apps/admin-console/README.md +5 -5
- package/samples/apps/admin-console/package.json +3 -3
- package/samples/apps/cap-billing-demo/README.md +15 -15
- package/samples/apps/config-demo/README.md +7 -7
- package/samples/apps/config-demo/package.json +2 -2
- package/samples/apps/marketing-demo/README.md +2 -2
- package/samples/apps/marketing-demo/package.json +2 -2
- package/samples/apps/showcase/package.json +3 -3
- package/samples/apps/styleguide/package.json +2 -2
- package/samples/apps/ui-walkthrough/README.md +8 -9
- package/samples/apps/ui-walkthrough/package.json +2 -2
- package/samples/apps/use-all-bundled/README.md +2 -2
- package/samples/apps/use-all-bundled/package.json +2 -2
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +18 -1
- package/samples/apps/user-data-rights-demo/README.md +12 -12
- package/samples/apps/workspaces/package.json +2 -2
- package/samples/recipes/apex-landing/README.md +3 -3
- package/samples/recipes/apex-landing/package.json +1 -1
- package/samples/recipes/apex-surface-auth/README.md +13 -9
- package/samples/recipes/auth-mfa/README.md +2 -4
- package/samples/recipes/auth-mfa/package.json +2 -2
- package/samples/recipes/boot-check/README.md +2 -2
- package/samples/recipes/compliance-profiles-demo/README.md +5 -5
- package/samples/recipes/compose-stacks/README.md +8 -8
- package/samples/recipes/cross-feature-events/README.md +1 -0
- package/samples/recipes/crypto-shredding-hr/README.md +3 -3
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/documents/README.md +25 -0
- package/samples/recipes/embedded-entity-form/package.json +2 -2
- package/samples/recipes/encrypted-tenant-config/README.md +8 -8
- package/samples/recipes/env-composition/README.md +10 -10
- package/samples/recipes/error-contract/README.md +9 -9
- package/samples/recipes/event-sourcing/README.md +9 -9
- package/samples/recipes/ledger-invoicing/README.md +6 -6
- package/samples/recipes/legal-pages/README.md +16 -16
- package/samples/recipes/managed-config/README.md +15 -11
- package/samples/recipes/managed-pages/README.md +7 -7
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pipeline-basics/README.md +13 -13
- package/samples/recipes/public-share-token/README.md +4 -4
- package/samples/recipes/record-detail-layout/package.json +3 -3
- package/samples/recipes/security-baseline/README.md +4 -1
- package/samples/recipes/session-revocation/README.md +1 -1
- package/samples/recipes/tenant-settings/README.md +4 -4
- package/samples/recipes/tier-admin/README.md +2 -2
- package/samples/recipes/user-data-rights/README.md +21 -19
- package/samples/recipes/wizard-form/package.json +3 -3
- 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
|
|
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
|
|
46
|
-
|
|
47
|
-
| Art. 15
|
|
48
|
-
| Art. 15+20 | `user-data-rights:write:request-export`
|
|
49
|
-
| Art. 17
|
|
50
|
-
| Art. 17
|
|
51
|
-
| Art. 18
|
|
52
|
-
| Art. 18
|
|
53
|
-
| Operator
|
|
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
|
|
81
|
-
|
|
82
|
-
| `kumiko_events`
|
|
83
|
-
| `read_export_jobs`
|
|
84
|
-
| `read_export_download_tokens` | Magic-Link-Hash + TTL + lastUsed-Audit
|
|
85
|
-
| `read_download_attempts`
|
|
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
|
|
92
|
-
|
|
93
|
-
| `audit-log.integration.test.ts`
|
|
94
|
-
| `cross-data-matrix.integration.test.ts`
|
|
95
|
-
| `download.integration.test.ts`
|
|
96
|
-
| `request-export.integration.test.ts`
|
|
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`
|
|
99
|
-
| `run-{export-jobs,forget-cleanup,user-export}.integration.test.ts`
|
|
100
|
-
| `policy-to-strategy.test.ts`
|
|
101
|
-
| `user-data-rights.integration.test.ts`
|
|
102
|
-
| `token-helpers.test.ts` + `zip-path.test.ts`
|
|
103
|
-
| `export-job-{idempotency,schema}.test.ts`
|
|
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
|
|
28
|
-
|
|
29
|
-
| `sysadmin@admin-console.local`
|
|
30
|
-
| `tenant-admin@admin-console.local` | tenant `TenantAdmin`
|
|
31
|
-
| `regular@admin-console.local`
|
|
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.
|
|
15
|
-
"react": "^19.
|
|
16
|
-
"react-dom": "^19.
|
|
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
|
|
14
|
-
| pro
|
|
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
|
|
91
|
-
|
|
92
|
-
| URL
|
|
93
|
-
| Email
|
|
94
|
-
| Password | `changeme`
|
|
95
|
-
| Tenant
|
|
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
|
|
114
|
-
|
|
115
|
-
| `mail-transport-inmemory`
|
|
116
|
-
| Hardcoded `DEMO_TIER_MAP`
|
|
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)
|
|
119
|
-
| Newsletter domain
|
|
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
|
|
30
|
-
|
|
31
|
-
| `siteName`
|
|
32
|
-
| `themeColor`
|
|
33
|
-
| `maxUploadSize`
|
|
34
|
-
| `emailNotifications` | user
|
|
35
|
-
| `autoApprove`
|
|
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.
|
|
17
|
-
"react-dom": "^19.
|
|
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
|
-
|
|
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.
|
|
18
|
-
"react-dom": "^19.
|
|
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.
|
|
17
|
-
"react": "^19.
|
|
18
|
-
"react-dom": "^19.
|
|
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.
|
|
9
|
-
"react-dom": "^19.
|
|
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
|
|
35
|
-
|
|
|
36
|
-
| Liste
|
|
37
|
-
| Detail
|
|
38
|
-
| Drawer
|
|
39
|
-
| Formular | `/vehicle-list`, click "Octavia" (`/vehicle-edit`)
|
|
40
|
-
| Wizard
|
|
41
|
-
| Mobile
|
|
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.
|
|
20
|
-
"react-dom": "^19.
|
|
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
|
|
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.
|
|
29
|
-
"react-dom": "^19.
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
| **Art. 15**
|
|
20
|
-
| **Art. 15+20** | `user-data-rights:write:request-export`
|
|
21
|
-
| **Art. 17**
|
|
22
|
-
| **Art. 18**
|
|
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
|
|
84
|
-
|
|
85
|
-
| URL
|
|
86
|
-
| Email
|
|
87
|
-
| Password | `changeme`
|
|
88
|
-
| Tenant
|
|
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.
|
|
15
|
-
"react-dom": "^19.
|
|
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
|
-
|
|
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
|
|
|
@@ -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,
|
|
32
|
-
|
|
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,
|
|
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()],
|
|
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",
|
|
56
|
-
{ path: "/signup",
|
|
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",
|
|
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
|
-
|
|
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.
|
|
9
|
+
"react": "^19.3.1"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@cosmicdrift/kumiko-testing": "workspace:*",
|
|
13
|
-
"@types/react": "^19.
|
|
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
|
|
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
|
|
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
|
|
12
|
-
|
|
13
|
-
| **A** (DACH)
|
|
14
|
-
| **B** (Switzerland) | `swiss-dsg`
|
|
15
|
-
| **C** (DACH-HR)
|
|
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
|
|
32
|
-
|
|
|
33
|
-
| `composeMailStack`
|
|
34
|
-
| `composeFileStack` without providers
|
|
35
|
-
| `composeGdprStack`
|
|
36
|
-
| `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail`
|
|
37
|
-
| `composeSecurityStack`
|
|
38
|
-
| `composeSaasStack({ billing: "stripe" })`
|
|
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
|
|