@cosmicdrift/kumiko-framework 0.362.0 → 0.364.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,181 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from packages/renderer-web/src/index.ts. Do not edit. -->
2
+
3
+ # Widgets
4
+
5
+ Value exports of `@cosmicdrift/kumiko-renderer-web`.
6
+
7
+ ## Components
8
+
9
+ | Name | Description |
10
+ | --- | --- |
11
+ | `ActionMenu` | |
12
+ | `AiTextArea` | Multi-line variant of {@link AiTextField}. |
13
+ | `AiTextField` | Single-line AI text field — ghost-text completion + correct/translate/ rewrite toolbar. |
14
+ | `AppLayout` | |
15
+ | `Avatar` | |
16
+ | `BareFormProvider` | |
17
+ | `BooleanField` | Checkbox/Switch = Field(layout:"inline") + Input(kind:"boolean"). |
18
+ | `Card` | |
19
+ | `CollapsibleSection` | Aufklappbare Sektion (native <details>, aber React-kontrolliert: sonst springt das open-Attribut bei jedem Parent-Re-Render auf den Initialwert zurück). |
20
+ | `ComparisonTable` | Transponierte Vergleichstabelle: Zeile = Kennzahl, Spalte = Variante, je Kennzahl optional die beste Spalte hervorgehoben. |
21
+ | `DateField` | Datumsfeld = Field + Input(kind:"date"), ISO yyyy-mm-dd. |
22
+ | `DefaultAppShell` | |
23
+ | `DetailList` | Read-only Schlüssel-Wert-Liste für Detail-Masken (Label links gedimmt, Wert rechts). |
24
+ | `DispatcherProvider` | |
25
+ | `Drawer` | Slide-in panel beside a list (e.g. mail reader next to the inbox) — thin wrapper over the Sheet primitive with header/body/footer slots so screens skip per-route Radix boilerplate. |
26
+ | `DropdownMenu` | |
27
+ | `DropdownMenuCheckboxItem` | |
28
+ | `DropdownMenuContent` | |
29
+ | `DropdownMenuItem` | |
30
+ | `DropdownMenuLabel` | |
31
+ | `DropdownMenuPortal` | |
32
+ | `DropdownMenuSeparator` | |
33
+ | `DropdownMenuTrigger` | |
34
+ | `EditorPanel` | |
35
+ | `EmptyState` | Leerer-Zustand mit optionalem Icon + CTA — die dashed-Box-Optik der entityList-Empty-States, als Standalone-Widget für Custom-Screens. |
36
+ | `ErrorState` | Fehler-Zustand auf Basis des Banner-Primitives, mit Retry-Button. |
37
+ | `FeedList` | Nicht-tabellarische Kurzliste (z.B. |
38
+ | `FileField` | Datei-Upload = Field + Input(kind:"file"\|"image") — FileRef-basiert. |
39
+ | `FloatingPanel` | Non-modal panel floating above the page: draggable by its header, resizable from all edges and corners, clamped to the viewport, geometry optionally remembered. |
40
+ | `FormScreenShell` | |
41
+ | `HeaderActionGroup` | |
42
+ | `Icon` | |
43
+ | `InfinityList` | Cursor-paginated scroll list (mail inbox, activity feeds) — loads the next page when the end sentinel becomes visible (IntersectionObserver), instead of a pager bar like QueryTable/entityList. |
44
+ | `KumikoLink` | |
45
+ | `KumikoScreen` | |
46
+ | `LanguageMenuItems` | Locale choices as dropdown entries, for a user menu that carries the shell controls. |
47
+ | `LanguageSwitcher` | |
48
+ | `LiveEventsProvider` | |
49
+ | `LoadingState` | Lade-Zustand: Skeleton-Zeilen in Card-Optik. |
50
+ | `LocaleProvider` | |
51
+ | `MiniStat` | Kompakte Kennzahl-Kachel (ohne Icon) — für dichte KPI-Raster. |
52
+ | `ModeSwitch` | Segmented-Control für sich ausschließende Modi — die prominente Alternative zum vergrabenen <select>. |
53
+ | `MoneyField` | |
54
+ | `NavProvider` | |
55
+ | `NavTree` | |
56
+ | `NumberField` | Zahlenfeld = Field + Input(kind:"number") in einem — nimmt der Screen die wiederholte id/name/value/onChange-Verdrahtung ab. |
57
+ | `PageSection` | |
58
+ | `PercentField` | |
59
+ | `PhotoSlots` | Guided photo capture: one tile per requested shot in a two-column grid, each its own camera/gallery picker, filled tiles show the thumbnail. |
60
+ | `PlainContentEditor` | |
61
+ | `PlanCard` | |
62
+ | `PlanGrid` | |
63
+ | `PrimitivesProvider` | |
64
+ | `ProfileMenu` | |
65
+ | `ProgressBar` | Progress bar, `value` 0..1 (clamped). |
66
+ | `ProgressList` | Liste aus Label/Wert-Kopfzeile + Fortschrittsbalken pro Eintrag (z.B. |
67
+ | `PublicShell` | One header and footer for anonymous pages; `focus` narrows to a task column, `card` centers a single card (sign-in). |
68
+ | `QueryTable` | |
69
+ | `RangeField` | Schieberegler = Field + Input(kind:"range"). |
70
+ | `RenderEdit` | |
71
+ | `RenderField` | |
72
+ | `RenderList` | |
73
+ | `ResizableHandle` | |
74
+ | `ResizablePanel` | |
75
+ | `ResizablePanelGroup` | |
76
+ | `ResultPanel` | Ergebnis-Sektion eines Rechners: SectionCard mit Empty-Zustand (Banner) oder Kennzahl-Liste (DetailList) + optionalen Extras (Tabelle, Hinweise). |
77
+ | `ResultTable` | Statische, prop-getriebene Ergebnistabelle für berechnete Zeilen (Tranchen, Szenarien). |
78
+ | `RichContentEditor` | |
79
+ | `ScreenWidthProvider` | |
80
+ | `SectionCard` | Sektion mit Titel + optionalem Action-Slot (Range-Umschalter, Filter) — ersetzt die wiederholten `<section className={CARD}>…<h2>`-Blöcke in Custom-Screens. |
81
+ | `SelectField` | Auswahlfeld = Field + Input(kind:"select"). |
82
+ | `SideBySideTable` | Small read-only grid with real table semantics (`th scope`) whose cells are arbitrary React nodes — for side-by-side comparisons like "from the document" vs. |
83
+ | `Sidebar` | |
84
+ | `SidebarBrand` | |
85
+ | `SidebarMenu` | |
86
+ | `SidebarMenuButton` | |
87
+ | `SidebarMenuItem` | |
88
+ | `SidebarPanel` | Renders its children into the second sidebar column when the shell offers one. |
89
+ | `SidebarProvider` | |
90
+ | `SidebarUser` | |
91
+ | `Sparkline` | |
92
+ | `Stack` | |
93
+ | `StatCard` | Icon-tragende Kennzahl-Kachel mit optionalem Delta-Chip, Trend-Zeile und Sparkline. |
94
+ | `StatusBadge` | Pill badge with a status dot. |
95
+ | `StatusBarChart` | Status bar strip (e.g. 90-day uptime): variable-height bars with gradient fade + tick line on top; the last entry gets a "now" accent stripe. |
96
+ | `StepBar` | Wizard step overview — numbered chips with a connector line between them. |
97
+ | `TextField` | Textfeld = Field + Input(kind:"text"). |
98
+ | `TextareaField` | Mehrzeiliges Textfeld = Field + Input(kind:"textarea"). |
99
+ | `ThemeMenuItem` | Theme toggle as a dropdown entry, for a user menu that carries the shell controls. |
100
+ | `ThemeToggle` | |
101
+ | `TimeseriesChart` | Zeitreihen-Linien-Chart (geglättete Linie + Flächen-Verlauf). |
102
+ | `ToastProvider` | |
103
+ | `TokensProvider` | |
104
+ | `Topbar` | |
105
+ | `UploadZone` | Drop zone + multi-file picker with a per-file status row (uploading/done/ error). |
106
+ | `UserRolesProvider` | |
107
+ | `WorkspaceShell` | |
108
+ | `WorkspaceSwitcher` | Requires an ancestor `SidebarProvider` (see `../ui/sidebar`) — this component renders `SidebarMenuButton`, which calls `useSidebar()` internally and throws ("useSidebar must be used within a SidebarProvider.") when rendered outside one. |
109
+
110
+ ## Hooks
111
+
112
+ | Name | Description |
113
+ | --- | --- |
114
+ | `useBrowserNavApi` | React-Hook der eine NavApi aus der Browser-History baut. |
115
+ | `useBrowserTokensApi` | Hook der eine TokensApi für den Browser baut. |
116
+ | `useDispatcher` | |
117
+ | `useDispatcherStatus` | |
118
+ | `useDraft` | Formular-State für Rechner-Screens: ein Draft-Objekt + `patch` für Teil-Updates + `field(name)` das die `{ id, name, value, onChange }`-Props fertig für die Feld-Widgets (NumberField/MoneyField/PercentField) liefert. |
119
+ | `useForm` | |
120
+ | `useIsNarrowViewport` | |
121
+ | `useLiveEvents` | Hook für useQuery-like Consumer. |
122
+ | `useLocale` | Liefert den aktuellen LocaleResolver und abonniert automatisch Locale-Änderungen — der aufrufende Component re-rendert sobald die Sprache gewechselt wird. |
123
+ | `useNav` | |
124
+ | `usePrimitives` | |
125
+ | `useQuery` | |
126
+ | `useResolvers` | |
127
+ | `useStore` | |
128
+ | `useStoreSelector` | |
129
+ | `useToast` | |
130
+ | `useTokenController` | Volle API inkl. |
131
+ | `useTokens` | Read-only Tokens-Hook. |
132
+ | `useTranslation` | `t("key", params)` resolves in this order: 1. |
133
+ | `useUserRoles` | |
134
+
135
+ ## Functions
136
+
137
+ | Name | Description |
138
+ | --- | --- |
139
+ | `applyTokensToCssVars` | Historisch: schrieb Tokens auf :root als CSS-vars. |
140
+ | `buildFileUploadFormData` | |
141
+ | `buildNavRegistrySlice` | |
142
+ | `buildNavRegistrySliceForApp` | |
143
+ | `cn` | shadcn-Standard-Helper: clsx für konditionales Zusammenstecken, tailwind-merge für Konfliktauflösung (z.B. |
144
+ | `createBrowserDraftStorage` | Default DraftStorage when createKumikoApp boots without one. |
145
+ | `createBrowserLocaleResolver` | Default-Resolver wenn createKumikoApp ohne `locale`-Option gebootet wird. |
146
+ | `createEventSourceLiveEvents` | Liefert einen `LiveEventSubscriber` der EventSource-backed ist. |
147
+ | `createKumikoApp` | |
148
+ | `createPublicSurface` | |
149
+ | `createStaticLocaleResolver` | Default-Resolver für Apps ohne eigene i18n-Schicht. |
150
+ | `filterAppSchemaNavsByAllowlist` | |
151
+ | `filterByAccess` | |
152
+ | `formatMoney` | |
153
+ | `formatPath` | |
154
+ | `parsePath` | |
155
+ | `parseTargetFromSearchParams` | Inverse — parse the current search-params back into a TargetRef. |
156
+ | `postWithDownload` | |
157
+ | `qualifyScreenId` | |
158
+ | `readLoadedBuild` | |
159
+ | `reparentAppSchemaNavs` | |
160
+ | `resolveDefaultId` | |
161
+ | `smoothPath` | Quadratic-durch-Mittelpunkte-Trick: glättet die Zick-Zack-Linie ohne Overshoot (~5 Zeilen statt Catmull-Rom/Bezier-Fit). |
162
+ | `synthesizeActionFormEntity` | Baut eine minimale EntityDefinition aus den Inline-Fields eines ActionForm- oder SecretMint-Screens. |
163
+ | `synthesizeActionFormScreen` | Wandelt ein ActionFormScreenDefinition oder SecretMintScreenDefinition in die EntityEditScreen-Shape die RenderEdit erwartet. |
164
+ | `uploadFileRef` | Posts one file to the stack's file endpoint (double-submit CSRF) and returns the new FileRef id. |
165
+
166
+ ## Constants
167
+
168
+ | Name | Description |
169
+ | --- | --- |
170
+ | `BROWSER_LOCALE_STORAGE_KEY` | |
171
+ | `STATUS_TONE_TEXT` | |
172
+ | `cssVarTokens` | Tokens mit var-string-Werten. |
173
+ | `defaultPrimitives` | |
174
+ | `defaultTokens` | Default-Tokens — identisch zu `cssVarTokens` (var-string-refs). |
175
+ | `lightTokens` | |
176
+
177
+ ## Classes
178
+
179
+ | Name | Description |
180
+ | --- | --- |
181
+ | `FileUploadFailedError` | Carries the server's own `error` message when the upload endpoint sent one. |
@@ -36,6 +36,8 @@ export type AnonymousAccessResolved = AnonymousAccessConfig & {
36
36
  export declare function authMiddleware(jwt: JwtHelper, options?: AuthMiddlewareOptions): (c: Context, next: Next) => Promise<Response | undefined>;
37
37
  export declare function getUser(c: Context): SessionUser;
38
38
  export declare function getAuthTransport(c: Context): AuthTransport | undefined;
39
+ /** Rejection code of the stale auth cookie this request was downgraded to anonymous from; undefined otherwise. */
40
+ export declare function getStaleCredentialCode(c: Context): string | undefined;
39
41
  /** JWT `exp` (epoch seconds) of the request's token; undefined for anonymous and verified-bearer (PAT) callers. */
40
42
  export declare function getAuthTokenExpiry(c: Context): number | undefined;
41
43
  export declare function resolveTenantTeardownStatus(tenantId: TenantId, resolveTenantLifecycleStatus: TenantLifecycleStatusResolver | undefined): Promise<string | undefined>;
@@ -1,3 +1,4 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { getCookie } from "hono/cookie";
2
3
  import { TENANT_TEARDOWN_STATUSES } from "../engine/active-membership.js";
3
4
  import { createAnonymousUser } from "../engine/system-user.js";
@@ -7,6 +8,7 @@ import { isForeignCookieOrigin } from "./origin-middleware.js";
7
8
  import { clearSessionCookies, getSessionCookies, hasDuplicateSessionCookie, } from "./session-cookies.js";
8
9
  const USER_KEY = "pipelineUser";
9
10
  const AUTH_TRANSPORT_KEY = "authTransport";
11
+ const STALE_CREDENTIAL_KEY = "staleCredentialCode";
10
12
  const AUTH_TOKEN_EXPIRY_KEY = "authTokenExpiry";
11
13
  export const CSRF_HEADER_NAME = "X-CSRF-Token";
12
14
  // Prefix that marks a bearer token as a long-lived Personal Access Token
@@ -160,6 +162,7 @@ export function authMiddleware(jwt, options = {}) {
160
162
  }
161
163
  const rejectInvalidCredential = async (rejection) => {
162
164
  if (isStaleCookieDowngradable(c, transport, anonymousAccess)) {
165
+ c.set(STALE_CREDENTIAL_KEY, rejection.code);
163
166
  return await handleAnonymous(c, anonymousAccess, next, resolveTenantLifecycleStatus, {
164
167
  staleCredential: { rejectionIfTenantUnresolved: rejection },
165
168
  });
@@ -235,6 +238,11 @@ export function getAuthTransport(c) {
235
238
  // @cast-boundary engine-bridge — Hono context.get returns unknown
236
239
  return c.get(AUTH_TRANSPORT_KEY);
237
240
  }
241
+ /** Rejection code of the stale auth cookie this request was downgraded to anonymous from; undefined otherwise. */
242
+ export function getStaleCredentialCode(c) {
243
+ const code = c.get(STALE_CREDENTIAL_KEY);
244
+ return typeof code === "string" ? code : undefined;
245
+ }
238
246
  /** JWT `exp` (epoch seconds) of the request's token; undefined for anonymous and verified-bearer (PAT) callers. */
239
247
  export function getAuthTokenExpiry(c) {
240
248
  // @cast-boundary engine-bridge — Hono context.get returns unknown
@@ -482,17 +490,18 @@ async function requestsCancelDestruction(c) {
482
490
  return false;
483
491
  try {
484
492
  const path = c.req.path;
485
- const body = (await c.req.raw.clone().json());
493
+ const body = await c.req.raw.clone().json();
486
494
  if (path === "/api/write") {
487
- return body.type === TENANT_LIFECYCLE_CANCEL_QN;
495
+ return propertyOf(body, "type") === TENANT_LIFECYCLE_CANCEL_QN;
488
496
  }
489
497
  if (path === "/api/batch") {
498
+ const commands = propertyOf(body, "commands");
490
499
  // every(), not some(): a batch mixing the cancel command with other
491
500
  // writes must NOT wave the whole batch through the teardown gate —
492
501
  // only a batch consisting solely of cancel-destruction is exempt.
493
- return (Array.isArray(body.commands) &&
494
- body.commands.length > 0 &&
495
- body.commands.every((command) => command.type === TENANT_LIFECYCLE_CANCEL_QN));
502
+ return (Array.isArray(commands) &&
503
+ commands.length > 0 &&
504
+ commands.every((command) => propertyOf(command, "type") === TENANT_LIFECYCLE_CANCEL_QN));
496
505
  }
497
506
  }
498
507
  catch {
@@ -12,7 +12,7 @@ import { Routes } from "./api-constants.js";
12
12
  import { getUser, } from "./auth-middleware.js";
13
13
  import { clientIpSourceFromHonoContext, createClientIpResolver, } from "./client-ip.js";
14
14
  import { resolvePostAuthLandingPath } from "./post-auth-landing.js";
15
- import { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, clearSessionCookies, getSessionCookies, isHostPrefixedCookieName, resolveSessionCookies, } from "./session-cookies.js";
15
+ import { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, clearSessionCookies, expireLegacySessionCookieVariants, getSessionCookies, resolveSessionCookies, } from "./session-cookies.js";
16
16
  import { generateToken } from "./tokens.js";
17
17
  // Double-entry cookie write used at login and switch-tenant. The auth cookie is
18
18
  // the HttpOnly carrier of the JWT; the csrf cookie is the JS-readable token the
@@ -35,11 +35,13 @@ function setAuthCookies(c, opts) {
35
35
  // would coexist with the new Domain cookie (RFC 6265 treats name+domain as
36
36
  // distinct) and the server could bind to the stale host-only token. With
37
37
  // __Host- names the unprefixed host-only cookie is a leftover from before
38
- // the prefix and is expired too.
39
- if (opts.domain !== undefined || isHostPrefixedCookieName(names.auth)) {
38
+ // the prefix and is expired too (and vice versa, see
39
+ // expireLegacySessionCookieVariants).
40
+ if (opts.domain !== undefined) {
40
41
  deleteCookie(c, AUTH_COOKIE_BASE, { path: "/" });
41
42
  deleteCookie(c, CSRF_COOKIE_BASE, { path: "/" });
42
43
  }
44
+ expireLegacySessionCookieVariants(c, opts.cookies);
43
45
  // Must precede the setCookie calls: browsers apply Set-Cookie in order and
44
46
  // a retired domain equal to the new one is rejected at config time.
45
47
  // Retired domains only ever carried unprefixed names (a __Host- cookie has no
@@ -57,6 +57,9 @@ export type SystemDispatchArgs = {
57
57
  readonly payload: unknown;
58
58
  readonly tenantId: TenantId;
59
59
  };
60
+ export type RoleDispatchArgs = SystemDispatchArgs & {
61
+ readonly roles: readonly string[];
62
+ };
60
63
  export type SignatureExtraRouteDeps = {
61
64
  readonly app: Hono<any, any>;
62
65
  readonly registry: Registry;
@@ -8,7 +8,7 @@ export type { AuthRoutesConfig, LoginRateLimiter, SessionChecker, SessionCreator
8
8
  export { createAuthRoutes, createInMemoryLoginRateLimiter, createRedisLoginRateLimiter, } from "./auth-routes.js";
9
9
  export type { ClientIpHeaderSource, ClientIpResolver } from "./client-ip.js";
10
10
  export { assertValidTrustedProxyHops, clientIpSourceFromHonoContext, createClientIpResolver, extractSocketAddress, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, UNKNOWN_CLIENT_IP, } from "./client-ip.js";
11
- export type { AnonymousExtraRoute, AnonymousExtraRouteDeps, ExtraRouteDefinition, ExtraRouteEntry, ExtraRouteRejectionOptions, ExtraRouteRejectionStatus, SignatureExtraRoute, SignatureExtraRouteDeps, SignatureExtraRouteVerifyDeps, SignatureExtraRouteVerifyRequest, SystemDispatchArgs, UserExtraRoute, UserExtraRouteDeps, } from "./extra-route.js";
11
+ export type { AnonymousExtraRoute, AnonymousExtraRouteDeps, ExtraRouteDefinition, ExtraRouteEntry, ExtraRouteRejectionOptions, ExtraRouteRejectionStatus, RoleDispatchArgs, SignatureExtraRoute, SignatureExtraRouteDeps, SignatureExtraRouteVerifyDeps, SignatureExtraRouteVerifyRequest, SystemDispatchArgs, UserExtraRoute, UserExtraRouteDeps, } from "./extra-route.js";
12
12
  export { ExtraRouteEntries, ExtraRouteRejection, signatureRoute } from "./extra-route.js";
13
13
  export type { CachedResponseInit, CachePolicy } from "./http-cache.js";
14
14
  export { cacheControlHeader, cachedResponse, computeRevisionEtag, computeStrongEtag, computeWeakEtag, etagMatches, parseIfNoneMatch, } from "./http-cache.js";
@@ -26,7 +26,7 @@ export type { MetricsRouteOptions } from "./route-registrars.js";
26
26
  export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.js";
27
27
  export { createApiRoutes } from "./routes.js";
28
28
  export type { KumikoServer, ServerOptions } from "./server.js";
29
- export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite } from "./server.js";
29
+ export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server.js";
30
30
  export type { ResolvedSessionCookies, SessionCookieBase, SessionCookieNames, } from "./session-cookies.js";
31
31
  export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.js";
32
32
  export type { SseBroker, SseClient, SseEvent } from "./sse-broker.js";
package/dist/api/index.js CHANGED
@@ -15,7 +15,7 @@ export { readonlyRequestContext as requestContext, } from "./request-context.js"
15
15
  export { buildRequestContextDataFromRequest, requestIdMiddleware, } from "./request-id-middleware.js";
16
16
  export { createMetricsApp, DEFAULT_MAX_REQUEST_BYTES, isPrometheusMeter, } from "./route-registrars.js";
17
17
  export { createApiRoutes } from "./routes.js";
18
- export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite } from "./server.js";
18
+ export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite, makeDispatchWriteAs, } from "./server.js";
19
19
  export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, getSessionCookies, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, resolveSessionCookies, sessionCookieName, sessionCookiesMiddleware, TENANT_COOKIE_BASE, } from "./session-cookies.js";
20
20
  export { createSseBroker } from "./sse-broker.js";
21
21
  export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
1
2
  import { redisChannelPrefixFromEnv, redisClientOptionsFromEnv, } from "../redis/client.js";
2
3
  import { createRedisPubSubSignal } from "../redis/pubsub-signal.js";
3
4
  import { createSseBroker, readNonEmptyStringArray, } from "./sse-broker.js";
@@ -37,7 +38,9 @@ export function createDefaultSseBroker(env = process.env) {
37
38
  function isSseEvent(value) {
38
39
  return (typeof value === "object" &&
39
40
  value !== null &&
41
+ "type" in value &&
40
42
  typeof value.type === "string" &&
43
+ "data" in value &&
41
44
  typeof value.data === "object" &&
42
45
  value.data !== null);
43
46
  }
@@ -50,25 +53,24 @@ function isSseEvent(value) {
50
53
  // sparing something it doesn't understand: fail-closed on a mixed rolling
51
54
  // deploy. Anything malformed also falls back to userwide.
52
55
  function parseInvalidationScope(payload) {
53
- if (typeof payload !== "object" || payload === null)
56
+ if (!isPlainObject(payload))
54
57
  return { kind: "user" };
55
- const record = payload;
56
- if ("scope" in record) {
57
- const { scope } = record;
58
+ if ("scope" in payload) {
59
+ const { scope } = payload;
58
60
  if (scope === "sessions") {
59
- const sessionIds = readNonEmptyStringArray(record["sessionIds"]);
61
+ const sessionIds = readNonEmptyStringArray(payload["sessionIds"]);
60
62
  if (sessionIds)
61
63
  return { kind: "sessions", sessionIds };
62
64
  }
63
65
  if (scope === "pat-tokens") {
64
- const tokenIds = readNonEmptyStringArray(record["tokenIds"]);
66
+ const tokenIds = readNonEmptyStringArray(payload["tokenIds"]);
65
67
  if (tokenIds)
66
68
  return { kind: "pat-tokens", tokenIds };
67
69
  }
68
70
  return { kind: "user" };
69
71
  }
70
- if ("keptSessionId" in record) {
71
- const { keptSessionId } = record;
72
+ if ("keptSessionId" in payload) {
73
+ const { keptSessionId } = payload;
72
74
  if (typeof keptSessionId === "string" && keptSessionId.length > 0) {
73
75
  return { kind: "all-except-session", keptSessionId };
74
76
  }
@@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception";
3
3
  import { streamSSE } from "hono/streaming";
4
4
  import { AccessDeniedError, reraiseAsKumikoError, serializeError, toKumikoError, ValidationError, } from "../errors/index.js";
5
5
  import { createFallbackLogger } from "../logging/index.js";
6
+ import { isPlainObject } from "../utils/is-plain-object.js";
6
7
  import { stringifyJson } from "../utils/safe-json.js";
7
8
  import { Routes } from "./api-constants.js";
8
9
  import { getAuthTokenExpiry, getUser } from "./auth-middleware.js";
@@ -43,7 +44,9 @@ export function createApiRoutes(dispatcher, options = {}) {
43
44
  api.post(Routes.batch, async (c) => {
44
45
  const user = getUser(c);
45
46
  const body = await readJsonObject(c);
46
- if (!Array.isArray(body.commands)) {
47
+ const requestId = parseRequestId(body["requestId"]);
48
+ const rawCommands = body["commands"];
49
+ if (!Array.isArray(rawCommands)) {
47
50
  // Client-shape violation → ValidationError (400, code=validation_error)
48
51
  // matches what a Zod-level schema failure would produce if /batch had
49
52
  // one. Client SDKs can key off the uniform validation contract.
@@ -53,25 +56,27 @@ export function createApiRoutes(dispatcher, options = {}) {
53
56
  path: "commands",
54
57
  code: "invalid_type",
55
58
  i18nKey: "errors.validation.invalid_type",
56
- params: { expected: "array", received: typeof body.commands },
59
+ params: { expected: "array", received: typeof rawCommands },
57
60
  },
58
61
  ],
59
62
  }));
60
63
  }
64
+ const commands = rawCommands.map((raw, index) => parseDispatchCommand(raw, `commands.${index}`));
61
65
  try {
62
- for (const cmd of body.commands)
66
+ for (const cmd of commands)
63
67
  assertPayloadDepthAllowed(cmd.payload);
64
68
  if (user.pat) {
65
- for (const cmd of body.commands)
69
+ for (const cmd of commands)
66
70
  assertPatAllowed(user, cmd.type);
67
71
  }
68
- const result = await dispatcher.batch(body.commands, user, body.requestId);
72
+ const result = await dispatcher.batch(commands, user, requestId);
69
73
  if (!result.isSuccess) {
70
74
  const err = reraiseAsKumikoError(result.error);
71
- const requestId = requestContext.get()?.requestId;
72
- const failedType = result.failedIndex != null ? body.commands[result.failedIndex]?.type : undefined;
73
- logServerFault(err, requestId, failedType);
74
- const { error } = serializeError(err, requestId, requestContext.get()?.errorDocs);
75
+ const ctxRequestId = requestContext.get()?.requestId;
76
+ const failedType = result.failedIndex != null ? commands[result.failedIndex]?.type : undefined;
77
+ logServerFault(err, ctxRequestId, failedType);
78
+ const { error } = serializeError(err, ctxRequestId, requestContext.get()?.errorDocs);
79
+ setRetryAfterHeader(c, err);
75
80
  // Keep failedIndex + results alongside the error envelope so callers
76
81
  // can tell which command in the batch failed and inspect the partial
77
82
  // results from the successful commands before the rollback.
@@ -86,7 +91,7 @@ export function createApiRoutes(dispatcher, options = {}) {
86
91
  }
87
92
  catch (e) {
88
93
  // single "type" doesn't apply to a batch; command types keep the fault context.
89
- return writeErrorResponse(c, toKumiko(e), body.commands?.map((cmd) => cmd.type).join(","));
94
+ return writeErrorResponse(c, toKumiko(e), commands.map((cmd) => cmd.type).join(","));
90
95
  }
91
96
  });
92
97
  api.post(Routes.query, async (c) => {
@@ -172,7 +177,7 @@ export function createApiRoutes(dispatcher, options = {}) {
172
177
  // in-flight .next(), so awaiting here would block the response until
173
178
  // that pending pull resolves (which may be never for an idle stream).
174
179
  void generator.return(undefined).catch(() => { });
175
- return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
180
+ return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
176
181
  }
177
182
  return streamSSE(c, async (stream) => {
178
183
  stream.onAbort(() => {
@@ -270,23 +275,37 @@ function invalidBodyError(path, expected, received) {
270
275
  });
271
276
  }
272
277
  // Malformed or non-object JSON is a client error; letting c.req.json() throw
273
- // would surface as an unclassified 500.
274
- // @cast-boundary request-body — shape is validated per route right after
278
+ // would surface as an unclassified 500. Callers validate the fields they read.
275
279
  async function readJsonObject(c) {
276
280
  const raw = await c.req.json().catch(() => undefined);
277
281
  if (raw === undefined)
278
282
  throw invalidBodyError("body", "json", "malformed");
279
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
283
+ if (!isPlainObject(raw)) {
280
284
  throw invalidBodyError("body", "object", raw === null ? "null" : typeof raw);
281
285
  }
282
286
  return raw;
283
287
  }
288
+ function parseRequestId(raw) {
289
+ if (raw === undefined || typeof raw === "string")
290
+ return raw;
291
+ throw invalidBodyError("requestId", "string", typeof raw);
292
+ }
293
+ // `path` prefixes the reported field path; empty for a top-level body.
294
+ function parseDispatchCommand(raw, path) {
295
+ const at = (key) => (path === "" ? key : `${path}.${key}`);
296
+ if (!isPlainObject(raw)) {
297
+ throw invalidBodyError(path === "" ? "body" : path, "object", raw === null ? "null" : typeof raw);
298
+ }
299
+ const type = raw["type"];
300
+ if (typeof type !== "string" || type === "") {
301
+ throw invalidBodyError(at("type"), "string", typeof type);
302
+ }
303
+ return { type, payload: raw["payload"] };
304
+ }
284
305
  async function readDispatchBody(c) {
285
306
  const body = await readJsonObject(c);
286
- if (typeof body.type !== "string" || body.type === "") {
287
- throw invalidBodyError("type", "string", typeof body.type);
288
- }
289
- return { type: body.type, payload: body.payload, requestId: body.requestId };
307
+ const command = parseDispatchCommand(body, "");
308
+ return { ...command, requestId: parseRequestId(body["requestId"]) };
290
309
  }
291
310
  // Rejects payloads nested deeper than MAX_PAYLOAD_DEPTH before they reach
292
311
  // per-handler Zod validation — recursive Zod parsing (and any downstream
@@ -381,6 +400,13 @@ function logServerFault(err, requestId, type) {
381
400
  stack: cause instanceof Error ? cause.stack : err.stack,
382
401
  });
383
402
  }
403
+ // Same floor of 1 as the rate-limit middleware: "Retry-After: 0" invites an instant retry loop.
404
+ function setRetryAfterHeader(c, err) {
405
+ // skip: most errors carry no retry hint, so the response gets no Retry-After header
406
+ if (err.retryAfterSeconds === undefined)
407
+ return;
408
+ c.header("Retry-After", String(Math.max(1, Math.ceil(err.retryAfterSeconds))));
409
+ }
384
410
  // For /write + /batch: keep the isSuccess flag so clients can flip on a single
385
411
  // boolean (mirrors the success shape). The actual error body is the
386
412
  // error-contract payload nested under .error.
@@ -388,13 +414,14 @@ function writeErrorResponse(c, err, type) {
388
414
  const requestId = requestContext.get()?.requestId;
389
415
  logServerFault(err, requestId, type);
390
416
  const { error } = serializeError(err, requestId, requestContext.get()?.errorDocs);
417
+ setRetryAfterHeader(c, err);
391
418
  return c.json({ isSuccess: false, error }, err.httpStatus); // @cast-boundary engine-payload
392
419
  }
393
420
  function clientAbortResponse(c, requestId, type) {
394
421
  logClientWarn("request aborted by client", requestId, type, {
395
422
  status: CLIENT_CLOSED_REQUEST_STATUS,
396
423
  });
397
- return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary non-standard client-closed-request status, Hono's union doesn't include it
424
+ return c.body(null, CLIENT_CLOSED_REQUEST_STATUS); // @cast-boundary third-party-type-gap — non-standard client-closed-request status, Hono's union doesn't include it
398
425
  }
399
426
  // For /query + /command: no isSuccess on success (just { data } / {ok}), so we
400
427
  // keep the same lean shape on failure — only the `error` key.
@@ -404,6 +431,7 @@ function queryErrorResponse(c, err, type) {
404
431
  return clientAbortResponse(c, requestId, type);
405
432
  logServerFault(err, requestId, type);
406
433
  const body = serializeError(err, requestId, requestContext.get()?.errorDocs);
434
+ setRetryAfterHeader(c, err);
407
435
  return c.json(body, err.httpStatus); // @cast-boundary engine-payload
408
436
  }
409
437
  // A raw throw under the request signal (signal.throwIfAborted(), an aborted
@@ -424,5 +452,6 @@ export function handleUncaughtRouteError(err, c) {
424
452
  return clientAbortResponse(c, requestId);
425
453
  const kumikoError = toKumiko(err);
426
454
  logServerFault(kumikoError, requestId);
455
+ setRetryAfterHeader(c, kumikoError);
427
456
  return c.json(serializeError(kumikoError, requestId, requestContext.get()?.errorDocs), kumikoError.httpStatus); // @cast-boundary engine-payload
428
457
  }
@@ -13,7 +13,7 @@ import { type SystemHooks } from "../pipeline/lifecycle-pipeline.js";
13
13
  import { type AuthEndpointRateLimitOptions, type GlobalIpRateLimitOptions } from "../rate-limit/index.js";
14
14
  import { type AnonymousAccessResolved } from "./auth-middleware.js";
15
15
  import { type AuthRoutesConfig } from "./auth-routes.js";
16
- import { type ExtraRouteDefinition, type SystemDispatchArgs } from "./extra-route.js";
16
+ import { type ExtraRouteDefinition, type RoleDispatchArgs, type SystemDispatchArgs } from "./extra-route.js";
17
17
  import { type JwtHelper, type JwtKeyring } from "./jwt.js";
18
18
  import type { SseBroker } from "./sse-broker.js";
19
19
  export type ServerOptions = {
@@ -82,4 +82,5 @@ export type KumikoServer = {
82
82
  export declare function withFileProviderResolver(registry: Registry, context: AppContext, cacheSync?: CacheSyncBus): AppContext;
83
83
  export declare function buildServer(options: ServerOptions): KumikoServer;
84
84
  export declare function makeDispatchSystemWrite(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<WriteResult>;
85
+ export declare function makeDispatchWriteAs(dispatcher: Dispatcher): (args: RoleDispatchArgs) => Promise<WriteResult>;
85
86
  export declare function makeDispatchSystemQuery(dispatcher: Dispatcher): (args: SystemDispatchArgs) => Promise<unknown>;
@@ -217,9 +217,7 @@ export function buildServer(options) {
217
217
  // Master for the payload-bucket HMAC: the current signing secret (the
218
218
  // keyring's signKid for rotation setups). buildServer always has one, so
219
219
  // the digest is available whenever a handler declares additionalRateLimits.
220
- const payloadDigest = createPayloadDigest(typeof options.jwtSecret === "string"
221
- ? options.jwtSecret
222
- : (options.jwtSecret.keys[options.jwtSecret.signKid] ?? ""));
220
+ const payloadDigest = createPayloadDigest(payloadDigestMasterSecret(options.jwtSecret));
223
221
  const rateLimitResolver = options.context.rateLimit ??
224
222
  (wrappedRedis && wantsResolver ? createRateLimitResolver({ redis: wrappedRedis }) : undefined);
225
223
  if (!rateLimitResolver) {
@@ -836,9 +834,7 @@ export function buildServer(options) {
836
834
  dispatchSystemQuery,
837
835
  clientIpResolver,
838
836
  });
839
- mountHonoRoute(app, route.method, route.path, honoHandler, route.entry === ExtraRouteEntries.signature
840
- ? bodyLimitForRoutePath(route.path, options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES)
841
- : []);
837
+ mountHonoRoute(app, route.method, route.path, honoHandler, bodyLimitForRoutePath(route.path, options.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES));
842
838
  }
843
839
  }
844
840
  // /version-Default registriert NACH feature-routes — Hono "first match
@@ -949,6 +945,13 @@ c, dispatcher, clientIpResolver) {
949
945
  export function makeDispatchSystemWrite(dispatcher) {
950
946
  return ({ handlerQn, payload, tenantId }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
951
947
  }
948
+ // Privilege-scope: the system user gets exactly the roles the caller names —
949
+ // no implicit SystemAdmin — so a seed can write data gated on app roles.
950
+ // Same trust requirement as makeDispatchSystemWrite: never expose to a raw
951
+ // request, only to code that already proved its own authenticity.
952
+ export function makeDispatchWriteAs(dispatcher) {
953
+ return ({ handlerQn, payload, tenantId, roles }) => dispatcher.write(handlerQn, payload, createSystemUser(tenantId, roles));
954
+ }
952
955
  export function makeDispatchSystemQuery(dispatcher) {
953
956
  return ({ handlerQn, payload, tenantId }) => dispatcher.query(handlerQn, payload, createSystemUser(tenantId, [ROLES.SystemAdmin]));
954
957
  }
@@ -1199,3 +1202,12 @@ function entitiesWithSearchableScreen(registry) {
1199
1202
  }
1200
1203
  return [...entities];
1201
1204
  }
1205
+ function payloadDigestMasterSecret(jwtSecret) {
1206
+ if (typeof jwtSecret === "string")
1207
+ return jwtSecret;
1208
+ const signingSecret = jwtSecret.keys[jwtSecret.signKid];
1209
+ if (signingSecret === undefined) {
1210
+ throw new Error(`[kumiko] jwtSecret.signKid "${jwtSecret.signKid}" not found in jwtSecret.keys`);
1211
+ }
1212
+ return signingSecret;
1213
+ }
@@ -19,5 +19,6 @@ export declare function hostOnlyDeleteOptions(name: string): {
19
19
  path: "/";
20
20
  secure?: true;
21
21
  };
22
+ export declare function expireLegacySessionCookieVariants(c: Context, cookies: ResolvedSessionCookies): void;
22
23
  export declare function clearSessionCookies(c: Context, cookies: ResolvedSessionCookies): void;
23
24
  export declare function hasDuplicateSessionCookie(c: Context, names: SessionCookieNames): boolean;
@@ -1,4 +1,4 @@
1
- import { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, isHostPrefixedCookieName, resolveSessionCookieNames, } from "@cosmicdrift/kumiko-types/session-cookies";
1
+ import { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, isHostPrefixedCookieName, resolveSessionCookieNames, sessionCookieName, } from "@cosmicdrift/kumiko-types/session-cookies";
2
2
  import { deleteCookie } from "hono/cookie";
3
3
  export { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, HOST_COOKIE_PREFIX, isHostPrefixedCookieName, resolveSessionCookieNames, sessionCookieName, TENANT_COOKIE_BASE, } from "@cosmicdrift/kumiko-types/session-cookies";
4
4
  const SESSION_COOKIES_KEY = "sessionCookies";
@@ -41,6 +41,24 @@ export function getSessionCookies(c, fallback) {
41
41
  export function hostOnlyDeleteOptions(name) {
42
42
  return isHostPrefixedCookieName(name) ? { path: "/", secure: true } : { path: "/" };
43
43
  }
44
+ // Expires the name variant the current config does not write, in both
45
+ // directions: unprefixed leftovers when names are `__Host-` prefixed, and the
46
+ // `__Host-` cookies when a later cookieDomain or secureCookies:false resolves
47
+ // to unprefixed names. Browsers prefer the prefixed csrf cookie when reading
48
+ // the token, so a leftover one would 403 every write until it expires.
49
+ export function expireLegacySessionCookieVariants(c, cookies) {
50
+ if (isHostPrefixedCookieName(cookies.names.auth)) {
51
+ deleteCookie(c, AUTH_COOKIE_BASE, { path: "/" });
52
+ deleteCookie(c, CSRF_COOKIE_BASE, { path: "/" });
53
+ // skip: the unprefixed names are the only legacy variant when the config writes __Host- names.
54
+ return;
55
+ }
56
+ if (cookies.secure) {
57
+ deleteCookie(c, sessionCookieName(AUTH_COOKIE_BASE, { secure: true }), hostPrefixedDelete);
58
+ deleteCookie(c, sessionCookieName(CSRF_COOKIE_BASE, { secure: true }), hostPrefixedDelete);
59
+ }
60
+ }
61
+ const hostPrefixedDelete = { path: "/", secure: true };
44
62
  // Expire every variant: the Domain cookie (current) and the host-only one
45
63
  // (from before cookieDomain), otherwise a cookie left over from an earlier
46
64
  // config keeps the session alive after logout. With __Host- names the
@@ -50,10 +68,7 @@ export function clearSessionCookies(c, cookies) {
50
68
  const { names, domain } = cookies;
51
69
  deleteCookie(c, names.auth, hostOnlyDeleteOptions(names.auth));
52
70
  deleteCookie(c, names.csrf, hostOnlyDeleteOptions(names.csrf));
53
- if (isHostPrefixedCookieName(names.auth)) {
54
- deleteCookie(c, AUTH_COOKIE_BASE, { path: "/" });
55
- deleteCookie(c, CSRF_COOKIE_BASE, { path: "/" });
56
- }
71
+ expireLegacySessionCookieVariants(c, cookies);
57
72
  if (domain !== undefined) {
58
73
  deleteCookie(c, AUTH_COOKIE_BASE, { path: "/", domain });
59
74
  deleteCookie(c, CSRF_COOKIE_BASE, { path: "/", domain });