@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.
- package/HOSTING-LIMITS.md +432 -0
- package/INTERACTION-CONVENTION.md +222 -1
- package/README.md +371 -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 +47 -1
- package/bin/pillar-seed.mjs +113 -13
- package/bin/pillar.mjs +345 -4
- 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 +41 -0
- 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 +206 -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
|
@@ -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";
|
|
@@ -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.
|