@axorum/client 0.0.0-stage → 1.0.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.
Files changed (79) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +530 -2
  3. package/dist/agent-uri.d.ts +30 -0
  4. package/dist/agent-uri.d.ts.map +1 -0
  5. package/dist/agent-uri.js +218 -0
  6. package/dist/agent-uri.js.map +1 -0
  7. package/dist/authorize.d.ts +671 -0
  8. package/dist/authorize.d.ts.map +1 -0
  9. package/dist/authorize.js +947 -0
  10. package/dist/authorize.js.map +1 -0
  11. package/dist/capabilities.d.ts +30 -0
  12. package/dist/capabilities.d.ts.map +1 -0
  13. package/dist/capabilities.js +40 -0
  14. package/dist/capabilities.js.map +1 -0
  15. package/dist/chart.d.ts +74 -0
  16. package/dist/chart.d.ts.map +1 -0
  17. package/dist/chart.js +107 -0
  18. package/dist/chart.js.map +1 -0
  19. package/dist/client.d.ts +644 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +1302 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/errors.d.ts +646 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +928 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/failover.d.ts +197 -0
  28. package/dist/failover.d.ts.map +1 -0
  29. package/dist/failover.js +289 -0
  30. package/dist/failover.js.map +1 -0
  31. package/dist/freshness.d.ts +65 -0
  32. package/dist/freshness.d.ts.map +1 -0
  33. package/dist/freshness.js +148 -0
  34. package/dist/freshness.js.map +1 -0
  35. package/dist/generated/agent-plane.d.ts +1863 -0
  36. package/dist/generated/agent-plane.d.ts.map +1 -0
  37. package/dist/generated/agent-plane.js +19 -0
  38. package/dist/generated/agent-plane.js.map +1 -0
  39. package/dist/idempotency.d.ts +160 -0
  40. package/dist/idempotency.d.ts.map +1 -0
  41. package/dist/idempotency.js +208 -0
  42. package/dist/idempotency.js.map +1 -0
  43. package/dist/index.d.ts +89 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +87 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/intent.d.ts +163 -0
  48. package/dist/intent.d.ts.map +1 -0
  49. package/dist/intent.js +82 -0
  50. package/dist/intent.js.map +1 -0
  51. package/dist/json.d.ts +49 -0
  52. package/dist/json.d.ts.map +1 -0
  53. package/dist/json.js +259 -0
  54. package/dist/json.js.map +1 -0
  55. package/dist/key.d.ts +64 -0
  56. package/dist/key.d.ts.map +1 -0
  57. package/dist/key.js +71 -0
  58. package/dist/key.js.map +1 -0
  59. package/dist/payment.d.ts +172 -0
  60. package/dist/payment.d.ts.map +1 -0
  61. package/dist/payment.js +178 -0
  62. package/dist/payment.js.map +1 -0
  63. package/dist/refusal.d.ts +121 -0
  64. package/dist/refusal.d.ts.map +1 -0
  65. package/dist/refusal.js +176 -0
  66. package/dist/refusal.js.map +1 -0
  67. package/dist/retry.d.ts +215 -0
  68. package/dist/retry.d.ts.map +1 -0
  69. package/dist/retry.js +297 -0
  70. package/dist/retry.js.map +1 -0
  71. package/dist/typeid.d.ts +64 -0
  72. package/dist/typeid.d.ts.map +1 -0
  73. package/dist/typeid.js +119 -0
  74. package/dist/typeid.js.map +1 -0
  75. package/dist/types.d.ts +245 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +14 -0
  78. package/dist/types.js.map +1 -0
  79. package/package.json +58 -3
package/dist/errors.js ADDED
@@ -0,0 +1,928 @@
1
+ /**
2
+ * Why a call to the Axorum agent plane failed — and what to do about it.
3
+ *
4
+ * # A refusal is not in here
5
+ *
6
+ * The single most important thing to know about this module is what it does **not**
7
+ * contain. An intent the policy in force forbids is not an error: it reaches the
8
+ * commit point, is judged, and is *recorded*, and that recording is the product. It
9
+ * resolves as an {@link AgentOutcome} with `posted: false` and
10
+ * `verdict: "ForbiddenRejected"`, over a `200`. A client that threw on a refusal would
11
+ * be throwing away the very artifact the ledger exists to produce.
12
+ *
13
+ * What *is* in here are the cases where the ledger never got to answer: the caller was
14
+ * not who they said they were, the pin did not hold, the cluster could not confirm,
15
+ * the wire broke.
16
+ *
17
+ * # Discriminating
18
+ *
19
+ * Two ways, both first-class — use whichever reads better where you are:
20
+ *
21
+ * ```ts
22
+ * // `instanceof`, when you want the narrowed fields:
23
+ * if (error instanceof StalePolicyError) {
24
+ * console.log(error.pinned, error.active); // both typed `string`
25
+ * }
26
+ *
27
+ * // `switch` on `code`, when you want exhaustiveness:
28
+ * switch (error.code) {
29
+ * case "commit_unavailable": // proposed; may or may not have landed — retry is safe
30
+ * case "idempotency_unseeded": // this request was not written and will not be; an
31
+ * // earlier attempt with the same transaction id may
32
+ * // still land — retry is safe
33
+ * case "idempotency_window_full": // no ledger state changed and the id is not
34
+ * // recorded; retry the same request after
35
+ * // `Retry-After`
36
+ * case "not_primary": // may or may not have landed; re-send the same id
37
+ * // through the configured endpoints
38
+ * }
39
+ * ```
40
+ *
41
+ * # Recovering
42
+ *
43
+ * Three accessors say what to do next, and they are deliberately distinct:
44
+ *
45
+ * - {@link AxorumError.isRetriable} — re-send the **identical** request. Safe even for
46
+ * a submission, because the client-minted `transaction` id is the substrate's
47
+ * idempotency key: a replay returns the stored outcome rather than double-posting.
48
+ * - {@link AxorumError.requiresRepin} — the policy moved. Re-pin against the policy now
49
+ * in force and submit again; {@link AxorumClient.submitPinned} does this for you.
50
+ * - {@link NotPrimaryError.primaryApiAddr}: you reached a follower. It names the
51
+ * primary's API origin as the follower last knew it, a hint that can name a primary
52
+ * that has just died. The client does not route on it; re-send the same transaction
53
+ * id through the configured endpoints.
54
+ *
55
+ * @packageDocumentation
56
+ */
57
+ import { describeAttempt } from "./failover.js";
58
+ /**
59
+ * The stable `snake_case` codes the frozen contract documents.
60
+ *
61
+ * An **open vocabulary**: a newer service may invent a code this client has never
62
+ * heard of, and must still be readable. So {@link AxorumErrorCode} does not close the
63
+ * set — this array is what {@link isKnownErrorCode} tests against.
64
+ */
65
+ export const WIRE_ERROR_CODES = [
66
+ "invalid_transaction_id",
67
+ "invalid_account_id",
68
+ "invalid_usage_id",
69
+ // `unauthorized` is what a party-scoped read earns when no `Authorization` header reaches
70
+ // the service at all. This client normally refuses that read locally — see
71
+ // {@link MissingAttestationError} — but a caller driving the wire another way will see it,
72
+ // and the contract documents it on every scoped read.
73
+ "unauthorized",
74
+ "unknown_agent",
75
+ "attestation",
76
+ // `forbidden` is `?actor=` naming a party other than the one the attestation is bound to.
77
+ // The only 403 on the plane that is not an attestation failure.
78
+ "forbidden",
79
+ "unknown_transaction",
80
+ "unknown_account",
81
+ "unknown_usage",
82
+ "stale_policy",
83
+ "policy_pin_mismatch",
84
+ "no_active_policy",
85
+ "not_primary",
86
+ "invalid_agent_uri",
87
+ "capability_mapping",
88
+ "multi_currency_mandate_charge",
89
+ "substrate_rejected",
90
+ "invalid_fulfillment",
91
+ "gateway_error",
92
+ "commit_unavailable",
93
+ // The post-upgrade idempotency seed (contract 5.0.0), on which nothing was written:
94
+ // `503 idempotency_unseeded` is retriable once the primary seeds its index; `500
95
+ // idempotency_seed_refused` is not, until the primary changes.
96
+ "idempotency_unseeded",
97
+ "idempotency_seed_refused",
98
+ // A replica that refused a proposal before making it (cluster mode): `503 not_normal`
99
+ // while it is in a view change or a recovery, `503 archive_pressure` while the primary's
100
+ // local archive is past its hold ceiling. Nothing was written; both are retriable.
101
+ "not_normal",
102
+ "archive_pressure",
103
+ // `503 quarantined`: replay verification found a divergence on the node, which refuses
104
+ // every request until an operator resolves it. `500 proposal_refused`: the node refused
105
+ // the proposal for a reason this build does not name. Nothing was written; neither is
106
+ // retriable.
107
+ "quarantined",
108
+ "proposal_refused",
109
+ // `transaction_reused` is a committed transaction id resubmitted with different content
110
+ // (contract 5.0.0). Nothing was written; the committer sees both content hashes, anyone else neither.
111
+ "transaction_reused",
112
+ // `stale_idempotency_key` is a retry key older than the window less the skew margin
113
+ // (contract 5.0.0). Nothing was written. The client raises the same code itself
114
+ // (StaleIdempotencyKeyError) when it refuses a key before sending, so one `switch`
115
+ // arm covers both.
116
+ "stale_idempotency_key",
117
+ // `invalid_key_mint` is a retry key whose mint time is more than the skew margin ahead of
118
+ // the service's clock, or disagrees with the time its UUIDv7 id records (contract 5.0.0).
119
+ // Nothing was written; the message names the check and its bound.
120
+ "invalid_key_mint",
121
+ // A body refused before the handler ran (contract 5.0.0). Nothing was written:
122
+ // `invalid_envelope` (422) names the field's path and a short reason, never the value
123
+ // sent; `unsupported_media_type` (415) is a body without `application/json`;
124
+ // `body_too_large` (413) exceeds the route's size limit.
125
+ "invalid_envelope",
126
+ "unsupported_media_type",
127
+ "body_too_large",
128
+ // `idempotency_window_full` is a `507`: the idempotency index is at its byte budget
129
+ // (contract 6.0.0). Refused at apply: no ledger state changed and the id is not
130
+ // recorded. Ids already held still replay, and the same request succeeds once a sweep
131
+ // evicts aged ids, so it is retriable.
132
+ "idempotency_window_full",
133
+ // `snapshot_capacity` is a `507`: cluster mode, the primary's next snapshot frame would
134
+ // not fit (contract 6.0.0). Nothing was written; retriable once a sweep or an operator
135
+ // makes room.
136
+ "snapshot_capacity",
137
+ ];
138
+ /**
139
+ * The codes this client raises on its own behalf, when the service never spoke.
140
+ *
141
+ * They are not wire codes and never appear in an error body — they are here so that
142
+ * every {@link AxorumError} has a `code` you can `switch` on, whether or not the
143
+ * failure got as far as an HTTP response.
144
+ */
145
+ export const CLIENT_ERROR_CODES = [
146
+ "config",
147
+ "missing_attestation",
148
+ // A connection that never opened: nothing was sent.
149
+ "connect",
150
+ "transport",
151
+ // The service's own rate limiter shed the request before any handler ran.
152
+ "rate_limited",
153
+ // A request whose deadline passed before it was sent.
154
+ "deadline_exceeded",
155
+ "endpoints_exhausted",
156
+ "unexpected_response",
157
+ "decode",
158
+ // A spec fetch whose headers stated no idempotency window.
159
+ "idempotency_window",
160
+ "invalid_payment",
161
+ "invalid_agent",
162
+ "authorize",
163
+ ];
164
+ /** Whether `code` is one this version of the contract documents. */
165
+ export function isKnownErrorCode(code) {
166
+ return WIRE_ERROR_CODES.includes(code);
167
+ }
168
+ /**
169
+ * The base class of every failure this client raises.
170
+ *
171
+ * Abstract on purpose: you never construct one, you catch one. Every subclass carries
172
+ * a stable {@link AxorumError.code}, so `instanceof` and `switch (error.code)` are both
173
+ * available and both correct.
174
+ */
175
+ export class AxorumError extends Error {
176
+ /** The HTTP status, or `null` when there was no HTTP answer to have one. */
177
+ status;
178
+ /** The service's structured error body, when it sent one. */
179
+ body;
180
+ constructor(message, status, body) {
181
+ super(message);
182
+ this.name = new.target.name;
183
+ this.status = status;
184
+ this.body = body;
185
+ }
186
+ /**
187
+ * Whether re-sending the **identical** request may succeed.
188
+ *
189
+ * True for a transport failure, for an unconfirmed commit, and for
190
+ * `503 idempotency_unseeded` (see {@link ApiError.isRetriable}). It is safe to retry a
191
+ * *submission* on any of them, because the `transaction` id in the envelope is
192
+ * client-minted and is the substrate's idempotency key: an identical resubmission of
193
+ * a transaction that already committed returns its stored outcome, marked
194
+ * `replayed: true`. You cannot double-post by retrying **within the service's
195
+ * idempotency window** ({@link AxorumClient.idempotencyWindow}, 24 hours at least);
196
+ * past it the answer is undefined, which is what {@link AxorumClient.submitKeyed}
197
+ * guards against.
198
+ *
199
+ * False for a stale pin (the identical request would fail identically — see
200
+ * {@link requiresRepin}) and for `not_primary` (the identical request to the *same*
201
+ * replica would fail identically; it has to go to the primary).
202
+ */
203
+ get isRetriable() {
204
+ return false;
205
+ }
206
+ /**
207
+ * Whether the fix is to re-pin the envelope against the policy now in force.
208
+ *
209
+ * True exactly for the `409` pin failures. {@link AxorumClient.submitPinned} acts on
210
+ * this for you.
211
+ */
212
+ get requiresRepin() {
213
+ return false;
214
+ }
215
+ /**
216
+ * Whether this answer proves the attempt that received it changed nothing a retry
217
+ * could duplicate: no ledger state, and no record of its transaction id.
218
+ *
219
+ * Not whether the call failed, nor what an earlier attempt did. An answer qualifies in
220
+ * one of two ways:
221
+ *
222
+ * - **Refused before it was proposed.** Nothing reached the log. Every member below is
223
+ * this kind.
224
+ * - **Refused when it was applied.** The operation is in the log, but the ledger's
225
+ * state did not change and the id was not recorded, so a retry once the window has
226
+ * room commits fresh rather than replaying. A `507` code of this kind joins
227
+ * {@link WROTE_NOTHING_507} with its contract row.
228
+ *
229
+ * The members, each with what proves it:
230
+ *
231
+ * | Class | Why it changed nothing |
232
+ * |---|---|
233
+ * | {@link ConnectError} | no connection opened, so no byte of the request left the process |
234
+ * | {@link RateLimitedError} | the service's limiter answers before any handler runs; rate-shed requests never reached the ledger (`docs/export-and-portability.md`) |
235
+ * | {@link ApiError} `503 idempotency_unseeded`, `500 idempotency_seed_refused`, `503 not_normal` or `503 archive_pressure`, its body read ({@link WROTE_NOTHING_UNPROPOSED}) | the replica turned the request away before proposing it: the contract row for each code |
236
+ * | {@link ApiError} with status `507` and a code in {@link WROTE_NOTHING_507} | the contract row for that code (`idempotency_window_full`, `snapshot_capacity`) |
237
+ * | {@link ConfigError}, {@link MissingAttestationError}, {@link InvalidPaymentError}, {@link InvalidAgentError}, {@link StaleIdempotencyKeyError}, {@link IdempotencyWindowError} | raised by this client before the request was sent |
238
+ * | {@link DeadlineExceededError} with `attempts: 0` | the deadline passed before the first attempt |
239
+ *
240
+ * Everything else is `false`, a status this version does not know included: a timeout
241
+ * or a broken connection after the request went out ({@link TransportError}), a `429`
242
+ * in any shape but the limiter's ({@link UnexpectedResponseError}), `421`, `423`, a
243
+ * `503` whose body is not listed (`commit_unavailable`, or one re-sent on its
244
+ * status alone, whose body no one read), and a `507` whose code is not listed. The same
245
+ * members as the Rust SDK's `AxorumError::wrote_nothing`.
246
+ *
247
+ * ```ts
248
+ * new ConnectError("connection refused").wroteNothing; // true
249
+ * new TransportError("timed out").wroteNothing; // false
250
+ * new UnexpectedResponseError(429, "<html>").wroteNothing; // false
251
+ * ```
252
+ */
253
+ get wroteNothing() {
254
+ return false;
255
+ }
256
+ }
257
+ /**
258
+ * The `507` error codes the agent-plane contract lists as changing **nothing a retry
259
+ * could duplicate**, so {@link AxorumError.wroteNothing} reads them as definite. A `507`
260
+ * with any other code may have written.
261
+ *
262
+ * A code qualifies either way {@link AxorumError.wroteNothing} describes: refused before
263
+ * it was proposed, or refused when applied with no state change and its id not recorded.
264
+ *
265
+ * The contract lists two: `idempotency_window_full`, refused at apply with no state
266
+ * change and its id not recorded, and `snapshot_capacity`, refused before it was
267
+ * proposed. The capacity `507`, `substrate_rejected` with `kind: "capacity"`, is not
268
+ * listed. A code joins this list with the contract row that says so.
269
+ */
270
+ export const WROTE_NOTHING_507 = [
271
+ "idempotency_window_full",
272
+ "snapshot_capacity",
273
+ ];
274
+ /**
275
+ * The codes a replica answers when it turned the request away before proposing it, each
276
+ * with the status it arrives on, so nothing reached the log: `503 idempotency_unseeded`,
277
+ * the primary's index is not seeded yet; `500 idempotency_seed_refused`, the ledger
278
+ * refused its seed; `503 not_normal`, the replica is in a view change or a recovery;
279
+ * `503 archive_pressure`, the primary's local archive is past its hold ceiling;
280
+ * `503 quarantined`, replay verification found a divergence on the node; `500
281
+ * proposal_refused`, the node refused the proposal for a reason this build does not
282
+ * name. Definite only with the body read: a `503` re-sent on its status alone may have been
283
+ * `commit_unavailable`.
284
+ */
285
+ export const WROTE_NOTHING_UNPROPOSED = [
286
+ [503, "idempotency_unseeded"],
287
+ [500, "idempotency_seed_refused"],
288
+ [503, "not_normal"],
289
+ [503, "archive_pressure"],
290
+ [503, "quarantined"],
291
+ [500, "proposal_refused"],
292
+ ];
293
+ /**
294
+ * Whether an answer with `status` and `code` is a `507` whose code `listed` names as
295
+ * changing nothing a retry could duplicate. Split on the code, never the status alone.
296
+ * Pure.
297
+ *
298
+ * @internal Exported for tests; {@link ApiError.wroteNothing} passes
299
+ * {@link WROTE_NOTHING_507}.
300
+ */
301
+ export function listedAsWritingNothing(status, code, listed) {
302
+ return status === 507 && listed.includes(code);
303
+ }
304
+ /** The `code` `acton-service`'s rate limiter puts in its `429` body. */
305
+ export const RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED";
306
+ /**
307
+ * The client could not be built — a base URL that is not a URL, for instance.
308
+ *
309
+ * Thrown by the {@link AxorumClient} constructor, before any request exists.
310
+ */
311
+ export class ConfigError extends AxorumError {
312
+ code = "config";
313
+ constructor(message) {
314
+ super(message, null, null);
315
+ }
316
+ /** Raised before the request was sent. */
317
+ get wroteNothing() {
318
+ return true;
319
+ }
320
+ }
321
+ /**
322
+ * A party-scoped read was attempted on a client that holds no attestation.
323
+ *
324
+ * Raised **before any request leaves the process**, and that is the point. The agent
325
+ * plane's reads are credentialed: without a token the service would answer `401`, and
326
+ * relaying that would tell the caller the *service* rejected their credential when in
327
+ * truth this client never sent one. So the failure names the actual mistake — the client
328
+ * was built without an `attestation` — and costs no round trip to discover.
329
+ *
330
+ * Fix it at construction, or hand an existing client a token with
331
+ * {@link AxorumClient.withAttestation}.
332
+ */
333
+ export class MissingAttestationError extends AxorumError {
334
+ code = "missing_attestation";
335
+ /** What was being read: `"a balance"`, `"a transaction"`, `"obligations"`. */
336
+ operation;
337
+ constructor(operation) {
338
+ super(`cannot read ${operation}: this client holds no attestation. The agent plane's reads are ` +
339
+ `answered only in the scope of the party the caller's PASETO attestation is bound to — ` +
340
+ `build the client with \`new AxorumClient({ baseUrl, attestation })\`, or re-credential ` +
341
+ `an existing one with \`withAttestation\`.`, null, null);
342
+ this.operation = operation;
343
+ }
344
+ /** Raised before the request was sent. */
345
+ get wroteNothing() {
346
+ return true;
347
+ }
348
+ }
349
+ /**
350
+ * A {@link Payment} could not be created: a non-positive amount, an unusable payee, or a
351
+ * timeout that is not a positive number. Raised locally, before any request.
352
+ *
353
+ * ```ts
354
+ * try {
355
+ * payment({ amount: { minor: 0n, currency: "USD" }, payee: "api.openai.com" });
356
+ * } catch (error) {
357
+ * if (error instanceof InvalidPaymentError) console.log(error.message);
358
+ * }
359
+ * ```
360
+ */
361
+ export class InvalidPaymentError extends AxorumError {
362
+ code = "invalid_payment";
363
+ constructor(message) {
364
+ super(message, null, null);
365
+ }
366
+ /** Raised before the request was sent. */
367
+ get wroteNothing() {
368
+ return true;
369
+ }
370
+ }
371
+ /**
372
+ * An {@link Agent} could not be created: its URI is not an `agent://` URI in canonical
373
+ * form, or its attestation is not a PASETO v4.public token. Raised locally, before any
374
+ * request.
375
+ *
376
+ * ```ts
377
+ * try {
378
+ * new Agent("agent://ACME.com/agent/buyer_01h455vb4pex5vsknk084sn02q", token);
379
+ * } catch (error) {
380
+ * if (error instanceof InvalidAgentError && error.canonical !== undefined) {
381
+ * console.log(`pass ${error.canonical}`); // agent://acme.com/agent/buyer_01h455vb4pex5vsknk084sn02q
382
+ * }
383
+ * }
384
+ * ```
385
+ */
386
+ export class InvalidAgentError extends AxorumError {
387
+ code = "invalid_agent";
388
+ /**
389
+ * The canonical form of a refused URI, with any query or fragment stripped, when that
390
+ * parses: the string to pass instead.
391
+ */
392
+ canonical;
393
+ constructor(message, canonical) {
394
+ super(message, null, null);
395
+ this.canonical = canonical;
396
+ }
397
+ /** Raised before the request was sent. */
398
+ get wroteNothing() {
399
+ return true;
400
+ }
401
+ }
402
+ /**
403
+ * No connection to the service could be opened: connection refused, DNS failure, an
404
+ * unreachable host. No byte of the request was sent, so it
405
+ * {@link AxorumError.wroteNothing | wrote nothing}.
406
+ *
407
+ * {@link isRetriable} — a dead socket may come back.
408
+ */
409
+ export class ConnectError extends AxorumError {
410
+ code = "connect";
411
+ /** The underlying `fetch` failure, kept so a diagnosis is not a guess. */
412
+ cause;
413
+ constructor(message, cause) {
414
+ super(message, null, null);
415
+ this.cause = cause;
416
+ }
417
+ get isRetriable() {
418
+ return true;
419
+ }
420
+ get wroteNothing() {
421
+ return true;
422
+ }
423
+ }
424
+ /**
425
+ * The request failed after its connection opened: it timed out, or the connection
426
+ * broke. Some or all of it may have reached the service, so it may have committed.
427
+ *
428
+ * {@link isRetriable} — a dead socket may come back.
429
+ */
430
+ export class TransportError extends AxorumError {
431
+ code = "transport";
432
+ /** The underlying `fetch` failure, kept so a diagnosis is not a guess. */
433
+ cause;
434
+ constructor(message, cause) {
435
+ super(message, null, null);
436
+ this.cause = cause;
437
+ }
438
+ get isRetriable() {
439
+ return true;
440
+ }
441
+ }
442
+ /**
443
+ * A request under a deadline was not sent, because the deadline had passed: for
444
+ * `authorize`, the call's time budget (`AUTHORIZE_DEADLINE_MS`, shared by all of its
445
+ * requests), or 30 seconds before its attestation's `exp` when that is sooner; or a retry
446
+ * policy's `deadlineMs`. Nothing was sent by the attempt that raised it.
447
+ *
448
+ * {@link isRetriable}: re-drive the same payment; a fresh call has a fresh budget.
449
+ */
450
+ export class DeadlineExceededError extends AxorumError {
451
+ code = "deadline_exceeded";
452
+ /** How many attempts of the request were sent before the deadline passed. */
453
+ attempts;
454
+ /** How long the request had run, in milliseconds. */
455
+ elapsedMs;
456
+ constructor(attempts, elapsedMs) {
457
+ super(`the request's deadline passed after ${Math.round(elapsedMs)} ms and ${attempts} attempt(s), ` +
458
+ `so it was not sent again; re-drive it with the same transaction id`, null, null);
459
+ this.attempts = attempts;
460
+ this.elapsedMs = elapsedMs;
461
+ }
462
+ get isRetriable() {
463
+ return true;
464
+ }
465
+ /** True when the deadline passed before the first attempt. */
466
+ get wroteNothing() {
467
+ return this.attempts === 0;
468
+ }
469
+ }
470
+ /**
471
+ * Every endpoint of a {@link AxorumClientOptions.failoverUrls | failover} set failed within
472
+ * the call's budget, after at least one move to the next endpoint: every attempt, in
473
+ * order, and the error the last one ended on.
474
+ *
475
+ * Any attempt may have been applied, so it never {@link AxorumError.wroteNothing | wrote
476
+ * nothing}: re-send under the same transaction id and the service replays it.
477
+ *
478
+ * {@link isRetriable} — a fresh call has a fresh budget.
479
+ */
480
+ export class EndpointsExhaustedError extends AxorumError {
481
+ code = "endpoints_exhausted";
482
+ /** Every attempt of the call, in the order sent. */
483
+ attempts;
484
+ /** The error the last attempt ended on. */
485
+ last;
486
+ /** Milliseconds from the call's start to giving up. */
487
+ elapsedMs;
488
+ cause;
489
+ constructor(attempts, last, elapsedMs) {
490
+ const advice = "an attempt may have been recorded; re-send under the same transaction id and the service replays it";
491
+ super(`every endpoint failed within the call's time budget (${attempts.length} attempt(s) in ` +
492
+ `${elapsedMs} ms: ${attempts.map(describeAttempt).join(", ")}); ${advice}. Last: ${last.message}`, last.status, last.body);
493
+ this.attempts = Object.freeze([...attempts]);
494
+ this.last = last;
495
+ this.cause = last;
496
+ this.elapsedMs = elapsedMs;
497
+ }
498
+ get isRetriable() {
499
+ return true;
500
+ }
501
+ }
502
+ /**
503
+ * `409` — the policy pin did not hold, and **nothing was written**.
504
+ *
505
+ * Covers both codes that mean it, because the recovery is identical: re-pin against
506
+ * {@link active} and resubmit. `stale_policy` is the pre-propose fast path;
507
+ * `policy_pin_mismatch` is the race-closing apply-time check. Which one it was is in
508
+ * {@link code}, if you care — and for choosing what to do next, you do not.
509
+ */
510
+ export class StalePolicyError extends AxorumError {
511
+ code;
512
+ /** The policy the request pinned. */
513
+ pinned;
514
+ /** The policy actually in force. **Re-pin against this one.** */
515
+ active;
516
+ constructor(code, message, pinned, active, body) {
517
+ super(`the policy pin did not hold and nothing was recorded: the request pinned policy '${pinned}', but policy '${active}' is in force — re-pin and resubmit (${message})`, 409, body);
518
+ this.code = code;
519
+ this.pinned = pinned;
520
+ this.active = active;
521
+ }
522
+ get requiresRepin() {
523
+ return true;
524
+ }
525
+ }
526
+ /**
527
+ * `409 no_active_policy` — no policy is in force at all.
528
+ *
529
+ * Not a pin problem: re-pinning cannot help, because there is nothing to pin to. An
530
+ * operator has to activate a policy.
531
+ */
532
+ export class NoActivePolicyError extends AxorumError {
533
+ code = "no_active_policy";
534
+ constructor(message, body = null) {
535
+ super(`no policy is in force on the Axorum ledger service: ${message}`, 409, body);
536
+ }
537
+ }
538
+ /**
539
+ * `401 unknown_agent` — the `agent://` URI is not bound to a party.
540
+ *
541
+ * An **authentication** failure at the gateway. It never reached the ledger and was
542
+ * never recorded, which is exactly why it is an error and a refusal is not.
543
+ */
544
+ export class UnknownAgentError extends AxorumError {
545
+ code = "unknown_agent";
546
+ /** The URI that is not bound, when the service named it. */
547
+ uri;
548
+ constructor(message, uri, body) {
549
+ super(uri === null
550
+ ? `the agent is not bound to a party: ${message}`
551
+ : `the agent '${uri}' is not bound to a party: ${message}`, 401, body);
552
+ this.uri = uri;
553
+ }
554
+ }
555
+ /**
556
+ * `403 attestation` — the PASETO v4.public capability attestation was rejected: bad
557
+ * signature, expired, wrong audience, or an untrusted issuer.
558
+ */
559
+ export class AttestationError extends AxorumError {
560
+ code = "attestation";
561
+ constructor(message, body) {
562
+ super(`the capability attestation was rejected: ${message}`, 403, body);
563
+ }
564
+ }
565
+ /**
566
+ * `421 not_primary` — cluster mode: this mutation reached a non-primary replica.
567
+ *
568
+ * The attempt may or may not have been recorded: a primary stepping down can answer
569
+ * `421` after proposing it. The primary's API origin, as the follower last knew it, is
570
+ * in {@link primaryApiAddr}: a hint, and **this client does not route on it**. Re-send
571
+ * the same transaction id through the configured endpoints; the ledger applies it at
572
+ * most once.
573
+ */
574
+ export class NotPrimaryError extends AxorumError {
575
+ code = "not_primary";
576
+ /**
577
+ * The primary's API origin as the follower last knew it, or none. Informational: it
578
+ * can name a primary that has just died. The client does not route on it; it rotates
579
+ * its configured endpoints.
580
+ *
581
+ * A `null` in the body and an absent key collapse to the same thing here.
582
+ */
583
+ primaryApiAddr;
584
+ /** Why this replica is not the primary, when it said. */
585
+ reason;
586
+ constructor(message, primaryApiAddr, reason, body) {
587
+ super(primaryApiAddr === null
588
+ ? `this replica is not the primary, and this attempt may or may not have been recorded: a primary stepping down can answer so after proposing it. It did not know which replica is. Re-send the same transaction id through the configured endpoints: ${message}`
589
+ : `this replica is not the primary, and this attempt may or may not have been recorded: a primary stepping down can answer so after proposing it. It named '${primaryApiAddr}' as the primary's API origin, a hint that may be stale. Re-send the same transaction id through the configured endpoints: ${message}`, 421, body);
590
+ this.primaryApiAddr = primaryApiAddr;
591
+ this.reason = reason;
592
+ }
593
+ }
594
+ /**
595
+ * `503 commit_unavailable` — cluster mode: the request was proposed and its commit could
596
+ * not be confirmed (timeout, node gone, or an ambiguous view change).
597
+ *
598
+ * **The mutation may or may not have landed.** {@link isRetriable} is `true`, and
599
+ * genuinely safe: submission is idempotent by `transaction` id, so re-sending the same
600
+ * envelope either lands it or replays the outcome it already has. Or read the
601
+ * transaction back with {@link AxorumClient.transaction} to find out which happened.
602
+ *
603
+ * A request stopped before it was proposed is never this error (contract 5.0.0): the
604
+ * service answers `503 idempotency_unseeded` when its idempotency index is not seeded
605
+ * yet (this request was not written and will not be; an earlier attempt with the same
606
+ * transaction id may still land; retriable), and `500 idempotency_seed_refused` when the
607
+ * ledger refused the seed (nothing written; do not retry). Both arrive as
608
+ * {@link ApiError}.
609
+ */
610
+ export class CommitUnavailableError extends AxorumError {
611
+ code = "commit_unavailable";
612
+ /** Why the commit is unconfirmed, when the service said. */
613
+ reason;
614
+ constructor(message, reason, body) {
615
+ super(`the commit could not be confirmed, so it may or may not have been recorded; retry or read the transaction back: ${message}`, 503, body);
616
+ this.reason = reason;
617
+ }
618
+ get isRetriable() {
619
+ return true;
620
+ }
621
+ }
622
+ /**
623
+ * `409 transaction_reused` — the transaction id already committed, carrying **different**
624
+ * content, and **nothing was written**.
625
+ *
626
+ * A transaction id is the retry key: resubmitting the identical intent under it answers
627
+ * the original outcome (`replayed: true`). Anything else under a committed id is a
628
+ * different transaction wearing an old name, and the ledger refuses to guess which one
629
+ * the caller meant. To the party that committed the id, both content hashes are here, so
630
+ * it can see the two are not the same intent without the service echoing any content
631
+ * back. Any other party is shown neither: the stored hash would tell it what the
632
+ * committer intended, so both fields are `undefined` and the id is simply taken.
633
+ *
634
+ * The fix is one of two, and only the caller knows which: mint a new transaction id for
635
+ * a genuinely new intent, or resubmit the identical intent to read the original answer.
636
+ */
637
+ export class TransactionReusedError extends AxorumError {
638
+ code = "transaction_reused";
639
+ /**
640
+ * The SHA-256 of the intent committed under the id, as 64 lowercase hex digits.
641
+ * Present only to the party that committed the id.
642
+ */
643
+ storedContentHash;
644
+ /**
645
+ * The SHA-256 of the rejected submission, as 64 lowercase hex digits. Present only to
646
+ * the party that committed the id, with {@link storedContentHash}.
647
+ */
648
+ submittedContentHash;
649
+ constructor(message, storedContentHash, submittedContentHash, body) {
650
+ const hashes = storedContentHash !== undefined && submittedContentHash !== undefined
651
+ ? ` (stored ${storedContentHash}, submitted ${submittedContentHash})`
652
+ : "";
653
+ super(`the transaction id already committed with different content and nothing was recorded; ` +
654
+ `mint a new transaction id for a new intent, or resubmit the identical intent to read ` +
655
+ `the original outcome${hashes}: ${message}`, 409, body);
656
+ this.storedContentHash = storedContentHash;
657
+ this.submittedContentHash = submittedContentHash;
658
+ }
659
+ }
660
+ /**
661
+ * A retry key the service's idempotency window no longer covers, refused **before any
662
+ * request left the process**.
663
+ *
664
+ * Within the window, resubmitting a committed transaction id replays the original
665
+ * outcome. Past it the id may have been forgotten, and the service refuses the
666
+ * resubmission `409 stale_idempotency_key` rather than commit it a second time. So a
667
+ * key older than `window_seconds` is not sent. Nothing was written, and nothing can
668
+ * have been.
669
+ *
670
+ * The fix is always the same: **mint a new transaction id**. If the original may have
671
+ * committed, read it back with {@link AxorumClient.transaction} first.
672
+ *
673
+ * `reason: "unclocked"` means the service advertises no wall-clock window (its
674
+ * heartbeat is disabled, so its window is counted in ticks that have no length in
675
+ * seconds). No key can be vouched for there, however young.
676
+ */
677
+ export class StaleIdempotencyKeyError extends AxorumError {
678
+ code = "stale_idempotency_key";
679
+ /** The transaction id the key was minted for. */
680
+ transaction;
681
+ /** Why the key was refused. */
682
+ reason;
683
+ /** The key's age in milliseconds, or `null` when the reason is `"unclocked"`. */
684
+ ageMs;
685
+ /** The advertised window in seconds, or `null` when the reason is `"unclocked"`. */
686
+ windowSeconds;
687
+ /** The advertised window in server ticks. */
688
+ windowTicks;
689
+ constructor(transaction, reason, ageMs, windowSeconds, windowTicks) {
690
+ super(reason === "unclocked"
691
+ ? `the retry key for transaction '${transaction}' cannot be vouched for: the service runs ` +
692
+ `no heartbeat, so its idempotency window is ${windowTicks} ticks with no wall-clock ` +
693
+ `length. Nothing was sent; mint a new transaction id, or submit without a key check.`
694
+ : `the retry key for transaction '${transaction}' is ${Math.floor((ageMs ?? 0) / 1000)} ` +
695
+ `seconds old, past the service's ${windowSeconds} second idempotency window, so a ` +
696
+ `resubmission might commit twice. Nothing was sent; mint a new transaction id (read ` +
697
+ `the original back with \`transaction\` if it may have committed).`, null, null);
698
+ this.transaction = transaction;
699
+ this.reason = reason;
700
+ this.ageMs = ageMs;
701
+ this.windowSeconds = windowSeconds;
702
+ this.windowTicks = windowTicks;
703
+ }
704
+ /** Raised before the request was sent. */
705
+ get wroteNothing() {
706
+ return true;
707
+ }
708
+ }
709
+ /**
710
+ * The spec fetch answered, but its headers did not state an idempotency window
711
+ * (contract 5.0.0).
712
+ *
713
+ * `reason: "not_advertised"`: the ticks or capacity header is absent, because the
714
+ * service predates contract 5.0.0 or its writer could not answer. Upgrade the service,
715
+ * or retry once it is healthy. `reason: "malformed"`: a header is present but states no
716
+ * count from 1 to `2^64 - 1`; {@link header} and {@link value} name it, and something
717
+ * (the service or a proxy in front of it) rewrote it.
718
+ *
719
+ * Until a window is read no key can be aged, so {@link AxorumClient.submitKeyed} cannot
720
+ * be used safely.
721
+ */
722
+ export class IdempotencyWindowError extends AxorumError {
723
+ code = "idempotency_window";
724
+ /** Why no window was read. */
725
+ reason;
726
+ /** The malformed header's name, or `null` when the reason is `"not_advertised"`. */
727
+ header;
728
+ /** The malformed header's value, or `null` when the reason is `"not_advertised"`. */
729
+ value;
730
+ constructor(status, reason, header, value) {
731
+ super(reason === "not_advertised"
732
+ ? "the Axorum ledger service advertises no idempotency window (it predates contract " +
733
+ "5.0.0, or its writer could not answer); upgrade the service or retry once it is healthy"
734
+ : `the Axorum ledger service's \`${header}\` header carries ${JSON.stringify(value)}, which ` +
735
+ "is not a count from 1 to 18446744073709551615; the service or a proxy in front of it " +
736
+ "rewrote the header", status, null);
737
+ this.reason = reason;
738
+ this.header = header;
739
+ this.value = value;
740
+ }
741
+ /** Raised before the request was sent. */
742
+ get wroteNothing() {
743
+ return true;
744
+ }
745
+ }
746
+ /**
747
+ * A documented agent-plane error with no more specific class: a bad id, an unknown
748
+ * account, an unknown transaction, a substrate rejection, a capability-mapping failure.
749
+ *
750
+ * These keep their whole {@link body}, extras and all. A class per code would be a
751
+ * bigger surface that told a caller nothing more — there is only one thing to do about
752
+ * an unknown account, and it is not a recovery.
753
+ */
754
+ export class ApiError extends AxorumError {
755
+ code;
756
+ /** The full structured body, extras and all. Never `null` on this class. */
757
+ body;
758
+ /**
759
+ * The wait the answer's `Retry-After` asked for, in milliseconds, or `null` when it
760
+ * named none. Read only: nothing in this client re-sends a `503` or a `507` on it.
761
+ */
762
+ retryAfterMs;
763
+ constructor(status, body, retryAfterMs = null) {
764
+ super(`the Axorum ledger service returned ${status}: ${body.message} (${body.error})`, status, body);
765
+ this.code = body.error;
766
+ this.body = body;
767
+ this.retryAfterMs = retryAfterMs;
768
+ }
769
+ /**
770
+ * True for `503 idempotency_unseeded`, `503 not_normal`, `503 archive_pressure`,
771
+ * `507 idempotency_window_full` and `507 snapshot_capacity`: the service had not seeded
772
+ * its idempotency index, the replica was in a view change, the primary's archive was
773
+ * behind, the index was at its byte budget, or the primary's snapshot store could not
774
+ * take the commit. None of them changed ledger state or recorded the id:
775
+ * `not_normal`, `archive_pressure` and `snapshot_capacity` are refused before they are
776
+ * proposed, `idempotency_window_full` at apply, and `idempotency_unseeded` at either. Re-sending the identical request is safe (the
777
+ * `transaction` id is the idempotency key); after a `507`, wait at least
778
+ * {@link retryAfterMs}. False for every other code here, the generic
779
+ * `507 substrate_rejected`, `500 idempotency_seed_refused`, `503 quarantined` and
780
+ * `500 proposal_refused` included: those need an operator.
781
+ */
782
+ get isRetriable() {
783
+ return ((this.status === 503 &&
784
+ (this.code === "idempotency_unseeded" ||
785
+ this.code === "not_normal" ||
786
+ this.code === "archive_pressure")) ||
787
+ (this.status === 507 &&
788
+ (this.code === "idempotency_window_full" || this.code === "snapshot_capacity")));
789
+ }
790
+ /**
791
+ * True for an unproposed code on its own status ({@link WROTE_NOTHING_UNPROPOSED}), and for a `507`
792
+ * whose code is in {@link WROTE_NOTHING_507}.
793
+ */
794
+ get wroteNothing() {
795
+ return (WROTE_NOTHING_UNPROPOSED.some(([status, code]) => this.status === status && this.code === code) ||
796
+ listedAsWritingNothing(this.status, this.code, WROTE_NOTHING_507));
797
+ }
798
+ }
799
+ /**
800
+ * A non-2xx whose body is not an agent-plane error body at all — a proxy's 502 page, a
801
+ * load balancer's 429, a gateway timeout.
802
+ *
803
+ * The bytes are kept **verbatim** in {@link rawBody}: an unreadable failure is exactly
804
+ * the moment a human needs to see what actually came back, so this class does not
805
+ * pretend to have understood it.
806
+ */
807
+ export class UnexpectedResponseError extends AxorumError {
808
+ code = "unexpected_response";
809
+ /** The response body, exactly as it arrived. */
810
+ rawBody;
811
+ constructor(status, rawBody) {
812
+ super(`the Axorum ledger service returned an unexpected ${status}: ${rawBody}`, status, null);
813
+ this.rawBody = rawBody;
814
+ }
815
+ }
816
+ /**
817
+ * `429` in the shape the service's own rate limiter answers with
818
+ * (`{"error": ..., "code": "RATE_LIMIT_EXCEEDED", "status": 429}`): the request was shed
819
+ * before any handler ran, so it {@link AxorumError.wroteNothing | wrote nothing}. A `429`
820
+ * in any other shape (a proxy's, a load balancer's) is an
821
+ * {@link UnexpectedResponseError}, and proves nothing.
822
+ *
823
+ * {@link isRetriable} — send it again later.
824
+ */
825
+ export class RateLimitedError extends AxorumError {
826
+ code = "rate_limited";
827
+ /** The limiter's own text, its body's `error`. */
828
+ detail;
829
+ constructor(detail) {
830
+ super(`the Axorum ledger service's rate limiter turned the request away before it was handled ` +
831
+ `(${detail}); send it again later`, 429, null);
832
+ this.detail = detail;
833
+ }
834
+ get isRetriable() {
835
+ return true;
836
+ }
837
+ get wroteNothing() {
838
+ return true;
839
+ }
840
+ }
841
+ /**
842
+ * A success body that did not decode — the service and this client disagree about the
843
+ * contract.
844
+ *
845
+ * The bytes are kept verbatim in {@link rawBody}, for the same reason as
846
+ * {@link UnexpectedResponseError}.
847
+ */
848
+ export class DecodeError extends AxorumError {
849
+ code = "decode";
850
+ /** The response body, exactly as it arrived. */
851
+ rawBody;
852
+ constructor(status, reason, rawBody) {
853
+ super(`the Axorum ledger service's ${status} response was not understood: ${reason} (body: ${rawBody})`, status, null);
854
+ this.rawBody = rawBody;
855
+ }
856
+ }
857
+ /** Whether `value` is a failure this client raised. A type guard, for a bare `catch`. */
858
+ export function isAxorumError(value) {
859
+ return value instanceof AxorumError;
860
+ }
861
+ /**
862
+ * Lifts a parsed agent-plane error body into the class that says what to do about it.
863
+ *
864
+ * **A pure function of `(status, body)`** — the whole of this client's error semantics,
865
+ * testable without a socket, and the reason the taxonomy can be trusted.
866
+ *
867
+ * @param status - The HTTP status.
868
+ * @param body - The parsed error body.
869
+ * @param retryAfterMs - The answer's `Retry-After`, in milliseconds, when it named one;
870
+ * an {@link ApiError} carries it.
871
+ * @returns The most specific {@link AxorumError} the pair identifies.
872
+ */
873
+ export function errorFromBody(status, body, retryAfterMs = null) {
874
+ const { error: code, message } = body;
875
+ if (status === 409 && (code === "stale_policy" || code === "policy_pin_mismatch")) {
876
+ // Requires both ids to actually be present. A pin failure that names no policies is
877
+ // a malformed body, and falls through to `ApiError` rather than being invented: a
878
+ // client cannot re-pin against a policy id it made up.
879
+ if (body.pinned !== undefined && body.active !== undefined) {
880
+ return new StalePolicyError(code, message, body.pinned, body.active, body);
881
+ }
882
+ return new ApiError(status, body);
883
+ }
884
+ if (status === 409 && code === "transaction_reused") {
885
+ // Both hashes (the committer's answer) or neither (anyone else's); exactly one is a
886
+ // malformed body: a caller comparing content cannot compare against a hash the client
887
+ // invented.
888
+ const { stored_content_hash: stored, submitted_content_hash: submitted } = body;
889
+ if ((stored === undefined) === (submitted === undefined)) {
890
+ return new TransactionReusedError(message, stored, submitted, body);
891
+ }
892
+ return new ApiError(status, body);
893
+ }
894
+ if (status === 409 && code === "no_active_policy") {
895
+ return new NoActivePolicyError(message, body);
896
+ }
897
+ if (status === 401 && code === "unknown_agent") {
898
+ return new UnknownAgentError(message, body.uri ?? null, body);
899
+ }
900
+ if (status === 403 && code === "attestation") {
901
+ return new AttestationError(message, body);
902
+ }
903
+ if (status === 421 && code === "not_primary") {
904
+ // The key is present-but-`null` when the follower does not know the primary, and
905
+ // absent on every other code. Neither gives a caller an address to act on, so both
906
+ // become `null`.
907
+ return new NotPrimaryError(message, body.primary_api_addr ?? null, body.reason ?? null, body);
908
+ }
909
+ if (status === 503 && code === "commit_unavailable") {
910
+ return new CommitUnavailableError(message, body.reason ?? null, body);
911
+ }
912
+ return new ApiError(status, body, retryAfterMs);
913
+ }
914
+ /**
915
+ * Whether a parsed JSON value is an agent-plane error body.
916
+ *
917
+ * The contract requires exactly two fields of every error body — `error` and `message`
918
+ * — so those two are what identify one. A non-2xx body without them did not come from
919
+ * the agent plane; it came from something in front of it.
920
+ */
921
+ export function isErrorBody(value) {
922
+ if (typeof value !== "object" || value === null) {
923
+ return false;
924
+ }
925
+ const candidate = value;
926
+ return typeof candidate["error"] === "string" && typeof candidate["message"] === "string";
927
+ }
928
+ //# sourceMappingURL=errors.js.map