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
@@ -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" },