@uuaid/pillar 0.2.0-alpha.8 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/HOSTING-LIMITS.md +432 -0
  2. package/INTERACTION-CONVENTION.md +222 -1
  3. package/README.md +371 -17
  4. package/bin/carrier.mjs +7 -1
  5. package/bin/pillar-ask.mjs +1 -0
  6. package/bin/pillar-blob.mjs +139 -0
  7. package/bin/pillar-directory.mjs +232 -0
  8. package/bin/pillar-escrow.mjs +675 -0
  9. package/bin/pillar-responder.mjs +47 -1
  10. package/bin/pillar-seed.mjs +113 -13
  11. package/bin/pillar.mjs +345 -4
  12. package/package.json +30 -7
  13. package/src/blob/blob.mjs +510 -0
  14. package/src/blob/broker.mjs +129 -0
  15. package/src/blob/http.mjs +334 -0
  16. package/src/blob/store.mjs +55 -0
  17. package/src/escrow/box.mjs +43 -0
  18. package/src/escrow/escrow-service.mjs +538 -0
  19. package/src/escrow/rails/evm-rail.mjs +427 -0
  20. package/src/escrow/rails/index.mjs +41 -0
  21. package/src/escrow/rails/zilligon-rail.mjs +73 -0
  22. package/src/escrow/terms.mjs +368 -0
  23. package/src/escrow/token-registry.mjs +231 -0
  24. package/src/escrow/verify.mjs +119 -0
  25. package/src/escrow/zilligon.mjs +266 -0
  26. package/src/identity/keychain.mjs +27 -10
  27. package/src/identity/tier.mjs +130 -0
  28. package/src/identity/trust.mjs +63 -0
  29. package/src/index.mjs +28 -2
  30. package/src/mailbox/mailbox.mjs +41 -0
  31. package/src/net/carrier-client.mjs +93 -14
  32. package/src/net/carrier-server.mjs +212 -15
  33. package/src/net/delegation.mjs +209 -0
  34. package/src/net/envelope.mjs +81 -0
  35. package/src/net/link-offer.mjs +107 -0
  36. package/src/net/refusals.mjs +126 -0
  37. package/src/net/seed-registry.mjs +467 -0
  38. package/src/net/seed-tier.mjs +185 -0
  39. package/src/node-check.mjs +32 -0
  40. package/src/observability.mjs +141 -0
  41. package/src/pillar.mjs +206 -17
  42. package/src/vda/core.mjs +290 -0
  43. package/src/vda/directory.mjs +87 -0
  44. package/src/vda/instruments.mjs +246 -0
  45. package/src/vda/shims.mjs +77 -0
@@ -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);