okengine 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) 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/config.ts +13 -3
  204. package/src/elements/gate/constants.ts +18 -0
  205. package/src/elements/gate/declare.ts +1 -1
  206. package/src/elements/gate/strategies.ts +2 -12
  207. package/src/elements/gate.test.ts +1 -1
  208. package/src/elements/gate.ts +2 -2
  209. package/src/elements/store/domain-ddl.test.ts +5 -12
  210. package/src/elements/store/emit-drizzle.ts +44 -19
  211. package/src/elements/store/index-boot.test.ts +15 -116
  212. package/src/elements/store/load-plugin-tables.ts +1 -1
  213. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  214. package/src/elements/store/resource-list-docs.test.ts +64 -44
  215. package/src/elements/store/resource.test.ts +17 -10
  216. package/src/elements/store/runtime.ts +9 -12
  217. package/src/elements/store/schema-decl.test.ts +16 -32
  218. package/src/elements/store/seed.test.ts +1 -2
  219. package/src/elements/store/seed.ts +7 -8
  220. package/src/elements/store/sql-condition.test.ts +2 -2
  221. package/src/elements/store/sql-session.test.ts +8 -10
  222. package/src/elements/store/sql-session.ts +10 -2
  223. package/src/elements/store.test.ts +13 -13
  224. package/src/elements/vault/audit.ts +233 -0
  225. package/src/elements/vault/boot-chain.test.ts +78 -0
  226. package/src/elements/vault/boot-chain.ts +43 -39
  227. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  228. package/src/elements/vault/builtin-adapter.ts +1184 -0
  229. package/src/elements/vault/crypto.test.ts +237 -0
  230. package/src/elements/vault/crypto.ts +431 -0
  231. package/src/elements/vault/declare.ts +164 -3
  232. package/src/elements/vault/errors.ts +56 -0
  233. package/src/elements/vault/kms-unseal.test.ts +76 -0
  234. package/src/elements/vault/kms-unseal.ts +181 -0
  235. package/src/elements/vault/path.ts +90 -0
  236. package/src/elements/vault/resilience.test.ts +102 -0
  237. package/src/elements/vault/resilience.ts +107 -0
  238. package/src/elements/vault/runtime.ts +36 -4
  239. package/src/elements/vault/security-checklist.test.ts +130 -0
  240. package/src/elements/vault/storage.ts +448 -0
  241. package/src/elements/vault/types.ts +288 -0
  242. package/src/elements/vault/unseal.ts +150 -0
  243. package/src/elements/vault.test.ts +72 -4
  244. package/src/elements/vault.ts +91 -2
  245. package/src/full.ts +248 -0
  246. package/src/http.ts +41 -0
  247. package/src/i18n/interpolate-simple.ts +37 -0
  248. package/src/i18n/messages.ts +51 -4
  249. package/src/i18n-entry.ts +7 -0
  250. package/src/index.ts +35 -145
  251. package/src/journal-entry.ts +20 -0
  252. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  253. package/src/kernel/app-auth.ts +98 -0
  254. package/src/kernel/app.ts +77 -110
  255. package/src/kernel/auto-registry.test.ts +2 -2
  256. package/src/kernel/boot-bind/ai.test.ts +5 -5
  257. package/src/kernel/boot-bind/ai.ts +2 -1
  258. package/src/kernel/boot-bind/channel.test.ts +6 -6
  259. package/src/kernel/boot-bind/channel.ts +3 -10
  260. package/src/kernel/boot-bind/clock.ts +2 -2
  261. package/src/kernel/boot-bind/gate.ts +5 -9
  262. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  263. package/src/kernel/boot-bind/journal.ts +2 -2
  264. package/src/kernel/boot-bind/store.test.ts +43 -44
  265. package/src/kernel/boot-bind/store.ts +16 -41
  266. package/src/kernel/boot-bind/vault.ts +16 -1
  267. package/src/kernel/boot.test.ts +13 -19
  268. package/src/kernel/boot.ts +31 -19
  269. package/src/kernel/budget-entry.ts +1 -1
  270. package/src/kernel/compensate.test.ts +288 -1
  271. package/src/kernel/compensate.ts +126 -0
  272. package/src/kernel/concurrency.test.ts +10 -9
  273. package/src/kernel/effects-stamping.test.ts +27 -2
  274. package/src/kernel/element-registries.ts +5 -3
  275. package/src/kernel/flow.ts +4 -2
  276. package/src/kernel/fx.test.ts +119 -3
  277. package/src/kernel/fx.ts +215 -40
  278. package/src/kernel/index.ts +14 -0
  279. package/src/kernel/journal.ts +130 -7
  280. package/src/kernel/pipeline.test.ts +20 -6
  281. package/src/kernel/redacted.test.ts +35 -0
  282. package/src/kernel/redacted.ts +24 -2
  283. package/src/kernel/router/create-default.ts +16 -0
  284. package/src/kernel/router/create-edge.ts +18 -0
  285. package/src/kernel/router/create.ts +23 -0
  286. package/src/kernel/router/index.ts +23 -0
  287. package/src/kernel/router/linear.ts +88 -0
  288. package/src/kernel/router/regexp.ts +188 -0
  289. package/src/kernel/router/smart.ts +120 -0
  290. package/src/kernel/router/trie.ts +108 -0
  291. package/src/kernel/router/types.ts +56 -0
  292. package/src/kernel/router.ts +18 -532
  293. package/src/kernel-entry.ts +43 -0
  294. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  295. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  296. package/src/plugins/config-source.test.ts +7 -7
  297. package/src/release/build-lib.ts +61 -0
  298. package/src/release/exports.test.ts +4 -2
  299. package/src/release/exports.ts +30 -4
  300. package/src/release/measure.ts +101 -22
  301. package/src/runs/duckdb.ts +7 -2
  302. package/src/shared/lazy-src.ts +79 -0
  303. package/src/shared/optional-peer.ts +41 -0
  304. package/src/term.test.ts +3 -44
  305. package/src/test/create-test-app.ts +12 -0
  306. package/src/test/provisions.integration.test.ts +1 -1
  307. package/src/test/reset-element-registries.ts +2 -0
  308. package/src/upgrade/codemods.test.ts +7 -20
  309. package/src/upgrade/codemods.ts +93 -60
  310. package/site/content/docs/recipes/openbao.mdx +0 -141
  311. package/src/cli/ask-dev-mode.ts +0 -40
  312. package/src/cli/dev-mode.test.ts +0 -45
  313. package/src/cli/dev-mode.ts +0 -78
  314. package/src/cli/openbao-bootstrap.test.ts +0 -150
  315. package/src/cli/openbao-bootstrap.ts +0 -321
  316. package/src/cli/openbao-restart.integration.test.ts +0 -145
  317. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  318. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  319. package/src/docker/recipes/openbao.ts +0 -47
  320. package/src/drivers/libsql.ts +0 -179
  321. package/src/drivers/sqlite.ts +0 -44
  322. package/src/drivers/vault-openbao.test.ts +0 -97
  323. package/src/drivers/vault-openbao.ts +0 -143
@@ -4,11 +4,13 @@
4
4
  */
5
5
 
6
6
  import { describe, expect, test } from "bun:test";
7
- import { createMemoryJournalStore } from "./journal.ts";
7
+ import { createMemoryJournalStore, createJournal } from "./journal.ts";
8
8
  import type { JournalRuntime } from "./boot-bind/journal.ts";
9
9
  import { flow } from "./flow.ts";
10
10
  import { oke } from "./app.ts";
11
11
  import { createRunsRuntime } from "../runs/runtime.ts";
12
+ import { runDurable } from "../elements/clock/durable.ts";
13
+ import { fail } from "./errors.ts";
12
14
 
13
15
  function memoryJournal(store = createMemoryJournalStore()): JournalRuntime {
14
16
  return {
@@ -125,4 +127,289 @@ describe("durable compensate", () => {
125
127
  expect((await store.list())[0]!.status).toBe("failed");
126
128
  await runs.close();
127
129
  });
130
+
131
+ test("per-step { undo } runs LIFO and skips the failed step", async () => {
132
+ const store = createMemoryJournalStore();
133
+ const undos: string[] = [];
134
+
135
+ const pay = flow("pay.perStep", {
136
+ durable: true,
137
+ do: async (_input: { orderId: string }, fx) => {
138
+ await fx.step("reserve", () => "reserved", {
139
+ undo: () => {
140
+ undos.push("undo:reserve");
141
+ },
142
+ });
143
+ await fx.step(
144
+ "charge",
145
+ () => {
146
+ throw new Error("card_declined");
147
+ },
148
+ {
149
+ undo: () => {
150
+ undos.push("undo:charge");
151
+ },
152
+ },
153
+ );
154
+ return true;
155
+ },
156
+ });
157
+
158
+ const app = oke({
159
+ name: "compensate-per-step",
160
+ env: "test",
161
+ startScheduler: false,
162
+ gate: { unguardedHttp: "allow" },
163
+ elements: { journal: memoryJournal(store) },
164
+ }).adopt(pay);
165
+ await app.boot();
166
+ await app.execute(pay, { orderId: "o1" }, { kind: "internal" });
167
+
168
+ expect(undos).toEqual(["undo:reserve"]);
169
+ const stepNames = (await store.list())[0]!.entries
170
+ .filter((e) => e.kind === "step")
171
+ .map((e) => (e as { name: string }).name);
172
+ expect(stepNames).toEqual(["reserve", "undo:reserve"]);
173
+ expect((await store.list())[0]!.status).toBe("failed");
174
+ });
175
+
176
+ test("per-step undo receives the journaled step value", async () => {
177
+ const store = createMemoryJournalStore();
178
+ let refunded: unknown;
179
+
180
+ const pay = flow("pay.value", {
181
+ durable: true,
182
+ do: async (_input, fx) => {
183
+ await fx.step("charge", () => ({ id: "ch_1" }), {
184
+ undo: (intent) => {
185
+ refunded = intent;
186
+ },
187
+ });
188
+ throw new Error("after_charge");
189
+ },
190
+ });
191
+
192
+ const app = oke({
193
+ name: "compensate-value",
194
+ env: "test",
195
+ startScheduler: false,
196
+ gate: { unguardedHttp: "allow" },
197
+ elements: { journal: memoryJournal(store) },
198
+ }).adopt(pay);
199
+ await app.boot();
200
+ await app.execute(pay, {}, { kind: "internal" });
201
+ expect(refunded).toEqual({ id: "ch_1" });
202
+ });
203
+
204
+ test("auto undos then flow.compensate", async () => {
205
+ const store = createMemoryJournalStore();
206
+ const order: string[] = [];
207
+
208
+ const pay = flow("pay.both", {
209
+ durable: true,
210
+ do: async (_input, fx) => {
211
+ await fx.step("reserve", () => "ok", {
212
+ undo: () => {
213
+ order.push("auto:reserve");
214
+ },
215
+ });
216
+ throw new Error("boom");
217
+ },
218
+ compensate: async (_ctx, fx) => {
219
+ await fx.step("undo:extra", async () => {
220
+ order.push("hook:extra");
221
+ });
222
+ },
223
+ });
224
+
225
+ const app = oke({
226
+ name: "compensate-both",
227
+ env: "test",
228
+ startScheduler: false,
229
+ gate: { unguardedHttp: "allow" },
230
+ elements: { journal: memoryJournal(store) },
231
+ }).adopt(pay);
232
+ await app.boot();
233
+ await app.execute(pay, {}, { kind: "internal" });
234
+ expect(order).toEqual(["auto:reserve", "hook:extra"]);
235
+ });
236
+
237
+ test("fx.fail triggers compensate on execute", async () => {
238
+ const store = createMemoryJournalStore();
239
+ const undos: string[] = [];
240
+
241
+ const pay = flow("pay.fail", {
242
+ durable: true,
243
+ do: async (_input, fx) => {
244
+ await fx.step("reserve", () => "ok", {
245
+ undo: () => {
246
+ undos.push("undo:reserve");
247
+ },
248
+ });
249
+ return fail("Declined", {});
250
+ },
251
+ });
252
+
253
+ const app = oke({
254
+ name: "compensate-fx-fail",
255
+ env: "test",
256
+ startScheduler: false,
257
+ gate: { unguardedHttp: "allow" },
258
+ elements: { journal: memoryJournal(store) },
259
+ }).adopt(pay);
260
+ await app.boot();
261
+ const result = await app.execute(pay, {}, { kind: "internal" });
262
+ expect(result.failure?.error.code).toBe("Declined");
263
+ expect(undos).toEqual(["undo:reserve"]);
264
+ expect((await store.list())[0]!.status).toBe("failed");
265
+ });
266
+
267
+ test("runDurable: fx.fail triggers compensate", async () => {
268
+ const store = createMemoryJournalStore();
269
+ const undos: string[] = [];
270
+ const pay = flow("pay.durableFail", {
271
+ durable: true,
272
+ do: async (_input, fx) => {
273
+ await fx.step("reserve", () => "ok", {
274
+ undo: () => {
275
+ undos.push("undo:reserve");
276
+ },
277
+ });
278
+ return fail("Declined", {});
279
+ },
280
+ });
281
+
282
+ const result = await runDurable({ flow: pay, journalStore: store });
283
+ expect(result.status).toBe("failed");
284
+ expect(undos).toEqual(["undo:reserve"]);
285
+ });
286
+
287
+ test("runDurable refuses resume of failed runs", async () => {
288
+ const store = createMemoryJournalStore();
289
+ let runs = 0;
290
+ const pay = flow("pay.terminal", {
291
+ durable: true,
292
+ do: async () => {
293
+ runs += 1;
294
+ throw new Error("boom");
295
+ },
296
+ });
297
+ const first = await runDurable({ flow: pay, journalStore: store });
298
+ expect(first.status).toBe("failed");
299
+ expect(runs).toBe(1);
300
+ const second = await runDurable({
301
+ flow: pay,
302
+ journalStore: store,
303
+ runId: first.status === "failed" ? first.runId : "",
304
+ });
305
+ expect(second.status).toBe("failed");
306
+ expect(runs).toBe(1); // do must not re-enter
307
+ });
308
+
309
+ test("orphan mid-undo resumes compensating without forward re-entry", async () => {
310
+ const store = createMemoryJournalStore();
311
+ const forward: string[] = [];
312
+ const undos: string[] = [];
313
+ const now = () => 1_000;
314
+
315
+ const pay = flow("pay.midUndo", {
316
+ durable: true,
317
+ do: async (_input, fx) => {
318
+ await fx.step(
319
+ "reserve",
320
+ () => {
321
+ forward.push("reserve");
322
+ return "r";
323
+ },
324
+ {
325
+ undo: () => {
326
+ undos.push("undo:reserve");
327
+ },
328
+ },
329
+ );
330
+ await fx.step(
331
+ "charge",
332
+ () => {
333
+ forward.push("charge");
334
+ return "c";
335
+ },
336
+ {
337
+ undo: () => {
338
+ undos.push("undo:charge");
339
+ },
340
+ },
341
+ );
342
+ return true;
343
+ },
344
+ });
345
+
346
+ // Seed: forwards done, first undo journaled, status compensating (crash mid-phase).
347
+ await store.put({
348
+ id: "mid-undo",
349
+ flow: "pay.midUndo",
350
+ input: {},
351
+ status: "compensating",
352
+ error: "after_steps",
353
+ entries: [
354
+ { kind: "step", name: "reserve", value: "r", at: now() },
355
+ { kind: "step", name: "charge", value: "c", at: now() },
356
+ { kind: "step", name: "undo:charge", value: undefined, at: now() },
357
+ ],
358
+ createdAt: now(),
359
+ updatedAt: now(),
360
+ });
361
+
362
+ const second = await runDurable({
363
+ flow: pay,
364
+ journalStore: store,
365
+ runId: "mid-undo",
366
+ now,
367
+ });
368
+ expect(second.status).toBe("failed");
369
+ expect(forward).toEqual([]); // registration pass only — no new forward work
370
+ expect(undos).toEqual(["undo:reserve"]);
371
+ const names = (await store.get("mid-undo"))!.entries
372
+ .filter((e) => e.kind === "step")
373
+ .map((e) => (e as { name: string }).name);
374
+ expect(names).toEqual(["reserve", "charge", "undo:charge", "undo:reserve"]);
375
+ });
376
+
377
+ test("flow.retry does not undo between attempts", async () => {
378
+ const store = createMemoryJournalStore();
379
+ const undos: string[] = [];
380
+ let attempts = 0;
381
+
382
+ const pay = flow("pay.retryUndo", {
383
+ durable: true,
384
+ retry: { retries: 2, delay: 0, jitter: false },
385
+ do: async (_input, fx) => {
386
+ await fx.step("reserve", () => "ok", {
387
+ undo: () => {
388
+ undos.push("undo:reserve");
389
+ },
390
+ });
391
+ attempts += 1;
392
+ if (attempts < 3) throw new Error("transient");
393
+ return true;
394
+ },
395
+ });
396
+
397
+ const result = await runDurable({ flow: pay, journalStore: store });
398
+ expect(result.status).toBe("completed");
399
+ expect(undos).toEqual([]);
400
+ expect(attempts).toBe(3);
401
+ });
402
+
403
+ test("duplicate forward step name throws", async () => {
404
+ const store = createMemoryJournalStore();
405
+ const journal = createJournal({ store });
406
+ const session = await journal.start("dup");
407
+ await session.step("a", () => 1);
408
+ try {
409
+ await session.step("a", () => 2);
410
+ expect.unreachable();
411
+ } catch (err) {
412
+ expect(String(err)).toMatch(/duplicate step name/);
413
+ }
414
+ });
128
415
  });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Durable compensation phase — auto per-step undos then optional flow.compensate.
3
+ *
4
+ * Runs under the same journal session after terminal failure. Uses status
5
+ * `compensating` so orphan reclaim resumes undos instead of forward `do`.
6
+ */
7
+
8
+ import type { AnyFlowDef } from "./flow.ts";
9
+ import type { Fx } from "./fx.ts";
10
+ import { isFlowFailure } from "./hooks.ts";
11
+ import { JOURNAL_UNDO_PREFIX, type JournalSession, type JournalStepEntry } from "./journal.ts";
12
+
13
+ /**
14
+ * Machine-readable code for journal / Runs failure fields.
15
+ *
16
+ * @param err - Thrown value or {@link FlowFailure}
17
+ */
18
+ export function failureCodeOf(err: unknown): string {
19
+ if (isFlowFailure(err)) return err.error.code;
20
+ if (err instanceof Error && err.name && err.name !== "Error") return err.name;
21
+ if (err instanceof Error && err.message) return err.message.slice(0, 120);
22
+ return "Error";
23
+ }
24
+
25
+ /** Forward step names only (excludes journaled `undo:*` entries). */
26
+ export function forwardCompletedSteps(session: JournalSession): readonly string[] {
27
+ return session.run.entries
28
+ .filter(
29
+ (e): e is JournalStepEntry => e.kind === "step" && !e.name.startsWith(JOURNAL_UNDO_PREFIX),
30
+ )
31
+ .map((e) => e.name);
32
+ }
33
+
34
+ /** Options for {@link runCompensationPhase}. */
35
+ export interface RunCompensationPhaseOptions {
36
+ /** Flow definition (optional `compensate` hook). */
37
+ readonly flow: AnyFlowDef;
38
+ /** Original flow input. */
39
+ readonly input: unknown;
40
+ /** Active journal session. */
41
+ readonly session: JournalSession;
42
+ /** Fx bound to the same journal. */
43
+ readonly fx: Fx;
44
+ /** Terminal failure that triggered compensation. */
45
+ readonly error: unknown;
46
+ }
47
+
48
+ /**
49
+ * Enter compensating status, run LIFO per-step undos, then `flow.compensate`,
50
+ * and commit `failed`.
51
+ *
52
+ * @param options - Session, fx, terminal error
53
+ */
54
+ export async function runCompensationPhase(options: RunCompensationPhaseOptions): Promise<void> {
55
+ const { flow, input, session, fx, error } = options;
56
+ const terminalCode = failureCodeOf(error);
57
+
58
+ if (session.run.status !== "compensating") {
59
+ await session.commit("compensating", { error: terminalCode });
60
+ }
61
+
62
+ session.setUndoExecution(true);
63
+ try {
64
+ const stack = [...session.undoStack()].reverse();
65
+ for (const frame of stack) {
66
+ const undoName = `${JOURNAL_UNDO_PREFIX}${frame.name}`;
67
+ await session.step(undoName, () => frame.undo(frame.value));
68
+ }
69
+
70
+ if (flow.compensate) {
71
+ try {
72
+ await flow.compensate(
73
+ {
74
+ input: input as never,
75
+ error,
76
+ completedSteps: forwardCompletedSteps(session),
77
+ },
78
+ fx,
79
+ );
80
+ } catch (compErr) {
81
+ await session.commit("failed", {
82
+ error: `compensate:${failureCodeOf(compErr)}`,
83
+ });
84
+ return;
85
+ }
86
+ }
87
+
88
+ if (session.run.status === "compensating" || session.run.status === "running") {
89
+ await session.commit("failed", { error: terminalCode });
90
+ }
91
+ } catch (undoErr) {
92
+ if (session.run.status !== "failed") {
93
+ await session.commit("failed", {
94
+ error: `compensate:${failureCodeOf(undoErr)}`,
95
+ });
96
+ }
97
+ } finally {
98
+ session.setUndoExecution(false);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Re-bind per-step undos by re-entering `do` without new forward work.
104
+ *
105
+ * @param flow - Flow definition
106
+ * @param input - Original input
107
+ * @param fx - Fx bound to the journal
108
+ * @param session - Compensating session
109
+ */
110
+ export async function rebindUndosFromDo(
111
+ flow: AnyFlowDef,
112
+ input: unknown,
113
+ fx: Fx,
114
+ session: JournalSession,
115
+ ): Promise<void> {
116
+ session.rewind();
117
+ session.beginRegistrationPass();
118
+ try {
119
+ await flow.do(input as never, fx);
120
+ } catch {
121
+ // JournalRegistrationComplete = caught up with journaled forwards.
122
+ // Other throws while replaying between steps: keep whatever undos bound.
123
+ } finally {
124
+ session.endRegistrationPass();
125
+ }
126
+ }
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { currentAbortSignal, isAbortError, withAbortSignal } from "./abort-scope.ts";
4
- import { createMemoryJournalStore } from "./journal.ts";
4
+ import { createJournal, createMemoryJournalStore } from "./journal.ts";
5
5
  import { createFx, createFxContext } from "./fx.ts";
6
6
  import { flow } from "./flow.ts";
7
7
  import { runDurable } from "../elements/clock/durable.ts";
@@ -216,12 +216,11 @@ describe("retry + durable journal", () => {
216
216
  test("fx.retry inside fx.step does not re-run completed step on resume", async () => {
217
217
  const journalStore = createMemoryJournalStore();
218
218
  let attempts = 0;
219
- let crash = true;
220
219
 
221
220
  const f = flow("pay.retry", {
222
221
  durable: true,
223
222
  do: async (_input, fx) => {
224
- const charged = await fx.step("charge", () =>
223
+ return fx.step("charge", () =>
225
224
  fx.retry(
226
225
  async () => {
227
226
  attempts += 1;
@@ -230,20 +229,22 @@ describe("retry + durable journal", () => {
230
229
  { retries: 2, delay: 0 },
231
230
  ),
232
231
  );
233
- if (crash) throw new Error("KILLED");
234
- return charged;
235
232
  },
236
233
  });
237
234
 
238
- const first = await runDurable({ flow: f, journalStore });
239
- expect(first.status).toBe("failed");
235
+ // Simulate crash after the journaled step: leave status `running`.
236
+ const journal = createJournal({ store: journalStore });
237
+ const crashed = await journal.start("pay.retry");
238
+ await crashed.step("charge", () => {
239
+ attempts += 1;
240
+ return { id: "ch_1" };
241
+ });
240
242
  expect(attempts).toBe(1);
241
243
 
242
- crash = false;
243
244
  const second = await runDurable({
244
245
  flow: f,
245
246
  journalStore,
246
- runId: first.status === "failed" ? first.runId : "",
247
+ runId: crashed.runId,
247
248
  });
248
249
  expect(second.status).toBe("completed");
249
250
  expect(attempts).toBe(1);
@@ -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?: (