@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 (281) hide show
  1. package/AGENTS.md +338 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +446 -0
  4. package/agents/examples/anonymous-access.md +288 -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 +319 -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 +315 -0
  20. package/agents/examples/custom-handlers.md +444 -0
  21. package/agents/examples/dashboard-stats.md +229 -0
  22. package/agents/examples/delivery-notifications.md +432 -0
  23. package/agents/examples/designer-demo.md +86 -0
  24. package/agents/examples/e2e-generator.md +211 -0
  25. package/agents/examples/embedded-entity-form.md +598 -0
  26. package/agents/examples/embedded.md +649 -0
  27. package/agents/examples/encrypted-tenant-config.md +414 -0
  28. package/agents/examples/error-contract.md +535 -0
  29. package/agents/examples/event-sourcing.md +1287 -0
  30. package/agents/examples/feature-toggles.md +274 -0
  31. package/agents/examples/field-access.md +271 -0
  32. package/agents/examples/files-post-processing.md +433 -0
  33. package/agents/examples/i18n.md +166 -0
  34. package/agents/examples/idempotency.md +179 -0
  35. package/agents/examples/inbound-mail-triage.md +315 -0
  36. package/agents/examples/lane-routing.md +246 -0
  37. package/agents/examples/ledger-banking.md +215 -0
  38. package/agents/examples/ledger-invoicing.md +236 -0
  39. package/agents/examples/legal-pages.md +393 -0
  40. package/agents/examples/lifecycle-hooks.md +368 -0
  41. package/agents/examples/managed-config.md +428 -0
  42. package/agents/examples/managed-pages.md +353 -0
  43. package/agents/examples/notes-history-basic.md +346 -0
  44. package/agents/examples/ownership.md +404 -0
  45. package/agents/examples/pdf-audit-export.md +306 -0
  46. package/agents/examples/pipeline-basics.md +792 -0
  47. package/agents/examples/public-share-token.md +351 -0
  48. package/agents/examples/rate-limiting.md +211 -0
  49. package/agents/examples/realtime-sse.md +187 -0
  50. package/agents/examples/record-detail-layout.md +297 -0
  51. package/agents/examples/reference-data.md +151 -0
  52. package/agents/examples/relations.md +280 -0
  53. package/agents/examples/screens-nav.md +353 -0
  54. package/agents/examples/search.md +233 -0
  55. package/agents/examples/secrets-demo.md +417 -0
  56. package/agents/examples/security-baseline.md +187 -0
  57. package/agents/examples/seed-migration.md +99 -0
  58. package/agents/examples/session-revocation.md +250 -0
  59. package/agents/examples/state-machine.md +920 -0
  60. package/agents/examples/tags-basic.md +401 -0
  61. package/agents/examples/tenant-isolation.md +173 -0
  62. package/agents/examples/tenant-settings.md +275 -0
  63. package/agents/examples/tier-admin.md +347 -0
  64. package/agents/examples/timezones.md +366 -0
  65. package/agents/examples/user-data-rights.md +418 -0
  66. package/agents/examples/user-profile.md +226 -0
  67. package/agents/examples/webhook-step.md +597 -0
  68. package/agents/examples/wizard-form.md +379 -0
  69. package/agents/examples/workflow-engine.md +1731 -0
  70. package/agents/examples/writeform-section.md +300 -0
  71. package/agents/examples.md +75 -0
  72. package/agents/features.md +768 -0
  73. package/agents/widgets.md +181 -0
  74. package/dist/api/auth-middleware.d.ts +2 -0
  75. package/dist/api/auth-middleware.js +7 -0
  76. package/dist/api/auth-routes.d.ts +0 -1
  77. package/dist/api/auth-routes.js +49 -46
  78. package/dist/api/extra-route.d.ts +3 -0
  79. package/dist/api/index.d.ts +2 -2
  80. package/dist/api/index.js +1 -1
  81. package/dist/api/routes.js +46 -17
  82. package/dist/api/server.d.ts +4 -3
  83. package/dist/api/server.js +39 -10
  84. package/dist/api/session-cookies.d.ts +1 -0
  85. package/dist/api/session-cookies.js +20 -5
  86. package/dist/api/sse-route.js +16 -1
  87. package/dist/bun-db/connection.js +3 -16
  88. package/dist/bun-db/index.d.ts +1 -1
  89. package/dist/bun-db/index.js +1 -1
  90. package/dist/bun-db/query.d.ts +15 -3
  91. package/dist/bun-db/query.js +76 -24
  92. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  93. package/dist/crypto/owner-transition-rekey.js +7 -3
  94. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  95. package/dist/crypto/pii-field-encryption.js +9 -2
  96. package/dist/crypto/subject-resolver.d.ts +1 -0
  97. package/dist/crypto/subject-resolver.js +7 -0
  98. package/dist/db/api.d.ts +4 -3
  99. package/dist/db/apply-entity-event.js +26 -2
  100. package/dist/db/blind-index-cleanup.js +2 -1
  101. package/dist/db/bun-provider.js +9 -18
  102. package/dist/db/collect-table-metas.js +11 -7
  103. package/dist/db/dialect.d.ts +1 -1
  104. package/dist/db/dialect.js +2 -0
  105. package/dist/db/eagerload.js +4 -3
  106. package/dist/db/event-store-executor-context.d.ts +6 -6
  107. package/dist/db/event-store-executor-context.js +25 -22
  108. package/dist/db/event-store-executor-read.js +31 -16
  109. package/dist/db/event-store-executor-write.d.ts +1 -1
  110. package/dist/db/event-store-executor-write.js +84 -63
  111. package/dist/db/index.d.ts +2 -1
  112. package/dist/db/index.js +2 -1
  113. package/dist/db/lock-guard.d.ts +4 -0
  114. package/dist/db/lock-guard.js +14 -0
  115. package/dist/db/migrate-generator.js +1 -1
  116. package/dist/db/migrate-runner.d.ts +1 -0
  117. package/dist/db/migrate-runner.js +64 -38
  118. package/dist/db/queries/backfill-pii.js +63 -25
  119. package/dist/db/queries/event-consumer.d.ts +6 -6
  120. package/dist/db/queries/event-consumer.js +2 -1
  121. package/dist/db/queries/event-store-transfer.js +15 -4
  122. package/dist/db/queries/event-store.d.ts +1 -1
  123. package/dist/db/queries/event-store.js +11 -10
  124. package/dist/db/queries/pii-backfill-state.js +3 -2
  125. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  126. package/dist/db/queries/postgres-array-literal.js +6 -0
  127. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  128. package/dist/db/queries/projection-rebuild.js +3 -2
  129. package/dist/db/queries/shadow-swap.d.ts +1 -1
  130. package/dist/db/queries/shadow-swap.js +12 -8
  131. package/dist/db/query.d.ts +1 -1
  132. package/dist/db/query.js +1 -1
  133. package/dist/db/retired-framework-tables.js +22 -4
  134. package/dist/db/table-builder.js +6 -2
  135. package/dist/db/tenant-db-runner.d.ts +4 -0
  136. package/dist/db/tenant-db-runner.js +17 -0
  137. package/dist/db/tenant-db.d.ts +4 -0
  138. package/dist/db/tenant-db.js +50 -5
  139. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  140. package/dist/derivatives/delete-stored-file.js +9 -0
  141. package/dist/derivatives/derivatives-context.js +1 -1
  142. package/dist/derivatives/index.d.ts +1 -1
  143. package/dist/derivatives/index.js +1 -1
  144. package/dist/engine/boot-validator/access-declarations.js +2 -2
  145. package/dist/engine/boot-validator/api-ext.js +3 -3
  146. package/dist/engine/boot-validator/boot-check.js +1 -1
  147. package/dist/engine/boot-validator/config-deps.js +13 -13
  148. package/dist/engine/boot-validator/detail-screens.js +1 -1
  149. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  150. package/dist/engine/boot-validator/entity-handler.js +87 -35
  151. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  152. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  153. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  154. package/dist/engine/boot-validator/nav.js +1 -1
  155. package/dist/engine/boot-validator/pii-retention.js +8 -8
  156. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  157. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  158. package/dist/engine/boot-validator/query-refs.js +14 -2
  159. package/dist/engine/boot-validator/screens.d.ts +2 -1
  160. package/dist/engine/boot-validator/screens.js +153 -95
  161. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  162. package/dist/engine/dedupe-features.js +31 -4
  163. package/dist/engine/define-handler.d.ts +2 -2
  164. package/dist/engine/define-handler.js +8 -1
  165. package/dist/engine/entity-handlers.js +14 -7
  166. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  167. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  168. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  169. package/dist/engine/feature-ast/render.js +10 -2
  170. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  171. package/dist/engine/feature-entity-handlers.js +12 -3
  172. package/dist/engine/handler-field-references.d.ts +21 -2
  173. package/dist/engine/handler-field-references.js +55 -17
  174. package/dist/engine/index.d.ts +3 -3
  175. package/dist/engine/index.js +1 -1
  176. package/dist/engine/schema-builder.js +2 -1
  177. package/dist/engine/screen-helpers.d.ts +1 -0
  178. package/dist/engine/screen-helpers.js +2 -2
  179. package/dist/engine/soft-delete-cleanup.js +10 -3
  180. package/dist/engine/steps/_read-source.js +5 -2
  181. package/dist/engine/steps/retry.js +2 -1
  182. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  183. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  184. package/dist/engine/template-variables.d.ts +3 -0
  185. package/dist/engine/template-variables.js +3 -0
  186. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  187. package/dist/engine/tier-resolver-extension.js +6 -0
  188. package/dist/engine/types/index.d.ts +1 -1
  189. package/dist/entrypoint/index.d.ts +2 -2
  190. package/dist/env/_zod-introspect.d.ts +1 -0
  191. package/dist/errors/classes.js +1 -0
  192. package/dist/errors/docs-url.js +6 -6
  193. package/dist/errors/index.d.ts +1 -1
  194. package/dist/errors/index.js +1 -1
  195. package/dist/errors/kumiko-error.d.ts +2 -0
  196. package/dist/errors/kumiko-error.js +2 -0
  197. package/dist/errors/subject-erased.d.ts +1 -0
  198. package/dist/errors/subject-erased.js +6 -0
  199. package/dist/errors/write-error-info.js +2 -0
  200. package/dist/event-store/admin-api.js +3 -2
  201. package/dist/event-store/errors.d.ts +9 -0
  202. package/dist/event-store/errors.js +20 -0
  203. package/dist/event-store/event-store.d.ts +6 -0
  204. package/dist/event-store/event-store.js +28 -6
  205. package/dist/event-store/index.d.ts +1 -1
  206. package/dist/event-store/index.js +1 -1
  207. package/dist/event-store/tenant-db-append.js +2 -2
  208. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  209. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  210. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  211. package/dist/files/bind-pending-file-refs.js +3 -3
  212. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  213. package/dist/files/bind-referenced-files.write.js +22 -13
  214. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  215. package/dist/files/create-file-ref-from-bytes.js +67 -4
  216. package/dist/files/create-file-ref.d.ts +24 -1
  217. package/dist/files/create-file-ref.js +67 -25
  218. package/dist/files/dispose-removed-file-refs.js +21 -6
  219. package/dist/files/feature.d.ts +1 -0
  220. package/dist/files/feature.js +2 -1
  221. package/dist/files/file-handle.d.ts +1 -1
  222. package/dist/files/file-handle.js +12 -0
  223. package/dist/files/file-routes.js +10 -6
  224. package/dist/files/index.d.ts +3 -3
  225. package/dist/files/index.js +2 -2
  226. package/dist/files/load-save-file-refs.d.ts +10 -0
  227. package/dist/files/load-save-file-refs.js +47 -0
  228. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  229. package/dist/files/purge-expired-file-ref-bytes.js +46 -21
  230. package/dist/files/resolve-content-type.js +40 -4
  231. package/dist/files/storage-tracking.js +5 -4
  232. package/dist/files/types.js +5 -8
  233. package/dist/files/zip-entry-names.d.ts +1 -0
  234. package/dist/files/zip-entry-names.js +46 -0
  235. package/dist/jobs/index.d.ts +1 -1
  236. package/dist/jobs/job-runner.d.ts +5 -1
  237. package/dist/jobs/job-runner.js +124 -37
  238. package/dist/observability/escape-hatch-report.d.ts +2 -1
  239. package/dist/observability/escape-hatch-report.js +48 -7
  240. package/dist/observability/index.d.ts +1 -1
  241. package/dist/observability/index.js +1 -1
  242. package/dist/observability/metrics-handle.d.ts +1 -0
  243. package/dist/observability/metrics-handle.js +7 -0
  244. package/dist/observability/standard-metrics.js +4 -4
  245. package/dist/pipeline/cascade-handler.js +5 -2
  246. package/dist/pipeline/dispatch-batch.js +79 -31
  247. package/dist/pipeline/dispatch-query.d.ts +2 -1
  248. package/dist/pipeline/dispatch-query.js +6 -6
  249. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  250. package/dist/pipeline/dispatch-shared.js +136 -48
  251. package/dist/pipeline/dispatch-stream.js +10 -4
  252. package/dist/pipeline/dispatch-write.js +34 -22
  253. package/dist/pipeline/dispatcher.js +3 -1
  254. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  255. package/dist/pipeline/event-dispatcher.js +11 -9
  256. package/dist/pipeline/event-retention.d.ts +1 -0
  257. package/dist/pipeline/event-retention.js +7 -1
  258. package/dist/pipeline/index.d.ts +1 -0
  259. package/dist/pipeline/index.js +1 -0
  260. package/dist/pipeline/notify-scope.js +3 -1
  261. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  262. package/dist/pipeline/pre-transaction-reservations.js +29 -9
  263. package/dist/rate-limit/bucket.d.ts +3 -3
  264. package/dist/rate-limit/bucket.js +14 -4
  265. package/dist/redis/cache-sync-bus.js +9 -3
  266. package/dist/schema-cli.js +12 -1
  267. package/dist/search/meilisearch-adapter.js +36 -9
  268. package/dist/stack/db.d.ts +2 -2
  269. package/dist/stack/test-stack.js +2 -1
  270. package/dist/testing/index.d.ts +1 -0
  271. package/dist/testing/index.js +1 -0
  272. package/dist/testing/multipart-helper.d.ts +6 -0
  273. package/dist/testing/multipart-helper.js +1 -1
  274. package/dist/testing/zip-fixture.d.ts +1 -0
  275. package/dist/testing/zip-fixture.js +33 -0
  276. package/dist/ui-types/card-column-roles.js +4 -2
  277. package/dist/ui-types/index.d.ts +1 -1
  278. package/dist/ui-types/index.js +1 -1
  279. package/dist/upgrade-cli.js +46 -13
  280. package/package.json +7 -5
  281. package/src/changes.json +512 -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>;
@@ -8,6 +8,7 @@ import { isForeignCookieOrigin } from "./origin-middleware.js";
8
8
  import { clearSessionCookies, getSessionCookies, hasDuplicateSessionCookie, } from "./session-cookies.js";
9
9
  const USER_KEY = "pipelineUser";
10
10
  const AUTH_TRANSPORT_KEY = "authTransport";
11
+ const STALE_CREDENTIAL_KEY = "staleCredentialCode";
11
12
  const AUTH_TOKEN_EXPIRY_KEY = "authTokenExpiry";
12
13
  export const CSRF_HEADER_NAME = "X-CSRF-Token";
13
14
  // Prefix that marks a bearer token as a long-lived Personal Access Token
@@ -161,6 +162,7 @@ export function authMiddleware(jwt, options = {}) {
161
162
  }
162
163
  const rejectInvalidCredential = async (rejection) => {
163
164
  if (isStaleCookieDowngradable(c, transport, anonymousAccess)) {
165
+ c.set(STALE_CREDENTIAL_KEY, rejection.code);
164
166
  return await handleAnonymous(c, anonymousAccess, next, resolveTenantLifecycleStatus, {
165
167
  staleCredential: { rejectionIfTenantUnresolved: rejection },
166
168
  });
@@ -236,6 +238,11 @@ export function getAuthTransport(c) {
236
238
  // @cast-boundary engine-bridge — Hono context.get returns unknown
237
239
  return c.get(AUTH_TRANSPORT_KEY);
238
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
+ }
239
246
  /** JWT `exp` (epoch seconds) of the request's token; undefined for anonymous and verified-bearer (PAT) callers. */
240
247
  export function getAuthTokenExpiry(c) {
241
248
  // @cast-boundary engine-bridge — Hono context.get returns unknown
@@ -6,7 +6,6 @@ import { type AuthSessionChecker, type AuthSessionStatus, type TokenVerifier } f
6
6
  import { type ClientIpResolver } from "./client-ip.js";
7
7
  import type { JwtHelper } from "./jwt.js";
8
8
  import type { PostAuthLandingResolver } from "./post-auth-landing.js";
9
- export declare function assertRetiredCookieDomains(retiredCookieDomains: readonly string[] | undefined, cookieDomain: string | undefined): void;
10
9
  export type LoginRateLimiter = {
11
10
  check(key: string): Promise<boolean>;
12
11
  reset(key: string): Promise<void>;
@@ -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
@@ -59,7 +61,7 @@ function normalizeCookieDomain(domain) {
59
61
  }
60
62
  // A malformed entry would corrupt the Set-Cookie header; an entry equal to the
61
63
  // current cookieDomain would make login delete the cookie it just sets.
62
- export function assertRetiredCookieDomains(retiredCookieDomains, cookieDomain) {
64
+ function assertRetiredCookieDomains(retiredCookieDomains, cookieDomain) {
63
65
  const current = cookieDomain === undefined ? undefined : normalizeCookieDomain(cookieDomain);
64
66
  for (const entry of retiredCookieDomains ?? []) {
65
67
  if (entry === "" || normalizeCookieDomain(entry) === "") {
@@ -320,6 +322,10 @@ export function createAuthRoutes(dispatcher, jwt, config) {
320
322
  retiredCookieDomains: config.retiredCookieDomains,
321
323
  });
322
324
  assertRetiredCookieDomains(config.retiredCookieDomains, cookieDomain);
325
+ if (config.mfaVerifyHandler && !config.switchTenantMfaGateHandler) {
326
+ throw new Error("createAuthRoutes: mfaVerifyHandler is set but switchTenantMfaGateHandler is not; " +
327
+ "POST /auth/switch-tenant would mint admin sessions without the second factor");
328
+ }
323
329
  const retiredCookieDomains = config.retiredCookieDomains ?? [];
324
330
  // Single hop-count-aware IP getter for every rate-limit call site below —
325
331
  // see AuthRoutesConfig.trustedProxyHops and client-ip.ts's resolveClientIp
@@ -328,19 +334,45 @@ export function createAuthRoutes(dispatcher, jwt, config) {
328
334
  // shared "unknown" bucket.
329
335
  const clientIpResolver = config.resolver ?? createClientIpResolver(config.trustedProxyHops ?? 0, "createAuthRoutes");
330
336
  const getClientIp = (c) => clientIpResolver.resolve(clientIpSourceFromHonoContext(c));
331
- async function switchTenantMfaGateResponse(c, user, targetTenantId) {
337
+ // undefined = no gate wired (setup without MFA). "clearedRoles" are the roles the gate decided on,
338
+ // so the minted session cannot differ from what the gate checked.
339
+ async function switchTenantMfaGate(c, user, targetTenantId) {
332
340
  if (!config.switchTenantMfaGateHandler)
333
341
  return undefined;
334
342
  const gate = await dispatcher.write(config.switchTenantMfaGateHandler, { userId: user.id, tenantId: targetTenantId }, createSystemUser(user.tenantId));
335
343
  if (!gate.isSuccess) {
336
344
  const status = gate.error.httpStatus; // @cast-boundary engine-payload
337
- return c.json({ isSuccess: false, error: gate.error }, status);
345
+ return { response: c.json({ isSuccess: false, error: gate.error }, status) };
338
346
  }
339
347
  // @cast-boundary engine-payload — the gate handler's three-shape result
340
348
  const gateData = gate.data;
341
349
  if (gateData.kind === "mfa-gate-clear")
342
- return undefined;
343
- return pendingMfaStepResponse(c, gateData);
350
+ return { clearedRoles: gateData.roles };
351
+ return { response: pendingMfaStepResponse(c, gateData) };
352
+ }
353
+ // Global roles (SystemAdmin etc.) come from the user feature when userQuery is wired, so they survive
354
+ // the tenant switch. buildSessionRoles calls stripForbiddenMembershipRoles on the membership side only — a backstop
355
+ // for a role a projection rebuild resurrected past command-time validation (engine/membership-roles).
356
+ async function resolveSwitchTenantRoles(user, membership) {
357
+ let globalRoles = [];
358
+ if (config.userQuery) {
359
+ try {
360
+ // @cast-boundary engine-payload — dispatcher.query returnt unknown,
361
+ // Per the AuthUserRow contract, userQuery returns a row with a roles column.
362
+ const userRow = (await dispatcher.query(config.userQuery, { id: user.id }, createSystemUser(user.tenantId)));
363
+ // roles is jsonb string[] (multiSelect); parseRoles also accepts legacy
364
+ // JSON-encoded text from pre-0.217.0 rows.
365
+ globalRoles = parseRoles(userRow?.roles ?? null);
366
+ }
367
+ catch (e) {
368
+ // Non-fatal: when global roles cannot be resolved the switch continues with tenant roles
369
+ // only. A server error carrying just the cause would hurt the UX more than a switch without
370
+ // SystemAdmin, which the user notices and reports.
371
+ if (!isUnknownHandlerError(e))
372
+ throw e;
373
+ }
374
+ }
375
+ return buildSessionRoles(globalRoles, membership.roles);
344
376
  }
345
377
  // Shared tail of every route that ends a request logged-in: create the
346
378
  // session record (if wired), sign the JWT, set the auth+csrf cookies. Was
@@ -709,13 +741,13 @@ export function createAuthRoutes(dispatcher, jwt, config) {
709
741
  }
710
742
  // Session creation + JWT sign + cookies — see mintSessionAndRespond.
711
743
  const token = await mintSessionAndRespond(c, data.session);
712
- const landingPath = landingPathFragment({
713
- flow: "signup",
744
+ const signupOutcome = {
714
745
  roles: data.session.roles,
715
746
  tenantId: data.session.tenantId,
716
747
  tenantKey: data.tenantKey,
717
748
  ...(data.handover !== undefined && { handover: data.handover }),
718
- });
749
+ };
750
+ const landingPath = landingPathFragment({ flow: "signup", ...signupOutcome });
719
751
  return c.json({
720
752
  isSuccess: true,
721
753
  token,
@@ -960,49 +992,20 @@ export function createAuthRoutes(dispatcher, jwt, config) {
960
992
  return switchTenantRejectionResponse(c, activeMembership.reason);
961
993
  }
962
994
  const membership = activeMembership.membership;
963
- // Globale Rollen aus user-feature lesen wenn userQuery wired —
964
- // tenant-unabhängige Rollen (SystemAdmin etc.) überleben so den
965
- // tenant-switch. `parseRoles` liegt utils-side, hier inline-deserialize
966
- // damit das Framework keine bundled-features-Imports kriegt.
967
- let globalRoles = [];
968
- if (config.userQuery) {
969
- try {
970
- // @cast-boundary engine-payload — dispatcher.query returnt unknown,
971
- // userQuery liefert per AuthUserRow-Contract eine row mit roles-spalte.
972
- const userRow = (await dispatcher.query(config.userQuery, { id: user.id }, createSystemUser(user.tenantId)));
973
- // roles is jsonb string[] (multiSelect); parseRoles also accepts legacy
974
- // JSON-encoded text from pre-0.217.0 rows.
975
- globalRoles = parseRoles(userRow?.roles ?? null);
976
- }
977
- catch (e) {
978
- // Non-fatal: globale Rollen kann nicht aufgelöst werden → switch
979
- // läuft weiter mit nur tenant-rollen. Server-error mit nur dem
980
- // Cause ohne Stack hochwerfen wäre für die UX schlimmer als ein
981
- // Tenant-Switch ohne SystemAdmin (User merkt's und meldet's). Log
982
- // it via the dispatcher so Ops sieht's.
983
- if (!isUnknownHandlerError(e))
984
- throw e;
985
- }
986
- }
995
+ const gate = await switchTenantMfaGate(c, user, targetTenantId);
996
+ if (gate !== undefined && "response" in gate)
997
+ return gate.response;
998
+ const sessionRoles = gate !== undefined ? gate.clearedRoles : await resolveSwitchTenantRoles(user, membership);
987
999
  // Issue new JWT with the target tenant and its roles. Claims MUST be
988
1000
  // recomputed for the new tenant — stale claims from the previous
989
1001
  // tenant would leak identity facts across tenancies (e.g. teamId from
990
1002
  // tenant A accidentally surviving into tenant B's session). The
991
1003
  // resolver runs each feature's r.authClaims() hook under the new
992
1004
  // TenantDb scope.
993
- // buildSessionRoles calls stripForbiddenMembershipRoles internally and
994
- // strips reserved roles from the membership side only — globalRoles
995
- // (where SystemAdmin legitimately lives) is never filtered. Backstop for a
996
- // membership role that a projection rebuild resurrected past command-time
997
- // validation (see engine/membership-roles).
998
- const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
999
- const pendingMfaStep = await switchTenantMfaGateResponse(c, user, targetTenantId);
1000
- if (pendingMfaStep !== undefined)
1001
- return pendingMfaStep;
1002
1005
  const targetSession = {
1003
1006
  id: user.id,
1004
1007
  tenantId: targetTenantId,
1005
- roles: mergedRoles,
1008
+ roles: sessionRoles,
1006
1009
  // Tenant-independent prefs must survive the switch (fw#2343).
1007
1010
  ...tenantIndependentPrefs(user),
1008
1011
  };
@@ -1021,7 +1024,7 @@ export function createAuthRoutes(dispatcher, jwt, config) {
1021
1024
  // fresh csrfToken is minted (inside the helper) so a compromised csrf-
1022
1025
  // value can't cross a tenant boundary.
1023
1026
  const newToken = await mintSessionAndRespond(c, sessionForJwt);
1024
- return c.json({ token: newToken, tenantId: targetTenantId, roles: mergedRoles });
1027
+ return c.json({ token: newToken, tenantId: targetTenantId, roles: sessionRoles });
1025
1028
  });
1026
1029
  return api;
1027
1030
  }
@@ -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, withFileProviderResolver, } 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, withFileProviderResolver, } 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";
@@ -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) => {
@@ -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,6 +414,7 @@ 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) {
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  import type { CacheSyncBus } from "@cosmicdrift/kumiko-types/cache-sync-types";
2
2
  import { Hono } from "hono";
3
- import type { PgClient } from "../db/connection.js";
3
+ import type { PgListenClient } from "../db/connection.js";
4
4
  import { type AppContext, type Registry, type RunIn, type WriteResult } from "../engine/types/index.js";
5
5
  import type { ErrorDocsConfig } from "../errors/docs-url.js";
6
6
  import type { Lifecycle } from "../lifecycle/index.js";
@@ -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 = {
@@ -43,7 +43,7 @@ export type ServerOptions = {
43
43
  jobTrigger?: boolean;
44
44
  accessInvalidation?: boolean;
45
45
  };
46
- pgClient?: PgClient;
46
+ pgClient?: PgListenClient;
47
47
  };
48
48
  observability?: ObservabilityProvider;
49
49
  observabilityOptions?: ObservabilityOptions;
@@ -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>;