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
|
@@ -14,7 +14,7 @@ import { resetBindings } from "../kernel/on.ts";
|
|
|
14
14
|
import { anonymous } from "./anonymous.ts";
|
|
15
15
|
import { magicLink } from "./magic-link.ts";
|
|
16
16
|
import { otp } from "./otp.ts";
|
|
17
|
-
import { passkey } from "./passkey.ts";
|
|
17
|
+
import { createPasskeyStore, passkey } from "./passkey.ts";
|
|
18
18
|
import { b64urlEncode, buildAuthenticatorData, signWebAuthnAssertion } from "./passkey-webauthn.ts";
|
|
19
19
|
import { twoFactor, verifyTotp, createTwoFactorStore } from "./two-factor.ts";
|
|
20
20
|
import { username } from "./username.ts";
|
|
@@ -96,6 +96,8 @@ async function buildCeremony(opts: {
|
|
|
96
96
|
rpId: string;
|
|
97
97
|
privateKey: CryptoKey;
|
|
98
98
|
signCount: number;
|
|
99
|
+
/** Authenticator flags (default UP|UV = 0x05). */
|
|
100
|
+
flags?: number;
|
|
99
101
|
}): Promise<{
|
|
100
102
|
clientDataJSON: string;
|
|
101
103
|
authenticatorData: string;
|
|
@@ -108,7 +110,7 @@ async function buildCeremony(opts: {
|
|
|
108
110
|
origin: opts.origin,
|
|
109
111
|
}),
|
|
110
112
|
);
|
|
111
|
-
const authData = await buildAuthenticatorData(opts.rpId, opts.signCount);
|
|
113
|
+
const authData = await buildAuthenticatorData(opts.rpId, opts.signCount, opts.flags ?? 0x05);
|
|
112
114
|
const sig = await signWebAuthnAssertion(opts.privateKey, authData, clientData);
|
|
113
115
|
return {
|
|
114
116
|
clientDataJSON: b64urlEncode(clientData),
|
|
@@ -117,6 +119,9 @@ async function buildCeremony(opts: {
|
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
type PasskeyRegOpts = { challenge: string; sessionId: string; userId: string; rpId: string };
|
|
123
|
+
type PasskeyAuthOpts = { challenge: string; sessionId: string; rpId: string };
|
|
124
|
+
|
|
120
125
|
describe("auth methods — gate posture (zero-trust)", () => {
|
|
121
126
|
test("every plugin HTTP binding declares gate posture; boot succeeds with deny", async () => {
|
|
122
127
|
const app = fullAuthApp();
|
|
@@ -274,12 +279,32 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
274
279
|
const enabled = (await enable.json()) as { data: { recoveryCodes: string[] } };
|
|
275
280
|
const code = enabled.data.recoveryCodes[0]!;
|
|
276
281
|
|
|
282
|
+
const challengeRes = await app.fetch(
|
|
283
|
+
jsonPost("/auth/sign-in/username", {
|
|
284
|
+
username: "twofa_user",
|
|
285
|
+
password: "CorrectHorse1!",
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
expect(challengeRes.status).toBe(200);
|
|
289
|
+
const challenge = (await challengeRes.json()) as {
|
|
290
|
+
data: { twoFactorRequired: true; challengeId: string };
|
|
291
|
+
};
|
|
292
|
+
expect(challenge.data.twoFactorRequired).toBe(true);
|
|
293
|
+
|
|
277
294
|
const first = await app.fetch(
|
|
278
|
-
jsonPost("/auth/two-factor/verify", {
|
|
295
|
+
jsonPost("/auth/two-factor/verify", { challengeId: challenge.data.challengeId, code }),
|
|
279
296
|
);
|
|
280
297
|
expect(first.status).toBe(200);
|
|
298
|
+
|
|
299
|
+
const challenge2 = await app.fetch(
|
|
300
|
+
jsonPost("/auth/sign-in/username", {
|
|
301
|
+
username: "twofa_user",
|
|
302
|
+
password: "CorrectHorse1!",
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
const c2 = (await challenge2.json()) as { data: { challengeId: string } };
|
|
281
306
|
const reuse = await app.fetch(
|
|
282
|
-
jsonPost("/auth/two-factor/verify", {
|
|
307
|
+
jsonPost("/auth/two-factor/verify", { challengeId: c2.data.challengeId, code }),
|
|
283
308
|
);
|
|
284
309
|
expect((await readError(reuse)).reason).toBe("invalid_credentials");
|
|
285
310
|
|
|
@@ -299,9 +324,7 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
299
324
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
300
325
|
),
|
|
301
326
|
);
|
|
302
|
-
const reg = (await regOpts.json()) as {
|
|
303
|
-
data: { challenge: string; userId: string; rpId: string };
|
|
304
|
-
};
|
|
327
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
305
328
|
const regCeremony = await buildCeremony({
|
|
306
329
|
type: "webauthn.create",
|
|
307
330
|
challenge: reg.data.challenge,
|
|
@@ -319,6 +342,7 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
319
342
|
publicKey: keys.publicKeyB64,
|
|
320
343
|
userId: reg.data.userId,
|
|
321
344
|
challenge: reg.data.challenge,
|
|
345
|
+
sessionId: reg.data.sessionId,
|
|
322
346
|
...regCeremony,
|
|
323
347
|
},
|
|
324
348
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
@@ -327,7 +351,7 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
327
351
|
expect(registered.status).toBe(200);
|
|
328
352
|
|
|
329
353
|
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
330
|
-
const auth = (await authOpts.json()) as { data:
|
|
354
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
331
355
|
const authCeremony = await buildCeremony({
|
|
332
356
|
type: "webauthn.get",
|
|
333
357
|
challenge: auth.data.challenge,
|
|
@@ -340,6 +364,7 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
340
364
|
jsonPost("/auth/passkey/authenticate", {
|
|
341
365
|
credentialId,
|
|
342
366
|
challenge: auth.data.challenge,
|
|
367
|
+
sessionId: auth.data.sessionId,
|
|
343
368
|
...authCeremony,
|
|
344
369
|
}),
|
|
345
370
|
);
|
|
@@ -349,6 +374,7 @@ describe("auth methods — single-use OTP / token / backup codes", () => {
|
|
|
349
374
|
jsonPost("/auth/passkey/authenticate", {
|
|
350
375
|
credentialId,
|
|
351
376
|
challenge: auth.data.challenge,
|
|
377
|
+
sessionId: auth.data.sessionId,
|
|
352
378
|
...authCeremony,
|
|
353
379
|
}),
|
|
354
380
|
);
|
|
@@ -375,7 +401,7 @@ describe("auth methods — anonymous non-escalation", () => {
|
|
|
375
401
|
),
|
|
376
402
|
);
|
|
377
403
|
expect(opts.status).toBe(200);
|
|
378
|
-
const optBody = (await opts.json()) as { data:
|
|
404
|
+
const optBody = (await opts.json()) as { data: PasskeyRegOpts };
|
|
379
405
|
expect(optBody.data.userId).toBe(anonBody.data.userId);
|
|
380
406
|
|
|
381
407
|
const keys = await generatePasskeyKeypair();
|
|
@@ -395,6 +421,7 @@ describe("auth methods — anonymous non-escalation", () => {
|
|
|
395
421
|
publicKey: keys.publicKeyB64,
|
|
396
422
|
userId: victim.userId,
|
|
397
423
|
challenge: optBody.data.challenge,
|
|
424
|
+
sessionId: optBody.data.sessionId,
|
|
398
425
|
...ceremony,
|
|
399
426
|
},
|
|
400
427
|
{ authorization: `Bearer ${anonBody.data.accessToken}` },
|
|
@@ -488,15 +515,39 @@ describe("auth methods — TOTP constant-time compare (confirmed issue)", () =>
|
|
|
488
515
|
const code = await generateTotpForTest(data.secret, t);
|
|
489
516
|
expect(await verifyTotp(data.secret, code, t)).toBe(true);
|
|
490
517
|
|
|
518
|
+
const challengeRes = await app.fetch(
|
|
519
|
+
jsonPost("/auth/sign-in/username", {
|
|
520
|
+
username: "totp_user",
|
|
521
|
+
password: "CorrectHorse1!",
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
const challenge = (await challengeRes.json()) as {
|
|
525
|
+
data: { challengeId: string; twoFactorRequired: true };
|
|
526
|
+
};
|
|
527
|
+
expect(challenge.data.twoFactorRequired).toBe(true);
|
|
528
|
+
|
|
491
529
|
const ok = await app.fetch(
|
|
492
|
-
jsonPost("/auth/two-factor/verify", {
|
|
530
|
+
jsonPost("/auth/two-factor/verify", {
|
|
531
|
+
challengeId: challenge.data.challengeId,
|
|
532
|
+
code,
|
|
533
|
+
}),
|
|
493
534
|
);
|
|
494
535
|
expect(ok.status).toBe(200);
|
|
495
536
|
|
|
496
537
|
// Wrong code of the same length — must fail (uses constant-time path).
|
|
538
|
+
const challengeBad = await app.fetch(
|
|
539
|
+
jsonPost("/auth/sign-in/username", {
|
|
540
|
+
username: "totp_user",
|
|
541
|
+
password: "CorrectHorse1!",
|
|
542
|
+
}),
|
|
543
|
+
);
|
|
544
|
+
const badCh = (await challengeBad.json()) as { data: { challengeId: string } };
|
|
497
545
|
const wrong = code === "000000" ? "000001" : "000000";
|
|
498
546
|
const bad = await app.fetch(
|
|
499
|
-
jsonPost("/auth/two-factor/verify", {
|
|
547
|
+
jsonPost("/auth/two-factor/verify", {
|
|
548
|
+
challengeId: badCh.data.challengeId,
|
|
549
|
+
code: wrong,
|
|
550
|
+
}),
|
|
500
551
|
);
|
|
501
552
|
expect((await readError(bad)).reason).toBe("invalid_credentials");
|
|
502
553
|
|
|
@@ -504,6 +555,160 @@ describe("auth methods — TOTP constant-time compare (confirmed issue)", () =>
|
|
|
504
555
|
});
|
|
505
556
|
});
|
|
506
557
|
|
|
558
|
+
describe("auth methods — 2FA method lock (July–August 2026 bypass)", () => {
|
|
559
|
+
test("TOTP enrollment mid email_otp challenge returns Forbidden", async () => {
|
|
560
|
+
const factors = createTwoFactorStore();
|
|
561
|
+
const app = oke({
|
|
562
|
+
name: `2fa-lock-${crypto.randomUUID()}`,
|
|
563
|
+
env: "test",
|
|
564
|
+
registry: "ignore",
|
|
565
|
+
gate: { auth: { secret: SECRET, emailAndPassword: { enabled: true } } },
|
|
566
|
+
})
|
|
567
|
+
.plug(username())
|
|
568
|
+
.plug(otp({ mode: "app", channels: ["email"], exposeDevOtp: true }))
|
|
569
|
+
.plug(twoFactor({ factors, exposeDevOtp: true }));
|
|
570
|
+
await app.boot({ env: "test" });
|
|
571
|
+
|
|
572
|
+
// First-factor session before 2FA is enabled (attacker may hold an old Bearer).
|
|
573
|
+
const session = await mintUsernameSession(app, "lock_victim");
|
|
574
|
+
|
|
575
|
+
// Seed email OTP as the configured second factor (method locked at challenge).
|
|
576
|
+
factors.byUserId.set(session.userId, {
|
|
577
|
+
userId: session.userId,
|
|
578
|
+
method: "email_otp",
|
|
579
|
+
secret: "",
|
|
580
|
+
enabled: true,
|
|
581
|
+
recoveryHashes: new Set(),
|
|
582
|
+
createdAt: Date.now(),
|
|
583
|
+
email: "lock_victim@example.com",
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const signIn = await app.fetch(
|
|
587
|
+
jsonPost("/auth/sign-in/username", {
|
|
588
|
+
username: "lock_victim",
|
|
589
|
+
password: "CorrectHorse1!",
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
expect(signIn.status).toBe(200);
|
|
593
|
+
const challenge = (await signIn.json()) as {
|
|
594
|
+
data: {
|
|
595
|
+
twoFactorRequired: true;
|
|
596
|
+
challengeId: string;
|
|
597
|
+
method: string;
|
|
598
|
+
accessToken?: string;
|
|
599
|
+
};
|
|
600
|
+
};
|
|
601
|
+
expect(challenge.data.twoFactorRequired).toBe(true);
|
|
602
|
+
expect(challenge.data.method).toBe("email_otp");
|
|
603
|
+
expect(challenge.data.accessToken).toBeUndefined();
|
|
604
|
+
|
|
605
|
+
// Mid-challenge: attempt TOTP QR enrollment without completing email OTP.
|
|
606
|
+
const enroll = await app.fetch(
|
|
607
|
+
jsonPost("/auth/two-factor/enable", {}, { authorization: `Bearer ${session.accessToken}` }),
|
|
608
|
+
);
|
|
609
|
+
expect(enroll.status).toBeGreaterThanOrEqual(400);
|
|
610
|
+
const err = await readError(enroll);
|
|
611
|
+
expect(err.code).toBe("Forbidden");
|
|
612
|
+
expect(err.reason).toBe("active_2fa_challenge");
|
|
613
|
+
|
|
614
|
+
await app.stop();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("email_otp challenge verify issues session; method change invalidates TOTP", async () => {
|
|
618
|
+
const factors = createTwoFactorStore();
|
|
619
|
+
const app = oke({
|
|
620
|
+
name: `2fa-change-${crypto.randomUUID()}`,
|
|
621
|
+
env: "test",
|
|
622
|
+
registry: "ignore",
|
|
623
|
+
gate: { auth: { secret: SECRET, emailAndPassword: { enabled: true } } },
|
|
624
|
+
})
|
|
625
|
+
.plug(username())
|
|
626
|
+
.plug(twoFactor({ factors, exposeDevOtp: true }));
|
|
627
|
+
await app.boot({ env: "test" });
|
|
628
|
+
|
|
629
|
+
const session = await mintUsernameSession(app, "change_user");
|
|
630
|
+
const enable = await app.fetch(
|
|
631
|
+
jsonPost("/auth/two-factor/enable", {}, { authorization: `Bearer ${session.accessToken}` }),
|
|
632
|
+
);
|
|
633
|
+
expect(enable.status).toBe(200);
|
|
634
|
+
const enabled = (await enable.json()) as { data: { secret: string } };
|
|
635
|
+
const oldSecret = enabled.data.secret;
|
|
636
|
+
|
|
637
|
+
// Step-up with TOTP, then change to email_otp.
|
|
638
|
+
const t = Math.floor(Date.now() / 1000);
|
|
639
|
+
const totpCode = await generateTotpForTest(oldSecret, t);
|
|
640
|
+
const stepUp = await app.fetch(
|
|
641
|
+
jsonPost(
|
|
642
|
+
"/auth/two-factor/step-up",
|
|
643
|
+
{ code: totpCode, purpose: "change" },
|
|
644
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
expect(stepUp.status).toBe(200);
|
|
648
|
+
|
|
649
|
+
const change = await app.fetch(
|
|
650
|
+
jsonPost(
|
|
651
|
+
"/auth/two-factor/change-method",
|
|
652
|
+
{ method: "email_otp", email: "change_user@example.com" },
|
|
653
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
654
|
+
),
|
|
655
|
+
);
|
|
656
|
+
expect(change.status).toBe(200);
|
|
657
|
+
const changed = (await change.json()) as { data: { method: string; devOtp?: string } };
|
|
658
|
+
expect(changed.data.method).toBe("email_otp");
|
|
659
|
+
expect(changed.data.devOtp).toBeTruthy();
|
|
660
|
+
|
|
661
|
+
const confirm = await app.fetch(
|
|
662
|
+
jsonPost(
|
|
663
|
+
"/auth/two-factor/confirm-change",
|
|
664
|
+
{ code: changed.data.devOtp },
|
|
665
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
expect(confirm.status).toBe(200);
|
|
669
|
+
expect(factors.byUserId.get(session.userId)?.method).toBe("email_otp");
|
|
670
|
+
expect(factors.byUserId.get(session.userId)?.secret).toBe("");
|
|
671
|
+
|
|
672
|
+
// Old TOTP no longer works for login.
|
|
673
|
+
const signIn = await app.fetch(
|
|
674
|
+
jsonPost("/auth/sign-in/username", {
|
|
675
|
+
username: "change_user",
|
|
676
|
+
password: "CorrectHorse1!",
|
|
677
|
+
}),
|
|
678
|
+
);
|
|
679
|
+
const ch = (await signIn.json()) as {
|
|
680
|
+
data: { challengeId: string; method: string; devOtp?: string };
|
|
681
|
+
};
|
|
682
|
+
expect(ch.data.method).toBe("email_otp");
|
|
683
|
+
const badTotp = await app.fetch(
|
|
684
|
+
jsonPost("/auth/two-factor/verify", {
|
|
685
|
+
challengeId: ch.data.challengeId,
|
|
686
|
+
code: await generateTotpForTest(oldSecret, Math.floor(Date.now() / 1000)),
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
689
|
+
expect((await readError(badTotp)).reason).toBe("invalid_credentials");
|
|
690
|
+
|
|
691
|
+
const signIn2 = await app.fetch(
|
|
692
|
+
jsonPost("/auth/sign-in/username", {
|
|
693
|
+
username: "change_user",
|
|
694
|
+
password: "CorrectHorse1!",
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
const ch2 = (await signIn2.json()) as {
|
|
698
|
+
data: { challengeId: string; devOtp?: string };
|
|
699
|
+
};
|
|
700
|
+
const ok = await app.fetch(
|
|
701
|
+
jsonPost("/auth/two-factor/verify", {
|
|
702
|
+
challengeId: ch2.data.challengeId,
|
|
703
|
+
code: ch2.data.devOtp,
|
|
704
|
+
}),
|
|
705
|
+
);
|
|
706
|
+
expect(ok.status).toBe(200);
|
|
707
|
+
|
|
708
|
+
await app.stop();
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
507
712
|
/** Compute a valid 6-digit TOTP for tests (RFC 6238, SHA-1, 30s). */
|
|
508
713
|
async function generateTotpForTest(secretBase32: string, nowSec: number): Promise<string> {
|
|
509
714
|
const B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
@@ -562,9 +767,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
562
767
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
563
768
|
),
|
|
564
769
|
);
|
|
565
|
-
const reg = (await regOpts.json()) as {
|
|
566
|
-
data: { challenge: string; userId: string; rpId: string };
|
|
567
|
-
};
|
|
770
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
568
771
|
const regCeremony = await buildCeremony({
|
|
569
772
|
type: "webauthn.create",
|
|
570
773
|
challenge: reg.data.challenge,
|
|
@@ -584,6 +787,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
584
787
|
publicKey: keys.publicKeyB64,
|
|
585
788
|
userId: reg.data.userId,
|
|
586
789
|
challenge: reg.data.challenge,
|
|
790
|
+
sessionId: reg.data.sessionId,
|
|
587
791
|
...regCeremony,
|
|
588
792
|
},
|
|
589
793
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
@@ -617,9 +821,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
617
821
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
618
822
|
),
|
|
619
823
|
);
|
|
620
|
-
const reg = (await regOpts.json()) as {
|
|
621
|
-
data: { challenge: string; userId: string; rpId: string };
|
|
622
|
-
};
|
|
824
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
623
825
|
const regCeremony = await buildCeremony({
|
|
624
826
|
type: "webauthn.create",
|
|
625
827
|
challenge: reg.data.challenge,
|
|
@@ -639,6 +841,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
639
841
|
publicKey: keys.publicKeyB64,
|
|
640
842
|
userId: reg.data.userId,
|
|
641
843
|
challenge: reg.data.challenge,
|
|
844
|
+
sessionId: reg.data.sessionId,
|
|
642
845
|
...regCeremony,
|
|
643
846
|
},
|
|
644
847
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
@@ -648,7 +851,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
648
851
|
).toBe(200);
|
|
649
852
|
|
|
650
853
|
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
651
|
-
const auth = (await authOpts.json()) as { data:
|
|
854
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
652
855
|
|
|
653
856
|
const evil = await buildCeremony({
|
|
654
857
|
type: "webauthn.get",
|
|
@@ -662,6 +865,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
662
865
|
jsonPost("/auth/passkey/authenticate", {
|
|
663
866
|
credentialId,
|
|
664
867
|
challenge: auth.data.challenge,
|
|
868
|
+
sessionId: auth.data.sessionId,
|
|
665
869
|
...evil,
|
|
666
870
|
}),
|
|
667
871
|
);
|
|
@@ -669,7 +873,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
669
873
|
|
|
670
874
|
// Fresh challenge after failed attempt (previous challenge was consumed).
|
|
671
875
|
const authOpts2 = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
672
|
-
const auth2 = (await authOpts2.json()) as { data:
|
|
876
|
+
const auth2 = (await authOpts2.json()) as { data: PasskeyAuthOpts };
|
|
673
877
|
const good = await buildCeremony({
|
|
674
878
|
type: "webauthn.get",
|
|
675
879
|
challenge: auth2.data.challenge,
|
|
@@ -682,6 +886,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
682
886
|
jsonPost("/auth/passkey/authenticate", {
|
|
683
887
|
credentialId,
|
|
684
888
|
challenge: auth2.data.challenge,
|
|
889
|
+
sessionId: auth2.data.sessionId,
|
|
685
890
|
...good,
|
|
686
891
|
}),
|
|
687
892
|
);
|
|
@@ -707,9 +912,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
707
912
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
708
913
|
),
|
|
709
914
|
);
|
|
710
|
-
const reg = (await regOpts.json()) as {
|
|
711
|
-
data: { challenge: string; userId: string; rpId: string };
|
|
712
|
-
};
|
|
915
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
713
916
|
const regCeremony = await buildCeremony({
|
|
714
917
|
type: "webauthn.create",
|
|
715
918
|
challenge: reg.data.challenge,
|
|
@@ -727,6 +930,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
727
930
|
publicKey: keys.publicKeyB64,
|
|
728
931
|
userId: reg.data.userId,
|
|
729
932
|
challenge: reg.data.challenge,
|
|
933
|
+
sessionId: reg.data.sessionId,
|
|
730
934
|
...regCeremony,
|
|
731
935
|
},
|
|
732
936
|
{ authorization: `Bearer ${session.accessToken}` },
|
|
@@ -734,7 +938,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
734
938
|
);
|
|
735
939
|
|
|
736
940
|
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
737
|
-
const auth = (await authOpts.json()) as { data:
|
|
941
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
738
942
|
const forged = await buildCeremony({
|
|
739
943
|
type: "webauthn.get",
|
|
740
944
|
challenge: auth.data.challenge,
|
|
@@ -747,6 +951,7 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
747
951
|
jsonPost("/auth/passkey/authenticate", {
|
|
748
952
|
credentialId,
|
|
749
953
|
challenge: auth.data.challenge,
|
|
954
|
+
sessionId: auth.data.sessionId,
|
|
750
955
|
...forged,
|
|
751
956
|
}),
|
|
752
957
|
);
|
|
@@ -755,3 +960,336 @@ describe("auth methods — passkey signature + origin (confirmed issue)", () =>
|
|
|
755
960
|
await app.stop();
|
|
756
961
|
});
|
|
757
962
|
});
|
|
963
|
+
|
|
964
|
+
describe("auth methods — passkey hardening (UV · session · counter · type)", () => {
|
|
965
|
+
async function registerPasskey(
|
|
966
|
+
app: ReturnType<typeof fullAuthApp>,
|
|
967
|
+
accessToken: string,
|
|
968
|
+
keys: { privateKey: CryptoKey; publicKeyB64: string },
|
|
969
|
+
): Promise<{ credentialId: string }> {
|
|
970
|
+
const regOpts = await app.fetch(
|
|
971
|
+
jsonPost("/auth/passkey/register/options", {}, { authorization: `Bearer ${accessToken}` }),
|
|
972
|
+
);
|
|
973
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
974
|
+
const regCeremony = await buildCeremony({
|
|
975
|
+
type: "webauthn.create",
|
|
976
|
+
challenge: reg.data.challenge,
|
|
977
|
+
origin: "http://localhost",
|
|
978
|
+
rpId: reg.data.rpId,
|
|
979
|
+
privateKey: keys.privateKey,
|
|
980
|
+
signCount: 0,
|
|
981
|
+
});
|
|
982
|
+
const credentialId = b64urlEncode(crypto.getRandomValues(new Uint8Array(16)));
|
|
983
|
+
const registered = await app.fetch(
|
|
984
|
+
jsonPost(
|
|
985
|
+
"/auth/passkey/register",
|
|
986
|
+
{
|
|
987
|
+
credentialId,
|
|
988
|
+
publicKey: keys.publicKeyB64,
|
|
989
|
+
userId: reg.data.userId,
|
|
990
|
+
challenge: reg.data.challenge,
|
|
991
|
+
sessionId: reg.data.sessionId,
|
|
992
|
+
...regCeremony,
|
|
993
|
+
},
|
|
994
|
+
{ authorization: `Bearer ${accessToken}` },
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
expect(registered.status).toBe(200);
|
|
998
|
+
return { credentialId };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
test("UV=false assertion is rejected with user_not_verified", async () => {
|
|
1002
|
+
const app = fullAuthApp();
|
|
1003
|
+
await app.boot({ env: "test" });
|
|
1004
|
+
const session = await mintUsernameSession(app, "pk_uv");
|
|
1005
|
+
const keys = await generatePasskeyKeypair();
|
|
1006
|
+
const { credentialId } = await registerPasskey(app, session.accessToken, keys);
|
|
1007
|
+
|
|
1008
|
+
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1009
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
1010
|
+
const ceremony = await buildCeremony({
|
|
1011
|
+
type: "webauthn.get",
|
|
1012
|
+
challenge: auth.data.challenge,
|
|
1013
|
+
origin: "http://localhost",
|
|
1014
|
+
rpId: auth.data.rpId,
|
|
1015
|
+
privateKey: keys.privateKey,
|
|
1016
|
+
signCount: 1,
|
|
1017
|
+
flags: 0x01, // UP only — no UV
|
|
1018
|
+
});
|
|
1019
|
+
const res = await app.fetch(
|
|
1020
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1021
|
+
credentialId,
|
|
1022
|
+
challenge: auth.data.challenge,
|
|
1023
|
+
sessionId: auth.data.sessionId,
|
|
1024
|
+
...ceremony,
|
|
1025
|
+
}),
|
|
1026
|
+
);
|
|
1027
|
+
expect((await readError(res)).reason).toBe("user_not_verified");
|
|
1028
|
+
|
|
1029
|
+
await app.stop();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test("wrong clientDataJSON.type is rejected on authenticate and register", async () => {
|
|
1033
|
+
const app = fullAuthApp();
|
|
1034
|
+
await app.boot({ env: "test" });
|
|
1035
|
+
const session = await mintUsernameSession(app, "pk_type");
|
|
1036
|
+
const keys = await generatePasskeyKeypair();
|
|
1037
|
+
const { credentialId } = await registerPasskey(app, session.accessToken, keys);
|
|
1038
|
+
|
|
1039
|
+
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1040
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
1041
|
+
const wrongType = await buildCeremony({
|
|
1042
|
+
type: "webauthn.create", // must be webauthn.get
|
|
1043
|
+
challenge: auth.data.challenge,
|
|
1044
|
+
origin: "http://localhost",
|
|
1045
|
+
rpId: auth.data.rpId,
|
|
1046
|
+
privateKey: keys.privateKey,
|
|
1047
|
+
signCount: 1,
|
|
1048
|
+
});
|
|
1049
|
+
const authRes = await app.fetch(
|
|
1050
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1051
|
+
credentialId,
|
|
1052
|
+
challenge: auth.data.challenge,
|
|
1053
|
+
sessionId: auth.data.sessionId,
|
|
1054
|
+
...wrongType,
|
|
1055
|
+
}),
|
|
1056
|
+
);
|
|
1057
|
+
expect((await readError(authRes)).reason).toBe("invalid_credentials");
|
|
1058
|
+
|
|
1059
|
+
const regOpts = await app.fetch(
|
|
1060
|
+
jsonPost(
|
|
1061
|
+
"/auth/passkey/register/options",
|
|
1062
|
+
{},
|
|
1063
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
1064
|
+
),
|
|
1065
|
+
);
|
|
1066
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
1067
|
+
const wrongRegType = await buildCeremony({
|
|
1068
|
+
type: "webauthn.get", // must be webauthn.create
|
|
1069
|
+
challenge: reg.data.challenge,
|
|
1070
|
+
origin: "http://localhost",
|
|
1071
|
+
rpId: reg.data.rpId,
|
|
1072
|
+
privateKey: keys.privateKey,
|
|
1073
|
+
signCount: 0,
|
|
1074
|
+
});
|
|
1075
|
+
const regRes = await app.fetch(
|
|
1076
|
+
jsonPost(
|
|
1077
|
+
"/auth/passkey/register",
|
|
1078
|
+
{
|
|
1079
|
+
credentialId: b64urlEncode(crypto.getRandomValues(new Uint8Array(8))),
|
|
1080
|
+
publicKey: keys.publicKeyB64,
|
|
1081
|
+
userId: reg.data.userId,
|
|
1082
|
+
challenge: reg.data.challenge,
|
|
1083
|
+
sessionId: reg.data.sessionId,
|
|
1084
|
+
...wrongRegType,
|
|
1085
|
+
},
|
|
1086
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
1087
|
+
),
|
|
1088
|
+
);
|
|
1089
|
+
expect((await readError(regRes)).reason).toBe("invalid_credentials");
|
|
1090
|
+
|
|
1091
|
+
await app.stop();
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test("sessionId mismatch is rejected; matching session + challenge succeeds", async () => {
|
|
1095
|
+
const app = fullAuthApp();
|
|
1096
|
+
await app.boot({ env: "test" });
|
|
1097
|
+
const session = await mintUsernameSession(app, "pk_sid");
|
|
1098
|
+
const keys = await generatePasskeyKeypair();
|
|
1099
|
+
const { credentialId } = await registerPasskey(app, session.accessToken, keys);
|
|
1100
|
+
|
|
1101
|
+
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1102
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
1103
|
+
const ceremony = await buildCeremony({
|
|
1104
|
+
type: "webauthn.get",
|
|
1105
|
+
challenge: auth.data.challenge,
|
|
1106
|
+
origin: "http://localhost",
|
|
1107
|
+
rpId: auth.data.rpId,
|
|
1108
|
+
privateKey: keys.privateKey,
|
|
1109
|
+
signCount: 1,
|
|
1110
|
+
});
|
|
1111
|
+
const mismatch = await app.fetch(
|
|
1112
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1113
|
+
credentialId,
|
|
1114
|
+
challenge: auth.data.challenge,
|
|
1115
|
+
sessionId: crypto.randomUUID(),
|
|
1116
|
+
...ceremony,
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
expect((await readError(mismatch)).reason).toBe("invalid_credentials");
|
|
1120
|
+
|
|
1121
|
+
const authOpts2 = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1122
|
+
const auth2 = (await authOpts2.json()) as { data: PasskeyAuthOpts };
|
|
1123
|
+
const good = await buildCeremony({
|
|
1124
|
+
type: "webauthn.get",
|
|
1125
|
+
challenge: auth2.data.challenge,
|
|
1126
|
+
origin: "http://localhost",
|
|
1127
|
+
rpId: auth2.data.rpId,
|
|
1128
|
+
privateKey: keys.privateKey,
|
|
1129
|
+
signCount: 1,
|
|
1130
|
+
});
|
|
1131
|
+
const ok = await app.fetch(
|
|
1132
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1133
|
+
credentialId,
|
|
1134
|
+
challenge: auth2.data.challenge,
|
|
1135
|
+
sessionId: auth2.data.sessionId,
|
|
1136
|
+
...good,
|
|
1137
|
+
}),
|
|
1138
|
+
);
|
|
1139
|
+
expect(ok.status).toBe(200);
|
|
1140
|
+
|
|
1141
|
+
await app.stop();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test("expired challenge (>5m) is rejected", async () => {
|
|
1145
|
+
let now = 1_700_000_000_000;
|
|
1146
|
+
const app = oke({
|
|
1147
|
+
name: `auth-sec-ttl-${crypto.randomUUID()}`,
|
|
1148
|
+
env: "test",
|
|
1149
|
+
registry: "ignore",
|
|
1150
|
+
gate: {
|
|
1151
|
+
auth: {
|
|
1152
|
+
secret: SECRET,
|
|
1153
|
+
emailAndPassword: { enabled: true },
|
|
1154
|
+
now: () => now,
|
|
1155
|
+
},
|
|
1156
|
+
unguardedHttp: "deny",
|
|
1157
|
+
},
|
|
1158
|
+
})
|
|
1159
|
+
.plug(username({ now: () => now }))
|
|
1160
|
+
.plug(passkey({ origins: ["http://localhost"], now: () => now }));
|
|
1161
|
+
await app.boot({ env: "test" });
|
|
1162
|
+
|
|
1163
|
+
const session = await mintUsernameSession(app, "pk_ttl");
|
|
1164
|
+
const keys = await generatePasskeyKeypair();
|
|
1165
|
+
|
|
1166
|
+
const regOpts = await app.fetch(
|
|
1167
|
+
jsonPost(
|
|
1168
|
+
"/auth/passkey/register/options",
|
|
1169
|
+
{},
|
|
1170
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
1171
|
+
),
|
|
1172
|
+
);
|
|
1173
|
+
const reg = (await regOpts.json()) as { data: PasskeyRegOpts };
|
|
1174
|
+
const regCeremony = await buildCeremony({
|
|
1175
|
+
type: "webauthn.create",
|
|
1176
|
+
challenge: reg.data.challenge,
|
|
1177
|
+
origin: "http://localhost",
|
|
1178
|
+
rpId: reg.data.rpId,
|
|
1179
|
+
privateKey: keys.privateKey,
|
|
1180
|
+
signCount: 0,
|
|
1181
|
+
});
|
|
1182
|
+
const credentialId = b64urlEncode(crypto.getRandomValues(new Uint8Array(16)));
|
|
1183
|
+
expect(
|
|
1184
|
+
(
|
|
1185
|
+
await app.fetch(
|
|
1186
|
+
jsonPost(
|
|
1187
|
+
"/auth/passkey/register",
|
|
1188
|
+
{
|
|
1189
|
+
credentialId,
|
|
1190
|
+
publicKey: keys.publicKeyB64,
|
|
1191
|
+
userId: reg.data.userId,
|
|
1192
|
+
challenge: reg.data.challenge,
|
|
1193
|
+
sessionId: reg.data.sessionId,
|
|
1194
|
+
...regCeremony,
|
|
1195
|
+
},
|
|
1196
|
+
{ authorization: `Bearer ${session.accessToken}` },
|
|
1197
|
+
),
|
|
1198
|
+
)
|
|
1199
|
+
).status,
|
|
1200
|
+
).toBe(200);
|
|
1201
|
+
|
|
1202
|
+
const authOpts = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1203
|
+
const auth = (await authOpts.json()) as { data: PasskeyAuthOpts };
|
|
1204
|
+
now += 5 * 60 * 1000 + 1;
|
|
1205
|
+
const ceremony = await buildCeremony({
|
|
1206
|
+
type: "webauthn.get",
|
|
1207
|
+
challenge: auth.data.challenge,
|
|
1208
|
+
origin: "http://localhost",
|
|
1209
|
+
rpId: auth.data.rpId,
|
|
1210
|
+
privateKey: keys.privateKey,
|
|
1211
|
+
signCount: 1,
|
|
1212
|
+
});
|
|
1213
|
+
const res = await app.fetch(
|
|
1214
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1215
|
+
credentialId,
|
|
1216
|
+
challenge: auth.data.challenge,
|
|
1217
|
+
sessionId: auth.data.sessionId,
|
|
1218
|
+
...ceremony,
|
|
1219
|
+
}),
|
|
1220
|
+
);
|
|
1221
|
+
expect((await readError(res)).reason).toBe("invalid_credentials");
|
|
1222
|
+
|
|
1223
|
+
await app.stop();
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test("non-increasing signCount deletes credential and returns reregister_required", async () => {
|
|
1227
|
+
const passkeys = createPasskeyStore();
|
|
1228
|
+
const app = oke({
|
|
1229
|
+
name: `auth-sec-clone-${crypto.randomUUID()}`,
|
|
1230
|
+
env: "test",
|
|
1231
|
+
registry: "ignore",
|
|
1232
|
+
gate: {
|
|
1233
|
+
auth: {
|
|
1234
|
+
secret: SECRET,
|
|
1235
|
+
emailAndPassword: { enabled: true },
|
|
1236
|
+
},
|
|
1237
|
+
unguardedHttp: "deny",
|
|
1238
|
+
},
|
|
1239
|
+
})
|
|
1240
|
+
.plug(username())
|
|
1241
|
+
.plug(passkey({ origins: ["http://localhost"], passkeys }));
|
|
1242
|
+
await app.boot({ env: "test" });
|
|
1243
|
+
|
|
1244
|
+
const session = await mintUsernameSession(app, "pk_clone");
|
|
1245
|
+
const keys = await generatePasskeyKeypair();
|
|
1246
|
+
const { credentialId } = await registerPasskey(app, session.accessToken, keys);
|
|
1247
|
+
|
|
1248
|
+
const authOpts1 = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1249
|
+
const auth1 = (await authOpts1.json()) as { data: PasskeyAuthOpts };
|
|
1250
|
+
const c1 = await buildCeremony({
|
|
1251
|
+
type: "webauthn.get",
|
|
1252
|
+
challenge: auth1.data.challenge,
|
|
1253
|
+
origin: "http://localhost",
|
|
1254
|
+
rpId: auth1.data.rpId,
|
|
1255
|
+
privateKey: keys.privateKey,
|
|
1256
|
+
signCount: 1,
|
|
1257
|
+
});
|
|
1258
|
+
expect(
|
|
1259
|
+
(
|
|
1260
|
+
await app.fetch(
|
|
1261
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1262
|
+
credentialId,
|
|
1263
|
+
challenge: auth1.data.challenge,
|
|
1264
|
+
sessionId: auth1.data.sessionId,
|
|
1265
|
+
...c1,
|
|
1266
|
+
}),
|
|
1267
|
+
)
|
|
1268
|
+
).status,
|
|
1269
|
+
).toBe(200);
|
|
1270
|
+
expect(passkeys.byCredentialId.get(credentialId)?.counter).toBe(1);
|
|
1271
|
+
|
|
1272
|
+
const authOpts2 = await app.fetch(jsonPost("/auth/passkey/authenticate/options", {}));
|
|
1273
|
+
const auth2 = (await authOpts2.json()) as { data: PasskeyAuthOpts };
|
|
1274
|
+
const c2 = await buildCeremony({
|
|
1275
|
+
type: "webauthn.get",
|
|
1276
|
+
challenge: auth2.data.challenge,
|
|
1277
|
+
origin: "http://localhost",
|
|
1278
|
+
rpId: auth2.data.rpId,
|
|
1279
|
+
privateKey: keys.privateKey,
|
|
1280
|
+
signCount: 1,
|
|
1281
|
+
});
|
|
1282
|
+
const clone = await app.fetch(
|
|
1283
|
+
jsonPost("/auth/passkey/authenticate", {
|
|
1284
|
+
credentialId,
|
|
1285
|
+
challenge: auth2.data.challenge,
|
|
1286
|
+
sessionId: auth2.data.sessionId,
|
|
1287
|
+
...c2,
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
1290
|
+
expect((await readError(clone)).reason).toBe("reregister_required");
|
|
1291
|
+
expect(passkeys.byCredentialId.has(credentialId)).toBe(false);
|
|
1292
|
+
|
|
1293
|
+
await app.stop();
|
|
1294
|
+
});
|
|
1295
|
+
});
|