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
|
@@ -18,8 +18,12 @@ import type {
|
|
|
18
18
|
ChannelSendResult,
|
|
19
19
|
SmsOtpTransport,
|
|
20
20
|
} from "../../drivers/channel-types.ts";
|
|
21
|
+
import type { DriverExternal } from "../../drivers/external.ts";
|
|
21
22
|
import type { ChannelMedium } from "../../manifest/types.ts";
|
|
23
|
+
import { CHANNEL_SCHEMA } from "../../kernel/errors-channel.ts";
|
|
24
|
+
import { OkeError } from "../../kernel/errors.ts";
|
|
22
25
|
import { emitBootWarn } from "../../runtime/boot-warn.ts";
|
|
26
|
+
import { isStandardSchema, type SchemaInput } from "../../validation/standard-schema.ts";
|
|
23
27
|
import type { ConsentStore } from "./consent.ts";
|
|
24
28
|
import type { ChannelTemplateDecl } from "./declare.ts";
|
|
25
29
|
import { DEFAULT_MEDIUM_COSTS, type MediumCosts } from "./costs.ts";
|
|
@@ -50,6 +54,33 @@ export type TemplateCatalog = Readonly<
|
|
|
50
54
|
>
|
|
51
55
|
>;
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Resolve driver-reported egress for a provider label in a fallback chain.
|
|
59
|
+
*
|
|
60
|
+
* @param drivers - Ordered driver chain
|
|
61
|
+
* @param provider - Provider / driver id that handled (or failed) the attempt
|
|
62
|
+
* @param index - Optional transport index when known
|
|
63
|
+
*/
|
|
64
|
+
function externalForChannelProvider(
|
|
65
|
+
drivers: readonly ChannelDriver[],
|
|
66
|
+
provider: string,
|
|
67
|
+
index?: number,
|
|
68
|
+
): DriverExternal | undefined {
|
|
69
|
+
if (typeof index === "number") {
|
|
70
|
+
const at = drivers[index]?.external;
|
|
71
|
+
if (at) return at;
|
|
72
|
+
}
|
|
73
|
+
const match = drivers.find(
|
|
74
|
+
(d) =>
|
|
75
|
+
d.id === provider ||
|
|
76
|
+
d.external?.provider === provider ||
|
|
77
|
+
d.transport?.provider === provider ||
|
|
78
|
+
d.smsTransport?.provider === provider ||
|
|
79
|
+
d.channel?.provider === provider,
|
|
80
|
+
);
|
|
81
|
+
return match?.external;
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
/** Options for {@link createChannelRuntime}. */
|
|
54
85
|
export interface CreateChannelRuntimeOptions {
|
|
55
86
|
/** Declared templates. */
|
|
@@ -266,27 +297,29 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
266
297
|
const provider =
|
|
267
298
|
transports[failedIndex]?.provider ?? chain[failedIndex]?.id ?? `driver-${failedIndex}`;
|
|
268
299
|
recorded.push({ provider, error });
|
|
300
|
+
const external = externalForChannelProvider(chain, provider, failedIndex);
|
|
269
301
|
attempts.push({
|
|
270
302
|
driverId: provider,
|
|
271
303
|
ok: false,
|
|
272
304
|
error: error instanceof Error ? error.message : String(error),
|
|
273
305
|
at: now(),
|
|
306
|
+
...(external !== undefined ? { external } : {}),
|
|
274
307
|
});
|
|
275
308
|
},
|
|
276
309
|
});
|
|
277
310
|
|
|
278
311
|
try {
|
|
279
312
|
const mailResult = await fallback.send(mail);
|
|
313
|
+
const providerIndex = mailResult.providerIndex ?? 0;
|
|
280
314
|
const driverId =
|
|
281
|
-
mailResult.provider ??
|
|
282
|
-
|
|
283
|
-
chain[0]?.id ??
|
|
284
|
-
"email";
|
|
315
|
+
mailResult.provider ?? transports[providerIndex]?.provider ?? chain[0]?.id ?? "email";
|
|
316
|
+
const external = externalForChannelProvider(chain, driverId, providerIndex);
|
|
285
317
|
attempts.push({
|
|
286
318
|
driverId,
|
|
287
319
|
ok: true,
|
|
288
320
|
at: now(),
|
|
289
321
|
messageId: mailResult.messageId,
|
|
322
|
+
...(external !== undefined ? { external } : {}),
|
|
290
323
|
});
|
|
291
324
|
return {
|
|
292
325
|
result: {
|
|
@@ -295,6 +328,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
295
328
|
driverId,
|
|
296
329
|
attempts,
|
|
297
330
|
mail: mailResult,
|
|
331
|
+
...(external !== undefined ? { external } : {}),
|
|
298
332
|
},
|
|
299
333
|
attempts,
|
|
300
334
|
};
|
|
@@ -308,30 +342,37 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
308
342
|
: recorded;
|
|
309
343
|
for (const a of fbAttempts) {
|
|
310
344
|
if (!attempts.some((x) => x.driverId === a.provider && !x.ok)) {
|
|
345
|
+
const external = externalForChannelProvider(chain, a.provider);
|
|
311
346
|
attempts.push({
|
|
312
347
|
driverId: a.provider,
|
|
313
348
|
ok: false,
|
|
314
349
|
error: a.error instanceof Error ? a.error.message : String(a.error),
|
|
315
350
|
at: now(),
|
|
351
|
+
...(external !== undefined ? { external } : {}),
|
|
316
352
|
});
|
|
317
353
|
}
|
|
318
354
|
}
|
|
319
355
|
// Permanent client errors abort failover by rethrowing the original error
|
|
320
356
|
// (no FallbackError.attempts / onFallback) — still record the failed try.
|
|
321
357
|
if (attempts.length === 0) {
|
|
358
|
+
const driverId = transports[0]?.provider ?? chain[0]?.id ?? "email";
|
|
359
|
+
const external = externalForChannelProvider(chain, driverId, 0);
|
|
322
360
|
attempts.push({
|
|
323
|
-
driverId
|
|
361
|
+
driverId,
|
|
324
362
|
ok: false,
|
|
325
363
|
error: err instanceof Error ? err.message : String(err),
|
|
326
364
|
at: now(),
|
|
365
|
+
...(external !== undefined ? { external } : {}),
|
|
327
366
|
});
|
|
328
367
|
}
|
|
368
|
+
const lastExternal = attempts[attempts.length - 1]?.external;
|
|
329
369
|
return {
|
|
330
370
|
result: {
|
|
331
371
|
ok: false,
|
|
332
372
|
messageId: crypto.randomUUID(),
|
|
333
373
|
driverId: "fallback",
|
|
334
374
|
attempts,
|
|
375
|
+
...(lastExternal !== undefined ? { external: lastExternal } : {}),
|
|
335
376
|
},
|
|
336
377
|
attempts,
|
|
337
378
|
};
|
|
@@ -361,11 +402,17 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
361
402
|
onFallback(failedIndex, error) {
|
|
362
403
|
const provider =
|
|
363
404
|
transports[failedIndex]?.provider ?? sms[failedIndex]?.driver.id ?? `sms-${failedIndex}`;
|
|
405
|
+
const external = externalForChannelProvider(
|
|
406
|
+
sms.map((s) => s.driver),
|
|
407
|
+
provider,
|
|
408
|
+
failedIndex,
|
|
409
|
+
);
|
|
364
410
|
attempts.push({
|
|
365
411
|
driverId: provider,
|
|
366
412
|
ok: false,
|
|
367
413
|
error: error instanceof Error ? error.message : String(error),
|
|
368
414
|
at: now(),
|
|
415
|
+
...(external !== undefined ? { external } : {}),
|
|
369
416
|
});
|
|
370
417
|
},
|
|
371
418
|
});
|
|
@@ -378,16 +425,17 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
378
425
|
|
|
379
426
|
try {
|
|
380
427
|
const sendResult = await fallback.send(body);
|
|
428
|
+
const providerIndex = sendResult.providerIndex ?? 0;
|
|
381
429
|
const driverId =
|
|
382
|
-
sendResult.provider ??
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"sms";
|
|
430
|
+
sendResult.provider ?? transports[providerIndex]?.provider ?? sms[0]?.driver.id ?? "sms";
|
|
431
|
+
const smsDrivers = sms.map((s) => s.driver);
|
|
432
|
+
const external = externalForChannelProvider(smsDrivers, driverId, providerIndex);
|
|
386
433
|
attempts.push({
|
|
387
434
|
driverId,
|
|
388
435
|
ok: true,
|
|
389
436
|
at: now(),
|
|
390
437
|
messageId: sendResult.messageId,
|
|
438
|
+
...(external !== undefined ? { external } : {}),
|
|
391
439
|
});
|
|
392
440
|
return {
|
|
393
441
|
result: {
|
|
@@ -395,10 +443,12 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
395
443
|
messageId: sendResult.messageId,
|
|
396
444
|
driverId,
|
|
397
445
|
attempts,
|
|
446
|
+
...(external !== undefined ? { external } : {}),
|
|
398
447
|
},
|
|
399
448
|
attempts,
|
|
400
449
|
};
|
|
401
450
|
} catch (err) {
|
|
451
|
+
const smsDrivers = sms.map((s) => s.driver);
|
|
402
452
|
const fbAttempts =
|
|
403
453
|
err &&
|
|
404
454
|
typeof err === "object" &&
|
|
@@ -408,28 +458,35 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
408
458
|
: [];
|
|
409
459
|
for (const a of fbAttempts) {
|
|
410
460
|
if (!attempts.some((x) => x.driverId === a.provider && !x.ok)) {
|
|
461
|
+
const external = externalForChannelProvider(smsDrivers, a.provider);
|
|
411
462
|
attempts.push({
|
|
412
463
|
driverId: a.provider,
|
|
413
464
|
ok: false,
|
|
414
465
|
error: a.error instanceof Error ? a.error.message : String(a.error),
|
|
415
466
|
at: now(),
|
|
467
|
+
...(external !== undefined ? { external } : {}),
|
|
416
468
|
});
|
|
417
469
|
}
|
|
418
470
|
}
|
|
419
471
|
if (attempts.length === 0) {
|
|
472
|
+
const driverId = transports[0]?.provider ?? sms[0]?.driver.id ?? "sms";
|
|
473
|
+
const external = externalForChannelProvider(smsDrivers, driverId, 0);
|
|
420
474
|
attempts.push({
|
|
421
|
-
driverId
|
|
475
|
+
driverId,
|
|
422
476
|
ok: false,
|
|
423
477
|
error: err instanceof Error ? err.message : String(err),
|
|
424
478
|
at: now(),
|
|
479
|
+
...(external !== undefined ? { external } : {}),
|
|
425
480
|
});
|
|
426
481
|
}
|
|
482
|
+
const lastExternal = attempts[attempts.length - 1]?.external;
|
|
427
483
|
return {
|
|
428
484
|
result: {
|
|
429
485
|
ok: false,
|
|
430
486
|
messageId: crypto.randomUUID(),
|
|
431
487
|
driverId: "fallback",
|
|
432
488
|
attempts,
|
|
489
|
+
...(lastExternal !== undefined ? { external: lastExternal } : {}),
|
|
433
490
|
},
|
|
434
491
|
attempts,
|
|
435
492
|
};
|
|
@@ -450,10 +507,19 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
450
507
|
if (!d.channel) continue;
|
|
451
508
|
try {
|
|
452
509
|
const r = await d.channel.send(message);
|
|
453
|
-
attempts.
|
|
510
|
+
const stampedAttempts = r.attempts.map((a) => ({
|
|
511
|
+
...a,
|
|
512
|
+
...(a.external === undefined && d.external !== undefined ? { external: d.external } : {}),
|
|
513
|
+
}));
|
|
514
|
+
attempts.push(...stampedAttempts);
|
|
454
515
|
if (r.ok) {
|
|
516
|
+
const external = r.external ?? d.external;
|
|
455
517
|
return {
|
|
456
|
-
result: {
|
|
518
|
+
result: {
|
|
519
|
+
...r,
|
|
520
|
+
attempts: [...attempts],
|
|
521
|
+
...(external !== undefined ? { external } : {}),
|
|
522
|
+
},
|
|
457
523
|
attempts,
|
|
458
524
|
};
|
|
459
525
|
}
|
|
@@ -463,6 +529,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
463
529
|
ok: false,
|
|
464
530
|
error: err instanceof Error ? err.message : String(err),
|
|
465
531
|
at: now(),
|
|
532
|
+
...(d.external !== undefined ? { external: d.external } : {}),
|
|
466
533
|
});
|
|
467
534
|
}
|
|
468
535
|
}
|
|
@@ -511,6 +578,31 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
511
578
|
);
|
|
512
579
|
}
|
|
513
580
|
|
|
581
|
+
async function validateChannelTemplateData(
|
|
582
|
+
template: string,
|
|
583
|
+
decl: ChannelTemplateDecl,
|
|
584
|
+
data: unknown,
|
|
585
|
+
): Promise<void> {
|
|
586
|
+
const schema = decl.schema as SchemaInput | undefined;
|
|
587
|
+
if (schema === undefined || schema === null || !isStandardSchema(schema)) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const result = await schema["~standard"].validate(data ?? {});
|
|
591
|
+
if (!result.issues) return;
|
|
592
|
+
const detail =
|
|
593
|
+
result.issues
|
|
594
|
+
.map((i) => {
|
|
595
|
+
const path = Array.isArray(i.path)
|
|
596
|
+
? i.path
|
|
597
|
+
.map((p) => (typeof p === "object" && p && "key" in p ? String(p.key) : String(p)))
|
|
598
|
+
.join(".")
|
|
599
|
+
: "";
|
|
600
|
+
return path.length > 0 ? `${path}: ${i.message}` : i.message;
|
|
601
|
+
})
|
|
602
|
+
.join("; ") || "invalid payload";
|
|
603
|
+
throw new OkeError(CHANNEL_SCHEMA, { resource: template, detail });
|
|
604
|
+
}
|
|
605
|
+
|
|
514
606
|
async function sendTemplate(
|
|
515
607
|
template: string,
|
|
516
608
|
opts: ChannelSendOptions,
|
|
@@ -519,6 +611,7 @@ export function createChannelRuntime(options: CreateChannelRuntimeOptions = {}):
|
|
|
519
611
|
if (!decl) {
|
|
520
612
|
throw new Error(`channel: unknown template "${template}"`);
|
|
521
613
|
}
|
|
614
|
+
await validateChannelTemplateData(template, decl, opts.data);
|
|
522
615
|
const medium = decl.medium;
|
|
523
616
|
const checkMedium: ChannelMedium = medium === "any" ? "email" : medium;
|
|
524
617
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured cron fields + clock convenience helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import { buildCronExpression, parseAtTime, serializeCronField } from "./cron-fields.ts";
|
|
8
|
+
import { applyClockTimezoneDefaults, clock, listClocks, resetClocks } from "./declare.ts";
|
|
9
|
+
|
|
10
|
+
describe("buildCronExpression", () => {
|
|
11
|
+
test("at sugar sets minute and hour", () => {
|
|
12
|
+
expect(buildCronExpression({ at: "06:00" })).toBe("0 6 * * *");
|
|
13
|
+
expect(buildCronExpression({ at: "6:00" })).toBe("0 6 * * *");
|
|
14
|
+
expect(buildCronExpression({ at: "08:30" })).toBe("30 8 * * *");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("lists, steps, and weekday names", () => {
|
|
18
|
+
expect(
|
|
19
|
+
buildCronExpression({
|
|
20
|
+
minute: "*/15",
|
|
21
|
+
hour: [9, 12, 17],
|
|
22
|
+
dayOfWeek: "1-5",
|
|
23
|
+
}),
|
|
24
|
+
).toBe("*/15 9,12,17 * * 1-5");
|
|
25
|
+
|
|
26
|
+
expect(
|
|
27
|
+
buildCronExpression({
|
|
28
|
+
at: "08:00",
|
|
29
|
+
dayOfWeek: ["mon", "wed", "fri"],
|
|
30
|
+
}),
|
|
31
|
+
).toBe("0 8 * * 1,3,5");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("at does not override explicit minute/hour", () => {
|
|
35
|
+
expect(buildCronExpression({ at: "06:00", minute: 15 })).toBe("15 6 * * *");
|
|
36
|
+
expect(buildCronExpression({ at: "06:00", hour: 9 })).toBe("0 9 * * *");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("rejects empty bag and bad at", () => {
|
|
40
|
+
expect(() => buildCronExpression({})).toThrow(/require at least one/);
|
|
41
|
+
expect(() => parseAtTime("nope")).toThrow(/HH:MM/);
|
|
42
|
+
expect(() => parseAtTime("25:00")).toThrow(/hour/);
|
|
43
|
+
expect(() => parseAtTime("12:99")).toThrow(/minute/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("serializeCronField maps day names", () => {
|
|
47
|
+
expect(serializeCronField("tue", "dayOfWeek")).toBe("2");
|
|
48
|
+
expect(serializeCronField(["sun", "sat"], "dayOfWeek")).toBe("0,6");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("clock helpers", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
resetClocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("daily / hourly / weekly / monthly / every / cron string", () => {
|
|
58
|
+
expect(clock.daily("reports.daily", { at: "06:00", timezone: "Asia/Riyadh" })).toMatchObject({
|
|
59
|
+
name: "reports.daily",
|
|
60
|
+
cron: "0 6 * * *",
|
|
61
|
+
timezone: "Asia/Riyadh",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(clock.hourly("metrics.rollup", { minute: 15 })).toMatchObject({
|
|
65
|
+
cron: "15 * * * *",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(clock.weekly("digest", { on: ["mon", "fri"], at: "09:00" })).toMatchObject({
|
|
69
|
+
cron: "0 9 * * 1,5",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(clock.monthly("billing", { on: [1, 15], at: "00:00" })).toMatchObject({
|
|
73
|
+
cron: "0 0 1,15 * *",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(clock.every("health.ping", "30s")).toMatchObject({
|
|
77
|
+
every: "30s",
|
|
78
|
+
cron: undefined,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(clock.cron("legacy", "@hourly")).toMatchObject({
|
|
82
|
+
cron: "@hourly",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("clock.cron structured fields", () => {
|
|
87
|
+
const decl = clock.cron("ops.sweep", {
|
|
88
|
+
minute: "*/15",
|
|
89
|
+
hour: [9, 12, 17],
|
|
90
|
+
dayOfWeek: "1-5",
|
|
91
|
+
timezone: "UTC",
|
|
92
|
+
overridable: true,
|
|
93
|
+
description: "weekday sweeps",
|
|
94
|
+
});
|
|
95
|
+
expect(decl).toMatchObject({
|
|
96
|
+
name: "ops.sweep",
|
|
97
|
+
cron: "*/15 9,12,17 * * 1-5",
|
|
98
|
+
timezone: "UTC",
|
|
99
|
+
overridable: true,
|
|
100
|
+
description: "weekday sweeps",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("invalid cron fails at declare", () => {
|
|
105
|
+
expect(() => clock.cron("bad", "not a cron")).toThrow(/invalid cron/);
|
|
106
|
+
expect(() => clock("bad", { cron: "99 99 * * *" })).toThrow(/invalid cron/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("helpers register into listClocks", () => {
|
|
110
|
+
clock.daily("a", { at: "01:00" });
|
|
111
|
+
clock.every("b", "1h");
|
|
112
|
+
expect(listClocks().map((c) => c.name)).toEqual(["a", "b"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("omitted timezone is marked defaulted", () => {
|
|
116
|
+
const d = clock.daily("x", { at: "06:00" });
|
|
117
|
+
expect(d.timezone).toBe("UTC");
|
|
118
|
+
expect(d.timezoneDefaulted).toBe(true);
|
|
119
|
+
const explicit = clock.daily("y", { at: "06:00", timezone: "UTC" });
|
|
120
|
+
expect(explicit.timezoneDefaulted).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("applyClockTimezoneDefaults", () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
resetClocks();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("rewrites defaulted clocks; keeps explicit", () => {
|
|
130
|
+
const a = clock.daily("a", { at: "06:00" });
|
|
131
|
+
const b = clock.daily("b", { at: "07:00", timezone: "Europe/London" });
|
|
132
|
+
const out = applyClockTimezoneDefaults([a, b], "Asia/Riyadh");
|
|
133
|
+
expect(out[0]).toMatchObject({ name: "a", timezone: "Asia/Riyadh" });
|
|
134
|
+
expect(out[0]?.timezoneDefaulted).toBeUndefined();
|
|
135
|
+
expect(out[1]).toMatchObject({ name: "b", timezone: "Europe/London" });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("no-op when default timezone omitted", () => {
|
|
139
|
+
const a = clock.daily("a", { at: "06:00" });
|
|
140
|
+
const out = applyClockTimezoneDefaults([a], undefined);
|
|
141
|
+
expect(out[0]?.timezone).toBe("UTC");
|
|
142
|
+
expect(out[0]?.timezoneDefaulted).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured cron field bag → five-field expression.
|
|
3
|
+
*
|
|
4
|
+
* Used by `clock.cron` / `clock.daily` / … so Manifest + Store still store a
|
|
5
|
+
* plain cron string. Only Bun five-field + nicknames — no Quartz calendars.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** One cron field: scalar, step/range string, or list. */
|
|
9
|
+
export type CronField = number | string | readonly (number | string)[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structured schedule fields for {@link buildCronExpression}.
|
|
13
|
+
*
|
|
14
|
+
* Omit a field to leave it `*`. `at: "HH:MM"` sets minute + hour when those
|
|
15
|
+
* are unset.
|
|
16
|
+
*/
|
|
17
|
+
export interface CronFields {
|
|
18
|
+
/** Minute (`0` · `[0, 30]` · step string like every-15). */
|
|
19
|
+
readonly minute?: CronField;
|
|
20
|
+
/** Hour (`9` · `[9, 12, 17]` · `"9-17"`). */
|
|
21
|
+
readonly hour?: CronField;
|
|
22
|
+
/** Day of month (`1` · `[1, 15]` · `"1-7"`). */
|
|
23
|
+
readonly dayOfMonth?: CronField;
|
|
24
|
+
/** Month (`1` · `[1, 6]` · step string). */
|
|
25
|
+
readonly month?: CronField;
|
|
26
|
+
/** Day of week (`1` · `"mon"` · `["mon", "fri"]` · `"1-5"`). */
|
|
27
|
+
readonly dayOfWeek?: CronField;
|
|
28
|
+
/** Wall-clock sugar `"HH:MM"` / `"H:MM"` → minute + hour when unset. */
|
|
29
|
+
readonly at?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DAY_NAME_TO_NUM: Readonly<Record<string, number>> = {
|
|
33
|
+
sun: 0,
|
|
34
|
+
sunday: 0,
|
|
35
|
+
mon: 1,
|
|
36
|
+
monday: 1,
|
|
37
|
+
tue: 2,
|
|
38
|
+
tues: 2,
|
|
39
|
+
tuesday: 2,
|
|
40
|
+
wed: 3,
|
|
41
|
+
wednesday: 3,
|
|
42
|
+
thu: 4,
|
|
43
|
+
thur: 4,
|
|
44
|
+
thurs: 4,
|
|
45
|
+
thursday: 4,
|
|
46
|
+
fri: 5,
|
|
47
|
+
friday: 5,
|
|
48
|
+
sat: 6,
|
|
49
|
+
saturday: 6,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse `at: "06:00"` / `"6:00"` into `{ minute, hour }`.
|
|
54
|
+
*
|
|
55
|
+
* @param at - Wall-clock string
|
|
56
|
+
* @throws TypeError when the format is invalid
|
|
57
|
+
*/
|
|
58
|
+
export function parseAtTime(at: string): { readonly minute: number; readonly hour: number } {
|
|
59
|
+
const trimmed = at.trim();
|
|
60
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
|
|
61
|
+
if (!m) {
|
|
62
|
+
throw new TypeError(`cron at: expected "HH:MM", got ${JSON.stringify(at)}`);
|
|
63
|
+
}
|
|
64
|
+
const hour = Number(m[1]);
|
|
65
|
+
const minute = Number(m[2]);
|
|
66
|
+
if (!Number.isInteger(hour) || hour < 0 || hour > 23) {
|
|
67
|
+
throw new TypeError(`cron at: hour must be 0–23, got ${JSON.stringify(at)}`);
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isInteger(minute) || minute < 0 || minute > 59) {
|
|
70
|
+
throw new TypeError(`cron at: minute must be 0–59, got ${JSON.stringify(at)}`);
|
|
71
|
+
}
|
|
72
|
+
return { hour, minute };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize one field token (day names → 0–6; numbers → string).
|
|
77
|
+
*
|
|
78
|
+
* @param value - Scalar field piece
|
|
79
|
+
* @param kind - Which cron slot (day names only apply to `dayOfWeek`)
|
|
80
|
+
*/
|
|
81
|
+
function normalizeToken(value: number | string, kind: "dayOfWeek" | "other"): string {
|
|
82
|
+
if (typeof value === "number") {
|
|
83
|
+
if (!Number.isInteger(value)) {
|
|
84
|
+
throw new TypeError(`cron field: expected integer, got ${value}`);
|
|
85
|
+
}
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
const trimmed = value.trim();
|
|
89
|
+
if (!trimmed) {
|
|
90
|
+
throw new TypeError("cron field: empty string");
|
|
91
|
+
}
|
|
92
|
+
if (kind === "dayOfWeek") {
|
|
93
|
+
const lower = trimmed.toLowerCase();
|
|
94
|
+
const mapped = DAY_NAME_TO_NUM[lower];
|
|
95
|
+
if (mapped !== undefined) return String(mapped);
|
|
96
|
+
}
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Serialize one cron field to a crontab token (`*` when omitted).
|
|
102
|
+
*
|
|
103
|
+
* @param field - Optional structured field
|
|
104
|
+
* @param kind - Slot kind for day-name mapping
|
|
105
|
+
*/
|
|
106
|
+
export function serializeCronField(
|
|
107
|
+
field: CronField | undefined,
|
|
108
|
+
kind: "dayOfWeek" | "other" = "other",
|
|
109
|
+
): string {
|
|
110
|
+
if (field === undefined) return "*";
|
|
111
|
+
if (Array.isArray(field)) {
|
|
112
|
+
if (field.length === 0) {
|
|
113
|
+
throw new TypeError("cron field: empty list");
|
|
114
|
+
}
|
|
115
|
+
return field.map((v) => normalizeToken(v as number | string, kind)).join(",");
|
|
116
|
+
}
|
|
117
|
+
return normalizeToken(field as number | string, kind);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Whether the bag has any schedule signal (fields or `at`).
|
|
122
|
+
*
|
|
123
|
+
* @param fields - Structured bag
|
|
124
|
+
*/
|
|
125
|
+
function hasScheduleSignal(fields: CronFields): boolean {
|
|
126
|
+
return (
|
|
127
|
+
fields.at !== undefined ||
|
|
128
|
+
fields.minute !== undefined ||
|
|
129
|
+
fields.hour !== undefined ||
|
|
130
|
+
fields.dayOfMonth !== undefined ||
|
|
131
|
+
fields.month !== undefined ||
|
|
132
|
+
fields.dayOfWeek !== undefined
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build a five-field cron expression from structured fields.
|
|
138
|
+
*
|
|
139
|
+
* @param fields - Field bag (`at` + minute/hour/dom/mon/dow)
|
|
140
|
+
* @throws TypeError when empty or `at` is invalid
|
|
141
|
+
*/
|
|
142
|
+
export function buildCronExpression(fields: CronFields): string {
|
|
143
|
+
if (!hasScheduleSignal(fields)) {
|
|
144
|
+
throw new TypeError(
|
|
145
|
+
"cron fields: require at least one of at, minute, hour, dayOfMonth, month, dayOfWeek",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let minute = fields.minute;
|
|
150
|
+
let hour = fields.hour;
|
|
151
|
+
if (fields.at !== undefined) {
|
|
152
|
+
const parsed = parseAtTime(fields.at);
|
|
153
|
+
if (minute === undefined) minute = parsed.minute;
|
|
154
|
+
if (hour === undefined) hour = parsed.hour;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return [
|
|
158
|
+
serializeCronField(minute),
|
|
159
|
+
serializeCronField(hour),
|
|
160
|
+
serializeCronField(fields.dayOfMonth),
|
|
161
|
+
serializeCronField(fields.month),
|
|
162
|
+
serializeCronField(fields.dayOfWeek, "dayOfWeek"),
|
|
163
|
+
].join(" ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Assert `Bun.cron.parse` accepts the expression (fail loud at declare).
|
|
168
|
+
*
|
|
169
|
+
* @param expression - Five-field cron or Bun nickname
|
|
170
|
+
* @param label - Error prefix (clock name / helper)
|
|
171
|
+
*/
|
|
172
|
+
export function assertValidCronExpression(expression: string, label: string): void {
|
|
173
|
+
try {
|
|
174
|
+
(
|
|
175
|
+
Bun.cron.parse as (
|
|
176
|
+
expression: string,
|
|
177
|
+
relativeDate?: Date | number,
|
|
178
|
+
options?: { readonly tz?: string },
|
|
179
|
+
) => Date | null
|
|
180
|
+
)(expression, Date.now(), { tz: "UTC" });
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
throw new TypeError(`${label}: invalid cron ${JSON.stringify(expression)} (${msg})`);
|
|
184
|
+
}
|
|
185
|
+
}
|