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

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.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Refusals in agent language: every refusal is a stable `code` plus one
3
+ * imperative `fix` line, and the CLIs print exactly `<code>: <fix>`.
4
+ *
5
+ * The carrier's codes are the `reason` (refused send) or `error` (refused
6
+ * inbox read) values it has always sent (carrier-server.mjs, envelope.mjs,
7
+ * delegation.mjs). Those values and their status codes never change here;
8
+ * the carrier only ADDS a `fix` field, and a client talking to an older
9
+ * carrier looks the fix up in this same map. The blob broker sends its own
10
+ * `code` + `fix`, which is printed as received.
11
+ *
12
+ * CLOSED AND STATIC. Every fix this package produces is a value of
13
+ * REFUSAL_FIXES: a fixed string, never built from request data, a remote
14
+ * reason or an exception message. A parameterised reason
15
+ * (`delegation-invalid:<why>`, `bad-version <v>`, `alg-not-active:<alg>`)
16
+ * resolves by its family key, so the parameter never reaches the fix.
17
+ *
18
+ * Documented, with statuses, in HOSTING-LIMITS.md "Refusal codes".
19
+ */
20
+
21
+ const RESEAL = "re-seal the envelope with seal() from @uuaid/pillar or @uuaid/pillar-client; this one does not verify";
22
+
23
+ /** code -> one-line fix. Frozen: add, never rename, never interpolate. */
24
+ export const REFUSAL_FIXES = Object.freeze({
25
+ // Carrier, POST /v1/envelopes
26
+ "rate-limited": "back off: budgets refill over a sliding 60 s, so retry after 5 s, doubling to 60 s",
27
+ "body-too-large": "send the payload as a blob (pillar-blob send); maxBodyBytes is your tier's inline limit",
28
+ "stale-envelope": "seal a new envelope and check your clock; createdAt must be within 24 h of carrier time",
29
+ "carrier-full": "use your other carriers and do not retry this one soon; its store is full",
30
+ "bad-recipient": "address the envelope to a full UUAID (uuaid:<namespace>:<type>:<id>)",
31
+ "bad-json": "POST the envelope as one JSON object",
32
+ "keyId-sender-mismatch": "sign with transportSignature.keyId equal to the sender UUAID, or send under a delegation",
33
+ "publicKey-sender-mismatch": "send from the UUAID your signing key derives, or attach the principal's delegation",
34
+ "bad-signature": "sign again with this identity's own key over the exact bytes; the signature does not verify",
35
+ "delegation-invalid": "mint a fresh delegation from the principal, then resend",
36
+ // Carrier, envelope shape and crypto (envelope.mjs open()): one fix for all
37
+ "not-object": RESEAL,
38
+ "bad-version": RESEAL,
39
+ "missing-or-bad-alg": RESEAL,
40
+ "alg-not-active": RESEAL,
41
+ "bad-publicKey": RESEAL,
42
+ "bad-signature-encoding": RESEAL,
43
+ "missing-enc": RESEAL,
44
+ "enc-alg-not-supported": RESEAL,
45
+ "enc-alg-not-active": RESEAL,
46
+ "bad-epk": RESEAL,
47
+ "bad-iv": RESEAL,
48
+ "bad-ct": RESEAL,
49
+ "bad-canonical": RESEAL,
50
+ "verify-threw": RESEAL,
51
+ // Carrier, GET /v1/inbox/<uuaid>
52
+ "missing-auth-headers": "send x-pillar-pubkey, x-pillar-ts and x-pillar-sig",
53
+ "bad-pubkey": "send x-pillar-pubkey as the raw Ed25519 key in 64 hex characters",
54
+ "bad-sig-encoding": "send x-pillar-sig as 128 hex characters",
55
+ "clock-skew": "fix your clock (NTP) to within 5 min of the carrier; retrying does not help",
56
+ "uuaid-pubkey-mismatch": "read the inbox of the UUAID your key derives, or send that UUAID's delegation in x-pillar-delegation",
57
+ "delegation-unparseable": "send x-pillar-delegation as the JSON delegation document",
58
+ "delegation-too-large": "send a delegation document under 8,192 bytes",
59
+ "delegation-principal-mismatch": "use a delegation whose principal is the inbox UUAID",
60
+ "delegation-delegate-key-mismatch": "sign with the key the delegation names as delegate",
61
+ "delegation-inbox-denied": "ask the principal for a delegation that covers inbox (or *)",
62
+ "busy": "retry after at least 2 s with jitter, or poll with wait=0 for a while",
63
+ // Carrier, any route
64
+ "not-found": "use POST /v1/envelopes or GET /v1/inbox/<uuaid>",
65
+ "not-a-seed": "do not list this carrier as a seed; it runs without a seed identity",
66
+ "internal-error": "retry later; if it repeats, tell the carrier operator",
67
+ // Client side: no server gave a code
68
+ "carrier-unreachable": "check the carrier URLs and your network, then retry",
69
+ "carrier-bad-response": "retry, or use another carrier; the carrier or its proxy answered without a reason",
70
+ "over-tier-limit": "send the payload as a blob (pillar-blob send, or Pillar.sendBlob)",
71
+ "blob-key-unproven": "use a uuaid:foundation:agent UUAID derived from this key (pillar init), or get a registry badge that names this key",
72
+ "sender-key-not-active": "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",
73
+ });
74
+
75
+ /**
76
+ * The fix for a carrier reason, or null for a reason this map does not know.
77
+ * An exact key wins; otherwise the reason's family (the part before the
78
+ * first ":" or space) is looked up, so `delegation-invalid:expired` and
79
+ * `bad-version 2` resolve to their family's fix. The result is always a
80
+ * value of REFUSAL_FIXES or null; nothing from `reason` is copied into it.
81
+ */
82
+ export function carrierFix(reason) {
83
+ if (typeof reason !== "string" || !reason) return null;
84
+ if (Object.hasOwn(REFUSAL_FIXES, reason)) return REFUSAL_FIXES[reason];
85
+ const family = reason.split(/[: ]/, 1)[0];
86
+ return family !== reason && Object.hasOwn(REFUSAL_FIXES, family) ? REFUSAL_FIXES[family] : null;
87
+ }
88
+
89
+ /**
90
+ * A carrier response body with its `fix` added. Pure addition: only a
91
+ * refusal (status >= 400) that names a `reason` or `error` and has no `fix`
92
+ * yet gains one; every other field, and every other body, is returned as is.
93
+ */
94
+ export function withCarrierFix(status, body) {
95
+ if (!(status >= 400) || !body || typeof body !== "object" || Array.isArray(body) || body.fix !== undefined) return body;
96
+ const fix = carrierFix(typeof body.reason === "string" ? body.reason : body.error);
97
+ return fix ? { ...body, fix } : body;
98
+ }
99
+
100
+ /**
101
+ * Terminal safety only: a line break or control character a remote sent
102
+ * becomes a space, so the output stays one line and cannot drive the
103
+ * terminal. Every other character is printed as received.
104
+ */
105
+ function oneLine(value) {
106
+ return String(value).replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
107
+ }
108
+
109
+ /**
110
+ * `<code>: <fix>` for an error that carries both (a carrier or broker
111
+ * refusal, or a client error that knows its next step), else null so the
112
+ * caller keeps its old output. A server's fix is printed as received.
113
+ */
114
+ export function refusalLine(error) {
115
+ const code = typeof error?.code === "string" ? oneLine(error.code) : "";
116
+ const fix = typeof error?.fix === "string" ? oneLine(error.fix) : "";
117
+ return code && fix ? `${code}: ${fix}` : null;
118
+ }
119
+
120
+ /** `{ code, fix }` of an error, for a result row, omitting what is absent. */
121
+ export function refusalFields(error) {
122
+ return {
123
+ ...(typeof error?.code === "string" && error.code ? { code: error.code } : {}),
124
+ ...(typeof error?.fix === "string" && error.fix ? { fix: error.fix } : {}),
125
+ };
126
+ }
package/src/pillar.mjs CHANGED
@@ -48,7 +48,7 @@ export class Pillar {
48
48
  blobStore = null, // BlobStore (src/blob/store.mjs) for the blob transport
49
49
  blobBroker = null, // challenge broker for blob reserve/finalize/redeem
50
50
  blobClient = null, // PillarBlobHttpClient for the production S3 broker path
51
- senderKeyCheck = null, // registry-active gate: (uuaid, pubkeyHex) => status
51
+ senderKeyCheck = null, // registry gate for senders not derived from their key: (uuaid, pubkeyHex) => status
52
52
  }) {
53
53
  if (!keychain?._identity) throw new Error("Pillar: keychain must be loaded before construction");
54
54
  this.keychain = keychain;
@@ -339,7 +339,7 @@ export class Pillar {
339
339
  * so every recipient can reply to the whole group without extra lookups.
340
340
  *
341
341
  * Failures are isolated: one undeliverable member never blocks the rest.
342
- * Returns { group, results: [{ member, ok, via?, envelopeId?, error? }] }.
342
+ * Returns { group, results: [{ member, ok, via?, envelopeId?, error?, code?, fix? }] }.
343
343
  */
344
344
  async sendGroup({ group, members, kind = "message", payload = {}, resolveTimeoutMs = 5000 }) {
345
345
  if (!Array.isArray(members) || members.length === 0) {
@@ -353,7 +353,7 @@ export class Pillar {
353
353
  const r = await this.send({ recipient: member, kind, payload: wrapped, resolveTimeoutMs });
354
354
  results.push({ member, ok: true, via: r.via, envelopeId: r.envelope.id });
355
355
  } catch (e) {
356
- results.push({ member, ok: false, error: e.message });
356
+ results.push({ member, ok: false, error: e.message, ...(typeof e.code === "string" ? { code: e.code } : {}), ...(typeof e.fix === "string" ? { fix: e.fix } : {}) });
357
357
  }
358
358
  }
359
359
  this.metrics.inc("sendGroup");
@@ -405,8 +405,8 @@ export class Pillar {
405
405
  /**
406
406
  * Redeem a blob-ref from an inbound payload — fetch + hash-check +
407
407
  * single-use broker redemption + AES-256-GCM decrypt with the ref's DEK.
408
- * The registry-active sender key gate runs BEFORE the broker redemption
409
- * burns. Returns { data, object, hash, size }.
408
+ * The sender key gate (derived from its key, else a registry badge) runs
409
+ * BEFORE the broker redemption burns. Returns { data, object, hash, size }.
410
410
  */
411
411
  async receiveBlob(payload, { envelopeId = null, now = Date.now() } = {}) {
412
412
  const ref = payload?.blobRef;