@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.
- package/AGENTS.md +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- 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>;
|
package/dist/api/auth-routes.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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:
|
|
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:
|
|
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;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/api/routes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
66
|
+
for (const cmd of commands)
|
|
63
67
|
assertPayloadDepthAllowed(cmd.payload);
|
|
64
68
|
if (user.pat) {
|
|
65
|
-
for (const cmd of
|
|
69
|
+
for (const cmd of commands)
|
|
66
70
|
assertPatAllowed(user, cmd.type);
|
|
67
71
|
}
|
|
68
|
-
const result = await dispatcher.batch(
|
|
72
|
+
const result = await dispatcher.batch(commands, user, requestId);
|
|
69
73
|
if (!result.isSuccess) {
|
|
70
74
|
const err = reraiseAsKumikoError(result.error);
|
|
71
|
-
const
|
|
72
|
-
const failedType = result.failedIndex != null ?
|
|
73
|
-
logServerFault(err,
|
|
74
|
-
const { error } = serializeError(err,
|
|
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),
|
|
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 (
|
|
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
|
-
|
|
287
|
-
|
|
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
|
}
|
package/dist/api/server.d.ts
CHANGED
|
@@ -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 {
|
|
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?:
|
|
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>;
|