@rig-ts/client 0.1.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/dist/index.d.mts +810 -0
- package/dist/index.mjs +994 -0
- package/package.json +33 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
//#region src/credential.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What authorizes each request.
|
|
4
|
+
*
|
|
5
|
+
* `apply` is handed the headers a request is about to go out with, rather than
|
|
6
|
+
* a value to return, because a credential that has to refresh first needs
|
|
7
|
+
* somewhere to await — and because a future one may want to set more than a
|
|
8
|
+
* single header.
|
|
9
|
+
*/
|
|
10
|
+
type Credential = {
|
|
11
|
+
apply(headers: Headers, signal?: AbortSignal): void | Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A credential that can do something about a 401.
|
|
15
|
+
*
|
|
16
|
+
* The runtime asks once, and only ever once per call: a blind retry on 401 is a
|
|
17
|
+
* way to lock an account out with a wrong password. Answering `false` leaves the
|
|
18
|
+
* 401 as the answer, which is what a credential with nothing left to try should
|
|
19
|
+
* say — throwing would replace the server's refusal with the client's opinion
|
|
20
|
+
* of it.
|
|
21
|
+
*/
|
|
22
|
+
type Reauthorizer = Credential & {
|
|
23
|
+
reauthorize(signal?: AbortSignal): Promise<boolean>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A credential that needs the client it authorizes in order to work.
|
|
27
|
+
*
|
|
28
|
+
* `Session` is the one: refreshing is itself a call, and the only moment the
|
|
29
|
+
* runtime can be known is when the credential is installed. Kept structural so
|
|
30
|
+
* the runtime does not have to import the session, which would be a cycle.
|
|
31
|
+
*/
|
|
32
|
+
type Bindable = {
|
|
33
|
+
bind(runtime: unknown): void;
|
|
34
|
+
};
|
|
35
|
+
/** Reports whether a credential can answer a 401 with something new. */
|
|
36
|
+
declare function isReauthorizer(c: Credential | undefined): c is Reauthorizer;
|
|
37
|
+
/**
|
|
38
|
+
* A bearer token that never changes.
|
|
39
|
+
*
|
|
40
|
+
* Right for a token from somewhere else — a test fixture, an environment
|
|
41
|
+
* variable, a token minted by the surrounding application. A token that expires
|
|
42
|
+
* wants a `Session`, which refreshes ahead of the expiry rather than discovering
|
|
43
|
+
* it through a failed request.
|
|
44
|
+
*/
|
|
45
|
+
declare function staticToken(token: string): Credential;
|
|
46
|
+
/**
|
|
47
|
+
* An API key, presented the same way a token is.
|
|
48
|
+
*
|
|
49
|
+
* The server tells the two apart by what the value is, not by how it arrived, so
|
|
50
|
+
* this is a separate name only because a caller holding one should not have to
|
|
51
|
+
* know that. It is {@link staticToken} and not a copy of it: two bodies that
|
|
52
|
+
* have to stay identical is how one of them eventually does not, and there is
|
|
53
|
+
* nothing here that could correctly differ — the day a key travels somewhere a
|
|
54
|
+
* token does not, this stops being an alias and starts being a function.
|
|
55
|
+
*/
|
|
56
|
+
declare const apiKey: typeof staticToken;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/retry.d.ts
|
|
59
|
+
/**
|
|
60
|
+
* How a call the server could not answer is sent again, and how long the client
|
|
61
|
+
* waits in between.
|
|
62
|
+
*
|
|
63
|
+
* It applies to the calls where sending the same request twice cannot mean two
|
|
64
|
+
* different things. A read and a delete are that by nature. A create and an
|
|
65
|
+
* update are made so: each one goes out named by an idempotency key, and a rig
|
|
66
|
+
* server that sees the same key twice answers the second one with what it
|
|
67
|
+
* answered the first rather than doing the work again.
|
|
68
|
+
*
|
|
69
|
+
* An upload is not, and is the one write this never repeats — a rig server does
|
|
70
|
+
* not record an upload route against a key, so a second send would store the
|
|
71
|
+
* file twice.
|
|
72
|
+
*/
|
|
73
|
+
type Retry = {
|
|
74
|
+
/**
|
|
75
|
+
* How many times a call is sent, the first try included. Absent takes
|
|
76
|
+
* {@link DEFAULT_ATTEMPTS}; one sends it once and reports whatever came
|
|
77
|
+
* back.
|
|
78
|
+
*/
|
|
79
|
+
attempts?: number;
|
|
80
|
+
/**
|
|
81
|
+
* The first backoff window in milliseconds, doubling per attempt after it.
|
|
82
|
+
* Absent takes {@link DEFAULT_RETRY_BASE_MS}.
|
|
83
|
+
*
|
|
84
|
+
* The first retry does not use it: it goes out immediately, because the
|
|
85
|
+
* commonest retryable failure there is — a pooled connection the server had
|
|
86
|
+
* already closed — is fixed by opening another one, and waiting a second
|
|
87
|
+
* first would only be slower.
|
|
88
|
+
*/
|
|
89
|
+
baseMs?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Bounds one backoff window, however many attempts have already failed.
|
|
92
|
+
* Absent takes {@link DEFAULT_RETRY_CAP_MS}.
|
|
93
|
+
*
|
|
94
|
+
* It does not bind at the default attempt count, which is why it is a
|
|
95
|
+
* setting rather than a number anybody has to choose: a caller who raises
|
|
96
|
+
* `attempts` to eight is asking for eight tries, not for a two-minute sleep
|
|
97
|
+
* in the middle of them.
|
|
98
|
+
*/
|
|
99
|
+
capMs?: number;
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/rate-limit.d.ts
|
|
103
|
+
/**
|
|
104
|
+
* What a response said about the caller's budget.
|
|
105
|
+
*
|
|
106
|
+
* It is the tightest limit the caller is under rather than a list of every one
|
|
107
|
+
* that applied: the server evaluates each and reports the one closest to
|
|
108
|
+
* refusing, because that is the only number a client can act on.
|
|
109
|
+
*/
|
|
110
|
+
type RateLimitStatus = {
|
|
111
|
+
/**
|
|
112
|
+
* The operation that was called, as the document names it — `listTodos`.
|
|
113
|
+
* A gauge keyed on it says which call is spending the budget.
|
|
114
|
+
*/
|
|
115
|
+
readonly op: string; /** How many calls the window allows. */
|
|
116
|
+
readonly limit: number; /** How many are left. Zero on the response that was refused. */
|
|
117
|
+
readonly remaining: number;
|
|
118
|
+
/**
|
|
119
|
+
* How long until the window frees, in milliseconds.
|
|
120
|
+
*
|
|
121
|
+
* Only stated on a refusal — an allowed response says how much is left but
|
|
122
|
+
* not when it comes back. Zero means the server did not say.
|
|
123
|
+
*/
|
|
124
|
+
readonly resetAfterMs: number; /** True when this response was the 429 rather than a call that went through. */
|
|
125
|
+
readonly refused: boolean;
|
|
126
|
+
};
|
|
127
|
+
/** How much of the budget has been spent. */
|
|
128
|
+
declare const used: (s: RateLimitStatus) => number;
|
|
129
|
+
/**
|
|
130
|
+
* How much of the budget is spent, from 0 to 1.
|
|
131
|
+
*
|
|
132
|
+
* It is the number worth alerting on, and it is here rather than left to the
|
|
133
|
+
* caller because the obvious arithmetic divides by zero: a response from a
|
|
134
|
+
* server with no limit configured carries no headers and leaves `limit` at 0.
|
|
135
|
+
*/
|
|
136
|
+
declare const fraction: (s: RateLimitStatus) => number;
|
|
137
|
+
/**
|
|
138
|
+
* Reads the status out of a response, or `undefined` when the server said
|
|
139
|
+
* nothing at all.
|
|
140
|
+
*
|
|
141
|
+
* A server with no `throttle:` block sends none of these headers, and a caller
|
|
142
|
+
* that treated an absent header as zero would read "no budget left" out of a
|
|
143
|
+
* server that has no limits.
|
|
144
|
+
*/
|
|
145
|
+
declare const rateLimitOf: (op: string, res: Response) => RateLimitStatus | undefined;
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/runtime.d.ts
|
|
148
|
+
/**
|
|
149
|
+
* The authentication profile, as the document describes it.
|
|
150
|
+
*
|
|
151
|
+
* The lifetimes here are the ones the server enforces rather than numbers a
|
|
152
|
+
* client author guessed, which is why a `Session` can refresh ahead of an expiry
|
|
153
|
+
* instead of waiting for a 401 to tell it.
|
|
154
|
+
*/
|
|
155
|
+
type AuthProfile = {
|
|
156
|
+
/** Where the endpoints sit, for example `/auth`. */basePath: string; /** The lifetime of the token that travels on every request, in milliseconds. */
|
|
157
|
+
accessTtlMs: number; /** How long an ordinary session lasts, in milliseconds. */
|
|
158
|
+
refreshTtlMs: number;
|
|
159
|
+
/**
|
|
160
|
+
* How long a refresh token stays usable after it has been exchanged, in
|
|
161
|
+
* milliseconds. It is also what this package refreshes ahead by: the server
|
|
162
|
+
* having decided how much slack a swap deserves, a client has no business
|
|
163
|
+
* picking a different number.
|
|
164
|
+
*/
|
|
165
|
+
rotationLeewayMs: number;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* What the generated client knows about the server and this package does not.
|
|
169
|
+
* Generated code fills it in; a caller never sees it.
|
|
170
|
+
*/
|
|
171
|
+
type ApiDescriptor = {
|
|
172
|
+
/** The prefix every route sits under, for example `/api/v1`. */basePath: string; /** The authentication profile, or absent for a project with none. */
|
|
173
|
+
auth?: AuthProfile;
|
|
174
|
+
/**
|
|
175
|
+
* The date the API surface this client was generated from last changed. It
|
|
176
|
+
* is sent on every request, which is how a server's logs can answer how old
|
|
177
|
+
* the oldest caller still calling is.
|
|
178
|
+
*/
|
|
179
|
+
revision?: string; /** Where `revision` is sent — the header the generated server reads. */
|
|
180
|
+
revisionHeader?: string;
|
|
181
|
+
};
|
|
182
|
+
/** What a caller supplies. Every field but `baseUrl` has a default. */
|
|
183
|
+
type Config = {
|
|
184
|
+
/**
|
|
185
|
+
* The origin the API is served from, for example `https://api.example.com`.
|
|
186
|
+
* A path on it is kept and the API's own base path is appended, so a server
|
|
187
|
+
* behind `/gateway` is a base URL and not a special case.
|
|
188
|
+
*
|
|
189
|
+
* A relative value works in a browser and is the ordinary same-origin case:
|
|
190
|
+
* `""` resolves against the page.
|
|
191
|
+
*/
|
|
192
|
+
baseUrl: string;
|
|
193
|
+
/**
|
|
194
|
+
* What authorizes each request. Absent sends no `Authorization` header,
|
|
195
|
+
* which is what a public API or a cookie-authenticated deployment wants.
|
|
196
|
+
*/
|
|
197
|
+
credential?: Credential;
|
|
198
|
+
/**
|
|
199
|
+
* Sent on every request. Where a tenant header, a trace header, or anything
|
|
200
|
+
* else a deployment adds belongs.
|
|
201
|
+
*/
|
|
202
|
+
headers?: HeadersInit;
|
|
203
|
+
/**
|
|
204
|
+
* How long a whole call may take, in milliseconds, retries and backoff
|
|
205
|
+
* included. Absent leaves it unbounded and lets the caller's own
|
|
206
|
+
* `AbortSignal` decide, which is what a browser application usually wants.
|
|
207
|
+
*/
|
|
208
|
+
timeoutMs?: number;
|
|
209
|
+
/**
|
|
210
|
+
* Supplies the value of the request-ID header, so a client-side log line and
|
|
211
|
+
* a server-side one can be joined. Absent sends none, and the server
|
|
212
|
+
* generates its own.
|
|
213
|
+
*/
|
|
214
|
+
requestId?: () => string; /** Defaults to `X-Request-Id`, which is what the generated server reads. */
|
|
215
|
+
requestIdHeader?: string;
|
|
216
|
+
/**
|
|
217
|
+
* Overrides the API revision this client says it was built against.
|
|
218
|
+
*
|
|
219
|
+
* Almost nobody should set it. The generated client carries the revision
|
|
220
|
+
* from the document it was generated from, which is the honest answer and
|
|
221
|
+
* the one the server's logs are for.
|
|
222
|
+
*/
|
|
223
|
+
revision?: string; /** Overrides where the revision is sent. */
|
|
224
|
+
revisionHeader?: string;
|
|
225
|
+
/**
|
|
226
|
+
* The transport. Swapping it is how a test intercepts requests and how a
|
|
227
|
+
* server runtime supplies its own. Defaults to the ambient `fetch`.
|
|
228
|
+
*/
|
|
229
|
+
fetch?: typeof fetch; /** The clock, for a test that has to cross a token expiry without waiting. */
|
|
230
|
+
now?: () => number; /** How a call the server could not answer is sent again. See {@link Retry}. */
|
|
231
|
+
retry?: Retry;
|
|
232
|
+
/**
|
|
233
|
+
* Called with what each response said about the caller's budget, when the
|
|
234
|
+
* server said anything at all.
|
|
235
|
+
*
|
|
236
|
+
* It is the other half of rate limiting, and the half a client can act on. A
|
|
237
|
+
* 429 is already handled without it — the client reads `Retry-After` and
|
|
238
|
+
* backs off — but by then the call has been refused. These numbers arrive on
|
|
239
|
+
* every response, so a caller that watches them can slow down, shed work, or
|
|
240
|
+
* raise an alarm while its calls are still succeeding.
|
|
241
|
+
*
|
|
242
|
+
* Deliberately a callback and not automatic pacing. A client library that
|
|
243
|
+
* silently waited because `remaining` was low would turn a batch job's
|
|
244
|
+
* throughput into a mystery, and it cannot know whether this caller would
|
|
245
|
+
* rather go slower or fail sooner. The numbers are handed over; the policy
|
|
246
|
+
* is the application's.
|
|
247
|
+
*
|
|
248
|
+
* Called once per attempt, before the call returns. Keep it quick, do not
|
|
249
|
+
* throw from it, and do not call back into the client.
|
|
250
|
+
*/
|
|
251
|
+
onRateLimit?: (status: RateLimitStatus) => void;
|
|
252
|
+
};
|
|
253
|
+
/** Where the generated server looks for a caller's own request identifier. */
|
|
254
|
+
declare const DEFAULT_REQUEST_ID_HEADER = "X-Request-Id";
|
|
255
|
+
/**
|
|
256
|
+
* Carries the API revision, and is what `rig.yaml`'s `api.revision_header`
|
|
257
|
+
* defaults to. A generated client passes its project's own, so this is the
|
|
258
|
+
* fallback for a client somebody built by hand.
|
|
259
|
+
*/
|
|
260
|
+
declare const DEFAULT_REVISION_HEADER = "API-Revision";
|
|
261
|
+
/**
|
|
262
|
+
* One client's configuration and the state it accumulates.
|
|
263
|
+
*
|
|
264
|
+
* The remembered QUERY decision is the reason this is state rather than
|
|
265
|
+
* configuration: a client that learned an intermediary refuses QUERY should not
|
|
266
|
+
* have to learn it again on the next search.
|
|
267
|
+
*/
|
|
268
|
+
declare class Runtime {
|
|
269
|
+
readonly api: ApiDescriptor;
|
|
270
|
+
readonly fetch: typeof fetch;
|
|
271
|
+
readonly now: () => number;
|
|
272
|
+
readonly retry: Retry;
|
|
273
|
+
readonly timeoutMs: number | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Where a caller's own request identifier is sent. Readable rather than
|
|
276
|
+
* private because the transport writes to it too: a call that names its own
|
|
277
|
+
* identifier has to land in the header this client was configured with, or a
|
|
278
|
+
* deployment that moved the header gets two of them disagreeing.
|
|
279
|
+
*/
|
|
280
|
+
readonly requestIdHeader: string;
|
|
281
|
+
/** The randomness in a backoff, held here so a test can make one deterministic. */
|
|
282
|
+
jitter: (n: number) => number;
|
|
283
|
+
private readonly baseUrl;
|
|
284
|
+
private readonly headers;
|
|
285
|
+
private readonly requestIdOf;
|
|
286
|
+
private readonly revision;
|
|
287
|
+
private readonly revisionHeader;
|
|
288
|
+
private credential;
|
|
289
|
+
private readonly onRateLimit;
|
|
290
|
+
/** Records that QUERY was refused once and is not worth trying again. */
|
|
291
|
+
private searchByPost;
|
|
292
|
+
constructor(config: Config, api: ApiDescriptor);
|
|
293
|
+
/**
|
|
294
|
+
* Hands the caller's budget to the configured callback, if the response said
|
|
295
|
+
* anything about it and anybody is listening.
|
|
296
|
+
*
|
|
297
|
+
* A throwing callback must not fail the call it was observing: it is
|
|
298
|
+
* telemetry about a request that otherwise succeeded, and turning a bad
|
|
299
|
+
* gauge into a failed write would be the wrong trade in both directions.
|
|
300
|
+
*/
|
|
301
|
+
observeRateLimit(op: string, res: Response): void;
|
|
302
|
+
/** The origin requests go to. */
|
|
303
|
+
get origin(): string;
|
|
304
|
+
/** The credential in force, or `undefined`. */
|
|
305
|
+
getCredential(): Credential | undefined;
|
|
306
|
+
/**
|
|
307
|
+
* Installs a credential, replacing whatever was there. It is what signing in
|
|
308
|
+
* does, and what a caller does by hand when they already hold a token from
|
|
309
|
+
* somewhere else.
|
|
310
|
+
*/
|
|
311
|
+
use(credential: Credential | undefined): void;
|
|
312
|
+
/** Whether this client has learned that QUERY does not get through. */
|
|
313
|
+
searchesByPost(): boolean;
|
|
314
|
+
/** Remembers a refused QUERY, so it is tried once per client and not once per call. */
|
|
315
|
+
rememberSearchByPost(): void;
|
|
316
|
+
/**
|
|
317
|
+
* The absolute URL for an operation.
|
|
318
|
+
*
|
|
319
|
+
* `extra` is what a call option added, and it wins where the two name the
|
|
320
|
+
* same parameter: the operation's own query came from the typed arguments of
|
|
321
|
+
* a generated method, and an option is the caller saying something about
|
|
322
|
+
* this one call afterwards.
|
|
323
|
+
*/
|
|
324
|
+
url(path: string, query: URLSearchParams | undefined, extra: URLSearchParams | undefined, root: boolean): string;
|
|
325
|
+
/** The headers every request from this client carries, before the call's own. */
|
|
326
|
+
baseHeaders(): Headers;
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/session.d.ts
|
|
330
|
+
/**
|
|
331
|
+
* The pair a sign-in returns, in the shape the wire uses.
|
|
332
|
+
*
|
|
333
|
+
* The names are the server's, verbatim — a client that renamed them would be a
|
|
334
|
+
* second description of the same exchange, and this is a value programs store
|
|
335
|
+
* between runs.
|
|
336
|
+
*/
|
|
337
|
+
type TokenPair = {
|
|
338
|
+
accessToken?: string;
|
|
339
|
+
refreshToken?: string; /** RFC 3339, or absent from a server that did not say. */
|
|
340
|
+
expiresAt?: string;
|
|
341
|
+
/**
|
|
342
|
+
* When the session itself ends. A client needs both: one says when to
|
|
343
|
+
* refresh, the other says when to stop trying.
|
|
344
|
+
*/
|
|
345
|
+
refreshExpiresAt?: string;
|
|
346
|
+
sessionId?: string;
|
|
347
|
+
};
|
|
348
|
+
/** Thrown when a session has nothing left to present. */
|
|
349
|
+
declare class NoSessionError extends Error {
|
|
350
|
+
constructor();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A credential that keeps itself fresh.
|
|
354
|
+
*
|
|
355
|
+
* It holds the pair a sign-in returned and exchanges the refresh token before
|
|
356
|
+
* the access token expires, using the leeway in the document's auth profile — so
|
|
357
|
+
* a page open all day makes a handful of refresh calls at moments of its own
|
|
358
|
+
* choosing, rather than discovering the expiry through a failed request in the
|
|
359
|
+
* middle of something.
|
|
360
|
+
*
|
|
361
|
+
* Several calls arriving at an expiry at once take turns: the ones behind find
|
|
362
|
+
* the work already done instead of each spending a rotation. That matters more
|
|
363
|
+
* than it sounds — the server bounds how often a session may rotate, and a
|
|
364
|
+
* client that raced with itself would spend that budget on nothing.
|
|
365
|
+
*/
|
|
366
|
+
declare class Session implements Reauthorizer, Credential {
|
|
367
|
+
private tokens;
|
|
368
|
+
private runtime;
|
|
369
|
+
/**
|
|
370
|
+
* Held for the length of an exchange, so a hundred callers discovering the
|
|
371
|
+
* same expiry produce one refresh.
|
|
372
|
+
*/
|
|
373
|
+
private inFlight;
|
|
374
|
+
/** Called when a new pair is issued — a place to persist it. */
|
|
375
|
+
onTokens: ((tokens: TokenPair) => void) | undefined;
|
|
376
|
+
constructor(tokens?: TokenPair);
|
|
377
|
+
/** The pair currently held, for a program that stores it between runs. */
|
|
378
|
+
getTokens(): TokenPair;
|
|
379
|
+
/** Identifies the session, for showing it in a list and revoking it. */
|
|
380
|
+
get sessionId(): string;
|
|
381
|
+
/**
|
|
382
|
+
* Swaps in a newly issued pair, which is what a refresh, a tenant switch and
|
|
383
|
+
* a password change all produce.
|
|
384
|
+
*/
|
|
385
|
+
replace(pair: TokenPair): void;
|
|
386
|
+
/** Receives the client this session refreshes through. */
|
|
387
|
+
bind(runtime: unknown): void;
|
|
388
|
+
/**
|
|
389
|
+
* Adds the token, refreshing first if this request might outlive it.
|
|
390
|
+
*/
|
|
391
|
+
apply(headers: Headers, signal?: AbortSignal): Promise<void>;
|
|
392
|
+
/**
|
|
393
|
+
* A 401 despite a token that looked current is a revoked or invalidated one,
|
|
394
|
+
* and the refresh token is the only thing left to try.
|
|
395
|
+
*
|
|
396
|
+
* It exchanges whatever the expiry says, because the expiry is exactly what
|
|
397
|
+
* has just been proved wrong.
|
|
398
|
+
*/
|
|
399
|
+
reauthorize(signal?: AbortSignal): Promise<boolean>;
|
|
400
|
+
/** Whether the access token is expired or close enough to it. */
|
|
401
|
+
private stale;
|
|
402
|
+
/**
|
|
403
|
+
* Exchanges the refresh token for a new pair, once however many callers ask.
|
|
404
|
+
*
|
|
405
|
+
* Answers false rather than throwing when there is nothing to exchange: a
|
|
406
|
+
* session that was never signed in leaves the 401 that prompted this as the
|
|
407
|
+
* answer, which is what the caller needs to see.
|
|
408
|
+
*/
|
|
409
|
+
private exchange;
|
|
410
|
+
}
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/op.d.ts
|
|
413
|
+
/**
|
|
414
|
+
* One call, as the generated method describes it.
|
|
415
|
+
*
|
|
416
|
+
* The path is relative to the API's base path and already has its parameters
|
|
417
|
+
* substituted: escaping an identifier into a route is the generated method's
|
|
418
|
+
* job, because only it knows which argument goes where.
|
|
419
|
+
*/
|
|
420
|
+
type Op = {
|
|
421
|
+
/**
|
|
422
|
+
* The operation this call is, as the document named it — `listTodos`,
|
|
423
|
+
* `createTodo`.
|
|
424
|
+
*
|
|
425
|
+
* A field rather than something derived from method and path, because the
|
|
426
|
+
* path here already has its identifiers substituted: a name built from it
|
|
427
|
+
* would be a new name for every row anybody ever fetched.
|
|
428
|
+
*/
|
|
429
|
+
name: string;
|
|
430
|
+
method: string;
|
|
431
|
+
path: string;
|
|
432
|
+
query?: URLSearchParams;
|
|
433
|
+
/**
|
|
434
|
+
* Encoded as JSON when it is present. `undefined` means no body at all,
|
|
435
|
+
* which is not the same as an empty object.
|
|
436
|
+
*/
|
|
437
|
+
body?: unknown;
|
|
438
|
+
/**
|
|
439
|
+
* A form body, sent instead of `body`.
|
|
440
|
+
*
|
|
441
|
+
* The two are exclusive rather than ordered: an op carrying both is a
|
|
442
|
+
* generated method with a bug in it, and it is reported rather than resolved
|
|
443
|
+
* by a precedence somebody would have to look up.
|
|
444
|
+
*/
|
|
445
|
+
form?: FormData;
|
|
446
|
+
/**
|
|
447
|
+
* The media type this call will take back. Absent means `application/json`,
|
|
448
|
+
* which is every endpoint but a download: a download answers with whatever
|
|
449
|
+
* the file turned out to be, and a client that insisted on JSON would be
|
|
450
|
+
* asking for the one thing it is not.
|
|
451
|
+
*/
|
|
452
|
+
accept?: string;
|
|
453
|
+
/**
|
|
454
|
+
* The path to POST to when `method` is QUERY and something between here and
|
|
455
|
+
* the server refuses it — the `_search` alias the router mounts beside the
|
|
456
|
+
* QUERY route. Absent means there is none, and a refusal is reported rather
|
|
457
|
+
* than worked around.
|
|
458
|
+
*/
|
|
459
|
+
fallback?: string;
|
|
460
|
+
/**
|
|
461
|
+
* Says the path is relative to the server rather than to the API's base
|
|
462
|
+
* path. The authentication endpoints are: `/auth/login` is mounted beside
|
|
463
|
+
* `/api/v1` and not inside it, because a sign-in is not a version of the
|
|
464
|
+
* application's API.
|
|
465
|
+
*/
|
|
466
|
+
root?: boolean;
|
|
467
|
+
};
|
|
468
|
+
/**
|
|
469
|
+
* The HTTP method a generated search uses.
|
|
470
|
+
*
|
|
471
|
+
* A method rather than a POST because a search is a read: it has a body, and it
|
|
472
|
+
* is safe and idempotent, and pretending otherwise is what made every API in the
|
|
473
|
+
* world invent `/_search`.
|
|
474
|
+
*/
|
|
475
|
+
declare const METHOD_QUERY = "QUERY";
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/errors.d.ts
|
|
478
|
+
/**
|
|
479
|
+
* A request the server refused.
|
|
480
|
+
*
|
|
481
|
+
* The code, not the status, is what to switch on. Three unrelated failures
|
|
482
|
+
* share a 400 and none of them share a code, which is the whole reason the
|
|
483
|
+
* generated server sends one.
|
|
484
|
+
*
|
|
485
|
+
* `TFields` is the shape of the body that caused the refusal. A caller does not
|
|
486
|
+
* name it: the generated client declares a guard per call that does, so
|
|
487
|
+
* `isTodoCreateError(err)` reads back what `todos.create` refused. Naming it by
|
|
488
|
+
* hand is what `fieldsAs` asks for, and it is why the per-call guard exists —
|
|
489
|
+
* the wrong shape decodes perfectly and answers with an empty object, because
|
|
490
|
+
* every member of a field-error shape is optional.
|
|
491
|
+
*/
|
|
492
|
+
declare class RigError<TFields = unknown> extends Error {
|
|
493
|
+
/**
|
|
494
|
+
* The HTTP status, for the cases where only it is meaningful — a 502 from
|
|
495
|
+
* something in front of the server, say, which carries no code.
|
|
496
|
+
*/
|
|
497
|
+
readonly status: number;
|
|
498
|
+
/**
|
|
499
|
+
* The machine-readable reason. Empty when the failure came from something
|
|
500
|
+
* that is not a rig server.
|
|
501
|
+
*/
|
|
502
|
+
readonly code: ErrorCode | "";
|
|
503
|
+
/** Prose, for a person. It is not meant to be parsed. */
|
|
504
|
+
readonly detail: string;
|
|
505
|
+
/**
|
|
506
|
+
* Correlates this failure with the server's logs. Quoting it in a bug
|
|
507
|
+
* report is the difference between a search and a guess.
|
|
508
|
+
*/
|
|
509
|
+
readonly requestId: string;
|
|
510
|
+
/**
|
|
511
|
+
* What was wrong with each member of the body — one member per field,
|
|
512
|
+
* holding what was wrong with it.
|
|
513
|
+
*
|
|
514
|
+
* `undefined` for every refusal but a 422. A 404 has a code and a message
|
|
515
|
+
* and nothing to put beside a control, and an empty object there would read
|
|
516
|
+
* as a body nobody complained about.
|
|
517
|
+
*/
|
|
518
|
+
readonly fields: TFields | undefined;
|
|
519
|
+
/**
|
|
520
|
+
* How long the server asked the caller to wait, in milliseconds, from the
|
|
521
|
+
* header of the same name in either form it takes: the seconds rig's own
|
|
522
|
+
* server sends, and the date something in front of it might. Zero when it
|
|
523
|
+
* said nothing, or asked for a moment already past.
|
|
524
|
+
*
|
|
525
|
+
* The SDK honours it for a call it may repeat, so this is mostly for the
|
|
526
|
+
* refusal that came back anyway — where the interval was longer than the
|
|
527
|
+
* call had left to spend.
|
|
528
|
+
*/
|
|
529
|
+
readonly retryAfterMs: number;
|
|
530
|
+
/**
|
|
531
|
+
* The start of the raw response, kept for a failure that decoded into
|
|
532
|
+
* nothing useful — a proxy's HTML error page, say. Bounded even where the
|
|
533
|
+
* read was not, so a large validation failure is complete in `fields` and
|
|
534
|
+
* cut short here.
|
|
535
|
+
*/
|
|
536
|
+
readonly body: string;
|
|
537
|
+
constructor(init: {
|
|
538
|
+
status: number;
|
|
539
|
+
code?: ErrorCode | "";
|
|
540
|
+
detail?: string;
|
|
541
|
+
requestId?: string;
|
|
542
|
+
fields?: TFields;
|
|
543
|
+
retryAfterMs?: number;
|
|
544
|
+
body?: string;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* The codes a rig server sends, matching `rig/runtime/rigerr`. The constant a
|
|
549
|
+
* client switches on is the constant a handler returned.
|
|
550
|
+
*/
|
|
551
|
+
declare const ErrorCode: {
|
|
552
|
+
readonly BadRequest: "BadRequest";
|
|
553
|
+
readonly Unauthorized: "Unauthorized";
|
|
554
|
+
readonly Forbidden: "Forbidden";
|
|
555
|
+
readonly NotFound: "NotFound";
|
|
556
|
+
readonly Conflict: "Conflict";
|
|
557
|
+
readonly UnprocessableEntity: "UnprocessableEntity";
|
|
558
|
+
readonly RateLimited: "RateLimited";
|
|
559
|
+
readonly TooLarge: "TooLarge";
|
|
560
|
+
readonly UnsupportedMediaType: "UnsupportedMediaType";
|
|
561
|
+
readonly UpgradeRequired: "UpgradeRequired";
|
|
562
|
+
readonly Internal: "Internal";
|
|
563
|
+
};
|
|
564
|
+
/** One of the codes in {@link ErrorCode}. */
|
|
565
|
+
type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
566
|
+
/**
|
|
567
|
+
* Why one member of a body was refused, matching `rig/runtime/rigerr`.
|
|
568
|
+
*
|
|
569
|
+
* The same nine codes for every project, so a form can decide what to show from
|
|
570
|
+
* the code and fall back to the message rather than parsing it.
|
|
571
|
+
*/
|
|
572
|
+
declare const FieldCode: {
|
|
573
|
+
readonly CannotBeEmpty: "CannotBeEmpty";
|
|
574
|
+
readonly CannotBeNull: "CannotBeNull";
|
|
575
|
+
readonly TooLong: "TooLong";
|
|
576
|
+
readonly TooShort: "TooShort";
|
|
577
|
+
readonly OutOfRange: "OutOfRange";
|
|
578
|
+
readonly InvalidValue: "InvalidValue";
|
|
579
|
+
readonly AlreadyExists: "AlreadyExists";
|
|
580
|
+
readonly NotFound: "NotFound";
|
|
581
|
+
readonly NotAllowed: "NotAllowed";
|
|
582
|
+
};
|
|
583
|
+
/** One of the codes in {@link FieldCode}. */
|
|
584
|
+
type FieldCode = (typeof FieldCode)[keyof typeof FieldCode];
|
|
585
|
+
/**
|
|
586
|
+
* What was wrong with one member of a body.
|
|
587
|
+
*
|
|
588
|
+
* It does not name the field. It is reached through the member of a generated
|
|
589
|
+
* field-error shape that stands for that field, so the name is where the value
|
|
590
|
+
* is and there is no second copy of it to disagree.
|
|
591
|
+
*/
|
|
592
|
+
type FieldError = {
|
|
593
|
+
code: FieldCode;
|
|
594
|
+
message: string;
|
|
595
|
+
};
|
|
596
|
+
/**
|
|
597
|
+
* Reports whether a thrown value is a refusal the server explained.
|
|
598
|
+
*
|
|
599
|
+
* False for anything that never reached the server — a DNS failure, an aborted
|
|
600
|
+
* request — because there is no envelope for a code or a field to have come
|
|
601
|
+
* from.
|
|
602
|
+
*/
|
|
603
|
+
declare function isRigError(err: unknown): err is RigError;
|
|
604
|
+
/**
|
|
605
|
+
* The code on a refusal, or the empty string for anything else.
|
|
606
|
+
*
|
|
607
|
+
* The predicates below are one line each on top of it; reach for it directly
|
|
608
|
+
* when switching over several codes at once.
|
|
609
|
+
*/
|
|
610
|
+
declare function codeOf(err: unknown): ErrorCode | "";
|
|
611
|
+
/** True when the row, or the route, was not there. */
|
|
612
|
+
declare const isNotFound: (err: unknown) => boolean;
|
|
613
|
+
/** True when the write lost a race, or would have broken a uniqueness rule. */
|
|
614
|
+
declare const isConflict: (err: unknown) => boolean;
|
|
615
|
+
/** True when the caller was not signed in, or the token had expired. */
|
|
616
|
+
declare const isUnauthorized: (err: unknown) => boolean;
|
|
617
|
+
/** True when the caller was signed in and still not allowed. */
|
|
618
|
+
declare const isForbidden: (err: unknown) => boolean;
|
|
619
|
+
/** True when the body was refused field by field. `fields` says which. */
|
|
620
|
+
declare const isInvalid: (err: unknown) => boolean;
|
|
621
|
+
/** True when the caller was asked to slow down. `retryAfterMs` says how long. */
|
|
622
|
+
declare const isRateLimited: (err: unknown) => boolean;
|
|
623
|
+
/** True when the body, or the upload, was over the limit. */
|
|
624
|
+
declare const isTooLarge: (err: unknown) => boolean;
|
|
625
|
+
/** True when the content type was not one this route accepts. */
|
|
626
|
+
declare const isUnsupportedMediaType: (err: unknown) => boolean;
|
|
627
|
+
/** True when the client is older than the API surface still supports. */
|
|
628
|
+
declare const isUpgradeRequired: (err: unknown) => boolean;
|
|
629
|
+
/**
|
|
630
|
+
* Reads a refusal back as the shape of the body that caused it.
|
|
631
|
+
*
|
|
632
|
+
* This is the hand-written counterpart of the generated per-call guards, for a
|
|
633
|
+
* request made through the runtime directly. It cannot check that the shape is
|
|
634
|
+
* the right one — that is what the generated guard is for.
|
|
635
|
+
*/
|
|
636
|
+
declare function fieldsAs<TFields>(err: unknown): TFields | undefined;
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/transport.d.ts
|
|
639
|
+
/** What a caller says about one call, on top of what the method already knows. */
|
|
640
|
+
type CallOptions = {
|
|
641
|
+
/** Added to this request, replacing a client-wide header of the same name. */headers?: HeadersInit;
|
|
642
|
+
/**
|
|
643
|
+
* Added to the query string, winning over the operation's own where both
|
|
644
|
+
* name the same parameter.
|
|
645
|
+
*/
|
|
646
|
+
query?: Record<string, string> | URLSearchParams; /** Aborts the call, retries and backoff included. */
|
|
647
|
+
signal?: AbortSignal; /** Bounds this call in milliseconds, overriding the client's own. */
|
|
648
|
+
timeoutMs?: number; /** Overrides how many times this one call is sent. */
|
|
649
|
+
attempts?: number;
|
|
650
|
+
/**
|
|
651
|
+
* Names this write, so a server that sees the same key twice answers the
|
|
652
|
+
* second send with what it answered the first.
|
|
653
|
+
*
|
|
654
|
+
* The SDK generates one for a write it may repeat. A caller's own is worth
|
|
655
|
+
* having where it is derived from the data — an import job naming a row by
|
|
656
|
+
* its line deduplicates a re-run of the whole job, which a fresh random name
|
|
657
|
+
* cannot.
|
|
658
|
+
*/
|
|
659
|
+
idempotencyKey?: string; /** Overrides the request identifier for this call. */
|
|
660
|
+
requestId?: string; /** Sends no credential, for a route that must be reached signed out. */
|
|
661
|
+
anonymous?: boolean;
|
|
662
|
+
/**
|
|
663
|
+
* Asks for every row in the tenant rather than the caller's own, on a
|
|
664
|
+
* resource that is owner-scoped. Refused unless the caller holds the
|
|
665
|
+
* permission for it.
|
|
666
|
+
*/
|
|
667
|
+
wide?: boolean;
|
|
668
|
+
/**
|
|
669
|
+
* Makes the read conditional. A 304 then comes back as `undefined` rather
|
|
670
|
+
* than as a failure — which is why it is gated on the option and not on the
|
|
671
|
+
* status alone: a 304 nobody asked for is an unexplained failure.
|
|
672
|
+
*/
|
|
673
|
+
ifNoneMatch?: string;
|
|
674
|
+
};
|
|
675
|
+
/**
|
|
676
|
+
* Performs a call the document says answers with a body, and decodes it.
|
|
677
|
+
*
|
|
678
|
+
* A 204 where a body was promised throws rather than resolving to `undefined`.
|
|
679
|
+
* That is a deliberate trade against {@link sendOptional}: the alternative is
|
|
680
|
+
* every generated method answering `T | undefined`, so every call site narrows a
|
|
681
|
+
* value that in practice is always there — and the one time it is not, the
|
|
682
|
+
* server and the document disagree, which is a bug and reads better as one.
|
|
683
|
+
*/
|
|
684
|
+
declare function send<T>(rt: Runtime, op: Op, opts?: CallOptions): Promise<T>;
|
|
685
|
+
/**
|
|
686
|
+
* Performs a call and decodes the response body, or `undefined` when there was
|
|
687
|
+
* none.
|
|
688
|
+
*
|
|
689
|
+
* This is the shape for an endpoint that can honestly answer either way, and for
|
|
690
|
+
* a request made through the runtime by hand.
|
|
691
|
+
*/
|
|
692
|
+
declare function sendOptional<T>(rt: Runtime, op: Op, opts?: CallOptions): Promise<T | undefined>;
|
|
693
|
+
/**
|
|
694
|
+
* Performs a call that answers with nothing, such as a delete.
|
|
695
|
+
*
|
|
696
|
+
* Any body is drained and discarded rather than parsed: an endpoint that grows
|
|
697
|
+
* one later should not break a client that never wanted it.
|
|
698
|
+
*/
|
|
699
|
+
declare function sendNoContent(rt: Runtime, op: Op, opts?: CallOptions): Promise<void>;
|
|
700
|
+
/**
|
|
701
|
+
* Performs a call and hands back the response unread, for a download.
|
|
702
|
+
*
|
|
703
|
+
* The caller owns the body from here: nothing below reads it, so streaming a
|
|
704
|
+
* large file does not require buffering it first.
|
|
705
|
+
*/
|
|
706
|
+
declare function sendContent(rt: Runtime, op: Op, opts?: CallOptions): Promise<Response>;
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/paginate.d.ts
|
|
709
|
+
/** One page of a paginated read, as far as the iteration cares. */
|
|
710
|
+
type Page<T> = {
|
|
711
|
+
items: T[];
|
|
712
|
+
/**
|
|
713
|
+
* Every row matching the query, ignoring pagination. It is what says whether
|
|
714
|
+
* there is another page.
|
|
715
|
+
*/
|
|
716
|
+
total: number; /** Where this page started, as the server reported it. */
|
|
717
|
+
offset: number;
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* Walks a paginated read to its end.
|
|
721
|
+
*
|
|
722
|
+
* `fetch` is handed the offset to ask for and returns one page; the limit is
|
|
723
|
+
* whatever the caller's query said, which `fetch` closes over. Iteration stops
|
|
724
|
+
* after the page that reaches the reported total, at the first failure, or when
|
|
725
|
+
* a page comes back empty — that last one is the guard against a server whose
|
|
726
|
+
* total disagrees with what it returns, which would otherwise be an infinite
|
|
727
|
+
* loop rather than a bug report.
|
|
728
|
+
*
|
|
729
|
+
* A failure is thrown, so `for await` reports it where the caller is standing.
|
|
730
|
+
* There is no partial answer: what came before the failure was yielded, and
|
|
731
|
+
* nothing after it is.
|
|
732
|
+
*
|
|
733
|
+
* ```ts
|
|
734
|
+
* for await (const todo of paginate(0, (offset) =>
|
|
735
|
+
* client.todos.list({ limit: 100, offset }).then(toPage)
|
|
736
|
+
* )) {
|
|
737
|
+
* …
|
|
738
|
+
* }
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
declare function paginate<T>(startOffset: number, fetch: (offset: number) => Promise<Page<T>>): AsyncGenerator<T, void, undefined>;
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region src/query.d.ts
|
|
744
|
+
/**
|
|
745
|
+
* The query-string writers the generated methods call.
|
|
746
|
+
*
|
|
747
|
+
* Every one of them writes nothing for `undefined`, which is the point: the
|
|
748
|
+
* generated server applies a parameter's default only when the parameter is
|
|
749
|
+
* absent, so a client that helpfully sent `limit=0` would get an empty page
|
|
750
|
+
* instead of the default one. Absent has to stay absent.
|
|
751
|
+
*
|
|
752
|
+
* The formats match what the server parses: RFC 3339 for a time, the canonical
|
|
753
|
+
* hyphenated form for a UUID, `"true"`/`"false"` for a boolean.
|
|
754
|
+
*/
|
|
755
|
+
/** The types a query parameter can be given as. */
|
|
756
|
+
type ParamValue = string | number | boolean | Date;
|
|
757
|
+
/** Writes one parameter, or nothing when the value is absent. */
|
|
758
|
+
declare function setParam(query: URLSearchParams, key: string, value: ParamValue | null | undefined): void;
|
|
759
|
+
/**
|
|
760
|
+
* Writes a repeated parameter, one key per value.
|
|
761
|
+
*
|
|
762
|
+
* An empty array writes nothing rather than an empty key, for the same reason a
|
|
763
|
+
* single absent value does: the server distinguishes "no filter" from "a filter
|
|
764
|
+
* matching nothing".
|
|
765
|
+
*/
|
|
766
|
+
declare function setParams(query: URLSearchParams, key: string, values: readonly ParamValue[] | null | undefined): void;
|
|
767
|
+
/**
|
|
768
|
+
* Escapes a value for a path segment.
|
|
769
|
+
*
|
|
770
|
+
* An identifier that arrived from somewhere else can be anything at all, and a
|
|
771
|
+
* slash in one would otherwise silently address a different route.
|
|
772
|
+
*/
|
|
773
|
+
declare function pathValue(value: string): string;
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/upload.d.ts
|
|
776
|
+
/** One file a caller is sending. */
|
|
777
|
+
type Upload = {
|
|
778
|
+
/**
|
|
779
|
+
* What the file is called. The server records it on the row and puts it in
|
|
780
|
+
* the download path, and it never becomes the storage key — so a name with a
|
|
781
|
+
* slash in it is a strange name and not a way out of the bucket.
|
|
782
|
+
*/
|
|
783
|
+
name: string; /** The bytes. A `File` from an `<input type="file">` is already one of these. */
|
|
784
|
+
body: Blob;
|
|
785
|
+
/**
|
|
786
|
+
* What the caller claims the bytes are. Absent takes the blob's own type,
|
|
787
|
+
* and it makes little difference either way: the server sniffs the content,
|
|
788
|
+
* and the sniffed type is the one it stores and the one it serves back.
|
|
789
|
+
*/
|
|
790
|
+
contentType?: string;
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* Builds the `multipart/form-data` body rig's upload endpoints take: the row,
|
|
794
|
+
* and the files beside it.
|
|
795
|
+
*
|
|
796
|
+
* It is the shape rig's own endpoints take rather than a general form encoder. A
|
|
797
|
+
* generated method calls it; a caller supplies the {@link Upload}s.
|
|
798
|
+
*
|
|
799
|
+
* `row` is left out entirely when absent, which is what a bare upload does:
|
|
800
|
+
* there is no row to send, only bytes for a row that already exists.
|
|
801
|
+
*
|
|
802
|
+
* An absent upload is left out the same way, so a generated method can name
|
|
803
|
+
* every file column of a create unconditionally. What makes a required one
|
|
804
|
+
* impossible to leave out is the generated shape it arrives in — the member is
|
|
805
|
+
* optional there only where the column is nullable — and not a check here,
|
|
806
|
+
* which would report at runtime what the compiler has already refused.
|
|
807
|
+
*/
|
|
808
|
+
declare function multipart(row: unknown, files: ReadonlyArray<readonly [field: string, upload: Upload | undefined]>): FormData;
|
|
809
|
+
//#endregion
|
|
810
|
+
export { type ApiDescriptor, type AuthProfile, type Bindable, type CallOptions, type Config, type Credential, DEFAULT_REQUEST_ID_HEADER, DEFAULT_REVISION_HEADER, ErrorCode, FieldCode, type FieldError, METHOD_QUERY, NoSessionError, type Op, type Page, type ParamValue, type RateLimitStatus, type Reauthorizer, type Retry, RigError, Runtime, Session, type TokenPair, type Upload, apiKey, codeOf, fieldsAs, fraction, isConflict, isForbidden, isInvalid, isNotFound, isRateLimited, isReauthorizer, isRigError, isTooLarge, isUnauthorized, isUnsupportedMediaType, isUpgradeRequired, multipart, paginate, pathValue, rateLimitOf, send, sendContent, sendNoContent, sendOptional, setParam, setParams, staticToken, used };
|