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
@@ -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,52 @@ 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
+ **Date:** 2026-09-10 · **Engine:** Bun 1.4.2 · **Postgres:** 16.15 (dedicated live instance, not PGlite)
184
+ **Hardware:** Apple M4, 24 GB · trend-analysis only (not SLA; macOS ≠ Linux prod)
185
+ **Artifact:** `g17-hybrid-search-1789039639500.json`
186
+
187
+ Synthetic 32-dim embeddings (deterministic hash bag — not a production model). Modes: text-only (BM25/GIN), vector-only (LSH buckets + cosine), hybrid (RRF k=60). Recall = precision@10 of the LSH/hybrid path vs **exact brute-force cosine** on the same in-memory vectors.
188
+
189
+ ### Latency (p50 / p99 ms)
190
+
191
+ | N | text p50 | text p99 | vector p50 | vector p99 | hybrid p50 | hybrid p99 |
192
+ | ---- | -------- | -------- | ---------- | ---------- | ---------- | ---------- |
193
+ | 1k | 1.32 | 3.6 | 1.35 | 2.19 | 1.43 | 2.04 |
194
+ | 10k | 2.77 | 3.85 | 3.74 | 4.38 | 2.99 | 5.97 |
195
+ | 100k | 39.86 | 59.73 | 57.05 | 126.14 | 52.08 | 288.09 |
196
+ | 1M | 877.57 | 2243.68 | 1165.5 | 3370.19 | 649.14 | 1861.91 |
197
+
198
+ ### Recall — LSH approx vs exact cosine (precision@10)
199
+
200
+ | N | vector P@10 | hybrid P@10 | exact cosine p50 (ms) | approx p50 (ms) |
201
+ | ---- | ----------- | ----------- | --------------------- | --------------- |
202
+ | 1k | **0.000** | 0.067 | 0.16 | 1.41 / 1.6 |
203
+ | 10k | **0.000** | **0.000** | ~1.6 | ~3.5 |
204
+ | 100k | **0.000** | **0.000** | ~36 | ~54 |
205
+ | 1M | 0.008 | **0.000** | ~445 | ~600 |
206
+
207
+ **Honest verdict:** on this corpus the random-hyperplane LSH path (K=64, Hamming-1 neighbors, candidate oversample ≤50) does **not** recover the exact cosine top-10. Treat built-in LSH as a cheap candidate generator for hybrid fusion — **not** a drop-in for HNSW/pgvector recall. Prefer BM25-only or an external `store.index` (`pgvector` / Meilisearch) when semantic recall matters.
208
+
209
+ ### EXPLAIN (ANALYZE, BUFFERS) at N=100k
210
+
211
+ Postgres 16 chose a **Seq Scan** with a Filter on `__oke_tsv @@ plainto_tsquery(…) OR __oke_lsh_body = ANY(…)` (Limit 50; execution ~0.27 ms; shared hit=27). GIN + LSH B-tree indexes exist; the planner did **not** emit BitmapOr on this hardware/stats for the measured query. Do not claim BitmapOr from design alone — re-check `EXPLAIN` on production data volumes and `ANALYZE`d tables.
212
+
213
+ ### Backfill kill / resume (50k pre-populated rows)
214
+
215
+ | Metric | Value |
216
+ | ---------------------------------- | ------ |
217
+ | Kill after embeds | 2,000 |
218
+ | Embeds at kill | 2,000 |
219
+ | Kill wall | 726 ms |
220
+ | Resume wall (re-run to completion) | 29.3 s |
221
+ | Final embedded rows | 50,000 |
222
+
223
+ Re-running `oke db search-backfill` after interrupt completed idempotently (safe re-entry; DF/stats rebuilt).
224
+
225
+ **Issues recorded in artifact:** eight low-recall notes (all sizes). No product fix applied mid-bench — numbers stand as measured.
@@ -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,