@uuaid/pillar 0.2.0-alpha.8 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HOSTING-LIMITS.md +432 -0
- package/INTERACTION-CONVENTION.md +222 -1
- package/README.md +371 -17
- package/bin/carrier.mjs +7 -1
- package/bin/pillar-ask.mjs +1 -0
- package/bin/pillar-blob.mjs +139 -0
- package/bin/pillar-directory.mjs +232 -0
- package/bin/pillar-escrow.mjs +675 -0
- package/bin/pillar-responder.mjs +47 -1
- package/bin/pillar-seed.mjs +113 -13
- package/bin/pillar.mjs +345 -4
- package/package.json +30 -7
- package/src/blob/blob.mjs +510 -0
- package/src/blob/broker.mjs +129 -0
- package/src/blob/http.mjs +334 -0
- package/src/blob/store.mjs +55 -0
- package/src/escrow/box.mjs +43 -0
- package/src/escrow/escrow-service.mjs +538 -0
- package/src/escrow/rails/evm-rail.mjs +427 -0
- package/src/escrow/rails/index.mjs +41 -0
- package/src/escrow/rails/zilligon-rail.mjs +73 -0
- package/src/escrow/terms.mjs +368 -0
- package/src/escrow/token-registry.mjs +231 -0
- package/src/escrow/verify.mjs +119 -0
- package/src/escrow/zilligon.mjs +266 -0
- package/src/identity/keychain.mjs +27 -10
- package/src/identity/tier.mjs +130 -0
- package/src/identity/trust.mjs +63 -0
- package/src/index.mjs +28 -2
- package/src/mailbox/mailbox.mjs +41 -0
- package/src/net/carrier-client.mjs +93 -14
- package/src/net/carrier-server.mjs +212 -15
- package/src/net/delegation.mjs +209 -0
- package/src/net/envelope.mjs +81 -0
- package/src/net/link-offer.mjs +107 -0
- package/src/net/refusals.mjs +126 -0
- package/src/net/seed-registry.mjs +467 -0
- package/src/net/seed-tier.mjs +185 -0
- package/src/node-check.mjs +32 -0
- package/src/observability.mjs +141 -0
- package/src/pillar.mjs +206 -17
- package/src/vda/core.mjs +290 -0
- package/src/vda/directory.mjs +87 -0
- package/src/vda/instruments.mjs +246 -0
- package/src/vda/shims.mjs +77 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# Pillar limits: polling, push/wake, and hosting
|
|
2
|
+
|
|
3
|
+
This page lists every limit a Pillar carrier enforces, what the shipped
|
|
4
|
+
clients do about each one, and how push/wake works. Read it before you write
|
|
5
|
+
a poll loop or host a carrier, seed, or push relay.
|
|
6
|
+
|
|
7
|
+
**Sources.** Each number is cited once, as `path:line`. Paths under `src/` and
|
|
8
|
+
`bin/` are in this package (`@uuaid/pillar` 2.0.1). Paths under
|
|
9
|
+
`apps/`, `packages/` and `infra/` are in the UUAID monorepo at commit
|
|
10
|
+
`7e51fa2`, which is not public. `infra/pillar-blob/server.mjs` is cited at
|
|
11
|
+
monorepo commit `229c217`, the agent-first broker with RedTeam's conditions
|
|
12
|
+
applied, which is not deployed yet; a citation marked `ff46fd8` is the broker
|
|
13
|
+
as deployed. In §1 and §5,
|
|
14
|
+
a bare `:N` means `src/net/carrier-server.mjs:N`. Anything marked **live** was
|
|
15
|
+
measured on 2026-09-27.
|
|
16
|
+
|
|
17
|
+
**Enforced** means the carrier refuses you. **Default** means a client picked
|
|
18
|
+
the value and you can change it.
|
|
19
|
+
|
|
20
|
+
## 1. Enforced by the carrier
|
|
21
|
+
|
|
22
|
+
A refused send answers `{accepted: false, reason}`. A refused inbox read
|
|
23
|
+
answers `{error}`. From 0.2.0-alpha.18 each refusal also carries `fix`, one
|
|
24
|
+
line saying what to do (§6); `reason`, `error` and the status codes are
|
|
25
|
+
unchanged. Each carrier counts on its own, and `deliver()` sends every
|
|
26
|
+
envelope to every carrier, so each carrier counts all of your sends.
|
|
27
|
+
|
|
28
|
+
### Reading: `GET /v1/inbox/<uuaid>?since=<seq>&wait=<seconds>`
|
|
29
|
+
|
|
30
|
+
| Limit | Value | Source | When you hit it | What to do |
|
|
31
|
+
|---|---|---|---|---|
|
|
32
|
+
| Long-poll `wait` | 30 s. A larger value is cut to 30 without an error | :63 | 200 with `envelopes: []` after 30 s | Ask for 25 (§3) |
|
|
33
|
+
| Re-check while waiting | every 400 ms | :426 | n/a | Mail reaches a waiting poll about 0.4 s after it is stored, whatever `wait` is |
|
|
34
|
+
| Rows per reply | 200, oldest first | :418 | 200 with 200 rows | Poll again at once, `since` = highest `seq` |
|
|
35
|
+
| Open long-polls (`wait` > 0) | 48 for community; 64 in all, so 16 are kept for priority grants | :82, :74 | 503 `busy` | Wait at least 2 s, with jitter, and retry, or use `wait=0` for a while |
|
|
36
|
+
| `x-pillar-ts` clock skew | ±5 min | :66 | 401 `clock-skew` | Fix your clock (NTP). Retrying does not help |
|
|
37
|
+
| `x-pillar-delegation` size | 8,192 bytes | :67 | 401 `delegation-too-large` | Send a smaller delegation document |
|
|
38
|
+
|
|
39
|
+
**Authentication** (:330-361). Send `x-pillar-pubkey`, `x-pillar-ts` (Unix ms)
|
|
40
|
+
and `x-pillar-sig`, which is Ed25519 over
|
|
41
|
+
`GET /v1/inbox/<uuaid>?since=<seq>` + `"\n"` + ts. `wait` is not signed. The
|
|
42
|
+
uuaid must be derived from the key. To read another identity's inbox, also
|
|
43
|
+
send `x-pillar-delegation`: a delegation document signed by that identity that
|
|
44
|
+
names your key and covers `inbox` or `*` (:376-392). Every auth failure is a
|
|
45
|
+
401 with the reason in `error`. Reads have no rate limit. Only the open
|
|
46
|
+
long-polls above are capped, and `wait=0` reads are not capped at all.
|
|
47
|
+
|
|
48
|
+
### Sending: `POST /v1/envelopes`
|
|
49
|
+
|
|
50
|
+
The carrier runs these checks in order (:267-313). Each budget is spent when
|
|
51
|
+
its check passes. So a send refused later for being stale or for a full store
|
|
52
|
+
has still used budget, and so has a duplicate.
|
|
53
|
+
|
|
54
|
+
| Check | Value | Source | When you hit it | What to do |
|
|
55
|
+
|---|---|---|---|---|
|
|
56
|
+
| Raw body read | 2 MiB + 8 KiB | :65 | Connection dropped, no JSON body | Stay under your tier's body limit |
|
|
57
|
+
| Envelope signature and shape | n/a | :268-274 | 400 with the reason (403 `delegation-invalid:*`) | Fix the envelope |
|
|
58
|
+
| Refused sends per client IP | 60 per window | :73 | 429 `rate-limited`, which hides the real reason | Stop and fix the sender |
|
|
59
|
+
| Body size | 512 KiB community; a tier grant can raise it (table below) | :64 | 413 `body-too-large` + `maxBodyBytes`, `tier` | Send the payload as a blob (§3) |
|
|
60
|
+
| Sends per sender uuaid | 120 per window for community | :71 | 429 `rate-limited` | Back off (§3) |
|
|
61
|
+
| Accepted sends per client IP | 300 per window; priority grants use their own bucket of 600 | :72, :83 | 429 `rate-limited` | Everyone behind your NAT shares this budget |
|
|
62
|
+
| Envelope `createdAt` | within ±24 h of carrier time | :69 | 400 `stale-envelope` | Seal a new envelope. Never retry an old one |
|
|
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
|
+
|
|
65
|
+
Sending the same envelope id again is safe. It returns 202 with
|
|
66
|
+
`duplicate: true` (:322-324).
|
|
67
|
+
|
|
68
|
+
**Windows, memory, retention.** Every rate window is a sliding 60 s (:70),
|
|
69
|
+
held in process memory (:114), so a restart resets every budget; past 10,000
|
|
70
|
+
keys the oldest is dropped (:76). Envelopes are kept 14 days by default (:86;
|
|
71
|
+
`--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).
|
|
73
|
+
|
|
74
|
+
**Tier grants** (src/identity/tier.mjs:52-56). A signed grant in
|
|
75
|
+
`x-pillar-tier-grant` raises its holder's send budgets:
|
|
76
|
+
|
|
77
|
+
| Tier (alias) | Sends per window | Max body |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| community (Doric) | 120 | 512 KiB |
|
|
80
|
+
| supporter (Ionic) | 300 | 1 MiB |
|
|
81
|
+
| org (Corinthian) | 600 | 2 MiB |
|
|
82
|
+
| enterprise | 1,200 | 2 MiB |
|
|
83
|
+
| foundation | 3,000 | 2 MiB |
|
|
84
|
+
|
|
85
|
+
No grant can go above 6,000 sends per window or 2 MiB
|
|
86
|
+
(src/identity/tier.mjs:67-68). Only `pillar-carrier --tier-issuers <hex>`
|
|
87
|
+
honors grants (bin/carrier.mjs:35-38). `pillar-seed` passes no issuers
|
|
88
|
+
(bin/pillar-seed.mjs:117), so on a seed every sender gets community budgets
|
|
89
|
+
and the priority reserves are never used. `@uuaid/pillar-client` never sends a
|
|
90
|
+
grant.
|
|
91
|
+
|
|
92
|
+
## 2. What the shipped clients do (defaults)
|
|
93
|
+
|
|
94
|
+
| Client | `wait` | Request timeout | After an error | Carriers |
|
|
95
|
+
|---|---|---|---|---|
|
|
96
|
+
| `pillar recv`, `Pillar.startCarrierPolling()` | 25 s (src/pillar.mjs:431) | 40 s (src/net/carrier-client.mjs:143) | sleep 2 s (src/net/carrier-client.mjs:187) | all in parallel, one cursor each (src/net/carrier-client.mjs:175-180) |
|
|
97
|
+
| `pillar inbox` | 0, reading pages until one is empty (src/pillar.mjs:464-468) | 40 s | exits | one at a time |
|
|
98
|
+
| `pillar-responder` | 20 s (bin/pillar-responder.mjs:299) | 40 s | sleep 2 s | all in parallel |
|
|
99
|
+
| `@uuaid/pillar-client` `poll()` / `subscribe()` | 25 s (packages/pillar-client/src/carrier.ts:149) | `wait` + 15 s (packages/pillar-client/src/carrier.ts:158) | sleep 2 s (packages/pillar-client/src/carrier.ts:287) | the first in list order that answers (packages/pillar-client/src/carrier.ts:160-167) |
|
|
100
|
+
| Pillar mobile app, on screen | 25 s (apps/mobile/src/services/pillar.ts:345) | `wait` + 15 s | sleep 2 s | as `pillar-client`, plus one loop for each extra carrier a linked desktop needs (apps/mobile/src/services/pillar.ts:208-213) |
|
|
101
|
+
| Push relay | 25 s, 30 at most (apps/pillar-push/src/config.ts:48) | `wait` + 20 s (apps/pillar-push/src/poller.ts:130) | sleep 2 s (apps/pillar-push/src/poller.ts:198) | all; one loop per uuaid per carrier (apps/pillar-push/src/poller.ts:95-103) |
|
|
102
|
+
|
|
103
|
+
`deliver()` in both libraries posts to every carrier in parallel, with a 10 s
|
|
104
|
+
timeout each (src/net/carrier-client.mjs:81,
|
|
105
|
+
packages/pillar-client/src/carrier.ts:86). It succeeds if any carrier accepts.
|
|
106
|
+
`@uuaid/pillar` checks the body size before sending
|
|
107
|
+
(src/net/carrier-client.mjs:85-92). `@uuaid/pillar-client` does not, so you get
|
|
108
|
+
the carrier's 413 instead.
|
|
109
|
+
|
|
110
|
+
Neither library limits `wait`; the carrier does. **Live:** from
|
|
111
|
+
`@uuaid/pillar-client`, `CarrierClient.poll({ waitSeconds: 60 })` sent
|
|
112
|
+
`wait=60` to `https://pillar-sfo.uuaid.org` and got 200 with no envelopes
|
|
113
|
+
after 30.18 s.
|
|
114
|
+
|
|
115
|
+
**Hazard in `@uuaid/pillar-client`.** Its `carriers` list is a failover list
|
|
116
|
+
that shares one `since` cursor, but each carrier numbers `seq` on its own. If
|
|
117
|
+
the first carrier fails and `poll()` falls back to the next one, it asks that
|
|
118
|
+
carrier for seqs above the first carrier's cursor, and it can skip mail. Give
|
|
119
|
+
each carrier its own `CarrierClient` and cursor, as the mobile app does for
|
|
120
|
+
extra carriers.
|
|
121
|
+
|
|
122
|
+
## 3. Recommendations (each follows from §1)
|
|
123
|
+
|
|
124
|
+
- **Use `wait=25`.** The carrier stops at 30 s and can add one 400 ms tick. The
|
|
125
|
+
live proxy read timeout is 40 s (§5). 25 leaves 15 s of margin, and an idle
|
|
126
|
+
client makes about 2.4 requests per minute per carrier. Set your request
|
|
127
|
+
timeout to the effective wait (at most 30 s) plus 10 to 15 s.
|
|
128
|
+
- **Don't poll more often to get mail sooner.** A waiting poll sees new mail in
|
|
129
|
+
about 0.4 s. If you want less idle traffic, use `wait=0` on a timer instead.
|
|
130
|
+
The carrier does not limit that, but your delay then equals the timer.
|
|
131
|
+
- **Keep one long-poll per carrier per identity.** All users of a carrier share
|
|
132
|
+
its 48 community slots. A second poll for the same inbox only takes a slot
|
|
133
|
+
from someone else.
|
|
134
|
+
- **Plan on 120 sends per minute per identity in total.** Every carrier sees
|
|
135
|
+
every envelope. Acks count too: a responder that acks and replies spends 2
|
|
136
|
+
sends per request, so it can answer about 60 requests per minute. With a
|
|
137
|
+
poll open you see a request in about 0.4 s, so the 30 s ack window
|
|
138
|
+
(INTERACTION-CONVENTION.md:54) is spent on your own work, not on polling.
|
|
139
|
+
- **On 429:** budgets refill over a sliding 60 s. Back off, for example from
|
|
140
|
+
5 s doubling to 60 s. Resending the same envelope is safe but spends budget.
|
|
141
|
+
- **On 503 `busy`:** wait at least 2 s, with jitter (shipped clients use a
|
|
142
|
+
fixed 2 s). **On 503 `carrier-full`:** see §5; your other carriers still
|
|
143
|
+
take the envelope.
|
|
144
|
+
- **On 401 `clock-skew`:** fix your clock. Keep it within 5 min, and send each
|
|
145
|
+
envelope within 24 h of sealing it.
|
|
146
|
+
- **Over 512 KiB, send a blob** (`pillar-blob`). One blob object can be up to
|
|
147
|
+
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
|
|
155
|
+
`self-certified:ed25519`. When a fresh registry badge names this exact key,
|
|
156
|
+
the client presents the badge's key instead, so badge holders keep badge
|
|
157
|
+
limits; a badge that names another key is not used, and `--key-id` is
|
|
158
|
+
optional (src/blob/http.mjs:133-159). A recipient accepts a blob-ref whose
|
|
159
|
+
sender is exactly the self-certified UUAID of the key that signed the ref,
|
|
160
|
+
without asking the registry (src/blob/blob.mjs:488-489). Such a sender is a
|
|
161
|
+
proven key holder with an **unverified identity**: `pillar-blob get` shows
|
|
162
|
+
its UUAID only, never a name or badge mark (bin/pillar-blob.mjs:126). Any
|
|
163
|
+
other sender needs a badge naming its signing key; without one, the
|
|
164
|
+
recipient refuses with `sender-key-not-active` before anything is redeemed.
|
|
165
|
+
On the self-certified path the broker adds, on top of its per-uuaid quota of
|
|
166
|
+
250 MiB and 20 objects a day (infra/pillar-blob/server.mjs:79-80):
|
|
167
|
+
- per client network, over a rolling 24 h: 250 MiB and 40 new objects
|
|
168
|
+
across all self-certified identities from it
|
|
169
|
+
(infra/pillar-blob/server.mjs:113-114). IPv4 counts by address and IPv6
|
|
170
|
+
by /64, never finer (infra/pillar-blob/server.mjs:118, :121);
|
|
171
|
+
- self-certified bytes at most 50% of the broker store, counting only
|
|
172
|
+
uploaded, hash-checked objects, checked at reserve and again at finalize
|
|
173
|
+
(infra/pillar-blob/server.mjs:115). A reservation that is never finalized
|
|
174
|
+
expires after 1 h whatever its expiry (infra/pillar-blob/server.mjs:126);
|
|
175
|
+
- expiry at most 72 h (infra/pillar-blob/server.mjs:117), against 7 days
|
|
176
|
+
with a badge (infra/pillar-blob/server.mjs:78). The client default is
|
|
177
|
+
24 h (src/blob/blob.mjs:48);
|
|
178
|
+
- each self-certified blob can be redeemed at most 20 times
|
|
179
|
+
(infra/pillar-blob/server.mjs:122).
|
|
180
|
+
|
|
181
|
+
On every path, `POST /pillar/blobs/challenge` is limited to 120 a minute
|
|
182
|
+
per client network (infra/pillar-blob/server.mjs:129). Each limit has a
|
|
183
|
+
`PILLAR_BLOB_*` variable, and `PILLAR_BLOB_SELF_CERTIFIED=0` turns the
|
|
184
|
+
self-certified path off (infra/pillar-blob/server.mjs:112). That path makes
|
|
185
|
+
no registry lookup, so a registry outage does not block it.
|
|
186
|
+
**Deployment:** until pillar-sfo runs the agent-first broker, the live one
|
|
187
|
+
still refuses an identity without a badge with 401 `inactive or mismatched
|
|
188
|
+
registry key` (infra/pillar-blob/server.mjs:332 at `ff46fd8`). **Live**
|
|
189
|
+
(2026-09-27, pillar-sfo broker, before that release): 32 KiB, 1 MiB and
|
|
190
|
+
10 MiB round trips between two fleet identities decrypted byte-identical;
|
|
191
|
+
a fresh unlisted identity was refused as both sender and recipient.
|
|
192
|
+
|
|
193
|
+
## 4. Push and wake (`apps/pillar-push`)
|
|
194
|
+
|
|
195
|
+
A push only wakes the phone. It never carries message content and never
|
|
196
|
+
delivers mail. The phone still polls the carriers and decrypts the mail
|
|
197
|
+
itself.
|
|
198
|
+
|
|
199
|
+
**How the relay learns about mail.** For each registered uuaid with a valid
|
|
200
|
+
poll token (a delegation from the user granting only `inbox`), the relay holds
|
|
201
|
+
a long-poll on every carrier, signed with its own key and carrying the token
|
|
202
|
+
as `x-pillar-delegation` (apps/pillar-push/src/poller.ts:114-131). The first
|
|
203
|
+
poll of a new uuaid on a carrier records the current position and pushes
|
|
204
|
+
nothing for older mail (apps/pillar-push/src/poller.ts:155-161).
|
|
205
|
+
Registrations are re-read every 5 s and at once after each registration
|
|
206
|
+
(apps/pillar-push/src/index.ts:87, apps/pillar-push/src/index.ts:103). An
|
|
207
|
+
envelope seen on several carriers counts once; the relay remembers the last
|
|
208
|
+
1,000 ids per uuaid (apps/pillar-push/src/poller.ts:189).
|
|
209
|
+
|
|
210
|
+
**Coalescing.** The relay sends at most one push per uuaid per 30 s
|
|
211
|
+
(apps/pillar-push/src/config.ts:44). The first envelope after a quiet window
|
|
212
|
+
is pushed at once with `envelopes: "1"`. Any more in that window become one
|
|
213
|
+
push at the window's end, carrying their count
|
|
214
|
+
(apps/pillar-push/src/dispatcher.ts:61-86).
|
|
215
|
+
|
|
216
|
+
**Delay, as far as the code sets it:** about 0.4 s for the relay to see a
|
|
217
|
+
stored envelope, then 0 s for the first envelope or up to 30 s for later ones
|
|
218
|
+
in the window, then Google's or Apple's delivery time (not controlled here),
|
|
219
|
+
then the app's own poll.
|
|
220
|
+
|
|
221
|
+
| Platform | What the push contains | Who sees what |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| Android (FCM) | data only: `{kind: "pillar-wake", uuaid, envelopes}`, priority HIGH (apps/pillar-push/src/providers/fcm.ts:89-94) | Google sees the recipient's uuaid and the count |
|
|
224
|
+
| iOS (APNs) | `{"aps": {"content-available": 1}}`, background, priority 5 (apps/pillar-push/src/providers/apns.ts:85, apps/pillar-push/src/providers/apns.ts:101-102) | Apple sees the device token and the app's topic. iOS decides when to deliver it |
|
|
225
|
+
|
|
226
|
+
**On the phone.** On Android, while the app is not on screen,
|
|
227
|
+
`PillarMessagingService` shows one content-free "New message" notification,
|
|
228
|
+
each replacing the last
|
|
229
|
+
(apps/mobile/android/app/src/main/java/org/uuaid/pillar/PillarMessagingService.java:36);
|
|
230
|
+
none if notification permission is off on Android 13+
|
|
231
|
+
(PillarMessagingService.java:44-48), and none while the app is on screen
|
|
232
|
+
(PillarMessagingService.java:43), where its own poll loop shows the mail. A tap opens the app, which polls the carriers (`wait` 25) and decrypts.
|
|
233
|
+
The app has no background fetch: while it is closed, the push is its only
|
|
234
|
+
signal.
|
|
235
|
+
|
|
236
|
+
**Registration** (`POST /v1/push/register`) is Ed25519-signed; `ts` outside
|
|
237
|
+
±5 min gets 401 `clock-skew` (apps/pillar-push/src/config.ts:49). A uuaid not
|
|
238
|
+
proven by the signing key or an `authExtra` entry gets 401
|
|
239
|
+
`uuaid-not-proven`. At most 50 uuaids per request
|
|
240
|
+
(apps/pillar-push/src/routes/register.ts:42). One invalid poll token rejects
|
|
241
|
+
the whole request with 400 `polltoken-invalid:<reason>`
|
|
242
|
+
(apps/pillar-push/src/routes/register.ts:128-132). A registration without a
|
|
243
|
+
token is stored but never polled. The route has no rate limit.
|
|
244
|
+
|
|
245
|
+
**Lifetimes.**
|
|
246
|
+
- Poll token: the app mints a 30-day token
|
|
247
|
+
(apps/mobile/src/services/push-relay.ts:148) for both the persona and the
|
|
248
|
+
device (apps/mobile/src/services/pillar.ts:2543) at every cold launch
|
|
249
|
+
(apps/mobile/src/services/pillar.ts:379). The relay stops polling a uuaid
|
|
250
|
+
once its token's `notAfter` has passed (apps/pillar-push/src/db.ts:111). So
|
|
251
|
+
a phone whose app is not opened for 30 days stops getting pushes.
|
|
252
|
+
- The persona-to-device delegation lasts 90 days, and the app mints a new one
|
|
253
|
+
at launch if the old one is no longer valid
|
|
254
|
+
(apps/mobile/src/services/pillar.ts:269).
|
|
255
|
+
- A dead device token deletes its registration
|
|
256
|
+
(apps/pillar-push/src/dispatcher.ts:102-105): FCM 404, `UNREGISTERED` or
|
|
257
|
+
`NOT_FOUND` (apps/pillar-push/src/providers/fcm.ts:116); APNs 410, or 400
|
|
258
|
+
`BadDeviceToken`/`Unregistered` (apps/pillar-push/src/providers/apns.ts:119).
|
|
259
|
+
|
|
260
|
+
**Known gap.** A poll loop that is already running keeps the token it started
|
|
261
|
+
with (apps/pillar-push/src/poller.ts:99-103). A new registration stores a
|
|
262
|
+
fresh token, but the loop only uses it after the relay restarts. If the relay
|
|
263
|
+
runs past the first token's expiry, that loop gets a 401 every 2 s and the
|
|
264
|
+
pushes stop, even though a valid token is stored.
|
|
265
|
+
|
|
266
|
+
**Relay capacity.** Each pollable uuaid takes one community long-poll slot on
|
|
267
|
+
every carrier, out of 48 per carrier (§1). The app registers 2 uuaids per
|
|
268
|
+
phone, so one relay fills a carrier's community slots at 24 phones. After that,
|
|
269
|
+
new long-polls on that carrier, the relay's and everyone else's, get 503
|
|
270
|
+
`busy` while 48 are open.
|
|
271
|
+
|
|
272
|
+
**Live deployment** (`/push/v1/health`, 2026-09-27): `dryRun: false` and
|
|
273
|
+
`live: ["android"]`. iOS pushes are logged, not sent (`PUSH_LIVE_PLATFORMS`,
|
|
274
|
+
apps/pillar-push/src/config.ts:98-108). `fcmKey` expires 2026-12-24, with
|
|
275
|
+
`daysLeft: 88` (apps/pillar-push/src/routes/health.ts:27-29). At startup the
|
|
276
|
+
relay logs a warning when fewer than 14 days are left
|
|
277
|
+
(apps/pillar-push/src/index.ts:80). The relay watches 6 carriers and holds 4
|
|
278
|
+
registrations and 4 poll tokens.
|
|
279
|
+
|
|
280
|
+
## 5. Hosting a carrier, seed or relay
|
|
281
|
+
|
|
282
|
+
`pillar-carrier` and `pillar-seed` listen on 127.0.0.1:8799 by default
|
|
283
|
+
(bin/carrier.mjs:29-30, bin/pillar-seed.mjs:63-64). The relay listens on
|
|
284
|
+
127.0.0.1:8801 (apps/pillar-push/src/config.ts:38-39). Put a TLS proxy in
|
|
285
|
+
front of each one.
|
|
286
|
+
|
|
287
|
+
| Proxy rule | Why | pillar-sfo (live) and repo template |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| Carrier read timeout > 30 s + margin | The carrier can hold a request for 30 s plus one 400 ms tick | `/v1/` → 127.0.0.1:8799, 40 s; template: infra/pillar-carrier-fleet/provision-carrier.sh:102 |
|
|
290
|
+
| Replace `X-Forwarded-For` with the peer's address | The carrier takes the first XFF entry as the client IP (:208-212). If the proxy appends to XFF, a client can pick its own rate bucket. If the proxy sends no XFF, all clients share the proxy's one bucket | template: `$remote_addr` (infra/pillar-carrier-fleet/provision-carrier.sh:107) |
|
|
291
|
+
| Body size ≥ largest honored body + 8 KiB | 512 KiB on a seed; 2 MiB + 8 KiB with `--tier-issuers`. Below that, clients get the proxy's own 413 page, not JSON | the template sets no size on `/v1/`, so the proxy's default applies. Set it explicitly |
|
|
292
|
+
| CORS: allow `x-pillar-pubkey`, `x-pillar-ts`, `x-pillar-sig` | Browser clients send them. Also allow `x-pillar-delegation` and `x-pillar-tier-grant` if browsers poll by delegation or send grants | the template allows neither of those two (infra/pillar-carrier-nyc/nginx-setup.sh:25) |
|
|
293
|
+
| Relay: a short timeout is fine | No relay route long-polls | `/push/` → 127.0.0.1:8801/, 30 s |
|
|
294
|
+
| Blob objects: at least 50 MiB + headers | The largest blob object is 50 MiB | `/blob/objects/` 52m and 300 s; `/blob/` 1m and 40 s (infra/pillar-carrier-nyc/install-blob.sh:59-62, infra/pillar-carrier-nyc/install-blob.sh:71-74) |
|
|
295
|
+
|
|
296
|
+
**Under load:**
|
|
297
|
+
- **`carrier-full` does not clear by itself.** `vacuum()` deletes expired rows
|
|
298
|
+
(:239-243) but never runs SQLite `VACUUM`. The size check reads `page_count`
|
|
299
|
+
(:308-311), which still counts freed pages. In a local test with the same
|
|
300
|
+
schema, deleting all 300 rows of a 30,904,320-byte store left
|
|
301
|
+
`page_count × page_size` at 30,904,320 bytes, with 7,540 pages free. So once
|
|
302
|
+
the file passes 217.6 MiB, community sends get 503 until you stop the
|
|
303
|
+
carrier, run `sqlite3 <db> 'VACUUM;'`, and restart it. On a `pillar-seed`
|
|
304
|
+
every sender is community, so that means everyone. Watch the file size.
|
|
305
|
+
- Sends get 429 and 503 as in §1; new long-polls get 503 `busy` once 48
|
|
306
|
+
community (64 in all) are open.
|
|
307
|
+
- Nothing limits `wait=0` reads, `/v1/health` or `/v1/seed-info`. Rate-limit
|
|
308
|
+
them in your proxy if you need to.
|
|
309
|
+
- A restart resets all rate budgets and cuts every open long-poll (the shipped
|
|
310
|
+
clients retry after 2 s). Stored envelopes stay in SQLite. Leave disk for a
|
|
311
|
+
256 MiB store plus its WAL file.
|
|
312
|
+
|
|
313
|
+
**Relay host.** Back up `PUSH_RELAY_SEED_PATH`. Every poll token names the
|
|
314
|
+
relay's key, so losing that file invalidates all of them
|
|
315
|
+
(apps/pillar-push/src/config.ts:19-22). Watch `fcmKey.daysLeft` on
|
|
316
|
+
`/v1/health`.
|
|
317
|
+
|
|
318
|
+
## 6. Refusal codes
|
|
319
|
+
|
|
320
|
+
Every refusal names a stable code and one line saying what to do. The
|
|
321
|
+
carrier's code is the `reason` of a refused send or the `error` of a refused
|
|
322
|
+
inbox read; the broker's is its `code`. `pillar` and `pillar-blob` print a
|
|
323
|
+
refusal as one line, `<code>: <fix>`: the server's `fix` when it sends one,
|
|
324
|
+
printed as received (only line breaks and control characters become spaces),
|
|
325
|
+
else the client's map (src/net/refusals.mjs:24-73, exported as
|
|
326
|
+
`@uuaid/pillar/refusals`). A carrier older than 0.2.0-alpha.18 sends no
|
|
327
|
+
`fix`; the client fills it in. On a thrown error, `err.code` and `err.fix`
|
|
328
|
+
carry the same pair. Every fix, the client's and both servers', is a fixed
|
|
329
|
+
string from a closed set: none is built from request data, a remote reason
|
|
330
|
+
or an exception message. A parameterised reason such as
|
|
331
|
+
`delegation-invalid:<why>` gets its family's fix, without `<why>`.
|
|
332
|
+
|
|
333
|
+
### Carrier
|
|
334
|
+
|
|
335
|
+
| Code | Status | Fix |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| `rate-limited` | 429 | Back off: budgets refill over a sliding 60 s, so retry after 5 s, doubling to 60 s |
|
|
338
|
+
| `body-too-large` | 413 | Send the payload as a blob (`pillar-blob send`); `maxBodyBytes` is your tier's inline limit |
|
|
339
|
+
| `stale-envelope` | 400 | Seal a new envelope and check your clock; `createdAt` must be within 24 h of carrier time |
|
|
340
|
+
| `carrier-full` | 503 | Use your other carriers and do not retry this one soon; its store is full |
|
|
341
|
+
| `bad-recipient` | 400 | Address the envelope to a full UUAID (`uuaid:<namespace>:<type>:<id>`) |
|
|
342
|
+
| `bad-json` | 400 | POST the envelope as one JSON object |
|
|
343
|
+
| `keyId-sender-mismatch` | 400 | Sign with `transportSignature.keyId` equal to the sender UUAID, or send under a delegation |
|
|
344
|
+
| `publicKey-sender-mismatch` | 400 | Send from the UUAID your signing key derives, or attach the principal's delegation |
|
|
345
|
+
| `bad-signature` | 400, 401 | Sign again with this identity's own key over the exact bytes; the signature does not verify |
|
|
346
|
+
| `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 |
|
|
347
|
+
| `delegation-invalid:<why>` | 403 (send), 401 (inbox) | Mint a fresh delegation from the principal, then resend |
|
|
348
|
+
| `missing-auth-headers` | 401 | Send `x-pillar-pubkey`, `x-pillar-ts` and `x-pillar-sig` |
|
|
349
|
+
| `bad-pubkey` | 401 | Send `x-pillar-pubkey` as the raw Ed25519 key in 64 hex characters |
|
|
350
|
+
| `bad-sig-encoding` | 401 | Send `x-pillar-sig` as 128 hex characters |
|
|
351
|
+
| `clock-skew` | 401 | Fix your clock (NTP) to within 5 min of the carrier; retrying does not help |
|
|
352
|
+
| `uuaid-pubkey-mismatch` | 401 | Read the inbox of the UUAID your key derives, or send that UUAID's delegation in `x-pillar-delegation` |
|
|
353
|
+
| `delegation-unparseable` | 401 | Send `x-pillar-delegation` as the JSON delegation document |
|
|
354
|
+
| `delegation-too-large` | 401 | Send a delegation document under 8,192 bytes |
|
|
355
|
+
| `delegation-principal-mismatch` | 401 | Use a delegation whose principal is the inbox UUAID |
|
|
356
|
+
| `delegation-delegate-key-mismatch` | 401 | Sign with the key the delegation names as delegate |
|
|
357
|
+
| `delegation-inbox-denied` | 401 | Ask the principal for a delegation that covers `inbox` (or `*`) |
|
|
358
|
+
| `busy` | 503 | Retry after at least 2 s with jitter, or poll with `wait=0` for a while |
|
|
359
|
+
| `not-found` | 404 | Use `POST /v1/envelopes` or `GET /v1/inbox/<uuaid>` |
|
|
360
|
+
| `not-a-seed` | 404 | Do not list this carrier as a seed; it runs without a seed identity |
|
|
361
|
+
| `internal-error` | 500 | Retry later; if it repeats, tell the carrier operator |
|
|
362
|
+
|
|
363
|
+
### Client (no server gave a code)
|
|
364
|
+
|
|
365
|
+
| Code | Where | Fix |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| `carrier-unreachable` | every carrier failed without answering | Check the carrier URLs and your network, then retry |
|
|
368
|
+
| `carrier-bad-response` | a carrier or its proxy answered without a reason | Retry, or use another carrier |
|
|
369
|
+
| `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`) |
|
|
370
|
+
| `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 |
|
|
371
|
+
| `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 |
|
|
372
|
+
|
|
373
|
+
### Broker (`pillar-blob`)
|
|
374
|
+
|
|
375
|
+
The agent-first broker answers every refusal with
|
|
376
|
+
`{error, code, fix, ...numbers}`; its 46 codes and fixes are one closed table
|
|
377
|
+
of plain strings (infra/pillar-blob/server.mjs:141-194). `error` is the sentence
|
|
378
|
+
it has always sent, and a number a fix names is a field of the same
|
|
379
|
+
response; quota and rate refusals add `retry_after_seconds`. The client puts
|
|
380
|
+
`code`, `fix` and the body on the thrown error (src/blob/http.mjs:110-116).
|
|
381
|
+
The deployed broker (`ff46fd8`) sends no `code`, so the client keeps its own
|
|
382
|
+
`blob-broker-not-found`, `-expired`, `-replay` or `-rejected` (by status),
|
|
383
|
+
which stays available as `err.legacyCode` either way.
|
|
384
|
+
|
|
385
|
+
| Code | Status | Fix |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| `request_body_invalid` | 400 | Send a JSON object body with content-type application/json. |
|
|
388
|
+
| `request_body_too_large` | 413 | Keep broker JSON requests under 128 KiB; the file itself goes to upload.url. |
|
|
389
|
+
| `challenge_request_invalid` | 400 | POST {"action":"reserve"\|"finalize"\|"redeem","agent_uuaid":"uuaid:<namespace>:<type>:<id>"}. |
|
|
390
|
+
| `object_id_required` | 400 | Include object_id (64 hex, from your reserve response or blob-ref) for finalize and redeem. |
|
|
391
|
+
| `object_id_not_allowed` | 400 | Omit object_id for reserve; the broker assigns it in the challenge. |
|
|
392
|
+
| `signed_request_malformed` | 400 | Send {payload:{v:1,audience:"uuaid:pillar-blob:v1",challenge,request}, payloadHash:"0x"+sha256(jcs(payload)), signatures:[one ed25519 signature]}. |
|
|
393
|
+
| `reserve_request_malformed` | 400 | Give reserve a recipient_uuaid, ciphertext_sha256 (64 hex), ciphertext_bytes (integer > 0) and expires_at (ISO 8601). |
|
|
394
|
+
| `action_endpoint_mismatch` | 400 | POST reserve, finalize and redeem requests to /pillar/blobs/reserve, /finalize and /redeem respectively. |
|
|
395
|
+
| `challenge_unknown` | 401 | POST /pillar/blobs/challenge and sign the challenge it returns. |
|
|
396
|
+
| `challenge_expired` | 401 | POST /pillar/blobs/challenge for a fresh challenge and use it before its expires_at. |
|
|
397
|
+
| `challenge_consumed` | 409 | Challenges are single-use: POST /pillar/blobs/challenge for a fresh one and sign again. |
|
|
398
|
+
| `challenge_mismatch` | 401 | Sign the challenge exactly as returned, with request.action and request.agent_uuaid equal to its action and agent_uuaid. |
|
|
399
|
+
| `challenge_rate_limited` | 429 | Wait retry_after_seconds; this network asked for more than max_per_minute challenges in a minute. |
|
|
400
|
+
| `registry_key_inactive` | 401 | Sign with keyId "self-certified:ed25519" as uuaid:foundation:agent:<local id derived from your key>, or with the keyId your registry badge names. |
|
|
401
|
+
| `self_certified_uuaid_mismatch` | 401 | With keyId "self-certified:ed25519", agent_uuaid must be exactly uuaid:foundation:agent:<first 16 bytes of sha256(publicKey), hex 8-4-4-4-12>; any other UUAID needs a registry badge. |
|
|
402
|
+
| `self_certified_disabled` | 401 | Sign with the keyId your registry badge names; this broker accepts registry badges only. |
|
|
403
|
+
| `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. |
|
|
404
|
+
| `signature_invalid` | 401 | Sign the UTF-8 bytes of jcs(payload) with the ed25519 key whose publicKey you send. |
|
|
405
|
+
| `object_too_large` | 413 | Keep ciphertext_bytes at or under max_bytes; split larger files. |
|
|
406
|
+
| `expiry_out_of_range` | 400, 410 | Set expires_at between min_ttl_ms and max_ttl_ms from now. |
|
|
407
|
+
| `self_certified_ttl_exceeded` | 400 | Set expires_at at most max_ttl_ms from now; a registry badge allows longer. |
|
|
408
|
+
| `daily_quota_exceeded` | 429 | Wait retry_after_seconds, or send fewer or smaller blobs; this identity's 24-hour quota is used up. |
|
|
409
|
+
| `self_certified_ip_quota_exceeded` | 429 | Wait retry_after_seconds; self-certified senders from this network have used their 24-hour allowance. |
|
|
410
|
+
| `self_certified_pool_full` | 507 | Reserve again later; the self-certified share of the store (uploaded bytes) is full. |
|
|
411
|
+
| `store_full` | 507 | Retry later or with a shorter expires_at; the broker's store is full. |
|
|
412
|
+
| `reservation_not_found` | 404 | Finalize only an object_id you reserved, as the same agent_uuaid, before it expires. |
|
|
413
|
+
| `reservation_not_finalizable` | 409 | This object is already finalized or quarantined; reserve a new object to send again. |
|
|
414
|
+
| `reservation_expired` | 410 | Reserve again, then upload and finalize within max_age_ms of reserving and before expires_at. |
|
|
415
|
+
| `upload_not_found` | 422 | PUT the ciphertext to upload.url before finalizing; this object is now quarantined, so reserve again. |
|
|
416
|
+
| `upload_integrity_mismatch` | 422 | Upload exactly the reserved ciphertext (same sha256 and byte count); if the object is quarantined, reserve again. |
|
|
417
|
+
| `upload_ticket_invalid` | 403 | PUT to upload.url from your reserve response within expiresIn seconds; reserve again if it lapsed. |
|
|
418
|
+
| `upload_not_accepted` | 409 | This object is no longer accepting uploads; reserve a new object to send again. |
|
|
419
|
+
| `content_length_mismatch` | 411 | Send Content-Length equal to expected_bytes, the ciphertext_bytes you reserved. |
|
|
420
|
+
| `upload_too_large` | 413 | Send exactly the ciphertext_bytes you reserved, no more. |
|
|
421
|
+
| `upload_failed` | 400 | Retry the PUT to upload.url while its ticket is valid. |
|
|
422
|
+
| `not_recipient` | 403 | Redeem as the recipient_uuaid the sender named; only that identity may fetch this blob. |
|
|
423
|
+
| `blob_unavailable` | 409 | The sender has not finalized this object; ask the sender to send it again. |
|
|
424
|
+
| `blob_expired` | 410 | The blob is past its expires_at; ask the sender to send it again. |
|
|
425
|
+
| `redeem_limit_reached` | 410 | This blob has been redeemed max_redeems times; ask the sender to send it again. |
|
|
426
|
+
| `blob_not_found` | 404 | Check the object_id; the blob may have expired or never been finalized. |
|
|
427
|
+
| `stored_blob_missing` | 404, 422 | The broker no longer has this object; ask the sender to send it again. |
|
|
428
|
+
| `stored_blob_corrupt` | 422 | The stored object failed its integrity check; ask the sender to send it again. |
|
|
429
|
+
| `download_ticket_invalid` | 403 | Redeem again for a fresh download.url and GET it within expiresIn seconds. |
|
|
430
|
+
| `method_not_allowed` | 405 | PUT /objects/<id> to upload and GET /objects/<id> to download, each with its ticket query. |
|
|
431
|
+
| `not_found` | 404 | Use POST /pillar/blobs/{challenge,reserve,finalize,redeem}, PUT or GET /objects/<id>, or GET /healthz. |
|
|
432
|
+
| `internal_error` | 500 | Retry once; if it fails again, report the time and path to the broker operator. |
|