@ziffer-io/client 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@ const anchor: TrustAnchor = {
27
27
  const client = new ZifferClient(process.env.ZIFFER_API_URL, process.env.ZIFFER_API_KEY);
28
28
  const submitted = await client.propose(proposal);
29
29
  const decision = await client.wait(submitted.decision_id, { timeoutMs: 30_000 });
30
- if (decision.outcome !== 'ALLOW') throw new Error(`ziffer refused: ${decision.clause}`);
30
+ if (decision.outcome !== 'ALLOW') throw new Error(`ziffer refused: ${decision.refusal_category}`);
31
31
 
32
32
  verifyReceipt(decision.receipt, new TextEncoder().encode(JSON.stringify(proposal)), anchor);
33
33
  await bank.transfer(amount, toAccount); // your line, unchanged
@@ -60,6 +60,54 @@ Every refusal is a thrown `Refusal` whose `clause` names the rule that fired; th
60
60
  clause, what it means and what to do is at https://ziffer.io/docs/refusals. Narrow with
61
61
  `instanceof Refusal`, record the clause, and do not retry it.
62
62
 
63
+ ## When a call does not get through
64
+
65
+ The client resends a request only when resending can help. That is: when the call got no answer at
66
+ all — the connection was refused, DNS failed, or the round trip took longer than 10 seconds — and
67
+ when the answer was 429, 502, 503 or 504.
68
+
69
+ Everything else reaches you as it is. A 400, 401, 403 or 404 means the request was refused on its
70
+ merits, and the same bytes will be refused the same way. A 500 is not resent either, and that one
71
+ is deliberate: ZIFFER reports its own degradations as 502 and 503, so a 500 is a fault it did not
72
+ expect, and repeating it repeats the fault.
73
+
74
+ Your bytes are sent again unchanged. A resent proposal lands on the hold the first attempt already
75
+ opened, so a retry never asks a second person to approve the same action.
76
+
77
+ There are at most five attempts. The wait between them doubles and is randomised, up to four
78
+ seconds before the last one — randomised so that a fleet of your processes that all saw the same
79
+ outage does not come back at the same instant. When an answer asks for a specific wait, in whole
80
+ seconds, the client waits exactly that long and adds nothing of its own. Retrying is also a
81
+ budget: ten retries the client decides for itself, one earned back per call that succeeds. A
82
+ client whose calls are all failing stops resending instead of adding to the load.
83
+
84
+ **The 10-second timeout is per attempt, not per call.** A call that retries can take longer than
85
+ 10 seconds and is not wrong for doing so. Waiting for a person to approve is not an attempt at
86
+ all: that is `client.wait`, which polls, and each poll is its own attempt with its own timeout.
87
+
88
+ ### Putting a bound on the whole call
89
+
90
+ ```ts
91
+ await client.propose(proposal, { deadlineMs: 5_000 });
92
+ ```
93
+
94
+ With a deadline the client never starts a wait that would end after it. It throws
95
+ `DeadlineExceeded` instead, and that error tells you what was actually failing — `lastError` and
96
+ `status` — so you can tell "the gateway was down" from "my bound was too short". Without a
97
+ deadline there is no such check, and a server that asks for a long wait gets it.
98
+
99
+ ### What the client has been doing
100
+
101
+ ```ts
102
+ const { retries_directed, retries_computed, retry_bucket_level } = client.retryCounters;
103
+ ```
104
+
105
+ How many resends the server asked for, how many the client decided on itself, and how much of the
106
+ retry budget is left (10 when full). The two counts run for the life of the client; read them
107
+ before and after a call for that call's own numbers. They are worth a gauge in your own metrics:
108
+ a `retries_computed` that climbs while `retry_bucket_level` sits at zero is the shape of an
109
+ outage you are riding out rather than one you are told about.
110
+
63
111
  ## Documentation
64
112
 
65
113
  - Quickstart: https://ziffer.io/docs/quickstart
package/dist/client.d.ts CHANGED
@@ -43,8 +43,31 @@
43
43
  * - A poll that outlives its deadline throws {@link WaitTimeout}. A timeout
44
44
  * is "no answer yet", never "the answer was no" (ingress-low's rule for
45
45
  * its own dependency, one layer out).
46
+ * - A call that was still retrying when the caller's deadline arrived throws
47
+ * {@link DeadlineExceeded}, carrying the last failure's name and status.
48
+ * Inside {@link ZifferClient.wait} it is caught and re-thrown as
49
+ * {@link WaitTimeout} with that `DeadlineExceeded` as its `cause`: `wait`
50
+ * has ONE name for "no answer yet" and a caller already catches it.
51
+ *
52
+ * # Timeouts and retries
53
+ *
54
+ * Every round trip carries an abort signal set to {@link REQUEST_TIMEOUT_MS}.
55
+ * **That timeout covers ONE ROUND TRIP** — the send and the reading of the
56
+ * answer's body — and is not a budget for the call: a call that retries may
57
+ * take several times as long and is not wrong for doing so. Waiting for a
58
+ * human to approve is not a round trip at all; that is {@link
59
+ * ZifferClient.wait}, which polls, and each poll is its own round trip under
60
+ * its own timeout.
61
+ *
62
+ * There is ONE request path and the retry loop is inside it, so `propose`,
63
+ * `decision` and every poll of `wait` retry under exactly the same rules
64
+ * (`retry.ts` holds them, `retry.test.ts` replays the corpus both SDKs share).
65
+ * The bytes are serialised once, above the loop, and the same bytes are
66
+ * resent: the gateway keys a pending hold on the hash of what it received, so
67
+ * identical bytes land on the same hold instead of opening a second one.
46
68
  */
47
69
  import type { wire } from '@ziffer-io/types';
70
+ import { type RetryCounters } from './retry.js';
48
71
  /**
49
72
  * The header every SUCCESSFUL answer carries (ACP-256 §5): the instant the key
50
73
  * that authenticated the call ends, RFC 3339 in UTC. Lowercase because that is
@@ -92,11 +115,50 @@ export interface WireProposalPayload {
92
115
  }
93
116
  /** The two §1 states. `decided` means the relay recorded a verdict; it does
94
117
  * NOT mean a receipt exists — `receipt` is present iff one does. */
118
+ /**
119
+ * ACP-392. One short message about something our documentation does not
120
+ * answer, and the ONLY thing this client sends that is not a Proposal.
121
+ *
122
+ * The three members are the route's CLOSED set: a fourth is refused by the
123
+ * gateway rather than dropped, so this type cannot quietly grow a field that
124
+ * carries a developer's environment off their machine. `context` is optional
125
+ * and absent means absent.
126
+ */
127
+ export interface Feedback {
128
+ /** Which tool could not answer — free text, because the tools naming
129
+ * themselves live on the other side of this wire. */
130
+ readonly tool: string;
131
+ /** What was asked. */
132
+ readonly question: string;
133
+ /** Anything else worth knowing. Never a credential: it is stored as written
134
+ * and read by an operator. */
135
+ readonly context?: string;
136
+ }
137
+ /** What `POST /v1/feedback` answers. `tenant` is the one thing the caller did
138
+ * not send — the key carried it. */
139
+ export interface FeedbackStored {
140
+ readonly stored: true;
141
+ readonly tenant: string;
142
+ }
95
143
  export type DecisionStatus = 'pending' | 'decided';
96
144
  /**
97
- * What both §1 routes answer. Refusals expose exactly `{outcome, clause}` —
98
- * the fields ingress-low's `relayed_response` exposes — and an absent
99
- * `clause` is ABSENT, never `null`: one object, one encoding.
145
+ * RF-4's refusal category (ACP-402): the one thing a caller's code needs to
146
+ * decide whether to try again, served INSTEAD of the clause on every
147
+ * tenant-facing decision route. A closed set of four, carried verbatim (RF-2):
148
+ * never mapped from anything, never invented, and a value outside the set is
149
+ * refused as a malformed answer rather than repaired into one of these.
150
+ *
151
+ * - `PolicyRefused` — a rule refused the action: never retry.
152
+ * - `PolicyBasisMoved` — the policy basis moved: retry once after activation.
153
+ * - `RateBounded` — retry later.
154
+ * - `ProposalMalformed` — fix the submission.
155
+ */
156
+ export type RefusalCategory = 'PolicyRefused' | 'PolicyBasisMoved' | 'RateBounded' | 'ProposalMalformed';
157
+ /**
158
+ * What both §1 routes answer. A refusal exposes `{outcome, refusal_category}`
159
+ * — never the clause, since ACP-402 (RF-4: the caller may be the agent being
160
+ * contained; the clause is in the console) — and an absent member is ABSENT,
161
+ * never `null`: one object, one encoding.
100
162
  */
101
163
  export interface Decision {
102
164
  /** The locator (T). Compare nothing against it; fetch with it. */
@@ -105,7 +167,11 @@ export interface Decision {
105
167
  /** The engine's outcome type — a fourth spelling of ALLOW / ATTEST / DENY
106
168
  * would be a fourth definition of the object every component agrees on. */
107
169
  readonly outcome?: wire.DecisionOutcome;
108
- readonly clause?: string;
170
+ /** RF-4's category, for a DENY or a refused release. Absent otherwise. */
171
+ readonly refusal_category?: RefusalCategory;
172
+ /** DR-15: the action is HELD until this instant (RFC 3339 UTC) and no
173
+ * receipt is readable yet. Absent once it releases or is refused. */
174
+ readonly held_until?: string;
109
175
  /**
110
176
  * The signed receipt, verbatim from the one route that serves receipts
111
177
  * (GET). Deliberately `unknown`: its ONLY consumer is `verifyReceipt`,
@@ -114,6 +180,87 @@ export interface Decision {
114
180
  */
115
181
  readonly receipt?: unknown;
116
182
  }
183
+ /** What a list item's `receipt` says. TWO WORDS, never the document: the
184
+ * receipt is served by exactly one route (`GET /v1/decisions/{id}`), and a
185
+ * boolean would read as "the receipt says no" — which is the sentence a DENY
186
+ * makes true and this member does not mean. */
187
+ export type ReceiptPresence = 'attached' | 'absent';
188
+ /**
189
+ * One row of {@link ZifferClient.list} (ACP-356).
190
+ *
191
+ * IT CARRIES NO CLAUSE AND NO RECEIPT DOCUMENT, and neither is an oversight.
192
+ * The list answers WHAT was decided and never WHY: the caller may be a
193
+ * compromised agent, and a sweepable list of clauses is an oracle over the
194
+ * customer's signed rules. The reason a request was refused is in the audit
195
+ * trail and, for a signed-in human, in the console — not at the API.
196
+ */
197
+ export interface DecisionListItem {
198
+ readonly decision_id: string;
199
+ readonly status: DecisionStatus;
200
+ readonly receipt: ReceiptPresence;
201
+ /** When the gateway recorded the submission. Strict RFC 3339 UTC, to the
202
+ * second — the same grammar `since` takes. */
203
+ readonly created_at: string;
204
+ /** The Policy Engine is still holding this Proposal for a quorum. NOT the
205
+ * same as `outcome === 'ATTEST'`, which only says one was asked for. */
206
+ readonly waiting: boolean;
207
+ /** Absent while pending. */
208
+ readonly outcome?: wire.DecisionOutcome;
209
+ /** When the hold ends (AT-5). Present only while `waiting`. */
210
+ readonly expires_at?: string;
211
+ /** RF-4's category — the category, never the clause. */
212
+ readonly refusal_category?: RefusalCategory;
213
+ /** DR-15: held until this instant, no receipt yet. */
214
+ readonly held_until?: string;
215
+ }
216
+ /**
217
+ * One page of {@link ZifferClient.list}, and where the next one starts.
218
+ *
219
+ * `next_cursor` is `null` at the end — never `undefined`, because both values
220
+ * are ANSWERS and a caller must tell "this is the end" from "the server did
221
+ * not say". Hand it back UNCHANGED: it is opaque, and a cursor built by a
222
+ * caller is a second implementation of the gateway's ordering.
223
+ */
224
+ export interface DecisionPage {
225
+ readonly items: readonly DecisionListItem[];
226
+ readonly next_cursor: string | null;
227
+ }
228
+ /** Per-call options for {@link ZifferClient.list}. */
229
+ export interface ListOptions extends RequestOptions {
230
+ /** Strict RFC 3339 UTC, exactly `2026-09-21T18:00:00Z`. Default: 24 hours
231
+ * ago. May not reach further back than 730 days. */
232
+ readonly since?: string;
233
+ /** 1..200. Default 50. */
234
+ readonly limit?: number;
235
+ /** `next_cursor` from a previous page, unchanged. */
236
+ readonly cursor?: string;
237
+ }
238
+ /**
239
+ * What `GET /v1/whoami` answers (ACP-391): the customer this key is bound to,
240
+ * and when the key stops being accepted.
241
+ *
242
+ * TWO MEMBERS AND NO THIRD. A wider "tenant info" was considered and dropped:
243
+ * the live policy epoch, the bundle's expiry, the attester names and the three
244
+ * windows are the POLICY AUTHOR's business, nothing in this client needs them
245
+ * to call {@link ZifferClient.propose}, and a refusal already names its own
246
+ * clause. What is genuinely unanswerable from this side of the wire is which
247
+ * customer the key in an environment variable belongs to.
248
+ *
249
+ * `key_expires_at` is the same value {@link API_KEY_EXPIRES_HEADER} carries on
250
+ * every 2xx — one date written twice by the gateway from one source, so a
251
+ * caller that ASKED does not have to reach for a header to be told.
252
+ */
253
+ export interface Identity {
254
+ /** The tenant the API key resolves to, as the key store holds it. Never a
255
+ * value this client chose: there is no parameter for it and a `tenant_id`
256
+ * in a proposal that disagrees is refused (`TenantMismatch`), not rewritten. */
257
+ readonly tenant_id: string;
258
+ /** Strict RFC 3339 UTC, rendered by the store. A string and not a parsed
259
+ * instant on purpose — the comparison that decides liveness is the store's,
260
+ * and a second place that parsed this would be a second opinion about when a
261
+ * key ends. */
262
+ readonly key_expires_at: string;
263
+ }
117
264
  /** §1 names, exported so callers and tests never retype the strings. The set
118
265
  * is OPEN — the gateway may name more; {@link ApiRefusal} carries any name
119
266
  * verbatim and this list closes nothing. */
@@ -122,6 +269,22 @@ export declare const ERROR_TENANT_MISMATCH = "TenantMismatch";
122
269
  export declare const ERROR_PROPOSAL_MALFORMED = "ProposalMalformed";
123
270
  export declare const ERROR_DECISION_UNKNOWN = "DecisionUnknown";
124
271
  export declare const ERROR_ADMISSION_UNAVAILABLE = "AdmissionUnavailable";
272
+ /** ACP-356. ONE name for every way the list's query string is not a legal
273
+ * one — which parameter, and why, is deliberately not said. */
274
+ export declare const ERROR_LIST_QUERY_MALFORMED = "ListQueryMalformed";
275
+ /** ACP-392. `POST /v1/feedback`'s four, in the gateway's own spelling
276
+ * (`services/gateway/src/gateway.rs::error_name`). They are exported for the
277
+ * same reason the five above are: a caller branching on a refusal should
278
+ * import the string rather than type it. */
279
+ export declare const ERROR_FEEDBACK_MALFORMED = "FeedbackMalformed";
280
+ export declare const ERROR_FEEDBACK_TOO_LARGE = "FeedbackTooLarge";
281
+ export declare const ERROR_FEEDBACK_RATE_LIMITED = "FeedbackRateLimited";
282
+ export declare const ERROR_FEEDBACK_UNAVAILABLE = "FeedbackUnavailable";
283
+ /** ACP-422. This key's quota on `propose`, `decision`, `list` and `whoami` is
284
+ * spent: 429 with `Retry-After`, which the retry policy already obeys
285
+ * (ACP-355). A different name from {@link ERROR_FEEDBACK_RATE_LIMITED}
286
+ * because it is a different allowance. */
287
+ export declare const ERROR_RATE_LIMITED = "RateLimited";
125
288
  /**
126
289
  * The gateway answered, and the answer was a named refusal. `error` is the
127
290
  * gateway's name, verbatim — the machine-readable half, as `Refusal.clause`
@@ -145,10 +308,18 @@ export declare class ResponseMalformed extends Error {
145
308
  constructor(message: string);
146
309
  }
147
310
  /** The deadline passed with the decision still `pending`. Not a refusal and
148
- * not an answer — the decision may still decide; the id remains fetchable. */
311
+ * not an answer — the decision may still decide; the id remains fetchable.
312
+ *
313
+ * `options.cause` is how the reason survives the rename. A poll that ran out
314
+ * of retry budget produced a {@link DeadlineExceeded} naming the last failure
315
+ * and its status; `wait` reports the event under the name its caller catches
316
+ * and hands the original through as `cause`, so nothing the caller could have
317
+ * learned from the poll is thrown away to keep one name at the surface. A
318
+ * rename that dropped the reason would make "no answer yet" indistinguishable
319
+ * from "the gateway was shedding for thirty seconds". */
149
320
  export declare class WaitTimeout extends Error {
150
321
  readonly decisionId: string;
151
- constructor(decisionId: string, timeoutMs: number);
322
+ constructor(decisionId: string, timeoutMs: number, options?: ErrorOptions);
152
323
  }
153
324
  /** Options for {@link ZifferClient.wait}. */
154
325
  export interface WaitOptions {
@@ -157,6 +328,55 @@ export interface WaitOptions {
157
328
  /** Delay between polls. Default 500. */
158
329
  readonly intervalMs?: number;
159
330
  }
331
+ /** What this client needs of `fetch`, and no more. Narrow on purpose: it is
332
+ * the seam the retry corpus is replayed through, and a seam typed as the whole
333
+ * of `fetch` would invite a test to script something this client never sends. */
334
+ export interface FetchInit {
335
+ readonly method: string;
336
+ readonly headers: Readonly<Record<string, string>>;
337
+ readonly body?: string;
338
+ readonly signal: AbortSignal;
339
+ }
340
+ /** The injectable `fetch`. The default is the global one. */
341
+ export type FetchLike = (url: string, init: FetchInit) => Promise<Response>;
342
+ /** Per-call options for {@link ZifferClient.propose} and
343
+ * {@link ZifferClient.decision}. */
344
+ export interface RequestOptions {
345
+ /**
346
+ * R5. How long from NOW this call may keep retrying, in milliseconds.
347
+ *
348
+ * Before every sleep, directed or computed, the client asks whether the
349
+ * sleep would end after this instant; if it would, it does not sleep and
350
+ * throws {@link DeadlineExceeded} carrying the last failure. Absent means
351
+ * no such check at all — the attempt cap and the token bucket are then the
352
+ * only bounds, and a server that asks for an hour gets an hour.
353
+ */
354
+ readonly deadlineMs?: number;
355
+ }
356
+ /**
357
+ * Construction-time options. Every one of them has a real default; the clock,
358
+ * the sleep, the draw and `fetch` exist as options so the shared retry corpus
359
+ * can be replayed deterministically, and for no other reason — a client built
360
+ * with none of them is the client a customer runs.
361
+ */
362
+ export interface ZifferClientOptions {
363
+ /** R8. The abort timeout on ONE round trip. Default
364
+ * {@link REQUEST_TIMEOUT_MS}. */
365
+ readonly timeoutMs?: number;
366
+ /** Default: the global `fetch`. */
367
+ readonly fetch?: FetchLike;
368
+ /** Default: `Date.now`. */
369
+ readonly now?: () => number;
370
+ /** Default: `setTimeout`. */
371
+ readonly sleep?: (ms: number) => Promise<void>;
372
+ /** R2's `u`, in [0,1). Default: `Math.random`. */
373
+ readonly random?: () => number;
374
+ /** R6's bucket at construction. Default: full
375
+ * ({@link RETRY_BUCKET_CAPACITY}). A test seam — a deployment that wanted a
376
+ * smaller retry budget would be asking for a different rule, not a
377
+ * different starting level. */
378
+ readonly retryBucketInitial?: number;
379
+ }
160
380
  /**
161
381
  * The client. One instance per (gateway, key); the KEY determines the tenant
162
382
  * server-side (§1), so there is nothing tenant-shaped to configure here —
@@ -165,33 +385,121 @@ export interface WaitOptions {
165
385
  export declare class ZifferClient {
166
386
  private readonly baseUrl;
167
387
  private readonly apiKey;
168
- constructor(baseUrl: string, apiKey: string);
388
+ private readonly timeoutMs;
389
+ private readonly fetchImpl;
390
+ private readonly now;
391
+ private readonly sleep;
392
+ private readonly random;
393
+ /** R6/R9: per CLIENT INSTANCE, as the rules say. Two clients do not share a
394
+ * retry budget, and one client's `propose` and `wait` do. */
395
+ private readonly retry;
396
+ constructor(baseUrl: string, apiKey: string, options?: ZifferClientOptions);
397
+ /**
398
+ * R9. The two retry counters and the bucket gauge, as a plain object taken
399
+ * at this instant. Cumulative over the client's life — a caller that wants
400
+ * a delta over one call reads it before and after.
401
+ */
402
+ get retryCounters(): RetryCounters;
169
403
  /**
170
404
  * POST /v1/proposals. The proposal is serialised as given — the caller's
171
405
  * values, no edits — and the answer never carries a receipt (§1): fetch it
172
406
  * with {@link decision} once decided.
407
+ *
408
+ * `opts.deadlineMs` bounds the RETRYING, not the round trip (R5).
173
409
  */
174
- propose(proposal: WireProposal): Promise<Decision>;
410
+ propose(proposal: WireProposal, opts?: RequestOptions): Promise<Decision>;
411
+ /**
412
+ * POST /v1/feedback — tell ZIFFER what our documentation did not answer
413
+ * (ACP-392).
414
+ *
415
+ * THE TEXT LEAVES THE MACHINE. It is stored under the tenant this API key
416
+ * carries and read by an operator; nothing is filtered on the way, so
417
+ * anything put in `question` or `context` is anything an operator will
418
+ * read. The gateway refuses a member this shape does not define, which is
419
+ * what stops the object growing a field that carries more than a sentence.
420
+ *
421
+ * # ONE round trip and NO retry, unlike every other call on this client
422
+ *
423
+ * `request` retries a 429 and honours its `Retry-After`, which is right for
424
+ * work: a proposal that was refused for backpressure still has to happen.
425
+ * This is not work. A rate-limited feedback message is a message that will
426
+ * not be stored, and riding out a 60-second bucket would block the coding
427
+ * agent that called it for a minute to deliver a sentence. So the failure
428
+ * is raised as it arrives — [`ApiRefusal`] with the gateway's own name —
429
+ * and the caller decides.
430
+ */
431
+ feedback(message: Feedback): Promise<FeedbackStored>;
175
432
  /**
176
433
  * GET /v1/decisions/{id}. `receipt` is present iff a signed receipt
177
434
  * exists; hand it to `verifyReceipt` with your OWN copy of the proposal
178
435
  * bytes — the id proves nothing (T), the recomputed hash is the binding.
179
436
  */
180
- decision(id: string): Promise<Decision>;
437
+ decision(id: string, opts?: RequestOptions): Promise<Decision>;
438
+ /** {@link decision}, with the deadline already resolved to an instant —
439
+ * which is what {@link wait} has and a caller does not. */
440
+ private fetchDecision;
441
+ /**
442
+ * GET /v1/whoami — which customer this key is bound to, and when it ends.
443
+ *
444
+ * The one call that asks about the CREDENTIAL rather than about a decision.
445
+ * It sends no body and takes no argument: there is nothing to name, because
446
+ * the key is the question. A dead key — expired, revoked or never minted —
447
+ * is one `ApiRefusal` (`ApiKeyUnknown`, 401) and the three cannot be told
448
+ * apart, which is deliberate at the gateway and is not this client's to
449
+ * undo.
450
+ */
451
+ whoami(opts?: RequestOptions): Promise<Identity>;
452
+ /**
453
+ * GET /v1/decisions — what ZIFFER holds and decided for THIS key.
454
+ *
455
+ * Every request waiting for approval (with when its hold ends) and every
456
+ * decision in the window, newest first. The tenant is the API key's and
457
+ * cannot be named any other way: there is no parameter for it and the
458
+ * gateway refuses one by name.
459
+ *
460
+ * Every bad parameter is one refusal, `ListQueryMalformed` (400). The API
461
+ * does not say which one, for the same reason an item carries no clause.
462
+ */
463
+ list(opts?: ListOptions): Promise<DecisionPage>;
181
464
  /**
182
465
  * Poll {@link decision} until `decided` or the deadline. A `WaitTimeout`
183
466
  * is "no answer yet", never a verdict; every named refusal (404 included)
184
467
  * propagates immediately — retrying `DecisionUnknown` would be the client
185
468
  * deciding the server was wrong.
469
+ *
470
+ * A poll that runs out of budget surfaces as `WaitTimeout` too, and NOT as
471
+ * the `DeadlineExceeded` the request path raised. Both mean "the wait's own
472
+ * deadline arrived with the decision still pending"; which of the two a
473
+ * caller saw depended on whether the last poll happened to be mid-retry,
474
+ * which is a detail of the gateway's load and not of this API. The
475
+ * `DeadlineExceeded` rides along as `cause`, so the last failure's name and
476
+ * status are still there for whoever wants them.
186
477
  */
187
478
  wait(id: string, opts?: WaitOptions): Promise<Decision>;
479
+ /** R5: the caller's duration becomes an instant on this client's clock,
480
+ * once, at the start of the call. `undefined` in, `undefined` out — no
481
+ * deadline means no check, not a check against infinity. */
482
+ private deadlineAt;
188
483
  /**
189
- * One request, one parse, one narrowing. 2xx returns the parsed body for
190
- * the caller's guard; anything else must be §1's `{"error": name}` and
191
- * throws {@link ApiRefusal} with the name verbatim. A non-JSON or unnamed
192
- * error body throws {@link ResponseMalformed} — an intermediary's HTML 502
193
- * is not the gateway's answer and is never dressed up as one.
484
+ * THE request path: one loop, R1..R6, for every call this client makes.
485
+ *
486
+ * `body` arrives already serialised and is resent unchanged (R7). The loop
487
+ * owns nothing about the rules themselves — `retry.ts` decides, this
488
+ * sleeps, sends again, or throws what the last attempt produced.
194
489
  */
195
490
  private request;
491
+ /**
492
+ * ONE round trip: one request, one parse, one narrowing. 2xx returns the
493
+ * parsed body for the caller's guard; anything else must be §1's
494
+ * `{"error": name}` and yields {@link ApiRefusal} with the name verbatim. A
495
+ * non-JSON or unnamed error body yields {@link ResponseMalformed} — an
496
+ * intermediary's HTML 502 is not the gateway's answer and is never dressed
497
+ * up as one, though it IS retried, because 502 is retryable whoever wrote it.
498
+ *
499
+ * Nothing is thrown from here: the failure is RETURNED, because whether it
500
+ * becomes the caller's error is the retry loop's question and not this
501
+ * method's.
502
+ */
503
+ private roundTrip;
196
504
  }
197
505
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}