okengine 0.21.1 → 0.23.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 (342) hide show
  1. package/AGENTS.md +11 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +54 -0
  6. package/package.json +53 -38
  7. package/site/content/docs/ai/mcp.mdx +13 -9
  8. package/site/content/docs/ai/skills.mdx +2 -2
  9. package/site/content/docs/client/calling.mdx +27 -19
  10. package/site/content/docs/client/index.mdx +1 -1
  11. package/site/content/docs/elements/ai/agents.mdx +61 -13
  12. package/site/content/docs/elements/ai/decide.mdx +536 -0
  13. package/site/content/docs/elements/ai/events.mdx +373 -0
  14. package/site/content/docs/elements/ai/index.mdx +9 -3
  15. package/site/content/docs/elements/ai/meta.json +1 -1
  16. package/site/content/docs/elements/ai/prompts.mdx +4 -2
  17. package/site/content/docs/elements/channel/email.mdx +1 -1
  18. package/site/content/docs/elements/channel/index.mdx +1 -1
  19. package/site/content/docs/elements/flow/http.mdx +4 -4
  20. package/site/content/docs/elements/flow/index.mdx +3 -3
  21. package/site/content/docs/elements/flow/meta.json +1 -1
  22. package/site/content/docs/elements/gate/auth.mdx +4 -1
  23. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  24. package/site/content/docs/elements/store/index.mdx +1 -1
  25. package/site/content/docs/elements/store/kv.mdx +5 -5
  26. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  27. package/site/content/docs/plugins/otp.mdx +31 -31
  28. package/site/content/docs/recipes/dragonfly.mdx +5 -5
  29. package/site/content/docs/recipes/index.mdx +6 -6
  30. package/site/content/docs/recipes/mailpit.mdx +1 -1
  31. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  32. package/site/content/docs/recipes/pgdog.mdx +1 -1
  33. package/site/content/docs/recipes/postgres.mdx +12 -8
  34. package/site/content/docs/recipes/redis.mdx +4 -4
  35. package/site/content/docs/reference/cli.mdx +2 -1
  36. package/site/content/docs/reference/configuration.mdx +11 -9
  37. package/site/content/docs/reference/errors.mdx +12 -2
  38. package/site/content/docs/reference/fx.mdx +35 -21
  39. package/site/content/docs/reference/idempotency.mdx +192 -0
  40. package/site/content/docs/reference/index.mdx +6 -1
  41. package/site/content/docs/reference/meta.json +2 -1
  42. package/site/content/docs/understand/meta.json +1 -1
  43. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  44. package/site/content/docs/understand/the-architecture.mdx +14 -12
  45. package/site/content/docs/understand/try-it.mdx +3 -3
  46. package/src/auth/api-key-sql.test.ts +44 -0
  47. package/src/auth/api-key-sql.ts +1 -0
  48. package/src/auth/api-keys.ts +30 -12
  49. package/src/auth/config.ts +6 -2
  50. package/src/auth/gate-auth.test.ts +20 -0
  51. package/src/cli/agents-md.test.ts +25 -0
  52. package/src/cli/ask-vault-gaps.test.ts +38 -1
  53. package/src/cli/ask-vault-gaps.ts +35 -2
  54. package/src/cli/competitor-mention-removal.test.ts +3 -3
  55. package/src/cli/decide.ts +174 -0
  56. package/src/cli/decision-lock-watch.test.ts +45 -0
  57. package/src/cli/decision-lock-watch.ts +51 -0
  58. package/src/cli/dev-app-runner.ts +34 -8
  59. package/src/cli/dev-auth-secret.test.ts +49 -0
  60. package/src/cli/dev-auth-secret.ts +49 -0
  61. package/src/cli/dev-hot.test.ts +41 -0
  62. package/src/cli/dev-hot.ts +48 -0
  63. package/src/cli/dev.test.ts +1 -1
  64. package/src/cli/dev.ts +24 -6
  65. package/src/cli/docker-cli.test.ts +1 -1
  66. package/src/cli/eval.ts +142 -1
  67. package/src/cli/index.ts +14 -1
  68. package/src/cli/load-config.images.test.ts +12 -12
  69. package/src/cli/load-config.ts +2 -2
  70. package/src/cli/mcp-from-console.ts +48 -0
  71. package/src/cli/registry.ts +35 -1
  72. package/src/cli/vault-cmd.test.ts +23 -0
  73. package/src/cli/vault-cmd.ts +7 -0
  74. package/src/client/agent.ts +307 -0
  75. package/src/client/create.ts +18 -1
  76. package/src/client/transport.test.ts +245 -0
  77. package/src/client/transport.ts +133 -19
  78. package/src/client/types.ts +29 -3
  79. package/src/client-react/index.ts +7 -1
  80. package/src/client-react/use-agent-run.test.ts +271 -0
  81. package/src/client-react/use-agent-run.ts +193 -0
  82. package/src/compiler/ai-approval.test.ts +36 -0
  83. package/src/compiler/ai-repair.test.ts +22 -0
  84. package/src/compiler/decisions.extract.test.ts +164 -0
  85. package/src/compiler/effects-infer.ts +753 -17
  86. package/src/compiler/extract.test.ts +37 -0
  87. package/src/compiler/extract.ts +381 -24
  88. package/src/compiler/fixtures/skyport.expected.json +24 -0
  89. package/src/compiler/fx-follow.test.ts +164 -0
  90. package/src/compiler/fx-index.ts +399 -0
  91. package/src/compiler/response.ts +35 -2
  92. package/src/config/index.ts +21 -3
  93. package/src/console/server/ai-runs-flows.ts +269 -0
  94. package/src/console/server/ai-runs.test.ts +323 -0
  95. package/src/console/server/ai-runs.ts +562 -0
  96. package/src/console/server/ai.ts +12 -1
  97. package/src/console/server/app.ts +10 -1
  98. package/src/console/server/decisions-flows.ts +157 -0
  99. package/src/console/server/decisions.test.ts +66 -0
  100. package/src/console/server/decisions.ts +229 -0
  101. package/src/console/server/flows-invoke.test.ts +2 -2
  102. package/src/console/server/flows.ts +50 -2
  103. package/src/console/server/invoke-user-flow.ts +9 -0
  104. package/src/console/server/runs-ingest.ts +16 -0
  105. package/src/console/server/serve.ts +5 -0
  106. package/src/console/server/state.ts +25 -1
  107. package/src/console/server/store-stats.test.ts +47 -0
  108. package/src/console/server/store-stats.ts +92 -18
  109. package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
  110. package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
  111. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
  112. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
  113. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
  114. package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
  115. package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
  116. package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
  117. package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
  118. package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
  119. package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
  120. package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
  121. package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
  122. package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
  123. package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
  124. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
  125. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
  126. package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
  127. package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
  128. package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
  129. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
  130. package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
  131. package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
  132. package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
  133. package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
  134. package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
  135. package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
  136. package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
  137. package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
  138. package/src/console/ui-next/dist/index.html +4 -3
  139. package/src/console/ui-next/seed-parked-approval.ts +126 -0
  140. package/src/console/ui-next/src/client.ts +224 -1
  141. package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
  142. package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
  143. package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
  144. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
  145. package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
  146. package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
  147. package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
  148. package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
  149. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
  150. package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
  151. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  152. package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
  153. package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
  154. package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
  155. package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
  156. package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
  157. package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
  158. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
  159. package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
  160. package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
  161. package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
  162. package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
  163. package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
  164. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
  165. package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
  166. package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
  167. package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
  168. package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
  169. package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
  170. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
  171. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
  172. package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
  173. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
  174. package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
  175. package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
  176. package/src/console/ui-next/src/router.tsx +17 -0
  177. package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
  178. package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
  179. package/src/console/ui-next/vite.config.ts +9 -0
  180. package/src/docker/docker.test.ts +13 -13
  181. package/src/docker/images-config.test.ts +12 -12
  182. package/src/docker/recipes/dragonfly.ts +3 -2
  183. package/src/docker/recipes/redis.ts +1 -1
  184. package/src/docker/stack-id.test.ts +1 -1
  185. package/src/drivers/ai-anthropic.ts +212 -21
  186. package/src/drivers/ai-mock.ts +34 -1
  187. package/src/drivers/ai-openai-compatible.ts +67 -26
  188. package/src/drivers/ai-preconnect.ts +18 -0
  189. package/src/drivers/ai-stream.test.ts +62 -0
  190. package/src/drivers/ai-types.ts +20 -0
  191. package/src/drivers/journal-postgres.ts +455 -3
  192. package/src/drivers/meilisearch.ts +2 -0
  193. package/src/drivers/memory-ddl.test.ts +18 -0
  194. package/src/drivers/memory.ts +21 -1
  195. package/src/drivers/pg-rls.test.ts +31 -0
  196. package/src/drivers/pg-rls.ts +15 -0
  197. package/src/drivers/postgres.test.ts +9 -0
  198. package/src/drivers/postgres.ts +49 -11
  199. package/src/drivers/vault-builtin.test.ts +10 -0
  200. package/src/drivers/vault-builtin.ts +13 -1
  201. package/src/drivers/vault-remote-bag.ts +5 -1
  202. package/src/drivers/vault-types.ts +6 -1
  203. package/src/elements/ai/agent-event-slot.ts +27 -0
  204. package/src/elements/ai/approval-http.ts +207 -0
  205. package/src/elements/ai/approval.test.ts +741 -0
  206. package/src/elements/ai/approval.ts +228 -0
  207. package/src/elements/ai/decisions/bind.ts +163 -0
  208. package/src/elements/ai/decisions/certificate.test.ts +291 -0
  209. package/src/elements/ai/decisions/certificate.ts +377 -0
  210. package/src/elements/ai/decisions/certify.ts +314 -0
  211. package/src/elements/ai/decisions/decide.test.ts +953 -0
  212. package/src/elements/ai/decisions/e2e.test.ts +399 -0
  213. package/src/elements/ai/decisions/export.test.ts +120 -0
  214. package/src/elements/ai/decisions/export.ts +146 -0
  215. package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
  216. package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
  217. package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
  218. package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
  219. package/src/elements/ai/decisions/http.test.ts +154 -0
  220. package/src/elements/ai/decisions/http.ts +240 -0
  221. package/src/elements/ai/decisions/labels.test.ts +282 -0
  222. package/src/elements/ai/decisions/labels.ts +257 -0
  223. package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
  224. package/src/elements/ai/decisions/openrouter.ts +36 -0
  225. package/src/elements/ai/decisions/provider.ts +102 -0
  226. package/src/elements/ai/decisions/typesafe.ts +36 -0
  227. package/src/elements/ai/declare.ts +252 -1
  228. package/src/elements/ai/events.test.ts +379 -0
  229. package/src/elements/ai/events.ts +138 -0
  230. package/src/elements/ai/run-events.test.ts +461 -0
  231. package/src/elements/ai/run-events.ts +522 -0
  232. package/src/elements/ai/runtime.ts +1186 -105
  233. package/src/elements/ai/stream-turn.ts +214 -0
  234. package/src/elements/ai/stream.live.test.ts +76 -0
  235. package/src/elements/ai/subagent.test.ts +136 -0
  236. package/src/elements/ai.test.ts +497 -3
  237. package/src/elements/ai.ts +16 -1
  238. package/src/elements/clock/durable.ts +27 -1
  239. package/src/elements/store/emit-drizzle.ts +2 -2
  240. package/src/elements/store/live-default.test.ts +2 -0
  241. package/src/elements/store/live-http.test.ts +1 -0
  242. package/src/elements/store/resource-list-docs.test.ts +2 -2
  243. package/src/elements/store/resource.test.ts +3 -3
  244. package/src/elements/store/schema-decl.test.ts +1 -0
  245. package/src/elements/store/sql-condition.ts +24 -7
  246. package/src/elements/store/sql-session.test.ts +51 -0
  247. package/src/elements/store/sql-session.ts +111 -1
  248. package/src/elements/store/upsert-app.test.ts +1 -1
  249. package/src/elements/vault/audit.test.ts +137 -0
  250. package/src/elements/vault/audit.ts +106 -13
  251. package/src/elements/vault/boot-chain.ts +9 -1
  252. package/src/elements/vault/builtin-adapter.ts +33 -3
  253. package/src/i18n/catalogs/ar.ts +9 -0
  254. package/src/i18n/catalogs/en.ts +9 -0
  255. package/src/index.ts +2 -0
  256. package/src/kernel/abort-scope.ts +33 -2
  257. package/src/kernel/agent-event-store.ts +448 -0
  258. package/src/kernel/app.ts +294 -19
  259. package/src/kernel/auto-cache.test.ts +6 -6
  260. package/src/kernel/boot-bind/ai.ts +3 -0
  261. package/src/kernel/boot-bind/store.test.ts +26 -0
  262. package/src/kernel/boot-bind/store.ts +82 -6
  263. package/src/kernel/boot-bind/vault.ts +1 -0
  264. package/src/kernel/boot.ts +34 -5
  265. package/src/kernel/builtin-errors.ts +10 -0
  266. package/src/kernel/capability.ts +3 -0
  267. package/src/kernel/client-descriptor.ts +1 -0
  268. package/src/kernel/concurrency.test.ts +33 -0
  269. package/src/kernel/decision-label-store.ts +441 -0
  270. package/src/kernel/effects-stamping.test.ts +2 -2
  271. package/src/kernel/effects.test.ts +2 -1
  272. package/src/kernel/effects.ts +13 -1
  273. package/src/kernel/element-registries.ts +3 -0
  274. package/src/kernel/errors-compiler.ts +20 -0
  275. package/src/kernel/errors-text.ts +4 -0
  276. package/src/kernel/errors.ts +2 -0
  277. package/src/kernel/external-effects.test.ts +36 -0
  278. package/src/kernel/flow.test.ts +2 -3
  279. package/src/kernel/flow.ts +24 -0
  280. package/src/kernel/fx-decide.ts +761 -0
  281. package/src/kernel/fx-fetch.ts +5 -1
  282. package/src/kernel/fx.test.ts +12 -5
  283. package/src/kernel/fx.ts +316 -29
  284. package/src/kernel/http-frame.test.ts +94 -0
  285. package/src/kernel/http-frame.ts +203 -0
  286. package/src/kernel/idempotency-store.ts +578 -0
  287. package/src/kernel/idempotency.test.ts +556 -0
  288. package/src/kernel/idempotency.ts +304 -0
  289. package/src/kernel/index.ts +1 -0
  290. package/src/kernel/journal.ts +160 -0
  291. package/src/kernel/json-result.ts +10 -0
  292. package/src/kernel/pipeline.test.ts +45 -0
  293. package/src/kernel/sse-id.ts +32 -0
  294. package/src/manifest/diff.ts +1 -0
  295. package/src/manifest/types.ts +37 -2
  296. package/src/mcp/ai-tools.test.ts +202 -0
  297. package/src/mcp/authorization.ts +66 -0
  298. package/src/mcp/session.ts +3 -0
  299. package/src/mcp/tools.ts +168 -0
  300. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  301. package/src/plugins/otp.test.ts +65 -1
  302. package/src/plugins/otp.ts +59 -7
  303. package/src/release/absolute-regression.test.ts +63 -0
  304. package/src/release/http-graph.test.ts +27 -0
  305. package/src/release/http-graph.ts +95 -0
  306. package/src/release/index.ts +3 -0
  307. package/src/release/limits.ts +17 -2
  308. package/src/release/measure.ts +144 -28
  309. package/src/runs/collect.ts +4 -1
  310. package/src/runs/parquet.test.ts +27 -1
  311. package/src/runs/parquet.ts +19 -0
  312. package/src/runs/types.ts +31 -0
  313. package/src/runtime/json-code-block.test.ts +106 -47
  314. package/src/runtime/json-code-block.ts +1209 -46
  315. package/src/term.test.ts +13 -0
  316. package/src/term.ts +81 -49
  317. package/src/test/create-test-app.test.ts +3 -1
  318. package/src/test/create-test-app.ts +40 -0
  319. package/src/test/live-signals.test.ts +1 -0
  320. package/src/test/provisions.integration.test.ts +1 -0
  321. package/src/test/tenant-isolation.test.ts +1 -0
  322. package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
  323. package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
  324. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  325. package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
  326. package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
  327. package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
  328. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  329. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  330. package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
  331. package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
  332. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  333. package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
  334. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  335. package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
  336. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  337. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  338. package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
  339. package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
  340. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
  341. package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
  342. package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "Reference"
3
3
  description: "Lookup pages — CLI, config, fx, errors, security, and plugins."
4
- icon: "BookMarked"
4
+ icon: "BookBookmark"
5
5
  source: "docs/spec/unified-theory.md"
6
6
  ---
7
7
 
@@ -23,6 +23,11 @@ Dense tables and command lists. Reach for these when you already know what you a
23
23
  />
24
24
  <Card title="fx" description="The complete fx surface and effects." href="/docs/reference/fx" />
25
25
  <Card title="Errors" description="OKE codes, denials, fixes." href="/docs/reference/errors" />
26
+ <Card
27
+ title="Idempotency"
28
+ description="One Idempotency-Key, one execution."
29
+ href="/docs/reference/idempotency"
30
+ />
26
31
  <Card
27
32
  title="Security"
28
33
  description="Host, Origin, planes, MCP posture."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "title": "Reference",
3
- "icon": "BookMarked",
3
+ "icon": "BookBookmark",
4
4
  "pages": [
5
5
  "index",
6
6
  "cli",
@@ -8,6 +8,7 @@
8
8
  "environment-variables",
9
9
  "fx",
10
10
  "errors",
11
+ "idempotency",
11
12
  "security",
12
13
  "i18n",
13
14
  "plugins",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "title": "Understand",
3
3
  "icon": "Compass",
4
- "pages": ["the-architecture", "try-it"]
4
+ "pages": ["the-architecture", "routing", "try-it"]
5
5
  }
@@ -588,28 +588,29 @@ plus a handwritten `http.get("/links")`.
588
588
 
589
589
  ## Learn more
590
590
 
591
+ - [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
592
+ - [Try it](/docs/understand/try-it) — scaffold, run, and hit a stamped path
591
593
  - [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
592
594
  - [Client](/docs/client/calling) — `api.links.get`, REST vs RPC, `$routes`
593
595
  - [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
594
- - [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
595
596
  - [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
596
597
 
597
598
  ## Next
598
599
 
599
600
  <Cards>
600
601
  <Card
601
- title="HTTP"
602
- description="REST verbs, RFC 10008 QUERY, CRUD mounts, and live SSE on Flow."
603
- href="/docs/elements/flow/http"
602
+ title="The Architecture"
603
+ description="One law, eight elements, derived Manifest — where routes come from."
604
+ href="/docs/understand/the-architecture"
604
605
  />
605
606
  <Card
606
- title="Client"
607
- description="Typed caller — createClient, envelopes, REST from $routes."
608
- href="/docs/client/calling"
607
+ title="Try it"
608
+ description="Scaffold shorter, run oke dev, and call a stamped path."
609
+ href="/docs/understand/try-it"
609
610
  />
610
611
  <Card
611
- title="Consumers"
612
- description="Signal workers, named Clock jobs, and SQL CDC — one Flow species."
613
- href="/docs/elements/flow/consumers"
612
+ title="HTTP"
613
+ description="REST verbs, RFC 10008 QUERY, CRUD mounts, and live SSE on Flow."
614
+ href="/docs/elements/flow/http"
614
615
  />
615
616
  </Cards>
@@ -27,11 +27,11 @@ app.post("/signup", async (req, res) => {
27
27
 
28
28
  It works. It ships. Then a traffic spike, a silent failure, a compliance question, and a double-submit turn those four lines into six files — endpoint, queue, worker, Redis connection, mail client, audit table — that never agreed with each other about anything.
29
29
 
30
- **A payment webhook.** A provider confirms a charge. Mark the order paid. Simple — until the provider retries the same webhook twice during a network hiccup, and "mark the order paid" needs to somehow know it already ran.
30
+ **A payment webhook.** A provider confirms a charge. Mark the order paid. The provider retries the same event during a hiccup. That attempt is a second run, visible in `fx.runs`. The fix is one line: a unique constraint or upsert on the provider's event id.
31
31
 
32
32
  **A nightly report.** Summarize yesterday's activity and email it to managers. Trivial — until a manager's access gets revoked at 11:58pm and the report that runs at midnight has no idea the permission it checked when the feature was built isn't the permission that holds right now.
33
33
 
34
- Three teams. Three domains. Nobody on any of them talked to the other two. And all three land on the identical fork: **something has to happen later, exactly once, provably — and nothing in the original four lines said what "provably" would end up costing.**
34
+ Three teams. Three domains. Nobody on any of them talked to the other two. And all three land on the identical fork: **something has to happen later, and the original four lines never said where the retry, the receipt, or the duplicate would be visible.** The door makes that visible. The fix is one line.
35
35
 
36
36
  ### Follow one all the way through
37
37
 
@@ -49,7 +49,7 @@ The endpoint is fast again. It's also no longer one system — it's an endpoint,
49
49
 
50
50
  <SixSystemsDrift />
51
51
 
52
- From here the same pattern repeats on a longer clock. A support ticket reveals a job failed silently — nobody had configured retries, so you add them, and now a specific number (3? 5? with what backoff?) lives in a file that nobody will remember the reasoning for in six weeks. Compliance asks for proof of every email sent — you add a table written to from inside the worker, and now that worker has two jobs instead of one, quietly capable of disagreeing with itself if the second write fails. Someone double-clicks submit — two jobs enqueue, two emails send, and idempotency becomes a fact that has to live in two systems that were never introduced to each other.
52
+ From here the same pattern repeats on a longer clock. A support ticket reveals a job failed silently — nobody had configured retries, so you add them, and now a specific number (3? 5? with what backoff?) lives in a file that nobody will remember the reasoning for in six weeks. Compliance asks for proof of every email sent — you add a table written to from inside the worker, and now that worker has two jobs instead of one, quietly capable of disagreeing with itself if the second write fails. Someone double-clicks submit — two jobs enqueue, two emails send. The duplicate is two runs. The fix is one line, a unique constraint or upsert, not a second protocol shared by a queue and a database.
53
53
 
54
54
  None of these were mistakes. Each one was the correct call, made by a competent engineer, in direct response to something that actually happened.
55
55
 
@@ -74,7 +74,7 @@ OKE removes the disagreement by removing the choice. It's one rule, in two parts
74
74
 
75
75
  **First: every trigger reduces to the same shape.** An HTTP request, a scheduled tick, a queue message, a database change — whatever wakes the code up, what follows has one identical anatomy: `on(Trigger) → Effects`. Not four systems that happen to look similar. One system, with four ways to wake it up.
76
76
 
77
- **Second: every effect passes through one door.** Nothing is allowed to touch a database, send an email, check a permission, or read the clock on its own — all of it goes through a single surface. Not because that's tidier. Because it's the only way retries, auditing, idempotency, and permission checks stop being infrastructure every team reinvents at the exact moment they get burned by not having it.
77
+ **Second: every effect passes through one door.** Nothing is allowed to touch a database, send an email, check a permission, or read the clock on its own — all of it goes through a single surface. The Manifest lists every effect. Least privilege is enforced at runtime. Every run is recorded in `fx.runs`. Durability is one line in a known place — `signal.once` retries, or `durable` plus `fx.step` — not a new system.
78
78
 
79
79
  <FlowShape />
80
80
 
@@ -193,7 +193,9 @@ do: async (input, fx) => {
193
193
  };
194
194
  ```
195
195
 
196
- This one rule is what made the month-8 drift above avoidable: if `fx` is the only door, retries, auditing, and idempotency stop being separate systems teams build by hand, and become properties of the one boundary everything already passes through.
196
+ This one rule is what made the month-8 drift visible: every effect is in the Manifest, every run is in `fx.runs`, and a duplicate is a second run you can see.
197
+ A retried HTTP call can send one `Idempotency-Key`, so that retry runs once.
198
+ A provider event still needs a unique constraint, because the provider does not send that header.
197
199
 
198
200
  ### Putting the five pieces together
199
201
 
@@ -223,14 +225,14 @@ export const signup = on(
223
225
 
224
226
  Nothing about the code above looks more complicated than the four lines that started the drift — because it isn't. The difference only shows up when the same pressure from that timeline hits it. Every fork was really the same question — _is this safe to retry, safe to audit, safe to run twice?_ — and now it has one home:
225
227
 
226
- | Then: a new system per incident | Now: one door, fixed answer |
227
- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
228
- | **Week 2 spike** — hand-build a queue + worker to run the send later | Running later, safely, is asked of the trigger or the effect itself |
229
- | **Month 2 silence** — a retry count in a worker nobody remembers | Retries are a property of the `fx` boundary every effect passes through |
230
- | **Month 4 audit** — a `sent_emails` table written by hand from a job | What was sent is already known — nothing sends outside `fx` |
231
- | **Month 6 double-submit** — dedup split across a queue and a DB | One call through one door — no second path for a duplicate |
228
+ | Then: a new system per incident | Now: the door makes it visible, and the fix is one line |
229
+ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
230
+ | **Week 2 spike** — hand-build a queue + worker to run the send later | The send is a Signal consumer. The Manifest lists it. The run is in `fx.runs`. |
231
+ | **Month 2 silence** — a retry count in a worker nobody remembers | The retry count lives on `signal.once` (default 3) or on `durable` + `fx.step`, which skips a step already journaled. A failed HTTP request is not retried by `fx`. |
232
+ | **Month 4 audit** — a `sent_emails` table written by hand from a job | The send is an effect in the Manifest and a run in `fx.runs`. Channel records a receipt. The default receipt ledger is process-local memory. |
233
+ | **Month 6 double-submit** — dedup split across a queue and a DB | A retry of one call runs once when it sends `Idempotency-Key`. Two submits are two calls unless the app passes the same key. Provider webhooks still need a unique constraint on the provider's event id, because providers do not send that header. `{ key }` serializes `once` per key. It does not deduplicate. |
232
234
 
233
- None of that required new code beyond what's above. It required the four lines to already be the kind of thing where those questions have a fixed answer, instead of a new one invented per team, per incident.
235
+ The door makes each of those problems visible, and the fix is one line in a known place. The signup sample is safe to run twice once the user row has a unique constraint or an upsert. The second attempt is a second run in `fx.runs`.
234
236
 
235
237
  ### Five kinds of trigger
236
238
 
@@ -405,7 +405,7 @@ test("creates a user", async () => {
405
405
  bun test # [!code focus]
406
406
  ```
407
407
 
408
- `createTestApp` swaps every driver behind `fx` for a deterministic one.
408
+ `createTestApp` swaps every driver behind `fx` for a deterministic one. It stamps Manifest tokens. Inline flows that are not an app pass `capability: "open"`.
409
409
 
410
410
  </Step>
411
411
 
@@ -762,7 +762,7 @@ return await fx.ask(triage, { message }); // [!code focus]
762
762
 
763
763
  ## Learn more
764
764
 
765
- - [Routing](/docs/elements/flow/routing) — when to omit the path, `list` / `create` / `get` filenames
765
+ - [Routing](/docs/understand/routing) — when to omit the path, `list` / `create` / `get` filenames
766
766
  - [Flow](/docs/elements/flow) — one species; `fx` is the door
767
767
  - [Signal](/docs/elements/signal) — `once` / `broadcast` / `live`
768
768
  - [Store](/docs/elements/store) — SQL, KV, files, search
@@ -775,7 +775,7 @@ return await fx.ask(triage, { message }); // [!code focus]
775
775
  <Card
776
776
  title="Routing"
777
777
  description="File-tree stamps, many-routes files, and reserved leaves."
778
- href="/docs/elements/flow/routing"
778
+ href="/docs/understand/routing"
779
779
  />
780
780
  <Card title="Flow" description="on(trigger) → do through fx." href="/docs/elements/flow" />
781
781
  <Card title="Signal" description="Move work off the request." href="/docs/elements/signal" />
@@ -44,4 +44,48 @@ describe("api-key SQL persist", () => {
44
44
  expect(other.keys.get("key_sql")?.creatorId).toBe("u1");
45
45
  expect(other.keys.get("key_sql")?.hash).toBe(created.row.hash);
46
46
  });
47
+
48
+ test("authenticate reloads an oke_ secret missing from memory", async () => {
49
+ const { authenticateApiKey } = await import("./api-keys.ts");
50
+ const rows = new Map<string, Record<string, unknown>>();
51
+ const sql: ApiKeySqlExec = {
52
+ async execute(_q, params) {
53
+ const id = String(params[0]);
54
+ rows.set(id, {
55
+ id,
56
+ plane: params[1],
57
+ hash: params[2],
58
+ name: params[3],
59
+ scopes: params[4],
60
+ expires_at: params[5],
61
+ rate_limit: params[6],
62
+ ip_allowlist: params[7],
63
+ creator_id: params[8],
64
+ creator_scopes: params[9],
65
+ created_at: params[10],
66
+ last_used_at: params[11],
67
+ revoked_at: params[12],
68
+ });
69
+ },
70
+ async all() {
71
+ return [...rows.values()];
72
+ },
73
+ };
74
+ const writer = createApiKeyStore({ pepper: "shared" });
75
+ bindApiKeySqlPersist(writer, sql);
76
+ const created = await createApiKey(writer, {
77
+ plane: "user",
78
+ name: "Auth Test",
79
+ scopes: ["member"],
80
+ creatorId: "user_admin",
81
+ creatorScopes: ["member"],
82
+ id: "key_live",
83
+ });
84
+ const reader = createApiKeyStore({ pepper: "shared" });
85
+ bindApiKeySqlPersist(reader, sql);
86
+ expect(reader.keys.size).toBe(0);
87
+ const row = await authenticateApiKey(reader, created.secret);
88
+ expect(row?.id).toBe("key_live");
89
+ expect(reader.keys.has("key_live")).toBe(true);
90
+ });
47
91
  });
@@ -97,6 +97,7 @@ export function bindApiKeySqlPersist(
97
97
  table: string = AUTH_TABLES.apiKeys,
98
98
  ): void {
99
99
  store.persist = (row) => persistApiKeyRow(sql, row, table);
100
+ store.reload = () => hydrateApiKeyStore(sql, store, table);
100
101
  }
101
102
 
102
103
  /** Runtime that can open the app's shared primary SQL connection. */
@@ -85,6 +85,11 @@ export interface ApiKeyStore {
85
85
  pepper?: string;
86
86
  /** Optional write-through persist (SQL). */
87
87
  persist?: (row: ApiKeyRow) => void | Promise<void>;
88
+ /**
89
+ * Reload rows from the durable table. `oke dev` keeps Console and the app
90
+ * in separate processes; a miss on an `oke_` secret refreshes this cache.
91
+ */
92
+ reload?: () => void | Promise<void>;
88
93
  /** Sliding-window hits for per-key rateLimit (subject = key id). */
89
94
  rateHits?: Map<string, number[]>;
90
95
  }
@@ -202,20 +207,33 @@ export async function authenticateApiKey(
202
207
  const now = options.now ?? (() => Date.now());
203
208
  const pepper = store.pepper ?? DEFAULT_PEPPER;
204
209
  const hash = await hashApiKeySecret(secret, pepper);
205
- for (const row of store.keys.values()) {
206
- if (row.hash !== hash) continue;
207
- if (row.revokedAt !== null) return null;
208
- if (row.expiresAt !== null && row.expiresAt <= now()) return null;
209
- if (row.ipAllowlist.length > 0) {
210
- const allowed = await allowlistAllowsIp(row.ipAllowlist, options.ip, options.lookup);
211
- if (!allowed) return null;
210
+ let row = matchApiKeyHash(store, hash);
211
+ if (!row && secret.startsWith("oke_") && store.reload) {
212
+ try {
213
+ await store.reload();
214
+ } catch {
215
+ // Durable read missed — the in-memory miss still stands.
212
216
  }
213
- if (row.rateLimit && !takeKeyRate(store, row.id, row.rateLimit, now())) return null;
214
- row.lastUsedAt = now();
215
- await persistRow(store, row);
216
- return row;
217
+ row = matchApiKeyHash(store, hash);
218
+ }
219
+ if (!row) return null;
220
+ if (row.revokedAt !== null) return null;
221
+ if (row.expiresAt !== null && row.expiresAt <= now()) return null;
222
+ if (row.ipAllowlist.length > 0) {
223
+ const allowed = await allowlistAllowsIp(row.ipAllowlist, options.ip, options.lookup);
224
+ if (!allowed) return null;
225
+ }
226
+ if (row.rateLimit && !takeKeyRate(store, row.id, row.rateLimit, now())) return null;
227
+ row.lastUsedAt = now();
228
+ await persistRow(store, row);
229
+ return row;
230
+ }
231
+
232
+ function matchApiKeyHash(store: ApiKeyStore, hash: string): ApiKeyRow | undefined {
233
+ for (const row of store.keys.values()) {
234
+ if (row.hash === hash) return row;
217
235
  }
218
- return null;
236
+ return undefined;
219
237
  }
220
238
 
221
239
  /**
@@ -4,6 +4,7 @@
4
4
  * Better Auth is inspiration for shape only; this is OKE-native.
5
5
  */
6
6
 
7
+ import { createEnv } from "../runtime/primitives.ts";
7
8
  import type { PasswordHashOptions } from "../runtime/types.ts";
8
9
  import type { SessionStore } from "./sessions.ts";
9
10
  import { createApiKeyStore, type ApiKeyStore } from "./api-keys.ts";
@@ -226,9 +227,12 @@ export function mintDevAuthSecret(): string {
226
227
  export function resolveGateAuth(options: ResolveGateAuthOptions): ResolvedGateAuth {
227
228
  const { auth, env } = options;
228
229
  const isProd = env === "prod" || env === "production";
229
- let secret = auth.secret;
230
+ let secret = auth.secret?.trim() ?? "";
230
231
  let secretMinted = false;
231
- if (!secret || secret.length === 0) {
232
+ if (secret.length === 0) {
233
+ secret = createEnv().get("OKE_AUTH_SECRET")?.trim() ?? "";
234
+ }
235
+ if (secret.length === 0) {
232
236
  if (isProd) {
233
237
  throw new Error(
234
238
  "gate.auth: secret is required in production (set gate.auth.secret or OKE_AUTH_SECRET)",
@@ -21,13 +21,33 @@ afterEach(() => {
21
21
  });
22
22
 
23
23
  describe("gate.auth — config", () => {
24
+ const prevAuthSecret = process.env.OKE_AUTH_SECRET;
25
+
26
+ afterEach(() => {
27
+ if (prevAuthSecret === undefined) delete process.env.OKE_AUTH_SECRET;
28
+ else process.env.OKE_AUTH_SECRET = prevAuthSecret;
29
+ });
30
+
24
31
  test("prod without secret fails", () => {
32
+ delete process.env.OKE_AUTH_SECRET;
25
33
  expect(() =>
26
34
  resolveGateAuth({ auth: { emailAndPassword: { enabled: true } }, env: "prod" }),
27
35
  ).toThrow(/secret is required/);
28
36
  });
29
37
 
38
+ test("OKE_AUTH_SECRET is the pepper when gate.auth.secret is omitted", () => {
39
+ process.env.OKE_AUTH_SECRET = "shared-dev-pepper";
40
+ const resolved = resolveGateAuth({
41
+ auth: { emailAndPassword: { enabled: true } },
42
+ env: "dev",
43
+ });
44
+ expect(resolved.secret).toBe("shared-dev-pepper");
45
+ expect(resolved.secretMinted).toBe(false);
46
+ expect(resolved.apiKeyStore.pepper).toBe("shared-dev-pepper");
47
+ });
48
+
30
49
  test("dev mints secret", () => {
50
+ delete process.env.OKE_AUTH_SECRET;
31
51
  const resolved = resolveGateAuth({
32
52
  auth: { emailAndPassword: { enabled: true } },
33
53
  env: "dev",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Commands named in the agent contract must exist on the CLI.
3
+ */
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { describe, expect, test } from "bun:test";
7
+ import { commandNames } from "./registry.ts";
8
+
9
+ describe("AGENTS.md commands", () => {
10
+ test("every oke command mentioned in AGENTS.md is registered", () => {
11
+ const text = readFileSync(new URL("../../AGENTS.md", import.meta.url), "utf8");
12
+ const registered = new Set(commandNames());
13
+ const mentioned = new Set<string>();
14
+ for (const match of text.matchAll(/`oke ([a-z][a-z0-9-]*)/g)) {
15
+ const name = match[1];
16
+ if (name) mentioned.add(name);
17
+ }
18
+ expect(mentioned.has("eval")).toBe(true);
19
+ expect(mentioned.has("decide")).toBe(true);
20
+ expect(text).not.toContain("oke decide certify");
21
+ for (const name of mentioned) {
22
+ expect(registered.has(name)).toBe(true);
23
+ }
24
+ });
25
+ });
@@ -3,12 +3,14 @@
3
3
  */
4
4
 
5
5
  import { afterEach, describe, expect, test } from "bun:test";
6
+ import { EventEmitter } from "node:events";
6
7
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
8
  import { tmpdir } from "node:os";
8
9
  import { join, resolve } from "node:path";
9
10
  import { resetRequiredEnvNames, resetSecrets } from "../elements/vault/declare.ts";
10
11
  import { resetBindings } from "../kernel/on.ts";
11
- import { maybeAskVaultGaps, probeVaultGaps } from "./ask-vault-gaps.ts";
12
+ import { maybeAskVaultGaps, probeVaultGaps, secretPromptWrite } from "./ask-vault-gaps.ts";
13
+ import { promptHidden } from "./vault-secure-input.ts";
12
14
 
13
15
  const OKE = resolve(import.meta.dir, "../index.ts");
14
16
 
@@ -93,6 +95,41 @@ describe("maybeAskVaultGaps", () => {
93
95
  });
94
96
  });
95
97
 
98
+ describe("secretPromptWrite", () => {
99
+ test("keeps the mask on the prompt line", async () => {
100
+ const stdin = new EventEmitter() as EventEmitter & {
101
+ setRawMode?: (mode: boolean) => void;
102
+ off?: (event: string, listener: (...args: unknown[]) => void) => EventEmitter;
103
+ };
104
+ stdin.setRawMode = () => undefined;
105
+ stdin.off = (event: string | symbol, listener: (...args: unknown[]) => void) => {
106
+ stdin.removeListener(event, listener);
107
+ return stdin;
108
+ };
109
+ const shown: string[] = [];
110
+ const echoed: string[] = [];
111
+ const pending = promptHidden("Enter value for OPENROUTER_API_KEY: ", {
112
+ stdin,
113
+ write: secretPromptWrite(
114
+ (text) => shown.push(text),
115
+ (text) => echoed.push(text),
116
+ ),
117
+ exit: () => {
118
+ throw new Error("exit should not run");
119
+ },
120
+ });
121
+ await Promise.resolve();
122
+ stdin.emit("data", "sk");
123
+ stdin.emit("data", "\r");
124
+ expect(await pending).toBe("sk");
125
+ expect(shown).toHaveLength(1);
126
+ expect(shown[0]).toContain("Enter value for OPENROUTER_API_KEY: ");
127
+ expect(shown[0]?.endsWith("\n")).toBe(false);
128
+ expect(echoed.join("")).toBe("**\n");
129
+ expect(shown.join("")).not.toContain("*");
130
+ });
131
+ });
132
+
96
133
  describe("probeVaultGaps", () => {
97
134
  test("sees secrets on oke({ secrets }) after consume drains the registry", async () => {
98
135
  const dir = mkdtempSync(join(tmpdir(), "probe-vault-"));
@@ -14,7 +14,7 @@ import { buildVaultBootChain, normalizeVaultDriverId } from "../elements/vault/b
14
14
  import { createVaultRuntime, VaultBootError, type VaultGap } from "../elements/vault/runtime.ts";
15
15
  import type { OkeOptions } from "../kernel/app.ts";
16
16
  import { requiredEnvRegistry, secretRegistry } from "../kernel/element-registries.ts";
17
- import { formatCliChrome } from "../term.ts";
17
+ import { formatCliChrome, formatStatusLine } from "../term.ts";
18
18
  import { loadOkeConfig } from "./load-config.ts";
19
19
  import { openEnvStore } from "./vault-cmd.ts";
20
20
  import { promptHidden } from "./vault-secure-input.ts";
@@ -147,6 +147,35 @@ function unionSecrets(
147
147
  return out;
148
148
  }
149
149
 
150
+ /**
151
+ * Writer for {@link promptHidden} during `oke dev`.
152
+ *
153
+ * The question is one chrome row with no trailing newline, so mask
154
+ * characters stay on that row. Echo (`*`, backspace, Enter) goes straight
155
+ * to stdout: `formatCliChrome` would turn each `*` into its own `│` line,
156
+ * and the anchored board would count each of those writes as a new row.
157
+ *
158
+ * @param sink - Line-oriented stdout (the dev board wrapper)
159
+ * @param echo - Raw echo. Defaults to `process.stdout.write`
160
+ */
161
+ export function secretPromptWrite(
162
+ sink: (text: string) => void,
163
+ echo: (text: string) => void = (text) => {
164
+ process.stdout.write(text);
165
+ },
166
+ ): (text: string) => void {
167
+ let opened = false;
168
+ return (text: string) => {
169
+ if (!opened) {
170
+ opened = true;
171
+ const line = formatStatusLine(text);
172
+ sink(line.endsWith("\n") ? line.slice(0, -1) : line);
173
+ return;
174
+ }
175
+ echo(text);
176
+ };
177
+ }
178
+
150
179
  /**
151
180
  * After compose hydrate (and usually schema push), fill Vault boot gaps
152
181
  * one-by-one into `.env.local`. Skips when non-TTY or no gaps.
@@ -175,7 +204,11 @@ export async function maybeAskVaultGaps(options: AskVaultGapsOptions): Promise<n
175
204
  const envPath = resolve(cwd, options.envFile ?? ".env.local");
176
205
  const store = await openEnvStore(envPath);
177
206
  const read =
178
- options.readSecret ?? ((prompt: string) => promptHidden(prompt, { write: chromeWrite }));
207
+ options.readSecret ??
208
+ ((prompt: string) =>
209
+ promptHidden(prompt, {
210
+ write: secretPromptWrite(write),
211
+ }));
179
212
 
180
213
  const remaining: VaultGap[] = [];
181
214
  for (const gap of gaps) {
@@ -11,8 +11,8 @@ import { join } from "node:path";
11
11
 
12
12
  const ROOT = join(import.meta.dir, "../..");
13
13
 
14
- /** Forbidden peer name — split so this file does not match itself. */
15
- const FORBIDDEN_HONO = ["Ho", "no"].join("");
14
+ /** Word-boundary router peer — `-F` false-positives on `Honored` / `canHonor`. */
15
+ const FORBIDDEN_HONO = ["\\b", "Ho", "no", "\\b"].join("");
16
16
  /** Forbidden peer name. */
17
17
  const FORBIDDEN_ELYSIA = ["Ely", "sia"].join("");
18
18
  /** Forbidden peer name (covers Enc + ore.ts). */
@@ -104,7 +104,7 @@ function assertZeroGitGrepRegex(pattern: string, ignorePrefixes: readonly string
104
104
 
105
105
  describe("named competitor mention removal gate", () => {
106
106
  test("tracked tree has zero mentions of the thin multi-runtime router peer", () => {
107
- assertZeroGitGrepFixed(FORBIDDEN_HONO);
107
+ assertZeroGitGrepRegex(FORBIDDEN_HONO);
108
108
  });
109
109
 
110
110
  test("tracked tree has zero mentions of the Bun-first peer framework", () => {