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
@@ -8,6 +8,7 @@
8
8
  import { z } from "zod";
9
9
  import { gate, type PolicyGateDecl } from "../elements/gate.ts";
10
10
  import type { GateDecl } from "../elements/gate/declare.ts";
11
+ import { applyBoundaryContract, stampBoundaryContract } from "../kernel/boundary-contract.ts";
11
12
  import { fail } from "../kernel/errors.ts";
12
13
  import { flow, type AnyFlowDef } from "../kernel/flow.ts";
13
14
  import type { Binding } from "../kernel/on.ts";
@@ -22,6 +23,7 @@ import {
22
23
  IdentityError,
23
24
  type IdentityStore,
24
25
  } from "./identity.ts";
26
+ import { getActiveGateAuthContext } from "./method-context.ts";
25
27
  import { PasswordPolicyError } from "./password-policy.ts";
26
28
  import {
27
29
  createSessionStore,
@@ -91,6 +93,17 @@ const SessionTokensOut = z.object({
91
93
  userId: z.string().optional(),
92
94
  });
93
95
 
96
+ const SignInOut = z.union([
97
+ SessionTokensOut,
98
+ z.object({
99
+ twoFactorRequired: z.literal(true),
100
+ challengeId: z.string(),
101
+ method: z.enum(["totp", "email_otp"]),
102
+ userId: z.string(),
103
+ devOtp: z.string().optional(),
104
+ }),
105
+ ]);
106
+
94
107
  const RefreshIn = z.object({
95
108
  refreshToken: z.string().min(1),
96
109
  });
@@ -120,6 +133,14 @@ const MeOut = z.object({
120
133
  email: z.string(),
121
134
  name: z.string(),
122
135
  emailVerified: z.boolean(),
136
+ /** Live principal scopes — for UI chrome only; Gate on Flows is real authz. */
137
+ scopes: z.array(z.string()),
138
+ /** Active tenant id when multi-tenancy is on; otherwise null. */
139
+ tenantId: z.string().nullable(),
140
+ /** Authenticating API key id when Bearer was a key secret; otherwise null. */
141
+ apiKeyId: z.string().nullable(),
142
+ /** Whether the session completed verification (email / MFA). */
143
+ sessionFresh: z.boolean().optional(),
123
144
  });
124
145
 
125
146
  /**
@@ -205,11 +226,14 @@ export function createAuthHttpBindings(
205
226
 
206
227
  const base = config.basePath;
207
228
 
208
- const refresh = flow("auth.refresh", {
209
- plane: "user",
229
+ const refreshContract = {
210
230
  in: RefreshIn,
211
231
  out: SessionTokensOut,
212
232
  errors: { AuthFailed, AuthRateLimited },
233
+ };
234
+
235
+ const refresh = flow("auth.refresh", {
236
+ plane: "user",
213
237
  do: async (input) => {
214
238
  try {
215
239
  const issued = await rotateRefresh(sessions, ctx.sessionCrypto(), input.refreshToken);
@@ -229,11 +253,14 @@ export function createAuthHttpBindings(
229
253
  },
230
254
  });
231
255
 
232
- const revoke = flow("auth.revoke", {
233
- plane: "user",
256
+ const revokeContract = {
234
257
  in: RevokeIn,
235
258
  out: z.object({ ok: z.literal(true) }),
236
259
  errors: { AuthFailed },
260
+ };
261
+
262
+ const revoke = flow("auth.revoke", {
263
+ plane: "user",
237
264
  do: async (input) => {
238
265
  if (input.refreshToken) {
239
266
  try {
@@ -248,10 +275,13 @@ export function createAuthHttpBindings(
248
275
  },
249
276
  });
250
277
 
251
- const me = flow("auth.me", {
252
- plane: "user",
278
+ const meContract = {
253
279
  out: MeOut,
254
280
  errors: { AuthFailed },
281
+ };
282
+
283
+ const me = flow("auth.me", {
284
+ plane: "user",
255
285
  do: (_input, fx) => {
256
286
  const userId = fx.auth.userId;
257
287
  if (!userId) return fail("AuthFailed", { reason: "unauthenticated" });
@@ -262,6 +292,10 @@ export function createAuthHttpBindings(
262
292
  email: user.email,
263
293
  name: user.name,
264
294
  emailVerified: user.emailVerified,
295
+ scopes: [...fx.auth.scopes],
296
+ tenantId: fx.tenant.id,
297
+ apiKeyId: fx.auth.apiKeyId ?? null,
298
+ ...(fx.auth.verified !== undefined ? { sessionFresh: fx.auth.verified } : {}),
265
299
  };
266
300
  },
267
301
  });
@@ -276,21 +310,24 @@ export function createAuthHttpBindings(
276
310
  const bindings: Binding[] = [
277
311
  bindAuthHttp(
278
312
  http
279
- .post(`${base}/refresh`)
313
+ .post(`${base}/refresh`, refreshContract)
280
314
  .public()
281
315
  .gate(...(refreshRate ? [refreshRate] : [])),
282
316
  refresh,
283
317
  ),
284
- bindAuthHttp(http.post(`${base}/revoke`).public(), revoke),
285
- bindAuthHttp(http.get(`${base}/me`).gate(AUTH_SESSION_GATE), me),
318
+ bindAuthHttp(http.post(`${base}/revoke`, revokeContract).public(), revoke),
319
+ bindAuthHttp(http.get(`${base}/me`, meContract).gate(AUTH_SESSION_GATE), me),
286
320
  ];
287
321
 
288
322
  if (config.emailAndPassword.enabled) {
289
- const signInEmail = flow("auth.signInEmail", {
290
- plane: "user",
323
+ const signInEmailContract = {
291
324
  in: EmailPasswordIn,
292
- out: SessionTokensOut,
325
+ out: SignInOut,
293
326
  errors: { AuthFailed, AuthRateLimited },
327
+ };
328
+
329
+ const signInEmail = flow("auth.signInEmail", {
330
+ plane: "user",
294
331
  do: async (input) => {
295
332
  // Rate limit is Gate KV (`signInRate` on the binding) — shared across
296
333
  // instances when `drivers.store.kv` is redis. No process-local email bag.
@@ -300,6 +337,11 @@ export function createAuthHttpBindings(
300
337
  if (config.emailAndPassword.requireEmailVerification && !user.emailVerified) {
301
338
  return fail("AuthFailed", { reason: "invalid_credentials" });
302
339
  }
340
+ const bridge = getActiveGateAuthContext()?.twoFactor;
341
+ if (bridge?.isEnabled(user.id)) {
342
+ const challenge = await bridge.beginLoginChallenge(user.id);
343
+ if (challenge) return challenge;
344
+ }
303
345
  const issued = await issueSessionWithScopes(sessions, ctx.sessionCrypto(), {
304
346
  id: user.id,
305
347
  plane: "user",
@@ -314,11 +356,14 @@ export function createAuthHttpBindings(
314
356
  },
315
357
  });
316
358
 
317
- const signUpEmail = flow("auth.signUpEmail", {
318
- plane: "user",
359
+ const signUpEmailContract = {
319
360
  in: EmailPasswordIn,
320
361
  out: SessionTokensOut,
321
362
  errors: { AuthFailed, AuthRateLimited },
363
+ };
364
+
365
+ const signUpEmail = flow("auth.signUpEmail", {
366
+ plane: "user",
322
367
  do: async (input) => {
323
368
  try {
324
369
  const user = await createUserWithPassword(identities, {
@@ -375,14 +420,14 @@ export function createAuthHttpBindings(
375
420
  bindings.push(
376
421
  bindAuthHttp(
377
422
  http
378
- .post(`${base}/sign-in/email`)
423
+ .post(`${base}/sign-in/email`, signInEmailContract)
379
424
  .public()
380
425
  .gate(...(signInRate ? [signInRate] : [])),
381
426
  signInEmail,
382
427
  ),
383
428
  bindAuthHttp(
384
429
  http
385
- .post(`${base}/sign-up/email`)
430
+ .post(`${base}/sign-up/email`, signUpEmailContract)
386
431
  .public()
387
432
  .gate(...(signUpRate ? [signUpRate] : [])),
388
433
  signUpEmail,
@@ -420,6 +465,9 @@ export function bindAuthHttp(trigger: HttpTrigger, flowDef: AnyFlowDef): Binding
420
465
  const list = flowDef.triggers as Trigger[];
421
466
  list.push(normalized);
422
467
  (flowDef as { $trigger: Trigger }).$trigger = normalized;
468
+ if (normalized.kind === "http" && normalized.contract !== undefined) {
469
+ applyBoundaryContract(flowDef, stampBoundaryContract(normalized.contract));
470
+ }
423
471
  return { trigger: normalized, flow: flowDef };
424
472
  }
425
473
 
@@ -205,8 +205,13 @@ describe("gate.auth — email Flows + security", () => {
205
205
  }),
206
206
  );
207
207
  expect(me.status).toBe(200);
208
- const meBody = (await me.json()) as { data: { email: string } };
208
+ const meBody = (await me.json()) as {
209
+ data: { email: string; scopes: string[]; tenantId: string | null; apiKeyId: string | null };
210
+ };
209
211
  expect(meBody.data.email).toBe("user@example.com");
212
+ expect(Array.isArray(meBody.data.scopes)).toBe(true);
213
+ expect(meBody.data.tenantId).toBeNull();
214
+ expect(meBody.data.apiKeyId).toBeNull();
210
215
 
211
216
  const refreshed = await app.fetch(
212
217
  new Request("http://localhost/auth/refresh", {
@@ -6,6 +6,8 @@ import type { PasswordHashOptions } from "../runtime/types.ts";
6
6
  import { createBunCrypto } from "../runtime/primitives.ts";
7
7
  import { assertNotBreached, type BreachCheckFn } from "./breach-check.ts";
8
8
  import { assertPasswordPolicy, type PasswordPolicyOptions } from "./password-policy.ts";
9
+ import { revokePrincipalSessions, type SessionStore } from "./sessions.ts";
10
+ import { okid } from "../okid.ts";
9
11
 
10
12
  /** User-plane identity row (logical). */
11
13
  export interface UserIdentityRow {
@@ -98,7 +100,7 @@ export async function ensureUserByEmail(
98
100
  const existing = store.users.get(existingId);
99
101
  if (existing) return existing;
100
102
  }
101
- const id = crypto.randomUUID();
103
+ const id = okid();
102
104
  const user: UserIdentityRow = {
103
105
  id,
104
106
  email,
@@ -140,7 +142,7 @@ export async function createUserWithPassword(
140
142
  const crypto = createBunCrypto();
141
143
  const now = options.now ?? (() => Date.now());
142
144
  const t = now();
143
- const id = options.id ?? crypto.randomUUID();
145
+ const id = options.id ?? okid();
144
146
  const user: UserIdentityRow = {
145
147
  id,
146
148
  email,
@@ -151,7 +153,7 @@ export async function createUserWithPassword(
151
153
  updatedAt: t,
152
154
  extra: { ...(options.extra ?? {}) },
153
155
  };
154
- const accountId = crypto.randomUUID();
156
+ const accountId = okid();
155
157
  const account: UserAccountRow = {
156
158
  id: accountId,
157
159
  userId: id,
@@ -277,7 +279,7 @@ export async function linkOrProvision(
277
279
  }
278
280
 
279
281
  const t = now();
280
- const userId = options.currentUserId ?? crypto.randomUUID();
282
+ const userId = options.currentUserId ?? okid();
281
283
  const existingUser = store.users.get(userId);
282
284
  const user: UserIdentityRow = existingUser ?? {
283
285
  id: userId,
@@ -290,7 +292,7 @@ export async function linkOrProvision(
290
292
  extra: {},
291
293
  };
292
294
  const account: UserAccountRow = {
293
- id: crypto.randomUUID(),
295
+ id: okid(),
294
296
  userId,
295
297
  provider,
296
298
  providerAccountId,
@@ -309,6 +311,151 @@ export async function linkOrProvision(
309
311
  return { user, account, created: true };
310
312
  }
311
313
 
314
+ /** Options for {@link completeVerifiedEmailSignIn}. */
315
+ export interface CompleteVerifiedEmailSignInOptions {
316
+ /** Passwordless provider (`magic-link` or `otp`). */
317
+ readonly provider: string;
318
+ /** Provider-scoped subject — normalized email for email OTP / magic-link. */
319
+ readonly providerAccountId: string;
320
+ /** Proven email address (ownership verified by consuming the challenge). */
321
+ readonly email: string;
322
+ /** Injectable clock. */
323
+ readonly now?: () => number;
324
+ }
325
+
326
+ /** Result of {@link completeVerifiedEmailSignIn}. */
327
+ export interface VerifiedEmailSignInResult extends LinkedCredential {
328
+ /**
329
+ * `true` when an existing unverified account was reclaimed and planted
330
+ * credentials were purged (CVE-2026-67327 pre-account hijack defense).
331
+ */
332
+ readonly credentialsCompromised: boolean;
333
+ }
334
+
335
+ /**
336
+ * Complete a proven-email passwordless sign-in (magic-link / email OTP).
337
+ *
338
+ * Unlike {@link linkOrProvision}, this path may reclaim an existing email
339
+ * owner because the challenge consumption proved inbox ownership:
340
+ * - Existing `provider:email` → re-auth (ensure `emailVerified`).
341
+ * - Unverified email owner → revoke sessions, clear password hashes, mark
342
+ * verified, attach the passwordless credential (pre-account hijack fix).
343
+ * - Verified email owner → attach passwordless credential; no credential purge.
344
+ * - No owner → provision a new verified user.
345
+ *
346
+ * @param store - Shared identity store
347
+ * @param sessions - Session store (for principal revoke on reclaim)
348
+ * @param options - Proven email + passwordless provider
349
+ */
350
+ export async function completeVerifiedEmailSignIn(
351
+ store: IdentityStore,
352
+ sessions: SessionStore,
353
+ options: CompleteVerifiedEmailSignInOptions,
354
+ ): Promise<VerifiedEmailSignInResult> {
355
+ const email = normalizeEmail(options.email);
356
+ if (!email.includes("@")) {
357
+ throw new IdentityError("invalid_email", "invalid email");
358
+ }
359
+ const providerAccountId = normalizeEmail(options.providerAccountId);
360
+ const now = options.now ?? (() => Date.now());
361
+ const t = now();
362
+ const key = `${options.provider}:${providerAccountId}`;
363
+
364
+ // 1. Existing passwordless credential → re-auth (no purge).
365
+ const existingAccountId = store.byProvider.get(key);
366
+ if (existingAccountId) {
367
+ const account = store.accounts.get(existingAccountId);
368
+ const user = account ? store.users.get(account.userId) : undefined;
369
+ if (account && user) {
370
+ if (!user.emailVerified) {
371
+ user.emailVerified = true;
372
+ user.updatedAt = t;
373
+ await store.persistUser?.(user);
374
+ }
375
+ return { user, account, created: false, credentialsCompromised: false };
376
+ }
377
+ }
378
+
379
+ const emailOwnerId = store.byEmail.get(email);
380
+ if (emailOwnerId) {
381
+ const user = store.users.get(emailOwnerId);
382
+ if (!user) {
383
+ throw new IdentityError("email_in_use", "email already registered to another credential");
384
+ }
385
+
386
+ const wasUnverified = !user.emailVerified;
387
+ if (wasUnverified) {
388
+ // Pre-account hijack: attacker planted an unverified password account.
389
+ revokePrincipalSessions(sessions, "user", user.id, t);
390
+ const persistAccountJobs: Array<void | Promise<void>> = [];
391
+ for (const account of store.accounts.values()) {
392
+ if (account.userId !== user.id || account.passwordHash === null) continue;
393
+ account.passwordHash = null;
394
+ account.updatedAt = t;
395
+ persistAccountJobs.push(store.persistAccount?.(account));
396
+ }
397
+ await Promise.all(persistAccountJobs);
398
+ user.emailVerified = true;
399
+ user.updatedAt = t;
400
+ await store.persistUser?.(user);
401
+ }
402
+
403
+ // Attach the passwordless provider if missing (reclaim or verified re-auth).
404
+ let account = existingAccountId ? store.accounts.get(existingAccountId) : undefined;
405
+ let created = false;
406
+ if (!account) {
407
+ account = {
408
+ id: okid(),
409
+ userId: user.id,
410
+ provider: options.provider,
411
+ providerAccountId,
412
+ passwordHash: null,
413
+ createdAt: t,
414
+ updatedAt: t,
415
+ };
416
+ store.accounts.set(account.id, account);
417
+ store.byProvider.set(key, account.id);
418
+ await store.persistAccount?.(account);
419
+ created = true;
420
+ }
421
+
422
+ return {
423
+ user,
424
+ account,
425
+ created,
426
+ credentialsCompromised: wasUnverified,
427
+ };
428
+ }
429
+
430
+ // 3. Fresh provision — verified from the start.
431
+ const userId = okid();
432
+ const user: UserIdentityRow = {
433
+ id: userId,
434
+ email,
435
+ name: email.split("@")[0] || "user",
436
+ emailVerified: true,
437
+ status: "active",
438
+ createdAt: t,
439
+ updatedAt: t,
440
+ extra: {},
441
+ };
442
+ const account: UserAccountRow = {
443
+ id: okid(),
444
+ userId,
445
+ provider: options.provider,
446
+ providerAccountId,
447
+ passwordHash: null,
448
+ createdAt: t,
449
+ updatedAt: t,
450
+ };
451
+ store.users.set(userId, user);
452
+ store.byEmail.set(email, userId);
453
+ store.accounts.set(account.id, account);
454
+ store.byProvider.set(key, account.id);
455
+ await Promise.all([store.persistUser?.(user), store.persistAccount?.(account)]);
456
+ return { user, account, created: true, credentialsCompromised: false };
457
+ }
458
+
312
459
  /**
313
460
  * Ensure a user row exists for `userId` (defensive — two-factor enable on an
314
461
  * established session principal).
package/src/auth/index.ts CHANGED
@@ -60,9 +60,35 @@ export {
60
60
  export {
61
61
  setActiveGateAuthContext,
62
62
  getActiveGateAuthContext,
63
+ patchActiveGateAuthContext,
63
64
  type ActiveGateAuthContext,
65
+ type TwoFactorAuthBridge,
64
66
  } from "./method-context.ts";
65
67
 
68
+ export {
69
+ createPendingTwoFactorStore,
70
+ createStepUpStore,
71
+ issueChallenge,
72
+ getChallenge,
73
+ findActiveForUser,
74
+ hasActiveChallenge,
75
+ consumeChallenge,
76
+ invalidateActiveForUser,
77
+ grantStepUp,
78
+ consumeStepUp,
79
+ hasStepUp,
80
+ twoFactorEmailOtpIdentifier,
81
+ DEFAULT_TWO_FACTOR_CHALLENGE_TTL_MS,
82
+ DEFAULT_STEP_UP_TTL_MS,
83
+ type TwoFactorMethod,
84
+ type PendingTwoFactorChallenge,
85
+ type PendingTwoFactorStore,
86
+ type StepUpGrant,
87
+ type StepUpStore,
88
+ type TwoFactorRequiredOut,
89
+ type IssueChallengeOptions,
90
+ } from "./two-factor-challenge.ts";
91
+
66
92
  export { createAuthSecondaryStorage, type AuthKv } from "./secondary-storage.ts";
67
93
 
68
94
  export {
@@ -90,6 +116,7 @@ export {
90
116
  ensureUserByEmail,
91
117
  ensureUserExists,
92
118
  linkOrProvision,
119
+ completeVerifiedEmailSignIn,
93
120
  IdentityError,
94
121
  type IdentityStore,
95
122
  type UserIdentityRow,
@@ -97,6 +124,8 @@ export {
97
124
  type CreateUserWithPasswordOptions,
98
125
  type LinkOrProvisionOptions,
99
126
  type LinkedCredential,
127
+ type CompleteVerifiedEmailSignInOptions,
128
+ type VerifiedEmailSignInResult,
100
129
  } from "./identity.ts";
101
130
 
102
131
  export {
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { OperatorInviteRow } from "./tables.ts";
6
+ import { okid } from "../okid.ts";
6
7
 
7
8
  /** Default invitation TTL (7 days). */
8
9
  export const INVITE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -41,7 +42,7 @@ export function createOperatorInvite(
41
42
  const now = options.now ?? (() => Date.now());
42
43
  const t = now();
43
44
  const row: OperatorInviteRow = {
44
- id: options.id ?? crypto.randomUUID(),
45
+ id: options.id ?? okid(),
45
46
  email: options.email,
46
47
  invitedBy: options.invitedBy,
47
48
  createdAt: t,
@@ -11,6 +11,26 @@ import type { BreachCheckFn } from "./breach-check.ts";
11
11
  import type { IdentityStore } from "./identity.ts";
12
12
  import type { PasswordPolicyOptions } from "./password-policy.ts";
13
13
  import type { SessionStore } from "./sessions.ts";
14
+ import type {
15
+ PendingTwoFactorStore,
16
+ StepUpStore,
17
+ TwoFactorRequiredOut,
18
+ } from "./two-factor-challenge.ts";
19
+ import type { VerificationStore } from "./verification.ts";
20
+
21
+ /**
22
+ * Bridge registered by `twoFactor()` so first-factor sign-in can withhold
23
+ * tokens and issue a method-locked pending challenge.
24
+ */
25
+ export interface TwoFactorAuthBridge {
26
+ /** Whether the user has an enabled second factor. */
27
+ isEnabled(userId: string): boolean;
28
+ /**
29
+ * Issue a locked login challenge (and email OTP when method is email_otp).
30
+ * Returns null when 2FA is not enabled for the user.
31
+ */
32
+ beginLoginChallenge(userId: string): Promise<TwoFactorRequiredOut | null>;
33
+ }
14
34
 
15
35
  /** Shared context for auth method plugins. */
16
36
  export interface ActiveGateAuthContext {
@@ -25,6 +45,14 @@ export interface ActiveGateAuthContext {
25
45
  readonly password?: PasswordHashOptions;
26
46
  /** From `gate.auth.breachCheck` — optional breach checker. */
27
47
  readonly breachCheck?: BreachCheckFn;
48
+ /** Shared pending login 2FA challenges (created at Gate auth wire). */
49
+ readonly pendingTwoFactor?: PendingTwoFactorStore;
50
+ /** Shared step-up grants for privileged 2FA ops. */
51
+ readonly stepUp?: StepUpStore;
52
+ /** Shared verification store for 2FA email OTP (and injectable tests). */
53
+ readonly twoFactorVerifications?: VerificationStore;
54
+ /** Registered by `.plug(twoFactor())`. */
55
+ twoFactor?: TwoFactorAuthBridge;
28
56
  }
29
57
 
30
58
  let active: ActiveGateAuthContext | undefined;
@@ -44,3 +72,16 @@ export function setActiveGateAuthContext(ctx: ActiveGateAuthContext | undefined)
44
72
  export function getActiveGateAuthContext(): ActiveGateAuthContext | undefined {
45
73
  return active;
46
74
  }
75
+
76
+ /**
77
+ * Mutate the active context in place (e.g. register `twoFactor` bridge).
78
+ *
79
+ * @param patch - Fields to merge onto the active context
80
+ */
81
+ export function patchActiveGateAuthContext(
82
+ patch: Partial<ActiveGateAuthContext>,
83
+ ): ActiveGateAuthContext | undefined {
84
+ if (!active) return undefined;
85
+ active = { ...active, ...patch };
86
+ return active;
87
+ }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { OAuthError } from "./errors.ts";
11
11
  import type { SigningKeyRow } from "./tables.ts";
12
+ import { okid } from "../../okid.ts";
12
13
 
13
14
  /** Default access-token TTL (10 minutes — short-lived per OAuth 2.1 posture). */
14
15
  export const OAUTH_ACCESS_TTL_MS = 10 * 60_000;
@@ -210,7 +211,7 @@ export async function createDpopSigner(): Promise<{
210
211
  publicJwk,
211
212
  async prove({ htm, htu, now = Date.now(), accessToken, nonce }) {
212
213
  const claims: Record<string, unknown> = {
213
- jti: crypto.randomUUID(),
214
+ jti: okid(),
214
215
  htm,
215
216
  htu: normalizeHtu(htu),
216
217
  iat: Math.floor(now / 1000),
@@ -11,6 +11,7 @@ import type {
11
11
  ClientCacheRow,
12
12
  ConsentRow,
13
13
  } from "./tables.ts";
14
+ import { okid } from "../../okid.ts";
14
15
 
15
16
  /**
16
17
  * Create an empty AS store bundle.
@@ -57,5 +58,5 @@ export async function hashSecret(raw: string): Promise<string> {
57
58
 
58
59
  /** Cryptographically random id (no dashes), same hygiene as session ids. */
59
60
  export function cryptoId(): string {
60
- return crypto.randomUUID().replace(/-/g, "");
61
+ return okid();
61
62
  }
@@ -12,6 +12,7 @@ import { createBunCrypto } from "../runtime/primitives.ts";
12
12
  import { assertNotBreached, type BreachCheckFn } from "./breach-check.ts";
13
13
  import { assertPasswordPolicy, type PasswordPolicyOptions } from "./password-policy.ts";
14
14
  import type { OperatorCredentialRow, OperatorRow, OperatorSsoLinkRow } from "./tables.ts";
15
+ import { okid } from "../okid.ts";
15
16
 
16
17
  /** Operator-plane store. */
17
18
  export interface OperatorStore {
@@ -74,7 +75,7 @@ export async function createOperator(
74
75
  }
75
76
  await assertNotBreached(options.password, options.breachCheck);
76
77
  const crypto = createBunCrypto();
77
- const id = options.id ?? crypto.randomUUID();
78
+ const id = options.id ?? okid();
78
79
  const row: OperatorRow = {
79
80
  id,
80
81
  email: options.email,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Defense-in-depth: session access JWT must allowlist HS256 and reject
3
+ * spoofed alg headers / non-JWS compact shapes (JWE = 5 parts).
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { createSessionStore, issueSession, SessionError, verifyAccess } from "./sessions.ts";
8
+
9
+ const SECRET = "test-secret-at-least-32-bytes-long!!";
10
+
11
+ function b64url(s: string): string {
12
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
13
+ }
14
+
15
+ async function hmacSha256B64url(secret: string, data: string): Promise<string> {
16
+ const key = await crypto.subtle.importKey(
17
+ "raw",
18
+ new TextEncoder().encode(secret),
19
+ { name: "HMAC", hash: "SHA-256" },
20
+ false,
21
+ ["sign"],
22
+ );
23
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
24
+ let bin = "";
25
+ for (const b of new Uint8Array(sig)) bin += String.fromCharCode(b);
26
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
27
+ }
28
+
29
+ describe("session access JWT alg enforcement", () => {
30
+ test("rejects alg: none", async () => {
31
+ const store = createSessionStore();
32
+ const header = b64url(JSON.stringify({ alg: "none", typ: "JWT" }));
33
+ const payload = b64url(
34
+ JSON.stringify({
35
+ sub: "u1",
36
+ plane: "user",
37
+ sid: "s1",
38
+ scopes: [],
39
+ iat: 1,
40
+ exp: 9_999_999_999_999,
41
+ }),
42
+ );
43
+ const token = `${header}.${payload}.`;
44
+ await expect(verifyAccess(store, SECRET, token)).rejects.toThrow(SessionError);
45
+ await expect(verifyAccess(store, SECRET, token)).rejects.toThrow(
46
+ "unsupported access token alg",
47
+ );
48
+ });
49
+
50
+ test("rejects mismatched alg even when HMAC is valid", async () => {
51
+ const store = createSessionStore();
52
+ const now = 1_700_000_000_000;
53
+ const issued = await issueSession(
54
+ store,
55
+ { secret: SECRET, now: () => now },
56
+ { id: "u1", plane: "user", scopes: ["read"] },
57
+ );
58
+ const payloadPart = issued.accessToken.split(".")[1]!;
59
+ const forgedHeader = b64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
60
+ const data = `${forgedHeader}.${payloadPart}`;
61
+ const sig = await hmacSha256B64url(SECRET, data);
62
+ const forged = `${data}.${sig}`;
63
+ await expect(verifyAccess(store, SECRET, forged, { now: () => now })).rejects.toThrow(
64
+ SessionError,
65
+ );
66
+ await expect(verifyAccess(store, SECRET, forged, { now: () => now })).rejects.toThrow(
67
+ "unsupported access token alg",
68
+ );
69
+ });
70
+
71
+ test("rejects JWE compact serialization (5 parts)", async () => {
72
+ const store = createSessionStore();
73
+ const jwe = "a.b.c.d.e";
74
+ await expect(verifyAccess(store, SECRET, jwe)).rejects.toThrow(SessionError);
75
+ await expect(verifyAccess(store, SECRET, jwe)).rejects.toThrow("malformed access token");
76
+ });
77
+ });