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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { oke } from "../kernel/app.ts";
|
|
11
|
+
import { call } from "../kernel/call.ts";
|
|
11
12
|
import { flow, resetFlowSeq } from "../kernel/flow.ts";
|
|
12
13
|
import { on, resetBindings } from "../kernel/on.ts";
|
|
13
14
|
import { http } from "../kernel/triggers.ts";
|
|
@@ -38,20 +39,15 @@ const NewNote = z.object({
|
|
|
38
39
|
const NotFound = z.object({});
|
|
39
40
|
|
|
40
41
|
const create = on(
|
|
41
|
-
http.post("/notes"),
|
|
42
|
+
http.post("/notes", { in: NewNote, out: NoteId }),
|
|
42
43
|
flow("notes.create", {
|
|
43
|
-
in: NewNote,
|
|
44
|
-
out: NoteId,
|
|
45
44
|
do: (input) => ({ id: `n_${input.title}` }),
|
|
46
45
|
}),
|
|
47
46
|
);
|
|
48
47
|
|
|
49
48
|
const get = on(
|
|
50
|
-
http.get("/notes/:id"),
|
|
49
|
+
http.get("/notes/:id", { in: NoteId, out: Note, errors: { NotFound } }),
|
|
51
50
|
flow("notes.get", {
|
|
52
|
-
in: NoteId,
|
|
53
|
-
out: Note,
|
|
54
|
-
errors: { NotFound },
|
|
55
51
|
do: ({ id }, fx) =>
|
|
56
52
|
id === "missing" ? fx.fail("NotFound", {}) : { id, title: "First", body: "Hello" },
|
|
57
53
|
}),
|
|
@@ -252,7 +248,7 @@ describe("Notes — typeof app carries contracts", () => {
|
|
|
252
248
|
});
|
|
253
249
|
|
|
254
250
|
test("untriggered adopted flow is RPC-only on the client", async () => {
|
|
255
|
-
const stats =
|
|
251
|
+
const stats = call("notes.stats", {
|
|
256
252
|
in: NoteId,
|
|
257
253
|
out: z.object({ clicks: z.number() }),
|
|
258
254
|
do: () => ({ clicks: 7 }),
|
|
@@ -280,8 +276,7 @@ describe("Notes — typeof app carries contracts", () => {
|
|
|
280
276
|
});
|
|
281
277
|
|
|
282
278
|
test("typeof app live exposure is subscribe, not JSON RPC", () => {
|
|
283
|
-
const orderStatus = signal("order-status", {
|
|
284
|
-
delivery: "live",
|
|
279
|
+
const orderStatus = signal.live("order-status", {
|
|
285
280
|
optional: true,
|
|
286
281
|
schema: z.object({ orderId: z.string(), status: z.string() }),
|
|
287
282
|
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SSE frame pump for live subscribe and JSON streams.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Callback for one parsed SSE data frame. */
|
|
8
|
+
export type SseFrameHandler = (event: unknown, id: string | undefined) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read an SSE response body until `[DONE]`, abort, or EOF.
|
|
12
|
+
*
|
|
13
|
+
* @param res - Fetch response (`text/event-stream`)
|
|
14
|
+
* @param onEvent - Frame handler
|
|
15
|
+
* @param signal - Abort signal
|
|
16
|
+
* @param onOpen - Called after content-type validation
|
|
17
|
+
*/
|
|
18
|
+
export async function readSse(
|
|
19
|
+
res: Response,
|
|
20
|
+
onEvent: SseFrameHandler,
|
|
21
|
+
signal: AbortSignal,
|
|
22
|
+
onOpen?: () => void,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
if (signal.aborted) return;
|
|
25
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
26
|
+
if (!res.ok || !res.body) {
|
|
27
|
+
const text = await res.text().catch(() => "");
|
|
28
|
+
throw sseError(res.status, text);
|
|
29
|
+
}
|
|
30
|
+
if (!ct.includes("text/event-stream")) {
|
|
31
|
+
const text = await res.text().catch(() => "");
|
|
32
|
+
throw sseError(res.status, text || `Expected text/event-stream, got ${ct || "none"}`);
|
|
33
|
+
}
|
|
34
|
+
onOpen?.();
|
|
35
|
+
const reader = res.body.getReader();
|
|
36
|
+
const dec = new TextDecoder();
|
|
37
|
+
let buf = "";
|
|
38
|
+
try {
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (signal.aborted) return;
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done) break;
|
|
43
|
+
buf += dec.decode(value, { stream: true });
|
|
44
|
+
let sep = buf.indexOf("\n\n");
|
|
45
|
+
while (sep >= 0) {
|
|
46
|
+
const raw = buf.slice(0, sep);
|
|
47
|
+
buf = buf.slice(sep + 2);
|
|
48
|
+
const stop = dispatchFrame(raw, onEvent, signal);
|
|
49
|
+
if (stop || signal.aborted) return;
|
|
50
|
+
sep = buf.indexOf("\n\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
reader.releaseLock();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Yield SSE JSON frames as an async iterable until `[DONE]`.
|
|
60
|
+
*
|
|
61
|
+
* @param res - Fetch response
|
|
62
|
+
* @param signal - Abort signal
|
|
63
|
+
*/
|
|
64
|
+
export async function* iterateSse(res: Response, signal: AbortSignal): AsyncGenerator<unknown> {
|
|
65
|
+
const queue: unknown[] = [];
|
|
66
|
+
let wake: (() => void) | undefined;
|
|
67
|
+
let done = false;
|
|
68
|
+
let err: unknown;
|
|
69
|
+
|
|
70
|
+
const pump = readSse(
|
|
71
|
+
res,
|
|
72
|
+
(event) => {
|
|
73
|
+
queue.push(event);
|
|
74
|
+
wake?.();
|
|
75
|
+
},
|
|
76
|
+
signal,
|
|
77
|
+
)
|
|
78
|
+
.then(() => {
|
|
79
|
+
done = true;
|
|
80
|
+
wake?.();
|
|
81
|
+
})
|
|
82
|
+
.catch((e) => {
|
|
83
|
+
err = e;
|
|
84
|
+
done = true;
|
|
85
|
+
wake?.();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
for (;;) {
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
yield queue.shift();
|
|
92
|
+
}
|
|
93
|
+
if (done) break;
|
|
94
|
+
await new Promise<void>((r) => {
|
|
95
|
+
wake = r;
|
|
96
|
+
});
|
|
97
|
+
wake = undefined;
|
|
98
|
+
}
|
|
99
|
+
if (err) throw err;
|
|
100
|
+
} finally {
|
|
101
|
+
await pump.catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function dispatchFrame(raw: string, onEvent: SseFrameHandler, signal: AbortSignal): boolean {
|
|
106
|
+
const dataLines: string[] = [];
|
|
107
|
+
let id: string | undefined;
|
|
108
|
+
for (const line of raw.split("\n")) {
|
|
109
|
+
if (line.startsWith("id:")) id = line.slice(3).replace(/^ /, "");
|
|
110
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
111
|
+
}
|
|
112
|
+
if (dataLines.length === 0) return false;
|
|
113
|
+
const data = dataLines.join("\n");
|
|
114
|
+
if (data === "[DONE]") return true;
|
|
115
|
+
if (signal.aborted) return true;
|
|
116
|
+
onEvent(JSON.parse(data) as unknown, id);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Build an Error with optional HTTP status. */
|
|
121
|
+
export function sseError(status: number, body: string): Error {
|
|
122
|
+
let message = `HTTP ${status}`;
|
|
123
|
+
if (body) {
|
|
124
|
+
try {
|
|
125
|
+
const json: unknown = JSON.parse(body);
|
|
126
|
+
if (json !== null && typeof json === "object" && "error" in json) {
|
|
127
|
+
const err = (json as { error?: { code?: string; data?: { message?: string } } }).error;
|
|
128
|
+
message = err?.data?.message ?? err?.code ?? message;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
message = body.slice(0, 200);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const e = new Error(message);
|
|
135
|
+
(e as Error & { status?: number }).status = status;
|
|
136
|
+
return e;
|
|
137
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finite SSE stream consumer for `fx.json.stream` Flows.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { iterateSse, sseError } from "./sse.ts";
|
|
8
|
+
import type { ClientFetch, ClientHeaders, ClientOptions, ClientRouteMap } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
/** Flow id → REST route for stream-only (non-live) SSE. */
|
|
11
|
+
export type StreamByFlow = Readonly<
|
|
12
|
+
Record<string, { readonly method: string; readonly path: string }>
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Collect `stream: true` routes that are not live exposures.
|
|
17
|
+
*
|
|
18
|
+
* @param $routes - Runtime route map
|
|
19
|
+
*/
|
|
20
|
+
export function flattenStreamRoutes($routes: ClientRouteMap | undefined): StreamByFlow {
|
|
21
|
+
const out: Record<string, { readonly method: string; readonly path: string }> = {};
|
|
22
|
+
if (!$routes) return out;
|
|
23
|
+
for (const [unit, flows] of Object.entries($routes)) {
|
|
24
|
+
if (!flows || typeof flows !== "object") continue;
|
|
25
|
+
for (const [flow, contract] of Object.entries(flows)) {
|
|
26
|
+
if (!contract || typeof contract !== "object") continue;
|
|
27
|
+
if (!("stream" in contract) || contract.stream !== true) continue;
|
|
28
|
+
if ("live" in contract && typeof contract.live === "string") continue;
|
|
29
|
+
const method = "method" in contract ? contract.method : undefined;
|
|
30
|
+
const path = "path" in contract ? contract.path : undefined;
|
|
31
|
+
if (typeof method === "string" && typeof path === "string") {
|
|
32
|
+
out[`${unit}.${flow}`] = { method, path };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Open a one-shot SSE stream (no auto-resubscribe).
|
|
41
|
+
*
|
|
42
|
+
* @param base - Origin
|
|
43
|
+
* @param route - REST method/path
|
|
44
|
+
* @param input - JSON body / path params
|
|
45
|
+
* @param opts - Client options
|
|
46
|
+
*/
|
|
47
|
+
export async function* openStream(
|
|
48
|
+
base: string,
|
|
49
|
+
route: { readonly method: string; readonly path: string },
|
|
50
|
+
input: unknown,
|
|
51
|
+
opts: ClientOptions,
|
|
52
|
+
): AsyncGenerator<unknown> {
|
|
53
|
+
const fetchFn: ClientFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
54
|
+
let refreshed = false;
|
|
55
|
+
for (;;) {
|
|
56
|
+
const ctrl = new AbortController();
|
|
57
|
+
const parent = opts.signal;
|
|
58
|
+
if (parent) {
|
|
59
|
+
if (parent.aborted) {
|
|
60
|
+
ctrl.abort();
|
|
61
|
+
} else {
|
|
62
|
+
parent.addEventListener("abort", () => ctrl.abort(), { once: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const { url, method, body } = streamRequest(base, route.method, route.path, input);
|
|
66
|
+
const headers = new Headers({ accept: "text/event-stream" });
|
|
67
|
+
const extra = typeof opts.headers === "function" ? await opts.headers() : opts.headers;
|
|
68
|
+
applyHeaders(headers, extra);
|
|
69
|
+
if (body !== undefined && !headers.has("content-type")) {
|
|
70
|
+
headers.set("content-type", "application/json");
|
|
71
|
+
}
|
|
72
|
+
const token =
|
|
73
|
+
opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
|
|
74
|
+
? await opts.auth.getToken()
|
|
75
|
+
: undefined;
|
|
76
|
+
if (token && !headers.has("authorization")) {
|
|
77
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
78
|
+
}
|
|
79
|
+
const res = await fetchFn(url, {
|
|
80
|
+
method,
|
|
81
|
+
headers,
|
|
82
|
+
body,
|
|
83
|
+
signal: ctrl.signal,
|
|
84
|
+
...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
|
|
85
|
+
});
|
|
86
|
+
if (
|
|
87
|
+
res.status === 401 &&
|
|
88
|
+
opts.auth &&
|
|
89
|
+
"refresh" in opts.auth &&
|
|
90
|
+
typeof opts.auth.refresh === "function" &&
|
|
91
|
+
!refreshed
|
|
92
|
+
) {
|
|
93
|
+
refreshed = true;
|
|
94
|
+
await opts.auth.refresh();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
const text = await res.text().catch(() => "");
|
|
99
|
+
throw sseError(res.status, text);
|
|
100
|
+
}
|
|
101
|
+
yield* iterateSse(res, ctrl.signal);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function streamRequest(
|
|
107
|
+
base: string,
|
|
108
|
+
method: string,
|
|
109
|
+
path: string,
|
|
110
|
+
input: unknown,
|
|
111
|
+
): { url: string; method: string; body: string | undefined } {
|
|
112
|
+
const params =
|
|
113
|
+
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
114
|
+
let pathOut = path;
|
|
115
|
+
const rest: Record<string, unknown> = {};
|
|
116
|
+
for (const [k, v] of Object.entries(params)) {
|
|
117
|
+
const token = `:${k}`;
|
|
118
|
+
if (pathOut.includes(token)) {
|
|
119
|
+
pathOut = pathOut.replaceAll(token, encodeURIComponent(String(v)));
|
|
120
|
+
} else {
|
|
121
|
+
rest[k] = v;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const upper = method.toUpperCase();
|
|
125
|
+
let body: string | undefined;
|
|
126
|
+
if (upper === "GET" || upper === "HEAD") {
|
|
127
|
+
const query: string[] = [];
|
|
128
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
129
|
+
if (v !== undefined) {
|
|
130
|
+
query.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const qs = query.length ? `?${query.join("&")}` : "";
|
|
134
|
+
return { url: `${base}${pathOut}${qs}`, method: upper, body: undefined };
|
|
135
|
+
}
|
|
136
|
+
body = JSON.stringify(Object.keys(rest).length > 0 ? rest : (input ?? {}));
|
|
137
|
+
return { url: `${base}${pathOut}`, method: upper, body };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyHeaders(headers: Headers, extra: ClientHeaders | undefined): void {
|
|
141
|
+
if (!extra) return;
|
|
142
|
+
if (Array.isArray(extra)) {
|
|
143
|
+
for (const [k, v] of extra) headers.set(k, v);
|
|
144
|
+
} else {
|
|
145
|
+
for (const [k, v] of Object.entries(extra)) headers.set(k, v);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -142,3 +142,35 @@ describe("transport — auth refresh", () => {
|
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
describe("transport — binary response", () => {
|
|
147
|
+
test("response blob returns Blob data", async () => {
|
|
148
|
+
const bytes = new Uint8Array([37, 80, 68, 70]); // %PDF
|
|
149
|
+
const api = createClient<PingApp>("http://app.test", {
|
|
150
|
+
fetch: async () =>
|
|
151
|
+
new Response(bytes, {
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: { "content-type": "application/pdf" },
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
const { data, error } = await api.sys.ping({ response: "blob" });
|
|
157
|
+
expect(error).toBeNull();
|
|
158
|
+
expect(data).toBeInstanceOf(Blob);
|
|
159
|
+
expect((data as unknown as Blob).type).toContain("pdf");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("response arrayBuffer returns bytes", async () => {
|
|
163
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
164
|
+
const api = createClient<PingApp>("http://app.test", {
|
|
165
|
+
fetch: async () =>
|
|
166
|
+
new Response(bytes, {
|
|
167
|
+
status: 200,
|
|
168
|
+
headers: { "content-type": "application/octet-stream" },
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
const { data, error } = await api.sys.ping({ response: "arrayBuffer" });
|
|
172
|
+
expect(error).toBeNull();
|
|
173
|
+
expect(data).toBeInstanceOf(ArrayBuffer);
|
|
174
|
+
expect(new Uint8Array(data as unknown as ArrayBuffer)).toEqual(bytes);
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/client/transport.ts
CHANGED
|
@@ -5,7 +5,20 @@
|
|
|
5
5
|
* Optional `routes` map switches to REST (`method` + path template).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ClientBodyInit,
|
|
10
|
+
ClientEnvelope,
|
|
11
|
+
ClientFetch,
|
|
12
|
+
ClientHeaders,
|
|
13
|
+
ClientOptions,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
|
|
16
|
+
/** Per-call transport options (binary decode, abort). */
|
|
17
|
+
export interface TransportCallOptions {
|
|
18
|
+
readonly headers?: ClientHeaders;
|
|
19
|
+
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}
|
|
9
22
|
|
|
10
23
|
/** Internal transport handle. */
|
|
11
24
|
export interface Transport {
|
|
@@ -14,9 +27,13 @@ export interface Transport {
|
|
|
14
27
|
*
|
|
15
28
|
* @param key - `unit/flow`
|
|
16
29
|
* @param input - JSON body / path-param source
|
|
17
|
-
* @param
|
|
30
|
+
* @param headersOrOpts - Per-call headers or full call options
|
|
18
31
|
*/
|
|
19
|
-
call(
|
|
32
|
+
call(
|
|
33
|
+
key: string,
|
|
34
|
+
input: unknown,
|
|
35
|
+
headersOrOpts?: ClientHeaders | TransportCallOptions,
|
|
36
|
+
): Promise<ClientEnvelope>;
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
/**
|
|
@@ -32,15 +49,24 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
32
49
|
const backoff = opts.retry?.backoff ?? 2;
|
|
33
50
|
|
|
34
51
|
return {
|
|
35
|
-
async call(key, input,
|
|
52
|
+
async call(key, input, headersOrOpts) {
|
|
53
|
+
const callOpts = normalizeCallOpts(headersOrOpts);
|
|
36
54
|
let refreshed = false;
|
|
37
55
|
let attempt = 0;
|
|
38
56
|
let delay = delay0;
|
|
57
|
+
const callClientOpts: ClientOptions =
|
|
58
|
+
callOpts.signal !== undefined ? { ...opts, signal: callOpts.signal } : opts;
|
|
39
59
|
|
|
40
60
|
for (;;) {
|
|
41
61
|
try {
|
|
42
|
-
const res = await once(base, key, input,
|
|
43
|
-
if (
|
|
62
|
+
const res = await once(base, key, input, callClientOpts, fetchFn, callOpts.headers);
|
|
63
|
+
if (
|
|
64
|
+
res.status === 401 &&
|
|
65
|
+
opts.auth &&
|
|
66
|
+
"refresh" in opts.auth &&
|
|
67
|
+
typeof opts.auth.refresh === "function" &&
|
|
68
|
+
!refreshed
|
|
69
|
+
) {
|
|
44
70
|
refreshed = true;
|
|
45
71
|
await opts.auth.refresh();
|
|
46
72
|
continue;
|
|
@@ -50,6 +76,9 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
50
76
|
if (structured) return structured;
|
|
51
77
|
throw new Error(`HTTP ${res.status}`);
|
|
52
78
|
}
|
|
79
|
+
if (callOpts.response === "blob" || callOpts.response === "arrayBuffer") {
|
|
80
|
+
return decodeBinary(res, callOpts.response);
|
|
81
|
+
}
|
|
53
82
|
return decode(res);
|
|
54
83
|
} catch (err) {
|
|
55
84
|
const transient = isTransient(err);
|
|
@@ -73,6 +102,23 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
73
102
|
};
|
|
74
103
|
}
|
|
75
104
|
|
|
105
|
+
function normalizeCallOpts(
|
|
106
|
+
headersOrOpts: ClientHeaders | TransportCallOptions | undefined,
|
|
107
|
+
): TransportCallOptions {
|
|
108
|
+
if (headersOrOpts === undefined) return {};
|
|
109
|
+
if (Array.isArray(headersOrOpts)) return { headers: headersOrOpts };
|
|
110
|
+
if (
|
|
111
|
+
typeof headersOrOpts === "object" &&
|
|
112
|
+
("response" in headersOrOpts || "signal" in headersOrOpts || "headers" in headersOrOpts)
|
|
113
|
+
) {
|
|
114
|
+
const o = headersOrOpts as TransportCallOptions;
|
|
115
|
+
if (o.response !== undefined || o.signal !== undefined || o.headers !== undefined) {
|
|
116
|
+
return o;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { headers: headersOrOpts as ClientHeaders };
|
|
120
|
+
}
|
|
121
|
+
|
|
76
122
|
/**
|
|
77
123
|
* Single HTTP attempt. Throws on network / abort / 5xx (retryable).
|
|
78
124
|
*
|
|
@@ -108,29 +154,34 @@ async function once(
|
|
|
108
154
|
} else if (callHeaders) {
|
|
109
155
|
for (const [k, v] of Object.entries(callHeaders)) headers.set(k, v);
|
|
110
156
|
}
|
|
111
|
-
if (body !== undefined && !headers.has("content-type")) {
|
|
157
|
+
if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
|
|
112
158
|
headers.set("content-type", "application/json");
|
|
113
159
|
}
|
|
114
160
|
|
|
115
|
-
const token =
|
|
161
|
+
const token =
|
|
162
|
+
opts.auth && "getToken" in opts.auth && typeof opts.auth.getToken === "function"
|
|
163
|
+
? await opts.auth.getToken()
|
|
164
|
+
: undefined;
|
|
116
165
|
if (token && !headers.has("authorization")) {
|
|
117
166
|
headers.set("authorization", `Bearer ${token}`);
|
|
118
167
|
}
|
|
119
168
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return await fetchFn(url, {
|
|
126
|
-
method,
|
|
127
|
-
headers,
|
|
128
|
-
body,
|
|
129
|
-
signal: ctrl?.signal,
|
|
130
|
-
});
|
|
131
|
-
} finally {
|
|
132
|
-
if (timer !== undefined) clearTimeout(timer);
|
|
169
|
+
const signals: AbortSignal[] = [];
|
|
170
|
+
if (opts.signal) signals.push(opts.signal);
|
|
171
|
+
if (opts.timeout !== undefined) {
|
|
172
|
+
const t = AbortSignal.timeout(opts.timeout);
|
|
173
|
+
signals.push(t);
|
|
133
174
|
}
|
|
175
|
+
const signal =
|
|
176
|
+
signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
177
|
+
|
|
178
|
+
return await fetchFn(url, {
|
|
179
|
+
method,
|
|
180
|
+
headers,
|
|
181
|
+
body: body as RequestInit["body"],
|
|
182
|
+
signal,
|
|
183
|
+
...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
|
|
184
|
+
});
|
|
134
185
|
}
|
|
135
186
|
|
|
136
187
|
/**
|
|
@@ -158,10 +209,11 @@ function rpcRequest(
|
|
|
158
209
|
base: string,
|
|
159
210
|
key: string,
|
|
160
211
|
input: unknown,
|
|
161
|
-
): { url: string; method: string; body:
|
|
212
|
+
): { url: string; method: string; body: ClientBodyInit | undefined } {
|
|
162
213
|
const url = `${base}/_oke/${key}`;
|
|
163
|
-
|
|
164
|
-
return { url, method: "POST", body };
|
|
214
|
+
if (input === undefined) return { url, method: "POST", body: undefined };
|
|
215
|
+
if (isRawBody(input)) return { url, method: "POST", body: input };
|
|
216
|
+
return { url, method: "POST", body: JSON.stringify(input ?? {}) };
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
function restRequest(
|
|
@@ -169,7 +221,10 @@ function restRequest(
|
|
|
169
221
|
method: string,
|
|
170
222
|
path: string,
|
|
171
223
|
input: unknown,
|
|
172
|
-
): { url: string; method: string; body:
|
|
224
|
+
): { url: string; method: string; body: ClientBodyInit | undefined } {
|
|
225
|
+
if (isRawBody(input)) {
|
|
226
|
+
return { url: `${base}${path}`, method: method.toUpperCase(), body: input };
|
|
227
|
+
}
|
|
173
228
|
const params =
|
|
174
229
|
input !== null && typeof input === "object" ? (input as Record<string, unknown>) : {};
|
|
175
230
|
let pathOut = path;
|
|
@@ -186,7 +241,7 @@ function restRequest(
|
|
|
186
241
|
}
|
|
187
242
|
|
|
188
243
|
const upper = method.toUpperCase();
|
|
189
|
-
let body:
|
|
244
|
+
let body: ClientBodyInit | undefined;
|
|
190
245
|
if (upper === "GET" || upper === "HEAD") {
|
|
191
246
|
for (const [k, v] of Object.entries(rest)) {
|
|
192
247
|
if (v !== undefined) {
|
|
@@ -204,6 +259,16 @@ function restRequest(
|
|
|
204
259
|
return { url: `${base}${pathOut}${qs}`, method: upper, body };
|
|
205
260
|
}
|
|
206
261
|
|
|
262
|
+
function isRawBody(input: unknown): input is ClientBodyInit {
|
|
263
|
+
if (input === null || input === undefined) return false;
|
|
264
|
+
if (typeof Blob !== "undefined" && input instanceof Blob) return true;
|
|
265
|
+
if (typeof FormData !== "undefined" && input instanceof FormData) return true;
|
|
266
|
+
if (typeof ArrayBuffer !== "undefined" && input instanceof ArrayBuffer) return true;
|
|
267
|
+
if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(input)) return true;
|
|
268
|
+
if (typeof ReadableStream !== "undefined" && input instanceof ReadableStream) return true;
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
207
272
|
async function decode(res: Response): Promise<ClientEnvelope> {
|
|
208
273
|
if (res.status === 204) {
|
|
209
274
|
return { data: undefined, error: null };
|
|
@@ -254,6 +319,38 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
254
319
|
};
|
|
255
320
|
}
|
|
256
321
|
|
|
322
|
+
async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promise<ClientEnvelope> {
|
|
323
|
+
if (!res.ok) {
|
|
324
|
+
const structured = await decodeIfEnvelopeClone(res);
|
|
325
|
+
if (structured) return structured;
|
|
326
|
+
return {
|
|
327
|
+
data: null,
|
|
328
|
+
error: {
|
|
329
|
+
code: "TransportError",
|
|
330
|
+
data: { message: `HTTP ${res.status}`, status: res.status },
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
|
|
335
|
+
return { data, error: null };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Try JSON envelope from an error response without assuming the body is reusable. */
|
|
339
|
+
async function decodeIfEnvelopeClone(res: Response): Promise<ClientEnvelope | null> {
|
|
340
|
+
const text = await res.text();
|
|
341
|
+
if (!text) return null;
|
|
342
|
+
let json: unknown;
|
|
343
|
+
try {
|
|
344
|
+
json = JSON.parse(text);
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
349
|
+
return json as ClientEnvelope;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
257
354
|
function isTransient(err: unknown): boolean {
|
|
258
355
|
if (!(err instanceof Error)) return false;
|
|
259
356
|
if (err.name === "AbortError") return false;
|