@join-pantheon/cli 0.30.0 → 0.31.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.
@@ -1,15 +1,21 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { parseJsonText } from "./text-file.js";
5
+ import { withFileLock } from "./file-lock.js";
6
+ import { writeWhole } from "./pins.js";
5
7
  export const POLICY_FILE = "worker-policy.json";
6
8
  export const PROFILE_FILE = "profile.json";
7
9
  export const HUD_FILE = join("bin", ".hud-action.json");
8
10
  export const CONFIG_FILE = "config.json";
9
- export const GUARDED_FILES = [POLICY_FILE, CONFIG_FILE, PROFILE_FILE, HUD_FILE];
11
+ export const CONTINUED_FILE = "continued.json";
12
+ export const CURSOR_FILE = "worker-cursor.json";
13
+ export const PINS_FILE = "keys.json";
14
+ export const NONCES_FILE = "nonces.json";
15
+ export const GUARDED_FILES = [POLICY_FILE, CONFIG_FILE, PROFILE_FILE, HUD_FILE, CONTINUED_FILE, CURSOR_FILE, PINS_FILE, NONCES_FILE];
10
16
  const sha = (b) => createHash("sha256").update(b).digest("hex");
11
17
  const read = (f) => { try {
12
- return readFileSync(f);
18
+ return statSync(f).isFile() ? readFileSync(f) : null;
13
19
  }
14
20
  catch {
15
21
  return null;
@@ -24,9 +30,15 @@ const parse = (b) => {
24
30
  }
25
31
  };
26
32
  const trusted = new Map();
27
- export function trustWrite(file) {
28
- const b = read(file);
33
+ const runBaseline = new Map();
34
+ export function trustWrite(file, prev) {
29
35
  const k = resolve(file);
36
+ if (prev !== undefined && runBaseline.has(k)) {
37
+ const ph = prev ? sha(prev) : null;
38
+ if (ph !== runBaseline.get(k) && !trusted.get(k)?.has(ph ?? "absent"))
39
+ return;
40
+ }
41
+ const b = read(file);
30
42
  const set = trusted.get(k) ?? new Set();
31
43
  set.add(b ? sha(b) : "absent");
32
44
  if (set.size > 50)
@@ -34,7 +46,10 @@ export function trustWrite(file) {
34
46
  trusted.set(k, set);
35
47
  }
36
48
  export function snapshotHome(home, files = GUARDED_FILES) {
37
- return { home, files: files.map((rel) => { const b = read(join(home, rel)); return { rel, bytes: b, hash: b ? sha(b) : null }; }) };
49
+ const s = { home, at: Date.now(), files: files.map((rel) => { const b = read(join(home, rel)); return { rel, bytes: b, hash: b ? sha(b) : null }; }) };
50
+ for (const f of s.files)
51
+ runBaseline.set(resolve(join(home, f.rel)), f.hash);
52
+ return s;
38
53
  }
39
54
  export function changedKeys(a, b, prefix = "", depth = 0) {
40
55
  const isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
@@ -54,7 +69,8 @@ export function changedKeys(a, b, prefix = "", depth = 0) {
54
69
  }
55
70
  const obj = (x) => (x && typeof x === "object" && !Array.isArray(x) ? x : {});
56
71
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
57
- export function narrowPolicy(before, now) {
72
+ export const askedHere = new Set();
73
+ export function narrowPolicy(before, now, keepAsks = askedHere) {
58
74
  const b = obj(before);
59
75
  const keys = [];
60
76
  const out = { ...now };
@@ -94,6 +110,91 @@ export function narrowPolicy(before, now) {
94
110
  keys.push(`denied.${cap} removed`);
95
111
  }
96
112
  out.denied = denied;
113
+ const bk = obj(b.asks), nk = obj(now.asks);
114
+ const pending = {};
115
+ for (const [cap, q] of Object.entries(nk)) {
116
+ if (!(cap in bk)) {
117
+ keys.push(`asks.${cap} added`);
118
+ continue;
119
+ }
120
+ if (!same(bk[cap], q)) {
121
+ keys.push(`asks.${cap} changed`);
122
+ pending[cap] = bk[cap];
123
+ continue;
124
+ }
125
+ pending[cap] = q;
126
+ }
127
+ if ("asks" in now || Object.keys(pending).length)
128
+ out.asks = pending;
129
+ const bo = obj(b.wakeOnce), no = obj(now.wakeOnce);
130
+ const once = {};
131
+ for (const [k, n] of Object.entries(no)) {
132
+ const was = typeof bo[k] === "number" ? bo[k] : 0;
133
+ if (typeof n === "number" && n > was) {
134
+ keys.push(`wakeOnce.${k} raised`);
135
+ if (was > 0)
136
+ once[k] = was;
137
+ continue;
138
+ }
139
+ once[k] = n;
140
+ }
141
+ if ("wakeOnce" in now || Object.keys(once).length)
142
+ out.wakeOnce = once;
143
+ const bn = obj(b.wakeNo), nn = obj(now.wakeNo);
144
+ const wakeNo = { ...nn };
145
+ for (const [k, at] of Object.entries(bn)) {
146
+ if (!(k in nn) || (typeof at === "number" && typeof nn[k] === "number" && nn[k] < at)) {
147
+ wakeNo[k] = at;
148
+ keys.push(`wakeNo.${k} lifted`);
149
+ }
150
+ }
151
+ const ba = obj(b.wakeAsks), na = obj(now.wakeAsks);
152
+ const asks = {};
153
+ for (const [id, q] of Object.entries(na)) {
154
+ if (!(id in ba)) {
155
+ if (keepAsks.has(id)) {
156
+ asks[id] = q;
157
+ continue;
158
+ }
159
+ keys.push(`wakeAsks.${id} added`);
160
+ continue;
161
+ }
162
+ if (!same(ba[id], q)) {
163
+ keys.push(`wakeAsks.${id} changed`);
164
+ asks[id] = ba[id];
165
+ continue;
166
+ }
167
+ asks[id] = q;
168
+ }
169
+ if ("wakeAsks" in now || Object.keys(asks).length)
170
+ out.wakeAsks = asks;
171
+ for (const f of ["wakersCarried", "wakersDeferred"]) {
172
+ const bf = obj(b[f]), nf = obj(now[f]);
173
+ const merged = { ...nf };
174
+ let lost = 0;
175
+ for (const [k, v] of Object.entries(bf))
176
+ if (!(k in nf) || !same(nf[k], v)) {
177
+ merged[k] = v;
178
+ lost++;
179
+ }
180
+ if (lost) {
181
+ out[f] = merged;
182
+ keys.push(`${f}: ${lost} removed`);
183
+ }
184
+ }
185
+ const bt = obj(b.wakeTeamFlag), nt = obj(now.wakeTeamFlag);
186
+ const flags = {};
187
+ for (const [k, v] of Object.entries(nt)) {
188
+ if (!(k in bt)) {
189
+ keys.push(`wakeTeamFlag.${k} added`);
190
+ continue;
191
+ }
192
+ flags[k] = v;
193
+ }
194
+ if ("wakeTeamFlag" in now)
195
+ out.wakeTeamFlag = flags;
196
+ if ("wakeNo" in now || Object.keys(wakeNo).length)
197
+ out.wakeNo = wakeNo;
97
198
  return { policy: out, keys };
98
199
  }
99
200
  export function narrowConfig(before, now) {
@@ -112,32 +213,205 @@ export function narrowConfig(before, now) {
112
213
  keys.push(`spaces.${id}.relay changed`);
113
214
  }
114
215
  }
216
+ for (const [id, was] of Object.entries(bs)) {
217
+ const w = obj(was), m = obj(spaces[id]);
218
+ if (id in spaces && w.since !== undefined && m.since !== w.since) {
219
+ spaces[id] = { ...m, since: w.since };
220
+ keys.push(`spaces.${id}.since changed`);
221
+ }
222
+ }
223
+ const ids = new Set(Object.values(bs).map((m) => obj(m).id).filter((x) => typeof x === "string"));
224
+ for (const id of Object.keys(spaces))
225
+ if (!(id in bs)) {
226
+ delete spaces[id];
227
+ keys.push(`spaces.${id} added${ids.has(obj(ns[id]).id) ? " (a second entry for an existing team)" : ""}`);
228
+ }
229
+ for (const [id, m] of Object.entries(spaces)) {
230
+ const w = obj(bs[id]);
231
+ if (w.id !== undefined && obj(m).id !== w.id) {
232
+ spaces[id] = { ...obj(m), id: w.id };
233
+ keys.push(`spaces.${id}.id changed`);
234
+ }
235
+ if (w.name !== undefined && obj(spaces[id]).name !== w.name) {
236
+ spaces[id] = { ...obj(spaces[id]), name: w.name };
237
+ keys.push(`spaces.${id}.name changed`);
238
+ }
239
+ }
115
240
  if ("spaces" in now)
116
241
  out.spaces = spaces;
242
+ if (now.default_space !== before.default_space) {
243
+ out.default_space = before.default_space;
244
+ keys.push("default_space changed");
245
+ }
246
+ if (!same(obj(now.network).proxy, obj(before.network).proxy)) {
247
+ out.network = { ...obj(now.network), proxy: obj(before.network).proxy };
248
+ if (obj(before.network).proxy === undefined)
249
+ delete out.network.proxy;
250
+ keys.push("network.proxy changed");
251
+ }
252
+ const bb = obj(before.bindings), nb = obj(now.bindings);
253
+ const bindings = {};
254
+ for (const [k, v] of Object.entries(nb)) {
255
+ if (!(k in bb)) {
256
+ keys.push(`bindings.${k} added`);
257
+ continue;
258
+ }
259
+ if (!same(bb[k], v)) {
260
+ keys.push(`bindings.${k} changed`);
261
+ bindings[k] = bb[k];
262
+ continue;
263
+ }
264
+ bindings[k] = v;
265
+ }
266
+ if ("bindings" in now || Object.keys(bindings).length)
267
+ out.bindings = bindings;
268
+ const bu = Array.isArray(before.unbound) ? before.unbound : [];
269
+ const nu = Array.isArray(now.unbound) ? now.unbound : [];
270
+ const lifted = bu.filter((x) => !nu.includes(x));
271
+ if (lifted.length) {
272
+ out.unbound = [...nu, ...lifted];
273
+ keys.push(`unbound: ${lifted.length} taken off`);
274
+ }
117
275
  return { config: out, keys };
118
276
  }
277
+ export function narrowPins(before, now) {
278
+ const b = obj(before), n = obj(now);
279
+ const keys = [];
280
+ const bt = obj(b.teams), nt = obj(n.teams);
281
+ const teams = {};
282
+ for (const id of new Set([...Object.keys(bt), ...Object.keys(nt)])) {
283
+ const was = obj(bt[id]), is = obj(nt[id]);
284
+ const wm = obj(was.members), im = obj(is.members);
285
+ const members = {};
286
+ for (const [m, pin] of Object.entries(im)) {
287
+ if (!(m in wm)) {
288
+ keys.push(`${id}.${m} pinned during the run`);
289
+ continue;
290
+ }
291
+ const w = obj(wm[m]), p = obj(pin);
292
+ const pendingOk = same(p.pending ?? null, w.pending ?? null);
293
+ if (p.fp !== w.fp || p.key !== w.key) {
294
+ members[m] = { ...w };
295
+ keys.push(`${id}.${m} key changed`);
296
+ continue;
297
+ }
298
+ if (!pendingOk) {
299
+ const { pending: _p, ...rest } = obj(pin);
300
+ members[m] = { ...rest, ...(w.pending ? { pending: w.pending } : {}) };
301
+ keys.push(`${id}.${m} held-back key changed during the run`);
302
+ continue;
303
+ }
304
+ if (w.signs && !p.signs) {
305
+ members[m] = { ...p, signs: w.signs };
306
+ keys.push(`${id}.${m} signing forgotten`);
307
+ continue;
308
+ }
309
+ members[m] = pin;
310
+ }
311
+ for (const [m, pin] of Object.entries(wm))
312
+ if (!(m in im)) {
313
+ members[m] = pin;
314
+ keys.push(`${id}.${m} unpinned`);
315
+ }
316
+ const t = { ...is, members };
317
+ if (was.sigs && !is.sigs) {
318
+ t.sigs = was.sigs;
319
+ keys.push(`${id}: signatures turned off`);
320
+ }
321
+ if (is.spaceId !== was.spaceId) {
322
+ if (was.spaceId === undefined)
323
+ delete t.spaceId;
324
+ else
325
+ t.spaceId = was.spaceId;
326
+ keys.push(`${id}: team id changed`);
327
+ }
328
+ teams[id] = t;
329
+ }
330
+ return { value: { ...n, v: 1, teams }, keys };
331
+ }
332
+ export function narrowNonces(before, now) {
333
+ const b = obj(before), n = obj(now);
334
+ const out = { ...n };
335
+ let lost = 0;
336
+ for (const [team, entries] of Object.entries(b)) {
337
+ const cur = { ...obj(n[team]) };
338
+ const forward = (c, w) => same(c, w) || (Array.isArray(c) && Array.isArray(w) && c[0] === w[0] && c[1] === w[1]
339
+ && (c[2] ?? null) === (w[2] ?? null) && typeof c[3] === "number" && (w[3] === undefined || w[3] === c[3]));
340
+ for (const [k, v] of Object.entries(obj(entries)))
341
+ if (!(k in cur) || !forward(cur[k], v)) {
342
+ cur[k] = v;
343
+ lost++;
344
+ }
345
+ out[team] = cur;
346
+ }
347
+ return { value: out, keys: lost ? [`${lost} seen request${lost === 1 ? "" : "s"} forgotten`] : [] };
348
+ }
119
349
  const WAKE_ORDER = { no: 0, approved: 1, anyone: 2 };
120
350
  export function narrowProfile(before, now) {
121
351
  const bw = obj(obj(before).agent).wake_default, nw = obj(now.agent).wake_default;
122
- const rank = (v) => (typeof v === "string" && v in WAKE_ORDER ? WAKE_ORDER[v] : WAKE_ORDER.approved);
123
- if (rank(nw) <= rank(bw))
124
- return { profile: now, keys: [] };
352
+ const rank = (v) => (typeof v === "string" && v in WAKE_ORDER ? WAKE_ORDER[v] : WAKE_ORDER.anyone);
125
353
  const agent = { ...obj(now.agent) };
126
- if (bw === undefined)
127
- delete agent.wake_default;
128
- else
354
+ const keys = [];
355
+ const ba = obj(obj(before).agent);
356
+ if (bw !== undefined && nw === undefined) {
129
357
  agent.wake_default = bw;
130
- return { profile: { ...now, agent }, keys: [`agent.wake_default loosened to ${String(nw)}`] };
358
+ keys.push("agent.wake_default removed");
359
+ }
360
+ else if (rank(nw) > rank(bw)) {
361
+ if (bw === undefined)
362
+ delete agent.wake_default;
363
+ else
364
+ agent.wake_default = bw;
365
+ keys.push(`agent.wake_default loosened to ${String(nw)}`);
366
+ }
367
+ if (agent.allow_bash === true && ba.allow_bash !== true) {
368
+ if (ba.allow_bash === undefined)
369
+ delete agent.allow_bash;
370
+ else
371
+ agent.allow_bash = ba.allow_bash;
372
+ keys.push("agent.allow_bash turned on");
373
+ }
374
+ else if (ba.allow_bash === false && agent.allow_bash !== false) {
375
+ agent.allow_bash = false;
376
+ keys.push("agent.allow_bash no removed");
377
+ }
378
+ if (agent.wake_enabled === true && ba.wake_enabled === false) {
379
+ agent.wake_enabled = false;
380
+ keys.push("agent.wake_enabled turned on");
381
+ }
382
+ if (!same(agent.primary_surface, ba.primary_surface) && ba.primary_surface !== undefined) {
383
+ agent.primary_surface = ba.primary_surface;
384
+ keys.push("agent.primary_surface changed");
385
+ }
386
+ return keys.length ? { profile: { ...now, agent }, keys } : { profile: now, keys: [] };
131
387
  }
132
388
  export function restoreHome(s) {
133
389
  const out = [];
390
+ const takesFiles = () => { const p = join(s.home, `.write-check.${process.pid}.${Date.now()}`); try {
391
+ writeFileSync(p, "", { flag: "wx", mode: 0o600 });
392
+ rmSync(p, { force: true });
393
+ return true;
394
+ }
395
+ catch {
396
+ return false;
397
+ } };
398
+ if (!takesFiles()) {
399
+ try {
400
+ chmodSync(s.home, 0o700);
401
+ }
402
+ catch { }
403
+ out.push(takesFiles() ? { rel: "the Pantheon folder", what: "reverted", keys: ["made read-only"] } : { rel: "the Pantheon folder", what: "reported", keys: ["cannot take new files — fix its permissions"] });
404
+ }
405
+ for (const f of s.files)
406
+ runBaseline.delete(resolve(join(s.home, f.rel)));
134
407
  for (const f of s.files) {
135
408
  const file = join(s.home, f.rel);
136
409
  const now = read(file);
137
410
  const hashNow = now ? sha(now) : null;
138
411
  if (hashNow === f.hash)
139
412
  continue;
140
- if (trusted.get(resolve(file))?.has(hashNow ?? "absent"))
413
+ const alwaysNarrow = f.rel === POLICY_FILE || f.rel === PROFILE_FILE || f.rel === CONFIG_FILE || f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE || f.rel === PINS_FILE || f.rel === NONCES_FILE;
414
+ if (!alwaysNarrow && trusted.get(resolve(file))?.has(hashNow ?? "absent"))
141
415
  continue;
142
416
  try {
143
417
  if (f.rel === HUD_FILE) {
@@ -147,6 +421,11 @@ export function restoreHome(s) {
147
421
  }
148
422
  continue;
149
423
  }
424
+ try {
425
+ if (!statSync(file).isFile())
426
+ rmSync(file, { recursive: true, force: true });
427
+ }
428
+ catch { }
150
429
  const a = parse(f.bytes), b = parse(now);
151
430
  if (f.rel === POLICY_FILE) {
152
431
  if (!b) {
@@ -164,6 +443,82 @@ export function restoreHome(s) {
164
443
  }
165
444
  continue;
166
445
  }
446
+ const locked = (fn) => {
447
+ if (!(f.rel === PINS_FILE || f.rel === NONCES_FILE || f.rel === CONTINUED_FILE))
448
+ return fn();
449
+ let ran = false;
450
+ try {
451
+ withFileLock(file, () => { ran = true; fn(); });
452
+ }
453
+ catch (e) {
454
+ if (ran)
455
+ throw e;
456
+ fn();
457
+ }
458
+ };
459
+ const put = (text) => { try {
460
+ writeWhole(file, text);
461
+ }
462
+ catch {
463
+ writeFileSync(file, text, { mode: 0o600 });
464
+ } };
465
+ if (f.rel === PINS_FILE || f.rel === NONCES_FILE) {
466
+ locked(() => {
467
+ const cur = parse(read(file));
468
+ const n = f.rel === PINS_FILE ? narrowPins(a, cur) : narrowNonces(a, cur);
469
+ if (n.keys.length) {
470
+ put(JSON.stringify(n.value, null, 2) + "\n");
471
+ trustWrite(file);
472
+ out.push({ rel: f.rel, what: "reverted", keys: n.keys });
473
+ }
474
+ });
475
+ continue;
476
+ }
477
+ if (f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE)
478
+ locked(() => {
479
+ const before = obj(a), now = obj(parse(read(file)));
480
+ const merged = { ...now };
481
+ let lost = 0;
482
+ for (const [k, v] of Object.entries(before)) {
483
+ if (f.rel === CURSOR_FILE) {
484
+ if (typeof v === "number" && !(typeof now[k] === "number" && now[k] >= v)) {
485
+ merged[k] = v;
486
+ lost++;
487
+ }
488
+ }
489
+ else if (Array.isArray(v)) {
490
+ const cur = Array.isArray(now[k]) ? now[k] : [];
491
+ const back = v.filter((x) => !cur.includes(x));
492
+ if (back.length) {
493
+ merged[k] = [...back, ...cur];
494
+ lost += back.length;
495
+ }
496
+ }
497
+ }
498
+ let sentDropped = 0;
499
+ if (f.rel === CONTINUED_FILE)
500
+ for (const [k, v] of Object.entries(merged)) {
501
+ if (!k.endsWith(" sent") || !Array.isArray(v))
502
+ continue;
503
+ const was = Array.isArray(before[k]) ? before[k] : [];
504
+ const kept = v.filter((x) => was.includes(x));
505
+ if (kept.length !== v.length) {
506
+ merged[k] = kept;
507
+ sentDropped += v.length - kept.length;
508
+ }
509
+ }
510
+ if (sentDropped && !lost) {
511
+ put(JSON.stringify(merged) + "\n");
512
+ trustWrite(file);
513
+ }
514
+ if (lost) {
515
+ put(JSON.stringify(merged) + "\n");
516
+ trustWrite(file);
517
+ out.push({ rel: f.rel, what: "reverted", keys: [f.rel === CURSOR_FILE ? "read position moved back" : `${lost} handled request${lost === 1 ? "" : "s"} unmarked`] });
518
+ }
519
+ });
520
+ if (f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE)
521
+ continue;
167
522
  if (f.rel === PROFILE_FILE) {
168
523
  if (!b)
169
524
  continue;
@@ -182,7 +537,7 @@ export function restoreHome(s) {
182
537
  trustWrite(file);
183
538
  out.push({ rel: f.rel, what: "reverted", keys: n.keys });
184
539
  }
185
- const rest = changedKeys(a, n.config).filter((k) => !/privateKey|token/i.test(k)).slice(0, 20);
540
+ const rest = changedKeys(a, n.config).filter((k) => !/privateKey|token/i.test(k) && !/^spaces\.[^.]+\.repos\b/.test(k)).slice(0, 20);
186
541
  if (rest.length)
187
542
  out.push({ rel: f.rel, what: "reported", keys: rest });
188
543
  }
@@ -205,6 +560,6 @@ export function describeChange(c) {
205
560
  return `${c.rel} was written while the run could write it — removed, not acted on`;
206
561
  if (c.what === "reported")
207
562
  return `${c.rel} changed while the run was going${c.keys.length ? ` (${c.keys.join(", ")})` : ""} — left as it is; if you did not change it, check it`;
208
- return `${c.rel}: what the run widened was taken back (${c.keys.join(", ")}) — if you did that yourself during the run, do it again`;
563
+ return `${c.rel}: what was widened while the run was going was taken back (${c.keys.join(", ")}) — if you did that yourself, do it again`;
209
564
  }
210
565
  //# sourceMappingURL=home-guard.js.map
package/dist/identity.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateKeyPairSync, sign, verify, createPublicKey, randomBytes } from "node:crypto";
1
+ import { generateKeyPairSync, sign, verify, createPublicKey, randomBytes, createHash } from "node:crypto";
2
2
  export function newKeypair() {
3
3
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
4
4
  return {
@@ -88,8 +88,84 @@ export function verifyRead(publicKeyB64, method, path, params, signatureB64) {
88
88
  return false;
89
89
  }
90
90
  }
91
+ export function canonicalKey(publicKeyB64) {
92
+ if (typeof publicKeyB64 !== "string" || publicKeyB64.length > 200)
93
+ return null;
94
+ try {
95
+ const raw = Buffer.from(publicKeyB64, "base64");
96
+ if (raw.length !== 44 || raw.toString("base64") !== publicKeyB64)
97
+ return null;
98
+ const k = createPublicKey({ key: raw, format: "der", type: "spki" });
99
+ if (k.asymmetricKeyType !== "ed25519")
100
+ return null;
101
+ const again = k.export({ type: "spki", format: "der" });
102
+ return again.equals(raw) ? publicKeyB64 : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
91
108
  export function fingerprint(publicKeyB64) {
92
109
  const raw = Buffer.from(publicKeyB64, "base64").subarray(-16).toString("hex");
93
110
  return (raw.match(/.{4}/g) ?? []).join("-");
94
111
  }
112
+ const sha256hex = (s) => createHash("sha256").update(s, "utf8").digest("hex");
113
+ export function canonicalJson(v) {
114
+ if (v === null || typeof v !== "object")
115
+ return JSON.stringify(v ?? null);
116
+ if (Array.isArray(v))
117
+ return `[${v.map(canonicalJson).join(",")}]`;
118
+ const o = v;
119
+ return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
120
+ }
121
+ export function newActId() { return `a_${Date.now().toString(36)}${randomBytes(9).toString("hex")}`; }
122
+ export function actPayload(f) {
123
+ const list = (a, none) => (a && a.length ? [...a].sort().join(",") : none);
124
+ return Buffer.from([
125
+ "pantheon-act/1", f.id, f.team, f.member, f.keyRole, f.nonce, String(f.ts),
126
+ f.act, f.goal ?? "-", f.ref ?? "-", f.to === null ? "*" : list(f.to, "*"),
127
+ f.defers_to ?? "-", list(f.affects, "-"), f.repo ?? "-",
128
+ f.summary ? sha256hex(f.summary) : "-",
129
+ sha256hex(f.body),
130
+ sha256hex(canonicalJson(f.meta ?? {})),
131
+ ].join("\n"), "utf8");
132
+ }
133
+ export function goalPayload(f) {
134
+ return Buffer.from([
135
+ "pantheon-goal/1", f.id, f.team, f.member, f.keyRole, f.nonce, String(f.ts),
136
+ sha256hex(f.title), sha256hex(f.acceptance), f.invite === null ? "*" : [...new Set(f.invite)].sort().join(",") || "-",
137
+ f.budget == null ? "-" : String(f.budget), f.deadline == null ? "-" : String(f.deadline), f.kind ?? "-",
138
+ ].join("\n"), "utf8");
139
+ }
140
+ export function delegatePayload(team, member, subFp, run, expires) {
141
+ return Buffer.from(`pantheon-delegate/1\n${team}\n${member}\nrun\n${subFp}\n${run}\n${expires}`, "utf8");
142
+ }
143
+ function privKey(b64) { return { key: Buffer.from(b64, "base64"), format: "der", type: "pkcs8" }; }
144
+ function pubKey(b64) { return createPublicKey({ key: Buffer.from(b64, "base64"), format: "der", type: "spki" }); }
145
+ export function signBytes(privateKeyB64, payload) {
146
+ return sign(null, payload, privKey(privateKeyB64)).toString("base64");
147
+ }
148
+ export function verifyBytes(publicKeyB64, payload, signatureB64) {
149
+ try {
150
+ return verify(null, payload, pubKey(publicKeyB64), Buffer.from(signatureB64, "base64"));
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ export function signDelegate(memberPrivateKeyB64, team, member, subPub, run, expires) {
157
+ return { pub: subPub, run, expires, sig: signBytes(memberPrivateKeyB64, delegatePayload(team, member, fingerprint(subPub), run, expires)) };
158
+ }
159
+ export function verifyDelegate(memberPublicKeyB64, team, member, cert, at = Date.now(), skewMs = 10 * 60_000) {
160
+ if (!cert || typeof cert.pub !== "string" || typeof cert.sig !== "string" || typeof cert.run !== "string" || !Number.isFinite(cert.expires))
161
+ return false;
162
+ if (!canonicalKey(cert.pub) || !canonicalKey(memberPublicKeyB64))
163
+ return false;
164
+ if (at > cert.expires + skewMs)
165
+ return false;
166
+ return verifyBytes(memberPublicKeyB64, delegatePayload(team, member, fingerprint(cert.pub), cert.run, cert.expires), cert.sig);
167
+ }
168
+ export function newActNonce() {
169
+ return randomBytes(16).toString("base64url");
170
+ }
95
171
  //# sourceMappingURL=identity.js.map
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { randomBytes } from "node:crypto";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { CONFIG_DIR } from "./config.js";
@@ -78,7 +79,9 @@ export async function superviseMcp() {
78
79
  pid: process.pid, child_pid: current?.proc.pid, cwd: process.cwd(), started: STARTED,
79
80
  supervisor: VERSION, child_version: current?.version ?? "unknown", agent: process.env.PANTHEON_AGENT,
80
81
  };
81
- writeFileSync(stateFile, JSON.stringify(s));
82
+ const tmp = `${stateFile}.${randomBytes(8).toString("hex")}.tmp`;
83
+ writeFileSync(tmp, JSON.stringify(s), { flag: "wx" });
84
+ renameSync(tmp, stateFile);
82
85
  }
83
86
  catch { }
84
87
  };