@uuaid/pillar 0.2.0-alpha.12 → 0.2.0-alpha.16

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.
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pillar-blob — encrypted temporary-object transport through the UUAID broker.
4
+ *
5
+ * pillar-blob send --to UUAID --recipient-pubkey HEX --data FILE [--broker URL]
6
+ * [--carriers URL,...] --key-id REGISTRY_KEY_ID [--ttl-ms N]
7
+ * pillar-blob get --ref-file FILE --expect-envelope ID --key-id REGISTRY_KEY_ID
8
+ * [--broker URL] [--out FILE|-]
9
+ *
10
+ * The broker never receives a DEK. `send` prints metadata only; the secret ref
11
+ * exists solely inside the recipient-encrypted Pillar envelope.
12
+ */
13
+ import { readFileSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { Keychain } from "../src/identity/keychain.mjs";
17
+ import { Pillar } from "../src/pillar.mjs";
18
+ import { PillarBlobHttpClient } from "../src/blob/http.mjs";
19
+ import { fetchBrokerBlob, DEFAULT_BLOB_TTL_MS, BlobError } from "../src/blob/blob.mjs";
20
+
21
+ function usage(code = 2) {
22
+ process.stderr.write("usage: pillar-blob send|get (run without arguments to see the source header)\n");
23
+ process.exit(code);
24
+ }
25
+
26
+ function parse(argv) {
27
+ const out = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const key = argv[i];
30
+ if (!key.startsWith("--") || i + 1 >= argv.length) usage();
31
+ out[key.slice(2)] = argv[++i];
32
+ }
33
+ return out;
34
+ }
35
+
36
+ function homeDir(args) {
37
+ return args.home ?? process.env.PILLAR_HOME ?? join(homedir(), ".pillar-fleet", "zz-master");
38
+ }
39
+
40
+ function loadHome(args) {
41
+ const home = homeDir(args);
42
+ const keychain = new Keychain(join(home, "keychain.json"));
43
+ keychain.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
44
+ let config = {};
45
+ try { config = JSON.parse(readFileSync(join(home, "config.json"), "utf-8")); } catch (_error) {}
46
+ const keyId = args["key-id"] ?? process.env.PILLAR_PRESENTATION_KEY_ID ?? config.presentationKeyId;
47
+ if (!keyId) throw new Error("--key-id (registry-bound presentation key id) is required");
48
+ const broker = args.broker ?? process.env.PILLAR_BLOB_BROKER ?? "https://api.uuaid.org";
49
+ const client = new PillarBlobHttpClient({ baseUrl: broker, keychain, keyId });
50
+ return { home, keychain, config, client };
51
+ }
52
+
53
+ function fail(error) {
54
+ const code = error instanceof BlobError ? error.code : "error";
55
+ process.stderr.write(`pillar-blob: ${code}: ${error?.message ?? String(error)}\n`);
56
+ process.exit(code === "blob-not-found" || code === "blob-expired" ? 3 : code.includes("integrity") ? 5 : 6);
57
+ }
58
+
59
+ const [command, ...rest] = process.argv.slice(2);
60
+ if (!command) usage();
61
+ const args = parse(rest);
62
+
63
+ try {
64
+ if (command === "send") {
65
+ if (!args.to || !args["recipient-pubkey"] || !args.data) usage();
66
+ const { home, keychain, config, client } = loadHome(args);
67
+ const carriers = (args.carriers ?? process.env.PILLAR_CARRIERS ?? (config.carriers ?? []).join(","))
68
+ .split(",").map((value) => value.trim()).filter(Boolean);
69
+ if (!carriers.length) throw new Error("no carriers configured");
70
+ const pillar = new Pillar({
71
+ keychain,
72
+ mailboxPath: join(home, "mailbox.db"),
73
+ carriers,
74
+ enableLibp2p: false,
75
+ blobClient: client,
76
+ });
77
+ await pillar.start();
78
+ try {
79
+ const result = await pillar.sendBlob({
80
+ recipient: args.to,
81
+ recipientPublicKey: args["recipient-pubkey"],
82
+ data: args.data === "-" ? readFileSync(0) : readFileSync(args.data),
83
+ ttlMs: Number(args["ttl-ms"] ?? DEFAULT_BLOB_TTL_MS),
84
+ });
85
+ process.stdout.write(`${JSON.stringify({
86
+ envelope_id: result.envelope.id,
87
+ via: result.via,
88
+ plaintext_bytes: result.blob.size,
89
+ ciphertext_sha256: result.blob.hash,
90
+ })}\n`);
91
+ } finally {
92
+ await pillar.stop();
93
+ }
94
+ } else if (command === "get") {
95
+ if (!args["ref-file"] || !args["expect-envelope"]) usage();
96
+ const { keychain, client } = loadHome(args);
97
+ const parsed = JSON.parse(readFileSync(args["ref-file"], "utf-8"));
98
+ const ref = parsed.blobRef ?? parsed;
99
+ const result = await fetchBrokerBlob({
100
+ ref,
101
+ envelopeId: args["expect-envelope"],
102
+ recipient: keychain._identity.uuaid,
103
+ client,
104
+ });
105
+ if (!args.out || args.out === "-") process.stdout.write(result.data);
106
+ else writeFileSync(args.out, result.data, { mode: 0o600 });
107
+ process.stderr.write(`pillar-blob: redeemed ${result.size} plaintext byte(s)\n`);
108
+ } else usage();
109
+ } catch (error) {
110
+ fail(error);
111
+ }
112
+
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pillar-directory — serve directory.json from the seed registry.
4
+ *
5
+ * pillar-directory --port 8800 --carrier https://pillar.uuaid.org \
6
+ * --agents ./agents.json --db /opt/pillar-directory/registry.db
7
+ *
8
+ * Replaces the hand-edited directory.json that every agent bootstraps from.
9
+ * The file could only be changed by whoever had shell on the host serving it,
10
+ * which is the reason the mesh had exactly one seed. Here the seed list is a
11
+ * table any operator can enter by proving they run a live seed, and the rung
12
+ * they hold is computed from probes this process makes itself.
13
+ *
14
+ * Routes:
15
+ * GET /directory.json the directory (carrier + agents + ranked seeds)
16
+ * POST /v1/seeds enroll { url, operator?, region? }
17
+ * GET /v1/seeds every enrolled seed and its standing
18
+ * GET /v1/seeds/<uuaid> one seed's standing + what is left to earn
19
+ * GET /v1/seeds/<uuaid>/grant that seed's current earned tier grant
20
+ * GET /v1/health
21
+ *
22
+ * `agents.json` is passed through untouched: this process owns the seed list,
23
+ * not the address book.
24
+ */
25
+ import { createServer } from "node:http";
26
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { join } from "node:path";
29
+ import { SeedRegistry } from "../src/net/seed-registry.mjs";
30
+ import { Keychain } from "../src/identity/keychain.mjs";
31
+
32
+ function parseArgs(argv) {
33
+ const args = {};
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a.startsWith("--")) {
37
+ const k = a.slice(2);
38
+ args[k] = (argv[i + 1] && !argv[i + 1].startsWith("--")) ? argv[++i] : true;
39
+ }
40
+ }
41
+ return args;
42
+ }
43
+
44
+ const args = parseArgs(process.argv.slice(2));
45
+ const home = args.home ?? process.env.DIRECTORY_HOME ?? join(homedir(), ".pillar-directory");
46
+ const port = Number(args.port ?? process.env.DIRECTORY_PORT ?? 8800);
47
+ const host = args.host ?? process.env.DIRECTORY_HOST ?? "127.0.0.1";
48
+ const dbPath = args.db ?? process.env.DIRECTORY_DB ?? join(home, "registry.db");
49
+ const carrier = args.carrier ?? process.env.DIRECTORY_CARRIER ?? null;
50
+ const agentsPath = args.agents ?? process.env.DIRECTORY_AGENTS ?? null;
51
+ const protocol = args.protocol ?? process.env.DIRECTORY_PROTOCOL ?? "uacp/1.0-draft";
52
+ const probeMinutes = Number(args["probe-minutes"] ?? process.env.DIRECTORY_PROBE_MINUTES ?? 10);
53
+ const allowPrivateUrls = Boolean(args["allow-private-urls"]);
54
+
55
+ const MAX_ENROLLMENTS_PER_HOUR = 10; // per client IP
56
+ const MAX_BODY = 8 * 1024;
57
+
58
+ mkdirSync(home, { recursive: true });
59
+ const keychain = new Keychain(join(home, "registry-keychain.json"));
60
+ if (!keychain.exists()) {
61
+ keychain.save(Keychain.generate());
62
+ console.log(`minted registry identity -> ${join(home, "registry-keychain.json")}`);
63
+ }
64
+ keychain.load();
65
+
66
+ const registry = new SeedRegistry({ keychain, carrierUrl: carrier, dbPath, protocol, allowPrivateUrls });
67
+
68
+ /**
69
+ * agents[] is read fresh on each render rather than cached at boot, so
70
+ * updating the address book does not require restarting the directory and
71
+ * dropping every in-flight enrollment.
72
+ */
73
+ function agents() {
74
+ if (!agentsPath || !existsSync(agentsPath)) return [];
75
+ try {
76
+ const raw = JSON.parse(readFileSync(agentsPath, "utf-8"));
77
+ return Array.isArray(raw) ? raw : (raw.agents ?? []);
78
+ } catch (e) {
79
+ console.error(`[directory] agents file unreadable: ${e.message}`);
80
+ return [];
81
+ }
82
+ }
83
+
84
+ const enrollRate = new Map(); // ip -> [ms]
85
+ function enrollAllowed(ip) {
86
+ const now = Date.now();
87
+ const arr = (enrollRate.get(ip) ?? []).filter(t => t > now - 3600_000);
88
+ if (arr.length >= MAX_ENROLLMENTS_PER_HOUR) { enrollRate.set(ip, arr); return false; }
89
+ arr.push(now);
90
+ enrollRate.set(ip, arr);
91
+ if (enrollRate.size > 10_000) {
92
+ for (const [k, v] of enrollRate) if (!v.some(t => t > now - 3600_000)) enrollRate.delete(k);
93
+ }
94
+ return true;
95
+ }
96
+
97
+ function send(res, code, obj) {
98
+ const body = JSON.stringify(obj, null, 2);
99
+ res.writeHead(code, {
100
+ "content-type": "application/json",
101
+ "content-length": Buffer.byteLength(body),
102
+ // The directory is public bootstrap data; browsers fetch it directly.
103
+ "access-control-allow-origin": "*",
104
+ "cache-control": "public, max-age=60",
105
+ });
106
+ res.end(body);
107
+ }
108
+
109
+ function readBody(req) {
110
+ return new Promise((resolve, reject) => {
111
+ let size = 0; const chunks = [];
112
+ req.on("data", c => {
113
+ size += c.length;
114
+ if (size > MAX_BODY) { reject(new Error("body too large")); req.destroy(); return; }
115
+ chunks.push(c);
116
+ });
117
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
118
+ req.on("error", reject);
119
+ });
120
+ }
121
+
122
+ const clientIp = (req) => {
123
+ const fwd = req.headers["x-forwarded-for"];
124
+ return fwd ? String(fwd).split(",")[0].trim() : (req.socket.remoteAddress ?? "unknown");
125
+ };
126
+
127
+ const server = createServer(async (req, res) => {
128
+ try {
129
+ const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
130
+
131
+ if (req.method === "GET" && (url.pathname === "/directory.json" || url.pathname === "/")) {
132
+ return send(res, 200, registry.directory({ agents: agents() }));
133
+ }
134
+
135
+ if (req.method === "POST" && url.pathname === "/v1/seeds") {
136
+ const ip = clientIp(req);
137
+ if (!enrollAllowed(ip)) return send(res, 429, { ok: false, reason: "rate-limited" });
138
+ let payload;
139
+ try { payload = JSON.parse(await readBody(req)); }
140
+ catch { return send(res, 400, { ok: false, reason: "bad-json" }); }
141
+ const r = await registry.enroll({
142
+ url: payload?.url,
143
+ operator: payload?.operator ?? null,
144
+ region: payload?.region ?? null,
145
+ });
146
+ if (!r.ok) console.log(`[directory] enroll refused ${payload?.url}: ${r.reason}`);
147
+ else if (!r.already) console.log(`[directory] enrolled ${r.url} as ${r.uuaid}`);
148
+ return send(res, r.ok ? 200 : 400, r);
149
+ }
150
+
151
+ if (req.method === "GET" && url.pathname === "/v1/seeds") {
152
+ return send(res, 200, { seeds: registry.all().map(r => registry.status(r.uuaid)) });
153
+ }
154
+
155
+ const grantMatch = /^\/v1\/seeds\/([^/]+)\/grant$/.exec(url.pathname);
156
+ if (req.method === "GET" && grantMatch) {
157
+ const uuaid = decodeURIComponent(grantMatch[1]);
158
+ // Served unauthenticated on purpose: a grant is only usable by the
159
+ // identity it names, and using it means signing an envelope with that
160
+ // identity's key. Holding someone else's grant buys nothing.
161
+ const grant = registry.grantFor(uuaid);
162
+ if (!grant) {
163
+ const known = registry.get(uuaid);
164
+ return send(res, known ? 200 : 404,
165
+ known ? { ok: true, grant: null, reason: "tier-earns-no-grant", tier: known.tier }
166
+ : { ok: false, reason: "unknown-seed" });
167
+ }
168
+ return send(res, 200, { ok: true, grant });
169
+ }
170
+
171
+ const seedMatch = /^\/v1\/seeds\/([^/]+)$/.exec(url.pathname);
172
+ if (req.method === "GET" && seedMatch) {
173
+ const st = registry.status(decodeURIComponent(seedMatch[1]));
174
+ return send(res, st.ok ? 200 : 404, st);
175
+ }
176
+
177
+ if (req.method === "GET" && url.pathname === "/v1/health") {
178
+ return send(res, 200, {
179
+ ok: true,
180
+ seeds: registry.all().length,
181
+ issuer: registry.issuerKey,
182
+ uptimeMs: Date.now() - startedAt,
183
+ });
184
+ }
185
+
186
+ send(res, 404, { error: "not-found" });
187
+ } catch (e) {
188
+ console.error(`[directory] 500: ${e.message}`);
189
+ send(res, 500, { error: "internal-error" });
190
+ }
191
+ });
192
+
193
+ const startedAt = Date.now();
194
+ await new Promise(r => server.listen(port, host, r));
195
+ console.log(`pillar-directory listening on http://${host}:${port}`);
196
+ console.log(` issuer: ${registry.issuerKey}`);
197
+ console.log(` carrier: ${carrier ?? "(none — set --carrier)"}`);
198
+ console.log(` db: ${dbPath}`);
199
+ console.log(` seeds: ${registry.all().length}`);
200
+ console.log(` probing every ${probeMinutes} min`);
201
+
202
+ /**
203
+ * The observation loop. Every seed is probed on the same cadence, so the
204
+ * probe counts in the ladder translate directly into wall-clock time:
205
+ * at the default 10 minutes, `steady`'s 20 probes is ~3.3 hours of proof on
206
+ * top of its 1-day minimum age.
207
+ */
208
+ let probing = false;
209
+ const probeTimer = setInterval(async () => {
210
+ if (probing) return; // never overlap: a slow seed must not stack rounds
211
+ probing = true;
212
+ try {
213
+ for (const r of await registry.observeAll()) {
214
+ if (r.changed) console.log(`[directory] ${r.url} ${r.previousTier} -> ${r.tier}`);
215
+ else if (!r.alive) console.log(`[directory] ${r.url} probe failed`);
216
+ }
217
+ } catch (e) {
218
+ console.error(`[directory] probe round failed: ${e.message}`);
219
+ } finally {
220
+ probing = false;
221
+ }
222
+ }, Math.max(1, probeMinutes) * 60_000);
223
+
224
+ const shutdown = async () => {
225
+ clearInterval(probeTimer);
226
+ await new Promise(r => server.close(r));
227
+ registry.close();
228
+ process.exit(0);
229
+ };
230
+ process.on("SIGINT", shutdown);
231
+ process.on("SIGTERM", shutdown);
@@ -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,35 @@
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
- * Defaults to loopback (RedTeam F5). To become a real seed, put it behind a
17
- * TLS-terminating proxy (nginx/Caddy) and advertise the PUBLIC https:// URL —
18
- * never the loopback one — as a seeds[] entry in a pillar directory.json:
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
- * { "url": "https://<your-host>", "operator": "<you>", "region": "<region>", "since": "<ISO date>" }
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
- * Anyone running `pillar init --directory <directory-url>` then picks you up
23
- * automatically as one of their carriers. You can also be someone's ENTIRE
24
- * carrier set (`--carriers https://<your-host>`) — two agents can talk with
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 { mkdirSync } from "node:fs";
43
+ import { homedir } from "node:os";
44
+ import { join } from "node:path";
27
45
  import { CarrierServer } from "../src/net/carrier-server.mjs";
46
+ import { Keychain } from "../src/identity/keychain.mjs";
28
47
 
29
48
  function parseArgs(argv) {
30
49
  const args = {};
@@ -39,16 +58,68 @@ function parseArgs(argv) {
39
58
  }
40
59
 
41
60
  const args = parseArgs(process.argv.slice(2));
61
+ const home = args.home ?? process.env.SEED_HOME ?? join(homedir(), ".pillar-seed");
42
62
  const port = Number(args.port ?? process.env.SEED_PORT ?? 8799);
43
63
  const host = args.host ?? process.env.SEED_HOST ?? "127.0.0.1";
44
64
  const dbPath = args.db ?? process.env.SEED_DB ?? "pillar-seed.db";
45
65
  const ttlDays = Number(args.ttl ?? process.env.SEED_TTL_DAYS ?? 14);
66
+ const publicUrl = args["public-url"] ?? process.env.SEED_PUBLIC_URL ?? null;
67
+ const registryUrl = typeof args.announce === "string" ? args.announce.replace(/\/+$/, "") : null;
68
+ const operator = args.operator ?? process.env.SEED_OPERATOR ?? null;
69
+ const region = args.region ?? process.env.SEED_REGION ?? null;
70
+
71
+ /**
72
+ * The seed's identity. Distinct from any agent keychain on the box: it names
73
+ * the SEED, is what /v1/seed-info is signed with, and is the subject of any
74
+ * tier grant the seed earns.
75
+ */
76
+ mkdirSync(home, { recursive: true });
77
+ const keychain = new Keychain(join(home, "seed-keychain.json"));
78
+ if (!keychain.exists()) {
79
+ keychain.save(Keychain.generate());
80
+ console.log(`minted seed identity -> ${join(home, "seed-keychain.json")}`);
81
+ }
82
+ keychain.load();
46
83
 
47
- const server = new CarrierServer({ dbPath, ttlDays });
84
+ /** Ask a registry where we stand and print the ladder position. */
85
+ async function printStanding(registry, uuaid) {
86
+ try {
87
+ const res = await fetch(`${registry}/v1/seeds/${encodeURIComponent(uuaid)}`);
88
+ if (res.status === 404) {
89
+ console.log(` standing: not enrolled at ${registry} — run with --announce ${registry}`);
90
+ return;
91
+ }
92
+ if (!res.ok) { console.log(` standing: registry ${res.status}`); return; }
93
+ const s = await res.json();
94
+ console.log(` tier: ${s.tier}${s.demoted ? " (DEMOTED — failing probes)" : ""} — ${s.blurb}`);
95
+ console.log(` earned: ${s.grantTier ? `${s.grantTier} grant${s.priority ? " + priority delivery" : ""}` : "nothing yet"}`);
96
+ const o = s.observed ?? {};
97
+ 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`);
98
+ if (s.next) console.log(` next: ${s.next} — needs ${s.missing.join(", ")}`);
99
+ else if (!s.demoted) console.log(` next: top of the ladder`);
100
+ } catch (e) {
101
+ console.log(` standing: unreachable — ${e.message}`);
102
+ }
103
+ }
104
+
105
+ // --status is a query, not a server: report and exit.
106
+ if (args.status) {
107
+ if (!registryUrl) {
108
+ console.error("--status needs --announce <registry-url> to know which registry to ask");
109
+ process.exit(1);
110
+ }
111
+ console.log(`seed ${keychain._identity.uuaid}`);
112
+ await printStanding(registryUrl, keychain._identity.uuaid);
113
+ process.exit(0);
114
+ }
115
+
116
+ const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl });
48
117
  const addr = await server.listen(port, host);
49
118
  console.log(`pillar-seed listening on http://${addr.address}:${addr.port}`);
119
+ console.log(` uuaid: ${keychain._identity.uuaid}`);
50
120
  console.log(` db: ${dbPath}`);
51
121
  console.log(` ttl: ${ttlDays} days`);
122
+ if (publicUrl) console.log(` public url: ${publicUrl}`);
52
123
 
53
124
  // Boot self-check on the address we actually serve — verify before claim.
54
125
  try {
@@ -60,11 +131,39 @@ try {
60
131
  console.log(` health: unreachable — ${e.message}`);
61
132
  }
62
133
 
63
- if (host === "127.0.0.1") {
134
+ if (registryUrl) {
135
+ if (!publicUrl) {
136
+ console.log(`\n --announce needs --public-url <https://your-host>: the registry enrolls a URL,`);
137
+ console.log(` and it must be one it can reach and that you can prove you operate.`);
138
+ } else {
139
+ console.log(`\n announcing ${publicUrl} to ${registryUrl} ...`);
140
+ try {
141
+ const res = await fetch(`${registryUrl}/v1/seeds`, {
142
+ method: "POST",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify({ url: publicUrl, operator, region }),
145
+ });
146
+ const body = await res.json().catch(() => ({}));
147
+ if (res.ok && body.ok) {
148
+ console.log(` enrolled${body.already ? " (already)" : ""} as ${body.uuaid}`);
149
+ await printStanding(registryUrl, keychain._identity.uuaid);
150
+ } else {
151
+ console.log(` enrollment refused: ${body.reason ?? res.status}`);
152
+ if (String(body.reason ?? "").startsWith("seed-unreachable")) {
153
+ console.log(` (the registry could not fetch ${publicUrl}/v1/seed-info — check DNS, TLS, and your proxy)`);
154
+ }
155
+ }
156
+ } catch (e) {
157
+ console.log(` enrollment failed: ${e.message}`);
158
+ }
159
+ }
160
+ } else if (host === "127.0.0.1") {
64
161
  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 and list it:`);
66
- console.log(` { "url": "https://<your-host>", "operator": "<you>", "region": "<region>", "since": "${new Date().toISOString().slice(0, 10)}" }`);
67
- console.log(` in the seeds[] array of your pillar directory.json.`);
162
+ console.log(` To join the mesh as a real seed: TLS-terminate a public URL, then`);
163
+ console.log(` pillar-seed --host 0.0.0.0 --public-url https://<your-host> \\`);
164
+ console.log(` --announce https://pillar.uuaid.org --operator <you> --region <region>`);
165
+ console.log(` Enrollment is permissionless: the registry verifies the URL against your`);
166
+ console.log(` seed key and lists you. Staying up is what earns the rest.`);
68
167
  }
69
168
 
70
169
  // Vacuum expired envelopes hourly.
package/bin/pillar.mjs CHANGED
@@ -436,8 +436,12 @@ async function cmdInbox(args) {
436
436
  // senders loaded, or every fresh envelope counts as a stranger.
437
437
  loadFleetAddressBook(fleetPath(args), p);
438
438
  const pulled = await p.drainCarrierInbox();
439
+ // fromPublicKey: the sender's verified Ed25519 key (transportSignature —
440
+ // open() binds it to `from`). Lets a drain-and-reply consumer (the fleet
441
+ // courier's auto-ack, a bot) seal back to a sender who is NOT in fleet.json
442
+ // via `pillar send … --to-pubkey <fromPublicKey>`. Additive field.
439
443
  for (const env of fresh) {
440
- console.log(JSON.stringify({ new: true, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload: env.payload }));
444
+ 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 }));
441
445
  }
442
446
  // Backlog: rows stored earlier (e.g. by a recv daemon) but never surfaced.
443
447
  const { decrypt } = await import("../src/net/envelope.mjs");
@@ -448,7 +452,7 @@ async function cmdInbox(args) {
448
452
  const env = JSON.parse(row.envelope_json);
449
453
  let payload = null;
450
454
  try { payload = decrypt(p.keychain, env); } catch (_e) { payload = { error: "decrypt failed" }; }
451
- console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, kind: env.kind, at: env.createdAt, payload }));
455
+ console.log(JSON.stringify({ new: false, id: env.id, from: env.sender, fromPublicKey: env.transportSignature?.publicKey ?? null, kind: env.kind, at: env.createdAt, payload }));
452
456
  }
453
457
  }
454
458
  const qCount = (p.mailbox.inboxQuarantined?.(500) ?? []).length;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uuaid/pillar",
3
- "version": "0.2.0-alpha.12",
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.",
3
+ "version": "0.2.0-alpha.16",
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",
@@ -11,17 +11,26 @@
11
11
  "pillar-ask": "bin/pillar-ask.mjs",
12
12
  "pillar-responder": "bin/pillar-responder.mjs",
13
13
  "pillar-seed": "bin/pillar-seed.mjs",
14
- "pillar-escrow": "bin/pillar-escrow.mjs"
14
+ "pillar-directory": "bin/pillar-directory.mjs",
15
+ "pillar-escrow": "bin/pillar-escrow.mjs",
16
+ "pillar-blob": "bin/pillar-blob.mjs"
15
17
  },
16
18
  "exports": {
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"
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
+ "./package.json": "./package.json"
25
34
  },
26
35
  "files": [
27
36
  "src",
@@ -41,7 +50,6 @@
41
50
  },
42
51
  "dependencies": {
43
52
  "@chainsafe/libp2p-noise": "^17.0.0",
44
- "@noble/curves": "^2.3.0",
45
53
  "@noble/hashes": "^2.3.0",
46
54
  "@chainsafe/libp2p-yamux": "^8.0.1",
47
55
  "@libp2p/autonat": "^3.0.26",