okengine 0.21.1 → 0.23.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 +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -69,7 +69,7 @@ invalidated. Delivery follows `channels` order for addresses you pass.
|
|
|
69
69
|
</Step>
|
|
70
70
|
|
|
71
71
|
<Step>
|
|
72
|
-
### Resend
|
|
72
|
+
### Resend
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
75
|
const { data } = await api.auth.resendOtp({
|
|
@@ -79,8 +79,8 @@ const { data } = await api.auth.resendOtp({
|
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
App mode keeps the same code and TTL. Provider mode is SMS only and asks the
|
|
83
|
+
provider for a new code. Default cooldown is 60 seconds either way.
|
|
84
84
|
|
|
85
85
|
</Step>
|
|
86
86
|
|
|
@@ -108,18 +108,18 @@ const { data } = await api.auth.verifyOtp({
|
|
|
108
108
|
|
|
109
109
|
## Modes
|
|
110
110
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
| Config
|
|
114
|
-
| Who owns the code
|
|
115
|
-
| Delivery
|
|
116
|
-
| Channels
|
|
117
|
-
| Resend
|
|
118
|
-
| `exposeDevOtp`
|
|
111
|
+
| | Provider mode | App mode |
|
|
112
|
+
| ----------------- | ----------------------------- | --------------------------------------------- |
|
|
113
|
+
| Config | `otp({ mode: "provider" })` | `otp({ mode: "app", channels: [...] })` |
|
|
114
|
+
| Who owns the code | Provider (Verify API) | Your app |
|
|
115
|
+
| Delivery | `fx.sendOtp` / `fx.verifyOtp` | `fx.deliverOtp` (Channel templates) |
|
|
116
|
+
| Channels | SMS only | `sms` · `whatsapp` · `email` (declared order) |
|
|
117
|
+
| Resend | SMS only, new provider code | Same code, any declared channel |
|
|
118
|
+
| `exposeDevOtp` | Forbidden | Optional (default off) |
|
|
119
119
|
|
|
120
120
|
<Callout type="warn" title="Provider mode limitation">
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
A provider resend is a new SMS code (`resendCooldownMs`, default 60s). The code never reaches OKE,
|
|
122
|
+
so SMS → email for the same code needs `mode: "app"`.
|
|
123
123
|
</Callout>
|
|
124
124
|
|
|
125
125
|
### Provider mode setup
|
|
@@ -154,27 +154,27 @@ Override copy with `oke({ channel.catalog })` or `.channelCatalog(…)` (later l
|
|
|
154
154
|
|
|
155
155
|
## Options
|
|
156
156
|
|
|
157
|
-
| Option | Type | Default | Meaning
|
|
158
|
-
| ------------------ | -------------------------------- | -------------------------- |
|
|
159
|
-
| `mode` | `"provider" \| "app"` | required | Delivery mechanism — no auto
|
|
160
|
-
| `channels` | `("sms"\|"whatsapp"\|"email")[]` | required in app mode | Build-time preferred order
|
|
161
|
-
| `ttlMs` | `number` | 10m | Challenge lifetime
|
|
162
|
-
| `resendCooldownMs` | `number` | 60s |
|
|
163
|
-
| `exposeDevOtp` | `boolean` | `false` | App mode only — raw OTP in response
|
|
164
|
-
| `from` | `string` | `OKE <no-reply@oke.local>` | Email template From
|
|
165
|
-
| `secret` | `string` | active\* | Auth secret (\*from `gate.auth`)
|
|
166
|
-
| `sessions` | `SessionStore` | active\* | Session store
|
|
167
|
-
| `identities` | `IdentityStore` | new | Email → user
|
|
168
|
-
| `phones` | `PhoneStore` | new | Phone → user
|
|
169
|
-
| `verifications` | `VerificationStore` | new | Challenge store
|
|
157
|
+
| Option | Type | Default | Meaning |
|
|
158
|
+
| ------------------ | -------------------------------- | -------------------------- | ------------------------------------ |
|
|
159
|
+
| `mode` | `"provider" \| "app"` | required | Delivery mechanism — no auto |
|
|
160
|
+
| `channels` | `("sms"\|"whatsapp"\|"email")[]` | required in app mode | Build-time preferred order |
|
|
161
|
+
| `ttlMs` | `number` | 10m | Challenge lifetime |
|
|
162
|
+
| `resendCooldownMs` | `number` | 60s | Spacing between resends (both modes) |
|
|
163
|
+
| `exposeDevOtp` | `boolean` | `false` | App mode only — raw OTP in response |
|
|
164
|
+
| `from` | `string` | `OKE <no-reply@oke.local>` | Email template From |
|
|
165
|
+
| `secret` | `string` | active\* | Auth secret (\*from `gate.auth`) |
|
|
166
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
167
|
+
| `identities` | `IdentityStore` | new | Email → user |
|
|
168
|
+
| `phones` | `PhoneStore` | new | Phone → user |
|
|
169
|
+
| `verifications` | `VerificationStore` | new | Challenge store |
|
|
170
170
|
|
|
171
171
|
## Surfaces
|
|
172
172
|
|
|
173
|
-
| Flow | Path | Gate | Mode
|
|
174
|
-
| ----------------- | ------------------------ | ------------------------ |
|
|
175
|
-
| `auth.requestOtp` | `POST /auth/otp/request` | `gate.public` + otp rate | both
|
|
176
|
-
| `auth.verifyOtp` | `POST /auth/otp/verify` | `gate.public` + otp rate | both
|
|
177
|
-
| `auth.resendOtp` | `POST /auth/otp/resend` | `gate.public` + otp rate |
|
|
173
|
+
| Flow | Path | Gate | Mode |
|
|
174
|
+
| ----------------- | ------------------------ | ------------------------ | ------------------------- |
|
|
175
|
+
| `auth.requestOtp` | `POST /auth/otp/request` | `gate.public` + otp rate | both |
|
|
176
|
+
| `auth.verifyOtp` | `POST /auth/otp/verify` | `gate.public` + otp rate | both |
|
|
177
|
+
| `auth.resendOtp` | `POST /auth/otp/resend` | `gate.public` + otp rate | both (provider: SMS only) |
|
|
178
178
|
|
|
179
179
|
## Troubleshooting
|
|
180
180
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Dragonfly"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Default store.kv image — multi-threaded Redis-wire runtime, memlock ulimit, OKE_STORE_KV_PASSWORD."
|
|
4
4
|
icon: "Zap"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Dragonfly is a multi-threaded
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Dragonfly is the default `store.kv` image in `dev` and `prod`: a multi-threaded Redis-protocol engine.
|
|
9
|
+
Same `redis://` wire format and the same `redis` driver, so Flows do not change.
|
|
10
|
+
Pin Redis or Valkey when you want those engines instead.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
13
|
Driver id stays `redis`. Dragonfly is an image choice — every KV call through `fx.store` behaves
|
|
@@ -23,7 +23,7 @@ zero Flow changes.
|
|
|
23
23
|
|
|
24
24
|
```typescript title="oke.config.ts"
|
|
25
25
|
images: {
|
|
26
|
-
"store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly",
|
|
26
|
+
"store.kv": "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
|
|
27
27
|
},
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -51,15 +51,15 @@ Driver id stays `redis` for every image below.
|
|
|
51
51
|
|
|
52
52
|
<Cards>
|
|
53
53
|
<Card
|
|
54
|
-
title="
|
|
55
|
-
description="Default store.kv —
|
|
56
|
-
href="/docs/recipes/
|
|
54
|
+
title="Dragonfly"
|
|
55
|
+
description="Default store.kv — multi-threaded Redis-wire runtime."
|
|
56
|
+
href="/docs/recipes/dragonfly"
|
|
57
57
|
/>
|
|
58
58
|
<Card title="Valkey" description="BSD-licensed Redis-wire fork." href="/docs/recipes/valkey" />
|
|
59
59
|
<Card
|
|
60
|
-
title="
|
|
61
|
-
description="
|
|
62
|
-
href="/docs/recipes/
|
|
60
|
+
title="Redis"
|
|
61
|
+
description="Opt-in Redis Open Source — requirepass + maxmemory."
|
|
62
|
+
href="/docs/recipes/redis"
|
|
63
63
|
/>
|
|
64
64
|
</Cards>
|
|
65
65
|
|
|
@@ -26,7 +26,7 @@ create-oke asks **Add PgDog connection pooling…?** (or pass `--pgdog`). Manual
|
|
|
26
26
|
```typescript title="oke.config.ts"
|
|
27
27
|
images: {
|
|
28
28
|
"store.sql": "postgres:18-alpine",
|
|
29
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
29
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
30
30
|
},
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -106,11 +106,11 @@ Scale out with [PgDog](/docs/recipes/pgdog) so `N × Bun.SQL pool` does not exha
|
|
|
106
106
|
Recreate `store-sql` after a recipe change — the data volume can stay.
|
|
107
107
|
</Callout>
|
|
108
108
|
|
|
109
|
-
| Step | What you do
|
|
110
|
-
| ------- |
|
|
111
|
-
| Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements`
|
|
112
|
-
| Create | `CREATE EXTENSION IF NOT EXISTS pg_stat_statements`
|
|
113
|
-
| Console | Store → SQL band → **Performance**
|
|
109
|
+
| Step | What you do |
|
|
110
|
+
| ------- | ---------------------------------------------------------------------- |
|
|
111
|
+
| Preload | Recipe already sets `shared_preload_libraries=pg_stat_statements` |
|
|
112
|
+
| Create | `oke db push` runs `CREATE EXTENSION IF NOT EXISTS pg_stat_statements` |
|
|
113
|
+
| Console | Store → SQL band → **Performance** |
|
|
114
114
|
|
|
115
115
|
Default `postgres:18-alpine` does **not** ship `hypopg` / `index_advisor`. Pin the
|
|
116
116
|
opt-in image when you want Suggest indexes:
|
|
@@ -137,8 +137,8 @@ set. Re-run `oke dev` so the stack writes `.env.local`, or export
|
|
|
137
137
|
<Accordion title="PgStatStatementsNotPreloaded">
|
|
138
138
|
|
|
139
139
|
The Console Performance view needs the library preloaded **and** the extension
|
|
140
|
-
created. Recreate `store-sql` so the new `command` applies, then run
|
|
141
|
-
|
|
140
|
+
created. Recreate `store-sql` so the new `command` applies, then run `oke db push`
|
|
141
|
+
(or open Performance again — it creates the extension when preload is already on).
|
|
142
142
|
|
|
143
143
|
Existing clusters that started before this recipe change keep the old
|
|
144
144
|
postmaster flags until recreate.
|
|
@@ -167,5 +167,9 @@ Postgres refusing auth.
|
|
|
167
167
|
<Cards>
|
|
168
168
|
<Card title="PgDog" description="Add pooling in front of Postgres." href="/docs/recipes/pgdog" />
|
|
169
169
|
<Card title="Neon" description="A managed alternative." href="/docs/providers/neon" />
|
|
170
|
-
<Card
|
|
170
|
+
<Card
|
|
171
|
+
title="Dragonfly"
|
|
172
|
+
description="The default store.kv image."
|
|
173
|
+
href="/docs/recipes/dragonfly"
|
|
174
|
+
/>
|
|
171
175
|
</Cards>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Redis"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Opt-in store.kv image — OKE_STORE_KV_PASSWORD, maxmemory policy, ephemeral data, REDIS_URL."
|
|
4
4
|
icon: "Zap"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Redis is
|
|
9
|
-
references containing `redis` or `keydb` and derives a
|
|
10
|
-
unauthenticated instance.
|
|
8
|
+
Redis is an opt-in `store.kv` image. The default pin is [Dragonfly](/docs/recipes/dragonfly).
|
|
9
|
+
`oke docker` matches image references containing `redis` or `keydb` and derives a
|
|
10
|
+
password-required server — no unauthenticated instance.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
13
|
The driver id stays `redis` for every Redis-wire image — Redis, Valkey, Dragonfly, or a managed
|
|
@@ -134,7 +134,8 @@ Override app / console / mcp listen ports in `oke.config.ts` `ports` — see
|
|
|
134
134
|
| `docker` | Derive compose · clean leftover stacks | `--prod\|-p`, `--out`, `--config`, `--manifest`; sub `clean` |
|
|
135
135
|
| `images` | List / pin digests | `list` · `pin` |
|
|
136
136
|
| `build` | Tree-shaken bundle | `--target\|-t bun\|node\|edge`, `--entry`, `--outdir` |
|
|
137
|
-
| `eval` | Prompt eval sets (CI gate) | `--manifest`
|
|
137
|
+
| `eval` | Prompt eval sets (CI gate) | `--manifest`, `--certify` (decision seed certificate; prompt evals stay as they are) |
|
|
138
|
+
| `decide` | Write a decision lockfile from the operator candidate | `promote <name>` (`--origin`, `--lock`) |
|
|
138
139
|
| `ai` | Configure AI driver + models | `setup` (`--provider`, `--chat`, `--vision`, `--embed`, `--yes`) |
|
|
139
140
|
| `branch` | Fork journaled state | `<name>`, `--at\|-a` |
|
|
140
141
|
| `replay` | Re-invoke a past Flow from Runs | `--request-id\|-r`, `--entry`, `--dry-run`, `--live` |
|
|
@@ -136,11 +136,13 @@ sql: {
|
|
|
136
136
|
},
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
| Field | Type | Meaning
|
|
140
|
-
| ---------- | ----------------- |
|
|
141
|
-
| `url` | connection string | Overrides the env-var resolution
|
|
142
|
-
| `pool` | `{ max?, min? }` | SQL pool
|
|
143
|
-
| `replicas` | `string[]` | Read-only
|
|
139
|
+
| Field | Type | Meaning |
|
|
140
|
+
| ---------- | ----------------- | --------------------------------------------------------------------------------- |
|
|
141
|
+
| `url` | connection string | Overrides the env-var resolution |
|
|
142
|
+
| `pool` | `{ max?, min? }` | `max` is the Bun.SQL pool size. Unset keeps 8. `min` is accepted and not applied |
|
|
143
|
+
| `replicas` | `string[]` | Read-only flows use these URLs. Each replica opens its own pool at the same `max` |
|
|
144
|
+
|
|
145
|
+
A store `max` of 20 does not share a pool another part of the process already opened at 8. Writes stay on the primary.
|
|
144
146
|
|
|
145
147
|
## images
|
|
146
148
|
|
|
@@ -150,18 +152,18 @@ Nested pins by element role — vendor choice lives here, never in driver ids:
|
|
|
150
152
|
images: {
|
|
151
153
|
store: {
|
|
152
154
|
sql: "postgres:18-alpine",
|
|
153
|
-
kv: "
|
|
155
|
+
kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0",
|
|
154
156
|
files: "rustfs/rustfs:1.0.0",
|
|
155
157
|
},
|
|
156
|
-
channel: { email: "axllent/mailpit:v1.31.
|
|
157
|
-
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
158
|
+
channel: { email: "axllent/mailpit:v1.31.2" },
|
|
159
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.59",
|
|
158
160
|
// proxy: "caddy:2-alpine",
|
|
159
161
|
},
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
Omitted image keys mean no container for that role. When both `store.sql` and `pgdog` are pinned, `DATABASE_URL` points at PgDog — see [Store](/docs/elements/store#multiple-environments).
|
|
163
165
|
|
|
164
|
-
For `store.kv`, pin
|
|
166
|
+
For `store.kv`, pin Dragonfly (default), Redis, or Valkey — driver id stays `redis`.
|
|
165
167
|
`{ durable: true }` KV lives in SQL (`oke_kv` on `DATABASE_URL`), not a second Redis image.
|
|
166
168
|
|
|
167
169
|
Compose does not manage AI inference — [OpenRouter](/docs/recipes/openrouter) or BYO
|
|
@@ -120,6 +120,10 @@ at **404**. Clients distinguish by envelope, not status. Domain codes stay **400
|
|
|
120
120
|
| Code | Status | Helper |
|
|
121
121
|
| ------------------------------------------------ | ----------- | ------------------------------------------------------ |
|
|
122
122
|
| `ValidationError` | `422` | none — failed `in`, `do` never runs |
|
|
123
|
+
| `IdempotencyKeyMissing` | `400` | none — `idempotency: "required"` and no header |
|
|
124
|
+
| `IdempotencyKeyInvalid` | `400` | none — header is not 16–255 printable ASCII |
|
|
125
|
+
| `IdempotencyKeyReused` | `422` | none — same key, different payload |
|
|
126
|
+
| `IdempotencyInProgress` | `409` | none — `Retry-After` until the in-flight call finishes |
|
|
123
127
|
| `Unauthorized` | `401` | `unauthorized` · also a gate denial |
|
|
124
128
|
| `Forbidden` | `403` | `forbidden` · also a gate denial |
|
|
125
129
|
| `NotFound` | `404` | `notFound` |
|
|
@@ -301,8 +305,14 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
301
305
|
<Accordions>
|
|
302
306
|
|
|
303
307
|
<Accordion title="I hit OKE1001–1009">
|
|
304
|
-
|
|
305
|
-
`effects` block
|
|
308
|
+
The capability token does not include what the Flow touched. Add the missing ledger entry. An
|
|
309
|
+
`effects` block must include every effect inference can see.
|
|
310
|
+
</Accordion>
|
|
311
|
+
|
|
312
|
+
<Accordion title="OKE1900 fx hidden from inference">
|
|
313
|
+
Cause: the Flow renames `fx`, lets a chain alias escape, or passes `fx` to a callee inference
|
|
314
|
+
cannot resolve. Fix: name the parameter `fx`, keep `const q = fx.store(db)` in the same function,
|
|
315
|
+
and include every effect the walk can see. Extra keys in an `effects` block are allowed.
|
|
306
316
|
</Accordion>
|
|
307
317
|
|
|
308
318
|
<Accordion title="OKE1040 pathless HTTP never stamped">
|
|
@@ -54,7 +54,9 @@ async function loadNote(id: string, fx: Fx) {
|
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
A narrower structural type will not match `store()` overloads. See [Flow](/docs/elements/flow).
|
|
57
|
+
Name that parameter `fx`. A narrower structural type will not match `store()` overloads. See [Flow](/docs/elements/flow).
|
|
58
|
+
|
|
59
|
+
Inference follows `fx` in four shapes: a direct `fx.*` call, a chain alias kept in the same function (`const q = fx.store(db)` then `q.select()`), a project helper whose parameter is named `fx`, and two package intrinsics — `liveQuery(fx, table, …)` records `reads: ["sql:<table>"]`, and `applySearchEmbedCdc` records the static `sqlRef` plus each column's embed model.
|
|
58
60
|
|
|
59
61
|
</Step>
|
|
60
62
|
|
|
@@ -155,11 +157,13 @@ on(
|
|
|
155
157
|
| `fx.auth.createTenant({ name, slug?, id? })` | `write` `auth:tenants` | Creator becomes a member. Session only |
|
|
156
158
|
| `fx.auth.upsertTenantRole({ tenantId, roleName, scopes })` | `write` `auth:tenants` | Application scopes only — `console:*` is unknown_scope |
|
|
157
159
|
|
|
158
|
-
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed) and propagates `fx.tenant.id`.
|
|
161
|
+
Read `fx.principal` for audit — it does not copy into `fx.auth`.
|
|
162
|
+
|
|
163
|
+
<Callout>
|
|
164
|
+
Gates never consult `fx.principal`. `return fx.fail(...)` is a `FlowFailure`; an unhandled `throw`
|
|
165
|
+
rethrows. The same rules apply to `app.call`.
|
|
166
|
+
</Callout>
|
|
163
167
|
|
|
164
168
|
## Concurrency and retry
|
|
165
169
|
|
|
@@ -190,11 +194,14 @@ const charge = await fx.step("charge", () =>
|
|
|
190
194
|
| `when` | thrown | Predicate; skips `AbortError` and sleep park |
|
|
191
195
|
|
|
192
196
|
<Callout title="Cooperative cancel">
|
|
193
|
-
Losing branches see `fx.signal` abort.
|
|
194
|
-
in
|
|
195
|
-
`Promise.all`.
|
|
197
|
+
Losing branches see `fx.signal` abort. `fx.fetch`, Meilisearch, remote Vault HTTP, and AI streams
|
|
198
|
+
cancel in flight. An effect that has not started throws `AbortError`.
|
|
196
199
|
</Callout>
|
|
197
200
|
|
|
201
|
+
SQL (`Bun.sql`), Redis, and a Channel send already on the wire through sently still finish. Those
|
|
202
|
+
clients expose no abort hook, and the shared connection stays open. Prefer `fx.all` over bare
|
|
203
|
+
`Promise.all`.
|
|
204
|
+
|
|
198
205
|
`fx.using(acquire, release, use)` scopes a process-local resource to one attempt: `release` runs
|
|
199
206
|
exactly once when `use` settles **or** when the ambient signal aborts (a sibling `fx.race` winner,
|
|
200
207
|
a failing `fx.all` sibling). It is not journaled — do not hold handles across durable park/resume.
|
|
@@ -221,9 +228,9 @@ contact a provider. Bodies live on `.template({ catalog })` (`{{field}}`, not IC
|
|
|
221
228
|
|
|
222
229
|
## Outbound HTTP
|
|
223
230
|
|
|
224
|
-
| Signature | Records | Notes
|
|
225
|
-
| ---------------------- | --------------------------- |
|
|
226
|
-
| `fx.fetch(url, init?)` | `fetch` on the URL hostname |
|
|
231
|
+
| Signature | Records | Notes |
|
|
232
|
+
| ---------------------- | --------------------------- | -------------------------------------------------------------------------- |
|
|
233
|
+
| `fx.fetch(url, init?)` | `fetch` on the URL hostname | Stamps `external: { host, kind: "third-party" }`. Aborts with `fx.signal`. |
|
|
227
234
|
|
|
228
235
|
Declare hosts in `effects.fetches` (e.g. `["api.stripe.com"]`). Prefer `fx.step`; use
|
|
229
236
|
`fx.retry` only when the remote API is safe to repeat. Dry runs never hit the network.
|
|
@@ -233,12 +240,13 @@ elements.
|
|
|
233
240
|
|
|
234
241
|
## AI
|
|
235
242
|
|
|
236
|
-
| Signature | Records | Returns
|
|
237
|
-
| ----------------------------------------------------- | ------------------------- |
|
|
238
|
-
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out`
|
|
239
|
-
| `fx.run(agent, input?)` | `ask` | Agent result
|
|
240
|
-
| `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included)
|
|
241
|
-
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed
|
|
243
|
+
| Signature | Records | Returns |
|
|
244
|
+
| ----------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `fx.ask(prompt, input?, { via?, tools?, maxSteps? })` | `ask` (+ `call` per tool) | Object validated against the prompt's `out` |
|
|
246
|
+
| `fx.run(agent, input?)` | `ask` | Agent result |
|
|
247
|
+
| `fx.stream(model, { prompt?, data?, via? })` | `ask` | `AsyncIterable<string>` — real driver stream; cancels via ambient `fx.signal` (HTTP disconnect included) |
|
|
248
|
+
| `fx.search(embed, query, { topK? })` | `read` | Matches from the index/embed |
|
|
249
|
+
| `fx.decide(decision, input)` | `decide` | `$.<question>.how` is `auto`, `reviewed`, or `abstained`. `audited` is a boolean flag, not a `how`. Abstain makes the value `null`. |
|
|
242
250
|
|
|
243
251
|
AI calls are nondeterministic: journaling is forced on and auto-cache disabled around them. `tools` are Flow refs — each model tool call goes through `fx.call` (same capability and Runs path).
|
|
244
252
|
|
|
@@ -340,9 +348,15 @@ to the key — see [Gate](/docs/elements/gate#api-keys).
|
|
|
340
348
|
<Accordions>
|
|
341
349
|
|
|
342
350
|
<Accordion title="OKE1001–1007 / 1008 / 1009 undeclared effect">
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
block
|
|
351
|
+
The Manifest token does not include what `do` touched. Add the missing ledger entry (`reads` ·
|
|
352
|
+
`writes` · `emits` · `sends` · `asks` · `embeds` · `secrets` · `calls` · `fetches`). An `effects`
|
|
353
|
+
block must include every effect inference can see ([Errors](/docs/reference/errors)).
|
|
354
|
+
</Accordion>
|
|
355
|
+
|
|
356
|
+
<Accordion title="OKE1900 fx hidden from inference">
|
|
357
|
+
Name the parameter `fx` and keep chain aliases in the same function. Passing `fx` to an unresolved
|
|
358
|
+
callee, or returning, reassigning, or storing `fx` or a chain alias, fails extract. An `effects`
|
|
359
|
+
block may add keys. It must include every effect the walk can see.
|
|
346
360
|
</Accordion>
|
|
347
361
|
|
|
348
362
|
<Accordion title="OKE1240 orphan emit">
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Idempotency"
|
|
3
|
+
description: "Send one Idempotency-Key so a retried mutating call runs once and a later retry replays the stored response."
|
|
4
|
+
icon: "KeyRound"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A checkout that times out should not charge the card twice. Send one `Idempotency-Key` for that attempt. The server runs `do` once and replays the stored response when the same key comes back.
|
|
9
|
+
|
|
10
|
+
<Callout title="The one rule">
|
|
11
|
+
Mint one key per logical call and send it on every retry of that call. Two submits are two calls
|
|
12
|
+
unless your app passes the same key. Provider webhooks do not send this header — keep a unique
|
|
13
|
+
constraint on the provider's event id.
|
|
14
|
+
</Callout>
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
<Steps>
|
|
19
|
+
|
|
20
|
+
<Step>
|
|
21
|
+
### Mark the flow
|
|
22
|
+
|
|
23
|
+
`idempotency: "required"` rejects a call that omits the header. Leave it off and the header is
|
|
24
|
+
optional. `"required"` on a read, GET, stream, or live flow fails extract.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { on, flow, http } from "okengine";
|
|
28
|
+
|
|
29
|
+
export const charge = on(
|
|
30
|
+
http.post("/charges", { in: ChargeIn, out: ChargeOut }),
|
|
31
|
+
flow("payments.charge", {
|
|
32
|
+
idempotency: "required",
|
|
33
|
+
do: async (input, fx) => {
|
|
34
|
+
return fx.store(db).insert(charges).values(input);
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
</Step>
|
|
41
|
+
|
|
42
|
+
<Step>
|
|
43
|
+
### Call it
|
|
44
|
+
|
|
45
|
+
The client sends `Idempotency-Key` on every non-GET call. Pass a string to reuse a key across
|
|
46
|
+
your own retries, or `false` to send none.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
await api.payments.charge({ amount: 1000 });
|
|
50
|
+
await api.payments.charge({ amount: 1000 }, { idempotencyKey: "checkout-attempt-9f3a2c" });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
</Step>
|
|
54
|
+
|
|
55
|
+
<Step>
|
|
56
|
+
### Read the replay
|
|
57
|
+
|
|
58
|
+
The first response is the live result. A later call with the same key, principal, and payload
|
|
59
|
+
returns that stored response and sets `Idempotent-Replayed: true`. The client copies that onto
|
|
60
|
+
`meta.idempotentReplayed`.
|
|
61
|
+
|
|
62
|
+
</Step>
|
|
63
|
+
|
|
64
|
+
</Steps>
|
|
65
|
+
|
|
66
|
+
## When the header is honored
|
|
67
|
+
|
|
68
|
+
All of these are required. Otherwise the header is ignored.
|
|
69
|
+
|
|
70
|
+
| Check | Honored when |
|
|
71
|
+
| ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| Entry | HTTP, and the method is not GET. RPC counts. `fx.call` has no request, so it does not. |
|
|
73
|
+
| Effects | Inferred effects include a write, emit, send, call, fetch, ask, or embed — or the flow uses `fx.raw`. Secrets alone do not qualify. |
|
|
74
|
+
| Shape | Not `stream`, and not a live SSE feed. |
|
|
75
|
+
| Option | Omitted or `{ ttl }` is `auto` (header optional). `"required"` or `{ required: true }` demands the header. `false` ignores it. |
|
|
76
|
+
|
|
77
|
+
`QUERY` is not GET. It qualifies when the rest of the row matches.
|
|
78
|
+
|
|
79
|
+
| Option | Meaning |
|
|
80
|
+
| ------------------------------- | --------------------------------------------------- |
|
|
81
|
+
| omitted | `auto` when the flow qualifies. Default TTL `24h`. |
|
|
82
|
+
| `"required"` | Missing header is `400 IdempotencyKeyMissing`. |
|
|
83
|
+
| `{ ttl: "1h" }` | `auto`, with that TTL. `ms`, `s`, `m`, `h`, or `d`. |
|
|
84
|
+
| `{ required: true, ttl: "1h" }` | Required, with that TTL. |
|
|
85
|
+
| `false` | Header ignored. |
|
|
86
|
+
|
|
87
|
+
An unparseable TTL, or `required` on a flow that cannot honor the header, fails extract.
|
|
88
|
+
|
|
89
|
+
The key is 16–255 printable ASCII characters (`0x20`–`0x7E`). Anything else is
|
|
90
|
+
`400 IdempotencyKeyInvalid`.
|
|
91
|
+
|
|
92
|
+
The scope is tenant, principal, and flow. The principal is `user:<id>`, else `apikey:<id>`, else
|
|
93
|
+
`anon`. The same key from two people is two records. The fingerprint is a hash of the flow name
|
|
94
|
+
and the validated input. The same key with a different payload is `422 IdempotencyKeyReused`.
|
|
95
|
+
|
|
96
|
+
## What a second request sees
|
|
97
|
+
|
|
98
|
+
The claim happens after the gate and after input validation. A `401`, `403`, or validation
|
|
99
|
+
failure stores nothing, so the same key can still succeed.
|
|
100
|
+
|
|
101
|
+
| Row | Response |
|
|
102
|
+
| ----------------------------- | -------------------------------------------------------------------------------------- |
|
|
103
|
+
| No row, or the TTL has passed | `do` runs. The response is stored until the TTL. |
|
|
104
|
+
| Same payload, completed | Stored status, body, `content-type`, and `location`, plus `Idempotent-Replayed: true`. |
|
|
105
|
+
| Same payload, still running | `409 IdempotencyInProgress` and `Retry-After` (seconds until the lease, at least 1). |
|
|
106
|
+
| Different payload | `422 IdempotencyKeyReused`. |
|
|
107
|
+
|
|
108
|
+
A stream, an SSE body, or a raw `Response` that is not a buffered JSON envelope is not stored.
|
|
109
|
+
The live response is returned and the row is deleted.
|
|
110
|
+
|
|
111
|
+
A throw before any non-read effect deletes the row. The same key may run again. A throw after a
|
|
112
|
+
write, emit, send, call, fetch, ask, embed, or `fx.raw` stores the `500 InternalError` envelope.
|
|
113
|
+
A new key is required. A declared `fx.fail(...)` is stored as that failure, not as a 500.
|
|
114
|
+
|
|
115
|
+
## Crashes
|
|
116
|
+
|
|
117
|
+
The in-progress lease is 30 seconds and is renewed while `do` runs. A disconnect does not cancel
|
|
118
|
+
a claimed `do`. The lease is what notices a crash.
|
|
119
|
+
|
|
120
|
+
| Flow | After the lease expires |
|
|
121
|
+
| ----------- | ----------------------------------------------------------------------------- |
|
|
122
|
+
| Not durable | The next request with the same key runs `do` again. That is at-least-once. |
|
|
123
|
+
| Durable | The next request resumes that journal run. Completed steps are not run again. |
|
|
124
|
+
|
|
125
|
+
A durable sleep stores the `204` and parks the journal run. A retry replays the `204`. The
|
|
126
|
+
scheduler continues the run.
|
|
127
|
+
|
|
128
|
+
## Where the body lives
|
|
129
|
+
|
|
130
|
+
Rows sit in `oke_idempotency` on the journal driver: memory in tests, `.oke/idempotency.json`
|
|
131
|
+
beside the journal file, and Postgres when that journal driver is bound.
|
|
132
|
+
|
|
133
|
+
The stored body can contain personal data and stays until the TTL. Expired rows are deleted on
|
|
134
|
+
the next claim and by a periodic sweep.
|
|
135
|
+
|
|
136
|
+
The Console flow contract shows an idempotency pill when the mode is `auto` or `required`.
|
|
137
|
+
|
|
138
|
+
## Troubleshooting
|
|
139
|
+
|
|
140
|
+
<Accordions>
|
|
141
|
+
<Accordion title="400 IdempotencyKeyMissing">
|
|
142
|
+
|
|
143
|
+
The catalog message is `This call requires an Idempotency-Key header.` The flow is `required`
|
|
144
|
+
and this request did not send the header. Send a key, or drop `"required"` if the header should
|
|
145
|
+
stay optional.
|
|
146
|
+
|
|
147
|
+
</Accordion>
|
|
148
|
+
<Accordion title="400 IdempotencyKeyInvalid">
|
|
149
|
+
|
|
150
|
+
The catalog message is `Idempotency-Key must be 16–255 printable ASCII characters.` Lengthen the
|
|
151
|
+
key or drop characters outside printable ASCII.
|
|
152
|
+
|
|
153
|
+
</Accordion>
|
|
154
|
+
<Accordion title="422 IdempotencyKeyReused">
|
|
155
|
+
|
|
156
|
+
The catalog message is `This Idempotency-Key was already used with a different request.` The
|
|
157
|
+
payload changed. Mint a new key for the new payload.
|
|
158
|
+
|
|
159
|
+
</Accordion>
|
|
160
|
+
<Accordion title="409 IdempotencyInProgress">
|
|
161
|
+
|
|
162
|
+
The catalog message is `This Idempotency-Key is still running. Retry after the given delay.`
|
|
163
|
+
Wait for `Retry-After`, then send the same key. The client does this when `retry` is configured.
|
|
164
|
+
|
|
165
|
+
</Accordion>
|
|
166
|
+
</Accordions>
|
|
167
|
+
|
|
168
|
+
## Learn more
|
|
169
|
+
|
|
170
|
+
- [Calling](/docs/client/calling) — `idempotencyKey` and the retry rule
|
|
171
|
+
- [Errors](/docs/reference/errors) — the four idempotency codes and their statuses
|
|
172
|
+
- [The Architecture](/docs/understand/the-architecture) — a retry of one call runs once
|
|
173
|
+
|
|
174
|
+
## Next
|
|
175
|
+
|
|
176
|
+
<Cards>
|
|
177
|
+
<Card
|
|
178
|
+
title="Calling"
|
|
179
|
+
description="Client retry and Idempotency-Key."
|
|
180
|
+
href="/docs/client/calling"
|
|
181
|
+
/>
|
|
182
|
+
<Card
|
|
183
|
+
title="Errors"
|
|
184
|
+
description="Statuses for the four idempotency codes."
|
|
185
|
+
href="/docs/reference/errors"
|
|
186
|
+
/>
|
|
187
|
+
<Card
|
|
188
|
+
title="The Architecture"
|
|
189
|
+
description="One retry runs once. Two submits are two calls."
|
|
190
|
+
href="/docs/understand/the-architecture"
|
|
191
|
+
/>
|
|
192
|
+
</Cards>
|