@agimon-ai/po-vault 0.0.0-alpha.61

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.
@@ -0,0 +1,655 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
27
+ const require_vaultRules = require("../vaultRules-CUudYo7G.cjs");
28
+ const require_contracts = require("../contracts-Do7L_d_L.cjs");
29
+ let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
30
+ let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
31
+ let _agimon_ai_po_db = require("@agimon-ai/po-db");
32
+ let node_path = require("node:path");
33
+ node_path = __toESM(node_path, 1);
34
+ let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
35
+ let node_buffer = require("node:buffer");
36
+ let node_crypto = require("node:crypto");
37
+ let node_fs = require("node:fs");
38
+ let node_fs_promises = require("node:fs/promises");
39
+ //#region src/services/vaultCrypto/index.ts
40
+ /** AES-256-GCM with a fresh 12-byte IV, bound to `aad`. Ported from the MCP proxy's FileTokenStore. */
41
+ function seal(key, plaintext, aad) {
42
+ const iv = (0, node_crypto.randomBytes)(12);
43
+ const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", key, iv);
44
+ cipher.setAAD(node_buffer.Buffer.from(aad, "utf8"));
45
+ const ciphertext = node_buffer.Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
46
+ return {
47
+ iv,
48
+ tag: cipher.getAuthTag(),
49
+ ciphertext
50
+ };
51
+ }
52
+ /** The plaintext, or `undefined` when the key, the AAD or any byte of the box is wrong. */
53
+ function open$1(key, box, aad) {
54
+ if (key.length !== 32 || box.iv.length !== 12 || box.tag.length !== 16) return void 0;
55
+ try {
56
+ const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", key, box.iv);
57
+ decipher.setAAD(node_buffer.Buffer.from(aad, "utf8"));
58
+ decipher.setAuthTag(box.tag);
59
+ return node_buffer.Buffer.concat([decipher.update(box.ciphertext), decipher.final()]).toString("utf8");
60
+ } catch {
61
+ return;
62
+ }
63
+ }
64
+ /** `iv | tag | ciphertext`, the layout of the stored key check. */
65
+ function packBox(box) {
66
+ return node_buffer.Buffer.concat([
67
+ box.iv,
68
+ box.tag,
69
+ box.ciphertext
70
+ ]);
71
+ }
72
+ function unpackBox(packed) {
73
+ const bytes = node_buffer.Buffer.from(packed.buffer, packed.byteOffset, packed.byteLength);
74
+ if (bytes.length <= 28) return void 0;
75
+ return {
76
+ iv: bytes.subarray(0, 12),
77
+ tag: bytes.subarray(12, 28),
78
+ ciphertext: bytes.subarray(28)
79
+ };
80
+ }
81
+ function randomKey() {
82
+ return (0, node_crypto.randomBytes)(32);
83
+ }
84
+ //#endregion
85
+ //#region src/services/vault/index.ts
86
+ /** The vault's schema in po-db. Every object carries the `vault_` prefix po-db enforces. */
87
+ const VAULT_MIGRATIONS = [{
88
+ version: 1,
89
+ statements: [
90
+ "CREATE TABLE vault_meta (name TEXT PRIMARY KEY, value BLOB NOT NULL) STRICT, WITHOUT ROWID",
91
+ `CREATE TABLE vault_entries (
92
+ namespace TEXT NOT NULL, key TEXT NOT NULL,
93
+ revision INTEGER NOT NULL CHECK (revision >= 1), updated_at INTEGER NOT NULL,
94
+ iv BLOB NOT NULL CHECK (length(iv) = 12), tag BLOB NOT NULL CHECK (length(tag) = 16),
95
+ ciphertext BLOB NOT NULL, PRIMARY KEY (namespace, key)
96
+ ) STRICT, WITHOUT ROWID`,
97
+ "CREATE TABLE vault_lease (name TEXT PRIMARY KEY, owner TEXT NOT NULL, expires_at INTEGER NOT NULL) STRICT, WITHOUT ROWID"
98
+ ]
99
+ }];
100
+ const SELECT_ENTRY = "SELECT revision, updated_at, iv, tag, ciphertext FROM vault_entries WHERE namespace = ? AND key = ?";
101
+ const PUT_LAST_WRITE_WINS = `INSERT INTO vault_entries (namespace, key, revision, updated_at, iv, tag, ciphertext)
102
+ VALUES (?, ?, 1, ?, ?, ?, ?)
103
+ ON CONFLICT (namespace, key) DO UPDATE SET revision = vault_entries.revision + 1, updated_at = excluded.updated_at,
104
+ iv = excluded.iv, tag = excluded.tag, ciphertext = excluded.ciphertext
105
+ RETURNING revision, updated_at`;
106
+ const PUT_CREATE_ONLY = `INSERT INTO vault_entries (namespace, key, revision, updated_at, iv, tag, ciphertext)
107
+ VALUES (?, ?, 1, ?, ?, ?, ?)
108
+ ON CONFLICT (namespace, key) DO NOTHING
109
+ RETURNING revision, updated_at`;
110
+ const PUT_EXPECT_REVISION = `UPDATE vault_entries SET revision = ?, updated_at = ?, iv = ?, tag = ?, ciphertext = ?
111
+ WHERE namespace = ? AND key = ? AND revision = ?
112
+ RETURNING revision, updated_at`;
113
+ const ACQUIRE_LEASE = `INSERT INTO vault_lease (name, owner, expires_at) VALUES (?, ?, ?)
114
+ ON CONFLICT (name) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at
115
+ WHERE vault_lease.expires_at < ? OR vault_lease.owner = excluded.owner
116
+ RETURNING owner`;
117
+ const RELEASE_LEASE = "DELETE FROM vault_lease WHERE name = ? AND owner = ?";
118
+ const SELECT_META = "SELECT value FROM vault_meta WHERE name = ?";
119
+ const INSERT_META = "INSERT INTO vault_meta (name, value) VALUES (?, ?) ON CONFLICT (name) DO NOTHING";
120
+ const REPLACE_META = "INSERT INTO vault_meta (name, value) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET value = excluded.value";
121
+ function entryAad(namespace, key) {
122
+ return `${namespace}\0${key}`;
123
+ }
124
+ function bytesEqual(left, right) {
125
+ return node_buffer.Buffer.compare(left, right) === 0;
126
+ }
127
+ function boxOf(row) {
128
+ return {
129
+ iv: row.iv,
130
+ tag: row.tag,
131
+ ciphertext: row.ciphertext
132
+ };
133
+ }
134
+ function provesKey(key, check) {
135
+ const box = unpackBox(check);
136
+ return box !== void 0 && open$1(key, box, "po-vault:check") === "po-vault-v1";
137
+ }
138
+ function isBusy(error) {
139
+ if (error === null || typeof error !== "object") return false;
140
+ const code = "errcode" in error ? error.errcode : void 0;
141
+ if (code === 5 || code === 6) return true;
142
+ return error instanceof Error && /database (?:table )?is locked|SQLITE_BUSY/u.test(error.message);
143
+ }
144
+ function sleep(ms, signal) {
145
+ return new Promise((resolve, reject) => {
146
+ const onAbort = () => {
147
+ clearTimeout(timer);
148
+ reject(signal?.reason);
149
+ };
150
+ const timer = setTimeout(() => {
151
+ signal?.removeEventListener("abort", onAbort);
152
+ resolve();
153
+ }, ms);
154
+ signal?.addEventListener("abort", onAbort, { once: true });
155
+ });
156
+ }
157
+ /**
158
+ * The vault on po-db: AES-256-GCM rows in `vault_entries` under one data-encryption key kept in
159
+ * the OS keyring or, without one, in an owner-only `key.bin`.
160
+ *
161
+ * Every read and write is a single statement, so nothing here holds the shared connection's
162
+ * write lock across a network call. Cross-process work that must span one goes through
163
+ * `withLock`, a lease row with a TTL.
164
+ */
165
+ function createVault(options) {
166
+ const { db, directory, keyFiles, onNotice } = options;
167
+ const database = db.database;
168
+ const now = options.now ?? Date.now;
169
+ const leaseTtlMs = options.leaseTtlMs ?? 3e4;
170
+ const leaseRenewMs = options.leaseRenewMs ?? 1e4;
171
+ const leaseWaitMs = options.leaseWaitMs ?? 35e3;
172
+ const renewals = /* @__PURE__ */ new Set();
173
+ let migrated;
174
+ let loaded;
175
+ let keyringModule;
176
+ let backend;
177
+ let closed = false;
178
+ async function guarded(work) {
179
+ try {
180
+ return await work();
181
+ } catch (error) {
182
+ throw require_vaultRules.asVaultError(error);
183
+ }
184
+ }
185
+ function ready() {
186
+ migrated ??= guarded(() => db.migrate(require_vaultRules.VAULT_DB_PREFIX, VAULT_MIGRATIONS)).catch((error) => {
187
+ migrated = void 0;
188
+ throw error;
189
+ });
190
+ return migrated;
191
+ }
192
+ async function readMeta(name) {
193
+ return (await database.get(SELECT_META, name))?.value;
194
+ }
195
+ async function keyringAccount() {
196
+ await database.run(INSERT_META, require_vaultRules.VAULT_META_ID, (0, node_crypto.randomBytes)(16));
197
+ const id = await readMeta(require_vaultRules.VAULT_META_ID);
198
+ if (id === void 0) throw new require_vaultRules.VaultError("unavailable");
199
+ return `${require_vaultRules.VAULT_KEYRING_ACCOUNT_PREFIX}${node_buffer.Buffer.from(id).toString("hex")}`;
200
+ }
201
+ async function gather(account) {
202
+ keyringModule ??= options.keyring().catch(() => null);
203
+ const keyring = await keyringModule;
204
+ const candidates = [];
205
+ let keyringFailed = false;
206
+ let fileFailed = false;
207
+ if (keyring !== null) try {
208
+ const password = await keyring.getPassword(account);
209
+ if (password !== null) {
210
+ const key = node_buffer.Buffer.from(password, "base64");
211
+ if (key.length === 32) candidates.push({
212
+ source: "keyring",
213
+ key
214
+ });
215
+ else keyringFailed = true;
216
+ }
217
+ } catch {
218
+ keyringFailed = true;
219
+ }
220
+ try {
221
+ const key = await keyFiles.readKeyFile(directory);
222
+ if (key !== void 0) candidates.push({
223
+ source: "file",
224
+ key
225
+ });
226
+ } catch {
227
+ fileFailed = true;
228
+ }
229
+ return {
230
+ candidates,
231
+ keyring,
232
+ keyringFailed,
233
+ fileFailed
234
+ };
235
+ }
236
+ /** The first candidate that decrypts `dek_check`, seeding the check from the first candidate when there is none. */
237
+ async function pick(gathered) {
238
+ let check = await readMeta(require_vaultRules.VAULT_META_CHECK);
239
+ if (check === void 0) {
240
+ const first = gathered.candidates[0];
241
+ if (first === void 0) return void 0;
242
+ await database.run(INSERT_META, require_vaultRules.VAULT_META_CHECK, packBox(seal(first.key, require_vaultRules.VAULT_CHECK_PLAINTEXT, require_vaultRules.VAULT_CHECK_AAD)));
243
+ check = await readMeta(require_vaultRules.VAULT_META_CHECK);
244
+ if (check === void 0) return void 0;
245
+ }
246
+ for (const candidate of gathered.candidates) {
247
+ if (!provesKey(candidate.key, check)) continue;
248
+ backend = candidate.source;
249
+ return {
250
+ key: candidate.key,
251
+ check
252
+ };
253
+ }
254
+ }
255
+ /**
256
+ * A source that failed may still hold the real key, so no new key replaces it. A throwing
257
+ * keyring only blocks once a check row exists: before that there is nothing to lose, and a
258
+ * host whose keyring daemon is missing falls back to `key.bin`.
259
+ */
260
+ function blocked(gathered, check) {
261
+ return gathered.fileFailed || gathered.keyringFailed && check !== void 0;
262
+ }
263
+ /** Stores a new key in the keyring, read back to prove it; otherwise in a new `key.bin`. Never both. */
264
+ async function store(gathered, account, key) {
265
+ if (gathered.keyring !== null) {
266
+ const encoded = node_buffer.Buffer.from(key).toString("base64");
267
+ try {
268
+ await gathered.keyring.setPassword(account, encoded);
269
+ if (await gathered.keyring.getPassword(account) === encoded) return "keyring";
270
+ } catch {}
271
+ }
272
+ if (gathered.candidates.some((candidate) => candidate.source === "file")) await keyFiles.removeKeyFile(directory);
273
+ await keyFiles.createKeyFile(directory, key);
274
+ return "file";
275
+ }
276
+ async function loadKey() {
277
+ await ready();
278
+ const account = await keyringAccount();
279
+ const first = await gather(account);
280
+ const found = await pick(first);
281
+ if (found !== void 0) return found;
282
+ if (blocked(first, await readMeta("dek_check"))) throw new require_vaultRules.VaultError("unavailable");
283
+ return lease(require_vaultRules.VAULT_INIT_LEASE, async () => {
284
+ const again = await gather(account);
285
+ const existing = await pick(again);
286
+ if (existing !== void 0) return existing;
287
+ const check = await readMeta(require_vaultRules.VAULT_META_CHECK);
288
+ if (blocked(again, check)) throw new require_vaultRules.VaultError("unavailable");
289
+ const key = randomKey();
290
+ const source = await store(again, account, key);
291
+ const packed = packBox(seal(key, require_vaultRules.VAULT_CHECK_PLAINTEXT, require_vaultRules.VAULT_CHECK_AAD));
292
+ if (check === void 0) await database.run(REPLACE_META, require_vaultRules.VAULT_META_CHECK, packed);
293
+ else {
294
+ await database.transaction(async (transaction) => {
295
+ await transaction.run("DELETE FROM vault_entries");
296
+ await transaction.run(REPLACE_META, require_vaultRules.VAULT_META_CHECK, packed);
297
+ });
298
+ onNotice(require_vaultRules.VAULT_KEY_LOST_NOTICE);
299
+ }
300
+ backend = source;
301
+ return {
302
+ key,
303
+ check: packed
304
+ };
305
+ });
306
+ }
307
+ function currentKey() {
308
+ loaded ??= guarded(loadKey).catch((error) => {
309
+ loaded = void 0;
310
+ throw error;
311
+ });
312
+ return loaded;
313
+ }
314
+ /** The key, reloaded first when another process has replaced `dek_check` since it was loaded. */
315
+ async function freshKey() {
316
+ const cached = await currentKey();
317
+ const check = await guarded(() => readMeta(require_vaultRules.VAULT_META_CHECK));
318
+ if (check !== void 0 && bytesEqual(check, cached.check)) return cached;
319
+ if (loaded !== void 0 && await loaded === cached) loaded = void 0;
320
+ return currentKey();
321
+ }
322
+ async function tryAcquire(name, owner) {
323
+ const at = now();
324
+ try {
325
+ return await database.get(ACQUIRE_LEASE, name, owner, at + leaseTtlMs, at) !== void 0;
326
+ } catch (error) {
327
+ if (isBusy(error)) return false;
328
+ throw new require_vaultRules.VaultError("unavailable");
329
+ }
330
+ }
331
+ async function lease(name, op, signal) {
332
+ if (closed) throw new require_vaultRules.VaultError("unavailable");
333
+ await ready();
334
+ const owner = (0, node_crypto.randomUUID)();
335
+ const deadline = now() + leaseWaitMs;
336
+ for (;;) {
337
+ signal?.throwIfAborted();
338
+ if (await tryAcquire(name, owner)) break;
339
+ if (now() >= deadline) throw new require_vaultRules.VaultError("unavailable");
340
+ await sleep(50 + Math.random() * 50, signal);
341
+ }
342
+ const controller = new AbortController();
343
+ const onAbort = () => controller.abort(signal?.reason);
344
+ signal?.addEventListener("abort", onAbort, { once: true });
345
+ const timer = setInterval(() => {
346
+ tryAcquire(name, owner).then((held) => {
347
+ if (!held) controller.abort(new require_vaultRules.VaultError("unavailable"));
348
+ }, () => void 0);
349
+ }, leaseRenewMs);
350
+ timer.unref?.();
351
+ renewals.add(timer);
352
+ try {
353
+ return await op(controller.signal);
354
+ } finally {
355
+ clearInterval(timer);
356
+ renewals.delete(timer);
357
+ signal?.removeEventListener("abort", onAbort);
358
+ try {
359
+ await database.run(RELEASE_LEASE, name, owner);
360
+ } catch {}
361
+ }
362
+ }
363
+ async function writeRow(sql, params) {
364
+ const row = await guarded(() => database.get(sql, ...params));
365
+ if (row === void 0) throw new require_vaultRules.VaultError("conflict");
366
+ return row;
367
+ }
368
+ return {
369
+ async get(namespace, key) {
370
+ require_vaultRules.assertEntryName(namespace, key);
371
+ await ready();
372
+ const row = await guarded(() => database.get(SELECT_ENTRY, namespace, key));
373
+ if (row === void 0) return void 0;
374
+ const aad = entryAad(namespace, key);
375
+ let loadedKey = await currentKey();
376
+ let text = open$1(loadedKey.key, boxOf(row), aad);
377
+ if (text === void 0) {
378
+ const check = await guarded(() => readMeta(require_vaultRules.VAULT_META_CHECK));
379
+ if (check === void 0 || !provesKey(loadedKey.key, check)) {
380
+ loaded = void 0;
381
+ loadedKey = await currentKey();
382
+ text = open$1(loadedKey.key, boxOf(row), aad);
383
+ }
384
+ }
385
+ if (text === void 0) {
386
+ await guarded(() => database.run("DELETE FROM vault_entries WHERE namespace = ? AND key = ? AND revision = ?", namespace, key, row.revision));
387
+ return;
388
+ }
389
+ return {
390
+ value: JSON.parse(text),
391
+ revision: row.revision,
392
+ updatedAt: row.updated_at
393
+ };
394
+ },
395
+ async put(namespace, key, value, putOptions) {
396
+ require_vaultRules.assertEntryName(namespace, key);
397
+ const expectRevision = putOptions?.expectRevision;
398
+ if (expectRevision !== void 0 && expectRevision !== null) require_vaultRules.assertRevision(expectRevision);
399
+ const text = require_vaultRules.serializeValue(value);
400
+ await ready();
401
+ const box = seal((await freshKey()).key, text, entryAad(namespace, key));
402
+ const at = now();
403
+ const row = expectRevision === void 0 ? await writeRow(PUT_LAST_WRITE_WINS, [
404
+ namespace,
405
+ key,
406
+ at,
407
+ box.iv,
408
+ box.tag,
409
+ box.ciphertext
410
+ ]) : expectRevision === null ? await writeRow(PUT_CREATE_ONLY, [
411
+ namespace,
412
+ key,
413
+ at,
414
+ box.iv,
415
+ box.tag,
416
+ box.ciphertext
417
+ ]) : await writeRow(PUT_EXPECT_REVISION, [
418
+ expectRevision + 1,
419
+ at,
420
+ box.iv,
421
+ box.tag,
422
+ box.ciphertext,
423
+ namespace,
424
+ key,
425
+ expectRevision
426
+ ]);
427
+ return {
428
+ value: JSON.parse(text),
429
+ revision: row.revision,
430
+ updatedAt: row.updated_at
431
+ };
432
+ },
433
+ async delete(namespace, key, deleteOptions) {
434
+ require_vaultRules.assertEntryName(namespace, key);
435
+ const expectRevision = deleteOptions?.expectRevision;
436
+ if (expectRevision !== void 0) require_vaultRules.assertRevision(expectRevision);
437
+ await ready();
438
+ if (expectRevision === void 0) {
439
+ await guarded(() => database.run("DELETE FROM vault_entries WHERE namespace = ? AND key = ?", namespace, key));
440
+ return;
441
+ }
442
+ await writeRow("DELETE FROM vault_entries WHERE namespace = ? AND key = ? AND revision = ? RETURNING revision, 0 AS updated_at", [
443
+ namespace,
444
+ key,
445
+ expectRevision
446
+ ]);
447
+ },
448
+ withLock(namespace, key, op, signal) {
449
+ try {
450
+ require_vaultRules.assertEntryName(namespace, key);
451
+ } catch (error) {
452
+ return Promise.reject(error);
453
+ }
454
+ if (typeof op !== "function") return Promise.reject(new require_vaultRules.VaultError("invalid"));
455
+ return lease(entryAad(namespace, key), op, signal);
456
+ },
457
+ status() {
458
+ return { backend };
459
+ },
460
+ close() {
461
+ closed = true;
462
+ for (const timer of renewals) clearInterval(timer);
463
+ renewals.clear();
464
+ }
465
+ };
466
+ }
467
+ //#endregion
468
+ //#region src/services/vaultDirectory/index.ts
469
+ /** Windows has no O_NOFOLLOW; there a plain open stands in. */
470
+ const NO_FOLLOW = node_fs.constants.O_NOFOLLOW ?? 0;
471
+ const GROUP_OTHER_BITS = 63;
472
+ const POSIX = process.platform !== "win32";
473
+ function currentUid() {
474
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
475
+ }
476
+ function ownedByUser(uid) {
477
+ const user = currentUid();
478
+ return user === void 0 || uid === user;
479
+ }
480
+ function errorCode(error) {
481
+ return error !== null && typeof error === "object" && "code" in error ? String(error.code) : void 0;
482
+ }
483
+ /**
484
+ * Creates `directory` owner-only, or checks and repairs an existing one.
485
+ *
486
+ * Refuses a symlink, a non-directory and a folder another user owns. Loose permission bits
487
+ * are tightened to 0700.
488
+ */
489
+ async function preparePrivateDirectory(directory) {
490
+ try {
491
+ await (0, node_fs_promises.mkdir)(directory, {
492
+ recursive: true,
493
+ mode: 448
494
+ });
495
+ const stats = await (0, node_fs_promises.lstat)(directory);
496
+ if (stats.isSymbolicLink() || !stats.isDirectory() || !ownedByUser(stats.uid)) throw new require_vaultRules.VaultError("unavailable");
497
+ if (POSIX && (stats.mode & GROUP_OTHER_BITS) !== 0) await (0, node_fs_promises.chmod)(directory, 448);
498
+ } catch (error) {
499
+ throw error instanceof require_vaultRules.VaultError ? error : new require_vaultRules.VaultError("unavailable");
500
+ }
501
+ }
502
+ /**
503
+ * The 32-byte key in `<directory>/key.bin`, or `undefined` when there is none.
504
+ *
505
+ * The file must be a regular, owner-only file with one link, opened without following a
506
+ * symlink; anything else throws `unavailable` so the caller never replaces a key it could not read.
507
+ */
508
+ async function readKeyFile(directory) {
509
+ let handle;
510
+ try {
511
+ handle = await (0, node_fs_promises.open)(node_path.default.join(directory, require_vaultRules.VAULT_KEY_FILE), node_fs.constants.O_RDONLY | NO_FOLLOW);
512
+ } catch (error) {
513
+ if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR") return void 0;
514
+ throw new require_vaultRules.VaultError("unavailable");
515
+ }
516
+ try {
517
+ const stats = await handle.stat();
518
+ if (!stats.isFile() || stats.nlink !== 1 || !ownedByUser(stats.uid) || POSIX && (stats.mode & GROUP_OTHER_BITS) !== 0 || stats.size !== 32) throw new require_vaultRules.VaultError("unavailable");
519
+ const key = Buffer.alloc(32);
520
+ const { bytesRead } = await handle.read(key, 0, 32, 0);
521
+ if (bytesRead !== 32) throw new require_vaultRules.VaultError("unavailable");
522
+ return key;
523
+ } catch (error) {
524
+ throw error instanceof require_vaultRules.VaultError ? error : new require_vaultRules.VaultError("unavailable");
525
+ } finally {
526
+ await handle.close();
527
+ }
528
+ }
529
+ /** Writes a new `key.bin` (0600, exclusive create, no symlink follow) and syncs it to disk. */
530
+ async function createKeyFile(directory, key) {
531
+ if (key.length !== 32) throw new require_vaultRules.VaultError("invalid");
532
+ await preparePrivateDirectory(directory);
533
+ try {
534
+ const handle = await (0, node_fs_promises.open)(node_path.default.join(directory, require_vaultRules.VAULT_KEY_FILE), node_fs.constants.O_WRONLY | node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | NO_FOLLOW, 384);
535
+ try {
536
+ await handle.write(key, 0, key.length, 0);
537
+ await handle.sync();
538
+ } finally {
539
+ await handle.close();
540
+ }
541
+ } catch {
542
+ throw new require_vaultRules.VaultError("unavailable");
543
+ }
544
+ }
545
+ /** Removes a `key.bin` the vault proved stale; a missing file is fine. */
546
+ async function removeKeyFile(directory) {
547
+ try {
548
+ await (0, node_fs_promises.unlink)(node_path.default.join(directory, require_vaultRules.VAULT_KEY_FILE));
549
+ } catch (error) {
550
+ if (errorCode(error) !== "ENOENT") throw new require_vaultRules.VaultError("unavailable");
551
+ }
552
+ }
553
+ //#endregion
554
+ //#region src/services/vaultKeyring/index.ts
555
+ /**
556
+ * The OS keyring through `@napi-rs/keyring`, loaded on first use, or `null` when this platform
557
+ * has no build of the module.
558
+ */
559
+ async function loadKeyring() {
560
+ try {
561
+ const { AsyncEntry } = await import("@napi-rs/keyring");
562
+ return {
563
+ getPassword: (account) => new AsyncEntry(require_vaultRules.VAULT_KEYRING_SERVICE, account).getPassword(),
564
+ setPassword: (account, password) => new AsyncEntry(require_vaultRules.VAULT_KEYRING_SERVICE, account).setPassword(password)
565
+ };
566
+ } catch {
567
+ return null;
568
+ }
569
+ }
570
+ //#endregion
571
+ //#region src/extensions/(backend)/_lib/root.server.ts
572
+ /**
573
+ * The vault, bound as `po/vault` at the global level on top of the shared `po/db`.
574
+ *
575
+ * Only a global host with a home directory builds it. The vault loads its key lazily on first
576
+ * use, so mounting never touches the keyring or `<po home>/vault`.
577
+ */
578
+ async function createPoVaultGlobalRoot(plugin) {
579
+ const host = plugin.host;
580
+ const homeDirectory = host.context.homeDirectory;
581
+ if (host.scope !== "global" || homeDirectory === void 0) return { value: void 0 };
582
+ const directory = node_path.default.join((0, _agimon_ai_po_core_piSettings.poHomeDirectory)(homeDirectory, host.context.environment ?? {}), require_vaultRules.VAULT_DIRECTORY);
583
+ let vault;
584
+ let boundDb;
585
+ const provider = plugin.context.inject([_agimon_ai_po_core_serverPlugin.PO_SERVER_SCOPE_SERVICE, _agimon_ai_po_db.PO_DB_SERVICE], (injected) => {
586
+ const db = (0, _agimon_ai_po_db.requirePoDb)(injected);
587
+ if (vault === void 0 || boundDb !== db) {
588
+ vault?.close();
589
+ boundDb = db;
590
+ vault = createVault({
591
+ db,
592
+ directory,
593
+ keyring: loadKeyring,
594
+ keyFiles: {
595
+ readKeyFile,
596
+ createKeyFile,
597
+ removeKeyFile
598
+ },
599
+ onNotice: (message) => host.context.onNotice(`Vault: ${message}`)
600
+ });
601
+ }
602
+ return (0, _agimon_ai_po_core_serverPlugin.requirePoServerScope)(injected).provide(require_contracts.PO_VAULT_CONTRACT, vault);
603
+ });
604
+ await provider.await();
605
+ return {
606
+ value: vault,
607
+ onDispose: async () => {
608
+ await provider.dispose();
609
+ vault?.close();
610
+ }
611
+ };
612
+ }
613
+ //#endregion
614
+ //#region src/extensions/(backend)/root.server.ts
615
+ var root_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoot)(createPoVaultGlobalRoot, { inject: [_agimon_ai_po_db.PO_DB_CONTRACT] });
616
+ //#endregion
617
+ //#region generated/server.ts
618
+ const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
619
+ name: "@agimon-ai/po-vault",
620
+ providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default),
621
+ injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({
622
+ global: [root_server_default],
623
+ workspace: [root_server_default],
624
+ session: [root_server_default]
625
+ }),
626
+ global: async (context) => {
627
+ const scopeGlobal = await context.scopeRoot("global", root_server_default, context);
628
+ return {
629
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal),
630
+ services: [...scopeGlobal.services ?? []]
631
+ };
632
+ },
633
+ workspace: async (context) => {
634
+ const scopeGlobal = await context.scopeRoot("global", root_server_default, context);
635
+ return {
636
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal),
637
+ services: [...scopeGlobal.services ?? []]
638
+ };
639
+ },
640
+ session: async (context) => {
641
+ const scopeGlobal = await context.scopeRoot("global", root_server_default, context);
642
+ return {
643
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeGlobal),
644
+ services: [...scopeGlobal.services ?? []],
645
+ get activities() {
646
+ return [...scopeGlobal.activities ?? []];
647
+ }
648
+ };
649
+ }
650
+ });
651
+ //#endregion
652
+ exports.default = serverPlugin;
653
+ exports.serverPlugin = serverPlugin;
654
+
655
+ //# sourceMappingURL=server.cjs.map