@cosmicdrift/kumiko-bundled-features 0.2.3 → 0.4.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 (127) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/package.json +19 -14
  3. package/src/auth-email-password/handlers/change-password.write.ts +1 -1
  4. package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
  5. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
  6. package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
  7. package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
  8. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
  9. package/src/auth-email-password/handlers/login.write.ts +1 -1
  10. package/src/auth-email-password/handlers/logout.write.ts +2 -2
  11. package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
  12. package/src/auth-email-password/web/auth-client.ts +1 -1
  13. package/src/billing-foundation/events.ts +1 -1
  14. package/src/billing-foundation/feature.ts +44 -47
  15. package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
  16. package/src/billing-foundation/handlers/process-event.write.ts +3 -3
  17. package/src/billing-foundation/projection.ts +1 -1
  18. package/src/billing-foundation/webhook-handler.ts +1 -1
  19. package/src/cap-counter/constants.ts +1 -1
  20. package/src/cap-counter/enforce-cap.ts +1 -1
  21. package/src/cap-counter/feature.ts +3 -7
  22. package/src/cap-counter/handlers/get-counter.query.ts +1 -1
  23. package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
  24. package/src/cap-counter/handlers/increment.write.ts +3 -3
  25. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  26. package/src/channel-email/email-channel.ts +1 -1
  27. package/src/channel-email/types.ts +1 -1
  28. package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -6
  29. package/src/compliance-profiles/handlers/needs-profile.query.ts +1 -1
  30. package/src/compliance-profiles/handlers/set-profile.write.ts +6 -8
  31. package/src/compliance-profiles/resolve-for-tenant.ts +7 -5
  32. package/src/compliance-profiles/seeding.ts +1 -1
  33. package/src/config/resolver.ts +1 -1
  34. package/src/data-retention/_internal/parse-override.ts +3 -2
  35. package/src/data-retention/handlers/policy-for.query.ts +1 -1
  36. package/src/data-retention/keep-for.ts +1 -1
  37. package/src/data-retention/presets.ts +1 -1
  38. package/src/data-retention/resolve-for-tenant.ts +1 -1
  39. package/src/delivery/__tests__/delivery.integration.ts +6 -0
  40. package/src/delivery/delivery-service.ts +4 -12
  41. package/src/delivery/feature.ts +7 -5
  42. package/src/delivery/index.ts +0 -1
  43. package/src/delivery/testing.ts +1 -2
  44. package/src/delivery/upsert-preference.ts +1 -1
  45. package/src/feature-toggles/feature.ts +1 -1
  46. package/src/feature-toggles/handlers/list.query.ts +1 -1
  47. package/src/feature-toggles/handlers/registered.query.ts +9 -2
  48. package/src/feature-toggles/handlers/set.write.ts +3 -3
  49. package/src/file-foundation/feature.ts +1 -1
  50. package/src/file-provider-s3/feature.ts +2 -2
  51. package/src/files-provider-s3/s3-provider.ts +2 -2
  52. package/src/jobs/handlers/list.query.ts +3 -3
  53. package/src/jobs/handlers/trigger.write.ts +1 -1
  54. package/src/legal-pages/constants.ts +1 -0
  55. package/src/legal-pages/web/client-plugin.ts +82 -0
  56. package/src/legal-pages/web/index.ts +4 -0
  57. package/src/mail-foundation/feature.ts +1 -1
  58. package/src/mail-transport-smtp/feature.ts +2 -2
  59. package/src/renderer-foundation/README.md +86 -0
  60. package/src/renderer-foundation/__tests__/api.test.ts +188 -0
  61. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +101 -0
  62. package/src/renderer-foundation/api.ts +106 -0
  63. package/src/renderer-foundation/constants.ts +21 -0
  64. package/src/renderer-foundation/feature.ts +47 -0
  65. package/src/renderer-foundation/index.ts +25 -0
  66. package/src/renderer-foundation/types.ts +109 -0
  67. package/src/renderer-simple/__tests__/adapter.test.ts +50 -0
  68. package/src/renderer-simple/feature.ts +28 -3
  69. package/src/renderer-simple/simple-renderer.ts +1 -1
  70. package/src/secrets/handlers/rotate.job.ts +2 -2
  71. package/src/sessions/handlers/cleanup.job.ts +2 -2
  72. package/src/step-dispatcher/feature.ts +62 -0
  73. package/src/step-dispatcher/index.ts +16 -0
  74. package/src/step-dispatcher/mail-runner.ts +32 -0
  75. package/src/step-dispatcher/webhook-runner.ts +67 -0
  76. package/src/subscription-mollie/plugin-methods.ts +1 -1
  77. package/src/subscription-mollie/verify-webhook.ts +9 -5
  78. package/src/subscription-stripe/verify-webhook.ts +3 -3
  79. package/src/template-resolver/README.md +89 -0
  80. package/src/template-resolver/__tests__/handlers.integration.ts +403 -0
  81. package/src/template-resolver/__tests__/template-resolver.integration.ts +570 -0
  82. package/src/template-resolver/api.ts +189 -0
  83. package/src/template-resolver/constants.ts +28 -0
  84. package/src/template-resolver/feature.ts +36 -0
  85. package/src/template-resolver/handlers/archive.write.ts +42 -0
  86. package/src/template-resolver/handlers/find-by-id.query.ts +45 -0
  87. package/src/template-resolver/handlers/list.query.ts +69 -0
  88. package/src/template-resolver/handlers/publish.write.ts +45 -0
  89. package/src/template-resolver/handlers/shared.ts +41 -0
  90. package/src/template-resolver/handlers/upsert-system.write.ts +75 -0
  91. package/src/template-resolver/handlers/upsert-tenant.write.ts +98 -0
  92. package/src/template-resolver/index.ts +28 -0
  93. package/src/template-resolver/qualified-names.ts +24 -0
  94. package/src/template-resolver/table.ts +67 -0
  95. package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
  96. package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
  97. package/src/tenant/handlers/remove-member.write.ts +1 -1
  98. package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
  99. package/src/tenant/handlers/update-member-roles.write.ts +3 -3
  100. package/src/text-content/__tests__/text-content.integration.ts +54 -0
  101. package/src/text-content/constants.ts +2 -0
  102. package/src/text-content/feature.ts +20 -4
  103. package/src/text-content/handlers/by-slug.query.ts +1 -0
  104. package/src/text-content/handlers/by-tenant.query.ts +58 -0
  105. package/src/text-content/handlers/set.write.ts +24 -1
  106. package/src/text-content/seeding.ts +9 -1
  107. package/src/text-content/table.ts +6 -0
  108. package/src/text-content/web/__tests__/editor-read-only.test.tsx +125 -0
  109. package/src/text-content/web/__tests__/group-blocks.test.ts +221 -0
  110. package/src/text-content/web/client-plugin.tsx +378 -0
  111. package/src/text-content/web/index.ts +8 -0
  112. package/src/tier-engine/feature.ts +8 -8
  113. package/src/user/handlers/find-for-auth.query.ts +1 -1
  114. package/src/user/seeding.ts +2 -2
  115. package/src/user-data-rights/feature.ts +4 -3
  116. package/src/user-data-rights/handlers/cancel-deletion.write.ts +1 -1
  117. package/src/user-data-rights/handlers/download-by-job.query.ts +8 -11
  118. package/src/user-data-rights/handlers/download-by-token.query.ts +14 -16
  119. package/src/user-data-rights/handlers/export-status.query.ts +1 -1
  120. package/src/user-data-rights/handlers/request-deletion.write.ts +1 -1
  121. package/src/user-data-rights/handlers/request-export.write.ts +2 -2
  122. package/src/user-data-rights/run-export-jobs.ts +2 -2
  123. package/src/user-data-rights/run-forget-cleanup.ts +27 -28
  124. package/src/user-data-rights/run-user-export.ts +1 -1
  125. package/src/user-data-rights/token-helpers.ts +2 -2
  126. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +1 -1
  127. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # @cosmicdrift/kumiko-bundled-features
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 825e7d2: Visual-Tree V.1.4 → V.1.6 — Feature-complete Editor + Folder-Hierarchy + Roving-tabindex.
8
+
9
+ **V.1.4** — explicit `folder?: string` Schema-Field auf text-block-entity. Slug bleibt
10
+ kebab-only validiert, Folder explizit gesetzt. Tree gruppiert via `groupBlocksByFolder`
11
+ (ersetzt `groupBlocksBySlugPrefix`). `Subscribe<T>` Signature um optional `emitError`
12
+ erweitert für explicit async-error-Pfade. ProviderBranch zeigt Error-Banner mit
13
+ Retry-Button. Drift-Test pinnt seedTextBlock-vs-set.write Slug-Validation.
14
+
15
+ **V.1.4b** — URL-State-Routing für Editor-Target via `nav.searchParams`. F5 + Back-Button
16
+ stellen den Editor-State wieder her. Format: `?t=text-content:edit&a_slug=...&a_lang=...`.
17
+ Plus `useDispatchTarget` hook ersetzt globalen `dispatchTarget` als empfohlenen Production-
18
+ Pfad (legacy bleibt für Test-Hooks).
19
+
20
+ **V.1.5** — Arrow-Key-Navigation (`<aside role="tree">`, ARIA-tree-Pattern) + SSE-driven
21
+ Tree-Refresh. `ClientFeatureDefinition.treeEntities?: string[]` listet Entity-Namen pro
22
+ Provider; live-events triggern provider-re-mount → Stale-Tree-state="stub"→"filled"
23
+ flippt nach save automatisch.
24
+
25
+ **V.1.5c+d** — Active-Node-Highlight (explicit blue + 2px border-l + scrollIntoView),
26
+ VS-Code-Polish (compact spacing, focus-visible, folder-icon-color text-amber, indent-
27
+ guides per ancestor-depth), Folder-Wrapper für legal-pages ("📁 Legal" + slug-first
28
+ Verschachtelung) und text-content ("📁 Content").
29
+
30
+ **V.1.6** — Multi-level Folder-Splitting (`folder="page/marketing"` → nested folders,
31
+ walk-or-create-pattern, folder/leaf-collision-tolerant). Roving-tabindex (nur focused-
32
+ treeitem hat tabIndex=0, Tab cyclt aus dem Tree raus).
33
+
34
+ 35/35 kumiko check PASS, 13/13 group-blocks + 22/22 text-content integration tests grün.
35
+ Browser + Keyboard lokal validated.
36
+
37
+ **Breaking**: `TreeContext` Type entfernt (V.1.2 SR2-Rip — war nie genutzt). Provider sind
38
+ session-bound: `TreeChildrenSubscribe = () => Subscribe<T>` statt `(ctx) => Subscribe<T>`.
39
+
40
+ **V.1.7-Followups**: useEffect-deps in VisualTree-focus-init (Performance), Cancellation-
41
+ Token in TreeProvider's fetch (emit-after-unmount-warning), inline-rename, drag-drop,
42
+ file-icons per slug-extension, parent-jump bei ArrowLeft auf collapsed-item.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [825e7d2]
47
+ - @cosmicdrift/kumiko-framework@0.4.0
48
+ - @cosmicdrift/kumiko-dispatcher-live@0.4.0
49
+ - @cosmicdrift/kumiko-renderer@0.4.0
50
+ - @cosmicdrift/kumiko-renderer-web@0.4.0
51
+
52
+ ## 0.3.0
53
+
54
+ ### Minor Changes
55
+
56
+ - 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
57
+ eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
58
+
59
+ ### Breaking Changes
60
+
61
+ - `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
62
+ feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
63
+ Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
64
+ `r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
65
+
66
+ ### Features
67
+
68
+ - **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
69
+ `wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
70
+ über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
71
+ einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
72
+ deterministisch gegen den ursprünglichen Eingangszustand laufen.
73
+ - **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
74
+ Component plus TreeProvider-Pattern; erste TreeProviders für
75
+ `text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
76
+ Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
77
+ - **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
78
+ Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
79
+ verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
80
+ - **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
81
+ als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
82
+ Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
83
+ (`eu-dsgvo`).
84
+
85
+ ### Fixes
86
+
87
+ - `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
88
+ Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
89
+ - `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
90
+ schmaler Cast-Surface, weniger Compiler-Knebel.
91
+ - `visual-tree`: runtime-isolation marker für client-konsumierte Files,
92
+ damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
93
+ - `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
94
+ in zwei Modulen noch der alte Name).
95
+ - `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
96
+ wieder push-ready.
97
+
98
+ ### Library-Updates
99
+
100
+ hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
101
+ bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
102
+ i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
103
+
104
+ ### Patch Changes
105
+
106
+ - Updated dependencies
107
+ - @cosmicdrift/kumiko-framework@0.3.0
108
+ - @cosmicdrift/kumiko-dispatcher-live@0.3.0
109
+ - @cosmicdrift/kumiko-renderer@0.3.0
110
+ - @cosmicdrift/kumiko-renderer-web@0.3.0
111
+
3
112
  ## 0.2.3
4
113
 
5
114
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -63,26 +63,31 @@
63
63
  "./feature-toggles": "./src/feature-toggles/index.ts",
64
64
  "./text-content": "./src/text-content/index.ts",
65
65
  "./text-content/seeding": "./src/text-content/seeding.ts",
66
- "./legal-pages": "./src/legal-pages/index.ts"
66
+ "./text-content/web": "./src/text-content/web/index.ts",
67
+ "./template-resolver": "./src/template-resolver/index.ts",
68
+ "./renderer-foundation": "./src/renderer-foundation/index.ts",
69
+ "./legal-pages": "./src/legal-pages/index.ts",
70
+ "./legal-pages/web": "./src/legal-pages/web/index.ts",
71
+ "./step-dispatcher": "./src/step-dispatcher/index.ts"
67
72
  },
68
73
  "dependencies": {
69
- "@aws-sdk/client-s3": "^3.700.0",
70
- "@aws-sdk/lib-storage": "^3.700.0",
71
- "@aws-sdk/s3-request-presigner": "^3.700.0",
72
- "@cosmicdrift/kumiko-dispatcher-live": "0.2.3",
73
- "@cosmicdrift/kumiko-framework": "0.2.3",
74
- "@cosmicdrift/kumiko-renderer": "0.2.3",
75
- "@cosmicdrift/kumiko-renderer-web": "0.2.3",
74
+ "@aws-sdk/client-s3": "^3.1045.0",
75
+ "@aws-sdk/lib-storage": "^3.1045.0",
76
+ "@aws-sdk/s3-request-presigner": "^3.1045.0",
77
+ "@cosmicdrift/kumiko-dispatcher-live": "0.4.0",
78
+ "@cosmicdrift/kumiko-framework": "0.4.0",
79
+ "@cosmicdrift/kumiko-renderer": "0.4.0",
80
+ "@cosmicdrift/kumiko-renderer-web": "0.4.0",
76
81
  "@mollie/api-client": "^4.5.0",
77
82
  "@node-rs/argon2": "^2.0.2",
78
83
  "@types/nodemailer": "^8.0.0",
79
84
  "clsx": "^2.1.1",
80
- "lucide-react": "^1.11.0",
81
- "marked": "^14.1.3",
85
+ "lucide-react": "^1.14.0",
86
+ "marked": "^18.0.3",
82
87
  "nodemailer": "^8.0.7",
83
- "react": "^19.2.0",
84
- "stripe": "^22.1.0",
85
- "tailwind-merge": "^3.0.2"
88
+ "react": "^19.2.6",
89
+ "stripe": "^22.1.1",
90
+ "tailwind-merge": "^3.6.0"
86
91
  },
87
92
  "publishConfig": {
88
93
  "registry": "https://registry.npmjs.org",
@@ -30,7 +30,7 @@ export const changePasswordWrite = defineWriteHandler({
30
30
  // Load self with passwordHash — only visible to the privileged caller.
31
31
  const me = (await ctx.queryAs(systemUser, UserQueries.findForAuth, {
32
32
  id: event.user.id,
33
- })) as { id: number; passwordHash: string | null; version: number } | null;
33
+ })) as { id: number; passwordHash: string | null; version: number } | null; // @cast-boundary db-runner
34
34
 
35
35
  if (!me?.passwordHash) {
36
36
  return invalidCredentials();
@@ -152,7 +152,7 @@ async function resolveStreamTenants(
152
152
  ): Promise<readonly TenantId[]> {
153
153
  const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
154
154
  userId: me.id,
155
- })) as Array<{ tenantId: TenantId }>;
155
+ })) as Array<{ tenantId: TenantId }>; // @cast-boundary db-runner
156
156
  return orderTenantsByPreference(memberships, me.lastActiveTenantId);
157
157
  }
158
158
 
@@ -114,10 +114,10 @@ export function createInviteAcceptWithLoginHandler() {
114
114
  if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
115
115
  return invalidInviteToken();
116
116
 
117
- const invitationTenantId = invitation["tenantId"] as TenantId;
118
- const invitationEmail = invitation["email"] as string;
119
- const invitationRole = invitation["role"] as string;
120
- const invitationVersion = invitation["version"] as number;
117
+ const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
118
+ const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
119
+ const invitationRole = invitation["role"] as string; // @cast-boundary db-row
120
+ const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
121
121
 
122
122
  // Email-Match vom User-Input (nicht aus session — User ist anon)
123
123
  if (event.payload.email.toLowerCase() !== invitationEmail) {
@@ -134,19 +134,19 @@ export function createInviteAcceptWithLoginHandler() {
134
134
  const userRow = await fetchOne(ctx.db.raw, userTable, eq(userTable.email, invitationEmail));
135
135
  if (!userRow?.["passwordHash"]) return invalidInviteToken();
136
136
  const passwordValid = await verifyPassword(
137
- userRow["passwordHash"] as string,
137
+ userRow["passwordHash"] as string, // @cast-boundary db-row
138
138
  event.payload.password,
139
139
  );
140
140
  if (!passwordValid) return invalidInviteToken();
141
141
 
142
- const userId = userRow["id"] as string;
142
+ const userId = userRow["id"] as string; // @cast-boundary db-row
143
143
 
144
144
  // Already-Member-Check (idempotent)
145
145
  const memberships = (await ctx.queryAs(
146
146
  createSystemUser(invitationTenantId),
147
147
  "tenant:query:memberships",
148
148
  { userId },
149
- )) as Array<{ tenantId: string }>;
149
+ )) as Array<{ tenantId: string }>; // @cast-boundary db-row
150
150
  const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
151
151
 
152
152
  // @cast-boundary db-runner — TenantDb.raw is DbRunner
@@ -107,16 +107,17 @@ export function createInviteAcceptHandler() {
107
107
  if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
108
108
  return invalidInviteToken();
109
109
 
110
- const invitationTenantId = invitation["tenantId"] as TenantId;
111
- const invitationEmail = invitation["email"] as string;
112
- const invitationRole = invitation["role"] as string;
113
- const invitationVersion = invitation["version"] as number;
110
+ const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
111
+ const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
112
+ const invitationRole = invitation["role"] as string; // @cast-boundary db-row
113
+ const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
114
114
 
115
115
  // Email-Match: User muss mit der eingeladenen Email matchen.
116
116
  // Sonst kann ein Angreifer mit Zugriff zur invitee-Mail seinen
117
117
  // eigenen Account dem Tenant zuschlagen.
118
118
  const userRow = await fetchOne(ctx.db.raw, userTable, eq(userTable.id, event.user.id));
119
- if (!userRow || (userRow["email"] as string).toLowerCase() !== invitationEmail) {
119
+ const userEmail = userRow?.["email"] as string | undefined; // @cast-boundary db-row
120
+ if (!userRow || !userEmail || userEmail.toLowerCase() !== invitationEmail) {
120
121
  return writeFailure(
121
122
  new UnprocessableError(AuthErrors.inviteEmailMismatch, {
122
123
  i18nKey: "auth.errors.inviteEmailMismatch",
@@ -131,7 +132,7 @@ export function createInviteAcceptHandler() {
131
132
  createSystemUser(invitationTenantId),
132
133
  "tenant:query:memberships",
133
134
  { userId: event.user.id },
134
- )) as Array<{ tenantId: string }>;
135
+ )) as Array<{ tenantId: string }>; // @cast-boundary db-row
135
136
  const alreadyMember = memberships.some((m) => m.tenantId === invitationTenantId);
136
137
 
137
138
  // @cast-boundary db-runner — TenantDb.raw is DbRunner
@@ -87,8 +87,8 @@ export function createInviteCreateHandler(opts: InviteCreateOptions = {}) {
87
87
  let invitationId: string;
88
88
  let token: string;
89
89
  if (existing) {
90
- invitationId = existing["id"] as string;
91
- const existingVersion = existing["version"] as number;
90
+ invitationId = existing["id"] as string; // @cast-boundary db-row
91
+ const existingVersion = existing["version"] as number; // @cast-boundary db-row
92
92
  // Resend-Idempotenz: Token aus Redis re-use wenn noch lebend.
93
93
  // Sonst neuen mintinen (alter ist abgelaufen).
94
94
  const existingToken = await getTokenForInvitation(ctx.redis, invitationId);
@@ -122,7 +122,7 @@ export function createInviteCreateHandler(opts: InviteCreateOptions = {}) {
122
122
  ctx.db,
123
123
  );
124
124
  if (!createResult.isSuccess) return createResult;
125
- invitationId = (createResult.data as { id: string }).id;
125
+ invitationId = (createResult.data as { id: string }).id; // @cast-boundary engine-payload
126
126
  token = generateToken();
127
127
  }
128
128
 
@@ -114,10 +114,10 @@ export function createInviteSignupCompleteHandler() {
114
114
  if (!invitation || invitation["status"] !== INVITATION_STATUS.pending)
115
115
  return invalidInviteToken();
116
116
 
117
- const invitationTenantId = invitation["tenantId"] as TenantId;
118
- const invitationEmail = invitation["email"] as string;
119
- const invitationRole = invitation["role"] as string;
120
- const invitationVersion = invitation["version"] as number;
117
+ const invitationTenantId = invitation["tenantId"] as TenantId; // @cast-boundary db-row
118
+ const invitationEmail = invitation["email"] as string; // @cast-boundary db-row
119
+ const invitationRole = invitation["role"] as string; // @cast-boundary db-row
120
+ const invitationVersion = invitation["version"] as number; // @cast-boundary db-row
121
121
 
122
122
  // User-Not-Exists-Check: wenn die Email schon registriert ist,
123
123
  // muss der User Branch 2 (acceptWithLogin) nutzen. Hier ist
@@ -180,7 +180,7 @@ export function createLoginHandler(opts: LoginHandlerOptions = {}) {
180
180
  // invitation, so we refuse with a dedicated error.
181
181
  const memberships = (await ctx.queryAs(systemUser, "tenant:query:memberships", {
182
182
  userId: found.id,
183
- })) as Array<{ tenantId: TenantId; roles: readonly string[] }>;
183
+ })) as Array<{ tenantId: TenantId; roles: readonly string[] }>; // @cast-boundary db-runner
184
184
 
185
185
  if (memberships.length === 0) {
186
186
  return noMembership();
@@ -1,4 +1,4 @@
1
- import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { access, defineWriteHandler, pipeline } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { z } from "zod";
3
3
 
4
4
  // Logout — JWT is stateless, so server-side we only return OK. A future
@@ -8,5 +8,5 @@ export const logoutWrite = defineWriteHandler({
8
8
  name: "logout",
9
9
  schema: z.object({}),
10
10
  access: { roles: access.authenticated },
11
- handler: async () => ({ isSuccess: true, data: { kind: "logged-out" } }),
11
+ perform: pipeline(({ r }) => [r.step.return({ isSuccess: true, data: { kind: "logged-out" } })]),
12
12
  });
@@ -117,7 +117,7 @@ export function createSignupConfirmHandler() {
117
117
  },
118
118
  });
119
119
 
120
- const tenantId = generateId() as TenantId;
120
+ const tenantId = generateId() as TenantId; // @cast-boundary engine-payload
121
121
  // Display-Name aus email-prefix als sinnvolles Default; User kann
122
122
  // den Tenant-Namen + sein eigenes displayName später ändern.
123
123
  const displayName = email.split("@")[0] ?? email;
@@ -244,7 +244,7 @@ export async function confirmSignup(
244
244
  body: JSON.stringify({ token, password }),
245
245
  });
246
246
  if (res.ok) {
247
- const body = (await res.json()) as SignupConfirmSuccess;
247
+ const body = (await res.json()) as SignupConfirmSuccess; // @cast-boundary engine-payload
248
248
  return { ok: true, data: body };
249
249
  }
250
250
  return { ok: false, error: await parseTokenFailure(res) };
@@ -18,7 +18,7 @@ import { BILLING_FOUNDATION_FEATURE, SubscriptionStatuses } from "./constants";
18
18
  export const SUBSCRIPTION_AGGREGATE_TYPE = "subscription" as const;
19
19
 
20
20
  // Event-name-Konstanten — short-form (für r.defineEvent) + qualifizierte
21
- // FQN (für ctx.appendEventUnsafe + projection-apply-keys).
21
+ // FQN (für ctx.unsafeAppendEvent + projection-apply-keys).
22
22
  export const SUBSCRIPTION_CREATED_EVENT_SHORT = "subscription-created" as const;
23
23
  export const SUBSCRIPTION_UPDATED_EVENT_SHORT = "subscription-updated" as const;
24
24
  export const SUBSCRIPTION_CANCELED_EVENT_SHORT = "subscription-canceled" as const;
@@ -41,7 +41,7 @@
41
41
  // (z.B. für analytics: "wie viele Wechsel im Monat?"), kommt ein
42
42
  // `subscription-provider-changed`-event-type später.
43
43
 
44
- import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
44
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
45
45
  import { BILLING_FOUNDATION_FEATURE, SUBSCRIPTION_PROVIDER_EXTENSION } from "./constants";
46
46
  import {
47
47
  INVOICE_PAID_EVENT_QN,
@@ -70,53 +70,50 @@ import {
70
70
  subscriptionsProjectionTable,
71
71
  } from "./projection";
72
72
 
73
- export const billingFoundationFeature: FeatureDefinition = defineFeature(
74
- BILLING_FOUNDATION_FEATURE,
75
- (r) => {
76
- // 5 fine-grained domain-events. Alle 5 nutzen denselben payload-
77
- // shape (= subscription-state-snapshot); der event-type taggt was
78
- // passiert ist. Future-consumer (billing-history, accounting)
79
- // listenen direkt auf den event-type ohne payload-discriminator.
80
- r.defineEvent(SUBSCRIPTION_CREATED_EVENT_SHORT, subscriptionEventPayloadSchema);
81
- r.defineEvent(SUBSCRIPTION_UPDATED_EVENT_SHORT, subscriptionEventPayloadSchema);
82
- r.defineEvent(SUBSCRIPTION_CANCELED_EVENT_SHORT, subscriptionEventPayloadSchema);
83
- r.defineEvent(INVOICE_PAID_EVENT_SHORT, subscriptionEventPayloadSchema);
84
- r.defineEvent(INVOICE_PAYMENT_FAILED_EVENT_SHORT, subscriptionEventPayloadSchema);
73
+ export const billingFoundationFeature = defineFeature(BILLING_FOUNDATION_FEATURE, (r) => {
74
+ // 5 fine-grained domain-events. Alle 5 nutzen denselben payload-
75
+ // shape (= subscription-state-snapshot); der event-type taggt was
76
+ // passiert ist. Future-consumer (billing-history, accounting)
77
+ // listenen direkt auf den event-type ohne payload-discriminator.
78
+ r.defineEvent(SUBSCRIPTION_CREATED_EVENT_SHORT, subscriptionEventPayloadSchema);
79
+ r.defineEvent(SUBSCRIPTION_UPDATED_EVENT_SHORT, subscriptionEventPayloadSchema);
80
+ r.defineEvent(SUBSCRIPTION_CANCELED_EVENT_SHORT, subscriptionEventPayloadSchema);
81
+ r.defineEvent(INVOICE_PAID_EVENT_SHORT, subscriptionEventPayloadSchema);
82
+ r.defineEvent(INVOICE_PAYMENT_FAILED_EVENT_SHORT, subscriptionEventPayloadSchema);
85
83
 
86
- // Inline projection: materialized current state in `read_subscriptions`.
87
- // Apply läuft in derselben TX wie ctx.appendEventUnsafe — read-your-
88
- // own-write ohne dispatcher-tick.
89
- r.projection({
90
- name: "subscription",
91
- source: SUBSCRIPTION_AGGREGATE_TYPE,
92
- table: subscriptionsProjectionTable,
93
- apply: {
94
- [SUBSCRIPTION_CREATED_EVENT_QN]: applySubscriptionCreated,
95
- [SUBSCRIPTION_UPDATED_EVENT_QN]: applySubscriptionUpdated,
96
- [SUBSCRIPTION_CANCELED_EVENT_QN]: applySubscriptionCanceled,
97
- [INVOICE_PAID_EVENT_QN]: applyInvoicePaid,
98
- [INVOICE_PAYMENT_FAILED_EVENT_QN]: applyInvoicePaymentFailed,
99
- },
100
- });
84
+ // Inline projection: materialized current state in `read_subscriptions`.
85
+ // Apply läuft in derselben TX wie ctx.unsafeAppendEvent — read-your-
86
+ // own-write ohne dispatcher-tick.
87
+ r.projection({
88
+ name: "subscription",
89
+ source: SUBSCRIPTION_AGGREGATE_TYPE,
90
+ table: subscriptionsProjectionTable,
91
+ apply: {
92
+ [SUBSCRIPTION_CREATED_EVENT_QN]: applySubscriptionCreated,
93
+ [SUBSCRIPTION_UPDATED_EVENT_QN]: applySubscriptionUpdated,
94
+ [SUBSCRIPTION_CANCELED_EVENT_QN]: applySubscriptionCanceled,
95
+ [INVOICE_PAID_EVENT_QN]: applyInvoicePaid,
96
+ [INVOICE_PAYMENT_FAILED_EVENT_QN]: applyInvoicePaymentFailed,
97
+ },
98
+ });
101
99
 
102
- // Plugin extension-point. Provider-Plugins registrieren sich hier.
103
- r.extendsRegistrar(SUBSCRIPTION_PROVIDER_EXTENSION, {
104
- onRegister: () => {
105
- // No side-effects at register-time.
106
- },
107
- });
100
+ // Plugin extension-point. Provider-Plugins registrieren sich hier.
101
+ r.extendsRegistrar(SUBSCRIPTION_PROVIDER_EXTENSION, {
102
+ onRegister: () => {
103
+ // No side-effects at register-time.
104
+ },
105
+ });
108
106
 
109
- // Custom write-handlers:
110
- // - process-event: programmatic entry-point vom webhook-handler;
111
- // dispatcht zu type-passendem appendEvent
112
- // - create-checkout-session: Tenant-Admin "Upgrade to Pro"-flow
113
- // - create-portal-session: Tenant-Admin "Manage Subscription"-flow
114
- r.writeHandler(processEventHandler);
115
- r.writeHandler(createCheckoutSessionHandler);
116
- r.writeHandler(createPortalSessionHandler);
107
+ // Custom write-handlers:
108
+ // - process-event: programmatic entry-point vom webhook-handler;
109
+ // dispatcht zu type-passendem appendEvent
110
+ // - create-checkout-session: Tenant-Admin "Upgrade to Pro"-flow
111
+ // - create-portal-session: Tenant-Admin "Manage Subscription"-flow
112
+ r.writeHandler(processEventHandler);
113
+ r.writeHandler(createCheckoutSessionHandler);
114
+ r.writeHandler(createPortalSessionHandler);
117
115
 
118
- // Custom list-query auf der subscription-projection (raw drizzle-
119
- // table; kein r.entity weil Schreiben via projection-apply läuft).
120
- r.queryHandler(listSubscriptionsQuery);
121
- },
122
- );
116
+ // Custom list-query auf der subscription-projection (raw drizzle-
117
+ // table; kein r.entity weil Schreiben via projection-apply läuft).
118
+ r.queryHandler(listSubscriptionsQuery);
119
+ });
@@ -28,7 +28,7 @@ export const createPortalSessionHandler: WriteHandlerDef = {
28
28
  schema: createPortalSessionSchema,
29
29
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
30
30
  handler: async (event, ctx) => {
31
- const payload = event.payload as CreatePortalSessionPayload;
31
+ const payload = event.payload as CreatePortalSessionPayload; // @cast-boundary engine-payload
32
32
  const tenantId = event.user.tenantId;
33
33
 
34
34
  // 1. Hol current subscription-row für den Tenant. Aggregate-id ist
@@ -41,8 +41,8 @@ export const createPortalSessionHandler: WriteHandlerDef = {
41
41
  "subscription-foundation: no active subscription for this tenant. Create one via create-checkout-session first.",
42
42
  );
43
43
  }
44
- const providerName = row["providerName"] as string;
45
- const providerCustomerId = row["providerCustomerId"] as string;
44
+ const providerName = row["providerName"] as string; // @cast-boundary db-row
45
+ const providerCustomerId = row["providerCustomerId"] as string; // @cast-boundary db-row
46
46
 
47
47
  // 2. Plugin-Lookup
48
48
  const usages = ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION);
@@ -9,7 +9,7 @@
9
9
  // kein zweiter append.
10
10
  // 2. Type-mapping: SubscriptionEvent.type (= normalisiert vom Plugin)
11
11
  // → einer der 5 ES-event-typen.
12
- // 3. ctx.appendEventUnsafe — Inline-projection materialisiert die
12
+ // 3. ctx.unsafeAppendEvent — Inline-projection materialisiert die
13
13
  // `read_subscriptions`-row in derselben TX.
14
14
 
15
15
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
@@ -69,7 +69,7 @@ const NORMALIZED_TO_ES_EVENT: Readonly<Record<string, string>> = {
69
69
  [SubscriptionEventTypes.canceled]: SUBSCRIPTION_CANCELED_EVENT_QN,
70
70
  [SubscriptionEventTypes.invoicePaid]: INVOICE_PAID_EVENT_QN,
71
71
  [SubscriptionEventTypes.invoicePaymentFailed]: INVOICE_PAYMENT_FAILED_EVENT_QN,
72
- };
72
+ } satisfies Readonly<Record<string, string>>;
73
73
 
74
74
  // =============================================================================
75
75
  // Handler
@@ -141,7 +141,7 @@ export const processEventHandler: WriteHandlerDef = {
141
141
  providerName: payload.providerName,
142
142
  rawPayload: payload.rawPayload,
143
143
  };
144
- await ctx.appendEventUnsafe({
144
+ await ctx.unsafeAppendEvent({
145
145
  aggregateId: aggId,
146
146
  aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
147
147
  type: esEventType,
@@ -1,7 +1,7 @@
1
1
  // Inline-projection für `read_subscriptions`. Materialisiert die 5
2
2
  // subscription-events in eine row pro Tenant.
3
3
  //
4
- // Apply läuft in derselben TX wie ctx.appendEventUnsafe — Caller sieht
4
+ // Apply läuft in derselben TX wie ctx.unsafeAppendEvent — Caller sieht
5
5
  // seinen Schreib-State sofort (kein dispatcher-tick nötig). PK = event.
6
6
  // aggregateId (= deterministic uuidv5 pro Tenant) → replays kollidieren
7
7
  // auf der PK statt doppelte rows zu erzeugen.
@@ -169,7 +169,7 @@ export function createSubscriptionWebhookHandler(deps: SubscriptionWebhookDeps)
169
169
  );
170
170
  }
171
171
 
172
- return c.json({ processed: true, ...((dispatched.data as object) ?? {}) }, 200);
172
+ return c.json({ processed: true, ...((dispatched.data as object) ?? {}) }, 200); // @cast-boundary engine-bridge
173
173
  };
174
174
  }
175
175
 
@@ -12,7 +12,7 @@ export const CAP_COUNTER_ROLLING_AGGREGATE_TYPE = "cap-counter-rolling" as const
12
12
  // Custom event-type für Rolling-Window-Counter. Symmetrisches Paar:
13
13
  // _SHORT — passt zu `r.defineEvent(short, schema)` im Registrar
14
14
  // (Framework prefixt automatisch zu QN)
15
- // _QN — qualifizierte Form für `ctx.appendEventUnsafe({type})`
15
+ // _QN — qualifizierte Form für `ctx.unsafeAppendEvent({type})`
16
16
  // + `events.type`-Spalte + `registry.getEvent(qn)`-Lookup
17
17
  // Beide MÜSSEN konsistent sein (drift-pin im feature-test).
18
18
  export const ROLLING_INCREMENTED_EVENT_SHORT = "rolling-incremented" as const;
@@ -118,7 +118,7 @@ export async function enforceCap(
118
118
  .limit(1);
119
119
 
120
120
  const row = rows[0];
121
- const value = row ? (row["value"] as number) : 0;
121
+ const value = row ? (row["value"] as number) : 0; // @cast-boundary db-row
122
122
 
123
123
  if (value >= hardThreshold) {
124
124
  throw new CapExceededError(options.capName, options.limit, value, tolerance);
@@ -46,11 +46,7 @@
46
46
  // config, kein secrets, kein tenant-feature nötig. Tenant-Scoping kommt
47
47
  // vom Framework-Default (Base-Column tenantId).
48
48
 
49
- import {
50
- defineEntityListHandler,
51
- defineFeature,
52
- type FeatureDefinition,
53
- } from "@cosmicdrift/kumiko-framework/engine";
49
+ import { defineEntityListHandler, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
54
50
  import { CAP_COUNTER_FEATURE, ROLLING_INCREMENTED_EVENT_SHORT } from "./constants";
55
51
  import { capCounterEntity } from "./entity";
56
52
  import { getCounterQuery } from "./handlers/get-counter.query";
@@ -63,11 +59,11 @@ import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write";
63
59
 
64
60
  const sysadminAccess = { access: { roles: ["SystemAdmin"] } } as const;
65
61
 
66
- export const capCounterFeature: FeatureDefinition = defineFeature(CAP_COUNTER_FEATURE, (r) => {
62
+ export const capCounterFeature = defineFeature(CAP_COUNTER_FEATURE, (r) => {
67
63
  r.entity("cap-counter", capCounterEntity);
68
64
 
69
65
  // Custom Domain-Event für Rolling-Counter. r.defineEvent registriert
70
- // das Schema beim Registry; ctx.appendEventUnsafe im Handler nutzt
66
+ // das Schema beim Registry; ctx.unsafeAppendEvent im Handler nutzt
71
67
  // dasselbe Schema für Append-Time-Validation. QN nach Prefixing:
72
68
  // "cap-counter:event:rolling-incremented" (siehe
73
69
  // ROLLING_INCREMENTED_EVENT_QN).
@@ -22,7 +22,7 @@ export const getCounterQuery: QueryHandlerDef = {
22
22
  schema: getCounterSchema,
23
23
  access: { roles: ["TenantAdmin", "SystemAdmin"] },
24
24
  handler: async (query, ctx) => {
25
- const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>;
25
+ const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>; // @cast-boundary engine-payload
26
26
 
27
27
  // ctx.db is tenant-scoped; filter by capName + periodStart explicitly.
28
28
  const rows = await ctx.db
@@ -60,11 +60,11 @@ export const incrementRollingCapHandler: WriteHandlerDef = {
60
60
  const payload = event.payload as IncrementRollingPayload;
61
61
  const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
62
62
 
63
- // appendEventUnsafe — bundled-features-Pfad (apps mit yarn kumiko
63
+ // unsafeAppendEvent — bundled-features-Pfad (apps mit yarn kumiko
64
64
  // codegen kriegen den strict-typed appendEvent-Wrapper). Schema-
65
65
  // Validation läuft trotzdem, weil r.defineEvent das Schema
66
66
  // registriert hat.
67
- await ctx.appendEventUnsafe({
67
+ await ctx.unsafeAppendEvent({
68
68
  aggregateId,
69
69
  aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
70
70
  type: ROLLING_INCREMENTED_EVENT_QN,
@@ -46,7 +46,7 @@ export const incrementCapHandler: WriteHandlerDef = {
46
46
  // which subsystem incremented.
47
47
  access: { roles: ["SystemAdmin"] },
48
48
  handler: async (event, ctx) => {
49
- const payload = event.payload as IncrementPayload;
49
+ const payload = event.payload as IncrementPayload; // @cast-boundary engine-payload
50
50
  const aggregateId = capCounterAggregateId(
51
51
  event.user.tenantId,
52
52
  payload.capName,
@@ -77,8 +77,8 @@ export const incrementCapHandler: WriteHandlerDef = {
77
77
  // clearer than a possibly-null deref later.
78
78
  throw new Error("cap-counter:increment: row vanished between length-check and read");
79
79
  }
80
- const currentValue = currentRow["value"] as number;
81
- const currentVersion = currentRow["version"] as number;
80
+ const currentValue = currentRow["value"] as number; // @cast-boundary db-row
81
+ const currentVersion = currentRow["version"] as number; // @cast-boundary db-row
82
82
  return executor.update(
83
83
  {
84
84
  id: aggregateId,