okengine 0.11.0 → 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 (68) 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/gate/config.ts +13 -3
  40. package/src/elements/gate/declare.ts +1 -1
  41. package/src/elements/gate/strategies.ts +2 -12
  42. package/src/elements/index.ts +2 -0
  43. package/src/elements/store/index-boot.test.ts +23 -6
  44. package/src/elements/store/resource.test.ts +38 -19
  45. package/src/elements/store/sql-session.test.ts +55 -58
  46. package/src/elements/vault/builtin-adapter.test.ts +115 -58
  47. package/src/elements/vault/builtin-adapter.ts +241 -47
  48. package/src/elements/vault/chaos-child.ts +424 -0
  49. package/src/elements/vault/chaos.test.ts +651 -0
  50. package/src/elements/vault/resilience.ts +6 -1
  51. package/src/elements/vault/security-checklist.test.ts +10 -8
  52. package/src/elements/vault/storage.ts +130 -27
  53. package/src/elements/vault/test-helpers.ts +368 -0
  54. package/src/elements/vault.ts +6 -0
  55. package/src/index.ts +2 -0
  56. package/src/kernel/app-auth.ts +98 -0
  57. package/src/kernel/app.ts +120 -78
  58. package/src/kernel/auto-registry.test.ts +52 -1
  59. package/src/kernel/boot.test.ts +4 -18
  60. package/src/kernel/element-registries.ts +19 -4
  61. package/src/kernel/errors.ts +3 -3
  62. package/src/kernel/fx.test.ts +25 -0
  63. package/src/kernel/fx.ts +4 -1
  64. package/src/manifest/types.ts +4 -0
  65. package/src/release/build-lib.ts +3 -0
  66. package/src/shared/lazy-src.ts +79 -0
  67. package/src/test/create-test-app.ts +16 -11
  68. package/src/test/reset-element-registries.ts +17 -9
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Phase 10 security checklist — threat-model assertions for the built-in vault.
3
+ *
4
+ * SQL-backed cases use the in-memory Vault SQL fake: these tests assert seal
5
+ * physics and audit integrity, not Postgres dialect / pgvector behavior.
3
6
  */
4
7
 
5
8
  import { describe, expect, test } from "bun:test";
6
- import { connectPglite } from "../../drivers/pglite.ts";
7
9
  import type { SqlConnection } from "../../drivers/types.ts";
8
10
  import {
9
11
  createBuiltinVaultAdapter,
@@ -23,16 +25,17 @@ import {
23
25
  import { VaultError } from "./errors.ts";
24
26
  import { canonicalizePath } from "./path.ts";
25
27
  import type { SqlExec } from "./storage.ts";
28
+ import { createMemoryVaultSql } from "./test-helpers.ts";
26
29
  import { createMemoryUnsealer } from "./unseal.ts";
27
30
 
28
- /** Fresh initialized + unsealed adapter. */
31
+ /** Fresh initialized + unsealed adapter over the in-memory Vault SQL fake. */
29
32
  async function harness(): Promise<{
30
33
  adapter: BuiltinVaultAdapter;
31
34
  db: SqlExec;
32
35
  conn: SqlConnection;
33
36
  close(): Promise<void>;
34
37
  }> {
35
- const conn = await connectPglite({ url: `memory://vault-sec-${crypto.randomUUID()}` });
38
+ const conn = createMemoryVaultSql();
36
39
  const db = sqlConnectionAsExec(conn);
37
40
  const adapter = createBuiltinVaultAdapter({ db });
38
41
  const init = await adapter.initialize!();
@@ -53,12 +56,11 @@ describe("vault security checklist", () => {
53
56
  try {
54
57
  const secret = `sk_live_${crypto.randomUUID()}`;
55
58
  await h.adapter.set("prod/api/stripe", secret);
56
- const rows = await h.conn.query(
57
- "select encode(encrypted_value, 'escape') as blob from oke_vault_secrets",
58
- );
59
+ const rows = await h.conn.query("select encrypted_value from oke_vault_secrets");
59
60
  for (const row of rows) {
60
- const blob = String((row as { blob?: unknown }).blob ?? "");
61
- expect(blob.includes(secret)).toBe(false);
61
+ const raw = (row as { encrypted_value?: unknown }).encrypted_value;
62
+ const bytes = raw instanceof Uint8Array ? raw : new Uint8Array();
63
+ expect(Buffer.from(bytes).toString("utf8").includes(secret)).toBe(false);
62
64
  }
63
65
  } finally {
64
66
  await h.close();
@@ -35,6 +35,27 @@ export interface SqlExec {
35
35
  * @param params - Positional parameters
36
36
  */
37
37
  execute(sql: string, params?: unknown[]): Promise<void>;
38
+ /**
39
+ * Optional transactional scope — required for `FOR UPDATE` / `SKIP LOCKED`
40
+ * lease and audit serialization (Clock / Signal class).
41
+ *
42
+ * @param fn - Work that must see a single snapshot under row locks
43
+ */
44
+ begin?<T>(fn: (tx: SqlExec) => Promise<T>): Promise<T>;
45
+ }
46
+
47
+ /**
48
+ * Run `fn` inside {@link SqlExec.begin} when available.
49
+ *
50
+ * @param db - SQL surface
51
+ * @param fn - Transactional work
52
+ */
53
+ export async function withSqlTransaction<T>(
54
+ db: SqlExec,
55
+ fn: (tx: SqlExec) => Promise<T>,
56
+ ): Promise<T> {
57
+ if (db.begin) return db.begin(fn);
58
+ return fn(db);
38
59
  }
39
60
 
40
61
  /** Encrypted secret versions. One row per `(path, version)`. */
@@ -122,10 +143,21 @@ CREATE TABLE IF NOT EXISTS oke_vault_status (
122
143
  rewrap_checkpoint text,
123
144
  rewrap_target_kek_version integer,
124
145
  rewrap_key_hash text,
146
+ rotate_locked_by text,
147
+ rotate_lease_expires_at bigint,
125
148
  updated_at timestamptz NOT NULL DEFAULT now(),
126
149
  CONSTRAINT oke_vault_status_singleton CHECK (id = 1)
127
150
  )`;
128
151
 
152
+ /**
153
+ * Claim the rotate-master lease — same predicate as Clock/Signal
154
+ * (`FOR UPDATE SKIP LOCKED` + lease-expiry reclaim).
155
+ */
156
+ export const CLAIM_ROTATE_LEASE_SQL: string = `SELECT id FROM oke_vault_status WHERE id = 1 AND ((rotate_locked_by IS NULL) OR (rotate_locked_by = $1) OR (rotate_lease_expires_at IS NULL) OR (rotate_lease_expires_at <= $2)) FOR UPDATE SKIP LOCKED`;
157
+
158
+ /** Default rotate-master lease TTL (ms) — lazy reclaim, no sweeper. */
159
+ export const DEFAULT_ROTATE_LEASE_MS: number = 30_000;
160
+
129
161
  /** Index / seed statements applied after the tables exist. */
130
162
  const POST_DDL_STATEMENTS: readonly string[] = [
131
163
  `CREATE INDEX IF NOT EXISTS oke_vault_secrets_path_idx ON oke_vault_secrets (path) WHERE deleted_at IS NULL`,
@@ -134,6 +166,8 @@ const POST_DDL_STATEMENTS: readonly string[] = [
134
166
  `CREATE INDEX IF NOT EXISTS oke_vault_keys_kek_version_idx ON oke_vault_keys (kek_version)`,
135
167
  `ALTER TABLE oke_vault_audit ADD COLUMN IF NOT EXISTS seq bigserial NOT NULL`,
136
168
  `ALTER TABLE oke_vault_status ADD COLUMN IF NOT EXISTS rewrap_key_hash text`,
169
+ `ALTER TABLE oke_vault_status ADD COLUMN IF NOT EXISTS rotate_locked_by text`,
170
+ `ALTER TABLE oke_vault_status ADD COLUMN IF NOT EXISTS rotate_lease_expires_at bigint`,
137
171
  `CREATE INDEX IF NOT EXISTS oke_vault_audit_seq_idx ON oke_vault_audit (seq)`,
138
172
  `CREATE INDEX IF NOT EXISTS oke_vault_audit_created_idx ON oke_vault_audit (created_at, id)`,
139
173
  `CREATE INDEX IF NOT EXISTS oke_vault_audit_path_idx ON oke_vault_audit (path)`,
@@ -391,13 +425,78 @@ export async function verifyAuditChain(db: SqlExec): Promise<AuditChainResult> {
391
425
  return { ok: true };
392
426
  }
393
427
 
428
+ /**
429
+ * Acquire the rotate-master lease (Clock/Signal SKIP LOCKED + lease-expiry).
430
+ *
431
+ * Exactly one concurrent claimant wins; losers get `false` immediately
432
+ * (no wait). A crashed holder's lease is reclaimed lazily when
433
+ * `rotate_lease_expires_at <= now`.
434
+ *
435
+ * @param db - SQL surface (uses {@link SqlExec.begin} when present)
436
+ * @param holderId - Claimant id (per rotation attempt)
437
+ * @param now - Epoch-ms
438
+ * @param leaseMs - Lease TTL
439
+ */
440
+ export async function acquireRotateLease(
441
+ db: SqlExec,
442
+ holderId: string,
443
+ now: number,
444
+ leaseMs: number = DEFAULT_ROTATE_LEASE_MS,
445
+ ): Promise<boolean> {
446
+ return withSqlTransaction(db, async (tx) => {
447
+ const claimed = await tx.query<{ id: number | string }>(CLAIM_ROTATE_LEASE_SQL, [
448
+ holderId,
449
+ now,
450
+ ]);
451
+ if (!claimed[0]) return false;
452
+ await tx.execute(
453
+ `UPDATE oke_vault_status
454
+ SET rotate_locked_by = $1, rotate_lease_expires_at = $2, updated_at = now()
455
+ WHERE id = 1`,
456
+ [holderId, now + leaseMs],
457
+ );
458
+ return true;
459
+ });
460
+ }
461
+
462
+ /**
463
+ * Drop the rotate-master lease when this holder still owns it.
464
+ *
465
+ * @param db - SQL surface
466
+ * @param holderId - Holder that acquired the lease
467
+ */
468
+ export async function releaseRotateLease(db: SqlExec, holderId: string): Promise<void> {
469
+ await db.execute(
470
+ `UPDATE oke_vault_status
471
+ SET rotate_locked_by = NULL, rotate_lease_expires_at = NULL, updated_at = now()
472
+ WHERE id = 1 AND rotate_locked_by = $1`,
473
+ [holderId],
474
+ );
475
+ }
476
+
477
+ /**
478
+ * Renew the rotate-master lease for a long rewrap.
479
+ *
480
+ * @param db - SQL surface
481
+ * @param holderId - Current holder
482
+ * @param now - Epoch-ms
483
+ * @param leaseMs - Lease TTL
484
+ */
485
+ export async function renewRotateLease(
486
+ db: SqlExec,
487
+ holderId: string,
488
+ now: number,
489
+ leaseMs: number = DEFAULT_ROTATE_LEASE_MS,
490
+ ): Promise<boolean> {
491
+ return acquireRotateLease(db, holderId, now, leaseMs);
492
+ }
493
+
394
494
  /**
395
495
  * Backend-side {@link AuditWriter} over `oke_vault_audit`.
396
496
  *
397
- * Reads the current chain head, links the new row to it, and inserts.
398
- * Concurrent appends must be serialized by the caller (a transaction with
399
- * `SELECT FOR UPDATE` on the head, or a single writer) — two racing
400
- * appends would otherwise share a `prev_hash`.
497
+ * Appends run inside a transaction that locks the status singleton with
498
+ * `SELECT FOR UPDATE` so concurrent writers cannot share a `prev_hash`
499
+ * (same exclusivity class as Signal competing consumers).
401
500
  *
402
501
  * @param db - SQL surface
403
502
  */
@@ -407,29 +506,33 @@ export function createSqlAuditWriter(db: SqlExec): AuditWriter {
407
506
  const createdAt = entry.at ?? new Date();
408
507
  const payload = toAuditHashPayload(entry, createdAt);
409
508
  try {
410
- const head = await db.query<{ row_hash: string }>(
411
- `SELECT row_hash FROM oke_vault_audit ORDER BY seq DESC LIMIT 1`,
412
- );
413
- const prevHash = head[0]?.row_hash ?? AUDIT_GENESIS_HASH;
414
- const rowHash = await computeAuditRowHash(prevHash, payload);
415
- await db.execute(
416
- `INSERT INTO oke_vault_audit
417
- (action, path, actor_type, actor_id, success, error_code, error_message, request_id, prev_hash, row_hash, created_at)
418
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
419
- [
420
- payload.action,
421
- payload.path,
422
- payload.actorType,
423
- payload.actorId,
424
- payload.success,
425
- payload.errorCode,
426
- payload.errorMessage,
427
- payload.requestId,
428
- prevHash,
429
- rowHash,
430
- createdAt,
431
- ],
432
- );
509
+ await withSqlTransaction(db, async (tx) => {
510
+ // Serialize writers on the singleton FOR UPDATE waits, does not skip.
511
+ await tx.query(`SELECT id FROM oke_vault_status WHERE id = 1 FOR UPDATE`);
512
+ const head = await tx.query<{ row_hash: string }>(
513
+ `SELECT row_hash FROM oke_vault_audit ORDER BY seq DESC LIMIT 1`,
514
+ );
515
+ const prevHash = head[0]?.row_hash ?? AUDIT_GENESIS_HASH;
516
+ const rowHash = await computeAuditRowHash(prevHash, payload);
517
+ await tx.execute(
518
+ `INSERT INTO oke_vault_audit
519
+ (action, path, actor_type, actor_id, success, error_code, error_message, request_id, prev_hash, row_hash, created_at)
520
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
521
+ [
522
+ payload.action,
523
+ payload.path,
524
+ payload.actorType,
525
+ payload.actorId,
526
+ payload.success,
527
+ payload.errorCode,
528
+ payload.errorMessage,
529
+ payload.requestId,
530
+ prevHash,
531
+ rowHash,
532
+ createdAt,
533
+ ],
534
+ );
535
+ });
433
536
  } catch (error) {
434
537
  if (error instanceof VaultError) throw error;
435
538
  throw new VaultError("BACKEND_ERROR", "vault: failed to append audit row");
@@ -0,0 +1,368 @@
1
+ /**
2
+ * In-memory Vault SQL fake + shared-PGlite helpers for Vault tests.
3
+ *
4
+ * - {@link createMemoryVaultSql} — logic-only unit tests (no WASM).
5
+ * - {@link resetVaultTables} — cheap isolation between dialect tests that
6
+ * share one warmed PGlite connection per file.
7
+ */
8
+
9
+ import type { SqlConnection, SqlRow } from "../../drivers/types.ts";
10
+
11
+ /** Vault tables truncated between shared-PGlite dialect tests. */
12
+ const VAULT_TABLES = [
13
+ "oke_vault_keys",
14
+ "oke_vault_secrets",
15
+ "oke_vault_audit",
16
+ "oke_vault_master",
17
+ "oke_vault_status",
18
+ ] as const;
19
+
20
+ /**
21
+ * Wipe every Vault table on a live connection and restart identity columns.
22
+ *
23
+ * Used when dialect tests share one warmed PGlite instance: TRUNCATE is
24
+ * milliseconds; a fresh `PGlite.create` is hundreds of ms to multi-second.
25
+ * No-ops when tables do not exist yet (first harness before DDL).
26
+ *
27
+ * @param conn - Open SQL connection (typically shared file-scoped PGlite)
28
+ */
29
+ export async function resetVaultTables(conn: SqlConnection): Promise<void> {
30
+ try {
31
+ await conn.exec(`TRUNCATE ${VAULT_TABLES.join(", ")} RESTART IDENTITY CASCADE`);
32
+ } catch {
33
+ // Tables not created yet — ensureVaultTables will seed on first use.
34
+ }
35
+ }
36
+
37
+ /** One row in an in-memory table. */
38
+ type Row = Record<string, unknown>;
39
+
40
+ /**
41
+ * Create an in-process {@link SqlConnection} that speaks the Vault SQL
42
+ * surface (Postgres `$n` placeholders, `bytea` as `Uint8Array`, auto `seq`).
43
+ *
44
+ * Pair with {@link import("./builtin-adapter.ts").sqlConnectionAsExec} when
45
+ * the adapter needs a {@link import("./storage.ts").SqlExec}.
46
+ *
47
+ * @returns Fresh isolated connection (empty vault tables)
48
+ */
49
+ export function createMemoryVaultSql(): SqlConnection {
50
+ const secrets: Row[] = [];
51
+ const keys: Row[] = [];
52
+ const audit: Row[] = [];
53
+ const master: Row[] = [];
54
+ let status: Row = defaultStatus();
55
+ let auditSeq = 0;
56
+
57
+ async function query(sql: string, params: readonly unknown[] = []): Promise<SqlRow[]> {
58
+ const text = normalize(sql);
59
+ const p = [...params];
60
+
61
+ if (isDdl(text) || text.startsWith("create index")) {
62
+ return [];
63
+ }
64
+
65
+ if (text === "begin" || text === "commit" || text === "rollback") {
66
+ return [];
67
+ }
68
+
69
+ if (text.startsWith("insert into oke_vault_status") && text.includes("on conflict")) {
70
+ // Singleton already seeded in {@link defaultStatus}.
71
+ return [];
72
+ }
73
+
74
+ if (text.startsWith("select id from oke_vault_status where id = 1 for update")) {
75
+ return [{ id: 1 }];
76
+ }
77
+
78
+ if (
79
+ text.startsWith("select id from oke_vault_status where id = 1 and") &&
80
+ text.includes("rotate_locked_by") &&
81
+ text.includes("for update skip locked")
82
+ ) {
83
+ const holderId = p[0];
84
+ const now = Number(p[1]);
85
+ const lockedBy = status.rotate_locked_by;
86
+ const expires = status.rotate_lease_expires_at;
87
+ const free =
88
+ lockedBy == null || lockedBy === holderId || expires == null || Number(expires) <= now;
89
+ return free ? [{ id: 1 }] : [];
90
+ }
91
+
92
+ if (text.startsWith("select sealed, initialized, master_key_present")) {
93
+ return [{ ...status }];
94
+ }
95
+
96
+ if (
97
+ text.startsWith("select key_hash, kek_version from oke_vault_master") &&
98
+ text.includes("order by created_at asc")
99
+ ) {
100
+ const sorted = [...master].sort((a, b) => toTime(a.created_at) - toTime(b.created_at));
101
+ const first = sorted[0];
102
+ return first ? [{ key_hash: first.key_hash, kek_version: first.kek_version }] : [];
103
+ }
104
+
105
+ if (text.startsWith("select count(distinct path) as count from oke_vault_secrets")) {
106
+ const live = secrets.filter((r) => r.deleted_at == null);
107
+ return [{ count: new Set(live.map((r) => r.path)).size }];
108
+ }
109
+
110
+ if (text.startsWith("select row_hash from oke_vault_audit order by seq desc limit 1")) {
111
+ const last = [...audit].sort((a, b) => Number(b.seq) - Number(a.seq))[0];
112
+ return last ? [{ row_hash: last.row_hash }] : [];
113
+ }
114
+
115
+ if (
116
+ text.startsWith("select id, action, path, actor_type") &&
117
+ text.includes("from oke_vault_audit") &&
118
+ text.includes("order by seq asc")
119
+ ) {
120
+ return [...audit].sort((a, b) => Number(a.seq) - Number(b.seq)).map((r) => ({ ...r }));
121
+ }
122
+
123
+ if (text.startsWith("select max(version) + 1 as next from oke_vault_secrets where path")) {
124
+ const path = p[0];
125
+ const versions = secrets.filter((r) => r.path === path).map((r) => Number(r.version));
126
+ const max = versions.length === 0 ? null : Math.max(...versions);
127
+ return [{ next: max === null ? null : max + 1 }];
128
+ }
129
+
130
+ if (
131
+ text.startsWith("insert into oke_vault_secrets") &&
132
+ text.includes("returning id, created_at, updated_at")
133
+ ) {
134
+ const now = new Date();
135
+ const row: Row = {
136
+ id: crypto.randomUUID(),
137
+ path: p[0],
138
+ encrypted_value: asBytes(p[1]),
139
+ iv: asBytes(p[2]),
140
+ auth_tag: asBytes(p[3]),
141
+ version: p[4],
142
+ metadata: typeof p[5] === "string" ? p[5] : JSON.stringify(p[5] ?? {}),
143
+ algorithm: p[6],
144
+ kek_version: p[7],
145
+ expires_at: p[8] ?? null,
146
+ created_at: now,
147
+ updated_at: now,
148
+ deleted_at: null,
149
+ };
150
+ secrets.push(row);
151
+ return [{ id: row.id as string, created_at: now, updated_at: now }];
152
+ }
153
+
154
+ if (text.startsWith("insert into oke_vault_keys")) {
155
+ keys.push({
156
+ id: crypto.randomUUID(),
157
+ secret_id: p[0],
158
+ encrypted_dek: asBytes(p[1]),
159
+ dek_iv: asBytes(p[2]),
160
+ dek_auth_tag: asBytes(p[3]),
161
+ algorithm: p[4],
162
+ kek_version: p[5],
163
+ created_at: new Date(),
164
+ });
165
+ return [];
166
+ }
167
+
168
+ if (text.startsWith("insert into oke_vault_master")) {
169
+ const now = new Date();
170
+ master.push({
171
+ id: crypto.randomUUID(),
172
+ key_hash: p[0],
173
+ kek_version: p[1] ?? 1,
174
+ created_at: now,
175
+ updated_at: now,
176
+ });
177
+ return [];
178
+ }
179
+
180
+ if (text.startsWith("insert into oke_vault_audit")) {
181
+ auditSeq += 1;
182
+ audit.push({
183
+ id: crypto.randomUUID(),
184
+ seq: auditSeq,
185
+ action: p[0],
186
+ path: p[1] ?? null,
187
+ actor_type: p[2],
188
+ actor_id: p[3] ?? null,
189
+ success: p[4],
190
+ error_code: p[5] ?? null,
191
+ error_message: p[6] ?? null,
192
+ request_id: p[7] ?? null,
193
+ prev_hash: p[8] ?? null,
194
+ row_hash: p[9],
195
+ created_at: p[10] instanceof Date ? p[10] : new Date(String(p[10])),
196
+ });
197
+ return [];
198
+ }
199
+
200
+ if (text.startsWith("update oke_vault_status")) {
201
+ applyStatusUpdate(status, text, p);
202
+ return [];
203
+ }
204
+
205
+ if (
206
+ text.startsWith("update oke_vault_audit set row_hash") &&
207
+ text.includes("where seq = (select max(seq) from oke_vault_audit)")
208
+ ) {
209
+ const last = [...audit].sort((a, b) => Number(b.seq) - Number(a.seq))[0];
210
+ if (last) last.row_hash = p[0];
211
+ return [];
212
+ }
213
+
214
+ if (
215
+ text.startsWith("select encode(encrypted_value") ||
216
+ text.startsWith("select encrypted_value")
217
+ ) {
218
+ return secrets.map((r) => {
219
+ const bytes = asBytes(r.encrypted_value);
220
+ if (text.includes("encode(")) {
221
+ return { blob: Buffer.from(bytes).toString("latin1") };
222
+ }
223
+ return { encrypted_value: bytes };
224
+ });
225
+ }
226
+
227
+ throw new Error(`memory vault sql: unsupported query: ${sql.trim().slice(0, 120)}`);
228
+ }
229
+
230
+ return {
231
+ driverId: "memory",
232
+ role: "primary",
233
+ query,
234
+ async exec(sql, params = []) {
235
+ await query(sql, params);
236
+ return { changes: 0 };
237
+ },
238
+ async close() {
239
+ /* in-memory — nothing to release */
240
+ },
241
+ };
242
+ }
243
+
244
+ /** Collapse whitespace / case so template literals match. */
245
+ function normalize(sql: string): string {
246
+ return sql.replace(/\s+/g, " ").trim().toLowerCase();
247
+ }
248
+
249
+ /** DDL / index / alter — tables are implicit in the fake. */
250
+ function isDdl(text: string): boolean {
251
+ return (
252
+ text.startsWith("create table") ||
253
+ text.startsWith("alter table") ||
254
+ text.startsWith("create index")
255
+ );
256
+ }
257
+
258
+ /** Seed the singleton status row (`id = 1`). */
259
+ function defaultStatus(): Row {
260
+ return {
261
+ id: 1,
262
+ sealed: true,
263
+ initialized: false,
264
+ master_key_present: false,
265
+ last_sealed_at: null,
266
+ last_unsealed_at: null,
267
+ seal_count: 0,
268
+ rewrap_checkpoint: null,
269
+ rewrap_target_kek_version: null,
270
+ rewrap_key_hash: null,
271
+ rotate_locked_by: null,
272
+ rotate_lease_expires_at: null,
273
+ updated_at: new Date(),
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Apply a status UPDATE by reading assigned columns from the SQL text.
279
+ *
280
+ * Vault only emits a handful of fixed UPDATE shapes; we mirror those
281
+ * assignments rather than parsing a full SET grammar.
282
+ */
283
+ function applyStatusUpdate(status: Row, text: string, params: unknown[]): void {
284
+ const now = new Date();
285
+ status.updated_at = now;
286
+
287
+ if (text.includes("initialized = true")) {
288
+ status.initialized = true;
289
+ status.sealed = true;
290
+ status.master_key_present = true;
291
+ return;
292
+ }
293
+
294
+ if (text.includes("sealed = false") && text.includes("last_unsealed_at")) {
295
+ status.sealed = false;
296
+ status.last_unsealed_at = now;
297
+ return;
298
+ }
299
+
300
+ if (text.includes("sealed = true") && text.includes("seal_count = seal_count + 1")) {
301
+ status.sealed = true;
302
+ status.last_sealed_at = now;
303
+ status.seal_count = Number(status.seal_count ?? 0) + 1;
304
+ return;
305
+ }
306
+
307
+ if (text.includes("rotate_locked_by = $1") && text.includes("rotate_lease_expires_at = $2")) {
308
+ status.rotate_locked_by = params[0] ?? null;
309
+ status.rotate_lease_expires_at = params[1] ?? null;
310
+ return;
311
+ }
312
+
313
+ if (text.includes("rotate_locked_by = null") && text.includes("rotate_lease_expires_at = null")) {
314
+ if (params[0] === undefined || status.rotate_locked_by === params[0]) {
315
+ status.rotate_locked_by = null;
316
+ status.rotate_lease_expires_at = null;
317
+ }
318
+ return;
319
+ }
320
+
321
+ if (text.includes("rewrap_target_kek_version = $1") && text.includes("rewrap_key_hash = $2")) {
322
+ status.rewrap_target_kek_version = params[0] ?? null;
323
+ status.rewrap_checkpoint = null;
324
+ status.rewrap_key_hash = params[1] ?? null;
325
+ return;
326
+ }
327
+
328
+ if (
329
+ text.includes("rewrap_checkpoint = $1") &&
330
+ text.includes("rewrap_target_kek_version = $2") &&
331
+ text.includes("rewrap_key_hash = $3")
332
+ ) {
333
+ status.rewrap_checkpoint = params[0] ?? null;
334
+ status.rewrap_target_kek_version = params[1] ?? null;
335
+ status.rewrap_key_hash = params[2] ?? null;
336
+ return;
337
+ }
338
+
339
+ if (
340
+ text.includes("rewrap_checkpoint = null") &&
341
+ text.includes("rewrap_target_kek_version = null") &&
342
+ text.includes("rewrap_key_hash = null")
343
+ ) {
344
+ status.rewrap_checkpoint = null;
345
+ status.rewrap_target_kek_version = null;
346
+ status.rewrap_key_hash = null;
347
+ return;
348
+ }
349
+
350
+ // Fallback: ignore unknown status updates.
351
+ void params;
352
+ }
353
+
354
+ /** Coerce driver-ish binary params to `Uint8Array`. */
355
+ function asBytes(value: unknown): Uint8Array {
356
+ if (value instanceof Uint8Array) return value;
357
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
358
+ if (typeof value === "string") return new TextEncoder().encode(value);
359
+ if (value == null) return new Uint8Array();
360
+ throw new Error(`memory vault sql: expected bytes, got ${typeof value}`);
361
+ }
362
+
363
+ /** Epoch-ms for ordering `created_at` columns. */
364
+ function toTime(value: unknown): number {
365
+ if (value instanceof Date) return value.getTime();
366
+ if (typeof value === "string" || typeof value === "number") return new Date(value).getTime();
367
+ return 0;
368
+ }
@@ -67,10 +67,12 @@ export type {
67
67
  } from "./vault/types.ts";
68
68
 
69
69
  export {
70
+ BACKUP_END_MARKER,
70
71
  BACKUP_MAGIC,
71
72
  createBuiltinVaultAdapter,
72
73
  DEFAULT_KEK_REWRAP_BATCH_SIZE,
73
74
  sqlConnectionAsExec,
75
+ writeBackupFileAtomic,
74
76
  } from "./vault/builtin-adapter.ts";
75
77
  export type {
76
78
  BuiltinVaultAdapter,
@@ -87,11 +89,15 @@ export {
87
89
  export type { AuditAction, AuditEntry, AuditSink, AuditWriter } from "./vault/audit.ts";
88
90
 
89
91
  export {
92
+ acquireRotateLease,
93
+ CLAIM_ROTATE_LEASE_SQL,
90
94
  createSqlAuditWriter,
95
+ DEFAULT_ROTATE_LEASE_MS,
91
96
  ensureVaultTables,
92
97
  purgeAuditBefore,
93
98
  purgeExpiredSecrets,
94
99
  readAuditPage,
100
+ releaseRotateLease,
95
101
  verifyAuditChain,
96
102
  VAULT_DDL_STATEMENTS,
97
103
  } from "./vault/storage.ts";
package/src/index.ts CHANGED
@@ -124,6 +124,8 @@ export {
124
124
 
125
125
  export {
126
126
  ai,
127
+ listAiDecls,
128
+ resetAiDecls,
127
129
  createAiRuntime,
128
130
  assertAllowPiiForAsk,
129
131
  AiPiiBuildError,