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.
- package/package.json +76 -10
- 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 -7
- package/site/content/docs/elements/channel.mdx +9 -7
- package/site/content/docs/elements/clock.mdx +8 -8
- package/site/content/docs/elements/flow.mdx +46 -47
- package/site/content/docs/elements/gate.mdx +6 -5
- package/site/content/docs/elements/signal.mdx +2 -2
- package/site/content/docs/elements/store.mdx +71 -78
- package/site/content/docs/elements/vault.mdx +89 -70
- package/site/content/docs/get-started/basic-usage.mdx +8 -8
- 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 +2 -2
- 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 +11 -4
- 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 +27 -15
- 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 +1 -1
- 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 +166 -121
- package/src/cli/dev.ts +138 -192
- 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 +55 -15
- 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 +16 -12
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +34 -28
- 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 +28 -10
- 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/fixtures/skyport/oke.config.ts +3 -3
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +16 -23
- package/src/config/driver-defaults.ts +33 -63
- package/src/config/index.ts +169 -144
- 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 +3 -8
- 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 +3 -10
- package/src/kernel/boot-bind/clock.ts +2 -2
- package/src/kernel/boot-bind/gate.ts +5 -9
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +2 -2
- package/src/kernel/boot-bind/store.test.ts +43 -44
- package/src/kernel/boot-bind/store.ts +16 -41
- package/src/kernel/boot-bind/vault.ts +16 -1
- 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/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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Secrets Manager backend for the `managed` vault driver.
|
|
3
|
+
*
|
|
4
|
+
* Reads are snapshotted at `open` (the {@link VaultBag} surface is
|
|
5
|
+
* synchronous); writes and deletes go straight to the API and are settled by
|
|
6
|
+
* `close`.
|
|
7
|
+
*
|
|
8
|
+
* `@aws-sdk/client-secrets-manager` is an optional peer, imported dynamically
|
|
9
|
+
* so a project on platform-injected secrets or another provider never pays for it.
|
|
10
|
+
* Tests inject an {@link AwsSecretsManagerClient} and skip the SDK entirely.
|
|
11
|
+
*
|
|
12
|
+
* SDK errors are never re-thrown: their messages can echo request payloads,
|
|
13
|
+
* which for this API means secret material. Every failure is translated into
|
|
14
|
+
* a {@link VaultError} with a message this module authored.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { VaultError } from "../elements/vault/errors.ts";
|
|
18
|
+
import type { VaultBag } from "./vault-types.ts";
|
|
19
|
+
|
|
20
|
+
/** Package name of the optional peer, resolved at call time. */
|
|
21
|
+
const AWS_SM_PACKAGE = "@aws-sdk/client-secrets-manager";
|
|
22
|
+
|
|
23
|
+
/** AWS error name for a secret id that does not exist. */
|
|
24
|
+
const NOT_FOUND = "ResourceNotFoundException";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Secret-name-addressed client the bag talks to.
|
|
28
|
+
*
|
|
29
|
+
* Names are bag names (`STRIPE_KEY`), never fully-qualified AWS secret ids —
|
|
30
|
+
* the prefix belongs to the client, so the bag stays prefix-agnostic.
|
|
31
|
+
*/
|
|
32
|
+
export interface AwsSecretsManagerClient {
|
|
33
|
+
/** Bag names visible to this credential under the configured prefix. */
|
|
34
|
+
list(): Promise<readonly string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Read one secret's string value.
|
|
37
|
+
*
|
|
38
|
+
* @param name - Bag name
|
|
39
|
+
*/
|
|
40
|
+
get(name: string): Promise<string | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Create or overwrite one secret.
|
|
43
|
+
*
|
|
44
|
+
* @param name - Bag name
|
|
45
|
+
* @param value - Cleartext
|
|
46
|
+
*/
|
|
47
|
+
put(name: string, value: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete one secret. A missing secret is not an error.
|
|
50
|
+
*
|
|
51
|
+
* @param name - Bag name
|
|
52
|
+
*/
|
|
53
|
+
remove(name: string): Promise<void>;
|
|
54
|
+
/** Release the underlying SDK client. */
|
|
55
|
+
close?(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options for {@link openAwsSecretsManagerBag}. */
|
|
59
|
+
export interface OpenAwsSecretsManagerOptions {
|
|
60
|
+
/** AWS region. Falls back to the SDK's own resolution chain. */
|
|
61
|
+
readonly region?: string;
|
|
62
|
+
/** Secret-name prefix scoping this app (`oke/prod/`). */
|
|
63
|
+
readonly prefix?: string;
|
|
64
|
+
/** Injected client for tests — skips the SDK import entirely. */
|
|
65
|
+
readonly client?: AwsSecretsManagerClient;
|
|
66
|
+
/**
|
|
67
|
+
* Declared names, used as the read set when `ListSecrets` is denied so a
|
|
68
|
+
* least-privilege credential still boots.
|
|
69
|
+
*/
|
|
70
|
+
readonly secrets?: Readonly<Record<string, string>>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Open a secret bag backed by AWS Secrets Manager.
|
|
75
|
+
*
|
|
76
|
+
* An unreachable or unauthorized API is fatal: the bag never silently
|
|
77
|
+
* degrades to a seed-only view, because that turns a credentials outage into
|
|
78
|
+
* a "missing secret contract" message pointing at the wrong thing.
|
|
79
|
+
*
|
|
80
|
+
* @param options - Region / prefix / injected client / declared names
|
|
81
|
+
* @throws VaultError `MISSING_PEER` when the SDK is absent and no client is injected
|
|
82
|
+
* @throws VaultError `BACKEND_ERROR` when the API rejects the read
|
|
83
|
+
*/
|
|
84
|
+
export async function openAwsSecretsManagerBag(
|
|
85
|
+
options: OpenAwsSecretsManagerOptions = {},
|
|
86
|
+
): Promise<VaultBag> {
|
|
87
|
+
const client = options.client ?? (await connectSecretsManager(options.region, options.prefix));
|
|
88
|
+
const map = new Map<string, string>(
|
|
89
|
+
Object.entries(options.secrets ?? {}).filter(
|
|
90
|
+
(e): e is [string, string] => typeof e[1] === "string",
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
let listed: readonly string[] = [];
|
|
95
|
+
try {
|
|
96
|
+
listed = await client.list();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// ListSecrets is commonly denied on least-privilege roles; only fail when
|
|
99
|
+
// there is no declared read set to fall back to.
|
|
100
|
+
if (map.size === 0) throw asVaultError(error, "vault: aws secrets manager list failed");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const names = listed.length > 0 ? listed : [...map.keys()];
|
|
104
|
+
for (const name of names) {
|
|
105
|
+
let value: string | undefined;
|
|
106
|
+
try {
|
|
107
|
+
value = await client.get(name);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw asVaultError(error, `vault: aws secrets manager read of "${name}" failed`);
|
|
110
|
+
}
|
|
111
|
+
if (value !== undefined) map.set(name, value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pending: Promise<void>[] = [];
|
|
115
|
+
let failure: VaultError | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Run a write in the background, holding its failure for `close`.
|
|
118
|
+
*
|
|
119
|
+
* @param work - In-flight API call
|
|
120
|
+
* @param message - Safe message when it rejects
|
|
121
|
+
*/
|
|
122
|
+
function track(work: Promise<void>, message: string): void {
|
|
123
|
+
pending.push(
|
|
124
|
+
work.catch((error: unknown) => {
|
|
125
|
+
failure ??= asVaultError(error, message);
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
driverId: "managed",
|
|
132
|
+
get(name) {
|
|
133
|
+
return map.get(name);
|
|
134
|
+
},
|
|
135
|
+
names() {
|
|
136
|
+
return [...map.keys()];
|
|
137
|
+
},
|
|
138
|
+
set(name, value) {
|
|
139
|
+
map.set(name, value);
|
|
140
|
+
track(client.put(name, value), `vault: aws secrets manager write of "${name}" failed`);
|
|
141
|
+
},
|
|
142
|
+
delete(name) {
|
|
143
|
+
const had = map.delete(name);
|
|
144
|
+
track(client.remove(name), `vault: aws secrets manager delete of "${name}" failed`);
|
|
145
|
+
return had;
|
|
146
|
+
},
|
|
147
|
+
async close() {
|
|
148
|
+
await Promise.all(pending);
|
|
149
|
+
await client.close?.();
|
|
150
|
+
if (failure) throw failure;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Response fields this module reads off the SDK. */
|
|
156
|
+
interface SecretsManagerResult {
|
|
157
|
+
readonly SecretString?: string;
|
|
158
|
+
readonly SecretList?: readonly { readonly Name?: string }[];
|
|
159
|
+
readonly NextToken?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Structural SDK client — the SDK's `SecretsManagerClient` satisfies it. */
|
|
163
|
+
interface SdkClientLike {
|
|
164
|
+
send(command: unknown): Promise<SecretsManagerResult>;
|
|
165
|
+
destroy?(): void;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Constructors pulled off the dynamically imported module. */
|
|
169
|
+
interface SecretsManagerBindings {
|
|
170
|
+
readonly SecretsManagerClient: new (config: { region?: string }) => SdkClientLike;
|
|
171
|
+
readonly ListSecretsCommand: new (input: {
|
|
172
|
+
Filters?: { Key: string; Values: string[] }[];
|
|
173
|
+
MaxResults?: number;
|
|
174
|
+
NextToken?: string;
|
|
175
|
+
}) => unknown;
|
|
176
|
+
readonly GetSecretValueCommand: new (input: { SecretId: string }) => unknown;
|
|
177
|
+
readonly PutSecretValueCommand: new (input: {
|
|
178
|
+
SecretId: string;
|
|
179
|
+
SecretString: string;
|
|
180
|
+
}) => unknown;
|
|
181
|
+
readonly CreateSecretCommand: new (input: { Name: string; SecretString: string }) => unknown;
|
|
182
|
+
readonly DeleteSecretCommand: new (input: {
|
|
183
|
+
SecretId: string;
|
|
184
|
+
ForceDeleteWithoutRecovery: boolean;
|
|
185
|
+
}) => unknown;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build a real client over the optional AWS SDK peer.
|
|
190
|
+
*
|
|
191
|
+
* @param region - AWS region
|
|
192
|
+
* @param prefix - Secret-name prefix scoping this app
|
|
193
|
+
* @throws VaultError `MISSING_PEER` when the SDK is absent
|
|
194
|
+
*/
|
|
195
|
+
async function connectSecretsManager(
|
|
196
|
+
region: string | undefined,
|
|
197
|
+
prefix: string | undefined,
|
|
198
|
+
): Promise<AwsSecretsManagerClient> {
|
|
199
|
+
const sdk = await loadSecretsManager();
|
|
200
|
+
const at = prefix ?? "";
|
|
201
|
+
const client = new sdk.SecretsManagerClient(region === undefined ? {} : { region });
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
async list() {
|
|
205
|
+
const names: string[] = [];
|
|
206
|
+
let token: string | undefined;
|
|
207
|
+
do {
|
|
208
|
+
const page: SecretsManagerResult = await client.send(
|
|
209
|
+
new sdk.ListSecretsCommand({
|
|
210
|
+
...(at === "" ? {} : { Filters: [{ Key: "name", Values: [at] }] }),
|
|
211
|
+
MaxResults: 100,
|
|
212
|
+
...(token === undefined ? {} : { NextToken: token }),
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
for (const entry of page.SecretList ?? []) {
|
|
216
|
+
const id = entry.Name;
|
|
217
|
+
if (id === undefined || !id.startsWith(at)) continue;
|
|
218
|
+
names.push(id.slice(at.length));
|
|
219
|
+
}
|
|
220
|
+
token = page.NextToken;
|
|
221
|
+
} while (token !== undefined);
|
|
222
|
+
return names;
|
|
223
|
+
},
|
|
224
|
+
async get(name) {
|
|
225
|
+
try {
|
|
226
|
+
const result = await client.send(
|
|
227
|
+
new sdk.GetSecretValueCommand({ SecretId: `${at}${name}` }),
|
|
228
|
+
);
|
|
229
|
+
return result.SecretString;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (errorName(error) === NOT_FOUND) return undefined;
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
async put(name, value) {
|
|
236
|
+
try {
|
|
237
|
+
await client.send(
|
|
238
|
+
new sdk.PutSecretValueCommand({ SecretId: `${at}${name}`, SecretString: value }),
|
|
239
|
+
);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (errorName(error) !== NOT_FOUND) throw error;
|
|
242
|
+
await client.send(
|
|
243
|
+
new sdk.CreateSecretCommand({ Name: `${at}${name}`, SecretString: value }),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
async remove(name) {
|
|
248
|
+
try {
|
|
249
|
+
await client.send(
|
|
250
|
+
new sdk.DeleteSecretCommand({
|
|
251
|
+
SecretId: `${at}${name}`,
|
|
252
|
+
ForceDeleteWithoutRecovery: true,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (errorName(error) !== NOT_FOUND) throw error;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async close() {
|
|
260
|
+
client.destroy?.();
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Import the optional `@aws-sdk/client-secrets-manager` peer.
|
|
267
|
+
*
|
|
268
|
+
* @throws VaultError `MISSING_PEER` with the exact install command
|
|
269
|
+
*/
|
|
270
|
+
async function loadSecretsManager(): Promise<SecretsManagerBindings> {
|
|
271
|
+
// Indirect specifier: the peer is optional, so it must not become a
|
|
272
|
+
// static dependency of the module graph.
|
|
273
|
+
const specifier: string = AWS_SM_PACKAGE;
|
|
274
|
+
let mod: unknown;
|
|
275
|
+
try {
|
|
276
|
+
mod = await import(specifier);
|
|
277
|
+
} catch {
|
|
278
|
+
throw new VaultError(
|
|
279
|
+
"MISSING_PEER",
|
|
280
|
+
`vault: managed provider "aws-secrets-manager" needs the optional peer \`${AWS_SM_PACKAGE}\` (bun add ${AWS_SM_PACKAGE})`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const bindings = mod as Partial<SecretsManagerBindings>;
|
|
284
|
+
if (
|
|
285
|
+
!bindings.SecretsManagerClient ||
|
|
286
|
+
!bindings.ListSecretsCommand ||
|
|
287
|
+
!bindings.GetSecretValueCommand ||
|
|
288
|
+
!bindings.PutSecretValueCommand ||
|
|
289
|
+
!bindings.CreateSecretCommand ||
|
|
290
|
+
!bindings.DeleteSecretCommand
|
|
291
|
+
) {
|
|
292
|
+
throw new VaultError(
|
|
293
|
+
"MISSING_PEER",
|
|
294
|
+
`vault: \`${AWS_SM_PACKAGE}\` is installed but does not export the SecretsManager client and commands`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
SecretsManagerClient: bindings.SecretsManagerClient,
|
|
299
|
+
ListSecretsCommand: bindings.ListSecretsCommand,
|
|
300
|
+
GetSecretValueCommand: bindings.GetSecretValueCommand,
|
|
301
|
+
PutSecretValueCommand: bindings.PutSecretValueCommand,
|
|
302
|
+
CreateSecretCommand: bindings.CreateSecretCommand,
|
|
303
|
+
DeleteSecretCommand: bindings.DeleteSecretCommand,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* AWS error discriminator (`ResourceNotFoundException`, …).
|
|
309
|
+
*
|
|
310
|
+
* Reads only `name`, never `message`: the latter can quote request payloads.
|
|
311
|
+
*
|
|
312
|
+
* @param error - Thrown value
|
|
313
|
+
*/
|
|
314
|
+
function errorName(error: unknown): string | undefined {
|
|
315
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
316
|
+
const name = (error as { name?: unknown }).name;
|
|
317
|
+
return typeof name === "string" ? name : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Translate a foreign failure into a secret-free {@link VaultError}.
|
|
322
|
+
*
|
|
323
|
+
* @param error - Thrown value
|
|
324
|
+
* @param message - Safe message authored by this module
|
|
325
|
+
*/
|
|
326
|
+
function asVaultError(error: unknown, message: string): VaultError {
|
|
327
|
+
if (error instanceof VaultError) return error;
|
|
328
|
+
const name = errorName(error);
|
|
329
|
+
if (name === "AccessDeniedException" || name === "UnrecognizedClientException") {
|
|
330
|
+
return new VaultError("PERMISSION_DENIED", `${message} — credentials denied`);
|
|
331
|
+
}
|
|
332
|
+
return new VaultError("BACKEND_ERROR", message);
|
|
333
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vault` driver — adapter wiring and the never-fail-boot degradation path.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { connectPglite } from "./pglite.ts";
|
|
10
|
+
import { builtinVaultDriver, openBuiltinVaultAdapter, VAULT_BAG_ADAPTER } from "./vault-builtin.ts";
|
|
11
|
+
import type { BuiltinVaultAdapter } from "../elements/vault/builtin-adapter.ts";
|
|
12
|
+
|
|
13
|
+
describe("builtin vault driver", () => {
|
|
14
|
+
test("openBuiltinVaultAdapter unseals an injected connection", async () => {
|
|
15
|
+
const connection = await connectPglite({ url: "memory://vault-driver-test" });
|
|
16
|
+
try {
|
|
17
|
+
const first = await openBuiltinVaultAdapter({ connection, env: {} });
|
|
18
|
+
const init = await first.adapter.initialize();
|
|
19
|
+
await first.adapter.unseal(init.masterKey);
|
|
20
|
+
await first.adapter.set("prod/api/stripe", "sk_live_x");
|
|
21
|
+
|
|
22
|
+
// A second open over the same connection unseals from the master key.
|
|
23
|
+
const second = await openBuiltinVaultAdapter({
|
|
24
|
+
connection,
|
|
25
|
+
env: { OKE_VAULT_MASTER_KEY: init.masterKey },
|
|
26
|
+
});
|
|
27
|
+
expect(second.adapter.getUnsealer()).not.toBeNull();
|
|
28
|
+
expect((await second.adapter.get("prod/api/stripe"))?.value).toBe("sk_live_x");
|
|
29
|
+
} finally {
|
|
30
|
+
await connection.close();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("open with no SQL configured degrades to the seed bag", async () => {
|
|
35
|
+
const bag = await builtinVaultDriver.open({
|
|
36
|
+
env: {},
|
|
37
|
+
secrets: { APP_NAME: "demo" },
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
expect(bag.driverId).toBe("vault");
|
|
41
|
+
expect(bag.get("APP_NAME")).toBe("demo");
|
|
42
|
+
expect(bag.get("MISSING")).toBeUndefined();
|
|
43
|
+
expect(bag.names()).toEqual(["APP_NAME"]);
|
|
44
|
+
} finally {
|
|
45
|
+
await bag.close?.();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("bag close auto-seals the adapter (SIGTERM → bootResult.close path)", async () => {
|
|
50
|
+
const connection = await connectPglite({ url: "memory://vault-autoseal" });
|
|
51
|
+
try {
|
|
52
|
+
const staging = await openBuiltinVaultAdapter({ connection, env: {} });
|
|
53
|
+
const init = await staging.adapter.initialize();
|
|
54
|
+
await staging.adapter.unseal(init.masterKey);
|
|
55
|
+
await staging.adapter.set("prod/api/stripe", "sk_live_z");
|
|
56
|
+
await staging.adapter.seal();
|
|
57
|
+
|
|
58
|
+
const bag = await builtinVaultDriver.open({
|
|
59
|
+
connection,
|
|
60
|
+
env: { OKE_VAULT_MASTER_KEY: init.masterKey },
|
|
61
|
+
});
|
|
62
|
+
const held = (bag as unknown as Record<symbol, BuiltinVaultAdapter | undefined>)[
|
|
63
|
+
VAULT_BAG_ADAPTER
|
|
64
|
+
];
|
|
65
|
+
expect(held).toBeDefined();
|
|
66
|
+
expect(held!.getUnsealer()).not.toBeNull();
|
|
67
|
+
expect(bag.get("prod/api/stripe")).toBe("sk_live_z");
|
|
68
|
+
|
|
69
|
+
await bag.close?.();
|
|
70
|
+
expect(held!.getUnsealer()).toBeNull();
|
|
71
|
+
await expect(held!.get("prod/api/stripe")).rejects.toThrow(/sealed/i);
|
|
72
|
+
} finally {
|
|
73
|
+
await connection.close();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("open snapshots live secrets from an initialized vault", async () => {
|
|
78
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-vault-driver-"));
|
|
79
|
+
try {
|
|
80
|
+
const staging = await connectPglite({ url: dir });
|
|
81
|
+
const opened = await openBuiltinVaultAdapter({ connection: staging, env: {} });
|
|
82
|
+
const init = await opened.adapter.initialize();
|
|
83
|
+
await opened.adapter.unseal(init.masterKey);
|
|
84
|
+
await opened.adapter.set("STRIPE_KEY", "sk_live_y");
|
|
85
|
+
await staging.close();
|
|
86
|
+
|
|
87
|
+
const bag = await builtinVaultDriver.open({
|
|
88
|
+
url: dir,
|
|
89
|
+
env: { OKE_VAULT_MASTER_KEY: init.masterKey },
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
expect(bag.names()).toEqual(["STRIPE_KEY"]);
|
|
93
|
+
expect(bag.get("STRIPE_KEY")).toBe("sk_live_y");
|
|
94
|
+
} finally {
|
|
95
|
+
await bag.close?.();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Without the master key the vault stays sealed and the bag is empty —
|
|
99
|
+
// boot then reports ordinary gaps instead of a driver failure.
|
|
100
|
+
const sealed = await builtinVaultDriver.open({ url: dir, env: {} });
|
|
101
|
+
try {
|
|
102
|
+
expect(sealed.names()).toEqual([]);
|
|
103
|
+
} finally {
|
|
104
|
+
await sealed.close?.();
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
await rm(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vault` driver — okengine's own encrypted-at-rest secret store.
|
|
3
|
+
*
|
|
4
|
+
* The built-in Vault is asynchronous and path-addressed
|
|
5
|
+
* ({@link BuiltinVaultAdapter}), while the boot resolution chain wants a
|
|
6
|
+
* synchronous name→value {@link VaultBag}. This driver bridges the two: at
|
|
7
|
+
* `open` it unseals with `OKE_VAULT_MASTER_KEY`, snapshots every live secret
|
|
8
|
+
* into a bag, and keeps the adapter around for write-through.
|
|
9
|
+
*
|
|
10
|
+
* Boot must never die because the operator has not run `oke vault init` yet,
|
|
11
|
+
* so an uninitialized, sealed, or unreachable backend degrades to the seed
|
|
12
|
+
* bag — the Vault runtime then reports the missing contracts as ordinary
|
|
13
|
+
* gaps, with the same message every other driver produces.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createBuiltinVaultAdapter,
|
|
18
|
+
sqlConnectionAsExec,
|
|
19
|
+
type BuiltinVaultAdapter,
|
|
20
|
+
} from "../elements/vault/builtin-adapter.ts";
|
|
21
|
+
import type { SqlConnection } from "./types.ts";
|
|
22
|
+
import type { VaultBag, VaultDriver, VaultOpenOptions } from "./vault-types.ts";
|
|
23
|
+
|
|
24
|
+
/** Environment variable holding the base64 master key. */
|
|
25
|
+
export const VAULT_MASTER_KEY_ENV = "OKE_VAULT_MASTER_KEY";
|
|
26
|
+
|
|
27
|
+
/** Test-only handle to the adapter a bag is sealing on {@link VaultBag.close}. */
|
|
28
|
+
export const VAULT_BAG_ADAPTER = Symbol.for("oke.vault.bagAdapter");
|
|
29
|
+
|
|
30
|
+
/** Options for {@link openBuiltinVaultAdapter}. */
|
|
31
|
+
export interface OpenBuiltinVaultOptions {
|
|
32
|
+
/** Postgres URL. Falls back to `DATABASE_URL` / `OKE_STORE_SQL_URL`, then PGlite. */
|
|
33
|
+
readonly url?: string;
|
|
34
|
+
/** Pre-opened SQL connection — wins over `url`. */
|
|
35
|
+
readonly connection?: SqlConnection;
|
|
36
|
+
/** Base64 master key to unseal with. Defaults to `OKE_VAULT_MASTER_KEY`. */
|
|
37
|
+
readonly masterKey?: string;
|
|
38
|
+
/** Injected env map for tests. */
|
|
39
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
40
|
+
/** DEKs re-wrapped per master-rotation batch. */
|
|
41
|
+
readonly kekRewrapBatchSize?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** An adapter plus the connection this module opened for it, if any. */
|
|
45
|
+
export interface OpenedBuiltinVault {
|
|
46
|
+
/** The built-in adapter, unsealed when a valid master key was available. */
|
|
47
|
+
readonly adapter: BuiltinVaultAdapter;
|
|
48
|
+
/** Close the underlying connection when this module opened it. */
|
|
49
|
+
close(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Open the built-in Vault adapter over SQL, unsealing when a master key is
|
|
54
|
+
* available.
|
|
55
|
+
*
|
|
56
|
+
* Used by the CLI (`oke vault …`) and Console, which need the full
|
|
57
|
+
* path-addressed surface rather than a flat bag.
|
|
58
|
+
*
|
|
59
|
+
* @param options - URL / connection / master key / batch size
|
|
60
|
+
*/
|
|
61
|
+
export async function openBuiltinVaultAdapter(
|
|
62
|
+
options: OpenBuiltinVaultOptions = {},
|
|
63
|
+
): Promise<OpenedBuiltinVault> {
|
|
64
|
+
const env = options.env ?? process.env;
|
|
65
|
+
const url = options.url ?? env.DATABASE_URL ?? env.OKE_STORE_SQL_URL;
|
|
66
|
+
|
|
67
|
+
let owned: SqlConnection | undefined;
|
|
68
|
+
let connection = options.connection;
|
|
69
|
+
if (!connection) {
|
|
70
|
+
owned = await connectVaultSql(url);
|
|
71
|
+
connection = owned;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const adapter = createBuiltinVaultAdapter({
|
|
75
|
+
db: sqlConnectionAsExec(connection),
|
|
76
|
+
...(options.kekRewrapBatchSize === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { kekRewrapBatchSize: options.kekRewrapBatchSize }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const masterKey = options.masterKey ?? env[VAULT_MASTER_KEY_ENV];
|
|
82
|
+
if (masterKey !== undefined && masterKey.trim().length > 0) {
|
|
83
|
+
await adapter.unseal(masterKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
adapter,
|
|
88
|
+
async close() {
|
|
89
|
+
await owned?.close();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Built-in Vault driver. Reads resolve from a snapshot taken at `open`.
|
|
96
|
+
*/
|
|
97
|
+
export const builtinVaultDriver: VaultDriver = {
|
|
98
|
+
id: "vault",
|
|
99
|
+
async open(options: VaultOpenOptions = {}): Promise<VaultBag> {
|
|
100
|
+
const map = new Map<string, string>(
|
|
101
|
+
Object.entries(options.secrets ?? {}).filter(
|
|
102
|
+
(e): e is [string, string] => typeof e[1] === "string",
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const env = options.env ?? process.env;
|
|
107
|
+
const url = options.url ?? env.DATABASE_URL ?? env.OKE_STORE_SQL_URL;
|
|
108
|
+
|
|
109
|
+
let opened: OpenedBuiltinVault | undefined;
|
|
110
|
+
// With no SQL configured there is nothing to snapshot, and spinning up a
|
|
111
|
+
// throwaway PGlite instance would cost every boot ~1s for an empty bag.
|
|
112
|
+
if (options.connection !== undefined || url !== undefined) {
|
|
113
|
+
try {
|
|
114
|
+
opened = await openBuiltinVaultAdapter({
|
|
115
|
+
env,
|
|
116
|
+
...(options.connection !== undefined ? { connection: options.connection } : { url }),
|
|
117
|
+
});
|
|
118
|
+
if (opened.adapter.getUnsealer() !== null) {
|
|
119
|
+
for (const entry of await opened.adapter.list()) {
|
|
120
|
+
const secret = await opened.adapter.get(entry.path);
|
|
121
|
+
if (secret) map.set(entry.path, secret.value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Uninitialized / sealed / unreachable: fall back to the seed bag so
|
|
126
|
+
// boot reports missing contracts instead of a driver stack trace.
|
|
127
|
+
await opened?.close().catch(() => undefined);
|
|
128
|
+
opened = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const adapter = opened?.adapter;
|
|
133
|
+
return {
|
|
134
|
+
driverId: "vault",
|
|
135
|
+
...(adapter === undefined ? {} : { [VAULT_BAG_ADAPTER]: adapter }),
|
|
136
|
+
get(name) {
|
|
137
|
+
return map.get(name);
|
|
138
|
+
},
|
|
139
|
+
names() {
|
|
140
|
+
return [...map.keys()];
|
|
141
|
+
},
|
|
142
|
+
set(name, value) {
|
|
143
|
+
map.set(name, value);
|
|
144
|
+
if (adapter && adapter.getUnsealer() !== null) {
|
|
145
|
+
void adapter.set(name, value).catch(() => undefined);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
delete(name) {
|
|
149
|
+
const had = map.delete(name);
|
|
150
|
+
if (adapter && adapter.getUnsealer() !== null) {
|
|
151
|
+
void adapter.delete(name).catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
return had;
|
|
154
|
+
},
|
|
155
|
+
async close() {
|
|
156
|
+
// Auto-seal on bag close (app stop / SIGTERM → bootResult.close → vault.close).
|
|
157
|
+
// Clears the in-memory master key before the SQL connection drops.
|
|
158
|
+
try {
|
|
159
|
+
await adapter?.seal();
|
|
160
|
+
} catch {
|
|
161
|
+
// Already sealed / never unsealed — ignore.
|
|
162
|
+
}
|
|
163
|
+
await opened?.close();
|
|
164
|
+
},
|
|
165
|
+
} as VaultBag;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Open the SQL connection backing the built-in Vault.
|
|
171
|
+
*
|
|
172
|
+
* A real Postgres URL uses the `postgres` driver; anything else (including
|
|
173
|
+
* no URL at all) uses PGlite so local dev and tests work with no services.
|
|
174
|
+
*
|
|
175
|
+
* @param url - Resolved SQL URL, when configured
|
|
176
|
+
*/
|
|
177
|
+
async function connectVaultSql(url: string | undefined): Promise<SqlConnection> {
|
|
178
|
+
if (url !== undefined && /^postgres(ql)?:\/\//.test(url)) {
|
|
179
|
+
const { connectPostgres } = await import("./postgres.ts");
|
|
180
|
+
return connectPostgres({ url });
|
|
181
|
+
}
|
|
182
|
+
const { connectPglite } = await import("./pglite.ts");
|
|
183
|
+
return connectPglite(url === undefined ? {} : { url });
|
|
184
|
+
}
|
|
@@ -16,6 +16,10 @@ const FORBIDDEN_DRIVER = ["so", "ps"].join("");
|
|
|
16
16
|
const FORBIDDEN_SECRETS_SAAS = ["infi", "sical"].join("");
|
|
17
17
|
/** Forbidden optional peer, split so this file does not match itself. */
|
|
18
18
|
const FORBIDDEN_AGE = ["age", "-", "encryption"].join("");
|
|
19
|
+
/** Forbidden external vault driver id, split so this file does not match itself. */
|
|
20
|
+
const FORBIDDEN_EXT_VAULT = ["open", "bao"].join("");
|
|
21
|
+
/** Forbidden vault-provider alias, split so this file does not match itself. */
|
|
22
|
+
const FORBIDDEN_VAULT_ALIAS = ["hashi", "corp"].join("");
|
|
19
23
|
|
|
20
24
|
/** Lockfiles regenerated by `bun install` — not source residue. */
|
|
21
25
|
const LOCKFILE_PREFIXES = [
|
|
@@ -62,4 +66,13 @@ describe("removed vault driver gate", () => {
|
|
|
62
66
|
test("tracked tree has zero matches for the removed age peer outside lockfiles", () => {
|
|
63
67
|
assertZeroGitGrep(FORBIDDEN_AGE, LOCKFILE_PREFIXES);
|
|
64
68
|
});
|
|
69
|
+
|
|
70
|
+
test("tracked tree has zero matches for the removed external vault driver", () => {
|
|
71
|
+
// Community-plugin notes in changelog / site docs may mention the name.
|
|
72
|
+
assertZeroGitGrep(FORBIDDEN_EXT_VAULT, ["changelog.md:", "site/content/docs/"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("tracked tree has zero matches for the removed vault alias outside docs notes", () => {
|
|
76
|
+
assertZeroGitGrep(FORBIDDEN_VAULT_ALIAS, ["changelog.md:", "site/content/docs/"]);
|
|
77
|
+
});
|
|
65
78
|
});
|