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
@@ -2,8 +2,9 @@
2
2
  * Passkey (WebAuthn) Gate auth method plugin.
3
3
  *
4
4
  * Registration and authentication verify clientDataJSON origin + challenge,
5
- * authenticatorData rpId hash, and ECDSA P-256 signature against the stored
6
- * SPKI public key. Presence-only authenticate is rejected.
5
+ * authenticatorData rpId hash + UV, and ECDSA P-256 signature against the
6
+ * stored SPKI public key. Challenges are bound to a ceremony sessionId (≤5m).
7
+ * UV=false and cloned-authenticator counters are rejected.
7
8
  */
8
9
 
9
10
  import { constantTimeEqual } from "../auth/constant-time.ts";
@@ -30,6 +31,7 @@ import {
30
31
  type AuthMethodOptions,
31
32
  } from "./auth/shared.ts";
32
33
  import { verifyWebAuthnCeremony } from "./passkey-webauthn.ts";
34
+ import { okid } from "../okid.ts";
33
35
 
34
36
  /** Stored passkey credential. */
35
37
  export interface PasskeyCredential {
@@ -72,11 +74,40 @@ const CeremonyIn = z.object({
72
74
  publicKey: z.string().min(1).optional(),
73
75
  userId: z.string().min(1).optional(),
74
76
  challenge: z.string().min(1),
77
+ /** Opaque ceremony session id from options (challenge binding). */
78
+ sessionId: z.string().min(1),
75
79
  clientDataJSON: z.string().min(1),
76
80
  authenticatorData: z.string().min(1),
77
81
  signature: z.string().min(1),
78
82
  });
79
83
 
84
+ /**
85
+ * Map WebAuthn verify failure reasons onto AuthFailed reasons.
86
+ *
87
+ * @param reason - Verifier reason
88
+ */
89
+ function authFailedReason(reason: string): string {
90
+ if (reason === "invalid_origin") return "invalid_origin";
91
+ if (reason === "user_not_verified") return "user_not_verified";
92
+ if (reason === "cloned_authenticator") return "reregister_required";
93
+ return "invalid_credentials";
94
+ }
95
+
96
+ /**
97
+ * Remove a credential from both indexes (clone / revoke).
98
+ *
99
+ * @param store - Passkey store
100
+ * @param cred - Credential to drop
101
+ */
102
+ function removeCredential(store: PasskeyStore, cred: PasskeyCredential): void {
103
+ store.byCredentialId.delete(cred.credentialId);
104
+ const list = store.byUserId.get(cred.userId);
105
+ if (!list) return;
106
+ const next = list.filter((c) => c.credentialId !== cred.credentialId);
107
+ if (next.length === 0) store.byUserId.delete(cred.userId);
108
+ else store.byUserId.set(cred.userId, next);
109
+ }
110
+
80
111
  /**
81
112
  * Passkey register / authenticate with cryptographic WebAuthn verify (`oke_passkeys`).
82
113
  *
@@ -90,40 +121,49 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
90
121
  const rpId = opts.rpId ?? "localhost";
91
122
  const origins = opts.origins ?? ["http://localhost", "https://localhost"];
92
123
 
93
- const registerOptions = flow("auth.passkeyRegisterOptions", {
94
- plane: "user",
124
+ const registerOptionsContract = {
95
125
  out: z.object({
96
126
  challenge: z.string(),
127
+ sessionId: z.string(),
97
128
  rpId: z.string(),
98
129
  userId: z.string(),
99
130
  }),
100
131
  errors: { AuthFailed },
132
+ };
133
+
134
+ const registerOptions = flow("auth.passkeyRegisterOptions", {
135
+ plane: "user",
101
136
  do: async (_input, fx) => {
102
137
  const userId = fx.auth.userId;
103
138
  if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
104
- const challenge = crypto.randomUUID().replace(/-/g, "");
139
+ const challenge = okid();
140
+ const sessionId = okid();
105
141
  const now = runtime.now();
106
142
  putVerification(challenges, {
107
- id: crypto.randomUUID(),
143
+ id: okid(),
108
144
  identifier: `passkey-reg:${userId}`,
109
145
  value: await hashChallenge(challenge),
110
146
  expiresAt: now + 5 * 60 * 1000,
111
147
  createdAt: now,
112
148
  consumedAt: null,
113
149
  attempts: 0,
150
+ sessionId,
114
151
  });
115
- return { challenge, rpId, userId };
152
+ return { challenge, sessionId, rpId, userId };
116
153
  },
117
154
  });
118
155
 
119
- const register = flow("auth.passkeyRegister", {
120
- plane: "user",
156
+ const registerContract = {
121
157
  in: CeremonyIn.extend({
122
158
  publicKey: z.string().min(1),
123
159
  userId: z.string().min(1),
124
160
  }),
125
161
  out: z.object({ ok: z.literal(true) }),
126
162
  errors: { AuthFailed },
163
+ };
164
+
165
+ const register = flow("auth.passkeyRegister", {
166
+ plane: "user",
127
167
  do: async (input, fx) => {
128
168
  const sessionUser = fx.auth.userId;
129
169
  if (!sessionUser || sessionUser !== input.userId) {
@@ -133,6 +173,7 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
133
173
  challenges,
134
174
  `passkey-reg:${input.userId}`,
135
175
  input.challenge,
176
+ input.sessionId,
136
177
  runtime.now(),
137
178
  );
138
179
  if (!consumed) return fail("AuthFailed", { reason: "invalid_credentials" });
@@ -148,9 +189,7 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
148
189
  signature: input.signature,
149
190
  });
150
191
  if (!verified.ok) {
151
- return fail("AuthFailed", {
152
- reason: verified.reason === "invalid_origin" ? "invalid_origin" : "invalid_credentials",
153
- });
192
+ return fail("AuthFailed", { reason: authFailedReason(verified.reason) });
154
193
  }
155
194
  if (passkeys.byCredentialId.has(input.credentialId)) {
156
195
  return fail("AuthFailed", { reason: "invalid_credentials" });
@@ -186,34 +225,39 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
186
225
  },
187
226
  });
188
227
 
189
- const authenticateOptions = flow("auth.passkeyAuthenticateOptions", {
190
- plane: "user",
228
+ const authenticateOptionsContract = {
191
229
  in: z.object({ email: z.string().optional() }),
192
230
  out: z.object({
193
231
  challenge: z.string(),
232
+ sessionId: z.string(),
194
233
  rpId: z.string(),
195
234
  allowCredentials: z.array(z.string()),
196
235
  }),
197
236
  errors: { AuthFailed, AuthRateLimited },
237
+ };
238
+
239
+ const authenticateOptions = flow("auth.passkeyAuthenticateOptions", {
240
+ plane: "user",
198
241
  do: async (input) => {
199
- const challenge = crypto.randomUUID().replace(/-/g, "");
242
+ const challenge = okid();
243
+ const sessionId = okid();
200
244
  const now = runtime.now();
201
245
  const key = input.email?.trim().toLowerCase() || "anonymous";
202
246
  putVerification(challenges, {
203
- id: crypto.randomUUID(),
247
+ id: okid(),
204
248
  identifier: `passkey-auth:${key}`,
205
249
  value: await hashChallenge(challenge),
206
250
  expiresAt: now + 5 * 60 * 1000,
207
251
  createdAt: now,
208
252
  consumedAt: null,
209
253
  attempts: 0,
254
+ sessionId,
210
255
  });
211
- return { challenge, rpId, allowCredentials: [] as string[] };
256
+ return { challenge, sessionId, rpId, allowCredentials: [] as string[] };
212
257
  },
213
258
  });
214
259
 
215
- const authenticate = flow("auth.passkeyAuthenticate", {
216
- plane: "user",
260
+ const authenticateContract = {
217
261
  in: CeremonyIn.extend({
218
262
  challenge: z.string().min(1),
219
263
  /** Challenge bucket key from authenticate options (default `anonymous`). */
@@ -221,6 +265,10 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
221
265
  }),
222
266
  out: SessionTokensOut,
223
267
  errors: { AuthFailed, AuthRateLimited },
268
+ };
269
+
270
+ const authenticate = flow("auth.passkeyAuthenticate", {
271
+ plane: "user",
224
272
  do: async (input) => {
225
273
  const cred = passkeys.byCredentialId.get(input.credentialId);
226
274
  if (!cred) return fail("AuthFailed", { reason: "invalid_credentials" });
@@ -230,6 +278,7 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
230
278
  challenges,
231
279
  `passkey-auth:${bucket}`,
232
280
  input.challenge,
281
+ input.sessionId,
233
282
  runtime.now(),
234
283
  );
235
284
  if (!consumed) return fail("AuthFailed", { reason: "invalid_credentials" });
@@ -246,9 +295,14 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
246
295
  previousSignCount: cred.counter,
247
296
  });
248
297
  if (!verified.ok) {
249
- return fail("AuthFailed", {
250
- reason: verified.reason === "invalid_origin" ? "invalid_origin" : "invalid_credentials",
251
- });
298
+ if (verified.reason === "cloned_authenticator") {
299
+ console.warn(
300
+ `[passkey] possible cloned authenticator: credentialId=${cred.credentialId} userId=${cred.userId}`,
301
+ );
302
+ removeCredential(passkeys, cred);
303
+ return fail("AuthFailed", { reason: "reregister_required" });
304
+ }
305
+ return fail("AuthFailed", { reason: authFailedReason(verified.reason) });
252
306
  }
253
307
  cred.counter = verified.signCount;
254
308
 
@@ -269,22 +323,40 @@ export function passkey(opts: PasskeyOptions = {}): PluginDef {
269
323
  return plugin("passkey", { version: "0.0.1", config: { method: "passkey" } })
270
324
  .needs("auth")
271
325
  .table("oke_passkeys", undefined, { plane: "user", description: "WebAuthn credentials" })
272
- .binding(bindSessionAuth("/passkey/register/options", registerOptions))
273
- .binding(bindSessionAuth("/passkey/register", register))
274
- .binding(bindPublicAuth("/passkey/authenticate/options", authenticateOptions, "otp"))
275
- .binding(bindPublicAuth("/passkey/authenticate", authenticate, "otp"));
326
+ .binding(bindSessionAuth("/passkey/register/options", registerOptions, registerOptionsContract))
327
+ .binding(bindSessionAuth("/passkey/register", register, registerContract))
328
+ .binding(
329
+ bindPublicAuth(
330
+ "/passkey/authenticate/options",
331
+ authenticateOptions,
332
+ "otp",
333
+ authenticateOptionsContract,
334
+ ),
335
+ )
336
+ .binding(bindPublicAuth("/passkey/authenticate", authenticate, "otp", authenticateContract));
276
337
  }
277
338
 
339
+ /**
340
+ * Consume a single-use challenge bound to a ceremony sessionId.
341
+ *
342
+ * @param store - Challenge store
343
+ * @param identifier - Bucket key
344
+ * @param challenge - Raw challenge
345
+ * @param sessionId - Ceremony session from options
346
+ * @param now - Clock
347
+ */
278
348
  async function consumeChallenge(
279
349
  store: VerificationStore,
280
350
  identifier: string,
281
351
  challenge: string,
352
+ sessionId: string,
282
353
  now: number,
283
354
  ): Promise<boolean> {
284
355
  const hash = await hashChallenge(challenge);
285
356
  for (const row of store.rows.values()) {
286
357
  if (row.identifier !== identifier) continue;
287
358
  if (row.consumedAt !== null || row.expiresAt <= now) continue;
359
+ if (row.sessionId !== sessionId) continue;
288
360
  if (!constantTimeEqual(row.value, hash)) continue;
289
361
  row.consumedAt = now;
290
362
  return true;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * CVE-2026-67327 — pre-account hijacking defense.
3
+ *
4
+ * Attacker parks an unverified email+password account; victim proves email via
5
+ * magic-link or OTP; planted password must be invalidated and attacker sessions
6
+ * revoked.
7
+ */
8
+
9
+ import { afterEach, describe, expect, test } from "bun:test";
10
+ import {
11
+ completeVerifiedEmailSignIn,
12
+ createIdentityStore,
13
+ createUserWithPassword,
14
+ type IdentityStore,
15
+ } from "../auth/identity.ts";
16
+ import { createSessionStore, issueSession } from "../auth/sessions.ts";
17
+ import { oke } from "../kernel/app.ts";
18
+ import { resetFlowSeq } from "../kernel/flow.ts";
19
+ import { resetBindings } from "../kernel/on.ts";
20
+ import { magicLink } from "./magic-link.ts";
21
+ import { otp } from "./otp.ts";
22
+
23
+ afterEach(() => {
24
+ resetBindings();
25
+ resetFlowSeq();
26
+ });
27
+
28
+ const SECRET = "test-secret-at-least-16";
29
+ const PLANTED_PASSWORD = "AttackerPlanted1!";
30
+
31
+ function jsonPost(path: string, body: unknown): Request {
32
+ return new Request(`http://localhost${path}`, {
33
+ method: "POST",
34
+ headers: { "content-type": "application/json" },
35
+ body: JSON.stringify(body),
36
+ });
37
+ }
38
+
39
+ function emailAuthApp(plug: "magic-link" | "otp") {
40
+ const app = oke({
41
+ name: `pre-account-${crypto.randomUUID()}`,
42
+ env: "test",
43
+ registry: "ignore",
44
+ gate: {
45
+ auth: {
46
+ secret: SECRET,
47
+ emailAndPassword: { enabled: true },
48
+ },
49
+ },
50
+ });
51
+ if (plug === "magic-link") return app.plug(magicLink({ exposeDevToken: true }));
52
+ return app.plug(otp({ mode: "app", channels: ["email"], exposeDevOtp: true }));
53
+ }
54
+
55
+ describe("completeVerifiedEmailSignIn — pre-account reclaim", () => {
56
+ test("reclaims unverified password account: clears hash, revokes sessions, marks verified", async () => {
57
+ const identities = createIdentityStore();
58
+ const sessions = createSessionStore();
59
+ const now = () => 5_000_000;
60
+
61
+ const planted = await createUserWithPassword(identities, {
62
+ email: "hijack@example.com",
63
+ password: PLANTED_PASSWORD,
64
+ now,
65
+ });
66
+ expect(planted.emailVerified).toBe(false);
67
+
68
+ const attackerSession = await issueSession(
69
+ sessions,
70
+ { secret: SECRET, now },
71
+ { id: planted.id, plane: "user", scopes: [] },
72
+ );
73
+ expect(attackerSession.session.revokedAt).toBeNull();
74
+
75
+ const result = await completeVerifiedEmailSignIn(identities, sessions, {
76
+ provider: "magic-link",
77
+ providerAccountId: "hijack@example.com",
78
+ email: "hijack@example.com",
79
+ now,
80
+ });
81
+
82
+ expect(result.user.id).toBe(planted.id);
83
+ expect(result.user.emailVerified).toBe(true);
84
+ expect(result.credentialsCompromised).toBe(true);
85
+ expect(result.created).toBe(true);
86
+
87
+ const credential = [...identities.accounts.values()].find(
88
+ (a) => a.provider === "credential" && a.userId === planted.id,
89
+ );
90
+ expect(credential?.passwordHash).toBeNull();
91
+ expect(attackerSession.session.revokedAt).toBe(5_000_000);
92
+ });
93
+
94
+ test("verified owner re-auth attaches passwordless without purging password", async () => {
95
+ const identities = createIdentityStore();
96
+ const sessions = createSessionStore();
97
+ const now = () => 6_000_000;
98
+
99
+ const owner = await createUserWithPassword(identities, {
100
+ email: "owner@example.com",
101
+ password: "LegitimatePass1!",
102
+ emailVerified: true,
103
+ now,
104
+ });
105
+ const beforeHash = [...identities.accounts.values()].find(
106
+ (a) => a.provider === "credential",
107
+ )?.passwordHash;
108
+ expect(beforeHash).toBeTruthy();
109
+
110
+ const result = await completeVerifiedEmailSignIn(identities, sessions, {
111
+ provider: "otp",
112
+ providerAccountId: "owner@example.com",
113
+ email: "owner@example.com",
114
+ now,
115
+ });
116
+
117
+ expect(result.user.id).toBe(owner.id);
118
+ expect(result.credentialsCompromised).toBe(false);
119
+ const afterHash = [...identities.accounts.values()].find(
120
+ (a) => a.provider === "credential",
121
+ )?.passwordHash;
122
+ expect(afterHash).toBe(beforeHash);
123
+ });
124
+ });
125
+
126
+ describe("CVE-2026-67327 — HTTP pre-account hijack", () => {
127
+ test("magic-link reclaim invalidates planted password", async () => {
128
+ const app = emailAuthApp("magic-link");
129
+ await app.boot({ env: "test" });
130
+ const identities = app.identities as IdentityStore;
131
+
132
+ const signUp = await app.fetch(
133
+ jsonPost("/auth/sign-up/email", {
134
+ email: "hijack@example.com",
135
+ password: PLANTED_PASSWORD,
136
+ }),
137
+ );
138
+ expect(signUp.status).toBe(200);
139
+ const planted = (await signUp.json()) as { data: { userId: string; refreshToken: string } };
140
+ expect(identities.users.get(planted.data.userId)?.emailVerified).toBe(false);
141
+
142
+ const req = await app.fetch(
143
+ jsonPost("/auth/magic-link/request", { email: "hijack@example.com" }),
144
+ );
145
+ expect(req.status).toBe(200);
146
+ const { data: challenge } = (await req.json()) as { data: { devToken: string } };
147
+
148
+ const verify = await app.fetch(
149
+ jsonPost("/auth/magic-link/verify", { token: challenge.devToken }),
150
+ );
151
+ expect(verify.status).toBe(200);
152
+ const session = (await verify.json()) as { data: { userId: string; accessToken: string } };
153
+ expect(session.data.userId).toBe(planted.data.userId);
154
+ expect(identities.users.get(planted.data.userId)?.emailVerified).toBe(true);
155
+
156
+ const credential = [...identities.accounts.values()].find(
157
+ (a) => a.provider === "credential" && a.userId === planted.data.userId,
158
+ );
159
+ expect(credential?.passwordHash).toBeNull();
160
+
161
+ const signIn = await app.fetch(
162
+ jsonPost("/auth/sign-in/email", {
163
+ email: "hijack@example.com",
164
+ password: PLANTED_PASSWORD,
165
+ }),
166
+ );
167
+ const err = (await signIn.json()) as { error: { data?: { reason?: string } } };
168
+ expect(err.error.data?.reason).toBe("invalid_credentials");
169
+
170
+ const refresh = await app.fetch(
171
+ jsonPost("/auth/refresh", { refreshToken: planted.data.refreshToken }),
172
+ );
173
+ expect(refresh.status).not.toBe(200);
174
+
175
+ await app.stop();
176
+ });
177
+
178
+ test("email OTP reclaim invalidates planted password", async () => {
179
+ const app = emailAuthApp("otp");
180
+ await app.boot({ env: "test" });
181
+ const identities = app.identities as IdentityStore;
182
+
183
+ const signUp = await app.fetch(
184
+ jsonPost("/auth/sign-up/email", {
185
+ email: "otp-hijack@example.com",
186
+ password: PLANTED_PASSWORD,
187
+ }),
188
+ );
189
+ expect(signUp.status).toBe(200);
190
+ const planted = (await signUp.json()) as { data: { userId: string } };
191
+
192
+ const req = await app.fetch(jsonPost("/auth/otp/request", { email: "otp-hijack@example.com" }));
193
+ expect(req.status).toBe(200);
194
+ const { data: challenge } = (await req.json()) as { data: { devOtp: string } };
195
+
196
+ const verify = await app.fetch(
197
+ jsonPost("/auth/otp/verify", {
198
+ email: "otp-hijack@example.com",
199
+ otp: challenge.devOtp,
200
+ }),
201
+ );
202
+ expect(verify.status).toBe(200);
203
+ const session = (await verify.json()) as { data: { userId: string } };
204
+ expect(session.data.userId).toBe(planted.data.userId);
205
+ expect(identities.users.get(planted.data.userId)?.emailVerified).toBe(true);
206
+
207
+ const credential = [...identities.accounts.values()].find(
208
+ (a) => a.provider === "credential" && a.userId === planted.data.userId,
209
+ );
210
+ expect(credential?.passwordHash).toBeNull();
211
+
212
+ const signIn = await app.fetch(
213
+ jsonPost("/auth/sign-in/email", {
214
+ email: "otp-hijack@example.com",
215
+ password: PLANTED_PASSWORD,
216
+ }),
217
+ );
218
+ const err = (await signIn.json()) as { error: { data?: { reason?: string } } };
219
+ expect(err.error.data?.reason).toBe("invalid_credentials");
220
+
221
+ await app.stop();
222
+ });
223
+ });