@uuaid/pillar 0.2.0-alpha.10 → 0.2.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/bin/carrier.mjs +6 -1
- package/bin/pillar-escrow.mjs +674 -0
- package/bin/pillar-responder.mjs +46 -1
- package/bin/pillar.mjs +164 -2
- package/package.json +13 -3
- package/src/escrow/box.mjs +43 -0
- package/src/escrow/escrow-service.mjs +538 -0
- package/src/escrow/rails/evm-rail.mjs +427 -0
- package/src/escrow/rails/index.mjs +41 -0
- package/src/escrow/rails/zilligon-rail.mjs +73 -0
- package/src/escrow/terms.mjs +368 -0
- package/src/escrow/token-registry.mjs +231 -0
- package/src/escrow/verify.mjs +119 -0
- package/src/escrow/zilligon.mjs +266 -0
- package/src/identity/tier.mjs +85 -0
- package/src/identity/trust.mjs +63 -0
- package/src/mailbox/mailbox.mjs +33 -2
- package/src/net/carrier-client.mjs +14 -4
- package/src/net/carrier-server.mjs +44 -6
- package/src/observability.mjs +141 -0
- package/src/pillar.mjs +80 -11
- package/src/vda/core.mjs +290 -0
- package/src/vda/directory.mjs +87 -0
- package/src/vda/instruments.mjs +246 -0
- package/src/vda/shims.mjs +77 -0
package/bin/pillar-responder.mjs
CHANGED
|
@@ -38,11 +38,16 @@ import { argv, exit } from "node:process";
|
|
|
38
38
|
import { spawn } from "node:child_process";
|
|
39
39
|
import { Keychain } from "../src/identity/keychain.mjs";
|
|
40
40
|
import { Pillar } from "../src/pillar.mjs";
|
|
41
|
+
import { loadTrustPolicy, evaluateTrust, POLICIES } from "../src/identity/trust.mjs";
|
|
42
|
+
import { loadFleetAddressBook } from "../src/address-book.mjs";
|
|
41
43
|
|
|
42
44
|
const DEFAULT_REPLY = "pillar-responder: request acked — owner will respond through the normal channel";
|
|
45
|
+
const STRANGER_REPLY = "pillar-responder: request received. This responder runs its command hook only for enrolled senders; ask the operator to `pillar trust allow` your uuaid.";
|
|
43
46
|
const MAX_REPLY_BYTES = 8192;
|
|
44
47
|
const HOOK_TIMEOUT_MS = 60_000;
|
|
45
48
|
const REPLAY_WINDOW_MS = 24 * 3600_000;
|
|
49
|
+
const SENDER_REPLIES_PER_HOUR = 30; // amplification cap: max hook/reply cycles per sender
|
|
50
|
+
const RATE_WINDOW_MS = 3600_000;
|
|
46
51
|
|
|
47
52
|
function parseArgs(argv) {
|
|
48
53
|
const args = { _: [] };
|
|
@@ -162,6 +167,26 @@ if (!carriers.length && !enableLibp2p) {
|
|
|
162
167
|
|
|
163
168
|
const replyText = args["reply-text"] ?? rcfg.replyText ?? DEFAULT_REPLY;
|
|
164
169
|
const hookCmd = args.command ?? rcfg.command ?? null;
|
|
170
|
+
// Trust gate. Default policy for the COMMAND HOOK is "directory": strangers
|
|
171
|
+
// still get ack + a fixed reply (universality preserved — first contact
|
|
172
|
+
// works), but attacker-controlled JSON only reaches the hook process from
|
|
173
|
+
// senders the operator knows. --hook-policy open restores old behavior.
|
|
174
|
+
const trust = loadTrustPolicy(home);
|
|
175
|
+
const hookPolicy = args["hook-policy"] ?? rcfg.hookPolicy ?? "directory";
|
|
176
|
+
if (!POLICIES.includes(hookPolicy)) {
|
|
177
|
+
console.error(`--hook-policy must be one of ${POLICIES.join("|")}`);
|
|
178
|
+
exit(2);
|
|
179
|
+
}
|
|
180
|
+
const senderRate = new Map(); // sender uuaid -> [ms timestamps]
|
|
181
|
+
function senderAllowed(sender) {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
let arr = senderRate.get(sender);
|
|
184
|
+
if (!arr) { arr = []; senderRate.set(sender, arr); }
|
|
185
|
+
while (arr.length && now - arr[0] > RATE_WINDOW_MS) arr.shift();
|
|
186
|
+
if (arr.length >= SENDER_REPLIES_PER_HOUR) return false;
|
|
187
|
+
arr.push(now);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
165
190
|
const hookTimeoutMs = Number(args["timeout-ms"] ?? rcfg.timeoutMs ?? HOOK_TIMEOUT_MS);
|
|
166
191
|
const hookMaxBytes = Number(args["max-reply-bytes"] ?? MAX_REPLY_BYTES);
|
|
167
192
|
const fwd = args["forward-to"] || rcfg.forwardTo?.uuaid
|
|
@@ -187,10 +212,14 @@ const p = new Pillar({
|
|
|
187
212
|
|
|
188
213
|
await p.start();
|
|
189
214
|
|
|
215
|
+
// Address book feeds the "directory" hook policy (known senders = fleet.json).
|
|
216
|
+
const abCount = loadFleetAddressBook(join(home, "fleet.json"), p);
|
|
217
|
+
|
|
190
218
|
console.log(`pillar-responder up — ${p.uuaid}`);
|
|
191
219
|
console.log(` pubkey: ${p.publicKeyHex}`);
|
|
192
220
|
console.log(` carriers: ${carriers.length ? carriers.join(", ") : "(none — libp2p only)"}`);
|
|
193
221
|
console.log(` reply: ${hookCmd ? `hook ${hookCmd}` : "builtin text"}`);
|
|
222
|
+
if (hookCmd) console.log(` hook gate: policy=${hookPolicy}, known senders=${abCount}, cap=${SENDER_REPLIES_PER_HOUR}/h per sender`);
|
|
194
223
|
if (fwd) console.log(` forwardTo: ${fwd.uuaid}`);
|
|
195
224
|
|
|
196
225
|
// Replay journaled-but-unfinished requests first (at-least-once).
|
|
@@ -215,7 +244,21 @@ p.onEnvelope(async (env) => {
|
|
|
215
244
|
const thread = env.payload?.thread ?? env.id;
|
|
216
245
|
let status = "ok";
|
|
217
246
|
let text = replyText;
|
|
218
|
-
|
|
247
|
+
// Amplification cap: over-budget senders get ack only, no reply/hook.
|
|
248
|
+
if (!senderAllowed(env.sender)) {
|
|
249
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
250
|
+
journalDone(home, env.id, { text: "(rate-capped: ack only)", status: "capped" });
|
|
251
|
+
console.log(` ${env.sender.slice(-8)} request ${env.id.slice(0, 16)}… → ack only (rate cap)`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// Hook gate: strangers get the fixed reply, never the command hook.
|
|
255
|
+
const gate = evaluateTrust({ ...trust, policy: hookPolicy }, env.sender, new Set(p.addressBook?.keys() ?? []));
|
|
256
|
+
if (gate.reason === "blocked") {
|
|
257
|
+
journalDone(home, env.id, { text: "(blocked sender: dropped)", status: "blocked" });
|
|
258
|
+
console.log(` ${env.sender.slice(-8)} request ${env.id.slice(0, 16)}… → dropped (blocked)`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (hookCmd && gate.deliver) {
|
|
219
262
|
const r = await runHook(hookCmd, {
|
|
220
263
|
id: env.id,
|
|
221
264
|
sender: env.sender,
|
|
@@ -229,6 +272,8 @@ p.onEnvelope(async (env) => {
|
|
|
229
272
|
status = "error";
|
|
230
273
|
text = r.reason;
|
|
231
274
|
}
|
|
275
|
+
} else if (hookCmd && !gate.deliver) {
|
|
276
|
+
text = STRANGER_REPLY; // stranger: fixed reply, hook never sees the payload
|
|
232
277
|
}
|
|
233
278
|
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
234
279
|
const replyPayload = { thread, inReplyTo: env.id, status, text };
|
package/bin/pillar.mjs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* pillar recv [--home ~/.pillar] [--follow] listen for envelopes and print them
|
|
10
10
|
* pillar inbox [--home ~/.pillar] [--unread] [--notify CMD] [--mark-read]
|
|
11
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
|
|
12
14
|
* pillar peers list currently connected peers
|
|
13
15
|
*
|
|
14
16
|
* All commands respect --home to point at an alternate pillar directory,
|
|
@@ -21,6 +23,7 @@ import { argv, exit } from "node:process";
|
|
|
21
23
|
import { Keychain } from "../src/identity/keychain.mjs";
|
|
22
24
|
import { Pillar } from "../src/pillar.mjs";
|
|
23
25
|
import { loadFleetAddressBook } from "../src/address-book.mjs";
|
|
26
|
+
import { loadTrustPolicy, POLICIES } from "../src/identity/trust.mjs";
|
|
24
27
|
|
|
25
28
|
function parseArgs(argv) {
|
|
26
29
|
const args = { _: [] };
|
|
@@ -60,6 +63,19 @@ function loadPillar(args) {
|
|
|
60
63
|
const carriers = args.carrier
|
|
61
64
|
? [args.carrier]
|
|
62
65
|
: (process.env.PILLAR_CARRIERS ? process.env.PILLAR_CARRIERS.split(",") : (cfg.carriers ?? []));
|
|
66
|
+
// Trust gate: --policy overrides trust.json's policy for this invocation.
|
|
67
|
+
const trust = loadTrustPolicy(home);
|
|
68
|
+
if (args.policy) {
|
|
69
|
+
if (!POLICIES.includes(args.policy)) {
|
|
70
|
+
console.error(`--policy must be one of ${POLICIES.join("|")}`);
|
|
71
|
+
exit(2);
|
|
72
|
+
}
|
|
73
|
+
trust.policy = args.policy;
|
|
74
|
+
}
|
|
75
|
+
// Tier grant: <home>/tier-grant.json (issued via `pillar tier issue`) is
|
|
76
|
+
// attached to carrier delivers — raises budgets at carriers trusting the issuer.
|
|
77
|
+
const tgPath = join(home, "tier-grant.json");
|
|
78
|
+
const tierGrant = existsSync(tgPath) ? JSON.parse(readFileSync(tgPath, "utf-8")) : null;
|
|
63
79
|
return new Pillar({
|
|
64
80
|
keychain: kc,
|
|
65
81
|
mailboxPath: mbPath,
|
|
@@ -69,6 +85,8 @@ function loadPillar(args) {
|
|
|
69
85
|
dhtClientMode: cfg.dhtClientMode ?? true,
|
|
70
86
|
carriers,
|
|
71
87
|
enableLibp2p: !(args["carrier-only"] || cfg.carrierOnly === true),
|
|
88
|
+
trust,
|
|
89
|
+
tierGrant,
|
|
72
90
|
});
|
|
73
91
|
}
|
|
74
92
|
|
|
@@ -414,6 +432,9 @@ async function cmdInbox(args) {
|
|
|
414
432
|
const fresh = [];
|
|
415
433
|
p.onEnvelope((env) => { fresh.push(env); });
|
|
416
434
|
await p.start();
|
|
435
|
+
// Address book BEFORE the drain: the trust "directory" policy needs known
|
|
436
|
+
// senders loaded, or every fresh envelope counts as a stranger.
|
|
437
|
+
loadFleetAddressBook(fleetPath(args), p);
|
|
417
438
|
const pulled = await p.drainCarrierInbox();
|
|
418
439
|
for (const env of fresh) {
|
|
419
440
|
console.log(JSON.stringify({ new: true, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload: env.payload }));
|
|
@@ -430,7 +451,8 @@ async function cmdInbox(args) {
|
|
|
430
451
|
console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload }));
|
|
431
452
|
}
|
|
432
453
|
}
|
|
433
|
-
|
|
454
|
+
const qCount = (p.mailbox.inboxQuarantined?.(500) ?? []).length;
|
|
455
|
+
console.log(`inbox: ${fresh.length} new, ${backlog.length} older unread${args.unread ? "" : " (--unread to print)"}${qCount ? `, ${qCount} quarantined (pillar trust quarantine)` : ""}`);
|
|
434
456
|
if (args["mark-read"]) {
|
|
435
457
|
for (const env of fresh) p.mailbox.markInboxRead?.(env.id);
|
|
436
458
|
if (args.unread) for (const row of backlog) p.mailbox.markInboxRead?.(row.envelope_id);
|
|
@@ -449,6 +471,144 @@ async function cmdInbox(args) {
|
|
|
449
471
|
exit(0);
|
|
450
472
|
}
|
|
451
473
|
|
|
474
|
+
async function cmdTrust(args) {
|
|
475
|
+
// pillar trust show policy + lists + quarantine count
|
|
476
|
+
// pillar trust set <open|directory|allowlist>
|
|
477
|
+
// pillar trust allow|block <uuaid> add to list (removes from the other)
|
|
478
|
+
// pillar trust unlist <uuaid> remove from both lists
|
|
479
|
+
// pillar trust quarantine [--release ID] list quarantined mail / release one
|
|
480
|
+
const home = homePath(args);
|
|
481
|
+
const tPath = join(home, "trust.json");
|
|
482
|
+
const t = loadTrustPolicy(home);
|
|
483
|
+
const sub = args._[1];
|
|
484
|
+
const save = () => writeFileSync(tPath, JSON.stringify({ policy: t.policy, allow: t.allow, block: t.block }, null, 2));
|
|
485
|
+
if (!sub) {
|
|
486
|
+
console.log(`policy: ${t.policy}${t.exists ? "" : " (no trust.json — default)"}`);
|
|
487
|
+
console.log(`allow (${t.allow.length}):`); for (const u of t.allow) console.log(` ${u}`);
|
|
488
|
+
console.log(`block (${t.block.length}):`); for (const u of t.block) console.log(` ${u}`);
|
|
489
|
+
const { Mailbox } = await import("../src/mailbox/mailbox.mjs");
|
|
490
|
+
const mb = new Mailbox(join(home, "mailbox.db"));
|
|
491
|
+
const q = mb.inboxQuarantined(500);
|
|
492
|
+
console.log(`quarantined: ${q.length} (pillar trust quarantine to list)`);
|
|
493
|
+
mb.close();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (sub === "set") {
|
|
497
|
+
const pol = args._[2];
|
|
498
|
+
if (!POLICIES.includes(pol)) { console.error(`usage: pillar trust set <${POLICIES.join("|")}>`); exit(2); }
|
|
499
|
+
t.policy = pol; save();
|
|
500
|
+
console.log(`policy: ${pol} (${tPath})`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (sub === "allow" || sub === "block") {
|
|
504
|
+
const u = args._[2];
|
|
505
|
+
if (!u?.startsWith("uuaid:")) { console.error(`usage: pillar trust ${sub} <uuaid:...>`); exit(2); }
|
|
506
|
+
t.allow = t.allow.filter(x => x !== u);
|
|
507
|
+
t.block = t.block.filter(x => x !== u);
|
|
508
|
+
t[sub === "allow" ? "allow" : "block"].push(u);
|
|
509
|
+
save();
|
|
510
|
+
console.log(`${sub}: ${u}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (sub === "unlist") {
|
|
514
|
+
const u = args._[2];
|
|
515
|
+
t.allow = t.allow.filter(x => x !== u);
|
|
516
|
+
t.block = t.block.filter(x => x !== u);
|
|
517
|
+
save();
|
|
518
|
+
console.log(`unlisted: ${u}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (sub === "quarantine") {
|
|
522
|
+
const { Mailbox } = await import("../src/mailbox/mailbox.mjs");
|
|
523
|
+
const mb = new Mailbox(join(home, "mailbox.db"));
|
|
524
|
+
if (args.release) {
|
|
525
|
+
mb.db.prepare("update inbox set quarantined = null where envelope_id = ?").run(args.release);
|
|
526
|
+
console.log(`released: ${args.release} (now visible to pillar inbox --unread)`);
|
|
527
|
+
mb.close();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
for (const row of mb.inboxQuarantined(500)) {
|
|
531
|
+
console.log(JSON.stringify({ id: row.envelope_id, from: row.sender, reason: row.quarantined, at: row.received_at }));
|
|
532
|
+
}
|
|
533
|
+
mb.close();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
console.error("usage: pillar trust [set <policy>|allow <uuaid>|block <uuaid>|unlist <uuaid>|quarantine [--release ID]]");
|
|
537
|
+
exit(2);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function cmdTier(args) {
|
|
541
|
+
// pillar tier show installed grant
|
|
542
|
+
// pillar tier issue <uuaid> <tier> [--expires ISO] [--rate N] [--max-body N] [--out FILE]
|
|
543
|
+
// sign a grant with THIS home's key (issuer)
|
|
544
|
+
// pillar tier install <grant.json> install a received grant into this home
|
|
545
|
+
// pillar tier verify <grant.json> --issuer HEX verify signature/expiry locally
|
|
546
|
+
const { makeTierGrant, verifyTierGrant, grantFingerprint, TIER_DEFAULTS } =
|
|
547
|
+
await import("../src/identity/tier.mjs");
|
|
548
|
+
const home = homePath(args);
|
|
549
|
+
const sub = args._[1];
|
|
550
|
+
if (!sub) {
|
|
551
|
+
const tgPath = join(home, "tier-grant.json");
|
|
552
|
+
if (!existsSync(tgPath)) { console.log("no tier grant installed (community tier)"); return; }
|
|
553
|
+
const g = JSON.parse(readFileSync(tgPath, "utf-8"));
|
|
554
|
+
console.log(`tier: ${g.tier} rate: ${g.envelopesPerMin}/min maxBody: ${g.maxBodyBytes}B`);
|
|
555
|
+
console.log(`subject: ${g.uuaid}`);
|
|
556
|
+
console.log(`issuer: ${g.issuer}`);
|
|
557
|
+
console.log(`expires: ${g.expires} fingerprint: ${grantFingerprint(g)}`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (sub === "issue") {
|
|
561
|
+
const [, , uuaid, tier] = args._;
|
|
562
|
+
if (!uuaid || !TIER_DEFAULTS[tier]) {
|
|
563
|
+
console.error(`usage: pillar tier issue <uuaid:...> <${Object.keys(TIER_DEFAULTS).join("|")}> [--expires ISO] [--rate N] [--max-body BYTES] [--out FILE]`);
|
|
564
|
+
exit(2);
|
|
565
|
+
}
|
|
566
|
+
const kc = new Keychain(join(home, "keychain.json"));
|
|
567
|
+
kc.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
|
|
568
|
+
const expires = args.expires ?? new Date(Date.now() + 365 * 86400_000).toISOString();
|
|
569
|
+
const grant = makeTierGrant(kc, {
|
|
570
|
+
uuaid, tier, expires,
|
|
571
|
+
envelopesPerMin: args.rate ? Number(args.rate) : undefined,
|
|
572
|
+
maxBodyBytes: args["max-body"] ? Number(args["max-body"]) : undefined,
|
|
573
|
+
});
|
|
574
|
+
const out = typeof args.out === "string" ? args.out : `tier-grant-${grantFingerprint(grant)}.json`;
|
|
575
|
+
writeFileSync(out, JSON.stringify(grant, null, 2));
|
|
576
|
+
console.log(`issued ${tier} grant for ${uuaid}`);
|
|
577
|
+
console.log(` rate ${grant.envelopesPerMin}/min, maxBody ${grant.maxBodyBytes}B, expires ${expires}`);
|
|
578
|
+
console.log(` issuer ${grant.issuer}`);
|
|
579
|
+
console.log(` -> ${out} (send to the grantee; they run: pillar tier install ${out})`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (sub === "install") {
|
|
583
|
+
const file = args._[2];
|
|
584
|
+
if (!file || !existsSync(file)) { console.error("usage: pillar tier install <grant.json>"); exit(2); }
|
|
585
|
+
const g = JSON.parse(readFileSync(file, "utf-8"));
|
|
586
|
+
const kc = new Keychain(join(home, "keychain.json"));
|
|
587
|
+
kc.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
|
|
588
|
+
if (g.uuaid !== kc._identity.uuaid) {
|
|
589
|
+
console.error(`grant subject ${g.uuaid} != this home's identity ${kc._identity.uuaid}`);
|
|
590
|
+
exit(1);
|
|
591
|
+
}
|
|
592
|
+
const v = verifyTierGrant(g, [g.issuer]); // structural check (sig by named issuer, expiry, ceilings)
|
|
593
|
+
if (!v.ok) { console.error(`grant invalid: ${v.reason}`); exit(1); }
|
|
594
|
+
writeFileSync(join(home, "tier-grant.json"), JSON.stringify(g, null, 2));
|
|
595
|
+
console.log(`installed ${g.tier} grant (fingerprint ${grantFingerprint(g)}) — sent with every carrier deliver`);
|
|
596
|
+
console.log(`note: budgets rise only at carriers that trust issuer ${g.issuer.slice(0, 16)}…`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (sub === "verify") {
|
|
600
|
+
const file = args._[2];
|
|
601
|
+
const issuer = args.issuer;
|
|
602
|
+
if (!file || !existsSync(file) || !issuer) { console.error("usage: pillar tier verify <grant.json> --issuer HEX"); exit(2); }
|
|
603
|
+
const g = JSON.parse(readFileSync(file, "utf-8"));
|
|
604
|
+
const v = verifyTierGrant(g, [String(issuer)]);
|
|
605
|
+
console.log(v.ok ? `VALID: ${g.tier} for ${g.uuaid} until ${g.expires}` : `INVALID: ${v.reason}`);
|
|
606
|
+
exit(v.ok ? 0 : 1);
|
|
607
|
+
}
|
|
608
|
+
console.error("usage: pillar tier [issue|install|verify]");
|
|
609
|
+
exit(2);
|
|
610
|
+
}
|
|
611
|
+
|
|
452
612
|
async function cmdPeers(args) {
|
|
453
613
|
const p = loadPillar(args);
|
|
454
614
|
await p.start();
|
|
@@ -469,9 +629,11 @@ async function main() {
|
|
|
469
629
|
case "groups": return cmdGroups(args);
|
|
470
630
|
case "recv": return cmdRecv(args);
|
|
471
631
|
case "inbox": return cmdInbox(args);
|
|
632
|
+
case "trust": return cmdTrust(args);
|
|
633
|
+
case "tier": return cmdTier(args);
|
|
472
634
|
case "peers": return cmdPeers(args);
|
|
473
635
|
default:
|
|
474
|
-
console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|peers> [--home DIR]`);
|
|
636
|
+
console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|trust|tier|peers> [--home DIR]`);
|
|
475
637
|
exit(2);
|
|
476
638
|
}
|
|
477
639
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuaid/pillar",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.12",
|
|
4
4
|
"description": "Pillar (Protocol for Inter-agent Linked Ledger And Relay) — 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). IAASO-3050 reference implementation: one command to mint an identity and join the network.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,10 +10,18 @@
|
|
|
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-escrow": "bin/pillar-escrow.mjs"
|
|
14
15
|
},
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
+
".": "./src/index.mjs",
|
|
18
|
+
"./keychain": "./src/identity/keychain.mjs",
|
|
19
|
+
"./jcs": "./src/identity/jcs.mjs",
|
|
20
|
+
"./tier": "./src/identity/tier.mjs",
|
|
21
|
+
"./envelope": "./src/net/envelope.mjs",
|
|
22
|
+
"./e2e": "./src/crypto/e2e.mjs",
|
|
23
|
+
"./carrier-client":"./src/net/carrier-client.mjs",
|
|
24
|
+
"./package.json": "./package.json"
|
|
17
25
|
},
|
|
18
26
|
"files": [
|
|
19
27
|
"src",
|
|
@@ -33,6 +41,8 @@
|
|
|
33
41
|
},
|
|
34
42
|
"dependencies": {
|
|
35
43
|
"@chainsafe/libp2p-noise": "^17.0.0",
|
|
44
|
+
"@noble/curves": "^2.3.0",
|
|
45
|
+
"@noble/hashes": "^2.3.0",
|
|
36
46
|
"@chainsafe/libp2p-yamux": "^8.0.1",
|
|
37
47
|
"@libp2p/autonat": "^3.0.26",
|
|
38
48
|
"@libp2p/bootstrap": "^12.0.29",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content box — symmetric AES-256-GCM under the escrowed 32-byte content
|
|
3
|
+
* key K. The seller seals the deliverable with K and sends the box directly
|
|
4
|
+
* to the buyer; the escrow holds only K (and the sha256 of the plaintext, in
|
|
5
|
+
* the signed terms) — it never sees the payload. The buyer can only open
|
|
6
|
+
* the box after K is released, and the GCM tag makes any tamper or wrong key
|
|
7
|
+
* fail loudly.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes, createCipheriv, createDecipheriv, createHash } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
export const BOX_ALG = "aes-256-gcm";
|
|
12
|
+
|
|
13
|
+
export function sealBox(key32, plaintext) {
|
|
14
|
+
if (!Buffer.isBuffer(key32) || key32.length !== 32) throw new Error("sealBox: key must be 32 bytes");
|
|
15
|
+
const iv = randomBytes(12);
|
|
16
|
+
const c = createCipheriv(BOX_ALG, key32, iv);
|
|
17
|
+
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
18
|
+
return {
|
|
19
|
+
alg: BOX_ALG,
|
|
20
|
+
iv: iv.toString("hex"),
|
|
21
|
+
ct: Buffer.concat([ct, c.getAuthTag()]).toString("hex"), // tag appended to ct
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Throws on wrong key, tamper, or malformed box — never returns garbage. */
|
|
26
|
+
export function openBox(key32, box) {
|
|
27
|
+
if (!Buffer.isBuffer(key32) || key32.length !== 32) throw new Error("openBox: key must be 32 bytes");
|
|
28
|
+
if (!box || box.alg !== BOX_ALG || typeof box.iv !== "string" || typeof box.ct !== "string") {
|
|
29
|
+
throw new Error("openBox: bad box");
|
|
30
|
+
}
|
|
31
|
+
const iv = Buffer.from(box.iv, "hex");
|
|
32
|
+
const buf = Buffer.from(box.ct, "hex");
|
|
33
|
+
if (iv.length !== 12 || buf.length < 16) throw new Error("openBox: bad lengths");
|
|
34
|
+
const ct = buf.subarray(0, buf.length - 16);
|
|
35
|
+
const tag = buf.subarray(buf.length - 16);
|
|
36
|
+
const d = createDecipheriv(BOX_ALG, key32, iv);
|
|
37
|
+
d.setAuthTag(tag);
|
|
38
|
+
return Buffer.concat([d.update(ct), d.final()]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sha256Hex(buf) {
|
|
42
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
43
|
+
}
|