okengine 0.11.1 → 0.11.2

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.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/ai.mdx +22 -1
  3. package/site/content/docs/elements/store.mdx +3 -1
  4. package/site/content/docs/elements/vault.mdx +19 -11
  5. package/site/content/docs/get-started/installation.mdx +7 -1
  6. package/site/content/docs/recipes/llama-cpp.mdx +10 -9
  7. package/site/content/docs/reference/cli.md +6 -2
  8. package/site/content/docs/reference/environment-variables.mdx +9 -9
  9. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  10. package/src/cli/ai-setup/apply.ts +61 -1
  11. package/src/cli/ask-seed.test.ts +4 -3
  12. package/src/cli/ask-seed.ts +5 -6
  13. package/src/cli/client-add.test.ts +2 -1
  14. package/src/cli/dev.test.ts +116 -0
  15. package/src/cli/dev.ts +107 -18
  16. package/src/cli/project-state.test.ts +50 -0
  17. package/src/cli/project-state.ts +123 -0
  18. package/src/cli/vault-cmd.test.ts +47 -18
  19. package/src/cli/vault-cmd.ts +2 -1
  20. package/src/compiler/extract.ts +12 -1
  21. package/src/console/server/console.test.ts +3 -1
  22. package/src/console/server/operator-db.test.ts +48 -17
  23. package/src/console/server/operator-db.ts +5 -1
  24. package/src/docker/derive.ts +24 -3
  25. package/src/docker/docker.test.ts +4 -2
  26. package/src/docker/index.ts +1 -0
  27. package/src/docker/recipes/index.ts +1 -0
  28. package/src/docker/recipes/llama-cpp.ts +20 -4
  29. package/src/drivers/ai-openai-compatible.ts +15 -3
  30. package/src/drivers/vault-builtin.test.ts +50 -42
  31. package/src/elements/ai/declare.ts +73 -3
  32. package/src/elements/ai/errors.test.ts +35 -0
  33. package/src/elements/ai/errors.ts +139 -0
  34. package/src/elements/ai/eval.ts +26 -1
  35. package/src/elements/ai/runtime.ts +140 -80
  36. package/src/elements/ai/tools.test.ts +1 -1
  37. package/src/elements/ai.test.ts +99 -2
  38. package/src/elements/ai.ts +11 -1
  39. package/src/elements/index.ts +2 -0
  40. package/src/elements/store/index-boot.test.ts +23 -6
  41. package/src/elements/store/resource.test.ts +38 -19
  42. package/src/elements/store/sql-session.test.ts +55 -58
  43. package/src/elements/vault/builtin-adapter.test.ts +115 -58
  44. package/src/elements/vault/builtin-adapter.ts +241 -47
  45. package/src/elements/vault/chaos-child.ts +424 -0
  46. package/src/elements/vault/chaos.test.ts +651 -0
  47. package/src/elements/vault/resilience.ts +6 -1
  48. package/src/elements/vault/security-checklist.test.ts +10 -8
  49. package/src/elements/vault/storage.ts +130 -27
  50. package/src/elements/vault/test-helpers.ts +368 -0
  51. package/src/elements/vault.ts +6 -0
  52. package/src/index.ts +2 -0
  53. package/src/kernel/app.ts +83 -10
  54. package/src/kernel/auto-registry.test.ts +52 -1
  55. package/src/kernel/element-registries.ts +19 -4
  56. package/src/kernel/errors.ts +3 -3
  57. package/src/kernel/fx.test.ts +25 -0
  58. package/src/kernel/fx.ts +4 -1
  59. package/src/manifest/types.ts +4 -0
  60. package/src/test/create-test-app.ts +16 -11
  61. package/src/test/reset-element-registries.ts +17 -9
@@ -6,7 +6,7 @@
6
6
  * native ANN index — no JS-side full scan survives.
7
7
  */
8
8
 
9
- import { afterEach, describe, expect, test } from "bun:test";
9
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
10
10
  import { connectPglite } from "../../drivers/pglite.ts";
11
11
  import { openPgvectorIndex } from "../../drivers/pgvector.ts";
12
12
  import { connectPostgres } from "../../drivers/postgres.ts";
@@ -22,6 +22,17 @@ const prev = {
22
22
  pglite: process.env.OKE_PGLITE_URL,
23
23
  };
24
24
 
25
+ /** Warmed PGlite for direct openPgvectorIndex cases (boot path opens its own). */
26
+ let sharedPglite: SqlConnection | undefined;
27
+
28
+ beforeAll(async () => {
29
+ sharedPglite = await connectPglite({ url: "memory://index-boot-shared" });
30
+ }, 15_000);
31
+
32
+ afterAll(async () => {
33
+ await sharedPglite?.close();
34
+ });
35
+
25
36
  afterEach(() => {
26
37
  if (prev.pglite === undefined) delete process.env.OKE_PGLITE_URL;
27
38
  else process.env.OKE_PGLITE_URL = prev.pglite;
@@ -43,7 +54,8 @@ function recordingConn(conn: SqlConnection): { conn: SqlConnection; statements:
43
54
  statements.push(sql);
44
55
  return conn.exec(sql, params);
45
56
  },
46
- close: () => conn.close(),
57
+ // Never close the underlying shared PGlite — afterAll owns that.
58
+ close: async () => {},
47
59
  },
48
60
  };
49
61
  }
@@ -63,12 +75,16 @@ describe("store.index boot wiring — memory", () => {
63
75
  });
64
76
 
65
77
  describe("store.index boot wiring — pglite + pgvector", () => {
78
+ // bindStore opens its own PGlite — cannot inject the shared instance; allow
79
+ // cold WASM under suite contention (same budget as sql conformance).
66
80
  test("boot: sql=pglite + index=pgvector resolves end to end", async () => {
67
81
  process.env.OKE_PGLITE_URL = "memory://";
68
82
  const kb = declareIndex("kb", { dims: 3 });
69
83
  const runtime = bindStore(
70
84
  {
71
- config: { drivers: { store: { sql: { test: "pglite" }, index: { test: "pgvector" } } } },
85
+ config: {
86
+ drivers: { store: { sql: { test: "pglite" }, index: { test: "pgvector" } } },
87
+ },
72
88
  stores: [kb],
73
89
  },
74
90
  "test",
@@ -86,10 +102,11 @@ describe("store.index boot wiring — pglite + pgvector", () => {
86
102
  expect(hits[0]!.meta).toEqual({ label: "near" });
87
103
  expect(await handle.delete("farther")).toBe(true);
88
104
  await runtime.close();
89
- });
105
+ }, 15_000);
90
106
 
91
107
  test("pglite runs the shared pgvector HNSW path — real ANN, not a scan", async () => {
92
- const real = await connectPglite({ url: "memory://" });
108
+ const real = sharedPglite!;
109
+ await real.exec(`DROP TABLE IF EXISTS oke_idx_ann CASCADE`);
93
110
  const { conn, statements } = recordingConn(real);
94
111
  const idx = await openPgvectorIndex({ name: "ann", dims: 3, sql: conn });
95
112
  await idx.upsert("a", [1, 0, 0]);
@@ -106,7 +123,7 @@ describe("store.index boot wiring — pglite + pgvector", () => {
106
123
  );
107
124
  expect(JSON.stringify(indexes)).toContain("USING hnsw");
108
125
  await idx.close();
109
- await real.close();
126
+ // shared connection — closed in afterAll
110
127
  });
111
128
  });
112
129
 
@@ -5,18 +5,19 @@
5
5
  * client-facing key.
6
6
  */
7
7
 
8
- import { afterEach, describe, expect, test } from "bun:test";
8
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
9
  import { integer, pgTable, text } from "drizzle-orm/pg-core";
10
10
  import { createInsertSchema, createSelectSchema } from "drizzle-zod";
11
11
  import { z } from "zod";
12
12
  import { pgliteDriver } from "../../drivers/pglite.ts";
13
+ import type { SqlConnection } from "../../drivers/types.ts";
13
14
  import { oke } from "../../kernel/app.ts";
14
15
  import { resetFlowSeq } from "../../kernel/flow.ts";
15
16
  import { on, resetBindings } from "../../kernel/on.ts";
16
17
  import { http } from "../../kernel/triggers.ts";
17
18
  import { createTestApp } from "../../test/create-test-app.ts";
18
19
  import { classify, field, id, now, PII_MASK, store } from "../store.ts";
19
- import { createSqlStoreHandle } from "./sql-session.ts";
20
+ import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
20
21
  import { mapRowToJs, resolveColumns } from "./table.ts";
21
22
 
22
23
  afterEach(() => {
@@ -24,6 +25,18 @@ afterEach(() => {
24
25
  resetFlowSeq();
25
26
  });
26
27
 
28
+ /** Posts table for the JS-key exit lock (needs real Postgres dialect via PGlite). */
29
+ const jsKeyPosts = pgTable("posts", {
30
+ id: text("id").primaryKey(),
31
+ title: text("title").notNull(),
32
+ createdAt: integer("created_at").notNull(),
33
+ });
34
+
35
+ /** File-scoped warmed PGlite for the SqlStoreHandle exit describe. */
36
+ let sharedConn: SqlConnection;
37
+ /** Handle over {@link sharedConn}. */
38
+ let sharedHandle: SqlStoreHandle;
39
+
27
40
  describe("mapRowToJs — declared keys only", () => {
28
41
  const posts = pgTable("posts", {
29
42
  id: text("id").primaryKey(),
@@ -47,40 +60,46 @@ describe("mapRowToJs — declared keys only", () => {
47
60
  });
48
61
 
49
62
  describe("SqlStoreHandle exit — list/get/create return JS keys only", () => {
50
- const posts = pgTable("posts", {
51
- id: text("id").primaryKey(),
52
- title: text("title").notNull(),
53
- createdAt: integer("created_at").notNull(),
54
- });
55
-
56
- test("select / findById / insert.returning never emit created_at", async () => {
57
- const conn = await pgliteDriver.connect({
58
- url: "memory://",
63
+ // Real Postgres dialect (PGlite) — share one warmed instance; TRUNCATE between tests.
64
+ beforeAll(async () => {
65
+ sharedConn = await pgliteDriver.connect({
66
+ url: "memory://resource-js-keys-shared",
59
67
  role: "primary",
60
68
  });
61
- const handle = createSqlStoreHandle("sql:app", {
62
- connection: conn,
69
+ sharedHandle = createSqlStoreHandle("sql:app", {
70
+ connection: sharedConn,
63
71
  classifications: new Map(),
64
72
  routedRole: "primary",
65
73
  domainDdl: "ensure",
66
74
  });
75
+ // Warm DDL via the insert path (pgTable is not a TableHandle for ensureTable).
76
+ await sharedHandle.insert(jsKeyPosts).values({ id: "_warmup", title: "x", createdAt: 0 });
77
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
78
+ }, 15_000);
79
+
80
+ afterAll(async () => {
81
+ await sharedConn.close();
82
+ });
67
83
 
68
- const [created] = await handle
69
- .insert(posts)
84
+ beforeEach(async () => {
85
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
86
+ });
87
+
88
+ test("select / findById / insert.returning never emit created_at", async () => {
89
+ const [created] = await sharedHandle
90
+ .insert(jsKeyPosts)
70
91
  .values({ id: "p1", title: "one", createdAt: 100 })
71
92
  .returning();
72
93
  expect(Object.keys(created!).sort()).toEqual(["createdAt", "id", "title"]);
73
94
  expect("created_at" in created!).toBe(false);
74
95
 
75
- const listed = await handle.select().from(posts);
96
+ const listed = await sharedHandle.select().from(jsKeyPosts);
76
97
  expect(Object.keys(listed[0]!).sort()).toEqual(["createdAt", "id", "title"]);
77
98
  expect("created_at" in listed[0]!).toBe(false);
78
99
 
79
- const got = await handle.findById(posts, "p1");
100
+ const got = await sharedHandle.findById(jsKeyPosts, "p1");
80
101
  expect(Object.keys(got!).sort()).toEqual(["createdAt", "id", "title"]);
81
102
  expect("created_at" in got!).toBe(false);
82
-
83
- await conn.close();
84
103
  });
85
104
  });
86
105
 
@@ -4,9 +4,13 @@
4
4
  * Relational `with:` / Drizzle RQB (`db.query.*.findMany({ with })`) is a
5
5
  * documented limitation: effects, cache keys, and PII masking assume one
6
6
  * table per call, so no relational query surface may appear on the handle.
7
+ *
8
+ * Upsert / orderBy / like need the real Postgres dialect (PGlite). One warmed
9
+ * in-memory PGlite is shared for the whole file (cold WASM once); TRUNCATE
10
+ * isolates each test without a fresh `PGlite.create`.
7
11
  */
8
12
 
9
- import { describe, expect, test } from "bun:test";
13
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
14
  import { and, desc, eq, like, lt, or } from "drizzle-orm";
11
15
  import { integer, pgTable, text } from "drizzle-orm/pg-core";
12
16
  import { pgliteDriver } from "../../drivers/pglite.ts";
@@ -19,31 +23,38 @@ const posts = pgTable("posts", {
19
23
  createdAt: integer("created_at").notNull(),
20
24
  });
21
25
 
22
- async function openHandle(): Promise<{ handle: SqlStoreHandle; conn: SqlConnection }> {
23
- const conn = await pgliteDriver.connect({
24
- url: "memory://",
26
+ /** File-scoped warmed PGlite opened in {@link beforeAll}. */
27
+ let sharedConn: SqlConnection;
28
+ /** Handle over {@link sharedConn}. */
29
+ let sharedHandle: SqlStoreHandle;
30
+
31
+ beforeAll(async () => {
32
+ sharedConn = await pgliteDriver.connect({
33
+ url: "memory://sql-session-shared",
25
34
  role: "primary",
26
35
  });
27
- const handle = createSqlStoreHandle("sql:app", {
28
- connection: conn,
36
+ sharedHandle = createSqlStoreHandle("sql:app", {
37
+ connection: sharedConn,
29
38
  classifications: new Map(),
30
39
  routedRole: "primary",
31
40
  domainDdl: "ensure",
32
41
  });
33
- return { handle, conn };
34
- }
42
+ // Warm DDL via the insert path (pgTable is not a TableHandle for ensureTable).
43
+ await sharedHandle.insert(posts).values({ id: "_warmup", title: "x", createdAt: 0 });
44
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
45
+ }, 15_000);
46
+
47
+ afterAll(async () => {
48
+ await sharedConn.close();
49
+ });
50
+
51
+ beforeEach(async () => {
52
+ await sharedConn.exec(`TRUNCATE "posts" RESTART IDENTITY CASCADE`);
53
+ });
35
54
 
36
55
  describe("SqlStoreHandle — no relational query surface (path b)", () => {
37
- test("created handle exposes exactly the single-table surface", async () => {
38
- const conn = await pgliteDriver.connect({ url: "memory://", role: "primary" });
39
- const handle = createSqlStoreHandle("sql:app", {
40
- connection: conn,
41
- classifications: new Map(),
42
- routedRole: "primary",
43
- domainDdl: "ensure",
44
- });
45
-
46
- expect(Object.keys(handle).sort()).toEqual(
56
+ test("created handle exposes exactly the single-table surface", () => {
57
+ expect(Object.keys(sharedHandle).sort()).toEqual(
47
58
  [
48
59
  "ref",
49
60
  "routedRole",
@@ -64,47 +75,40 @@ describe("SqlStoreHandle — no relational query surface (path b)", () => {
64
75
  );
65
76
 
66
77
  // No Drizzle RQB / with: surface — ever.
67
- expect("query" in handle).toBe(false);
68
- expect("findMany" in handle).toBe(false);
69
- expect("findFirst" in handle).toBe(false);
70
- expect("with" in handle).toBe(false);
71
-
72
- await conn.close();
78
+ expect("query" in sharedHandle).toBe(false);
79
+ expect("findMany" in sharedHandle).toBe(false);
80
+ expect("findFirst" in sharedHandle).toBe(false);
81
+ expect("with" in sharedHandle).toBe(false);
73
82
  });
74
83
  });
75
84
 
76
85
  describe("SqlStoreHandle — orderBy / limit select chain", () => {
77
86
  test("orderBy + limit without where", async () => {
78
- const { handle, conn } = await openHandle();
79
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
80
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
81
- await handle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
87
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
88
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
89
+ await sharedHandle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
82
90
 
83
- const rows = await handle.select().from(posts).orderBy(desc(posts.createdAt)).limit(2);
91
+ const rows = await sharedHandle.select().from(posts).orderBy(desc(posts.createdAt)).limit(2);
84
92
  expect(rows.map((r) => r.id)).toEqual(["p2", "p3"]);
85
- await conn.close();
86
93
  });
87
94
 
88
95
  test("limit directly on from() — no where required", async () => {
89
- const { handle, conn } = await openHandle();
90
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
91
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
96
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
97
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
92
98
 
93
- const rows = await handle.select().from(posts).limit(1);
99
+ const rows = await sharedHandle.select().from(posts).limit(1);
94
100
  expect(rows).toHaveLength(1);
95
- await conn.close();
96
101
  });
97
102
 
98
103
  test("composite cursor predicate: where → orderBy → limit", async () => {
99
- const { handle, conn } = await openHandle();
100
- await handle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
101
- await handle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
102
- await handle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
104
+ await sharedHandle.insert(posts).values({ id: "p1", title: "one", createdAt: 100 });
105
+ await sharedHandle.insert(posts).values({ id: "p2", title: "two", createdAt: 300 });
106
+ await sharedHandle.insert(posts).values({ id: "p3", title: "three", createdAt: 200 });
103
107
  // Tie on createdAt=200 with an id that sorts after the cursor id.
104
- await handle.insert(posts).values({ id: "p0", title: "four", createdAt: 200 });
108
+ await sharedHandle.insert(posts).values({ id: "p0", title: "four", createdAt: 200 });
105
109
 
106
110
  const order = [desc(posts.createdAt), desc(posts.id)] as const;
107
- const page1 = await handle
111
+ const page1 = await sharedHandle
108
112
  .select()
109
113
  .from(posts)
110
114
  .orderBy(...order)
@@ -112,61 +116,54 @@ describe("SqlStoreHandle — orderBy / limit select chain", () => {
112
116
  expect(page1.map((r) => r.id)).toEqual(["p2", "p3"]);
113
117
 
114
118
  // (createdAt, id) < (200, "p3") — the second page of a keyset cursor.
115
- const page2 = await handle
119
+ const page2 = await sharedHandle
116
120
  .select()
117
121
  .from(posts)
118
122
  .where(or(lt(posts.createdAt, 200), and(eq(posts.createdAt, 200), lt(posts.id, "p3"))))
119
123
  .orderBy(...order)
120
124
  .limit(2);
121
125
  expect(page2.map((r) => r.id)).toEqual(["p0", "p1"]);
122
- await conn.close();
123
126
  });
124
127
 
125
128
  test("like filters rows through the session (postgres case-sensitive)", async () => {
126
- const { handle, conn } = await openHandle();
127
- await handle.insert(posts).values({ id: "p1", title: "alpha", createdAt: 100 });
128
- await handle.insert(posts).values({ id: "p2", title: "bravo", createdAt: 200 });
129
+ await sharedHandle.insert(posts).values({ id: "p1", title: "alpha", createdAt: 100 });
130
+ await sharedHandle.insert(posts).values({ id: "p2", title: "bravo", createdAt: 200 });
129
131
 
130
- const rows = await handle.select().from(posts).where(like(posts.title, "a%"));
132
+ const rows = await sharedHandle.select().from(posts).where(like(posts.title, "a%"));
131
133
  expect(rows.map((r) => r.id)).toEqual(["p1"]);
132
- await conn.close();
133
134
  });
134
135
  });
135
136
 
136
137
  describe("SqlStoreHandle — upsert", () => {
137
138
  test("default inserts once then already-existed without touching the row", async () => {
138
- const { handle, conn } = await openHandle();
139
- const first = await handle.upsert(
139
+ const first = await sharedHandle.upsert(
140
140
  posts,
141
141
  { id: "welcome" },
142
142
  { id: "welcome", title: "Hello", createdAt: 1 },
143
143
  );
144
144
  expect(first.status).toBe("upserted");
145
145
 
146
- const second = await handle.upsert(
146
+ const second = await sharedHandle.upsert(
147
147
  posts,
148
148
  { id: "welcome" },
149
149
  { id: "welcome", title: "Changed", createdAt: 2 },
150
150
  );
151
151
  expect(second.status).toBe("already-existed");
152
152
 
153
- const row = await handle.findById(posts, "welcome");
153
+ const row = await sharedHandle.findById(posts, "welcome");
154
154
  expect(row).toEqual({ id: "welcome", title: "Hello", createdAt: 1 });
155
- await conn.close();
156
155
  });
157
156
 
158
157
  test("onExisting update changes matched columns", async () => {
159
- const { handle, conn } = await openHandle();
160
- await handle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
161
- const updated = await handle.upsert(
158
+ await sharedHandle.upsert(posts, { id: "n1" }, { id: "n1", title: "one", createdAt: 10 });
159
+ const updated = await sharedHandle.upsert(
162
160
  posts,
163
161
  { id: "n1" },
164
162
  { id: "n1", title: "two", createdAt: 20 },
165
163
  { onExisting: "update" },
166
164
  );
167
165
  expect(updated.status).toBe("changed");
168
- const row = await handle.findById(posts, "n1");
166
+ const row = await sharedHandle.findById(posts, "n1");
169
167
  expect(row).toEqual({ id: "n1", title: "two", createdAt: 20 });
170
- await conn.close();
171
168
  });
172
169
  });
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Runs against a real PGlite instance so the SQL (DDL, `bytea` round-trip,
5
5
  * `DISTINCT ON`, `jsonb`) is exercised rather than mocked.
6
+ *
7
+ * One warmed in-memory PGlite is shared for the whole file (cold WASM once);
8
+ * {@link resetVaultTables} isolates each test without a fresh `PGlite.create`.
6
9
  */
7
10
 
8
- import { describe, expect, test } from "bun:test";
11
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
12
  import { connectPglite } from "../../drivers/pglite.ts";
10
13
  import type { SqlConnection } from "../../drivers/types.ts";
11
14
  import {
@@ -16,10 +19,22 @@ import {
16
19
  import { generateMasterKey, masterKeyToBase64 } from "./crypto.ts";
17
20
  import { isVaultError, VaultError } from "./errors.ts";
18
21
  import type { SqlExec } from "./storage.ts";
22
+ import { resetVaultTables } from "./test-helpers.ts";
19
23
 
20
24
  const PATH = "prod/api/stripe";
21
25
  const VALUE = "sk_live_do_not_log_me";
22
26
 
27
+ /** File-scoped warmed PGlite — opened in {@link beforeAll}. */
28
+ let sharedConn: SqlConnection;
29
+
30
+ beforeAll(async () => {
31
+ sharedConn = await connectPglite({ url: "memory://vault-adapter-shared" });
32
+ }, 15_000);
33
+
34
+ afterAll(async () => {
35
+ await sharedConn.close();
36
+ });
37
+
23
38
  /**
24
39
  * Adapt a driver connection to the Vault's {@link SqlExec} surface.
25
40
  *
@@ -29,7 +44,7 @@ function asExec(conn: SqlConnection): SqlExec {
29
44
  return sqlConnectionAsExec(conn);
30
45
  }
31
46
 
32
- /** An adapter over a fresh in-memory PGlite, plus its teardown. */
47
+ /** An initialized, unsealed adapter over the shared PGlite, plus a no-op close. */
33
48
  interface Harness {
34
49
  readonly adapter: BuiltinVaultAdapter;
35
50
  readonly db: SqlExec;
@@ -38,13 +53,13 @@ interface Harness {
38
53
  }
39
54
 
40
55
  /**
41
- * Boot an initialized, unsealed adapter over a fresh PGlite instance.
56
+ * Boot an initialized, unsealed adapter over the shared PGlite (tables reset).
42
57
  *
43
58
  * @param options - Rewrap batch size for rotation tests
44
59
  */
45
60
  async function harness(options: { batchSize?: number } = {}): Promise<Harness> {
46
- const conn = await connectPglite({ url: "memory://vault-test" });
47
- const db = asExec(conn);
61
+ await resetVaultTables(sharedConn);
62
+ const db = asExec(sharedConn);
48
63
  const adapter = createBuiltinVaultAdapter({
49
64
  db,
50
65
  ...(options.batchSize === undefined ? {} : { kekRewrapBatchSize: options.batchSize }),
@@ -55,31 +70,29 @@ async function harness(options: { batchSize?: number } = {}): Promise<Harness> {
55
70
  adapter,
56
71
  db,
57
72
  masterKey: init.masterKey,
58
- close: () => conn.close(),
73
+ async close() {
74
+ /* shared connection — closed in afterAll */
75
+ },
59
76
  };
60
77
  }
61
78
 
62
79
  describe("builtin vault adapter — lifecycle", () => {
63
80
  test("initialize returns the master key once and refuses a second run", async () => {
64
- const conn = await connectPglite({ url: "memory://vault-test" });
65
- try {
66
- const adapter = createBuiltinVaultAdapter({ db: asExec(conn) });
67
- const init = await adapter.initialize();
81
+ await resetVaultTables(sharedConn);
82
+ const adapter = createBuiltinVaultAdapter({ db: asExec(sharedConn) });
83
+ const init = await adapter.initialize();
68
84
 
69
- expect(init.masterKey).toMatch(/^[A-Za-z0-9+/]+=*$/);
70
- expect(init.verifyHash).toMatch(/^[0-9a-f]{64}$/);
71
- expect(init.kekVersion).toBe(1);
85
+ expect(init.masterKey).toMatch(/^[A-Za-z0-9+/]+=*$/);
86
+ expect(init.verifyHash).toMatch(/^[0-9a-f]{64}$/);
87
+ expect(init.kekVersion).toBe(1);
72
88
 
73
- const status = await adapter.status();
74
- expect(status.initialized).toBe(true);
75
- expect(status.sealed).toBe(true);
76
- expect(status.masterKeyPresent).toBe(true);
89
+ const status = await adapter.status();
90
+ expect(status.initialized).toBe(true);
91
+ expect(status.sealed).toBe(true);
92
+ expect(status.masterKeyPresent).toBe(true);
77
93
 
78
- const failure = await adapter.initialize().catch((e: unknown) => e);
79
- expect(isVaultError(failure, "ALREADY_INITIALIZED")).toBe(true);
80
- } finally {
81
- await conn.close();
82
- }
94
+ const failure = await adapter.initialize().catch((e: unknown) => e);
95
+ expect(isVaultError(failure, "ALREADY_INITIALIZED")).toBe(true);
83
96
  });
84
97
 
85
98
  test("unseal + set/get round-trips a secret", async () => {
@@ -303,51 +316,94 @@ describe("builtin vault adapter — master rotation", () => {
303
316
  }
304
317
  });
305
318
 
306
- test("continueRotateMaster cold-resumes from rewrap_key_hash after process death", async () => {
307
- const conn = await connectPglite({ url: "memory://vault-cold-resume" });
308
- const db = asExec(conn);
319
+ test("rotateMaster: concurrent callers exactly one wins, loser fails before any DEK rewrite", async () => {
320
+ const h = await harness({ batchSize: 2 });
309
321
  try {
310
- const first = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
311
- const init = await first.initialize();
312
- await first.unseal(init.masterKey);
313
- for (let i = 0; i < 5; i += 1) {
314
- await first.set(`prod/api/key-${i}`, `value-${i}`);
322
+ for (let i = 0; i < 6; i += 1) {
323
+ await h.adapter.set(`prod/race/${i}`, `v-${i}`);
315
324
  }
325
+ const other = createBuiltinVaultAdapter({
326
+ db: h.db,
327
+ kekRewrapBatchSize: 2,
328
+ rotateLeaseMs: 5_000,
329
+ });
330
+ await other.unseal(h.masterKey);
316
331
 
317
- const next = generateMasterKey();
318
- const nextB64 = masterKeyToBase64(next);
319
- let progress = await first.rotateMaster(next);
320
- expect(progress.remaining).toBe(3);
321
- expect((await first.status()).rewrapTargetKekVersion).toBe(2);
332
+ const keyA = generateMasterKey();
333
+ const keyB = generateMasterKey();
334
+ const before = await h.db.query<{ n: string }>(
335
+ `SELECT COUNT(*)::text AS n FROM oke_vault_keys WHERE kek_version = 2`,
336
+ );
337
+ expect(before[0]?.n).toBe("0");
322
338
 
323
- // Simulate process death: drop in-memory pending, reopen over the same DB.
324
- const second = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
325
- await second.unseal(init.masterKey);
326
- expect((await second.status()).rewrapTargetKekVersion).toBe(2);
339
+ const [ra, rb] = await Promise.allSettled([
340
+ h.adapter.rotateMaster(keyA),
341
+ other.rotateMaster(keyB),
342
+ ]);
343
+ expect([ra, rb].filter((r) => r.status === "fulfilled")).toHaveLength(1);
344
+ expect([ra, rb].filter((r) => r.status === "rejected")).toHaveLength(1);
327
345
 
328
- const wrong = await second.continueRotateMaster().catch((e: unknown) => e);
329
- expect(isVaultError(wrong, "INVALID_KEY")).toBe(true);
346
+ const loser = ra.status === "rejected" ? ra : rb;
347
+ expect(
348
+ loser.status === "rejected" && loser.reason instanceof Error ? loser.reason.message : "",
349
+ ).toMatch(/lease held|already in progress/i);
330
350
 
331
- progress = await second.continueRotateMaster(nextB64);
332
- while (progress.remaining > 0) {
333
- progress = await second.continueRotateMaster(nextB64);
334
- }
335
- expect(progress.remaining).toBe(0);
336
- expect((await second.status()).kekVersion).toBe(2);
337
- expect((await second.status()).rewrapTargetKekVersion).toBeUndefined();
351
+ // Loser must not have wrapped any DEK under its key: at most the winner's batch.
352
+ const mid = await h.db.query<{ kek_version: string; n: string }>(
353
+ `SELECT kek_version::text, COUNT(*)::text AS n FROM oke_vault_keys GROUP BY kek_version ORDER BY 1`,
354
+ );
355
+ const v2 = mid.find((r) => r.kek_version === "2");
356
+ expect(Number(v2?.n ?? "0")).toBeLessThanOrEqual(2);
357
+ } finally {
358
+ await h.close();
359
+ }
360
+ });
338
361
 
339
- for (let i = 0; i < 5; i += 1) {
340
- expect((await second.get(`prod/api/key-${i}`))?.value).toBe(`value-${i}`);
341
- }
362
+ test("continueRotateMaster cold-resumes from rewrap_key_hash after process death", async () => {
363
+ await resetVaultTables(sharedConn);
364
+ const db = asExec(sharedConn);
365
+ const first = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
366
+ const init = await first.initialize();
367
+ await first.unseal(init.masterKey);
368
+ for (let i = 0; i < 5; i += 1) {
369
+ await first.set(`prod/api/key-${i}`, `value-${i}`);
370
+ }
342
371
 
343
- await second.seal();
344
- const stale = await second.unseal(init.masterKey).catch((e: unknown) => e);
345
- expect(isVaultError(stale, "INVALID_KEY")).toBe(true);
346
- await second.unseal(nextB64);
347
- expect((await second.get("prod/api/key-0"))?.value).toBe("value-0");
348
- } finally {
349
- await conn.close();
372
+ const next = generateMasterKey();
373
+ const nextB64 = masterKeyToBase64(next);
374
+ let progress = await first.rotateMaster(next);
375
+ expect(progress.remaining).toBe(3);
376
+ expect((await first.status()).rewrapTargetKekVersion).toBe(2);
377
+
378
+ // Simulate process death: drop in-memory pending, expire the rotate lease
379
+ // (lazy reclaim — same physics as SIGKILL + lease TTL).
380
+ await db.execute(`UPDATE oke_vault_status SET rotate_lease_expires_at = $1 WHERE id = 1`, [
381
+ Date.now() - 1,
382
+ ]);
383
+ const second = createBuiltinVaultAdapter({ db, kekRewrapBatchSize: 2 });
384
+ await second.unseal(init.masterKey);
385
+ expect((await second.status()).rewrapTargetKekVersion).toBe(2);
386
+
387
+ const wrong = await second.continueRotateMaster().catch((e: unknown) => e);
388
+ expect(isVaultError(wrong, "INVALID_KEY")).toBe(true);
389
+
390
+ progress = await second.continueRotateMaster(nextB64);
391
+ while (progress.remaining > 0) {
392
+ progress = await second.continueRotateMaster(nextB64);
393
+ }
394
+ expect(progress.remaining).toBe(0);
395
+ expect((await second.status()).kekVersion).toBe(2);
396
+ expect((await second.status()).rewrapTargetKekVersion).toBeUndefined();
397
+
398
+ for (let i = 0; i < 5; i += 1) {
399
+ expect((await second.get(`prod/api/key-${i}`))?.value).toBe(`value-${i}`);
350
400
  }
401
+
402
+ await second.seal();
403
+ const stale = await second.unseal(init.masterKey).catch((e: unknown) => e);
404
+ expect(isVaultError(stale, "INVALID_KEY")).toBe(true);
405
+ await second.unseal(nextB64);
406
+ expect((await second.get("prod/api/key-0"))?.value).toBe("value-0");
351
407
  });
352
408
  });
353
409
 
@@ -458,6 +514,7 @@ describe("builtin vault adapter — audit and backup", () => {
458
514
 
459
515
  const blob = await h.adapter.exportBackup();
460
516
  expect(Buffer.from(blob).toString("utf8")).toStartWith("oke-vault-backup-v1\n");
517
+ expect(Buffer.from(blob).toString("utf8")).toEndWith("oke-vault-backup-end\n");
461
518
  expect(Buffer.from(blob).toString("utf8")).not.toContain("prod/db/main");
462
519
 
463
520
  await h.adapter.delete("prod/api/stripe");