@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.
- package/LICENSE +202 -0
- package/README.md +530 -2
- package/dist/agent-uri.d.ts +30 -0
- package/dist/agent-uri.d.ts.map +1 -0
- package/dist/agent-uri.js +218 -0
- package/dist/agent-uri.js.map +1 -0
- package/dist/authorize.d.ts +671 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +947 -0
- package/dist/authorize.js.map +1 -0
- package/dist/capabilities.d.ts +30 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +40 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/chart.d.ts +74 -0
- package/dist/chart.d.ts.map +1 -0
- package/dist/chart.js +107 -0
- package/dist/chart.js.map +1 -0
- package/dist/client.d.ts +644 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1302 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +646 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +928 -0
- package/dist/errors.js.map +1 -0
- package/dist/failover.d.ts +197 -0
- package/dist/failover.d.ts.map +1 -0
- package/dist/failover.js +289 -0
- package/dist/failover.js.map +1 -0
- package/dist/freshness.d.ts +65 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +148 -0
- package/dist/freshness.js.map +1 -0
- package/dist/generated/agent-plane.d.ts +1863 -0
- package/dist/generated/agent-plane.d.ts.map +1 -0
- package/dist/generated/agent-plane.js +19 -0
- package/dist/generated/agent-plane.js.map +1 -0
- package/dist/idempotency.d.ts +160 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +208 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/intent.d.ts +163 -0
- package/dist/intent.d.ts.map +1 -0
- package/dist/intent.js +82 -0
- package/dist/intent.js.map +1 -0
- package/dist/json.d.ts +49 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +259 -0
- package/dist/json.js.map +1 -0
- package/dist/key.d.ts +64 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/key.js +71 -0
- package/dist/key.js.map +1 -0
- package/dist/payment.d.ts +172 -0
- package/dist/payment.d.ts.map +1 -0
- package/dist/payment.js +178 -0
- package/dist/payment.js.map +1 -0
- package/dist/refusal.d.ts +121 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +176 -0
- package/dist/refusal.js.map +1 -0
- package/dist/retry.d.ts +215 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +297 -0
- package/dist/retry.js.map +1 -0
- package/dist/typeid.d.ts +64 -0
- package/dist/typeid.d.ts.map +1 -0
- package/dist/typeid.js +119 -0
- package/dist/typeid.js.map +1 -0
- package/dist/types.d.ts +245 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -3
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-call `authorize`: who is paying, whether they may, and the pin loop that gets
|
|
3
|
+
* the ledger's answer.
|
|
4
|
+
*
|
|
5
|
+
* Everything here but {@link runAuthorize} is a pure function, testable without a
|
|
6
|
+
* socket. Re-sending a request that failed in transit is the client's job, under
|
|
7
|
+
* `authorize`'s own policy and one deadline for the call; see `retry.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import { AxorumError, IdempotencyWindowError, type StaleKeyReason, type WindowHeaderReason } from "./errors.js";
|
|
12
|
+
import { type EndpointAttempt } from "./failover.js";
|
|
13
|
+
import { pinIntent } from "./intent.js";
|
|
14
|
+
import { type IdempotencyKey } from "./idempotency.js";
|
|
15
|
+
import { type RetryReason } from "./retry.js";
|
|
16
|
+
import { type Payment } from "./payment.js";
|
|
17
|
+
import type { AgentOutcome, Envelope, ErrorBody, IdempotencyWindow, ObligationInstance, Refusal } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Where the agent's attestation comes from: a token, or a function asked for one on
|
|
20
|
+
* **every submission** (the first, and each re-pin), so a token that expired meanwhile
|
|
21
|
+
* is replaced rather than resent. A transport re-send carries the same bytes.
|
|
22
|
+
*/
|
|
23
|
+
export type AttestationSource = string | (() => Promise<string>);
|
|
24
|
+
/**
|
|
25
|
+
* The paying agent: its `agent://` URI and where its attestation comes from.
|
|
26
|
+
*
|
|
27
|
+
* The URI must be in **canonical form**: the complete grammar the service parses, with a
|
|
28
|
+
* lowercase host, a port without leading zeros, an IPv6 address as RFC 5952 writes it,
|
|
29
|
+
* and no query or fragment. A keyed payment derives its id from these exact bytes, so
|
|
30
|
+
* one spelling per agent is what makes this SDK and the Rust one derive the same id. The
|
|
31
|
+
* service answers `unknown_agent` for a well-formed URI no party is bound to.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { Agent } from "@axorum/client";
|
|
35
|
+
*
|
|
36
|
+
* // A static token:
|
|
37
|
+
* const buyer = new Agent("agent://acme.com/agent/buyer_01h455vb4pex5vsknk084sn02q", paseto);
|
|
38
|
+
*
|
|
39
|
+
* // Or a source asked on every submission, so an expiring token is refreshed:
|
|
40
|
+
* const refreshing = new Agent(
|
|
41
|
+
* "agent://acme.com/agent/buyer_01h455vb4pex5vsknk084sn02q",
|
|
42
|
+
* () => issuer.mint(),
|
|
43
|
+
* );
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare class Agent {
|
|
47
|
+
#private;
|
|
48
|
+
/** The `agent://` URI, in canonical form: exactly the string the constructor accepted. */
|
|
49
|
+
readonly uri: string;
|
|
50
|
+
/**
|
|
51
|
+
* @throws {InvalidAgentError} If `uri` is not an `agent://` URI in canonical form (its
|
|
52
|
+
* `canonical` then names the form to pass, when there is one), or a static
|
|
53
|
+
* attestation is not a PASETO v4.public token.
|
|
54
|
+
*/
|
|
55
|
+
constructor(uri: string, attestation: AttestationSource);
|
|
56
|
+
/**
|
|
57
|
+
* The attestation for one submission: the token, or a fresh one from the source,
|
|
58
|
+
* with at least {@link ATTESTATION_MIN_REMAINING_MS} left before its `exp`.
|
|
59
|
+
*
|
|
60
|
+
* The service allows no leeway on `exp`, and a submission may be re-sent for up to
|
|
61
|
+
* its budget, so a token with less left could expire between two sends of the same
|
|
62
|
+
* payment. A source whose token is that close is asked once more; nothing is sent
|
|
63
|
+
* unless a token is fresh.
|
|
64
|
+
*
|
|
65
|
+
* @param now - The current time in epoch milliseconds; the system clock by default.
|
|
66
|
+
* @throws {AuthorizeError} Of kind `attestation_expiring` if no token had enough time
|
|
67
|
+
* left, or `attestation_unavailable` if the source failed or returned a token whose
|
|
68
|
+
* claims cannot be read.
|
|
69
|
+
*/
|
|
70
|
+
attestation(transaction: string, now?: () => number): Promise<string>;
|
|
71
|
+
}
|
|
72
|
+
/** Why `token` is not a usable PASETO v4.public token, or `undefined`. Pure. */
|
|
73
|
+
export declare function attestationProblem(token: string): string | undefined;
|
|
74
|
+
/** The policy allowed the payment, and its legs posted. Pay. */
|
|
75
|
+
export interface Allowed {
|
|
76
|
+
readonly allowed: true;
|
|
77
|
+
/** The payment's transaction id. */
|
|
78
|
+
readonly transaction: string;
|
|
79
|
+
/** Duties the agent now owes, such as filing a receipt. */
|
|
80
|
+
readonly obligations: readonly ObligationInstance[];
|
|
81
|
+
/** The signed receipt, when the deployment signs. */
|
|
82
|
+
readonly receipt: Envelope | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Whether this answer is the stored one for a transaction that had already committed
|
|
85
|
+
* (a re-drive), rather than a fresh judgement. A replay carries the **original**
|
|
86
|
+
* `justification_hash`; see {@link justificationMatches}.
|
|
87
|
+
*/
|
|
88
|
+
readonly replayed: boolean;
|
|
89
|
+
/** The ledger's full answer. */
|
|
90
|
+
readonly outcome: AgentOutcome;
|
|
91
|
+
}
|
|
92
|
+
/** The policy refused the payment. It is recorded, nothing posted. Do not pay. */
|
|
93
|
+
export interface Refused {
|
|
94
|
+
readonly allowed: false;
|
|
95
|
+
/** The payment's transaction id. */
|
|
96
|
+
readonly transaction: string;
|
|
97
|
+
/** Why: branch on `code` or `rule.name`, show `explanation` to a human. */
|
|
98
|
+
readonly refusal: Refusal;
|
|
99
|
+
/** The signed receipt of the refusal, when the deployment signs. */
|
|
100
|
+
readonly receipt: Envelope | undefined;
|
|
101
|
+
/** Whether this is the stored answer of an already-committed transaction. */
|
|
102
|
+
readonly replayed: boolean;
|
|
103
|
+
/** The ledger's full answer. */
|
|
104
|
+
readonly outcome: AgentOutcome;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The ledger's answer to a payment: {@link Allowed} or {@link Refused}. A refusal is an
|
|
108
|
+
* answer, not an error.
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* const answer = await axorum.authorize(buyer, lunch);
|
|
112
|
+
* if (answer.allowed) {
|
|
113
|
+
* await pay();
|
|
114
|
+
* } else {
|
|
115
|
+
* console.log(answer.refusal.rule?.name, answer.refusal.explanation);
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export type Authorization = Allowed | Refused;
|
|
120
|
+
/**
|
|
121
|
+
* The answer an outcome gives, or `undefined` for one no agent payment can produce (a
|
|
122
|
+
* posted `ForbiddenRecorded`, an unposted allowing verdict, or a refusal that does not
|
|
123
|
+
* say why). Pure; fails closed: the refusal is always the service's, never invented.
|
|
124
|
+
*/
|
|
125
|
+
export declare function judge(outcome: AgentOutcome): Authorization | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Whether the justification recorded for this answer's transaction is `text`: its
|
|
128
|
+
* SHA-256 equals the answer's `justification_hash`. On a replay the recorded
|
|
129
|
+
* justification is the one the first submission carried, so a re-drive that reworded
|
|
130
|
+
* it resolves `false` here. Async only because it hashes with WebCrypto.
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const answer = await axorum.authorize(buyer, order);
|
|
134
|
+
* if (answer.replayed && !(await justificationMatches(answer, order.justification))) {
|
|
135
|
+
* log.warn("the ledger kept the first justification, not this one");
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare function justificationMatches(answer: Authorization, text: string): Promise<boolean>;
|
|
140
|
+
/** The lowercase-hex SHA-256 of `text`'s UTF-8 bytes, as the service hashes it. */
|
|
141
|
+
export declare function justificationHash(text: string): Promise<string>;
|
|
142
|
+
/**
|
|
143
|
+
* Why no answer was received. Every cause is safe to re-drive: the same payment carries
|
|
144
|
+
* the same transaction id, so a committed one replays.
|
|
145
|
+
*/
|
|
146
|
+
export type Cause =
|
|
147
|
+
/** No connection opened, so nothing was sent. */
|
|
148
|
+
{
|
|
149
|
+
readonly kind: "connect";
|
|
150
|
+
readonly message: string;
|
|
151
|
+
}
|
|
152
|
+
/** The service's own rate limiter shed the request before any handler ran. */
|
|
153
|
+
| {
|
|
154
|
+
readonly kind: "rate_limited";
|
|
155
|
+
readonly message: string;
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: "transport";
|
|
158
|
+
readonly message: string;
|
|
159
|
+
} | {
|
|
160
|
+
readonly kind: "not_primary";
|
|
161
|
+
readonly reason: string | null;
|
|
162
|
+
} | {
|
|
163
|
+
readonly kind: "commit_unavailable";
|
|
164
|
+
readonly reason: string | null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A `502`, `503` or `504`. `code` is the error code its body named (`idempotency_unseeded`,
|
|
168
|
+
* or one this SDK does not know yet), or `null` when the body was not the service's: a
|
|
169
|
+
* proxy's page.
|
|
170
|
+
*/
|
|
171
|
+
| {
|
|
172
|
+
readonly kind: "unavailable";
|
|
173
|
+
readonly status: number;
|
|
174
|
+
readonly code: string | null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The call's time budget ran out before its next request could be sent: the call's
|
|
178
|
+
* deadline, or its submission's (the attestation's `exp` less 30 s) when that is sooner.
|
|
179
|
+
*/
|
|
180
|
+
| {
|
|
181
|
+
readonly kind: "deadline_exceeded";
|
|
182
|
+
readonly elapsedMs: number;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* An earlier attempt of the call ended without a definite answer (`earlier`: its
|
|
186
|
+
* status, or `"timeout"`), so it may have committed. The failure the call then ended
|
|
187
|
+
* with, `last`, is not the ledger's answer about this transaction id and cannot say
|
|
188
|
+
* otherwise.
|
|
189
|
+
*/
|
|
190
|
+
| {
|
|
191
|
+
readonly kind: "earlier_attempt_in_doubt";
|
|
192
|
+
readonly earlier: string;
|
|
193
|
+
readonly last: string;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* What went wrong in an authorize call. Branch on `kind`.
|
|
197
|
+
*
|
|
198
|
+
* Every kind but `indeterminate` fails identically on a re-drive until something
|
|
199
|
+
* outside the call changes (a token, a policy, a chart, the registry).
|
|
200
|
+
*/
|
|
201
|
+
export type AuthorizeFailure =
|
|
202
|
+
/** `403 attestation`: the token was rejected. Refresh it. */
|
|
203
|
+
{
|
|
204
|
+
readonly kind: "attestation_rejected";
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The attestation source failed (`cause` holds what it threw), or gave a token whose
|
|
208
|
+
* claims cannot be read. Nothing was sent.
|
|
209
|
+
*/
|
|
210
|
+
| {
|
|
211
|
+
readonly kind: "attestation_unavailable";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The token expires too soon to outlast a submission's re-sends, and the source gave
|
|
215
|
+
* no fresher one. Nothing was sent.
|
|
216
|
+
*/
|
|
217
|
+
| {
|
|
218
|
+
readonly kind: "attestation_expiring";
|
|
219
|
+
readonly expiresAt: string;
|
|
220
|
+
}
|
|
221
|
+
/** `401 unknown_agent`: no party is bound to this `agent://` URI. */
|
|
222
|
+
| {
|
|
223
|
+
readonly kind: "unknown_agent";
|
|
224
|
+
}
|
|
225
|
+
/** `409 no_active_policy`: nothing can judge the payment. */
|
|
226
|
+
| {
|
|
227
|
+
readonly kind: "no_active_policy";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The payment's currency was never declared by this deployment (a `422
|
|
231
|
+
* substrate_rejected` with cause `undeclared_currency`). Nothing was recorded. The
|
|
232
|
+
* operator must declare it, then open the default chart in it.
|
|
233
|
+
*/
|
|
234
|
+
| {
|
|
235
|
+
readonly kind: "currency_not_declared";
|
|
236
|
+
readonly currency: string;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The default chart is not open in the payment's currency (a `422 substrate_rejected`
|
|
240
|
+
* with cause `unknown_account`). The service refuses an undeclared currency before it
|
|
241
|
+
* looks any account up ({@link currency_not_declared}), so the currency is declared and
|
|
242
|
+
* only the chart is missing. Nothing was recorded. The operator must open the chart.
|
|
243
|
+
*/
|
|
244
|
+
| {
|
|
245
|
+
readonly kind: "chart_not_provisioned";
|
|
246
|
+
readonly currency: string;
|
|
247
|
+
}
|
|
248
|
+
/** The payee is not in the counterparty registry a policy aggregate reads. */
|
|
249
|
+
| {
|
|
250
|
+
readonly kind: "unknown_payee";
|
|
251
|
+
readonly payee: string;
|
|
252
|
+
}
|
|
253
|
+
/** The policy in force does not declare the default chart's action. */
|
|
254
|
+
| {
|
|
255
|
+
readonly kind: "policy_contract";
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* `409 transaction_reused`: this id committed with different content. The hashes
|
|
259
|
+
* cover agent, entries, action and bindings, rail and usage moves; justification,
|
|
260
|
+
* evidence, attestation and policy are not content, so changing only those replays.
|
|
261
|
+
*/
|
|
262
|
+
| {
|
|
263
|
+
readonly kind: "transaction_reused";
|
|
264
|
+
readonly storedContentHash: string | null;
|
|
265
|
+
readonly submittedContentHash: string | null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The payment's key came from your records and the service states no idempotency
|
|
269
|
+
* window (`reason` `"not_advertised"`: its writer could not answer; `"malformed"`: `header` carries `value`, which states no count), so no
|
|
270
|
+
* key from your records can be aged against it. Nothing was sent.
|
|
271
|
+
*/
|
|
272
|
+
| {
|
|
273
|
+
readonly kind: "key_window_unstated";
|
|
274
|
+
readonly reason: WindowHeaderReason;
|
|
275
|
+
readonly header: string | null;
|
|
276
|
+
readonly value: string | null;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The payment's key came from your records (its `origin` is not `minted`) and the service's idempotency window no longer covers it, so the ledger
|
|
280
|
+
* may have forgotten the id and would apply it again. `reason` `"expired"`: the key is
|
|
281
|
+
* `ageMs` old, at or past the `windowSeconds` window; read the transaction to learn
|
|
282
|
+
* whether it landed, and authorize a new payment only if it did not. `"unclocked"`: the
|
|
283
|
+
* service runs no heartbeat (`tick_interval_ms` is 0), so its `windowTicks` window has
|
|
284
|
+
* no wall-clock length and vouches for no key from your records; reconcile the same
|
|
285
|
+
* way first. A new `payment()` goes ahead there, as a key minted in this process
|
|
286
|
+
* was never sent before. Nothing was sent.
|
|
287
|
+
*/
|
|
288
|
+
| {
|
|
289
|
+
readonly kind: "stale_key";
|
|
290
|
+
readonly reason: StaleKeyReason;
|
|
291
|
+
readonly ageMs: number | null;
|
|
292
|
+
readonly windowSeconds: bigint | null;
|
|
293
|
+
readonly windowTicks: bigint;
|
|
294
|
+
}
|
|
295
|
+
/** The payment was keyed for another agent. Nothing was sent. */
|
|
296
|
+
| {
|
|
297
|
+
readonly kind: "keyed_for_another_agent";
|
|
298
|
+
readonly keyedFor: string;
|
|
299
|
+
readonly agent: string;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* `507`: a ledger capacity bound is full. `retryAfterMs` is the answer's `Retry-After`:
|
|
303
|
+
* when it names a wait, a sweep may make room by then; otherwise an operator must act.
|
|
304
|
+
*/
|
|
305
|
+
| {
|
|
306
|
+
readonly kind: "capacity";
|
|
307
|
+
readonly retryAfterMs: number | null;
|
|
308
|
+
}
|
|
309
|
+
/** The policy changed {@link MAX_AUTHORIZE_PINS} times under the call. */
|
|
310
|
+
| {
|
|
311
|
+
readonly kind: "stale_policy";
|
|
312
|
+
readonly pinned: string;
|
|
313
|
+
readonly active: string;
|
|
314
|
+
}
|
|
315
|
+
/** The answer was not received. **It may or may not have committed.** */
|
|
316
|
+
| {
|
|
317
|
+
readonly kind: "indeterminate";
|
|
318
|
+
readonly lastCause: Cause;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Nothing from this call was written: its last answer {@link AxorumError.wroteNothing |
|
|
322
|
+
* wrote nothing}, and no earlier attempt of the call left doubt. `cause` names that
|
|
323
|
+
* answer. `tried` lists every attempt of the call, in order, the last included, and is
|
|
324
|
+
* empty when the budget ran out before the first request was sent; a re-sent status is
|
|
325
|
+
* listed by its status only. A `421` is always doubt, so a call that met one is never
|
|
326
|
+
* `not_submitted`. Never a refusal. Authorize the same {@link Payment} again.
|
|
327
|
+
* {@link NOT_SUBMITTED_CAVEAT}: "If an earlier call with this key was ambiguous, the
|
|
328
|
+
* transaction may still commit; retry with the same key and the server will replay."
|
|
329
|
+
*/
|
|
330
|
+
| {
|
|
331
|
+
readonly kind: "not_submitted";
|
|
332
|
+
readonly tried: readonly EndpointAttempt[];
|
|
333
|
+
readonly cause: NotSubmittedCause;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The ledger answered with an outcome no agent payment can produce: a posted
|
|
337
|
+
* `ForbiddenRecorded`, or a refusal that does not say why. Do not pay.
|
|
338
|
+
*/
|
|
339
|
+
| {
|
|
340
|
+
readonly kind: "unexpected_verdict";
|
|
341
|
+
readonly outcome: AgentOutcome;
|
|
342
|
+
}
|
|
343
|
+
/** Any other documented refusal to process the request. */
|
|
344
|
+
| {
|
|
345
|
+
readonly kind: "rejected";
|
|
346
|
+
readonly status: number;
|
|
347
|
+
readonly body: ErrorBody;
|
|
348
|
+
}
|
|
349
|
+
/** A `200` that did not decode: it may have committed. Read it back. */
|
|
350
|
+
| {
|
|
351
|
+
readonly kind: "undecodable";
|
|
352
|
+
}
|
|
353
|
+
/** A non-Axorum non-2xx (a proxy's `429`, an HTML page). */
|
|
354
|
+
| {
|
|
355
|
+
readonly kind: "unexpected";
|
|
356
|
+
readonly status: number | null;
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* The answer a {@link AuthorizeFailure | `not_submitted`} call ended on, which wrote
|
|
360
|
+
* nothing. The Rust SDK's `NotSubmittedCause`, kind for kind.
|
|
361
|
+
*/
|
|
362
|
+
export type NotSubmittedCause =
|
|
363
|
+
/** No connection to the service could be opened. */
|
|
364
|
+
{
|
|
365
|
+
readonly kind: "connect";
|
|
366
|
+
readonly message: string;
|
|
367
|
+
}
|
|
368
|
+
/** The service's rate limiter turned the attempt away before it was handled. */
|
|
369
|
+
| {
|
|
370
|
+
readonly kind: "rate_limited";
|
|
371
|
+
readonly message: string;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `503 idempotency_unseeded`: the primary had not seeded its idempotency index, and
|
|
375
|
+
* turned the payment away before proposing it, on every attempt the budget allowed.
|
|
376
|
+
*/
|
|
377
|
+
| {
|
|
378
|
+
readonly kind: "unseeded";
|
|
379
|
+
readonly message: string;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* `500 idempotency_seed_refused`: the ledger refused the primary's seed, and the
|
|
383
|
+
* primary turns every payment away before proposing it until the primary changes.
|
|
384
|
+
*/
|
|
385
|
+
| {
|
|
386
|
+
readonly kind: "seed_refused";
|
|
387
|
+
readonly message: string;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* `503 not_normal`: the replica was in a view change or a recovery, and turned the
|
|
391
|
+
* payment away before proposing it, on every attempt the budget allowed.
|
|
392
|
+
*/
|
|
393
|
+
| {
|
|
394
|
+
readonly kind: "not_normal";
|
|
395
|
+
readonly message: string;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* `503 archive_pressure`: the primary's local archive was past its hold ceiling, and
|
|
399
|
+
* the primary turned the payment away before proposing it, on every attempt the budget
|
|
400
|
+
* allowed.
|
|
401
|
+
*/
|
|
402
|
+
| {
|
|
403
|
+
readonly kind: "archive_pressure";
|
|
404
|
+
readonly message: string;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* `503 quarantined`: replay verification found a divergence on the node, and the node
|
|
408
|
+
* turned the payment away before proposing it, on every attempt the budget allowed. It
|
|
409
|
+
* does not clear on its own.
|
|
410
|
+
*/
|
|
411
|
+
| {
|
|
412
|
+
readonly kind: "quarantined";
|
|
413
|
+
readonly message: string;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* `500 proposal_refused`: the node refused the proposal for a reason this build does
|
|
417
|
+
* not name, before proposing it.
|
|
418
|
+
*/
|
|
419
|
+
| {
|
|
420
|
+
readonly kind: "proposal_refused";
|
|
421
|
+
readonly message: string;
|
|
422
|
+
}
|
|
423
|
+
/** The call's time budget ran out before the payment was sent. */
|
|
424
|
+
| {
|
|
425
|
+
readonly kind: "deadline_exceeded";
|
|
426
|
+
readonly elapsedMs: number;
|
|
427
|
+
}
|
|
428
|
+
/** Another answer that {@link AxorumError.wroteNothing | wrote nothing}. */
|
|
429
|
+
| {
|
|
430
|
+
readonly kind: "other";
|
|
431
|
+
readonly message: string;
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* What a {@link AuthorizeFailure | `not_submitted`} failure always says: this call sent
|
|
435
|
+
* nothing, but an earlier ambiguous one may still land.
|
|
436
|
+
*/
|
|
437
|
+
export declare const NOT_SUBMITTED_CAVEAT = "If an earlier call with this key was ambiguous, the transaction may still commit; retry with the same key and the server will replay.";
|
|
438
|
+
/** How many policy rotations one call absorbs by re-pinning. */
|
|
439
|
+
export declare const MAX_AUTHORIZE_PINS = 3;
|
|
440
|
+
/**
|
|
441
|
+
* An authorize call that got no answer. Carries the payment's transaction id, which is
|
|
442
|
+
* what makes recovery safe: re-drive the **same** {@link Payment} and a payment that
|
|
443
|
+
* already committed replays its answer instead of paying twice.
|
|
444
|
+
*
|
|
445
|
+
* ```ts
|
|
446
|
+
* try {
|
|
447
|
+
* const answer = await axorum.authorize(buyer, lunch);
|
|
448
|
+
* } catch (error) {
|
|
449
|
+
* if (error instanceof AuthorizeError && error.retryable) {
|
|
450
|
+
* queue.later(lunch); // same Payment, same transaction id
|
|
451
|
+
* } else if (error instanceof AuthorizeError && error.needsFreshAttestation) {
|
|
452
|
+
* await refreshToken();
|
|
453
|
+
* } else {
|
|
454
|
+
* throw error;
|
|
455
|
+
* }
|
|
456
|
+
* }
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
export declare class AuthorizeError extends AxorumError {
|
|
460
|
+
readonly code = "authorize";
|
|
461
|
+
/** The payment's transaction id. */
|
|
462
|
+
readonly transaction: string;
|
|
463
|
+
/** What went wrong. */
|
|
464
|
+
readonly failure: AuthorizeFailure;
|
|
465
|
+
/** The underlying failure, when there was one. */
|
|
466
|
+
readonly cause: unknown;
|
|
467
|
+
constructor(transaction: string, failure: AuthorizeFailure, cause?: unknown);
|
|
468
|
+
/** The failure's kind, for a `switch`. */
|
|
469
|
+
get kind(): AuthorizeFailure["kind"];
|
|
470
|
+
/**
|
|
471
|
+
* Whether re-driving the same payment later may get an answer: after `indeterminate`,
|
|
472
|
+
* when the answer was not received, and after `not_submitted`, when this call
|
|
473
|
+
* submitted nothing. Every other kind fails identically on a re-drive until something
|
|
474
|
+
* outside the call changes.
|
|
475
|
+
*/
|
|
476
|
+
get retryable(): boolean;
|
|
477
|
+
/** Whether the fix is a fresh attestation. */
|
|
478
|
+
get needsFreshAttestation(): boolean;
|
|
479
|
+
}
|
|
480
|
+
/** What the pin loop does about one failed request. */
|
|
481
|
+
export type Disposition = {
|
|
482
|
+
readonly action: "unanswered";
|
|
483
|
+
readonly cause: Cause;
|
|
484
|
+
} | {
|
|
485
|
+
readonly action: "repin";
|
|
486
|
+
readonly active: string;
|
|
487
|
+
readonly pinned: string;
|
|
488
|
+
} | {
|
|
489
|
+
readonly action: "fail";
|
|
490
|
+
readonly failure: AuthorizeFailure;
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* Decides what to do about a failed request. Pure: the whole table.
|
|
494
|
+
*
|
|
495
|
+
* Re-pinned: a `409` stale pin. Unanswered (`indeterminate`): transport failures, `421`,
|
|
496
|
+
* `502`/`503`/`504`, and a request the deadline left no time to send. Everything else is
|
|
497
|
+
* a typed failure.
|
|
498
|
+
*/
|
|
499
|
+
export declare function classify(error: unknown, pay: Payment): Disposition;
|
|
500
|
+
/**
|
|
501
|
+
* The cause of a `not_submitted` call that ended on `error`, or `undefined` when `error`
|
|
502
|
+
* does not prove its attempt {@link AxorumError.wroteNothing | wrote nothing}. A call
|
|
503
|
+
* that moved between URLs ended on its last attempt's failure. Pure.
|
|
504
|
+
*/
|
|
505
|
+
export declare function unsent(error: unknown): NotSubmittedCause | undefined;
|
|
506
|
+
/**
|
|
507
|
+
* What to do about a failed request of the call, given the first attempt of the call
|
|
508
|
+
* left in doubt, if any, and every attempt `tried`, the last included: `not_submitted`
|
|
509
|
+
* exactly when `error` wrote nothing ({@link unsent}) and nothing was in doubt;
|
|
510
|
+
* {@link classify} otherwise, which the caller reads {@link afterDoubt}. Pure.
|
|
511
|
+
*/
|
|
512
|
+
export declare function dispose(error: unknown, pay: Payment, earlier: string | undefined, tried: readonly EndpointAttempt[]): Disposition;
|
|
513
|
+
/**
|
|
514
|
+
* How a call ends on a request that failed with `error`, given the first attempt of the
|
|
515
|
+
* call left in doubt, if any, and every attempt `tried`, the last included: its
|
|
516
|
+
* {@link dispose | disposition}, a failure read {@link afterDoubt | after the doubt}.
|
|
517
|
+
* Pure.
|
|
518
|
+
*/
|
|
519
|
+
export declare function conclude(error: unknown, pay: Payment, earlier: string | undefined, tried: readonly EndpointAttempt[]): AuthorizeFailure;
|
|
520
|
+
/**
|
|
521
|
+
* Whether an attempt the transport sent again after `reason` may have committed: unless
|
|
522
|
+
* what it saw {@link AxorumError.wroteNothing | wrote nothing}. Only a connection that
|
|
523
|
+
* never opened is definite: a re-sent status arrives without its body, so even a `429`,
|
|
524
|
+
* a `421` or a `507` cannot show the code that would make it definite. Every other
|
|
525
|
+
* outcome, a status this version does not know included, is doubt. Pure; the same rule
|
|
526
|
+
* as the Rust SDK.
|
|
527
|
+
*/
|
|
528
|
+
export declare function leavesDoubt(reason: RetryReason): boolean;
|
|
529
|
+
/**
|
|
530
|
+
* Whether `error` is the ledger's own answer about this transaction id, given after it
|
|
531
|
+
* looked the id up: `409 transaction_reused`, or a `422 substrate_rejected` whose cause
|
|
532
|
+
* is decided after that lookup (every cause but `undeclared_currency` and
|
|
533
|
+
* `amount_overflow`; `unknown_account`, `unknown_counterparty` and the action-contract
|
|
534
|
+
* causes are among them). Had an earlier attempt committed, the service would have
|
|
535
|
+
* answered with its replay instead, so the answer stands after an attempt in doubt.
|
|
536
|
+
* Pure.
|
|
537
|
+
*/
|
|
538
|
+
export declare function settlesTheId(error: unknown): boolean;
|
|
539
|
+
/**
|
|
540
|
+
* `failure`, given the first attempt of the call left in doubt, if any, and the error it
|
|
541
|
+
* came from. Once an earlier attempt is in doubt, an end that read alone says the payment
|
|
542
|
+
* did not land becomes `indeterminate`, cause `earlier_attempt_in_doubt`: a failure whose
|
|
543
|
+
* error does not {@link settlesTheId | settle the id}, and an `indeterminate` end whose
|
|
544
|
+
* error {@link AxorumError.wroteNothing | wrote nothing}. A re-pin, and an
|
|
545
|
+
* `indeterminate` end that is itself in doubt, are unchanged. Pure.
|
|
546
|
+
*/
|
|
547
|
+
export declare function afterDoubt(failure: AuthorizeFailure, earlier: string | undefined, cause?: unknown): AuthorizeFailure;
|
|
548
|
+
/**
|
|
549
|
+
* Whether the call sends a submission again after `error` itself, rather than through
|
|
550
|
+
* the transport: every `503`, read with its body. The transport re-sends on the status
|
|
551
|
+
* alone and cannot tell a `503 idempotency_unseeded`, which wrote nothing, from a
|
|
552
|
+
* `503 commit_unavailable`, which may commit. Pure.
|
|
553
|
+
*/
|
|
554
|
+
export declare function redrives(error: unknown): boolean;
|
|
555
|
+
/**
|
|
556
|
+
* The pause before the call sends a submission again for the `redriven + 1`th time, under
|
|
557
|
+
* {@link AUTHORIZE_RETRY_POLICY} with `draw`, or `undefined` when it would reach the
|
|
558
|
+
* deadline `remainingMs` away. Pure.
|
|
559
|
+
*/
|
|
560
|
+
export declare function redrivePause(redriven: number, draw: number, remainingMs: number): number | undefined;
|
|
561
|
+
/**
|
|
562
|
+
* What the call ends on when the send after a re-drive pause fails with `error`: the
|
|
563
|
+
* `503` it paused after, `pausedAfter`, when the budget ran out before that send could
|
|
564
|
+
* leave (a deadline with no attempt), since the call's last answer is still that `503`;
|
|
565
|
+
* else `error`. The `503` was already heard as re-driven, so the caller lists it once.
|
|
566
|
+
* Pure.
|
|
567
|
+
*/
|
|
568
|
+
export declare function endedAfterRedrive(pausedAfter: unknown, error: unknown): {
|
|
569
|
+
error: unknown;
|
|
570
|
+
heard: boolean;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* The attempt a submission's `error` ended on, as `tried` lists it: the last of a call
|
|
574
|
+
* that moved between URLs, else the one at `endpoint`; `undefined` when nothing was sent
|
|
575
|
+
* or no endpoint is known. Pure.
|
|
576
|
+
*/
|
|
577
|
+
export declare function finalAttempt(error: unknown, endpoint: string | undefined): EndpointAttempt | undefined;
|
|
578
|
+
/**
|
|
579
|
+
* Every attempt a read of the call (the pin, the window) that failed with `error` made:
|
|
580
|
+
* those of a read that moved between URLs, else none, since no recorder hears a read.
|
|
581
|
+
* Pure.
|
|
582
|
+
*/
|
|
583
|
+
export declare function attemptsOf(error: unknown): readonly EndpointAttempt[];
|
|
584
|
+
/**
|
|
585
|
+
* The recorder of one call: hears every attempt its submissions are sent again after,
|
|
586
|
+
* from the transport (a re-sent status, by its status alone) and from the call (a `503`,
|
|
587
|
+
* its body read), and keeps them in order, and the first in doubt. Shared by every
|
|
588
|
+
* submission of the call, so doubt carries across re-pins. The Rust SDK's `Doubt`.
|
|
589
|
+
*/
|
|
590
|
+
export declare class Doubt {
|
|
591
|
+
#private;
|
|
592
|
+
/** The first outcome in doubt, by its label (`"503"`, `"timeout"`), or `undefined`. */
|
|
593
|
+
get earlier(): string | undefined;
|
|
594
|
+
/** Every attempt heard so far, in order. */
|
|
595
|
+
get tried(): readonly EndpointAttempt[];
|
|
596
|
+
/** Hears an attempt the transport sent again: in doubt unless its connection never opened. */
|
|
597
|
+
resent(attempt: EndpointAttempt): void;
|
|
598
|
+
/** Hears a submission the call sent again after reading its `error`: in doubt unless it wrote nothing. */
|
|
599
|
+
redriven(endpoint: string, error: AxorumError): void;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* What bounds one submission: its deadline on the `performance.now()` clock, and who
|
|
603
|
+
* hears each attempt.
|
|
604
|
+
*/
|
|
605
|
+
export interface SubmissionBound {
|
|
606
|
+
readonly deadline: number;
|
|
607
|
+
/** Hears each attempt as the transport sends it again: where it went, how it ended, whether the re-send moved on. */
|
|
608
|
+
readonly onResend: (attempt: EndpointAttempt) => void;
|
|
609
|
+
/** Hears each submission the call sends again itself after reading its `503`: where it went, and the error. */
|
|
610
|
+
readonly onRedrive?: (endpoint: string, error: AxorumError) => void;
|
|
611
|
+
/** Hears where the submission's last attempt went, when it ended without an answer. */
|
|
612
|
+
readonly onEnded?: (endpoint: string) => void;
|
|
613
|
+
}
|
|
614
|
+
/** What {@link runAuthorize} needs from the client: the edges, and nothing else. */
|
|
615
|
+
export interface AuthorizeTransport {
|
|
616
|
+
/** Reads the policy in force. */
|
|
617
|
+
activePolicy(signal: AbortSignal | undefined): Promise<string | null>;
|
|
618
|
+
/**
|
|
619
|
+
* Sends one pinned envelope on `authorize`'s lane, within `bound` or the call's
|
|
620
|
+
* deadline, whichever comes first.
|
|
621
|
+
*/
|
|
622
|
+
submit(envelope: ReturnType<typeof pinIntent>, signal: AbortSignal | undefined, bound: SubmissionBound): Promise<AgentOutcome>;
|
|
623
|
+
/** The pin the client believes is in force, shared by its clones. */
|
|
624
|
+
readonly pin: {
|
|
625
|
+
value: string | undefined;
|
|
626
|
+
};
|
|
627
|
+
/**
|
|
628
|
+
* The wall-clock window the client last read, if it read one. A safe floor: the
|
|
629
|
+
* service never shrinks its window in ticks (`axorum-serve`'s
|
|
630
|
+
* `idempotency::boot_policy` never lowers it, and the admin route refuses a shorter
|
|
631
|
+
* one with `409 idempotency_window_shrink`), and the service's own
|
|
632
|
+
* `409 stale_idempotency_key` catches what the copy misses.
|
|
633
|
+
*/
|
|
634
|
+
keptWindow(): IdempotencyWindow | undefined;
|
|
635
|
+
/**
|
|
636
|
+
* Reads the service's idempotency window, keeping it when it states a wall-clock
|
|
637
|
+
* length. Throws {@link IdempotencyWindowError} when the headers state none.
|
|
638
|
+
*/
|
|
639
|
+
readWindow(signal: AbortSignal | undefined): Promise<IdempotencyWindow>;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Why a keyed payment must not be sent, or `undefined` to go ahead: the pure half of the
|
|
643
|
+
* key gate. A payment keyed for another agent would name that agent's transaction.
|
|
644
|
+
*/
|
|
645
|
+
export declare function keyGate(agentUri: string, pay: Payment): AuthorizeFailure | undefined;
|
|
646
|
+
/**
|
|
647
|
+
* Whether a refusal from the client's kept copy of the window is read again before it
|
|
648
|
+
* stands: only a stale key, and only when the copy was kept rather than just read. The
|
|
649
|
+
* window never shrinks, so a copy that covers the key needs no second look, and one
|
|
650
|
+
* that refuses it may be out of date because the window grew.
|
|
651
|
+
*/
|
|
652
|
+
export declare function rereads(kept: boolean, gated: AuthorizeFailure | undefined): boolean;
|
|
653
|
+
/**
|
|
654
|
+
* Whether a key may be sent at `nowMs` under the service's window, as its headers stated
|
|
655
|
+
* it (an {@link IdempotencyWindowError} when they stated none): `undefined` to send,
|
|
656
|
+
* else why not. A key minted in this process (`held` false) was never sent before and
|
|
657
|
+
* always goes ahead; one from the caller's records goes only while the window covers
|
|
658
|
+
* it. Pure; `spec/fixtures/key-gate-v1.json` pins it in both SDKs.
|
|
659
|
+
*/
|
|
660
|
+
export declare function vouch(held: boolean, key: IdempotencyKey, window: IdempotencyWindow | IdempotencyWindowError, nowMs: number): AuthorizeFailure | undefined;
|
|
661
|
+
/**
|
|
662
|
+
* The pin loop: the only part of `authorize` that does I/O. Pins (from the shared cache,
|
|
663
|
+
* or by reading the active policy), submits, and re-pins at most
|
|
664
|
+
* {@link MAX_AUTHORIZE_PINS} times. It owns no transport retry; the client's lane holds
|
|
665
|
+
* every request to the call's deadline.
|
|
666
|
+
*
|
|
667
|
+
* @param caller - The caller's own cancellation. When it fires, its reason is thrown
|
|
668
|
+
* as-is, never dressed up as a ledger failure.
|
|
669
|
+
*/
|
|
670
|
+
export declare function runAuthorize(transport: AuthorizeTransport, agent: Agent, pay: Payment, caller: AbortSignal | undefined): Promise<Authorization>;
|
|
671
|
+
//# sourceMappingURL=authorize.d.ts.map
|