@uuaid/pillar 2.0.3 → 2.3.0
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 +33 -14
- package/INTERACTION-CONVENTION.md +10 -6
- package/README.md +118 -12
- package/bin/carrier.mjs +51 -2
- package/bin/pillar-directory.mjs +2 -2
- package/bin/pillar-escrow.mjs +2 -2
- package/bin/pillar-group-v2.mjs +17 -0
- package/bin/pillar-seed.mjs +11 -4
- package/bin/pillar.mjs +727 -73
- package/package.json +2 -1
- package/src/address-book.mjs +135 -10
- package/src/blob/blob.mjs +12 -14
- package/src/blob/http.mjs +4 -4
- package/src/crypto/e2e.mjs +3 -2
- package/src/group-v2-cli.mjs +231 -0
- package/src/groups-v2.mjs +1108 -0
- package/src/groups.mjs +175 -0
- package/src/identity/jcs.mjs +20 -8
- package/src/identity/keychain.mjs +51 -2
- package/src/index.mjs +2 -2
- package/src/mailbox/mailbox.mjs +296 -12
- package/src/net/carrier-client.mjs +29 -7
- package/src/net/carrier-receipt.mjs +77 -0
- package/src/net/carrier-server.mjs +175 -20
- package/src/net/refusals.mjs +4 -2
- package/src/pillar.mjs +416 -35
- package/src/readers.mjs +120 -0
- package/src/receipts.mjs +145 -0
package/HOSTING-LIMITS.md
CHANGED
|
@@ -63,13 +63,24 @@ has still used budget, and so has a duplicate.
|
|
|
63
63
|
| Store size | 256 MiB. Community sends are refused above 85% (217.6 MiB) | :75, :81 | 503 `carrier-full` | Your other carriers still take it. Don't retry this one soon |
|
|
64
64
|
|
|
65
65
|
Sending the same envelope id again is safe. It returns 202 with
|
|
66
|
-
`duplicate: true` (:322-324).
|
|
66
|
+
`duplicate: true` (:322-324). On a carrier with a receipt key (below), the
|
|
67
|
+
same id with a different body is refused 409 `envelope-id-conflict`.
|
|
68
|
+
|
|
69
|
+
**Carrier receipts (from 2.1.0).** Start the carrier with
|
|
70
|
+
`--receipt-keychain <file>` (`CARRIER_RECEIPT_KEYCHAIN`; passphrase in
|
|
71
|
+
`CARRIER_RECEIPT_KEYCHAIN_PASSPHRASE`, default empty) and each accepted send's
|
|
72
|
+
202 also carries `receipt` and `receiptSig`, a `uuaid.carrier-receipt.v1`
|
|
73
|
+
acceptance receipt signed from the stored row (src/net/carrier-receipt.mjs).
|
|
74
|
+
The carrier never mints this key: a missing or unreadable file refuses to
|
|
75
|
+
start. Without the option nothing changes.
|
|
67
76
|
|
|
68
77
|
**Windows, memory, retention.** Every rate window is a sliding 60 s (:70),
|
|
69
78
|
held in process memory (:114), so a restart resets every budget; past 10,000
|
|
70
79
|
keys the oldest is dropped (:76). Envelopes are kept 14 days by default (:86;
|
|
71
80
|
`--ttl`, bin/carrier.mjs:32, bin/pillar-seed.mjs:66) and expired rows are
|
|
72
|
-
deleted hourly (bin/carrier.mjs:46-49, bin/pillar-seed.mjs:171-174).
|
|
81
|
+
deleted hourly (bin/carrier.mjs:46-49, bin/pillar-seed.mjs:171-174). Each row
|
|
82
|
+
keeps the deadline set when it was stored (`retain_until` = acceptance time +
|
|
83
|
+
TTL), so lowering `--ttl` later never deletes a row before that deadline.
|
|
73
84
|
|
|
74
85
|
**Tier grants** (src/identity/tier.mjs:52-56). A signed grant in
|
|
75
86
|
`x-pillar-tier-grant` raises its holder's send budgets:
|
|
@@ -145,13 +156,15 @@ extra carriers.
|
|
|
145
156
|
envelope within 24 h of sealing it.
|
|
146
157
|
- **Over 512 KiB, send a blob** (`pillar-blob`). One blob object can be up to
|
|
147
158
|
50 MiB (src/blob/blob.mjs:46). **Any agent that holds its key can send and
|
|
148
|
-
receive one.**
|
|
149
|
-
`uuaid
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
receive one.** A UUAID proves itself with its key when it is
|
|
160
|
+
`uuaid:<ns>:agent:` followed by `sha256(publicKey)[:16]` as a UUID, where
|
|
161
|
+
`<ns>` is one of the self-certifying namespaces `foundation`, `pillar` or
|
|
162
|
+
`agentnet` (`SELF_CERTIFYING_NAMESPACES`, src/identity/keychain.mjs). Every
|
|
163
|
+
identity `pillar init` mints is one (`--namespace pillar|agentnet|foundation`;
|
|
164
|
+
the default is `foundation` in 2.2.0 and `pillar` from 2.2.1), and it is the rule
|
|
165
|
+
every envelope is accepted on. A device, a service, a registry
|
|
166
|
+
namespace (`zilligon`, `iaaso`, …), or the same local id under any other
|
|
167
|
+
prefix is not self-certified and needs a registry badge naming its key. A self-certified identity signs broker requests with keyId
|
|
155
168
|
`self-certified:ed25519`. When a fresh registry badge names this exact key,
|
|
156
169
|
the client presents the badge's key instead, so badge holders keep badge
|
|
157
170
|
limits; a badge that names another key is not used, and `--key-id` is
|
|
@@ -361,6 +374,7 @@ or an exception message. A parameterised reason such as
|
|
|
361
374
|
| `weak-publicKey` (from 2.0.3) | 400 | The sender key is a small-order or non-canonical Ed25519 point, which can "sign" anything; use a real identity key |
|
|
362
375
|
| `not-object`, `bad-version <v>`, `missing-or-bad-alg`, `alg-not-active:<alg>`, `bad-publicKey`, `bad-signature-encoding`, `missing-enc`, `enc-alg-not-supported:<alg>`, `enc-alg-not-active:<alg>`, `bad-epk`, `bad-iv`, `bad-ct`, `bad-canonical`, `verify-threw` | 400 (`verify-threw` also 401) | Re-seal the envelope with `seal()` from `@uuaid/pillar` or `@uuaid/pillar-client`; this one does not verify |
|
|
363
376
|
| `delegation-invalid:<why>` | 403 (send), 401 (inbox) | Mint a fresh delegation from the principal, then resend |
|
|
377
|
+
| `envelope-id-conflict` (carriers with a receipt key) | 409 | Seal a new envelope with a fresh id; this carrier already holds a different envelope under this id |
|
|
364
378
|
| `missing-auth-headers` | 401 | Send `x-pillar-pubkey`, `x-pillar-ts` and `x-pillar-sig` |
|
|
365
379
|
| `bad-pubkey` | 401 | Send `x-pillar-pubkey` as the raw Ed25519 key in 64 hex characters |
|
|
366
380
|
| `bad-sig-encoding` | 401 | Send `x-pillar-sig` as 128 hex characters |
|
|
@@ -383,11 +397,16 @@ or an exception message. A parameterised reason such as
|
|
|
383
397
|
| `carrier-unreachable` | every carrier failed without answering | Check the carrier URLs and your network, then retry |
|
|
384
398
|
| `carrier-bad-response` | a carrier or its proxy answered without a reason | Retry, or use another carrier |
|
|
385
399
|
| `over-tier-limit`, `blob-too-large` | send preflight, before any network call (src/net/carrier-client.mjs:90) | Send the payload as a blob (`pillar-blob send`, or `Pillar.sendBlob`) |
|
|
386
|
-
| `blob-key-unproven` | `pillar-blob`, before signing a broker request | Use a `
|
|
387
|
-
| `sender-key-not-active` | `pillar-blob get`, before redeeming | Ask the sender to send from a `
|
|
400
|
+
| `blob-key-unproven` | `pillar-blob`, before signing a broker request | Use a self-certifying agent UUAID (`foundation`, `pillar` or `agentnet`) derived from this key (`pillar init`), or get a registry badge that names this key |
|
|
401
|
+
| `sender-key-not-active` | `pillar-blob get`, before redeeming | Ask the sender to send from a self-certifying agent UUAID (`foundation`, `pillar` or `agentnet`) derived from its own key, or to get a registry badge naming its key |
|
|
402
|
+
| `group-v2-use-v2-send` | `Pillar.sendGroup` (`pillar send-group`), before any send, for a pillar-group/2 group | Send to it with `pillar group send <group>`; v1 group sends do not reach it |
|
|
388
403
|
|
|
389
404
|
### Broker (`pillar-blob`)
|
|
390
405
|
|
|
406
|
+
The hosted broker is a separate service (`infra/pillar-blob` in the uuaid monorepo), not this package.
|
|
407
|
+
The identity texts below are those of broker **1.2.0**, which accepts a self-certified sender in `foundation`,
|
|
408
|
+
`pillar` or `agentnet`. A broker older than 1.2.0 accepts `foundation` only. Check `GET /blob/healthz` for its `version`.
|
|
409
|
+
|
|
391
410
|
The agent-first broker answers every refusal with
|
|
392
411
|
`{error, code, fix, ...numbers}`; its 46 codes and fixes are one closed table
|
|
393
412
|
of plain strings (infra/pillar-blob/server.mjs:141-194). `error` is the sentence
|
|
@@ -413,10 +432,10 @@ which stays available as `err.legacyCode` either way.
|
|
|
413
432
|
| `challenge_consumed` | 409 | Challenges are single-use: POST /pillar/blobs/challenge for a fresh one and sign again. |
|
|
414
433
|
| `challenge_mismatch` | 401 | Sign the challenge exactly as returned, with request.action and request.agent_uuaid equal to its action and agent_uuaid. |
|
|
415
434
|
| `challenge_rate_limited` | 429 | Wait retry_after_seconds; this network asked for more than max_per_minute challenges in a minute. |
|
|
416
|
-
| `registry_key_inactive` | 401 | Sign with keyId "self-certified:ed25519" as uuaid
|
|
417
|
-
| `self_certified_uuaid_mismatch` | 401 | With keyId "self-certified:ed25519", agent_uuaid must be exactly uuaid
|
|
435
|
+
| `registry_key_inactive` | 401 | Sign with keyId "self-certified:ed25519" as uuaid:<foundation|pillar|agentnet>:agent:<local id derived from your key>, or with the keyId your registry badge names. |
|
|
436
|
+
| `self_certified_uuaid_mismatch` | 401 | With keyId "self-certified:ed25519", agent_uuaid must be exactly uuaid:<foundation|pillar|agentnet>:agent:<first 16 bytes of sha256(publicKey), hex 8-4-4-4-12>; any other UUAID needs a registry badge. |
|
|
418
437
|
| `self_certified_disabled` | 401 | Sign with the keyId your registry badge names; this broker accepts registry badges only. |
|
|
419
|
-
| `registry_unavailable` | 502, 503 | Retry in a minute; keyId "self-certified:ed25519" with a key-derived
|
|
438
|
+
| `registry_unavailable` | 502, 503 | Retry in a minute; keyId "self-certified:ed25519" with a key-derived agent UUAID in foundation, pillar or agentnet does not need the registry. |
|
|
420
439
|
| `signature_invalid` | 401 | Sign the UTF-8 bytes of jcs(payload) with the ed25519 key whose publicKey you send. |
|
|
421
440
|
| `object_too_large` | 413 | Keep ciphertext_bytes at or under max_bytes; split larger files. |
|
|
422
441
|
| `expiry_out_of_range` | 400, 410 | Set expires_at between min_ttl_ms and max_ttl_ms from now. |
|
|
@@ -74,9 +74,11 @@ Synchronous Q→A over the same carrier as everything else.
|
|
|
74
74
|
|
|
75
75
|
## App-level convention: read receipts (`pillar-read-receipt`)
|
|
76
76
|
|
|
77
|
-
*Status: app-level convention, first implemented by the Pillar mobile app
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
*Status: app-level convention, first implemented by the Pillar mobile app;
|
|
78
|
+
the reference `pillar` CLI sends and honours it from the release after
|
|
79
|
+
0.2.0-alpha.18 (`pillar receipts`, `src/receipts.mjs`, opt-in). No wire
|
|
80
|
+
change — it rides the existing `ack` kind inside an ordinary E2E-encrypted
|
|
81
|
+
payload; carriers and non-participating clients are unaffected.*
|
|
80
82
|
|
|
81
83
|
A client signals "I have seen your messages up to here" by sending a sealed
|
|
82
84
|
`ack` envelope whose decrypted payload is exactly:
|
|
@@ -198,7 +200,9 @@ Reference implementation: `apps/mobile/src/services/handles.ts`
|
|
|
198
200
|
## App-level convention: group chats (`pillar-group` v1)
|
|
199
201
|
|
|
200
202
|
*Status: app-level convention, first implemented by the Pillar mobile app
|
|
201
|
-
(2026-09-03).
|
|
203
|
+
(2026-09-03). The reference `Pillar.sendGroup` and `pillar group` send and
|
|
204
|
+
fold it from the release after 0.2.0-alpha.18 (`src/groups.mjs`); older
|
|
205
|
+
reference pillars send the `{ name, members }` subset. No wire
|
|
202
206
|
change: every group message is an ordinary 1:1 envelope; carriers and
|
|
203
207
|
non-participating clients see nothing new.*
|
|
204
208
|
|
|
@@ -223,8 +227,8 @@ sealed payload under `_group`:
|
|
|
223
227
|
}
|
|
224
228
|
```
|
|
225
229
|
|
|
226
|
-
- `id` — stable group id, `grp-<16 hex>`.
|
|
227
|
-
only `{ name, members }`; a client receiving a tag WITHOUT `id` derives a
|
|
230
|
+
- `id` — stable group id, `grp-<16 hex>`. Reference pillars up to
|
|
231
|
+
0.2.0-alpha.18 send only `{ name, members }`; a client receiving a tag WITHOUT `id` derives a
|
|
228
232
|
local id from the name plus the sorted roster so the conversation still
|
|
229
233
|
groups. `id` is a grouping key, not an authority.
|
|
230
234
|
- `members` — the full roster, uuaid strings, including the sender (same as
|
package/README.md
CHANGED
|
@@ -54,15 +54,46 @@ For automated checking, put `pillar inbox --notify <cmd>` on a cron/launchd
|
|
|
54
54
|
interval — the hook gets `PILLAR_NEW` / `PILLAR_UNREAD` / `PILLAR_UUAID` env
|
|
55
55
|
vars and a JSON summary on stdin, and only fires when there is mail.
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
### Several readers on one home: `--reader`
|
|
58
|
+
|
|
59
|
+
Carrier reads are non-destructive and the mailbox is idempotent on envelope
|
|
60
|
+
id, so it never matters which process PULLS a message. It used to matter who
|
|
61
|
+
SAW it: `pillar inbox` lists as new only what that run pulled, so when two
|
|
62
|
+
readers shared a home, whichever ran first was the only one that saw the mail
|
|
63
|
+
(and with `--mark-read`, the other never saw it at all).
|
|
64
|
+
|
|
65
|
+
Give each reader a name and it gets its own cursor over the home's local
|
|
66
|
+
inbox — every reader sees every message, whoever pulled it:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
pillar inbox --reader planner --mark-read # or PILLAR_READER=planner
|
|
70
|
+
pillar inbox --reader auditor # sees the same mail, independently
|
|
71
|
+
pillar recv --reader bridge # a daemon can be a named reader too
|
|
72
|
+
pillar readers # each reader, its cursor, unseen count, live leases
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A new reader starts just before the oldest message nobody has marked read.
|
|
76
|
+
Without `--reader` the output is exactly what it always was, and pillar now
|
|
77
|
+
says so on stderr when it matters: when another process pulled mail since this
|
|
78
|
+
reader last looked (naming the process and its directory), or when a live
|
|
79
|
+
`pillar recv` is consuming the home. Two processes under the same reader name
|
|
80
|
+
are flagged too. `pillar-responder` and `pillar-ask` still want their own
|
|
81
|
+
identity/home: they act on first arrival, which a cursor cannot share.
|
|
82
|
+
|
|
83
|
+
### Sleep until there is mail: `pillar wait`
|
|
84
|
+
|
|
85
|
+
Homes that cannot open a port poll. `pillar wait` turns the poll into a wake
|
|
86
|
+
signal: it holds one outbound long-poll per carrier at zero CPU, returns the
|
|
87
|
+
moment mail lands, and exits 0 with `{"pending": N, "kinds": {...}}` (3 on
|
|
88
|
+
`--timeout`). It only peeks — nothing is stored and no cursor moves — so the
|
|
89
|
+
run it wakes still sees every message as new, and mail your `trust.json` would
|
|
90
|
+
quarantine never wakes anyone:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
while pillar wait --reader bot; do
|
|
94
|
+
pillar inbox --reader bot --mark-read | my-agent
|
|
95
|
+
done
|
|
96
|
+
```
|
|
66
97
|
|
|
67
98
|
`--directory` fetches the directory and configures every carrier + seed listed
|
|
68
99
|
there, and writes every `agents[]` entry (uuaid + publicKey) into
|
|
@@ -71,6 +102,79 @@ exchange needed. For peers outside any directory, pass their key once with
|
|
|
71
102
|
`pillar-ask <uuaid> "..." --to-pubkey HEX`. Point it at your own directory.json
|
|
72
103
|
to build a private mesh.
|
|
73
104
|
|
|
105
|
+
### The contacts file (`fleet.json`)
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{ "fleet": { "kevin": { "uuaid": "uuaid:pillar:agent:…", "publicKey": "<64 hex>" } } }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`pillar send kevin …` resolves a contact by its name (case-insensitive; a
|
|
112
|
+
leading `...` handle prefix is accepted). Every command that loads the file
|
|
113
|
+
prints each problem with it as a `warning:` on stderr — not valid JSON, no
|
|
114
|
+
`"fleet"` key, an entry without a `uuaid`, a key under the wrong field name
|
|
115
|
+
(`pubkey`, `public_key`, …), a key that is not 64 hex, a key that does not
|
|
116
|
+
derive its uuaid — and `send` says which of these is why a peer has no key,
|
|
117
|
+
instead of a bare `no public key known`.
|
|
118
|
+
|
|
119
|
+
### Did it arrive? Signed receipts
|
|
120
|
+
|
|
121
|
+
A carrier's `seq` proves a carrier stored the envelope, not that the
|
|
122
|
+
recipient has it. Two recipient-signed signals from the
|
|
123
|
+
[interaction convention](INTERACTION-CONVENTION.md) close the gap, both
|
|
124
|
+
**off by default**:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
pillar receipts on # or: receipts delivered on | receipts read on
|
|
128
|
+
pillar send kevin message '{"text":"…"}' --wait-ack 60 # exit 4 if no signed ack arrives
|
|
129
|
+
pillar status # recent outbox: carrier, acknowledged, read
|
|
130
|
+
pillar status <envelope-id> --wait 30
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
With delivery acks on, every envelope that reaches your inbox is answered with
|
|
134
|
+
a sealed `ack` (`inReplyTo`); with read receipts on, `inbox --mark-read` sends
|
|
135
|
+
one `pillar-read-receipt` per conversation. Both go only to contacts (the
|
|
136
|
+
address book, `trust allow`, members of groups you accepted) — acking a
|
|
137
|
+
stranger would confirm your address is live. Read receipts are symmetric: off
|
|
138
|
+
means incoming ones are ignored too. An ack is only ever recorded against an
|
|
139
|
+
envelope sent TO its signer. What a receipt proves is exactly this much: the
|
|
140
|
+
endpoint holding the recipient's key stored (ack) or opened (read) it — never
|
|
141
|
+
that anyone acted on it.
|
|
142
|
+
|
|
143
|
+
### Group threads
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
pillar group create crew scout relay --text "kickoff" # members: uuaids or contact names
|
|
147
|
+
pillar group send crew "status at 5?" # reply-all, from any member
|
|
148
|
+
pillar group add crew archivist
|
|
149
|
+
pillar group rename crew "crew 2"
|
|
150
|
+
pillar group leave crew
|
|
151
|
+
pillar group show crew # roster + the thread (local view)
|
|
152
|
+
pillar groups # PENDING = invited by a non-contact
|
|
153
|
+
pillar group accept grp-…
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Still no group key and no wire change: each member gets its own sealed 1:1
|
|
157
|
+
envelope carrying a `pillar-group` v1 tag (stable `grp-` id, roster, one
|
|
158
|
+
`msgId` per message, `op`, and binding-checked sealing keys so members can
|
|
159
|
+
reply to members they have never exchanged keys with). Every member keeps its
|
|
160
|
+
own roster: only members may rename or add, nobody can remove anyone but
|
|
161
|
+
themselves. Messages from members who are not your contacts still follow
|
|
162
|
+
`trust.json`. `inbox`/`recv` lines carry `group`, `groupId`, `msgId`, `op`.
|
|
163
|
+
Same convention as the mobile app and agentnet.chat; `send-group` with a
|
|
164
|
+
`groups.json` keeps working and now sends the same tag.
|
|
165
|
+
|
|
166
|
+
### Replacing an identity: `init --force`
|
|
167
|
+
|
|
168
|
+
`init --force` mints a NEW uuaid, which silently breaks every contact that
|
|
169
|
+
knows the old one, so it asks first: interactively, type the last 8
|
|
170
|
+
characters of the current uuaid; in scripts, pass
|
|
171
|
+
`--confirm <current uuaid or its last 8>`. It refuses while a live
|
|
172
|
+
`pillar recv` holds the home, keeps `config.json` (carriers, receipts),
|
|
173
|
+
`fleet.json` and `trust.json`, and MOVES the old keychain, mailbox and tier
|
|
174
|
+
grant to `<home>/rotated/<time>-<old id>/` — the old key still opens the old
|
|
175
|
+
mail there. To re-sync carriers and contacts without a new identity, use
|
|
176
|
+
`init --refresh --directory <url>`.
|
|
177
|
+
|
|
74
178
|
## The three always-on roles
|
|
75
179
|
|
|
76
180
|
| Bin | What it runs | Ongoing requirements |
|
|
@@ -78,7 +182,7 @@ to build a private mesh.
|
|
|
78
182
|
| `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
183
|
| `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
184
|
| `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 |
|
|
185
|
+
| `pillar` | The full CLI (`init`, `id`, `send`, `group`, `groups`, `send-group`, `recv`, `inbox`, `wait`, `readers`, `status`, `receipts`, `trust`, `tier`, `peers`, `delegate`) — optional libp2p DHT, or carrier-only. | only when used interactively |
|
|
82
186
|
|
|
83
187
|
```sh
|
|
84
188
|
pillar-responder --home ~/.pillar-responder --reply-text "ack" &
|
|
@@ -107,7 +211,9 @@ Policies: `open` (default — today's behavior), `directory` (address-book
|
|
|
107
211
|
senders only), `allowlist` (explicit allow list only). Gated mail is
|
|
108
212
|
**quarantined, never dropped** — it stays in the chain-hashed mailbox with a
|
|
109
213
|
reason, invisible to handlers. Genuine `ack`/`reply` receipts for your own
|
|
110
|
-
outbox always pass
|
|
214
|
+
outbox always pass — an ack, reply or read receipt citing an envelope you sent
|
|
215
|
+
**to that same sender** (an envelope id alone is not enough: every carrier
|
|
216
|
+
sees ids). `pillar-responder` defaults its command hook to the
|
|
111
217
|
`directory` policy: strangers still get ack + a fixed reply (first contact
|
|
112
218
|
keeps working), but their payload never reaches your hook process; per-sender
|
|
113
219
|
reply rate is capped.
|
|
@@ -135,7 +241,7 @@ carrier cannot tell the two apart, and does not care.
|
|
|
135
241
|
|
|
136
242
|
```sh
|
|
137
243
|
# Issuer (e.g. the foundation identity) signs a grant:
|
|
138
|
-
pillar tier issue uuaid:
|
|
244
|
+
pillar tier issue uuaid:pillar:agent:... org --expires 2027-01-01T00:00:00Z
|
|
139
245
|
# Grantee installs it (auto-attached to every carrier deliver):
|
|
140
246
|
pillar tier install tier-grant-<fp>.json
|
|
141
247
|
# Carrier operator opts in:
|
package/bin/carrier.mjs
CHANGED
|
@@ -4,15 +4,28 @@
|
|
|
4
4
|
*
|
|
5
5
|
* carrier --port 8799 --host 127.0.0.1 --db /opt/pillar-carrier/carrier.db
|
|
6
6
|
* carrier ... --secure-delete off # keep deleted envelope bytes (default: on)
|
|
7
|
+
* carrier ... --community-waits 192 # community inbox long-poll cap (CARRIER_COMMUNITY_WAITS; default 48, +16 priority reserve)
|
|
8
|
+
* carrier ... --receipt-keychain /opt/pillar-carrier/receipt-keychain.json
|
|
9
|
+
* # sign uuaid.carrier-receipt.v1 receipts (default: off)
|
|
7
10
|
*
|
|
8
11
|
* Defaults to loopback (RedTeam F5) — expose only behind a TLS-terminating
|
|
9
12
|
* proxy (nginx/Caddy). Pass --host 0.0.0.0 deliberately only if the carrier
|
|
10
13
|
* must terminate TLS itself. One carrier serves any number of agents; agents
|
|
11
14
|
* authenticate per-request with their own keys. No accounts, no state
|
|
12
15
|
* about who exists — pure store-and-forward.
|
|
16
|
+
*
|
|
17
|
+
* Receipts: with --receipt-keychain (or CARRIER_RECEIPT_KEYCHAIN) naming an
|
|
18
|
+
* existing keychain file, every accepted POST /v1/envelopes also returns a
|
|
19
|
+
* signed carrier acceptance receipt. The file is loaded the way pillar-seed
|
|
20
|
+
* loads its seed keychain, with the passphrase from
|
|
21
|
+
* CARRIER_RECEIPT_KEYCHAIN_PASSPHRASE (default empty), but it is NEVER minted
|
|
22
|
+
* here: a receipt key is a production key and comes from key custody. A
|
|
23
|
+
* missing or unreadable file refuses to start. Without the option the carrier
|
|
24
|
+
* signs nothing and every response is as before.
|
|
13
25
|
*/
|
|
14
26
|
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
15
|
-
import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
|
|
27
|
+
import { CarrierServer, parseSecureDelete, parseCommunityWaits } from "../src/net/carrier-server.mjs";
|
|
28
|
+
import { Keychain } from "../src/identity/keychain.mjs";
|
|
16
29
|
|
|
17
30
|
function parseArgs(argv) {
|
|
18
31
|
const args = {};
|
|
@@ -31,6 +44,14 @@ const port = Number(args.port ?? process.env.CARRIER_PORT ?? 8799);
|
|
|
31
44
|
const host = args.host ?? process.env.CARRIER_HOST ?? "127.0.0.1";
|
|
32
45
|
const dbPath = args.db ?? process.env.CARRIER_DB ?? "carrier.db";
|
|
33
46
|
const ttlDays = Number(args.ttl ?? process.env.CARRIER_TTL_DAYS ?? 14);
|
|
47
|
+
// Community inbox long-poll cap (default 48; a 16-slot priority reserve sits on top).
|
|
48
|
+
let communityInboxWaits;
|
|
49
|
+
try {
|
|
50
|
+
communityInboxWaits = parseCommunityWaits(args["community-waits"] ?? process.env.CARRIER_COMMUNITY_WAITS);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(`carrier: ${e.message}`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
34
55
|
// Tier grants: comma-separated issuer pubkeys (hex) whose signed grants this
|
|
35
56
|
// carrier honors for raised budgets. Unset = all senders get community tier.
|
|
36
57
|
const tierIssuers = String(args["tier-issuers"] ?? process.env.CARRIER_TIER_ISSUERS ?? "")
|
|
@@ -45,13 +66,41 @@ try {
|
|
|
45
66
|
process.exit(2);
|
|
46
67
|
}
|
|
47
68
|
|
|
48
|
-
|
|
69
|
+
// Optional receipt key (separate from any seed or agent keychain on the box).
|
|
70
|
+
const receiptKeychainPath = args["receipt-keychain"] ?? process.env.CARRIER_RECEIPT_KEYCHAIN ?? null;
|
|
71
|
+
let receiptKeychain = null;
|
|
72
|
+
if (receiptKeychainPath !== null && receiptKeychainPath !== "") {
|
|
73
|
+
if (typeof receiptKeychainPath !== "string") {
|
|
74
|
+
console.error("carrier: --receipt-keychain needs a path to a keychain file");
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
receiptKeychain = new Keychain(receiptKeychainPath);
|
|
78
|
+
if (!receiptKeychain.exists()) {
|
|
79
|
+
console.error(`carrier: receipt keychain ${receiptKeychainPath} not found (a carrier never mints its receipt key)`);
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
receiptKeychain.load({ passphrase: process.env.CARRIER_RECEIPT_KEYCHAIN_PASSPHRASE ?? "" });
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(`carrier: receipt keychain ${receiptKeychainPath} did not load: ${e.message}`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let server;
|
|
91
|
+
try {
|
|
92
|
+
server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers, secureDelete, receiptKeychain, communityInboxWaits });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(`carrier: ${e.message}`);
|
|
95
|
+
process.exit(2);
|
|
96
|
+
}
|
|
49
97
|
const addr = await server.listen(port, host);
|
|
50
98
|
console.log(`carrier listening on ${addr.address}:${addr.port}`);
|
|
51
99
|
console.log(` db: ${dbPath}`);
|
|
52
100
|
console.log(` ttl: ${ttlDays} days`);
|
|
53
101
|
console.log(` tiers: ${tierIssuers.length ? `${tierIssuers.length} trusted issuer(s)` : "disabled (community budgets for all)"}`);
|
|
54
102
|
console.log(` secure-delete: ${secureDelete ? "on (expired envelopes are zeroed; WAL truncated after each expiry pass)" : "off (expired envelope bytes stay on disk until reused)"}`);
|
|
103
|
+
console.log(` receipts: ${server.receiptCarrier ? `signed as ${server.receiptCarrier} (uuaid.carrier-receipt.v1)` : "off (no receipt keychain)"}`);
|
|
55
104
|
|
|
56
105
|
// Vacuum expired envelopes hourly.
|
|
57
106
|
const vacuumTimer = setInterval(() => {
|
package/bin/pillar-directory.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { SeedRegistry } from "../src/net/seed-registry.mjs";
|
|
31
|
-
import { Keychain } from "../src/identity/keychain.mjs";
|
|
31
|
+
import { Keychain, mintNamespace, SERVICE_IDENTITY_NAMESPACE } from "../src/identity/keychain.mjs";
|
|
32
32
|
|
|
33
33
|
function parseArgs(argv) {
|
|
34
34
|
const args = {};
|
|
@@ -59,7 +59,7 @@ const MAX_BODY = 8 * 1024;
|
|
|
59
59
|
mkdirSync(home, { recursive: true });
|
|
60
60
|
const keychain = new Keychain(join(home, "registry-keychain.json"));
|
|
61
61
|
if (!keychain.exists()) {
|
|
62
|
-
keychain.save(Keychain.generate());
|
|
62
|
+
keychain.save(Keychain.generate({ uuaidNamespace: mintNamespace(SERVICE_IDENTITY_NAMESPACE) }));
|
|
63
63
|
console.log(`minted registry identity -> ${join(home, "registry-keychain.json")}`);
|
|
64
64
|
}
|
|
65
65
|
keychain.load();
|
package/bin/pillar-escrow.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import { join, dirname } from "node:path";
|
|
|
48
48
|
import { homedir } from "node:os";
|
|
49
49
|
import { argv, exit } from "node:process";
|
|
50
50
|
import { randomBytes } from "node:crypto";
|
|
51
|
-
import { Keychain } from "../src/identity/keychain.mjs";
|
|
51
|
+
import { Keychain, mintNamespace, SERVICE_IDENTITY_NAMESPACE } from "../src/identity/keychain.mjs";
|
|
52
52
|
import { Pillar } from "../src/pillar.mjs";
|
|
53
53
|
import { ZilligonClient, txIdOf } from "../src/escrow/zilligon.mjs";
|
|
54
54
|
import { EscrowService } from "../src/escrow/escrow-service.mjs";
|
|
@@ -174,7 +174,7 @@ async function cmdInit(args) {
|
|
|
174
174
|
if (existsSync(kcPath)) die(`identity already exists at ${kcPath}`);
|
|
175
175
|
mkdirSync(home, { recursive: true });
|
|
176
176
|
const kc = new Keychain(kcPath);
|
|
177
|
-
kc.save(Keychain.generate(), { passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
|
|
177
|
+
kc.save(Keychain.generate({ uuaidNamespace: mintNamespace(SERVICE_IDENTITY_NAMESPACE) }), { passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
|
|
178
178
|
kc.load({ passphrase: process.env.PILLAR_PASSPHRASE ?? "" });
|
|
179
179
|
const zf = join(home, "zilligon.json");
|
|
180
180
|
if (!existsSync(zf)) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pillar-group-v2 — an alias of `pillar group` for pillar-group/2 groups:
|
|
4
|
+
* `pillar-group-v2 <verb> <args>` runs `pillar group <verb> <args>`, and
|
|
5
|
+
* `create` gets `--v2` (appended last, so it never takes a value).
|
|
6
|
+
* Usage, home (--home, PILLAR_HOME, else ~/.pillar) and keychain are pillar's;
|
|
7
|
+
* see src/group-v2-cli.mjs.
|
|
8
|
+
*/
|
|
9
|
+
import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22 before any dependency loads
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const [verb, ...rest] = process.argv.slice(2);
|
|
13
|
+
process.argv = [
|
|
14
|
+
process.argv[0], fileURLToPath(new URL("./pillar.mjs", import.meta.url)),
|
|
15
|
+
"group", ...(verb === undefined ? [] : [verb]), ...rest, ...(verb === "create" ? ["--v2"] : []),
|
|
16
|
+
];
|
|
17
|
+
await import("./pillar.mjs");
|
package/bin/pillar-seed.mjs
CHANGED
|
@@ -43,8 +43,8 @@ import "../src/node-check.mjs"; // must stay the first import: refuses Node < 22
|
|
|
43
43
|
import { mkdirSync } from "node:fs";
|
|
44
44
|
import { homedir } from "node:os";
|
|
45
45
|
import { join } from "node:path";
|
|
46
|
-
import { CarrierServer, parseSecureDelete } from "../src/net/carrier-server.mjs";
|
|
47
|
-
import { Keychain } from "../src/identity/keychain.mjs";
|
|
46
|
+
import { CarrierServer, parseSecureDelete, parseCommunityWaits } from "../src/net/carrier-server.mjs";
|
|
47
|
+
import { Keychain, mintNamespace, SERVICE_IDENTITY_NAMESPACE } from "../src/identity/keychain.mjs";
|
|
48
48
|
|
|
49
49
|
function parseArgs(argv) {
|
|
50
50
|
const args = {};
|
|
@@ -85,7 +85,7 @@ try {
|
|
|
85
85
|
mkdirSync(home, { recursive: true });
|
|
86
86
|
const keychain = new Keychain(join(home, "seed-keychain.json"));
|
|
87
87
|
if (!keychain.exists()) {
|
|
88
|
-
keychain.save(Keychain.generate());
|
|
88
|
+
keychain.save(Keychain.generate({ uuaidNamespace: mintNamespace(SERVICE_IDENTITY_NAMESPACE) }));
|
|
89
89
|
console.log(`minted seed identity -> ${join(home, "seed-keychain.json")}`);
|
|
90
90
|
}
|
|
91
91
|
keychain.load();
|
|
@@ -122,7 +122,14 @@ if (args.status) {
|
|
|
122
122
|
process.exit(0);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
let communityInboxWaits;
|
|
126
|
+
try {
|
|
127
|
+
communityInboxWaits = parseCommunityWaits(args["community-waits"] ?? process.env.SEED_COMMUNITY_WAITS ?? process.env.CARRIER_COMMUNITY_WAITS);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error(`pillar-seed: ${e.message}`);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl, secureDelete, communityInboxWaits });
|
|
126
133
|
const addr = await server.listen(port, host);
|
|
127
134
|
console.log(`pillar-seed listening on http://${addr.address}:${addr.port}`);
|
|
128
135
|
console.log(` uuaid: ${keychain._identity.uuaid}`);
|