okengine 0.18.5 → 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/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 +20 -10
- 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 +63 -36
- package/site/content/docs/meta.json +8 -7
- 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/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 +257 -43
- 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 -144
- 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 -114
- 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-3-EFj-2G.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-cVFnA4HH.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-CMIgUbD0.js +0 -66
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.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-l8FeKfnP.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-CL-d_mLE.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
package/src/auth/bindings.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { gate, type PolicyGateDecl } from "../elements/gate.ts";
|
|
10
10
|
import type { GateDecl } from "../elements/gate/declare.ts";
|
|
11
|
+
import { applyBoundaryContract, stampBoundaryContract } from "../kernel/boundary-contract.ts";
|
|
11
12
|
import { fail } from "../kernel/errors.ts";
|
|
12
13
|
import { flow, type AnyFlowDef } from "../kernel/flow.ts";
|
|
13
14
|
import type { Binding } from "../kernel/on.ts";
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
IdentityError,
|
|
23
24
|
type IdentityStore,
|
|
24
25
|
} from "./identity.ts";
|
|
26
|
+
import { getActiveGateAuthContext } from "./method-context.ts";
|
|
25
27
|
import { PasswordPolicyError } from "./password-policy.ts";
|
|
26
28
|
import {
|
|
27
29
|
createSessionStore,
|
|
@@ -91,6 +93,17 @@ const SessionTokensOut = z.object({
|
|
|
91
93
|
userId: z.string().optional(),
|
|
92
94
|
});
|
|
93
95
|
|
|
96
|
+
const SignInOut = z.union([
|
|
97
|
+
SessionTokensOut,
|
|
98
|
+
z.object({
|
|
99
|
+
twoFactorRequired: z.literal(true),
|
|
100
|
+
challengeId: z.string(),
|
|
101
|
+
method: z.enum(["totp", "email_otp"]),
|
|
102
|
+
userId: z.string(),
|
|
103
|
+
devOtp: z.string().optional(),
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
|
|
94
107
|
const RefreshIn = z.object({
|
|
95
108
|
refreshToken: z.string().min(1),
|
|
96
109
|
});
|
|
@@ -120,6 +133,14 @@ const MeOut = z.object({
|
|
|
120
133
|
email: z.string(),
|
|
121
134
|
name: z.string(),
|
|
122
135
|
emailVerified: z.boolean(),
|
|
136
|
+
/** Live principal scopes — for UI chrome only; Gate on Flows is real authz. */
|
|
137
|
+
scopes: z.array(z.string()),
|
|
138
|
+
/** Active tenant id when multi-tenancy is on; otherwise null. */
|
|
139
|
+
tenantId: z.string().nullable(),
|
|
140
|
+
/** Authenticating API key id when Bearer was a key secret; otherwise null. */
|
|
141
|
+
apiKeyId: z.string().nullable(),
|
|
142
|
+
/** Whether the session completed verification (email / MFA). */
|
|
143
|
+
sessionFresh: z.boolean().optional(),
|
|
123
144
|
});
|
|
124
145
|
|
|
125
146
|
/**
|
|
@@ -205,11 +226,14 @@ export function createAuthHttpBindings(
|
|
|
205
226
|
|
|
206
227
|
const base = config.basePath;
|
|
207
228
|
|
|
208
|
-
const
|
|
209
|
-
plane: "user",
|
|
229
|
+
const refreshContract = {
|
|
210
230
|
in: RefreshIn,
|
|
211
231
|
out: SessionTokensOut,
|
|
212
232
|
errors: { AuthFailed, AuthRateLimited },
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const refresh = flow("auth.refresh", {
|
|
236
|
+
plane: "user",
|
|
213
237
|
do: async (input) => {
|
|
214
238
|
try {
|
|
215
239
|
const issued = await rotateRefresh(sessions, ctx.sessionCrypto(), input.refreshToken);
|
|
@@ -229,11 +253,14 @@ export function createAuthHttpBindings(
|
|
|
229
253
|
},
|
|
230
254
|
});
|
|
231
255
|
|
|
232
|
-
const
|
|
233
|
-
plane: "user",
|
|
256
|
+
const revokeContract = {
|
|
234
257
|
in: RevokeIn,
|
|
235
258
|
out: z.object({ ok: z.literal(true) }),
|
|
236
259
|
errors: { AuthFailed },
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const revoke = flow("auth.revoke", {
|
|
263
|
+
plane: "user",
|
|
237
264
|
do: async (input) => {
|
|
238
265
|
if (input.refreshToken) {
|
|
239
266
|
try {
|
|
@@ -248,10 +275,13 @@ export function createAuthHttpBindings(
|
|
|
248
275
|
},
|
|
249
276
|
});
|
|
250
277
|
|
|
251
|
-
const
|
|
252
|
-
plane: "user",
|
|
278
|
+
const meContract = {
|
|
253
279
|
out: MeOut,
|
|
254
280
|
errors: { AuthFailed },
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const me = flow("auth.me", {
|
|
284
|
+
plane: "user",
|
|
255
285
|
do: (_input, fx) => {
|
|
256
286
|
const userId = fx.auth.userId;
|
|
257
287
|
if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
|
|
@@ -262,6 +292,10 @@ export function createAuthHttpBindings(
|
|
|
262
292
|
email: user.email,
|
|
263
293
|
name: user.name,
|
|
264
294
|
emailVerified: user.emailVerified,
|
|
295
|
+
scopes: [...fx.auth.scopes],
|
|
296
|
+
tenantId: fx.tenant.id,
|
|
297
|
+
apiKeyId: fx.auth.apiKeyId ?? null,
|
|
298
|
+
...(fx.auth.verified !== undefined ? { sessionFresh: fx.auth.verified } : {}),
|
|
265
299
|
};
|
|
266
300
|
},
|
|
267
301
|
});
|
|
@@ -276,21 +310,24 @@ export function createAuthHttpBindings(
|
|
|
276
310
|
const bindings: Binding[] = [
|
|
277
311
|
bindAuthHttp(
|
|
278
312
|
http
|
|
279
|
-
.post(`${base}/refresh
|
|
313
|
+
.post(`${base}/refresh`, refreshContract)
|
|
280
314
|
.public()
|
|
281
315
|
.gate(...(refreshRate ? [refreshRate] : [])),
|
|
282
316
|
refresh,
|
|
283
317
|
),
|
|
284
|
-
bindAuthHttp(http.post(`${base}/revoke
|
|
285
|
-
bindAuthHttp(http.get(`${base}/me
|
|
318
|
+
bindAuthHttp(http.post(`${base}/revoke`, revokeContract).public(), revoke),
|
|
319
|
+
bindAuthHttp(http.get(`${base}/me`, meContract).gate(AUTH_SESSION_GATE), me),
|
|
286
320
|
];
|
|
287
321
|
|
|
288
322
|
if (config.emailAndPassword.enabled) {
|
|
289
|
-
const
|
|
290
|
-
plane: "user",
|
|
323
|
+
const signInEmailContract = {
|
|
291
324
|
in: EmailPasswordIn,
|
|
292
|
-
out:
|
|
325
|
+
out: SignInOut,
|
|
293
326
|
errors: { AuthFailed, AuthRateLimited },
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const signInEmail = flow("auth.signInEmail", {
|
|
330
|
+
plane: "user",
|
|
294
331
|
do: async (input) => {
|
|
295
332
|
// Rate limit is Gate KV (`signInRate` on the binding) — shared across
|
|
296
333
|
// instances when `drivers.store.kv` is redis. No process-local email bag.
|
|
@@ -300,6 +337,11 @@ export function createAuthHttpBindings(
|
|
|
300
337
|
if (config.emailAndPassword.requireEmailVerification && !user.emailVerified) {
|
|
301
338
|
return fail("AuthFailed", { reason: "invalid_credentials" });
|
|
302
339
|
}
|
|
340
|
+
const bridge = getActiveGateAuthContext()?.twoFactor;
|
|
341
|
+
if (bridge?.isEnabled(user.id)) {
|
|
342
|
+
const challenge = await bridge.beginLoginChallenge(user.id);
|
|
343
|
+
if (challenge) return challenge;
|
|
344
|
+
}
|
|
303
345
|
const issued = await issueSessionWithScopes(sessions, ctx.sessionCrypto(), {
|
|
304
346
|
id: user.id,
|
|
305
347
|
plane: "user",
|
|
@@ -314,11 +356,14 @@ export function createAuthHttpBindings(
|
|
|
314
356
|
},
|
|
315
357
|
});
|
|
316
358
|
|
|
317
|
-
const
|
|
318
|
-
plane: "user",
|
|
359
|
+
const signUpEmailContract = {
|
|
319
360
|
in: EmailPasswordIn,
|
|
320
361
|
out: SessionTokensOut,
|
|
321
362
|
errors: { AuthFailed, AuthRateLimited },
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const signUpEmail = flow("auth.signUpEmail", {
|
|
366
|
+
plane: "user",
|
|
322
367
|
do: async (input) => {
|
|
323
368
|
try {
|
|
324
369
|
const user = await createUserWithPassword(identities, {
|
|
@@ -375,14 +420,14 @@ export function createAuthHttpBindings(
|
|
|
375
420
|
bindings.push(
|
|
376
421
|
bindAuthHttp(
|
|
377
422
|
http
|
|
378
|
-
.post(`${base}/sign-in/email
|
|
423
|
+
.post(`${base}/sign-in/email`, signInEmailContract)
|
|
379
424
|
.public()
|
|
380
425
|
.gate(...(signInRate ? [signInRate] : [])),
|
|
381
426
|
signInEmail,
|
|
382
427
|
),
|
|
383
428
|
bindAuthHttp(
|
|
384
429
|
http
|
|
385
|
-
.post(`${base}/sign-up/email
|
|
430
|
+
.post(`${base}/sign-up/email`, signUpEmailContract)
|
|
386
431
|
.public()
|
|
387
432
|
.gate(...(signUpRate ? [signUpRate] : [])),
|
|
388
433
|
signUpEmail,
|
|
@@ -420,6 +465,9 @@ export function bindAuthHttp(trigger: HttpTrigger, flowDef: AnyFlowDef): Binding
|
|
|
420
465
|
const list = flowDef.triggers as Trigger[];
|
|
421
466
|
list.push(normalized);
|
|
422
467
|
(flowDef as { $trigger: Trigger }).$trigger = normalized;
|
|
468
|
+
if (normalized.kind === "http" && normalized.contract !== undefined) {
|
|
469
|
+
applyBoundaryContract(flowDef, stampBoundaryContract(normalized.contract));
|
|
470
|
+
}
|
|
423
471
|
return { trigger: normalized, flow: flowDef };
|
|
424
472
|
}
|
|
425
473
|
|
|
@@ -205,8 +205,13 @@ describe("gate.auth — email Flows + security", () => {
|
|
|
205
205
|
}),
|
|
206
206
|
);
|
|
207
207
|
expect(me.status).toBe(200);
|
|
208
|
-
const meBody = (await me.json()) as {
|
|
208
|
+
const meBody = (await me.json()) as {
|
|
209
|
+
data: { email: string; scopes: string[]; tenantId: string | null; apiKeyId: string | null };
|
|
210
|
+
};
|
|
209
211
|
expect(meBody.data.email).toBe("user@example.com");
|
|
212
|
+
expect(Array.isArray(meBody.data.scopes)).toBe(true);
|
|
213
|
+
expect(meBody.data.tenantId).toBeNull();
|
|
214
|
+
expect(meBody.data.apiKeyId).toBeNull();
|
|
210
215
|
|
|
211
216
|
const refreshed = await app.fetch(
|
|
212
217
|
new Request("http://localhost/auth/refresh", {
|
package/src/auth/identity.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { PasswordHashOptions } from "../runtime/types.ts";
|
|
|
6
6
|
import { createBunCrypto } from "../runtime/primitives.ts";
|
|
7
7
|
import { assertNotBreached, type BreachCheckFn } from "./breach-check.ts";
|
|
8
8
|
import { assertPasswordPolicy, type PasswordPolicyOptions } from "./password-policy.ts";
|
|
9
|
+
import { revokePrincipalSessions, type SessionStore } from "./sessions.ts";
|
|
10
|
+
import { okid } from "../okid.ts";
|
|
9
11
|
|
|
10
12
|
/** User-plane identity row (logical). */
|
|
11
13
|
export interface UserIdentityRow {
|
|
@@ -98,7 +100,7 @@ export async function ensureUserByEmail(
|
|
|
98
100
|
const existing = store.users.get(existingId);
|
|
99
101
|
if (existing) return existing;
|
|
100
102
|
}
|
|
101
|
-
const id =
|
|
103
|
+
const id = okid();
|
|
102
104
|
const user: UserIdentityRow = {
|
|
103
105
|
id,
|
|
104
106
|
email,
|
|
@@ -140,7 +142,7 @@ export async function createUserWithPassword(
|
|
|
140
142
|
const crypto = createBunCrypto();
|
|
141
143
|
const now = options.now ?? (() => Date.now());
|
|
142
144
|
const t = now();
|
|
143
|
-
const id = options.id ??
|
|
145
|
+
const id = options.id ?? okid();
|
|
144
146
|
const user: UserIdentityRow = {
|
|
145
147
|
id,
|
|
146
148
|
email,
|
|
@@ -151,7 +153,7 @@ export async function createUserWithPassword(
|
|
|
151
153
|
updatedAt: t,
|
|
152
154
|
extra: { ...(options.extra ?? {}) },
|
|
153
155
|
};
|
|
154
|
-
const accountId =
|
|
156
|
+
const accountId = okid();
|
|
155
157
|
const account: UserAccountRow = {
|
|
156
158
|
id: accountId,
|
|
157
159
|
userId: id,
|
|
@@ -277,7 +279,7 @@ export async function linkOrProvision(
|
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
const t = now();
|
|
280
|
-
const userId = options.currentUserId ??
|
|
282
|
+
const userId = options.currentUserId ?? okid();
|
|
281
283
|
const existingUser = store.users.get(userId);
|
|
282
284
|
const user: UserIdentityRow = existingUser ?? {
|
|
283
285
|
id: userId,
|
|
@@ -290,7 +292,7 @@ export async function linkOrProvision(
|
|
|
290
292
|
extra: {},
|
|
291
293
|
};
|
|
292
294
|
const account: UserAccountRow = {
|
|
293
|
-
id:
|
|
295
|
+
id: okid(),
|
|
294
296
|
userId,
|
|
295
297
|
provider,
|
|
296
298
|
providerAccountId,
|
|
@@ -309,6 +311,151 @@ export async function linkOrProvision(
|
|
|
309
311
|
return { user, account, created: true };
|
|
310
312
|
}
|
|
311
313
|
|
|
314
|
+
/** Options for {@link completeVerifiedEmailSignIn}. */
|
|
315
|
+
export interface CompleteVerifiedEmailSignInOptions {
|
|
316
|
+
/** Passwordless provider (`magic-link` or `otp`). */
|
|
317
|
+
readonly provider: string;
|
|
318
|
+
/** Provider-scoped subject — normalized email for email OTP / magic-link. */
|
|
319
|
+
readonly providerAccountId: string;
|
|
320
|
+
/** Proven email address (ownership verified by consuming the challenge). */
|
|
321
|
+
readonly email: string;
|
|
322
|
+
/** Injectable clock. */
|
|
323
|
+
readonly now?: () => number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Result of {@link completeVerifiedEmailSignIn}. */
|
|
327
|
+
export interface VerifiedEmailSignInResult extends LinkedCredential {
|
|
328
|
+
/**
|
|
329
|
+
* `true` when an existing unverified account was reclaimed and planted
|
|
330
|
+
* credentials were purged (CVE-2026-67327 pre-account hijack defense).
|
|
331
|
+
*/
|
|
332
|
+
readonly credentialsCompromised: boolean;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Complete a proven-email passwordless sign-in (magic-link / email OTP).
|
|
337
|
+
*
|
|
338
|
+
* Unlike {@link linkOrProvision}, this path may reclaim an existing email
|
|
339
|
+
* owner because the challenge consumption proved inbox ownership:
|
|
340
|
+
* - Existing `provider:email` → re-auth (ensure `emailVerified`).
|
|
341
|
+
* - Unverified email owner → revoke sessions, clear password hashes, mark
|
|
342
|
+
* verified, attach the passwordless credential (pre-account hijack fix).
|
|
343
|
+
* - Verified email owner → attach passwordless credential; no credential purge.
|
|
344
|
+
* - No owner → provision a new verified user.
|
|
345
|
+
*
|
|
346
|
+
* @param store - Shared identity store
|
|
347
|
+
* @param sessions - Session store (for principal revoke on reclaim)
|
|
348
|
+
* @param options - Proven email + passwordless provider
|
|
349
|
+
*/
|
|
350
|
+
export async function completeVerifiedEmailSignIn(
|
|
351
|
+
store: IdentityStore,
|
|
352
|
+
sessions: SessionStore,
|
|
353
|
+
options: CompleteVerifiedEmailSignInOptions,
|
|
354
|
+
): Promise<VerifiedEmailSignInResult> {
|
|
355
|
+
const email = normalizeEmail(options.email);
|
|
356
|
+
if (!email.includes("@")) {
|
|
357
|
+
throw new IdentityError("invalid_email", "invalid email");
|
|
358
|
+
}
|
|
359
|
+
const providerAccountId = normalizeEmail(options.providerAccountId);
|
|
360
|
+
const now = options.now ?? (() => Date.now());
|
|
361
|
+
const t = now();
|
|
362
|
+
const key = `${options.provider}:${providerAccountId}`;
|
|
363
|
+
|
|
364
|
+
// 1. Existing passwordless credential → re-auth (no purge).
|
|
365
|
+
const existingAccountId = store.byProvider.get(key);
|
|
366
|
+
if (existingAccountId) {
|
|
367
|
+
const account = store.accounts.get(existingAccountId);
|
|
368
|
+
const user = account ? store.users.get(account.userId) : undefined;
|
|
369
|
+
if (account && user) {
|
|
370
|
+
if (!user.emailVerified) {
|
|
371
|
+
user.emailVerified = true;
|
|
372
|
+
user.updatedAt = t;
|
|
373
|
+
await store.persistUser?.(user);
|
|
374
|
+
}
|
|
375
|
+
return { user, account, created: false, credentialsCompromised: false };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const emailOwnerId = store.byEmail.get(email);
|
|
380
|
+
if (emailOwnerId) {
|
|
381
|
+
const user = store.users.get(emailOwnerId);
|
|
382
|
+
if (!user) {
|
|
383
|
+
throw new IdentityError("email_in_use", "email already registered to another credential");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const wasUnverified = !user.emailVerified;
|
|
387
|
+
if (wasUnverified) {
|
|
388
|
+
// Pre-account hijack: attacker planted an unverified password account.
|
|
389
|
+
revokePrincipalSessions(sessions, "user", user.id, t);
|
|
390
|
+
const persistAccountJobs: Array<void | Promise<void>> = [];
|
|
391
|
+
for (const account of store.accounts.values()) {
|
|
392
|
+
if (account.userId !== user.id || account.passwordHash === null) continue;
|
|
393
|
+
account.passwordHash = null;
|
|
394
|
+
account.updatedAt = t;
|
|
395
|
+
persistAccountJobs.push(store.persistAccount?.(account));
|
|
396
|
+
}
|
|
397
|
+
await Promise.all(persistAccountJobs);
|
|
398
|
+
user.emailVerified = true;
|
|
399
|
+
user.updatedAt = t;
|
|
400
|
+
await store.persistUser?.(user);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Attach the passwordless provider if missing (reclaim or verified re-auth).
|
|
404
|
+
let account = existingAccountId ? store.accounts.get(existingAccountId) : undefined;
|
|
405
|
+
let created = false;
|
|
406
|
+
if (!account) {
|
|
407
|
+
account = {
|
|
408
|
+
id: okid(),
|
|
409
|
+
userId: user.id,
|
|
410
|
+
provider: options.provider,
|
|
411
|
+
providerAccountId,
|
|
412
|
+
passwordHash: null,
|
|
413
|
+
createdAt: t,
|
|
414
|
+
updatedAt: t,
|
|
415
|
+
};
|
|
416
|
+
store.accounts.set(account.id, account);
|
|
417
|
+
store.byProvider.set(key, account.id);
|
|
418
|
+
await store.persistAccount?.(account);
|
|
419
|
+
created = true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
user,
|
|
424
|
+
account,
|
|
425
|
+
created,
|
|
426
|
+
credentialsCompromised: wasUnverified,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 3. Fresh provision — verified from the start.
|
|
431
|
+
const userId = okid();
|
|
432
|
+
const user: UserIdentityRow = {
|
|
433
|
+
id: userId,
|
|
434
|
+
email,
|
|
435
|
+
name: email.split("@")[0] || "user",
|
|
436
|
+
emailVerified: true,
|
|
437
|
+
status: "active",
|
|
438
|
+
createdAt: t,
|
|
439
|
+
updatedAt: t,
|
|
440
|
+
extra: {},
|
|
441
|
+
};
|
|
442
|
+
const account: UserAccountRow = {
|
|
443
|
+
id: okid(),
|
|
444
|
+
userId,
|
|
445
|
+
provider: options.provider,
|
|
446
|
+
providerAccountId,
|
|
447
|
+
passwordHash: null,
|
|
448
|
+
createdAt: t,
|
|
449
|
+
updatedAt: t,
|
|
450
|
+
};
|
|
451
|
+
store.users.set(userId, user);
|
|
452
|
+
store.byEmail.set(email, userId);
|
|
453
|
+
store.accounts.set(account.id, account);
|
|
454
|
+
store.byProvider.set(key, account.id);
|
|
455
|
+
await Promise.all([store.persistUser?.(user), store.persistAccount?.(account)]);
|
|
456
|
+
return { user, account, created: true, credentialsCompromised: false };
|
|
457
|
+
}
|
|
458
|
+
|
|
312
459
|
/**
|
|
313
460
|
* Ensure a user row exists for `userId` (defensive — two-factor enable on an
|
|
314
461
|
* established session principal).
|
package/src/auth/index.ts
CHANGED
|
@@ -60,9 +60,35 @@ export {
|
|
|
60
60
|
export {
|
|
61
61
|
setActiveGateAuthContext,
|
|
62
62
|
getActiveGateAuthContext,
|
|
63
|
+
patchActiveGateAuthContext,
|
|
63
64
|
type ActiveGateAuthContext,
|
|
65
|
+
type TwoFactorAuthBridge,
|
|
64
66
|
} from "./method-context.ts";
|
|
65
67
|
|
|
68
|
+
export {
|
|
69
|
+
createPendingTwoFactorStore,
|
|
70
|
+
createStepUpStore,
|
|
71
|
+
issueChallenge,
|
|
72
|
+
getChallenge,
|
|
73
|
+
findActiveForUser,
|
|
74
|
+
hasActiveChallenge,
|
|
75
|
+
consumeChallenge,
|
|
76
|
+
invalidateActiveForUser,
|
|
77
|
+
grantStepUp,
|
|
78
|
+
consumeStepUp,
|
|
79
|
+
hasStepUp,
|
|
80
|
+
twoFactorEmailOtpIdentifier,
|
|
81
|
+
DEFAULT_TWO_FACTOR_CHALLENGE_TTL_MS,
|
|
82
|
+
DEFAULT_STEP_UP_TTL_MS,
|
|
83
|
+
type TwoFactorMethod,
|
|
84
|
+
type PendingTwoFactorChallenge,
|
|
85
|
+
type PendingTwoFactorStore,
|
|
86
|
+
type StepUpGrant,
|
|
87
|
+
type StepUpStore,
|
|
88
|
+
type TwoFactorRequiredOut,
|
|
89
|
+
type IssueChallengeOptions,
|
|
90
|
+
} from "./two-factor-challenge.ts";
|
|
91
|
+
|
|
66
92
|
export { createAuthSecondaryStorage, type AuthKv } from "./secondary-storage.ts";
|
|
67
93
|
|
|
68
94
|
export {
|
|
@@ -90,6 +116,7 @@ export {
|
|
|
90
116
|
ensureUserByEmail,
|
|
91
117
|
ensureUserExists,
|
|
92
118
|
linkOrProvision,
|
|
119
|
+
completeVerifiedEmailSignIn,
|
|
93
120
|
IdentityError,
|
|
94
121
|
type IdentityStore,
|
|
95
122
|
type UserIdentityRow,
|
|
@@ -97,6 +124,8 @@ export {
|
|
|
97
124
|
type CreateUserWithPasswordOptions,
|
|
98
125
|
type LinkOrProvisionOptions,
|
|
99
126
|
type LinkedCredential,
|
|
127
|
+
type CompleteVerifiedEmailSignInOptions,
|
|
128
|
+
type VerifiedEmailSignInResult,
|
|
100
129
|
} from "./identity.ts";
|
|
101
130
|
|
|
102
131
|
export {
|
package/src/auth/invites.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { OperatorInviteRow } from "./tables.ts";
|
|
6
|
+
import { okid } from "../okid.ts";
|
|
6
7
|
|
|
7
8
|
/** Default invitation TTL (7 days). */
|
|
8
9
|
export const INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -41,7 +42,7 @@ export function createOperatorInvite(
|
|
|
41
42
|
const now = options.now ?? (() => Date.now());
|
|
42
43
|
const t = now();
|
|
43
44
|
const row: OperatorInviteRow = {
|
|
44
|
-
id: options.id ??
|
|
45
|
+
id: options.id ?? okid(),
|
|
45
46
|
email: options.email,
|
|
46
47
|
invitedBy: options.invitedBy,
|
|
47
48
|
createdAt: t,
|
|
@@ -11,6 +11,26 @@ import type { BreachCheckFn } from "./breach-check.ts";
|
|
|
11
11
|
import type { IdentityStore } from "./identity.ts";
|
|
12
12
|
import type { PasswordPolicyOptions } from "./password-policy.ts";
|
|
13
13
|
import type { SessionStore } from "./sessions.ts";
|
|
14
|
+
import type {
|
|
15
|
+
PendingTwoFactorStore,
|
|
16
|
+
StepUpStore,
|
|
17
|
+
TwoFactorRequiredOut,
|
|
18
|
+
} from "./two-factor-challenge.ts";
|
|
19
|
+
import type { VerificationStore } from "./verification.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bridge registered by `twoFactor()` so first-factor sign-in can withhold
|
|
23
|
+
* tokens and issue a method-locked pending challenge.
|
|
24
|
+
*/
|
|
25
|
+
export interface TwoFactorAuthBridge {
|
|
26
|
+
/** Whether the user has an enabled second factor. */
|
|
27
|
+
isEnabled(userId: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Issue a locked login challenge (and email OTP when method is email_otp).
|
|
30
|
+
* Returns null when 2FA is not enabled for the user.
|
|
31
|
+
*/
|
|
32
|
+
beginLoginChallenge(userId: string): Promise<TwoFactorRequiredOut | null>;
|
|
33
|
+
}
|
|
14
34
|
|
|
15
35
|
/** Shared context for auth method plugins. */
|
|
16
36
|
export interface ActiveGateAuthContext {
|
|
@@ -25,6 +45,14 @@ export interface ActiveGateAuthContext {
|
|
|
25
45
|
readonly password?: PasswordHashOptions;
|
|
26
46
|
/** From `gate.auth.breachCheck` — optional breach checker. */
|
|
27
47
|
readonly breachCheck?: BreachCheckFn;
|
|
48
|
+
/** Shared pending login 2FA challenges (created at Gate auth wire). */
|
|
49
|
+
readonly pendingTwoFactor?: PendingTwoFactorStore;
|
|
50
|
+
/** Shared step-up grants for privileged 2FA ops. */
|
|
51
|
+
readonly stepUp?: StepUpStore;
|
|
52
|
+
/** Shared verification store for 2FA email OTP (and injectable tests). */
|
|
53
|
+
readonly twoFactorVerifications?: VerificationStore;
|
|
54
|
+
/** Registered by `.plug(twoFactor())`. */
|
|
55
|
+
twoFactor?: TwoFactorAuthBridge;
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
let active: ActiveGateAuthContext | undefined;
|
|
@@ -44,3 +72,16 @@ export function setActiveGateAuthContext(ctx: ActiveGateAuthContext | undefined)
|
|
|
44
72
|
export function getActiveGateAuthContext(): ActiveGateAuthContext | undefined {
|
|
45
73
|
return active;
|
|
46
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Mutate the active context in place (e.g. register `twoFactor` bridge).
|
|
78
|
+
*
|
|
79
|
+
* @param patch - Fields to merge onto the active context
|
|
80
|
+
*/
|
|
81
|
+
export function patchActiveGateAuthContext(
|
|
82
|
+
patch: Partial<ActiveGateAuthContext>,
|
|
83
|
+
): ActiveGateAuthContext | undefined {
|
|
84
|
+
if (!active) return undefined;
|
|
85
|
+
active = { ...active, ...patch };
|
|
86
|
+
return active;
|
|
87
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { OAuthError } from "./errors.ts";
|
|
11
11
|
import type { SigningKeyRow } from "./tables.ts";
|
|
12
|
+
import { okid } from "../../okid.ts";
|
|
12
13
|
|
|
13
14
|
/** Default access-token TTL (10 minutes — short-lived per OAuth 2.1 posture). */
|
|
14
15
|
export const OAUTH_ACCESS_TTL_MS = 10 * 60_000;
|
|
@@ -210,7 +211,7 @@ export async function createDpopSigner(): Promise<{
|
|
|
210
211
|
publicJwk,
|
|
211
212
|
async prove({ htm, htu, now = Date.now(), accessToken, nonce }) {
|
|
212
213
|
const claims: Record<string, unknown> = {
|
|
213
|
-
jti:
|
|
214
|
+
jti: okid(),
|
|
214
215
|
htm,
|
|
215
216
|
htu: normalizeHtu(htu),
|
|
216
217
|
iat: Math.floor(now / 1000),
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ClientCacheRow,
|
|
12
12
|
ConsentRow,
|
|
13
13
|
} from "./tables.ts";
|
|
14
|
+
import { okid } from "../../okid.ts";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Create an empty AS store bundle.
|
|
@@ -57,5 +58,5 @@ export async function hashSecret(raw: string): Promise<string> {
|
|
|
57
58
|
|
|
58
59
|
/** Cryptographically random id (no dashes), same hygiene as session ids. */
|
|
59
60
|
export function cryptoId(): string {
|
|
60
|
-
return
|
|
61
|
+
return okid();
|
|
61
62
|
}
|
package/src/auth/operator.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createBunCrypto } from "../runtime/primitives.ts";
|
|
|
12
12
|
import { assertNotBreached, type BreachCheckFn } from "./breach-check.ts";
|
|
13
13
|
import { assertPasswordPolicy, type PasswordPolicyOptions } from "./password-policy.ts";
|
|
14
14
|
import type { OperatorCredentialRow, OperatorRow, OperatorSsoLinkRow } from "./tables.ts";
|
|
15
|
+
import { okid } from "../okid.ts";
|
|
15
16
|
|
|
16
17
|
/** Operator-plane store. */
|
|
17
18
|
export interface OperatorStore {
|
|
@@ -74,7 +75,7 @@ export async function createOperator(
|
|
|
74
75
|
}
|
|
75
76
|
await assertNotBreached(options.password, options.breachCheck);
|
|
76
77
|
const crypto = createBunCrypto();
|
|
77
|
-
const id = options.id ??
|
|
78
|
+
const id = options.id ?? okid();
|
|
78
79
|
const row: OperatorRow = {
|
|
79
80
|
id,
|
|
80
81
|
email: options.email,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defense-in-depth: session access JWT must allowlist HS256 and reject
|
|
3
|
+
* spoofed alg headers / non-JWS compact shapes (JWE = 5 parts).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { createSessionStore, issueSession, SessionError, verifyAccess } from "./sessions.ts";
|
|
8
|
+
|
|
9
|
+
const SECRET = "test-secret-at-least-32-bytes-long!!";
|
|
10
|
+
|
|
11
|
+
function b64url(s: string): string {
|
|
12
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function hmacSha256B64url(secret: string, data: string): Promise<string> {
|
|
16
|
+
const key = await crypto.subtle.importKey(
|
|
17
|
+
"raw",
|
|
18
|
+
new TextEncoder().encode(secret),
|
|
19
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
20
|
+
false,
|
|
21
|
+
["sign"],
|
|
22
|
+
);
|
|
23
|
+
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
|
|
24
|
+
let bin = "";
|
|
25
|
+
for (const b of new Uint8Array(sig)) bin += String.fromCharCode(b);
|
|
26
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("session access JWT alg enforcement", () => {
|
|
30
|
+
test("rejects alg: none", async () => {
|
|
31
|
+
const store = createSessionStore();
|
|
32
|
+
const header = b64url(JSON.stringify({ alg: "none", typ: "JWT" }));
|
|
33
|
+
const payload = b64url(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
sub: "u1",
|
|
36
|
+
plane: "user",
|
|
37
|
+
sid: "s1",
|
|
38
|
+
scopes: [],
|
|
39
|
+
iat: 1,
|
|
40
|
+
exp: 9_999_999_999_999,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
const token = `${header}.${payload}.`;
|
|
44
|
+
await expect(verifyAccess(store, SECRET, token)).rejects.toThrow(SessionError);
|
|
45
|
+
await expect(verifyAccess(store, SECRET, token)).rejects.toThrow(
|
|
46
|
+
"unsupported access token alg",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("rejects mismatched alg even when HMAC is valid", async () => {
|
|
51
|
+
const store = createSessionStore();
|
|
52
|
+
const now = 1_700_000_000_000;
|
|
53
|
+
const issued = await issueSession(
|
|
54
|
+
store,
|
|
55
|
+
{ secret: SECRET, now: () => now },
|
|
56
|
+
{ id: "u1", plane: "user", scopes: ["read"] },
|
|
57
|
+
);
|
|
58
|
+
const payloadPart = issued.accessToken.split(".")[1]!;
|
|
59
|
+
const forgedHeader = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
60
|
+
const data = `${forgedHeader}.${payloadPart}`;
|
|
61
|
+
const sig = await hmacSha256B64url(SECRET, data);
|
|
62
|
+
const forged = `${data}.${sig}`;
|
|
63
|
+
await expect(verifyAccess(store, SECRET, forged, { now: () => now })).rejects.toThrow(
|
|
64
|
+
SessionError,
|
|
65
|
+
);
|
|
66
|
+
await expect(verifyAccess(store, SECRET, forged, { now: () => now })).rejects.toThrow(
|
|
67
|
+
"unsupported access token alg",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("rejects JWE compact serialization (5 parts)", async () => {
|
|
72
|
+
const store = createSessionStore();
|
|
73
|
+
const jwe = "a.b.c.d.e";
|
|
74
|
+
await expect(verifyAccess(store, SECRET, jwe)).rejects.toThrow(SessionError);
|
|
75
|
+
await expect(verifyAccess(store, SECRET, jwe)).rejects.toThrow("malformed access token");
|
|
76
|
+
});
|
|
77
|
+
});
|