@uuaid/pillar 0.2.0-alpha.8 → 2.0.1

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 (45) hide show
  1. package/HOSTING-LIMITS.md +432 -0
  2. package/INTERACTION-CONVENTION.md +222 -1
  3. package/README.md +371 -17
  4. package/bin/carrier.mjs +7 -1
  5. package/bin/pillar-ask.mjs +1 -0
  6. package/bin/pillar-blob.mjs +139 -0
  7. package/bin/pillar-directory.mjs +232 -0
  8. package/bin/pillar-escrow.mjs +675 -0
  9. package/bin/pillar-responder.mjs +47 -1
  10. package/bin/pillar-seed.mjs +113 -13
  11. package/bin/pillar.mjs +345 -4
  12. package/package.json +30 -7
  13. package/src/blob/blob.mjs +510 -0
  14. package/src/blob/broker.mjs +129 -0
  15. package/src/blob/http.mjs +334 -0
  16. package/src/blob/store.mjs +55 -0
  17. package/src/escrow/box.mjs +43 -0
  18. package/src/escrow/escrow-service.mjs +538 -0
  19. package/src/escrow/rails/evm-rail.mjs +427 -0
  20. package/src/escrow/rails/index.mjs +41 -0
  21. package/src/escrow/rails/zilligon-rail.mjs +73 -0
  22. package/src/escrow/terms.mjs +368 -0
  23. package/src/escrow/token-registry.mjs +231 -0
  24. package/src/escrow/verify.mjs +119 -0
  25. package/src/escrow/zilligon.mjs +266 -0
  26. package/src/identity/keychain.mjs +27 -10
  27. package/src/identity/tier.mjs +130 -0
  28. package/src/identity/trust.mjs +63 -0
  29. package/src/index.mjs +28 -2
  30. package/src/mailbox/mailbox.mjs +41 -0
  31. package/src/net/carrier-client.mjs +93 -14
  32. package/src/net/carrier-server.mjs +212 -15
  33. package/src/net/delegation.mjs +209 -0
  34. package/src/net/envelope.mjs +81 -0
  35. package/src/net/link-offer.mjs +107 -0
  36. package/src/net/refusals.mjs +126 -0
  37. package/src/net/seed-registry.mjs +467 -0
  38. package/src/net/seed-tier.mjs +185 -0
  39. package/src/node-check.mjs +32 -0
  40. package/src/observability.mjs +141 -0
  41. package/src/pillar.mjs +206 -17
  42. package/src/vda/core.mjs +290 -0
  43. package/src/vda/directory.mjs +87 -0
  44. package/src/vda/instruments.mjs +246 -0
  45. package/src/vda/shims.mjs +77 -0
package/bin/pillar.mjs CHANGED
@@ -7,11 +7,16 @@
7
7
  * pillar id [--home ~/.pillar] print our UUAID + PeerId + addrs
8
8
  * pillar send <recipient> <kind> <json> send a signed envelope
9
9
  * pillar recv [--home ~/.pillar] [--follow] listen for envelopes and print them
10
+ * pillar inbox [--home ~/.pillar] [--unread] [--notify CMD] [--mark-read]
11
+ * one-shot: pull pending mail, print, exit
12
+ * pillar trust [set|allow|block|unlist|quarantine] recipient consent policy
13
+ * pillar tier [issue|install|verify] signed volume/size tier grants
10
14
  * pillar peers list currently connected peers
11
15
  *
12
16
  * All commands respect --home to point at an alternate pillar directory,
13
17
  * useful for running multiple pillars on one machine (Zestro dogfood etc.).
14
18
  */
19
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
15
20
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
16
21
  import { join, dirname } from "node:path";
17
22
  import { homedir } from "node:os";
@@ -19,6 +24,10 @@ import { argv, exit } from "node:process";
19
24
  import { Keychain } from "../src/identity/keychain.mjs";
20
25
  import { Pillar } from "../src/pillar.mjs";
21
26
  import { loadFleetAddressBook } from "../src/address-book.mjs";
27
+ import { loadTrustPolicy, POLICIES } from "../src/identity/trust.mjs";
28
+ import { parseLinkOffer, isLinkOfferLive, LINK_GRANT_KINDS } from "../src/net/link-offer.mjs";
29
+ import { mintDelegationDoc } from "../src/net/delegation.mjs";
30
+ import { refusalLine } from "../src/net/refusals.mjs";
22
31
 
23
32
  function parseArgs(argv) {
24
33
  const args = { _: [] };
@@ -58,6 +67,19 @@ function loadPillar(args) {
58
67
  const carriers = args.carrier
59
68
  ? [args.carrier]
60
69
  : (process.env.PILLAR_CARRIERS ? process.env.PILLAR_CARRIERS.split(",") : (cfg.carriers ?? []));
70
+ // Trust gate: --policy overrides trust.json's policy for this invocation.
71
+ const trust = loadTrustPolicy(home);
72
+ if (args.policy) {
73
+ if (!POLICIES.includes(args.policy)) {
74
+ console.error(`--policy must be one of ${POLICIES.join("|")}`);
75
+ exit(2);
76
+ }
77
+ trust.policy = args.policy;
78
+ }
79
+ // Tier grant: <home>/tier-grant.json (issued via `pillar tier issue`) is
80
+ // attached to carrier delivers — raises budgets at carriers trusting the issuer.
81
+ const tgPath = join(home, "tier-grant.json");
82
+ const tierGrant = existsSync(tgPath) ? JSON.parse(readFileSync(tgPath, "utf-8")) : null;
61
83
  return new Pillar({
62
84
  keychain: kc,
63
85
  mailboxPath: mbPath,
@@ -67,6 +89,8 @@ function loadPillar(args) {
67
89
  dhtClientMode: cfg.dhtClientMode ?? true,
68
90
  carriers,
69
91
  enableLibp2p: !(args["carrier-only"] || cfg.carrierOnly === true),
92
+ trust,
93
+ tierGrant,
70
94
  });
71
95
  }
72
96
 
@@ -245,6 +269,121 @@ async function cmdSend(args) {
245
269
  }
246
270
  }
247
271
 
272
+
273
+ // ── delegate ──────────────────────────────────────────────────────────────────
274
+ // The desktop-link ceiling enforced by chat-web (browser-chat-session.ts,
275
+ // LINK_TTL_MS). A grant with a longer window verifies fine and is then DROPPED
276
+ // by the browser without a word, so this side refuses it loudly instead.
277
+ const LINK_TTL_DAYS_MAX = 7;
278
+
279
+ /**
280
+ * pillar delegate --to-browser '<uuaid://link/...>'
281
+ *
282
+ * Pair a browser tab to THIS pillar identity. The tab publishes an offer
283
+ * carrying its own freshly minted device key; this mints a delegation naming
284
+ * that key as delegate and this identity as principal, and sends it as a
285
+ * `_link` grant to the tab's inbox.
286
+ *
287
+ * The persona seed never leaves this machine — only a signed, time-boxed
288
+ * document does. Identical in every respect to what a phone sends: the
289
+ * handshake authenticates the PRINCIPAL (the envelope sender must equal the
290
+ * delegation principal), and never the kind of device the principal runs on.
291
+ * That is what lets a headless agent hold a desktop.
292
+ */
293
+ async function cmdDelegate(args) {
294
+ const offerRaw = typeof args["to-browser"] === "string" ? args["to-browser"] : args._[1];
295
+ if (!offerRaw || args["to-browser"] === true) {
296
+ console.error("usage: pillar delegate --to-browser '<uuaid://link/...>' [--ttl-days N] [--name NAME]");
297
+ console.error(" Paste the offer string the browser shows under its QR code.");
298
+ exit(2);
299
+ }
300
+
301
+ const offer = parseLinkOffer(offerRaw);
302
+ if (!offer) {
303
+ console.error("not a valid link offer. Expected: uuaid://link/<delegate-uuaid>?pk=<64hex>&nonce=<hex>&exp=<iso>");
304
+ exit(2);
305
+ }
306
+ if (!isLinkOfferLive(offer)) {
307
+ console.error(`offer expired at ${offer.exp} — reload the browser tab for a fresh one`);
308
+ exit(1);
309
+ }
310
+
311
+ const ttlDays = args["ttl-days"] !== undefined ? Number(args["ttl-days"]) : LINK_TTL_DAYS_MAX;
312
+ if (!Number.isFinite(ttlDays) || ttlDays <= 0) {
313
+ console.error("--ttl-days must be a positive number");
314
+ exit(2);
315
+ }
316
+ // Refuse rather than mint a grant the far side discards in silence.
317
+ if (ttlDays > LINK_TTL_DAYS_MAX) {
318
+ console.error(`--ttl-days ${ttlDays} exceeds the ${LINK_TTL_DAYS_MAX}-day desktop-link ceiling.`);
319
+ console.error("A longer grant is a VALID document that the browser drops without reporting why.");
320
+ exit(2);
321
+ }
322
+
323
+ const p = loadPillar(args);
324
+ await p.start();
325
+ try {
326
+ const me = p.keychain._identity;
327
+ const delegation = mintDelegationDoc(p.keychain, {
328
+ delegate: offer.delegate,
329
+ delegatePublicKey: offer.pk,
330
+ kinds: [...LINK_GRANT_KINDS], // never "*": that would let the desktop read this identity's inbox
331
+ ttlDays,
332
+ });
333
+
334
+ // The snapshot's persona MUST equal the delegation principal exactly — the
335
+ // browser rejects the whole grant on any mismatch, treating a disagreeing
336
+ // snapshot as evidence that neither half can be trusted.
337
+ // fleet.json is { fleet: { "<role>": { uuaid, publicKey, ... } } } — keyed by
338
+ // ROLE, which is also the human-readable name the desktop shows. A row
339
+ // without a publicKey cannot be sealed to, so it is not a contact.
340
+ const contacts = [];
341
+ const fp = fleetPath(args);
342
+ if (existsSync(fp)) {
343
+ let fleet = {};
344
+ try {
345
+ fleet = JSON.parse(readFileSync(fp, "utf-8")).fleet ?? {};
346
+ } catch {
347
+ console.error(`warning: ${fp} is not readable JSON — sending 0 contacts`);
348
+ }
349
+ for (const [role, entry] of Object.entries(fleet)) {
350
+ if (typeof entry?.uuaid !== "string" || typeof entry?.publicKey !== "string") continue;
351
+ contacts.push({ uuaid: entry.uuaid, publicKey: entry.publicKey, name: String(role) });
352
+ }
353
+ }
354
+
355
+ const payload = {
356
+ _link: { v: 1, op: "grant", nonce: offer.nonce },
357
+ delegation,
358
+ snapshot: {
359
+ persona: { uuaid: me.uuaid, publicKey: me.publicKeyHex },
360
+ displayName: typeof args.name === "string" ? args.name : null,
361
+ contacts,
362
+ },
363
+ };
364
+
365
+ const { envelope } = await p.send({
366
+ recipient: offer.delegate,
367
+ kind: "message",
368
+ payload,
369
+ recipientPublicKey: offer.pk,
370
+ resolveTimeoutMs: 5000,
371
+ });
372
+
373
+ console.log(`linked ${offer.delegate}`);
374
+ console.log(` principal: ${me.uuaid}`);
375
+ console.log(` window: ${delegation.notBefore} -> ${delegation.notAfter} (${ttlDays}d)`);
376
+ console.log(` contacts: ${contacts.length}`);
377
+ console.log(` envelope: ${envelope.id}`);
378
+ console.log("");
379
+ console.log("The tab pairs when it polls. If it does not, the grant was refused in");
380
+ console.log("silence — check the offer is unexpired and unclaimed, and that the tab");
381
+ console.log("still shows the same QR (a reload rotates the nonce and voids this grant).");
382
+ } finally {
383
+ await p.stop();
384
+ }
385
+ }
386
+
248
387
  async function cmdSendGroup(args) {
249
388
  const [groupName, kind, jsonStr] = args._.slice(1);
250
389
  if (!groupName || !kind) {
@@ -266,7 +405,7 @@ async function cmdSendGroup(args) {
266
405
  let failed = 0;
267
406
  for (const r of results) {
268
407
  if (r.ok) console.log(` ✓ ${r.member} via ${r.via} ${r.envelopeId}`);
269
- else { failed++; console.error(` ✗ ${r.member} ${r.error}`); }
408
+ else { failed++; console.error(` ✗ ${r.member} ${refusalLine(r) ?? r.error}`); }
270
409
  }
271
410
  console.log(`group "${groupName}": ${results.length - failed}/${results.length} delivered`);
272
411
  if (failed) exit(1);
@@ -375,7 +514,7 @@ async function cmdRecv(args) {
375
514
  await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "error", reason: e.message.slice(0, 300), text: `zestro bridge error: ${e.message.slice(0, 300)}` } });
376
515
  }
377
516
  }
378
- } else if (bridge) {
517
+ } else if (bridge && env.kind !== "ack" && env.kind !== "reply") {
379
518
  try {
380
519
  const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
381
520
  console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
@@ -388,6 +527,10 @@ async function cmdRecv(args) {
388
527
  console.error(` → zestro bridge error: ${e.message}`);
389
528
  }
390
529
  }
530
+ } else if (bridge) {
531
+ // ack/reply are receipts for OUR earlier sends — bridging them would
532
+ // inject a Zestro agent turn per receipt. Print-only (line above).
533
+ console.log(` → zestro: not bridged (kind=${env.kind} is a receipt)`);
391
534
  }
392
535
  });
393
536
 
@@ -397,6 +540,198 @@ async function cmdRecv(args) {
397
540
  await new Promise(() => {});
398
541
  }
399
542
 
543
+ async function cmdInbox(args) {
544
+ // One-shot mail check for ANY identity: point --home at the pillar dir that
545
+ // minted the uuaid. Pulls pending envelopes from the carrier (no long-poll),
546
+ // prints them decrypted, reports unread backlog, exits. --notify CMD runs a
547
+ // shell hook when there is mail (cron/launchd-friendly); --mark-read stamps
548
+ // read_at on everything printed.
549
+ args["carrier-only"] = true; // inbox check needs the carrier only — no libp2p listener
550
+ const p = loadPillar(args);
551
+ const fresh = [];
552
+ p.onEnvelope((env) => { fresh.push(env); });
553
+ await p.start();
554
+ // Address book BEFORE the drain: the trust "directory" policy needs known
555
+ // senders loaded, or every fresh envelope counts as a stranger.
556
+ loadFleetAddressBook(fleetPath(args), p);
557
+ const pulled = await p.drainCarrierInbox();
558
+ // fromPublicKey: the sender's verified Ed25519 key (transportSignature —
559
+ // open() binds it to `from`). Lets a drain-and-reply consumer (the fleet
560
+ // courier's auto-ack, a bot) seal back to a sender who is NOT in fleet.json
561
+ // via `pillar send … --to-pubkey <fromPublicKey>`. Additive field.
562
+ for (const env of fresh) {
563
+ console.log(JSON.stringify({ new: true, id: env.id, from: env.sender, fromPublicKey: env.transportSignature?.publicKey ?? null, kind: env.kind, at: env.createdAt, payload: env.payload }));
564
+ }
565
+ // Backlog: rows stored earlier (e.g. by a recv daemon) but never surfaced.
566
+ const { decrypt } = await import("../src/net/envelope.mjs");
567
+ const freshIds = new Set(fresh.map(e => e.id));
568
+ const backlog = (p.mailbox.inboxUnread?.(500) ?? []).filter(r => !freshIds.has(r.envelope_id));
569
+ if (args.unread) {
570
+ for (const row of backlog) {
571
+ const env = JSON.parse(row.envelope_json);
572
+ let payload = null;
573
+ try { payload = decrypt(p.keychain, env); } catch (_e) { payload = { error: "decrypt failed" }; }
574
+ console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, fromPublicKey: env.transportSignature?.publicKey ?? null, kind: env.kind, at: env.createdAt, payload }));
575
+ }
576
+ }
577
+ const qCount = (p.mailbox.inboxQuarantined?.(500) ?? []).length;
578
+ console.log(`inbox: ${fresh.length} new, ${backlog.length} older unread${args.unread ? "" : " (--unread to print)"}${qCount ? `, ${qCount} quarantined (pillar trust quarantine)` : ""}`);
579
+ if (args["mark-read"]) {
580
+ for (const env of fresh) p.mailbox.markInboxRead?.(env.id);
581
+ if (args.unread) for (const row of backlog) p.mailbox.markInboxRead?.(row.envelope_id);
582
+ }
583
+ if (args.notify && typeof args.notify === "string" && (pulled > 0 || backlog.length > 0)) {
584
+ const { spawnSync } = await import("node:child_process");
585
+ const r = spawnSync(args.notify, {
586
+ shell: true,
587
+ input: JSON.stringify({ uuaid: p.uuaid, new: pulled, unread: backlog.length }) + "\n",
588
+ env: { ...process.env, PILLAR_NEW: String(pulled), PILLAR_UNREAD: String(backlog.length), PILLAR_UUAID: p.uuaid },
589
+ stdio: ["pipe", "inherit", "inherit"],
590
+ });
591
+ if (r.status !== 0) console.error(`notify hook exited ${r.status}`);
592
+ }
593
+ await p.stop();
594
+ exit(0);
595
+ }
596
+
597
+ async function cmdTrust(args) {
598
+ // pillar trust show policy + lists + quarantine count
599
+ // pillar trust set <open|directory|allowlist>
600
+ // pillar trust allow|block <uuaid> add to list (removes from the other)
601
+ // pillar trust unlist <uuaid> remove from both lists
602
+ // pillar trust quarantine [--release ID] list quarantined mail / release one
603
+ const home = homePath(args);
604
+ const tPath = join(home, "trust.json");
605
+ const t = loadTrustPolicy(home);
606
+ const sub = args._[1];
607
+ const save = () => writeFileSync(tPath, JSON.stringify({ policy: t.policy, allow: t.allow, block: t.block }, null, 2));
608
+ if (!sub) {
609
+ console.log(`policy: ${t.policy}${t.exists ? "" : " (no trust.json — default)"}`);
610
+ console.log(`allow (${t.allow.length}):`); for (const u of t.allow) console.log(` ${u}`);
611
+ console.log(`block (${t.block.length}):`); for (const u of t.block) console.log(` ${u}`);
612
+ const { Mailbox } = await import("../src/mailbox/mailbox.mjs");
613
+ const mb = new Mailbox(join(home, "mailbox.db"));
614
+ const q = mb.inboxQuarantined(500);
615
+ console.log(`quarantined: ${q.length} (pillar trust quarantine to list)`);
616
+ mb.close();
617
+ return;
618
+ }
619
+ if (sub === "set") {
620
+ const pol = args._[2];
621
+ if (!POLICIES.includes(pol)) { console.error(`usage: pillar trust set <${POLICIES.join("|")}>`); exit(2); }
622
+ t.policy = pol; save();
623
+ console.log(`policy: ${pol} (${tPath})`);
624
+ return;
625
+ }
626
+ if (sub === "allow" || sub === "block") {
627
+ const u = args._[2];
628
+ if (!u?.startsWith("uuaid:")) { console.error(`usage: pillar trust ${sub} <uuaid:...>`); exit(2); }
629
+ t.allow = t.allow.filter(x => x !== u);
630
+ t.block = t.block.filter(x => x !== u);
631
+ t[sub === "allow" ? "allow" : "block"].push(u);
632
+ save();
633
+ console.log(`${sub}: ${u}`);
634
+ return;
635
+ }
636
+ if (sub === "unlist") {
637
+ const u = args._[2];
638
+ t.allow = t.allow.filter(x => x !== u);
639
+ t.block = t.block.filter(x => x !== u);
640
+ save();
641
+ console.log(`unlisted: ${u}`);
642
+ return;
643
+ }
644
+ if (sub === "quarantine") {
645
+ const { Mailbox } = await import("../src/mailbox/mailbox.mjs");
646
+ const mb = new Mailbox(join(home, "mailbox.db"));
647
+ if (args.release) {
648
+ mb.db.prepare("update inbox set quarantined = null where envelope_id = ?").run(args.release);
649
+ console.log(`released: ${args.release} (now visible to pillar inbox --unread)`);
650
+ mb.close();
651
+ return;
652
+ }
653
+ for (const row of mb.inboxQuarantined(500)) {
654
+ console.log(JSON.stringify({ id: row.envelope_id, from: row.sender, reason: row.quarantined, at: row.received_at }));
655
+ }
656
+ mb.close();
657
+ return;
658
+ }
659
+ console.error("usage: pillar trust [set <policy>|allow <uuaid>|block <uuaid>|unlist <uuaid>|quarantine [--release ID]]");
660
+ exit(2);
661
+ }
662
+
663
+ async function cmdTier(args) {
664
+ // pillar tier show installed grant
665
+ // pillar tier issue <uuaid> <tier> [--expires ISO] [--rate N] [--max-body N] [--out FILE]
666
+ // sign a grant with THIS home's key (issuer)
667
+ // pillar tier install <grant.json> install a received grant into this home
668
+ // pillar tier verify <grant.json> --issuer HEX verify signature/expiry locally
669
+ const { makeTierGrant, verifyTierGrant, grantFingerprint, TIER_DEFAULTS } =
670
+ await import("../src/identity/tier.mjs");
671
+ const home = homePath(args);
672
+ const sub = args._[1];
673
+ if (!sub) {
674
+ const tgPath = join(home, "tier-grant.json");
675
+ if (!existsSync(tgPath)) { console.log("no tier grant installed (community tier)"); return; }
676
+ const g = JSON.parse(readFileSync(tgPath, "utf-8"));
677
+ console.log(`tier: ${g.tier} rate: ${g.envelopesPerMin}/min maxBody: ${g.maxBodyBytes}B`);
678
+ console.log(`subject: ${g.uuaid}`);
679
+ console.log(`issuer: ${g.issuer}`);
680
+ console.log(`expires: ${g.expires} fingerprint: ${grantFingerprint(g)}`);
681
+ return;
682
+ }
683
+ if (sub === "issue") {
684
+ const [, , uuaid, tier] = args._;
685
+ if (!uuaid || !TIER_DEFAULTS[tier]) {
686
+ console.error(`usage: pillar tier issue <uuaid:...> <${Object.keys(TIER_DEFAULTS).join("|")}> [--expires ISO] [--rate N] [--max-body BYTES] [--out FILE]`);
687
+ exit(2);
688
+ }
689
+ const kc = new Keychain(join(home, "keychain.json"));
690
+ kc.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
691
+ const expires = args.expires ?? new Date(Date.now() + 365 * 86400_000).toISOString();
692
+ const grant = makeTierGrant(kc, {
693
+ uuaid, tier, expires,
694
+ envelopesPerMin: args.rate ? Number(args.rate) : undefined,
695
+ maxBodyBytes: args["max-body"] ? Number(args["max-body"]) : undefined,
696
+ });
697
+ const out = typeof args.out === "string" ? args.out : `tier-grant-${grantFingerprint(grant)}.json`;
698
+ writeFileSync(out, JSON.stringify(grant, null, 2));
699
+ console.log(`issued ${tier} grant for ${uuaid}`);
700
+ console.log(` rate ${grant.envelopesPerMin}/min, maxBody ${grant.maxBodyBytes}B, expires ${expires}`);
701
+ console.log(` issuer ${grant.issuer}`);
702
+ console.log(` -> ${out} (send to the grantee; they run: pillar tier install ${out})`);
703
+ return;
704
+ }
705
+ if (sub === "install") {
706
+ const file = args._[2];
707
+ if (!file || !existsSync(file)) { console.error("usage: pillar tier install <grant.json>"); exit(2); }
708
+ const g = JSON.parse(readFileSync(file, "utf-8"));
709
+ const kc = new Keychain(join(home, "keychain.json"));
710
+ kc.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
711
+ if (g.uuaid !== kc._identity.uuaid) {
712
+ console.error(`grant subject ${g.uuaid} != this home's identity ${kc._identity.uuaid}`);
713
+ exit(1);
714
+ }
715
+ const v = verifyTierGrant(g, [g.issuer]); // structural check (sig by named issuer, expiry, ceilings)
716
+ if (!v.ok) { console.error(`grant invalid: ${v.reason}`); exit(1); }
717
+ writeFileSync(join(home, "tier-grant.json"), JSON.stringify(g, null, 2));
718
+ console.log(`installed ${g.tier} grant (fingerprint ${grantFingerprint(g)}) — sent with every carrier deliver`);
719
+ console.log(`note: budgets rise only at carriers that trust issuer ${g.issuer.slice(0, 16)}…`);
720
+ return;
721
+ }
722
+ if (sub === "verify") {
723
+ const file = args._[2];
724
+ const issuer = args.issuer;
725
+ if (!file || !existsSync(file) || !issuer) { console.error("usage: pillar tier verify <grant.json> --issuer HEX"); exit(2); }
726
+ const g = JSON.parse(readFileSync(file, "utf-8"));
727
+ const v = verifyTierGrant(g, [String(issuer)]);
728
+ console.log(v.ok ? `VALID: ${g.tier} for ${g.uuaid} until ${g.expires}` : `INVALID: ${v.reason}`);
729
+ exit(v.ok ? 0 : 1);
730
+ }
731
+ console.error("usage: pillar tier [issue|install|verify]");
732
+ exit(2);
733
+ }
734
+
400
735
  async function cmdPeers(args) {
401
736
  const p = loadPillar(args);
402
737
  await p.start();
@@ -416,11 +751,17 @@ async function main() {
416
751
  case "send-group": return cmdSendGroup(args);
417
752
  case "groups": return cmdGroups(args);
418
753
  case "recv": return cmdRecv(args);
754
+ case "inbox": return cmdInbox(args);
755
+ case "trust": return cmdTrust(args);
756
+ case "tier": return cmdTier(args);
419
757
  case "peers": return cmdPeers(args);
758
+ case "delegate": return cmdDelegate(args);
420
759
  default:
421
- console.error(`usage: pillar <init|id|send|send-group|groups|recv|peers> [--home DIR]`);
760
+ console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|trust|tier|peers|delegate> [--home DIR]`);
422
761
  exit(2);
423
762
  }
424
763
  }
425
764
 
426
- main().catch(e => { console.error(e); exit(1); });
765
+ // A carrier or broker refusal prints one line, `<code>: <fix>` (src/net/refusals.mjs);
766
+ // anything else keeps the full error.
767
+ main().catch(e => { console.error(refusalLine(e) ?? e); exit(1); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uuaid/pillar",
3
- "version": "0.2.0-alpha.8",
4
- "description": "IAASO-3050 reference implementation. Every agent runs its own PDP pillar, discovers peers via a Kademlia DHT, and exchanges cryptographically signed + Noise-encrypted messages over libp2p. One command to install and join the network.",
3
+ "version": "2.0.1",
4
+ "description": "Pillar (Protocol for Inter-agent Linked Ledger And Relay) \u2014 the secure delivery layer of UACP, the Universal Agent Communication Protocol. UACP = UUAID (identity) + Envelope (signed, encrypted container) + Pillar (relay, mailbox, routing, store-and-forward). Reference implementation of the UACP delivery layer; includes VDA-1 (IAASO-3901, in ballot) delivery attestation and a blind escrow for the Zilligon ZGN / EVM rails.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "src/index.mjs",
@@ -10,29 +10,52 @@
10
10
  "pillar-carrier": "bin/carrier.mjs",
11
11
  "pillar-ask": "bin/pillar-ask.mjs",
12
12
  "pillar-responder": "bin/pillar-responder.mjs",
13
- "pillar-seed": "bin/pillar-seed.mjs"
13
+ "pillar-seed": "bin/pillar-seed.mjs",
14
+ "pillar-directory": "bin/pillar-directory.mjs",
15
+ "pillar-escrow": "bin/pillar-escrow.mjs",
16
+ "pillar-blob": "bin/pillar-blob.mjs"
14
17
  },
15
18
  "exports": {
16
- ".": "./src/index.mjs"
19
+ ".": "./src/index.mjs",
20
+ "./keychain": "./src/identity/keychain.mjs",
21
+ "./jcs": "./src/identity/jcs.mjs",
22
+ "./tier": "./src/identity/tier.mjs",
23
+ "./envelope": "./src/net/envelope.mjs",
24
+ "./delegation": "./src/net/delegation.mjs",
25
+ "./e2e": "./src/crypto/e2e.mjs",
26
+ "./carrier-client": "./src/net/carrier-client.mjs",
27
+ "./seed-tier": "./src/net/seed-tier.mjs",
28
+ "./seed-registry": "./src/net/seed-registry.mjs",
29
+ "./blob": "./src/blob/blob.mjs",
30
+ "./blob/http": "./src/blob/http.mjs",
31
+ "./blob/store": "./src/blob/store.mjs",
32
+ "./blob/broker": "./src/blob/broker.mjs",
33
+ "./refusals": "./src/net/refusals.mjs",
34
+ "./package.json": "./package.json"
17
35
  },
18
36
  "files": [
19
37
  "src",
20
38
  "bin",
21
39
  "README.md",
22
40
  "INTERACTION-CONVENTION.md",
41
+ "HOSTING-LIMITS.md",
23
42
  "LICENSE"
24
43
  ],
25
44
  "scripts": {
26
45
  "test": "node --test --test-reporter spec test/unit/*.test.mjs",
27
46
  "test:int": "node --test --test-reporter spec test/integration/*.test.mjs",
28
47
  "test:chaos": "node --test --test-reporter spec test/chaos/*.test.mjs",
29
- "test:all": "node --test --test-reporter spec test/unit/*.test.mjs test/integration/*.test.mjs test/chaos/*.test.mjs"
48
+ "test:all": "node --test --test-reporter spec test/unit/*.test.mjs test/integration/*.test.mjs test/chaos/*.test.mjs",
49
+ "preflight": "node scripts/preflight-publish.mjs",
50
+ "prepublishOnly": "node scripts/preflight-publish.mjs",
51
+ "postpublish": "node scripts/verify-publish.mjs"
30
52
  },
31
53
  "engines": {
32
- "node": ">=20"
54
+ "node": ">=22"
33
55
  },
34
56
  "dependencies": {
35
57
  "@chainsafe/libp2p-noise": "^17.0.0",
58
+ "@noble/hashes": "^2.3.0",
36
59
  "@chainsafe/libp2p-yamux": "^8.0.1",
37
60
  "@libp2p/autonat": "^3.0.26",
38
61
  "@libp2p/bootstrap": "^12.0.29",
@@ -48,7 +71,7 @@
48
71
  "@libp2p/websockets": "^10.1.19",
49
72
  "@multiformats/multiaddr": "^13.0.3",
50
73
  "@noble/curves": "^2.3.0",
51
- "better-sqlite3": "^11.10.0",
74
+ "better-sqlite3": "^12.11.1",
52
75
  "it-pipe": "^3.0.1",
53
76
  "it-pushable": "^3.2.4",
54
77
  "libp2p": "^3.3.8",