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.
Files changed (512) hide show
  1. package/AGENTS.md +18 -10
  2. package/README.md +2 -2
  3. package/package.json +51 -50
  4. package/site/content/docs/ai/index.mdx +25 -6
  5. package/site/content/docs/ai/llms-txt.mdx +1 -1
  6. package/site/content/docs/ai/meta.json +1 -1
  7. package/site/content/docs/ai/skills.mdx +4 -1
  8. package/site/content/docs/ai/try-it.mdx +58 -0
  9. package/site/content/docs/client/auth.mdx +208 -0
  10. package/site/content/docs/client/calling.mdx +451 -0
  11. package/site/content/docs/client/index.mdx +209 -0
  12. package/site/content/docs/client/live.mdx +234 -0
  13. package/site/content/docs/client/meta.json +5 -0
  14. package/site/content/docs/client/react.mdx +249 -0
  15. package/site/content/docs/elements/ai/agents.mdx +232 -0
  16. package/site/content/docs/elements/ai/index.mdx +341 -0
  17. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  18. package/site/content/docs/elements/ai/meta.json +5 -0
  19. package/site/content/docs/elements/ai/models.mdx +283 -0
  20. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  21. package/site/content/docs/elements/channel/email.mdx +266 -0
  22. package/site/content/docs/elements/channel/index.mdx +344 -0
  23. package/site/content/docs/elements/channel/meta.json +5 -0
  24. package/site/content/docs/elements/channel/push.mdx +218 -0
  25. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  26. package/site/content/docs/elements/channel/sms.mdx +264 -0
  27. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  28. package/site/content/docs/elements/clock/index.mdx +300 -0
  29. package/site/content/docs/elements/clock/meta.json +5 -0
  30. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  31. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  32. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  33. package/site/content/docs/elements/flow/http.mdx +1080 -0
  34. package/site/content/docs/elements/flow/index.mdx +618 -0
  35. package/site/content/docs/elements/flow/meta.json +5 -0
  36. package/site/content/docs/elements/flow/routing.mdx +559 -0
  37. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  38. package/site/content/docs/elements/gate/auth.mdx +371 -0
  39. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  40. package/site/content/docs/elements/gate/index.mdx +418 -0
  41. package/site/content/docs/elements/gate/meta.json +5 -0
  42. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  43. package/site/content/docs/elements/gate/rls.mdx +303 -0
  44. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  45. package/site/content/docs/elements/index.mdx +52 -11
  46. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  47. package/site/content/docs/elements/signal/index.mdx +352 -0
  48. package/site/content/docs/elements/signal/live.mdx +590 -0
  49. package/site/content/docs/elements/signal/meta.json +5 -0
  50. package/site/content/docs/elements/signal/once.mdx +596 -0
  51. package/site/content/docs/elements/store/files.mdx +659 -0
  52. package/site/content/docs/elements/store/index.mdx +310 -0
  53. package/site/content/docs/elements/store/kv.mdx +629 -0
  54. package/site/content/docs/elements/store/meta.json +5 -0
  55. package/site/content/docs/elements/store/search.mdx +946 -0
  56. package/site/content/docs/elements/store/sql.mdx +937 -0
  57. package/site/content/docs/elements/vault/config.mdx +256 -0
  58. package/site/content/docs/elements/vault/index.mdx +356 -0
  59. package/site/content/docs/elements/vault/meta.json +5 -0
  60. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  61. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  62. package/site/content/docs/index.mdx +78 -30
  63. package/site/content/docs/meta.json +11 -8
  64. package/site/content/docs/plugins/cors.mdx +2 -1
  65. package/site/content/docs/plugins/csrf.mdx +4 -1
  66. package/site/content/docs/plugins/headers.mdx +2 -1
  67. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  68. package/site/content/docs/plugins/magic-link.mdx +6 -0
  69. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  70. package/site/content/docs/plugins/oauth.mdx +1 -1
  71. package/site/content/docs/plugins/otp.mdx +12 -1
  72. package/site/content/docs/plugins/passkey.mdx +39 -15
  73. package/site/content/docs/plugins/two-factor.mdx +91 -34
  74. package/site/content/docs/plugins/username.mdx +1 -1
  75. package/site/content/docs/recipes/caddy.mdx +3 -3
  76. package/site/content/docs/recipes/index.mdx +18 -28
  77. package/site/content/docs/recipes/mailpit.mdx +1 -1
  78. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  79. package/site/content/docs/recipes/meta.json +1 -4
  80. package/site/content/docs/recipes/nginx.mdx +2 -2
  81. package/site/content/docs/recipes/openrouter.mdx +283 -0
  82. package/site/content/docs/recipes/pgdog.mdx +3 -3
  83. package/site/content/docs/recipes/postgres.mdx +1 -2
  84. package/site/content/docs/recipes/rustfs.mdx +2 -2
  85. package/site/content/docs/recipes/traefik.mdx +3 -3
  86. package/site/content/docs/reference/cli.mdx +231 -0
  87. package/site/content/docs/reference/configuration.mdx +30 -10
  88. package/site/content/docs/reference/environment-variables.mdx +93 -20
  89. package/site/content/docs/reference/errors.mdx +132 -29
  90. package/site/content/docs/reference/fx.mdx +137 -73
  91. package/site/content/docs/reference/index.mdx +13 -21
  92. package/site/content/docs/reference/meta.json +4 -5
  93. package/site/content/docs/reference/okid.mdx +42 -17
  94. package/site/content/docs/reference/plugins.mdx +4 -3
  95. package/site/content/docs/reference/security.mdx +197 -0
  96. package/site/content/docs/understand/meta.json +5 -0
  97. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  98. package/site/content/docs/understand/the-model.mdx +32 -0
  99. package/site/content/docs/understand/the-problem.mdx +74 -0
  100. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  101. package/src/auth/api-keys.ts +2 -1
  102. package/src/auth/bindings.ts +64 -16
  103. package/src/auth/gate-auth.test.ts +6 -1
  104. package/src/auth/identity.ts +152 -5
  105. package/src/auth/index.ts +29 -0
  106. package/src/auth/invites.ts +2 -1
  107. package/src/auth/method-context.ts +41 -0
  108. package/src/auth/oauth-as/crypto.ts +2 -1
  109. package/src/auth/oauth-as/stores.ts +2 -1
  110. package/src/auth/operator.ts +2 -1
  111. package/src/auth/sessions-jwt.test.ts +77 -0
  112. package/src/auth/sessions.ts +20 -4
  113. package/src/auth/tenants.ts +3 -2
  114. package/src/auth/two-factor-challenge.test.ts +61 -0
  115. package/src/auth/two-factor-challenge.ts +284 -0
  116. package/src/auth/verification.ts +5 -0
  117. package/src/bench/REPORT.md +49 -0
  118. package/src/bench/g03-signal-once.bench.ts +1 -1
  119. package/src/bench/g10-observability-contention.bench.ts +1 -1
  120. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  121. package/src/bench/load-app.ts +2 -10
  122. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  123. package/src/cli/ai-setup/apply.ts +294 -52
  124. package/src/cli/ai-setup/catalog.ts +238 -1342
  125. package/src/cli/ai-setup/index.ts +14 -99
  126. package/src/cli/ai-setup/prompts.ts +60 -443
  127. package/src/cli/ask-seed.test.ts +96 -0
  128. package/src/cli/ask-seed.ts +82 -0
  129. package/src/cli/ask-vault-gaps.test.ts +84 -0
  130. package/src/cli/ask-vault-gaps.ts +146 -0
  131. package/src/cli/build.ts +2 -2
  132. package/src/cli/client-add.test.ts +26 -1
  133. package/src/cli/client-add.ts +106 -27
  134. package/src/cli/competitor-mention-removal.test.ts +1 -1
  135. package/src/cli/db-seed.ts +2 -0
  136. package/src/cli/db.ts +150 -11
  137. package/src/cli/dev.ts +119 -211
  138. package/src/cli/docker-clean.ts +2 -2
  139. package/src/cli/docker-cli.test.ts +1 -1
  140. package/src/cli/doctor-diff.ts +4 -2
  141. package/src/cli/doctor-pii.test.ts +1 -1
  142. package/src/cli/doctor.ts +14 -1
  143. package/src/cli/hero-meta.test.ts +4 -2
  144. package/src/cli/load-config.images.test.ts +8 -10
  145. package/src/cli/load-config.ts +1 -1
  146. package/src/cli/registry.ts +22 -10
  147. package/src/cli/replay.ts +3 -1
  148. package/src/cli/start.ts +1 -1
  149. package/src/cli/tui/keys.ts +2 -3
  150. package/src/client/auth/cookies.ts +74 -0
  151. package/src/client/auth/create-auth-client.ts +636 -0
  152. package/src/client/auth/denials.ts +99 -0
  153. package/src/client/auth/session.ts +283 -0
  154. package/src/client/auth.test.ts +251 -0
  155. package/src/client/auth.ts +39 -114
  156. package/src/client/create-with-session.ts +245 -0
  157. package/src/client/create.ts +51 -5
  158. package/src/client/index.ts +11 -1
  159. package/src/client/live.ts +11 -79
  160. package/src/client/notes-contract.test.ts +5 -10
  161. package/src/client/sse.ts +137 -0
  162. package/src/client/stream.ts +147 -0
  163. package/src/client/transport.test.ts +32 -0
  164. package/src/client/transport.ts +123 -26
  165. package/src/client/types.ts +118 -14
  166. package/src/client-react/index.ts +185 -24
  167. package/src/client-react/use-live-query.ts +13 -3
  168. package/src/compiler/aot.test.ts +7 -4
  169. package/src/compiler/effects-embed.test.ts +56 -0
  170. package/src/compiler/effects-fetch.test.ts +43 -0
  171. package/src/compiler/effects-infer.ts +58 -2
  172. package/src/compiler/extract.test.ts +258 -44
  173. package/src/compiler/extract.ts +566 -48
  174. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  175. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  176. package/src/compiler/fixtures/skyport.expected.json +4 -4
  177. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  178. package/src/compiler/response.ts +2 -2
  179. package/src/compiler/search-writer-isolation.test.ts +40 -0
  180. package/src/config/index.ts +11 -0
  181. package/src/console/server/app.ts +17 -23
  182. package/src/console/server/bind.ts +4 -0
  183. package/src/console/server/console.test.ts +2 -2
  184. package/src/console/server/flows-invoke.test.ts +50 -34
  185. package/src/console/server/flows.ts +436 -255
  186. package/src/console/server/runs-ingest.test.ts +6 -3
  187. package/src/console/server/serve.ts +1 -1
  188. package/src/console/server/signals.test.ts +1 -5
  189. package/src/console/server/signals.ts +8 -6
  190. package/src/console/server/state.ts +6 -1
  191. package/src/console/server/store.ts +1 -1
  192. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  193. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  194. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  195. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  196. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  197. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  198. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  199. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  200. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  201. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  202. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  203. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  204. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  205. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  206. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  207. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  208. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  209. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  210. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  211. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  212. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  213. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  214. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  215. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  216. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  217. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  218. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  219. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  220. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  221. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  222. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  223. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  224. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  225. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  226. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  227. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  228. package/src/console/ui-next/dist/index.html +6 -13
  229. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  230. package/src/console/ui-next/src/client.ts +7 -2
  231. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  232. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  233. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  235. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  236. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  237. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  238. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  239. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  240. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  241. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  242. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  243. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  244. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  245. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  246. package/src/docker/ai-model-status.test.ts +1 -12
  247. package/src/docker/ai-model-status.ts +10 -38
  248. package/src/docker/compose-up.test.ts +100 -0
  249. package/src/docker/compose-up.ts +292 -0
  250. package/src/docker/compose.ts +4 -14
  251. package/src/docker/derive.ts +9 -34
  252. package/src/docker/docker.test.ts +16 -261
  253. package/src/docker/images-config.test.ts +13 -25
  254. package/src/docker/index.ts +1 -24
  255. package/src/docker/recipes/index.ts +0 -23
  256. package/src/docker/recipes/pgdog.ts +6 -4
  257. package/src/docker/stack-id.test.ts +2 -2
  258. package/src/docker/stack-id.ts +0 -1
  259. package/src/docker/types.ts +2 -2
  260. package/src/drivers/ai-anthropic.ts +10 -0
  261. package/src/drivers/ai-openai-compatible.ts +34 -7
  262. package/src/drivers/ai-providers.test.ts +0 -107
  263. package/src/drivers/ai-stream.test.ts +0 -34
  264. package/src/drivers/ai-types.ts +17 -8
  265. package/src/drivers/channel-fcm.ts +6 -1
  266. package/src/drivers/channel-msegat.ts +6 -1
  267. package/src/drivers/channel-resend.ts +5 -1
  268. package/src/drivers/channel-smtp.ts +5 -1
  269. package/src/drivers/channel-sndr.ts +10 -1
  270. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  271. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  272. package/src/drivers/channel-taqnyat.ts +6 -1
  273. package/src/drivers/channel-types.ts +10 -0
  274. package/src/drivers/channel-unifonic.ts +6 -1
  275. package/src/drivers/channel-wa-cloud.ts +6 -1
  276. package/src/drivers/channel-webpush.ts +6 -1
  277. package/src/drivers/external.ts +33 -0
  278. package/src/drivers/index.ts +0 -10
  279. package/src/drivers/meilisearch.ts +6 -0
  280. package/src/drivers/oauth-types.ts +4 -0
  281. package/src/drivers/postgres.test.ts +74 -3
  282. package/src/drivers/postgres.ts +175 -6
  283. package/src/drivers/signal-types.ts +5 -5
  284. package/src/drivers/types.ts +7 -0
  285. package/src/drivers/vault-types.ts +5 -0
  286. package/src/elements/ai/declare.ts +17 -4
  287. package/src/elements/ai/eval.ts +1 -1
  288. package/src/elements/ai/mcp-http.ts +1 -1
  289. package/src/elements/ai/pii.ts +1 -1
  290. package/src/elements/ai/providers.test.ts +289 -0
  291. package/src/elements/ai/providers.ts +169 -0
  292. package/src/elements/ai/runtime.ts +68 -5
  293. package/src/elements/ai/schema.ts +2 -2
  294. package/src/elements/ai.test.ts +7 -7
  295. package/src/elements/ai.ts +17 -1
  296. package/src/elements/channel/runtime.ts +105 -12
  297. package/src/elements/clock/cron-fields.test.ts +144 -0
  298. package/src/elements/clock/cron-fields.ts +185 -0
  299. package/src/elements/clock/declare.ts +247 -3
  300. package/src/elements/clock.ts +21 -1
  301. package/src/elements/index.ts +15 -0
  302. package/src/elements/signal/chaos-child.ts +1 -2
  303. package/src/elements/signal/declare.ts +65 -37
  304. package/src/elements/signal/delivery-modes.test.ts +11 -31
  305. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  306. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  307. package/src/elements/signal/key-ordering.test.ts +6 -26
  308. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  309. package/src/elements/signal/optional-emit.test.ts +6 -9
  310. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  311. package/src/elements/signal/orphan-messages.test.ts +4 -15
  312. package/src/elements/signal/reconcile.test.ts +2 -2
  313. package/src/elements/signal/runtime.ts +1 -1
  314. package/src/elements/signal/schema-emit.test.ts +4 -8
  315. package/src/elements/signal.test.ts +24 -49
  316. package/src/elements/signal.ts +9 -2
  317. package/src/elements/store/domain-ddl.test.ts +1 -1
  318. package/src/elements/store/live-http.test.ts +9 -4
  319. package/src/elements/store/prepare-row.test.ts +123 -0
  320. package/src/elements/store/resource.ts +43 -12
  321. package/src/elements/store/schema-decl.ts +117 -0
  322. package/src/elements/store/search-backfill.ts +205 -0
  323. package/src/elements/store/search-bind.ts +66 -0
  324. package/src/elements/store/search-bm25.ts +78 -0
  325. package/src/elements/store/search-ddl.ts +156 -0
  326. package/src/elements/store/search-embed-flow.ts +141 -0
  327. package/src/elements/store/search-errors.ts +34 -0
  328. package/src/elements/store/search-fusion.ts +112 -0
  329. package/src/elements/store/search-lsh.ts +179 -0
  330. package/src/elements/store/search-runtime.ts +300 -0
  331. package/src/elements/store/search.test.ts +144 -0
  332. package/src/elements/store/sql-session.test.ts +1 -0
  333. package/src/elements/store/sql-session.ts +83 -3
  334. package/src/elements/store/table.ts +23 -2
  335. package/src/elements/store/upsert-app.test.ts +1 -3
  336. package/src/elements/store.ts +50 -0
  337. package/src/full.ts +4 -2
  338. package/src/http.ts +4 -1
  339. package/src/i18n/catalogs/ar.ts +14 -6
  340. package/src/i18n/catalogs/en.ts +14 -6
  341. package/src/index.ts +6 -1
  342. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  343. package/src/kernel/adopt-routes.ts +9 -1
  344. package/src/kernel/app-auth.ts +5 -0
  345. package/src/kernel/app.ts +102 -9
  346. package/src/kernel/auto-cache.test.ts +6 -12
  347. package/src/kernel/auto-registry.test.ts +3 -3
  348. package/src/kernel/boot-bind/ai.test.ts +22 -19
  349. package/src/kernel/boot-bind/ai.ts +5 -43
  350. package/src/kernel/boot-bind/clock.ts +23 -5
  351. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  352. package/src/kernel/boot.test.ts +11 -6
  353. package/src/kernel/boot.ts +66 -16
  354. package/src/kernel/boundary-contract.ts +93 -0
  355. package/src/kernel/budget.test.ts +1 -1
  356. package/src/kernel/call.ts +89 -0
  357. package/src/kernel/capability.ts +6 -0
  358. package/src/kernel/client-descriptor.ts +123 -0
  359. package/src/kernel/clock-timezone.test.ts +80 -0
  360. package/src/kernel/correlation.test.ts +1 -1
  361. package/src/kernel/dry-run.ts +11 -8
  362. package/src/kernel/effects-stamping.test.ts +42 -9
  363. package/src/kernel/effects.test.ts +4 -2
  364. package/src/kernel/effects.ts +51 -9
  365. package/src/kernel/errors-channel.ts +17 -0
  366. package/src/kernel/errors-live-resume.ts +3 -2
  367. package/src/kernel/errors-tenant.ts +7 -4
  368. package/src/kernel/errors.registry-helpers.ts +96 -0
  369. package/src/kernel/errors.registry.test.ts +139 -25
  370. package/src/kernel/errors.ts +102 -23
  371. package/src/kernel/external-effects.test.ts +196 -0
  372. package/src/kernel/flow.test.ts +13 -4
  373. package/src/kernel/flow.ts +69 -71
  374. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  375. package/src/kernel/fx-dead-letters.test.ts +3 -3
  376. package/src/kernel/fx-fetch.ts +55 -0
  377. package/src/kernel/fx-live-stream.ts +2 -2
  378. package/src/kernel/fx-live.test.ts +8 -8
  379. package/src/kernel/fx.test.ts +17 -11
  380. package/src/kernel/fx.ts +180 -57
  381. package/src/kernel/horizontal-child.ts +1 -1
  382. package/src/kernel/http-query.test.ts +3 -6
  383. package/src/kernel/index.ts +13 -3
  384. package/src/kernel/instance-id.ts +1 -1
  385. package/src/kernel/instances.test.ts +40 -0
  386. package/src/kernel/instances.ts +47 -9
  387. package/src/kernel/live-http.test.ts +6 -7
  388. package/src/kernel/live-http.ts +6 -2
  389. package/src/kernel/live-resume.test.ts +1 -1
  390. package/src/kernel/mcp-tool.test.ts +3 -3
  391. package/src/kernel/on.ts +98 -4
  392. package/src/kernel/pipeline.test.ts +12 -15
  393. package/src/kernel/plugin-elements.test.ts +1 -1
  394. package/src/kernel/stamp-http.test.ts +3 -3
  395. package/src/kernel/triggers.ts +210 -95
  396. package/src/kernel-entry.ts +0 -1
  397. package/src/manifest/diff.ts +11 -1
  398. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  399. package/src/manifest/types.ts +46 -2
  400. package/src/mcp/docs-index.ts +3 -3
  401. package/src/mcp/docs-mcp.test.ts +2 -2
  402. package/src/mcp/docs-tools.ts +1 -1
  403. package/src/okid.test.ts +56 -0
  404. package/src/okid.ts +55 -11
  405. package/src/plugins/anonymous.ts +8 -4
  406. package/src/plugins/auth/shared.ts +27 -3
  407. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  408. package/src/plugins/auth-methods.security.test.ts +561 -23
  409. package/src/plugins/config-source.ts +1 -1
  410. package/src/plugins/csrf.test.ts +65 -0
  411. package/src/plugins/index.ts +2 -0
  412. package/src/plugins/magic-link.ts +36 -16
  413. package/src/plugins/oauth/shared.ts +3 -1
  414. package/src/plugins/oauth.ts +22 -14
  415. package/src/plugins/otp.ts +37 -21
  416. package/src/plugins/passkey-webauthn.ts +9 -6
  417. package/src/plugins/passkey.ts +98 -26
  418. package/src/plugins/pre-account-hijack.test.ts +223 -0
  419. package/src/plugins/two-factor.ts +466 -37
  420. package/src/plugins/username.ts +19 -7
  421. package/src/release/build-lib.ts +1 -0
  422. package/src/release/limits.ts +2 -2
  423. package/src/release/measure.ts +1 -1
  424. package/src/runs/collect.test.ts +0 -2
  425. package/src/runtime/json-code-block.test.ts +250 -20
  426. package/src/runtime/json-code-block.ts +1261 -41
  427. package/src/term.test.ts +1 -1
  428. package/src/test/create-test-app.test.ts +12 -12
  429. package/src/test/live-signals.test.ts +6 -6
  430. package/src/test/provisions.integration.test.ts +10 -14
  431. package/src/test/reset-element-registries.ts +1 -1
  432. package/src/test/tenant-isolation.test.ts +12 -6
  433. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  434. package/site/content/docs/deployment/docker.mdx +0 -227
  435. package/site/content/docs/deployment/index.mdx +0 -83
  436. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  437. package/site/content/docs/deployment/meta.json +0 -5
  438. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  439. package/site/content/docs/elements/ai.mdx +0 -385
  440. package/site/content/docs/elements/channel.mdx +0 -346
  441. package/site/content/docs/elements/clock.mdx +0 -244
  442. package/site/content/docs/elements/flow.mdx +0 -420
  443. package/site/content/docs/elements/gate.mdx +0 -436
  444. package/site/content/docs/elements/signal.mdx +0 -380
  445. package/site/content/docs/elements/store.mdx +0 -1099
  446. package/site/content/docs/elements/vault.mdx +0 -405
  447. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  448. package/site/content/docs/get-started/index.mdx +0 -43
  449. package/site/content/docs/get-started/installation.mdx +0 -220
  450. package/site/content/docs/get-started/introduction.mdx +0 -132
  451. package/site/content/docs/get-started/meta.json +0 -13
  452. package/site/content/docs/get-started/project-structure.mdx +0 -925
  453. package/site/content/docs/get-started/testing.mdx +0 -328
  454. package/site/content/docs/get-started/why.mdx +0 -162
  455. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  456. package/site/content/docs/recipes/ollama.mdx +0 -142
  457. package/site/content/docs/recipes/sglang.mdx +0 -105
  458. package/site/content/docs/recipes/vllm.mdx +0 -106
  459. package/site/content/docs/reference/cli.md +0 -232
  460. package/site/content/docs/reference/client.mdx +0 -469
  461. package/site/content/docs/reference/security.md +0 -74
  462. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  463. package/src/cli/ai-setup/recommend.test.ts +0 -225
  464. package/src/cli/ai-setup/recommend.ts +0 -225
  465. package/src/cli/dev-controls.test.ts +0 -122
  466. package/src/cli/dev-controls.ts +0 -164
  467. package/src/cli/tui/DevLive.tsx +0 -126
  468. package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
  469. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  470. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  471. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  472. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  473. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  474. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  475. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  476. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  477. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  478. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  479. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  480. package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
  481. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  482. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  483. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  484. package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
  485. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  486. package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
  487. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  488. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  489. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  490. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  491. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  492. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  493. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  494. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  495. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  496. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  497. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  498. package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
  499. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  500. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  501. package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
  502. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  503. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  504. package/src/docker/ollama-pull.ts +0 -232
  505. package/src/docker/recipes/llama-cpp.ts +0 -298
  506. package/src/docker/recipes/ollama.ts +0 -44
  507. package/src/docker/recipes/sglang.ts +0 -55
  508. package/src/docker/recipes/vllm.ts +0 -44
  509. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  510. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  511. package/src/drivers/ai-ollama.ts +0 -389
  512. 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", { userId: session.userId, code }),
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", { userId: session.userId, code }),
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: { challenge: string; rpId: string } };
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: { challenge: string; userId: string } };
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", { userId: session.userId, code }),
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", { userId: session.userId, code: wrong }),
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: { challenge: string; rpId: string } };
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: { challenge: string; rpId: string } };
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: { challenge: string; rpId: string } };
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
+ });