@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 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.** Exactly one UUAID proves itself with its key:
149
- `uuaid:foundation:agent:` followed by `sha256(publicKey)[:16]` as a UUID,
150
- rebuilt in full and compared as a whole string (src/blob/blob.mjs:177-193).
151
- Every identity `pillar init` mints is one, and it is the rule every envelope
152
- is accepted on. A device, a service, another namespace, or the same local id
153
- under any other prefix is not self-certified and needs a registry badge
154
- naming its key. A self-certified identity signs broker requests with keyId
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 `uuaid:foundation:agent` UUAID derived from this key (`pillar init`), or get a registry badge that names this key |
387
- | `sender-key-not-active` | `pillar-blob get`, before redeeming | Ask the sender to send from a `uuaid:foundation:agent` UUAID derived from its own key, or to get a registry badge naming its key |
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:foundation:agent:&lt;local id derived from your key>, or with the keyId your registry badge names. |
417
- | `self_certified_uuaid_mismatch` | 401 | With keyId "self-certified:ed25519", agent_uuaid must be exactly uuaid:foundation:agent:&lt;first 16 bytes of sha256(publicKey), hex 8-4-4-4-12>; any other UUAID needs a registry badge. |
435
+ | `registry_key_inactive` | 401 | Sign with keyId "self-certified:ed25519" as uuaid:&lt;foundation|pillar|agentnet>:agent:&lt;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:&lt;foundation|pillar|agentnet>:agent:&lt;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 uuaid:foundation:agent UUAID does not need the registry. |
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
- No wire change — it rides the existing `ack` kind inside an ordinary
79
- E2E-encrypted payload; carriers and non-participating clients are unaffected.*
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). Superset of the reference `Pillar.sendGroup` shape. No wire
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>`. The reference `sendGroup` sends
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
- > **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.
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. `pillar-responder` defaults its command hook to the
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:foundation:agent:... org --expires 2027-01-01T00:00:00Z
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
- const server = new CarrierServer({ dbPath, ttlDays, trustedTierIssuers: tierIssuers, secureDelete });
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(() => {
@@ -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();
@@ -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");
@@ -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
- const server = new CarrierServer({ dbPath, ttlDays, keychain, publicUrl, secureDelete });
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}`);