@uuaid/pillar 0.2.0-alpha.13 → 0.2.0-alpha.17

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,113 @@
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 "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
14
+ import { readFileSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { Keychain } from "../src/identity/keychain.mjs";
18
+ import { Pillar } from "../src/pillar.mjs";
19
+ import { PillarBlobHttpClient } from "../src/blob/http.mjs";
20
+ import { fetchBrokerBlob, DEFAULT_BLOB_TTL_MS, BlobError } from "../src/blob/blob.mjs";
21
+
22
+ function usage(code = 2) {
23
+ process.stderr.write("usage: pillar-blob send|get (run without arguments to see the source header)\n");
24
+ process.exit(code);
25
+ }
26
+
27
+ function parse(argv) {
28
+ const out = {};
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const key = argv[i];
31
+ if (!key.startsWith("--") || i + 1 >= argv.length) usage();
32
+ out[key.slice(2)] = argv[++i];
33
+ }
34
+ return out;
35
+ }
36
+
37
+ function homeDir(args) {
38
+ return args.home ?? process.env.PILLAR_HOME ?? join(homedir(), ".pillar-fleet", "zz-master");
39
+ }
40
+
41
+ function loadHome(args) {
42
+ const home = homeDir(args);
43
+ const keychain = new Keychain(join(home, "keychain.json"));
44
+ keychain.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
45
+ let config = {};
46
+ try { config = JSON.parse(readFileSync(join(home, "config.json"), "utf-8")); } catch (_error) {}
47
+ const keyId = args["key-id"] ?? process.env.PILLAR_PRESENTATION_KEY_ID ?? config.presentationKeyId;
48
+ if (!keyId) throw new Error("--key-id (registry-bound presentation key id) is required");
49
+ const broker = args.broker ?? process.env.PILLAR_BLOB_BROKER ?? "https://api.uuaid.org";
50
+ const client = new PillarBlobHttpClient({ baseUrl: broker, keychain, keyId });
51
+ return { home, keychain, config, client };
52
+ }
53
+
54
+ function fail(error) {
55
+ const code = error instanceof BlobError ? error.code : "error";
56
+ process.stderr.write(`pillar-blob: ${code}: ${error?.message ?? String(error)}\n`);
57
+ process.exit(code === "blob-not-found" || code === "blob-expired" ? 3 : code.includes("integrity") ? 5 : 6);
58
+ }
59
+
60
+ const [command, ...rest] = process.argv.slice(2);
61
+ if (!command) usage();
62
+ const args = parse(rest);
63
+
64
+ try {
65
+ if (command === "send") {
66
+ if (!args.to || !args["recipient-pubkey"] || !args.data) usage();
67
+ const { home, keychain, config, client } = loadHome(args);
68
+ const carriers = (args.carriers ?? process.env.PILLAR_CARRIERS ?? (config.carriers ?? []).join(","))
69
+ .split(",").map((value) => value.trim()).filter(Boolean);
70
+ if (!carriers.length) throw new Error("no carriers configured");
71
+ const pillar = new Pillar({
72
+ keychain,
73
+ mailboxPath: join(home, "mailbox.db"),
74
+ carriers,
75
+ enableLibp2p: false,
76
+ blobClient: client,
77
+ });
78
+ await pillar.start();
79
+ try {
80
+ const result = await pillar.sendBlob({
81
+ recipient: args.to,
82
+ recipientPublicKey: args["recipient-pubkey"],
83
+ data: args.data === "-" ? readFileSync(0) : readFileSync(args.data),
84
+ ttlMs: Number(args["ttl-ms"] ?? DEFAULT_BLOB_TTL_MS),
85
+ });
86
+ process.stdout.write(`${JSON.stringify({
87
+ envelope_id: result.envelope.id,
88
+ via: result.via,
89
+ plaintext_bytes: result.blob.size,
90
+ ciphertext_sha256: result.blob.hash,
91
+ })}\n`);
92
+ } finally {
93
+ await pillar.stop();
94
+ }
95
+ } else if (command === "get") {
96
+ if (!args["ref-file"] || !args["expect-envelope"]) usage();
97
+ const { keychain, client } = loadHome(args);
98
+ const parsed = JSON.parse(readFileSync(args["ref-file"], "utf-8"));
99
+ const ref = parsed.blobRef ?? parsed;
100
+ const result = await fetchBrokerBlob({
101
+ ref,
102
+ envelopeId: args["expect-envelope"],
103
+ recipient: keychain._identity.uuaid,
104
+ client,
105
+ });
106
+ if (!args.out || args.out === "-") process.stdout.write(result.data);
107
+ else writeFileSync(args.out, result.data, { mode: 0o600 });
108
+ process.stderr.write(`pillar-blob: redeemed ${result.size} plaintext byte(s)\n`);
109
+ } else usage();
110
+ } catch (error) {
111
+ fail(error);
112
+ }
113
+
@@ -0,0 +1,232 @@
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 "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
26
+ import { createServer } from "node:http";
27
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import { join } from "node:path";
30
+ import { SeedRegistry } from "../src/net/seed-registry.mjs";
31
+ import { Keychain } from "../src/identity/keychain.mjs";
32
+
33
+ function parseArgs(argv) {
34
+ const args = {};
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a.startsWith("--")) {
38
+ const k = a.slice(2);
39
+ args[k] = (argv[i + 1] && !argv[i + 1].startsWith("--")) ? argv[++i] : true;
40
+ }
41
+ }
42
+ return args;
43
+ }
44
+
45
+ const args = parseArgs(process.argv.slice(2));
46
+ const home = args.home ?? process.env.DIRECTORY_HOME ?? join(homedir(), ".pillar-directory");
47
+ const port = Number(args.port ?? process.env.DIRECTORY_PORT ?? 8800);
48
+ const host = args.host ?? process.env.DIRECTORY_HOST ?? "127.0.0.1";
49
+ const dbPath = args.db ?? process.env.DIRECTORY_DB ?? join(home, "registry.db");
50
+ const carrier = args.carrier ?? process.env.DIRECTORY_CARRIER ?? null;
51
+ const agentsPath = args.agents ?? process.env.DIRECTORY_AGENTS ?? null;
52
+ const protocol = args.protocol ?? process.env.DIRECTORY_PROTOCOL ?? "uacp/1.0-draft";
53
+ const probeMinutes = Number(args["probe-minutes"] ?? process.env.DIRECTORY_PROBE_MINUTES ?? 10);
54
+ const allowPrivateUrls = Boolean(args["allow-private-urls"]);
55
+
56
+ const MAX_ENROLLMENTS_PER_HOUR = 10; // per client IP
57
+ const MAX_BODY = 8 * 1024;
58
+
59
+ mkdirSync(home, { recursive: true });
60
+ const keychain = new Keychain(join(home, "registry-keychain.json"));
61
+ if (!keychain.exists()) {
62
+ keychain.save(Keychain.generate());
63
+ console.log(`minted registry identity -> ${join(home, "registry-keychain.json")}`);
64
+ }
65
+ keychain.load();
66
+
67
+ const registry = new SeedRegistry({ keychain, carrierUrl: carrier, dbPath, protocol, allowPrivateUrls });
68
+
69
+ /**
70
+ * agents[] is read fresh on each render rather than cached at boot, so
71
+ * updating the address book does not require restarting the directory and
72
+ * dropping every in-flight enrollment.
73
+ */
74
+ function agents() {
75
+ if (!agentsPath || !existsSync(agentsPath)) return [];
76
+ try {
77
+ const raw = JSON.parse(readFileSync(agentsPath, "utf-8"));
78
+ return Array.isArray(raw) ? raw : (raw.agents ?? []);
79
+ } catch (e) {
80
+ console.error(`[directory] agents file unreadable: ${e.message}`);
81
+ return [];
82
+ }
83
+ }
84
+
85
+ const enrollRate = new Map(); // ip -> [ms]
86
+ function enrollAllowed(ip) {
87
+ const now = Date.now();
88
+ const arr = (enrollRate.get(ip) ?? []).filter(t => t > now - 3600_000);
89
+ if (arr.length >= MAX_ENROLLMENTS_PER_HOUR) { enrollRate.set(ip, arr); return false; }
90
+ arr.push(now);
91
+ enrollRate.set(ip, arr);
92
+ if (enrollRate.size > 10_000) {
93
+ for (const [k, v] of enrollRate) if (!v.some(t => t > now - 3600_000)) enrollRate.delete(k);
94
+ }
95
+ return true;
96
+ }
97
+
98
+ function send(res, code, obj) {
99
+ const body = JSON.stringify(obj, null, 2);
100
+ res.writeHead(code, {
101
+ "content-type": "application/json",
102
+ "content-length": Buffer.byteLength(body),
103
+ // The directory is public bootstrap data; browsers fetch it directly.
104
+ "access-control-allow-origin": "*",
105
+ "cache-control": "public, max-age=60",
106
+ });
107
+ res.end(body);
108
+ }
109
+
110
+ function readBody(req) {
111
+ return new Promise((resolve, reject) => {
112
+ let size = 0; const chunks = [];
113
+ req.on("data", c => {
114
+ size += c.length;
115
+ if (size > MAX_BODY) { reject(new Error("body too large")); req.destroy(); return; }
116
+ chunks.push(c);
117
+ });
118
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
119
+ req.on("error", reject);
120
+ });
121
+ }
122
+
123
+ const clientIp = (req) => {
124
+ const fwd = req.headers["x-forwarded-for"];
125
+ return fwd ? String(fwd).split(",")[0].trim() : (req.socket.remoteAddress ?? "unknown");
126
+ };
127
+
128
+ const server = createServer(async (req, res) => {
129
+ try {
130
+ const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
131
+
132
+ if (req.method === "GET" && (url.pathname === "/directory.json" || url.pathname === "/")) {
133
+ return send(res, 200, registry.directory({ agents: agents() }));
134
+ }
135
+
136
+ if (req.method === "POST" && url.pathname === "/v1/seeds") {
137
+ const ip = clientIp(req);
138
+ if (!enrollAllowed(ip)) return send(res, 429, { ok: false, reason: "rate-limited" });
139
+ let payload;
140
+ try { payload = JSON.parse(await readBody(req)); }
141
+ catch { return send(res, 400, { ok: false, reason: "bad-json" }); }
142
+ const r = await registry.enroll({
143
+ url: payload?.url,
144
+ operator: payload?.operator ?? null,
145
+ region: payload?.region ?? null,
146
+ });
147
+ if (!r.ok) console.log(`[directory] enroll refused ${payload?.url}: ${r.reason}`);
148
+ else if (!r.already) console.log(`[directory] enrolled ${r.url} as ${r.uuaid}`);
149
+ return send(res, r.ok ? 200 : 400, r);
150
+ }
151
+
152
+ if (req.method === "GET" && url.pathname === "/v1/seeds") {
153
+ return send(res, 200, { seeds: registry.all().map(r => registry.status(r.uuaid)) });
154
+ }
155
+
156
+ const grantMatch = /^\/v1\/seeds\/([^/]+)\/grant$/.exec(url.pathname);
157
+ if (req.method === "GET" && grantMatch) {
158
+ const uuaid = decodeURIComponent(grantMatch[1]);
159
+ // Served unauthenticated on purpose: a grant is only usable by the
160
+ // identity it names, and using it means signing an envelope with that
161
+ // identity's key. Holding someone else's grant buys nothing.
162
+ const grant = registry.grantFor(uuaid);
163
+ if (!grant) {
164
+ const known = registry.get(uuaid);
165
+ return send(res, known ? 200 : 404,
166
+ known ? { ok: true, grant: null, reason: "tier-earns-no-grant", tier: known.tier }
167
+ : { ok: false, reason: "unknown-seed" });
168
+ }
169
+ return send(res, 200, { ok: true, grant });
170
+ }
171
+
172
+ const seedMatch = /^\/v1\/seeds\/([^/]+)$/.exec(url.pathname);
173
+ if (req.method === "GET" && seedMatch) {
174
+ const st = registry.status(decodeURIComponent(seedMatch[1]));
175
+ return send(res, st.ok ? 200 : 404, st);
176
+ }
177
+
178
+ if (req.method === "GET" && url.pathname === "/v1/health") {
179
+ return send(res, 200, {
180
+ ok: true,
181
+ seeds: registry.all().length,
182
+ issuer: registry.issuerKey,
183
+ uptimeMs: Date.now() - startedAt,
184
+ });
185
+ }
186
+
187
+ send(res, 404, { error: "not-found" });
188
+ } catch (e) {
189
+ console.error(`[directory] 500: ${e.message}`);
190
+ send(res, 500, { error: "internal-error" });
191
+ }
192
+ });
193
+
194
+ const startedAt = Date.now();
195
+ await new Promise(r => server.listen(port, host, r));
196
+ console.log(`pillar-directory listening on http://${host}:${port}`);
197
+ console.log(` issuer: ${registry.issuerKey}`);
198
+ console.log(` carrier: ${carrier ?? "(none — set --carrier)"}`);
199
+ console.log(` db: ${dbPath}`);
200
+ console.log(` seeds: ${registry.all().length}`);
201
+ console.log(` probing every ${probeMinutes} min`);
202
+
203
+ /**
204
+ * The observation loop. Every seed is probed on the same cadence, so the
205
+ * probe counts in the ladder translate directly into wall-clock time:
206
+ * at the default 10 minutes, `steady`'s 20 probes is ~3.3 hours of proof on
207
+ * top of its 1-day minimum age.
208
+ */
209
+ let probing = false;
210
+ const probeTimer = setInterval(async () => {
211
+ if (probing) return; // never overlap: a slow seed must not stack rounds
212
+ probing = true;
213
+ try {
214
+ for (const r of await registry.observeAll()) {
215
+ if (r.changed) console.log(`[directory] ${r.url} ${r.previousTier} -> ${r.tier}`);
216
+ else if (!r.alive) console.log(`[directory] ${r.url} probe failed`);
217
+ }
218
+ } catch (e) {
219
+ console.error(`[directory] probe round failed: ${e.message}`);
220
+ } finally {
221
+ probing = false;
222
+ }
223
+ }, Math.max(1, probeMinutes) * 60_000);
224
+
225
+ const shutdown = async () => {
226
+ clearInterval(probeTimer);
227
+ await new Promise(r => server.close(r));
228
+ registry.close();
229
+ process.exit(0);
230
+ };
231
+ process.on("SIGINT", shutdown);
232
+ process.on("SIGTERM", shutdown);
@@ -42,6 +42,7 @@
42
42
  * (who absorbs the fee: the seller, the buyer, or 50/50) or --payout N (the
43
43
  * parties' own number; a "mutual" agreement). No default is ever derived.
44
44
  */
45
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
45
46
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
46
47
  import { join, dirname } from "node:path";
47
48
  import { homedir } from "node:os";
@@ -31,6 +31,7 @@
31
31
  * --reply-text "zz-master 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";
@@ -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
- * 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 "../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
- const server = new CarrierServer({ dbPath, ttlDays });
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 (host === "127.0.0.1") {
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 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.`);
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
@@ -16,6 +16,7 @@
16
16
  * All commands respect --home to point at an alternate pillar directory,
17
17
  * useful for running multiple pillars on one machine (Zestro dogfood etc.).
18
18
  */
19
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
19
20
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
20
21
  import { join, dirname } from "node:path";
21
22
  import { homedir } from "node:os";
@@ -436,8 +437,12 @@ async function cmdInbox(args) {
436
437
  // senders loaded, or every fresh envelope counts as a stranger.
437
438
  loadFleetAddressBook(fleetPath(args), p);
438
439
  const pulled = await p.drainCarrierInbox();
440
+ // fromPublicKey: the sender's verified Ed25519 key (transportSignature —
441
+ // open() binds it to `from`). Lets a drain-and-reply consumer (the fleet
442
+ // courier's auto-ack, a bot) seal back to a sender who is NOT in fleet.json
443
+ // via `pillar send … --to-pubkey <fromPublicKey>`. Additive field.
439
444
  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 }));
445
+ 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
446
  }
442
447
  // Backlog: rows stored earlier (e.g. by a recv daemon) but never surfaced.
443
448
  const { decrypt } = await import("../src/net/envelope.mjs");
@@ -448,7 +453,7 @@ async function cmdInbox(args) {
448
453
  const env = JSON.parse(row.envelope_json);
449
454
  let payload = null;
450
455
  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 }));
456
+ 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
457
  }
453
458
  }
454
459
  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.13",
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.17",
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",
@@ -34,14 +43,16 @@
34
43
  "test": "node --test --test-reporter spec test/unit/*.test.mjs",
35
44
  "test:int": "node --test --test-reporter spec test/integration/*.test.mjs",
36
45
  "test:chaos": "node --test --test-reporter spec test/chaos/*.test.mjs",
37
- "test:all": "node --test --test-reporter spec test/unit/*.test.mjs test/integration/*.test.mjs test/chaos/*.test.mjs"
46
+ "test:all": "node --test --test-reporter spec test/unit/*.test.mjs test/integration/*.test.mjs test/chaos/*.test.mjs",
47
+ "preflight": "node scripts/preflight-publish.mjs",
48
+ "prepublishOnly": "node scripts/preflight-publish.mjs",
49
+ "postpublish": "node scripts/verify-publish.mjs"
38
50
  },
39
51
  "engines": {
40
- "node": ">=20"
52
+ "node": ">=22"
41
53
  },
42
54
  "dependencies": {
43
55
  "@chainsafe/libp2p-noise": "^17.0.0",
44
- "@noble/curves": "^2.3.0",
45
56
  "@noble/hashes": "^2.3.0",
46
57
  "@chainsafe/libp2p-yamux": "^8.0.1",
47
58
  "@libp2p/autonat": "^3.0.26",
@@ -58,7 +69,7 @@
58
69
  "@libp2p/websockets": "^10.1.19",
59
70
  "@multiformats/multiaddr": "^13.0.3",
60
71
  "@noble/curves": "^2.3.0",
61
- "better-sqlite3": "^11.10.0",
72
+ "better-sqlite3": "^12.11.1",
62
73
  "it-pipe": "^3.0.1",
63
74
  "it-pushable": "^3.2.4",
64
75
  "libp2p": "^3.3.8",