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
@@ -130,8 +130,9 @@ describe("boot — lazy element needs", () => {
130
130
  // + `tryListFlowsUnits` in `boot.ts` (`.adopt()` barrel staleness
131
131
  // check, OKE1009) — part of the already-lazily-loaded boot graph, not
132
132
  // the `oke()` construction path, but `Bun.build` still traces and
133
- // sums the dynamic-import chunk (~54.3 kB → 54.7 kB cap).
134
- expect(total).toBeLessThan(54_700);
133
+ // sums the dynamic-import chunk (~54.3 kB → 54.7 kB → 55.0 kB after
134
+ // ConfigEnv local/docker → dev/test/prod + sqlite removal).
135
+ expect(total).toBeLessThan(55_000);
135
136
  } finally {
136
137
  await rm(dir, { recursive: true, force: true });
137
138
  }
@@ -273,11 +274,18 @@ describe("boot — cron autostart via real timer loop", () => {
273
274
  }),
274
275
  );
275
276
 
276
- // env: "local" → startScheduler defaults ON. No createTestApp / advance.
277
+ // Non-test env → startScheduler defaults ON. No createTestApp / advance.
277
278
  const app = oke({
278
279
  name: "cron-autostart",
279
- env: "local",
280
+ env: "dev",
280
281
  schedulerIntervalMs: 1000,
282
+ config: {
283
+ drivers: {
284
+ clock: { dev: "memory" },
285
+ store: { sql: { dev: "memory" }, kv: { dev: "memory" } },
286
+ channel: { email: { dev: "console" } },
287
+ },
288
+ },
281
289
  });
282
290
  await app.boot();
283
291
 
@@ -216,7 +216,9 @@ export interface ElementNeeds {
216
216
  export function resolveElementNeeds(options: BootOptions): ElementNeeds {
217
217
  const pre = options.elements ?? {};
218
218
  let vault =
219
- pre.vault !== undefined || (options.vault?.secrets ?? options.secrets ?? []).length > 0;
219
+ pre.vault !== undefined ||
220
+ (options.vault?.secrets ?? options.secrets ?? []).length > 0 ||
221
+ (options.vault?.requiredEnv?.length ?? 0) > 0;
220
222
  let store = pre.store !== undefined || (options.stores?.length ?? 0) > 0;
221
223
  let signal = pre.signal !== undefined || (options.signals?.length ?? 0) > 0;
222
224
  let clock = pre.clock !== undefined || (options.clocks?.length ?? 0) > 0;
@@ -283,9 +285,10 @@ async function loadBind<T>(name: string): Promise<T> {
283
285
  export async function bootApplication(input: BootOptions = {}): Promise<BootResult> {
284
286
  const docker =
285
287
  input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
286
- // `-d` always selects the `docker` driver profile not a mix of local/test +
287
- // prod store overrides when `$options.env` is unset.
288
- const env: ConfigEnv = input.env ?? (docker ? "docker" : "local");
288
+ // Three targets: `prod` when NODE_ENV=production; `dev` when compose infra
289
+ // is up (`oke dev` / `OKE_DOCKER=1`); otherwise `test` (PGLite / memory).
290
+ const env: ConfigEnv =
291
+ input.env ?? (process.env.NODE_ENV === "production" ? "prod" : docker ? "dev" : "test");
289
292
  let config = input.config;
290
293
  if (config === undefined) {
291
294
  try {
@@ -482,12 +485,13 @@ export async function bootApplication(input: BootOptions = {}): Promise<BootResu
482
485
  const capabilities = await mintCapabilities(options.flows ?? [], env, {
483
486
  manifest: options.manifest,
484
487
  rootDir: options.rootDir,
488
+ docker,
485
489
  });
486
490
 
487
491
  // 9. `.adopt()` barrel freshness — opt-in only (same `rootDir` gate as
488
492
  // capability minting above; never a filesystem read the kernel performs
489
493
  // on its own).
490
- await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir);
494
+ await assertAdoptBarrelFresh(options.flows ?? [], env, options.rootDir, docker);
491
495
 
492
496
  return {
493
497
  vault,
@@ -556,10 +560,9 @@ async function tryAutoExtractManifest(rootDir: string): Promise<Manifest | undef
556
560
  * an explicit {@link BootOptions.manifest}, or a lazy AoT extract from
557
561
  * {@link BootOptions.rootDir} / `OKE_ROOT_DIR`.
558
562
  *
559
- * When neither is available: `local` / `test` stay open (today's dev-loop
560
- * behavior, unbroken) with a once-per-process `oke boot:` warning; `docker`
561
- * / `prod` fail loud (`OKE1008`) — docker mirrors prod's posture, never a
562
- * silent open door in a deploy-shaped environment.
563
+ * When neither is available: `test` stays open with a once-per-process
564
+ * `oke boot:` warning; `dev` with compose infra and `prod` fail loud
565
+ * (`OKE1008`) — never a silent open door in a deploy-shaped environment.
563
566
  *
564
567
  * @param flows - Adopted flows
565
568
  * @param env - Resolved {@link ConfigEnv}
@@ -567,8 +570,12 @@ async function tryAutoExtractManifest(rootDir: string): Promise<Manifest | undef
567
570
  */
568
571
  export async function mintCapabilities(
569
572
  flows: readonly AnyFlowDef[],
570
- env: ConfigEnv = "local",
571
- options: { readonly manifest?: Manifest; readonly rootDir?: string } = {},
573
+ env: ConfigEnv = "test",
574
+ options: {
575
+ readonly manifest?: Manifest;
576
+ readonly rootDir?: string;
577
+ readonly docker?: boolean;
578
+ } = {},
572
579
  ): Promise<Map<string, CapabilityToken>> {
573
580
  const map = new Map<string, CapabilityToken>();
574
581
 
@@ -591,7 +598,8 @@ export async function mintCapabilities(
591
598
  continue;
592
599
  }
593
600
 
594
- if (env === "docker" || env === "prod") {
601
+ const strict = env === "prod" || (env === "dev" && options.docker === true);
602
+ if (strict) {
595
603
  throwOke("NO_EFFECTS_DECLARED", { flow: f.name });
596
604
  }
597
605
  if (!noEffectsWarned) {
@@ -600,7 +608,7 @@ export async function mintCapabilities(
600
608
  `oke boot: flow "${f.name}" (and possibly others) has no declared effects and no ` +
601
609
  "Manifest-derived effects — running with an OPEN capability token (every access " +
602
610
  "allowed, ledgered but not gated). Run `oke build`, or boot with `manifest` / " +
603
- "`rootDir`, before deploying — docker/prod refuse to boot this way.",
611
+ "`rootDir`, before deploying — dev+compose/prod refuse to boot this way.",
604
612
  );
605
613
  }
606
614
  map.set(f.name, createCapabilityToken(f.name, undefined));
@@ -645,18 +653,21 @@ async function tryListFlowsUnits(rootDir: string): Promise<readonly string[] | u
645
653
  *
646
654
  * Opt-in only via {@link BootOptions.rootDir} / `OKE_ROOT_DIR` (same gate as
647
655
  * {@link mintCapabilities}'s Manifest fallback) — never a filesystem read
648
- * the kernel performs on its own. `local` / `test` warn once per process;
649
- * `docker` / `prod` fail loud (`OKE1009`) — same class as `NO_EFFECTS_DECLARED`,
650
- * never a silently-incomplete route table in a deploy-shaped environment.
656
+ * the kernel performs on its own. `test` warns once per process; `prod`
657
+ * (and `dev` with compose) fail loud (`OKE1009`) — same class as
658
+ * `NO_EFFECTS_DECLARED`, never a silently-incomplete route table in a
659
+ * deploy-shaped environment.
651
660
  *
652
661
  * @param flows - Adopted flows
653
662
  * @param env - Resolved {@link ConfigEnv}
654
663
  * @param rootDir - Explicit project root (falls back to `OKE_ROOT_DIR`)
664
+ * @param docker - Compose infra active (`oke dev`)
655
665
  */
656
666
  export async function assertAdoptBarrelFresh(
657
667
  flows: readonly AnyFlowDef[],
658
- env: ConfigEnv = "local",
668
+ env: ConfigEnv = "test",
659
669
  rootDir?: string,
670
+ docker = false,
660
671
  ): Promise<void> {
661
672
  const dir = rootDir ?? process.env["OKE_ROOT_DIR"];
662
673
  if (!dir) return;
@@ -670,7 +681,8 @@ export async function assertAdoptBarrelFresh(
670
681
  const missing = diskUnits.filter((u) => !adoptedUnits.has(u));
671
682
  if (missing.length === 0) return;
672
683
 
673
- if (env === "docker" || env === "prod") {
684
+ const strict = env === "prod" || (env === "dev" && docker);
685
+ if (strict) {
674
686
  throwOke("ADOPT_BARREL_STALE", { unit: missing[0]! });
675
687
  }
676
688
  if (!staleAdoptBarrelWarned) {
@@ -678,7 +690,7 @@ export async function assertAdoptBarrelFresh(
678
690
  emitBootWarn(
679
691
  `oke boot: src/flows/${missing[0]} exists on disk but adopted no flows — the ` +
680
692
  '.adopt() barrel ("src/flows/generated.ts") is stale. Run `oke dev` or `oke build` ' +
681
- "to regenerate it — docker/prod refuse to boot this way.",
693
+ "to regenerate it — dev+compose/prod refuse to boot this way.",
682
694
  );
683
695
  }
684
696
  }
@@ -14,7 +14,7 @@ import { flow } from "./flow.ts";
14
14
  import { createFx } from "./fx.ts";
15
15
  import { mergeHooks, runPipeline } from "./hooks.ts";
16
16
  import { on, resetBindings } from "./on.ts";
17
- import { createRouter } from "./router.ts";
17
+ import { createEdgeRouter as createRouter } from "./router/create-edge.ts";
18
18
  import { http } from "./triggers.ts";
19
19
 
20
20
  /**
@@ -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);