okengine 0.10.2 → 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.
- package/package.json +76 -10
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/deployment/docker-swarm.mdx +48 -111
- package/site/content/docs/deployment/docker.mdx +43 -36
- package/site/content/docs/deployment/index.mdx +9 -9
- package/site/content/docs/deployment/kubernetes.mdx +4 -4
- package/site/content/docs/elements/ai.mdx +7 -13
- package/site/content/docs/elements/channel.mdx +9 -13
- package/site/content/docs/elements/clock.mdx +8 -14
- package/site/content/docs/elements/flow.mdx +46 -54
- package/site/content/docs/elements/gate.mdx +6 -11
- package/site/content/docs/elements/signal.mdx +2 -8
- package/site/content/docs/elements/store.mdx +79 -93
- package/site/content/docs/elements/vault.mdx +89 -76
- package/site/content/docs/get-started/basic-usage.mdx +8 -14
- package/site/content/docs/get-started/installation.mdx +36 -36
- package/site/content/docs/get-started/introduction.mdx +11 -0
- package/site/content/docs/get-started/why.mdx +3 -4
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +11 -11
- package/site/content/docs/plugins/otp.mdx +2 -2
- package/site/content/docs/plugins/passkey.mdx +1 -1
- package/site/content/docs/plugins/two-factor.mdx +1 -1
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +1 -1
- package/site/content/docs/recipes/cockroachdb.mdx +2 -2
- package/site/content/docs/recipes/dragonfly.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -2
- package/site/content/docs/recipes/llama-cpp.mdx +21 -23
- package/site/content/docs/recipes/mailpit.mdx +11 -11
- package/site/content/docs/recipes/meilisearch.mdx +10 -10
- package/site/content/docs/recipes/meta.json +0 -1
- package/site/content/docs/recipes/ollama.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +12 -9
- package/site/content/docs/recipes/postgres.mdx +3 -3
- package/site/content/docs/recipes/redis.mdx +2 -2
- package/site/content/docs/recipes/rustfs.mdx +10 -7
- package/site/content/docs/recipes/sglang.mdx +1 -1
- package/site/content/docs/recipes/supabase-docker.mdx +1 -1
- package/site/content/docs/recipes/timescale.mdx +2 -2
- package/site/content/docs/recipes/valkey.mdx +1 -1
- package/site/content/docs/recipes/vllm.mdx +1 -1
- package/site/content/docs/recipes/yugabytedb.mdx +2 -2
- package/site/content/docs/reference/cli.md +61 -38
- package/site/content/docs/reference/configuration.mdx +145 -70
- package/site/content/docs/reference/environment-variables.mdx +36 -28
- package/site/content/docs/reference/errors.mdx +28 -29
- package/site/content/docs/reference/fx.mdx +14 -7
- package/site/content/docs/reference/i18n.mdx +17 -6
- package/site/content/docs/reference/index.mdx +5 -0
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/migrating-environments.mdx +158 -0
- package/site/content/docs/reference/plugins.mdx +2 -2
- package/src/auth/gate-auth.test.ts +12 -10
- package/src/cli/ai-setup/ai-setup.test.ts +30 -16
- package/src/cli/ai-setup/apply.ts +111 -15
- package/src/cli/ai-setup/catalog.ts +3 -3
- package/src/cli/ai-setup/index.ts +1 -1
- package/src/cli/ai-setup/prompts.ts +1 -1
- package/src/cli/ai-setup/recommend.test.ts +1 -1
- package/src/cli/ask-seed.test.ts +3 -3
- package/src/cli/build.ts +8 -9
- package/src/cli/db-auto-push.test.ts +6 -3
- package/src/cli/db-auto-push.ts +7 -5
- package/src/cli/db-seed.ts +7 -8
- package/src/cli/db.test.ts +70 -71
- package/src/cli/db.ts +86 -12
- package/src/cli/dev-controller.test.ts +35 -0
- package/src/cli/dev-controller.ts +298 -0
- package/src/cli/dev-controls.ts +11 -65
- package/src/cli/dev-db-push.test.ts +47 -15
- package/src/cli/dev-schema-sync.test.ts +11 -11
- package/src/cli/dev-schema-sync.ts +10 -10
- package/src/cli/dev-session-lock.test.ts +106 -0
- package/src/cli/dev-session-lock.ts +209 -0
- package/src/cli/dev.test.ts +246 -114
- package/src/cli/dev.ts +153 -205
- package/src/cli/doc-staleness.test.ts +3 -6
- package/src/cli/docker-clean.test.ts +33 -1
- package/src/cli/docker-clean.ts +28 -33
- package/src/cli/docker-cli.test.ts +12 -11
- package/src/cli/docker.ts +95 -20
- package/src/cli/doctor.test.ts +40 -0
- package/src/cli/doctor.ts +142 -9
- package/src/cli/drizzle-env.test.ts +11 -13
- package/src/cli/drizzle-env.ts +18 -11
- package/src/cli/ensure-drizzle-config.ts +11 -14
- package/src/cli/hero-meta.test.ts +15 -15
- package/src/cli/hero-meta.ts +10 -9
- package/src/cli/index.ts +36 -6
- package/src/cli/load-config.images.test.ts +44 -10
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +35 -29
- package/src/cli/meilisearch-local.ts +1 -2
- package/src/cli/mode.ts +7 -58
- package/src/cli/registry.ts +175 -19
- package/src/cli/resolve-dev-sql-env.test.ts +8 -16
- package/src/cli/resolve-dev-sql-env.ts +13 -15
- package/src/cli/safe-defaults.test.ts +29 -11
- package/src/cli/schema.ts +65 -16
- package/src/cli/test.ts +58 -0
- package/src/cli/tui/App.tsx +252 -0
- package/src/cli/tui/DevLive.tsx +116 -0
- package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
- package/src/cli/tui/components/HeaderBar.tsx +67 -0
- package/src/cli/tui/components/LogStream.tsx +47 -0
- package/src/cli/tui/components/PanelCard.tsx +39 -0
- package/src/cli/tui/components/SelectRow.tsx +37 -0
- package/src/cli/tui/components/SlashPalette.tsx +172 -0
- package/src/cli/tui/components/Spinner.tsx +32 -0
- package/src/cli/tui/components/StatusBar.tsx +51 -0
- package/src/cli/tui/components/StatusDot.tsx +37 -0
- package/src/cli/tui/components/TabBar.tsx +49 -0
- package/src/cli/tui/components/Wordmark.tsx +43 -0
- package/src/cli/tui/format.ts +38 -0
- package/src/cli/tui/keys.test.ts +18 -0
- package/src/cli/tui/keys.ts +26 -0
- package/src/cli/tui/launch.test.ts +18 -0
- package/src/cli/tui/launch.ts +57 -0
- package/src/cli/tui/panels/Dashboard.tsx +201 -0
- package/src/cli/tui/panels/Database.tsx +198 -0
- package/src/cli/tui/panels/Dev.tsx +257 -0
- package/src/cli/tui/panels/Docker.tsx +357 -0
- package/src/cli/tui/panels/Navigator.test.ts +22 -0
- package/src/cli/tui/panels/Navigator.tsx +213 -0
- package/src/cli/tui/slash-catalog.test.ts +31 -0
- package/src/cli/tui/slash-catalog.ts +184 -0
- package/src/cli/tui/slash-run.ts +54 -0
- package/src/cli/tui/state/ports.ts +95 -0
- package/src/cli/tui/theme.ts +24 -0
- package/src/cli/vault-cmd.test.ts +402 -0
- package/src/cli/vault-cmd.ts +727 -69
- package/src/cli/vault-secure-input.test.ts +56 -0
- package/src/cli/vault-secure-input.ts +124 -0
- package/src/compiler/effects-infer.ts +7 -0
- package/src/compiler/extract.ts +12 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/generate-adopt.ts +27 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +115 -0
- package/src/config/driver-defaults.ts +156 -0
- package/src/config/index.ts +246 -131
- package/src/config/resolve-driver.test.ts +14 -19
- package/src/console/server/app.ts +4 -2
- package/src/console/server/console.test.ts +58 -1
- package/src/console/server/flows.ts +22 -2
- package/src/console/server/index.ts +3 -0
- package/src/console/server/operator-db.test.ts +55 -13
- package/src/console/server/operator-db.ts +305 -263
- package/src/console/server/serve.ts +16 -7
- package/src/console/server/state.ts +31 -9
- package/src/console/server/vault.test.ts +120 -5
- package/src/console/server/vault.ts +208 -12
- package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
- package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
- package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/shell/client.ts +16 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
- package/src/console/ui/vault/VaultA11yView.tsx +33 -1
- package/src/console/ui/vault/backend.test.ts +75 -0
- package/src/console/ui/vault/backend.ts +117 -0
- package/src/console/ui/vault/fixture.ts +21 -2
- package/src/console/ui/vault/index.ts +11 -1
- package/src/console/ui/vault/types.ts +26 -0
- package/src/docker/compose.ts +164 -61
- package/src/docker/derive.ts +106 -12
- package/src/docker/docker.test.ts +156 -110
- package/src/docker/helpers.ts +98 -0
- package/src/docker/images-config.test.ts +163 -0
- package/src/docker/index.ts +30 -5
- package/src/docker/recipes/index.ts +0 -3
- package/src/docker/recipes/llama-cpp.ts +4 -4
- package/src/docker/recipes/pgdog.ts +9 -2
- package/src/docker/resources.test.ts +71 -0
- package/src/docker/resources.ts +176 -0
- package/src/docker/stack-id.test.ts +11 -4
- package/src/docker/stack-id.ts +42 -32
- package/src/docker/stack.integration.test.ts +4 -15
- package/src/docker/types.ts +35 -3
- package/src/drivers/ai-ollama.integration.test.ts +2 -2
- package/src/drivers/conformance.test.ts +11 -12
- package/src/drivers/drizzle-dialect.test.ts +0 -4
- package/src/drivers/drizzle-dialect.ts +7 -13
- package/src/drivers/index.ts +24 -7
- package/src/drivers/journal-postgres.test.ts +3 -1
- package/src/drivers/journal-postgres.ts +4 -2
- package/src/drivers/meilisearch.ts +1 -1
- package/src/drivers/pglite.ts +28 -5
- package/src/drivers/types.ts +9 -11
- package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
- package/src/drivers/vault-aws-secrets-manager.ts +333 -0
- package/src/drivers/vault-builtin.test.ts +110 -0
- package/src/drivers/vault-builtin.ts +184 -0
- package/src/drivers/vault-driver-removal.test.ts +13 -0
- package/src/drivers/vault-managed.test.ts +95 -0
- package/src/drivers/vault-managed.ts +101 -25
- package/src/drivers/vault-types.ts +25 -5
- package/src/elements/ai/runtime.ts +1 -1
- package/src/elements/clock/chaos.test.ts +4 -1
- package/src/elements/clock/durable.ts +57 -4
- package/src/elements/clock.test.ts +13 -14
- package/src/elements/gate/constants.ts +18 -0
- package/src/elements/gate.test.ts +1 -1
- package/src/elements/gate.ts +2 -2
- package/src/elements/store/domain-ddl.test.ts +5 -12
- package/src/elements/store/emit-drizzle.ts +44 -19
- package/src/elements/store/index-boot.test.ts +15 -116
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/resource-list-docs.fixture.ts +12 -6
- package/src/elements/store/resource-list-docs.test.ts +64 -44
- package/src/elements/store/resource.test.ts +17 -10
- package/src/elements/store/runtime.ts +9 -12
- package/src/elements/store/schema-decl.test.ts +16 -32
- package/src/elements/store/seed.test.ts +1 -2
- package/src/elements/store/seed.ts +7 -8
- package/src/elements/store/sql-condition.test.ts +2 -2
- package/src/elements/store/sql-session.test.ts +8 -10
- package/src/elements/store/sql-session.ts +10 -2
- package/src/elements/store.test.ts +13 -13
- package/src/elements/vault/audit.ts +233 -0
- package/src/elements/vault/boot-chain.test.ts +78 -0
- package/src/elements/vault/boot-chain.ts +43 -39
- package/src/elements/vault/builtin-adapter.test.ts +479 -0
- package/src/elements/vault/builtin-adapter.ts +1184 -0
- package/src/elements/vault/crypto.test.ts +237 -0
- package/src/elements/vault/crypto.ts +431 -0
- package/src/elements/vault/declare.ts +164 -3
- package/src/elements/vault/errors.ts +56 -0
- package/src/elements/vault/kms-unseal.test.ts +76 -0
- package/src/elements/vault/kms-unseal.ts +181 -0
- package/src/elements/vault/path.ts +90 -0
- package/src/elements/vault/resilience.test.ts +102 -0
- package/src/elements/vault/resilience.ts +107 -0
- package/src/elements/vault/runtime.ts +36 -4
- package/src/elements/vault/security-checklist.test.ts +130 -0
- package/src/elements/vault/storage.ts +448 -0
- package/src/elements/vault/types.ts +288 -0
- package/src/elements/vault/unseal.ts +150 -0
- package/src/elements/vault.test.ts +72 -4
- package/src/elements/vault.ts +91 -2
- package/src/full.ts +248 -0
- package/src/http.ts +41 -0
- package/src/i18n/interpolate-simple.ts +37 -0
- package/src/i18n/messages.ts +51 -4
- package/src/i18n-entry.ts +7 -0
- package/src/index.ts +35 -145
- package/src/journal-entry.ts +20 -0
- package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
- package/src/kernel/app.ts +40 -42
- package/src/kernel/auto-registry.test.ts +2 -2
- package/src/kernel/boot-bind/ai.test.ts +5 -5
- package/src/kernel/boot-bind/ai.ts +2 -1
- package/src/kernel/boot-bind/channel.test.ts +6 -6
- package/src/kernel/boot-bind/channel.ts +5 -4
- package/src/kernel/boot-bind/clock.ts +5 -5
- package/src/kernel/boot-bind/gate.ts +6 -7
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +5 -5
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +55 -37
- package/src/kernel/boot-bind/store.ts +24 -34
- package/src/kernel/boot-bind/vault.ts +19 -4
- package/src/kernel/boot.test.ts +12 -4
- package/src/kernel/boot.ts +31 -19
- package/src/kernel/budget-entry.ts +1 -1
- package/src/kernel/compensate.test.ts +288 -1
- package/src/kernel/compensate.ts +126 -0
- package/src/kernel/concurrency.test.ts +10 -9
- package/src/kernel/effects-stamping.test.ts +27 -2
- package/src/kernel/element-registries.ts +5 -3
- package/src/kernel/flow.ts +4 -2
- package/src/kernel/fx.test.ts +119 -3
- package/src/kernel/fx.ts +215 -40
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +130 -7
- package/src/kernel/pipeline.test.ts +20 -6
- package/src/kernel/redacted.test.ts +35 -0
- package/src/kernel/redacted.ts +24 -2
- package/src/kernel/router/create-default.ts +16 -0
- package/src/kernel/router/create-edge.ts +18 -0
- package/src/kernel/router/create.ts +23 -0
- package/src/kernel/router/index.ts +23 -0
- package/src/kernel/router/linear.ts +88 -0
- package/src/kernel/router/regexp.ts +188 -0
- package/src/kernel/router/smart.ts +120 -0
- package/src/kernel/router/trie.ts +108 -0
- package/src/kernel/router/types.ts +56 -0
- package/src/kernel/router.ts +18 -532
- package/src/kernel-entry.ts +43 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/plugins/config-source.test.ts +7 -7
- package/src/release/build-lib.ts +58 -0
- package/src/release/exports.test.ts +4 -2
- package/src/release/exports.ts +30 -4
- package/src/release/measure.ts +101 -22
- package/src/runs/duckdb.ts +7 -2
- package/src/shared/optional-peer.ts +41 -0
- package/src/term.test.ts +3 -44
- package/src/test/create-test-app.ts +12 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/test/reset-element-registries.ts +2 -0
- package/src/upgrade/codemods.test.ts +7 -20
- package/src/upgrade/codemods.ts +93 -60
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
- package/site/content/docs/recipes/openbao.mdx +0 -141
- package/src/cli/ask-dev-mode.ts +0 -40
- package/src/cli/dev-mode.test.ts +0 -45
- package/src/cli/dev-mode.ts +0 -78
- package/src/cli/openbao-bootstrap.test.ts +0 -150
- package/src/cli/openbao-bootstrap.ts +0 -321
- package/src/cli/openbao-restart.integration.test.ts +0 -145
- package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
- package/src/docker/recipes/openbao.ts +0 -47
- package/src/drivers/libsql.ts +0 -179
- package/src/drivers/sqlite.ts +0 -44
- package/src/drivers/vault-openbao.test.ts +0 -97
- package/src/drivers/vault-openbao.ts +0 -143
package/src/cli/vault-cmd.ts
CHANGED
|
@@ -1,43 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke vault` —
|
|
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
|
-
/**
|
|
10
|
-
|
|
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(
|
|
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
|
+
}
|