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
|
@@ -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
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed vault driver — provider selection, deferred providers, delegation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { isVaultError } from "../elements/vault/errors.ts";
|
|
7
|
+
import { createManagedVaultBag, managedVaultDriver } from "./vault-managed.ts";
|
|
8
|
+
import type { AwsSecretsManagerClient } from "./vault-aws-secrets-manager.ts";
|
|
9
|
+
|
|
10
|
+
/** In-memory stand-in for the AWS Secrets Manager API. */
|
|
11
|
+
function fakeAwsClient(seed: Record<string, string>): AwsSecretsManagerClient {
|
|
12
|
+
const store = new Map(Object.entries(seed));
|
|
13
|
+
return {
|
|
14
|
+
async list() {
|
|
15
|
+
return [...store.keys()];
|
|
16
|
+
},
|
|
17
|
+
async get(name) {
|
|
18
|
+
return store.get(name);
|
|
19
|
+
},
|
|
20
|
+
async put(name, value) {
|
|
21
|
+
store.set(name, value);
|
|
22
|
+
},
|
|
23
|
+
async remove(name) {
|
|
24
|
+
store.delete(name);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("managed vault driver", () => {
|
|
30
|
+
test("no provider keeps the platform-injected map", async () => {
|
|
31
|
+
const bag = await managedVaultDriver.open({
|
|
32
|
+
env: { INJECTED: "from-platform" },
|
|
33
|
+
secrets: { SEEDED: "from-seed" },
|
|
34
|
+
});
|
|
35
|
+
expect(bag.driverId).toBe("managed");
|
|
36
|
+
expect(bag.get("INJECTED")).toBe("from-platform");
|
|
37
|
+
expect(bag.get("SEEDED")).toBe("from-seed");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("aws-secrets-manager provider reads through the injected client", async () => {
|
|
41
|
+
const bag = await createManagedVaultBag({
|
|
42
|
+
provider: "aws-secrets-manager",
|
|
43
|
+
client: fakeAwsClient({ STRIPE_KEY: "sk_live" }),
|
|
44
|
+
});
|
|
45
|
+
expect(bag.driverId).toBe("managed");
|
|
46
|
+
expect(bag.get("STRIPE_KEY")).toBe("sk_live");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("provider id is case- and space-insensitive", async () => {
|
|
50
|
+
const bag = await createManagedVaultBag({
|
|
51
|
+
provider: " AWS-Secrets-Manager ",
|
|
52
|
+
client: fakeAwsClient({ A: "1" }),
|
|
53
|
+
});
|
|
54
|
+
expect(bag.get("A")).toBe("1");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("rejects removed external vault providers", async () => {
|
|
58
|
+
// Split tokens so the removal gate never matches this file.
|
|
59
|
+
const removed = [["open", "bao"].join(""), ["hashi", "corp"].join("")];
|
|
60
|
+
for (const provider of removed) {
|
|
61
|
+
let failed: unknown;
|
|
62
|
+
try {
|
|
63
|
+
await createManagedVaultBag({ provider });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
failed = error;
|
|
66
|
+
}
|
|
67
|
+
expect(isVaultError(failed, "UNSUPPORTED")).toBe(true);
|
|
68
|
+
expect((failed as Error).message).toContain(provider);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("deferred providers fail UNSUPPORTED, never silently empty", async () => {
|
|
73
|
+
for (const provider of ["azure-key-vault", "gcp-secret-manager", "doppler", "1password"]) {
|
|
74
|
+
let failed: unknown;
|
|
75
|
+
try {
|
|
76
|
+
await createManagedVaultBag({ provider });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
failed = error;
|
|
79
|
+
}
|
|
80
|
+
expect(isVaultError(failed, "UNSUPPORTED")).toBe(true);
|
|
81
|
+
expect((failed as Error).message).toContain(provider);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("unknown provider fails UNSUPPORTED with the supported list", async () => {
|
|
86
|
+
let failed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
await createManagedVaultBag({ provider: "acme-secrets" });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
failed = error;
|
|
91
|
+
}
|
|
92
|
+
expect(isVaultError(failed, "UNSUPPORTED")).toBe(true);
|
|
93
|
+
expect((failed as Error).message).toContain("aws-secrets-manager");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,38 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `managed` vault driver —
|
|
2
|
+
* `managed` vault driver — one selector over the supported secret backends.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* With no `provider` the driver keeps its original shape: values arrive as an
|
|
5
|
+
* injected map (or env), the way Fly / Railway / K8s hand them over, and OKE
|
|
6
|
+
* never invents a vendor client.
|
|
7
|
+
*
|
|
8
|
+
* With a `provider` the driver delegates, and the bag reports the backend's
|
|
9
|
+
* own protocol id — `managed` is a selector, not a protocol. Backends load
|
|
10
|
+
* dynamically so a project on one provider never pays for the others.
|
|
11
|
+
*
|
|
12
|
+
* Official provider: `aws-secrets-manager`. Community plugins can implement
|
|
13
|
+
* additional adapters; unknown ids fail loud with {@link VaultError} `UNSUPPORTED`.
|
|
5
14
|
*/
|
|
6
15
|
|
|
16
|
+
import { VaultError } from "../elements/vault/errors.ts";
|
|
17
|
+
import type { AwsSecretsManagerClient } from "./vault-aws-secrets-manager.ts";
|
|
7
18
|
import type { VaultBag, VaultDriver, VaultOpenOptions } from "./vault-types.ts";
|
|
8
19
|
|
|
20
|
+
/** Official backends `managed` ships today. */
|
|
21
|
+
export type ManagedVaultProviderId = "aws-secrets-manager";
|
|
22
|
+
|
|
23
|
+
/** Providers named in config but not implemented in core — reported, not guessed. */
|
|
24
|
+
const DEFERRED_PROVIDERS: readonly string[] = [
|
|
25
|
+
"azure-key-vault",
|
|
26
|
+
"gcp-secret-manager",
|
|
27
|
+
"doppler",
|
|
28
|
+
"1password",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Supported provider ids, for error messages. */
|
|
32
|
+
const SUPPORTED_PROVIDERS = "aws-secrets-manager";
|
|
33
|
+
|
|
34
|
+
/** Options for {@link createManagedVaultBag}. */
|
|
35
|
+
export interface ManagedVaultOptions extends VaultOpenOptions {
|
|
36
|
+
/** Injected AWS Secrets Manager client for tests. */
|
|
37
|
+
readonly client?: AwsSecretsManagerClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Open the bag for the selected managed provider.
|
|
42
|
+
*
|
|
43
|
+
* @param options - Provider id plus that provider's connection options
|
|
44
|
+
* @throws VaultError `UNSUPPORTED` for a deferred or unknown provider
|
|
45
|
+
*/
|
|
46
|
+
export async function createManagedVaultBag(options: ManagedVaultOptions = {}): Promise<VaultBag> {
|
|
47
|
+
const provider = options.provider?.trim().toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (provider === undefined || provider === "") return injectedBag(options);
|
|
50
|
+
|
|
51
|
+
if (provider === "aws-secrets-manager") {
|
|
52
|
+
const { openAwsSecretsManagerBag } = await import("./vault-aws-secrets-manager.ts");
|
|
53
|
+
return openAwsSecretsManagerBag({
|
|
54
|
+
...(options.region === undefined ? {} : { region: options.region }),
|
|
55
|
+
...(options.mount === undefined ? {} : { prefix: options.mount }),
|
|
56
|
+
...(options.client === undefined ? {} : { client: options.client }),
|
|
57
|
+
...(options.secrets === undefined ? {} : { secrets: options.secrets }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (DEFERRED_PROVIDERS.includes(provider)) {
|
|
62
|
+
throw new VaultError(
|
|
63
|
+
"UNSUPPORTED",
|
|
64
|
+
`vault: managed provider "${provider}" is not implemented yet — official: ${SUPPORTED_PROVIDERS}. ` +
|
|
65
|
+
"Community adapters can implement ManagedAdapter / open via a plugin.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new VaultError(
|
|
70
|
+
"UNSUPPORTED",
|
|
71
|
+
`vault: unknown managed provider "${provider}" (expected ${SUPPORTED_PROVIDERS}). ` +
|
|
72
|
+
'Official backends: aws-secrets-manager or built-in vault (drivers.vault: "vault").',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Platform-injected bag — env vars plus an explicit seed map.
|
|
78
|
+
*
|
|
79
|
+
* @param options - Seed / env overrides
|
|
80
|
+
*/
|
|
81
|
+
function injectedBag(options: VaultOpenOptions): VaultBag {
|
|
82
|
+
const fromSecrets = Object.entries(options.secrets ?? {}).filter(
|
|
83
|
+
(e): e is [string, string] => typeof e[1] === "string",
|
|
84
|
+
);
|
|
85
|
+
const fromEnv = Object.entries(options.env ?? process.env).filter(
|
|
86
|
+
(e): e is [string, string] => typeof e[1] === "string" && e[1].length > 0,
|
|
87
|
+
);
|
|
88
|
+
const map = new Map<string, string>([...fromEnv, ...fromSecrets]);
|
|
89
|
+
return {
|
|
90
|
+
driverId: "managed",
|
|
91
|
+
get(name) {
|
|
92
|
+
return map.get(name);
|
|
93
|
+
},
|
|
94
|
+
names() {
|
|
95
|
+
return [...map.keys()];
|
|
96
|
+
},
|
|
97
|
+
set(name, value) {
|
|
98
|
+
map.set(name, value);
|
|
99
|
+
},
|
|
100
|
+
delete(name) {
|
|
101
|
+
return map.delete(name);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
9
106
|
/**
|
|
10
107
|
* Managed / platform vault driver.
|
|
11
108
|
*/
|
|
12
109
|
export const managedVaultDriver: VaultDriver = {
|
|
13
110
|
id: "managed",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
(e): e is [string, string] => typeof e[1] === "string",
|
|
17
|
-
);
|
|
18
|
-
const fromEnv = Object.entries(options.env ?? process.env).filter(
|
|
19
|
-
(e): e is [string, string] => typeof e[1] === "string" && e[1].length > 0,
|
|
20
|
-
);
|
|
21
|
-
const map = new Map<string, string>([...fromEnv, ...fromSecrets]);
|
|
22
|
-
return {
|
|
23
|
-
driverId: "managed",
|
|
24
|
-
get(name) {
|
|
25
|
-
return map.get(name);
|
|
26
|
-
},
|
|
27
|
-
names() {
|
|
28
|
-
return [...map.keys()];
|
|
29
|
-
},
|
|
30
|
-
set(name, value) {
|
|
31
|
-
map.set(name, value);
|
|
32
|
-
},
|
|
33
|
-
delete(name) {
|
|
34
|
-
return map.delete(name);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
111
|
+
open(options: ManagedVaultOptions = {}): Promise<VaultBag> {
|
|
112
|
+
return createManagedVaultBag(options);
|
|
37
113
|
},
|
|
38
114
|
};
|
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protocol-named vault driver contracts.
|
|
3
3
|
*
|
|
4
|
-
* Driver ids: `env` · `
|
|
4
|
+
* Driver ids: `env` · `vault` · `managed` · `memory`.
|
|
5
5
|
* Never vendor names (`neon`, …) — vendor choice lives in `images`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Protocol ids for vault drivers.
|
|
10
|
+
*
|
|
11
|
+
* `vault` is okengine's own encrypted-at-rest store; `managed` selects a
|
|
12
|
+
* remote provider (official: `aws-secrets-manager`) or a platform-injected bag.
|
|
13
|
+
*/
|
|
14
|
+
export type VaultDriverId = "env" | "vault" | "managed" | "memory";
|
|
10
15
|
|
|
11
16
|
/** Options when opening a vault backend. */
|
|
12
17
|
export interface VaultOpenOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Pre-opened SQL connection for the built-in `vault` driver (tests / CLI).
|
|
20
|
+
* Wins over {@link url} when set.
|
|
21
|
+
*/
|
|
22
|
+
readonly connection?: import("./types.ts").SqlConnection;
|
|
13
23
|
/** Path to a dotenv file (`env` driver). */
|
|
14
24
|
readonly path?: string;
|
|
15
25
|
/** Env prefix filter for the `env` driver (default: none — whole `process.env`). */
|
|
16
26
|
readonly envPrefix?: string;
|
|
17
27
|
/** Injected env map for tests (defaults to `process.env`). */
|
|
18
28
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
19
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Backend behind the `managed` driver — official: `aws-secrets-manager`.
|
|
31
|
+
* Omit for platform-injected secrets. Community adapters may accept other ids.
|
|
32
|
+
*/
|
|
33
|
+
readonly provider?: string;
|
|
34
|
+
/** Base URL for remote managed providers (community adapters). */
|
|
20
35
|
readonly url?: string;
|
|
21
36
|
/** Token / API key for remote vaults. */
|
|
22
37
|
readonly token?: string;
|
|
23
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Mount / project path for remote vaults. Doubles as the secret-name prefix
|
|
40
|
+
* for `aws-secrets-manager` (`oke/prod/` → `oke/prod/STRIPE_KEY`).
|
|
41
|
+
*/
|
|
24
42
|
readonly mount?: string;
|
|
43
|
+
/** Cloud region for regional managed providers. */
|
|
44
|
+
readonly region?: string;
|
|
25
45
|
/** Seed map for `memory` / `managed` drivers. */
|
|
26
46
|
readonly secrets?: Readonly<Record<string, string>>;
|
|
27
47
|
/** HTTP fetch override for remote drivers (tests). */
|
|
@@ -717,7 +717,7 @@ export function createAiRuntime(options: CreateAiRuntimeOptions = {}): AiRuntime
|
|
|
717
717
|
}
|
|
718
718
|
if (index.driverId === "meilisearch") {
|
|
719
719
|
throw new Error(
|
|
720
|
-
`ai: embed into "${decl.into}" needs a vector index (memory/pgvector
|
|
720
|
+
`ai: embed into "${decl.into}" needs a vector index (memory/pgvector) — ` +
|
|
721
721
|
`"${index.driverId}" is full-text; embeddings don't apply`,
|
|
722
722
|
);
|
|
723
723
|
}
|
|
@@ -87,7 +87,10 @@ describe("chaos — multi-process leader election", () => {
|
|
|
87
87
|
const markerPath = join(dir, "held.json");
|
|
88
88
|
const fireLogPath = join(dir, "fires.jsonl");
|
|
89
89
|
const leaseMs = 120;
|
|
90
|
-
|
|
90
|
+
// Shared-runner / loaded laptops routinely miss a 50ms tick; keep this a
|
|
91
|
+
// loose upper bound (not a product SLA) so SIGKILL takeover still proves
|
|
92
|
+
// lease physics without flaking on scheduling noise.
|
|
93
|
+
const tickSlackMs = 500;
|
|
91
94
|
try {
|
|
92
95
|
const leader = Bun.spawn({
|
|
93
96
|
cmd: ["bun", childPath, "hold-lease", storePath, "leader", markerPath, String(leaseMs)],
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { fxRetry } from "../../kernel/concurrency.ts";
|
|
10
|
+
import { failureCodeOf, rebindUndosFromDo, runCompensationPhase } from "../../kernel/compensate.ts";
|
|
10
11
|
import type { AnyFlowDef } from "../../kernel/flow.ts";
|
|
11
12
|
import { createFxContext, type CreateFxOptions, type Fx } from "../../kernel/fx.ts";
|
|
13
|
+
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
12
14
|
import {
|
|
13
15
|
createJournal,
|
|
14
16
|
isJournalSuspend,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
type JournalSession,
|
|
18
20
|
type JournalStore,
|
|
19
21
|
} from "../../kernel/journal.ts";
|
|
22
|
+
|
|
20
23
|
/** Outcome of one durable attempt. */
|
|
21
24
|
export type DurableResult<O = unknown> =
|
|
22
25
|
| {
|
|
@@ -74,7 +77,21 @@ export async function runDurable<O = unknown>(
|
|
|
74
77
|
|
|
75
78
|
if (options.runId) {
|
|
76
79
|
const existing = await options.journalStore.get(options.runId);
|
|
77
|
-
if (existing
|
|
80
|
+
if (!existing) {
|
|
81
|
+
return {
|
|
82
|
+
status: "failed",
|
|
83
|
+
runId: options.runId,
|
|
84
|
+
error: `journal: run "${options.runId}" not found`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (existing.status === "failed" || existing.status === "completed") {
|
|
88
|
+
return {
|
|
89
|
+
status: "failed",
|
|
90
|
+
runId: existing.id,
|
|
91
|
+
error: existing.error ?? `journal: run is already ${existing.status}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (existing.wakeAt !== undefined && now() < existing.wakeAt) {
|
|
78
95
|
const sleepEntry = [...existing.entries].reverse().find((e) => e.kind === "sleep");
|
|
79
96
|
return {
|
|
80
97
|
status: "sleeping",
|
|
@@ -102,6 +119,23 @@ export async function runDurable<O = unknown>(
|
|
|
102
119
|
durable: true,
|
|
103
120
|
});
|
|
104
121
|
|
|
122
|
+
if (session.run.status === "compensating") {
|
|
123
|
+
await rebindUndosFromDo(options.flow, options.input, fx, session);
|
|
124
|
+
const priorError = session.run.error ?? "Error";
|
|
125
|
+
await runCompensationPhase({
|
|
126
|
+
flow: options.flow,
|
|
127
|
+
input: options.input,
|
|
128
|
+
session,
|
|
129
|
+
fx,
|
|
130
|
+
error: new Error(priorError),
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
status: "failed",
|
|
134
|
+
runId: session.runId,
|
|
135
|
+
error: session.run.error ?? priorError,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
105
139
|
try {
|
|
106
140
|
const run = () => {
|
|
107
141
|
// Same journal session across attempts — rewind so completed steps replay.
|
|
@@ -109,6 +143,20 @@ export async function runDurable<O = unknown>(
|
|
|
109
143
|
return options.flow.do(options.input as never, fx);
|
|
110
144
|
};
|
|
111
145
|
const output = await (options.flow.retry ? fxRetry(run, options.flow.retry) : run());
|
|
146
|
+
if (isFlowFailure(output)) {
|
|
147
|
+
await runCompensationPhase({
|
|
148
|
+
flow: options.flow,
|
|
149
|
+
input: options.input,
|
|
150
|
+
session,
|
|
151
|
+
fx,
|
|
152
|
+
error: output,
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
status: "failed",
|
|
156
|
+
runId: session.runId,
|
|
157
|
+
error: session.run.error ?? failureCodeOf(output),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
112
160
|
await session.commit("completed", { output });
|
|
113
161
|
return {
|
|
114
162
|
status: "completed",
|
|
@@ -124,12 +172,17 @@ export async function runDurable<O = unknown>(
|
|
|
124
172
|
label: err.label,
|
|
125
173
|
};
|
|
126
174
|
}
|
|
127
|
-
|
|
128
|
-
|
|
175
|
+
await runCompensationPhase({
|
|
176
|
+
flow: options.flow,
|
|
177
|
+
input: options.input,
|
|
178
|
+
session,
|
|
179
|
+
fx,
|
|
180
|
+
error: err,
|
|
181
|
+
});
|
|
129
182
|
return {
|
|
130
183
|
status: "failed",
|
|
131
184
|
runId: session.runId,
|
|
132
|
-
error:
|
|
185
|
+
error: session.run.error ?? failureCodeOf(err),
|
|
133
186
|
};
|
|
134
187
|
}
|
|
135
188
|
}
|
|
@@ -17,7 +17,11 @@ import { tmpdir } from "node:os";
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
19
19
|
import { flow } from "../kernel/flow.ts";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createFileJournalStore,
|
|
22
|
+
createJournal,
|
|
23
|
+
createMemoryJournalStore,
|
|
24
|
+
} from "../kernel/journal.ts";
|
|
21
25
|
import {
|
|
22
26
|
clock,
|
|
23
27
|
createClockRuntime,
|
|
@@ -65,7 +69,6 @@ describe("durable journal", () => {
|
|
|
65
69
|
const calls: string[] = [];
|
|
66
70
|
const journalStore = createMemoryJournalStore();
|
|
67
71
|
const tt = createTimeTravel(0);
|
|
68
|
-
let crashAfterFirstStep = true;
|
|
69
72
|
|
|
70
73
|
const charge = flow("payments.charge", {
|
|
71
74
|
durable: true,
|
|
@@ -74,9 +77,6 @@ describe("durable journal", () => {
|
|
|
74
77
|
calls.push("create-intent");
|
|
75
78
|
return { id: "pi_1" };
|
|
76
79
|
});
|
|
77
|
-
if (crashAfterFirstStep) {
|
|
78
|
-
throw new Error("KILLED");
|
|
79
|
-
}
|
|
80
80
|
return fx.step("confirm", () => {
|
|
81
81
|
calls.push("confirm");
|
|
82
82
|
return intent.id === "pi_1";
|
|
@@ -84,17 +84,16 @@ describe("durable journal", () => {
|
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// Simulate crash after the first step: leave status `running` (orphans,
|
|
88
|
+
// not terminal `failed` — failed runs refuse resume).
|
|
89
|
+
const journal = createJournal({ store: journalStore, now: tt.now });
|
|
90
|
+
const crashed = await journal.start("payments.charge", { orderId: "o1" });
|
|
91
|
+
await crashed.step("create-intent", () => {
|
|
92
|
+
calls.push("create-intent");
|
|
93
|
+
return { id: "pi_1" };
|
|
92
94
|
});
|
|
93
|
-
|
|
94
|
-
expect(calls).toEqual(["create-intent"]);
|
|
95
|
+
const runId = crashed.runId;
|
|
95
96
|
|
|
96
|
-
crashAfterFirstStep = false;
|
|
97
|
-
const runId = first.status === "failed" ? first.runId : "";
|
|
98
97
|
const second = await runDurable({
|
|
99
98
|
flow: charge,
|
|
100
99
|
input: { orderId: "o1" },
|