@uuaid/pillar 0.2.0-alpha.9 → 2.0.2
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/HOSTING-LIMITS.md +432 -0
- package/INTERACTION-CONVENTION.md +222 -1
- package/README.md +358 -17
- package/bin/carrier.mjs +7 -1
- package/bin/pillar-ask.mjs +1 -0
- package/bin/pillar-blob.mjs +139 -0
- package/bin/pillar-directory.mjs +232 -0
- package/bin/pillar-escrow.mjs +675 -0
- package/bin/pillar-responder.mjs +49 -3
- package/bin/pillar-seed.mjs +113 -13
- package/bin/pillar.mjs +299 -7
- package/package.json +30 -7
- package/src/blob/blob.mjs +510 -0
- package/src/blob/broker.mjs +129 -0
- package/src/blob/http.mjs +334 -0
- package/src/blob/store.mjs +55 -0
- 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/keychain.mjs +27 -10
- package/src/identity/tier.mjs +130 -0
- package/src/identity/trust.mjs +63 -0
- package/src/index.mjs +28 -2
- package/src/mailbox/mailbox.mjs +33 -2
- package/src/net/carrier-client.mjs +93 -14
- package/src/net/carrier-server.mjs +212 -15
- package/src/net/delegation.mjs +209 -0
- package/src/net/envelope.mjs +81 -0
- package/src/net/link-offer.mjs +107 -0
- package/src/net/refusals.mjs +126 -0
- package/src/net/seed-registry.mjs +467 -0
- package/src/net/seed-tier.mjs +185 -0
- package/src/node-check.mjs +32 -0
- package/src/observability.mjs +141 -0
- package/src/pillar.mjs +180 -17
- 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
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
* Carrier-only by default (outbound HTTPS only — runs on any machine or
|
|
28
28
|
* webserver; no inbound ports, no libp2p). Pass --libp2p to also listen.
|
|
29
29
|
*
|
|
30
|
-
* pillar-responder --home ~/.pillar-
|
|
31
|
-
* --reply-text "
|
|
30
|
+
* pillar-responder --home ~/.pillar-responder \
|
|
31
|
+
* --reply-text "my-agent is away — left a message" \
|
|
32
32
|
* --forward-to uuaid:foundation:agent:… --forward-pubkey <hex>
|
|
33
33
|
*/
|
|
34
|
+
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
34
35
|
import { existsSync, readFileSync, appendFileSync } from "node:fs";
|
|
35
36
|
import { join } from "node:path";
|
|
36
37
|
import { homedir } from "node:os";
|
|
@@ -38,11 +39,16 @@ import { argv, exit } from "node:process";
|
|
|
38
39
|
import { spawn } from "node:child_process";
|
|
39
40
|
import { Keychain } from "../src/identity/keychain.mjs";
|
|
40
41
|
import { Pillar } from "../src/pillar.mjs";
|
|
42
|
+
import { loadTrustPolicy, evaluateTrust, POLICIES } from "../src/identity/trust.mjs";
|
|
43
|
+
import { loadFleetAddressBook } from "../src/address-book.mjs";
|
|
41
44
|
|
|
42
45
|
const DEFAULT_REPLY = "pillar-responder: request acked — owner will respond through the normal channel";
|
|
46
|
+
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
47
|
const MAX_REPLY_BYTES = 8192;
|
|
44
48
|
const HOOK_TIMEOUT_MS = 60_000;
|
|
45
49
|
const REPLAY_WINDOW_MS = 24 * 3600_000;
|
|
50
|
+
const SENDER_REPLIES_PER_HOUR = 30; // amplification cap: max hook/reply cycles per sender
|
|
51
|
+
const RATE_WINDOW_MS = 3600_000;
|
|
46
52
|
|
|
47
53
|
function parseArgs(argv) {
|
|
48
54
|
const args = { _: [] };
|
|
@@ -162,6 +168,26 @@ if (!carriers.length && !enableLibp2p) {
|
|
|
162
168
|
|
|
163
169
|
const replyText = args["reply-text"] ?? rcfg.replyText ?? DEFAULT_REPLY;
|
|
164
170
|
const hookCmd = args.command ?? rcfg.command ?? null;
|
|
171
|
+
// Trust gate. Default policy for the COMMAND HOOK is "directory": strangers
|
|
172
|
+
// still get ack + a fixed reply (universality preserved — first contact
|
|
173
|
+
// works), but attacker-controlled JSON only reaches the hook process from
|
|
174
|
+
// senders the operator knows. --hook-policy open restores old behavior.
|
|
175
|
+
const trust = loadTrustPolicy(home);
|
|
176
|
+
const hookPolicy = args["hook-policy"] ?? rcfg.hookPolicy ?? "directory";
|
|
177
|
+
if (!POLICIES.includes(hookPolicy)) {
|
|
178
|
+
console.error(`--hook-policy must be one of ${POLICIES.join("|")}`);
|
|
179
|
+
exit(2);
|
|
180
|
+
}
|
|
181
|
+
const senderRate = new Map(); // sender uuaid -> [ms timestamps]
|
|
182
|
+
function senderAllowed(sender) {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
let arr = senderRate.get(sender);
|
|
185
|
+
if (!arr) { arr = []; senderRate.set(sender, arr); }
|
|
186
|
+
while (arr.length && now - arr[0] > RATE_WINDOW_MS) arr.shift();
|
|
187
|
+
if (arr.length >= SENDER_REPLIES_PER_HOUR) return false;
|
|
188
|
+
arr.push(now);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
165
191
|
const hookTimeoutMs = Number(args["timeout-ms"] ?? rcfg.timeoutMs ?? HOOK_TIMEOUT_MS);
|
|
166
192
|
const hookMaxBytes = Number(args["max-reply-bytes"] ?? MAX_REPLY_BYTES);
|
|
167
193
|
const fwd = args["forward-to"] || rcfg.forwardTo?.uuaid
|
|
@@ -187,10 +213,14 @@ const p = new Pillar({
|
|
|
187
213
|
|
|
188
214
|
await p.start();
|
|
189
215
|
|
|
216
|
+
// Address book feeds the "directory" hook policy (known senders = fleet.json).
|
|
217
|
+
const abCount = loadFleetAddressBook(join(home, "fleet.json"), p);
|
|
218
|
+
|
|
190
219
|
console.log(`pillar-responder up — ${p.uuaid}`);
|
|
191
220
|
console.log(` pubkey: ${p.publicKeyHex}`);
|
|
192
221
|
console.log(` carriers: ${carriers.length ? carriers.join(", ") : "(none — libp2p only)"}`);
|
|
193
222
|
console.log(` reply: ${hookCmd ? `hook ${hookCmd}` : "builtin text"}`);
|
|
223
|
+
if (hookCmd) console.log(` hook gate: policy=${hookPolicy}, known senders=${abCount}, cap=${SENDER_REPLIES_PER_HOUR}/h per sender`);
|
|
194
224
|
if (fwd) console.log(` forwardTo: ${fwd.uuaid}`);
|
|
195
225
|
|
|
196
226
|
// Replay journaled-but-unfinished requests first (at-least-once).
|
|
@@ -215,7 +245,21 @@ p.onEnvelope(async (env) => {
|
|
|
215
245
|
const thread = env.payload?.thread ?? env.id;
|
|
216
246
|
let status = "ok";
|
|
217
247
|
let text = replyText;
|
|
218
|
-
|
|
248
|
+
// Amplification cap: over-budget senders get ack only, no reply/hook.
|
|
249
|
+
if (!senderAllowed(env.sender)) {
|
|
250
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
251
|
+
journalDone(home, env.id, { text: "(rate-capped: ack only)", status: "capped" });
|
|
252
|
+
console.log(` ${env.sender.slice(-8)} request ${env.id.slice(0, 16)}… → ack only (rate cap)`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
// Hook gate: strangers get the fixed reply, never the command hook.
|
|
256
|
+
const gate = evaluateTrust({ ...trust, policy: hookPolicy }, env.sender, new Set(p.addressBook?.keys() ?? []));
|
|
257
|
+
if (gate.reason === "blocked") {
|
|
258
|
+
journalDone(home, env.id, { text: "(blocked sender: dropped)", status: "blocked" });
|
|
259
|
+
console.log(` ${env.sender.slice(-8)} request ${env.id.slice(0, 16)}… → dropped (blocked)`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (hookCmd && gate.deliver) {
|
|
219
263
|
const r = await runHook(hookCmd, {
|
|
220
264
|
id: env.id,
|
|
221
265
|
sender: env.sender,
|
|
@@ -229,6 +273,8 @@ p.onEnvelope(async (env) => {
|
|
|
229
273
|
status = "error";
|
|
230
274
|
text = r.reason;
|
|
231
275
|
}
|
|
276
|
+
} else if (hookCmd && !gate.deliver) {
|
|
277
|
+
text = STRANGER_REPLY; // stranger: fixed reply, hook never sees the payload
|
|
232
278
|
}
|
|
233
279
|
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
234
280
|
const replyPayload = { thread, inReplyTo: env.id, status, text };
|
package/bin/pillar-seed.mjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* the p2p store-and-forward mesh, like a LimeWire/BitTorrent seed peer.
|
|
5
5
|
*
|
|
6
6
|
* pillar-seed --port 8799 --db /opt/pillar-seed/seed.db
|
|
7
|
+
* pillar-seed --public-url https://seed.example.org --announce https://pillar.uuaid.org
|
|
8
|
+
* pillar-seed --status --announce https://pillar.uuaid.org
|
|
7
9
|
*
|
|
8
10
|
* A seed IS a carrier server with a public URL — nothing else. Agents
|
|
9
11
|
* anywhere list your URL in their carriers[] and then:
|
|
@@ -13,18 +15,36 @@
|
|
|
13
15
|
* Minimal ongoing requirements: one process, one SQLite file, outbound-free
|
|
14
16
|
* (all traffic comes TO you). That is the whole contract.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* JOINING
|
|
19
|
+
*
|
|
20
|
+
* Announce yourself to a registry with --announce and you are enrolled: the
|
|
21
|
+
* registry fetches /v1/seed-info from the URL you gave, checks that it is
|
|
22
|
+
* signed by your seed key, and lists you. No account, no review, no waiting
|
|
23
|
+
* on whoever owns the directory file.
|
|
24
|
+
*
|
|
25
|
+
* WHAT YOU EARN
|
|
26
|
+
*
|
|
27
|
+
* The registry then probes you — liveness plus a real envelope round-tripped
|
|
28
|
+
* through your seed — and promotes you as the evidence accumulates:
|
|
29
|
+
*
|
|
30
|
+
* probation -> steady (a day) -> durable (a week) -> anchor (a month)
|
|
19
31
|
*
|
|
20
|
-
*
|
|
32
|
+
* From `steady` up, your OWN identity is issued a signed tier grant: raised
|
|
33
|
+
* send budgets at every carrier that trusts the registry's issuer key. From
|
|
34
|
+
* `durable` up that grant also carries priority, which reserves you
|
|
35
|
+
* admission capacity at those carriers when they are shedding load. Run
|
|
36
|
+
* --status to see your rung and exactly what is left to reach the next one.
|
|
21
37
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* only your seed between them, no official carrier involved.
|
|
38
|
+
* Defaults to loopback (RedTeam F5). To become a real seed, put it behind a
|
|
39
|
+
* TLS-terminating proxy (nginx/Caddy) and pass the PUBLIC https:// URL as
|
|
40
|
+
* --public-url — never the loopback one.
|
|
26
41
|
*/
|
|
42
|
+
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
43
|
+
import { mkdirSync } from "node:fs";
|
|
44
|
+
import { homedir } from "node:os";
|
|
45
|
+
import { join } from "node:path";
|
|
27
46
|
import { CarrierServer } from "../src/net/carrier-server.mjs";
|
|
47
|
+
import { Keychain } from "../src/identity/keychain.mjs";
|
|
28
48
|
|
|
29
49
|
function parseArgs(argv) {
|
|
30
50
|
const args = {};
|
|
@@ -39,16 +59,68 @@ function parseArgs(argv) {
|
|
|
39
59
|
}
|
|
40
60
|
|
|
41
61
|
const args = parseArgs(process.argv.slice(2));
|
|
62
|
+
const home = args.home ?? process.env.SEED_HOME ?? join(homedir(), ".pillar-seed");
|
|
42
63
|
const port = Number(args.port ?? process.env.SEED_PORT ?? 8799);
|
|
43
64
|
const host = args.host ?? process.env.SEED_HOST ?? "127.0.0.1";
|
|
44
65
|
const dbPath = args.db ?? process.env.SEED_DB ?? "pillar-seed.db";
|
|
45
66
|
const ttlDays = Number(args.ttl ?? process.env.SEED_TTL_DAYS ?? 14);
|
|
67
|
+
const publicUrl = args["public-url"] ?? process.env.SEED_PUBLIC_URL ?? null;
|
|
68
|
+
const registryUrl = typeof args.announce === "string" ? args.announce.replace(/\/+$/, "") : null;
|
|
69
|
+
const operator = args.operator ?? process.env.SEED_OPERATOR ?? null;
|
|
70
|
+
const region = args.region ?? process.env.SEED_REGION ?? null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The seed's identity. Distinct from any agent keychain on the box: it names
|
|
74
|
+
* the SEED, is what /v1/seed-info is signed with, and is the subject of any
|
|
75
|
+
* tier grant the seed earns.
|
|
76
|
+
*/
|
|
77
|
+
mkdirSync(home, { recursive: true });
|
|
78
|
+
const keychain = new Keychain(join(home, "seed-keychain.json"));
|
|
79
|
+
if (!keychain.exists()) {
|
|
80
|
+
keychain.save(Keychain.generate());
|
|
81
|
+
console.log(`minted seed identity -> ${join(home, "seed-keychain.json")}`);
|
|
82
|
+
}
|
|
83
|
+
keychain.load();
|
|
46
84
|
|
|
47
|
-
|
|
85
|
+
/** Ask a registry where we stand and print the ladder position. */
|
|
86
|
+
async function printStanding(registry, uuaid) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(`${registry}/v1/seeds/${encodeURIComponent(uuaid)}`);
|
|
89
|
+
if (res.status === 404) {
|
|
90
|
+
console.log(` standing: not enrolled at ${registry} — run with --announce ${registry}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) { console.log(` standing: registry ${res.status}`); return; }
|
|
94
|
+
const s = await res.json();
|
|
95
|
+
console.log(` tier: ${s.tier}${s.demoted ? " (DEMOTED — failing probes)" : ""} — ${s.blurb}`);
|
|
96
|
+
console.log(` earned: ${s.grantTier ? `${s.grantTier} grant${s.priority ? " + priority delivery" : ""}` : "nothing yet"}`);
|
|
97
|
+
const o = s.observed ?? {};
|
|
98
|
+
console.log(` observed: ${o.probes?.passed ?? 0}/${o.probes?.total ?? 0} probes (${((o.uptime ?? 0) * 100).toFixed(1)}% uptime), ${o.roundTrips?.passed ?? 0} relay round-trips`);
|
|
99
|
+
if (s.next) console.log(` next: ${s.next} — needs ${s.missing.join(", ")}`);
|
|
100
|
+
else if (!s.demoted) console.log(` next: top of the ladder`);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.log(` standing: unreachable — ${e.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --status is a query, not a server: report and exit.
|
|
107
|
+
if (args.status) {
|
|
108
|
+
if (!registryUrl) {
|
|
109
|
+
console.error("--status needs --announce <registry-url> to know which registry to ask");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
console.log(`seed ${keychain._identity.uuaid}`);
|
|
113
|
+
await printStanding(registryUrl, keychain._identity.uuaid);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl });
|
|
48
118
|
const addr = await server.listen(port, host);
|
|
49
119
|
console.log(`pillar-seed listening on http://${addr.address}:${addr.port}`);
|
|
120
|
+
console.log(` uuaid: ${keychain._identity.uuaid}`);
|
|
50
121
|
console.log(` db: ${dbPath}`);
|
|
51
122
|
console.log(` ttl: ${ttlDays} days`);
|
|
123
|
+
if (publicUrl) console.log(` public url: ${publicUrl}`);
|
|
52
124
|
|
|
53
125
|
// Boot self-check on the address we actually serve — verify before claim.
|
|
54
126
|
try {
|
|
@@ -60,11 +132,39 @@ try {
|
|
|
60
132
|
console.log(` health: unreachable — ${e.message}`);
|
|
61
133
|
}
|
|
62
134
|
|
|
63
|
-
if (
|
|
135
|
+
if (registryUrl) {
|
|
136
|
+
if (!publicUrl) {
|
|
137
|
+
console.log(`\n --announce needs --public-url <https://your-host>: the registry enrolls a URL,`);
|
|
138
|
+
console.log(` and it must be one it can reach and that you can prove you operate.`);
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`\n announcing ${publicUrl} to ${registryUrl} ...`);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(`${registryUrl}/v1/seeds`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "content-type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ url: publicUrl, operator, region }),
|
|
146
|
+
});
|
|
147
|
+
const body = await res.json().catch(() => ({}));
|
|
148
|
+
if (res.ok && body.ok) {
|
|
149
|
+
console.log(` enrolled${body.already ? " (already)" : ""} as ${body.uuaid}`);
|
|
150
|
+
await printStanding(registryUrl, keychain._identity.uuaid);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(` enrollment refused: ${body.reason ?? res.status}`);
|
|
153
|
+
if (String(body.reason ?? "").startsWith("seed-unreachable")) {
|
|
154
|
+
console.log(` (the registry could not fetch ${publicUrl}/v1/seed-info — check DNS, TLS, and your proxy)`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.log(` enrollment failed: ${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (host === "127.0.0.1") {
|
|
64
162
|
console.log(`\n loopback seed — reachable only from this machine.`);
|
|
65
|
-
console.log(` To join the mesh as a real seed: TLS-terminate a public URL
|
|
66
|
-
console.log(`
|
|
67
|
-
console.log(`
|
|
163
|
+
console.log(` To join the mesh as a real seed: TLS-terminate a public URL, then`);
|
|
164
|
+
console.log(` pillar-seed --host 0.0.0.0 --public-url https://<your-host> \\`);
|
|
165
|
+
console.log(` --announce https://pillar.uuaid.org --operator <you> --region <region>`);
|
|
166
|
+
console.log(` Enrollment is permissionless: the registry verifies the URL against your`);
|
|
167
|
+
console.log(` seed key and lists you. Staying up is what earns the rest.`);
|
|
68
168
|
}
|
|
69
169
|
|
|
70
170
|
// Vacuum expired envelopes hourly.
|
package/bin/pillar.mjs
CHANGED
|
@@ -9,11 +9,14 @@
|
|
|
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,
|
|
15
17
|
* useful for running multiple pillars on one machine (Zestro dogfood etc.).
|
|
16
18
|
*/
|
|
19
|
+
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
17
20
|
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
21
|
import { join, dirname } from "node:path";
|
|
19
22
|
import { homedir } from "node:os";
|
|
@@ -21,6 +24,10 @@ import { argv, exit } from "node:process";
|
|
|
21
24
|
import { Keychain } from "../src/identity/keychain.mjs";
|
|
22
25
|
import { Pillar } from "../src/pillar.mjs";
|
|
23
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";
|
|
24
31
|
|
|
25
32
|
function parseArgs(argv) {
|
|
26
33
|
const args = { _: [] };
|
|
@@ -60,6 +67,19 @@ function loadPillar(args) {
|
|
|
60
67
|
const carriers = args.carrier
|
|
61
68
|
? [args.carrier]
|
|
62
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;
|
|
63
83
|
return new Pillar({
|
|
64
84
|
keychain: kc,
|
|
65
85
|
mailboxPath: mbPath,
|
|
@@ -69,6 +89,8 @@ function loadPillar(args) {
|
|
|
69
89
|
dhtClientMode: cfg.dhtClientMode ?? true,
|
|
70
90
|
carriers,
|
|
71
91
|
enableLibp2p: !(args["carrier-only"] || cfg.carrierOnly === true),
|
|
92
|
+
trust,
|
|
93
|
+
tierGrant,
|
|
72
94
|
});
|
|
73
95
|
}
|
|
74
96
|
|
|
@@ -247,6 +269,121 @@ async function cmdSend(args) {
|
|
|
247
269
|
}
|
|
248
270
|
}
|
|
249
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
|
+
|
|
250
387
|
async function cmdSendGroup(args) {
|
|
251
388
|
const [groupName, kind, jsonStr] = args._.slice(1);
|
|
252
389
|
if (!groupName || !kind) {
|
|
@@ -268,7 +405,7 @@ async function cmdSendGroup(args) {
|
|
|
268
405
|
let failed = 0;
|
|
269
406
|
for (const r of results) {
|
|
270
407
|
if (r.ok) console.log(` ✓ ${r.member} via ${r.via} ${r.envelopeId}`);
|
|
271
|
-
else { failed++; console.error(` ✗ ${r.member} ${r.error}`); }
|
|
408
|
+
else { failed++; console.error(` ✗ ${r.member} ${refusalLine(r) ?? r.error}`); }
|
|
272
409
|
}
|
|
273
410
|
console.log(`group "${groupName}": ${results.length - failed}/${results.length} delivered`);
|
|
274
411
|
if (failed) exit(1);
|
|
@@ -377,7 +514,7 @@ async function cmdRecv(args) {
|
|
|
377
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)}` } });
|
|
378
515
|
}
|
|
379
516
|
}
|
|
380
|
-
} else if (bridge) {
|
|
517
|
+
} else if (bridge && env.kind !== "ack" && env.kind !== "reply") {
|
|
381
518
|
try {
|
|
382
519
|
const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
|
|
383
520
|
console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
|
|
@@ -390,6 +527,10 @@ async function cmdRecv(args) {
|
|
|
390
527
|
console.error(` → zestro bridge error: ${e.message}`);
|
|
391
528
|
}
|
|
392
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)`);
|
|
393
534
|
}
|
|
394
535
|
});
|
|
395
536
|
|
|
@@ -410,9 +551,16 @@ async function cmdInbox(args) {
|
|
|
410
551
|
const fresh = [];
|
|
411
552
|
p.onEnvelope((env) => { fresh.push(env); });
|
|
412
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);
|
|
413
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.
|
|
414
562
|
for (const env of fresh) {
|
|
415
|
-
console.log(JSON.stringify({ new: true, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload: env.payload }));
|
|
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 }));
|
|
416
564
|
}
|
|
417
565
|
// Backlog: rows stored earlier (e.g. by a recv daemon) but never surfaced.
|
|
418
566
|
const { decrypt } = await import("../src/net/envelope.mjs");
|
|
@@ -423,10 +571,11 @@ async function cmdInbox(args) {
|
|
|
423
571
|
const env = JSON.parse(row.envelope_json);
|
|
424
572
|
let payload = null;
|
|
425
573
|
try { payload = decrypt(p.keychain, env); } catch (_e) { payload = { error: "decrypt failed" }; }
|
|
426
|
-
console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload }));
|
|
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 }));
|
|
427
575
|
}
|
|
428
576
|
}
|
|
429
|
-
|
|
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)` : ""}`);
|
|
430
579
|
if (args["mark-read"]) {
|
|
431
580
|
for (const env of fresh) p.mailbox.markInboxRead?.(env.id);
|
|
432
581
|
if (args.unread) for (const row of backlog) p.mailbox.markInboxRead?.(row.envelope_id);
|
|
@@ -445,6 +594,144 @@ async function cmdInbox(args) {
|
|
|
445
594
|
exit(0);
|
|
446
595
|
}
|
|
447
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
|
+
|
|
448
735
|
async function cmdPeers(args) {
|
|
449
736
|
const p = loadPillar(args);
|
|
450
737
|
await p.start();
|
|
@@ -465,11 +752,16 @@ async function main() {
|
|
|
465
752
|
case "groups": return cmdGroups(args);
|
|
466
753
|
case "recv": return cmdRecv(args);
|
|
467
754
|
case "inbox": return cmdInbox(args);
|
|
755
|
+
case "trust": return cmdTrust(args);
|
|
756
|
+
case "tier": return cmdTier(args);
|
|
468
757
|
case "peers": return cmdPeers(args);
|
|
758
|
+
case "delegate": return cmdDelegate(args);
|
|
469
759
|
default:
|
|
470
|
-
console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|peers> [--home DIR]`);
|
|
760
|
+
console.error(`usage: pillar <init|id|send|send-group|groups|recv|inbox|trust|tier|peers|delegate> [--home DIR]`);
|
|
471
761
|
exit(2);
|
|
472
762
|
}
|
|
473
763
|
}
|
|
474
764
|
|
|
475
|
-
|
|
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); });
|