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.
- package/package.json +1 -1
- package/site/content/docs/elements/ai.mdx +22 -1
- package/site/content/docs/elements/store.mdx +3 -1
- package/site/content/docs/elements/vault.mdx +19 -11
- package/site/content/docs/get-started/installation.mdx +7 -1
- package/site/content/docs/recipes/llama-cpp.mdx +10 -9
- package/site/content/docs/reference/cli.md +6 -2
- package/site/content/docs/reference/environment-variables.mdx +9 -9
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +61 -1
- package/src/cli/ask-seed.test.ts +4 -3
- package/src/cli/ask-seed.ts +5 -6
- package/src/cli/client-add.test.ts +2 -1
- package/src/cli/dev.test.ts +116 -0
- package/src/cli/dev.ts +107 -18
- package/src/cli/project-state.test.ts +50 -0
- package/src/cli/project-state.ts +123 -0
- package/src/cli/vault-cmd.test.ts +47 -18
- package/src/cli/vault-cmd.ts +2 -1
- package/src/compiler/extract.ts +12 -1
- package/src/console/server/console.test.ts +3 -1
- package/src/console/server/operator-db.test.ts +48 -17
- package/src/console/server/operator-db.ts +5 -1
- package/src/docker/derive.ts +24 -3
- package/src/docker/docker.test.ts +4 -2
- package/src/docker/index.ts +1 -0
- package/src/docker/recipes/index.ts +1 -0
- package/src/docker/recipes/llama-cpp.ts +20 -4
- package/src/drivers/ai-openai-compatible.ts +15 -3
- package/src/drivers/vault-builtin.test.ts +50 -42
- package/src/elements/ai/declare.ts +73 -3
- package/src/elements/ai/errors.test.ts +35 -0
- package/src/elements/ai/errors.ts +139 -0
- package/src/elements/ai/eval.ts +26 -1
- package/src/elements/ai/runtime.ts +140 -80
- package/src/elements/ai/tools.test.ts +1 -1
- package/src/elements/ai.test.ts +99 -2
- package/src/elements/ai.ts +11 -1
- package/src/elements/gate/config.ts +13 -3
- package/src/elements/gate/declare.ts +1 -1
- package/src/elements/gate/strategies.ts +2 -12
- package/src/elements/index.ts +2 -0
- package/src/elements/store/index-boot.test.ts +23 -6
- package/src/elements/store/resource.test.ts +38 -19
- package/src/elements/store/sql-session.test.ts +55 -58
- package/src/elements/vault/builtin-adapter.test.ts +115 -58
- package/src/elements/vault/builtin-adapter.ts +241 -47
- package/src/elements/vault/chaos-child.ts +424 -0
- package/src/elements/vault/chaos.test.ts +651 -0
- package/src/elements/vault/resilience.ts +6 -1
- package/src/elements/vault/security-checklist.test.ts +10 -8
- package/src/elements/vault/storage.ts +130 -27
- package/src/elements/vault/test-helpers.ts +368 -0
- package/src/elements/vault.ts +6 -0
- package/src/index.ts +2 -0
- package/src/kernel/app-auth.ts +98 -0
- package/src/kernel/app.ts +120 -78
- package/src/kernel/auto-registry.test.ts +52 -1
- package/src/kernel/boot.test.ts +4 -18
- package/src/kernel/element-registries.ts +19 -4
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/fx.test.ts +25 -0
- package/src/kernel/fx.ts +4 -1
- package/src/manifest/types.ts +4 -0
- package/src/release/build-lib.ts +3 -0
- package/src/shared/lazy-src.ts +79 -0
- package/src/test/create-test-app.ts +16 -11
- 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
|
|
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
|
|
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
|
-
|
|
47
|
-
const db = asExec(
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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("
|
|
307
|
-
const
|
|
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
|
-
|
|
311
|
-
|
|
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
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
329
|
-
expect(
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
expect((
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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");
|