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
@@ -2,17 +2,17 @@
2
2
  * Shared Vault resolution-chain assembly for app boot and Console.
3
3
  *
4
4
  * Order (first hit wins): process.env → .env.local → compose .env.docker →
5
- * backend layer (`env` seed / openbao / memory / managed).
5
+ * backend layer (`env` seed / vault / memory / managed).
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { resolve } from "node:path";
10
10
  import type { ConfigEnv } from "../../config/index.ts";
11
11
  import {
12
+ builtinVaultDriver,
12
13
  envVaultDriver,
13
14
  managedVaultDriver,
14
15
  memoryVaultDriver,
15
- openbaoVaultDriver,
16
16
  } from "../../drivers/index.ts";
17
17
  import { parseDotenv } from "../../drivers/vault-dotenv-parse.ts";
18
18
  import type { VaultDriverId } from "../../drivers/vault-types.ts";
@@ -25,32 +25,30 @@ export interface BuildVaultBootChainOptions {
25
25
  readonly cwd?: string;
26
26
  /** Resolved `drivers.vault` id for the active env. */
27
27
  readonly driverId: VaultDriverId;
28
- /** Active config env — controls fail-loud vs soft fallback for openbao. */
28
+ /** Active config env. */
29
29
  readonly env?: ConfigEnv;
30
30
  /** Seed secrets for the terminal memory / managed layer. */
31
31
  readonly seed?: Readonly<Record<string, string>>;
32
- /**
33
- * When `openbao` is selected but credentials are missing:
34
- * - `"throw"` — fail loud (default for `prod` / `docker`)
35
- * - `"memory"` — soft-fallback to a seeded memory bag (Console / local)
36
- */
37
- readonly missingOpenbao?: "throw" | "memory";
38
32
  }
39
33
 
40
34
  /**
41
35
  * Normalize a config label to a {@link VaultDriverId}.
42
36
  *
43
- * Accepts legacy `"dotenv"` as an alias for `"env"`.
37
+ * Accepts legacy `"dotenv"` as an alias for `"env"`, and `"builtin"` as an
38
+ * alias for the built-in `"vault"` store.
44
39
  *
45
40
  * @param raw - Config string
46
41
  */
47
42
  export function normalizeVaultDriverId(raw: string): VaultDriverId {
48
43
  if (raw === "dotenv") return "env";
49
- if (raw === "env" || raw === "openbao" || raw === "memory" || raw === "managed") {
44
+ if (raw === "builtin") return "vault";
45
+ if (raw === "env" || raw === "vault" || raw === "memory" || raw === "managed") {
50
46
  return raw;
51
47
  }
52
48
  throw new Error(
53
- `oke boot: unknown vault driver "${raw}" (expected env · openbao · memory · managed)`,
49
+ `oke boot: unknown vault driver "${raw}" (expected env · vault · memory · managed). ` +
50
+ 'Official backends: built-in vault (drivers.vault: "vault"), ENV, or managed with ' +
51
+ 'provider "aws-secrets-manager".',
54
52
  );
55
53
  }
56
54
 
@@ -69,9 +67,11 @@ export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultC
69
67
  composeText = readFileSync(composeEnv.path, "utf8").toString();
70
68
  }
71
69
  const envMap = parseDotenv(composeText);
72
- const openbaoUrl = process.env.OKE_VAULT_URL ?? envMap.get("OKE_VAULT_URL");
73
- const openbaoToken = process.env.OKE_VAULT_TOKEN ?? envMap.get("OKE_VAULT_TOKEN");
74
- const openbaoMount = process.env.OKE_VAULT_MOUNT ?? envMap.get("OKE_VAULT_MOUNT") ?? "secret";
70
+ const vaultUrl = process.env.OKE_VAULT_URL ?? envMap.get("OKE_VAULT_URL");
71
+ const vaultToken = process.env.OKE_VAULT_TOKEN ?? envMap.get("OKE_VAULT_TOKEN");
72
+ const vaultMount = process.env.OKE_VAULT_MOUNT ?? envMap.get("OKE_VAULT_MOUNT");
73
+ const vaultProvider = process.env.OKE_VAULT_PROVIDER ?? envMap.get("OKE_VAULT_PROVIDER");
74
+ const vaultRegion = process.env.OKE_VAULT_REGION ?? envMap.get("OKE_VAULT_REGION");
75
75
 
76
76
  const envLayers: VaultChainLayer[] = [
77
77
  { driver: envVaultDriver, source: "process.env" },
@@ -96,14 +96,36 @@ export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultC
96
96
  options: { secrets: seed },
97
97
  },
98
98
  ];
99
- case "managed":
99
+ case "managed": {
100
+ // No provider: the platform injected the values as env vars, so the
101
+ // driver's own env+seed bag is the whole chain.
102
+ if (vaultProvider === undefined || vaultProvider === "") {
103
+ return [
104
+ {
105
+ driver: managedVaultDriver,
106
+ source: "driver",
107
+ options: { secrets: seed },
108
+ },
109
+ ];
110
+ }
111
+ // With a remote provider the bag is remote-only, so the env layers go
112
+ // in front — an operator override in `process.env` still wins.
100
113
  return [
114
+ ...envLayers,
101
115
  {
102
116
  driver: managedVaultDriver,
103
117
  source: "driver",
104
- options: { secrets: seed },
118
+ options: {
119
+ provider: vaultProvider,
120
+ secrets: seed,
121
+ ...(vaultUrl === undefined ? {} : { url: vaultUrl }),
122
+ ...(vaultToken === undefined ? {} : { token: vaultToken }),
123
+ ...(vaultMount === undefined ? {} : { mount: vaultMount }),
124
+ ...(vaultRegion === undefined ? {} : { region: vaultRegion }),
125
+ },
105
126
  },
106
127
  ];
128
+ }
107
129
  case "env":
108
130
  return [
109
131
  ...envLayers,
@@ -113,35 +135,17 @@ export function buildVaultBootChain(options: BuildVaultBootChainOptions): VaultC
113
135
  options: { secrets: seed },
114
136
  },
115
137
  ];
116
- case "openbao": {
117
- if (openbaoUrl && openbaoToken) {
118
- return [
119
- ...envLayers,
120
- {
121
- driver: openbaoVaultDriver,
122
- source: "driver",
123
- options: { url: openbaoUrl, token: openbaoToken, mount: openbaoMount },
124
- },
125
- ];
126
- }
127
- const env = options.env ?? "local";
128
- const missing =
129
- options.missingOpenbao ?? (env === "prod" || env === "docker" ? "throw" : "memory");
130
- if (missing === "throw") {
131
- throw new Error(
132
- 'oke boot: vault driver "openbao" needs OKE_VAULT_URL and OKE_VAULT_TOKEN ' +
133
- "(did `oke dev -d` bootstrap OpenBao / write docker/.env.docker?)",
134
- );
135
- }
138
+ case "vault":
139
+ // The built-in store sits behind the env layers: an operator override
140
+ // in `process.env` still wins, exactly as it does for every driver.
136
141
  return [
137
142
  ...envLayers,
138
143
  {
139
- driver: memoryVaultDriver,
144
+ driver: builtinVaultDriver,
140
145
  source: "driver",
141
146
  options: { secrets: seed },
142
147
  },
143
148
  ];
144
- }
145
149
  default: {
146
150
  const _exhaustive: never = options.driverId;
147
151
  throw new Error(`oke boot: unhandled vault driver ${String(_exhaustive)}`);
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Built-in Vault adapter — seal physics, versioning, rotation, audit chain.
3
+ *
4
+ * Runs against a real PGlite instance so the SQL (DDL, `bytea` round-trip,
5
+ * `DISTINCT ON`, `jsonb`) is exercised rather than mocked.
6
+ */
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { connectPglite } from "../../drivers/pglite.ts";
10
+ import type { SqlConnection } from "../../drivers/types.ts";
11
+ import {
12
+ createBuiltinVaultAdapter,
13
+ sqlConnectionAsExec,
14
+ type BuiltinVaultAdapter,
15
+ } from "./builtin-adapter.ts";
16
+ import { generateMasterKey, masterKeyToBase64 } from "./crypto.ts";
17
+ import { isVaultError, VaultError } from "./errors.ts";
18
+ import type { SqlExec } from "./storage.ts";
19
+
20
+ const PATH = "prod/api/stripe";
21
+ const VALUE = "sk_live_do_not_log_me";
22
+
23
+ /**
24
+ * Adapt a driver connection to the Vault's {@link SqlExec} surface.
25
+ *
26
+ * @param conn - Open PGlite connection
27
+ */
28
+ function asExec(conn: SqlConnection): SqlExec {
29
+ return sqlConnectionAsExec(conn);
30
+ }
31
+
32
+ /** An adapter over a fresh in-memory PGlite, plus its teardown. */
33
+ interface Harness {
34
+ readonly adapter: BuiltinVaultAdapter;
35
+ readonly db: SqlExec;
36
+ readonly masterKey: string;
37
+ close(): Promise<void>;
38
+ }
39
+
40
+ /**
41
+ * Boot an initialized, unsealed adapter over a fresh PGlite instance.
42
+ *
43
+ * @param options - Rewrap batch size for rotation tests
44
+ */
45
+ async function harness(options: { batchSize?: number } = {}): Promise<Harness> {
46
+ const conn = await connectPglite({ url: "memory://vault-test" });
47
+ const db = asExec(conn);
48
+ const adapter = createBuiltinVaultAdapter({
49
+ db,
50
+ ...(options.batchSize === undefined ? {} : { kekRewrapBatchSize: options.batchSize }),
51
+ });
52
+ const init = await adapter.initialize();
53
+ await adapter.unseal(init.masterKey);
54
+ return {
55
+ adapter,
56
+ db,
57
+ masterKey: init.masterKey,
58
+ close: () => conn.close(),
59
+ };
60
+ }
61
+
62
+ describe("builtin vault adapter — lifecycle", () => {
63
+ test("initialize returns the master key once and refuses a second run", async () => {
64
+ const conn = await connectPglite({ url: "memory://vault-test" });
65
+ try {
66
+ const adapter = createBuiltinVaultAdapter({ db: asExec(conn) });
67
+ const init = await adapter.initialize();
68
+
69
+ expect(init.masterKey).toMatch(/^[A-Za-z0-9+/]+=*$/);
70
+ expect(init.verifyHash).toMatch(/^[0-9a-f]{64}$/);
71
+ expect(init.kekVersion).toBe(1);
72
+
73
+ const status = await adapter.status();
74
+ expect(status.initialized).toBe(true);
75
+ expect(status.sealed).toBe(true);
76
+ expect(status.masterKeyPresent).toBe(true);
77
+
78
+ const failure = await adapter.initialize().catch((e: unknown) => e);
79
+ expect(isVaultError(failure, "ALREADY_INITIALIZED")).toBe(true);
80
+ } finally {
81
+ await conn.close();
82
+ }
83
+ });
84
+
85
+ test("unseal + set/get round-trips a secret", async () => {
86
+ const h = await harness();
87
+ try {
88
+ const written = await h.adapter.set(PATH, VALUE, { metadata: { owner: "billing" } });
89
+ expect(written.version).toBe(1);
90
+ expect(written.kekVersion).toBe(1);
91
+ expect(written.metadata).toEqual({ owner: "billing" });
92
+
93
+ const read = await h.adapter.get(PATH);
94
+ expect(read?.value).toBe(VALUE);
95
+ expect(read?.version).toBe(1);
96
+ expect(read?.metadata).toEqual({ owner: "billing" });
97
+ expect(read?.algorithm).toBe("aes-256-gcm");
98
+ } finally {
99
+ await h.close();
100
+ }
101
+ });
102
+
103
+ test("nothing readable is stored in cleartext", async () => {
104
+ const h = await harness();
105
+ try {
106
+ await h.adapter.set(PATH, VALUE);
107
+ const rows = await h.db.query<{ encrypted_value: Uint8Array }>(
108
+ `SELECT encrypted_value FROM oke_vault_secrets`,
109
+ );
110
+ const stored = Buffer.from(rows[0]!.encrypted_value).toString("utf8");
111
+ expect(stored).not.toContain("sk_live");
112
+ } finally {
113
+ await h.close();
114
+ }
115
+ });
116
+
117
+ test("a sealed vault rejects reads and writes", async () => {
118
+ const h = await harness();
119
+ try {
120
+ await h.adapter.set(PATH, VALUE);
121
+ await h.adapter.seal();
122
+
123
+ expect((await h.adapter.status()).sealed).toBe(true);
124
+ expect(h.adapter.getUnsealer()).toBeNull();
125
+
126
+ const read = await h.adapter.get(PATH).catch((e: unknown) => e);
127
+ expect(isVaultError(read, "SEALED")).toBe(true);
128
+ const write = await h.adapter.set(PATH, "x").catch((e: unknown) => e);
129
+ expect(isVaultError(write, "SEALED")).toBe(true);
130
+
131
+ await h.adapter.unseal(h.masterKey);
132
+ expect((await h.adapter.get(PATH))?.value).toBe(VALUE);
133
+ } finally {
134
+ await h.close();
135
+ }
136
+ });
137
+
138
+ test("the wrong master key cannot unseal", async () => {
139
+ const h = await harness();
140
+ try {
141
+ await h.adapter.seal();
142
+ const wrong = masterKeyToBase64(generateMasterKey());
143
+ const failure = await h.adapter.unseal(wrong).catch((e: unknown) => e);
144
+ expect(isVaultError(failure, "INVALID_KEY")).toBe(true);
145
+ expect((failure as VaultError).message).not.toContain(wrong);
146
+ expect(h.adapter.getUnsealer()).toBeNull();
147
+ } finally {
148
+ await h.close();
149
+ }
150
+ });
151
+ });
152
+
153
+ describe("builtin vault adapter — versions and listing", () => {
154
+ test("each write adds a version and reads return the newest", async () => {
155
+ const h = await harness();
156
+ try {
157
+ await h.adapter.set(PATH, "v1");
158
+ const second = await h.adapter.set(PATH, "v2");
159
+ const third = await h.adapter.rotate(PATH, "v3");
160
+
161
+ expect(second.version).toBe(2);
162
+ expect(third.version).toBe(3);
163
+ expect((await h.adapter.get(PATH))?.value).toBe("v3");
164
+ expect((await h.adapter.get(PATH, { version: 1 }))?.value).toBe("v1");
165
+ } finally {
166
+ await h.close();
167
+ }
168
+ });
169
+
170
+ test("soft delete hides every version and a later write keeps counting up", async () => {
171
+ const h = await harness();
172
+ try {
173
+ await h.adapter.set(PATH, "v1");
174
+ await h.adapter.set(PATH, "v2");
175
+
176
+ expect(await h.adapter.delete(PATH)).toBe(true);
177
+ expect(await h.adapter.delete(PATH)).toBe(false);
178
+ expect(await h.adapter.get(PATH)).toBeUndefined();
179
+ expect((await h.adapter.list()).map((e) => e.path)).toEqual([]);
180
+ expect((await h.adapter.list({ includeDeleted: true })).map((e) => e.path)).toEqual([PATH]);
181
+
182
+ const revived = await h.adapter.set(PATH, "v3");
183
+ expect(revived.version).toBe(3);
184
+ expect((await h.adapter.get(PATH))?.value).toBe("v3");
185
+ } finally {
186
+ await h.close();
187
+ }
188
+ });
189
+
190
+ test("list reports one entry per path and honours a prefix", async () => {
191
+ const h = await harness();
192
+ try {
193
+ await h.adapter.set("prod/api/stripe", "a");
194
+ await h.adapter.set("prod/api/stripe", "b");
195
+ await h.adapter.set("prod/db/main", "c");
196
+ await h.adapter.set("staging/api/stripe", "d");
197
+
198
+ const all = await h.adapter.list();
199
+ expect(all.map((e) => e.path)).toEqual([
200
+ "prod/api/stripe",
201
+ "prod/db/main",
202
+ "staging/api/stripe",
203
+ ]);
204
+ expect(all[0]?.version).toBe(2);
205
+
206
+ const scoped = await h.adapter.list({ prefix: "prod/api" });
207
+ expect(scoped.map((e) => e.path)).toEqual(["prod/api/stripe"]);
208
+
209
+ expect((await h.adapter.status()).secretCount).toBe(3);
210
+ } finally {
211
+ await h.close();
212
+ }
213
+ });
214
+
215
+ test("an expired secret fails closed unless the caller opts in", async () => {
216
+ const h = await harness();
217
+ try {
218
+ await h.adapter.set(PATH, VALUE, { expiresAt: new Date(Date.now() - 1_000) });
219
+
220
+ const failure = await h.adapter.get(PATH).catch((e: unknown) => e);
221
+ expect(isVaultError(failure, "EXPIRED")).toBe(true);
222
+ expect((await h.adapter.get(PATH, { allowExpired: true }))?.value).toBe(VALUE);
223
+ } finally {
224
+ await h.close();
225
+ }
226
+ });
227
+
228
+ test("paths are canonicalized and traversal is rejected", async () => {
229
+ const h = await harness();
230
+ try {
231
+ await h.adapter.set("/prod//api///stripe/", VALUE);
232
+ expect((await h.adapter.get(PATH))?.value).toBe(VALUE);
233
+ await expect(h.adapter.get("prod/../etc")).rejects.toThrow(VaultError);
234
+ } finally {
235
+ await h.close();
236
+ }
237
+ });
238
+ });
239
+
240
+ describe("builtin vault adapter — master rotation", () => {
241
+ test("rotateMaster re-wraps every DEK and secrets stay readable", async () => {
242
+ const h = await harness({ batchSize: 2 });
243
+ try {
244
+ for (let i = 0; i < 5; i += 1) {
245
+ await h.adapter.set(`prod/api/key-${i}`, `value-${i}`);
246
+ }
247
+
248
+ const next = generateMasterKey();
249
+ let progress = await h.adapter.rotateMaster(next);
250
+ expect(progress.kekVersion).toBe(2);
251
+ expect(progress.remaining).toBe(3);
252
+
253
+ // Mid-rewrap the vault dual-reads both KEK generations.
254
+ expect((await h.adapter.get("prod/api/key-0"))?.value).toBe("value-0");
255
+ expect((await h.adapter.get("prod/api/key-4"))?.value).toBe("value-4");
256
+
257
+ while (progress.remaining > 0) {
258
+ progress = await h.adapter.continueRotateMaster();
259
+ }
260
+ expect(progress.remaining).toBe(0);
261
+ expect((await h.adapter.status()).kekVersion).toBe(2);
262
+
263
+ for (let i = 0; i < 5; i += 1) {
264
+ const secret = await h.adapter.get(`prod/api/key-${i}`);
265
+ expect(secret?.value).toBe(`value-${i}`);
266
+ expect(secret?.kekVersion).toBe(2);
267
+ }
268
+
269
+ // The old master key is retired; only the new one unseals.
270
+ await h.adapter.seal();
271
+ const stale = await h.adapter.unseal(h.masterKey).catch((e: unknown) => e);
272
+ expect(isVaultError(stale, "INVALID_KEY")).toBe(true);
273
+ await h.adapter.unseal(masterKeyToBase64(next));
274
+ expect((await h.adapter.get("prod/api/key-2"))?.value).toBe("value-2");
275
+ } finally {
276
+ await h.close();
277
+ }
278
+ });
279
+
280
+ test("rotateMaster without a key returns the generated one exactly once", async () => {
281
+ const h = await harness();
282
+ try {
283
+ await h.adapter.set(PATH, VALUE);
284
+ const progress = await h.adapter.rotateMaster();
285
+ expect(progress.remaining).toBe(0);
286
+ expect(progress.masterKey).toBeString();
287
+
288
+ await h.adapter.seal();
289
+ await h.adapter.unseal(progress.masterKey!);
290
+ expect((await h.adapter.get(PATH))?.value).toBe(VALUE);
291
+ } finally {
292
+ await h.close();
293
+ }
294
+ });
295
+
296
+ test("continueRotateMaster without a rotation in flight is rejected", async () => {
297
+ const h = await harness();
298
+ try {
299
+ const failure = await h.adapter.continueRotateMaster().catch((e: unknown) => e);
300
+ expect(isVaultError(failure, "UNSUPPORTED")).toBe(true);
301
+ } finally {
302
+ await h.close();
303
+ }
304
+ });
305
+
306
+ test("continueRotateMaster cold-resumes from rewrap_key_hash after process death", async () => {
307
+ const conn = await connectPglite({ url: "memory://vault-cold-resume" });
308
+ const db = asExec(conn);
309
+ try {
310
+ const first = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
311
+ const init = await first.initialize();
312
+ await first.unseal(init.masterKey);
313
+ for (let i = 0; i < 5; i += 1) {
314
+ await first.set(`prod/api/key-${i}`, `value-${i}`);
315
+ }
316
+
317
+ const next = generateMasterKey();
318
+ const nextB64 = masterKeyToBase64(next);
319
+ let progress = await first.rotateMaster(next);
320
+ expect(progress.remaining).toBe(3);
321
+ expect((await first.status()).rewrapTargetKekVersion).toBe(2);
322
+
323
+ // Simulate process death: drop in-memory pending, reopen over the same DB.
324
+ const second = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
325
+ await second.unseal(init.masterKey);
326
+ expect((await second.status()).rewrapTargetKekVersion).toBe(2);
327
+
328
+ const wrong = await second.continueRotateMaster().catch((e: unknown) => e);
329
+ expect(isVaultError(wrong, "INVALID_KEY")).toBe(true);
330
+
331
+ progress = await second.continueRotateMaster(nextB64);
332
+ while (progress.remaining > 0) {
333
+ progress = await second.continueRotateMaster(nextB64);
334
+ }
335
+ expect(progress.remaining).toBe(0);
336
+ expect((await second.status()).kekVersion).toBe(2);
337
+ expect((await second.status()).rewrapTargetKekVersion).toBeUndefined();
338
+
339
+ for (let i = 0; i < 5; i += 1) {
340
+ expect((await second.get(`prod/api/key-${i}`))?.value).toBe(`value-${i}`);
341
+ }
342
+
343
+ await second.seal();
344
+ const stale = await second.unseal(init.masterKey).catch((e: unknown) => e);
345
+ expect(isVaultError(stale, "INVALID_KEY")).toBe(true);
346
+ await second.unseal(nextB64);
347
+ expect((await second.get("prod/api/key-0"))?.value).toBe("value-0");
348
+ } finally {
349
+ await conn.close();
350
+ }
351
+ });
352
+ });
353
+
354
+ describe("builtin vault adapter — audit and backup", () => {
355
+ test("the audit chain verifies after a full run of operations", async () => {
356
+ const h = await harness();
357
+ try {
358
+ await h.adapter.set(PATH, VALUE);
359
+ await h.adapter.get(PATH);
360
+ await h.adapter.rotate(PATH, "v2");
361
+ await h.adapter.list();
362
+ await h.adapter.delete(PATH);
363
+ await h.adapter.seal();
364
+ await h.adapter.unseal(h.masterKey);
365
+
366
+ expect(await h.adapter.verifyAudit()).toEqual({ ok: true });
367
+
368
+ const rows = await h.db.query<{ action: string; path: string | null }>(
369
+ `SELECT action, path FROM oke_vault_audit ORDER BY seq ASC`,
370
+ );
371
+ expect(rows.map((r) => r.action)).toContain("initialize");
372
+ expect(rows.map((r) => r.action)).toContain("rotate");
373
+ expect(rows.every((r) => r.path === null || !r.path.includes("sk_live"))).toBe(true);
374
+ } finally {
375
+ await h.close();
376
+ }
377
+ });
378
+
379
+ test("a tampered audit row is reported with its id", async () => {
380
+ const h = await harness();
381
+ try {
382
+ await h.adapter.set(PATH, VALUE);
383
+ await h.adapter.get(PATH);
384
+ const rows = await h.db.query<{ id: string }>(
385
+ `SELECT id FROM oke_vault_audit ORDER BY seq ASC`,
386
+ );
387
+ const victim = rows[1]!.id;
388
+ await h.db.execute(`UPDATE oke_vault_audit SET action = 'delete' WHERE id = $1`, [victim]);
389
+
390
+ const result = await h.adapter.verifyAudit();
391
+ expect(result.ok).toBe(false);
392
+ expect(result.brokenAt).toBe(victim);
393
+ } finally {
394
+ await h.close();
395
+ }
396
+ });
397
+
398
+ test("purgeAuditBefore drops old rows and the retained chain still verifies", async () => {
399
+ const h = await harness();
400
+ try {
401
+ await h.adapter.set(PATH, VALUE);
402
+ const removed = await h.adapter.purgeAuditBefore(new Date(Date.now() + 60_000));
403
+ expect(removed).toBeGreaterThan(0);
404
+ expect(await h.adapter.verifyAudit()).toEqual({ ok: true });
405
+ } finally {
406
+ await h.close();
407
+ }
408
+ });
409
+
410
+ test("purgeExpired dry-run counts then hard-deletes expired rows", async () => {
411
+ const h = await harness();
412
+ try {
413
+ await h.adapter.set("app/live", "keep-me");
414
+ await h.adapter.set("app/stale", "drop-me", {
415
+ expiresAt: new Date(Date.now() - 60_000),
416
+ });
417
+
418
+ const dry = await h.adapter.purgeExpired({ dryRun: true });
419
+ expect(dry.count).toBe(1);
420
+ expect(dry.paths).toEqual(["app/stale"]);
421
+ // Dry-run must not remove rows.
422
+ expect(
423
+ (
424
+ await h.db.query<{ n: string }>(
425
+ `SELECT COUNT(*)::text AS n FROM oke_vault_secrets WHERE path = $1`,
426
+ ["app/stale"],
427
+ )
428
+ )[0]?.n,
429
+ ).toBe("1");
430
+
431
+ const live = await h.adapter.purgeExpired();
432
+ expect(live.count).toBe(1);
433
+ expect(
434
+ (
435
+ await h.db.query<{ n: string }>(
436
+ `SELECT COUNT(*)::text AS n FROM oke_vault_secrets WHERE path = $1`,
437
+ ["app/stale"],
438
+ )
439
+ )[0]?.n,
440
+ ).toBe("0");
441
+ expect((await h.adapter.get("app/live"))?.value).toBe("keep-me");
442
+
443
+ const audit = await h.adapter.listAudit({ limit: 5 });
444
+ const purge = audit.find((r) => r.action === "purge" && r.errorMessage?.includes("expired"));
445
+ expect(purge?.errorMessage).toBe("expired count=1");
446
+ // No per-path audit bloat for the purge itself.
447
+ expect(purge?.path).toBeNull();
448
+ } finally {
449
+ await h.close();
450
+ }
451
+ });
452
+
453
+ test("backup round-trips through the backup KEK and rejects foreign bytes", async () => {
454
+ const h = await harness();
455
+ try {
456
+ await h.adapter.set("prod/api/stripe", "a");
457
+ await h.adapter.set("prod/db/main", "b", { metadata: { tier: "primary" } });
458
+
459
+ const blob = await h.adapter.exportBackup();
460
+ expect(Buffer.from(blob).toString("utf8")).toStartWith("oke-vault-backup-v1\n");
461
+ expect(Buffer.from(blob).toString("utf8")).not.toContain("prod/db/main");
462
+
463
+ await h.adapter.delete("prod/api/stripe");
464
+ await h.adapter.delete("prod/db/main");
465
+ expect(await h.adapter.list()).toHaveLength(0);
466
+
467
+ await h.adapter.importBackup(blob);
468
+ expect((await h.adapter.get("prod/api/stripe"))?.value).toBe("a");
469
+ const restored = await h.adapter.get("prod/db/main");
470
+ expect(restored?.value).toBe("b");
471
+ expect(restored?.metadata).toEqual({ tier: "primary" });
472
+
473
+ const junk = await h.adapter.importBackup(new Uint8Array([1, 2, 3])).catch((e: unknown) => e);
474
+ expect(isVaultError(junk, "UNSUPPORTED")).toBe(true);
475
+ } finally {
476
+ await h.close();
477
+ }
478
+ });
479
+ });