okengine 0.18.4 → 0.19.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 +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/index.mdx +25 -6
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +52 -11
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +78 -30
- package/site/content/docs/meta.json +11 -8
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/competitor-mention-removal.test.ts +1 -1
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +258 -44
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -132
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -162
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- package/src/drivers/ollama.ts +0 -14
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "HTTP"
|
|
3
|
+
description: "Synchronous REST endpoints, RFC 10008 QUERY, CRUD mounts, live SSE streams, and gate chains on Flow."
|
|
4
|
+
icon: "Globe"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
HTTP triggers bind web requests directly to Flows. Every standard REST verb is available alongside RFC 10008 QUERY for safe body reads, multi-verb CRUD mounts, and live Server-Sent Events (SSE).
|
|
9
|
+
|
|
10
|
+
For developers building APIs on okengine — declare the route, attach gates, return typed data.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
An HTTP trigger parses the request, checks attached gates, and invokes `flow({ do })`.
|
|
14
|
+
All business logic runs inside the Flow via `fx`. Declare `in`, `out`, and `errors` on
|
|
15
|
+
the HTTP bag when the route has a body, path params, or domain failures.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Smallest Example
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Define the route
|
|
24
|
+
|
|
25
|
+
```typescript title="src/flows/main/ping.ts"
|
|
26
|
+
import { on, flow, http } from "okengine";
|
|
27
|
+
|
|
28
|
+
export const ping = on(
|
|
29
|
+
http.get().public(),
|
|
30
|
+
flow({
|
|
31
|
+
do: () => ({ status: "ok" }),
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
</Step>
|
|
37
|
+
|
|
38
|
+
<Step>
|
|
39
|
+
### Call the endpoint
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -X GET http://localhost:6530/ping -H "accept: application/json"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Response:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"data": { "status": "ok" },
|
|
50
|
+
"error": null
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
</Step>
|
|
55
|
+
|
|
56
|
+
</Steps>
|
|
57
|
+
|
|
58
|
+
<Callout title="Omit path and name">
|
|
59
|
+
Tree default: `http.get()` and `flow({ do })` — no path or name strings. The
|
|
60
|
+
file stamps both. Pass either only for
|
|
61
|
+
[control](/docs/elements/flow/routing#when-to-omit--when-to-pass).
|
|
62
|
+
</Callout>
|
|
63
|
+
|
|
64
|
+
## Progressive Patterns
|
|
65
|
+
|
|
66
|
+
Explore HTTP flow patterns from minimal handlers to schema-validated, error-handling, and gate-protected endpoints:
|
|
67
|
+
|
|
68
|
+
<Tabs items={["Minimal", "Validated", "Failures", "Gates"]}>
|
|
69
|
+
|
|
70
|
+
<Tab value="Minimal">
|
|
71
|
+
|
|
72
|
+
Return data directly with automatic JSON response enveloping and zero boilerplate:
|
|
73
|
+
|
|
74
|
+
```typescript title="src/flows/main/health.ts"
|
|
75
|
+
import { on, flow, http } from "okengine";
|
|
76
|
+
|
|
77
|
+
export const health = on(
|
|
78
|
+
http.get().public(),
|
|
79
|
+
flow({
|
|
80
|
+
do: () => ({ ok: true }),
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
</Tab>
|
|
86
|
+
|
|
87
|
+
<Tab value="Validated">
|
|
88
|
+
|
|
89
|
+
Extract URL parameters and JSON body with runtime schema validation on the HTTP bag:
|
|
90
|
+
|
|
91
|
+
```typescript title="src/flows/notes/create.ts"
|
|
92
|
+
import { on, flow, http } from "okengine";
|
|
93
|
+
import { z } from "zod";
|
|
94
|
+
|
|
95
|
+
export const create = on(
|
|
96
|
+
http.post({
|
|
97
|
+
in: z.object({ title: z.string().min(1) }),
|
|
98
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
99
|
+
}),
|
|
100
|
+
flow({
|
|
101
|
+
do: async ({ title }, fx) => {
|
|
102
|
+
const id = fx.id();
|
|
103
|
+
return { id, title };
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
</Tab>
|
|
110
|
+
|
|
111
|
+
<Tab value="Failures">
|
|
112
|
+
|
|
113
|
+
Declare typed domain errors and return clean failure responses using `fx.fail`:
|
|
114
|
+
|
|
115
|
+
```typescript title="src/flows/orders/[id]/get.ts"
|
|
116
|
+
import { on, flow, http } from "okengine";
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
|
|
119
|
+
export const get = on(
|
|
120
|
+
http.get({
|
|
121
|
+
in: z.object({ id: z.string() }),
|
|
122
|
+
out: z.object({ id: z.string(), sku: z.string(), qty: z.number() }),
|
|
123
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
124
|
+
}),
|
|
125
|
+
flow({
|
|
126
|
+
do: async ({ id }, fx) => {
|
|
127
|
+
const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id));
|
|
128
|
+
if (!order) return fx.fail("NotFound", { id });
|
|
129
|
+
return order;
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</Tab>
|
|
136
|
+
|
|
137
|
+
<Tab value="Gates">
|
|
138
|
+
|
|
139
|
+
Chain policies and rate limits on the trigger with `.gate(...)`.
|
|
140
|
+
`fx.json.empty()` answers `204 No Content` with no body:
|
|
141
|
+
|
|
142
|
+
```typescript title="src/flows/account/delete.ts"
|
|
143
|
+
import { on, flow, http, gate } from "okengine";
|
|
144
|
+
import { eq } from "drizzle-orm";
|
|
145
|
+
import { db, users } from "@/schema";
|
|
146
|
+
|
|
147
|
+
const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
148
|
+
const admin = gate.scope("admin");
|
|
149
|
+
const deleteRate = gate.rate({ max: 5, per: "1m", keyBy: "user" });
|
|
150
|
+
|
|
151
|
+
export const deleteAccount = on(
|
|
152
|
+
http.delete().gate(member, admin, deleteRate),
|
|
153
|
+
flow({
|
|
154
|
+
do: async (_, fx) => {
|
|
155
|
+
await fx.store(db).delete(users).where(eq(users.id, fx.auth.userId!));
|
|
156
|
+
return fx.json.empty();
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
</Tab>
|
|
163
|
+
|
|
164
|
+
</Tabs>
|
|
165
|
+
|
|
166
|
+
## Method Reference
|
|
167
|
+
|
|
168
|
+
| Method | Signature | Purpose | Body Allowed | Idempotent |
|
|
169
|
+
| --------------- | -------------------------- | --------------------------------------- | --------------- | -------------- |
|
|
170
|
+
| `http.get` | `http.get(path?)` | Fetch a resource or list | No | Yes |
|
|
171
|
+
| `http.post` | `http.post(path?)` | Create resource / command | Yes | No |
|
|
172
|
+
| `http.put` | `http.put(path?)` | Replace entire resource | Yes | Yes |
|
|
173
|
+
| `http.patch` | `http.patch(path?)` | Partial resource update | Yes | No |
|
|
174
|
+
| `http.delete` | `http.delete(path?)` | Remove a resource | Optional | Yes |
|
|
175
|
+
| `http.query` | `http.query(path?)` | Safe read with JSON body | Yes (RFC 10008) | Yes |
|
|
176
|
+
| `http.head` | `http.head(path?)` | Retrieve response headers | No | Yes |
|
|
177
|
+
| `http.options` | `http.options(path?)` | Discover allowed methods | No | Yes |
|
|
178
|
+
| `http.resource` | `http.resource(path, ops)` | Five CRUD verbs; live when on | Verb-dependent | Verb-dependent |
|
|
179
|
+
| `http.live` | `http.live(signal)` | Firehose SSE on `GET /_oke/live/{name}` | No | Yes |
|
|
180
|
+
|
|
181
|
+
## Path Conventions
|
|
182
|
+
|
|
183
|
+
**Default — omit the path.** Tree files stamp the URL from disk (`http.get()`).
|
|
184
|
+
Pass a path only for [control](/docs/elements/flow/routing#when-to-omit--when-to-pass).
|
|
185
|
+
|
|
186
|
+
**Control — explicit path** — pass the URL template when the folder should not own the route:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
http.get("/organizations/:orgId/members/:memberId");
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Pathless** — omit so the compiler stamps from disk location:
|
|
193
|
+
|
|
194
|
+
```typescript title="src/flows/users/[id]/get.ts"
|
|
195
|
+
import { on, flow, http } from "okengine";
|
|
196
|
+
|
|
197
|
+
// Stamped automatically to GET /users/:id · flow users.get
|
|
198
|
+
export const get = on(http.get(), flow({ do: async ({ id }) => ({ id }) }));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Full file-tree rules: [Routing](/docs/elements/flow/routing).
|
|
202
|
+
|
|
203
|
+
## Request Parsing
|
|
204
|
+
|
|
205
|
+
Before `flow({ do })`, the HTTP engine merges request parts into one object checked against
|
|
206
|
+
the trigger's `in`:
|
|
207
|
+
|
|
208
|
+
1. **Path parameters** — `:param` segments (e.g. `{ id: "123" }`).
|
|
209
|
+
2. **Query string** — `?sort=desc` keys at the root.
|
|
210
|
+
3. **JSON body** — object fields merged into the same root.
|
|
211
|
+
4. **Headers & cookies** — bags under `headers` / `cookie` when `do` reads them (declare the same keys in `in`).
|
|
212
|
+
|
|
213
|
+
```typescript title="src/flows/items/[id]/update.ts"
|
|
214
|
+
import { on, flow, http } from "okengine";
|
|
215
|
+
import { z } from "zod";
|
|
216
|
+
import { eq } from "drizzle-orm";
|
|
217
|
+
import { db, items } from "@/schema";
|
|
218
|
+
|
|
219
|
+
export const update = on(
|
|
220
|
+
http.patch({
|
|
221
|
+
// :id from the path merges with JSON body { title }
|
|
222
|
+
in: z.object({
|
|
223
|
+
id: z.string(),
|
|
224
|
+
title: z.string().min(1),
|
|
225
|
+
}),
|
|
226
|
+
}),
|
|
227
|
+
flow({
|
|
228
|
+
do: async ({ id, title }, fx) => {
|
|
229
|
+
await fx.store(db).update(items).set({ title }).where(eq(items.id, id));
|
|
230
|
+
return { id, title };
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Read request metadata by naming `headers` / `cookie` in `in` and destructuring them in `do`
|
|
237
|
+
(header names are lower-cased):
|
|
238
|
+
|
|
239
|
+
```typescript title="src/flows/uploads/create.ts"
|
|
240
|
+
import { on, flow, http } from "okengine";
|
|
241
|
+
import { z } from "zod";
|
|
242
|
+
|
|
243
|
+
export const create = on(
|
|
244
|
+
http.post({
|
|
245
|
+
in: z.object({
|
|
246
|
+
name: z.string().min(1),
|
|
247
|
+
headers: z.object({
|
|
248
|
+
"content-type": z.string().optional(),
|
|
249
|
+
"x-request-id": z.string().optional(),
|
|
250
|
+
}),
|
|
251
|
+
cookie: z.object({
|
|
252
|
+
sid: z.string().optional(),
|
|
253
|
+
}),
|
|
254
|
+
}),
|
|
255
|
+
}),
|
|
256
|
+
flow({
|
|
257
|
+
do: async ({ name, headers, cookie }, fx) => {
|
|
258
|
+
return {
|
|
259
|
+
id: fx.id(),
|
|
260
|
+
name,
|
|
261
|
+
contentType: headers["content-type"] ?? "application/octet-stream",
|
|
262
|
+
session: cookie.sid ?? null,
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## HTTP Methods
|
|
270
|
+
|
|
271
|
+
Each verb binds with `on(http.<method>(), flow({…}))`. Omit the path on tree
|
|
272
|
+
files; pass one only for [control](/docs/elements/flow/routing#when-to-omit--when-to-pass).
|
|
273
|
+
|
|
274
|
+
<Tabs items={["GET", "POST", "PUT", "PATCH", "DELETE", "QUERY", "HEAD", "OPTIONS"]}>
|
|
275
|
+
|
|
276
|
+
<Tab value="GET">
|
|
277
|
+
|
|
278
|
+
Fetch a resource or collection:
|
|
279
|
+
|
|
280
|
+
```typescript title="src/flows/notes/[id]/get.ts"
|
|
281
|
+
import { on, flow, http } from "okengine";
|
|
282
|
+
import { z } from "zod";
|
|
283
|
+
import { eq } from "drizzle-orm";
|
|
284
|
+
import { db, notes } from "@/schema";
|
|
285
|
+
|
|
286
|
+
export const get = on(
|
|
287
|
+
http.get({
|
|
288
|
+
in: z.object({ id: z.string() }),
|
|
289
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
290
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
291
|
+
}),
|
|
292
|
+
flow({
|
|
293
|
+
do: async ({ id }, fx) => {
|
|
294
|
+
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
295
|
+
if (!note) return fx.fail("NotFound", { id });
|
|
296
|
+
return note;
|
|
297
|
+
},
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
</Tab>
|
|
303
|
+
|
|
304
|
+
<Tab value="POST">
|
|
305
|
+
|
|
306
|
+
Create a resource or run a command. `fx.json.create(value)` returns `201 Created` with
|
|
307
|
+
`{ data: value, error: null }`:
|
|
308
|
+
|
|
309
|
+
```typescript title="src/flows/notes/create.ts"
|
|
310
|
+
import { on, flow, http } from "okengine";
|
|
311
|
+
import { z } from "zod";
|
|
312
|
+
import { db, notes } from "@/schema";
|
|
313
|
+
|
|
314
|
+
export const create = on(
|
|
315
|
+
http.post({
|
|
316
|
+
in: z.object({ title: z.string().min(1) }),
|
|
317
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
318
|
+
}),
|
|
319
|
+
flow({
|
|
320
|
+
do: async ({ title }, fx) => {
|
|
321
|
+
const id = fx.id();
|
|
322
|
+
await fx.store(db).insert(notes).values({ id, title });
|
|
323
|
+
return fx.json.create({ id, title });
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
</Tab>
|
|
330
|
+
|
|
331
|
+
<Tab value="PUT">
|
|
332
|
+
|
|
333
|
+
Replace an entire resource (idempotent full write):
|
|
334
|
+
|
|
335
|
+
```typescript title="src/flows/notes/[id]/replace.ts"
|
|
336
|
+
import { on, flow, http } from "okengine";
|
|
337
|
+
import { z } from "zod";
|
|
338
|
+
import { eq } from "drizzle-orm";
|
|
339
|
+
import { db, notes } from "@/schema";
|
|
340
|
+
|
|
341
|
+
export const replace = on(
|
|
342
|
+
http.put({
|
|
343
|
+
in: z.object({
|
|
344
|
+
id: z.string(),
|
|
345
|
+
title: z.string().min(1),
|
|
346
|
+
body: z.string(),
|
|
347
|
+
}),
|
|
348
|
+
out: z.object({ id: z.string(), title: z.string(), body: z.string() }),
|
|
349
|
+
}),
|
|
350
|
+
flow({
|
|
351
|
+
do: async ({ id, title, body }, fx) => {
|
|
352
|
+
await fx.store(db).update(notes).set({ title, body }).where(eq(notes.id, id));
|
|
353
|
+
return { id, title, body };
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
</Tab>
|
|
360
|
+
|
|
361
|
+
<Tab value="PATCH">
|
|
362
|
+
|
|
363
|
+
Apply a partial update (only declared fields change):
|
|
364
|
+
|
|
365
|
+
```typescript title="src/flows/notes/[id]/update.ts"
|
|
366
|
+
import { on, flow, http } from "okengine";
|
|
367
|
+
import { z } from "zod";
|
|
368
|
+
import { eq } from "drizzle-orm";
|
|
369
|
+
import { db, notes } from "@/schema";
|
|
370
|
+
|
|
371
|
+
export const update = on(
|
|
372
|
+
http.patch({
|
|
373
|
+
in: z.object({
|
|
374
|
+
id: z.string(),
|
|
375
|
+
title: z.string().min(1).optional(),
|
|
376
|
+
}),
|
|
377
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
378
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
379
|
+
}),
|
|
380
|
+
flow({
|
|
381
|
+
do: async ({ id, title }, fx) => {
|
|
382
|
+
if (title !== undefined) {
|
|
383
|
+
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
384
|
+
}
|
|
385
|
+
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
386
|
+
if (!note) return fx.fail("NotFound", { id });
|
|
387
|
+
return note;
|
|
388
|
+
},
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
</Tab>
|
|
394
|
+
|
|
395
|
+
<Tab value="DELETE">
|
|
396
|
+
|
|
397
|
+
Remove a resource. `fx.json.empty()` returns `204 No Content` with no body:
|
|
398
|
+
|
|
399
|
+
```typescript title="src/flows/notes/[id]/remove.ts"
|
|
400
|
+
import { on, flow, http } from "okengine";
|
|
401
|
+
import { z } from "zod";
|
|
402
|
+
import { eq } from "drizzle-orm";
|
|
403
|
+
import { db, notes } from "@/schema";
|
|
404
|
+
|
|
405
|
+
export const remove = on(
|
|
406
|
+
http.delete({ in: z.object({ id: z.string() }) }),
|
|
407
|
+
flow({
|
|
408
|
+
do: async ({ id }, fx) => {
|
|
409
|
+
await fx.store(db).delete(notes).where(eq(notes.id, id));
|
|
410
|
+
return fx.json.empty();
|
|
411
|
+
},
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
</Tab>
|
|
417
|
+
|
|
418
|
+
<Tab value="QUERY">
|
|
419
|
+
|
|
420
|
+
Safe, idempotent read with a JSON body (RFC 10008) — filters that would overflow a URL:
|
|
421
|
+
|
|
422
|
+
```typescript title="src/flows/orders/search.ts"
|
|
423
|
+
import { on, flow, http } from "okengine";
|
|
424
|
+
import { z } from "zod";
|
|
425
|
+
import { db } from "@/schema";
|
|
426
|
+
|
|
427
|
+
export const search = on(
|
|
428
|
+
http.query({
|
|
429
|
+
in: z.object({
|
|
430
|
+
filters: z.array(z.string()),
|
|
431
|
+
dateRange: z.object({ from: z.string(), to: z.string() }),
|
|
432
|
+
}),
|
|
433
|
+
out: z.array(z.object({ id: z.string(), total: z.number() })),
|
|
434
|
+
}),
|
|
435
|
+
flow({
|
|
436
|
+
do: async ({ filters, dateRange }, fx) => {
|
|
437
|
+
return await fx.store(db).queryOrders(filters, dateRange);
|
|
438
|
+
},
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Clients must send `Content-Type: application/json`.
|
|
444
|
+
|
|
445
|
+
Some browsers, HTTP libraries, and reverse proxies still reject or strip bodies on methods other
|
|
446
|
+
than `POST`/`PUT`/`PATCH`. Prefer modern clients, or fall back to `POST` for the same search
|
|
447
|
+
contract when you must support older stacks.
|
|
448
|
+
|
|
449
|
+
</Tab>
|
|
450
|
+
|
|
451
|
+
<Tab value="HEAD">
|
|
452
|
+
|
|
453
|
+
Probe existence / headers without returning a body. `head` is not a reserved
|
|
454
|
+
leaf, so pass the path when the URL must match GET (`/notes/:id`):
|
|
455
|
+
|
|
456
|
+
```typescript title="src/flows/notes/[id]/head.ts"
|
|
457
|
+
import { on, flow, http } from "okengine";
|
|
458
|
+
import { z } from "zod";
|
|
459
|
+
import { eq } from "drizzle-orm";
|
|
460
|
+
import { db, notes } from "@/schema";
|
|
461
|
+
|
|
462
|
+
export const head = on(
|
|
463
|
+
http.head("/notes/:id", {
|
|
464
|
+
in: z.object({ id: z.string() }),
|
|
465
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
466
|
+
}),
|
|
467
|
+
flow({
|
|
468
|
+
do: async ({ id }, fx) => {
|
|
469
|
+
const [note] = await fx
|
|
470
|
+
.store(db)
|
|
471
|
+
.select({ id: notes.id })
|
|
472
|
+
.from(notes)
|
|
473
|
+
.where(eq(notes.id, id));
|
|
474
|
+
if (!note) return fx.fail("NotFound", { id });
|
|
475
|
+
return;
|
|
476
|
+
},
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
</Tab>
|
|
482
|
+
|
|
483
|
+
<Tab value="OPTIONS">
|
|
484
|
+
|
|
485
|
+
Advertise allowed verbs. Same idea — pass the collection path explicitly when
|
|
486
|
+
the leaf name would otherwise add a segment:
|
|
487
|
+
|
|
488
|
+
```typescript title="src/flows/notes/options.ts"
|
|
489
|
+
import { on, flow, http } from "okengine";
|
|
490
|
+
|
|
491
|
+
export const options = on(
|
|
492
|
+
http.options("/notes"),
|
|
493
|
+
flow({
|
|
494
|
+
do: () => ({
|
|
495
|
+
allow: ["GET", "POST", "PUT", "PATCH", "DELETE", "QUERY", "HEAD", "OPTIONS"],
|
|
496
|
+
}),
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
</Tab>
|
|
502
|
+
|
|
503
|
+
</Tabs>
|
|
504
|
+
|
|
505
|
+
## Resources
|
|
506
|
+
|
|
507
|
+
<Callout title="Detailed section">
|
|
508
|
+
If you only need the basic mount, jump to the example below. `on(http.resource(path, ops))` takes
|
|
509
|
+
**no Flow as a second argument** — `.all()` is the bag; options live on `store.resource`. A second
|
|
510
|
+
argument throws `on(http.resource(...)) takes no second argument`.
|
|
511
|
+
</Callout>
|
|
512
|
+
|
|
513
|
+
`http.resource(path, ops)` mounts five CRUD Flows in one `on()` call. Pass
|
|
514
|
+
`store.resource(…).all()` or any bag with `list` · `create` · `get` · `update` · `remove`.
|
|
515
|
+
|
|
516
|
+
Chain `.gate(...)` / `.public()` once — every verb (and live, when present) gets the same gates.
|
|
517
|
+
|
|
518
|
+
<Tabs items={["Define", "Mount"]}>
|
|
519
|
+
|
|
520
|
+
<Tab value="Define">
|
|
521
|
+
|
|
522
|
+
`store.resource` builds the five Flows. The factory registers no routes.
|
|
523
|
+
|
|
524
|
+
```typescript title="src/flows/notes/resource.ts"
|
|
525
|
+
import { store } from "okengine";
|
|
526
|
+
import { z } from "zod";
|
|
527
|
+
import { db, notesTable } from "@/schema";
|
|
528
|
+
|
|
529
|
+
export const notesResource = store.resource(db, notesTable, {
|
|
530
|
+
in: z.object({ title: z.string().min(1) }),
|
|
531
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
</Tab>
|
|
536
|
+
|
|
537
|
+
<Tab value="Mount">
|
|
538
|
+
|
|
539
|
+
`.gate(member)` stamps every verb. The client sees `api.notes.list` / `.create` / `.get` /
|
|
540
|
+
`.update` / `.remove` after `oke({ name: "app" }).adopt({ notes })`.
|
|
541
|
+
|
|
542
|
+
```typescript title="src/flows/notes/index.ts"
|
|
543
|
+
import { on, http } from "okengine";
|
|
544
|
+
import { member } from "@/core/gate";
|
|
545
|
+
import { notesResource } from "./resource";
|
|
546
|
+
|
|
547
|
+
export const notes = on(http.resource("/notes", notesResource.all()).gate(member));
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
</Tab>
|
|
551
|
+
|
|
552
|
+
</Tabs>
|
|
553
|
+
|
|
554
|
+
The URL id segment is always `:id`. Update is **PATCH**, not PUT. There is no
|
|
555
|
+
pathless `http.resource()` — pass an explicit base path.
|
|
556
|
+
|
|
557
|
+
| Op | Method | Path | Typical status |
|
|
558
|
+
| -------- | -------- | ------------- | ---------------------------------- |
|
|
559
|
+
| `list` | `GET` | `/notes` | `200` + `{ data, error, meta }` |
|
|
560
|
+
| `create` | `POST` | `/notes` | `201 Created` (`fx.json.create`) |
|
|
561
|
+
| `get` | `GET` | `/notes/:id` | `200`, or `NotFound` |
|
|
562
|
+
| `update` | `PATCH` | `/notes/:id` | `200`, or `NotFound` |
|
|
563
|
+
| `remove` | `DELETE` | `/notes/:id` | `204 No Content` (`fx.json.empty`) |
|
|
564
|
+
| `live` | `GET` | `/notes/live` | SSE — only when live is on |
|
|
565
|
+
|
|
566
|
+
<Accordions>
|
|
567
|
+
|
|
568
|
+
<Accordion title="Resource Options">
|
|
569
|
+
Third argument to `store.resource(db, table, options)`:
|
|
570
|
+
|
|
571
|
+
| Option | Type | Default | Meaning |
|
|
572
|
+
| ---------- | --------- | -------------------------------- | ------------------------------------------------------------------------ |
|
|
573
|
+
| `in` | Schema | _(required)_ | Create body (`POST`) |
|
|
574
|
+
| `out` | Schema | _(required)_ | Item shape (get / list / update return) |
|
|
575
|
+
| `update` | Schema | `in` | Patch fields. Wire body is `{ id, ...patch }` |
|
|
576
|
+
| `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
|
|
577
|
+
| `errors` | error map | `{ NotFound }` | Typed failures on get / update / remove |
|
|
578
|
+
| `id` | column | table PK | Column bound to `:id` |
|
|
579
|
+
| `list` | object | see List Options | List query grammar (`GET /notes`) |
|
|
580
|
+
| `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
|
|
581
|
+
| `live` | `boolean` | omitted | Live query surface; see Resource Live |
|
|
582
|
+
|
|
583
|
+
</Accordion>
|
|
584
|
+
|
|
585
|
+
<Accordion title="List Options">
|
|
586
|
+
Nested on `store.resource(…, { list: { … } })`. Search / filter / order / select
|
|
587
|
+
use a column scope: `"all"` · column array · `"none"`.
|
|
588
|
+
|
|
589
|
+
| Option | Type | Default | Meaning |
|
|
590
|
+
| ----------- | ------------------------ | ------------------------------------------------ | ------------------------------ |
|
|
591
|
+
| `mode` | `"cursor"` \| `"offset"` | `"cursor"` when `cursor` is set, else `"offset"` | Pagination |
|
|
592
|
+
| `cursor` | columns | `[]` | Keyset columns |
|
|
593
|
+
| `direction` | `"asc"` \| `"desc"` | `"desc"` | Default sort when no `?order=` |
|
|
594
|
+
| `limit` | `number` | `20` | Default page size |
|
|
595
|
+
| `maxLimit` | `number` | `100` | Cap on `?limit=` |
|
|
596
|
+
| `count` | `"exact"` \| `"none"` | `"exact"` | Offset-only `COUNT(*)` |
|
|
597
|
+
| `search` | column scope | `"none"` | `?search=` / `?q=` |
|
|
598
|
+
| `filter` | column scope | `"none"` | `?col=eq.x` grammar |
|
|
599
|
+
| `order` | column scope | cursor columns, else `"all"` | `?order=` |
|
|
600
|
+
| `select` | column scope | `"all"` | `?select=` projection |
|
|
601
|
+
|
|
602
|
+
</Accordion>
|
|
603
|
+
|
|
604
|
+
<Accordion title="Resource Members">
|
|
605
|
+
|
|
606
|
+
| Member | Kind | Meaning |
|
|
607
|
+
| ----------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
|
|
608
|
+
| `all()` | method, no args | Bag for `http.resource(path, notesResource.all())` — five Flows, plus `live` when enabled |
|
|
609
|
+
| `list` · `create` · `get` · `update` · `remove` | Flow | One verb. Bind with `http.get` / `http.post` / `http.patch` / `http.delete` |
|
|
610
|
+
| `page(input)` | method | Compile list-query input for a handwritten `fx.store(db).page` |
|
|
611
|
+
| `live` | `{ signal, flow }?` | Live surface when `live: true` (or the project default drained on) |
|
|
612
|
+
|
|
613
|
+
</Accordion>
|
|
614
|
+
|
|
615
|
+
<Accordion title="Resource Live">
|
|
616
|
+
A sixth route appears only when the resource is live. It is **not** a signal
|
|
617
|
+
firehose — each subscriber gets classified row events (RLS + list filters).
|
|
618
|
+
|
|
619
|
+
| `live` on the resource | Result |
|
|
620
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
621
|
+
| `{ live: true }` | Mount `GET <path>/live` now |
|
|
622
|
+
| omitted | Mount only if `oke({ store: { live: true } })` |
|
|
623
|
+
| `{ live: false }` | Never mount live for this resource |
|
|
624
|
+
| table `store.schema.live(false)` | Opts that table out of the project default. `{ live: true }` on the resource still wins. |
|
|
625
|
+
|
|
626
|
+
```typescript title="src/flows/notes/resource.ts"
|
|
627
|
+
const notesResource = store.resource(db, notesTable, {
|
|
628
|
+
in: z.object({ title: z.string().min(1) }),
|
|
629
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
630
|
+
live: true,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
export const notes = on(http.resource("/notes", notesResource.all()).gate(member));
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**Consequence:** `GET /notes/live` rides the same `.gate(...)` chain as list/get.
|
|
637
|
+
|
|
638
|
+
Query-string filters use the resource list grammar; pagination cursors do **not**
|
|
639
|
+
gate membership — a row enters or leaves the window when filters / RLS change.
|
|
640
|
+
|
|
641
|
+
Wire events (consumed with `useLiveQuery` on the [typed client](/docs/client/react)):
|
|
642
|
+
|
|
643
|
+
| `kind` | Meaning |
|
|
644
|
+
| --------- | ----------------------------------------------------------- |
|
|
645
|
+
| `upsert` | Row visible under stamp + query — merge by primary key |
|
|
646
|
+
| `revoked` | Row left visibility (`reason: "rls"` or `"query"`) — remove |
|
|
647
|
+
| `delete` | Row deleted — remove |
|
|
648
|
+
|
|
649
|
+
Live queries need an RLS-capable SQL driver (`postgres` / `pglite`) and a gated
|
|
650
|
+
identity on the request. Extract fails without a primary key:
|
|
651
|
+
|
|
652
|
+
```text
|
|
653
|
+
extract: live: true on table "notes" requires a primary key column (upsert/revoked/delete address rows by PK)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
Missing `updatedAt` / `updated_at`, or no RLS policies, warn at extract — they
|
|
657
|
+
do not fail the build.
|
|
658
|
+
|
|
659
|
+
</Accordion>
|
|
660
|
+
|
|
661
|
+
<Accordion title="Subset & Override">
|
|
662
|
+
`http.resource` always mounts all five CRUD keys. To expose only some verbs,
|
|
663
|
+
bind those Flows on individual triggers. To replace one verb, spread `.all()`
|
|
664
|
+
and override that key — the other four stay:
|
|
665
|
+
|
|
666
|
+
```typescript title="src/flows/notes/list.ts"
|
|
667
|
+
import { on, http, store } from "okengine";
|
|
668
|
+
import { z } from "zod";
|
|
669
|
+
import { member } from "@/core/gate";
|
|
670
|
+
import { db, notesTable } from "@/schema";
|
|
671
|
+
|
|
672
|
+
const notesResource = store.resource(db, notesTable, {
|
|
673
|
+
in: z.object({ title: z.string().min(1) }),
|
|
674
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
export const list = on(http.get().gate(member), notesResource.list);
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
```typescript title="src/flows/notes/[id]/get.ts"
|
|
681
|
+
export const get = on(http.get().gate(member), notesResource.get);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Override one verb on a resource mount — path is required on `http.resource`:
|
|
685
|
+
|
|
686
|
+
```typescript title="src/flows/notes/index.ts"
|
|
687
|
+
import { on, flow, http } from "okengine";
|
|
688
|
+
import { eq } from "drizzle-orm";
|
|
689
|
+
|
|
690
|
+
export const notes = on(
|
|
691
|
+
http
|
|
692
|
+
.resource("/notes", {
|
|
693
|
+
...notesResource.all(),
|
|
694
|
+
remove: flow({
|
|
695
|
+
do: async ({ id }, fx) => {
|
|
696
|
+
await fx
|
|
697
|
+
.store(db)
|
|
698
|
+
.update(notesTable)
|
|
699
|
+
.set({ archived: true })
|
|
700
|
+
.where(eq(notesTable.id, id));
|
|
701
|
+
return fx.json.empty();
|
|
702
|
+
},
|
|
703
|
+
}),
|
|
704
|
+
})
|
|
705
|
+
.gate(member),
|
|
706
|
+
);
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
A handwritten bag works the same way — each value must be a `flow(…)`:
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
on(
|
|
713
|
+
http.resource("/notes", {
|
|
714
|
+
list: flow("notes.list", { do: () => [] }),
|
|
715
|
+
create: flow("notes.create", { do: () => ({ id: "n1" }) }),
|
|
716
|
+
get: flow("notes.get", { do: () => ({ id: "n1" }) }),
|
|
717
|
+
update: flow("notes.update", { do: () => ({ id: "n1" }) }),
|
|
718
|
+
remove: flow("notes.remove", { do: (_, fx) => fx.json.empty() }),
|
|
719
|
+
}),
|
|
720
|
+
);
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Missing or non-Flow keys throw `on(http.resource(...)) expects the five CRUD FlowDefs`.
|
|
724
|
+
A `GET /notes` you also declared by hand collides at boot (**OKE1041**).
|
|
725
|
+
|
|
726
|
+
</Accordion>
|
|
727
|
+
|
|
728
|
+
</Accordions>
|
|
729
|
+
|
|
730
|
+
## Live Streams
|
|
731
|
+
|
|
732
|
+
<Callout title="Detailed section">
|
|
733
|
+
If you only need the basic firehose, jump to the example below. `.live(…)` is GET-only —
|
|
734
|
+
`on(http.post("/x").live(signal))` throws `on(http.*.live(signal)): live exposure must be GET`.
|
|
735
|
+
</Callout>
|
|
736
|
+
|
|
737
|
+
`http.live(signal)` is one-arg `on()` — the engine synthesizes the stream Flow
|
|
738
|
+
(`fx.live` + `effects.reads: ["signal:<name>"]`). Chain `.gate(...)` like any GET.
|
|
739
|
+
|
|
740
|
+
```typescript title="src/flows/orders/firehose.ts"
|
|
741
|
+
import { on, http, signal } from "okengine";
|
|
742
|
+
import { z } from "zod";
|
|
743
|
+
import { member } from "@/core/gate";
|
|
744
|
+
|
|
745
|
+
export const orderStatus = signal.live("order-status", {
|
|
746
|
+
optional: true,
|
|
747
|
+
schema: z.object({
|
|
748
|
+
orderId: z.string(),
|
|
749
|
+
status: z.enum(["placed", "fulfilling", "shipped"]),
|
|
750
|
+
}),
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
export const firehose = on(http.live(orderStatus).gate(member));
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
curl -N http://localhost:6530/_oke/live/order-status \
|
|
758
|
+
-H "accept: text/event-stream" \
|
|
759
|
+
-H "authorization: Bearer …"
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
Response `Content-Type` is `text/event-stream`. Frames are JSON `data:` lines
|
|
763
|
+
(optional `id:` for resume), then `data: [DONE]`.
|
|
764
|
+
|
|
765
|
+
<Accordions>
|
|
766
|
+
|
|
767
|
+
<Accordion title="Exposure Shapes">
|
|
768
|
+
Three GET shapes expose a live SSE body. Pick the physics first, then the path.
|
|
769
|
+
|
|
770
|
+
| Declaration | Path | Physics |
|
|
771
|
+
| -------------------------------------------------- | ----------------------- | ---------------------------------------------- |
|
|
772
|
+
| `on(http.live(signal))` | `GET /_oke/live/{name}` | Signal tape — every event |
|
|
773
|
+
| `on(http.get(path).live(signal))` | Your path | Signal tape — auto-match on `:params` |
|
|
774
|
+
| `on(http.get(path).live(table), flow)` | Your path | Live **query** — `liveQuery(fx, table, input)` |
|
|
775
|
+
| `store.resource({ live: true })` + `http.resource` | `GET <path>/live` | Same live-query physics as `.live(table)` |
|
|
776
|
+
|
|
777
|
+
Signal names in the default firehose path are `encodeURIComponent`'d
|
|
778
|
+
(`chat.message` stays readable; slashes in internal names are escaped).
|
|
779
|
+
|
|
780
|
+
</Accordion>
|
|
781
|
+
|
|
782
|
+
<Accordion title="Filtered Paths">
|
|
783
|
+
Path params become a filter: an event is forwarded when each `:param` that
|
|
784
|
+
**exists on the payload** equals the request value. Params missing from the
|
|
785
|
+
payload are skipped (the event still flows). No params = firehose.
|
|
786
|
+
|
|
787
|
+
```typescript title="src/flows/orders/events.ts"
|
|
788
|
+
import { on, http } from "okengine";
|
|
789
|
+
import { member } from "@/core/gate";
|
|
790
|
+
import { orderStatus } from "@/signals/orders";
|
|
791
|
+
|
|
792
|
+
export const events = on(http.get("/orders/:orderId/events").gate(member).live(orderStatus));
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
`GET /orders/ord_1/events` receives `{ orderId: "ord_1", status: "shipped" }`
|
|
796
|
+
and drops events for other orders.
|
|
797
|
+
|
|
798
|
+
</Accordion>
|
|
799
|
+
|
|
800
|
+
<Accordion title="Custom Match">
|
|
801
|
+
Pass your own Flow as the second argument to `on()` when auto-match is not
|
|
802
|
+
enough. Return `fx.live(signal, { match })` from `do` — do not wrap it with
|
|
803
|
+
`fx.json.stream`.
|
|
804
|
+
|
|
805
|
+
```typescript title="src/flows/orders/vip-feed.ts"
|
|
806
|
+
import { on, flow, http } from "okengine";
|
|
807
|
+
import { member } from "@/core/gate";
|
|
808
|
+
import { orderStatus } from "@/signals/orders";
|
|
809
|
+
|
|
810
|
+
export const vipFeed = on(
|
|
811
|
+
http.get("/orders/vip/events").gate(member).live(orderStatus),
|
|
812
|
+
flow("orders.vipFeed", {
|
|
813
|
+
do: (_input, fx) =>
|
|
814
|
+
fx.live(orderStatus, {
|
|
815
|
+
match: (payload) => payload.status === "shipped",
|
|
816
|
+
}),
|
|
817
|
+
}),
|
|
818
|
+
);
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**Consequence:** a custom Flow stamps a distinct match key, so it can coexist
|
|
822
|
+
with the auto-match route for the same signal (different path). Two synthesized
|
|
823
|
+
firehoses that share signal **and** gates fail uniqueness — see Uniqueness.
|
|
824
|
+
|
|
825
|
+
</Accordion>
|
|
826
|
+
|
|
827
|
+
<Accordion title="Live Queries">
|
|
828
|
+
For a handwritten list that should stream the same classified CDC as
|
|
829
|
+
`store.resource({ live: true })`, bind the table on GET and open the window
|
|
830
|
+
with `liveQuery`:
|
|
831
|
+
|
|
832
|
+
```typescript title="src/flows/tasks/live.ts"
|
|
833
|
+
import { on, flow, http, liveQuery } from "okengine";
|
|
834
|
+
import { member } from "@/core/gate";
|
|
835
|
+
import { tasks } from "@/schema";
|
|
836
|
+
|
|
837
|
+
export const tasksLive = on(
|
|
838
|
+
http.get("/tasks/live").gate(member).live(tasks),
|
|
839
|
+
flow("tasks.live", {
|
|
840
|
+
do: async (input, fx) =>
|
|
841
|
+
liveQuery(fx, tasks, input, {
|
|
842
|
+
filter: [tasks.status],
|
|
843
|
+
search: [tasks.title],
|
|
844
|
+
order: "all",
|
|
845
|
+
}),
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
Same driver, identity, and extract guardrails as Resource Live. Prefer
|
|
851
|
+
`http.resource` + `{ live: true }` when you already mount the five CRUD ops.
|
|
852
|
+
|
|
853
|
+
</Accordion>
|
|
854
|
+
|
|
855
|
+
<Accordion title="Uniqueness">
|
|
856
|
+
Boot keys each live HTTP route as `(signal, gates, match)`. Match is the
|
|
857
|
+
sorted path-param names, or `custom:<flow>` when you passed a Flow, or
|
|
858
|
+
`(firehose)` when there are no params.
|
|
859
|
+
|
|
860
|
+
| Pair | Boots? |
|
|
861
|
+
| --------------------------------------- | ----------------------------------------- |
|
|
862
|
+
| Member `:orderId` + admin firehose | Yes — gates and match differ |
|
|
863
|
+
| Same params, different gates | Yes — the client disambiguates with `via` |
|
|
864
|
+
| Two member firehoses on different paths | No — **OKE1050** |
|
|
865
|
+
| Same method + path twice | No — **OKE1041** first |
|
|
866
|
+
|
|
867
|
+
**OKE1050** cause: `Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}).`
|
|
868
|
+
Fix: a different gate, a path-param filter, or drop the extra route.
|
|
869
|
+
|
|
870
|
+
</Accordion>
|
|
871
|
+
|
|
872
|
+
<Accordion title="Client Subscription">
|
|
873
|
+
`signal.live` is HTTP SSE. `for await` stays on the server; the browser
|
|
874
|
+
uses a callback.
|
|
875
|
+
|
|
876
|
+
The client picks the unique exposure whose `matchKey` fields are a subset of
|
|
877
|
+
the input, preferring the largest match (`{ orderId }` beats firehose). A tie
|
|
878
|
+
needs `via: "unit.flow"`.
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
const stop = api.live(
|
|
882
|
+
orderStatus,
|
|
883
|
+
{ orderId: "ord_1" },
|
|
884
|
+
{
|
|
885
|
+
onEvent: (event) => {
|
|
886
|
+
/* { orderId, status } */
|
|
887
|
+
},
|
|
888
|
+
onError: (err) => {
|
|
889
|
+
/* 4xx, envelope, or drop */
|
|
890
|
+
},
|
|
891
|
+
autoResubscribe: false,
|
|
892
|
+
},
|
|
893
|
+
);
|
|
894
|
+
stop();
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
`api.orders.events({ orderId }, { onEvent })` is the same shape on the exposing
|
|
898
|
+
Flow. Reconnects send `Last-Event-ID` from the last `id:` received.
|
|
899
|
+
|
|
900
|
+
A **410** `LiveResumeGap` (**OKE1210**) means that cursor is gone — drop it
|
|
901
|
+
and replay the remaining tape (`autoResubscribe: true`).
|
|
902
|
+
|
|
903
|
+
Resource live queries use `useLiveQuery` (snapshot + classified events), not
|
|
904
|
+
`api.live`. See [Client · Live](/docs/client/live).
|
|
905
|
+
|
|
906
|
+
</Accordion>
|
|
907
|
+
|
|
908
|
+
</Accordions>
|
|
909
|
+
|
|
910
|
+
## Trigger Modifiers
|
|
911
|
+
|
|
912
|
+
Every HTTP trigger supports fluent modifier chaining before binding to `on()`.
|
|
913
|
+
Resource mounts accept `.gate(...)` and `.public()` only — live on a resource
|
|
914
|
+
comes from `store.resource({ live: true })`, not `.live()`.
|
|
915
|
+
|
|
916
|
+
**Gates** — attach policy and rate handles. They evaluate in declaration order; first denial wins:
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
import { gate } from "okengine";
|
|
920
|
+
import { member } from "@/core/gate";
|
|
921
|
+
|
|
922
|
+
http.post().gate(member, gate.scope("editor"), gate.rate({ max: 100, per: "1m", keyBy: "user" }));
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Public** — explicitly marks the endpoint as open without authentication:
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
http.get().public();
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
## Response Envelopes
|
|
932
|
+
|
|
933
|
+
Every HTTP flow returns the same envelope shape. You choose status and optional `meta` — not a
|
|
934
|
+
custom wrapper.
|
|
935
|
+
|
|
936
|
+
<Callout title="Envelope is fixed">
|
|
937
|
+
Success and failure always use `{ data, error }` (optional top-level `meta`). There is no API to
|
|
938
|
+
replace that shape. Use `fx.json.*` for status codes and `meta`; use `fx.fail` for typed errors.
|
|
939
|
+
</Callout>
|
|
940
|
+
|
|
941
|
+
**Success** — returning a value from `do` produces `200 OK`:
|
|
942
|
+
|
|
943
|
+
```json
|
|
944
|
+
{ "data": { "id": "123" }, "error": null }
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Returning `undefined` produces a `204 No Content` response with an empty body.
|
|
948
|
+
|
|
949
|
+
**Custom status** — `fx.json.create` for `201 Created`, or `fx.json.ok` with optional `meta`:
|
|
950
|
+
|
|
951
|
+
```typescript
|
|
952
|
+
return fx.json.create({ id: "ord_1" });
|
|
953
|
+
// or
|
|
954
|
+
return fx.json.ok({ id: "ord_1" }, { meta: { traceId: fx.runId } });
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Typed failures** — `fx.fail(code, data)` formats the error envelope and maps status:
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
return fx.fail("NotFound", { id: "123" });
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
```json
|
|
964
|
+
{
|
|
965
|
+
"data": null,
|
|
966
|
+
"error": {
|
|
967
|
+
"code": "NotFound",
|
|
968
|
+
"message": "Resource not found",
|
|
969
|
+
"data": { "id": "123" }
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Standard status code mappings:
|
|
975
|
+
|
|
976
|
+
- `ValidationError` → `422 Unprocessable Entity`
|
|
977
|
+
- `Unauthorized` → `401 Unauthorized`
|
|
978
|
+
- `Forbidden` → `403 Forbidden`
|
|
979
|
+
- `RateLimited` → `429 Too Many Requests`
|
|
980
|
+
- Custom error codes → `400 Bad Request`
|
|
981
|
+
|
|
982
|
+
## Troubleshooting
|
|
983
|
+
|
|
984
|
+
<Accordions>
|
|
985
|
+
|
|
986
|
+
<Accordion title="404 Not Found — route missing">
|
|
987
|
+
No Flow is bound to that method + path. Check the explicit path, or for pathless routes the
|
|
988
|
+
file-tree stamp (`src/flows/users/[id]/get.ts` → `GET /users/:id`). A bare `404` with body `Not
|
|
989
|
+
Found` means the router found no match.
|
|
990
|
+
</Accordion>
|
|
991
|
+
|
|
992
|
+
<Accordion title="405 Method Not Allowed on valid route">
|
|
993
|
+
The path exists but has not been bound to the requested HTTP verb. The response contains an
|
|
994
|
+
`Allow` header listing valid methods for that path.
|
|
995
|
+
</Accordion>
|
|
996
|
+
|
|
997
|
+
<Accordion title="415 Unsupported Media Type on QUERY">
|
|
998
|
+
RFC 10008 requires `Content-Type: application/json` for `http.query` requests. Ensure your client
|
|
999
|
+
sends this header with a valid JSON payload.
|
|
1000
|
+
</Accordion>
|
|
1001
|
+
|
|
1002
|
+
<Accordion title="422 ValidationError on request">
|
|
1003
|
+
The merged input payload failed validation against the trigger's `in` schema. Check the
|
|
1004
|
+
`error.data.issues` array for the specific field validation failure.
|
|
1005
|
+
</Accordion>
|
|
1006
|
+
|
|
1007
|
+
<Accordion title="Browser blocked by CORS / missing Access-Control-*">
|
|
1008
|
+
Cross-origin access is closed until you plug the [`cors`](/docs/plugins/cors) plugin with an
|
|
1009
|
+
explicit `origin`. Same-origin calls need no CORS headers. Preflight `OPTIONS` is answered by the
|
|
1010
|
+
plugin even when the path is bound to other methods.
|
|
1011
|
+
</Accordion>
|
|
1012
|
+
|
|
1013
|
+
<Accordion title="TypeError: on(http.resource(...)) takes no second argument">
|
|
1014
|
+
The ops bag already holds the five Flows. Call `on(http.resource("/notes",
|
|
1015
|
+
notesResource.all()).gate(member))` — do not pass a `flow(...)` as the second argument.
|
|
1016
|
+
</Accordion>
|
|
1017
|
+
|
|
1018
|
+
<Accordion title="TypeError: on(http.resource(...)) expects the five CRUD FlowDefs">
|
|
1019
|
+
The bag must include `list`, `create`, `get`, `update`, and `remove`, each a `flow(...)`. To
|
|
1020
|
+
expose fewer verbs, bind those Flows on `http.get` / `http.post` yourself instead of
|
|
1021
|
+
`http.resource`.
|
|
1022
|
+
</Accordion>
|
|
1023
|
+
|
|
1024
|
+
<Accordion title="OKE1041 — method + path bound twice">
|
|
1025
|
+
Cause: `{method} {path} is bound twice (flow "{flow}").` A resource mount plus a handwritten
|
|
1026
|
+
`http.get("/notes")` (or two mounts on the same base path) collide. Drop one binding.
|
|
1027
|
+
</Accordion>
|
|
1028
|
+
|
|
1029
|
+
<Accordion title="TypeError: live exposure must be GET">
|
|
1030
|
+
Live SSE feeds declared via `.live(signal)` can only be attached to `GET` triggers
|
|
1031
|
+
(`http.get(...)` or `http.live(...)`). Other verbs reject live stream synthesis.
|
|
1032
|
+
</Accordion>
|
|
1033
|
+
|
|
1034
|
+
<Accordion title="OKE1050 — live signal exposed twice">
|
|
1035
|
+
Cause: `Live signal "{signal}" is exposed twice with the same gates ({gates}) and match ({match}
|
|
1036
|
+
).` Two firehoses (`http.live` or param-less `.live`) that share the signal and gates cannot boot.
|
|
1037
|
+
Change the gate, add a path-param filter, or remove a route.
|
|
1038
|
+
</Accordion>
|
|
1039
|
+
|
|
1040
|
+
<Accordion title="OKE1210 — 410 LiveResumeGap">
|
|
1041
|
+
Cause: `Cursor "{afterId}" missing on "{signal}".` That `Last-Event-ID` is gone from the tape.
|
|
1042
|
+
Reconnect without it; remaining events replay. `autoResubscribe: true` does this after backoff.
|
|
1043
|
+
</Accordion>
|
|
1044
|
+
|
|
1045
|
+
<Accordion title="live query requires a primary key / RLS driver">
|
|
1046
|
+
Extract: `live: true on table "…" requires a primary key column`. Runtime: `live query for "…"
|
|
1047
|
+
requires an RLS-capable SQL driver (postgres / pglite)` or `requires a gated identity`. Attach
|
|
1048
|
+
`.gate(...)` and declare a PK.
|
|
1049
|
+
</Accordion>
|
|
1050
|
+
|
|
1051
|
+
</Accordions>
|
|
1052
|
+
|
|
1053
|
+
## Learn more
|
|
1054
|
+
|
|
1055
|
+
- [Store](/docs/elements/store) — `store.resource`, list query grammar, SQL facet
|
|
1056
|
+
- [Signal · Live](/docs/elements/signal/live) — `signal.live` tapes
|
|
1057
|
+
- [Client](/docs/client/live) — `api.live`, `useLive`, `useLiveQuery`
|
|
1058
|
+
- [fx](/docs/reference/fx) — `fx.live`, `fx.json.stream`, `fx.json.create`
|
|
1059
|
+
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on triggers
|
|
1060
|
+
- [Errors](/docs/reference/errors) — OKE1041 · OKE1050 · OKE1210
|
|
1061
|
+
|
|
1062
|
+
## Next
|
|
1063
|
+
|
|
1064
|
+
<Cards>
|
|
1065
|
+
<Card
|
|
1066
|
+
title="Gate Element"
|
|
1067
|
+
description="Configure authentication, authorization, and rate limiting."
|
|
1068
|
+
href="/docs/elements/gate"
|
|
1069
|
+
/>
|
|
1070
|
+
<Card
|
|
1071
|
+
title="Consumers"
|
|
1072
|
+
description="Signal workers, named Clock jobs, and SQL CDC — one Flow species."
|
|
1073
|
+
href="/docs/elements/flow/consumers"
|
|
1074
|
+
/>
|
|
1075
|
+
<Card
|
|
1076
|
+
title="Durable Workflows"
|
|
1077
|
+
description="Step journaling and multi-step distributed execution."
|
|
1078
|
+
href="/docs/elements/flow/workflows"
|
|
1079
|
+
/>
|
|
1080
|
+
</Cards>
|