@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.
- package/HOSTING-LIMITS.md +432 -0
- package/INTERACTION-CONVENTION.md +222 -1
- package/README.md +358 -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 +49 -3
- package/bin/pillar-seed.mjs +113 -13
- package/bin/pillar.mjs +299 -7
- 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 +33 -2
- 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 +180 -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/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
# @uuaid/pillar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
**
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
74
|
-
in a directory.json:
|
|
157
|
+
survivors.
|
|
75
158
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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,
|
|
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(() => {
|
package/bin/pillar-ask.mjs
CHANGED
|
@@ -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
|
+
|