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
@@ -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");