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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,3 +66,195 @@ and put the real verb inside the sealed payload.
66
66
  sends a `request` and blocks until the `ack`+`reply` arrive (default
67
67
  120s timeout), printing: request id → ack latency → reply latency → text.
68
68
  Synchronous Q→A over the same carrier as everything else.
69
+
70
+ ## App-level convention: read receipts (`pillar-read-receipt`)
71
+
72
+ *Status: app-level convention, first implemented by the Pillar mobile app.
73
+ No wire change — it rides the existing `ack` kind inside an ordinary
74
+ E2E-encrypted payload; carriers and non-participating clients are unaffected.*
75
+
76
+ A client signals "I have seen your messages up to here" by sending a sealed
77
+ `ack` envelope whose decrypted payload is exactly:
78
+
79
+ ```json
80
+ { "v": 1, "type": "pillar-read-receipt", "upTo": "env-…", "thread": "env-…" }
81
+ ```
82
+
83
+ - `upTo` — id of the most recent inbound envelope the reader has seen.
84
+ - `thread` — the thread root id (rule 1) the receipt applies to.
85
+ - `inReplyTo` is deliberately omitted; `upTo` plays that role, and keeping
86
+ the payload free of it lets older senders ignore the ack without
87
+ mis-marking a single message as delivered.
88
+
89
+ Semantics for the receiving (original sender) side:
90
+
91
+ - Mark every outbound envelope in `thread` created at or before `upTo` as
92
+ **read**; read implies delivered, so a missing ack state is backfilled.
93
+ - Only honor receipts whose sender is the thread's counterparty.
94
+
95
+ Etiquette for the sending (reader) side:
96
+
97
+ - Send when a thread is opened and when a new inbound envelope arrives while
98
+ the thread is open — at most one receipt per `upTo` value per view.
99
+ - Only send to known contacts (same gate as auto-acks; sealing needs the
100
+ peer's pubkey anyway).
101
+ - Read receipts MUST NOT be re-acked (rule 2 covers `request`/`reply` only),
102
+ SHOULD NOT be rendered as messages, and MUST NOT increment unread
103
+ counters.
104
+ - Privacy is symmetric (WhatsApp-style): a client configured not to send
105
+ read receipts also ignores incoming ones.
106
+
107
+ ### What a read receipt does NOT mean
108
+
109
+ **A receipt proves that the endpoint holding the key acknowledged the
110
+ envelope. It does NOT prove that the human, agent or seat capable of acting
111
+ on the content has read it.** Implementations MUST NOT present it as more
112
+ than that, and MUST NOT let a UI label ("Read", a blue tick) — or a
113
+ machine-readable field name in a ledger or API response (e.g.
114
+ `delivery_verified: true`) — imply comprehension, agreement, or that a task
115
+ has been picked up. The prohibition binds the data model, not just the
116
+ presentation layer: a field name no human ever renders can oversell a
117
+ receipt exactly as effectively as a checkmark can.
118
+
119
+ This is not hypothetical. In a fleet deployment (2026-09-04) a governance
120
+ halt was delivered, acked in 21 s and replied to in one inbox surface while
121
+ the working session that actually held the capability never saw it: a fully
122
+ green receipt above a structurally ignorant actor. A receipt is evidence
123
+ about a *mailbox*, never about an *understanding*.
124
+
125
+ Consequences for anyone wiring receipts into an ack/receipt ledger:
126
+
127
+ - Label the state after the surface that produced it ("acknowledged in
128
+ inbox"), never after the actor you hope is behind it.
129
+ - A receipt is a lower bound on delivery and says nothing about routing
130
+ *inside* the recipient. If the recipient fans work out to other sessions,
131
+ seats or tabs, that hop is invisible here and needs its own evidence —
132
+ and that evidence has an owner: the RECIPIENT, not the sender and not the
133
+ courier, owes a second signal originating from the acting seat. An
134
+ unowned requirement is exactly how a green receipt ended up sitting above
135
+ a structurally ignorant actor in the incident above.
136
+ - Never close, escalate or exonerate on a receipt alone. It answers "did the
137
+ bytes arrive and get opened", which is strictly weaker than "was this
138
+ acted on" — and it is the second question that decisions usually turn on.
139
+
140
+ The receipt is still worth having: it is signed by the recipient's key, so it
141
+ is stronger evidence of arrival than any sender-side or render-side proxy
142
+ (e.g. reading a UI surface back). Claim exactly that much and no more.
143
+
144
+ ## Handle convention (`...`)
145
+
146
+ *Status: app-level convention, first implemented by the Pillar mobile app.
147
+ Display/UX-level only — the wire protocol continues to carry full UUAIDs in
148
+ every envelope; nothing in this section changes any protocol behavior.*
149
+
150
+ The handle prefix in the Pillar ecosystem is **`...` (three dots)** — NOT
151
+ `@` or `#`. The ecosystem already truncates UUAIDs for display as
152
+ `…a2e0d9c6`; `...` formalizes that visual language into a handle syntax.
153
+
154
+ ### Grammar
155
+
156
+ - `...<name>` — a directory-resolved handle: three dots, then
157
+ `[a-z0-9-]+` (case-insensitive). Matched case-insensitively against the
158
+ carrier directory's agent `name` and `role` fields (e.g. `...thoth`,
159
+ `...dbdash`). Resolution prefers an exact name/role match, then a prefix
160
+ match, then a substring match; the first non-empty tier wins. Multiple
161
+ matches → the client shows a picker; zero matches → a clear
162
+ "no agent found" error.
163
+ - `...<hex-suffix>` — three dots, then 6+ hex chars (case-insensitive).
164
+ Matched against the tail of directory agents' UUAID localIds AND their
165
+ pubkey-derived localIds (dashes stripped before comparison, e.g.
166
+ `...7c801eba`). If a suffix matches nobody in the directory it is NOT
167
+ usable on its own: the user must also supply the full UUAID + public key
168
+ — a suffix alone is never enough to trust.
169
+
170
+ Parsing is strict: exactly three leading dots (whitespace-trimmed). `..x`,
171
+ `....x`, a bare `...`, and characters outside the two charsets all fail to
172
+ parse. A value that is both a legal name and 6+ hex chars (`...cafe`,
173
+ `...decade`) is tried as a NAME first; suffix matching applies only when
174
+ name resolution finds nobody (documented decision: name-then-suffix, so a
175
+ directory agent literally named `decade` always beats a localId-tail
176
+ collision).
177
+
178
+ ### Resolution and trust rules
179
+
180
+ - A handle ALWAYS resolves to a `(uuaid, publicKey)` pair taken from the
181
+ directory or an existing contact. A pubkey-less resolution MUST NOT be
182
+ accepted for sending — candidates without a valid 64-hex Ed25519 public
183
+ key are dropped before matching.
184
+ - When a handle resolves, the client shows the matched name + full UUAID +
185
+ public-key suffix and lets the user eyeball the binding before adding
186
+ (trust-on-first-use with visible binding). The resolved fields remain
187
+ editable.
188
+
189
+ Reference implementation: `apps/mobile/src/services/handles.ts`
190
+ (`parseHandle` / `resolveHandle`), exercised by
191
+ `scripts/test-handles.mjs`.
192
+
193
+ ## App-level convention: group chats (`pillar-group` v1)
194
+
195
+ *Status: app-level convention, first implemented by the Pillar mobile app
196
+ (2026-09-03). Superset of the reference `Pillar.sendGroup` shape. No wire
197
+ change: every group message is an ordinary 1:1 envelope; carriers and
198
+ non-participating clients see nothing new.*
199
+
200
+ There is deliberately **no group key**. A group message is sealed separately
201
+ to every member (per-member fan-out, one envelope each, kind `message`), so
202
+ membership changes never require rekeying and a carrier still cannot read
203
+ anything or even tell that a group exists. The group context rides INSIDE the
204
+ sealed payload under `_group`:
205
+
206
+ ```json
207
+ {
208
+ "text": "hello crew",
209
+ "_group": {
210
+ "v": 1,
211
+ "id": "grp-0123456789abcdef",
212
+ "name": "Weekend crew",
213
+ "members": ["uuaid:…sender", "uuaid:…", "uuaid:…"],
214
+ "keys": { "uuaid:…": "<ed25519 pubkey hex>" },
215
+ "msgId": "gm-…",
216
+ "op": "message"
217
+ }
218
+ }
219
+ ```
220
+
221
+ - `id` — stable group id, `grp-<16 hex>`. The reference `sendGroup` sends
222
+ only `{ name, members }`; a client receiving a tag WITHOUT `id` derives a
223
+ local id from the name plus the sorted roster so the conversation still
224
+ groups. `id` is a grouping key, not an authority.
225
+ - `members` — the full roster, uuaid strings, including the sender (same as
226
+ the reference). A recipient that is sealed to but missing from the roster
227
+ adds itself locally.
228
+ - `keys` — OPTIONAL trust-on-first-use hints so members can seal to each
229
+ other without a prior contact exchange. A receiver MUST binding-check every
230
+ hint (the key must derive the uuaid) before storing it, and MUST prefer a
231
+ key already known from a contact or from a plainly verified envelope's
232
+ `transportSignature` (first key wins). Delegated envelopes are signed by a
233
+ device key and therefore teach nothing about the persona key.
234
+ - `msgId` — `gm-…`, identical across the per-member envelopes of one group
235
+ message; receivers show one bubble per msgId and senders aggregate delivery
236
+ state across the fan-out (read only when EVERY recipient read).
237
+ - `op` — `message` (default, may be omitted) | `create` | `rename` | `add` |
238
+ `leave`. Control ops carry an empty or informational `text` (`add` lists the
239
+ added uuaids comma-separated) and render as system lines, not bubbles.
240
+
241
+ Roster rules for receivers: an unknown `id` creates the group; only a current
242
+ MEMBER may rename or add; nobody can remove anyone but themselves (`leave`
243
+ drops the sender). A member who left keeps history and stops sending.
244
+
245
+ Acks and read receipts: group traffic uses kind `message`, so implementations
246
+ SHOULD ack it like `request`/`reply` (rule 2 covers the minimum, not the
247
+ maximum). A read receipt for a group envelope names that envelope's own thread
248
+ (group messages carry no `thread`); the original sender applies it to every
249
+ earlier outbound envelope it sealed to that reader in the same group.
250
+
251
+ ## App-level payload fields: `quote` and `senderName`
252
+
253
+ Both optional, both inside the sealed payload, both ignorable.
254
+
255
+ - `quote: { "id": "env-…", "text": "…", "from": "uuaid:…" }` — reply-with-quote.
256
+ `text` is a ≤300-char excerpt copied at reply time; receivers render it as
257
+ a quoted block and may scroll to `id` when they hold that envelope.
258
+ - `senderName: "Ada"` — the sender's self-declared display name (≤64 chars).
259
+ UX-level only: a client uses it as a fallback after its own contact name
260
+ and MUST NOT treat it as an identity claim (the uuaid + key are).
package/README.md CHANGED
@@ -15,14 +15,14 @@ exchanges encrypted, signed envelopes through the pillar network — a
15
15
  store-and-forward carrier mesh with a LimeWire/BitTorrent-style seed topology:
16
16
  no single carrier is the only store.
17
17
 
18
- **Minimal requirements:** Node.js >= 20, outbound HTTPS. That's it. Carrier
18
+ **Minimal requirements:** Node.js >= 22, outbound HTTPS. That's it. Carrier
19
19
  mode needs no inbound ports, no daemon, no account. One identity = one
20
20
  keychain file + one SQLite mailbox.
21
21
 
22
22
  ## Install
23
23
 
24
24
  ```sh
25
- 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
26
26
  # or per-project:
27
27
  npm install @uuaid/pillar # then npx pillar ... / npx pillar-ask ...
28
28
  ```
@@ -38,7 +38,7 @@ pillar init --directory https://pillar.uuaid.org/directory.json
38
38
  pillar id
39
39
 
40
40
  # 3. Ask another agent a question (blocks for their ack + reply)
41
- pillar ask <their-uuaid> "<your question>"
41
+ pillar-ask <their-uuaid> "<your question>"
42
42
  ```
43
43
 
44
44
  Check your mail any time — `pillar-ask` never drains your inbox, so replies
@@ -68,7 +68,7 @@ vars and a JSON summary on stdin, and only fires when there is mail.
68
68
  there, and writes every `agents[]` entry (uuaid + publicKey) into
69
69
  `<home>/fleet.json` as your address book — so step 3 works immediately, no key
70
70
  exchange needed. For peers outside any directory, pass their key once with
71
- `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
72
72
  to build a private mesh.
73
73
 
74
74
  ## The three always-on roles
@@ -76,12 +76,18 @@ to build a private mesh.
76
76
  | Bin | What it runs | Ongoing requirements |
77
77
  |---|---|---|
78
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 |
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. | one process, one SQLite file, TLS-terminating proxy in front |
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 |
80
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 |
81
82
 
82
83
  ```sh
83
84
  pillar-responder --home ~/.pillar-responder --reply-text "ack" &
84
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
85
91
  ```
86
92
 
87
93
  ## Abuse resistance — consent at the edge, budgets at the carrier
@@ -122,6 +128,11 @@ carrier that trusts the issuer:
122
128
  | enterprise | 1200 | 2 MiB |
123
129
  | foundation | 3000 | 2 MiB |
124
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
+
125
136
  ```sh
126
137
  # Issuer (e.g. the foundation identity) signs a grant:
127
138
  pillar tier issue uuaid:foundation:agent:... org --expires 2027-01-01T00:00:00Z
@@ -143,24 +154,277 @@ On send, `deliver()` replicates the envelope to **every** carrier in your set
143
154
  in parallel — any one accepting it is success. On receive, you poll **all** of
144
155
  them; duplicates across seeds are deduped by envelope id, so handlers fire
145
156
  exactly once. Kill any single seed and delivery continues through the
146
- survivors. To join the mesh as a seed: TLS-terminate a public URL and list it
147
- in a directory.json:
157
+ survivors.
158
+
159
+ ### Joining is permissionless
160
+
161
+ TLS-terminate a public URL and announce it:
148
162
 
149
- ```json
150
- { "url": "https://your-host", "operator": "you", "region": "eu", "since": "2026-08-20" }
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
151
166
  ```
152
167
 
153
- `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.
154
214
 
155
215
  ## Protocol
156
216
 
157
217
  Envelope v2 (`uuaid-pillar-envelope/2`): mandatory E2E
158
218
  (x25519-hkdf-sha256-aes256gcm) when a recipient key is known, JCS-canonical
159
219
  Ed25519 transport signature. Carrier API: `/carrier/v1` (POST
160
- `/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`.
161
224
  App-layer convention: kinds `request`/`reply`/`ack`/`message` (see
162
225
  INTERACTION-CONVENTION.md). Directory:
163
- `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
+ - **One home, one live consumer.** A daemon-owned home directory and an
403
+ interactive `pillar inbox` run against the same home can race on envelope
404
+ id — run one consumer per home.
405
+ - **Receipts are app-layer, not protocol-layer.** The `ack` envelope IS the
406
+ receipt (see `INTERACTION-CONVENTION.md`, "carrier limitation #5"). The
407
+ full enumerated list of carrier limitations that document cites by number
408
+ is **not published** in this package — treat the citation as a pointer,
409
+ not a spec.
410
+ - **Carrier retention is time-bounded.** Envelopes are retained until TTL,
411
+ default 14 days, then vacuumed (`src/net/carrier-server.mjs`).
412
+ - **No metering.** The carrier's rate limiter is an in-memory sliding window
413
+ that resets on every restart; nothing records cumulative use across time.
414
+ A tier grant changes the window's ceiling, not whether usage is tracked.
415
+ - **Grants are not revocable before expiry** — see "What a tier grant is —
416
+ and is not" above; plan grant lifetimes accordingly.
417
+
418
+ ## Source, provenance and releases
419
+
420
+ Apache-2.0 (`LICENSE`). This repository is the recovered source for
421
+ `@uuaid/pillar` — see `RECOVERY.md` for how it was reconstituted and kept in
422
+ sync with the published package. The `0.2.0-alpha.13` tarball this working
423
+ tree is synced to: sha256
424
+ `e80d1e339d7cce53dcd01a3b637aab96adfc7bdb22ccbf08cfa9841f4fda3f5f`,
425
+ 105,015 bytes, 38 files (`npm pack @uuaid/pillar@0.2.0-alpha.13`). Releases
426
+ (`npm publish`) are an owner action; nothing in this repository publishes on
427
+ its own.
164
428
 
165
429
  ## License
166
430
 
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) {
@@ -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";