okengine 0.18.5 → 0.19.1

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/llms-txt.mdx +1 -1
  5. package/site/content/docs/ai/skills.mdx +4 -1
  6. package/site/content/docs/client/auth.mdx +208 -0
  7. package/site/content/docs/client/calling.mdx +451 -0
  8. package/site/content/docs/client/index.mdx +208 -0
  9. package/site/content/docs/client/live.mdx +234 -0
  10. package/site/content/docs/client/meta.json +5 -0
  11. package/site/content/docs/client/react.mdx +249 -0
  12. package/site/content/docs/elements/ai/agents.mdx +235 -0
  13. package/site/content/docs/elements/ai/index.mdx +342 -0
  14. package/site/content/docs/elements/ai/mcp.mdx +275 -0
  15. package/site/content/docs/elements/ai/meta.json +5 -0
  16. package/site/content/docs/elements/ai/models.mdx +283 -0
  17. package/site/content/docs/elements/ai/prompts.mdx +257 -0
  18. package/site/content/docs/elements/channel/email.mdx +269 -0
  19. package/site/content/docs/elements/channel/index.mdx +344 -0
  20. package/site/content/docs/elements/channel/meta.json +5 -0
  21. package/site/content/docs/elements/channel/push.mdx +218 -0
  22. package/site/content/docs/elements/channel/receipts.mdx +206 -0
  23. package/site/content/docs/elements/channel/sms.mdx +264 -0
  24. package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
  25. package/site/content/docs/elements/clock/index.mdx +301 -0
  26. package/site/content/docs/elements/clock/meta.json +5 -0
  27. package/site/content/docs/elements/clock/schedules.mdx +508 -0
  28. package/site/content/docs/elements/clock/sleep.mdx +448 -0
  29. package/site/content/docs/elements/flow/consumers.mdx +707 -0
  30. package/site/content/docs/elements/flow/http.mdx +1080 -0
  31. package/site/content/docs/elements/flow/index.mdx +618 -0
  32. package/site/content/docs/elements/flow/meta.json +5 -0
  33. package/site/content/docs/elements/flow/routing.mdx +559 -0
  34. package/site/content/docs/elements/flow/workflows.mdx +640 -0
  35. package/site/content/docs/elements/gate/auth.mdx +371 -0
  36. package/site/content/docs/elements/gate/authorization.mdx +298 -0
  37. package/site/content/docs/elements/gate/index.mdx +418 -0
  38. package/site/content/docs/elements/gate/meta.json +5 -0
  39. package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
  40. package/site/content/docs/elements/gate/rls.mdx +303 -0
  41. package/site/content/docs/elements/gate/tenancy.mdx +375 -0
  42. package/site/content/docs/elements/index.mdx +20 -10
  43. package/site/content/docs/elements/signal/broadcast.mdx +519 -0
  44. package/site/content/docs/elements/signal/index.mdx +352 -0
  45. package/site/content/docs/elements/signal/live.mdx +591 -0
  46. package/site/content/docs/elements/signal/meta.json +5 -0
  47. package/site/content/docs/elements/signal/once.mdx +597 -0
  48. package/site/content/docs/elements/store/files.mdx +667 -0
  49. package/site/content/docs/elements/store/index.mdx +313 -0
  50. package/site/content/docs/elements/store/kv.mdx +637 -0
  51. package/site/content/docs/elements/store/meta.json +5 -0
  52. package/site/content/docs/elements/store/search.mdx +950 -0
  53. package/site/content/docs/elements/store/sql.mdx +941 -0
  54. package/site/content/docs/elements/vault/config.mdx +259 -0
  55. package/site/content/docs/elements/vault/index.mdx +356 -0
  56. package/site/content/docs/elements/vault/meta.json +5 -0
  57. package/site/content/docs/elements/vault/rotation.mdx +276 -0
  58. package/site/content/docs/elements/vault/secrets.mdx +288 -0
  59. package/site/content/docs/index.mdx +52 -40
  60. package/site/content/docs/meta.json +8 -7
  61. package/site/content/docs/plugins/cors.mdx +2 -1
  62. package/site/content/docs/plugins/csrf.mdx +4 -1
  63. package/site/content/docs/plugins/headers.mdx +2 -1
  64. package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
  65. package/site/content/docs/plugins/magic-link.mdx +6 -0
  66. package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
  67. package/site/content/docs/plugins/oauth.mdx +1 -1
  68. package/site/content/docs/plugins/otp.mdx +12 -1
  69. package/site/content/docs/plugins/passkey.mdx +39 -15
  70. package/site/content/docs/plugins/two-factor.mdx +91 -34
  71. package/site/content/docs/plugins/username.mdx +1 -1
  72. package/site/content/docs/recipes/caddy.mdx +3 -3
  73. package/site/content/docs/recipes/index.mdx +18 -28
  74. package/site/content/docs/recipes/mailpit.mdx +1 -1
  75. package/site/content/docs/recipes/meilisearch.mdx +3 -3
  76. package/site/content/docs/recipes/meta.json +1 -4
  77. package/site/content/docs/recipes/nginx.mdx +2 -2
  78. package/site/content/docs/recipes/openrouter.mdx +283 -0
  79. package/site/content/docs/recipes/pgdog.mdx +3 -3
  80. package/site/content/docs/recipes/postgres.mdx +1 -2
  81. package/site/content/docs/recipes/rustfs.mdx +2 -2
  82. package/site/content/docs/recipes/traefik.mdx +3 -3
  83. package/site/content/docs/reference/cli.mdx +231 -0
  84. package/site/content/docs/reference/configuration.mdx +30 -10
  85. package/site/content/docs/reference/environment-variables.mdx +93 -20
  86. package/site/content/docs/reference/errors.mdx +132 -29
  87. package/site/content/docs/reference/fx.mdx +138 -73
  88. package/site/content/docs/reference/index.mdx +13 -21
  89. package/site/content/docs/reference/meta.json +4 -5
  90. package/site/content/docs/reference/okid.mdx +42 -17
  91. package/site/content/docs/reference/plugins.mdx +4 -3
  92. package/site/content/docs/reference/security.mdx +197 -0
  93. package/site/content/docs/understand/meta.json +5 -0
  94. package/site/content/docs/understand/the-architecture.mdx +264 -0
  95. package/site/content/docs/understand/try-it.mdx +58 -0
  96. package/src/auth/api-keys.ts +2 -1
  97. package/src/auth/bindings.ts +64 -16
  98. package/src/auth/gate-auth.test.ts +6 -1
  99. package/src/auth/identity.ts +152 -5
  100. package/src/auth/index.ts +29 -0
  101. package/src/auth/invites.ts +2 -1
  102. package/src/auth/method-context.ts +41 -0
  103. package/src/auth/oauth-as/crypto.ts +2 -1
  104. package/src/auth/oauth-as/stores.ts +2 -1
  105. package/src/auth/operator.ts +2 -1
  106. package/src/auth/sessions-jwt.test.ts +77 -0
  107. package/src/auth/sessions.ts +20 -4
  108. package/src/auth/tenants.ts +3 -2
  109. package/src/auth/two-factor-challenge.test.ts +61 -0
  110. package/src/auth/two-factor-challenge.ts +284 -0
  111. package/src/auth/verification.ts +5 -0
  112. package/src/bench/REPORT.md +80 -0
  113. package/src/bench/g03-signal-once.bench.ts +1 -1
  114. package/src/bench/g10-observability-contention.bench.ts +1 -1
  115. package/src/bench/g17-hybrid-search.bench.ts +572 -0
  116. package/src/bench/load-app.ts +2 -10
  117. package/src/cli/ai-setup/ai-setup.test.ts +331 -42
  118. package/src/cli/ai-setup/apply.ts +294 -52
  119. package/src/cli/ai-setup/catalog.ts +238 -1342
  120. package/src/cli/ai-setup/index.ts +14 -99
  121. package/src/cli/ai-setup/prompts.ts +60 -443
  122. package/src/cli/ask-seed.test.ts +96 -0
  123. package/src/cli/ask-seed.ts +82 -0
  124. package/src/cli/ask-vault-gaps.test.ts +84 -0
  125. package/src/cli/ask-vault-gaps.ts +146 -0
  126. package/src/cli/build.ts +2 -2
  127. package/src/cli/client-add.test.ts +26 -1
  128. package/src/cli/client-add.ts +106 -27
  129. package/src/cli/db-seed.ts +2 -0
  130. package/src/cli/db.ts +150 -11
  131. package/src/cli/dev.ts +119 -211
  132. package/src/cli/docker-clean.ts +2 -2
  133. package/src/cli/docker-cli.test.ts +1 -1
  134. package/src/cli/doctor-diff.ts +4 -2
  135. package/src/cli/doctor-pii.test.ts +1 -1
  136. package/src/cli/doctor.ts +14 -1
  137. package/src/cli/hero-meta.test.ts +4 -2
  138. package/src/cli/load-config.images.test.ts +8 -10
  139. package/src/cli/load-config.ts +1 -1
  140. package/src/cli/registry.ts +22 -10
  141. package/src/cli/replay.ts +3 -1
  142. package/src/cli/start.ts +1 -1
  143. package/src/cli/tui/keys.ts +2 -3
  144. package/src/client/auth/cookies.ts +74 -0
  145. package/src/client/auth/create-auth-client.ts +636 -0
  146. package/src/client/auth/denials.ts +99 -0
  147. package/src/client/auth/session.ts +283 -0
  148. package/src/client/auth.test.ts +251 -0
  149. package/src/client/auth.ts +39 -114
  150. package/src/client/create-with-session.ts +245 -0
  151. package/src/client/create.ts +51 -5
  152. package/src/client/index.ts +11 -1
  153. package/src/client/live.ts +11 -79
  154. package/src/client/notes-contract.test.ts +5 -10
  155. package/src/client/sse.ts +137 -0
  156. package/src/client/stream.ts +147 -0
  157. package/src/client/transport.test.ts +32 -0
  158. package/src/client/transport.ts +123 -26
  159. package/src/client/types.ts +118 -14
  160. package/src/client-react/index.ts +185 -24
  161. package/src/client-react/use-live-query.ts +13 -3
  162. package/src/compiler/aot.test.ts +7 -4
  163. package/src/compiler/effects-embed.test.ts +56 -0
  164. package/src/compiler/effects-fetch.test.ts +43 -0
  165. package/src/compiler/effects-infer.ts +58 -2
  166. package/src/compiler/extract.test.ts +257 -43
  167. package/src/compiler/extract.ts +566 -48
  168. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
  169. package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
  170. package/src/compiler/fixtures/skyport.expected.json +4 -4
  171. package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
  172. package/src/compiler/response.ts +2 -2
  173. package/src/compiler/search-writer-isolation.test.ts +40 -0
  174. package/src/config/index.ts +11 -0
  175. package/src/console/server/app.ts +17 -23
  176. package/src/console/server/bind.ts +4 -0
  177. package/src/console/server/console.test.ts +2 -2
  178. package/src/console/server/flows-invoke.test.ts +50 -34
  179. package/src/console/server/flows.ts +436 -255
  180. package/src/console/server/runs-ingest.test.ts +6 -3
  181. package/src/console/server/serve.ts +1 -1
  182. package/src/console/server/signals.test.ts +1 -5
  183. package/src/console/server/signals.ts +8 -6
  184. package/src/console/server/state.ts +6 -1
  185. package/src/console/server/store.ts +1 -1
  186. package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
  187. package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
  188. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
  189. package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
  190. package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
  191. package/src/console/ui-next/dist/assets/access-page-C_qLDhTq.js +4 -0
  192. package/src/console/ui-next/dist/assets/agent-disclosure-BHVqr3TN.js +1 -0
  193. package/src/console/ui-next/dist/assets/cache-glyph-CKe92lRQ.js +1 -0
  194. package/src/console/ui-next/dist/assets/call-pii-button-DEwTl8ZX.js +1 -0
  195. package/src/console/ui-next/dist/assets/collapsible-BCBtDrCt.js +1 -0
  196. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
  197. package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
  198. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
  199. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
  200. package/src/console/ui-next/dist/assets/duration-tone-JroqeuCp.js +9 -0
  201. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
  202. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
  203. package/src/console/ui-next/dist/assets/flows-page-CVHa0RTt.js +1 -0
  204. package/src/console/ui-next/dist/assets/highlighted-json-DjJW6hqe.js +154 -0
  205. package/src/console/ui-next/dist/assets/http-method-DC5HBdLU.js +1 -0
  206. package/src/console/ui-next/dist/assets/index-CYjiZ3WO.js +63 -0
  207. package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
  208. package/src/console/ui-next/dist/assets/observability-page-CAYMyKb3.js +4 -0
  209. package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
  210. package/src/console/ui-next/dist/assets/replica-lag-yAQYLv75.js +18 -0
  211. package/src/console/ui-next/dist/assets/request-meta-D0yusGxJ.js +1 -0
  212. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
  213. package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
  214. package/src/console/ui-next/dist/assets/store-page-BTKJeJ02.js +41 -0
  215. package/src/console/ui-next/dist/assets/trace-detail-sheet-Bp-Yygs5.js +2 -0
  216. package/src/console/ui-next/dist/assets/tree-expand-toggle-DoaVDfAM.js +55 -0
  217. package/src/console/ui-next/dist/assets/units-page-BRz7xyYL.js +1 -0
  218. package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
  219. package/src/console/ui-next/dist/assets/vault-page-3jQt-bOJ.js +2 -0
  220. package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
  221. package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
  222. package/src/console/ui-next/dist/index.html +6 -13
  223. package/src/console/ui-next/seed-invoke-host.ts +21 -18
  224. package/src/console/ui-next/src/client.ts +7 -2
  225. package/src/console/ui-next/src/features/flows/graph/element-map.test.ts +1 -1
  226. package/src/console/ui-next/src/features/flows/graph/element-map.ts +5 -3
  227. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
  228. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
  229. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
  230. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +4 -5
  231. package/src/console/ui-next/src/features/flows/traces/trace-gates.ts +3 -6
  232. package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
  233. package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
  234. package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
  235. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
  236. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
  237. package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
  238. package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
  239. package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
  240. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
  241. package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
  242. package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
  243. package/src/docker/ai-model-status.test.ts +1 -12
  244. package/src/docker/ai-model-status.ts +10 -38
  245. package/src/docker/compose-up.test.ts +100 -0
  246. package/src/docker/compose-up.ts +292 -0
  247. package/src/docker/compose.ts +4 -14
  248. package/src/docker/derive.ts +9 -34
  249. package/src/docker/docker.test.ts +16 -261
  250. package/src/docker/images-config.test.ts +13 -25
  251. package/src/docker/index.ts +1 -24
  252. package/src/docker/recipes/index.ts +0 -23
  253. package/src/docker/recipes/pgdog.ts +6 -4
  254. package/src/docker/stack-id.test.ts +2 -2
  255. package/src/docker/stack-id.ts +0 -1
  256. package/src/docker/types.ts +2 -2
  257. package/src/drivers/ai-anthropic.ts +10 -0
  258. package/src/drivers/ai-openai-compatible.ts +34 -7
  259. package/src/drivers/ai-providers.test.ts +0 -107
  260. package/src/drivers/ai-stream.test.ts +0 -34
  261. package/src/drivers/ai-types.ts +17 -8
  262. package/src/drivers/channel-fcm.ts +6 -1
  263. package/src/drivers/channel-msegat.ts +6 -1
  264. package/src/drivers/channel-resend.ts +5 -1
  265. package/src/drivers/channel-smtp.ts +5 -1
  266. package/src/drivers/channel-sndr.ts +10 -1
  267. package/src/drivers/channel-taqnyat-mail.ts +5 -1
  268. package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
  269. package/src/drivers/channel-taqnyat.ts +6 -1
  270. package/src/drivers/channel-types.ts +10 -0
  271. package/src/drivers/channel-unifonic.ts +6 -1
  272. package/src/drivers/channel-wa-cloud.ts +6 -1
  273. package/src/drivers/channel-webpush.ts +6 -1
  274. package/src/drivers/external.ts +33 -0
  275. package/src/drivers/index.ts +0 -10
  276. package/src/drivers/meilisearch.ts +6 -0
  277. package/src/drivers/oauth-types.ts +4 -0
  278. package/src/drivers/postgres.test.ts +74 -3
  279. package/src/drivers/postgres.ts +175 -6
  280. package/src/drivers/signal-types.ts +5 -5
  281. package/src/drivers/types.ts +7 -0
  282. package/src/drivers/vault-types.ts +5 -0
  283. package/src/elements/ai/declare.ts +17 -4
  284. package/src/elements/ai/eval.ts +1 -1
  285. package/src/elements/ai/mcp-http.ts +1 -1
  286. package/src/elements/ai/pii.ts +1 -1
  287. package/src/elements/ai/providers.test.ts +289 -0
  288. package/src/elements/ai/providers.ts +169 -0
  289. package/src/elements/ai/runtime.ts +68 -5
  290. package/src/elements/ai/schema.ts +2 -2
  291. package/src/elements/ai.test.ts +7 -7
  292. package/src/elements/ai.ts +17 -1
  293. package/src/elements/channel/runtime.ts +105 -12
  294. package/src/elements/clock/cron-fields.test.ts +144 -0
  295. package/src/elements/clock/cron-fields.ts +185 -0
  296. package/src/elements/clock/declare.ts +247 -3
  297. package/src/elements/clock.ts +21 -1
  298. package/src/elements/gate/declare.ts +1 -1
  299. package/src/elements/gate.ts +1 -1
  300. package/src/elements/index.ts +15 -0
  301. package/src/elements/signal/chaos-child.ts +1 -2
  302. package/src/elements/signal/declare.ts +65 -37
  303. package/src/elements/signal/delivery-modes.test.ts +11 -31
  304. package/src/elements/signal/dry-run-replay.test.ts +1 -7
  305. package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
  306. package/src/elements/signal/key-ordering.test.ts +6 -26
  307. package/src/elements/signal/lease-reclaim.test.ts +2 -6
  308. package/src/elements/signal/optional-emit.test.ts +6 -9
  309. package/src/elements/signal/order-lifecycle.test.ts +4 -17
  310. package/src/elements/signal/orphan-messages.test.ts +4 -15
  311. package/src/elements/signal/reconcile.test.ts +2 -2
  312. package/src/elements/signal/runtime.ts +1 -1
  313. package/src/elements/signal/schema-emit.test.ts +4 -8
  314. package/src/elements/signal.test.ts +24 -49
  315. package/src/elements/signal.ts +9 -2
  316. package/src/elements/store/domain-ddl.test.ts +1 -1
  317. package/src/elements/store/live-http.test.ts +9 -4
  318. package/src/elements/store/prepare-row.test.ts +123 -0
  319. package/src/elements/store/resource.ts +43 -12
  320. package/src/elements/store/schema-decl.ts +117 -0
  321. package/src/elements/store/search-backfill.ts +205 -0
  322. package/src/elements/store/search-bind.ts +66 -0
  323. package/src/elements/store/search-bm25.ts +78 -0
  324. package/src/elements/store/search-ddl.ts +156 -0
  325. package/src/elements/store/search-embed-flow.ts +141 -0
  326. package/src/elements/store/search-errors.ts +34 -0
  327. package/src/elements/store/search-fusion.ts +112 -0
  328. package/src/elements/store/search-lsh.ts +214 -0
  329. package/src/elements/store/search-runtime.pglite.test.ts +186 -0
  330. package/src/elements/store/search-runtime.ts +314 -0
  331. package/src/elements/store/search.test.ts +226 -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 +52 -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 +2 -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 -144
  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 -114
  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-3-EFj-2G.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-cVFnA4HH.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-CMIgUbD0.js +0 -66
  484. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
  485. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
  486. package/src/console/ui-next/dist/assets/observability-page-CQ3p34ip.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-l8FeKfnP.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-CL-d_mLE.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
@@ -407,17 +407,23 @@ export class SessionError extends Error {
407
407
  }
408
408
  }
409
409
 
410
+ /** Compact JWS algorithm for session access tokens (HMAC-SHA256). */
411
+ const ACCESS_TOKEN_ALG = "HS256" as const;
412
+
410
413
  async function hashToken(raw: string): Promise<string> {
411
414
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(raw));
412
415
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
413
416
  }
414
417
 
415
418
  function cryptoRandomId(): string {
416
- return crypto.randomUUID().replace(/-/g, "");
419
+ // Refresh / opaque session secrets — not application ids (those use OKID).
420
+ const bytes = new Uint8Array(32);
421
+ crypto.getRandomValues(bytes);
422
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
417
423
  }
418
424
 
419
425
  async function signAccess(secret: string, claims: AccessClaims): Promise<string> {
420
- const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
426
+ const header = b64url(JSON.stringify({ alg: ACCESS_TOKEN_ALG, typ: "JWT" }));
421
427
  const payload = b64url(JSON.stringify(claims));
422
428
  const data = `${header}.${payload}`;
423
429
  const sig = await hmac(secret, data);
@@ -426,9 +432,19 @@ async function signAccess(secret: string, claims: AccessClaims): Promise<string>
426
432
 
427
433
  async function verifyAccessSignature(secret: string, token: string): Promise<AccessClaims> {
428
434
  const parts = token.split(".");
435
+ // JWS compact = 3 parts; JWE compact = 5 — reject anything else before crypto.
429
436
  if (parts.length !== 3) throw new SessionError("malformed access token");
430
- const [header, payload, sig] = parts as [string, string, string];
431
- const data = `${header}.${payload}`;
437
+ const [headerPart, payload, sig] = parts as [string, string, string];
438
+ let header: { alg?: string };
439
+ try {
440
+ header = JSON.parse(b64urlDecode(headerPart)) as { alg?: string };
441
+ } catch {
442
+ throw new SessionError("malformed access token header");
443
+ }
444
+ if (header.alg !== ACCESS_TOKEN_ALG) {
445
+ throw new SessionError("unsupported access token alg");
446
+ }
447
+ const data = `${headerPart}.${payload}`;
432
448
  const expected = await hmac(secret, data);
433
449
  if (sig !== expected) throw new SessionError("invalid access token signature");
434
450
  return JSON.parse(b64urlDecode(payload)) as AccessClaims;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { isApplicationScope } from "../elements/gate/permissions.ts";
9
9
  import type { TenantMemberRow, TenantRoleRow, TenantRow } from "./tables.ts";
10
+ import { okid } from "../okid.ts";
10
11
 
11
12
  export type { TenantMemberRow, TenantRoleRow, TenantRow };
12
13
 
@@ -99,7 +100,7 @@ export async function createTenant(
99
100
  ): Promise<TenantRow> {
100
101
  const now = options.now ?? (() => Date.now());
101
102
  const t = now();
102
- const id = options.id ?? crypto.randomUUID();
103
+ const id = options.id ?? okid();
103
104
  if (store.tenants.has(id)) {
104
105
  throw new TenantError("tenant_exists", `tenant already exists: ${id}`);
105
106
  }
@@ -180,7 +181,7 @@ export function addMember(store: TenantStore, options: AddMemberOptions): Tenant
180
181
  }
181
182
  const now = options.now ?? (() => Date.now());
182
183
  const row: TenantMemberRow = {
183
- id: options.id ?? crypto.randomUUID(),
184
+ id: options.id ?? okid(),
184
185
  tenantId: options.tenantId,
185
186
  userId: options.userId,
186
187
  role: options.role ?? DEFAULT_TENANT_ROLE,
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Pending 2FA challenge + step-up grant store.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ consumeChallenge,
8
+ consumeStepUp,
9
+ createPendingTwoFactorStore,
10
+ createStepUpStore,
11
+ findActiveForUser,
12
+ getChallenge,
13
+ grantStepUp,
14
+ hasActiveChallenge,
15
+ issueChallenge,
16
+ twoFactorEmailOtpIdentifier,
17
+ } from "./two-factor-challenge.ts";
18
+
19
+ describe("pending two-factor challenge store", () => {
20
+ test("issue locks method and replaces prior active challenge", () => {
21
+ const store = createPendingTwoFactorStore();
22
+ const first = issueChallenge(store, {
23
+ userId: "u1",
24
+ method: "email_otp",
25
+ now: 1_000,
26
+ ttlMs: 60_000,
27
+ });
28
+ expect(first.method).toBe("email_otp");
29
+ expect(hasActiveChallenge(store, "u1", 1_000)).toBe(true);
30
+
31
+ const second = issueChallenge(store, {
32
+ userId: "u1",
33
+ method: "totp",
34
+ now: 2_000,
35
+ ttlMs: 60_000,
36
+ });
37
+ expect(second.id).not.toBe(first.id);
38
+ expect(getChallenge(store, first.id, 2_000)).toBeUndefined();
39
+ expect(findActiveForUser(store, "u1", 2_000)?.method).toBe("totp");
40
+ });
41
+
42
+ test("consume clears active pointer", () => {
43
+ const store = createPendingTwoFactorStore();
44
+ const row = issueChallenge(store, { userId: "u1", method: "totp", now: 1_000 });
45
+ consumeChallenge(store, row, 1_500);
46
+ expect(hasActiveChallenge(store, "u1", 1_500)).toBe(false);
47
+ expect(getChallenge(store, row.id, 1_500)).toBeUndefined();
48
+ });
49
+
50
+ test("step-up grant is single-use and purpose-scoped", () => {
51
+ const stepUp = createStepUpStore();
52
+ grantStepUp(stepUp, "u1", "change", 1_000, 60_000);
53
+ expect(consumeStepUp(stepUp, "u1", "enroll", 1_100)).toBe(false);
54
+ expect(consumeStepUp(stepUp, "u1", ["change", "enroll"], 1_100)).toBe(true);
55
+ expect(consumeStepUp(stepUp, "u1", "change", 1_200)).toBe(false);
56
+ });
57
+
58
+ test("twoFactorEmailOtpIdentifier namespaces by user", () => {
59
+ expect(twoFactorEmailOtpIdentifier("abc")).toBe("2fa:abc");
60
+ });
61
+ });
@@ -0,0 +1,284 @@
1
+ import { okid } from "../okid.ts";
2
+ /**
3
+ * Pending login 2FA challenges + short-lived step-up grants.
4
+ *
5
+ * One active unresolved challenge per user; method is locked at issue time
6
+ * so enrollment / method-switch cannot bypass the configured factor.
7
+ */
8
+
9
+ /** Locked second-factor method for a login challenge. */
10
+ export type TwoFactorMethod = "totp" | "email_otp";
11
+
12
+ /** Default login-challenge TTL (align with OTP). */
13
+ export const DEFAULT_TWO_FACTOR_CHALLENGE_TTL_MS = 10 * 60 * 1000;
14
+
15
+ /** Default step-up grant TTL for privileged 2FA ops. */
16
+ export const DEFAULT_STEP_UP_TTL_MS = 5 * 60 * 1000;
17
+
18
+ /** One pending login 2FA challenge. */
19
+ export interface PendingTwoFactorChallenge {
20
+ id: string;
21
+ userId: string;
22
+ method: TwoFactorMethod;
23
+ createdAt: number;
24
+ expiresAt: number;
25
+ consumedAt: number | null;
26
+ }
27
+
28
+ /** In-memory pending 2FA challenge store. */
29
+ export interface PendingTwoFactorStore {
30
+ readonly byId: Map<string, PendingTwoFactorChallenge>;
31
+ /** userId → active challenge id (at most one). */
32
+ readonly byUserId: Map<string, string>;
33
+ }
34
+
35
+ /**
36
+ * Create an empty pending 2FA challenge store.
37
+ */
38
+ export function createPendingTwoFactorStore(): PendingTwoFactorStore {
39
+ return { byId: new Map(), byUserId: new Map() };
40
+ }
41
+
42
+ /** Options for {@link issueChallenge}. */
43
+ export interface IssueChallengeOptions {
44
+ readonly userId: string;
45
+ readonly method: TwoFactorMethod;
46
+ readonly now?: number;
47
+ readonly ttlMs?: number;
48
+ }
49
+
50
+ /**
51
+ * Invalidate any prior active challenge for the user and issue a new locked one.
52
+ *
53
+ * @param store - Pending store
54
+ * @param options - User, method, clock
55
+ */
56
+ export function issueChallenge(
57
+ store: PendingTwoFactorStore,
58
+ options: IssueChallengeOptions,
59
+ ): PendingTwoFactorChallenge {
60
+ const now = options.now ?? Date.now();
61
+ const ttlMs = options.ttlMs ?? DEFAULT_TWO_FACTOR_CHALLENGE_TTL_MS;
62
+ invalidateActiveForUser(store, options.userId, now);
63
+ const row: PendingTwoFactorChallenge = {
64
+ id: okid(),
65
+ userId: options.userId,
66
+ method: options.method,
67
+ createdAt: now,
68
+ expiresAt: now + ttlMs,
69
+ consumedAt: null,
70
+ };
71
+ store.byId.set(row.id, row);
72
+ store.byUserId.set(options.userId, row.id);
73
+ return row;
74
+ }
75
+
76
+ /**
77
+ * Find a non-consumed, non-expired challenge by id.
78
+ *
79
+ * @param store - Pending store
80
+ * @param challengeId - Challenge id
81
+ * @param now - Clock
82
+ */
83
+ export function getChallenge(
84
+ store: PendingTwoFactorStore,
85
+ challengeId: string,
86
+ now: number = Date.now(),
87
+ ): PendingTwoFactorChallenge | undefined {
88
+ const row = store.byId.get(challengeId);
89
+ if (!row) return undefined;
90
+ if (row.consumedAt !== null) return undefined;
91
+ if (row.expiresAt <= now) {
92
+ clearUserPointer(store, row);
93
+ return undefined;
94
+ }
95
+ return row;
96
+ }
97
+
98
+ /**
99
+ * Find the active unresolved challenge for a user, if any.
100
+ *
101
+ * @param store - Pending store
102
+ * @param userId - User id
103
+ * @param now - Clock
104
+ */
105
+ export function findActiveForUser(
106
+ store: PendingTwoFactorStore,
107
+ userId: string,
108
+ now: number = Date.now(),
109
+ ): PendingTwoFactorChallenge | undefined {
110
+ const id = store.byUserId.get(userId);
111
+ if (!id) return undefined;
112
+ const row = getChallenge(store, id, now);
113
+ if (!row) {
114
+ store.byUserId.delete(userId);
115
+ return undefined;
116
+ }
117
+ return row;
118
+ }
119
+
120
+ /**
121
+ * True when the user has an active unresolved login 2FA challenge.
122
+ *
123
+ * @param store - Pending store
124
+ * @param userId - User id
125
+ * @param now - Clock
126
+ */
127
+ export function hasActiveChallenge(
128
+ store: PendingTwoFactorStore,
129
+ userId: string,
130
+ now: number = Date.now(),
131
+ ): boolean {
132
+ return findActiveForUser(store, userId, now) !== undefined;
133
+ }
134
+
135
+ /**
136
+ * Consume a challenge after successful verification.
137
+ *
138
+ * @param store - Pending store
139
+ * @param row - Active challenge
140
+ * @param now - Clock
141
+ */
142
+ export function consumeChallenge(
143
+ store: PendingTwoFactorStore,
144
+ row: PendingTwoFactorChallenge,
145
+ now: number = Date.now(),
146
+ ): void {
147
+ row.consumedAt = now;
148
+ clearUserPointer(store, row);
149
+ }
150
+
151
+ /**
152
+ * Invalidate the active challenge for a user (fresh sign-in / abort).
153
+ *
154
+ * @param store - Pending store
155
+ * @param userId - User id
156
+ * @param now - Clock
157
+ */
158
+ export function invalidateActiveForUser(
159
+ store: PendingTwoFactorStore,
160
+ userId: string,
161
+ now: number = Date.now(),
162
+ ): void {
163
+ const id = store.byUserId.get(userId);
164
+ if (!id) return;
165
+ const row = store.byId.get(id);
166
+ if (row && row.consumedAt === null) {
167
+ row.consumedAt = now;
168
+ }
169
+ store.byUserId.delete(userId);
170
+ }
171
+
172
+ function clearUserPointer(store: PendingTwoFactorStore, row: PendingTwoFactorChallenge): void {
173
+ if (store.byUserId.get(row.userId) === row.id) {
174
+ store.byUserId.delete(row.userId);
175
+ }
176
+ }
177
+
178
+ /** Privileged step-up grant after verifying the current 2FA method. */
179
+ export interface StepUpGrant {
180
+ userId: string;
181
+ expiresAt: number;
182
+ purpose: "enroll" | "change" | "disable";
183
+ }
184
+
185
+ /** In-memory step-up grant store. */
186
+ export interface StepUpStore {
187
+ readonly byUserId: Map<string, StepUpGrant>;
188
+ }
189
+
190
+ /**
191
+ * Create an empty step-up grant store.
192
+ */
193
+ export function createStepUpStore(): StepUpStore {
194
+ return { byUserId: new Map() };
195
+ }
196
+
197
+ /**
198
+ * Record a short-lived step-up grant for privileged 2FA operations.
199
+ *
200
+ * @param store - Step-up store
201
+ * @param userId - User id
202
+ * @param purpose - Intended privileged op
203
+ * @param now - Clock
204
+ * @param ttlMs - Grant TTL
205
+ */
206
+ export function grantStepUp(
207
+ store: StepUpStore,
208
+ userId: string,
209
+ purpose: StepUpGrant["purpose"],
210
+ now: number = Date.now(),
211
+ ttlMs: number = DEFAULT_STEP_UP_TTL_MS,
212
+ ): StepUpGrant {
213
+ const grant: StepUpGrant = { userId, purpose, expiresAt: now + ttlMs };
214
+ store.byUserId.set(userId, grant);
215
+ return grant;
216
+ }
217
+
218
+ /**
219
+ * Consume a valid step-up grant (single-use). Returns false if missing/expired/wrong purpose.
220
+ *
221
+ * @param store - Step-up store
222
+ * @param userId - User id
223
+ * @param purpose - Required purpose (or any of the listed)
224
+ * @param now - Clock
225
+ */
226
+ export function consumeStepUp(
227
+ store: StepUpStore,
228
+ userId: string,
229
+ purpose: StepUpGrant["purpose"] | readonly StepUpGrant["purpose"][],
230
+ now: number = Date.now(),
231
+ ): boolean {
232
+ const grant = store.byUserId.get(userId);
233
+ if (!grant) return false;
234
+ if (grant.expiresAt <= now) {
235
+ store.byUserId.delete(userId);
236
+ return false;
237
+ }
238
+ const allowed = typeof purpose === "string" ? [purpose] : purpose;
239
+ if (!allowed.includes(grant.purpose)) return false;
240
+ store.byUserId.delete(userId);
241
+ return true;
242
+ }
243
+
244
+ /**
245
+ * Peek whether a valid step-up grant exists (does not consume).
246
+ *
247
+ * @param store - Step-up store
248
+ * @param userId - User id
249
+ * @param purpose - Required purpose
250
+ * @param now - Clock
251
+ */
252
+ export function hasStepUp(
253
+ store: StepUpStore,
254
+ userId: string,
255
+ purpose: StepUpGrant["purpose"] | readonly StepUpGrant["purpose"][],
256
+ now: number = Date.now(),
257
+ ): boolean {
258
+ const grant = store.byUserId.get(userId);
259
+ if (!grant) return false;
260
+ if (grant.expiresAt <= now) {
261
+ store.byUserId.delete(userId);
262
+ return false;
263
+ }
264
+ const allowed = typeof purpose === "string" ? [purpose] : purpose;
265
+ return allowed.includes(grant.purpose);
266
+ }
267
+
268
+ /** Sign-in response when 2FA must complete before session issue. */
269
+ export interface TwoFactorRequiredOut {
270
+ readonly twoFactorRequired: true;
271
+ readonly challengeId: string;
272
+ readonly method: TwoFactorMethod;
273
+ readonly userId: string;
274
+ readonly devOtp?: string;
275
+ }
276
+
277
+ /**
278
+ * Verification-store identifier for email OTP used as a second factor.
279
+ *
280
+ * @param userId - User id
281
+ */
282
+ export function twoFactorEmailOtpIdentifier(userId: string): string {
283
+ return `2fa:${userId}`;
284
+ }
@@ -27,6 +27,11 @@ export interface VerificationRow {
27
27
  email?: string | null;
28
28
  /** E.164 phone captured at request. */
29
29
  phone?: string | null;
30
+ /**
31
+ * Ceremony session id (passkey WebAuthn) — challenge is bound to this id.
32
+ * Other auth methods leave it unset.
33
+ */
34
+ sessionId?: string | null;
30
35
  }
31
36
 
32
37
  /** In-memory verification store. */
@@ -174,3 +174,83 @@ Artifact: `G15-postgres-degradation-…json` (+ extra stress-level artifacts)
174
174
  2. G3b wave-2 steady-state sat below the ~500-subscriber target; fd-per-subscriber trend unaffected.
175
175
  3. Channel bulk throughput now bounded by per-transport serialization (correctness over throughput); revisit if fan-out email volume matters.
176
176
  4. G15 is in-process backpressure only (see honesty note).
177
+
178
+ ---
179
+
180
+ ## G17 — Hybrid SQL search (BM25 / LSH / fusion)
181
+
182
+ **Command:** `OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL bun test ./src/bench/g17-hybrid-search.bench.ts --timeout 3600000`
183
+ **Hardware:** Apple M4, 24 GB · trend-analysis only (not SLA; macOS ≠ Linux prod)
184
+
185
+ Synthetic 32-dim embeddings (deterministic hash bag — not a production model). Modes: text-only (BM25/GIN), vector-only (LSH + cosine), hybrid (RRF k=60). Recall = precision@10 of the LSH/hybrid path vs **exact brute-force cosine** on the same in-memory vectors.
186
+
187
+ ### Before — v0.19.0 (2026-09-10)
188
+
189
+ **Postgres:** 16.15 · **Artifact:** `g17-hybrid-search-1789039639500.json`
190
+ Query path: K=64 bucket equality plus Hamming-1 (`= ANY(65 buckets)`), oversample ≤50.
191
+
192
+ #### Latency (p50 / p99 ms)
193
+
194
+ | N | text p50 | text p99 | vector p50 | vector p99 | hybrid p50 | hybrid p99 |
195
+ | ---- | -------- | -------- | ---------- | ---------- | ---------- | ---------- |
196
+ | 1k | 1.32 | 3.6 | 1.35 | 2.19 | 1.43 | 2.04 |
197
+ | 10k | 2.77 | 3.85 | 3.74 | 4.38 | 2.99 | 5.97 |
198
+ | 100k | 39.86 | 59.73 | 57.05 | 126.14 | 52.08 | 288.09 |
199
+ | 1M | 877.57 | 2243.68 | 1165.5 | 3370.19 | 649.14 | 1861.91 |
200
+
201
+ #### Recall — LSH approx vs exact cosine (precision@10)
202
+
203
+ | N | vector P@10 | hybrid P@10 | exact cosine p50 (ms) | approx p50 (ms) |
204
+ | ---- | ----------- | ----------- | --------------------- | --------------- |
205
+ | 1k | **0.000** | 0.067 | 0.16 | 1.41 / 1.6 |
206
+ | 10k | **0.000** | **0.000** | ~1.6 | ~3.5 |
207
+ | 100k | **0.000** | **0.000** | ~36 | ~54 |
208
+ | 1M | 0.008 | **0.000** | ~445 | ~600 |
209
+
210
+ **Root cause (not an LSH-vs-HNSW tradeoff):** write-time and query-time buckets were byte-identical; `neighborBuckets` did emit 65 probes; `= ANY(?::bigint[])` matched stored rows. Hamming-1 at K=64 cannot see true neighbors. On this generator, same-topic docs sit at cosine ≈0.96 / Hamming ~5; a topic-string query vs those docs is cosine ≈0.82 / expected Hamming ~12. P(Hamming ≤ 1) at K=64 is ~10⁻⁵. A 40-row hand case recovered **0/10** exact neighbors via Hamming-1 and **10/10** via Hamming-rank + cosine. The hash-bag embedding is clustered (8 topics, near-duplicate bodies) — that makes exact top-10 at large N a lottery among the cluster, but it does **not** explain the zero collapse at N=40.
211
+
212
+ The 2026-09-10 EXPLAIN (Seq Scan, `tsv @@ q OR lsh = ANY(…)`) was a separate planner finding on that query shape.
213
+
214
+ ### After — Hamming-rank candidate path (2026-09-11)
215
+
216
+ **Date:** 2026-09-11 · **Engine:** Bun 1.4.2 · **Postgres:** 16 (dedicated live instance, not PGlite)
217
+ **Artifact:** `g17-hybrid-search-1789135622349.json`
218
+ Fix: UNION of GIN lexical `LIMIT` and SimHash k-NN (`ORDER BY bit_count((lsh # query)::bit(64)) LIMIT oversample`). Same K=64, same oversample formula `max(limit×5, 50)` capped at 500. Not a K/oversample paper-over.
219
+
220
+ #### Latency (p50 / p99 ms)
221
+
222
+ | N | text p50 | text p99 | vector p50 | vector p99 | hybrid p50 | hybrid p99 |
223
+ | ---- | -------- | -------- | ---------- | ---------- | ---------- | ---------- |
224
+ | 1k | 0.88 | 2.8 | 1.27 | 2.37 | 1.28 | 1.54 |
225
+ | 10k | 4.5 | 17.21 | 9.36 | 29.03 | 6.88 | 32.48 |
226
+ | 100k | 21.03 | 27.61 | 30.61 | 49.8 | 34.08 | 89.99 |
227
+ | 1M | 305.61 | 377.7 | 388.98 | 548.36 | 391.96 | 469.76 |
228
+
229
+ #### Recall — LSH approx vs exact cosine (precision@10)
230
+
231
+ | N | vector P@10 | hybrid P@10 | exact cosine p50 (ms) | approx p50 (ms) |
232
+ | ---- | ----------- | ----------- | --------------------- | --------------- |
233
+ | 1k | 0.167 | 0.117 | 0.6 / 0.41 | 1.25 / 1.3 |
234
+ | 10k | 0.100 | 0.017 | ~6 | ~9 |
235
+ | 100k | 0.017 | 0.008 | ~45 | ~32 |
236
+ | 1M | 0.017 | 0.000 | ~535 | ~363 |
237
+
238
+ **Honest verdict after the fix:** the near-total collapse at 10k/100k is gone on the vector path (smooth 0.17 → 0.10 → 0.017). Remaining low precision@10 vs exact cosine is the real K=64 SimHash + 50-candidate oversample tradeoff on this near-duplicate hash-bag corpus — not a hashing mismatch. Still **not** a drop-in for HNSW/pgvector. Prefer BM25-only or an external `store.index` (`pgvector` / Meilisearch) when semantic recall matters.
239
+
240
+ ### EXPLAIN (ANALYZE, BUFFERS) at N=100k (after)
241
+
242
+ UNION Append (~14.7 ms): **Bitmap Index Scan** on the GIN tsvector (50 lexical rows) plus **Parallel Seq Scan** + top-N heapsort on `bit_count((lsh # query)::bit(64))` (50 SimHash-nearest). Hamming-rank cannot use the LSH B-tree (equality index). Do not claim BitmapOr from design alone — re-check `EXPLAIN` on production data volumes and `ANALYZE`d tables.
243
+
244
+ ### Backfill kill / resume (50k pre-populated rows)
245
+
246
+ | Metric | Before (2026-09-10) | After (2026-09-11) |
247
+ | ---------------------------------- | ------------------- | ------------------ |
248
+ | Kill after embeds | 2,000 | 2,000 |
249
+ | Embeds at kill | 2,000 | 2,000 |
250
+ | Kill wall | 726 ms | 732 ms |
251
+ | Resume wall (re-run to completion) | 29.3 s | 28.3 s |
252
+ | Final embedded rows | 50,000 | 50,000 |
253
+
254
+ Re-running `oke db search-backfill` after interrupt completed idempotently (safe re-entry; DF/stats rebuilt).
255
+
256
+ **Issues recorded in after artifact:** eight low-recall notes (P@10 still < 0.3 at every size — honest SimHash-vs-exact remainder, not the Hamming-1 zero collapse).
@@ -33,7 +33,7 @@ let bus: SignalBus;
33
33
  beforeAll(async () => {
34
34
  if (!LIVE_PG) throw new Error("G3 needs live Postgres: set OKE_TEST_POSTGRES=1 + DATABASE_URL");
35
35
  sql = createBunSignalSql(LIVE_PG);
36
- const decl = signal(SIGNAL_NAME, { delivery: "once", retries: 2, deadLetter: true });
36
+ const decl = signal.once(SIGNAL_NAME, { retries: 2, deadLetter: true });
37
37
  bus = await openPostgresSignal({
38
38
  signals: new Map([[decl.name, decl]]),
39
39
  sql,
@@ -137,7 +137,7 @@ async function runArm(
137
137
  const obsLats: number[] = [];
138
138
  let obsQueries = 0;
139
139
  if (observers && sql) {
140
- const decl = signal("g10-watch", { delivery: "once", optional: true });
140
+ const decl = signal.once("g10-watch", { optional: true });
141
141
  const consoleBus: SignalBus = await openPostgresSignal({
142
142
  signals: new Map([[decl.name, decl]]),
143
143
  sql,