@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.
Files changed (45) hide show
  1. package/HOSTING-LIMITS.md +432 -0
  2. package/INTERACTION-CONVENTION.md +222 -1
  3. package/README.md +358 -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 +49 -3
  10. package/bin/pillar-seed.mjs +113 -13
  11. package/bin/pillar.mjs +299 -7
  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 +33 -2
  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 +180 -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
package/README.md CHANGED
@@ -1,18 +1,28 @@
1
1
  # @uuaid/pillar
2
2
 
3
- Signed-envelope messaging between agents. Every agent mints its own identity
4
- (UUAID + keypair) with one command, then exchanges encrypted, signed envelopes
5
- through the pillar network — a store-and-forward carrier mesh with a
6
- LimeWire/BitTorrent-style seed topology: no single carrier is the only store.
3
+ **Pillar** — **P**rotocol for **I**nter-agent **L**inked **L**edger **A**nd
4
+ **R**elay — is the secure delivery layer of **UACP**, the Universal Agent
5
+ Communication Protocol:
7
6
 
8
- **Minimal requirements:** Node.js >= 20, outbound HTTPS. That's it. Carrier
7
+ > **UACP = UUAID (who) + Envelope (what) + Pillar (how it moves).**
8
+
9
+ UUAID handles identity, discovery, keys, and resolution. The Envelope is the
10
+ canonical signed, encrypted message container. Pillar moves it: encrypted
11
+ relay, chain-hashed mailbox ledger, routing, and store-and-forward transport.
12
+
13
+ Every agent mints its own identity (UUAID + keypair) with one command, then
14
+ exchanges encrypted, signed envelopes through the pillar network — a
15
+ store-and-forward carrier mesh with a LimeWire/BitTorrent-style seed topology:
16
+ no single carrier is the only store.
17
+
18
+ **Minimal requirements:** Node.js >= 22, outbound HTTPS. That's it. Carrier
9
19
  mode needs no inbound ports, no daemon, no account. One identity = one
10
20
  keychain file + one SQLite mailbox.
11
21
 
12
22
  ## Install
13
23
 
14
24
  ```sh
15
- npm install -g @uuaid/pillar # global bins: pillar, pillar-carrier, pillar-ask, pillar-responder, pillar-seed
25
+ npm install -g @uuaid/pillar # global bins: pillar, pillar-carrier, pillar-ask, pillar-responder, pillar-seed, pillar-directory, pillar-escrow
16
26
  # or per-project:
17
27
  npm install @uuaid/pillar # then npx pillar ... / npx pillar-ask ...
18
28
  ```
@@ -28,7 +38,7 @@ pillar init --directory https://pillar.uuaid.org/directory.json
28
38
  pillar id
29
39
 
30
40
  # 3. Ask another agent a question (blocks for their ack + reply)
31
- pillar ask <their-uuaid> "<your question>"
41
+ pillar-ask <their-uuaid> "<your question>"
32
42
  ```
33
43
 
34
44
  Check your mail any time — `pillar-ask` never drains your inbox, so replies
@@ -44,11 +54,21 @@ For automated checking, put `pillar inbox --notify <cmd>` on a cron/launchd
44
54
  interval — the hook gets `PILLAR_NEW` / `PILLAR_UNREAD` / `PILLAR_UUAID` env
45
55
  vars and a JSON summary on stdin, and only fires when there is mail.
46
56
 
57
+ > **One home, one live consumer.** Never point `pillar inbox` (or `pillar-ask`)
58
+ > at a home directory that a running `pillar-responder`/`pillar recv` daemon
59
+ > owns. Carrier reads are non-destructive, but the shared mailbox is idempotent
60
+ > on envelope id — whichever process appends an envelope first fires its
61
+ > handlers, and the other sees a duplicate and stays silent. Concretely: a
62
+ > drain can consume a request the responder then never acks (cursor advance),
63
+ > and an ask from a daemon-owned home can time out even though the reply
64
+ > arrived (the daemon appended it first). Give interactive tools their own
65
+ > identity/home.
66
+
47
67
  `--directory` fetches the directory and configures every carrier + seed listed
48
68
  there, and writes every `agents[]` entry (uuaid + publicKey) into
49
69
  `<home>/fleet.json` as your address book — so step 3 works immediately, no key
50
70
  exchange needed. For peers outside any directory, pass their key once with
51
- `pillar ask <uuaid> "..." --to-pubkey HEX`. Point it at your own directory.json
71
+ `pillar-ask <uuaid> "..." --to-pubkey HEX`. Point it at your own directory.json
52
72
  to build a private mesh.
53
73
 
54
74
  ## The three always-on roles
@@ -56,38 +76,359 @@ to build a private mesh.
56
76
  | Bin | What it runs | Ongoing requirements |
57
77
  |---|---|---|
58
78
  | `pillar-responder` | Always-on auto-attendant: acks requests instantly, replies from fixed text or a command hook. Run it as a service; agents anywhere get ack+reply without you being live. | one process, outbound HTTPS |
59
- | `pillar-seed` | A carrier seed — makes you part of the p2p mesh. Any agent that lists your URL replicates every envelope to you and polls you in parallel with other seeds. | one process, one SQLite file, TLS-terminating proxy in front |
60
- | `pillar` | The full CLI (`init`, `id`, `send`, `send-group`, `recv`, `inbox`, `peers`) — optional libp2p DHT, or carrier-only. | only when used interactively |
79
+ | `pillar-seed` | A carrier seed — makes you part of the p2p mesh. Any agent that lists your URL replicates every envelope to you and polls you in parallel with other seeds. `--announce` enrolls it in a registry; staying up earns tier grants and priority delivery. | one process, one SQLite file, TLS-terminating proxy in front |
80
+ | `pillar-directory` | The seed registry: serves `directory.json`, accepts permissionless seed enrollment, probes enrolled seeds, and promotes them. Run one if you publish a directory. | one process, one SQLite file |
81
+ | `pillar` | The full CLI (`init`, `id`, `send`, `send-group`, `recv`, `inbox`, `trust`, `tier`, `peers`) — optional libp2p DHT, or carrier-only. | only when used interactively |
61
82
 
62
83
  ```sh
63
84
  pillar-responder --home ~/.pillar-responder --reply-text "ack" &
64
85
  pillar-seed --port 8799 --db /var/lib/pillar/seed.db & # loopback by default
86
+
87
+ # join the mesh for real (one command, behind your TLS proxy):
88
+ pillar-seed --host 0.0.0.0 --public-url https://seed.example.org \
89
+ --announce https://pillar.uuaid.org --operator you --region eu
90
+ pillar-seed --status --announce https://pillar.uuaid.org # your rung + what's left
91
+ ```
92
+
93
+ ## Abuse resistance — consent at the edge, budgets at the carrier
94
+
95
+ Identity is unforgeable (the sender derives from the signing key), the
96
+ carrier is rate-limited and payload-blind, and **consent lives with the
97
+ recipient**:
98
+
99
+ ```sh
100
+ pillar trust set directory # only address-book senders reach handlers
101
+ pillar trust allow uuaid:... # enroll a specific sender (any policy)
102
+ pillar trust block uuaid:... # never deliver (stored as quarantined evidence)
103
+ pillar trust quarantine # inspect gated mail; --release ID to accept one
104
+ ```
105
+
106
+ Policies: `open` (default — today's behavior), `directory` (address-book
107
+ senders only), `allowlist` (explicit allow list only). Gated mail is
108
+ **quarantined, never dropped** — it stays in the chain-hashed mailbox with a
109
+ reason, invisible to handlers. Genuine `ack`/`reply` receipts for your own
110
+ outbox always pass. `pillar-responder` defaults its command hook to the
111
+ `directory` policy: strangers still get ack + a fixed reply (first contact
112
+ keeps working), but their payload never reaches your hook process; per-sender
113
+ reply rate is capped.
114
+
115
+ ## Tier grants — qualified volume for foundations, orgs, enterprises
116
+
117
+ Default (community) carrier budgets: 120 envelopes/min per sender, 512 KiB
118
+ per envelope. Organizations that qualify through the UUAID Foundation's
119
+ review receive a **signed tier grant** — a portable JSON credential
120
+ (Ed25519, expiry-bound, offline-verifiable) that raises their budgets at any
121
+ carrier that trusts the issuer:
122
+
123
+ | Tier | Envelopes/min | Max body |
124
+ |---|---|---|
125
+ | community (default, no grant) | 120 | 512 KiB |
126
+ | supporter | 300 | 1 MiB |
127
+ | org | 600 | 2 MiB |
128
+ | enterprise | 1200 | 2 MiB |
129
+ | foundation | 3000 | 2 MiB |
130
+
131
+ There are two ways to hold a grant. The first is off-wire: an issuer decides
132
+ you qualify and signs one. The second is earned — run a public seed and the
133
+ registry promotes you and issues one automatically (see Seed topology). The
134
+ carrier cannot tell the two apart, and does not care.
135
+
136
+ ```sh
137
+ # Issuer (e.g. the foundation identity) signs a grant:
138
+ pillar tier issue uuaid:foundation:agent:... org --expires 2027-01-01T00:00:00Z
139
+ # Grantee installs it (auto-attached to every carrier deliver):
140
+ pillar tier install tier-grant-<fp>.json
141
+ # Carrier operator opts in:
142
+ pillar-carrier --tier-issuers <issuer-pubkey-hex>
65
143
  ```
66
144
 
145
+ Grants are verified locally (signature + expiry + ceiling clamp) — no
146
+ registry lookup, no account, consistent with the self-authenticating design.
147
+ An invalid or foreign grant silently falls back to community budgets; it can
148
+ never break plain delivery. Wire format unchanged: the grant rides an HTTP
149
+ header (`x-pillar-tier-grant`), not the envelope.
150
+
67
151
  ## Seed topology — how messages survive
68
152
 
69
153
  On send, `deliver()` replicates the envelope to **every** carrier in your set
70
154
  in parallel — any one accepting it is success. On receive, you poll **all** of
71
155
  them; duplicates across seeds are deduped by envelope id, so handlers fire
72
156
  exactly once. Kill any single seed and delivery continues through the
73
- survivors. To join the mesh as a seed: TLS-terminate a public URL and list it
74
- in a directory.json:
157
+ survivors.
75
158
 
76
- ```json
77
- { "url": "https://your-host", "operator": "you", "region": "eu", "since": "2026-08-20" }
159
+ ### Joining is permissionless
160
+
161
+ TLS-terminate a public URL and announce it:
162
+
163
+ ```sh
164
+ pillar-seed --host 0.0.0.0 --public-url https://seed.example.org \
165
+ --announce https://pillar.uuaid.org --operator you --region eu
78
166
  ```
79
167
 
80
- `pillar init --directory <that-directory>` picks it up automatically.
168
+ The registry fetches `https://seed.example.org/v1/seed-info` and checks the
169
+ Ed25519 signature over it. That document names the URL and is signed by the
170
+ seed's key, so the check proves the announcer controls both — which is the
171
+ entire admission test. No account, no review, no waiting on whoever owns the
172
+ directory file. `pillar init --directory <that-directory>` then picks you up
173
+ automatically, as it always did.
174
+
175
+ ### Staying up is what earns something
176
+
177
+ The registry probes every enrolled seed on a schedule: liveness, plus a real
178
+ envelope round-tripped through it (delivered, stored, and served back to an
179
+ authenticated poller). Only observations the registry makes itself count —
180
+ a seed's own counters are published at `/v1/seed-info` for display and are
181
+ ignored by the ladder, because an operator controls them.
182
+
183
+ | Rung | Earned at | What it grants the operator |
184
+ |---|---|---|
185
+ | `probation` | enrollment | listed last in `directory.json` |
186
+ | `steady` | 1 day, 20 probes ≥95% up, 10 round-trips | `supporter` budgets |
187
+ | `durable` | 7 days, 150 probes ≥99% up, 100 round-trips | `org` budgets + **priority delivery** |
188
+ | `anchor` | 30 days, 700 probes ≥99.5% up, 1000 round-trips | `enterprise` budgets + priority, listed first |
189
+
190
+ The reward lands on the operator's **own** identity as a signed tier grant
191
+ (see Abuse resistance below), so carrying other agents' traffic is the
192
+ cheapest route to raised budgets for your own. Grants carry a 14-day expiry
193
+ and are re-issued as service continues; a seed that goes dark is simply not
194
+ renewed, which is why there is no revocation list. Three consecutive failed
195
+ probes drop a seed to `probation` and out of `directory.json` entirely —
196
+ handing a fresh agent a host that just failed three probes is how you get a
197
+ first-run experience of silent message loss.
198
+
199
+ ### Priority delivery
200
+
201
+ A `durable` or `anchor` grant is signed with `priority: true`. Carriers that
202
+ trust the issuer keep reserves that community traffic cannot touch: the last
203
+ 15% of the store, a separate per-IP send budget, and 16 of the 64 long-poll
204
+ slots. Under normal load the two lanes are identical — the privilege only
205
+ becomes visible at the moment a carrier starts shedding. Because `priority`
206
+ sits inside the signed body, it can be neither added to a grant after issue
207
+ nor stripped from one.
208
+
209
+ On the client, `CarrierClient` takes the `seeds[].tier` values from
210
+ `directory.json` as `carrierTiers` and returns the receipt from the most
211
+ proven carrier that accepted. Delivery itself is unchanged — still fan-out to
212
+ every carrier — but the `seq` and carrier a caller records now name a seed
213
+ likely to still be there.
81
214
 
82
215
  ## Protocol
83
216
 
84
217
  Envelope v2 (`uuaid-pillar-envelope/2`): mandatory E2E
85
218
  (x25519-hkdf-sha256-aes256gcm) when a recipient key is known, JCS-canonical
86
219
  Ed25519 transport signature. Carrier API: `/carrier/v1` (POST
87
- `/v1/envelopes`, GET `/v1/inbox/<uuaid>` long-poll, GET `/v1/health`).
220
+ `/v1/envelopes`, GET `/v1/inbox/<uuaid>` long-poll, GET `/v1/health`, GET
221
+ `/v1/seed-info` when running as an enrolled seed). Registry API: POST
222
+ `/v1/seeds` (enroll), GET `/v1/seeds[/<uuaid>[/grant]]`, GET
223
+ `/directory.json`.
88
224
  App-layer convention: kinds `request`/`reply`/`ack`/`message` (see
89
225
  INTERACTION-CONVENTION.md). Directory:
90
- `https://pillar.uuaid.org/directory.json` (protocol, carrier, agents, seeds).
226
+ `https://pillar.uuaid.org/directory.json` (protocol, carrier, issuer, agents,
227
+ seeds — each seed carrying its earned `tier`, `priority`, and observed
228
+ `uptime`).
229
+
230
+ ### Delegated envelopes (UACP/1.0 ext)
231
+
232
+ Since `0.2.0-alpha.14` a carrier ALSO accepts envelopes whose sender did not
233
+ sign personally: a long-lived persona (the **principal**) can authorize a
234
+ short-lived device identity (the **delegate**) to act for it. The principal
235
+ signs a delegation document (`uuaid-delegation/1`, JCS-canonical Ed25519,
236
+ naming both UUAIDs + public keys, a `kinds` allowlist, and a
237
+ `notBefore`/`notAfter` window); the delegate embeds that document in the
238
+ envelope's optional `delegation` field — INSIDE the signed body — and signs
239
+ the envelope with its own key (`transportSignature.keyId` = delegate UUAID).
240
+
241
+ Verification (`open()` in `src/net/envelope.mjs`, doc rules in
242
+ `src/net/delegation.mjs`) — all five must pass:
243
+
244
+ 1. envelope signature valid under the **delegate's** pubkey, and
245
+ `transportSignature.keyId` === delegate UUAID;
246
+ 2. delegation document signature valid under the **principal's** pubkey,
247
+ `delegation.principal` === envelope `sender`, and `delegation.delegate`
248
+ === the signing key's UUAID;
249
+ 3. local-id binding for BOTH identities — `sha256(pubkey)[:16]`
250
+ (UUID-dash formatted) === the UUAID's local-id segment;
251
+ 4. `now` within `[notBefore, notAfter]`;
252
+ 5. envelope `kind` covered by the delegation `kinds` allowlist (`*` = all).
253
+
254
+ Any failure rejects `403` with `delegation-invalid:<reason>` (reason strings
255
+ match `@uuaid/pillar-client`'s `verifyDelegation`/`verifyDelegatedEnvelope`).
256
+ When `delegation` is ABSENT, verification is byte-for-byte the legacy
257
+ self-authenticating-sender rule — zero regression. Storage, mailbox
258
+ attribution, and rate limits stay keyed to the envelope `sender`, which for
259
+ delegated mail IS the principal.
260
+
261
+ Mesh caveat: carriers running older builds still reject delegated envelopes
262
+ (`keyId-sender-mismatch`). `deliver()` fans out to every configured carrier
263
+ and succeeds if ANY accepts, but delegated mail only lands where the
264
+ extension runs — today that is `https://pillar-sfo.uuaid.org`; the primary
265
+ `https://pillar.uuaid.org` upgrade awaits an owner decision. Clients SHOULD
266
+ treat per-carrier outcomes (`deliver()`'s `all` receipts) as partial
267
+ acceptance until the mesh is uniform.
268
+
269
+ ## pillar-escrow — blind escrow on the ZGN / EVM rails
270
+
271
+ `pillar-escrow` (`bin/pillar-escrow.mjs`) is a sixth CLI binary, shipped since
272
+ alpha.13 and undocumented until this section. It runs a blind-escrow exchange
273
+ between three ordinary identities — seller, buyer, escrow — where the escrow
274
+ never sees the traded content and holds only a 32-byte content key.
275
+
276
+ Flow (`bin/pillar-escrow.mjs` header comment):
277
+
278
+ ```
279
+ seller --offer(signed terms + K)--> escrow --offer-ack--> seller
280
+ seller --deliverable(ciphertext)-------------------------> buyer (escrow never sees it)
281
+ buyer --accept(signed terms)-----> escrow --accept-ack--> buyer
282
+ buyer --rail transfer of price---> escrow's rail handle
283
+ buyer --paid {offerId,txId}------> escrow
284
+ escrow attests the payment from ITS OWN wallet/rail record, then:
285
+ rail transfer of payout ------> seller
286
+ key {K, receipt} -------------> buyer (buyer opens the box, checks the hash)
287
+ receipt -----------------------> seller
288
+ ```
289
+
290
+ Subcommands:
291
+
292
+ - `init` — create a home identity and a `zilligon.json` credential template
293
+ - `run-escrow` — run the always-on escrow service, journal-first and
294
+ crash-recoverable (`src/escrow/escrow-service.mjs`)
295
+ - `sell` — one-shot seller flow: seal the deliverable, send signed terms,
296
+ wait for release
297
+ - `buy` — one-shot buyer flow: pay on the rail, then open the box once K
298
+ arrives
299
+
300
+ What the escrow can and cannot see:
301
+
302
+ **It is a trusted party for money.** "Blind" means the escrow never sees the
303
+ deliverable (it holds only the 32-byte content key K); it does receive the buyer's
304
+ payment into its own wallet and pays the seller from it (`bin/pillar-escrow.mjs` flow,
305
+ `src/escrow/escrow-service.mjs`). No on-chain contract enforces release — the parties
306
+ trust the escrow operator's journal-first process and its signed receipts.
307
+
308
+ - **Can see:** the signed terms (parties, price, content hash, fee
309
+ allocation), the 32-byte content key K, and its own rail transactions.
310
+ - **Cannot see:** the deliverable itself. It is sealed with AES-256-GCM under
311
+ K (`src/escrow/box.mjs`) and sent seller→buyer directly.
312
+ - Payment attestation is escrow-side and independent of the buyer's claimed
313
+ `txId` — it is read from the escrow's own wallet rows / chain receipt, not
314
+ derived from the buyer's story (`src/escrow/zilligon.mjs`,
315
+ `src/escrow/rails/evm-rail.mjs`).
316
+
317
+ Rails (`src/escrow/rails/`):
318
+
319
+ - **Zilligon ZGN** (`zilligon-rail.mjs`) — wraps a `ZilligonClient`; amounts
320
+ are integer Gons.
321
+ - **EVM** (`evm-rail.mjs`) — native coin (POL/ETH) and ERC-20. Payout is
322
+ sign-then-journal-then-broadcast with a fixed nonce, so crash recovery
323
+ re-broadcasts the identical transaction instead of double-spending. The
324
+ signing key is never stored on the rail object; a `privateKeyProvider`
325
+ callback is invoked per signature (the default reads `PILLAR_EVM_KEY` from
326
+ the environment at call time).
327
+ - The token/chain allowlist (`src/escrow/token-registry.mjs`) is fail-closed
328
+ once wired: an unregistered chain or token is rejected at offer time.
329
+ Inclusion is an explicit reviewer decision, not automatic — the registry
330
+ records provenance on every entry.
331
+
332
+ Journal-first recovery: every state transition and release step is appended
333
+ to a JSONL journal before (or immediately after, with a recovery marker) the
334
+ irreversible rail action, so `reconcile()` can re-run release after a crash
335
+ without double-paying (`src/escrow/escrow-service.mjs`).
336
+
337
+ **Fees and terms are between the two parties, not the escrow.** The rail's
338
+ tip fee sits outside the signed agreement; the parties choose who absorbs it
339
+ (`--fee-allocation seller|buyer|split`, or an explicit `--payout` amount) —
340
+ the escrow never derives a default (`src/escrow/terms.mjs`).
341
+
342
+ ## Verifiable Delivery Attestation (VDA-1)
343
+
344
+ `src/vda/` implements **VDA-1**, a delivery-attestation format proposed as
345
+ **IAASO-3901** at the IAASO governance registry. **The proposal is currently
346
+ in ballot — it has not been adopted.** It is not exported from
347
+ `src/index.mjs`, and the `package.json` `exports` map (`.`, `./keychain`, `./jcs`,
348
+ `./tier`, `./envelope`, `./e2e`, `./carrier-client`, `./package.json`) blocks deep imports
349
+ of `src/vda/*` from the npm package — use it from a checkout of this repository, or wait
350
+ for a `./vda` export (proposed, not shipped).
351
+
352
+ What it provides:
353
+
354
+ - Claim normalization to a closed set of per-kind field allowlists
355
+ (`transfer`, `data-delivery`, `message-delivery`, `media-availability`,
356
+ `fiat-settlement`), dropping unknown fields before hashing
357
+ (`src/vda/core.mjs`).
358
+ - Content-addressed attestation ids and a signed (Ed25519 over JCS)
359
+ attestation envelope, verifiable fully offline.
360
+ - Composite receipts and quorum over multiple attestations (worst-verdict
361
+ wins).
362
+ - An append-only verifier key directory (`src/vda/directory.mjs`) —
363
+ `directoryId = sha256(jcs(records))`, so two holders can compare
364
+ directories by hash; rotation is append, never rewrite.
365
+ - Instruments (`src/vda/instruments.mjs`): `transfer` claims verify against
366
+ the rail's own chain receipt (`src/escrow/verify.mjs`'s
367
+ `TransferVerifier`); `data-delivery` uses a recipient-signed delivery ack;
368
+ `fiat-settlement` wraps an issuer artifact and can reach at best
369
+ `issuer-attested`.
370
+ - Shims (`src/vda/shims.mjs`) that translate x402 settlements and emit A2A
371
+ artifacts / an MCP tool descriptor — translation only, never an upgrade in
372
+ trust.
373
+
374
+ Closed verdict set: `verified`, `failed`, `pending`, `instrument-blind`,
375
+ `issuer-attested`, `unsupported`. A rail structurally unable to answer a
376
+ claim returns `instrument-blind`, never a silent negative
377
+ (`src/vda/core.mjs`).
378
+
379
+ ## What a tier grant is — and is not
380
+
381
+ A tier grant (`src/identity/tier.mjs`) raises a sender's carrier budget for
382
+ rate and body size — see "Tier grants" above for the ladder. It is a
383
+ **throughput entitlement, not a vetting, KYC, or compliance credential.**
384
+
385
+ - Hard ceilings any grant can reach, regardless of what it claims:
386
+ `ABSOLUTE_MAX_RATE` = 6,000 envelopes/min, `ABSOLUTE_MAX_BODY` = 2 MiB
387
+ (`src/identity/tier.mjs`).
388
+ - Issuing a grant does not identity-verify, background-check, or vet the
389
+ holder. "Qualification" — whatever process decides who receives one — is
390
+ entirely off-wire; this module only makes an already-decided grant
391
+ unforgeable and portable.
392
+ - A grant **cannot be revoked before its `expires` date.** The only lever a
393
+ carrier operator has is to stop trusting the issuer's key outright
394
+ (`src/net/carrier-server.mjs`).
395
+ - Carrier trust of an issuer is opt-in per carrier
396
+ (`pillar-carrier --tier-issuers <pubkey-hex>`); a carrier that trusts no
397
+ issuer treats every sender as community-tier no matter what grant is
398
+ presented.
399
+
400
+ ## Limits and hazards for integrators
401
+
402
+ - **Every number is in [`HOSTING-LIMITS.md`](HOSTING-LIMITS.md).** It covers
403
+ the long-poll cap and cadence, body sizes, rate windows, what 429/503/413
404
+ mean and what to do about each, push/wake, and proxy settings for hosts.
405
+ Each number cites its source line.
406
+ - **One home, one live consumer.** A daemon-owned home directory and an
407
+ interactive `pillar inbox` run against the same home can race on envelope
408
+ id — run one consumer per home.
409
+ - **Receipts are app-layer, not protocol-layer.** The `ack` envelope IS the
410
+ receipt (see `INTERACTION-CONVENTION.md`, "carrier limitation #5"). The
411
+ numbered list that citation implies was never published, and no items #1-#4
412
+ exist in this tree. `HOSTING-LIMITS.md` is now the published list of what a
413
+ carrier enforces. It does not use that numbering.
414
+ - **Carrier retention is time-bounded.** Envelopes are retained until TTL,
415
+ default 14 days, then vacuumed (`src/net/carrier-server.mjs`).
416
+ - **No metering.** The carrier's rate limiter is an in-memory sliding window
417
+ that resets on every restart; nothing records cumulative use across time.
418
+ A tier grant changes the window's ceiling, not whether usage is tracked.
419
+ - **Grants are not revocable before expiry** — see "What a tier grant is —
420
+ and is not" above; plan grant lifetimes accordingly.
421
+
422
+ ## Source, provenance and releases
423
+
424
+ Apache-2.0 (`LICENSE`). This repository is the recovered source for
425
+ `@uuaid/pillar` — see `RECOVERY.md` for how it was reconstituted and kept in
426
+ sync with the published package. The `0.2.0-alpha.13` tarball this working
427
+ tree is synced to: sha256
428
+ `e80d1e339d7cce53dcd01a3b637aab96adfc7bdb22ccbf08cfa9841f4fda3f5f`,
429
+ 105,015 bytes, 38 files (`npm pack @uuaid/pillar@0.2.0-alpha.13`). Releases
430
+ (`npm publish`) are an owner action; nothing in this repository publishes on
431
+ its own.
91
432
 
92
433
  ## License
93
434
 
package/bin/carrier.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  * authenticate per-request with their own keys. No accounts, no state
11
11
  * about who exists — pure store-and-forward.
12
12
  */
13
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
13
14
  import { CarrierServer } from "../src/net/carrier-server.mjs";
14
15
 
15
16
  function parseArgs(argv) {
@@ -29,12 +30,17 @@ const port = Number(args.port ?? process.env.CARRIER_PORT ?? 8799);
29
30
  const host = args.host ?? process.env.CARRIER_HOST ?? "127.0.0.1";
30
31
  const dbPath = args.db ?? process.env.CARRIER_DB ?? "carrier.db";
31
32
  const ttlDays = Number(args.ttl ?? process.env.CARRIER_TTL_DAYS ?? 14);
33
+ // Tier grants: comma-separated issuer pubkeys (hex) whose signed grants this
34
+ // carrier honors for raised budgets. Unset = all senders get community tier.
35
+ const tierIssuers = String(args["tier-issuers"] ?? process.env.CARRIER_TIER_ISSUERS ?? "")
36
+ .split(",").map(s => s.trim()).filter(s => /^[0-9a-f]{64}$/i.test(s));
32
37
 
33
- const server = new CarrierServer({ dbPath, ttlDays });
38
+ const server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers });
34
39
  const addr = await server.listen(port, host);
35
40
  console.log(`carrier listening on ${addr.address}:${addr.port}`);
36
41
  console.log(` db: ${dbPath}`);
37
42
  console.log(` ttl: ${ttlDays} days`);
43
+ console.log(` tiers: ${tierIssuers.length ? `${tierIssuers.length} trusted issuer(s)` : "disabled (community budgets for all)"}`);
38
44
 
39
45
  // Vacuum expired envelopes hourly.
40
46
  const vacuumTimer = setInterval(() => {
@@ -10,6 +10,7 @@
10
10
  * Exits 0 on reply(status ok), 3 on reply(status error), 4 on timeout.
11
11
  * See INTERACTION-CONVENTION.md for the wire contract.
12
12
  */
13
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
13
14
  import { existsSync, readFileSync } from "node:fs";
14
15
  import { join } from "node:path";
15
16
  import { homedir } from "node:os";
@@ -0,0 +1,139 @@
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 KEY_ID] [--ttl-ms N]
7
+ * pillar-blob get --ref-file FILE --expect-envelope ID [--key-id KEY_ID]
8
+ * [--broker URL] [--out FILE|-]
9
+ *
10
+ * --key-id is optional. Without it (and without PILLAR_PRESENTATION_KEY_ID or
11
+ * config.presentationKeyId) the client presents the registry badge's key when
12
+ * a badge names this identity's key, else "self-certified:ed25519": every
13
+ * identity `pillar init` mints derives its UUAID from its key, so it needs no
14
+ * badge.
15
+ *
16
+ * The broker never receives a DEK. `send` prints metadata only; the secret ref
17
+ * exists solely inside the recipient-encrypted Pillar envelope.
18
+ *
19
+ * `get` names the sender on stderr. A self-certified sender (its UUAID is
20
+ * exactly the uuaid:foundation:agent UUAID of the key that signed the ref)
21
+ * is a proven key holder whose identity is UNVERIFIED: it is shown as its
22
+ * UUAID only, never with a name or badge mark from the registry.
23
+ */
24
+ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
25
+ import { readFileSync, writeFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { join } from "node:path";
28
+ import { Keychain } from "../src/identity/keychain.mjs";
29
+ import { Pillar } from "../src/pillar.mjs";
30
+ import { PillarBlobHttpClient, BLOB_BROKERS, DEFAULT_BLOB_BROKER } from "../src/blob/http.mjs";
31
+ import { fetchBrokerBlob, DEFAULT_BLOB_TTL_MS, BlobError } from "../src/blob/blob.mjs";
32
+ import { refusalLine } from "../src/net/refusals.mjs";
33
+
34
+ function usage(code = 2) {
35
+ process.stderr.write("usage: pillar-blob send|get (run without arguments to see the source header)\n");
36
+ process.exit(code);
37
+ }
38
+
39
+ function parse(argv) {
40
+ const out = {};
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const key = argv[i];
43
+ if (!key.startsWith("--") || i + 1 >= argv.length) usage();
44
+ out[key.slice(2)] = argv[++i];
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function homeDir(args) {
50
+ return args.home ?? process.env.PILLAR_HOME ?? join(homedir(), ".pillar");
51
+ }
52
+
53
+ function loadHome(args) {
54
+ const home = homeDir(args);
55
+ const keychain = new Keychain(join(home, "keychain.json"));
56
+ keychain.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
57
+ let config = {};
58
+ try { config = JSON.parse(readFileSync(join(home, "config.json"), "utf-8")); } catch (_error) {}
59
+ // Optional: omitted, the client resolves badge key -> self-certified:ed25519.
60
+ const keyId = args["key-id"] ?? process.env.PILLAR_PRESENTATION_KEY_ID ?? config.presentationKeyId ?? null;
61
+ const broker = args.broker ?? process.env.PILLAR_BLOB_BROKER ?? DEFAULT_BLOB_BROKER;
62
+ const client = new PillarBlobHttpClient({ baseUrl: broker, keychain, keyId });
63
+ return { home, keychain, config, client, keyId };
64
+ }
65
+
66
+ // A broker or carrier refusal prints one line, `<code>: <fix>`: the broker's
67
+ // code/fix, or the carrier's reason and its fix (src/net/refusals.mjs).
68
+ // Anything else keeps the old `pillar-blob: <code>: <message>` line. Exit
69
+ // codes follow the client's own code (legacyCode when the broker renamed it),
70
+ // so they are what they were before alpha.18.
71
+ function fail(error) {
72
+ const code = error instanceof BlobError ? (error.legacyCode ?? error.code) : "error";
73
+ process.stderr.write(`${refusalLine(error) ?? `pillar-blob: ${code}: ${error?.message ?? String(error)}`}\n`);
74
+ process.exit(code === "blob-not-found" || code === "blob-expired" ? 3 : code.includes("integrity") ? 5 : 6);
75
+ }
76
+
77
+ const [command, ...rest] = process.argv.slice(2);
78
+ if (!command) usage();
79
+ const args = parse(rest);
80
+
81
+ try {
82
+ if (command === "send") {
83
+ if (!args.to || !args["recipient-pubkey"] || !args.data) usage();
84
+ const { home, keychain, config, client } = loadHome(args);
85
+ const carriers = (args.carriers ?? process.env.PILLAR_CARRIERS ?? (config.carriers ?? []).join(","))
86
+ .split(",").map((value) => value.trim()).filter(Boolean);
87
+ if (!carriers.length) throw new Error("no carriers configured");
88
+ const pillar = new Pillar({
89
+ keychain,
90
+ mailboxPath: join(home, "mailbox.db"),
91
+ carriers,
92
+ enableLibp2p: false,
93
+ blobClient: client,
94
+ });
95
+ await pillar.start();
96
+ try {
97
+ const result = await pillar.sendBlob({
98
+ recipient: args.to,
99
+ recipientPublicKey: args["recipient-pubkey"],
100
+ data: args.data === "-" ? readFileSync(0) : readFileSync(args.data),
101
+ ttlMs: Number(args["ttl-ms"] ?? DEFAULT_BLOB_TTL_MS),
102
+ });
103
+ process.stdout.write(`${JSON.stringify({
104
+ envelope_id: result.envelope.id,
105
+ via: result.via,
106
+ plaintext_bytes: result.blob.size,
107
+ ciphertext_sha256: result.blob.hash,
108
+ })}\n`);
109
+ } finally {
110
+ await pillar.stop();
111
+ }
112
+ } else if (command === "get") {
113
+ if (!args["ref-file"] || !args["expect-envelope"]) usage();
114
+ const loaded = loadHome(args);
115
+ const { keychain } = loaded;
116
+ let { client } = loaded;
117
+ const parsed = JSON.parse(readFileSync(args["ref-file"], "utf-8"));
118
+ const ref = parsed.blobRef ?? parsed;
119
+ // With no --broker or PILLAR_BLOB_BROKER, redeem where the sender stored it,
120
+ // if that is one of the known brokers; any other broker must be named explicitly.
121
+ if (!args.broker && !process.env.PILLAR_BLOB_BROKER && BLOB_BROKERS.includes(ref?.broker)) {
122
+ client = new PillarBlobHttpClient({ baseUrl: ref.broker, keychain, keyId: loaded.keyId });
123
+ }
124
+ const result = await fetchBrokerBlob({
125
+ ref,
126
+ envelopeId: args["expect-envelope"],
127
+ recipient: keychain._identity.uuaid,
128
+ client,
129
+ });
130
+ if (!args.out || args.out === "-") process.stdout.write(result.data);
131
+ else writeFileSync(args.out, result.data, { mode: 0o600 });
132
+ const sender = String(result.sender).replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
133
+ const proof = result.senderProof === "registry-badge" ? "registry badge" : "unverified identity, self-certified key";
134
+ process.stderr.write(`pillar-blob: redeemed ${result.size} plaintext byte(s) from ${sender} (${proof})\n`);
135
+ } else usage();
136
+ } catch (error) {
137
+ fail(error);
138
+ }
139
+