okengine 0.10.3 → 0.11.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 (318) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  3. package/site/content/docs/deployment/docker.mdx +43 -36
  4. package/site/content/docs/deployment/index.mdx +9 -9
  5. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  6. package/site/content/docs/elements/ai.mdx +7 -7
  7. package/site/content/docs/elements/channel.mdx +9 -7
  8. package/site/content/docs/elements/clock.mdx +8 -8
  9. package/site/content/docs/elements/flow.mdx +46 -47
  10. package/site/content/docs/elements/gate.mdx +6 -5
  11. package/site/content/docs/elements/signal.mdx +2 -2
  12. package/site/content/docs/elements/store.mdx +71 -78
  13. package/site/content/docs/elements/vault.mdx +89 -70
  14. package/site/content/docs/get-started/basic-usage.mdx +8 -8
  15. package/site/content/docs/get-started/installation.mdx +36 -36
  16. package/site/content/docs/get-started/introduction.mdx +11 -0
  17. package/site/content/docs/get-started/why.mdx +2 -2
  18. package/site/content/docs/plugins/anonymous.mdx +1 -1
  19. package/site/content/docs/plugins/magic-link.mdx +11 -11
  20. package/site/content/docs/plugins/otp.mdx +2 -2
  21. package/site/content/docs/plugins/passkey.mdx +1 -1
  22. package/site/content/docs/plugins/two-factor.mdx +1 -1
  23. package/site/content/docs/plugins/username.mdx +1 -1
  24. package/site/content/docs/recipes/caddy.mdx +1 -1
  25. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  26. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  27. package/site/content/docs/recipes/index.mdx +1 -2
  28. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  29. package/site/content/docs/recipes/mailpit.mdx +11 -11
  30. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  31. package/site/content/docs/recipes/meta.json +0 -1
  32. package/site/content/docs/recipes/ollama.mdx +1 -1
  33. package/site/content/docs/recipes/pgdog.mdx +12 -9
  34. package/site/content/docs/recipes/postgres.mdx +3 -3
  35. package/site/content/docs/recipes/redis.mdx +2 -2
  36. package/site/content/docs/recipes/rustfs.mdx +10 -7
  37. package/site/content/docs/recipes/sglang.mdx +1 -1
  38. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  39. package/site/content/docs/recipes/timescale.mdx +2 -2
  40. package/site/content/docs/recipes/valkey.mdx +1 -1
  41. package/site/content/docs/recipes/vllm.mdx +1 -1
  42. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  43. package/site/content/docs/reference/cli.md +61 -38
  44. package/site/content/docs/reference/configuration.mdx +145 -70
  45. package/site/content/docs/reference/environment-variables.mdx +36 -28
  46. package/site/content/docs/reference/errors.mdx +28 -29
  47. package/site/content/docs/reference/fx.mdx +11 -4
  48. package/site/content/docs/reference/i18n.mdx +17 -6
  49. package/site/content/docs/reference/index.mdx +5 -0
  50. package/site/content/docs/reference/meta.json +1 -0
  51. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  52. package/site/content/docs/reference/plugins.mdx +2 -2
  53. package/src/auth/gate-auth.test.ts +12 -10
  54. package/src/cli/ai-setup/ai-setup.test.ts +27 -15
  55. package/src/cli/ai-setup/apply.ts +111 -15
  56. package/src/cli/ai-setup/catalog.ts +3 -3
  57. package/src/cli/ai-setup/index.ts +1 -1
  58. package/src/cli/ai-setup/prompts.ts +1 -1
  59. package/src/cli/ai-setup/recommend.test.ts +1 -1
  60. package/src/cli/ask-seed.test.ts +3 -3
  61. package/src/cli/build.ts +1 -1
  62. package/src/cli/db-auto-push.test.ts +6 -3
  63. package/src/cli/db-auto-push.ts +7 -5
  64. package/src/cli/db-seed.ts +7 -8
  65. package/src/cli/db.test.ts +70 -71
  66. package/src/cli/db.ts +86 -12
  67. package/src/cli/dev-controller.test.ts +35 -0
  68. package/src/cli/dev-controller.ts +298 -0
  69. package/src/cli/dev-controls.ts +11 -65
  70. package/src/cli/dev-db-push.test.ts +47 -15
  71. package/src/cli/dev-schema-sync.test.ts +11 -11
  72. package/src/cli/dev-schema-sync.ts +10 -10
  73. package/src/cli/dev-session-lock.test.ts +106 -0
  74. package/src/cli/dev-session-lock.ts +209 -0
  75. package/src/cli/dev.test.ts +166 -121
  76. package/src/cli/dev.ts +138 -192
  77. package/src/cli/doc-staleness.test.ts +3 -6
  78. package/src/cli/docker-clean.test.ts +33 -1
  79. package/src/cli/docker-clean.ts +28 -33
  80. package/src/cli/docker-cli.test.ts +12 -11
  81. package/src/cli/docker.ts +95 -20
  82. package/src/cli/doctor.test.ts +40 -0
  83. package/src/cli/doctor.ts +55 -15
  84. package/src/cli/drizzle-env.test.ts +11 -13
  85. package/src/cli/drizzle-env.ts +18 -11
  86. package/src/cli/ensure-drizzle-config.ts +11 -14
  87. package/src/cli/hero-meta.test.ts +15 -15
  88. package/src/cli/hero-meta.ts +10 -9
  89. package/src/cli/index.ts +36 -6
  90. package/src/cli/load-config.images.test.ts +16 -12
  91. package/src/cli/load-config.source.test.ts +50 -0
  92. package/src/cli/load-config.ts +34 -28
  93. package/src/cli/meilisearch-local.ts +1 -2
  94. package/src/cli/mode.ts +7 -58
  95. package/src/cli/registry.ts +175 -19
  96. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  97. package/src/cli/resolve-dev-sql-env.ts +13 -15
  98. package/src/cli/safe-defaults.test.ts +28 -10
  99. package/src/cli/schema.ts +65 -16
  100. package/src/cli/test.ts +58 -0
  101. package/src/cli/tui/App.tsx +252 -0
  102. package/src/cli/tui/DevLive.tsx +116 -0
  103. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  104. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  105. package/src/cli/tui/components/LogStream.tsx +47 -0
  106. package/src/cli/tui/components/PanelCard.tsx +39 -0
  107. package/src/cli/tui/components/SelectRow.tsx +37 -0
  108. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  109. package/src/cli/tui/components/Spinner.tsx +32 -0
  110. package/src/cli/tui/components/StatusBar.tsx +51 -0
  111. package/src/cli/tui/components/StatusDot.tsx +37 -0
  112. package/src/cli/tui/components/TabBar.tsx +49 -0
  113. package/src/cli/tui/components/Wordmark.tsx +43 -0
  114. package/src/cli/tui/format.ts +38 -0
  115. package/src/cli/tui/keys.test.ts +18 -0
  116. package/src/cli/tui/keys.ts +26 -0
  117. package/src/cli/tui/launch.test.ts +18 -0
  118. package/src/cli/tui/launch.ts +57 -0
  119. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  120. package/src/cli/tui/panels/Database.tsx +198 -0
  121. package/src/cli/tui/panels/Dev.tsx +257 -0
  122. package/src/cli/tui/panels/Docker.tsx +357 -0
  123. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  124. package/src/cli/tui/panels/Navigator.tsx +213 -0
  125. package/src/cli/tui/slash-catalog.test.ts +31 -0
  126. package/src/cli/tui/slash-catalog.ts +184 -0
  127. package/src/cli/tui/slash-run.ts +54 -0
  128. package/src/cli/tui/state/ports.ts +95 -0
  129. package/src/cli/tui/theme.ts +24 -0
  130. package/src/cli/vault-cmd.test.ts +402 -0
  131. package/src/cli/vault-cmd.ts +727 -69
  132. package/src/cli/vault-secure-input.test.ts +56 -0
  133. package/src/cli/vault-secure-input.ts +124 -0
  134. package/src/compiler/effects-infer.ts +7 -0
  135. package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
  136. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  137. package/src/compiler/fixtures/skyport.expected.json +1 -1
  138. package/src/compiler-entry.ts +7 -0
  139. package/src/config/define-config.test.ts +111 -47
  140. package/src/config/driver-defaults.test.ts +16 -23
  141. package/src/config/driver-defaults.ts +33 -63
  142. package/src/config/index.ts +169 -144
  143. package/src/config/resolve-driver.test.ts +14 -19
  144. package/src/console/server/app.ts +4 -2
  145. package/src/console/server/console.test.ts +58 -1
  146. package/src/console/server/flows.ts +22 -2
  147. package/src/console/server/index.ts +3 -0
  148. package/src/console/server/operator-db.test.ts +55 -13
  149. package/src/console/server/operator-db.ts +305 -263
  150. package/src/console/server/serve.ts +16 -7
  151. package/src/console/server/state.ts +31 -9
  152. package/src/console/server/vault.test.ts +120 -5
  153. package/src/console/server/vault.ts +208 -12
  154. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  155. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  156. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  157. package/src/console/ui/dist/index.html +2 -2
  158. package/src/console/ui/shell/client.ts +16 -0
  159. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  160. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  161. package/src/console/ui/vault/backend.test.ts +75 -0
  162. package/src/console/ui/vault/backend.ts +117 -0
  163. package/src/console/ui/vault/fixture.ts +21 -2
  164. package/src/console/ui/vault/index.ts +11 -1
  165. package/src/console/ui/vault/types.ts +26 -0
  166. package/src/docker/compose.ts +164 -61
  167. package/src/docker/derive.ts +106 -12
  168. package/src/docker/docker.test.ts +156 -110
  169. package/src/docker/helpers.ts +98 -0
  170. package/src/docker/images-config.test.ts +3 -8
  171. package/src/docker/index.ts +30 -5
  172. package/src/docker/recipes/index.ts +0 -3
  173. package/src/docker/recipes/llama-cpp.ts +4 -4
  174. package/src/docker/recipes/pgdog.ts +9 -2
  175. package/src/docker/resources.test.ts +71 -0
  176. package/src/docker/resources.ts +176 -0
  177. package/src/docker/stack-id.test.ts +11 -4
  178. package/src/docker/stack-id.ts +42 -32
  179. package/src/docker/stack.integration.test.ts +4 -15
  180. package/src/docker/types.ts +35 -3
  181. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  182. package/src/drivers/conformance.test.ts +11 -12
  183. package/src/drivers/drizzle-dialect.test.ts +0 -4
  184. package/src/drivers/drizzle-dialect.ts +7 -13
  185. package/src/drivers/index.ts +24 -7
  186. package/src/drivers/journal-postgres.test.ts +3 -1
  187. package/src/drivers/journal-postgres.ts +4 -2
  188. package/src/drivers/meilisearch.ts +1 -1
  189. package/src/drivers/pglite.ts +28 -5
  190. package/src/drivers/types.ts +9 -11
  191. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  192. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  193. package/src/drivers/vault-builtin.test.ts +110 -0
  194. package/src/drivers/vault-builtin.ts +184 -0
  195. package/src/drivers/vault-driver-removal.test.ts +13 -0
  196. package/src/drivers/vault-managed.test.ts +95 -0
  197. package/src/drivers/vault-managed.ts +101 -25
  198. package/src/drivers/vault-types.ts +25 -5
  199. package/src/elements/ai/runtime.ts +1 -1
  200. package/src/elements/clock/chaos.test.ts +4 -1
  201. package/src/elements/clock/durable.ts +57 -4
  202. package/src/elements/clock.test.ts +13 -14
  203. package/src/elements/gate/constants.ts +18 -0
  204. package/src/elements/gate.test.ts +1 -1
  205. package/src/elements/gate.ts +2 -2
  206. package/src/elements/store/domain-ddl.test.ts +5 -12
  207. package/src/elements/store/emit-drizzle.ts +44 -19
  208. package/src/elements/store/index-boot.test.ts +15 -116
  209. package/src/elements/store/load-plugin-tables.ts +1 -1
  210. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  211. package/src/elements/store/resource-list-docs.test.ts +64 -44
  212. package/src/elements/store/resource.test.ts +17 -10
  213. package/src/elements/store/runtime.ts +9 -12
  214. package/src/elements/store/schema-decl.test.ts +16 -32
  215. package/src/elements/store/seed.test.ts +1 -2
  216. package/src/elements/store/seed.ts +7 -8
  217. package/src/elements/store/sql-condition.test.ts +2 -2
  218. package/src/elements/store/sql-session.test.ts +8 -10
  219. package/src/elements/store/sql-session.ts +10 -2
  220. package/src/elements/store.test.ts +13 -13
  221. package/src/elements/vault/audit.ts +233 -0
  222. package/src/elements/vault/boot-chain.test.ts +78 -0
  223. package/src/elements/vault/boot-chain.ts +43 -39
  224. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  225. package/src/elements/vault/builtin-adapter.ts +1184 -0
  226. package/src/elements/vault/crypto.test.ts +237 -0
  227. package/src/elements/vault/crypto.ts +431 -0
  228. package/src/elements/vault/declare.ts +164 -3
  229. package/src/elements/vault/errors.ts +56 -0
  230. package/src/elements/vault/kms-unseal.test.ts +76 -0
  231. package/src/elements/vault/kms-unseal.ts +181 -0
  232. package/src/elements/vault/path.ts +90 -0
  233. package/src/elements/vault/resilience.test.ts +102 -0
  234. package/src/elements/vault/resilience.ts +107 -0
  235. package/src/elements/vault/runtime.ts +36 -4
  236. package/src/elements/vault/security-checklist.test.ts +130 -0
  237. package/src/elements/vault/storage.ts +448 -0
  238. package/src/elements/vault/types.ts +288 -0
  239. package/src/elements/vault/unseal.ts +150 -0
  240. package/src/elements/vault.test.ts +72 -4
  241. package/src/elements/vault.ts +91 -2
  242. package/src/full.ts +248 -0
  243. package/src/http.ts +41 -0
  244. package/src/i18n/interpolate-simple.ts +37 -0
  245. package/src/i18n/messages.ts +51 -4
  246. package/src/i18n-entry.ts +7 -0
  247. package/src/index.ts +35 -145
  248. package/src/journal-entry.ts +20 -0
  249. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  250. package/src/kernel/app.ts +40 -42
  251. package/src/kernel/auto-registry.test.ts +2 -2
  252. package/src/kernel/boot-bind/ai.test.ts +5 -5
  253. package/src/kernel/boot-bind/ai.ts +2 -1
  254. package/src/kernel/boot-bind/channel.test.ts +6 -6
  255. package/src/kernel/boot-bind/channel.ts +3 -10
  256. package/src/kernel/boot-bind/clock.ts +2 -2
  257. package/src/kernel/boot-bind/gate.ts +5 -9
  258. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  259. package/src/kernel/boot-bind/journal.ts +2 -2
  260. package/src/kernel/boot-bind/store.test.ts +43 -44
  261. package/src/kernel/boot-bind/store.ts +16 -41
  262. package/src/kernel/boot-bind/vault.ts +16 -1
  263. package/src/kernel/boot.test.ts +12 -4
  264. package/src/kernel/boot.ts +31 -19
  265. package/src/kernel/budget-entry.ts +1 -1
  266. package/src/kernel/compensate.test.ts +288 -1
  267. package/src/kernel/compensate.ts +126 -0
  268. package/src/kernel/concurrency.test.ts +10 -9
  269. package/src/kernel/effects-stamping.test.ts +27 -2
  270. package/src/kernel/element-registries.ts +5 -3
  271. package/src/kernel/flow.ts +4 -2
  272. package/src/kernel/fx.test.ts +119 -3
  273. package/src/kernel/fx.ts +215 -40
  274. package/src/kernel/index.ts +14 -0
  275. package/src/kernel/journal.ts +130 -7
  276. package/src/kernel/pipeline.test.ts +20 -6
  277. package/src/kernel/redacted.test.ts +35 -0
  278. package/src/kernel/redacted.ts +24 -2
  279. package/src/kernel/router/create-default.ts +16 -0
  280. package/src/kernel/router/create-edge.ts +18 -0
  281. package/src/kernel/router/create.ts +23 -0
  282. package/src/kernel/router/index.ts +23 -0
  283. package/src/kernel/router/linear.ts +88 -0
  284. package/src/kernel/router/regexp.ts +188 -0
  285. package/src/kernel/router/smart.ts +120 -0
  286. package/src/kernel/router/trie.ts +108 -0
  287. package/src/kernel/router/types.ts +56 -0
  288. package/src/kernel/router.ts +18 -532
  289. package/src/kernel-entry.ts +43 -0
  290. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  291. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  292. package/src/plugins/config-source.test.ts +7 -7
  293. package/src/release/build-lib.ts +58 -0
  294. package/src/release/exports.test.ts +4 -2
  295. package/src/release/exports.ts +30 -4
  296. package/src/release/measure.ts +101 -22
  297. package/src/runs/duckdb.ts +7 -2
  298. package/src/shared/optional-peer.ts +41 -0
  299. package/src/term.test.ts +3 -44
  300. package/src/test/create-test-app.ts +12 -0
  301. package/src/test/provisions.integration.test.ts +1 -1
  302. package/src/test/reset-element-registries.ts +2 -0
  303. package/src/upgrade/codemods.test.ts +7 -20
  304. package/src/upgrade/codemods.ts +93 -60
  305. package/site/content/docs/recipes/openbao.mdx +0 -141
  306. package/src/cli/ask-dev-mode.ts +0 -40
  307. package/src/cli/dev-mode.test.ts +0 -45
  308. package/src/cli/dev-mode.ts +0 -78
  309. package/src/cli/openbao-bootstrap.test.ts +0 -150
  310. package/src/cli/openbao-bootstrap.ts +0 -321
  311. package/src/cli/openbao-restart.integration.test.ts +0 -145
  312. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  313. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  314. package/src/docker/recipes/openbao.ts +0 -47
  315. package/src/drivers/libsql.ts +0 -179
  316. package/src/drivers/sqlite.ts +0 -44
  317. package/src/drivers/vault-openbao.test.ts +0 -97
  318. package/src/drivers/vault-openbao.ts +0 -143
@@ -91,8 +91,22 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
91
91
  const create = buildUnannotatedCreateFlow(db);
92
92
  const app = oke({ name: "stamp-docker", gate: { policies: [gate.public] } }).adopt({ create });
93
93
 
94
+ const memoryDrivers = {
95
+ store: {
96
+ sql: { dev: "memory", prod: "memory" },
97
+ kv: { dev: "memory", prod: "memory" },
98
+ },
99
+ channel: { email: { dev: "console", prod: "console" } },
100
+ } as const;
94
101
  await expect(
95
- app.boot({ env: "docker", stores: [db], unguardedHttp: "allow", startScheduler: false }),
102
+ app.boot({
103
+ env: "dev",
104
+ docker: true,
105
+ stores: [db],
106
+ unguardedHttp: "allow",
107
+ startScheduler: false,
108
+ config: { drivers: memoryDrivers },
109
+ }),
96
110
  ).rejects.toThrow(/OKE1008/);
97
111
  });
98
112
 
@@ -104,7 +118,18 @@ describe("boot-level: undeclared-effects flow, no manifest / rootDir", () => {
104
118
  const app = oke({ name: "stamp-prod", gate: { policies: [gate.public] } }).adopt({ create });
105
119
 
106
120
  await expect(
107
- app.boot({ env: "prod", stores: [db], unguardedHttp: "allow", startScheduler: false }),
121
+ app.boot({
122
+ env: "prod",
123
+ stores: [db],
124
+ unguardedHttp: "allow",
125
+ startScheduler: false,
126
+ config: {
127
+ drivers: {
128
+ store: { sql: { prod: "memory" }, kv: { prod: "memory" } },
129
+ channel: { email: { prod: "console" } },
130
+ },
131
+ },
132
+ }),
108
133
  ).rejects.toThrow(/OKE1008/);
109
134
  });
110
135
  });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Module-evaluation registries for `store.sql` / `store.files`, `vault.secret`,
3
- * `signal()`, and `channel.<medium>().template()` — the plain arrays behind
4
- * each declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
5
- * trigger-drain `bindings` array).
3
+ * `vault.env.required`, `signal()`, and `channel.<medium>().template()` — the
4
+ * plain arrays behind each declare module's `listX()` / `resetX()` pair
5
+ * (mirrors `on.ts`'s trigger-drain `bindings` array).
6
6
  *
7
7
  * Lives here, not in the declare modules themselves, so {@link oke} can read
8
8
  * every registry with one lightweight import instead of statically pulling
@@ -21,6 +21,8 @@ import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
21
21
  export const storeRegistry: StoreDecl[] = [];
22
22
  /** `vault.secret` declarations since the last reset. */
23
23
  export const secretRegistry: VaultSecretDecl[] = [];
24
+ /** `vault.env.required(name)` environment variables since the last reset. */
25
+ export const requiredEnvRegistry: string[] = [];
24
26
  /** `signal()` declarations since the last reset. */
25
27
  export const signalRegistry: SignalDecl[] = [];
26
28
  /** Medium-binder `.template()` declarations since the last reset. */
@@ -71,8 +71,10 @@ export interface FlowOptions<I = unknown, O = unknown, E extends FlowErrorMap =
71
71
  readonly breaking?: boolean;
72
72
  /**
73
73
  * Compensation phase after terminal failure on a durable flow.
74
- * Runs under the same journal session before `commit("failed")`.
75
- * Bodies must use distinct `fx.step("undo:…")` names never reuse
74
+ * Runs under the same journal session after auto per-step `{ undo }`
75
+ * handlers and before `commit("failed")`. Prefer `fx.step(name, fn, { undo })`
76
+ * for step-local reverse work; use this hook for cross-cutting cleanup.
77
+ * Manual bodies must use distinct `fx.step("undo:…")` names — never reuse
76
78
  * forward step names. Never called on retryable attempts or sleep park.
77
79
  */
78
80
  readonly compensate?: (
@@ -100,7 +100,7 @@ describe("fx — effect ledger", () => {
100
100
  await fx.emit("order-placed", {});
101
101
  await fx.send("order-confirmed", { to: "u1" });
102
102
  await fx.ask("triage@1", {});
103
- expect(fx.vault("STRIPE_KEY").reveal()).toBe("sk_test");
103
+ expect((await fx.vault.get("STRIPE_KEY")).reveal()).toBe("sk_test");
104
104
  await fx.call("payments.charge", {});
105
105
 
106
106
  expect(ledger.entries).toHaveLength(7);
@@ -112,6 +112,105 @@ describe("fx — effect ledger", () => {
112
112
  });
113
113
  });
114
114
 
115
+ describe("fx.vault — object surface", () => {
116
+ /** Minimal in-memory adapter covering the surface `fx.vault` uses. */
117
+ function memoryAdapter() {
118
+ const rows = new Map<string, { value: string; version: number }>();
119
+ const actors: string[] = [];
120
+ return {
121
+ rows,
122
+ actors,
123
+ adapter: {
124
+ id: "memory",
125
+ async get(path: string) {
126
+ const row = rows.get(path);
127
+ return row === undefined ? undefined : { path, ...row };
128
+ },
129
+ async set(path: string, value: string, opts?: { actor?: { id?: string } }) {
130
+ if (opts?.actor?.id) actors.push(opts.actor.id);
131
+ const version = (rows.get(path)?.version ?? 0) + 1;
132
+ rows.set(path, { value, version });
133
+ return { path, value, version };
134
+ },
135
+ async rotate(path: string, value: string) {
136
+ const version = (rows.get(path)?.version ?? 0) + 1;
137
+ rows.set(path, { value, version });
138
+ return { path, value, version };
139
+ },
140
+ async delete(path: string) {
141
+ return rows.delete(path);
142
+ },
143
+ async list(opts?: { prefix?: string }) {
144
+ return [...rows.keys()]
145
+ .filter((p) => opts?.prefix === undefined || p.startsWith(opts.prefix))
146
+ .map((path) => ({ path, version: rows.get(path)!.version }));
147
+ },
148
+ async status() {
149
+ return { sealed: false, initialized: true, secretCount: rows.size };
150
+ },
151
+ },
152
+ };
153
+ }
154
+
155
+ test("mutations run through the adapter, ledger `secret`, and carry the flow actor", async () => {
156
+ const { adapter, actors, rows } = memoryAdapter();
157
+ const ledger = createEffectLedger();
158
+ const fx = createFx({
159
+ flow: "ops.rotateKey",
160
+ effects: { secrets: ["prod/api/stripe"] },
161
+ ledger,
162
+ // The adapter's structural shape is all `fx.vault` consumes.
163
+ vaultAdapter: adapter as never,
164
+ });
165
+
166
+ expect(await fx.vault.set("prod/api/stripe", "sk_1")).toEqual({
167
+ path: "prod/api/stripe",
168
+ version: 1,
169
+ });
170
+ expect(await fx.vault.rotate("prod/api/stripe", "sk_2")).toEqual({
171
+ path: "prod/api/stripe",
172
+ version: 2,
173
+ });
174
+ expect(await fx.vault.list("prod/")).toEqual(["prod/api/stripe"]);
175
+ expect(await fx.vault.status()).toEqual({
176
+ sealed: false,
177
+ initialized: true,
178
+ backend: "memory",
179
+ });
180
+ expect(await fx.vault.delete("prod/api/stripe")).toBe(true);
181
+
182
+ expect(rows.size).toBe(0);
183
+ expect(actors).toEqual(["ops.rotateKey"]);
184
+ expect(ledger.entries.map((e) => e.kind)).toEqual(["secret", "secret", "secret"]);
185
+ });
186
+
187
+ test("an undeclared path is refused before the adapter is touched", async () => {
188
+ const { adapter, rows } = memoryAdapter();
189
+ const fx = createFx({
190
+ flow: "ops.rotateKey",
191
+ effects: { secrets: ["prod/api/stripe"] },
192
+ vaultAdapter: adapter as never,
193
+ });
194
+
195
+ await expect(fx.vault.set("prod/api/other", "sk_1")).rejects.toThrow(/OKE1006/);
196
+ expect(rows.size).toBe(0);
197
+ });
198
+
199
+ test("mutations without a bound adapter name the missing backend", async () => {
200
+ const fx = createFx({
201
+ flow: "ops.rotateKey",
202
+ effects: { secrets: ["prod/api/stripe"] },
203
+ });
204
+
205
+ await expect(fx.vault.set("prod/api/stripe", "sk_1")).rejects.toThrow(
206
+ /fx\.vault\.set needs a bound Vault backend/,
207
+ );
208
+ await expect(fx.vault.status()).rejects.toThrow(
209
+ /fx\.vault\.status needs a bound Vault backend/,
210
+ );
211
+ });
212
+ });
213
+
115
214
  describe("fx — wholesale swap", () => {
116
215
  test("fx is swappable wholesale (plain object interface)", async () => {
117
216
  const calls: string[] = [];
@@ -144,8 +243,25 @@ describe("fx — wholesale swap", () => {
144
243
  now: () => 0,
145
244
  sleep: async () => undefined,
146
245
  },
147
- vault() {
148
- return new Redacted("");
246
+ vault: {
247
+ async get() {
248
+ return new Redacted("");
249
+ },
250
+ async set() {
251
+ throw new Error("vault.set must not be used");
252
+ },
253
+ async rotate() {
254
+ throw new Error("vault.rotate must not be used");
255
+ },
256
+ async delete() {
257
+ throw new Error("vault.delete must not be used");
258
+ },
259
+ async list() {
260
+ return [];
261
+ },
262
+ async status() {
263
+ return { sealed: false, initialized: true, backend: "memory" };
264
+ },
149
265
  },
150
266
  cache: {
151
267
  get: async () => undefined,
package/src/kernel/fx.ts CHANGED
@@ -28,7 +28,7 @@ import type {
28
28
  import type { SqlRow } from "../drivers/types.ts";
29
29
  import type { SignalRuntime } from "../elements/signal.ts";
30
30
  import type { SignalEmitOptions } from "../drivers/signal-types.ts";
31
- import type { VaultRuntime } from "../elements/vault.ts";
31
+ import type { VaultActor, VaultAdapter, VaultRuntime } from "../elements/vault.ts";
32
32
  import type { ChannelRuntime } from "../elements/channel.ts";
33
33
  import type { AiRuntime } from "../elements/ai.ts";
34
34
  import { parseDurationMs } from "../elements/clock/duration.ts";
@@ -51,19 +51,22 @@ import {
51
51
  type FxThunk,
52
52
  } from "./concurrency.ts";
53
53
  import { maskRedactedDeep, Redacted } from "./redacted.ts";
54
- import type { JournalSession } from "./journal.ts";
54
+ import type { JournalSession, JournalStepOptions } from "./journal.ts";
55
+
56
+ /** Options for {@link Fx.step} — see {@link JournalStepOptions}. */
57
+ export type StepOptions<T> = JournalStepOptions<T>;
55
58
  import type { RunTelemetry } from "./run-telemetry.ts";
56
59
  import type { RunsRuntime } from "../runs/runtime.ts";
57
60
  import type { RunsRow, WideEvent } from "../runs/types.ts";
58
- import {
59
- evaluateSloBreaches,
60
- windowStatsForFlow,
61
- type RunWindowStats,
62
- type SloBreach,
63
- } from "../runs/window.ts";
61
+ import type { RunWindowStats, SloBreach } from "../runs/window.ts";
64
62
  import { translate, type MessageCatalogs } from "../i18n/messages.ts";
65
63
  import type { AppMessageKey, MessageValues } from "../i18n/types.ts";
66
64
 
65
+ /** Lazy runs/window helpers — kept off the cold `oke` static graph. */
66
+ async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
67
+ return import("../runs/window.ts");
68
+ }
69
+
67
70
  /** Resource ref Flows declare to read the Runs store via {@link Fx.runs}. */
68
71
  export const RUNS_RESOURCE = "runs";
69
72
 
@@ -339,6 +342,83 @@ export interface FxJson {
339
342
  with<T>(data: T, meta: Record<string, unknown>): JsonResult<T>;
340
343
  }
341
344
 
345
+ /** Options for {@link FxVault.set}. */
346
+ export interface FxVaultSetOptions {
347
+ /** Relative expiry from now, in milliseconds. */
348
+ readonly ttlMs?: number;
349
+ /** Non-sensitive metadata stored beside the ciphertext. */
350
+ readonly metadata?: Record<string, unknown>;
351
+ }
352
+
353
+ /** Identity of a written secret version. */
354
+ export interface FxVaultWriteResult {
355
+ /** Canonical path that was written. */
356
+ readonly path: string;
357
+ /** Monotonic version of the new value. */
358
+ readonly version: number;
359
+ }
360
+
361
+ /** Operational state of the bound Vault backend. */
362
+ export interface FxVaultStatus {
363
+ /** Whether the master key is currently unavailable. */
364
+ readonly sealed: boolean;
365
+ /** Whether the backend has been initialized. */
366
+ readonly initialized: boolean;
367
+ /** Backend id (`sql`, `memory`, `vault`, …). */
368
+ readonly backend: string;
369
+ }
370
+
371
+ /**
372
+ * Vault surface on `fx`.
373
+ *
374
+ * `get` reads through the boot-time resolution chain and is the only method
375
+ * every app has: the mutation and introspection methods need a bound
376
+ * {@link CreateFxOptions.vaultAdapter} (the encrypted-at-rest backend) and
377
+ * throw without one.
378
+ */
379
+ export interface FxVault {
380
+ /**
381
+ * Read a vault secret (records `secret`).
382
+ *
383
+ * Returns a {@link Redacted} — printing / logging / serializing it yields a
384
+ * placeholder, never the value. Call `.reveal()` at the one boundary that
385
+ * needs the real value (e.g. passing a credential to a driver).
386
+ *
387
+ * @param secret - Secret name or handle
388
+ */
389
+ get(secret: NamedRef): Promise<Redacted<string>>;
390
+ /**
391
+ * Write a new version of a path (records `secret`).
392
+ *
393
+ * @param path - Secret path or handle
394
+ * @param value - Cleartext value
395
+ * @param options - TTL / metadata
396
+ */
397
+ set(path: NamedRef, value: string, options?: FxVaultSetOptions): Promise<FxVaultWriteResult>;
398
+ /**
399
+ * Write a new version with a fresh data key, retiring the previous one
400
+ * (records `secret`).
401
+ *
402
+ * @param path - Secret path or handle
403
+ * @param value - New cleartext value
404
+ */
405
+ rotate(path: NamedRef, value: string): Promise<FxVaultWriteResult>;
406
+ /**
407
+ * Crypto-shred a path (records `secret`). Returns whether anything went.
408
+ *
409
+ * @param path - Secret path or handle
410
+ */
411
+ delete(path: NamedRef): Promise<boolean>;
412
+ /**
413
+ * Enumerate secret paths (never values).
414
+ *
415
+ * @param prefix - Canonical path prefix filter
416
+ */
417
+ list(prefix?: string): Promise<readonly string[]>;
418
+ /** Seal / initialization state of the backend. */
419
+ status(): Promise<FxVaultStatus>;
420
+ }
421
+
342
422
  /**
343
423
  * Flow-facing read door to the Runs wide-event store.
344
424
  *
@@ -420,16 +500,8 @@ export interface Fx {
420
500
  readonly runs: FxRuns;
421
501
  /** Clock surface. */
422
502
  readonly clock: FxClock;
423
- /**
424
- * Read a vault secret (records `secret`).
425
- *
426
- * Returns a {@link Redacted} — printing / logging / serializing it yields a
427
- * placeholder, never the value. Call `.reveal()` at the one boundary that
428
- * needs the real value (e.g. passing a credential to a driver).
429
- *
430
- * @param secret - Secret name or handle
431
- */
432
- vault(secret: NamedRef): Redacted<string>;
503
+ /** Vault surface — `fx.vault.get(secret)` and the adapter-backed mutations. */
504
+ readonly vault: FxVault;
433
505
  /** Cache surface. */
434
506
  readonly cache: FxCache;
435
507
  /**
@@ -544,8 +616,9 @@ export interface Fx {
544
616
  *
545
617
  * @param name - Step name
546
618
  * @param fn - Step body
619
+ * @param opts - Optional `{ undo }` — durable compensation on terminal failure
547
620
  */
548
- step<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
621
+ step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T>;
549
622
  /**
550
623
  * Ambient abort signal for the current structured-concurrency branch.
551
624
  * Outside `all` / `race`, a never-aborted signal.
@@ -619,7 +692,7 @@ export interface CreateFxOptions {
619
692
  readonly principal?: FxPrincipal;
620
693
  /** Tenant. */
621
694
  readonly tenant?: FxTenant;
622
- /** Secret name → value map for `fx.vault`. */
695
+ /** Secret name → value map for `fx.vault.get`. */
623
696
  readonly secrets?: Readonly<Record<string, string>>;
624
697
  /** Seed data for in-memory stores: ref → key → value. */
625
698
  readonly storeData?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
@@ -651,10 +724,16 @@ export interface CreateFxOptions {
651
724
  */
652
725
  readonly runsRuntime?: RunsRuntime;
653
726
  /**
654
- * Optional vault runtime. When set, `fx.vault` reads through it and
727
+ * Optional vault runtime. When set, `fx.vault.get` reads through it and
655
728
  * `fx.log` redacts loaded secret values automatically.
656
729
  */
657
730
  readonly vaultRuntime?: VaultRuntime;
731
+ /**
732
+ * Optional encrypted-at-rest Vault backend. Required by the `fx.vault`
733
+ * mutation / introspection methods (`set`, `rotate`, `delete`, `list`,
734
+ * `status`); reads never need it.
735
+ */
736
+ readonly vaultAdapter?: VaultAdapter;
658
737
  /**
659
738
  * Optional channel runtime. When set, `fx.send` delivers through it
660
739
  * (templates, consent, fallback chains, receipts).
@@ -1229,6 +1308,116 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1229
1308
  },
1230
1309
  };
1231
1310
 
1311
+ const vaultActor: VaultActor = {
1312
+ type: "flow",
1313
+ id: options.flow,
1314
+ ...(options.runId !== undefined ? { requestId: options.runId } : {}),
1315
+ };
1316
+
1317
+ /**
1318
+ * Capability-check a vault path and ledger the `secret` effect around it.
1319
+ *
1320
+ * Kept off {@link gated} deliberately: secret access is never journaled,
1321
+ * so a durable replay re-reads the live value instead of resurrecting a
1322
+ * rotated one from the journal.
1323
+ *
1324
+ * @param name - Secret path
1325
+ * @param body - Work to run under the gate
1326
+ */
1327
+ async function gatedSecret<T>(name: string, body: () => T | Promise<T>): Promise<T> {
1328
+ capability.assert("secret", name);
1329
+ const timestamp = now();
1330
+ try {
1331
+ return await body();
1332
+ } finally {
1333
+ ledger.record({
1334
+ kind: "secret",
1335
+ resource: name,
1336
+ timestamp,
1337
+ duration: Math.max(0, now() - timestamp),
1338
+ reversibility: reversibilityOf("secret"),
1339
+ });
1340
+ }
1341
+ }
1342
+
1343
+ /**
1344
+ * @param op - Method name for the error message
1345
+ */
1346
+ function vaultAdapterFor(op: string): VaultAdapter {
1347
+ const adapter = options.vaultAdapter;
1348
+ if (!adapter) {
1349
+ throw new Error(
1350
+ `fx.vault.${op} needs a bound Vault backend — configure the vault element (drivers.vault = "vault") or pass vaultAdapter to createFx`,
1351
+ );
1352
+ }
1353
+ return adapter;
1354
+ }
1355
+
1356
+ /**
1357
+ * @param op - Method name for the error message
1358
+ * @param path - Secret path
1359
+ */
1360
+ function refuseDryRunVaultWrite(op: string, path: string): void {
1361
+ if (isDryRun()) {
1362
+ throw new DryRunWriteIsolationError(
1363
+ `fx.vault.${op}("${path}") cannot isolate writes during dry-run; dry-run refused rather than risk mutating a live secret.`,
1364
+ );
1365
+ }
1366
+ }
1367
+
1368
+ const vaultSurface: FxVault = {
1369
+ get(secret) {
1370
+ const name = resolveName(secret);
1371
+ return gatedSecret(name, () => {
1372
+ const value = options.vaultRuntime
1373
+ ? options.vaultRuntime.read(name)
1374
+ : (secrets[name] ?? `[secret:${name}]`);
1375
+ return new Redacted(value);
1376
+ });
1377
+ },
1378
+ set(path, value, setOptions) {
1379
+ const name = resolveName(path);
1380
+ return gatedSecret(name, async () => {
1381
+ refuseDryRunVaultWrite("set", name);
1382
+ const written = await vaultAdapterFor("set").set(name, value, {
1383
+ ...(setOptions?.ttlMs !== undefined ? { ttlMs: setOptions.ttlMs } : {}),
1384
+ ...(setOptions?.metadata !== undefined ? { metadata: setOptions.metadata } : {}),
1385
+ actor: vaultActor,
1386
+ });
1387
+ return { path: written.path, version: written.version };
1388
+ });
1389
+ },
1390
+ rotate(path, value) {
1391
+ const name = resolveName(path);
1392
+ return gatedSecret(name, async () => {
1393
+ refuseDryRunVaultWrite("rotate", name);
1394
+ const written = await vaultAdapterFor("rotate").rotate(name, value, {
1395
+ actor: vaultActor,
1396
+ });
1397
+ return { path: written.path, version: written.version };
1398
+ });
1399
+ },
1400
+ delete(path) {
1401
+ const name = resolveName(path);
1402
+ return gatedSecret(name, async () => {
1403
+ refuseDryRunVaultWrite("delete", name);
1404
+ return vaultAdapterFor("delete").delete(name, { actor: vaultActor });
1405
+ });
1406
+ },
1407
+ async list(prefix) {
1408
+ const entries = await vaultAdapterFor("list").list({
1409
+ ...(prefix !== undefined ? { prefix } : {}),
1410
+ actor: vaultActor,
1411
+ });
1412
+ return entries.map((entry) => entry.path);
1413
+ },
1414
+ async status() {
1415
+ const adapter = vaultAdapterFor("status");
1416
+ const state = await adapter.status();
1417
+ return { sealed: state.sealed, initialized: state.initialized, backend: adapter.id };
1418
+ },
1419
+ };
1420
+
1232
1421
  const runsSurface: FxRuns = {
1233
1422
  query(sql) {
1234
1423
  return gated("read", RUNS_RESOURCE, async () => {
@@ -1251,6 +1440,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1251
1440
  if (!options.runsRuntime) {
1252
1441
  throw new Error("fx.runs.window requires a bound runs runtime (oke({ runs }))");
1253
1442
  }
1443
+ const { windowStatsForFlow } = await loadRunsWindow();
1254
1444
  const events = await options.runsRuntime.all();
1255
1445
  return windowStatsForFlow(events, flowName, now(), windowMs);
1256
1446
  });
@@ -1260,6 +1450,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1260
1450
  if (!options.runsRuntime) {
1261
1451
  throw new Error("fx.runs.checkSlo requires a bound runs runtime (oke({ runs }))");
1262
1452
  }
1453
+ const { windowStatsForFlow, evaluateSloBreaches } = await loadRunsWindow();
1263
1454
  const events = await options.runsRuntime.all();
1264
1455
  const stats = windowStatsForFlow(events, flowName, now(), windowMs);
1265
1456
  return evaluateSloBreaches(stats, slo);
@@ -1294,23 +1485,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1294
1485
  });
1295
1486
  },
1296
1487
  clock,
1297
- vault(secret) {
1298
- const name = resolveName(secret);
1299
- // vault is sync in the public examples; check + record synchronously
1300
- capability.assert("secret", name);
1301
- const timestamp = now();
1302
- const value = options.vaultRuntime
1303
- ? options.vaultRuntime.read(name)
1304
- : (secrets[name] ?? `[secret:${name}]`);
1305
- ledger.record({
1306
- kind: "secret",
1307
- resource: name,
1308
- timestamp,
1309
- duration: Math.max(0, now() - timestamp),
1310
- reversibility: reversibilityOf("secret"),
1311
- });
1312
- return new Redacted(value);
1313
- },
1488
+ vault: vaultSurface,
1314
1489
  cache,
1315
1490
  send(template, opts) {
1316
1491
  const name = resolveName(template);
@@ -1538,9 +1713,9 @@ export function createFxContext(options: CreateFxOptions): FxContext {
1538
1713
  return { [jsonResultBrand]: true, status: 200, value: data, meta } as JsonResult<T>;
1539
1714
  },
1540
1715
  },
1541
- async step<T>(name: string, fn: () => T | Promise<T>): Promise<T> {
1716
+ async step<T>(name: string, fn: () => T | Promise<T>, opts?: StepOptions<T>): Promise<T> {
1542
1717
  if (journal) {
1543
- return journal.step(name, fn);
1718
+ return journal.step(name, fn, opts);
1544
1719
  }
1545
1720
  return await fn();
1546
1721
  },
@@ -136,6 +136,7 @@ export {
136
136
  type FxThunk,
137
137
  type JsonResult,
138
138
  type NamedRef,
139
+ type StepOptions,
139
140
  } from "./fx.ts";
140
141
 
141
142
  export {
@@ -177,10 +178,13 @@ export {
177
178
  createFileJournalStore,
178
179
  hasJournalLease,
179
180
  isJournalLeaseBusy,
181
+ isJournalRegistrationComplete,
180
182
  isJournalSuspend,
181
183
  JournalLeaseBusy,
184
+ JournalRegistrationComplete,
182
185
  JournalSuspend,
183
186
  JOURNAL_DEFAULT_LEASE_MS,
187
+ JOURNAL_UNDO_PREFIX,
184
188
  type Journal,
185
189
  type JournalEntry,
186
190
  type JournalLeaseOptions,
@@ -188,10 +192,20 @@ export {
188
192
  type JournalRun,
189
193
  type JournalRunStatus,
190
194
  type JournalSession,
195
+ type JournalStepOptions,
191
196
  type JournalStore,
197
+ type JournalUndoFrame,
192
198
  type CreateJournalOptions,
193
199
  } from "./journal.ts";
194
200
 
201
+ export {
202
+ failureCodeOf,
203
+ forwardCompletedSteps,
204
+ rebindUndosFromDo,
205
+ runCompensationPhase,
206
+ type RunCompensationPhaseOptions,
207
+ } from "./compensate.ts";
208
+
195
209
  export {
196
210
  AFTER_HANDLER_STAGES,
197
211
  BEFORE_HANDLER_STAGES,