@cosmicdrift/kumiko-samples 0.404.0 → 0.405.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) 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/changes.json +6 -0
  5. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +11 -47
  6. package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +4 -0
  7. package/packages/bundled-features/src/auth-email-password/web/default-topbar-actions.tsx +9 -1
  8. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  9. package/packages/bundled-features/src/auth-email-password/web/invite-accept-screen.tsx +10 -41
  10. package/packages/bundled-features/src/auth-email-password/web/tenant-label.ts +15 -0
  11. package/packages/bundled-features/src/auth-email-password/web/tenant-menu-items.tsx +2 -7
  12. package/packages/bundled-features/src/auth-email-password/web/tenant-switcher.tsx +7 -12
  13. package/packages/bundled-features/src/auth-email-password/web/use-redirect-authed-from-login.ts +39 -0
  14. package/packages/bundled-features/src/auth-foundation/feature.ts +8 -21
  15. package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
  16. package/packages/bundled-features/src/auth-mfa/handlers/reencrypt.job.ts +9 -15
  17. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +17 -14
  18. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +6 -8
  19. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +6 -8
  20. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +16 -57
  21. package/packages/bundled-features/src/billing-foundation/changes.json +6 -0
  22. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +3 -13
  23. package/packages/bundled-features/src/billing-foundation/feature.ts +1 -5
  24. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -8
  25. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +2 -8
  26. package/packages/bundled-features/src/billing-foundation/index.ts +8 -0
  27. package/packages/bundled-features/src/billing-foundation/projection.ts +19 -33
  28. package/packages/bundled-features/src/billing-foundation/provider-event-seen.ts +14 -0
  29. package/packages/bundled-features/src/billing-foundation/provider-shared.ts +61 -0
  30. package/packages/bundled-features/src/billing-foundation/tenant-destroy-hook.ts +8 -4
  31. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  32. package/packages/bundled-features/src/config/handlers/reencrypt.job.ts +9 -15
  33. package/packages/bundled-features/src/delivery/delivery-service.ts +2 -10
  34. package/packages/bundled-features/src/documents/changes.json +8 -0
  35. package/packages/bundled-features/src/file-derivatives/feature.ts +9 -21
  36. package/packages/bundled-features/src/file-foundation/feature.ts +2 -6
  37. package/packages/bundled-features/src/files-tenant-data/hooks.ts +1 -6
  38. package/packages/bundled-features/src/folders/feature.ts +2 -8
  39. package/packages/bundled-features/src/folders/web/folder-manager.tsx +7 -32
  40. package/packages/bundled-features/src/folders/web/folder-section.tsx +5 -25
  41. package/packages/bundled-features/src/form-draft/handlers/save.write.ts +6 -16
  42. package/packages/bundled-features/src/host-routing/README.md +10 -10
  43. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +2 -6
  44. package/packages/bundled-features/src/inbound-mail-foundation/handlers/thread-rollup.ts +1 -0
  45. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +5 -13
  46. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +9 -19
  47. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +1 -15
  48. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +1 -14
  49. package/packages/bundled-features/src/jobs/job-run-logger.ts +37 -40
  50. package/packages/bundled-features/src/jobs/parse-subject.ts +15 -0
  51. package/packages/bundled-features/src/ledger/feature.ts +2 -7
  52. package/packages/bundled-features/src/legal-pages/README.md +21 -13
  53. package/packages/bundled-features/src/mail-foundation/feature.ts +3 -7
  54. package/packages/bundled-features/src/managed-pages/render-page-body.ts +4 -12
  55. package/packages/bundled-features/src/notes-history/feature.ts +7 -8
  56. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +1 -9
  57. package/packages/bundled-features/src/personal-access-tokens/hash.ts +2 -2
  58. package/packages/bundled-features/src/personal-access-tokens/parse-scope-names.ts +9 -0
  59. package/packages/bundled-features/src/personal-access-tokens/resolver.ts +1 -9
  60. package/packages/bundled-features/src/renderer-foundation/README.md +6 -4
  61. package/packages/bundled-features/src/renderer-foundation/feature.ts +3 -16
  62. package/packages/bundled-features/src/renderer-pdf/README.md +5 -1
  63. package/packages/bundled-features/src/secrets/handlers/rotate.job.ts +13 -19
  64. package/packages/bundled-features/src/shared/changes.json +6 -0
  65. package/packages/bundled-features/src/shared/collect-extension-options.ts +14 -0
  66. package/packages/bundled-features/src/shared/hmac-signature.ts +15 -0
  67. package/packages/bundled-features/src/shared/index.ts +6 -0
  68. package/packages/bundled-features/src/shared/is-unique-constraint-failure.ts +11 -0
  69. package/packages/bundled-features/src/shared/mail-locale-templates.ts +64 -0
  70. package/packages/bundled-features/src/shared/resolve-feature-access.ts +16 -0
  71. package/packages/bundled-features/src/shared/signed-token.ts +4 -14
  72. package/packages/bundled-features/src/shared/single-use-token-store.ts +2 -2
  73. package/packages/bundled-features/src/shared/stream-tenant-derived-state.ts +5 -8
  74. package/packages/bundled-features/src/shared/tenant-db-cache.ts +19 -0
  75. package/packages/bundled-features/src/shared/web/status-banner.tsx +20 -0
  76. package/packages/bundled-features/src/shared/web/use-dispatched-writes.ts +31 -0
  77. package/packages/bundled-features/src/subscription-paddle/feature.ts +5 -17
  78. package/packages/bundled-features/src/subscription-paddle/price-methods.ts +7 -29
  79. package/packages/bundled-features/src/subscription-paddle/runtime.ts +6 -17
  80. package/packages/bundled-features/src/subscription-stripe/feature.ts +5 -19
  81. package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +5 -20
  82. package/packages/bundled-features/src/subscription-stripe/runtime.ts +3 -19
  83. package/packages/bundled-features/src/tags/feature.ts +2 -8
  84. package/packages/bundled-features/src/tags/web/tag-manager.tsx +5 -25
  85. package/packages/bundled-features/src/tags/web/tag-section.tsx +3 -18
  86. package/packages/bundled-features/src/template-resolver/README.md +29 -29
  87. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -2
  88. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +3 -10
  89. package/packages/bundled-features/src/template-resolver/web/collection-entry-create-form.tsx +1 -0
  90. package/packages/bundled-features/src/tenant/handlers/decrypt-member-users.ts +30 -0
  91. package/packages/bundled-features/src/tenant/handlers/members.query.ts +3 -23
  92. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +8 -20
  93. package/packages/bundled-features/src/tenant-lifecycle/archive-and-delete-tenant-rows.ts +28 -0
  94. package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
  95. package/packages/bundled-features/src/tenant-lifecycle/index.ts +5 -0
  96. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +1 -0
  97. package/packages/bundled-features/src/tier-engine/feature.ts +1 -1
  98. package/packages/bundled-features/src/user-data-rights/COMPLIANCE.md +36 -36
  99. package/packages/bundled-features/src/user-data-rights/README.md +33 -33
  100. package/packages/bundled-features/src/user-data-rights/build-hook-db.ts +40 -0
  101. package/packages/bundled-features/src/user-data-rights/handlers/request-export.write.ts +2 -19
  102. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +11 -47
  103. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -35
  104. package/packages/bundled-features/src/user-data-rights/run-user-export.ts +2 -31
  105. package/packages/bundled-features/src/user-data-rights/token-helpers.ts +3 -15
  106. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +1 -18
  107. package/packages/bundled-features/src/user-data-rights/web/public-deletion-gate.tsx +1 -0
  108. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +1 -18
  109. package/samples/apps/admin-console/README.md +5 -5
  110. package/samples/apps/admin-console/package.json +3 -3
  111. package/samples/apps/cap-billing-demo/README.md +15 -15
  112. package/samples/apps/config-demo/README.md +7 -7
  113. package/samples/apps/config-demo/package.json +2 -2
  114. package/samples/apps/marketing-demo/README.md +2 -2
  115. package/samples/apps/marketing-demo/package.json +2 -2
  116. package/samples/apps/showcase/package.json +3 -3
  117. package/samples/apps/styleguide/package.json +2 -2
  118. package/samples/apps/ui-walkthrough/README.md +8 -9
  119. package/samples/apps/ui-walkthrough/package.json +2 -2
  120. package/samples/apps/use-all-bundled/README.md +2 -2
  121. package/samples/apps/use-all-bundled/package.json +2 -2
  122. package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +18 -1
  123. package/samples/apps/user-data-rights-demo/README.md +12 -12
  124. package/samples/apps/workspaces/package.json +2 -2
  125. package/samples/recipes/apex-landing/README.md +3 -3
  126. package/samples/recipes/apex-landing/package.json +1 -1
  127. package/samples/recipes/apex-surface-auth/README.md +13 -9
  128. package/samples/recipes/auth-mfa/README.md +2 -4
  129. package/samples/recipes/auth-mfa/package.json +2 -2
  130. package/samples/recipes/boot-check/README.md +2 -2
  131. package/samples/recipes/compliance-profiles-demo/README.md +5 -5
  132. package/samples/recipes/compose-stacks/README.md +8 -8
  133. package/samples/recipes/cross-feature-events/README.md +1 -0
  134. package/samples/recipes/crypto-shredding-hr/README.md +3 -3
  135. package/samples/recipes/custom-fields-basic/README.md +1 -1
  136. package/samples/recipes/documents/README.md +25 -0
  137. package/samples/recipes/embedded-entity-form/package.json +2 -2
  138. package/samples/recipes/encrypted-tenant-config/README.md +8 -8
  139. package/samples/recipes/env-composition/README.md +10 -10
  140. package/samples/recipes/error-contract/README.md +9 -9
  141. package/samples/recipes/event-sourcing/README.md +9 -9
  142. package/samples/recipes/ledger-invoicing/README.md +6 -6
  143. package/samples/recipes/legal-pages/README.md +16 -16
  144. package/samples/recipes/managed-config/README.md +15 -11
  145. package/samples/recipes/managed-pages/README.md +7 -7
  146. package/samples/recipes/pdf-audit-export/package.json +1 -1
  147. package/samples/recipes/pipeline-basics/README.md +13 -13
  148. package/samples/recipes/public-share-token/README.md +4 -4
  149. package/samples/recipes/record-detail-layout/package.json +3 -3
  150. package/samples/recipes/security-baseline/README.md +4 -1
  151. package/samples/recipes/session-revocation/README.md +1 -1
  152. package/samples/recipes/tenant-settings/README.md +4 -4
  153. package/samples/recipes/tier-admin/README.md +2 -2
  154. package/samples/recipes/user-data-rights/README.md +21 -19
  155. package/samples/recipes/wizard-form/package.json +3 -3
  156. package/samples/recipes/writeform-section/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { createFallbackLogger, type Logger } from "@cosmicdrift/kumiko-framework/logging";
1
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
2
2
  // User data export pipeline — GDPR Art. 15 (access) +
3
3
  // Art. 20 (data portability).
4
4
  //
@@ -29,7 +29,6 @@ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
29
29
  import {
30
30
  collectEncryptedFieldNames,
31
31
  configuredEntityFieldEncryption,
32
- createTenantDb,
33
32
  decryptEntityFieldValues,
34
33
  selectMany,
35
34
  } from "@cosmicdrift/kumiko-framework/db";
@@ -44,12 +43,9 @@ import {
44
43
  type UserDataExportSnippet,
45
44
  } from "@cosmicdrift/kumiko-framework/engine";
46
45
  import { runInSystemRowLevelSecurityScope } from "@cosmicdrift/kumiko-framework/internal/row-level-security";
47
- import {
48
- createEscapeHatchReporter,
49
- UNATTRIBUTED_ACTOR,
50
- } from "@cosmicdrift/kumiko-framework/pipeline";
51
46
  import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
52
47
  import { tenantMembershipsTable } from "../tenant/index.ts";
48
+ import { buildHookDb } from "./build-hook-db.ts";
53
49
  import { buildFileRefZipPath } from "./zip-path.ts";
54
50
 
55
51
  type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
@@ -108,31 +104,6 @@ interface HookEntry {
108
104
  readonly escapeHatchReason: string | undefined;
109
105
  }
110
106
 
111
- function buildHookDb(
112
- db: DbRunner,
113
- tenantId: TenantId,
114
- entry: HookEntry,
115
- args: Pick<RunUserExportArgs, "escapeHatchAuditSink" | "escapeHatchAuditLog" | "actor">,
116
- ) {
117
- const report = createEscapeHatchReporter({
118
- handler: `${EXT_USER_DATA}:${entry.entityName}`,
119
- tenantId,
120
- actor: args.actor ?? UNATTRIBUTED_ACTOR,
121
- sink: args.escapeHatchAuditSink,
122
- log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
123
- });
124
- return createTenantDb({
125
- db,
126
- tenantId,
127
- mode: "tenant",
128
- grants: {
129
- unsafeRaw:
130
- entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
131
- report,
132
- },
133
- });
134
- }
135
-
136
107
  /**
137
108
  * Pure function: invokes all EXT_USER_DATA hooks for each tenant
138
109
  * membership and collects snippets into a UserExportBundle.
@@ -13,10 +13,8 @@
13
13
  // **Multi-use within TTL:** The token is NOT "consumed" on download, so the user
14
14
  // can download repeatedly until expiresAt. This matches Google-Takeout (7d) and
15
15
  // Facebook-Data-Download (4d).
16
- //
17
- // **Universal Web-Crypto API:** crypto.getRandomValues + crypto.subtle.digest
18
- // runs identically in Bun, Node 19+, and browsers, avoiding platform-specific
19
- // imports.
16
+
17
+ import { hashOpaqueToken } from "@cosmicdrift/kumiko-framework/crypto";
20
18
 
21
19
  /**
22
20
  * Generates a new download token. The plain token is sent to the user by email;
@@ -41,9 +39,7 @@ export async function generateDownloadToken(): Promise<{
41
39
  * database (no constant-time comparison, see the header comment).
42
40
  */
43
41
  export async function hashDownloadToken(plain: string): Promise<string> {
44
- const encoded = new TextEncoder().encode(plain);
45
- const digest = await crypto.subtle.digest("SHA-256", encoded);
46
- return uint8ArrayToHex(new Uint8Array(digest));
42
+ return hashOpaqueToken(plain);
47
43
  }
48
44
 
49
45
  function uint8ArrayToBase64Url(bytes: Uint8Array): string {
@@ -54,11 +50,3 @@ function uint8ArrayToBase64Url(bytes: Uint8Array): string {
54
50
  }
55
51
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
56
52
  }
57
-
58
- function uint8ArrayToHex(bytes: Uint8Array): string {
59
- let out = "";
60
- for (let i = 0; i < bytes.length; i++) {
61
- out += (bytes[i] as number).toString(16).padStart(2, "0"); // @cast-boundary dynamic-key
62
- }
63
- return out;
64
- }
@@ -19,6 +19,7 @@ import {
19
19
  import { postWithDownload } from "@cosmicdrift/kumiko-renderer/web";
20
20
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
21
21
  import { type ReactNode, useEffect, useState } from "react";
22
+ import { type SectionStatus, StatusBanner } from "../../shared/web/status-banner.tsx";
22
23
  import {
23
24
  EXPORT_JOB_STATUS,
24
25
  type ExportJobStatus,
@@ -41,12 +42,6 @@ type ExportStatusResult =
41
42
  | { readonly hasJob: false }
42
43
  | { readonly hasJob: true; readonly job: ExportJob };
43
44
 
44
- type SectionStatus =
45
- | { kind: "idle" }
46
- | { kind: "submitting" }
47
- | { kind: "success"; messageKey: string }
48
- | { kind: "error"; messageKey: string };
49
-
50
45
  // Dispatcher failures include i18nKey only when the handler sets one; read it
51
46
  // at the boundary with a generic fallback.
52
47
  function failureKey(error: unknown): string {
@@ -63,18 +58,6 @@ export function formatDate(iso: string | null | undefined): string {
63
58
  return tIndex > 0 ? iso.slice(0, tIndex) : iso;
64
59
  }
65
60
 
66
- function StatusBanner({ status }: { readonly status: SectionStatus }): ReactNode {
67
- const t = useTranslation();
68
- const { Banner } = usePrimitives();
69
- if (status.kind === "success") {
70
- return <Banner variant="info">{t(status.messageKey)}</Banner>;
71
- }
72
- if (status.kind === "error") {
73
- return <Banner variant="error">{t(status.messageKey)}</Banner>;
74
- }
75
- return null;
76
- }
77
-
78
61
  // Mounted by the renderer's ExtensionSectionMount (already wrapped in
79
62
  // <Section title={...} testId="section-extension-...">) — takes no props of
80
63
  // its own, ExtensionSectionProps (entityName/entityId/...) are unused since
@@ -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 />);
@@ -16,14 +16,9 @@ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
16
16
  import { type ReactNode, useState } from "react";
17
17
  import { AuthHandlers } from "../../auth-email-password/web/constants.ts";
18
18
  import { requestEmailVerification } from "../../auth-email-password/web/index.ts";
19
+ import { type SectionStatus, StatusBanner } from "../../shared/web/status-banner.tsx";
19
20
  import { UserProfileHandlers } from "./constants.ts";
20
21
 
21
- type SectionStatus =
22
- | { kind: "idle" }
23
- | { kind: "submitting" }
24
- | { kind: "success"; messageKey: string }
25
- | { kind: "error"; messageKey: string };
26
-
27
22
  // Dispatcher failures carry an i18nKey only when the handler sets one —
28
23
  // boundary read with a generic fallback.
29
24
  function failureKey(error: unknown): string {
@@ -31,18 +26,6 @@ function failureKey(error: unknown): string {
31
26
  return typeof key === "string" ? key : "profile.errors.generic";
32
27
  }
33
28
 
34
- function StatusBanner({ status }: { readonly status: SectionStatus }): ReactNode {
35
- const t = useTranslation();
36
- const { Banner } = usePrimitives();
37
- if (status.kind === "success") {
38
- return <Banner variant="info">{t(status.messageKey)}</Banner>;
39
- }
40
- if (status.kind === "error") {
41
- return <Banner variant="error">{t(status.messageKey)}</Banner>;
42
- }
43
- return null;
44
- }
45
-
46
29
  // Mounted by the renderer's ExtensionSectionMount (already wrapped in
47
30
  // <Section title={...} testId="section-extension-...">) — takes no props of
48
31
  // its own. Deliberately NOT a <Form>: RenderEdit already renders a host
@@ -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