okengine 0.3.5 → 0.4.3
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/AGENTS.md +6 -0
- package/manifest.v1.schema.json +43 -1
- package/package.json +27 -12
- package/site/content/docs/ai/skills.mdx +11 -7
- package/site/content/docs/console/vault.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +2 -0
- package/site/content/docs/elements/channel.mdx +7 -4
- package/site/content/docs/elements/clock.mdx +2 -4
- package/site/content/docs/elements/flow.mdx +19 -10
- package/site/content/docs/elements/gate.mdx +18 -11
- package/site/content/docs/elements/signal.mdx +9 -12
- package/site/content/docs/elements/store.mdx +69 -8
- package/site/content/docs/elements/vault.mdx +11 -12
- package/site/content/docs/get-started/basic-usage.mdx +76 -41
- package/site/content/docs/get-started/installation.mdx +95 -43
- package/site/content/docs/get-started/introduction.mdx +128 -75
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/why.mdx +141 -0
- package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
- package/site/content/docs/plugins/security-headers.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/environment-variables.mdx +10 -0
- package/site/content/docs/reference/fx.mdx +56 -3
- package/site/content/docs/reference/plugins.mdx +18 -18
- package/src/cli/competitor-mention-removal.test.ts +117 -0
- package/src/cli/dev.ts +20 -0
- package/src/cli/meilisearch-local.test.ts +69 -0
- package/src/cli/meilisearch-local.ts +188 -0
- package/src/compiler/extract.test.ts +63 -0
- package/src/compiler/extract.ts +36 -15
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
- package/src/console/server/channels.ts +2 -0
- package/src/console/server/clock.ts +3 -0
- package/src/console/server/flows.ts +13 -0
- package/src/console/server/gates.ts +2 -0
- package/src/console/server/plugins.ts +20 -1
- package/src/console/server/signals.ts +5 -0
- package/src/console/server/store.test.ts +17 -0
- package/src/console/server/store.ts +35 -1
- package/src/console/ui/channels/types.ts +1 -0
- package/src/console/ui/clock/types.ts +1 -0
- package/src/console/ui/display.test.ts +14 -0
- package/src/console/ui/display.ts +9 -0
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
- package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
- package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
- package/src/console/ui/dist/assets/panel-channels-9T3ybqRu.js +1 -0
- package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
- package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
- package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
- package/src/console/ui/dist/assets/panel-gates-BQGYXvjT.js +1 -0
- package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
- package/src/console/ui/dist/assets/panel-plugins-D0PsmVw2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
- package/src/console/ui/dist/assets/panel-signals-Bbg4ewpP.js +1 -0
- package/src/console/ui/dist/assets/panel-store-CPCbsDRa.js +1 -0
- package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
- package/src/console/ui/dist/assets/panel-vault-D1_MvOmo.js +1 -0
- package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/gates/types.ts +1 -0
- package/src/console/ui/plugins/fixture.ts +7 -0
- package/src/console/ui/plugins/types.ts +3 -0
- package/src/console/ui/shell/client.ts +3 -0
- package/src/console/ui/shell/panels/channels/ChannelsPanel.tsx +7 -2
- package/src/console/ui/shell/panels/clock/ClockPanel.tsx +9 -2
- package/src/console/ui/shell/panels/gates/GatesPanel.tsx +12 -5
- package/src/console/ui/shell/panels/plugins/PluginsPanel.tsx +18 -4
- package/src/console/ui/shell/panels/signals/SignalsPanel.tsx +13 -2
- package/src/console/ui/shell/panels/store/StorePanel.tsx +11 -4
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +9 -3
- package/src/console/ui/signals/types.ts +1 -0
- package/src/console/ui/store/fixture.ts +5 -0
- package/src/console/ui/store/types.ts +2 -0
- package/src/docker/compose.ts +5 -0
- package/src/docker/docker.test.ts +41 -0
- package/src/docker/recipes/index.ts +10 -2
- package/src/docker/recipes/meilisearch.ts +31 -0
- package/src/drivers/conformance.test.ts +26 -0
- package/src/drivers/conformance.ts +40 -3
- package/src/drivers/drizzle-dialect.test.ts +4 -0
- package/src/drivers/drizzle-dialect.ts +8 -4
- package/src/drivers/index.ts +18 -2
- package/src/drivers/libsql.ts +179 -0
- package/src/drivers/meilisearch.integration.test.ts +77 -0
- package/src/drivers/meilisearch.test.ts +181 -0
- package/src/drivers/meilisearch.ts +208 -0
- package/src/drivers/memory.ts +4 -4
- package/src/drivers/pglite.ts +79 -0
- package/src/drivers/pgvector.ts +60 -25
- package/src/drivers/types.ts +106 -16
- package/src/drivers/vault-driver-removal.test.ts +6 -0
- package/src/drivers/vault-types.ts +4 -4
- package/src/elements/ai/runtime.ts +6 -0
- package/src/elements/ai.test.ts +22 -0
- package/src/elements/channel/declare.ts +5 -0
- package/src/elements/clock/declare.ts +5 -0
- package/src/elements/clock/durable.ts +7 -1
- package/src/elements/gate/declare.ts +28 -5
- package/src/elements/gate.ts +1 -0
- package/src/elements/signal/declare.ts +5 -0
- package/src/elements/store/declare.ts +10 -2
- package/src/elements/store/index-boot.test.ts +299 -0
- package/src/elements/store/runtime.ts +110 -17
- package/src/elements/store/schema-decl.ts +7 -0
- package/src/elements/store.ts +2 -0
- package/src/elements/vault.test.ts +27 -4
- package/src/elements/vault.ts +1 -1
- package/src/index.ts +4 -0
- package/src/kernel/abort-scope.ts +116 -0
- package/src/kernel/app.ts +12 -2
- package/src/kernel/boot-bind/store.test.ts +68 -1
- package/src/kernel/boot-bind/store.ts +92 -2
- package/src/kernel/concurrency.test.ts +294 -0
- package/src/kernel/concurrency.ts +220 -0
- package/src/kernel/flow.ts +9 -0
- package/src/kernel/fx.test.ts +23 -2
- package/src/kernel/fx.ts +80 -4
- package/src/kernel/index.ts +23 -0
- package/src/kernel/journal.ts +9 -0
- package/src/kernel/plugin/capabilities.test.ts +18 -0
- package/src/kernel/plugin.ts +11 -3
- package/src/kernel/redacted.ts +74 -0
- package/src/kernel/registry.ts +29 -6
- package/src/kernel/router.ts +3 -3
- package/src/manifest/types.ts +21 -0
- package/src/release/measure.ts +4 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/site/content/docs/get-started/comparison.mdx +0 -65
- package/src/console/ui/dist/assets/index-BWo8R7NR.js +0 -10
- package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
- package/src/console/ui/dist/assets/panel-channels-BOmQ-onL.js +0 -1
- package/src/console/ui/dist/assets/panel-clock-giAq0Ccv.js +0 -1
- package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
- package/src/console/ui/dist/assets/panel-gates-XclZxWD5.js +0 -1
- package/src/console/ui/dist/assets/panel-overview-BznEOTnb.js +0 -1
- package/src/console/ui/dist/assets/panel-plugins-CcGM1g64.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-CGWNHLR4.js +0 -1
- package/src/console/ui/dist/assets/panel-signals-CNywkdak.js +0 -1
- package/src/console/ui/dist/assets/panel-store-KmTbFHMH.js +0 -1
- package/src/console/ui/dist/assets/panel-traces-DBLx2ilD.js +0 -1
- package/src/console/ui/dist/assets/panel-vault-CEnFc0dk.js +0 -1
- package/src/drivers/vault-infisical.ts +0 -57
|
@@ -17,6 +17,8 @@ export interface SignalOptions {
|
|
|
17
17
|
* - `live` — client-subscribable stream (replayable)
|
|
18
18
|
*/
|
|
19
19
|
readonly delivery: SignalDelivery;
|
|
20
|
+
/** Optional human description for Console / docs (falls back to the signal name). */
|
|
21
|
+
readonly description?: string;
|
|
20
22
|
/** Max delivery attempts before dead-letter (once). */
|
|
21
23
|
readonly retries?: number;
|
|
22
24
|
/** Preserve exhausted messages in the DLQ (once). */
|
|
@@ -35,6 +37,8 @@ export interface SignalDecl<T = unknown> {
|
|
|
35
37
|
readonly name: string;
|
|
36
38
|
/** Delivery physics. */
|
|
37
39
|
readonly delivery: SignalDelivery;
|
|
40
|
+
/** Optional human description. */
|
|
41
|
+
readonly description?: string;
|
|
38
42
|
/** Max retries before DLQ. */
|
|
39
43
|
readonly retries: number;
|
|
40
44
|
/** Whether exhausted messages enter the DLQ. */
|
|
@@ -64,6 +68,7 @@ export function signal<T = unknown>(name: string, options: SignalOptions): Signa
|
|
|
64
68
|
return {
|
|
65
69
|
name,
|
|
66
70
|
delivery: options.delivery,
|
|
71
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
67
72
|
retries: options.retries ?? 3,
|
|
68
73
|
deadLetter: options.deadLetter ?? true,
|
|
69
74
|
schema: options.schema,
|
|
@@ -40,6 +40,8 @@ export interface StoreDeclBase {
|
|
|
40
40
|
|
|
41
41
|
/** Options for {@link store.sql}. */
|
|
42
42
|
export interface SqlStoreOptions {
|
|
43
|
+
/** Optional human description for Console / docs (falls back to the store name). */
|
|
44
|
+
readonly description?: string;
|
|
43
45
|
/**
|
|
44
46
|
* Schema tables — okengine {@link TableHandle}s and/or Drizzle tables.
|
|
45
47
|
* Classification is read from TableHandle columns when present.
|
|
@@ -56,6 +58,7 @@ export interface SqlStoreOptions {
|
|
|
56
58
|
export interface SqlStoreDecl extends StoreDeclBase {
|
|
57
59
|
readonly facet: "sql";
|
|
58
60
|
readonly ref: `sql:${string}`;
|
|
61
|
+
readonly description?: string;
|
|
59
62
|
readonly schema?: SqlStoreOptions["schema"];
|
|
60
63
|
readonly classify?: SqlStoreOptions["classify"];
|
|
61
64
|
/**
|
|
@@ -94,6 +97,8 @@ export interface FilesStoreDecl extends StoreDeclBase {
|
|
|
94
97
|
|
|
95
98
|
/** Options for {@link store.index}. */
|
|
96
99
|
export interface IndexStoreOptions {
|
|
100
|
+
/** Optional human description for Console / docs (falls back to the index name). */
|
|
101
|
+
readonly description?: string;
|
|
97
102
|
/** Vector dimensions (default 3 for tests; set explicitly in apps). */
|
|
98
103
|
readonly dims?: number;
|
|
99
104
|
}
|
|
@@ -102,6 +107,7 @@ export interface IndexStoreOptions {
|
|
|
102
107
|
export interface IndexStoreDecl extends StoreDeclBase {
|
|
103
108
|
readonly facet: "index";
|
|
104
109
|
readonly ref: `index:${string}`;
|
|
110
|
+
readonly description?: string;
|
|
105
111
|
readonly dims?: number;
|
|
106
112
|
}
|
|
107
113
|
|
|
@@ -119,6 +125,7 @@ export function sql(name: string, options: SqlStoreOptions = {}): SqlStoreDecl {
|
|
|
119
125
|
facet: "sql",
|
|
120
126
|
name,
|
|
121
127
|
ref: `sql:${name}`,
|
|
128
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
122
129
|
schema: options.schema,
|
|
123
130
|
classify: options.classify,
|
|
124
131
|
table(table) {
|
|
@@ -147,7 +154,7 @@ export function kv(name: string, options: KvStoreOptions = {}): KvStoreDecl {
|
|
|
147
154
|
facet: "kv",
|
|
148
155
|
name,
|
|
149
156
|
ref: `kv:${name}`,
|
|
150
|
-
description: options.description,
|
|
157
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
151
158
|
};
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -162,7 +169,7 @@ export function files(name: string, options: FilesStoreOptions = {}): FilesStore
|
|
|
162
169
|
facet: "files",
|
|
163
170
|
name,
|
|
164
171
|
ref: `files:${name}`,
|
|
165
|
-
description: options.description,
|
|
172
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
166
173
|
};
|
|
167
174
|
}
|
|
168
175
|
|
|
@@ -177,6 +184,7 @@ export function index(name: string, options: IndexStoreOptions = {}): IndexStore
|
|
|
177
184
|
facet: "index",
|
|
178
185
|
name,
|
|
179
186
|
ref: `index:${name}`,
|
|
187
|
+
...(options.description !== undefined ? { description: options.description } : {}),
|
|
180
188
|
dims: options.dims,
|
|
181
189
|
};
|
|
182
190
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end `store.index()` boot wiring — the CONFIGURED driver resolves at
|
|
3
|
+
* real boot for every backend (memory / libsql / pgvector over pglite /
|
|
4
|
+
* pgvector over real Postgres), SQL-backed indexes share store.sql's
|
|
5
|
+
* already-open connection (never a second, redundant one), and search runs
|
|
6
|
+
* through each engine's native ANN index — no JS-side full scan survives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
connectLibsql,
|
|
12
|
+
libsqlDriver,
|
|
13
|
+
libsqlIndexDriver,
|
|
14
|
+
openLibsqlIndex,
|
|
15
|
+
} from "../../drivers/libsql.ts";
|
|
16
|
+
import { connectPglite } from "../../drivers/pglite.ts";
|
|
17
|
+
import { openPgvectorIndex } from "../../drivers/pgvector.ts";
|
|
18
|
+
import { connectPostgres } from "../../drivers/postgres.ts";
|
|
19
|
+
import type { SqlConnection, SqlDriver } from "../../drivers/types.ts";
|
|
20
|
+
import { bindStore } from "../../kernel/boot-bind/store.ts";
|
|
21
|
+
import {
|
|
22
|
+
createStoreRuntime,
|
|
23
|
+
index as declareIndex,
|
|
24
|
+
sql as declareSql,
|
|
25
|
+
type SqlStoreHandle,
|
|
26
|
+
type VectorIndexStoreFxHandle,
|
|
27
|
+
} from "../store.ts";
|
|
28
|
+
|
|
29
|
+
const WRITE_CTX = (name: string) => ({
|
|
30
|
+
effects: { reads: [`index:${name}` as const], writes: [`index:${name}` as const] },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const prev = {
|
|
34
|
+
libsql: process.env.OKE_LIBSQL_URL,
|
|
35
|
+
pglite: process.env.OKE_PGLITE_URL,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (prev.libsql === undefined) delete process.env.OKE_LIBSQL_URL;
|
|
40
|
+
else process.env.OKE_LIBSQL_URL = prev.libsql;
|
|
41
|
+
if (prev.pglite === undefined) delete process.env.OKE_PGLITE_URL;
|
|
42
|
+
else process.env.OKE_PGLITE_URL = prev.pglite;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** Wrap a real connection so the SQL it executes can be asserted on. */
|
|
46
|
+
function recordingConn(conn: SqlConnection): { conn: SqlConnection; statements: string[] } {
|
|
47
|
+
const statements: string[] = [];
|
|
48
|
+
return {
|
|
49
|
+
statements,
|
|
50
|
+
conn: {
|
|
51
|
+
driverId: conn.driverId,
|
|
52
|
+
role: conn.role,
|
|
53
|
+
query: (sql, params) => {
|
|
54
|
+
statements.push(sql);
|
|
55
|
+
return conn.query(sql, params);
|
|
56
|
+
},
|
|
57
|
+
exec: (sql, params) => {
|
|
58
|
+
statements.push(sql);
|
|
59
|
+
return conn.exec(sql, params);
|
|
60
|
+
},
|
|
61
|
+
close: () => conn.close(),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("store.index boot wiring — memory", () => {
|
|
67
|
+
test("boot with no configured index driver resolves memory end to end", async () => {
|
|
68
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
69
|
+
const runtime = bindStore({ stores: [kb] }, "local", () => Date.now());
|
|
70
|
+
const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
|
|
71
|
+
expect(handle.driverId).toBe("memory");
|
|
72
|
+
await handle.upsert("near", [1, 0, 0]);
|
|
73
|
+
await handle.upsert("far", [0, 1, 0]);
|
|
74
|
+
const hits = await handle.search([1, 0, 0], 2);
|
|
75
|
+
expect(hits[0]?.id).toBe("near");
|
|
76
|
+
await runtime.close();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("store.index boot wiring — libsql", () => {
|
|
81
|
+
test("boot: sql=libsql + index=libsql resolves native ANN end to end", async () => {
|
|
82
|
+
process.env.OKE_LIBSQL_URL = ":memory:";
|
|
83
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
84
|
+
const runtime = bindStore(
|
|
85
|
+
{
|
|
86
|
+
config: { drivers: { store: { sql: { local: "libsql" }, index: { local: "libsql" } } } },
|
|
87
|
+
stores: [kb],
|
|
88
|
+
},
|
|
89
|
+
"local",
|
|
90
|
+
() => Date.now(),
|
|
91
|
+
);
|
|
92
|
+
const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
|
|
93
|
+
expect(handle.driverId).toBe("libsql");
|
|
94
|
+
|
|
95
|
+
await handle.upsert("near", [1, 0.1, 0], { label: "near" });
|
|
96
|
+
await handle.upsert("far", [0, 1, 0]);
|
|
97
|
+
await handle.upsert("farther", [-1, 0, 0]);
|
|
98
|
+
const hits = await handle.search([1, 0, 0], 3);
|
|
99
|
+
expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
|
|
100
|
+
expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
|
|
101
|
+
expect(hits[0]!.meta).toEqual({ label: "near" });
|
|
102
|
+
expect(await handle.delete("farther")).toBe(true);
|
|
103
|
+
await runtime.close();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("sql and index facets share one already-open libsql connection", async () => {
|
|
107
|
+
let connects = 0;
|
|
108
|
+
const countingLibsql: SqlDriver = {
|
|
109
|
+
...libsqlDriver,
|
|
110
|
+
connect: async (opts) => {
|
|
111
|
+
connects++;
|
|
112
|
+
return libsqlDriver.connect(opts);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const docs = declareSql("docs");
|
|
116
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
117
|
+
const runtime = createStoreRuntime({
|
|
118
|
+
drivers: { sql: countingLibsql, index: libsqlIndexDriver },
|
|
119
|
+
sql: { docs: { name: "docs", primary: { url: ":memory:" } } },
|
|
120
|
+
index: { kb: { url: ":memory:" } },
|
|
121
|
+
});
|
|
122
|
+
// Index first: it must open the connection the sql facet then reuses.
|
|
123
|
+
const idxHandle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
|
|
124
|
+
await idxHandle.upsert("a", [1, 0, 0]);
|
|
125
|
+
const sqlHandle = (await runtime.open(docs, {
|
|
126
|
+
effects: { reads: ["sql:docs"], writes: ["sql:docs"] },
|
|
127
|
+
})) as SqlStoreHandle;
|
|
128
|
+
await sqlHandle.raw("CREATE TABLE IF NOT EXISTS t_share (id TEXT)", []);
|
|
129
|
+
expect(connects).toBe(1);
|
|
130
|
+
await runtime.close();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("libsql index searches through vector_top_k — real ANN, not a scan", async () => {
|
|
134
|
+
const real = await connectLibsql({ url: ":memory:" });
|
|
135
|
+
const { conn, statements } = recordingConn(real);
|
|
136
|
+
const idx = await openLibsqlIndex({ name: "ann", dims: 3, sql: conn });
|
|
137
|
+
await idx.upsert("x", [1, 0, 0]);
|
|
138
|
+
await idx.upsert("y", [0, 1, 0]);
|
|
139
|
+
const hits = await idx.search([1, 0, 0], 1);
|
|
140
|
+
expect(hits[0]?.id).toBe("x");
|
|
141
|
+
expect(statements.some((s) => s.includes("libsql_vector_idx"))).toBe(true);
|
|
142
|
+
expect(statements.some((s) => s.includes("vector_top_k"))).toBe(true);
|
|
143
|
+
const plan = await real.query(
|
|
144
|
+
`EXPLAIN QUERY PLAN SELECT * FROM vector_top_k('oke_idx_ann_vec', vector32('[1,0,0]'), 1)`,
|
|
145
|
+
);
|
|
146
|
+
expect(JSON.stringify(plan)).toContain("vector_top_k");
|
|
147
|
+
await idx.close();
|
|
148
|
+
await real.close();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("store.index boot wiring — pglite + pgvector", () => {
|
|
153
|
+
test("boot: sql=pglite + index=pgvector resolves end to end", async () => {
|
|
154
|
+
process.env.OKE_PGLITE_URL = "memory://";
|
|
155
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
156
|
+
const runtime = bindStore(
|
|
157
|
+
{
|
|
158
|
+
config: { drivers: { store: { sql: { local: "pglite" }, index: { local: "pgvector" } } } },
|
|
159
|
+
stores: [kb],
|
|
160
|
+
},
|
|
161
|
+
"local",
|
|
162
|
+
() => Date.now(),
|
|
163
|
+
);
|
|
164
|
+
const handle = (await runtime.open(kb, WRITE_CTX("kb"))) as VectorIndexStoreFxHandle;
|
|
165
|
+
expect(handle.driverId).toBe("pgvector");
|
|
166
|
+
|
|
167
|
+
await handle.upsert("near", [1, 0.1, 0], { label: "near" });
|
|
168
|
+
await handle.upsert("far", [0, 1, 0]);
|
|
169
|
+
await handle.upsert("farther", [-1, 0, 0]);
|
|
170
|
+
const hits = await handle.search([1, 0, 0], 3);
|
|
171
|
+
expect(hits.map((h: { id: string }) => h.id)).toEqual(["near", "far", "farther"]);
|
|
172
|
+
expect(hits[0]!.score).toBeGreaterThan(hits[1]!.score);
|
|
173
|
+
expect(hits[0]!.meta).toEqual({ label: "near" });
|
|
174
|
+
expect(await handle.delete("farther")).toBe(true);
|
|
175
|
+
await runtime.close();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("pglite runs the shared pgvector HNSW path — real ANN, not a scan", async () => {
|
|
179
|
+
const real = await connectPglite({ url: "memory://" });
|
|
180
|
+
const { conn, statements } = recordingConn(real);
|
|
181
|
+
const idx = await openPgvectorIndex({ name: "ann", dims: 3, sql: conn });
|
|
182
|
+
await idx.upsert("a", [1, 0, 0]);
|
|
183
|
+
await idx.upsert("b", [0, 1, 0]);
|
|
184
|
+
const hits = await idx.search([1, 0, 0], 2);
|
|
185
|
+
expect(hits[0]?.id).toBe("a");
|
|
186
|
+
expect(statements.some((s) => s.includes("CREATE EXTENSION IF NOT EXISTS vector"))).toBe(true);
|
|
187
|
+
expect(
|
|
188
|
+
statements.some((s) => s.includes("USING hnsw") && s.includes("vector_cosine_ops")),
|
|
189
|
+
).toBe(true);
|
|
190
|
+
expect(statements.some((s) => s.includes("<=>"))).toBe(true);
|
|
191
|
+
const indexes = await real.query(
|
|
192
|
+
`SELECT indexdef FROM pg_indexes WHERE tablename = 'oke_idx_ann'`,
|
|
193
|
+
);
|
|
194
|
+
expect(JSON.stringify(indexes)).toContain("USING hnsw");
|
|
195
|
+
await idx.close();
|
|
196
|
+
await real.close();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("store.index boot wiring — fail loud", () => {
|
|
201
|
+
test("pgvector index with memory sql driver throws — never silently memory", async () => {
|
|
202
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
203
|
+
const runtime = bindStore(
|
|
204
|
+
{
|
|
205
|
+
config: { drivers: { store: { sql: { local: "memory" }, index: { local: "pgvector" } } } },
|
|
206
|
+
stores: [kb],
|
|
207
|
+
},
|
|
208
|
+
"local",
|
|
209
|
+
() => Date.now(),
|
|
210
|
+
);
|
|
211
|
+
await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/cannot share sql driver/);
|
|
212
|
+
await runtime.close();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("libsql index with sqlite sql driver throws", async () => {
|
|
216
|
+
const kb = declareIndex("kb", { dims: 3 });
|
|
217
|
+
const runtime = bindStore(
|
|
218
|
+
{
|
|
219
|
+
config: { drivers: { store: { sql: { local: "sqlite" }, index: { local: "libsql" } } } },
|
|
220
|
+
stores: [kb],
|
|
221
|
+
},
|
|
222
|
+
"local",
|
|
223
|
+
() => Date.now(),
|
|
224
|
+
);
|
|
225
|
+
await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/cannot share sql driver/);
|
|
226
|
+
await runtime.close();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("meilisearch without OKE_STORE_INDEX_URL throws at bind — never sqlUrl, never memory", () => {
|
|
230
|
+
const prev = process.env.OKE_STORE_INDEX_URL;
|
|
231
|
+
delete process.env.OKE_STORE_INDEX_URL;
|
|
232
|
+
try {
|
|
233
|
+
const kb = declareIndex("kb");
|
|
234
|
+
expect(() =>
|
|
235
|
+
bindStore(
|
|
236
|
+
{ config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
|
|
237
|
+
"local",
|
|
238
|
+
() => Date.now(),
|
|
239
|
+
),
|
|
240
|
+
).toThrow(/OKE_STORE_INDEX_URL/);
|
|
241
|
+
} finally {
|
|
242
|
+
if (prev === undefined) delete process.env.OKE_STORE_INDEX_URL;
|
|
243
|
+
else process.env.OKE_STORE_INDEX_URL = prev;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("meilisearch binds its own URL + key (never the sql connection)", async () => {
|
|
248
|
+
const prevUrl = process.env.OKE_STORE_INDEX_URL;
|
|
249
|
+
const prevKey = process.env.OKE_STORE_INDEX_KEY;
|
|
250
|
+
process.env.OKE_STORE_INDEX_URL = "http://127.0.0.1:7700";
|
|
251
|
+
process.env.OKE_STORE_INDEX_KEY = "master-key";
|
|
252
|
+
try {
|
|
253
|
+
const kb = declareIndex("kb");
|
|
254
|
+
const runtime = bindStore(
|
|
255
|
+
{ config: { drivers: { store: { index: { local: "meilisearch" } } } }, stores: [kb] },
|
|
256
|
+
"local",
|
|
257
|
+
() => Date.now(),
|
|
258
|
+
);
|
|
259
|
+
// No live server in unit tests: opening must fail loud (unreachable),
|
|
260
|
+
// proving the binding pointed at HTTP, not silently degraded to memory.
|
|
261
|
+
await expect(runtime.open(kb, WRITE_CTX("kb"))).rejects.toThrow(/meilisearch index/);
|
|
262
|
+
await runtime.close();
|
|
263
|
+
} finally {
|
|
264
|
+
if (prevUrl === undefined) delete process.env.OKE_STORE_INDEX_URL;
|
|
265
|
+
else process.env.OKE_STORE_INDEX_URL = prevUrl;
|
|
266
|
+
if (prevKey === undefined) delete process.env.OKE_STORE_INDEX_KEY;
|
|
267
|
+
else process.env.OKE_STORE_INDEX_KEY = prevKey;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const PGVECTOR_URL = process.env.OKE_TEST_PGVECTOR_URL;
|
|
273
|
+
if (!PGVECTOR_URL) {
|
|
274
|
+
console.log("skip: live pgvector e2e (OKE_TEST_PGVECTOR_URL not set)");
|
|
275
|
+
}
|
|
276
|
+
const liveTest = PGVECTOR_URL ? test : test.skip;
|
|
277
|
+
|
|
278
|
+
describe("store.index boot wiring — real Postgres pgvector", () => {
|
|
279
|
+
liveTest("live Postgres runs real HNSW ANN, not a scan", async () => {
|
|
280
|
+
const real = await connectPostgres({ url: PGVECTOR_URL });
|
|
281
|
+
const { conn, statements } = recordingConn(real);
|
|
282
|
+
const idx = await openPgvectorIndex({ name: "live", dims: 3, sql: conn });
|
|
283
|
+
await idx.upsert("near", [1, 0.1, 0]);
|
|
284
|
+
await idx.upsert("far", [0, 1, 0]);
|
|
285
|
+
await idx.upsert("farther", [-1, 0, 0]);
|
|
286
|
+
const hits = await idx.search([1, 0, 0], 3);
|
|
287
|
+
expect(hits.map((h) => h.id)).toEqual(["near", "far", "farther"]);
|
|
288
|
+
expect(statements.some((s) => s.includes("CREATE EXTENSION IF NOT EXISTS vector"))).toBe(true);
|
|
289
|
+
expect(statements.some((s) => s.includes("USING hnsw"))).toBe(true);
|
|
290
|
+
expect(statements.some((s) => s.includes("<=>"))).toBe(true);
|
|
291
|
+
const indexes = await real.query(
|
|
292
|
+
`SELECT indexdef FROM pg_indexes WHERE tablename = 'oke_idx_live'`,
|
|
293
|
+
);
|
|
294
|
+
expect(JSON.stringify(indexes)).toContain("USING hnsw");
|
|
295
|
+
await idx.delete("farther");
|
|
296
|
+
await idx.close();
|
|
297
|
+
await real.close();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -14,9 +14,16 @@ import type {
|
|
|
14
14
|
KvDriver,
|
|
15
15
|
KvNamespace,
|
|
16
16
|
SqlConnection,
|
|
17
|
+
SqlConnectOptions,
|
|
17
18
|
SqlDriver,
|
|
19
|
+
SqlRole,
|
|
18
20
|
SqlRow,
|
|
21
|
+
TextIndexSearchOptions,
|
|
22
|
+
TextIndexSearchResult,
|
|
23
|
+
VectorIndexDriver,
|
|
24
|
+
VectorIndexStore,
|
|
19
25
|
} from "../../drivers/types.ts";
|
|
26
|
+
import { indexDriverNeedsSql } from "../../drivers/types.ts";
|
|
20
27
|
import { buildClassificationMap, type MaskRowsOptions } from "./classify.ts";
|
|
21
28
|
import {
|
|
22
29
|
createStoreCache,
|
|
@@ -62,7 +69,15 @@ export interface CreateStoreRuntimeOptions {
|
|
|
62
69
|
readonly files?: Readonly<Record<string, { readonly root?: string; readonly client?: unknown }>>;
|
|
63
70
|
/** Index open options keyed by store name. */
|
|
64
71
|
readonly index?: Readonly<
|
|
65
|
-
Record<
|
|
72
|
+
Record<
|
|
73
|
+
string,
|
|
74
|
+
{
|
|
75
|
+
readonly dims?: number;
|
|
76
|
+
readonly url?: string;
|
|
77
|
+
readonly apiKey?: string;
|
|
78
|
+
readonly sql?: SqlConnection;
|
|
79
|
+
}
|
|
80
|
+
>
|
|
66
81
|
>;
|
|
67
82
|
/** Clock for cache TTLs. */
|
|
68
83
|
readonly now?: () => number;
|
|
@@ -113,18 +128,30 @@ export interface FilesStoreFxHandle {
|
|
|
113
128
|
list(prefix?: string): Promise<string[]>;
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
/**
|
|
117
|
-
export interface
|
|
131
|
+
/** Vector index handle on `fx.store`. */
|
|
132
|
+
export interface VectorIndexStoreFxHandle {
|
|
118
133
|
readonly ref: `index:${string}`;
|
|
119
|
-
readonly driverId: "memory" | "pgvector";
|
|
134
|
+
readonly driverId: "memory" | "pgvector" | "libsql";
|
|
120
135
|
upsert(id: string, vector: readonly number[], meta?: Record<string, unknown>): Promise<void>;
|
|
121
136
|
search(
|
|
122
137
|
vector: readonly number[],
|
|
123
138
|
topK?: number,
|
|
124
|
-
): Promise<
|
|
139
|
+
): Promise<ReadonlyArray<{ id: string; score: number; meta?: Record<string, unknown> }>>;
|
|
125
140
|
delete(id: string): Promise<boolean>;
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
/** Full-text index handle on `fx.store`. */
|
|
144
|
+
export interface TextIndexStoreFxHandle {
|
|
145
|
+
readonly ref: `index:${string}`;
|
|
146
|
+
readonly driverId: "meilisearch";
|
|
147
|
+
upsert(id: string, document: Record<string, unknown>): Promise<void>;
|
|
148
|
+
search(q: string, opts?: TextIndexSearchOptions): Promise<TextIndexSearchResult>;
|
|
149
|
+
delete(id: string): Promise<boolean>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Index handle on `fx.store` — discriminated by `driverId`. */
|
|
153
|
+
export type IndexStoreFxHandle = VectorIndexStoreFxHandle | TextIndexStoreFxHandle;
|
|
154
|
+
|
|
128
155
|
/** Store runtime. */
|
|
129
156
|
export interface StoreRuntime {
|
|
130
157
|
/** Shared multi-tier cache. */
|
|
@@ -186,6 +213,19 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
186
213
|
const kvNs = new Map<string, KvNamespace>();
|
|
187
214
|
const fileBuckets = new Map<string, FilesBucket>();
|
|
188
215
|
const indexes = new Map<string, IndexStore>();
|
|
216
|
+
const clientIds = new WeakMap<object, number>();
|
|
217
|
+
let nextClientId = 0;
|
|
218
|
+
|
|
219
|
+
/** Injected test clients with the same URL still get distinct cache entries. */
|
|
220
|
+
function clientTag(client: unknown): string {
|
|
221
|
+
if ((typeof client !== "object" && typeof client !== "function") || client === null) return "";
|
|
222
|
+
let id = clientIds.get(client as object);
|
|
223
|
+
if (id === undefined) {
|
|
224
|
+
id = ++nextClientId;
|
|
225
|
+
clientIds.set(client as object, id);
|
|
226
|
+
}
|
|
227
|
+
return `#c${id}`;
|
|
228
|
+
}
|
|
189
229
|
|
|
190
230
|
function classificationsFor(decl: SqlStoreDecl): ClassificationMap {
|
|
191
231
|
const nested: Record<
|
|
@@ -215,13 +255,7 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
215
255
|
const driver = options.drivers.sql;
|
|
216
256
|
if (!driver) throw new Error("No sql driver configured");
|
|
217
257
|
|
|
218
|
-
const
|
|
219
|
-
let conn = sqlConns.get(cacheKey);
|
|
220
|
-
if (!conn) {
|
|
221
|
-
conn = await driver.connect(target.options);
|
|
222
|
-
sqlConns.set(cacheKey, conn);
|
|
223
|
-
}
|
|
224
|
-
|
|
258
|
+
const conn = await sharedSqlConn(driver, target.role, target.options);
|
|
225
259
|
return createSqlStoreHandle(`sql:${decl.name}`, {
|
|
226
260
|
connection: conn,
|
|
227
261
|
classifications: classificationsFor(decl),
|
|
@@ -231,6 +265,51 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
231
265
|
});
|
|
232
266
|
}
|
|
233
267
|
|
|
268
|
+
/**
|
|
269
|
+
* One connection per driver+role+URL (+ injected client) — `store.sql` and a
|
|
270
|
+
* SQL-backed `store.index` share the same already-open connection, never a
|
|
271
|
+
* second one.
|
|
272
|
+
*/
|
|
273
|
+
async function sharedSqlConn(
|
|
274
|
+
driver: SqlDriver,
|
|
275
|
+
role: SqlRole,
|
|
276
|
+
connectOptions: SqlConnectOptions,
|
|
277
|
+
): Promise<SqlConnection> {
|
|
278
|
+
const cacheKey = `${driver.id}:${role}:${connectOptions.url ?? ""}${clientTag(connectOptions.client)}`;
|
|
279
|
+
let conn = sqlConns.get(cacheKey);
|
|
280
|
+
if (!conn) {
|
|
281
|
+
conn = await driver.connect({ ...connectOptions, role });
|
|
282
|
+
sqlConns.set(cacheKey, conn);
|
|
283
|
+
}
|
|
284
|
+
return conn;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** SQL-backed index drivers borrow the sql facet's connection. */
|
|
288
|
+
async function sqlConnForIndex(
|
|
289
|
+
driver: VectorIndexDriver & { id: "pgvector" | "libsql" },
|
|
290
|
+
url: string | undefined,
|
|
291
|
+
): Promise<SqlConnection> {
|
|
292
|
+
const sqlDriver = options.drivers.sql;
|
|
293
|
+
if (!sqlDriver) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`oke store: index driver "${driver.id}" needs a configured sql driver to share its connection`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const compatible =
|
|
299
|
+
driver.id === "pgvector"
|
|
300
|
+
? sqlDriver.id === "postgres" || sqlDriver.id === "pglite"
|
|
301
|
+
: sqlDriver.id === "libsql";
|
|
302
|
+
if (!compatible) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`oke store: index driver "${driver.id}" cannot share sql driver "${sqlDriver.id}" — ` +
|
|
305
|
+
(driver.id === "pgvector"
|
|
306
|
+
? `pgvector needs store.sql on "postgres" or "pglite"`
|
|
307
|
+
: `libsql index needs store.sql on "libsql"`),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return sharedSqlConn(sqlDriver, "primary", { url });
|
|
311
|
+
}
|
|
312
|
+
|
|
234
313
|
async function openKv(decl: KvStoreDecl): Promise<KvStoreFxHandle> {
|
|
235
314
|
const driver = options.drivers.kv;
|
|
236
315
|
if (!driver) throw new Error("No kv driver configured");
|
|
@@ -288,16 +367,30 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
288
367
|
name: decl.name,
|
|
289
368
|
dims: binding.dims ?? decl.dims ?? 3,
|
|
290
369
|
url: binding.url,
|
|
291
|
-
|
|
370
|
+
apiKey: binding.apiKey,
|
|
371
|
+
sql: indexDriverNeedsSql(driver)
|
|
372
|
+
? (binding.sql ?? (await sqlConnForIndex(driver, binding.url)))
|
|
373
|
+
: undefined,
|
|
292
374
|
});
|
|
293
375
|
indexes.set(decl.name, idx);
|
|
294
376
|
}
|
|
377
|
+
if (idx.driverId === "meilisearch") {
|
|
378
|
+
const text = idx;
|
|
379
|
+
return {
|
|
380
|
+
ref: `index:${decl.name}`,
|
|
381
|
+
driverId: text.driverId,
|
|
382
|
+
upsert: (id, document) => text.upsert(id, document),
|
|
383
|
+
search: (q, opts) => text.search(q, opts),
|
|
384
|
+
delete: (id) => text.delete(id),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const vector = idx as VectorIndexStore;
|
|
295
388
|
return {
|
|
296
389
|
ref: `index:${decl.name}`,
|
|
297
|
-
driverId:
|
|
298
|
-
upsert: (id,
|
|
299
|
-
search: (
|
|
300
|
-
delete: (id) =>
|
|
390
|
+
driverId: vector.driverId,
|
|
391
|
+
upsert: (id, vec, meta) => vector.upsert(id, vec, meta),
|
|
392
|
+
search: (vec, topK) => vector.search(vec, topK),
|
|
393
|
+
delete: (id) => vector.delete(id),
|
|
301
394
|
};
|
|
302
395
|
}
|
|
303
396
|
|
|
@@ -55,6 +55,8 @@ export interface SchemaColumnDecl extends ColumnDef {
|
|
|
55
55
|
readonly tableName?: string;
|
|
56
56
|
/** Foreign key when `.references()` was used. */
|
|
57
57
|
readonly references?: ColumnReference;
|
|
58
|
+
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
59
|
+
readonly description?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Table from {@link store.schema.table} — extends {@link TableHandle}. */
|
|
@@ -75,6 +77,8 @@ export interface FieldBuilder {
|
|
|
75
77
|
retain(duration: string): FieldBuilder;
|
|
76
78
|
/** Override snake_case SQL name. */
|
|
77
79
|
as(sqlName: string): FieldBuilder;
|
|
80
|
+
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
81
|
+
describe(description: string): FieldBuilder;
|
|
78
82
|
/**
|
|
79
83
|
* Declare a foreign key to another column (dialect-agnostic).
|
|
80
84
|
*
|
|
@@ -100,6 +104,7 @@ interface FieldState {
|
|
|
100
104
|
readonly defaultFnKind?: DefaultFnKind;
|
|
101
105
|
readonly classification?: ColumnClassification;
|
|
102
106
|
readonly sqlName?: string;
|
|
107
|
+
readonly description?: string;
|
|
103
108
|
readonly references?: ColumnReference;
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -145,6 +150,7 @@ function createBuilder(state: FieldState): FieldBuilder {
|
|
|
145
150
|
retain: (duration) =>
|
|
146
151
|
next({ classification: mergeClassification(state.classification, { retain: duration }) }),
|
|
147
152
|
as: (sqlName) => next({ sqlName }),
|
|
153
|
+
describe: (description) => next({ description }),
|
|
148
154
|
references: (ref, actions) =>
|
|
149
155
|
next({
|
|
150
156
|
references: actions ? { ref, actions } : { ref },
|
|
@@ -163,6 +169,7 @@ function createBuilder(state: FieldState): FieldBuilder {
|
|
|
163
169
|
...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
|
|
164
170
|
...(state.defaultFnKind ? { defaultFnKind: state.defaultFnKind } : {}),
|
|
165
171
|
...(state.classification ? { classification: state.classification } : {}),
|
|
172
|
+
...(state.description !== undefined ? { description: state.description } : {}),
|
|
166
173
|
...(state.references ? { references: state.references } : {}),
|
|
167
174
|
};
|
|
168
175
|
},
|
package/src/elements/store.ts
CHANGED
|
@@ -116,6 +116,8 @@ export type {
|
|
|
116
116
|
KvStoreFxHandle,
|
|
117
117
|
FilesStoreFxHandle,
|
|
118
118
|
IndexStoreFxHandle,
|
|
119
|
+
VectorIndexStoreFxHandle,
|
|
120
|
+
TextIndexStoreFxHandle,
|
|
119
121
|
} from "./store/runtime.ts";
|
|
120
122
|
|
|
121
123
|
export { fileKeyWarnings, contentAddressedKey, projectFileKeys } from "./store/files-policy.ts";
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { describe, expect, test } from "bun:test";
|
|
10
10
|
import { envVaultDriver, memoryVaultDriver } from "../drivers/index.ts";
|
|
11
11
|
import { createFx } from "../kernel/fx.ts";
|
|
12
|
+
import { REDACTED_PLACEHOLDER, Redacted } from "../kernel/redacted.ts";
|
|
12
13
|
import {
|
|
13
14
|
createVaultRuntime,
|
|
14
15
|
fingerprintSecretSync,
|
|
@@ -200,17 +201,39 @@ describe("redaction + fingerprints", () => {
|
|
|
200
201
|
const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
|
|
201
202
|
const { fx } = createFxContextWithVault(runtime, lines);
|
|
202
203
|
|
|
203
|
-
const
|
|
204
|
-
expect(
|
|
204
|
+
const wrapped = fx.vault("STRIPE_KEY");
|
|
205
|
+
expect(wrapped.reveal()).toBe(secret);
|
|
205
206
|
|
|
206
|
-
fx.log.info(`charging with ${
|
|
207
|
+
fx.log.info(`charging with ${wrapped}`, { key: wrapped, nested: { k: wrapped } });
|
|
208
|
+
// Revealed cleartext still hits the known-value substring scrub.
|
|
207
209
|
fx.log.error(secret, { stripe: secret });
|
|
208
210
|
|
|
209
211
|
for (const line of lines) {
|
|
210
212
|
expect(line.message).not.toContain(secret);
|
|
211
213
|
expect(JSON.stringify(line.data ?? {})).not.toContain(secret);
|
|
212
|
-
expect(line.message).toContain(SECRET_MASK);
|
|
213
214
|
}
|
|
215
|
+
expect(lines[0]!.message).toContain(REDACTED_PLACEHOLDER);
|
|
216
|
+
expect(lines[1]!.message).toContain(SECRET_MASK);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("Redacted from fx.vault is masked in fx.log without a vault runtime", () => {
|
|
220
|
+
const secret = "sk_no_vault_runtime";
|
|
221
|
+
const lines: Array<{ message: string; data?: Record<string, unknown> }> = [];
|
|
222
|
+
const fx = createFx({
|
|
223
|
+
flow: "test.redacted",
|
|
224
|
+
secrets: { STRIPE_KEY: secret },
|
|
225
|
+
onLog: (_level, message, data) => lines.push({ message, data }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const wrapped = fx.vault("STRIPE_KEY");
|
|
229
|
+
expect(wrapped).toBeInstanceOf(Redacted);
|
|
230
|
+
expect(String(wrapped)).toBe(REDACTED_PLACEHOLDER);
|
|
231
|
+
expect(`${wrapped}`).toBe(REDACTED_PLACEHOLDER);
|
|
232
|
+
expect(JSON.stringify({ v: wrapped })).toBe(`{"v":"${REDACTED_PLACEHOLDER}"}`);
|
|
233
|
+
|
|
234
|
+
fx.log.info("paying", { key: wrapped, nested: { k: [wrapped] } });
|
|
235
|
+
expect(JSON.stringify(lines[0]!.data)).not.toContain(secret);
|
|
236
|
+
expect(lines[0]!.data?.key).toBe(REDACTED_PLACEHOLDER);
|
|
214
237
|
});
|
|
215
238
|
|
|
216
239
|
test("fingerprint is stable and not the value", async () => {
|