midsummer-sol 0.1.3 → 0.2.0

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 (5) hide show
  1. package/index.js +818 -51
  2. package/package.json +9 -33
  3. package/sol-mcp.js +373 -28
  4. package/sol-secret-mcp.js +3250 -0
  5. package/sol.js +9210 -1957
@@ -0,0 +1,3250 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+ import { createRequire } from "node:module";
4
+ var __defProp = Object.defineProperty;
5
+ var __returnValue = (v) => v;
6
+ function __exportSetter(name, newValue) {
7
+ this[name] = __returnValue.bind(null, newValue);
8
+ }
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true,
14
+ configurable: true,
15
+ set: __exportSetter.bind(all, name)
16
+ });
17
+ };
18
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
+
21
+ // src/crypto.ts
22
+ var exports_crypto = {};
23
+ __export(exports_crypto, {
24
+ wrapCekTo: () => wrapCekTo,
25
+ verifyPubkeySig: () => verifyPubkeySig,
26
+ unwrapCekWith: () => unwrapCekWith,
27
+ signPubkey: () => signPubkey,
28
+ sealWithRecovery: () => sealWithRecovery,
29
+ sealToAccounts: () => sealToAccounts,
30
+ sealContent: () => sealContent,
31
+ sameActor: () => sameActor,
32
+ rotate: () => rotate,
33
+ recoveryKeyPair: () => recoveryKeyPair,
34
+ pubFingerprint: () => pubFingerprint,
35
+ openWithRecovery: () => openWithRecovery,
36
+ openContent: () => openContent,
37
+ isRecipient: () => isRecipient,
38
+ generateRecoveryCode: () => generateRecoveryCode,
39
+ deriveIdentity: () => deriveIdentity,
40
+ ciphertextOf: () => ciphertextOf,
41
+ UNREADABLE: () => UNREADABLE,
42
+ KeyRing: () => KeyRing
43
+ });
44
+ import { createCipheriv, createDecipheriv, createHash as createHash2, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, scryptSync, sign as edSign, timingSafeEqual, verify as edVerify } from "node:crypto";
45
+
46
+ class KeyRing {
47
+ keys = new Map;
48
+ ensure(actor) {
49
+ let k = this.keys.get(actor);
50
+ if (!k) {
51
+ k = randomBytes(32);
52
+ this.keys.set(actor, k);
53
+ }
54
+ return k;
55
+ }
56
+ key(actor) {
57
+ return this.keys.get(actor);
58
+ }
59
+ serialize() {
60
+ const out = {};
61
+ for (const [a, k] of this.keys)
62
+ out[a] = k.toString("base64");
63
+ return out;
64
+ }
65
+ load(data) {
66
+ for (const [a, k] of Object.entries(data))
67
+ this.keys.set(a, Buffer.from(k, "base64"));
68
+ }
69
+ }
70
+ function aeadSeal(key, plain) {
71
+ const iv = randomBytes(12);
72
+ const c = createCipheriv("aes-256-gcm", key, iv);
73
+ const ct = Buffer.concat([c.update(plain), c.final()]);
74
+ return { iv: iv.toString("base64"), ct: ct.toString("base64"), tag: c.getAuthTag().toString("base64") };
75
+ }
76
+ function aeadOpen(key, box) {
77
+ const d = createDecipheriv("aes-256-gcm", key, Buffer.from(box.iv, "base64"));
78
+ d.setAuthTag(Buffer.from(box.tag, "base64"));
79
+ return Buffer.concat([d.update(Buffer.from(box.ct, "base64")), d.final()]);
80
+ }
81
+ function sealContent(ring, content, recipients, epoch = 1) {
82
+ const cek = randomBytes(32);
83
+ const body = aeadSeal(cek, Buffer.from(content, "utf8"));
84
+ const lockboxes = {};
85
+ for (const r of recipients)
86
+ lockboxes[r] = aeadSeal(ring.ensure(r), cek);
87
+ return { body, lockboxes, epoch };
88
+ }
89
+ function openContent(ring, box, actor, self) {
90
+ if (self) {
91
+ const alb = box.asym?.[self.accountId];
92
+ if (alb) {
93
+ try {
94
+ const cek = unwrapCekWith(self.privateKey, alb);
95
+ return aeadOpen(cek, box.body).toString("utf8");
96
+ } catch {
97
+ return UNREADABLE;
98
+ }
99
+ }
100
+ }
101
+ const lb = box.lockboxes[actor];
102
+ const key = ring.key(actor);
103
+ if (!lb || !key)
104
+ return UNREADABLE;
105
+ try {
106
+ const cek = aeadOpen(key, lb);
107
+ return aeadOpen(cek, box.body).toString("utf8");
108
+ } catch {
109
+ return UNREADABLE;
110
+ }
111
+ }
112
+ function isRecipient(box, actor) {
113
+ return actor in box.lockboxes || !!box.asym?.[actor];
114
+ }
115
+ function ciphertextOf(box) {
116
+ return box.body.ct;
117
+ }
118
+ function rotate(ring, box, content, newRecipients) {
119
+ return sealContent(ring, content, newRecipients, box.epoch + 1);
120
+ }
121
+ function sameActor(a, b) {
122
+ const ab = Buffer.from(a);
123
+ const bb = Buffer.from(b);
124
+ return ab.length === bb.length && timingSafeEqual(ab, bb);
125
+ }
126
+ function deriveWrapKey(shared, epkB64, recipientPubB64) {
127
+ if (shared.every((b) => b === 0))
128
+ throw new Error("invalid ECDH shared secret");
129
+ const info = Buffer.concat([RECOVERY_INFO, Buffer.from(epkB64, "base64"), Buffer.from(recipientPubB64, "base64")]);
130
+ return Buffer.from(hkdfSync("sha256", shared, Buffer.alloc(0), info, 32));
131
+ }
132
+ function recoveryKeyPair() {
133
+ const { publicKey, privateKey } = generateKeyPairSync("x25519");
134
+ return { publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"), privateKey };
135
+ }
136
+ function wrapCekTo(recipientPublicKeyB64, cek) {
137
+ const recipientPub = createPublicKey({ key: Buffer.from(recipientPublicKeyB64, "base64"), type: "spki", format: "der" });
138
+ const eph = generateKeyPairSync("x25519");
139
+ const epk = eph.publicKey.export({ type: "spki", format: "der" }).toString("base64");
140
+ const shared = diffieHellman({ privateKey: eph.privateKey, publicKey: recipientPub });
141
+ return { epk, box: aeadSeal(deriveWrapKey(shared, epk, recipientPublicKeyB64), cek) };
142
+ }
143
+ function unwrapCekWith(privateKey, lb) {
144
+ const recipientPubB64 = createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64");
145
+ const epk = createPublicKey({ key: Buffer.from(lb.epk, "base64"), type: "spki", format: "der" });
146
+ const shared = diffieHellman({ privateKey, publicKey: epk });
147
+ return aeadOpen(deriveWrapKey(shared, lb.epk, recipientPubB64), lb.box);
148
+ }
149
+ function sealWithRecovery(ring, content, recipients, recoveryPubKeys = {}, epoch = 1) {
150
+ const cek = randomBytes(32);
151
+ const body = aeadSeal(cek, Buffer.from(content, "utf8"));
152
+ const lockboxes = {};
153
+ for (const r of recipients)
154
+ lockboxes[r] = aeadSeal(ring.ensure(r), cek);
155
+ const recovery = {};
156
+ for (const [id, pub] of Object.entries(recoveryPubKeys))
157
+ recovery[id] = wrapCekTo(pub, cek);
158
+ return { body, lockboxes, recovery, epoch };
159
+ }
160
+ function openWithRecovery(privateKey, box, recoveryId) {
161
+ const lb = box.recovery?.[recoveryId];
162
+ if (!lb)
163
+ return UNREADABLE;
164
+ try {
165
+ const cek = unwrapCekWith(privateKey, lb);
166
+ return aeadOpen(cek, box.body).toString("utf8");
167
+ } catch {
168
+ return UNREADABLE;
169
+ }
170
+ }
171
+ function sealToAccounts(content, recipientPubKeys, opts = {}) {
172
+ const cek = randomBytes(32);
173
+ const body = aeadSeal(cek, Buffer.from(content, "utf8"));
174
+ const asym = {};
175
+ for (const [acct, pub] of Object.entries(recipientPubKeys))
176
+ asym[acct] = wrapCekTo(pub, cek);
177
+ const lockboxes = {};
178
+ if (opts.ring && opts.localRecipients)
179
+ for (const r of opts.localRecipients)
180
+ lockboxes[r] = aeadSeal(opts.ring.ensure(r), cek);
181
+ const recovery = {};
182
+ for (const [id, pub] of Object.entries(opts.recoveryPubKeys ?? {}))
183
+ recovery[id] = wrapCekTo(pub, cek);
184
+ const box = { body, lockboxes, asym, epoch: opts.epoch ?? 1 };
185
+ if (Object.keys(recovery).length)
186
+ box.recovery = recovery;
187
+ return box;
188
+ }
189
+ function x25519FromSeed(seed) {
190
+ const privateKey = createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
191
+ return { publicKey: createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64"), privateKey };
192
+ }
193
+ function ed25519FromSeed(seed) {
194
+ const privateKey = createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]), format: "der", type: "pkcs8" });
195
+ return { publicKey: createPublicKey(privateKey).export({ type: "spki", format: "der" }).toString("base64"), privateKey };
196
+ }
197
+ function generateRecoveryCode(words = 12) {
198
+ const out = [];
199
+ for (let i = 0;i < words; i++)
200
+ out.push(WORDLIST[randomBytes(2).readUInt16BE(0) % WORDLIST.length]);
201
+ return out.join(" ");
202
+ }
203
+ function deriveIdentity(accountId, recoveryCode, keyEpoch = 1) {
204
+ const salt = Buffer.from(`sol/identity/v1\x00${accountId}\x00epoch=${keyEpoch}`);
205
+ const xSeed = scryptSync(recoveryCode.normalize("NFKD"), Buffer.concat([salt, Buffer.from("\x00x25519")]), 32, KDF_PARAMS);
206
+ const eSeed = scryptSync(recoveryCode.normalize("NFKD"), Buffer.concat([salt, Buffer.from("\x00ed25519")]), 32, KDF_PARAMS);
207
+ const x = x25519FromSeed(xSeed);
208
+ const e = ed25519FromSeed(eSeed);
209
+ return { accountId, x25519Pub: x.publicKey, x25519Priv: x.privateKey, edPub: e.publicKey, edPriv: e.privateKey, keyEpoch };
210
+ }
211
+ function pubkeyBindingBytes(accountId, x25519Pub, keyEpoch) {
212
+ return Buffer.from(`sol/keydir/v1\x00${accountId}\x00${x25519Pub}\x00epoch=${keyEpoch}`);
213
+ }
214
+ function signPubkey(id) {
215
+ return edSign(null, pubkeyBindingBytes(id.accountId, id.x25519Pub, id.keyEpoch), id.edPriv).toString("base64");
216
+ }
217
+ function verifyPubkeySig(entry) {
218
+ try {
219
+ const edPub = createPublicKey({ key: Buffer.from(entry.edPub, "base64"), type: "spki", format: "der" });
220
+ return edVerify(null, pubkeyBindingBytes(entry.accountId, entry.x25519Pub, entry.keyEpoch), edPub, Buffer.from(entry.sig, "base64"));
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ function pubFingerprint(x25519Pub) {
226
+ const h = createHash2("sha256").update(Buffer.from(x25519Pub, "base64")).digest("hex");
227
+ return h.slice(0, 32).match(/.{4}/g).join("-");
228
+ }
229
+ var UNREADABLE = "<<unreadable>>", RECOVERY_INFO, X25519_PKCS8_PREFIX, ED25519_PKCS8_PREFIX, KDF_PARAMS, WORDLIST;
230
+ var init_crypto = __esm(() => {
231
+ RECOVERY_INFO = Buffer.from("forge-vcs/recovery/v1\x00");
232
+ X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
233
+ ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
234
+ KDF_PARAMS = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
235
+ WORDLIST = "abandon ability able about above absorb abstract access accident account accuse achieve acid acoustic acquire across action actor actual adapt add address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis".split(" ");
236
+ });
237
+
238
+ // src/sign.ts
239
+ import { createHash as createHash4, generateKeyPairSync as generateKeyPairSync2, createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2, sign as edSign2, verify as edVerify2 } from "node:crypto";
240
+ function fingerprintOf(pub) {
241
+ const der = Buffer.from(pub, "base64");
242
+ return "sol1:" + createHash4("sha256").update(der).digest("hex").slice(0, 16);
243
+ }
244
+ var ED25519_PKCS8_PREFIX2;
245
+ var init_sign = __esm(() => {
246
+ ED25519_PKCS8_PREFIX2 = Buffer.from("302e020100300506032b657004220420", "hex");
247
+ });
248
+
249
+ // src/bin/identity-store.ts
250
+ import { chmodSync as chmodSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
251
+ import { homedir as homedir2 } from "node:os";
252
+ import { dirname as dirname2, join as join5 } from "node:path";
253
+ function loadIdentity2() {
254
+ if (!existsSync6(IDENTITY_PATH2))
255
+ return;
256
+ try {
257
+ return JSON.parse(readFileSync7(IDENTITY_PATH2, "utf8"));
258
+ } catch {
259
+ return;
260
+ }
261
+ }
262
+ function loadVerified() {
263
+ if (!existsSync6(VERIFIED_PATH2))
264
+ return {};
265
+ try {
266
+ return JSON.parse(readFileSync7(VERIFIED_PATH2, "utf8"));
267
+ } catch {
268
+ return {};
269
+ }
270
+ }
271
+ function pinVerified(accountId, fingerprint) {
272
+ const all = loadVerified();
273
+ all[accountId] = fingerprint;
274
+ mkdirSync4(dirname2(VERIFIED_PATH2), { recursive: true });
275
+ writeFileSync6(VERIFIED_PATH2, JSON.stringify(all, null, 2), { mode: 384 });
276
+ try {
277
+ chmodSync2(VERIFIED_PATH2, 384);
278
+ } catch {}
279
+ }
280
+ var IDENTITY_PATH2, EXPORT_KDF2, VERIFIED_PATH2;
281
+ var init_identity_store = __esm(() => {
282
+ IDENTITY_PATH2 = join5(homedir2(), ".sol", "identity.json");
283
+ EXPORT_KDF2 = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
284
+ VERIFIED_PATH2 = join5(homedir2(), ".sol", "verified-keys.json");
285
+ });
286
+
287
+ // src/bin/seal-audience.ts
288
+ var exports_seal_audience = {};
289
+ __export(exports_seal_audience, {
290
+ slotForPath: () => slotForPath,
291
+ resolveRecipient: () => resolveRecipient,
292
+ recordNameSlot: () => recordNameSlot,
293
+ recordHiddenPath: () => recordHiddenPath,
294
+ recordAudience: () => recordAudience,
295
+ parseRecipient: () => parseRecipient,
296
+ loadSelfIdentity: () => loadSelfIdentity,
297
+ loadNameSlots: () => loadNameSlots,
298
+ loadManageIdentity: () => loadManageIdentity,
299
+ loadHiddenPaths: () => loadHiddenPaths,
300
+ loadAudiences: () => loadAudiences,
301
+ isHiddenPath: () => isHiddenPath,
302
+ hiddenPathSet: () => hiddenPathSet,
303
+ forgetHiddenPath: () => forgetHiddenPath,
304
+ fetchKey: () => fetchKey
305
+ });
306
+ import { existsSync as existsSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync3 } from "node:fs";
307
+ import { join as join6 } from "node:path";
308
+ function parseRecipient(raw) {
309
+ const t = raw.startsWith("@") ? raw.slice(1) : raw;
310
+ const at = t.indexOf("@");
311
+ if (at > 0)
312
+ return { raw, display: t.slice(0, at), accountId: t.slice(at + 1) };
313
+ return { raw, display: t, accountId: t };
314
+ }
315
+ async function fetchKey(dirUrl, accountId) {
316
+ try {
317
+ const res = await fetch(`${dirUrl}/keys/${encodeURIComponent(accountId)}`);
318
+ if (!res.ok)
319
+ return;
320
+ return await res.json();
321
+ } catch {
322
+ return;
323
+ }
324
+ }
325
+ async function resolveRecipient(dirUrl, spec) {
326
+ const entry = await fetchKey(dirUrl, spec.accountId);
327
+ if (!entry)
328
+ return;
329
+ const fingerprint = pubFingerprint(entry.x25519Pub);
330
+ const selfSigned = verifyPubkeySig(entry);
331
+ const pins = loadVerified();
332
+ const pinned = pins[spec.accountId];
333
+ const pinMismatch = pinned !== undefined && pinned.replace(/-/g, "") !== fingerprint.replace(/-/g, "");
334
+ if (selfSigned && pinned === undefined)
335
+ pinVerified(spec.accountId, fingerprint);
336
+ return { spec, entry, fingerprint, selfSigned, pinned: pinned !== undefined && !pinMismatch, pinMismatch };
337
+ }
338
+ function loadSelfIdentity(opts = {}) {
339
+ const stored = loadIdentity2();
340
+ if (!stored)
341
+ return;
342
+ const recoveryCode = opts.recoveryCode ?? process.env.SOL_RECOVERY_CODE;
343
+ if (!recoveryCode)
344
+ return;
345
+ const id = deriveIdentity(stored.accountId, recoveryCode, stored.keyEpoch);
346
+ if (id.x25519Pub !== stored.x25519Pub)
347
+ throw new Error("SOL_RECOVERY_CODE does not match this identity (derived a different key)");
348
+ return { accountId: stored.accountId, privateKey: id.x25519Priv };
349
+ }
350
+ function loadManageIdentity(opts = {}) {
351
+ const stored = loadIdentity2();
352
+ if (!stored)
353
+ return;
354
+ const recoveryCode = opts.recoveryCode ?? process.env.SOL_RECOVERY_CODE;
355
+ if (!recoveryCode)
356
+ return;
357
+ const id = deriveIdentity(stored.accountId, recoveryCode, stored.keyEpoch);
358
+ if (id.x25519Pub !== stored.x25519Pub)
359
+ throw new Error("SOL_RECOVERY_CODE does not match this identity (derived a different key)");
360
+ const signer = { priv: id.edPriv, pub: id.edPub, fingerprint: fingerprintOf(id.edPub) };
361
+ return {
362
+ accountId: stored.accountId,
363
+ x25519Pub: id.x25519Pub,
364
+ signer,
365
+ open: { accountId: stored.accountId, privateKey: id.x25519Priv }
366
+ };
367
+ }
368
+ function loadNameSlots(solDir) {
369
+ const p = nameSlotsPath(solDir);
370
+ if (!existsSync7(p))
371
+ return {};
372
+ try {
373
+ return JSON.parse(readFileSync8(p, "utf8"));
374
+ } catch {
375
+ return {};
376
+ }
377
+ }
378
+ function slotForPath(solDir, realPath) {
379
+ return loadNameSlots(solDir)[realPath];
380
+ }
381
+ function recordNameSlot(solDir, realPath, slotId) {
382
+ const all = loadNameSlots(solDir);
383
+ all[realPath] = slotId;
384
+ writeFileSync7(nameSlotsPath(solDir), JSON.stringify(all, null, 2), { mode: 384 });
385
+ try {
386
+ chmodSync3(nameSlotsPath(solDir), 384);
387
+ } catch {}
388
+ }
389
+ function loadHiddenPaths(solDir) {
390
+ const p = hiddenPathsPath(solDir);
391
+ if (!existsSync7(p))
392
+ return {};
393
+ try {
394
+ return JSON.parse(readFileSync8(p, "utf8"));
395
+ } catch {
396
+ return {};
397
+ }
398
+ }
399
+ function isHiddenPath(solDir, realPath) {
400
+ return loadHiddenPaths(solDir)[realPath] !== undefined;
401
+ }
402
+ function hiddenPathSet(solDir) {
403
+ return new Set(Object.keys(loadHiddenPaths(solDir)));
404
+ }
405
+ function recordHiddenPath(solDir, realPath, level) {
406
+ const all = loadHiddenPaths(solDir);
407
+ if (all[realPath] === "existence" && level === "name")
408
+ return;
409
+ all[realPath] = level;
410
+ writeFileSync7(hiddenPathsPath(solDir), JSON.stringify(all, null, 2), { mode: 384 });
411
+ try {
412
+ chmodSync3(hiddenPathsPath(solDir), 384);
413
+ } catch {}
414
+ }
415
+ function forgetHiddenPath(solDir, realPath) {
416
+ const all = loadHiddenPaths(solDir);
417
+ if (all[realPath] === undefined)
418
+ return;
419
+ delete all[realPath];
420
+ writeFileSync7(hiddenPathsPath(solDir), JSON.stringify(all, null, 2), { mode: 384 });
421
+ try {
422
+ chmodSync3(hiddenPathsPath(solDir), 384);
423
+ } catch {}
424
+ }
425
+ function loadAudiences(solDir) {
426
+ const p = audiencePath2(solDir);
427
+ if (!existsSync7(p))
428
+ return {};
429
+ try {
430
+ return JSON.parse(readFileSync8(p, "utf8"));
431
+ } catch {
432
+ return {};
433
+ }
434
+ }
435
+ function recordAudience(solDir, a) {
436
+ const all = loadAudiences(solDir);
437
+ all[a.path] = a;
438
+ writeFileSync7(audiencePath2(solDir), JSON.stringify(all, null, 2), { mode: 384 });
439
+ try {
440
+ chmodSync3(audiencePath2(solDir), 384);
441
+ } catch {}
442
+ }
443
+ var audiencePath2 = (solDir) => join6(solDir, "seal-audience.json"), nameSlotsPath = (solDir) => join6(solDir, "name-slots.json"), hiddenPathsPath = (solDir) => join6(solDir, "hidden-paths.json");
444
+ var init_seal_audience = __esm(() => {
445
+ init_crypto();
446
+ init_sign();
447
+ init_identity_store();
448
+ });
449
+
450
+ // src/bin/sol-secret-mcp.ts
451
+ import { existsSync as existsSync8 } from "fs";
452
+ import { join as join7 } from "path";
453
+
454
+ // ../vault-sdk/src/sanitizer.ts
455
+ import { createHash } from "node:crypto";
456
+
457
+ // ../vault-sdk/src/patterns.ts
458
+ var KNOWN_PREFIXES = [
459
+ {
460
+ pattern: /\bsk_live_[a-zA-Z0-9]{10,99}\b/g,
461
+ name: "STRIPE_SECRET",
462
+ type: "api_key",
463
+ confidence: "high"
464
+ },
465
+ {
466
+ pattern: /\bsk_test_[a-zA-Z0-9]{10,99}\b/g,
467
+ name: "STRIPE_TEST",
468
+ type: "api_key",
469
+ confidence: "high"
470
+ },
471
+ {
472
+ pattern: /\bpk_live_[a-zA-Z0-9]{10,99}\b/g,
473
+ name: "STRIPE_PUB",
474
+ type: "api_key",
475
+ confidence: "high"
476
+ },
477
+ {
478
+ pattern: /\brk_live_[a-zA-Z0-9]{10,99}\b/g,
479
+ name: "STRIPE_RESTRICTED",
480
+ type: "api_key",
481
+ confidence: "high"
482
+ },
483
+ {
484
+ pattern: /\bwhsec_[a-zA-Z0-9]{10,99}\b/g,
485
+ name: "STRIPE_WEBHOOK",
486
+ type: "webhook",
487
+ confidence: "high"
488
+ },
489
+ {
490
+ pattern: /\bsk-ant-api03-[a-zA-Z0-9_-]{80,}\b/g,
491
+ name: "ANTHROPIC_KEY",
492
+ type: "api_key",
493
+ confidence: "high"
494
+ },
495
+ {
496
+ pattern: /\bsk-proj-[a-zA-Z0-9_-]{40,}\b/g,
497
+ name: "OPENAI_KEY",
498
+ type: "api_key",
499
+ confidence: "high"
500
+ },
501
+ {
502
+ pattern: /\bsk-[a-zA-Z0-9]{40,50}\b/g,
503
+ name: "OPENAI_LEGACY",
504
+ type: "api_key",
505
+ confidence: "medium"
506
+ },
507
+ {
508
+ pattern: /\bAKIA[A-Z2-7]{16}\b/g,
509
+ name: "AWS_ACCESS_KEY",
510
+ type: "access_key",
511
+ confidence: "high"
512
+ },
513
+ {
514
+ pattern: /\bASIA[A-Z2-7]{16}\b/g,
515
+ name: "AWS_TEMP_KEY",
516
+ type: "access_key",
517
+ confidence: "high"
518
+ },
519
+ {
520
+ pattern: /\bAIza[a-zA-Z0-9_-]{35}\b/g,
521
+ name: "GOOGLE_API_KEY",
522
+ type: "api_key",
523
+ confidence: "high"
524
+ },
525
+ {
526
+ pattern: /\bghp_[a-zA-Z0-9]{36,}\b/g,
527
+ name: "GITHUB_PAT",
528
+ type: "access_token",
529
+ confidence: "high"
530
+ },
531
+ {
532
+ pattern: /\bgho_[a-zA-Z0-9]{36,}\b/g,
533
+ name: "GITHUB_OAUTH",
534
+ type: "access_token",
535
+ confidence: "high"
536
+ },
537
+ {
538
+ pattern: /\bghu_[a-zA-Z0-9]{36,}\b/g,
539
+ name: "GITHUB_USER",
540
+ type: "access_token",
541
+ confidence: "high"
542
+ },
543
+ {
544
+ pattern: /\bghs_[a-zA-Z0-9]{36,}\b/g,
545
+ name: "GITHUB_APP",
546
+ type: "access_token",
547
+ confidence: "high"
548
+ },
549
+ {
550
+ pattern: /\bghr_[a-zA-Z0-9]{36,}\b/g,
551
+ name: "GITHUB_REFRESH",
552
+ type: "refresh_token",
553
+ confidence: "high"
554
+ },
555
+ {
556
+ pattern: /\bgithub_pat_[a-zA-Z0-9_]{80,}\b/g,
557
+ name: "GITHUB_FINE_PAT",
558
+ type: "access_token",
559
+ confidence: "high"
560
+ },
561
+ {
562
+ pattern: /\bglpat-[a-zA-Z0-9_-]{20,}\b/g,
563
+ name: "GITLAB_PAT",
564
+ type: "access_token",
565
+ confidence: "high"
566
+ },
567
+ {
568
+ pattern: /\bxoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}\b/g,
569
+ name: "SLACK_BOT",
570
+ type: "access_token",
571
+ confidence: "high"
572
+ },
573
+ {
574
+ pattern: /\bxoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}\b/g,
575
+ name: "SLACK_USER",
576
+ type: "access_token",
577
+ confidence: "high"
578
+ },
579
+ {
580
+ pattern: /\bxapp-[0-9]{1}-[A-Za-z0-9]{30,}\b/g,
581
+ name: "SLACK_APP",
582
+ type: "access_token",
583
+ confidence: "high"
584
+ },
585
+ {
586
+ pattern: /\bSG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}\b/g,
587
+ name: "SENDGRID_KEY",
588
+ type: "api_key",
589
+ confidence: "high"
590
+ },
591
+ {
592
+ pattern: /\bre_[a-zA-Z0-9]{30,}\b/g,
593
+ name: "RESEND_KEY",
594
+ type: "api_key",
595
+ confidence: "medium"
596
+ },
597
+ {
598
+ pattern: /\bnpm_[a-zA-Z0-9]{36,}\b/g,
599
+ name: "NPM_TOKEN",
600
+ type: "access_token",
601
+ confidence: "high"
602
+ },
603
+ {
604
+ pattern: /\bpypi-[a-zA-Z0-9_-]{50,}\b/g,
605
+ name: "PYPI_TOKEN",
606
+ type: "access_token",
607
+ confidence: "high"
608
+ },
609
+ {
610
+ pattern: /\bsntrys_[a-zA-Z0-9+/]{50,}\b/g,
611
+ name: "SENTRY_TOKEN",
612
+ type: "access_token",
613
+ confidence: "high"
614
+ },
615
+ {
616
+ pattern: /\bhvs\.[a-zA-Z0-9_-]{24,}\b/g,
617
+ name: "HASHICORP_TOKEN",
618
+ type: "access_token",
619
+ confidence: "high"
620
+ },
621
+ {
622
+ pattern: /\bvercel_[a-zA-Z0-9]{24,}\b/g,
623
+ name: "VERCEL_TOKEN",
624
+ type: "access_token",
625
+ confidence: "high"
626
+ },
627
+ {
628
+ pattern: /\bshpat_[a-fA-F0-9]{32}\b/g,
629
+ name: "SHOPIFY_PAT",
630
+ type: "access_token",
631
+ confidence: "high"
632
+ }
633
+ ];
634
+ var STRUCTURAL_PATTERNS = [
635
+ {
636
+ pattern: /\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9/\\_-]{17,}\.[a-zA-Z0-9/\\_-]{10,}=*)\b/g,
637
+ name: "JWT",
638
+ type: "access_token",
639
+ confidence: "high"
640
+ },
641
+ {
642
+ pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]{20,}?-----END/g,
643
+ name: "PRIVATE_KEY",
644
+ type: "private_key",
645
+ confidence: "high"
646
+ },
647
+ {
648
+ pattern: /\b((?:postgres|mysql|mongodb|redis|amqp|mssql)(?:ql)?:\/\/[^\s'"]{10,})\b/g,
649
+ name: "CONNECTION_STRING",
650
+ type: "connection_string",
651
+ confidence: "high"
652
+ }
653
+ ];
654
+ var ENV_NAME_MAP = {
655
+ STRIPE_SECRET: "STRIPE_SECRET_KEY",
656
+ STRIPE_TEST: "STRIPE_TEST_KEY",
657
+ STRIPE_PUB: "STRIPE_PUBLISHABLE_KEY",
658
+ ANTHROPIC_KEY: "ANTHROPIC_API_KEY",
659
+ OPENAI_KEY: "OPENAI_API_KEY",
660
+ OPENAI_LEGACY: "OPENAI_API_KEY",
661
+ AWS_ACCESS_KEY: "AWS_ACCESS_KEY_ID",
662
+ GOOGLE_API_KEY: "GOOGLE_API_KEY",
663
+ GITHUB_PAT: "GITHUB_TOKEN",
664
+ GITHUB_FINE_PAT: "GITHUB_TOKEN",
665
+ GITLAB_PAT: "GITLAB_TOKEN",
666
+ SLACK_BOT: "SLACK_BOT_TOKEN",
667
+ SENDGRID_KEY: "SENDGRID_API_KEY",
668
+ RESEND_KEY: "RESEND_API_KEY",
669
+ NPM_TOKEN: "NPM_TOKEN",
670
+ SENTRY_TOKEN: "SENTRY_AUTH_TOKEN",
671
+ VERCEL_TOKEN: "VERCEL_TOKEN",
672
+ JWT: "AUTH_TOKEN",
673
+ CONNECTION_STRING: "DATABASE_URL",
674
+ PRIVATE_KEY: "PRIVATE_KEY"
675
+ };
676
+ function suggestEnvName(baseName) {
677
+ return ENV_NAME_MAP[baseName] ?? baseName;
678
+ }
679
+
680
+ // ../vault-sdk/src/entropy.ts
681
+ var BASE64_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=");
682
+ var HEX_CHARS = new Set("0123456789abcdefABCDEF");
683
+ var THRESHOLDS = {
684
+ hex: 3,
685
+ base64: 4.5,
686
+ mixed: 4
687
+ };
688
+ function detectCharset(str) {
689
+ let b64 = 0;
690
+ let hex = 0;
691
+ for (const ch of str) {
692
+ if (BASE64_CHARS.has(ch))
693
+ b64++;
694
+ if (HEX_CHARS.has(ch))
695
+ hex++;
696
+ }
697
+ const len = str.length;
698
+ if (hex / len > 0.95)
699
+ return "hex";
700
+ if (b64 / len > 0.95)
701
+ return "base64";
702
+ return "mixed";
703
+ }
704
+ function shannonEntropy(str) {
705
+ const freq = {};
706
+ for (const ch of str)
707
+ freq[ch] = (freq[ch] ?? 0) + 1;
708
+ let entropy = 0;
709
+ const len = str.length;
710
+ for (const count of Object.values(freq)) {
711
+ const p = count / len;
712
+ entropy -= p * Math.log2(p);
713
+ }
714
+ return entropy;
715
+ }
716
+ function isHighEntropy(str) {
717
+ if (str.length < 30)
718
+ return false;
719
+ if (/^[a-z]+$/.test(str))
720
+ return false;
721
+ if (/^[A-Z_]+$/.test(str))
722
+ return false;
723
+ if (str.includes("/") && str.split("/").length > 2)
724
+ return false;
725
+ if (/^https?:/.test(str))
726
+ return false;
727
+ if (/\$\{\{|\{\{/.test(str))
728
+ return false;
729
+ if (/^(.)\1+$/.test(str))
730
+ return false;
731
+ if (/^(your|changeme|placeholder|example|xxxxxxx|test)/i.test(str))
732
+ return false;
733
+ if (/^\d+$/.test(str))
734
+ return false;
735
+ const charset = detectCharset(str);
736
+ const entropy = shannonEntropy(str);
737
+ const threshold = THRESHOLDS[charset];
738
+ if (entropy < threshold)
739
+ return false;
740
+ const hasUpper = /[A-Z]/.test(str);
741
+ const hasLower = /[a-z]/.test(str);
742
+ const hasDigit = /[0-9]/.test(str);
743
+ return [hasUpper, hasLower, hasDigit].filter(Boolean).length >= 2;
744
+ }
745
+ var SECRET_KEYWORDS = /\b(password|passwd|pwd|secret|token|key|apikey|api_key|credential|auth|private|access_key|secret_key|bearer|authorization)\b/gi;
746
+ function hasKeywordNearby(text, start, end) {
747
+ const window = text.substring(Math.max(0, start - 80), Math.min(text.length, end + 80));
748
+ SECRET_KEYWORDS.lastIndex = 0;
749
+ return SECRET_KEYWORDS.test(window);
750
+ }
751
+
752
+ // ../vault-sdk/src/sanitizer.ts
753
+ function shortHash(value) {
754
+ return createHash("sha256").update(value).digest("hex").substring(0, 8);
755
+ }
756
+
757
+ class VaultSanitizer {
758
+ opts;
759
+ constructor(options = {}) {
760
+ this.opts = {
761
+ minEntropyLength: options.minEntropyLength ?? 30,
762
+ customPatterns: options.customPatterns ?? [],
763
+ onSecret: options.onSecret ?? (() => {})
764
+ };
765
+ }
766
+ sanitize(input) {
767
+ let text = input;
768
+ const secrets = [];
769
+ const allPrefixes = [
770
+ ...KNOWN_PREFIXES,
771
+ ...this.opts.customPatterns.map((p) => ({
772
+ ...p,
773
+ confidence: "high"
774
+ }))
775
+ ];
776
+ for (const rule of allPrefixes) {
777
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
778
+ let m;
779
+ while ((m = re.exec(text)) !== null) {
780
+ const val = m[0];
781
+ if (val.length < 10)
782
+ continue;
783
+ const r = this.replace(text, val, rule.name, rule.type, rule.confidence, true);
784
+ text = r.text;
785
+ secrets.push(r.secret);
786
+ re.lastIndex = 0;
787
+ }
788
+ }
789
+ for (const rule of STRUCTURAL_PATTERNS) {
790
+ const re = new RegExp(rule.pattern.source, rule.pattern.flags);
791
+ let m;
792
+ while ((m = re.exec(text)) !== null) {
793
+ const val = rule.group !== undefined ? m[rule.group] : m[0];
794
+ if (!val || val.length < 10 || val.includes("[vault:"))
795
+ continue;
796
+ const r = this.replace(text, val, rule.name, rule.type, rule.confidence, true);
797
+ text = r.text;
798
+ secrets.push(r.secret);
799
+ re.lastIndex = 0;
800
+ }
801
+ }
802
+ const entropyRe = /(?<![a-zA-Z0-9_/+=\-])([a-zA-Z0-9_/+=\-]{30,})(?![a-zA-Z0-9_/+=\-])/g;
803
+ let em;
804
+ while ((em = entropyRe.exec(text)) !== null) {
805
+ const c = em[1];
806
+ if (c.includes("[vault:") || c.includes("[REDACTED:"))
807
+ continue;
808
+ if (!isHighEntropy(c))
809
+ continue;
810
+ const nearKw = hasKeywordNearby(text, em.index, em.index + c.length);
811
+ const conf = nearKw ? "high" : "medium";
812
+ const tp = nearKw ? "credential" : "unknown";
813
+ const r = this.replace(text, c, "SECRET", tp, conf, false);
814
+ text = r.text;
815
+ secrets.push(r.secret);
816
+ entropyRe.lastIndex = 0;
817
+ }
818
+ for (const s of secrets)
819
+ this.opts.onSecret(s);
820
+ return { sanitized: text, secrets, hasSecrets: secrets.length > 0 };
821
+ }
822
+ generateGuidance(secrets) {
823
+ if (secrets.length === 0)
824
+ return "";
825
+ const auto = secrets.filter((s) => s.autoClassified);
826
+ const unknown = secrets.filter((s) => !s.autoClassified);
827
+ let g = `
828
+
829
+ <vault-context>
830
+ `;
831
+ g += `Secrets were detected and replaced with [vault:REF] references.
832
+
833
+ `;
834
+ if (auto.length > 0) {
835
+ g += `Auto-classified:
836
+ `;
837
+ for (const s of auto)
838
+ g += ` - [vault:${s.ref}] (${s.type}) → env var: ${s.suggestedEnvName}
839
+ `;
840
+ }
841
+ if (unknown.length > 0) {
842
+ g += `Needs naming — ask the user what env var to use:
843
+ `;
844
+ for (const s of unknown)
845
+ g += ` - [vault:${s.ref}] (${s.type})
846
+ `;
847
+ }
848
+ g += `
849
+ Use: vault run -- <command>
850
+ `;
851
+ g += "</vault-context>";
852
+ return g;
853
+ }
854
+ replace(text, value, name, type, confidence, autoClassified) {
855
+ const hash = shortHash(value);
856
+ const ref = `${name}_${hash}`;
857
+ return {
858
+ text: text.replace(value, `[vault:${ref}]`),
859
+ secret: {
860
+ ref,
861
+ value,
862
+ type,
863
+ confidence,
864
+ suggestedEnvName: suggestEnvName(name),
865
+ autoClassified
866
+ }
867
+ };
868
+ }
869
+ }
870
+
871
+ // src/bin/secret.ts
872
+ import { readFileSync as readFileSync5, readSync, writeSync } from "node:fs";
873
+ import { spawnSync } from "node:child_process";
874
+
875
+ // src/secret/toml.ts
876
+ function parseToml(src) {
877
+ const tables = [];
878
+ let current = { path: [], keys: {} };
879
+ tables.push(current);
880
+ const lines = src.split(`
881
+ `);
882
+ for (let i = 0;i < lines.length; i++) {
883
+ const raw = lines[i];
884
+ const line = stripComment(raw).trim();
885
+ if (!line)
886
+ continue;
887
+ if (line.startsWith("[")) {
888
+ const close = line.indexOf("]");
889
+ if (close < 0)
890
+ throw new Error(`toml: unterminated table header on line ${i + 1}: ${raw.trim()}`);
891
+ const path = parseHeaderPath(line.slice(1, close));
892
+ current = { path, keys: {} };
893
+ tables.push(current);
894
+ continue;
895
+ }
896
+ const eq = line.indexOf("=");
897
+ if (eq < 0)
898
+ throw new Error(`toml: expected key = value on line ${i + 1}: ${raw.trim()}`);
899
+ const key = line.slice(0, eq).trim();
900
+ const valStr = line.slice(eq + 1).trim();
901
+ current.keys[key] = parseValue(valStr, i + 1);
902
+ }
903
+ return { tables };
904
+ }
905
+ function stripComment(line) {
906
+ let inStr = false;
907
+ for (let i = 0;i < line.length; i++) {
908
+ const c = line[i];
909
+ if (c === '"')
910
+ inStr = !inStr;
911
+ else if (!inStr && (c === "#" || c === ";"))
912
+ return line.slice(0, i);
913
+ }
914
+ return line;
915
+ }
916
+ function parseHeaderPath(s) {
917
+ const out = [];
918
+ let i = 0;
919
+ const t = s.trim();
920
+ while (i < t.length) {
921
+ if (t[i] === '"') {
922
+ const end = t.indexOf('"', i + 1);
923
+ if (end < 0)
924
+ throw new Error(`toml: unterminated quoted table segment in [${s}]`);
925
+ out.push(t.slice(i + 1, end));
926
+ i = end + 1;
927
+ } else {
928
+ let j = i;
929
+ while (j < t.length && t[j] !== ".")
930
+ j++;
931
+ const seg = t.slice(i, j).trim();
932
+ if (seg)
933
+ out.push(seg);
934
+ i = j;
935
+ }
936
+ while (i < t.length && (t[i] === "." || t[i] === " "))
937
+ i++;
938
+ }
939
+ return out;
940
+ }
941
+ function parseValue(s, lineNo) {
942
+ if (s.startsWith("[")) {
943
+ const close = s.lastIndexOf("]");
944
+ if (close < 0)
945
+ throw new Error(`toml: unterminated array on line ${lineNo}`);
946
+ const inner = s.slice(1, close).trim();
947
+ if (!inner)
948
+ return [];
949
+ return splitArray(inner).map((e) => parseScalarString(e.trim(), lineNo));
950
+ }
951
+ if (s.startsWith('"'))
952
+ return parseScalarString(s, lineNo);
953
+ if (s === "true")
954
+ return true;
955
+ if (s === "false")
956
+ return false;
957
+ if (/^-?\d+$/.test(s))
958
+ return parseInt(s, 10);
959
+ if (/^-?\d+\.\d+$/.test(s))
960
+ return parseFloat(s);
961
+ if (/^[A-Za-z0-9_./:+-]+$/.test(s))
962
+ return s;
963
+ throw new Error(`toml: unparseable value on line ${lineNo}: ${s}`);
964
+ }
965
+ function splitArray(s) {
966
+ const out = [];
967
+ let depth = 0;
968
+ let inStr = false;
969
+ let start = 0;
970
+ for (let i = 0;i < s.length; i++) {
971
+ const c = s[i];
972
+ if (c === '"')
973
+ inStr = !inStr;
974
+ else if (!inStr && c === "," && depth === 0) {
975
+ out.push(s.slice(start, i));
976
+ start = i + 1;
977
+ }
978
+ }
979
+ const last = s.slice(start).trim();
980
+ if (last)
981
+ out.push(last);
982
+ return out;
983
+ }
984
+ function parseScalarString(s, lineNo) {
985
+ if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
986
+ return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
987
+ }
988
+ throw new Error(`toml: expected a quoted string on line ${lineNo}: ${s}`);
989
+ }
990
+ function serializeToml(doc) {
991
+ const out = [];
992
+ for (const table of doc.tables) {
993
+ if (table.path.length)
994
+ out.push(`[${table.path.map(headerSegment).join(".")}]`);
995
+ for (const [k, v] of Object.entries(table.keys))
996
+ out.push(`${k} = ${serializeValue(v)}`);
997
+ out.push("");
998
+ }
999
+ return out.join(`
1000
+ `).replace(/\n+$/, `
1001
+ `);
1002
+ }
1003
+ function headerSegment(seg) {
1004
+ return /^[A-Za-z0-9_-]+$/.test(seg) ? seg : `"${seg}"`;
1005
+ }
1006
+ function serializeValue(v) {
1007
+ if (Array.isArray(v))
1008
+ return `[${v.map((e) => `"${escapeStr(e)}"`).join(", ")}]`;
1009
+ if (typeof v === "string")
1010
+ return `"${escapeStr(v)}"`;
1011
+ return String(v);
1012
+ }
1013
+ function escapeStr(s) {
1014
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1015
+ }
1016
+ function tableAt(doc, path) {
1017
+ return doc.tables.find((t) => t.path.length === path.length && t.path.every((s, i) => s === path[i]));
1018
+ }
1019
+ function tablesUnder(doc, prefix) {
1020
+ return doc.tables.filter((t) => t.path.length === prefix.length + 1 && prefix.every((s, i) => s === t.path[i]));
1021
+ }
1022
+ function topTable(doc) {
1023
+ return doc.tables.find((t) => t.path.length === 0) ?? { path: [], keys: {} };
1024
+ }
1025
+ // src/secret/manifest.ts
1026
+ var SECRET_SLOT_FIELDS = ["type", "seal", "aud", "hide_name"];
1027
+ function parseEnvFile(env, src) {
1028
+ const doc = parseToml(src);
1029
+ const top = topTable(doc);
1030
+ const out = { env, config: {}, secrets: [] };
1031
+ if (typeof top.keys["@extends"] === "string")
1032
+ out.extends = top.keys["@extends"];
1033
+ if (Array.isArray(top.keys["@audience"]))
1034
+ out.audience = top.keys["@audience"];
1035
+ const config = tableAt(doc, ["config"]);
1036
+ if (config) {
1037
+ for (const [k, v] of Object.entries(config.keys)) {
1038
+ out.config[k] = Array.isArray(v) ? v.join(",") : String(v);
1039
+ }
1040
+ }
1041
+ for (const t of tablesUnder(doc, ["secret"])) {
1042
+ out.secrets.push(parseSecretSlot(env, t));
1043
+ }
1044
+ return out;
1045
+ }
1046
+ function parseSecretSlot(env, t) {
1047
+ const name = t.path[1];
1048
+ for (const k of Object.keys(t.keys)) {
1049
+ if (!SECRET_SLOT_FIELDS.includes(k)) {
1050
+ if (k === "value") {
1051
+ throw new Error(`fail-closed: [secret.${name}] in ${env} carries a plaintext \`value\` — secrets are SEALED, never written in cleartext. use \`sol secret set ${name} --env ${env}\` (value via TTY/--stdin), or put plaintext under [config].`);
1052
+ }
1053
+ throw new Error(`[secret.${name}] in ${env}: unknown field \`${k}\` — a secret slot may only set ${SECRET_SLOT_FIELDS.join("/")}.`);
1054
+ }
1055
+ }
1056
+ const slot = { name };
1057
+ if (typeof t.keys.type === "string")
1058
+ slot.type = t.keys.type;
1059
+ if (typeof t.keys.seal === "string")
1060
+ slot.seal = t.keys.seal;
1061
+ if (Array.isArray(t.keys.aud))
1062
+ slot.aud = t.keys.aud;
1063
+ if (t.keys.hide_name === true)
1064
+ slot.hideName = true;
1065
+ return slot;
1066
+ }
1067
+ function serializeEnvFile(f) {
1068
+ const doc = { tables: [] };
1069
+ const top = { path: [], keys: {} };
1070
+ if (f.extends)
1071
+ top.keys["@extends"] = f.extends;
1072
+ if (f.audience)
1073
+ top.keys["@audience"] = f.audience;
1074
+ doc.tables.push(top);
1075
+ if (Object.keys(f.config).length) {
1076
+ doc.tables.push({ path: ["config"], keys: { ...f.config } });
1077
+ }
1078
+ for (const s of f.secrets) {
1079
+ const keys = {};
1080
+ if (s.type)
1081
+ keys.type = s.type;
1082
+ if (s.seal)
1083
+ keys.seal = s.seal;
1084
+ if (s.aud)
1085
+ keys.aud = s.aud;
1086
+ if (s.hideName)
1087
+ keys.hide_name = true;
1088
+ doc.tables.push({ path: ["secret", s.name], keys });
1089
+ }
1090
+ return serializeToml(doc);
1091
+ }
1092
+ function resolveEnv(file, base) {
1093
+ const config = { ...base?.config ?? {}, ...file.config };
1094
+ const byName = new Map;
1095
+ for (const s of base?.secrets ?? [])
1096
+ byName.set(s.name, s);
1097
+ for (const s of file.secrets) {
1098
+ const prior = byName.get(s.name);
1099
+ byName.set(s.name, prior ? { ...prior, ...s } : s);
1100
+ }
1101
+ return { env: file.env, audience: file.audience ?? base?.audience, config, secrets: [...byName.values()] };
1102
+ }
1103
+ function parseManifest(src) {
1104
+ const doc = parseToml(src);
1105
+ const envs = tableAt(doc, ["envs"]);
1106
+ const order = Array.isArray(envs?.keys.order) ? envs.keys.order : [];
1107
+ const extendsOf = {};
1108
+ for (const t of tablesUnder(doc, ["env"])) {
1109
+ const name = t.path[1];
1110
+ if (typeof t.keys.extends === "string")
1111
+ extendsOf[name] = t.keys.extends;
1112
+ else
1113
+ extendsOf[name] = "base";
1114
+ }
1115
+ const runtime = {};
1116
+ const rt = tableAt(doc, ["runtime"]);
1117
+ if (rt) {
1118
+ for (const [k, v] of Object.entries(rt.keys))
1119
+ if (typeof v === "string")
1120
+ runtime[k] = v;
1121
+ }
1122
+ return { order, extendsOf, runtime };
1123
+ }
1124
+ function serializeManifest(m) {
1125
+ const doc = { tables: [] };
1126
+ doc.tables.push({ path: ["envs"], keys: { order: m.order } });
1127
+ for (const env of m.order)
1128
+ doc.tables.push({ path: ["env", env], keys: { extends: m.extendsOf[env] ?? "base" } });
1129
+ if (Object.keys(m.runtime).length)
1130
+ doc.tables.push({ path: ["runtime"], keys: { ...m.runtime } });
1131
+ return serializeToml(doc);
1132
+ }
1133
+ function buildSchema(order, resolved) {
1134
+ const vars = {};
1135
+ const audiences = {};
1136
+ for (const r of resolved) {
1137
+ if (r.audience)
1138
+ audiences[r.env] = r.audience;
1139
+ for (const [name, value] of Object.entries(r.config)) {
1140
+ const v = vars[name] ??= { kind: "config", envs: {} };
1141
+ v.envs[r.env] = value;
1142
+ }
1143
+ for (const s of r.secrets) {
1144
+ const v = vars[s.name] ??= { kind: "secret", envs: {}, audience: {} };
1145
+ if (s.type && !v.type)
1146
+ v.type = s.type;
1147
+ if (s.hideName)
1148
+ v.hidden = true;
1149
+ v.envs[r.env] = s.seal ? "sealed" : "unset";
1150
+ if (s.aud)
1151
+ (v.audience ??= {})[r.env] = s.aud;
1152
+ }
1153
+ }
1154
+ return { vars, audiences, order };
1155
+ }
1156
+ function serializeSchema(schema) {
1157
+ return JSON.stringify(sortDeep(schema), null, 2) + `
1158
+ `;
1159
+ }
1160
+ function sortDeep(v) {
1161
+ if (Array.isArray(v))
1162
+ return v.map(sortDeep);
1163
+ if (v && typeof v === "object") {
1164
+ const out = {};
1165
+ for (const k of Object.keys(v).sort())
1166
+ out[k] = sortDeep(v[k]);
1167
+ return out;
1168
+ }
1169
+ return v;
1170
+ }
1171
+ // src/secret/audience.ts
1172
+ function parseAudienceDoc(src) {
1173
+ const doc = parseToml(src);
1174
+ const handles = {};
1175
+ for (const kind of ["role", "team", "machine"]) {
1176
+ for (const t of tablesUnder(doc, [kind])) {
1177
+ const handle = `${kind}:${t.path[1]}`;
1178
+ handles[handle] = recipientsFromTable(handle, t);
1179
+ }
1180
+ }
1181
+ const clone = {};
1182
+ const cl = tableAt(doc, ["clone"]);
1183
+ if (cl) {
1184
+ for (const [env, v] of Object.entries(cl.keys))
1185
+ if (Array.isArray(v))
1186
+ clone[env] = v;
1187
+ }
1188
+ return { handles, clone };
1189
+ }
1190
+ function recipientsFromTable(handle, t) {
1191
+ const accounts = Array.isArray(t.keys.accounts) ? t.keys.accounts : [];
1192
+ const pubkeys = Array.isArray(t.keys.pubkeys) ? t.keys.pubkeys : [];
1193
+ const edpubs = Array.isArray(t.keys.edpubs) ? t.keys.edpubs : [];
1194
+ const out = [];
1195
+ for (let i = 0;i < accounts.length; i++) {
1196
+ out.push({ handle, accountId: accounts[i], x25519Pub: pubkeys[i] ?? "", edPub: edpubs[i] || undefined });
1197
+ }
1198
+ return out;
1199
+ }
1200
+ function serializeAudienceDoc(a) {
1201
+ const doc = { tables: [] };
1202
+ for (const [handle, recips] of Object.entries(a.handles)) {
1203
+ const [kind, name] = splitHandle(handle);
1204
+ doc.tables.push({
1205
+ path: [kind, name],
1206
+ keys: { accounts: recips.map((r) => r.accountId), pubkeys: recips.map((r) => r.x25519Pub), edpubs: recips.map((r) => r.edPub ?? "") }
1207
+ });
1208
+ }
1209
+ if (Object.keys(a.clone).length)
1210
+ doc.tables.push({ path: ["clone"], keys: { ...a.clone } });
1211
+ return serializeToml(doc);
1212
+ }
1213
+ function splitHandle(handle) {
1214
+ const i = handle.indexOf(":");
1215
+ if (i < 0)
1216
+ return ["role", handle];
1217
+ return [handle.slice(0, i), handle.slice(i + 1)];
1218
+ }
1219
+ function resolveHandles(gate, handles) {
1220
+ const recipientPubKeys = {};
1221
+ const recipients = [];
1222
+ const accountIds = new Set;
1223
+ const unresolved = [];
1224
+ for (const h of handles) {
1225
+ const recips = gate.handles[h];
1226
+ if (!recips || !recips.length) {
1227
+ unresolved.push(h);
1228
+ continue;
1229
+ }
1230
+ for (const r of recips) {
1231
+ if (!r.x25519Pub) {
1232
+ unresolved.push(`${h} (${r.accountId}: no pinned pubkey)`);
1233
+ continue;
1234
+ }
1235
+ recipientPubKeys[r.accountId] = r.x25519Pub;
1236
+ recipients.push(r);
1237
+ accountIds.add(r.accountId);
1238
+ }
1239
+ }
1240
+ return { recipientPubKeys, recipients, accountIds: [...accountIds], unresolved };
1241
+ }
1242
+ function audienceHasAccount(gate, handles, accountId) {
1243
+ for (const h of handles)
1244
+ for (const r of gate.handles[h] ?? [])
1245
+ if (r.accountId === accountId)
1246
+ return true;
1247
+ return false;
1248
+ }
1249
+ // src/secret/store.ts
1250
+ init_crypto();
1251
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1252
+ import { createHash as createHash3 } from "node:crypto";
1253
+ import { join } from "node:path";
1254
+ var envDir = (solDir) => join(solDir, "env");
1255
+ var sealDir = (solDir) => join(envDir(solDir), "seal");
1256
+ var manifestPath = (solDir) => join(envDir(solDir), "manifest.toml");
1257
+ var audiencePath = (solDir) => join(envDir(solDir), "audience.toml");
1258
+ var schemaLockPath = (solDir) => join(envDir(solDir), "schema.lock");
1259
+ var envFilePath = (solDir, env) => join(envDir(solDir), `${env}.env.toml`);
1260
+ var stanzaPath = (solDir, digest) => join(sealDir(solDir), `${digestFile(digest)}.stanzas`);
1261
+ function digestFile(digest) {
1262
+ const i = digest.indexOf(":");
1263
+ return i < 0 ? digest : digest.slice(i + 1);
1264
+ }
1265
+ function sealDigest(boxJson) {
1266
+ return "sha256:" + createHash3("sha256").update(boxJson).digest("hex").slice(0, 32);
1267
+ }
1268
+
1269
+ class TamperedSealError extends Error {
1270
+ expected;
1271
+ actual;
1272
+ constructor(expected, actual) {
1273
+ super(`tampered sealed leaf: the bytes at ${expected}.stanzas hash to ${actual} — the stanza was overwritten in place (content-address mismatch). a forged box sealed to the public key cannot pass; reseal with \`sol secret set\` or revert the tampering.`);
1274
+ this.expected = expected;
1275
+ this.actual = actual;
1276
+ this.name = "TamperedSealError";
1277
+ }
1278
+ }
1279
+ function stanzaMatchesAddress(raw, expected) {
1280
+ return sealDigest(raw) === expected;
1281
+ }
1282
+ function writeSealedValue(solDir, value, recipientPubKeys, epoch = 1) {
1283
+ const box = sealToAccounts(value, recipientPubKeys, { epoch });
1284
+ const boxJson = JSON.stringify(box);
1285
+ const digest = sealDigest(boxJson);
1286
+ mkdirSync(sealDir(solDir), { recursive: true });
1287
+ writeFileSync(stanzaPath(solDir, digest), boxJson, { mode: 384 });
1288
+ return digest;
1289
+ }
1290
+ function readSealedBox(solDir, digest) {
1291
+ const r = readSealedBoxChecked(solDir, digest);
1292
+ if (r.kind === "absent")
1293
+ return;
1294
+ if (r.kind === "tampered")
1295
+ throw new TamperedSealError(digest, r.actual);
1296
+ return r.box;
1297
+ }
1298
+ function readSealedBoxChecked(solDir, digest) {
1299
+ const p = stanzaPath(solDir, digest);
1300
+ if (!existsSync(p))
1301
+ return { kind: "absent" };
1302
+ const raw = readFileSync(p, "utf8");
1303
+ if (!stanzaMatchesAddress(raw, digest))
1304
+ return { kind: "tampered", actual: sealDigest(raw) };
1305
+ return { kind: "box", box: JSON.parse(raw) };
1306
+ }
1307
+ function recipientsOf(box) {
1308
+ return Object.keys(box.asym ?? {});
1309
+ }
1310
+ function openSealedValue(solDir, digest, actor, self) {
1311
+ const r = readSealedBoxChecked(solDir, digest);
1312
+ if (r.kind === "absent")
1313
+ return;
1314
+ if (r.kind === "tampered")
1315
+ return UNREADABLE;
1316
+ return openContent(new KeyRing, r.box, actor, self);
1317
+ }
1318
+ function openOutcome(solDir, digest, actor, self) {
1319
+ const r = readSealedBoxChecked(solDir, digest);
1320
+ if (r.kind === "absent")
1321
+ return { kind: "absent" };
1322
+ if (r.kind === "tampered")
1323
+ return { kind: "tampered", actual: r.actual };
1324
+ const box = r.box;
1325
+ const mineAsym = !!(self && box.asym?.[self.accountId]);
1326
+ if (!mineAsym)
1327
+ return { kind: "not-recipient" };
1328
+ const opened = openContent(new KeyRing, box, actor, self);
1329
+ if (opened === UNREADABLE)
1330
+ return { kind: "decrypt-failed" };
1331
+ return { kind: "value", value: opened };
1332
+ }
1333
+ function canWriteSealed(solDir, digest, actor, self) {
1334
+ const out = openOutcome(solDir, digest, actor, self);
1335
+ return out.kind === "value" || out.kind === "absent";
1336
+ }
1337
+ function formatSolRef(env, name) {
1338
+ return `sol://${env}/${name}`;
1339
+ }
1340
+ function projectField(value, field) {
1341
+ if (!field)
1342
+ return value;
1343
+ let doc;
1344
+ try {
1345
+ doc = JSON.parse(value);
1346
+ } catch {
1347
+ throw new Error(`#${field}: the sealed value is not a JSON document — no sub-field to project`);
1348
+ }
1349
+ if (!doc || typeof doc !== "object" || !(field in doc)) {
1350
+ throw new Error(`#${field}: no such field in the sealed value`);
1351
+ }
1352
+ return String(doc[field]);
1353
+ }
1354
+ // src/secret/identity.ts
1355
+ init_sign();
1356
+ function handleHasPubkey(gate, handle, x25519Pub) {
1357
+ if (!x25519Pub)
1358
+ return false;
1359
+ return (gate.handles[handle] ?? []).some((r) => r.x25519Pub === x25519Pub);
1360
+ }
1361
+ var ADMIN_HANDLES = ["role:owner", "role:admin"];
1362
+ function isGuardianPubkey(gate, x25519Pub) {
1363
+ if (!x25519Pub)
1364
+ return false;
1365
+ for (const h of ADMIN_HANDLES)
1366
+ if (handleHasPubkey(gate, h, x25519Pub))
1367
+ return true;
1368
+ return false;
1369
+ }
1370
+ // src/secret/journal.ts
1371
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1372
+ import { createHash as createHash5, sign as edSign3, verify as edVerify3, createPublicKey as createPublicKey3 } from "node:crypto";
1373
+ import { join as join2 } from "node:path";
1374
+ init_sign();
1375
+ var journalPath = (solDir) => join2(envDir(solDir), "journal.jsonl");
1376
+ function canonicalJournal(e) {
1377
+ return JSON.stringify({
1378
+ op: e.op,
1379
+ ...e.env !== undefined ? { env: e.env } : {},
1380
+ ...e.name !== undefined ? { name: e.name } : {},
1381
+ ...e.handle !== undefined ? { handle: e.handle } : {},
1382
+ ...e.members !== undefined ? { members: e.members } : {},
1383
+ author: e.author,
1384
+ authorKey: e.authorKey,
1385
+ recipientAtOp: e.recipientAtOp,
1386
+ ...e.prevSeal !== undefined ? { prevSeal: e.prevSeal } : {},
1387
+ ...e.newSeal !== undefined ? { newSeal: e.newSeal } : {},
1388
+ ...e.prevAud !== undefined ? { prevAud: e.prevAud } : {},
1389
+ ...e.newAud !== undefined ? { newAud: e.newAud } : {}
1390
+ });
1391
+ }
1392
+ function hashEntry(prevHash, e) {
1393
+ return "jh:" + createHash5("sha256").update(canonicalJournal(e) + "\x00" + (prevHash ?? "")).digest("hex").slice(0, 32);
1394
+ }
1395
+ function chainTip(solDir) {
1396
+ const entries = readJournal(solDir);
1397
+ return entries.length ? entries[entries.length - 1].entryHash : undefined;
1398
+ }
1399
+ function appendJournal(solDir, body, signer) {
1400
+ const prevHash = chainTip(solDir);
1401
+ const entry = { ...body, authorKey: signer.pub, prevHash };
1402
+ entry.entryHash = hashEntry(prevHash, entry);
1403
+ entry.sig = edSign3(null, Buffer.from(entry.entryHash), signer.priv).toString("base64");
1404
+ appendFileSync(journalPath(solDir), JSON.stringify(entry) + `
1405
+ `, { mode: 420 });
1406
+ return entry;
1407
+ }
1408
+ function readJournal(solDir) {
1409
+ const p = journalPath(solDir);
1410
+ if (!existsSync2(p))
1411
+ return [];
1412
+ return readFileSync2(p, "utf8").split(`
1413
+ `).filter((l) => l.trim()).map((l) => JSON.parse(l));
1414
+ }
1415
+ function verifyJournal(solDir) {
1416
+ return verifyJournalEntries(readJournal(solDir));
1417
+ }
1418
+ function verifyJournalEntries(entries) {
1419
+ const issues = [];
1420
+ let prev;
1421
+ for (let i = 0;i < entries.length; i++) {
1422
+ const e = entries[i];
1423
+ if (e.prevHash !== prev) {
1424
+ issues.push(`entry ${i} (${e.op} ${e.env ?? ""}/${e.name ?? e.handle ?? ""}): prevHash mismatch — the log was edited/reordered`);
1425
+ }
1426
+ const expect = hashEntry(prev, e);
1427
+ if (e.entryHash !== expect) {
1428
+ issues.push(`entry ${i} (${e.op}): entryHash mismatch — the entry body was tampered`);
1429
+ }
1430
+ if (!e.authorKey || !e.sig) {
1431
+ issues.push(`entry ${i} (${e.op}): unsigned management entry — a fabricated row carries no author signature`);
1432
+ } else {
1433
+ try {
1434
+ const pub = createPublicKey3({ key: Buffer.from(e.authorKey, "base64"), format: "der", type: "spki" });
1435
+ if (!edVerify3(null, Buffer.from(e.entryHash ?? ""), pub, Buffer.from(e.sig, "base64"))) {
1436
+ issues.push(`entry ${i} (${e.op}): bad signature — not signed by the embedded author key`);
1437
+ }
1438
+ } catch {
1439
+ issues.push(`entry ${i} (${e.op}): malformed author key / signature`);
1440
+ }
1441
+ }
1442
+ prev = e.entryHash;
1443
+ }
1444
+ return { ok: issues.length === 0, issues, entries };
1445
+ }
1446
+ function authorFingerprint(e) {
1447
+ return e.authorKey ? fingerprintOf(e.authorKey) : undefined;
1448
+ }
1449
+ // src/secret/anchor.ts
1450
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1451
+ import { join as join3 } from "node:path";
1452
+ init_sign();
1453
+ var creatorPinPath = (solDir) => join3(envDir(solDir), "creator.pin");
1454
+ var headPinPath = (solDir) => join3(envDir(solDir), "head.pin");
1455
+ function repoSolDir(solDir) {
1456
+ return solDir;
1457
+ }
1458
+ function opLogAnchor(solDir) {
1459
+ const p = join3(repoSolDir(solDir), "HEAD");
1460
+ if (!existsSync3(p))
1461
+ return {};
1462
+ try {
1463
+ const h = JSON.parse(readFileSync3(p, "utf8"));
1464
+ return { logTip: h.logTip, head: h.head, seq: h.seq };
1465
+ } catch {
1466
+ return {};
1467
+ }
1468
+ }
1469
+ function opLogGenesis(solDir) {
1470
+ const p = join3(repoSolDir(solDir), "ops.jsonl");
1471
+ if (!existsSync3(p))
1472
+ return;
1473
+ try {
1474
+ const first = readFileSync3(p, "utf8").split(`
1475
+ `).find((l) => l.trim());
1476
+ if (!first)
1477
+ return;
1478
+ const op = JSON.parse(first);
1479
+ return op.entryHash;
1480
+ } catch {
1481
+ return;
1482
+ }
1483
+ }
1484
+ function readCreatorPin(solDir) {
1485
+ const p = creatorPinPath(solDir);
1486
+ if (!existsSync3(p))
1487
+ return;
1488
+ try {
1489
+ return JSON.parse(readFileSync3(p, "utf8"));
1490
+ } catch {
1491
+ return;
1492
+ }
1493
+ }
1494
+ function writeCreatorPin(solDir, pin) {
1495
+ const full = { v: 1, ...pin, at: Date.now() };
1496
+ writeFileSync3(creatorPinPath(solDir), JSON.stringify(full, null, 2), { mode: 420 });
1497
+ return full;
1498
+ }
1499
+ function creatorFingerprint(solDir) {
1500
+ const pin = readCreatorPin(solDir);
1501
+ return pin?.authKey ? fingerprintOf(pin.authKey) : undefined;
1502
+ }
1503
+ function readHeadPin(solDir) {
1504
+ const p = headPinPath(solDir);
1505
+ if (!existsSync3(p))
1506
+ return;
1507
+ try {
1508
+ return JSON.parse(readFileSync3(p, "utf8"));
1509
+ } catch {
1510
+ return;
1511
+ }
1512
+ }
1513
+ function updateHeadPin(solDir) {
1514
+ const entries = readJournal(solDir);
1515
+ const journalHead = entries.length ? entries[entries.length - 1].entryHash : undefined;
1516
+ const { logTip } = opLogAnchor(solDir);
1517
+ const pin = { v: 1, journalHead, journalLen: entries.length, opLogTip: logTip, at: Date.now() };
1518
+ writeFileSync3(headPinPath(solDir), JSON.stringify(pin, null, 2), { mode: 420 });
1519
+ return pin;
1520
+ }
1521
+ function validateMonotonicHead(solDir) {
1522
+ const out = [];
1523
+ const entries = readJournal(solDir);
1524
+ if (!entries.length)
1525
+ return out;
1526
+ const pin = readHeadPin(solDir);
1527
+ if (!pin) {
1528
+ out.push({ severity: "error", check: "journal-monotonic", message: "no head pin (.sol/env/head.pin) for a journal that carries management entries — the monotonic anchor was deleted; rollback/truncation can no longer be ruled out. re-pin via a signed management op or restore the pin." });
1529
+ return out;
1530
+ }
1531
+ if (entries.length < pin.journalLen) {
1532
+ out.push({ severity: "error", check: "journal-monotonic", message: `journal TRUNCATED: ${entries.length} entries on disk but the head pin recorded ${pin.journalLen} — the tail (a later set/rotation) was dropped, resurrecting a stale value. restore the full journal or re-pin from an authoritative copy.` });
1533
+ return out;
1534
+ }
1535
+ if (pin.journalHead && !entries.some((e) => e.entryHash === pin.journalHead)) {
1536
+ out.push({ severity: "error", check: "journal-monotonic", message: `journal ROLLED BACK: the pinned head ${pin.journalHead} is absent from the live chain — \`.sol/env\` was rewound to an earlier snapshot (a rotated/revoked value may be resurrected). restore from the op-log-anchored head or re-validate against the remote.` });
1537
+ }
1538
+ const live = opLogAnchor(solDir);
1539
+ if (pin.opLogTip && live.logTip && pin.opLogTip !== live.logTip) {
1540
+ out.push({
1541
+ severity: "warn",
1542
+ check: "journal-monotonic",
1543
+ message: `the env-journal head pin is anchored to op-log tip ${pin.opLogTip} but the live op-log is at ${live.logTip}. if no unrelated commit advanced the op-log, \`.sol/env\` may have been rolled back to an earlier snapshot (the pin reverted with it). a hard verdict needs an EXTERNAL anchor (the remote tracking the env-journal head) — pure offline total rollback is not locally detectable.`
1544
+ });
1545
+ }
1546
+ return out;
1547
+ }
1548
+ function validateSealJournalBinding(solDir, liveSeals) {
1549
+ const out = [];
1550
+ const journal = readJournal(solDir);
1551
+ if (!journal.length)
1552
+ return out;
1553
+ const journalSeal = new Map;
1554
+ const sealAuthored = new Set;
1555
+ for (const e of journal) {
1556
+ if (!e.env || !e.name)
1557
+ continue;
1558
+ const key = `${e.env}/${e.name}`;
1559
+ if (e.newSeal)
1560
+ sealAuthored.add(key);
1561
+ if (e.newSeal === e.prevSeal)
1562
+ continue;
1563
+ journalSeal.set(key, e.newSeal);
1564
+ }
1565
+ for (const [key, manifestSeal] of liveSeals) {
1566
+ if (!sealAuthored.has(key)) {
1567
+ out.push({
1568
+ severity: "error",
1569
+ check: "seal-journal-binding",
1570
+ message: `${key}: the manifest pins a seal (${manifestSeal}) but the SIGNED journal records NO authorized set op for it — an unanchored/forged seal. a non-recipient may seal a box to the owner's PUBLIC key and repoint the manifest while rolling the journal back; with no journal authority behind it the seal is REJECTED. reseal with \`sol secret set\` (which writes a signed op) or revert the tampering.`
1571
+ });
1572
+ continue;
1573
+ }
1574
+ const headSeal = journalSeal.get(key);
1575
+ if (headSeal !== undefined && headSeal !== manifestSeal) {
1576
+ out.push({
1577
+ severity: "error",
1578
+ check: "seal-journal-binding",
1579
+ message: `${key}: the manifest seal (${manifestSeal}) does NOT match the seal the signed journal head records (${headSeal}). either the manifest was repointed to an unauthored stanza, or the journal was rewound below the live seal. the live content address must equal the signed-journal head seal.`
1580
+ });
1581
+ }
1582
+ }
1583
+ return out;
1584
+ }
1585
+ function validateJournalReconciliation(solDir, liveSeals, liveEnvs) {
1586
+ const out = [];
1587
+ const journal = readJournal(solDir);
1588
+ if (!journal.length)
1589
+ return out;
1590
+ const journalHeadSeal = new Map;
1591
+ const journalEnvs = new Set;
1592
+ for (const e of journal) {
1593
+ if (!e.env || !e.name)
1594
+ continue;
1595
+ journalEnvs.add(e.env);
1596
+ if (e.newSeal === e.prevSeal)
1597
+ continue;
1598
+ journalHeadSeal.set(`${e.env}/${e.name}`, e.newSeal);
1599
+ }
1600
+ for (const [key, headSeal] of journalHeadSeal) {
1601
+ if (!headSeal)
1602
+ continue;
1603
+ if (!liveSeals.has(key)) {
1604
+ out.push({
1605
+ severity: "error",
1606
+ check: "journal-reconcile",
1607
+ message: `${key}: the SIGNED journal head records this slot as SEALED (${headSeal}) but it is no longer a declared sealed secret in the working tree — the [secret.${key.split("/")[1]}] stanza was DELETED (a silent destroy/lockout). the journal is the source of truth; restore the declaration or record an authorized unset.`
1608
+ });
1609
+ }
1610
+ }
1611
+ for (const env of journalEnvs) {
1612
+ if (!liveEnvs.has(env)) {
1613
+ out.push({
1614
+ severity: "error",
1615
+ check: "journal-reconcile",
1616
+ message: `${env}: the SIGNED journal records secret ops in env "${env}" but it is missing from manifest order — the env was DROPPED, hiding its sealed secrets (owner reveal -> "unknown env"). restore the env to the manifest order or record an authorized removal.`
1617
+ });
1618
+ }
1619
+ }
1620
+ return out;
1621
+ }
1622
+ function validateGenesisAnchor(solDir) {
1623
+ const out = [];
1624
+ const pin = readCreatorPin(solDir);
1625
+ const liveGenesis = opLogGenesis(solDir);
1626
+ if (pin?.opGenesis && liveGenesis && pin.opGenesis !== liveGenesis) {
1627
+ out.push({
1628
+ severity: "error",
1629
+ check: "genesis-anchor",
1630
+ message: `creator.pin is anchored to op-log genesis ${pin.opGenesis} but the live op-log genesis is ${liveGenesis} — the creator pin was FORGED (an intruder named themselves creator and rebuilt the journal/gate around their key) OR the op-log was replaced. the pin's genesis MUST equal the content-addressed op-log birth certificate. restore the true creator pin / op-log.`
1631
+ });
1632
+ }
1633
+ const head = readHeadPin(solDir);
1634
+ const live = opLogAnchor(solDir);
1635
+ if (head?.opLogTip && !live.logTip) {
1636
+ out.push({
1637
+ severity: "error",
1638
+ check: "genesis-anchor",
1639
+ message: `head.pin records op-log tip ${head.opLogTip} but the live op-log has NO tip (.sol/HEAD is absent/empty) — the op-log anchor was deleted, removing the external reference the head pin binds to. restore the op-log or re-anchor.`
1640
+ });
1641
+ }
1642
+ return out;
1643
+ }
1644
+ // src/secret/env-state.ts
1645
+ init_sign();
1646
+ // src/secret/gate-derive.ts
1647
+ init_sign();
1648
+ var ADMIN_HANDLES2 = ["role:owner", "role:admin"];
1649
+ var isGuardianHandle = (h) => ADMIN_HANDLES2.includes(h);
1650
+ function guardianFingerprints(handles) {
1651
+ const fps = new Set;
1652
+ for (const h of ADMIN_HANDLES2)
1653
+ for (const r of handles[h] ?? [])
1654
+ if (r.authKey)
1655
+ fps.add(fingerprintOf(r.authKey));
1656
+ return fps;
1657
+ }
1658
+ function authorizedFor(handle, handles, authorFpr) {
1659
+ const guardians = guardianFingerprints(handles);
1660
+ if (isGuardianHandle(handle))
1661
+ return !!authorFpr && guardians.has(authorFpr);
1662
+ const existing = handles[handle] ?? [];
1663
+ if (!existing.length)
1664
+ return true;
1665
+ const memberFprs = new Set(existing.filter((r) => r.authKey).map((r) => fingerprintOf(r.authKey)));
1666
+ return !!authorFpr && memberFprs.has(authorFpr) || !!authorFpr && guardians.has(authorFpr);
1667
+ }
1668
+ function deriveAuthenticGate(solDir) {
1669
+ const v = verifyJournal(solDir);
1670
+ const handles = {};
1671
+ const issues = [...v.issues];
1672
+ if (!v.ok) {
1673
+ return { handles, issues, trustworthy: false };
1674
+ }
1675
+ const creatorFpr = creatorFingerprint(solDir);
1676
+ let bootstrapped = false;
1677
+ for (const e of v.entries) {
1678
+ if (e.op !== "audience-add" && e.op !== "audience-rm" && e.op !== "gate-bootstrap")
1679
+ continue;
1680
+ if (!e.handle)
1681
+ continue;
1682
+ const authorFpr = authorFingerprint(e);
1683
+ const members = (e.members ?? []).map((m) => ({ accountId: m.accountId, pubkey: m.pubkey, authKey: m.authKey }));
1684
+ if (e.op === "gate-bootstrap") {
1685
+ const selfSeeds = members.some((m) => m.authKey && fingerprintOf(m.authKey) === authorFpr);
1686
+ if (!selfSeeds) {
1687
+ issues.push(`gate-bootstrap for ${e.handle}: author is not among the seeded guardians — refused`);
1688
+ continue;
1689
+ }
1690
+ if (creatorFpr && authorFpr !== creatorFpr) {
1691
+ issues.push(`gate-bootstrap for ${e.handle}: authored by ${e.author || "unidentified"} (${authorFpr ?? "no key"}), NOT the pinned repo creator (${creatorFpr}) — a re-bootstrap by a foreign key cannot seize genesis. refused.`);
1692
+ continue;
1693
+ }
1694
+ bootstrapped = true;
1695
+ handles[e.handle] = members;
1696
+ continue;
1697
+ }
1698
+ if (!authorizedFor(e.handle, handles, authorFpr)) {
1699
+ issues.push(`${e.op} on ${e.handle} by ${e.author || "unidentified"} was not authorized (not a member/guardian) — dropped from the authentic gate`);
1700
+ continue;
1701
+ }
1702
+ handles[e.handle] = members;
1703
+ }
1704
+ return { handles, issues, trustworthy: true };
1705
+ }
1706
+ // src/secret/authz.ts
1707
+ function isGateAdmin(gate, id) {
1708
+ return isGuardianPubkey(gate, id?.x25519Pub);
1709
+ }
1710
+ function isHandleMember(gate, handle, id) {
1711
+ return handleHasPubkey(gate, handle, id?.x25519Pub);
1712
+ }
1713
+ function isGuardianHandle2(handle) {
1714
+ return ADMIN_HANDLES.includes(handle);
1715
+ }
1716
+ function canEditHandle(gate, handle, id) {
1717
+ const exists = !!gate.handles[handle] && gate.handles[handle].length > 0;
1718
+ if (isGuardianHandle2(handle)) {
1719
+ if (isGuardianPubkey(gate, id?.x25519Pub))
1720
+ return { allowed: true };
1721
+ return {
1722
+ allowed: false,
1723
+ reason: `refused: ${handle} is a GUARDIAN handle — only a current role:owner/role:admin may add a guardian (self-enrollment into a guardian role is refused even when empty). the repo creator is the bootstrapped guardian.`
1724
+ };
1725
+ }
1726
+ if (!exists)
1727
+ return { allowed: true };
1728
+ if (isHandleMember(gate, handle, id) || isGateAdmin(gate, id))
1729
+ return { allowed: true };
1730
+ return {
1731
+ allowed: false,
1732
+ reason: `refused: only a current member of ${handle} (or a role:owner/role:admin) may add/remove recipients. set SOL_RECOVERY_CODE, or have a member run this. creating a NEW handle stays open.`
1733
+ };
1734
+ }
1735
+ function canDeclare(solDir, prior, actor, id) {
1736
+ const guarded = !!prior && (!!prior.seal || !!(prior.aud && prior.aud.length));
1737
+ if (!guarded)
1738
+ return { allowed: true, guarded: false };
1739
+ if (prior.seal)
1740
+ return { allowed: canWriteSealed(solDir, prior.seal, actor, id?.open), guarded: true };
1741
+ return { allowed: !!id, guarded: true };
1742
+ }
1743
+ // src/secret/model.ts
1744
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
1745
+ init_sign();
1746
+ var BASE_ENV = "base";
1747
+ function envInitialized(solDir) {
1748
+ return existsSync4(manifestPath(solDir));
1749
+ }
1750
+ function loadWorld(solDir) {
1751
+ if (!envInitialized(solDir))
1752
+ throw new Error("no env manifest — run `sol env init` (or `sol secret declare` creates one)");
1753
+ const manifest = parseManifest(readFileSync4(manifestPath(solDir), "utf8"));
1754
+ const gate = existsSync4(audiencePath(solDir)) ? parseAudienceDoc(readFileSync4(audiencePath(solDir), "utf8")) : { handles: {}, clone: {} };
1755
+ const files = {};
1756
+ const baseP = envFilePath(solDir, BASE_ENV);
1757
+ if (existsSync4(baseP))
1758
+ files[BASE_ENV] = parseEnvFile(BASE_ENV, readFileSync4(baseP, "utf8"));
1759
+ for (const env of manifest.order) {
1760
+ const p = envFilePath(solDir, env);
1761
+ if (existsSync4(p))
1762
+ files[env] = parseEnvFile(env, readFileSync4(p, "utf8"));
1763
+ }
1764
+ return { manifest, gate, files };
1765
+ }
1766
+ function resolveOne(world, env) {
1767
+ const file = world.files[env];
1768
+ if (!file)
1769
+ throw new Error(`unknown env: ${env} (registered: ${world.manifest.order.join(", ") || "none"})`);
1770
+ const base = file.extends ? world.files[file.extends] : undefined;
1771
+ return resolveEnv(file, base);
1772
+ }
1773
+ function resolveAll(world) {
1774
+ return world.manifest.order.map((e) => resolveOne(world, e));
1775
+ }
1776
+ function audienceFor(world, env, entryAud) {
1777
+ if (entryAud && entryAud.length)
1778
+ return entryAud;
1779
+ return world.files[env]?.audience ?? [];
1780
+ }
1781
+ function ensureEnvDir(solDir) {
1782
+ mkdirSync2(envDir(solDir), { recursive: true });
1783
+ mkdirSync2(sealDir(solDir), { recursive: true });
1784
+ }
1785
+ function writeEnvFile(solDir, f) {
1786
+ ensureEnvDir(solDir);
1787
+ writeFileSync4(envFilePath(solDir, f.env), serializeEnvFile(f), { mode: 420 });
1788
+ }
1789
+ function writeManifest(solDir, m) {
1790
+ ensureEnvDir(solDir);
1791
+ writeFileSync4(manifestPath(solDir), serializeManifest(m), { mode: 420 });
1792
+ }
1793
+ function writeAudienceDoc(solDir, a) {
1794
+ ensureEnvDir(solDir);
1795
+ writeFileSync4(audiencePath(solDir), serializeAudienceDoc(a), { mode: 420 });
1796
+ }
1797
+ function regenerateSchema(solDir, world) {
1798
+ const schema = buildSchema(world.manifest.order, resolveAll(world));
1799
+ const text = serializeSchema(schema);
1800
+ ensureEnvDir(solDir);
1801
+ writeFileSync4(schemaLockPath(solDir), text, { mode: 420 });
1802
+ return text;
1803
+ }
1804
+ function loadSchema(solDir) {
1805
+ const p = schemaLockPath(solDir);
1806
+ if (!existsSync4(p))
1807
+ return;
1808
+ return JSON.parse(readFileSync4(p, "utf8"));
1809
+ }
1810
+ function validateWorld(solDir, world, opts = {}) {
1811
+ const issues = [];
1812
+ const resolved = resolveAll(world);
1813
+ const jv = verifyJournal(solDir);
1814
+ for (const m of jv.issues)
1815
+ issues.push({ severity: "error", check: "journal-integrity", message: `journal: ${m}` });
1816
+ const authentic = deriveAuthenticGate(solDir);
1817
+ for (const m of authentic.issues)
1818
+ issues.push({ severity: "error", check: "gate-authenticity", message: `gate: ${m}` });
1819
+ if (authentic.trustworthy && Object.keys(authentic.handles).length) {
1820
+ issues.push(...gateDriftIssues(authentic, world.gate));
1821
+ }
1822
+ for (const r of resolved) {
1823
+ for (const s of r.secrets) {
1824
+ if (!s.seal)
1825
+ continue;
1826
+ const read = readSealedBoxChecked(solDir, s.seal);
1827
+ if (read.kind === "absent") {
1828
+ issues.push({ severity: "error", check: "fail-closed", message: `${r.env}/${s.name}: \`seal\` pins ${s.seal} but no sealed leaf exists at .sol/env/seal/` });
1829
+ continue;
1830
+ }
1831
+ if (read.kind === "tampered") {
1832
+ issues.push({ severity: "error", check: "content-address", message: `${r.env}/${s.name}: the sealed leaf at ${s.seal} was OVERWRITTEN IN PLACE — its bytes hash to ${read.actual}, not the pinned content address. a forged box (sealed to the public key) was substituted. reseal with \`sol secret set\` or revert.` });
1833
+ continue;
1834
+ }
1835
+ const box = read.box;
1836
+ if (!box.body || !box.asym) {
1837
+ issues.push({ severity: "error", check: "fail-closed", message: `${r.env}/${s.name}: the leaf at ${s.seal} is not a SealedBox (plaintext under the secrets namespace is rejected)` });
1838
+ }
1839
+ }
1840
+ }
1841
+ const allNames = new Set;
1842
+ for (const r of resolved)
1843
+ for (const s of r.secrets)
1844
+ allNames.add(s.name);
1845
+ for (const r of resolved)
1846
+ for (const k of Object.keys(r.config))
1847
+ allNames.add(k);
1848
+ for (const name of allNames) {
1849
+ const present = resolved.filter((r) => r.secrets.some((s) => s.name === name) || (name in r.config)).map((r) => r.env);
1850
+ const missing = resolved.filter((r) => !present.includes(r.env)).map((r) => r.env);
1851
+ if (missing.length && present.length) {
1852
+ issues.push({ severity: "warn", check: "cross-env-drift", message: `${name}: declared in [${present.join(", ")}] but missing in [${missing.join(", ")}]` });
1853
+ }
1854
+ }
1855
+ for (const r of resolved) {
1856
+ for (const s of r.secrets) {
1857
+ if (!s.seal)
1858
+ continue;
1859
+ const aud = audienceFor(world, r.env, s.aud);
1860
+ const resolvedAud = resolveHandles(world.gate, aud);
1861
+ for (const u of resolvedAud.unresolved) {
1862
+ issues.push({ severity: "error", check: "audience-integrity", message: `${r.env}/${s.name}: audience handle not in audience.toml: ${u}` });
1863
+ }
1864
+ const read = readSealedBoxChecked(solDir, s.seal);
1865
+ const box = read.kind === "box" ? read.box : undefined;
1866
+ if (box) {
1867
+ const actual = new Set(recipientsOf(box));
1868
+ const expected = new Set(resolvedAud.accountIds);
1869
+ const extra = [...actual].filter((a) => !expected.has(a));
1870
+ const missing = [...expected].filter((a) => !actual.has(a));
1871
+ if (extra.length || missing.length) {
1872
+ issues.push({ severity: "error", check: "audience-integrity", message: `${r.env}/${s.name}: sealed recipients drift from the audience (missing: [${missing.join(", ")}], extra: [${extra.join(", ")}]) — reseal with \`sol secret set\` or \`sol secret audience\`` });
1873
+ }
1874
+ }
1875
+ if (opts.agentAccountId && audienceHasAccount(world.gate, aud, opts.agentAccountId)) {
1876
+ issues.push({ severity: "error", check: "agent-blind", message: `${r.env}/${s.name}: the agent identity ${opts.agentAccountId} is in the seal audience — secrets must be agent-blind` });
1877
+ }
1878
+ }
1879
+ const runtimeHandle = world.manifest.runtime[r.env];
1880
+ if (runtimeHandle && r.secrets.some((s) => s.seal)) {
1881
+ const aud = world.files[r.env]?.audience ?? [];
1882
+ if (!aud.includes(runtimeHandle)) {
1883
+ issues.push({ severity: "warn", check: "audience-integrity", message: `${r.env}: the runtime recipient ${runtimeHandle} is not in the env @audience — the runtime can't inject` });
1884
+ }
1885
+ }
1886
+ }
1887
+ if (opts.schemaOnDisk !== undefined) {
1888
+ const fresh = serializeSchema(buildSchema(world.manifest.order, resolved));
1889
+ if (fresh !== opts.schemaOnDisk) {
1890
+ issues.push({ severity: "error", check: "schema-stale", message: "schema.lock is stale — run `sol env schema --write` to regenerate" });
1891
+ }
1892
+ }
1893
+ issues.push(...validateManagementPlane(solDir, resolved, authentic));
1894
+ if (!readCreatorPin(solDir) && authentic.trustworthy && Object.keys(authentic.handles).length) {
1895
+ issues.push({ severity: "warn", check: "genesis-anchor", message: "this repo has a journal-derived gate but no creator pin (.sol/env/creator.pin) — the genesis is UNANCHORED; a re-bootstrap by a foreign key cannot be distinguished from the true creator. fix: run `sol env anchor` (as the operator, with SOL_RECOVERY_CODE set) to pin the current creator, or restore the lost pin." });
1896
+ }
1897
+ const liveSeals = new Map;
1898
+ for (const r of resolved)
1899
+ for (const s of r.secrets)
1900
+ if (s.seal)
1901
+ liveSeals.set(`${r.env}/${s.name}`, s.seal);
1902
+ const liveEnvs = new Set(world.manifest.order);
1903
+ issues.push(...validateSealJournalBinding(solDir, liveSeals));
1904
+ issues.push(...validateJournalReconciliation(solDir, liveSeals, liveEnvs));
1905
+ issues.push(...validateGenesisAnchor(solDir));
1906
+ issues.push(...validateMonotonicHead(solDir));
1907
+ return issues;
1908
+ }
1909
+ function gateDriftIssues(authentic, live) {
1910
+ const out = [];
1911
+ const norm = (rs) => {
1912
+ const m = new Map;
1913
+ for (const r of rs)
1914
+ m.set(r.accountId, r.pubkey ?? r.x25519Pub ?? "");
1915
+ return m;
1916
+ };
1917
+ const handles = new Set([...Object.keys(authentic.handles), ...Object.keys(live.handles)]);
1918
+ for (const h of handles) {
1919
+ const a = norm(authentic.handles[h] ?? []);
1920
+ const l = norm(live.handles[h] ?? []);
1921
+ for (const [acct, pub] of a) {
1922
+ if (!l.has(acct))
1923
+ out.push({ severity: "error", check: "gate-authenticity", message: `${h}: ${acct} is in the authentic (journal-signed) gate but MISSING from audience.toml — the gate file was tampered` });
1924
+ else if (l.get(acct) !== pub)
1925
+ out.push({ severity: "error", check: "gate-authenticity", message: `${h}: ${acct}'s pinned pubkey in audience.toml does NOT match the journal-authentic key — a pubkey SWAP (poisoned gate). reseals would target the wrong key.` });
1926
+ }
1927
+ for (const acct of l.keys()) {
1928
+ if (!a.has(acct))
1929
+ out.push({ severity: "error", check: "gate-authenticity", message: `${h}: ${acct} was hand-added to audience.toml but is NOT in the authentic (journal-signed) gate — self-enrollment / gate poisoning. it is IGNORED on reseal.` });
1930
+ }
1931
+ }
1932
+ return out;
1933
+ }
1934
+ function validateManagementPlane(solDir, resolved, authentic) {
1935
+ const issues = [];
1936
+ const journal = readJournal(solDir);
1937
+ if (!journal.length)
1938
+ return issues;
1939
+ const fprToAccount = new Map;
1940
+ for (const recips of Object.values(authentic.handles))
1941
+ for (const r of recips)
1942
+ if (r.authKey)
1943
+ fprToAccount.set(fingerprintOf(r.authKey), r.accountId);
1944
+ const lastChange = new Map;
1945
+ const blanked = new Map;
1946
+ for (const e of journal) {
1947
+ if (!e.env || !e.name)
1948
+ continue;
1949
+ const key = `${e.env}/${e.name}`;
1950
+ const changesSeal = e.newSeal !== e.prevSeal;
1951
+ const changesAud = JSON.stringify(e.newAud ?? null) !== JSON.stringify(e.prevAud ?? null);
1952
+ if (changesSeal || changesAud)
1953
+ lastChange.set(key, e);
1954
+ if (e.prevSeal && !e.newSeal)
1955
+ blanked.set(key, e);
1956
+ if (e.newSeal && e.recipientAtOp)
1957
+ blanked.delete(key);
1958
+ }
1959
+ for (const [key, e] of lastChange) {
1960
+ if (!e.newSeal)
1961
+ continue;
1962
+ if (!e.recipientAtOp) {
1963
+ issues.push({
1964
+ severity: "error",
1965
+ check: "management-plane",
1966
+ message: `${key}: seal/audience was changed by a non-recipient author (${e.author || "unidentified"}) — a non-recipient cannot repoint a sealed secret. reseal as a recipient or revert the tampering.`
1967
+ });
1968
+ continue;
1969
+ }
1970
+ const read = readSealedBoxChecked(solDir, e.newSeal);
1971
+ const box = read.kind === "box" ? read.box : undefined;
1972
+ if (box) {
1973
+ const fpr = authorFingerprint(e);
1974
+ const acct = fpr ? fprToAccount.get(fpr) : undefined;
1975
+ const boxRecipients = new Set(recipientsOf(box));
1976
+ if (!acct || !boxRecipients.has(acct)) {
1977
+ issues.push({
1978
+ severity: "error",
1979
+ check: "management-plane",
1980
+ message: `${key}: the seal was authored by a key that is NOT among the sealed recipients (claimed author ${e.author || "unidentified"}) — recipientAtOp is unsubstantiated by the ciphertext. a non-recipient cannot author a seal here.`
1981
+ });
1982
+ }
1983
+ }
1984
+ }
1985
+ for (const [key, e] of blanked) {
1986
+ issues.push({
1987
+ severity: "error",
1988
+ check: "management-plane",
1989
+ message: `${key}: an already-sealed secret was BLANKED to unset by ${e.author || "unidentified"} (sealed->unset) — a destructive re-declare. recover the prior ciphertext (${e.prevSeal}) or reseal as a recipient.`
1990
+ });
1991
+ }
1992
+ return issues;
1993
+ }
1994
+ function diffEnvs(a, b) {
1995
+ const shapeOf = (r) => {
1996
+ const m = new Map;
1997
+ for (const [k] of Object.entries(r.config))
1998
+ m.set(k, "config");
1999
+ for (const s of r.secrets)
2000
+ m.set(s.name, `secret${s.type ? `:${s.type}` : ""}${s.aud ? ` aud=[${s.aud.join(",")}]` : ""}`);
2001
+ return m;
2002
+ };
2003
+ const sa = shapeOf(a);
2004
+ const sb = shapeOf(b);
2005
+ const out = [];
2006
+ for (const [name, shape] of sa) {
2007
+ if (!sb.has(name))
2008
+ out.push({ name, change: "removed", detail: shape });
2009
+ else if (sb.get(name) !== shape)
2010
+ out.push({ name, change: "changed", detail: `${shape} -> ${sb.get(name)}` });
2011
+ }
2012
+ for (const [name, shape] of sb)
2013
+ if (!sa.has(name))
2014
+ out.push({ name, change: "added", detail: shape });
2015
+ return out.sort((x, y) => x.name.localeCompare(y.name));
2016
+ }
2017
+ function auditEnv(solDir, env) {
2018
+ const world = loadWorld(solDir);
2019
+ const resolved = resolveOne(world, env);
2020
+ const authentic = deriveAuthenticGate(solDir);
2021
+ const whoCanDecrypt = resolved.secrets.map((s) => {
2022
+ const aud = s.aud ?? resolved.audience ?? [];
2023
+ const recipients = aud.flatMap((h) => (authentic.handles[h] ?? []).map((r) => r.accountId));
2024
+ return {
2025
+ name: s.hideName ? "(name-hidden)" : s.name,
2026
+ sealed: !!s.seal,
2027
+ audience: aud,
2028
+ recipients: [...new Set(recipients)]
2029
+ };
2030
+ });
2031
+ const drift = [];
2032
+ for (const [handle, recips] of Object.entries(world.gate.handles)) {
2033
+ const fileAccts = new Set(recips.map((r) => r.accountId));
2034
+ const authAccts = new Set((authentic.handles[handle] ?? []).map((r) => r.accountId));
2035
+ const fileOnly = [...fileAccts].filter((a) => !authAccts.has(a));
2036
+ const authenticOnly = [...authAccts].filter((a) => !fileAccts.has(a));
2037
+ if (fileOnly.length || authenticOnly.length)
2038
+ drift.push({ handle, fileOnly, authenticOnly });
2039
+ }
2040
+ return { env, journalTrustworthy: authentic.trustworthy, journalIssues: authentic.issues, whoCanDecrypt, drift };
2041
+ }
2042
+ // src/secret/audience-cli.ts
2043
+ var HANDLE_KINDS = ["role", "team", "machine"];
2044
+ function assertHandle(ctx, handle) {
2045
+ const [kind, name] = splitHandle(handle);
2046
+ if (!handle.includes(":") || !HANDLE_KINDS.includes(kind) || !name) {
2047
+ ctx.die(`bad handle "${handle}" — expected role:NAME | team:NAME | machine:NAME`);
2048
+ }
2049
+ }
2050
+ async function resolvePub(ctx, account, explicitPub) {
2051
+ if (explicitPub)
2052
+ return explicitPub;
2053
+ const mine = ctx.selfPub(account);
2054
+ if (mine)
2055
+ return mine;
2056
+ const dir = await ctx.dirPub(account);
2057
+ if (dir)
2058
+ return dir;
2059
+ return ctx.die(`no published X25519 key for ${account} — they must \`sol keys publish\` first, ` + `or pass --pubkey <sol1pk_…> to pin it explicitly (offline).`);
2060
+ }
2061
+ async function audienceAdd(ctx, gate, handle, account, explicitPub) {
2062
+ assertHandle(ctx, handle);
2063
+ if (!account)
2064
+ ctx.die("usage: sol env audience add <handle> <account> [--pubkey <sol1pk_…>]");
2065
+ const pub = await resolvePub(ctx, account, explicitPub);
2066
+ const edPub = ctx.selfEdPub?.(account) ?? await ctx.dirEdPub?.(account) ?? undefined;
2067
+ const recips = gate.handles[handle] ??= [];
2068
+ const existing = recips.find((r) => r.accountId === account);
2069
+ if (existing) {
2070
+ existing.x25519Pub = pub;
2071
+ if (edPub)
2072
+ existing.edPub = edPub;
2073
+ } else
2074
+ recips.push({ handle, accountId: account, x25519Pub: pub, edPub });
2075
+ return { gate, pub };
2076
+ }
2077
+ function audienceRm(ctx, gate, handle, account) {
2078
+ assertHandle(ctx, handle);
2079
+ const recips = gate.handles[handle];
2080
+ if (!recips || !recips.length)
2081
+ ctx.die(`no such audience handle: ${handle}`);
2082
+ if (!account) {
2083
+ delete gate.handles[handle];
2084
+ return gate;
2085
+ }
2086
+ const next = recips.filter((r) => r.accountId !== account);
2087
+ if (next.length === recips.length)
2088
+ ctx.die(`${account} is not in ${handle}`);
2089
+ if (next.length)
2090
+ gate.handles[handle] = next;
2091
+ else
2092
+ delete gate.handles[handle];
2093
+ return gate;
2094
+ }
2095
+ function audienceRows(gate) {
2096
+ const rows = [];
2097
+ for (const handle of Object.keys(gate.handles).sort()) {
2098
+ for (const r of gate.handles[handle])
2099
+ rows.push({ handle, account: r.accountId, pubkey: r.x25519Pub });
2100
+ }
2101
+ return rows;
2102
+ }
2103
+ function recipientsOfHandle(gate, handle) {
2104
+ return gate.handles[handle] ?? [];
2105
+ }
2106
+ // src/secret/runtime-recipient.ts
2107
+ init_crypto();
2108
+ function runtimeHandle(env) {
2109
+ return `machine:runtime-${env}`;
2110
+ }
2111
+ function runtimeAccountId(env) {
2112
+ return `runtime-${env}`;
2113
+ }
2114
+ var RUNTIME_ROOT_BINDING = "SOL_RUNTIME_ROOT";
2115
+ async function provisionRuntimeRecipient(env, roots, opts = {}) {
2116
+ const { generateRecoveryCode: generateRecoveryCode2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
2117
+ const root = (opts.mintRoot ?? generateRecoveryCode2)();
2118
+ const keyEpoch = opts.keyEpoch ?? 1;
2119
+ const accountId = runtimeAccountId(env);
2120
+ const id = deriveIdentity(accountId, root, keyEpoch);
2121
+ await roots.put(env, root);
2122
+ return { env, handle: runtimeHandle(env), accountId, x25519Pub: id.x25519Pub, edPub: id.edPub, keyEpoch };
2123
+ }
2124
+ function runtimeSelfFromRoot(env, root, keyEpoch = 1) {
2125
+ const id = deriveIdentity(runtimeAccountId(env), root, keyEpoch);
2126
+ return { accountId: runtimeAccountId(env), privateKey: id.x25519Priv };
2127
+ }
2128
+
2129
+ class ExportError extends Error {
2130
+ constructor(message) {
2131
+ super(message);
2132
+ this.name = "ExportError";
2133
+ }
2134
+ }
2135
+ function exportEnvWith(solDir, env, self) {
2136
+ if (!self) {
2137
+ throw new ExportError(`refused to export ${env}: no runtime identity loaded — the bootstrap root is absent (custody it in the CF Secrets Store and read it via env.${RUNTIME_ROOT_BINDING}). a non-recipient cannot export ciphertext.`);
2138
+ }
2139
+ const world = loadWorld(solDir);
2140
+ const resolved = resolveOne(world, env);
2141
+ const secrets = {};
2142
+ const skipped = [];
2143
+ for (const s of resolved.secrets) {
2144
+ if (!s.seal || s.hideName)
2145
+ continue;
2146
+ const out = openOutcome(solDir, s.seal, self.accountId, self);
2147
+ if (out.kind === "tampered") {
2148
+ throw new ExportError(`refused to export ${env}: ${s.name} (${s.seal}) was TAMPERED in place — its bytes hash to ${out.actual}, not the pinned address. no secrets exported.`);
2149
+ }
2150
+ if (out.kind === "value") {
2151
+ secrets[s.name] = out.value;
2152
+ continue;
2153
+ }
2154
+ skipped.push(s.name);
2155
+ }
2156
+ const names = Object.keys(secrets);
2157
+ if (!names.length) {
2158
+ throw new ExportError(`no secrets in ${env} are readable by the runtime recipient (${self.accountId}) — is the runtime in the env audience? add it with \`sol env audience add ${runtimeHandle(env)} ${runtimeAccountId(env)} --pubkey ...\`.`);
2159
+ }
2160
+ return { secrets, names, skipped };
2161
+ }
2162
+ function renderExport(secrets, format) {
2163
+ if (format === "json")
2164
+ return JSON.stringify(secrets);
2165
+ return Object.entries(secrets).map(([k, v]) => `${k}='${v.replace(/'/g, `'\\''`)}'`).join(`
2166
+ `);
2167
+ }
2168
+ // src/bin/secret.ts
2169
+ function flag(args, name) {
2170
+ const i = args.indexOf(name);
2171
+ if (i >= 0 && i + 1 < args.length)
2172
+ return args[i + 1];
2173
+ const pre = args.find((a) => a.startsWith(`${name}=`));
2174
+ return pre ? pre.slice(name.length + 1) : undefined;
2175
+ }
2176
+ function has(args, name) {
2177
+ return args.includes(name);
2178
+ }
2179
+ function positionals(args) {
2180
+ const out = [];
2181
+ for (let i = 0;i < args.length; i++) {
2182
+ const a = args[i];
2183
+ if (a.startsWith("-")) {
2184
+ if (!a.includes("=") && i + 1 < args.length && !args[i + 1].startsWith("-") && VALUE_FLAGS.has(a))
2185
+ i++;
2186
+ continue;
2187
+ }
2188
+ out.push(a);
2189
+ }
2190
+ return out;
2191
+ }
2192
+ var VALUE_FLAGS = new Set(["--env", "--audience", "--fd", "-m", "--field", "--pubkey", "--agent", "--envs", "--type", "--format"]);
2193
+ function requireEnv(ctx, args) {
2194
+ const e = flag(args, "--env");
2195
+ if (!e)
2196
+ ctx.die("missing --env <name>");
2197
+ return e;
2198
+ }
2199
+ function manage(ctx) {
2200
+ return ctx.loadManageIdentity?.();
2201
+ }
2202
+ function requireSigner(ctx, what) {
2203
+ const id = manage(ctx);
2204
+ if (!id)
2205
+ ctx.die(`refused: ${what} is a management-plane op that must be cryptographically authored — set SOL_RECOVERY_CODE so the op is signed + chained (a keyless caller cannot mutate the gate or a sealed slot).`);
2206
+ return { id, signer: id.signer };
2207
+ }
2208
+ function journal2(ctx, id, body) {
2209
+ appendJournal(ctx.solDir, { ...body, author: id.accountId }, id.signer);
2210
+ updateHeadPin(ctx.solDir);
2211
+ }
2212
+ function memberSnapshot(gate, handle) {
2213
+ return (gate.handles[handle] ?? []).map((r) => ({ accountId: r.accountId, pubkey: r.x25519Pub, authKey: r.edPub ?? "" }));
2214
+ }
2215
+ function readSecretValue(ctx, args) {
2216
+ const fd = flag(args, "--fd");
2217
+ if (fd !== undefined) {
2218
+ const n = parseInt(fd, 10);
2219
+ if (!Number.isInteger(n))
2220
+ ctx.die("--fd expects a file descriptor number");
2221
+ return readAllFromFd(n).replace(/\n$/, "");
2222
+ }
2223
+ if (has(args, "--stdin")) {
2224
+ return readAllFromFd(0).replace(/\n$/, "");
2225
+ }
2226
+ if (!process.stdin.isTTY) {
2227
+ ctx.die("no value source: not a TTY — pass the value via --stdin or --fd <n> (NEVER on the command line)");
2228
+ }
2229
+ return promptNoEcho(ctx);
2230
+ }
2231
+ function readAllFromFd(fd) {
2232
+ const chunks = [];
2233
+ const buf = Buffer.alloc(65536);
2234
+ for (;; ) {
2235
+ let n = 0;
2236
+ try {
2237
+ n = readSync(fd, buf, 0, buf.length, null);
2238
+ } catch (e) {
2239
+ if (e?.code === "EAGAIN")
2240
+ continue;
2241
+ if (e?.code === "EOF")
2242
+ break;
2243
+ throw e;
2244
+ }
2245
+ if (n === 0)
2246
+ break;
2247
+ chunks.push(Buffer.from(buf.subarray(0, n)));
2248
+ }
2249
+ return Buffer.concat(chunks).toString("utf8");
2250
+ }
2251
+ function promptNoEcho(ctx) {
2252
+ process.stderr.write("value (input hidden): ");
2253
+ const stty = (mode) => spawnSync("stty", [mode], { stdio: ["inherit", "inherit", "inherit"] });
2254
+ const off = stty("-echo");
2255
+ try {
2256
+ return readLineFromTty();
2257
+ } finally {
2258
+ if (off.status === 0)
2259
+ stty("echo");
2260
+ process.stderr.write(`
2261
+ `);
2262
+ }
2263
+ }
2264
+ function readLineFromTty() {
2265
+ const buf = Buffer.alloc(1);
2266
+ let line = "";
2267
+ for (;; ) {
2268
+ let n = 0;
2269
+ try {
2270
+ n = readSync(0, buf, 0, 1, null);
2271
+ } catch (e) {
2272
+ if (e?.code === "EAGAIN")
2273
+ continue;
2274
+ break;
2275
+ }
2276
+ if (n === 0)
2277
+ break;
2278
+ const c = buf.toString("utf8");
2279
+ if (c === `
2280
+ ` || c === "\r")
2281
+ break;
2282
+ line += c;
2283
+ }
2284
+ return line;
2285
+ }
2286
+ function sealRecipients(ctx, world, env, entryAud) {
2287
+ const handles = audienceFor(world, env, entryAud);
2288
+ if (!handles.length)
2289
+ ctx.die(`no seal audience for ${env} — set @audience in the env file or pass --audience on declare`);
2290
+ const resolved = resolveHandles(world.gate, handles);
2291
+ if (resolved.unresolved.length)
2292
+ ctx.die(`unresolved audience handle(s): ${resolved.unresolved.join(", ")} — add them to .sol/env/audience.toml`);
2293
+ if (!Object.keys(resolved.recipientPubKeys).length)
2294
+ ctx.die(`audience [${handles.join(", ")}] resolved to ZERO recipients — refusing to seal an unreadable value`);
2295
+ return { recipientPubKeys: resolved.recipientPubKeys, handles, recipients: resolved.recipients };
2296
+ }
2297
+ function authenticSealRecipients(ctx, world, env, entryAud) {
2298
+ const handles = audienceFor(world, env, entryAud);
2299
+ if (!handles.length)
2300
+ ctx.die(`no seal audience for ${env} — set @audience in the env file or pass --audience on declare`);
2301
+ const authentic = deriveAuthenticGate(ctx.solDir);
2302
+ if (!authentic.trustworthy)
2303
+ ctx.die(`refused: the management journal does not verify (chain/signature broken) — refusing to seal to an untrustworthy gate. run \`sol env validate\` and revert the tampering.`);
2304
+ if (!Object.keys(authentic.handles).length) {
2305
+ const fb = sealRecipients(ctx, world, env, entryAud);
2306
+ return { recipientPubKeys: fb.recipientPubKeys, handles: fb.handles };
2307
+ }
2308
+ const recipientPubKeys = {};
2309
+ const unresolved = [];
2310
+ for (const h of handles) {
2311
+ const recips = authentic.handles[h];
2312
+ if (!recips || !recips.length) {
2313
+ unresolved.push(h);
2314
+ continue;
2315
+ }
2316
+ for (const r of recips)
2317
+ recipientPubKeys[r.accountId] = r.pubkey;
2318
+ }
2319
+ if (unresolved.length)
2320
+ ctx.die(`audience handle(s) not in the authentic (journal-signed) gate: ${unresolved.join(", ")} — only journal-authored gate membership can be sealed to. add via \`sol env audience add\` (signed).`);
2321
+ if (!Object.keys(recipientPubKeys).length)
2322
+ ctx.die(`audience [${handles.join(", ")}] resolved to ZERO authentic recipients — refusing to seal an unreadable value`);
2323
+ return { recipientPubKeys, handles };
2324
+ }
2325
+ function runEnv(ctx, sub, args) {
2326
+ const json = has(args, "--json");
2327
+ switch (sub) {
2328
+ case "init":
2329
+ return envInit(ctx, args);
2330
+ case "ls":
2331
+ return envLs(ctx, json);
2332
+ case "show":
2333
+ return envShow(ctx, positionals(args)[0], json);
2334
+ case "schema":
2335
+ return envSchema(ctx, args, json);
2336
+ case "diff":
2337
+ return envDiff(ctx, positionals(args), json);
2338
+ case "validate":
2339
+ return envValidate(ctx, args, json);
2340
+ case "verify":
2341
+ return envValidate(ctx, args.includes("--remote") ? args : [...args, "--remote"], json);
2342
+ case "audience":
2343
+ return envAudience(ctx, args, json);
2344
+ case "audit":
2345
+ return envAudit(ctx, positionals(args)[0], json);
2346
+ case "anchor":
2347
+ return envAnchor(ctx, json);
2348
+ default:
2349
+ ctx.die(`unknown \`sol env\` subcommand: ${sub ?? "(none)"} — try ls | show | schema | diff | validate | verify | audit | anchor | audience | init`);
2350
+ }
2351
+ }
2352
+ async function envAudience(ctx, args, json) {
2353
+ const pos = positionals(args);
2354
+ const op = pos[0];
2355
+ const me = manage(ctx);
2356
+ const audCtx = {
2357
+ solDir: ctx.solDir,
2358
+ die: ctx.die,
2359
+ selfPub: ctx.selfPub ?? (() => {
2360
+ return;
2361
+ }),
2362
+ dirPub: ctx.dirPub ?? (async () => {
2363
+ return;
2364
+ }),
2365
+ selfEdPub: (account) => me && me.accountId === account ? me.signer.pub : ctx.selfEdPub?.(account),
2366
+ dirEdPub: ctx.dirEdPub ?? (async () => {
2367
+ return;
2368
+ })
2369
+ };
2370
+ if (op === "add" && !envInitialized(ctx.solDir))
2371
+ envInitScaffold(ctx, undefined, manage(ctx));
2372
+ const world = loadWorld(ctx.solDir);
2373
+ if (op === "ls") {
2374
+ const handle = pos[1];
2375
+ const rows = handle ? recipientsOfHandle(world.gate, handle).map((r) => ({ handle, account: r.accountId, pubkey: r.x25519Pub })) : audienceRows(world.gate);
2376
+ if (json)
2377
+ return void console.log(JSON.stringify(rows, null, 2));
2378
+ if (!rows.length)
2379
+ return void console.log(handle ? `no recipients under ${handle}` : "audience.toml is empty — add recipients with `sol env audience add <handle> <account>`");
2380
+ for (const r of rows)
2381
+ console.log(`${r.handle.padEnd(22)} ${r.account.padEnd(28)} ${r.pubkey || "(no pubkey)"}`);
2382
+ return;
2383
+ }
2384
+ if (op === "add") {
2385
+ const handle = pos[1];
2386
+ const account = pos[2];
2387
+ if (!handle || !account)
2388
+ ctx.die("usage: sol env audience add <handle> <account> [--pubkey <sol1pk_…>]");
2389
+ const { id } = requireSigner(ctx, `audience add ${handle}`);
2390
+ guardHandleEdit(ctx, world.gate, handle, id, account);
2391
+ const prevAud = (world.gate.handles[handle] ?? []).map((r) => r.accountId);
2392
+ const { gate, pub } = await audienceAdd(audCtx, world.gate, handle, account, flag(args, "--pubkey"));
2393
+ writeAudienceDoc(ctx.solDir, gate);
2394
+ journal2(ctx, id, { op: "audience-add", handle, members: memberSnapshot(gate, handle), recipientAtOp: true, prevAud, newAud: (gate.handles[handle] ?? []).map((r) => r.accountId), at: Date.now() });
2395
+ if (json)
2396
+ return void console.log(JSON.stringify({ added: { handle, account, pubkey: pub } }, null, 2));
2397
+ console.log(`added ${account} to ${handle} (pubkey ${pub})`);
2398
+ return;
2399
+ }
2400
+ if (op === "rm") {
2401
+ const handle = pos[1];
2402
+ const account = pos[2];
2403
+ if (!handle)
2404
+ ctx.die("usage: sol env audience rm <handle> [<account>] (no account drops the whole handle)");
2405
+ const { id } = requireSigner(ctx, `audience rm ${handle}`);
2406
+ guardHandleEdit(ctx, world.gate, handle, id, account);
2407
+ const prevAud = (world.gate.handles[handle] ?? []).map((r) => r.accountId);
2408
+ const gate = audienceRm(audCtx, world.gate, handle, account);
2409
+ writeAudienceDoc(ctx.solDir, gate);
2410
+ journal2(ctx, id, { op: "audience-rm", handle, members: memberSnapshot(gate, handle), recipientAtOp: true, prevAud, newAud: (gate.handles[handle] ?? []).map((r) => r.accountId), at: Date.now() });
2411
+ if (json)
2412
+ return void console.log(JSON.stringify({ removed: { handle, account: account ?? null } }, null, 2));
2413
+ console.log(account ? `removed ${account} from ${handle}` : `removed handle ${handle}`);
2414
+ return;
2415
+ }
2416
+ ctx.die("usage: sol env audience add|rm|ls [<handle>] [<account>]");
2417
+ }
2418
+ function guardHandleEdit(ctx, gate, handle, id, adding) {
2419
+ const verdict = canEditHandle(gate, handle, id);
2420
+ if (verdict.allowed)
2421
+ return;
2422
+ ctx.die(verdict.reason ?? `refused: ${ctx.actor} may not edit ${handle}.`);
2423
+ }
2424
+ function envInit(ctx, args) {
2425
+ if (envInitialized(ctx.solDir))
2426
+ ctx.die("env manifest already exists at .sol/env/manifest.toml");
2427
+ const order = (flag(args, "--envs") ?? "development,staging,production").split(",").map((s) => s.trim()).filter(Boolean);
2428
+ envInitScaffold(ctx, order, manage(ctx));
2429
+ console.log(`initialized .sol/env/ with envs: ${order.join(", ")}`);
2430
+ }
2431
+ function envInitScaffold(ctx, order = ["development", "staging", "production"], creator) {
2432
+ const extendsOf = {};
2433
+ for (const e of order)
2434
+ extendsOf[e] = BASE_ENV;
2435
+ writeManifest(ctx.solDir, { order, extendsOf, runtime: {} });
2436
+ writeEnvFile(ctx.solDir, { env: BASE_ENV, config: {}, secrets: [] });
2437
+ for (const e of order)
2438
+ writeEnvFile(ctx.solDir, { env: e, extends: BASE_ENV, config: {}, secrets: [] });
2439
+ const gate = { handles: {}, clone: {} };
2440
+ if (creator)
2441
+ gate.handles["role:owner"] = [{ handle: "role:owner", accountId: creator.accountId, x25519Pub: creator.x25519Pub, edPub: creator.signer.pub }];
2442
+ writeAudienceDoc(ctx.solDir, gate);
2443
+ if (creator) {
2444
+ if (!readCreatorPin(ctx.solDir)) {
2445
+ writeCreatorPin(ctx.solDir, { accountId: creator.accountId, authKey: creator.signer.pub, x25519Pub: creator.x25519Pub, opGenesis: opLogGenesis(ctx.solDir) });
2446
+ }
2447
+ journal2(ctx, creator, {
2448
+ op: "gate-bootstrap",
2449
+ handle: "role:owner",
2450
+ members: [{ accountId: creator.accountId, pubkey: creator.x25519Pub, authKey: creator.signer.pub }],
2451
+ recipientAtOp: true,
2452
+ newAud: [creator.accountId],
2453
+ at: Date.now()
2454
+ });
2455
+ }
2456
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2457
+ }
2458
+ function envLs(ctx, json) {
2459
+ const world = loadWorld(ctx.solDir);
2460
+ const rows = world.manifest.order.map((env) => {
2461
+ const r = resolveOne(world, env);
2462
+ return { env, extends: world.files[env]?.extends, config: Object.keys(r.config).length, secrets: r.secrets.length, runtime: world.manifest.runtime[env] };
2463
+ });
2464
+ if (json)
2465
+ return void console.log(JSON.stringify(rows, null, 2));
2466
+ for (const r of rows)
2467
+ console.log(`${r.env.padEnd(14)} extends=${(r.extends ?? "-").padEnd(8)} config=${r.config} secrets=${r.secrets}${r.runtime ? ` runtime=${r.runtime}` : ""}`);
2468
+ }
2469
+ function envShow(ctx, env, json) {
2470
+ if (!env)
2471
+ ctx.die("usage: sol env show <env>");
2472
+ const world = loadWorld(ctx.solDir);
2473
+ const r = resolveOne(world, env);
2474
+ const view = {
2475
+ env: r.env,
2476
+ audience: r.audience,
2477
+ config: r.config,
2478
+ secrets: r.secrets.map((s) => ({ name: s.name, type: s.type, audience: s.aud ?? r.audience, set: !!s.seal, hidden: !!s.hideName }))
2479
+ };
2480
+ if (json)
2481
+ return void console.log(JSON.stringify(view, null, 2));
2482
+ console.log(`env: ${r.env}${r.audience ? ` @audience=[${r.audience.join(", ")}]` : ""}`);
2483
+ for (const [k, v] of Object.entries(r.config))
2484
+ console.log(` config ${k} = ${v}`);
2485
+ for (const s of view.secrets)
2486
+ console.log(` secret ${s.name}${s.type ? `:${s.type}` : ""} = ${s.set ? "[sealed]" : "[unset]"} aud=[${(s.audience ?? []).join(", ")}]`);
2487
+ }
2488
+ function envSchema(ctx, args, json) {
2489
+ const world = loadWorld(ctx.solDir);
2490
+ if (has(args, "--write")) {
2491
+ regenerateSchema(ctx.solDir, world);
2492
+ console.log("wrote .sol/env/schema.lock");
2493
+ return;
2494
+ }
2495
+ const schema = loadSchema(ctx.solDir) ?? JSON.parse(regenerateSchema(ctx.solDir, world));
2496
+ if (json)
2497
+ return void console.log(JSON.stringify(schema, null, 2));
2498
+ for (const [name, v] of Object.entries(schema.vars)) {
2499
+ const per = Object.entries(v.envs).map(([e, st]) => `${e}=${v.kind === "secret" ? `[${st}]` : st}`).join(" ");
2500
+ console.log(`${v.kind === "secret" ? "secret" : "config"} ${name}${v.type ? `:${v.type}` : ""} ${per}`);
2501
+ }
2502
+ }
2503
+ function envDiff(ctx, pos, json) {
2504
+ if (pos.length < 2)
2505
+ ctx.die("usage: sol env diff <a> <b>");
2506
+ const world = loadWorld(ctx.solDir);
2507
+ const entries = diffEnvs(resolveOne(world, pos[0]), resolveOne(world, pos[1]));
2508
+ if (json)
2509
+ return void console.log(JSON.stringify(entries, null, 2));
2510
+ if (!entries.length)
2511
+ return void console.log(`${pos[0]} and ${pos[1]} are shape-identical`);
2512
+ for (const e of entries)
2513
+ console.log(`${e.change === "added" ? "+" : e.change === "removed" ? "-" : "~"} ${e.name}: ${e.detail}`);
2514
+ }
2515
+ async function envValidate(ctx, args, json) {
2516
+ const world = loadWorld(ctx.solDir);
2517
+ const schemaOnDisk = readSchemaText(ctx.solDir);
2518
+ const agentAccountId = flag(args, "--agent") ?? process.env.SOL_AGENT_ACCOUNT;
2519
+ const issues = validateWorld(ctx.solDir, world, { agentAccountId, schemaOnDisk });
2520
+ if (args.includes("--remote") && ctx.remoteAnchorVerify) {
2521
+ const verdict = await ctx.remoteAnchorVerify();
2522
+ if (verdict)
2523
+ issues.push(...verdict.issues);
2524
+ else
2525
+ issues.push({ severity: "warn", check: "remote-anchor", message: "no remote configured (set SOL_TOKEN + `sol remote`) — the EXTERNAL anchor was not consulted; the env root-of-trust is local-only (a local re-key is not detectable offline)." });
2526
+ } else if (args.includes("--remote")) {
2527
+ issues.push({ severity: "warn", check: "remote-anchor", message: "no remote anchor available in this context — run via the `sol` CLI with a configured remote to consult the external owner anchor." });
2528
+ }
2529
+ if (json)
2530
+ return void console.log(JSON.stringify(issues, null, 2));
2531
+ if (!issues.length)
2532
+ return void console.log("valid: fail-closed OK, audiences consistent, schema fresh");
2533
+ for (const i of issues)
2534
+ console.log(`${i.severity === "error" ? "ERROR" : "warn "} [${i.check}] ${i.message}`);
2535
+ if (issues.some((i) => i.severity === "error"))
2536
+ process.exitCode = 1;
2537
+ }
2538
+ function readSchemaText(solDir) {
2539
+ try {
2540
+ return readFileSync5(schemaLockPath(solDir), "utf8");
2541
+ } catch {
2542
+ return;
2543
+ }
2544
+ }
2545
+ function envAudit(ctx, env, json) {
2546
+ if (!env)
2547
+ ctx.die("usage: sol env audit <env> (== sol secret audit --env <env>)");
2548
+ printAudit(auditEnv(ctx.solDir, env), json);
2549
+ }
2550
+ function printAudit(audit, json) {
2551
+ if (json)
2552
+ return void console.log(JSON.stringify(audit, null, 2));
2553
+ console.log(`audit ${audit.env} journal=${audit.journalTrustworthy ? "TRUSTWORTHY" : "UNTRUSTWORTHY"}`);
2554
+ if (!audit.journalTrustworthy && audit.journalIssues.length) {
2555
+ for (const i of audit.journalIssues)
2556
+ console.log(` journal issue: ${i}`);
2557
+ }
2558
+ console.log(" who-can-decrypt (journal-authentic):");
2559
+ for (const s of audit.whoCanDecrypt) {
2560
+ console.log(` ${s.name.padEnd(24)} ${s.sealed ? "[sealed]" : "[unset] "} aud=[${s.audience.join(", ")}] -> ${s.recipients.length ? s.recipients.join(", ") : "(NO recipient — unreadable)"}`);
2561
+ }
2562
+ if (audit.drift.length) {
2563
+ console.log(" DRIFT (audience.toml vs the signed-journal gate):");
2564
+ for (const d of audit.drift) {
2565
+ if (d.fileOnly.length)
2566
+ console.log(` ${d.handle}: file-only (NOT journal-authored) -> ${d.fileOnly.join(", ")}`);
2567
+ if (d.authenticOnly.length)
2568
+ console.log(` ${d.handle}: journal-only (missing from file) -> ${d.authenticOnly.join(", ")}`);
2569
+ }
2570
+ } else {
2571
+ console.log(" no file-vs-journal drift.");
2572
+ }
2573
+ }
2574
+ function envAnchor(ctx, json) {
2575
+ if (!envInitialized(ctx.solDir))
2576
+ ctx.die("no .sol/env/ to anchor — run `sol env init` (or `sol secret declare`) first");
2577
+ if (readCreatorPin(ctx.solDir))
2578
+ ctx.die("already anchored: .sol/env/creator.pin exists — re-pinning is refused (it would be the re-bootstrap attack). nothing to do.");
2579
+ const { id } = requireSigner(ctx, "env anchor");
2580
+ writeCreatorPin(ctx.solDir, { accountId: id.accountId, authKey: id.signer.pub, x25519Pub: id.x25519Pub, opGenesis: opLogGenesis(ctx.solDir) });
2581
+ if (json)
2582
+ return void console.log(JSON.stringify({ anchored: { accountId: id.accountId, authKey: id.signer.pub, opGenesis: opLogGenesis(ctx.solDir) } }, null, 2));
2583
+ console.log(`anchored .sol/env/ to creator @${id.accountId} (creator.pin written, cross-bound to the op-log genesis). \`sol env validate\` no longer warns UNANCHORED.`);
2584
+ }
2585
+ function runSecret(ctx, sub, args) {
2586
+ switch (sub) {
2587
+ case "declare":
2588
+ return secretDeclare(ctx, args);
2589
+ case "ref":
2590
+ return secretRef(ctx, args);
2591
+ case "set":
2592
+ return secretSet(ctx, args);
2593
+ case "config":
2594
+ return secretConfig(ctx, args);
2595
+ case "list":
2596
+ return secretList(ctx, args);
2597
+ case "reveal":
2598
+ return secretReveal(ctx, args);
2599
+ case "inject":
2600
+ return secretInject(ctx, args);
2601
+ case "audience":
2602
+ return secretAudience(ctx, args);
2603
+ case "export":
2604
+ return secretExport(ctx, args);
2605
+ case "audit":
2606
+ return printAudit(auditEnv(ctx.solDir, requireEnv(ctx, args)), has(args, "--json"));
2607
+ case "runtime":
2608
+ return secretRuntime(ctx, positionals(args)[0], args);
2609
+ default:
2610
+ ctx.die(`unknown \`sol secret\` subcommand: ${sub ?? "(none)"} — declare | ref | set | config set | list | reveal | inject | export | audit | runtime | audience`);
2611
+ }
2612
+ }
2613
+ function secretDeclare(ctx, args) {
2614
+ const env = requireEnv(ctx, args);
2615
+ const name = positionals(args)[0];
2616
+ if (!name)
2617
+ ctx.die("usage: sol secret declare NAME --env E [--audience h1,h2] [--hide-name] [--type T]");
2618
+ const world = loadWorld(ctx.solDir);
2619
+ const file = world.files[env];
2620
+ if (!file)
2621
+ ctx.die(`unknown env: ${env}`);
2622
+ const audArg = flag(args, "--audience");
2623
+ const aud = audArg ? audArg.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
2624
+ const prior = file.secrets.find((s) => s.name === name);
2625
+ const id = manage(ctx);
2626
+ const gate = canDeclare(ctx.solDir, prior, ctx.actor, id);
2627
+ const guarded = gate.guarded;
2628
+ if (guarded && !gate.allowed) {
2629
+ ctx.die(`refused: ${ctx.actor} is not an authorized writer of ${name}@${env} — re-declaring an already-sealed secret (or repointing its audience) requires a current recipient (set SOL_RECOVERY_CODE, or have a recipient run this). declaring a NEW slot stays open.`);
2630
+ }
2631
+ if (guarded && !id)
2632
+ requireSigner(ctx, `declare ${name}@${env}`);
2633
+ const slot = { name };
2634
+ if (flag(args, "--type"))
2635
+ slot.type = flag(args, "--type");
2636
+ if (aud)
2637
+ slot.aud = aud;
2638
+ else if (prior?.aud)
2639
+ slot.aud = prior.aud;
2640
+ if (has(args, "--hide-name"))
2641
+ slot.hideName = true;
2642
+ else if (prior?.hideName)
2643
+ slot.hideName = true;
2644
+ const reset = has(args, "--reset");
2645
+ if (prior?.seal && !reset)
2646
+ slot.seal = prior.seal;
2647
+ file.secrets = file.secrets.filter((s) => s.name !== name);
2648
+ file.secrets.push(slot);
2649
+ writeEnvFile(ctx.solDir, file);
2650
+ if (id) {
2651
+ journal2(ctx, id, {
2652
+ op: "declare",
2653
+ env,
2654
+ name,
2655
+ recipientAtOp: recipientAtOp(ctx, prior?.seal, id),
2656
+ prevSeal: prior?.seal,
2657
+ newSeal: slot.seal,
2658
+ prevAud: prior?.aud,
2659
+ newAud: slot.aud,
2660
+ at: Date.now()
2661
+ });
2662
+ }
2663
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2664
+ const stateNote = slot.seal ? "(seal preserved)" : prior?.seal && reset ? `(prior ciphertext ${prior.seal} kept for recovery)` : "UNSET";
2665
+ console.log(`declared secret ${name} in ${env} ${slot.seal ? stateNote : `(${stateNote} — value via \`sol secret set ${name} --env ${env}\`)`}`);
2666
+ }
2667
+ function recipientAtOp(ctx, priorSeal, id) {
2668
+ if (!priorSeal)
2669
+ return true;
2670
+ return canWriteSealed(ctx.solDir, priorSeal, ctx.actor, id?.open);
2671
+ }
2672
+ function secretRef(ctx, args) {
2673
+ const env = requireEnv(ctx, args);
2674
+ const name = positionals(args)[0];
2675
+ if (!name)
2676
+ ctx.die("usage: sol secret ref NAME --env E");
2677
+ const world = loadWorld(ctx.solDir);
2678
+ const slot = resolveOne(world, env).secrets.find((s) => s.name === name);
2679
+ if (!slot)
2680
+ ctx.die(`${name} is not declared in ${env} — \`sol secret declare\` it first`);
2681
+ console.log(formatSolRef(env, name));
2682
+ }
2683
+ function secretSet(ctx, args) {
2684
+ const env = requireEnv(ctx, args);
2685
+ const name = positionals(args)[0];
2686
+ if (!name)
2687
+ ctx.die("usage: sol secret set NAME --env E (value via no-echo TTY / --stdin / --fd, NEVER argv)");
2688
+ const world = loadWorld(ctx.solDir);
2689
+ const file = world.files[env];
2690
+ if (!file)
2691
+ ctx.die(`unknown env: ${env}`);
2692
+ let slot = file.secrets.find((s) => s.name === name);
2693
+ if (!slot) {
2694
+ slot = { name };
2695
+ file.secrets.push(slot);
2696
+ }
2697
+ const id = manage(ctx);
2698
+ if (slot.seal && !canWriteSealed(ctx.solDir, slot.seal, ctx.actor, id?.open)) {
2699
+ ctx.die(`refused: ${ctx.actor} is not an authorized writer of ${name}@${env} — only a current recipient may overwrite a sealed value (set SOL_RECOVERY_CODE, or have a recipient run this). declaring a slot stays open; SETTING a value is gated.`);
2700
+ }
2701
+ if (!id)
2702
+ requireSigner(ctx, `set ${name}@${env}`);
2703
+ const { recipientPubKeys, handles } = authenticSealRecipients(ctx, world, env, slot.aud);
2704
+ const value = readSecretValue(ctx, args);
2705
+ if (!value)
2706
+ ctx.die("refusing to seal an empty value");
2707
+ const priorSeal = slot.seal;
2708
+ const priorAud = slot.aud;
2709
+ const digest = writeSealedValueViaStore(ctx, value, recipientPubKeys);
2710
+ slot.seal = digest;
2711
+ slot.aud = handles;
2712
+ writeEnvFile(ctx.solDir, file);
2713
+ journal2(ctx, id, {
2714
+ op: "set",
2715
+ env,
2716
+ name,
2717
+ recipientAtOp: recipientAtOp(ctx, priorSeal, id),
2718
+ prevSeal: priorSeal,
2719
+ newSeal: digest,
2720
+ prevAud: priorAud,
2721
+ newAud: handles,
2722
+ at: Date.now()
2723
+ });
2724
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2725
+ console.log(`sealed ${name} in ${env} to [${handles.join(", ")}] (${digest}) — agent-blind, host-blind.`);
2726
+ }
2727
+ function secretConfig(ctx, args) {
2728
+ if (positionals(args)[0] !== "set")
2729
+ ctx.die("usage: sol secret config set KEY VAL --env E");
2730
+ const env = requireEnv(ctx, args);
2731
+ const pos = positionals(args).slice(1);
2732
+ const key = pos[0];
2733
+ const val = pos[1];
2734
+ if (!key || val === undefined)
2735
+ ctx.die("usage: sol secret config set KEY VAL --env E");
2736
+ const world = loadWorld(ctx.solDir);
2737
+ const file = world.files[env];
2738
+ if (!file)
2739
+ ctx.die(`unknown env: ${env}`);
2740
+ if (resolveOne(world, env).secrets.some((s) => s.name === key)) {
2741
+ ctx.die(`${key} is a SECRET in ${env} — it cannot be set as plaintext config. use \`sol secret set ${key} --env ${env}\`.`);
2742
+ }
2743
+ file.config[key] = val;
2744
+ writeEnvFile(ctx.solDir, file);
2745
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2746
+ console.log(`set config ${key} in ${env}`);
2747
+ }
2748
+ function secretList(ctx, args) {
2749
+ const env = requireEnv(ctx, args);
2750
+ const world = loadWorld(ctx.solDir);
2751
+ const rows = resolveOne(world, env).secrets.map((s) => ({
2752
+ name: s.name,
2753
+ env,
2754
+ type: s.type,
2755
+ audience: s.aud ?? world.files[env]?.audience ?? [],
2756
+ set: !!s.seal,
2757
+ hidden: !!s.hideName
2758
+ }));
2759
+ if (has(args, "--json"))
2760
+ return void console.log(JSON.stringify(rows, null, 2));
2761
+ for (const r of rows)
2762
+ console.log(`${r.name.padEnd(24)} ${r.set ? "[sealed]" : "[unset] "} aud=[${r.audience.join(", ")}]${r.hidden ? " (name-hidden)" : ""}`);
2763
+ }
2764
+ function secretReveal(ctx, args) {
2765
+ const env = requireEnv(ctx, args);
2766
+ const name = positionals(args)[0];
2767
+ if (!name)
2768
+ ctx.die("usage: sol secret reveal NAME --env E");
2769
+ const value = openSecret(ctx, env, name, flag(args, "--field"));
2770
+ process.stdout.write(value + `
2771
+ `);
2772
+ }
2773
+ function secretInject(ctx, args) {
2774
+ const env = requireEnv(ctx, args);
2775
+ const dashdash = args.indexOf("--");
2776
+ if (dashdash < 0 || dashdash === args.length - 1)
2777
+ ctx.die("usage: sol secret inject --env E -- <cmd> [args...]");
2778
+ const cmd = args.slice(dashdash + 1);
2779
+ const world = loadWorld(ctx.solDir);
2780
+ const self = ctx.loadSelfIdentity();
2781
+ const injected = {};
2782
+ let revealedCount = 0;
2783
+ for (const s of resolveOne(world, env).secrets) {
2784
+ if (!s.seal || s.hideName)
2785
+ continue;
2786
+ refuseUnanchoredSeal(ctx, env, s.name, s.seal);
2787
+ const out = openOutcome(ctx.solDir, s.seal, ctx.actor, self);
2788
+ if (out.kind === "tampered") {
2789
+ ctx.die(`refused to inject: ${s.name}@${env} (${s.seal}) was TAMPERED in place — its bytes hash to ${out.actual}, not the pinned address. reseal or revert; no secrets injected.`);
2790
+ }
2791
+ if (out.kind !== "value")
2792
+ continue;
2793
+ injected[s.name] = out.value;
2794
+ revealedCount++;
2795
+ }
2796
+ if (!revealedCount)
2797
+ ctx.die(`no secrets in ${env} are readable by ${ctx.actor} — are you a recipient? (set SOL_RECOVERY_CODE)`);
2798
+ process.stderr.write(`sol: injecting ${revealedCount} secret(s) into the subprocess env: ${Object.keys(injected).join(", ")}
2799
+ `);
2800
+ const res = spawnSync(cmd[0], cmd.slice(1), { stdio: "inherit", env: { ...process.env, ...injected } });
2801
+ process.exitCode = res.status ?? 1;
2802
+ }
2803
+ function secretExport(ctx, args) {
2804
+ const env = requireEnv(ctx, args);
2805
+ const format = flag(args, "--format") ?? "dotenv";
2806
+ if (format !== "dotenv" && format !== "json")
2807
+ ctx.die("--format expects dotenv | json");
2808
+ const runtimeRoot = process.env.SOL_RUNTIME_RECOVERY_CODE;
2809
+ const self = runtimeRoot ? runtimeSelfFromRoot(env, runtimeRoot) : ctx.loadSelfIdentity();
2810
+ let result;
2811
+ try {
2812
+ result = exportEnvWith(ctx.solDir, env, self);
2813
+ } catch (e) {
2814
+ if (e instanceof ExportError)
2815
+ ctx.die(e.message);
2816
+ throw e;
2817
+ }
2818
+ const rendered = renderExport(result.secrets, format);
2819
+ const fd = flag(args, "--fd");
2820
+ if (fd !== undefined) {
2821
+ const n = parseInt(fd, 10);
2822
+ if (!Number.isInteger(n))
2823
+ ctx.die("--fd expects a file descriptor number");
2824
+ writeSync(n, rendered + `
2825
+ `);
2826
+ } else if (process.stdout.isTTY) {
2827
+ ctx.die(`refused: \`sol secret export\` writes ${result.names.length} secret value(s) — refusing to print onto a TTY. redirect to a file/pipe or pass --fd <n> (the value never belongs in a log/terminal).`);
2828
+ } else {
2829
+ process.stdout.write(rendered + `
2830
+ `);
2831
+ }
2832
+ process.stderr.write(`sol: exported ${result.names.length} secret(s) for ${env} as ${format}: ${result.names.join(", ")}${result.skipped.length ? ` (not a recipient of: ${result.skipped.join(", ")})` : ""}
2833
+ `);
2834
+ }
2835
+ function secretRuntime(ctx, op, args) {
2836
+ const json = has(args, "--json");
2837
+ if (op === "ls")
2838
+ return runtimeLs(ctx, json);
2839
+ if (op === "show")
2840
+ return runtimeShow(ctx, requireEnv(ctx, args), json);
2841
+ if (op === "provision")
2842
+ return runtimeProvision(ctx, args);
2843
+ ctx.die("usage: sol secret runtime provision --env E [--fd N] | ls | show --env E");
2844
+ }
2845
+ function runtimeLs(ctx, json) {
2846
+ const world = loadWorld(ctx.solDir);
2847
+ const rows = world.manifest.order.map((env) => ({ env, runtime: world.manifest.runtime[env] ?? null }));
2848
+ if (json)
2849
+ return void console.log(JSON.stringify(rows, null, 2));
2850
+ for (const r of rows)
2851
+ console.log(`${r.env.padEnd(14)} runtime=${r.runtime ?? "(none — `sol secret runtime provision --env " + r.env + "`)"}`);
2852
+ }
2853
+ function runtimeShow(ctx, env, json) {
2854
+ const world = loadWorld(ctx.solDir);
2855
+ const handle = world.manifest.runtime[env];
2856
+ const account = runtimeAccountId(env);
2857
+ const authentic = deriveAuthenticGate(ctx.solDir);
2858
+ const inAudience = handle ? (authentic.handles[handle] ?? []).some((r) => r.accountId === account) : false;
2859
+ const view = { env, runtimeHandle: handle ?? null, runtimeAccount: handle ? account : null, inAuthenticGate: inAudience };
2860
+ if (json)
2861
+ return void console.log(JSON.stringify(view, null, 2));
2862
+ if (!handle)
2863
+ return void console.log(`${env}: no runtime recipient — provision one with \`sol secret runtime provision --env ${env}\``);
2864
+ console.log(`${env}: runtime ${handle} (${account}) in-authentic-gate=${inAudience ? "yes" : "NO (add via `sol env audience add` or re-provision)"}`);
2865
+ }
2866
+ async function runtimeProvision(ctx, args) {
2867
+ const json = has(args, "--json");
2868
+ const env = requireEnv(ctx, args);
2869
+ if (!envInitialized(ctx.solDir))
2870
+ ctx.die(`no .sol/env/ — run \`sol env init\` (or \`sol secret declare\`) first`);
2871
+ const { id } = requireSigner(ctx, `runtime provision ${env}`);
2872
+ const fdArg = flag(args, "--fd");
2873
+ let sinkFd;
2874
+ if (fdArg !== undefined) {
2875
+ const n = parseInt(fdArg, 10);
2876
+ if (!Number.isInteger(n))
2877
+ ctx.die("--fd expects a file descriptor number");
2878
+ sinkFd = n;
2879
+ } else if (process.stdout.isTTY) {
2880
+ ctx.die("refused: provisioning mints the bootstrap ROOT (the env's standing decryptor) — refusing to print it onto a TTY. pass --fd <n> (custody it write-only) or redirect stdout to a secure file/pipe.");
2881
+ } else {
2882
+ sinkFd = 1;
2883
+ }
2884
+ let captured;
2885
+ const sink = {
2886
+ async get() {
2887
+ return;
2888
+ },
2889
+ async put(_e, root) {
2890
+ captured = root;
2891
+ }
2892
+ };
2893
+ const prov = await provisionRuntimeRecipient(env, sink);
2894
+ if (!envInitialized(ctx.solDir))
2895
+ envInitScaffold(ctx, undefined, id);
2896
+ const world = loadWorld(ctx.solDir);
2897
+ const audCtx = {
2898
+ solDir: ctx.solDir,
2899
+ die: ctx.die,
2900
+ selfPub: (a) => a === prov.accountId ? prov.x25519Pub : undefined,
2901
+ dirPub: async () => {
2902
+ return;
2903
+ },
2904
+ selfEdPub: (a) => a === prov.accountId ? prov.edPub : undefined,
2905
+ dirEdPub: async () => {
2906
+ return;
2907
+ }
2908
+ };
2909
+ guardHandleEdit(ctx, world.gate, prov.handle, id, prov.accountId);
2910
+ const prevAud = (world.gate.handles[prov.handle] ?? []).map((r) => r.accountId);
2911
+ const { gate } = await audienceAdd(audCtx, world.gate, prov.handle, prov.accountId, prov.x25519Pub);
2912
+ writeAudienceDoc(ctx.solDir, gate);
2913
+ journal2(ctx, id, { op: "audience-add", handle: prov.handle, members: memberSnapshot(gate, prov.handle), recipientAtOp: true, prevAud, newAud: (gate.handles[prov.handle] ?? []).map((r) => r.accountId), at: Date.now() });
2914
+ world.manifest.runtime[env] = prov.handle;
2915
+ writeManifest(ctx.solDir, world.manifest);
2916
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2917
+ if (captured === undefined)
2918
+ ctx.die("provisioning did not mint a bootstrap root (internal error)");
2919
+ writeSync(sinkFd, captured + `
2920
+ `);
2921
+ process.stderr.write(`sol: provisioned runtime ${prov.handle} for ${env} (account ${prov.accountId}, X25519 ${prov.x25519Pub.slice(0, 16)}…).
2922
+ ` + ` the BOOTSTRAP ROOT was written to ${fdArg !== undefined ? `fd ${sinkFd}` : "stdout (non-TTY sink)"} — CUSTODY IT WRITE-ONLY in your runtime secret store
2923
+ ` + ` (e.g. the CF Secrets Store binding env.SOL_RUNTIME_ROOT). the runtime reads it at boot to export; the agent/host never holds it.
2924
+ `);
2925
+ if (json) {
2926
+ const pub = JSON.stringify({ env, handle: prov.handle, accountId: prov.accountId, x25519Pub: prov.x25519Pub, edPub: prov.edPub, keyEpoch: prov.keyEpoch, rootSink: fdArg !== undefined ? `fd:${sinkFd}` : "stdout" }, null, 2);
2927
+ if (fdArg !== undefined)
2928
+ console.log(pub);
2929
+ else
2930
+ process.stderr.write(pub + `
2931
+ `);
2932
+ }
2933
+ }
2934
+ function secretAudience(ctx, args) {
2935
+ const pos = positionals(args);
2936
+ const op = pos[0];
2937
+ const name = pos[1];
2938
+ const handle = pos[2];
2939
+ const env = requireEnv(ctx, args);
2940
+ if (op !== "add" && op !== "rm" || !name || !handle)
2941
+ ctx.die("usage: sol secret audience add|rm NAME --env E <handle>");
2942
+ const world = loadWorld(ctx.solDir);
2943
+ const file = world.files[env];
2944
+ if (!file)
2945
+ ctx.die(`unknown env: ${env}`);
2946
+ const slot = file.secrets.find((s) => s.name === name);
2947
+ if (!slot)
2948
+ ctx.die(`${name} is not declared in ${env}`);
2949
+ const id = manage(ctx);
2950
+ const priorSeal = slot.seal;
2951
+ const priorAud = slot.aud;
2952
+ const current = new Set(slot.aud ?? world.files[env]?.audience ?? []);
2953
+ if (op === "add")
2954
+ current.add(handle);
2955
+ else
2956
+ current.delete(handle);
2957
+ const nextAud = [...current];
2958
+ if (!id)
2959
+ requireSigner(ctx, `secret audience ${op} ${name}@${env}`);
2960
+ if (slot.seal) {
2961
+ const opened = openSealedValue(ctx.solDir, slot.seal, ctx.actor, id?.open);
2962
+ if (opened === undefined || opened === UNREADABLE) {
2963
+ ctx.die(`cannot reseal ${name}: you are not a current recipient (a recipient must run the audience change; set SOL_RECOVERY_CODE)`);
2964
+ }
2965
+ const { recipientPubKeys, handles } = authenticSealRecipients(ctx, { ...world, files: { ...world.files, [env]: { ...file, audience: nextAud } } }, env, nextAud);
2966
+ const box = readSealedBoxEpoch(ctx, slot.seal);
2967
+ const digest = writeSealedValueViaStore(ctx, opened, recipientPubKeys, box + 1);
2968
+ slot.seal = digest;
2969
+ slot.aud = handles;
2970
+ } else {
2971
+ slot.aud = nextAud;
2972
+ }
2973
+ writeEnvFile(ctx.solDir, file);
2974
+ journal2(ctx, id, {
2975
+ op: "secret-audience",
2976
+ env,
2977
+ name,
2978
+ handle,
2979
+ recipientAtOp: recipientAtOp(ctx, priorSeal, id),
2980
+ prevSeal: priorSeal,
2981
+ newSeal: slot.seal,
2982
+ prevAud: priorAud,
2983
+ newAud: slot.aud,
2984
+ at: Date.now()
2985
+ });
2986
+ regenerateSchema(ctx.solDir, loadWorld(ctx.solDir));
2987
+ console.log(`${op === "add" ? "added" : "removed"} ${handle} ${op === "add" ? "to" : "from"} ${name}@${env} -> aud=[${slot.aud.join(", ")}]${slot.seal ? " (resealed, epoch bumped)" : ""}`);
2988
+ }
2989
+ function writeSealedValueViaStore(ctx, value, recipientPubKeys, epoch = 1) {
2990
+ return writeSealedValue(ctx.solDir, value, recipientPubKeys, epoch);
2991
+ }
2992
+ function readSealedBoxEpoch(ctx, digest) {
2993
+ return readSealedBox(ctx.solDir, digest)?.epoch ?? 1;
2994
+ }
2995
+ function refuseUnanchoredSeal(ctx, env, name, seal) {
2996
+ const key = `${env}/${name}`;
2997
+ const issues = validateSealJournalBinding(ctx.solDir, new Map([[key, seal]]));
2998
+ if (issues.length) {
2999
+ ctx.die(`refused: ${key} (${seal}) is sealed under a FORGED/unanchored manifest seal — the SIGNED journal records no authorized set op for it (a non-recipient may seal a box to the owner's PUBLIC key and repoint the manifest while rolling the journal back). the value is NOT returned. run \`sol env validate\` and reseal with \`sol secret set ${name} --env ${env}\` or revert the tampering.`);
3000
+ }
3001
+ }
3002
+ function openSecret(ctx, env, name, field) {
3003
+ const world = loadWorld(ctx.solDir);
3004
+ const slot = resolveOne(world, env).secrets.find((s) => s.name === name);
3005
+ if (!slot)
3006
+ ctx.die(`${name} is not declared in ${env}`);
3007
+ if (!slot.seal)
3008
+ ctx.die(`${name} in ${env} is declared but UNSET`);
3009
+ refuseUnanchoredSeal(ctx, env, name, slot.seal);
3010
+ const self = ctx.loadSelfIdentity();
3011
+ const out = openOutcome(ctx.solDir, slot.seal, ctx.actor, self);
3012
+ switch (out.kind) {
3013
+ case "value":
3014
+ return projectField(out.value, field);
3015
+ case "absent":
3016
+ ctx.die(`no sealed leaf for ${name} in ${env}`);
3017
+ case "tampered":
3018
+ ctx.die(`refused: the sealed leaf for ${name}@${env} (${slot.seal}) was TAMPERED — its bytes hash to ${out.actual}, not the pinned content address. a forged box was substituted in place. reseal with \`sol secret set ${name} --env ${env}\` or revert the tampering; the value is NOT returned.`);
3019
+ case "not-recipient":
3020
+ ctx.die(`refused: ${ctx.actor} is not in the audience for ${name}@${env} (host-blind / agent-blind — you hold no key)`);
3021
+ case "decrypt-failed":
3022
+ ctx.die(`decrypt failed for ${name}@${env}: a lockbox exists for ${ctx.actor} but the ciphertext won't open — the sealed leaf (${slot.seal}) is corrupted or tampered, or SOL_RECOVERY_CODE is wrong. reseal with \`sol secret set ${name} --env ${env}\`.`);
3023
+ }
3024
+ }
3025
+
3026
+ // src/secret/mcp-tools.ts
3027
+ class AgentRefusal extends Error {
3028
+ }
3029
+ function agentCtx(cfg) {
3030
+ return {
3031
+ solDir: cfg.solDir,
3032
+ actor: cfg.actor ?? "agent",
3033
+ loadSelfIdentity: () => {
3034
+ return;
3035
+ },
3036
+ loadManageIdentity: cfg.loadManageIdentity,
3037
+ die: (msg) => {
3038
+ throw new AgentRefusal(msg);
3039
+ },
3040
+ selfPub: cfg.selfPub,
3041
+ dirPub: cfg.dirPub,
3042
+ selfEdPub: cfg.selfEdPub,
3043
+ dirEdPub: cfg.dirEdPub,
3044
+ remoteAnchorVerify: cfg.remoteAnchorVerify
3045
+ };
3046
+ }
3047
+ async function capture(fn) {
3048
+ const lines = [];
3049
+ const orig = console.log;
3050
+ console.log = (...args) => {
3051
+ lines.push(args.map((a) => typeof a === "string" ? a : String(a)).join(" "));
3052
+ };
3053
+ try {
3054
+ await fn();
3055
+ } finally {
3056
+ console.log = orig;
3057
+ }
3058
+ return lines.join(`
3059
+ `);
3060
+ }
3061
+ var sanitizer = new VaultSanitizer;
3062
+ function sanitizeEgress(text) {
3063
+ return sanitizer.sanitize(text).sanitized;
3064
+ }
3065
+ var AGENT_TOOLS = [
3066
+ { name: "env_list", description: "List every environment with inheritance + var counts. Schema only, no values.", inputSchema: { type: "object", properties: {} } },
3067
+ { name: "env_show", description: "Show one resolved environment (base ⊕ overlay) as JSON: config values + per-secret name/type/audience/set?. NEVER a secret value.", inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] } },
3068
+ { name: "env_schema", description: "The cross-env schema.lock: every var name, kind, type, per-env presence + audience across ALL environments. Values dark.", inputSchema: { type: "object", properties: {} } },
3069
+ { name: "env_diff", description: "Value-blind cross-env diff: added / removed / changed-SHAPE vars between two environments. Never value-equality.", inputSchema: { type: "object", properties: { a: { type: "string" }, b: { type: "string" } }, required: ["a", "b"] } },
3070
+ { name: "env_validate", description: "Schema integrity: fail-closed, cross-env drift, ref + audience consistency, agent-in-no-audience invariant. Set remote=true to also cross-check the external owner anchor.", inputSchema: { type: "object", properties: { remote: { type: "string", description: '"true" to also consult the remote anchor' } } } },
3071
+ { name: "secret_declare", description: "Declare a secret SLOT (name + audience + type) in an env. NO value — the slot is sealed-pending until a human/CI sets it via a side-channel. A brand-new slot is agent-safe; re-pointing an already-sealed slot is recipient-gated.", inputSchema: { type: "object", properties: { env: { type: "string" }, name: { type: "string" }, audience: { type: "string", description: "comma-separated handles, e.g. role:deploy,team:payments" }, type: { type: "string" }, hide_name: { type: "string", description: '"true" to hide the name in the host-visible schema' } }, required: ["env", "name"] } },
3072
+ { name: "secret_ref", description: "Resolve a declared secret to its sol:// reference — the handle the agent puts in code/config. Never a value.", inputSchema: { type: "object", properties: { env: { type: "string" }, name: { type: "string" } }, required: ["env", "name"] } },
3073
+ { name: "secret_list", description: "List declared secrets in an env: name + type + audience + set? status. NEVER values.", inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] } },
3074
+ { name: "secret_audit", description: "Audit an env: who-can-decrypt (the journal-authentic audience per secret) + drift (file-gate vs authentic-gate divergence, validation issues). Identifiers only, never values.", inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] } },
3075
+ { name: "secret_audience_ls", description: "List the audience gate (audience.toml): handle -> recipient accounts + pubkeys. Public material only.", inputSchema: { type: "object", properties: { handle: { type: "string", description: "optional — scope to one handle" } } } },
3076
+ { name: "secret_audience_add", description: "Add a recipient account to an audience handle (signed gate op). Authz-gated per the CLI: a guardian handle is never self-enrollable; a keyless agent is refused.", inputSchema: { type: "object", properties: { handle: { type: "string" }, account: { type: "string" }, pubkey: { type: "string", description: "optional sol1pk_… (else fetched from the key directory)" } }, required: ["handle", "account"] } },
3077
+ { name: "secret_audience_rm", description: "Remove a recipient account from an audience handle (signed gate op). No account drops the whole handle. Same authz as the CLI.", inputSchema: { type: "object", properties: { handle: { type: "string" }, account: { type: "string" } }, required: ["handle"] } },
3078
+ { name: "deploy_trigger", description: "Check an environment is deploy-ready and emit the runtime export command the deploy executes. The agent orchestrates; the RUNTIME recipient (holding SOL_RUNTIME_RECOVERY_CODE) is the only decryptor. Returns readiness STATUS (each secret: sealed? runtime-recipient?), never a value.", inputSchema: { type: "object", properties: { env: { type: "string" } }, required: ["env"] } },
3079
+ { name: "secret_export_trigger", description: "Same readiness check as deploy_trigger, plus the masked `sol secret export` invocation a deploy/boot runs to materialize NAME=value into a secure sink. The agent cannot read the output. Returns STATUS + the command, never values.", inputSchema: { type: "object", properties: { env: { type: "string" }, format: { type: "string", description: "dotenv | json (default dotenv)" } }, required: ["env"] } }
3080
+ ];
3081
+ function deployReadiness(solDir, env, format) {
3082
+ const world = loadWorld(solDir);
3083
+ const resolved = resolveOne(world, env);
3084
+ const authentic = deriveAuthenticGate(solDir);
3085
+ const runtimeAcct = runtimeAccountId(env);
3086
+ const runtimeMember = (handles) => handles.some((h) => (authentic.handles[h] ?? []).some((r) => r.accountId === runtimeAcct));
3087
+ const secrets = resolved.secrets.map((s) => {
3088
+ const aud = s.aud ?? resolved.audience ?? [];
3089
+ return {
3090
+ name: s.hideName ? "(name-hidden)" : s.name,
3091
+ sealed: !!s.seal,
3092
+ audience: aud,
3093
+ runtimeRecipient: runtimeMember(aud)
3094
+ };
3095
+ });
3096
+ const sealed = secrets.filter((s) => s.sealed);
3097
+ const unsealed = secrets.filter((s) => !s.sealed).map((s) => s.name);
3098
+ const notRuntime = sealed.filter((s) => !s.runtimeRecipient).map((s) => s.name);
3099
+ const blockers = [];
3100
+ if (unsealed.length)
3101
+ blockers.push(`unsealed (no value yet): ${unsealed.join(", ")}`);
3102
+ if (notRuntime.length)
3103
+ blockers.push(`runtime ${runtimeHandle(env)} is NOT in the audience of: ${notRuntime.join(", ")}`);
3104
+ if (!authentic.trustworthy)
3105
+ blockers.push("the management journal does not verify (run env_validate)");
3106
+ const fmt = format === "json" ? "json" : "dotenv";
3107
+ return {
3108
+ env,
3109
+ runtimeHandle: runtimeHandle(env),
3110
+ runtimeAccount: runtimeAcct,
3111
+ secrets,
3112
+ ready: blockers.length ? "blocked" : "ready",
3113
+ blockers,
3114
+ command: `sol secret export --env ${env} --format ${fmt} --fd 3 3>secrets.out`
3115
+ };
3116
+ }
3117
+ async function callMcpTool(ctx, name, args = {}) {
3118
+ const str = (k) => typeof args[k] === "string" ? args[k] : undefined;
3119
+ const flag2 = (k) => args[k] === true || args[k] === "true";
3120
+ const env = str("env");
3121
+ const envFlag = env ? ["--env", env] : [];
3122
+ const run = async () => {
3123
+ switch (name) {
3124
+ case "env_list":
3125
+ return capture(() => runEnv(ctx, "ls", ["--json"]));
3126
+ case "env_show":
3127
+ return capture(() => runEnv(ctx, "show", [env, "--json"]));
3128
+ case "env_schema":
3129
+ return capture(() => runEnv(ctx, "schema", ["--json"]));
3130
+ case "env_diff":
3131
+ return capture(() => runEnv(ctx, "diff", [str("a"), str("b"), "--json"]));
3132
+ case "env_validate":
3133
+ return capture(() => runEnv(ctx, "validate", flag2("remote") ? ["--remote", "--json"] : ["--json"]));
3134
+ case "secret_declare": {
3135
+ const a = [str("name"), ...envFlag];
3136
+ if (str("audience"))
3137
+ a.push("--audience", str("audience"));
3138
+ if (str("type"))
3139
+ a.push("--type", str("type"));
3140
+ if (flag2("hide_name"))
3141
+ a.push("--hide-name");
3142
+ return capture(() => runSecret(ctx, "declare", a));
3143
+ }
3144
+ case "secret_ref":
3145
+ return capture(() => runSecret(ctx, "ref", [str("name"), ...envFlag]));
3146
+ case "secret_list":
3147
+ return capture(() => runSecret(ctx, "list", [...envFlag, "--json"]));
3148
+ case "secret_audit":
3149
+ return JSON.stringify(auditEnv(ctx.solDir, env), null, 2);
3150
+ case "secret_audience_ls":
3151
+ return capture(() => runEnv(ctx, "audience", ["ls", ...str("handle") ? [str("handle")] : [], "--json"]));
3152
+ case "secret_audience_add": {
3153
+ const a = ["add", str("handle"), str("account"), "--json"];
3154
+ if (str("pubkey"))
3155
+ a.push("--pubkey", str("pubkey"));
3156
+ return capture(() => runEnv(ctx, "audience", a));
3157
+ }
3158
+ case "secret_audience_rm":
3159
+ return capture(() => runEnv(ctx, "audience", ["rm", str("handle"), ...str("account") ? [str("account")] : [], "--json"]));
3160
+ case "deploy_trigger":
3161
+ return JSON.stringify(deployReadiness(ctx.solDir, env), null, 2);
3162
+ case "secret_export_trigger":
3163
+ return JSON.stringify(deployReadiness(ctx.solDir, env, str("format")), null, 2);
3164
+ case "secret_set":
3165
+ case "secret_reveal":
3166
+ case "secret_inject":
3167
+ case "secret_export":
3168
+ throw new AgentRefusal(`refused: ${name} is not an agent tool — a value INPUT is a human side-channel and a value OUTPUT is recipient-gated. the agent is in no audience and cannot read or write a value.`);
3169
+ default:
3170
+ throw new AgentRefusal(`unknown tool: ${name}`);
3171
+ }
3172
+ };
3173
+ try {
3174
+ const out = await run();
3175
+ return { text: sanitizeEgress(out) };
3176
+ } catch (e) {
3177
+ const msg = e instanceof Error ? e.message : String(e);
3178
+ return { text: sanitizeEgress("sol: " + msg), isError: true };
3179
+ }
3180
+ }
3181
+
3182
+ // src/bin/identity-store.ts
3183
+ import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
3184
+ import { homedir } from "node:os";
3185
+ import { dirname, join as join4 } from "node:path";
3186
+ var IDENTITY_PATH = join4(homedir(), ".sol", "identity.json");
3187
+ function loadIdentity() {
3188
+ if (!existsSync5(IDENTITY_PATH))
3189
+ return;
3190
+ try {
3191
+ return JSON.parse(readFileSync6(IDENTITY_PATH, "utf8"));
3192
+ } catch {
3193
+ return;
3194
+ }
3195
+ }
3196
+ var EXPORT_KDF = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
3197
+ var VERIFIED_PATH = join4(homedir(), ".sol", "verified-keys.json");
3198
+
3199
+ // src/bin/sol-secret-mcp.ts
3200
+ async function startSecretMcp(opts = {}) {
3201
+ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
3202
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
3203
+ const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
3204
+ const solDir = opts.solDir || process.env.SOL_DIR || join7(process.cwd(), ".sol");
3205
+ if (!existsSync8(solDir)) {
3206
+ process.stderr.write(`sol-secret-mcp: no .sol at ${solDir} \u2014 run \`sol init\` first (or set SOL_DIR)
3207
+ `);
3208
+ process.exit(1);
3209
+ }
3210
+ const dirUrl = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
3211
+ async function fetchKey2(account) {
3212
+ const { fetchKey: f } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
3213
+ return f(dirUrl, account);
3214
+ }
3215
+ const ctx = agentCtx({
3216
+ solDir,
3217
+ actor: process.env.SOL_ACTOR || "agent",
3218
+ selfPub: (account) => {
3219
+ const id = loadIdentity();
3220
+ return id && id.accountId === account ? id.x25519Pub : undefined;
3221
+ },
3222
+ selfEdPub: (account) => {
3223
+ const id = loadIdentity();
3224
+ return id && id.accountId === account ? id.edPub : undefined;
3225
+ },
3226
+ dirPub: async (account) => (await fetchKey2(account))?.x25519Pub,
3227
+ dirEdPub: async (account) => (await fetchKey2(account))?.edPub
3228
+ });
3229
+ if (process.env.SOL_RECOVERY_CODE) {
3230
+ const { loadManageIdentity: loadManageIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
3231
+ ctx.loadManageIdentity = () => loadManageIdentity2();
3232
+ }
3233
+ const server = new Server({ name: "sol-secrets", version: "0.1.0" }, { capabilities: { tools: {} } });
3234
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: AGENT_TOOLS }));
3235
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
3236
+ const r = await callMcpTool(ctx, req.params.name, req.params.arguments ?? {});
3237
+ return { content: [{ type: "text", text: r.text }], isError: r.isError };
3238
+ });
3239
+ await server.connect(new StdioServerTransport);
3240
+ }
3241
+ if (__require.main == __require.module) {
3242
+ startSecretMcp().catch((e) => {
3243
+ process.stderr.write(`sol-secret-mcp: ${e?.message ?? e}
3244
+ `);
3245
+ process.exit(1);
3246
+ });
3247
+ }
3248
+ export {
3249
+ startSecretMcp
3250
+ };