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
@@ -1,43 +1,33 @@
1
1
  /**
2
- * `oke vault` — set · list · import · key rotate.
2
+ * `oke vault` — two loops behind one command.
3
+ *
4
+ * The **env loop** (`set` · `list` · `import` · `key rotate`) edits a dotenv
5
+ * file, and is what a solo developer uses before any backend exists.
6
+ *
7
+ * The **builtin loop** (`init` · `status` · `seal` · `unseal` · `rotate` ·
8
+ * `rotate-master` · `audit` · `backup` · `restore` · `purge-expired`) drives
9
+ * okengine's own encrypted-at-rest store over SQL through
10
+ * {@link openBuiltinVaultAdapter}. Every invocation is a fresh process, so
11
+ * the master key must be supplied per command via `--key -` (stdin), an
12
+ * interactive prompt, `--key <base64>`, or `OKE_VAULT_MASTER_KEY`.
13
+ *
14
+ * Never pass master keys as CLI arguments in shared environments — they land
15
+ * in shell history. Prefer `--key -` or the interactive prompt.
16
+ *
17
+ * No cleartext secret value is ever printed. `init` and `rotate-master`
18
+ * print a master key exactly once, because that is the only moment it
19
+ * exists outside the operator's hands.
3
20
  */
4
21
 
5
- import { existsSync, readFileSync } from "node:fs";
6
22
  import { resolve } from "node:path";
23
+ import type { SqlConnection } from "../drivers/types.ts";
24
+ import { openBuiltinVaultAdapter, VAULT_MASTER_KEY_ENV } from "../drivers/vault-builtin.ts";
7
25
  import { parseDotenv, formatDotenv } from "../drivers/vault-dotenv-parse.ts";
26
+ import type { BuiltinVaultAdapter } from "../elements/vault/builtin-adapter.ts";
27
+ import { promptHidden, readStdinSecure } from "./vault-secure-input.ts";
8
28
 
9
- /** Resolved OpenBao target for direct API writes (docker / prod vault). */
10
- interface OpenBaoTarget {
11
- readonly url: string;
12
- readonly token: string;
13
- readonly mount: string;
14
- }
15
-
16
- /**
17
- * Resolve a live OpenBao target when the active vault is OpenBao.
18
- *
19
- * Sources, in order: `OKE_VAULT_*` env · persisted `.oke/openbao/` host
20
- * material + `docker/.env.docker` URL. Returns `null` when neither resolves
21
- * (local dotenv loop) so `oke vault set` keeps the file path.
22
- *
23
- * @param cwd - Project root
24
- */
25
- export async function resolveOpenBaoTarget(cwd: string): Promise<OpenBaoTarget | null> {
26
- const mount = process.env.OKE_VAULT_MOUNT ?? "secret";
27
- const envUrl = process.env.OKE_VAULT_URL;
28
- const envToken = process.env.OKE_VAULT_TOKEN;
29
- if (envUrl && envToken) {
30
- return { url: envUrl.replace(/\/$/, ""), token: envToken, mount };
31
- }
32
- const appTokenPath = resolve(cwd, ".oke/openbao/app.token");
33
- const composeEnvPath = resolve(cwd, "docker/.env.docker");
34
- if (!existsSync(appTokenPath) || !existsSync(composeEnvPath)) return null;
35
- const envFile = parseDotenv(readFileSync(composeEnvPath, "utf8"));
36
- const url = envFile.get("OKE_VAULT_URL");
37
- const token = readFileSync(appTokenPath, "utf8").trim();
38
- if (!url || !token) return null;
39
- return { url: url.replace(/\/$/, ""), token, mount };
40
- }
29
+ /** Env var for the *new* master key when resuming an interrupted `rotate-master`. */
30
+ const VAULT_NEW_MASTER_KEY_ENV = "OKE_VAULT_NEW_MASTER_KEY";
41
31
 
42
32
  /** In-memory / file-backed vault bag for CLI (dotenv-shaped). */
43
33
  export interface VaultCliStore {
@@ -54,6 +44,27 @@ export interface VaultCmdOptions {
54
44
  readonly store?: VaultCliStore;
55
45
  readonly write?: (text: string) => void;
56
46
  readonly readLine?: (prompt: string) => Promise<string>;
47
+ /**
48
+ * Hidden master-key prompt (TTY). Injected by tests; defaults to
49
+ * {@link promptHidden}.
50
+ */
51
+ readonly readSecret?: (prompt: string) => Promise<string>;
52
+ /**
53
+ * Full stdin read for `--key -` / `--new-key -`. Injected by tests;
54
+ * defaults to {@link readStdinSecure}.
55
+ */
56
+ readonly readStdin?: () => Promise<string>;
57
+ /**
58
+ * Environment for master-key and SQL-URL resolution. Defaults to
59
+ * `process.env`.
60
+ */
61
+ readonly env?: Readonly<Record<string, string | undefined>>;
62
+ /**
63
+ * Pre-opened SQL connection for the builtin loop. Injected by tests so a
64
+ * sequence of subcommands shares one in-memory database; the CLI never
65
+ * closes a connection it did not open.
66
+ */
67
+ readonly sql?: SqlConnection;
57
68
  }
58
69
 
59
70
  /**
@@ -81,11 +92,25 @@ export async function openEnvStore(path: string): Promise<VaultCliStore> {
81
92
  };
82
93
  }
83
94
 
95
+ /** Subcommands served by the SQL-backed builtin Vault rather than dotenv. */
96
+ const BUILTIN_SUBCOMMANDS: ReadonlySet<string> = new Set([
97
+ "init",
98
+ "status",
99
+ "seal",
100
+ "unseal",
101
+ "rotate",
102
+ "rotate-master",
103
+ "audit",
104
+ "backup",
105
+ "restore",
106
+ "purge-expired",
107
+ ]);
108
+
84
109
  /**
85
110
  * Run a vault subcommand.
86
111
  *
87
112
  * @param args - Args after `vault`
88
- * @param options - Store injection
113
+ * @param options - Store / connection / env injection
89
114
  */
90
115
  export async function vaultCli(
91
116
  args: readonly string[],
@@ -94,14 +119,14 @@ export async function vaultCli(
94
119
  const write = options.write ?? ((t) => process.stdout.write(t));
95
120
  const [sub, ...rest] = args;
96
121
  if (!sub || sub === "--help" || sub === "-h") {
97
- write(`oke vault set <NAME> [value]
98
- oke vault list
99
- oke vault import <file>
100
- oke vault key rotate
101
- `);
122
+ write(VAULT_HELP);
102
123
  return sub ? 0 : 1;
103
124
  }
104
125
 
126
+ if (BUILTIN_SUBCOMMANDS.has(sub)) {
127
+ return runBuiltinVault(sub, rest, options, write);
128
+ }
129
+
105
130
  const cwd = options.cwd ?? process.cwd();
106
131
  const envFile = resolve(cwd, options.envFile ?? ".env.local");
107
132
  const store = options.store ?? (await openEnvStore(envFile));
@@ -112,35 +137,6 @@ oke vault key rotate
112
137
  console.error("Usage: oke vault set <NAME> [value]");
113
138
  return 1;
114
139
  }
115
- const ob = await resolveOpenBaoTarget(cwd);
116
- if (ob && !options.store && !options.envFile) {
117
- let value = rest[1];
118
- if (value === undefined) {
119
- const read =
120
- options.readLine ??
121
- (async () => {
122
- write(`Enter value for ${name}: `);
123
- const chunks: Buffer[] = [];
124
- for await (const chunk of Bun.stdin.stream()) {
125
- chunks.push(Buffer.from(chunk));
126
- if (Buffer.concat(chunks).includes(0x0a)) break;
127
- }
128
- return Buffer.concat(chunks).toString("utf8").trim();
129
- });
130
- value = await read(`Enter value for ${name}: `);
131
- }
132
- const res = await fetch(`${ob.url}/v1/${ob.mount}/data/${encodeURIComponent(name)}`, {
133
- method: "POST",
134
- headers: { "X-Vault-Token": ob.token, "content-type": "application/json" },
135
- body: JSON.stringify({ data: { value } }),
136
- });
137
- if (!res.ok) {
138
- console.error(`oke vault: OpenBao write failed (${res.status}) — sealed / unauthorized?`);
139
- return 1;
140
- }
141
- write(`oke vault: set ${name} (openbao)\n`);
142
- return 0;
143
- }
144
140
  let value = rest[1];
145
141
  if (value === undefined) {
146
142
  const read =
@@ -201,3 +197,665 @@ oke vault key rotate
201
197
  console.error(`Unknown vault command: ${sub}`);
202
198
  return 1;
203
199
  }
200
+
201
+ /** `oke vault --help` body — both loops, env first. */
202
+ const VAULT_HELP = `oke vault set <NAME> [value]
203
+ oke vault list
204
+ oke vault import <file>
205
+ oke vault key rotate
206
+
207
+ Builtin vault (encrypted at rest, SQL-backed):
208
+ oke vault init
209
+ oke vault status [--json]
210
+ oke vault seal
211
+ oke vault unseal [--key <base64|-]
212
+ oke vault rotate <path> [value]
213
+ oke vault rotate-master [--key <base64|-] [--new-key <base64|-]
214
+ oke vault audit [--limit <n>] [--path <p>] [--json]
215
+ oke vault audit verify
216
+ oke vault audit purge --before <iso>
217
+ oke vault purge-expired [--dry-run] [--before <iso>]
218
+ oke vault backup <file>
219
+ oke vault restore <file>
220
+
221
+ Master key resolution (in order): --key - (stdin), --key <base64>,
222
+ ${VAULT_MASTER_KEY_ENV}, then an interactive hidden prompt.
223
+ Never pass master keys as CLI arguments in shared environments — they land
224
+ in shell history. Prefer --key - or the prompt.
225
+ --url overrides the SQL URL (DATABASE_URL / OKE_STORE_SQL_URL).
226
+ Resume an interrupted rotate-master with --new-key - / --new-key <base64>
227
+ (or ${VAULT_NEW_MASTER_KEY_ENV}).
228
+ `;
229
+
230
+ /** Flags understood by the builtin-vault subcommands. */
231
+ interface VaultFlags {
232
+ /** Non-flag arguments in order. */
233
+ readonly positionals: readonly string[];
234
+ /** Base64 master key (`--key`) — current / old key for unseal. Use `-` for stdin. */
235
+ readonly key?: string;
236
+ /** Base64 new master key (`--new-key`) — resume interrupted rotate-master. Use `-` for stdin. */
237
+ readonly newKey?: string;
238
+ /** SQL URL override (`--url`). */
239
+ readonly url?: string;
240
+ /** Path filter for `audit` (`--path`). */
241
+ readonly path?: string;
242
+ /** Exclusive upper bound for `audit purge` / `purge-expired` (`--before`). */
243
+ readonly before?: string;
244
+ /** Row cap for `audit` (`--limit`). */
245
+ readonly limit?: number;
246
+ /** Count without deleting (`purge-expired --dry-run`). */
247
+ readonly dryRun: boolean;
248
+ /** Machine-parseable output (`--json` / `-j`). */
249
+ readonly json: boolean;
250
+ }
251
+
252
+ /** Flags that consume the next argument (or a `--flag=value` suffix). */
253
+ const VALUE_FLAGS = ["--key", "--new-key", "--url", "--path", "--before", "--limit"] as const;
254
+
255
+ /**
256
+ * Parse builtin-vault flags, leaving everything else as a positional.
257
+ *
258
+ * Only the builtin loop is parsed this way: `oke vault set` takes arbitrary
259
+ * values that may legitimately begin with a dash.
260
+ *
261
+ * @param args - Args after the subcommand
262
+ */
263
+ function parseVaultFlags(args: readonly string[]): VaultFlags {
264
+ const positionals: string[] = [];
265
+ const values = new Map<string, string>();
266
+ let json = false;
267
+ let dryRun = false;
268
+
269
+ for (let i = 0; i < args.length; i += 1) {
270
+ const arg = args[i]!;
271
+ if (arg === "--json" || arg === "-j") {
272
+ json = true;
273
+ continue;
274
+ }
275
+ if (arg === "--dry-run") {
276
+ dryRun = true;
277
+ continue;
278
+ }
279
+ const inline = VALUE_FLAGS.find((f) => arg.startsWith(`${f}=`));
280
+ if (inline) {
281
+ values.set(inline, arg.slice(inline.length + 1));
282
+ continue;
283
+ }
284
+ const flag = VALUE_FLAGS.find((f) => f === arg);
285
+ if (flag) {
286
+ const next = args[i + 1];
287
+ if (next !== undefined) {
288
+ values.set(flag, next);
289
+ i += 1;
290
+ }
291
+ continue;
292
+ }
293
+ positionals.push(arg);
294
+ }
295
+
296
+ const limit = values.get("--limit");
297
+ const parsedLimit = limit === undefined ? Number.NaN : Number.parseInt(limit, 10);
298
+ return {
299
+ positionals,
300
+ json,
301
+ dryRun,
302
+ ...(values.has("--key") ? { key: values.get("--key")! } : {}),
303
+ ...(values.has("--new-key") ? { newKey: values.get("--new-key")! } : {}),
304
+ ...(values.has("--url") ? { url: values.get("--url")! } : {}),
305
+ ...(values.has("--path") ? { path: values.get("--path")! } : {}),
306
+ ...(values.has("--before") ? { before: values.get("--before")! } : {}),
307
+ ...(Number.isFinite(parsedLimit) && parsedLimit > 0 ? { limit: parsedLimit } : {}),
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Open the builtin adapter for one subcommand and close what we opened.
313
+ *
314
+ * @param options - CLI options (env / injected connection)
315
+ * @param flags - Parsed flags
316
+ * @param masterKey - Key to unseal with; `""` leaves the vault sealed
317
+ * @param run - Body receiving the adapter
318
+ */
319
+ async function withVault<T>(
320
+ options: VaultCmdOptions,
321
+ flags: VaultFlags,
322
+ masterKey: string,
323
+ run: (adapter: BuiltinVaultAdapter) => Promise<T>,
324
+ ): Promise<T> {
325
+ const opened = await openBuiltinVaultAdapter({
326
+ env: options.env ?? process.env,
327
+ masterKey,
328
+ ...(flags.url === undefined ? {} : { url: flags.url }),
329
+ ...(options.sql === undefined ? {} : { connection: options.sql }),
330
+ });
331
+ try {
332
+ return await run(opened.adapter);
333
+ } finally {
334
+ await opened.close();
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Resolve the master key for a subcommand that must read or write secrets.
340
+ *
341
+ * Order: `--key -` (stdin) → `--key <value>` → env → interactive prompt
342
+ * when `required` is true.
343
+ *
344
+ * @param flags - Parsed flags
345
+ * @param options - CLI options (env / IO injection)
346
+ * @param required - When true, prompt if nothing else supplies a key
347
+ */
348
+ async function resolveMasterKey(
349
+ flags: VaultFlags,
350
+ options: VaultCmdOptions,
351
+ required = false,
352
+ ): Promise<string | null> {
353
+ if (flags.key === "-") {
354
+ const fromStdin = await (options.readStdin ?? (() => readStdinSecure()))();
355
+ return fromStdin.trim().length > 0 ? fromStdin.trim() : null;
356
+ }
357
+ if (flags.key !== undefined && flags.key.trim().length > 0) {
358
+ return flags.key.trim();
359
+ }
360
+ const env = options.env ?? process.env;
361
+ const fromEnv = env[VAULT_MASTER_KEY_ENV];
362
+ if (fromEnv !== undefined && fromEnv.trim().length > 0) {
363
+ return fromEnv.trim();
364
+ }
365
+ if (!required) return null;
366
+ if (options.readSecret) {
367
+ const prompted = (await options.readSecret("Enter current master key: ")).trim();
368
+ return prompted.length > 0 ? prompted : null;
369
+ }
370
+ // Never block a non-interactive pipe waiting for a hidden prompt.
371
+ if (!process.stdin.isTTY) return null;
372
+ const prompted = (
373
+ await promptHidden("Enter current master key: ", { write: options.write })
374
+ ).trim();
375
+ return prompted.length > 0 ? prompted : null;
376
+ }
377
+
378
+ /**
379
+ * Resolve the *new* master key when resuming `rotate-master`.
380
+ *
381
+ * @param flags - Parsed flags
382
+ * @param options - CLI options
383
+ * @param required - When true, prompt if nothing else supplies a key
384
+ */
385
+ async function resolveNewMasterKey(
386
+ flags: VaultFlags,
387
+ options: VaultCmdOptions,
388
+ required = false,
389
+ ): Promise<string | undefined> {
390
+ if (flags.newKey === "-") {
391
+ const fromStdin = await (options.readStdin ?? (() => readStdinSecure()))();
392
+ const trimmed = fromStdin.trim();
393
+ return trimmed.length > 0 ? trimmed : undefined;
394
+ }
395
+ if (flags.newKey !== undefined && flags.newKey.trim().length > 0) {
396
+ return flags.newKey.trim();
397
+ }
398
+ const env = options.env ?? process.env;
399
+ const fromEnv = env[VAULT_NEW_MASTER_KEY_ENV];
400
+ if (fromEnv !== undefined && fromEnv.trim().length > 0) {
401
+ return fromEnv.trim();
402
+ }
403
+ if (!required) return undefined;
404
+ if (options.readSecret) {
405
+ const prompted = (await options.readSecret("Enter new master key: ")).trim();
406
+ return prompted.length > 0 ? prompted : undefined;
407
+ }
408
+ if (!process.stdin.isTTY) return undefined;
409
+ const prompted = (await promptHidden("Enter new master key: ", { write: options.write })).trim();
410
+ return prompted.length > 0 ? prompted : undefined;
411
+ }
412
+
413
+ /**
414
+ * Run one builtin-vault subcommand.
415
+ *
416
+ * @param sub - Subcommand name
417
+ * @param rest - Args after the subcommand
418
+ * @param options - CLI options
419
+ * @param write - stdout sink
420
+ */
421
+ async function runBuiltinVault(
422
+ sub: string,
423
+ rest: readonly string[],
424
+ options: VaultCmdOptions,
425
+ write: (text: string) => void,
426
+ ): Promise<number> {
427
+ const flags = parseVaultFlags(rest);
428
+ const cwd = options.cwd ?? process.cwd();
429
+ try {
430
+ switch (sub) {
431
+ case "init":
432
+ return await runVaultInit(options, flags, write);
433
+ case "status":
434
+ return await runVaultStatus(options, flags, write);
435
+ case "seal":
436
+ await withVault(options, flags, "", (a) => a.seal());
437
+ write("oke vault: sealed\n");
438
+ return 0;
439
+ case "unseal":
440
+ return await runVaultUnseal(options, flags, write);
441
+ case "rotate":
442
+ return await runVaultRotate(options, flags, write);
443
+ case "rotate-master":
444
+ return await runVaultRotateMaster(options, flags, write);
445
+ case "audit":
446
+ return await runVaultAudit(options, flags, write);
447
+ case "backup":
448
+ return await runVaultBackup(options, flags, cwd, write);
449
+ case "restore":
450
+ return await runVaultRestore(options, flags, cwd, write);
451
+ case "purge-expired":
452
+ return await runVaultPurgeExpired(options, flags, write);
453
+ default:
454
+ console.error(`Unknown vault command: ${sub}`);
455
+ return 1;
456
+ }
457
+ } catch (error) {
458
+ // VaultError messages are secret-free by construction.
459
+ console.error(`oke vault: ${error instanceof Error ? error.message : String(error)}`);
460
+ return 1;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * `oke vault init` — create backend state and print the master key once.
466
+ *
467
+ * @param options - CLI options
468
+ * @param flags - Parsed flags
469
+ * @param write - stdout sink
470
+ */
471
+ async function runVaultInit(
472
+ options: VaultCmdOptions,
473
+ flags: VaultFlags,
474
+ write: (text: string) => void,
475
+ ): Promise<number> {
476
+ // Unsealing is impossible before the master record exists.
477
+ const result = await withVault(options, flags, "", (a) => a.initialize());
478
+ write(`oke vault: initialized (kek v${result.kekVersion})\n`);
479
+ write("\nMaster key — shown once, store it out of band:\n");
480
+ write(` ${result.masterKey}\n`);
481
+ write(
482
+ `\nLater commands: pipe it with --key - , enter it at the prompt, or set ${VAULT_MASTER_KEY_ENV}.\n`,
483
+ );
484
+ write("Never pass master keys as CLI arguments in shared environments.\n");
485
+ return 0;
486
+ }
487
+
488
+ /**
489
+ * `oke vault status` — seal state, KEK generation, secret count, rewrap.
490
+ *
491
+ * `sealed` is the state of *this* invocation: the vault is unsealed only
492
+ * when a usable master key was supplied.
493
+ *
494
+ * @param options - CLI options
495
+ * @param flags - Parsed flags
496
+ * @param write - stdout sink
497
+ */
498
+ async function runVaultStatus(
499
+ options: VaultCmdOptions,
500
+ flags: VaultFlags,
501
+ write: (text: string) => void,
502
+ ): Promise<number> {
503
+ // Optional key: do not prompt when absent (status works sealed).
504
+ const status = await withVault(
505
+ options,
506
+ flags,
507
+ (await resolveMasterKey(flags, options)) ?? "",
508
+ (a) => a.status(),
509
+ );
510
+ if (flags.json) {
511
+ write(`${JSON.stringify(status, null, 2)}\n`);
512
+ return 0;
513
+ }
514
+ write("oke vault status\n");
515
+ write(field("initialized", status.initialized ? "yes" : "no"));
516
+ write(field("sealed", status.sealed ? "yes" : "no"));
517
+ write(field("master key", status.masterKeyPresent ? "present" : "absent"));
518
+ write(field("kek version", String(status.kekVersion)));
519
+ write(field("secrets", String(status.secretCount)));
520
+ write(field("seal count", String(status.sealCount)));
521
+ if (status.lastUnsealedAt) write(field("last unsealed", status.lastUnsealedAt.toISOString()));
522
+ if (status.lastSealedAt) write(field("last sealed", status.lastSealedAt.toISOString()));
523
+ if (status.rewrapTargetKekVersion !== undefined) {
524
+ write(field("rewrap target", `kek v${status.rewrapTargetKekVersion}`));
525
+ }
526
+ if (!status.initialized) write("\nRun `oke vault init` to create the vault.\n");
527
+ return 0;
528
+ }
529
+
530
+ /**
531
+ * `oke vault unseal` — verify the master key and clear the sealed flag.
532
+ *
533
+ * @param options - CLI options
534
+ * @param flags - Parsed flags
535
+ * @param write - stdout sink
536
+ */
537
+ async function runVaultUnseal(
538
+ options: VaultCmdOptions,
539
+ flags: VaultFlags,
540
+ write: (text: string) => void,
541
+ ): Promise<number> {
542
+ const key = await resolveMasterKey(flags, options, true);
543
+ if (key === null) {
544
+ console.error(
545
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
546
+ );
547
+ return 1;
548
+ }
549
+ // Opening with a key unseals and records the audit row; a wrong key throws.
550
+ const status = await withVault(options, flags, key, (a) => a.status());
551
+ if (status.sealed) {
552
+ console.error("oke vault: unseal failed — vault is still sealed");
553
+ return 1;
554
+ }
555
+ write(`oke vault: unsealed (kek v${status.kekVersion}, ${status.secretCount} secret(s))\n`);
556
+ return 0;
557
+ }
558
+
559
+ /**
560
+ * `oke vault rotate <path> [value]` — write a new version under a fresh DEK.
561
+ *
562
+ * With no value the current cleartext is re-encrypted, which rotates the
563
+ * data key without changing what readers see.
564
+ *
565
+ * @param options - CLI options
566
+ * @param flags - Parsed flags
567
+ * @param write - stdout sink
568
+ */
569
+ async function runVaultRotate(
570
+ options: VaultCmdOptions,
571
+ flags: VaultFlags,
572
+ write: (text: string) => void,
573
+ ): Promise<number> {
574
+ const path = flags.positionals[0];
575
+ if (!path) {
576
+ console.error("Usage: oke vault rotate <path> [value]");
577
+ return 1;
578
+ }
579
+ const key = await resolveMasterKey(flags, options, true);
580
+ if (key === null) {
581
+ console.error(
582
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
583
+ );
584
+ return 1;
585
+ }
586
+
587
+ const rotated = await withVault(options, flags, key, async (adapter) => {
588
+ const current = await adapter.get(path);
589
+ if (!current) return null;
590
+ return adapter.rotate(path, flags.positionals[1] ?? current.value);
591
+ });
592
+ if (!rotated) {
593
+ console.error(`oke vault: no such secret: ${path}`);
594
+ return 1;
595
+ }
596
+ write(`oke vault: rotated ${rotated.path} → v${rotated.version} (fresh data key)\n`);
597
+ return 0;
598
+ }
599
+
600
+ /**
601
+ * `oke vault rotate-master` — new KEK generation, re-wrapping every DEK.
602
+ *
603
+ * Runs the rewrap to completion so the command leaves no half-migrated
604
+ * state behind: an interrupted rotation would keep two master keys live.
605
+ *
606
+ * @param options - CLI options
607
+ * @param flags - Parsed flags
608
+ * @param write - stdout sink
609
+ */
610
+ async function runVaultRotateMaster(
611
+ options: VaultCmdOptions,
612
+ flags: VaultFlags,
613
+ write: (text: string) => void,
614
+ ): Promise<number> {
615
+ const key = await resolveMasterKey(flags, options, true);
616
+ if (key === null) {
617
+ console.error(
618
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
619
+ );
620
+ return 1;
621
+ }
622
+
623
+ // Prompt for --new-key only when resuming an interrupted rotation needs it;
624
+ // a fresh rotateMaster() generates the new key itself.
625
+ const newKey = await resolveNewMasterKey(flags, options, false);
626
+
627
+ const outcome = await withVault(options, flags, key, async (adapter) => {
628
+ const status = await adapter.status();
629
+ const resumeNeedsKey = status.rewrapTargetKekVersion !== undefined;
630
+ const resumeKey =
631
+ resumeNeedsKey && newKey === undefined
632
+ ? await resolveNewMasterKey(flags, options, true)
633
+ : newKey;
634
+ let progress = resumeNeedsKey
635
+ ? await adapter.continueRotateMaster(resumeKey)
636
+ : await adapter.rotateMaster();
637
+ const masterKey = progress.masterKey;
638
+ let batches = 1;
639
+ while (progress.remaining > 0) {
640
+ progress = await adapter.continueRotateMaster(resumeKey);
641
+ batches += 1;
642
+ }
643
+ return { kekVersion: progress.kekVersion, batches, masterKey };
644
+ });
645
+
646
+ write(`oke vault: master rotated → kek v${outcome.kekVersion} (${outcome.batches} batch(es))\n`);
647
+ if (outcome.masterKey !== undefined) {
648
+ write("\nNew master key — shown once, store it out of band:\n");
649
+ write(` ${outcome.masterKey}\n`);
650
+ write(`\nThe previous ${VAULT_MASTER_KEY_ENV} no longer opens this vault.\n`);
651
+ }
652
+ return 0;
653
+ }
654
+
655
+ /**
656
+ * `oke vault audit [verify|purge]` — read, verify, or trim the audit chain.
657
+ *
658
+ * Audit rows describe operations, never values, so none of these need an
659
+ * unsealed vault.
660
+ *
661
+ * @param options - CLI options
662
+ * @param flags - Parsed flags
663
+ * @param write - stdout sink
664
+ */
665
+ async function runVaultAudit(
666
+ options: VaultCmdOptions,
667
+ flags: VaultFlags,
668
+ write: (text: string) => void,
669
+ ): Promise<number> {
670
+ const action = flags.positionals[0];
671
+
672
+ if (action === "verify") {
673
+ const result = await withVault(options, flags, "", (a) => a.verifyAudit());
674
+ if (flags.json) {
675
+ write(`${JSON.stringify(result, null, 2)}\n`);
676
+ return result.ok ? 0 : 1;
677
+ }
678
+ if (result.ok) {
679
+ write("oke vault: audit chain intact\n");
680
+ return 0;
681
+ }
682
+ console.error(`oke vault: audit chain broken at row ${result.brokenAt}`);
683
+ return 1;
684
+ }
685
+
686
+ if (action === "purge") {
687
+ if (flags.before === undefined) {
688
+ console.error("Usage: oke vault audit purge --before <iso>");
689
+ return 1;
690
+ }
691
+ const before = new Date(flags.before);
692
+ if (Number.isNaN(before.getTime())) {
693
+ console.error(`oke vault: --before is not a date: ${flags.before}`);
694
+ return 1;
695
+ }
696
+ const removed = await withVault(options, flags, "", (a) => a.purgeAuditBefore(before));
697
+ write(`oke vault: purged ${removed} audit row(s) before ${before.toISOString()}\n`);
698
+ return 0;
699
+ }
700
+
701
+ if (action !== undefined) {
702
+ console.error(`Unknown vault audit command: ${action}`);
703
+ return 1;
704
+ }
705
+
706
+ const rows = await withVault(options, flags, "", (a) =>
707
+ a.listAudit({
708
+ limit: flags.limit ?? 50,
709
+ ...(flags.path === undefined ? {} : { path: flags.path }),
710
+ }),
711
+ );
712
+ if (flags.json) {
713
+ write(`${JSON.stringify(rows, null, 2)}\n`);
714
+ return 0;
715
+ }
716
+ if (rows.length === 0) {
717
+ write("oke vault: (no audit rows)\n");
718
+ return 0;
719
+ }
720
+ for (const row of rows) {
721
+ const actor = row.actorId === null ? row.actorType : `${row.actorType}:${row.actorId}`;
722
+ write(
723
+ `${row.createdAt.toISOString()} ${padRight(row.action, 10)} ${
724
+ row.success ? "ok " : "FAIL"
725
+ } ${padRight(actor, 12)} ${row.path ?? "-"}\n`,
726
+ );
727
+ }
728
+ return 0;
729
+ }
730
+
731
+ /**
732
+ * `oke vault backup <file>` — every live secret in one encrypted bundle.
733
+ *
734
+ * @param options - CLI options
735
+ * @param flags - Parsed flags
736
+ * @param cwd - Project root for relative paths
737
+ * @param write - stdout sink
738
+ */
739
+ async function runVaultBackup(
740
+ options: VaultCmdOptions,
741
+ flags: VaultFlags,
742
+ cwd: string,
743
+ write: (text: string) => void,
744
+ ): Promise<number> {
745
+ const file = flags.positionals[0];
746
+ if (!file) {
747
+ console.error("Usage: oke vault backup <file>");
748
+ return 1;
749
+ }
750
+ const key = await resolveMasterKey(flags, options, true);
751
+ if (key === null) {
752
+ console.error(
753
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
754
+ );
755
+ return 1;
756
+ }
757
+ const blob = await withVault(options, flags, key, (a) => a.exportBackup());
758
+ await Bun.write(resolve(cwd, file), blob);
759
+ write(`oke vault: wrote ${blob.byteLength} encrypted byte(s) to ${file}\n`);
760
+ return 0;
761
+ }
762
+
763
+ /**
764
+ * `oke vault restore <file>` — replay a bundle through the normal write path.
765
+ *
766
+ * @param options - CLI options
767
+ * @param flags - Parsed flags
768
+ * @param cwd - Project root for relative paths
769
+ * @param write - stdout sink
770
+ */
771
+ async function runVaultRestore(
772
+ options: VaultCmdOptions,
773
+ flags: VaultFlags,
774
+ cwd: string,
775
+ write: (text: string) => void,
776
+ ): Promise<number> {
777
+ const file = flags.positionals[0];
778
+ if (!file) {
779
+ console.error("Usage: oke vault restore <file>");
780
+ return 1;
781
+ }
782
+ const key = await resolveMasterKey(flags, options, true);
783
+ if (key === null) {
784
+ console.error(
785
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
786
+ );
787
+ return 1;
788
+ }
789
+ const source = Bun.file(resolve(cwd, file));
790
+ if (!(await source.exists())) {
791
+ console.error(`oke vault: no such file: ${file}`);
792
+ return 1;
793
+ }
794
+ const blob = new Uint8Array(await source.arrayBuffer());
795
+ const restored = await withVault(options, flags, key, async (adapter) => {
796
+ await adapter.importBackup(blob);
797
+ return (await adapter.list()).length;
798
+ });
799
+ write(`oke vault: restored ${file} (${restored} live secret(s))\n`);
800
+ return 0;
801
+ }
802
+
803
+ /**
804
+ * `oke vault purge-expired` — hard-delete secret rows past `expires_at`.
805
+ *
806
+ * @param options - CLI options
807
+ * @param flags - Parsed flags
808
+ * @param write - stdout sink
809
+ */
810
+ async function runVaultPurgeExpired(
811
+ options: VaultCmdOptions,
812
+ flags: VaultFlags,
813
+ write: (text: string) => void,
814
+ ): Promise<number> {
815
+ const key = await resolveMasterKey(flags, options, true);
816
+ if (key === null) {
817
+ console.error(
818
+ `oke vault: pass --key - (stdin), --key <base64>, set ${VAULT_MASTER_KEY_ENV}, or enter at the prompt`,
819
+ );
820
+ return 1;
821
+ }
822
+ let before = new Date();
823
+ if (flags.before !== undefined) {
824
+ before = new Date(flags.before);
825
+ if (Number.isNaN(before.getTime())) {
826
+ console.error(`oke vault: --before is not a date: ${flags.before}`);
827
+ return 1;
828
+ }
829
+ }
830
+ const result = await withVault(options, flags, key, (a) =>
831
+ a.purgeExpired({ before, dryRun: flags.dryRun }),
832
+ );
833
+ if (flags.dryRun) {
834
+ write(
835
+ `oke vault: would purge ${result.count} expired secret row(s) before ${before.toISOString()}\n`,
836
+ );
837
+ return 0;
838
+ }
839
+ write(`oke vault: purged ${result.count} expired secret row(s) before ${before.toISOString()}\n`);
840
+ return 0;
841
+ }
842
+
843
+ /**
844
+ * Format one `label value` status line.
845
+ *
846
+ * @param label - Left column
847
+ * @param value - Right column
848
+ */
849
+ function field(label: string, value: string): string {
850
+ return ` ${padRight(label, 14)} ${value}\n`;
851
+ }
852
+
853
+ /**
854
+ * Pad `text` to `width` with spaces.
855
+ *
856
+ * @param text - Source text
857
+ * @param width - Minimum width
858
+ */
859
+ function padRight(text: string, width: number): string {
860
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
861
+ }