okengine 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (510) 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/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/meta.json +1 -1
  6. package/site/content/docs/ai/skills.mdx +4 -1
  7. package/site/content/docs/ai/try-it.mdx +58 -0
  8. package/site/content/docs/client/auth.mdx +208 -0
  9. package/site/content/docs/client/calling.mdx +451 -0
  10. package/site/content/docs/client/index.mdx +209 -0
  11. package/site/content/docs/client/live.mdx +234 -0
  12. package/site/content/docs/client/meta.json +5 -0
  13. package/site/content/docs/client/react.mdx +249 -0
  14. package/site/content/docs/elements/ai/agents.mdx +232 -0
  15. package/site/content/docs/elements/ai/index.mdx +341 -0
  16. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  17. package/site/content/docs/elements/ai/meta.json +5 -0
  18. package/site/content/docs/elements/ai/models.mdx +283 -0
  19. package/site/content/docs/elements/ai/prompts.mdx +256 -0
  20. package/site/content/docs/elements/channel/email.mdx +266 -0
  21. package/site/content/docs/elements/channel/index.mdx +344 -0
  22. package/site/content/docs/elements/channel/meta.json +5 -0
  23. package/site/content/docs/elements/channel/push.mdx +218 -0
  24. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  25. package/site/content/docs/elements/channel/sms.mdx +264 -0
  26. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  27. package/site/content/docs/elements/clock/index.mdx +300 -0
  28. package/site/content/docs/elements/clock/meta.json +5 -0
  29. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  30. package/site/content/docs/elements/clock/sleep.mdx +445 -0
  31. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  32. package/site/content/docs/elements/flow/http.mdx +1080 -0
  33. package/site/content/docs/elements/flow/index.mdx +618 -0
  34. package/site/content/docs/elements/flow/meta.json +5 -0
  35. package/site/content/docs/elements/flow/routing.mdx +559 -0
  36. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  37. package/site/content/docs/elements/gate/auth.mdx +371 -0
  38. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  39. package/site/content/docs/elements/gate/index.mdx +418 -0
  40. package/site/content/docs/elements/gate/meta.json +5 -0
  41. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  42. package/site/content/docs/elements/gate/rls.mdx +303 -0
  43. package/site/content/docs/elements/gate/tenancy.mdx +372 -0
  44. package/site/content/docs/elements/index.mdx +20 -10
  45. package/site/content/docs/elements/signal/broadcast.mdx +517 -0
  46. package/site/content/docs/elements/signal/index.mdx +352 -0
  47. package/site/content/docs/elements/signal/live.mdx +590 -0
  48. package/site/content/docs/elements/signal/meta.json +5 -0
  49. package/site/content/docs/elements/signal/once.mdx +596 -0
  50. package/site/content/docs/elements/store/files.mdx +659 -0
  51. package/site/content/docs/elements/store/index.mdx +310 -0
  52. package/site/content/docs/elements/store/kv.mdx +629 -0
  53. package/site/content/docs/elements/store/meta.json +5 -0
  54. package/site/content/docs/elements/store/search.mdx +946 -0
  55. package/site/content/docs/elements/store/sql.mdx +937 -0
  56. package/site/content/docs/elements/vault/config.mdx +256 -0
  57. package/site/content/docs/elements/vault/index.mdx +356 -0
  58. package/site/content/docs/elements/vault/meta.json +5 -0
  59. package/site/content/docs/elements/vault/rotation.mdx +273 -0
  60. package/site/content/docs/elements/vault/secrets.mdx +285 -0
  61. package/site/content/docs/index.mdx +63 -36
  62. package/site/content/docs/meta.json +8 -7
  63. package/site/content/docs/plugins/cors.mdx +2 -1
  64. package/site/content/docs/plugins/csrf.mdx +4 -1
  65. package/site/content/docs/plugins/headers.mdx +2 -1
  66. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  67. package/site/content/docs/plugins/magic-link.mdx +6 -0
  68. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  69. package/site/content/docs/plugins/oauth.mdx +1 -1
  70. package/site/content/docs/plugins/otp.mdx +12 -1
  71. package/site/content/docs/plugins/passkey.mdx +39 -15
  72. package/site/content/docs/plugins/two-factor.mdx +91 -34
  73. package/site/content/docs/plugins/username.mdx +1 -1
  74. package/site/content/docs/recipes/caddy.mdx +3 -3
  75. package/site/content/docs/recipes/index.mdx +18 -28
  76. package/site/content/docs/recipes/mailpit.mdx +1 -1
  77. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  78. package/site/content/docs/recipes/meta.json +1 -4
  79. package/site/content/docs/recipes/nginx.mdx +2 -2
  80. package/site/content/docs/recipes/openrouter.mdx +283 -0
  81. package/site/content/docs/recipes/pgdog.mdx +3 -3
  82. package/site/content/docs/recipes/postgres.mdx +1 -2
  83. package/site/content/docs/recipes/rustfs.mdx +2 -2
  84. package/site/content/docs/recipes/traefik.mdx +3 -3
  85. package/site/content/docs/reference/cli.mdx +231 -0
  86. package/site/content/docs/reference/configuration.mdx +30 -10
  87. package/site/content/docs/reference/environment-variables.mdx +93 -20
  88. package/site/content/docs/reference/errors.mdx +132 -29
  89. package/site/content/docs/reference/fx.mdx +137 -73
  90. package/site/content/docs/reference/index.mdx +13 -21
  91. package/site/content/docs/reference/meta.json +4 -5
  92. package/site/content/docs/reference/okid.mdx +42 -17
  93. package/site/content/docs/reference/plugins.mdx +4 -3
  94. package/site/content/docs/reference/security.mdx +197 -0
  95. package/site/content/docs/understand/meta.json +5 -0
  96. package/site/content/docs/understand/the-anatomy.mdx +132 -0
  97. package/site/content/docs/understand/the-model.mdx +32 -0
  98. package/site/content/docs/understand/the-problem.mdx +74 -0
  99. package/site/content/docs/understand/the-vocabulary.mdx +26 -0
  100. package/src/auth/api-keys.ts +2 -1
  101. package/src/auth/bindings.ts +64 -16
  102. package/src/auth/gate-auth.test.ts +6 -1
  103. package/src/auth/identity.ts +152 -5
  104. package/src/auth/index.ts +29 -0
  105. package/src/auth/invites.ts +2 -1
  106. package/src/auth/method-context.ts +41 -0
  107. package/src/auth/oauth-as/crypto.ts +2 -1
  108. package/src/auth/oauth-as/stores.ts +2 -1
  109. package/src/auth/operator.ts +2 -1
  110. package/src/auth/sessions-jwt.test.ts +77 -0
  111. package/src/auth/sessions.ts +20 -4
  112. package/src/auth/tenants.ts +3 -2
  113. package/src/auth/two-factor-challenge.test.ts +61 -0
  114. package/src/auth/two-factor-challenge.ts +284 -0
  115. package/src/auth/verification.ts +5 -0
  116. package/src/bench/REPORT.md +49 -0
  117. package/src/bench/g03-signal-once.bench.ts +1 -1
  118. package/src/bench/g10-observability-contention.bench.ts +1 -1
  119. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  120. package/src/bench/load-app.ts +2 -10
  121. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  122. package/src/cli/ai-setup/apply.ts +294 -52
  123. package/src/cli/ai-setup/catalog.ts +238 -1342
  124. package/src/cli/ai-setup/index.ts +14 -99
  125. package/src/cli/ai-setup/prompts.ts +60 -443
  126. package/src/cli/ask-seed.test.ts +96 -0
  127. package/src/cli/ask-seed.ts +82 -0
  128. package/src/cli/ask-vault-gaps.test.ts +84 -0
  129. package/src/cli/ask-vault-gaps.ts +146 -0
  130. package/src/cli/build.ts +2 -2
  131. package/src/cli/client-add.test.ts +26 -1
  132. package/src/cli/client-add.ts +106 -27
  133. package/src/cli/db-seed.ts +2 -0
  134. package/src/cli/db.ts +150 -11
  135. package/src/cli/dev.ts +119 -211
  136. package/src/cli/docker-clean.ts +2 -2
  137. package/src/cli/docker-cli.test.ts +1 -1
  138. package/src/cli/doctor-diff.ts +4 -2
  139. package/src/cli/doctor-pii.test.ts +1 -1
  140. package/src/cli/doctor.ts +14 -1
  141. package/src/cli/hero-meta.test.ts +4 -2
  142. package/src/cli/load-config.images.test.ts +8 -10
  143. package/src/cli/load-config.ts +1 -1
  144. package/src/cli/registry.ts +22 -10
  145. package/src/cli/replay.ts +3 -1
  146. package/src/cli/start.ts +1 -1
  147. package/src/cli/tui/keys.ts +2 -3
  148. package/src/client/auth/cookies.ts +74 -0
  149. package/src/client/auth/create-auth-client.ts +636 -0
  150. package/src/client/auth/denials.ts +99 -0
  151. package/src/client/auth/session.ts +283 -0
  152. package/src/client/auth.test.ts +251 -0
  153. package/src/client/auth.ts +39 -114
  154. package/src/client/create-with-session.ts +245 -0
  155. package/src/client/create.ts +51 -5
  156. package/src/client/index.ts +11 -1
  157. package/src/client/live.ts +11 -79
  158. package/src/client/notes-contract.test.ts +5 -10
  159. package/src/client/sse.ts +137 -0
  160. package/src/client/stream.ts +147 -0
  161. package/src/client/transport.test.ts +32 -0
  162. package/src/client/transport.ts +123 -26
  163. package/src/client/types.ts +118 -14
  164. package/src/client-react/index.ts +185 -24
  165. package/src/client-react/use-live-query.ts +13 -3
  166. package/src/compiler/aot.test.ts +7 -4
  167. package/src/compiler/effects-embed.test.ts +56 -0
  168. package/src/compiler/effects-fetch.test.ts +43 -0
  169. package/src/compiler/effects-infer.ts +58 -2
  170. package/src/compiler/extract.test.ts +257 -43
  171. package/src/compiler/extract.ts +566 -48
  172. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  173. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  174. package/src/compiler/fixtures/skyport.expected.json +4 -4
  175. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  176. package/src/compiler/response.ts +2 -2
  177. package/src/compiler/search-writer-isolation.test.ts +40 -0
  178. package/src/config/index.ts +11 -0
  179. package/src/console/server/app.ts +17 -23
  180. package/src/console/server/bind.ts +4 -0
  181. package/src/console/server/console.test.ts +2 -2
  182. package/src/console/server/flows-invoke.test.ts +50 -34
  183. package/src/console/server/flows.ts +436 -255
  184. package/src/console/server/runs-ingest.test.ts +6 -3
  185. package/src/console/server/serve.ts +1 -1
  186. package/src/console/server/signals.test.ts +1 -5
  187. package/src/console/server/signals.ts +8 -6
  188. package/src/console/server/state.ts +6 -1
  189. package/src/console/server/store.ts +1 -1
  190. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  191. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  192. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  193. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  194. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  195. package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
  196. package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
  197. package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
  198. package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
  199. package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
  200. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  201. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  202. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  203. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  204. package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
  205. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  206. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  207. package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
  208. package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
  209. package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
  210. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  211. package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
  212. package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
  213. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  214. package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
  215. package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
  216. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  217. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  218. package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
  219. package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
  220. package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
  221. package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
  222. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  223. package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
  224. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  225. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  226. package/src/console/ui-next/dist/index.html +6 -13
  227. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  228. package/src/console/ui-next/src/client.ts +7 -2
  229. package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
  230. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  231. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  232. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  234. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  235. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  236. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  237. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  238. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  239. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  240. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  241. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  242. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  243. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  244. package/src/docker/ai-model-status.test.ts +1 -12
  245. package/src/docker/ai-model-status.ts +10 -38
  246. package/src/docker/compose-up.test.ts +100 -0
  247. package/src/docker/compose-up.ts +292 -0
  248. package/src/docker/compose.ts +4 -14
  249. package/src/docker/derive.ts +9 -34
  250. package/src/docker/docker.test.ts +16 -261
  251. package/src/docker/images-config.test.ts +13 -25
  252. package/src/docker/index.ts +1 -24
  253. package/src/docker/recipes/index.ts +0 -23
  254. package/src/docker/recipes/pgdog.ts +6 -4
  255. package/src/docker/stack-id.test.ts +2 -2
  256. package/src/docker/stack-id.ts +0 -1
  257. package/src/docker/types.ts +2 -2
  258. package/src/drivers/ai-anthropic.ts +10 -0
  259. package/src/drivers/ai-openai-compatible.ts +34 -7
  260. package/src/drivers/ai-providers.test.ts +0 -107
  261. package/src/drivers/ai-stream.test.ts +0 -34
  262. package/src/drivers/ai-types.ts +17 -8
  263. package/src/drivers/channel-fcm.ts +6 -1
  264. package/src/drivers/channel-msegat.ts +6 -1
  265. package/src/drivers/channel-resend.ts +5 -1
  266. package/src/drivers/channel-smtp.ts +5 -1
  267. package/src/drivers/channel-sndr.ts +10 -1
  268. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  269. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  270. package/src/drivers/channel-taqnyat.ts +6 -1
  271. package/src/drivers/channel-types.ts +10 -0
  272. package/src/drivers/channel-unifonic.ts +6 -1
  273. package/src/drivers/channel-wa-cloud.ts +6 -1
  274. package/src/drivers/channel-webpush.ts +6 -1
  275. package/src/drivers/external.ts +33 -0
  276. package/src/drivers/index.ts +0 -10
  277. package/src/drivers/meilisearch.ts +6 -0
  278. package/src/drivers/oauth-types.ts +4 -0
  279. package/src/drivers/postgres.test.ts +74 -3
  280. package/src/drivers/postgres.ts +175 -6
  281. package/src/drivers/signal-types.ts +5 -5
  282. package/src/drivers/types.ts +7 -0
  283. package/src/drivers/vault-types.ts +5 -0
  284. package/src/elements/ai/declare.ts +17 -4
  285. package/src/elements/ai/eval.ts +1 -1
  286. package/src/elements/ai/mcp-http.ts +1 -1
  287. package/src/elements/ai/pii.ts +1 -1
  288. package/src/elements/ai/providers.test.ts +289 -0
  289. package/src/elements/ai/providers.ts +169 -0
  290. package/src/elements/ai/runtime.ts +68 -5
  291. package/src/elements/ai/schema.ts +2 -2
  292. package/src/elements/ai.test.ts +7 -7
  293. package/src/elements/ai.ts +17 -1
  294. package/src/elements/channel/runtime.ts +105 -12
  295. package/src/elements/clock/cron-fields.test.ts +144 -0
  296. package/src/elements/clock/cron-fields.ts +185 -0
  297. package/src/elements/clock/declare.ts +247 -3
  298. package/src/elements/clock.ts +21 -1
  299. package/src/elements/index.ts +15 -0
  300. package/src/elements/signal/chaos-child.ts +1 -2
  301. package/src/elements/signal/declare.ts +65 -37
  302. package/src/elements/signal/delivery-modes.test.ts +11 -31
  303. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  304. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  305. package/src/elements/signal/key-ordering.test.ts +6 -26
  306. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  307. package/src/elements/signal/optional-emit.test.ts +6 -9
  308. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  309. package/src/elements/signal/orphan-messages.test.ts +4 -15
  310. package/src/elements/signal/reconcile.test.ts +2 -2
  311. package/src/elements/signal/runtime.ts +1 -1
  312. package/src/elements/signal/schema-emit.test.ts +4 -8
  313. package/src/elements/signal.test.ts +24 -49
  314. package/src/elements/signal.ts +9 -2
  315. package/src/elements/store/domain-ddl.test.ts +1 -1
  316. package/src/elements/store/live-http.test.ts +9 -4
  317. package/src/elements/store/prepare-row.test.ts +123 -0
  318. package/src/elements/store/resource.ts +43 -12
  319. package/src/elements/store/schema-decl.ts +117 -0
  320. package/src/elements/store/search-backfill.ts +205 -0
  321. package/src/elements/store/search-bind.ts +66 -0
  322. package/src/elements/store/search-bm25.ts +78 -0
  323. package/src/elements/store/search-ddl.ts +156 -0
  324. package/src/elements/store/search-embed-flow.ts +141 -0
  325. package/src/elements/store/search-errors.ts +34 -0
  326. package/src/elements/store/search-fusion.ts +112 -0
  327. package/src/elements/store/search-lsh.ts +179 -0
  328. package/src/elements/store/search-runtime.ts +300 -0
  329. package/src/elements/store/search.test.ts +144 -0
  330. package/src/elements/store/sql-session.test.ts +1 -0
  331. package/src/elements/store/sql-session.ts +83 -3
  332. package/src/elements/store/table.ts +23 -2
  333. package/src/elements/store/upsert-app.test.ts +1 -3
  334. package/src/elements/store.ts +50 -0
  335. package/src/full.ts +4 -2
  336. package/src/http.ts +4 -1
  337. package/src/i18n/catalogs/ar.ts +14 -6
  338. package/src/i18n/catalogs/en.ts +14 -6
  339. package/src/index.ts +6 -1
  340. package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
  341. package/src/kernel/adopt-routes.ts +9 -1
  342. package/src/kernel/app-auth.ts +5 -0
  343. package/src/kernel/app.ts +102 -9
  344. package/src/kernel/auto-cache.test.ts +6 -12
  345. package/src/kernel/auto-registry.test.ts +3 -3
  346. package/src/kernel/boot-bind/ai.test.ts +22 -19
  347. package/src/kernel/boot-bind/ai.ts +5 -43
  348. package/src/kernel/boot-bind/clock.ts +23 -5
  349. package/src/kernel/boot-bind/honor-config.test.ts +4 -4
  350. package/src/kernel/boot.test.ts +11 -6
  351. package/src/kernel/boot.ts +66 -16
  352. package/src/kernel/boundary-contract.ts +93 -0
  353. package/src/kernel/budget.test.ts +1 -1
  354. package/src/kernel/call.ts +89 -0
  355. package/src/kernel/capability.ts +6 -0
  356. package/src/kernel/client-descriptor.ts +123 -0
  357. package/src/kernel/clock-timezone.test.ts +80 -0
  358. package/src/kernel/correlation.test.ts +1 -1
  359. package/src/kernel/dry-run.ts +11 -8
  360. package/src/kernel/effects-stamping.test.ts +42 -9
  361. package/src/kernel/effects.test.ts +4 -2
  362. package/src/kernel/effects.ts +51 -9
  363. package/src/kernel/errors-channel.ts +17 -0
  364. package/src/kernel/errors-live-resume.ts +3 -2
  365. package/src/kernel/errors-tenant.ts +7 -4
  366. package/src/kernel/errors.registry-helpers.ts +96 -0
  367. package/src/kernel/errors.registry.test.ts +139 -25
  368. package/src/kernel/errors.ts +102 -23
  369. package/src/kernel/external-effects.test.ts +196 -0
  370. package/src/kernel/flow.test.ts +13 -4
  371. package/src/kernel/flow.ts +69 -71
  372. package/src/kernel/fx-ask-telemetry.test.ts +2 -2
  373. package/src/kernel/fx-dead-letters.test.ts +3 -3
  374. package/src/kernel/fx-fetch.ts +55 -0
  375. package/src/kernel/fx-live-stream.ts +2 -2
  376. package/src/kernel/fx-live.test.ts +8 -8
  377. package/src/kernel/fx.test.ts +17 -11
  378. package/src/kernel/fx.ts +180 -57
  379. package/src/kernel/horizontal-child.ts +1 -1
  380. package/src/kernel/http-query.test.ts +3 -6
  381. package/src/kernel/index.ts +13 -3
  382. package/src/kernel/instance-id.ts +1 -1
  383. package/src/kernel/instances.test.ts +40 -0
  384. package/src/kernel/instances.ts +47 -9
  385. package/src/kernel/live-http.test.ts +6 -7
  386. package/src/kernel/live-http.ts +6 -2
  387. package/src/kernel/live-resume.test.ts +1 -1
  388. package/src/kernel/mcp-tool.test.ts +3 -3
  389. package/src/kernel/on.ts +98 -4
  390. package/src/kernel/pipeline.test.ts +12 -15
  391. package/src/kernel/plugin-elements.test.ts +1 -1
  392. package/src/kernel/stamp-http.test.ts +3 -3
  393. package/src/kernel/triggers.ts +210 -95
  394. package/src/kernel-entry.ts +0 -1
  395. package/src/manifest/diff.ts +11 -1
  396. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  397. package/src/manifest/types.ts +46 -2
  398. package/src/mcp/docs-index.ts +3 -3
  399. package/src/mcp/docs-mcp.test.ts +2 -2
  400. package/src/mcp/docs-tools.ts +1 -1
  401. package/src/okid.test.ts +56 -0
  402. package/src/okid.ts +55 -11
  403. package/src/plugins/anonymous.ts +8 -4
  404. package/src/plugins/auth/shared.ts +27 -3
  405. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  406. package/src/plugins/auth-methods.security.test.ts +561 -23
  407. package/src/plugins/config-source.ts +1 -1
  408. package/src/plugins/csrf.test.ts +65 -0
  409. package/src/plugins/index.ts +2 -0
  410. package/src/plugins/magic-link.ts +36 -16
  411. package/src/plugins/oauth/shared.ts +3 -1
  412. package/src/plugins/oauth.ts +22 -14
  413. package/src/plugins/otp.ts +37 -21
  414. package/src/plugins/passkey-webauthn.ts +9 -6
  415. package/src/plugins/passkey.ts +98 -26
  416. package/src/plugins/pre-account-hijack.test.ts +223 -0
  417. package/src/plugins/two-factor.ts +466 -37
  418. package/src/plugins/username.ts +19 -7
  419. package/src/release/build-lib.ts +1 -0
  420. package/src/release/limits.ts +2 -2
  421. package/src/release/measure.ts +1 -1
  422. package/src/runs/collect.test.ts +0 -2
  423. package/src/runtime/json-code-block.test.ts +250 -20
  424. package/src/runtime/json-code-block.ts +1261 -41
  425. package/src/term.test.ts +1 -1
  426. package/src/test/create-test-app.test.ts +12 -12
  427. package/src/test/live-signals.test.ts +6 -6
  428. package/src/test/provisions.integration.test.ts +10 -14
  429. package/src/test/reset-element-registries.ts +1 -1
  430. package/src/test/tenant-isolation.test.ts +12 -6
  431. package/site/content/docs/deployment/docker-swarm.mdx +0 -164
  432. package/site/content/docs/deployment/docker.mdx +0 -227
  433. package/site/content/docs/deployment/index.mdx +0 -83
  434. package/site/content/docs/deployment/kubernetes.mdx +0 -176
  435. package/site/content/docs/deployment/meta.json +0 -5
  436. package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
  437. package/site/content/docs/elements/ai.mdx +0 -385
  438. package/site/content/docs/elements/channel.mdx +0 -346
  439. package/site/content/docs/elements/clock.mdx +0 -244
  440. package/site/content/docs/elements/flow.mdx +0 -420
  441. package/site/content/docs/elements/gate.mdx +0 -436
  442. package/site/content/docs/elements/signal.mdx +0 -380
  443. package/site/content/docs/elements/store.mdx +0 -1099
  444. package/site/content/docs/elements/vault.mdx +0 -405
  445. package/site/content/docs/get-started/basic-usage.mdx +0 -173
  446. package/site/content/docs/get-started/index.mdx +0 -43
  447. package/site/content/docs/get-started/installation.mdx +0 -220
  448. package/site/content/docs/get-started/introduction.mdx +0 -144
  449. package/site/content/docs/get-started/meta.json +0 -13
  450. package/site/content/docs/get-started/project-structure.mdx +0 -925
  451. package/site/content/docs/get-started/testing.mdx +0 -328
  452. package/site/content/docs/get-started/why.mdx +0 -114
  453. package/site/content/docs/recipes/llama-cpp.mdx +0 -151
  454. package/site/content/docs/recipes/ollama.mdx +0 -142
  455. package/site/content/docs/recipes/sglang.mdx +0 -105
  456. package/site/content/docs/recipes/vllm.mdx +0 -106
  457. package/site/content/docs/reference/cli.md +0 -232
  458. package/site/content/docs/reference/client.mdx +0 -469
  459. package/site/content/docs/reference/security.md +0 -74
  460. package/src/cli/ai-setup/detect-ollama.ts +0 -213
  461. package/src/cli/ai-setup/recommend.test.ts +0 -225
  462. package/src/cli/ai-setup/recommend.ts +0 -225
  463. package/src/cli/dev-controls.test.ts +0 -122
  464. package/src/cli/dev-controls.ts +0 -164
  465. package/src/cli/tui/DevLive.tsx +0 -126
  466. package/src/console/ui-next/dist/assets/access-page-3-EFj-2G.js +0 -4
  467. package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
  468. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
  469. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
  470. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
  471. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
  472. package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
  473. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
  474. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
  475. package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
  476. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
  477. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
  478. package/src/console/ui-next/dist/assets/flows-page-cVFnA4HH.js +0 -1
  479. package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
  480. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
  481. package/src/console/ui-next/dist/assets/index-CMIgUbD0.js +0 -66
  482. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  483. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  484. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.js +0 -4
  485. package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
  486. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
  487. package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
  488. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
  489. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
  490. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
  491. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
  492. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
  493. package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
  494. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
  495. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
  496. package/src/console/ui-next/dist/assets/units-page-l8FeKfnP.js +0 -1
  497. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
  498. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
  499. package/src/console/ui-next/dist/assets/vault-page-CL-d_mLE.js +0 -2
  500. package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
  501. package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
  502. package/src/docker/ollama-pull.ts +0 -232
  503. package/src/docker/recipes/llama-cpp.ts +0 -298
  504. package/src/docker/recipes/ollama.ts +0 -44
  505. package/src/docker/recipes/sglang.ts +0 -55
  506. package/src/docker/recipes/vllm.ts +0 -44
  507. package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
  508. package/src/drivers/ai-ollama.integration.test.ts +0 -184
  509. package/src/drivers/ai-ollama.ts +0 -389
  510. package/src/drivers/ollama.ts +0 -14
@@ -1,35 +1,77 @@
1
1
  /**
2
- * TOTP two-factor Gate auth method plugin (RFC 6238, HMAC-SHA1).
2
+ * TOTP / email-OTP two-factor Gate auth method plugin.
3
3
  *
4
- * v1 verify accepts `{ userId, code }` after password sign-in when 2FA is enabled.
4
+ * Login challenges are method-locked. Enrollment and method change are
5
+ * privileged (step-up) and Forbidden while an unresolved login challenge exists.
5
6
  */
6
7
 
7
8
  import { constantTimeEqual } from "../auth/constant-time.ts";
8
- import { IdentityError, ensureUserExists } from "../auth/identity.ts";
9
+ import { IdentityError, ensureUserExists, getUserById } from "../auth/identity.ts";
10
+ import { getActiveGateAuthContext, patchActiveGateAuthContext } from "../auth/method-context.ts";
11
+ import { sealOtp } from "../auth/otp-seal.ts";
9
12
  import { issueSessionWithScopes } from "../auth/sessions.ts";
13
+ import {
14
+ consumeChallenge,
15
+ createPendingTwoFactorStore,
16
+ createStepUpStore,
17
+ getChallenge,
18
+ grantStepUp,
19
+ hasActiveChallenge,
20
+ consumeStepUp,
21
+ issueChallenge,
22
+ twoFactorEmailOtpIdentifier,
23
+ type PendingTwoFactorStore,
24
+ type StepUpStore,
25
+ type TwoFactorMethod,
26
+ type TwoFactorRequiredOut,
27
+ } from "../auth/two-factor-challenge.ts";
28
+ import {
29
+ consumeVerification,
30
+ createVerificationStore,
31
+ findActiveVerification,
32
+ generateOtp,
33
+ hashChallenge,
34
+ invalidateVerifications,
35
+ putVerification,
36
+ type VerificationStore,
37
+ } from "../auth/verification.ts";
38
+ import { channel } from "../elements/channel.ts";
10
39
  import { plugin, type PluginDef } from "../kernel/plugin.ts";
11
40
  import {
12
41
  AuthFailed,
13
42
  AuthRateLimited,
43
+ Forbidden,
14
44
  SessionTokensOut,
15
45
  bindPublicAuth,
16
46
  bindSessionAuth,
17
47
  createMethodRuntime,
18
48
  fail,
19
49
  flow,
50
+ resolveMethodSecret,
20
51
  resolveSharedIdentities,
21
52
  z,
22
53
  type AuthMethodOptions,
23
54
  } from "./auth/shared.ts";
55
+ import { okid } from "../okid.ts";
24
56
 
25
- /** Per-user TOTP + recovery state. */
57
+ /** Per-user TOTP / email-OTP + recovery state. */
26
58
  export interface TwoFactorRow {
27
59
  userId: string;
60
+ method: TwoFactorMethod;
61
+ /** Base32 TOTP secret; empty when method is email_otp. */
28
62
  secret: string;
29
63
  enabled: boolean;
30
64
  /** SHA-256 hex of recovery codes (unused codes only). */
31
65
  recoveryHashes: Set<string>;
32
66
  createdAt: number;
67
+ /** Override email for email_otp (else identity email). */
68
+ email?: string;
69
+ /** Pending method swap awaiting confirmation. */
70
+ pending?: {
71
+ method: TwoFactorMethod;
72
+ secret: string;
73
+ recoveryHashes: Set<string>;
74
+ };
33
75
  }
34
76
 
35
77
  /** In-memory two-factor store. */
@@ -49,10 +91,25 @@ export interface TwoFactorOptions extends AuthMethodOptions {
49
91
  readonly factors?: TwoFactorStore;
50
92
  /** Issuer label for otpauth URLs (default `oke`). */
51
93
  readonly issuer?: string;
94
+ /** Shared pending login challenges (defaults to active Gate auth store). */
95
+ readonly pending?: PendingTwoFactorStore;
96
+ /** Step-up grants for privileged ops. */
97
+ readonly stepUp?: StepUpStore;
98
+ /** Verification store for email OTP as second factor. */
99
+ readonly verifications?: VerificationStore;
100
+ /** Expose `devOtp` on login challenge / change-method when email_otp. */
101
+ readonly exposeDevOtp?: boolean;
52
102
  }
53
103
 
104
+ const EnableOut = z.object({
105
+ secret: z.string(),
106
+ otpauthUrl: z.string(),
107
+ recoveryCodes: z.array(z.string()),
108
+ method: z.literal("totp"),
109
+ });
110
+
54
111
  /**
55
- * Enable / verify / disable TOTP two-factor (`oke_two_factor`).
112
+ * Enable / verify / disable / change TOTP or email OTP two-factor (`oke_two_factor`).
56
113
  *
57
114
  * @param opts - Secret / session / factor store
58
115
  */
@@ -61,27 +118,138 @@ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
61
118
  const identities = resolveSharedIdentities(opts);
62
119
  const factors = opts.factors ?? createTwoFactorStore();
63
120
  const issuer = opts.issuer ?? "oke";
121
+ const secret = resolveMethodSecret(opts);
122
+ const active = getActiveGateAuthContext();
123
+ const pending = opts.pending ?? active?.pendingTwoFactor ?? createPendingTwoFactorStore();
124
+ const stepUp = opts.stepUp ?? active?.stepUp ?? createStepUpStore();
125
+ const verifications =
126
+ opts.verifications ?? active?.twoFactorVerifications ?? createVerificationStore();
127
+ const exposeDevOtp = opts.exposeDevOtp === true;
128
+
129
+ const emailTmpl = channel.email({ from: "OKE <no-reply@oke.local>" }).template("auth-2fa-email", {
130
+ description: "Two-factor email OTP",
131
+ schema: z.object({ email: z.string(), otp: z.string() }),
132
+ locales: ["en", "ar"],
133
+ });
134
+
135
+ async function provisionEmailOtp(userId: string, now: number): Promise<string | undefined> {
136
+ const user = getUserById(identities, userId);
137
+ const row = factors.byUserId.get(userId);
138
+ const email = row?.email ?? (user?.email && user.email.includes("@") ? user.email : undefined);
139
+ if (!email) return undefined;
140
+ const otpCode = generateOtp(6);
141
+ const identifier = twoFactorEmailOtpIdentifier(userId);
142
+ invalidateVerifications(verifications, identifier, now);
143
+ const sealed = await sealOtp(secret, otpCode);
144
+ putVerification(verifications, {
145
+ id: okid(),
146
+ identifier,
147
+ value: await hashChallenge(otpCode),
148
+ expiresAt: now + 10 * 60 * 1000,
149
+ createdAt: now,
150
+ consumedAt: null,
151
+ attempts: 0,
152
+ sealedOtp: sealed,
153
+ lastDeliveredAt: now,
154
+ lastChannel: "email",
155
+ email,
156
+ phone: null,
157
+ });
158
+ return otpCode;
159
+ }
160
+
161
+ async function beginLoginChallenge(userId: string): Promise<TwoFactorRequiredOut | null> {
162
+ const row = factors.byUserId.get(userId);
163
+ if (!row?.enabled) return null;
164
+ const now = runtime.now();
165
+ const challenge = issueChallenge(pending, {
166
+ userId,
167
+ method: row.method,
168
+ now,
169
+ });
170
+ let devOtp: string | undefined;
171
+ if (row.method === "email_otp") {
172
+ const code = await provisionEmailOtp(userId, now);
173
+ if (exposeDevOtp && code) devOtp = code;
174
+ }
175
+ return {
176
+ twoFactorRequired: true as const,
177
+ challengeId: challenge.id,
178
+ method: row.method,
179
+ userId,
180
+ ...(devOtp ? { devOtp } : {}),
181
+ };
182
+ }
183
+
184
+ patchActiveGateAuthContext({
185
+ pendingTwoFactor: pending,
186
+ stepUp,
187
+ twoFactorVerifications: verifications,
188
+ twoFactor: {
189
+ isEnabled: (userId) => factors.byUserId.get(userId)?.enabled === true,
190
+ beginLoginChallenge,
191
+ },
192
+ });
193
+
194
+ async function verifyCurrentFactor(userId: string, code: string, now: number): Promise<boolean> {
195
+ const row = factors.byUserId.get(userId);
196
+ if (!row?.enabled) return false;
197
+ const trimmed = code.trim().replace(/\s+/g, "");
198
+ if (row.method === "totp") {
199
+ if (await verifyTotp(row.secret, trimmed)) return true;
200
+ const hash = await sha256Hex(trimmed);
201
+ if (row.recoveryHashes.has(hash)) {
202
+ row.recoveryHashes.delete(hash);
203
+ return true;
204
+ }
205
+ return false;
206
+ }
207
+ // email_otp
208
+ const vRow = findActiveVerification(verifications, twoFactorEmailOtpIdentifier(userId), now);
209
+ if (!vRow) return false;
210
+ if (vRow.attempts >= 5) {
211
+ consumeVerification(vRow, now);
212
+ return false;
213
+ }
214
+ const hash = await hashChallenge(trimmed);
215
+ if (!constantTimeEqual(hash, vRow.value)) {
216
+ vRow.attempts += 1;
217
+ if (vRow.attempts >= 5) consumeVerification(vRow, now);
218
+ return false;
219
+ }
220
+ consumeVerification(vRow, now);
221
+ return true;
222
+ }
223
+
224
+ const enableContract = {
225
+ out: EnableOut,
226
+ errors: { AuthFailed, Forbidden },
227
+ };
64
228
 
65
229
  const enable = flow("auth.twoFactorEnable", {
66
230
  plane: "user",
67
- out: z.object({
68
- secret: z.string(),
69
- otpauthUrl: z.string(),
70
- recoveryCodes: z.array(z.string()),
71
- }),
72
- errors: { AuthFailed },
73
231
  do: async (_input, fx) => {
74
232
  const userId = fx.auth.userId;
75
233
  if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
234
+ const now = runtime.now();
235
+ if (hasActiveChallenge(pending, userId, now)) {
236
+ return fail("Forbidden", { reason: "active_2fa_challenge" });
237
+ }
76
238
  try {
77
- await ensureUserExists(identities, userId, runtime.now());
239
+ await ensureUserExists(identities, userId, now);
78
240
  } catch (err) {
79
241
  if (err instanceof IdentityError) {
80
242
  return fail("AuthFailed", { reason: "unauthenticated" });
81
243
  }
82
244
  throw err;
83
245
  }
84
- const secret = generateBase32Secret(20);
246
+ const existing = factors.byUserId.get(userId);
247
+ if (existing?.enabled) {
248
+ if (!consumeStepUp(stepUp, userId, ["enroll", "change"], now)) {
249
+ return fail("Forbidden", { reason: "step_up_required" });
250
+ }
251
+ }
252
+ const totpSecret = generateBase32Secret(20);
85
253
  const recoveryCodes = Array.from({ length: 8 }, () => randomRecoveryCode());
86
254
  const recoveryHashes = new Set<string>();
87
255
  for (const code of recoveryCodes) {
@@ -89,39 +257,43 @@ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
89
257
  }
90
258
  factors.byUserId.set(userId, {
91
259
  userId,
92
- secret,
260
+ method: "totp",
261
+ secret: totpSecret,
93
262
  enabled: true,
94
263
  recoveryHashes,
95
- createdAt: runtime.now(),
264
+ createdAt: now,
96
265
  });
97
266
  const label = encodeURIComponent(userId);
98
- const otpauthUrl = `otpauth://totp/${encodeURIComponent(issuer)}:${label}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
99
- return { secret, otpauthUrl, recoveryCodes };
267
+ const otpauthUrl = `otpauth://totp/${encodeURIComponent(issuer)}:${label}?secret=${totpSecret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
268
+ return { secret: totpSecret, otpauthUrl, recoveryCodes, method: "totp" as const };
100
269
  },
101
270
  });
102
271
 
103
- const verify = flow("auth.twoFactorVerify", {
104
- plane: "user",
272
+ const verifyContract = {
105
273
  in: z.object({
106
- userId: z.string().min(1),
107
- code: z.string().min(6).max(16),
274
+ challengeId: z.string().min(1),
275
+ code: z.string().min(4).max(16),
108
276
  }),
109
277
  out: SessionTokensOut,
110
278
  errors: { AuthFailed, AuthRateLimited },
279
+ };
280
+
281
+ const verify = flow("auth.twoFactorVerify", {
282
+ plane: "user",
111
283
  do: async (input) => {
112
- const row = factors.byUserId.get(input.userId);
284
+ const now = runtime.now();
285
+ const challenge = getChallenge(pending, input.challengeId, now);
286
+ if (!challenge) return fail("AuthFailed", { reason: "invalid_credentials" });
287
+ const row = factors.byUserId.get(challenge.userId);
113
288
  if (!row?.enabled) return fail("AuthFailed", { reason: "invalid_credentials" });
114
- const code = input.code.trim().replace(/\s+/g, "");
115
- const totpOk = await verifyTotp(row.secret, code);
116
- if (!totpOk) {
117
- const hash = await sha256Hex(code);
118
- if (!row.recoveryHashes.has(hash)) {
119
- return fail("AuthFailed", { reason: "invalid_credentials" });
120
- }
121
- row.recoveryHashes.delete(hash);
289
+ if (row.method !== challenge.method) {
290
+ return fail("AuthFailed", { reason: "invalid_credentials" });
122
291
  }
292
+ const ok = await verifyCurrentFactor(challenge.userId, input.code, now);
293
+ if (!ok) return fail("AuthFailed", { reason: "invalid_credentials" });
294
+ consumeChallenge(pending, challenge, now);
123
295
  const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
124
- id: input.userId,
296
+ id: challenge.userId,
125
297
  plane: "user",
126
298
  scopes: [],
127
299
  });
@@ -129,18 +301,264 @@ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
129
301
  accessToken: issued.accessToken,
130
302
  refreshToken: issued.refreshToken,
131
303
  accessExpiresAt: issued.accessExpiresAt,
132
- userId: input.userId,
304
+ userId: challenge.userId,
133
305
  };
134
306
  },
135
307
  });
136
308
 
137
- const disable = flow("auth.twoFactorDisable", {
309
+ const stepUpContract = {
310
+ in: z.object({
311
+ code: z.string().min(4).max(16),
312
+ purpose: z.enum(["enroll", "change", "disable"]).default("enroll"),
313
+ }),
314
+ out: z.object({ ok: z.literal(true), purpose: z.enum(["enroll", "change", "disable"]) }),
315
+ errors: { AuthFailed, Forbidden },
316
+ };
317
+
318
+ const stepUpFlow = flow("auth.twoFactorStepUp", {
319
+ plane: "user",
320
+ effects: { sends: ["auth-2fa-email"] },
321
+ do: async (input, fx) => {
322
+ const userId = fx.auth.userId;
323
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
324
+ const now = runtime.now();
325
+ if (hasActiveChallenge(pending, userId, now)) {
326
+ return fail("Forbidden", { reason: "active_2fa_challenge" });
327
+ }
328
+ const row = factors.byUserId.get(userId);
329
+ if (!row?.enabled) return fail("AuthFailed", { reason: "invalid_credentials" });
330
+ // Ensure email OTP is provisioned if caller needs a fresh code for step-up.
331
+ if (row.method === "email_otp") {
332
+ const activeOtp = findActiveVerification(
333
+ verifications,
334
+ twoFactorEmailOtpIdentifier(userId),
335
+ now,
336
+ );
337
+ if (!activeOtp) await provisionEmailOtp(userId, now);
338
+ }
339
+ const ok = await verifyCurrentFactor(userId, input.code, now);
340
+ if (!ok) return fail("AuthFailed", { reason: "invalid_credentials" });
341
+ const purpose = input.purpose;
342
+ grantStepUp(stepUp, userId, purpose, now);
343
+ return { ok: true as const, purpose };
344
+ },
345
+ });
346
+
347
+ const changeMethodContract = {
348
+ in: z.object({
349
+ method: z.enum(["totp", "email_otp"]),
350
+ /** Required when switching to email_otp if the identity has no email. */
351
+ email: z.string().min(3).optional(),
352
+ }),
353
+ out: z.union([
354
+ EnableOut,
355
+ z.object({
356
+ ok: z.literal(true),
357
+ method: z.literal("email_otp"),
358
+ pending: z.literal(true),
359
+ devOtp: z.string().optional(),
360
+ }),
361
+ ]),
362
+ errors: { AuthFailed, Forbidden },
363
+ };
364
+
365
+ const changeMethod = flow("auth.twoFactorChangeMethod", {
366
+ plane: "user",
367
+ effects: { sends: ["auth-2fa-email"] },
368
+ do: async (input, fx) => {
369
+ const userId = fx.auth.userId;
370
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
371
+ const now = runtime.now();
372
+ if (hasActiveChallenge(pending, userId, now)) {
373
+ return fail("Forbidden", { reason: "active_2fa_challenge" });
374
+ }
375
+ if (!consumeStepUp(stepUp, userId, ["change", "enroll"], now)) {
376
+ return fail("Forbidden", { reason: "step_up_required" });
377
+ }
378
+ const current = factors.byUserId.get(userId);
379
+ if (!current?.enabled) {
380
+ return fail("AuthFailed", { reason: "invalid_credentials" });
381
+ }
382
+ if (input.method === "totp") {
383
+ const totpSecret = generateBase32Secret(20);
384
+ const recoveryCodes = Array.from({ length: 8 }, () => randomRecoveryCode());
385
+ const recoveryHashes = new Set<string>();
386
+ for (const code of recoveryCodes) {
387
+ recoveryHashes.add(await sha256Hex(code));
388
+ }
389
+ current.pending = {
390
+ method: "totp",
391
+ secret: totpSecret,
392
+ recoveryHashes,
393
+ };
394
+ // Re-grant so confirm can proceed in the same privileged window.
395
+ grantStepUp(stepUp, userId, "change", now);
396
+ const label = encodeURIComponent(userId);
397
+ const otpauthUrl = `otpauth://totp/${encodeURIComponent(issuer)}:${label}?secret=${totpSecret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`;
398
+ return {
399
+ secret: totpSecret,
400
+ otpauthUrl,
401
+ recoveryCodes,
402
+ method: "totp" as const,
403
+ };
404
+ }
405
+ // email_otp pending — confirm with emailed code
406
+ const user = getUserById(identities, userId);
407
+ const email =
408
+ input.email ??
409
+ current.email ??
410
+ (user?.email && user.email.includes("@") ? user.email : undefined);
411
+ if (!email || !email.includes("@")) {
412
+ return fail("AuthFailed", { reason: "invalid_email" });
413
+ }
414
+ current.email = email;
415
+ current.pending = {
416
+ method: "email_otp",
417
+ secret: "",
418
+ recoveryHashes: new Set(),
419
+ };
420
+ const code = await provisionEmailOtp(userId, now);
421
+ grantStepUp(stepUp, userId, "change", now);
422
+ return {
423
+ ok: true as const,
424
+ method: "email_otp" as const,
425
+ pending: true as const,
426
+ ...(exposeDevOtp && code ? { devOtp: code } : {}),
427
+ };
428
+ },
429
+ });
430
+
431
+ const confirmChangeContract = {
432
+ in: z.object({
433
+ code: z.string().min(4).max(16),
434
+ }),
435
+ out: z.object({
436
+ ok: z.literal(true),
437
+ method: z.enum(["totp", "email_otp"]),
438
+ }),
439
+ errors: { AuthFailed, Forbidden },
440
+ };
441
+
442
+ const confirmChange = flow("auth.twoFactorConfirmChange", {
443
+ plane: "user",
444
+ do: async (input, fx) => {
445
+ const userId = fx.auth.userId;
446
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
447
+ const now = runtime.now();
448
+ if (hasActiveChallenge(pending, userId, now)) {
449
+ return fail("Forbidden", { reason: "active_2fa_challenge" });
450
+ }
451
+ if (!consumeStepUp(stepUp, userId, ["change", "enroll"], now)) {
452
+ return fail("Forbidden", { reason: "step_up_required" });
453
+ }
454
+ const current = factors.byUserId.get(userId);
455
+ const pendingEnroll = current?.pending;
456
+ if (!current?.enabled || !pendingEnroll) {
457
+ return fail("AuthFailed", { reason: "invalid_credentials" });
458
+ }
459
+ if (pendingEnroll.method === "totp") {
460
+ if (!(await verifyTotp(pendingEnroll.secret, input.code.trim().replace(/\s+/g, "")))) {
461
+ // Restore step-up so the user can retry confirm.
462
+ grantStepUp(stepUp, userId, "change", now);
463
+ return fail("AuthFailed", { reason: "invalid_credentials" });
464
+ }
465
+ // Invalidate old method immediately — replace with pending TOTP.
466
+ current.method = "totp";
467
+ current.secret = pendingEnroll.secret;
468
+ current.recoveryHashes = pendingEnroll.recoveryHashes;
469
+ current.pending = undefined;
470
+ current.email = undefined;
471
+ return { ok: true as const, method: "totp" as const };
472
+ }
473
+ // Confirm email_otp by consuming the sealed OTP for this user.
474
+ const vRow = findActiveVerification(verifications, twoFactorEmailOtpIdentifier(userId), now);
475
+ if (!vRow) return fail("AuthFailed", { reason: "invalid_credentials" });
476
+ const trimmed = input.code.trim().replace(/\s+/g, "");
477
+ const hash = await hashChallenge(trimmed);
478
+ if (!constantTimeEqual(hash, vRow.value)) {
479
+ vRow.attempts += 1;
480
+ grantStepUp(stepUp, userId, "change", now);
481
+ return fail("AuthFailed", { reason: "invalid_credentials" });
482
+ }
483
+ consumeVerification(vRow, now);
484
+ // Invalidate old TOTP secret / recovery immediately.
485
+ current.method = "email_otp";
486
+ current.secret = "";
487
+ current.recoveryHashes = new Set();
488
+ current.pending = undefined;
489
+ return { ok: true as const, method: "email_otp" as const };
490
+ },
491
+ });
492
+
493
+ const requestEmailOtpContract = {
494
+ in: z.object({
495
+ challengeId: z.string().min(1).optional(),
496
+ }),
497
+ out: z.object({
498
+ ok: z.literal(true),
499
+ devOtp: z.string().optional(),
500
+ }),
501
+ errors: { AuthFailed, AuthRateLimited, Forbidden },
502
+ };
503
+
504
+ const requestEmailOtp = flow("auth.twoFactorRequestEmailOtp", {
138
505
  plane: "user",
506
+ effects: { sends: [emailTmpl.name] },
507
+ do: async (input, fx) => {
508
+ const now = runtime.now();
509
+ let userId = fx.auth.userId;
510
+ if (input.challengeId) {
511
+ const challenge = getChallenge(pending, input.challengeId, now);
512
+ if (!challenge) return fail("AuthFailed", { reason: "invalid_credentials" });
513
+ if (challenge.method !== "email_otp") {
514
+ return fail("Forbidden", { reason: "method_locked" });
515
+ }
516
+ userId = challenge.userId;
517
+ }
518
+ if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
519
+ const row = factors.byUserId.get(userId);
520
+ if (!row?.enabled || row.method !== "email_otp") {
521
+ return fail("AuthFailed", { reason: "invalid_credentials" });
522
+ }
523
+ const code = await provisionEmailOtp(userId, now);
524
+ if (!code) return fail("AuthFailed", { reason: "invalid_credentials" });
525
+ const user = getUserById(identities, userId);
526
+ const email = row.email ?? user?.email;
527
+ if (email) {
528
+ await fx.deliverOtp({
529
+ channels: ["email"],
530
+ templates: { email: emailTmpl.name, sms: emailTmpl.name, whatsapp: emailTmpl.name },
531
+ email,
532
+ data: { otp: code, email },
533
+ });
534
+ }
535
+ return {
536
+ ok: true as const,
537
+ ...(exposeDevOtp ? { devOtp: code } : {}),
538
+ };
539
+ },
540
+ });
541
+
542
+ const disableContract = {
139
543
  out: z.object({ ok: z.literal(true) }),
140
- errors: { AuthFailed },
544
+ errors: { AuthFailed, Forbidden },
545
+ };
546
+
547
+ const disable = flow("auth.twoFactorDisable", {
548
+ plane: "user",
141
549
  do: (_input, fx) => {
142
550
  const userId = fx.auth.userId;
143
551
  if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
552
+ const now = runtime.now();
553
+ if (hasActiveChallenge(pending, userId, now)) {
554
+ return fail("Forbidden", { reason: "active_2fa_challenge" });
555
+ }
556
+ const existing = factors.byUserId.get(userId);
557
+ if (existing?.enabled) {
558
+ if (!consumeStepUp(stepUp, userId, ["disable", "change"], now)) {
559
+ return fail("Forbidden", { reason: "step_up_required" });
560
+ }
561
+ }
144
562
  factors.byUserId.delete(userId);
145
563
  return { ok: true as const };
146
564
  },
@@ -149,9 +567,20 @@ export function twoFactor(opts: TwoFactorOptions = {}): PluginDef {
149
567
  return plugin("twoFactor", { version: "0.0.1", config: { method: "two-factor" } })
150
568
  .needs("auth")
151
569
  .table("oke_two_factor", undefined, { plane: "user", description: "TOTP secrets + recovery" })
152
- .binding(bindSessionAuth("/two-factor/enable", enable))
153
- .binding(bindPublicAuth("/two-factor/verify", verify, "otp"))
154
- .binding(bindSessionAuth("/two-factor/disable", disable));
570
+ .binding(bindSessionAuth("/two-factor/enable", enable, enableContract))
571
+ .binding(bindPublicAuth("/two-factor/verify", verify, "otp", verifyContract))
572
+ .binding(bindSessionAuth("/two-factor/step-up", stepUpFlow, stepUpContract))
573
+ .binding(bindSessionAuth("/two-factor/change-method", changeMethod, changeMethodContract))
574
+ .binding(bindSessionAuth("/two-factor/confirm-change", confirmChange, confirmChangeContract))
575
+ .binding(
576
+ bindPublicAuth(
577
+ "/two-factor/request-email-otp",
578
+ requestEmailOtp,
579
+ "otp",
580
+ requestEmailOtpContract,
581
+ ),
582
+ )
583
+ .binding(bindSessionAuth("/two-factor/disable", disable, disableContract));
155
584
  }
156
585
 
157
586
  /**
@@ -18,6 +18,7 @@ import {
18
18
  AuthFailed,
19
19
  AuthRateLimited,
20
20
  SessionTokensOut,
21
+ SignInOut,
21
22
  bindPublicAuth,
22
23
  createMethodRuntime,
23
24
  fail,
@@ -322,11 +323,14 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
322
323
  resolveUsernamePolicy(usernamePolicy);
323
324
  const crypto = createBunCrypto();
324
325
 
325
- const signUp = flow("auth.signUpUsername", {
326
- plane: "user",
326
+ const signUpContract = {
327
327
  in: UsernameIn,
328
328
  out: SessionTokensOut,
329
329
  errors: { AuthFailed, AuthRateLimited },
330
+ };
331
+
332
+ const signUp = flow("auth.signUpUsername", {
333
+ plane: "user",
330
334
  do: async (input) => {
331
335
  const key = normalizeUsername(input.username);
332
336
  try {
@@ -402,11 +406,14 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
402
406
  },
403
407
  });
404
408
 
405
- const signIn = flow("auth.signInUsername", {
406
- plane: "user",
409
+ const signInContract = {
407
410
  in: UsernameIn,
408
- out: SessionTokensOut,
411
+ out: SignInOut,
409
412
  errors: { AuthFailed, AuthRateLimited },
413
+ };
414
+
415
+ const signIn = flow("auth.signInUsername", {
416
+ plane: "user",
410
417
  do: async (input) => {
411
418
  const key = normalizeUsername(input.username);
412
419
  const row = usernames.byUsername.get(key);
@@ -427,6 +434,11 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
427
434
  }
428
435
  throw err;
429
436
  }
437
+ const bridge = getActiveGateAuthContext()?.twoFactor;
438
+ if (bridge?.isEnabled(userId)) {
439
+ const challenge = await bridge.beginLoginChallenge(userId);
440
+ if (challenge) return challenge;
441
+ }
430
442
  const issued = await issueSessionWithScopes(runtime.sessions, runtime.crypto, {
431
443
  id: userId,
432
444
  plane: "user",
@@ -444,8 +456,8 @@ export function username(opts: UsernamePluginOptions = {}): PluginDef {
444
456
  return plugin("username", { version: "0.0.1", config: { method: "username" } })
445
457
  .needs("auth")
446
458
  .table("oke_usernames", undefined, { plane: "user", description: "Username credentials" })
447
- .binding(bindPublicAuth("/sign-up/username", signUp, "signUp"))
448
- .binding(bindPublicAuth("/sign-in/username", signIn, "signIn"));
459
+ .binding(bindPublicAuth("/sign-up/username", signUp, "signUp", signUpContract))
460
+ .binding(bindPublicAuth("/sign-in/username", signIn, "signIn", signInContract));
449
461
  }
450
462
 
451
463
  let dummyPasswordHash: string | null = null;
@@ -35,6 +35,7 @@ const ENTRIES: readonly { readonly src: string; readonly out: string }[] = [
35
35
  { src: "src/kernel/pipeline-tenant.ts", out: "dist/pipeline-tenant.js" },
36
36
  { src: "src/kernel/fx-live-stream.ts", out: "dist/fx-live-stream.js" },
37
37
  { src: "src/kernel/errors-live-resume.ts", out: "dist/errors-live-resume.js" },
38
+ { src: "src/kernel/errors-channel.ts", out: "dist/errors-channel.js" },
38
39
  { src: "src/kernel/errors-tenant.ts", out: "dist/errors-tenant.js" },
39
40
  { src: "src/kernel/clock-durable.ts", out: "dist/clock-durable.js" },
40
41
  { src: "src/kernel/clock-reconcile.ts", out: "dist/clock-reconcile.js" },