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
@@ -156,7 +156,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
156
156
 
157
157
  test("app.fetch list/get/create/update: only createdAt + masked email, never raw SQL keys", async () => {
158
158
  const app = buildApp();
159
- const t = await createTestApp(app);
159
+ const t = await createTestApp(app, { capability: "open" });
160
160
 
161
161
  // create → 201
162
162
  const created = await app.fetch(
@@ -234,7 +234,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
234
234
 
235
235
  test("list cursor meta walks next and previous", async () => {
236
236
  const app = buildApp();
237
- const t = await createTestApp(app);
237
+ const t = await createTestApp(app, { capability: "open" });
238
238
  for (const email of ["a@example.com", "b@example.com", "c@example.com"]) {
239
239
  const created = await app.fetch(
240
240
  new Request("http://localhost/contacts", {
@@ -317,7 +317,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
317
317
  config: { drivers: { store: { sql: { test: "pglite" } } } },
318
318
  });
319
319
 
320
- const t = await createTestApp(app);
320
+ const t = await createTestApp(app, { capability: "open" });
321
321
  const created = await app.fetch(
322
322
  new Request("http://localhost/people", {
323
323
  method: "POST",
@@ -289,6 +289,7 @@ describe("store.schema RLS extras", () => {
289
289
  "scope_booking_create_insert",
290
290
  ]);
291
291
  const src = emitDrizzleSource([bookings], "postgres");
292
+ expect(src).toContain("pgTable.withRLS");
292
293
  expect(src).toContain("pgPolicy");
293
294
  expect(src).toContain("oke.gate() = ");
294
295
  expect(src).toContain("oke.user()");
@@ -215,6 +215,8 @@ function compileSqlNode(node: unknown): CompiledNode {
215
215
 
216
216
  // Group level: only structural parens and ` and ` / ` or ` separators may
217
217
  // appear next to nested conditions — anything else is a loud failure.
218
+ // `1.0.0-rc.5-5935859` glues the parens onto the separator (`"(("`,
219
+ // `") and ("`, `"))`).
218
220
  let joiner: "AND" | "OR" | undefined;
219
221
  for (const chunk of chunks) {
220
222
  if (isDrizzleSql(chunk)) continue;
@@ -222,14 +224,13 @@ function compileSqlNode(node: unknown): CompiledNode {
222
224
  if (text === undefined) {
223
225
  throw new TypeError("sql where: unsupported mixed condition");
224
226
  }
225
- const token = text.trim().toLowerCase();
226
- if (token === "" || token === "(" || token === ")") continue;
227
- if (token === "and" || token === "or") {
228
- const next = token.toUpperCase() as "AND" | "OR";
229
- if (joiner !== undefined && joiner !== next) {
227
+ const sep = groupSeparator(text);
228
+ if (sep === "skip") continue;
229
+ if (sep === "AND" || sep === "OR") {
230
+ if (joiner !== undefined && joiner !== sep) {
230
231
  throw new TypeError("sql where: mixed AND/OR at one level — parenthesize explicitly");
231
232
  }
232
- joiner = next;
233
+ joiner = sep;
233
234
  continue;
234
235
  }
235
236
  throw new TypeError(`sql where: unsupported fragment ${JSON.stringify(text)}`);
@@ -329,6 +330,22 @@ function compileLeaf(chunks: readonly unknown[]): CompiledNode {
329
330
  };
330
331
  }
331
332
 
333
+ /**
334
+ * Classify a group-level string chunk.
335
+ *
336
+ * Empty or parenthesis-only text is structural. `and` / `or` may sit inside
337
+ * the parentheses (`") and ("`). Anything else is not a separator.
338
+ *
339
+ * @param text - Raw `StringChunk` text
340
+ */
341
+ function groupSeparator(text: string): "skip" | "AND" | "OR" | undefined {
342
+ const token = text.trim().toLowerCase().replace(/[()]/g, "").trim();
343
+ if (token === "") return "skip";
344
+ if (token === "and") return "AND";
345
+ if (token === "or") return "OR";
346
+ return undefined;
347
+ }
348
+
332
349
  function chunksOf(node: unknown): readonly unknown[] {
333
350
  return (node as { queryChunks: unknown[] }).queryChunks;
334
351
  }
@@ -353,7 +370,7 @@ function isStringChunk(chunk: object): boolean {
353
370
  /**
354
371
  * Joined text of a Drizzle `StringChunk`.
355
372
  *
356
- * drizzle-orm `1.0.0-rc.5-169397b` stores `value` as a `string`. Earlier
373
+ * drizzle-orm `1.0.0-rc.5-5935859` stores `value` as a `string`. Earlier
357
374
  * snapshots (and the synthetic chunks in `resource.ts`) store `string[]`.
358
375
  */
359
376
  function chunkText(chunk: unknown): string | undefined {
@@ -156,6 +156,57 @@ describe("SqlStoreHandle — upsert", () => {
156
156
  expect(row).toEqual({ id: "welcome", title: "Hello", createdAt: 1 });
157
157
  });
158
158
 
159
+ test("primary-key insert and update each hit the connection once", async () => {
160
+ let queries = 0;
161
+ const counting = {
162
+ driverId: sharedConn.driverId,
163
+ role: sharedConn.role,
164
+ query(sql: string, params?: readonly unknown[]) {
165
+ queries += 1;
166
+ return sharedConn.query(sql, params);
167
+ },
168
+ exec: (sql: string, params?: readonly unknown[]) => sharedConn.exec(sql, params),
169
+ close: () => sharedConn.close(),
170
+ } satisfies SqlConnection;
171
+ const handle = createSqlStoreHandle("sql:app", {
172
+ connection: counting,
173
+ classifications: new Map(),
174
+ routedRole: "primary",
175
+ domainDdl: "off",
176
+ });
177
+
178
+ queries = 0;
179
+ const inserted = await handle.upsert(
180
+ posts,
181
+ { id: "once" },
182
+ { id: "once", title: "a", createdAt: 1 },
183
+ );
184
+ expect(inserted.status).toBe("upserted");
185
+ expect(queries).toBe(1);
186
+
187
+ queries = 0;
188
+ const updated = await handle.upsert(
189
+ posts,
190
+ { id: "once" },
191
+ { id: "once", title: "b", createdAt: 2 },
192
+ { onExisting: "update" },
193
+ );
194
+ expect(updated.status).toBe("changed");
195
+ expect(queries).toBe(1);
196
+
197
+ const row = await sharedHandle.findById(posts, "once");
198
+ expect(row).toEqual({ id: "once", title: "b", createdAt: 2 });
199
+
200
+ queries = 0;
201
+ const other = await handle.upsert(
202
+ posts,
203
+ { title: "missing" },
204
+ { id: "other", title: "missing", createdAt: 3 },
205
+ );
206
+ expect(other.status).toBe("upserted");
207
+ expect(queries).toBe(2);
208
+ });
209
+
159
210
  test("onExisting update changes matched columns", async () => {
160
211
  await sharedHandle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
161
212
  const updated = await sharedHandle.upsert(
@@ -424,7 +424,9 @@ export interface SqlStoreHandle {
424
424
  exists(table: TableHandle | unknown, idOrWhere: string | WhereMap): Promise<boolean>;
425
425
  /**
426
426
  * Insert when no row matches `matchOn`; never touches an existing match
427
- * unless `options.onExisting` is `"update"`.
427
+ * unless `options.onExisting` is `"update"`. A match that is only the
428
+ * primary key is one `INSERT … ON CONFLICT` round trip on postgres and
429
+ * pglite. Memory SQL, and any other predicate, stays a read then a write.
428
430
  *
429
431
  * @param table - Table
430
432
  * @param matchOn - Equality map or Drizzle condition identifying the row
@@ -685,6 +687,101 @@ export function createSqlStoreHandle(
685
687
  return coerceCompiledWhere(table, compileWhere(where));
686
688
  }
687
689
 
690
+ /**
691
+ * Primary-key equality only. Any other predicate cannot use `ON CONFLICT`.
692
+ *
693
+ * @param compiled - Match clause
694
+ * @param pkCol - Primary key SQL name
695
+ */
696
+ function primaryKeyEquality(
697
+ compiled: ReturnType<typeof compileWhere>,
698
+ pkCol: string,
699
+ ): { readonly value: unknown } | undefined {
700
+ if (compiled.predicates.length !== 1) return undefined;
701
+ const pred = compiled.predicates[0];
702
+ if (!pred || pred.op !== "=" || pred.column !== pkCol) return undefined;
703
+ return { value: pred.value };
704
+ }
705
+
706
+ /**
707
+ * One round trip: `INSERT … ON CONFLICT (pk)`.
708
+ *
709
+ * `DO NOTHING` when the existing row must stay. `DO UPDATE` captures the
710
+ * pre-image in a CTE so CDC still sees before and after.
711
+ *
712
+ * @param table - Table
713
+ * @param name - SQL table name
714
+ * @param pkCol - Primary key SQL name
715
+ * @param pkValue - Bound primary key
716
+ * @param values - Insert / update payload
717
+ * @param onExisting - `"update"` rewrites the existing row
718
+ */
719
+ async function upsertOnPrimaryKey(
720
+ table: TableHandle | unknown,
721
+ name: string,
722
+ pkCol: string,
723
+ pkValue: unknown,
724
+ values: SqlRow,
725
+ onExisting: "update" | undefined,
726
+ ): Promise<UpsertResult> {
727
+ const prepared = prepareInsertRow(table, values);
728
+ if (!(pkCol in prepared)) prepared[pkCol] = pkValue;
729
+ const cols = Object.keys(prepared);
730
+ const colList = cols.map(quoteIdent).join(", ");
731
+ const placeholders = cols.map(() => "?").join(", ");
732
+ const params = cols.map((col) => prepared[col]);
733
+ const quotedPk = quoteIdent(pkCol);
734
+ const quotedName = quoteIdent(name);
735
+ const setEntries =
736
+ onExisting === "update"
737
+ ? Object.entries(prepareUpdateRow(table, values)).filter(([col]) => col !== pkCol)
738
+ : [];
739
+ if (onExisting !== "update" || setEntries.length === 0) {
740
+ const rows = await query(
741
+ `INSERT INTO ${quotedName} (${colList}) VALUES (${placeholders})` +
742
+ ` ON CONFLICT (${quotedPk}) DO NOTHING RETURNING *`,
743
+ params,
744
+ );
745
+ if (rows.length === 0) {
746
+ return { status: onExisting === "update" ? "changed" : "already-existed" };
747
+ }
748
+ notifySqlCdc({
749
+ tableName: name,
750
+ op: "insert",
751
+ before: null,
752
+ after: toJs(table, rows)[0] ?? null,
753
+ });
754
+ return { status: "upserted" };
755
+ }
756
+ const setSql = setEntries
757
+ .map(([col]) => `${quoteIdent(col)} = EXCLUDED.${quoteIdent(col)}`)
758
+ .join(", ");
759
+ // The old row is a CTE, then read from an outer SELECT. INSERT …
760
+ // RETURNING cannot see that CTE on PGlite, so the before image would
761
+ // be null and an update would look like an insert.
762
+ const result = await query(
763
+ `WITH __oke_old AS (` +
764
+ `SELECT * FROM ${quotedName} WHERE ${quotedPk} = ? FOR UPDATE` +
765
+ `), __oke_new AS (` +
766
+ `INSERT INTO ${quotedName} (${colList}) VALUES (${placeholders})` +
767
+ ` ON CONFLICT (${quotedPk}) DO UPDATE SET ${setSql}` +
768
+ ` RETURNING *` +
769
+ `) SELECT (SELECT row_to_json(__oke_old) FROM __oke_old) AS __oke_before_data,` +
770
+ ` (SELECT row_to_json(__oke_new) FROM __oke_new) AS __oke_after_data`,
771
+ [pkValue, ...params],
772
+ );
773
+ const raw = result[0];
774
+ const before = raw ? jsonImageToJs(table, raw.__oke_before_data) : null;
775
+ const after = raw ? jsonImageToJs(table, raw.__oke_after_data) : null;
776
+ notifySqlCdc({
777
+ tableName: name,
778
+ op: before ? "update" : "insert",
779
+ before,
780
+ after,
781
+ });
782
+ return { status: before ? "changed" : "upserted" };
783
+ }
784
+
688
785
  /**
689
786
  * Postgres `row_to_json` images come back as JSON strings on pglite and
690
787
  * objects on Bun.SQL — normalize to the JS row mapping (snake→camel).
@@ -1026,6 +1123,19 @@ export function createSqlStoreHandle(
1026
1123
  if (!compiled.clause) {
1027
1124
  throw new Error("upsert() requires at least one matchOn predicate");
1028
1125
  }
1126
+ const pkCol = resolvePkColumn(table);
1127
+ const pkMatch = primaryKeyEquality(compiled, pkCol);
1128
+ // Memory SQL has no ON CONFLICT. Postgres and PGlite do.
1129
+ if (pkMatch && connection.driverId !== "memory") {
1130
+ return upsertOnPrimaryKey(
1131
+ table,
1132
+ name,
1133
+ pkCol,
1134
+ pkMatch.value,
1135
+ values,
1136
+ upsertOptions?.onExisting,
1137
+ );
1138
+ }
1029
1139
  const found = await query(
1030
1140
  `SELECT 1 AS "ok" FROM ${quoteIdent(name)} WHERE ${compiled.clause} LIMIT 1`,
1031
1141
  compiled.params,
@@ -65,7 +65,7 @@ describe("fx.store(db).upsert via app.fetch (real boot)", () => {
65
65
  gate: { policies: [gate.public] },
66
66
  }).adopt({ seed });
67
67
  Object.assign(app.$options, { env: "test", stores: [db], unguardedHttp: "allow" });
68
- await createTestApp(app);
68
+ await createTestApp(app, { capability: "open" });
69
69
 
70
70
  async function post(body: unknown) {
71
71
  const res = await app.fetch(
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Audit sinks — secret-free stdout/webhook payloads, and chain commands
3
+ * that exist only for the `db` sink.
4
+ */
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { createAuditSink, type AuditEntry } from "./audit.ts";
8
+ import { buildVaultBootChain } from "./boot-chain.ts";
9
+ import { createBuiltinVaultAdapter, sqlConnectionAsExec } from "./builtin-adapter.ts";
10
+ import { isVaultError } from "./errors.ts";
11
+ import { createMemoryVaultSql } from "./test-helpers.ts";
12
+
13
+ const ENTRY: AuditEntry = {
14
+ action: "set",
15
+ path: "prod/api",
16
+ actorType: "flow",
17
+ actorId: "orders.place",
18
+ success: true,
19
+ requestId: "req-1",
20
+ at: new Date("2026-01-02T03:04:05.000Z"),
21
+ };
22
+
23
+ const SECRET = "sk_live_do_not_log_me";
24
+
25
+ describe("webhook audit sink", () => {
26
+ test("missing or non-http webhookUrl fails at construction", () => {
27
+ expect(() => createAuditSink("webhook")).toThrow(/webhookUrl/);
28
+ expect(
29
+ isVaultError(
30
+ catchError(() => createAuditSink("webhook")),
31
+ "MISSING_PEER",
32
+ ),
33
+ ).toBe(true);
34
+ expect(
35
+ isVaultError(
36
+ catchError(() => createAuditSink("webhook", { webhookUrl: "/relative" })),
37
+ "MISSING_PEER",
38
+ ),
39
+ ).toBe(true);
40
+ expect(
41
+ isVaultError(
42
+ catchError(() => createAuditSink("webhook", { webhookUrl: "ftp://audit.example/hook" })),
43
+ "MISSING_PEER",
44
+ ),
45
+ ).toBe(true);
46
+ });
47
+
48
+ test("POSTs the stdout-shaped JSON, refuses redirects, and omits secret values", async () => {
49
+ const bodies: string[] = [];
50
+ const inits: RequestInit[] = [];
51
+ const sink = createAuditSink("webhook", {
52
+ webhookUrl: "https://audit.example/hook",
53
+ fetch: (async (_url, init) => {
54
+ inits.push(init ?? {});
55
+ bodies.push(String(init?.body ?? ""));
56
+ return new Response(null, { status: 204 });
57
+ }) as typeof fetch,
58
+ });
59
+
60
+ await sink.append(ENTRY);
61
+
62
+ expect(inits[0]?.method).toBe("POST");
63
+ expect(inits[0]?.redirect).toBe("error");
64
+ expect(inits[0]?.signal).toBeInstanceOf(AbortSignal);
65
+ const parsed = JSON.parse(bodies[0] ?? "{}") as Record<string, unknown>;
66
+ expect(parsed).toEqual({
67
+ sink: "oke.vault.audit",
68
+ action: "set",
69
+ path: "prod/api",
70
+ actorType: "flow",
71
+ actorId: "orders.place",
72
+ success: true,
73
+ errorCode: null,
74
+ errorMessage: null,
75
+ requestId: "req-1",
76
+ createdAt: "2026-01-02T03:04:05.000Z",
77
+ });
78
+ expect(bodies[0]).not.toContain(SECRET);
79
+ expect(parsed).not.toHaveProperty("value");
80
+ });
81
+
82
+ test("a non-2xx response rejects without echoing the body", async () => {
83
+ const sink = createAuditSink("webhook", {
84
+ webhookUrl: "https://audit.example/hook",
85
+ fetch: (async () => new Response(SECRET, { status: 502 })) as unknown as typeof fetch,
86
+ });
87
+ const failure = await sink.append(ENTRY).catch((err: unknown) => err);
88
+ expect(isVaultError(failure, "BACKEND_ERROR")).toBe(true);
89
+ expect((failure as Error).message).not.toContain(SECRET);
90
+ expect((failure as Error).message).toContain("502");
91
+ });
92
+ });
93
+
94
+ describe("vault boot chain", () => {
95
+ test("forwards vault.audit only onto the builtin driver layer", () => {
96
+ const audit = { sink: "webhook" as const, webhookUrl: "https://audit.example/hook" };
97
+ const chain = buildVaultBootChain({ driverId: "vault", audit });
98
+ expect(chain[0]?.driver.id).toBe("vault");
99
+ expect(chain[0]?.options?.audit).toEqual(audit);
100
+ expect(chain.slice(1).every((layer) => layer.options?.audit === undefined)).toBe(true);
101
+ });
102
+ });
103
+
104
+ describe("non-db audit commands", () => {
105
+ test("verify refuses a webhook sink before touching the chain", async () => {
106
+ const sql = createMemoryVaultSql();
107
+ try {
108
+ const adapter = createBuiltinVaultAdapter({
109
+ db: sqlConnectionAsExec(sql),
110
+ audit: { sink: "webhook", webhookUrl: "https://audit.example/hook" },
111
+ fetch: (async () => new Response(null, { status: 500 })) as unknown as typeof fetch,
112
+ });
113
+ const failure = await adapter.verifyAudit().catch((err: unknown) => err);
114
+ expect(isVaultError(failure, "UNSUPPORTED")).toBe(true);
115
+ expect((failure as Error).message).toContain('audit.sink "db"');
116
+
117
+ const init = await adapter.initialize();
118
+ expect(init.masterKey.length).toBeGreaterThan(0);
119
+ } finally {
120
+ await sql.close();
121
+ }
122
+ });
123
+ });
124
+
125
+ /**
126
+ * Capture a thrown construction error.
127
+ *
128
+ * @param fn - Throwing call
129
+ */
130
+ function catchError(fn: () => unknown): unknown {
131
+ try {
132
+ fn();
133
+ return undefined;
134
+ } catch (err) {
135
+ return err;
136
+ }
137
+ }
@@ -9,8 +9,10 @@
9
9
 
10
10
  import { VaultError } from "./errors.ts";
11
11
  import type { VaultErrorCode } from "./errors.ts";
12
- import type { VaultActorType } from "./types.ts";
13
- import type { VaultAuditSinkKind } from "./types.ts";
12
+ import type { VaultActorType, VaultAuditConfig, VaultAuditSinkKind } from "./types.ts";
13
+
14
+ /** Bound on a webhook audit POST so a dead receiver cannot stall a write. */
15
+ const AUDIT_WEBHOOK_TIMEOUT_MS = 5_000;
14
16
 
15
17
  /** Operations recorded in the audit chain. */
16
18
  export type AuditAction =
@@ -110,8 +112,10 @@ export interface AuditWriter {
110
112
  export interface CreateAuditSinkOptions {
111
113
  /** Required for the `db` sink. */
112
114
  readonly writer?: AuditWriter;
113
- /** Required for the `webhook` sink (not yet implemented). */
115
+ /** Required for the `webhook` sink. Absolute `http:` or `https:` URL. */
114
116
  readonly webhookUrl?: string;
117
+ /** Fetch implementation for the `webhook` sink (tests). */
118
+ readonly fetch?: typeof globalThis.fetch;
115
119
  /** Line writer for the `stdout` sink (tests). */
116
120
  readonly write?: (line: string) => void;
117
121
  /** Clock for `stdout` timestamps (tests). */
@@ -169,23 +173,98 @@ export async function computeAuditRowHash(
169
173
  return Buffer.from(new Uint8Array(digest)).toString("hex");
170
174
  }
171
175
 
176
+ /**
177
+ * Reject a webhook URL that is missing or not an absolute http(s) URL.
178
+ *
179
+ * @param webhookUrl - Configured destination
180
+ * @returns Absolute URL string
181
+ * @throws VaultError `MISSING_PEER` when the URL is absent or not http(s)
182
+ */
183
+ function requireWebhookUrl(webhookUrl: string | undefined): string {
184
+ const raw = webhookUrl?.trim() ?? "";
185
+ if (raw.length === 0) {
186
+ throw new VaultError("MISSING_PEER", "vault: webhook audit sink requires webhookUrl");
187
+ }
188
+ let parsed: URL;
189
+ try {
190
+ parsed = new URL(raw);
191
+ } catch {
192
+ throw new VaultError(
193
+ "MISSING_PEER",
194
+ "vault: webhook audit sink requires an absolute http(s) webhookUrl",
195
+ );
196
+ }
197
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
198
+ throw new VaultError(
199
+ "MISSING_PEER",
200
+ "vault: webhook audit sink requires an absolute http(s) webhookUrl",
201
+ );
202
+ }
203
+ return parsed.href;
204
+ }
205
+
206
+ /**
207
+ * JSON body shared by the `stdout` and `webhook` sinks.
208
+ *
209
+ * Only normalized metadata is serialized — an entry never holds a value.
210
+ *
211
+ * @param entry - Operation to record
212
+ * @param now - Clock when the entry has no `at`
213
+ */
214
+ function auditEventJson(entry: AuditEntry, now: () => Date): string {
215
+ return JSON.stringify({
216
+ sink: "oke.vault.audit",
217
+ ...toAuditHashPayload(entry, entry.at ?? now()),
218
+ });
219
+ }
220
+
172
221
  /**
173
222
  * Build an audit sink.
174
223
  *
175
224
  * - `db` delegates to an {@link AuditWriter} supplied by storage.
176
225
  * - `stdout` writes one secret-free JSON line per operation.
177
- * - `webhook` is not implemented yet and throws `UNSUPPORTED`.
226
+ * - `webhook` POSTs that same JSON. Delivery failure throws; callers that
227
+ * record after a successful operation swallow it.
228
+ *
229
+ * Hash-chain verify and purge apply only to the `db` sink.
178
230
  *
179
231
  * @param kind - Sink kind from `audit.sink`
180
232
  * @param opts - Writer / webhook URL / test seams
181
- * @throws VaultError `UNSUPPORTED` for `webhook`, or `MISSING_PEER` when `db` has no writer
233
+ * @throws VaultError `MISSING_PEER` when `db` has no writer or `webhook` has no absolute URL
182
234
  */
183
235
  export function createAuditSink(
184
236
  kind: VaultAuditSinkKind,
185
237
  opts: CreateAuditSinkOptions = {},
186
238
  ): AuditSink {
187
239
  if (kind === "webhook") {
188
- throw new VaultError("UNSUPPORTED", "vault: webhook audit sink is not implemented");
240
+ const url = requireWebhookUrl(opts.webhookUrl);
241
+ const fetchFn = opts.fetch ?? globalThis.fetch;
242
+ const now = opts.now ?? (() => new Date());
243
+ return {
244
+ kind: "webhook",
245
+ async append(entry) {
246
+ const body = auditEventJson(entry, now);
247
+ let response: Response;
248
+ try {
249
+ response = await fetchFn(url, {
250
+ method: "POST",
251
+ headers: { "content-type": "application/json" },
252
+ body,
253
+ redirect: "error",
254
+ signal: AbortSignal.timeout(AUDIT_WEBHOOK_TIMEOUT_MS),
255
+ });
256
+ } catch (err) {
257
+ if (err instanceof VaultError) throw err;
258
+ throw new VaultError("BACKEND_ERROR", "vault: webhook audit sink request failed");
259
+ }
260
+ if (!response.ok) {
261
+ throw new VaultError(
262
+ "BACKEND_ERROR",
263
+ `vault: webhook audit sink failed (${response.status})`,
264
+ );
265
+ }
266
+ },
267
+ };
189
268
  }
190
269
 
191
270
  if (kind === "db") {
@@ -206,17 +285,31 @@ export function createAuditSink(
206
285
  return {
207
286
  kind: "stdout",
208
287
  async append(entry) {
209
- // Only normalized metadata is serialized — an entry never holds a value.
210
- write(
211
- JSON.stringify({
212
- sink: "oke.vault.audit",
213
- ...toAuditHashPayload(entry, entry.at ?? now()),
214
- }),
215
- );
288
+ write(auditEventJson(entry, now));
216
289
  },
217
290
  };
218
291
  }
219
292
 
293
+ /**
294
+ * Pick the sink for a built-in adapter from `vault.audit`.
295
+ *
296
+ * `enabled: false` drops every row. Omitted config is the `db` chain.
297
+ *
298
+ * @param config - `vault.audit` block, when present
299
+ * @param opts - SQL writer and optional fetch
300
+ */
301
+ export function auditSinkForConfig(
302
+ config: VaultAuditConfig | undefined,
303
+ opts: { readonly writer: AuditWriter; readonly fetch?: typeof globalThis.fetch },
304
+ ): AuditSink {
305
+ if (config?.enabled === false) return createNullAuditSink();
306
+ return createAuditSink(config?.sink ?? "db", {
307
+ writer: opts.writer,
308
+ ...(config?.webhookUrl === undefined ? {} : { webhookUrl: config.webhookUrl }),
309
+ ...(opts.fetch === undefined ? {} : { fetch: opts.fetch }),
310
+ });
311
+ }
312
+
220
313
  /**
221
314
  * Sink that drops every entry (`audit.enabled: false`).
222
315
  *
@@ -22,6 +22,7 @@ import {
22
22
  import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
23
23
  import type { VaultDriverId } from "../../drivers/vault-types.ts";
24
24
  import { resolveComposeEnvPath } from "./chain.ts";
25
+ import type { VaultAuditConfig } from "./types.ts";
25
26
  import type { VaultChainLayer } from "./runtime.ts";
26
27
 
27
28
  /** Options for {@link buildVaultBootChain}. */
@@ -34,6 +35,10 @@ export interface BuildVaultBootChainOptions {
34
35
  readonly env?: ConfigEnv;
35
36
  /** Seed secrets for the terminal memory / managed layer. */
36
37
  readonly seed?: Readonly<Record<string, string>>;
38
+ /**
39
+ * Built-in store audit block. Forwarded only to the `vault` driver layer.
40
+ */
41
+ readonly audit?: VaultAuditConfig;
37
42
  }
38
43
 
39
44
  /**
@@ -142,7 +147,10 @@ export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultC
142
147
  {
143
148
  driver: builtinVaultDriver,
144
149
  source: "driver",
145
- options: { secrets: seed },
150
+ options: {
151
+ secrets: seed,
152
+ ...(options.audit === undefined ? {} : { audit: options.audit }),
153
+ },
146
154
  },
147
155
  ...envLayers,
148
156
  ];