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
|
@@ -56,7 +56,7 @@ describe("store.schema.table + field.*", () => {
|
|
|
56
56
|
const decl = store.sql("notes", { schema: { notes } });
|
|
57
57
|
const runtime = createStoreRuntime({
|
|
58
58
|
drivers: {},
|
|
59
|
-
sql: { notes: { name: "notes", primary: {
|
|
59
|
+
sql: { notes: { name: "notes", primary: {} } },
|
|
60
60
|
});
|
|
61
61
|
// Register so classificationsFor merges table columns.
|
|
62
62
|
runtime.register(decl);
|
|
@@ -68,10 +68,10 @@ describe("store.schema.table + field.*", () => {
|
|
|
68
68
|
expect(masked[0]!.email).toBe(PII_MASK);
|
|
69
69
|
|
|
70
70
|
// Runtime path: open handle and query — ensure classificationsFromTable feeds mask.
|
|
71
|
-
const {
|
|
71
|
+
const { memorySqlDriver } = await import("../../drivers/index.ts");
|
|
72
72
|
const rt = createStoreRuntime({
|
|
73
|
-
drivers: { sql:
|
|
74
|
-
sql: { notes: { name: "notes", primary: {
|
|
73
|
+
drivers: { sql: memorySqlDriver },
|
|
74
|
+
sql: { notes: { name: "notes", primary: {} } },
|
|
75
75
|
});
|
|
76
76
|
rt.register(decl);
|
|
77
77
|
const handle = (await rt.open(decl, {
|
|
@@ -84,7 +84,7 @@ describe("store.schema.table + field.*", () => {
|
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
describe("emitDrizzleSource —
|
|
87
|
+
describe("emitDrizzleSource — postgres pgTable from one declaration", () => {
|
|
88
88
|
const notes = store.schema.table("notes", {
|
|
89
89
|
id: field.text().primaryKey().defaultFn(id),
|
|
90
90
|
title: field.text().notNull(),
|
|
@@ -92,24 +92,17 @@ describe("emitDrizzleSource — both dialects from one declaration", () => {
|
|
|
92
92
|
createdAt: field.integer().notNull().defaultFn(now),
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
test("
|
|
96
|
-
const src = emitDrizzleSource([notes], "sqlite");
|
|
97
|
-
expect(src.startsWith(GENERATED_SCHEMA_HEADER)).toBe(true);
|
|
98
|
-
expect(src).toContain('from "drizzle-orm/sqlite-core"');
|
|
99
|
-
expect(src).toContain("sqliteTable");
|
|
100
|
-
expect(src).not.toContain("pgTable");
|
|
101
|
-
expect(src).toContain('text("id").primaryKey().$defaultFn(id)');
|
|
102
|
-
expect(src).toContain('integer("created_at").notNull().$defaultFn(now)');
|
|
103
|
-
expect(src).toContain('text("body").notNull()');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("postgres emits pgTable from the same decl", () => {
|
|
95
|
+
test("postgres emits pgTable", () => {
|
|
107
96
|
const src = emitDrizzleSource([notes], "postgres");
|
|
97
|
+
expect(src.startsWith(GENERATED_SCHEMA_HEADER)).toBe(true);
|
|
108
98
|
expect(src).toContain('from "drizzle-orm/pg-core"');
|
|
109
99
|
expect(src).toContain("pgTable");
|
|
110
100
|
expect(src).not.toContain("sqliteTable");
|
|
101
|
+
expect(src).not.toContain("drizzle-orm/sqlite-core");
|
|
111
102
|
expect(src).toContain('text("id").primaryKey().$defaultFn(id)');
|
|
112
|
-
|
|
103
|
+
// Postgres INTEGER is 32-bit — abstract integer maps to bigint for ms clocks.
|
|
104
|
+
expect(src).toContain('bigint("created_at", { mode: "number" }).notNull().$defaultFn(now)');
|
|
105
|
+
expect(src).toContain('text("body").notNull()');
|
|
113
106
|
});
|
|
114
107
|
});
|
|
115
108
|
|
|
@@ -142,9 +135,9 @@ describe("plugin table columns in generated schema", () => {
|
|
|
142
135
|
expect(pluginTables[0]!.columns.actorId!.classification?.pii).toBe(true);
|
|
143
136
|
|
|
144
137
|
const merged = mergeSchemaTables([notes], pluginTables);
|
|
145
|
-
const src = emitDrizzleSource(merged, "
|
|
146
|
-
expect(src).toContain('
|
|
147
|
-
expect(src).toContain('
|
|
138
|
+
const src = emitDrizzleSource(merged, "postgres");
|
|
139
|
+
expect(src).toContain('pgTable("notes"');
|
|
140
|
+
expect(src).toContain('pgTable("audit_events"');
|
|
148
141
|
expect(src).toContain('text("actor_id").notNull()');
|
|
149
142
|
});
|
|
150
143
|
|
|
@@ -195,21 +188,12 @@ describe("emitDrizzleSource — references + relations (Linkly-shaped)", () => {
|
|
|
195
188
|
},
|
|
196
189
|
}));
|
|
197
190
|
|
|
198
|
-
test("
|
|
199
|
-
const src = emitDrizzleSource([links, daily], "sqlite", { relations: [relations] });
|
|
200
|
-
expect(src).toContain("sqliteTable");
|
|
201
|
-
expect(src).toContain(".references(() => links.code)");
|
|
202
|
-
expect(src).toContain('import { defineRelations } from "drizzle-orm"');
|
|
203
|
-
expect(src).toContain("defineRelations({ links, daily }");
|
|
204
|
-
expect(src).toContain("r.many.daily({ from: r.links.code, to: r.daily.code })");
|
|
205
|
-
expect(src).toContain("r.one.links({ from: r.daily.code, to: r.links.code, optional: false })");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("postgres emit includes same FK + defineRelations from one decl", () => {
|
|
191
|
+
test("postgres emit includes FK + defineRelations", () => {
|
|
209
192
|
const src = emitDrizzleSource([links, daily], "postgres", { relations: [relations] });
|
|
210
193
|
expect(src).toContain("pgTable");
|
|
211
194
|
expect(src).not.toContain("sqliteTable");
|
|
212
195
|
expect(src).toContain(".references(() => links.code)");
|
|
196
|
+
expect(src).toContain('import { defineRelations } from "drizzle-orm"');
|
|
213
197
|
expect(src).toContain("defineRelations({ links, daily }");
|
|
214
198
|
expect(src).toContain("r.many.daily({ from: r.links.code, to: r.daily.code })");
|
|
215
199
|
expect(src).toContain("r.one.links({ from: r.daily.code, to: r.links.code, optional: false })");
|
|
@@ -19,8 +19,7 @@ describe("defineSeed / resolveSeedCategory", () => {
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
test("resolveSeedCategory matches the env matrix", () => {
|
|
22
|
-
expect(resolveSeedCategory("
|
|
23
|
-
expect(resolveSeedCategory("docker")).toBe("dev");
|
|
22
|
+
expect(resolveSeedCategory("dev")).toBe("dev");
|
|
24
23
|
expect(resolveSeedCategory("prod")).toBe("prod");
|
|
25
24
|
expect(resolveSeedCategory("test")).toBe(null);
|
|
26
25
|
});
|
|
@@ -17,7 +17,7 @@ export type SeedFns = SeedFn | readonly SeedFn[];
|
|
|
17
17
|
* Seed categories for {@link defineSeed}.
|
|
18
18
|
*
|
|
19
19
|
* - `essential` — every environment, always
|
|
20
|
-
* - `dev` — `
|
|
20
|
+
* - `dev` — `dev` ConfigEnv only (Docker Compose laptop)
|
|
21
21
|
* - `prod` — `prod` only
|
|
22
22
|
*/
|
|
23
23
|
export interface SeedDef {
|
|
@@ -52,17 +52,16 @@ export function normalizeSeedFns(fns?: SeedFns): SeedFn[] {
|
|
|
52
52
|
/**
|
|
53
53
|
* Which optional category runs for `env` (single source of truth).
|
|
54
54
|
*
|
|
55
|
-
* | env
|
|
56
|
-
* |
|
|
57
|
-
* |
|
|
58
|
-
* |
|
|
59
|
-
* |
|
|
60
|
-
* | prod | prod |
|
|
55
|
+
* | env | category |
|
|
56
|
+
* | ---- | -------- |
|
|
57
|
+
* | dev | dev |
|
|
58
|
+
* | test | (none) |
|
|
59
|
+
* | prod | prod |
|
|
61
60
|
*
|
|
62
61
|
* @param env - Resolved {@link ConfigEnv}
|
|
63
62
|
*/
|
|
64
63
|
export function resolveSeedCategory(env: ConfigEnv): SeedCategory | null {
|
|
65
|
-
if (env === "
|
|
64
|
+
if (env === "dev") return "dev";
|
|
66
65
|
if (env === "prod") return "prod";
|
|
67
66
|
return null;
|
|
68
67
|
}
|
|
@@ -20,10 +20,10 @@ import {
|
|
|
20
20
|
or,
|
|
21
21
|
sql,
|
|
22
22
|
} from "drizzle-orm";
|
|
23
|
-
import { integer,
|
|
23
|
+
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
24
24
|
import { compileOrderBy, compileWhere } from "./sql-condition.ts";
|
|
25
25
|
|
|
26
|
-
const notes =
|
|
26
|
+
const notes = pgTable("notes", {
|
|
27
27
|
id: text("id").primaryKey(),
|
|
28
28
|
title: text("title").notNull(),
|
|
29
29
|
createdAt: integer("created_at").notNull(),
|
|
@@ -7,22 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import { Database } from "bun:sqlite";
|
|
11
10
|
import { and, desc, eq, like, lt, or } from "drizzle-orm";
|
|
12
|
-
import { integer,
|
|
13
|
-
import {
|
|
11
|
+
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
12
|
+
import { pgliteDriver } from "../../drivers/pglite.ts";
|
|
14
13
|
import type { SqlConnection } from "../../drivers/types.ts";
|
|
15
14
|
import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
|
|
16
15
|
|
|
17
|
-
const posts =
|
|
16
|
+
const posts = pgTable("posts", {
|
|
18
17
|
id: text("id").primaryKey(),
|
|
19
18
|
title: text("title").notNull(),
|
|
20
19
|
createdAt: integer("created_at").notNull(),
|
|
21
20
|
});
|
|
22
21
|
|
|
23
22
|
async function openHandle(): Promise<{ handle: SqlStoreHandle; conn: SqlConnection }> {
|
|
24
|
-
const conn = await
|
|
25
|
-
|
|
23
|
+
const conn = await pgliteDriver.connect({
|
|
24
|
+
url: "memory://",
|
|
26
25
|
role: "primary",
|
|
27
26
|
});
|
|
28
27
|
const handle = createSqlStoreHandle("sql:app", {
|
|
@@ -36,8 +35,7 @@ async function openHandle(): Promise<{ handle: SqlStoreHandle; conn: SqlConnecti
|
|
|
36
35
|
|
|
37
36
|
describe("SqlStoreHandle — no relational query surface (path b)", () => {
|
|
38
37
|
test("created handle exposes exactly the single-table surface", async () => {
|
|
39
|
-
const
|
|
40
|
-
const conn = await sqliteDriver.connect({ client: db, role: "primary" });
|
|
38
|
+
const conn = await pgliteDriver.connect({ url: "memory://", role: "primary" });
|
|
41
39
|
const handle = createSqlStoreHandle("sql:app", {
|
|
42
40
|
connection: conn,
|
|
43
41
|
classifications: new Map(),
|
|
@@ -124,12 +122,12 @@ describe("SqlStoreHandle — orderBy / limit select chain", () => {
|
|
|
124
122
|
await conn.close();
|
|
125
123
|
});
|
|
126
124
|
|
|
127
|
-
test("like filters rows through the session (
|
|
125
|
+
test("like filters rows through the session (postgres case-sensitive)", async () => {
|
|
128
126
|
const { handle, conn } = await openHandle();
|
|
129
127
|
await handle.insert(posts).values({ id: "p1", title: "alpha", createdAt: 100 });
|
|
130
128
|
await handle.insert(posts).values({ id: "p2", title: "bravo", createdAt: 200 });
|
|
131
129
|
|
|
132
|
-
const rows = await handle.select().from(posts).where(like(posts.title, "
|
|
130
|
+
const rows = await handle.select().from(posts).where(like(posts.title, "a%"));
|
|
133
131
|
expect(rows.map((r) => r.id)).toEqual(["p1"]);
|
|
134
132
|
await conn.close();
|
|
135
133
|
});
|
|
@@ -363,9 +363,13 @@ export function createSqlStoreHandle(
|
|
|
363
363
|
if (cols.length === 0) return;
|
|
364
364
|
const name = resolveTableName(table);
|
|
365
365
|
const pk = cols.find((c) => c.primary)?.sqlName;
|
|
366
|
+
// Postgres/PGLite INTEGER is 32-bit — abstract `integer` (ms timestamps via
|
|
367
|
+
// `now`) must map to BIGINT. SQLite INTEGER is flexible up to 64-bit.
|
|
368
|
+
const pgWideInt = connection.driverId === "postgres" || connection.driverId === "pglite";
|
|
366
369
|
const colSql = cols
|
|
367
370
|
.map((c) => {
|
|
368
|
-
const
|
|
371
|
+
const base = pgWideInt && c.sqlType === "INTEGER" ? "BIGINT" : c.sqlType;
|
|
372
|
+
const typ = pk && c.sqlName === pk ? `${base} PRIMARY KEY` : base;
|
|
369
373
|
return `${quoteIdent(c.sqlName)} ${typ}`;
|
|
370
374
|
})
|
|
371
375
|
.join(", ");
|
|
@@ -734,6 +738,10 @@ export function createSqlStoreHandle(
|
|
|
734
738
|
async ensureTable(table) {
|
|
735
739
|
const cols = Object.values(table.columns);
|
|
736
740
|
const pk = resolvePkColumn(table);
|
|
741
|
+
const intType =
|
|
742
|
+
connection.driverId === "postgres" || connection.driverId === "pglite"
|
|
743
|
+
? "BIGINT"
|
|
744
|
+
: "INTEGER";
|
|
737
745
|
const colSql = cols
|
|
738
746
|
.map((c) => {
|
|
739
747
|
const typ =
|
|
@@ -743,7 +751,7 @@ export function createSqlStoreHandle(
|
|
|
743
751
|
c.name === "qty" ||
|
|
744
752
|
c.name === "createdAt" ||
|
|
745
753
|
c.name === "created_at"
|
|
746
|
-
?
|
|
754
|
+
? intType
|
|
747
755
|
: "TEXT";
|
|
748
756
|
return `${quoteIdent(c.name)} ${typ}`;
|
|
749
757
|
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Store element acceptance:
|
|
3
|
-
* - same flow runs unchanged against
|
|
3
|
+
* - same flow runs unchanged against memory and postgres
|
|
4
4
|
* - read-only flow provably hits a replica
|
|
5
5
|
* - auto-invalidation fires on exactly the writes touching the read's keys
|
|
6
6
|
* - SELECT * masks classified columns
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import { createPostgresFakeClient,
|
|
10
|
+
import { createPostgresFakeClient, memorySqlDriver, postgresDriver } from "../drivers/index.ts";
|
|
11
11
|
import type { Effects } from "../manifest/types.ts";
|
|
12
12
|
import {
|
|
13
13
|
classify,
|
|
@@ -73,7 +73,7 @@ describe("store facets", () => {
|
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
describe("same flow against
|
|
76
|
+
describe("same flow against memory and postgres", () => {
|
|
77
77
|
test("insert / select / findById unchanged across drivers", async () => {
|
|
78
78
|
const decl = store.sql("notes", {
|
|
79
79
|
schema: { notes },
|
|
@@ -81,7 +81,7 @@ describe("same flow against sqlite and postgres", () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
for (const [label, driver, client] of [
|
|
84
|
-
["
|
|
84
|
+
["memory", memorySqlDriver, undefined],
|
|
85
85
|
["postgres", postgresDriver, createPostgresFakeClient()],
|
|
86
86
|
] as const) {
|
|
87
87
|
const runtime = createStoreRuntime({
|
|
@@ -89,7 +89,7 @@ describe("same flow against sqlite and postgres", () => {
|
|
|
89
89
|
sql: {
|
|
90
90
|
notes: {
|
|
91
91
|
name: "notes",
|
|
92
|
-
primary: {
|
|
92
|
+
primary: { client },
|
|
93
93
|
},
|
|
94
94
|
},
|
|
95
95
|
});
|
|
@@ -218,8 +218,8 @@ describe("tier-1 cache invalidation from effects", () => {
|
|
|
218
218
|
|
|
219
219
|
test("runtime putTier1 + onWriteEffects", async () => {
|
|
220
220
|
const runtime = createStoreRuntime({
|
|
221
|
-
drivers: { sql:
|
|
222
|
-
sql: { notes: { name: "notes", primary: {
|
|
221
|
+
drivers: { sql: memorySqlDriver },
|
|
222
|
+
sql: { notes: { name: "notes", primary: {} } },
|
|
223
223
|
});
|
|
224
224
|
const keys = runtime.putTier1({ reads: ["sql:notes"] }, ["cached"]);
|
|
225
225
|
expect(runtime.cache.get<string[]>(keys[0]!)).toEqual(["cached"]);
|
|
@@ -237,7 +237,7 @@ describe("exists and increment helpers", () => {
|
|
|
237
237
|
|
|
238
238
|
test("exists returns correctly for present / absent rows across sqlite and postgres", async () => {
|
|
239
239
|
for (const [label, driver, client] of [
|
|
240
|
-
["
|
|
240
|
+
["memory", memorySqlDriver, undefined],
|
|
241
241
|
["postgres", postgresDriver, createPostgresFakeClient()],
|
|
242
242
|
] as const) {
|
|
243
243
|
const decl = store.sql("counters");
|
|
@@ -246,7 +246,7 @@ describe("exists and increment helpers", () => {
|
|
|
246
246
|
sql: {
|
|
247
247
|
counters: {
|
|
248
248
|
name: "counters",
|
|
249
|
-
primary: {
|
|
249
|
+
primary: { client },
|
|
250
250
|
},
|
|
251
251
|
},
|
|
252
252
|
});
|
|
@@ -267,7 +267,7 @@ describe("exists and increment helpers", () => {
|
|
|
267
267
|
|
|
268
268
|
test("increment under 100 concurrent callers lands on the correct final value", async () => {
|
|
269
269
|
for (const [label, driver, client] of [
|
|
270
|
-
["
|
|
270
|
+
["memory", memorySqlDriver, undefined],
|
|
271
271
|
["postgres", postgresDriver, createPostgresFakeClient()],
|
|
272
272
|
] as const) {
|
|
273
273
|
const decl = store.sql("counters");
|
|
@@ -276,7 +276,7 @@ describe("exists and increment helpers", () => {
|
|
|
276
276
|
sql: {
|
|
277
277
|
counters: {
|
|
278
278
|
name: "counters",
|
|
279
|
-
primary: {
|
|
279
|
+
primary: { client },
|
|
280
280
|
},
|
|
281
281
|
},
|
|
282
282
|
});
|
|
@@ -308,8 +308,8 @@ describe("PII masking at the driver boundary", () => {
|
|
|
308
308
|
classify: { notes: { email: classify({ pii: true }) } },
|
|
309
309
|
});
|
|
310
310
|
const runtime = createStoreRuntime({
|
|
311
|
-
drivers: { sql:
|
|
312
|
-
sql: { notes: { name: "notes", primary: {
|
|
311
|
+
drivers: { sql: memorySqlDriver },
|
|
312
|
+
sql: { notes: { name: "notes", primary: {} } },
|
|
313
313
|
});
|
|
314
314
|
runtime.register(decl);
|
|
315
315
|
const handle = asSql(
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault audit trail — a tamper-evident hash chain of *operations*.
|
|
3
|
+
*
|
|
4
|
+
* An audit row records that something happened to a path, never what the
|
|
5
|
+
* value was. Each row hashes its own payload together with the previous
|
|
6
|
+
* row's hash, so removing or editing a row breaks every hash after it
|
|
7
|
+
* (`verifyAuditChain` in `storage.ts` walks the chain).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { VaultError } from "./errors.ts";
|
|
11
|
+
import type { VaultErrorCode } from "./errors.ts";
|
|
12
|
+
import type { VaultActorType } from "./types.ts";
|
|
13
|
+
import type { VaultAuditSinkKind } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
/** Operations recorded in the audit chain. */
|
|
16
|
+
export type AuditAction =
|
|
17
|
+
| "get"
|
|
18
|
+
| "set"
|
|
19
|
+
| "delete"
|
|
20
|
+
| "rotate"
|
|
21
|
+
| "list"
|
|
22
|
+
| "seal"
|
|
23
|
+
| "unseal"
|
|
24
|
+
| "initialize"
|
|
25
|
+
| "rewrap"
|
|
26
|
+
| "purge";
|
|
27
|
+
|
|
28
|
+
/** Hash-chain genesis marker used for the first row. */
|
|
29
|
+
export const AUDIT_GENESIS_HASH = "genesis";
|
|
30
|
+
|
|
31
|
+
/** One operation to append to the chain. */
|
|
32
|
+
export interface AuditEntry {
|
|
33
|
+
/** What happened. */
|
|
34
|
+
readonly action: AuditAction;
|
|
35
|
+
/** Canonical path, when the operation targets one. */
|
|
36
|
+
readonly path?: string;
|
|
37
|
+
/** Actor class. */
|
|
38
|
+
readonly actorType: VaultActorType;
|
|
39
|
+
/** Actor id within its class. */
|
|
40
|
+
readonly actorId?: string;
|
|
41
|
+
/** Whether the operation succeeded. */
|
|
42
|
+
readonly success: boolean;
|
|
43
|
+
/** Failure reason when `success` is `false`. */
|
|
44
|
+
readonly errorCode?: VaultErrorCode;
|
|
45
|
+
/** Safe, secret-free failure description. */
|
|
46
|
+
readonly errorMessage?: string;
|
|
47
|
+
/** Correlation id for the enclosing request/run. */
|
|
48
|
+
readonly requestId?: string;
|
|
49
|
+
/** Event time. Defaults to now at append time. */
|
|
50
|
+
readonly at?: Date;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Exact field set covered by a row hash.
|
|
55
|
+
*
|
|
56
|
+
* Normalized (`null` rather than `undefined`, ISO-8601 timestamps) so a row
|
|
57
|
+
* read back from SQL hashes identically to the row that was written.
|
|
58
|
+
*/
|
|
59
|
+
export interface AuditHashPayload {
|
|
60
|
+
/** What happened. */
|
|
61
|
+
readonly action: AuditAction;
|
|
62
|
+
/** Canonical path or `null`. */
|
|
63
|
+
readonly path: string | null;
|
|
64
|
+
/** Actor class. */
|
|
65
|
+
readonly actorType: VaultActorType;
|
|
66
|
+
/** Actor id or `null`. */
|
|
67
|
+
readonly actorId: string | null;
|
|
68
|
+
/** Whether the operation succeeded. */
|
|
69
|
+
readonly success: boolean;
|
|
70
|
+
/** Failure reason or `null`. */
|
|
71
|
+
readonly errorCode: VaultErrorCode | null;
|
|
72
|
+
/** Failure description or `null`. */
|
|
73
|
+
readonly errorMessage: string | null;
|
|
74
|
+
/** Correlation id or `null`. */
|
|
75
|
+
readonly requestId: string | null;
|
|
76
|
+
/** ISO-8601 event time. */
|
|
77
|
+
readonly createdAt: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Destination for audit rows. */
|
|
81
|
+
export interface AuditSink {
|
|
82
|
+
/** Sink kind, mirroring the configured `audit.sink`. */
|
|
83
|
+
readonly kind: VaultAuditSinkKind;
|
|
84
|
+
/**
|
|
85
|
+
* Append one operation.
|
|
86
|
+
*
|
|
87
|
+
* @param entry - Operation to record
|
|
88
|
+
*/
|
|
89
|
+
append(entry: AuditEntry): Promise<void>;
|
|
90
|
+
/** Release sink resources, if any. */
|
|
91
|
+
close?(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Backend-side audit writer.
|
|
96
|
+
*
|
|
97
|
+
* `storage.ts` implements this over `oke_vault_audit`; the `db` sink is a
|
|
98
|
+
* thin adapter so the audit module never imports SQL.
|
|
99
|
+
*/
|
|
100
|
+
export interface AuditWriter {
|
|
101
|
+
/**
|
|
102
|
+
* Persist one operation, computing and linking its row hash.
|
|
103
|
+
*
|
|
104
|
+
* @param entry - Operation to record
|
|
105
|
+
*/
|
|
106
|
+
appendAuditEntry(entry: AuditEntry): Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Options accepted by {@link createAuditSink}. */
|
|
110
|
+
export interface CreateAuditSinkOptions {
|
|
111
|
+
/** Required for the `db` sink. */
|
|
112
|
+
readonly writer?: AuditWriter;
|
|
113
|
+
/** Required for the `webhook` sink (not yet implemented). */
|
|
114
|
+
readonly webhookUrl?: string;
|
|
115
|
+
/** Line writer for the `stdout` sink (tests). */
|
|
116
|
+
readonly write?: (line: string) => void;
|
|
117
|
+
/** Clock for `stdout` timestamps (tests). */
|
|
118
|
+
readonly now?: () => Date;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize an entry into the exact fields a row hash covers.
|
|
123
|
+
*
|
|
124
|
+
* @param entry - Operation to record
|
|
125
|
+
* @param createdAt - Event time
|
|
126
|
+
*/
|
|
127
|
+
export function toAuditHashPayload(entry: AuditEntry, createdAt: Date): AuditHashPayload {
|
|
128
|
+
return {
|
|
129
|
+
action: entry.action,
|
|
130
|
+
path: entry.path ?? null,
|
|
131
|
+
actorType: entry.actorType,
|
|
132
|
+
actorId: entry.actorId ?? null,
|
|
133
|
+
success: entry.success,
|
|
134
|
+
errorCode: entry.errorCode ?? null,
|
|
135
|
+
errorMessage: entry.errorMessage ?? null,
|
|
136
|
+
requestId: entry.requestId ?? null,
|
|
137
|
+
createdAt: createdAt.toISOString(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Hash one audit row into the chain.
|
|
143
|
+
*
|
|
144
|
+
* Fields are joined with NUL in a fixed order — no JSON key-order or
|
|
145
|
+
* whitespace ambiguity — and prefixed with the previous row's hash.
|
|
146
|
+
*
|
|
147
|
+
* @param prevHash - Previous row hash, or `null` / {@link AUDIT_GENESIS_HASH} for the first row
|
|
148
|
+
* @param payload - Normalized row fields
|
|
149
|
+
* @returns Lowercase hex SHA-256 digest
|
|
150
|
+
*/
|
|
151
|
+
export async function computeAuditRowHash(
|
|
152
|
+
prevHash: string | null,
|
|
153
|
+
payload: AuditHashPayload,
|
|
154
|
+
): Promise<string> {
|
|
155
|
+
const parts = [
|
|
156
|
+
"oke-vault-audit-v1",
|
|
157
|
+
prevHash ?? AUDIT_GENESIS_HASH,
|
|
158
|
+
payload.action,
|
|
159
|
+
payload.path ?? "",
|
|
160
|
+
payload.actorType,
|
|
161
|
+
payload.actorId ?? "",
|
|
162
|
+
payload.success ? "1" : "0",
|
|
163
|
+
payload.errorCode ?? "",
|
|
164
|
+
payload.errorMessage ?? "",
|
|
165
|
+
payload.requestId ?? "",
|
|
166
|
+
payload.createdAt,
|
|
167
|
+
];
|
|
168
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(parts.join("\0")));
|
|
169
|
+
return Buffer.from(new Uint8Array(digest)).toString("hex");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build an audit sink.
|
|
174
|
+
*
|
|
175
|
+
* - `db` delegates to an {@link AuditWriter} supplied by storage.
|
|
176
|
+
* - `stdout` writes one secret-free JSON line per operation.
|
|
177
|
+
* - `webhook` is not implemented yet and throws `UNSUPPORTED`.
|
|
178
|
+
*
|
|
179
|
+
* @param kind - Sink kind from `audit.sink`
|
|
180
|
+
* @param opts - Writer / webhook URL / test seams
|
|
181
|
+
* @throws VaultError `UNSUPPORTED` for `webhook`, or `MISSING_PEER` when `db` has no writer
|
|
182
|
+
*/
|
|
183
|
+
export function createAuditSink(
|
|
184
|
+
kind: VaultAuditSinkKind,
|
|
185
|
+
opts: CreateAuditSinkOptions = {},
|
|
186
|
+
): AuditSink {
|
|
187
|
+
if (kind === "webhook") {
|
|
188
|
+
throw new VaultError("UNSUPPORTED", "vault: webhook audit sink is not implemented");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (kind === "db") {
|
|
192
|
+
const writer = opts.writer;
|
|
193
|
+
if (!writer) {
|
|
194
|
+
throw new VaultError("MISSING_PEER", "vault: db audit sink requires a storage writer");
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: "db",
|
|
198
|
+
append(entry) {
|
|
199
|
+
return writer.appendAuditEntry(entry);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const write = opts.write ?? ((line: string) => void process.stdout.write(`${line}\n`));
|
|
205
|
+
const now = opts.now ?? (() => new Date());
|
|
206
|
+
return {
|
|
207
|
+
kind: "stdout",
|
|
208
|
+
async append(entry) {
|
|
209
|
+
// Only normalized metadata is serialized — an entry never holds a value.
|
|
210
|
+
write(
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
sink: "oke.vault.audit",
|
|
213
|
+
...toAuditHashPayload(entry, entry.at ?? now()),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Sink that drops every entry (`audit.enabled: false`).
|
|
222
|
+
*
|
|
223
|
+
* Reports `kind: "stdout"` because {@link VaultAuditSinkKind} has no
|
|
224
|
+
* "off" member — callers gate on config, not on this field.
|
|
225
|
+
*/
|
|
226
|
+
export function createNullAuditSink(): AuditSink {
|
|
227
|
+
return {
|
|
228
|
+
kind: "stdout",
|
|
229
|
+
async append() {
|
|
230
|
+
// Intentionally empty — auditing is disabled.
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault boot chain — managed provider wiring and driver-id normalization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import type { VaultOpenOptions } from "../../drivers/vault-types.ts";
|
|
7
|
+
import { buildVaultBootChain, normalizeVaultDriverId } from "./boot-chain.ts";
|
|
8
|
+
|
|
9
|
+
/** Env keys these tests own. */
|
|
10
|
+
const OWNED = [
|
|
11
|
+
"OKE_VAULT_PROVIDER",
|
|
12
|
+
"OKE_VAULT_URL",
|
|
13
|
+
"OKE_VAULT_TOKEN",
|
|
14
|
+
"OKE_VAULT_MOUNT",
|
|
15
|
+
"OKE_VAULT_REGION",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
for (const key of OWNED) delete process.env[key];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("managed vault boot chain", () => {
|
|
23
|
+
test("no provider stays a single platform-injected layer", () => {
|
|
24
|
+
const chain = buildVaultBootChain({ driverId: "managed", env: "test", seed: { A: "1" } });
|
|
25
|
+
expect(chain).toHaveLength(1);
|
|
26
|
+
expect(chain[0]?.driver.id).toBe("managed");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a provider puts the env layers in front of the managed layer", () => {
|
|
30
|
+
process.env.OKE_VAULT_PROVIDER = "aws-secrets-manager";
|
|
31
|
+
process.env.OKE_VAULT_REGION = "eu-central-1";
|
|
32
|
+
process.env.OKE_VAULT_MOUNT = "oke/prod/";
|
|
33
|
+
|
|
34
|
+
const chain = buildVaultBootChain({ driverId: "managed", env: "prod" });
|
|
35
|
+
expect(chain[0]?.driver.id).toBe("env");
|
|
36
|
+
const last = chain.at(-1);
|
|
37
|
+
expect(last?.driver.id).toBe("managed");
|
|
38
|
+
const options = last?.options as VaultOpenOptions;
|
|
39
|
+
expect(options.provider).toBe("aws-secrets-manager");
|
|
40
|
+
expect(options.region).toBe("eu-central-1");
|
|
41
|
+
expect(options.mount).toBe("oke/prod/");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("aws-secrets-manager carries region and prefix without inventing a mount", () => {
|
|
45
|
+
process.env.OKE_VAULT_PROVIDER = "aws-secrets-manager";
|
|
46
|
+
process.env.OKE_VAULT_REGION = "eu-central-1";
|
|
47
|
+
|
|
48
|
+
const chain = buildVaultBootChain({ driverId: "managed", env: "prod" });
|
|
49
|
+
const options = chain.at(-1)?.options as VaultOpenOptions;
|
|
50
|
+
expect(options.region).toBe("eu-central-1");
|
|
51
|
+
expect(options.mount).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("built-in vault sits behind env layers", () => {
|
|
55
|
+
const chain = buildVaultBootChain({ driverId: "vault", env: "prod" });
|
|
56
|
+
expect(chain.map((l) => l.driver.id)).toContain("env");
|
|
57
|
+
expect(chain.at(-1)?.driver.id).toBe("vault");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("normalizeVaultDriverId", () => {
|
|
62
|
+
test("maps legacy labels onto protocol ids", () => {
|
|
63
|
+
expect(normalizeVaultDriverId("dotenv")).toBe("env");
|
|
64
|
+
expect(normalizeVaultDriverId("builtin")).toBe("vault");
|
|
65
|
+
expect(normalizeVaultDriverId("managed")).toBe("managed");
|
|
66
|
+
expect(normalizeVaultDriverId("vault")).toBe("vault");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("rejects an unknown label", () => {
|
|
70
|
+
expect(() => normalizeVaultDriverId("acme")).toThrow(/unknown vault driver/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects removed external vault driver id", () => {
|
|
74
|
+
const removed = ["open", "bao"].join("");
|
|
75
|
+
expect(() => normalizeVaultDriverId(removed)).toThrow(/unknown vault driver/);
|
|
76
|
+
expect(() => normalizeVaultDriverId(removed)).toThrow(/aws-secrets-manager/);
|
|
77
|
+
});
|
|
78
|
+
});
|