@typeship-ax/mcp 0.21.0 → 0.22.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/AGENTS.md +15 -11
- package/README.md +22 -53
- package/api.json +9998 -10118
- package/api.md +8983 -9120
- package/dist/arguments.d.ts +47 -0
- package/dist/arguments.d.ts.map +1 -0
- package/dist/arguments.js +254 -0
- package/dist/core/http.d.ts +162 -19
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +381 -48
- package/dist/core/pagination.d.ts +42 -6
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +111 -17
- package/dist/credential-storage.d.ts +10 -3
- package/dist/credential-storage.d.ts.map +1 -1
- package/dist/credential-storage.js +15 -6
- package/dist/dates.d.ts +1 -1
- package/dist/dates.js +1 -1
- package/dist/errors.d.ts +20 -84
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -108
- package/dist/fields.d.ts +29 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +101 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -25
- package/dist/mcp-authorization.d.ts.map +1 -1
- package/dist/mcp-authorization.js +34 -10
- package/dist/mcp-protocol.d.ts +87 -44
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +552 -478
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +127 -28
- package/dist/named-credentials.d.ts +19 -0
- package/dist/named-credentials.d.ts.map +1 -1
- package/dist/named-credentials.js +81 -1
- package/dist/oauth-request.d.ts +7 -1
- package/dist/oauth-request.d.ts.map +1 -1
- package/dist/oauth-request.js +26 -4
- package/dist/oauth-session.d.ts +13 -1
- package/dist/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +34 -18
- package/dist/ops.d.ts +53 -5
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +49 -40
- package/dist/resources/api-keys.d.ts +10 -7
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +10 -31
- package/dist/resources/deliveries.d.ts +88 -4
- package/dist/resources/deliveries.d.ts.map +1 -1
- package/dist/resources/deliveries.js +95 -18
- package/dist/resources/drafts.d.ts +15 -15
- package/dist/resources/drafts.d.ts.map +1 -1
- package/dist/resources/drafts.js +11 -64
- package/dist/resources/files.d.ts +4 -4
- package/dist/resources/files.d.ts.map +1 -1
- package/dist/resources/files.js +3 -12
- package/dist/resources/generations.d.ts +14 -14
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +21 -45
- package/dist/resources/organization.d.ts +4 -4
- package/dist/resources/organization.d.ts.map +1 -1
- package/dist/resources/organization.js +3 -10
- package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
- package/dist/resources/packages.d.ts.map +1 -0
- package/dist/resources/{generate.js → packages.js} +13 -29
- package/dist/resources/projects.d.ts +50 -50
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +60 -116
- package/dist/resources/releases.d.ts +21 -16
- package/dist/resources/releases.d.ts.map +1 -1
- package/dist/resources/releases.js +18 -39
- package/dist/resources/spec-revisions.d.ts +15 -6
- package/dist/resources/spec-revisions.d.ts.map +1 -1
- package/dist/resources/spec-revisions.js +6 -28
- package/dist/resources/specs.d.ts +7 -7
- package/dist/resources/specs.d.ts.map +1 -1
- package/dist/resources/specs.js +6 -34
- package/dist/resources/targets.d.ts +48 -48
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +58 -114
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +78 -76
- package/dist/search.d.ts +54 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +421 -0
- package/dist/types.d.ts +499 -339
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -18
- package/dist/worker.js +2 -2
- package/package.json +5 -2
- package/server.json +5 -5
- package/src/arguments.ts +242 -0
- package/src/core/http.ts +457 -58
- package/src/core/pagination.ts +129 -18
- package/src/credential-storage.ts +16 -6
- package/src/dates.ts +1 -1
- package/src/errors.ts +46 -115
- package/src/fields.ts +91 -0
- package/src/index.ts +45 -28
- package/src/mcp-authorization.ts +29 -9
- package/src/mcp-protocol.ts +580 -428
- package/src/mcp.ts +113 -26
- package/src/named-credentials.ts +66 -1
- package/src/oauth-request.ts +32 -6
- package/src/oauth-session.ts +37 -19
- package/src/ops.ts +82 -44
- package/src/resources/api-keys.ts +34 -48
- package/src/resources/deliveries.ts +211 -30
- package/src/resources/drafts.ts +60 -107
- package/src/resources/files.ts +19 -20
- package/src/resources/generations.ts +57 -75
- package/src/resources/organization.ts +11 -16
- package/src/resources/{generate.ts → packages.ts} +43 -51
- package/src/resources/projects.ts +145 -200
- package/src/resources/releases.ts +48 -65
- package/src/resources/spec-revisions.ts +38 -47
- package/src/resources/specs.ts +39 -59
- package/src/resources/targets.ts +143 -193
- package/src/schemas.ts +78 -76
- package/src/search.ts +434 -0
- package/src/types.ts +538 -357
- package/src/worker.ts +2 -2
- package/dist/resources/generate.d.ts.map +0 -1
- package/dist/resources/publications.d.ts +0 -47
- package/dist/resources/publications.d.ts.map +0 -1
- package/dist/resources/publications.js +0 -70
- package/src/resources/publications.ts +0 -140
package/src/core/http.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime core. Generated by
|
|
2
|
+
* Runtime core. Generated by Typeship — https://typeship.dev
|
|
3
3
|
* Zero dependencies: built on the platform fetch API (Node 20+, browsers, edge).
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -12,6 +12,14 @@ export interface RequestOptions {
|
|
|
12
12
|
timeoutMs?: number;
|
|
13
13
|
/** Retry attempts after the first try. Overrides the client default. */
|
|
14
14
|
maxRetries?: number;
|
|
15
|
+
/** For a list that pages by URL (a Link header or a next-page URL field):
|
|
16
|
+
* start from this page URL, as a previous page's nextPageParams() gave it.
|
|
17
|
+
* It must be on the API's origin. */
|
|
18
|
+
pageUrl?: string;
|
|
19
|
+
/** Receives this call's response metadata (status, headers, ETag,
|
|
20
|
+
* rate-limit state) once a response arrives, whether the call succeeded
|
|
21
|
+
* or failed. A 304 Not Modified raises `NotModifiedError`. */
|
|
22
|
+
onResponse?: (response: ResponseMeta) => void;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
export interface ResponseMeta {
|
|
@@ -24,13 +32,82 @@ export interface ResponseMeta {
|
|
|
24
32
|
/** Request identifier from the JSON response body, or from headers for
|
|
25
33
|
* raw and bodyless responses. */
|
|
26
34
|
requestId?: string;
|
|
35
|
+
/** The `ETag` header, for a later conditional request (`If-None-Match`). */
|
|
36
|
+
etag?: string;
|
|
37
|
+
/** The `Last-Modified` header, for a later `If-Modified-Since`. */
|
|
38
|
+
lastModified?: string;
|
|
39
|
+
/** True for a 304 Not Modified: the resource matched the conditional
|
|
40
|
+
* request, and the call raised `NotModifiedError`. */
|
|
41
|
+
notModified?: boolean;
|
|
42
|
+
/** Set when the API said the call was rate limited. */
|
|
43
|
+
rateLimit?: RateLimitInfo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** When a rate-limited call can be retried. Both fields are absent when the
|
|
47
|
+
* API did not say. */
|
|
48
|
+
export interface RateLimitInfo {
|
|
49
|
+
/** The instant the limit resets (from `Retry-After` or `x-ratelimit-reset`). */
|
|
50
|
+
retryAt?: Date;
|
|
51
|
+
/** Milliseconds from when the response arrived until `retryAt`. */
|
|
52
|
+
retryAfterMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Response headers that carry a request identifier, most specific first. */
|
|
56
|
+
const REQUEST_ID_HEADERS = ["request-id", "x-request-id", "x-github-request-id", "twilio-request-id", "x-amzn-requestid", "x-slack-req-id", "cf-ray"];
|
|
57
|
+
|
|
58
|
+
function requestIdFromHeaders(headers: Headers): string | undefined {
|
|
59
|
+
for (const name of REQUEST_ID_HEADERS) {
|
|
60
|
+
const value = headers.get(name);
|
|
61
|
+
if (value) return value;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Whether a response is a rate limit, and when to retry. A 429 always is. A
|
|
68
|
+
* 403 is when it says the quota is spent (`x-ratelimit-remaining: 0`) or asks
|
|
69
|
+
* the caller to wait (`Retry-After`), which is how GitHub signals its primary
|
|
70
|
+
* and secondary limits. Undefined for every other response.
|
|
71
|
+
*/
|
|
72
|
+
export function rateLimitInfo(status: number, headers: Headers, now = Date.now()): RateLimitInfo | undefined {
|
|
73
|
+
const retryAfter = headers.get("retry-after");
|
|
74
|
+
const remaining = headers.get("x-ratelimit-remaining") ?? headers.get("ratelimit-remaining");
|
|
75
|
+
const limited = status === 429 || (status === 403 && (retryAfter !== null || (remaining !== null && remaining.trim() === "0")));
|
|
76
|
+
if (!limited) return undefined;
|
|
77
|
+
let waitMs: number | undefined;
|
|
78
|
+
if (retryAfter !== null && retryAfter.trim() !== "") {
|
|
79
|
+
const seconds = Number(retryAfter);
|
|
80
|
+
if (Number.isFinite(seconds)) waitMs = Math.max(seconds, 0) * 1000;
|
|
81
|
+
else if (!Number.isNaN(Date.parse(retryAfter))) waitMs = Math.max(Date.parse(retryAfter) - now, 0);
|
|
82
|
+
}
|
|
83
|
+
const reset = headers.get("x-ratelimit-reset") ?? headers.get("ratelimit-reset");
|
|
84
|
+
if (waitMs === undefined && reset !== null && reset.trim() !== "" && Number.isFinite(Number(reset))) {
|
|
85
|
+
const value = Number(reset);
|
|
86
|
+
// An epoch timestamp (GitHub, Twitter) or seconds until the reset (the
|
|
87
|
+
// IETF RateLimit header fields).
|
|
88
|
+
waitMs = Math.max(value > 1e9 ? value * 1000 - now : value * 1000, 0);
|
|
89
|
+
}
|
|
90
|
+
if (waitMs === undefined) return {};
|
|
91
|
+
return { retryAt: new Date(now + waitMs), retryAfterMs: waitMs };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rateLimitStep(info: RateLimitInfo): string {
|
|
95
|
+
if (!info.retryAt) return "Rate limited: wait before retrying.";
|
|
96
|
+
// Round up to the second so "wait until" is never early.
|
|
97
|
+
const at = new Date(Math.ceil(info.retryAt.getTime() / 1000) * 1000);
|
|
98
|
+
return "Rate limited: wait until " + at.toISOString().replace(/\.\d{3}Z$/, "Z") + ", then retry.";
|
|
27
99
|
}
|
|
28
100
|
|
|
29
101
|
/**
|
|
30
102
|
* A static credential or a callback resolved before every attempt. Use a
|
|
31
103
|
* callback for tokens that expire (OAuth access tokens, STS, Vault).
|
|
32
104
|
*/
|
|
33
|
-
export type AuthValue = string | (() => string | Promise<string>);
|
|
105
|
+
export type AuthValue = string | ((context: CredentialContext) => string | Promise<string>);
|
|
106
|
+
|
|
107
|
+
/** Passed to credential callbacks. `rejected` is true once, on the attempt
|
|
108
|
+
* after the API answered 401 to the value this callback last returned:
|
|
109
|
+
* refresh or replace the token instead of returning it again. */
|
|
110
|
+
export interface CredentialContext { rejected: boolean }
|
|
34
111
|
|
|
35
112
|
/** Passed to onRequest/onResponse hooks. Mutations to headers and url in
|
|
36
113
|
* onRequest apply to the outgoing request. */
|
|
@@ -92,53 +169,190 @@ export class SdkError extends Error {
|
|
|
92
169
|
}
|
|
93
170
|
}
|
|
94
171
|
|
|
172
|
+
function errorFields(body: unknown): { error?: Record<string, unknown>; value?: Record<string, unknown>; first?: Record<string, unknown> } {
|
|
173
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return {};
|
|
174
|
+
const value = body as Record<string, unknown>;
|
|
175
|
+
const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error as Record<string, unknown> : undefined;
|
|
176
|
+
const first = Array.isArray(value.errors) && value.errors[0] && typeof value.errors[0] === "object" ? value.errors[0] as Record<string, unknown> : undefined;
|
|
177
|
+
return { error: nested, value, first };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The API's own error code: `error.code`, `code`, `error.type`, then
|
|
181
|
+
* `errors[0].code`; numeric codes (Twilio's 20404) as strings. */
|
|
95
182
|
function errorCode(body: unknown, fallback: string): string {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const code = value.code ?? first?.code;
|
|
183
|
+
const { error, value, first } = errorFields(body);
|
|
184
|
+
if (!value) return fallback;
|
|
185
|
+
for (const code of [error?.code, value.code, error?.type, first?.code]) {
|
|
100
186
|
if (typeof code === "string" && code.trim()) return code;
|
|
187
|
+
if (typeof code === "number" && Number.isFinite(code)) return String(code);
|
|
101
188
|
}
|
|
189
|
+
// Slack-style `{"ok": false, "error": "invalid_auth"}`: the error is a code.
|
|
190
|
+
if (typeof value.error === "string" && /^[A-Za-z][\w.-]{0,63}$/.test(value.error)) return value.error;
|
|
102
191
|
return fallback;
|
|
103
192
|
}
|
|
104
193
|
|
|
194
|
+
/** The API's own message: `error.message`, `message`, `errors[0].message`,
|
|
195
|
+
* `detail`, `error_description`, then a string `error`. */
|
|
105
196
|
function errorDetail(body: unknown): string {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
197
|
+
const { error, value, first } = errorFields(body);
|
|
198
|
+
if (!value) return "";
|
|
199
|
+
for (const detail of [error?.message, value.message, first?.message, value.detail, value.error_description, value.error]) {
|
|
200
|
+
if (typeof detail === "string" && detail.trim()) return detail.trim();
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
111
203
|
}
|
|
112
204
|
|
|
113
205
|
function nextStep(status: number): string {
|
|
206
|
+
if (status >= 200 && status < 300) return "It arrived in a successful HTTP response; the body says why.";
|
|
114
207
|
if (status === 401) return "Check the credential and retry.";
|
|
115
208
|
if (status === 403) return "Check the credential's permissions and retry.";
|
|
116
209
|
if (status === 404) return "Check the requested identifier or path.";
|
|
117
210
|
if (status === 409) return "Refresh the resource and retry the change.";
|
|
211
|
+
if (status === 413) return "Send less data in one request.";
|
|
118
212
|
if (status === 422 || status === 400) return "Correct the request and retry.";
|
|
119
213
|
if (status === 429) return "Wait before retrying the request.";
|
|
120
214
|
if (status >= 500) return "Retry later; contact the API provider if this continues.";
|
|
121
215
|
return "Inspect the error body and correct the request before retrying.";
|
|
122
216
|
}
|
|
123
217
|
|
|
124
|
-
/** Base class for every HTTP error response.
|
|
218
|
+
/** Base class for every HTTP error response. The message leads with the
|
|
219
|
+
* status and the API's own message ("HTTP 404: No such customer"), then the
|
|
220
|
+
* next step. `code` is the API's error code when the body names one. */
|
|
125
221
|
export class ApiError<S extends number = number, B = unknown> extends SdkError {
|
|
126
222
|
readonly status: S;
|
|
127
223
|
readonly body: B;
|
|
128
224
|
readonly response: ResponseMeta;
|
|
129
225
|
|
|
130
|
-
|
|
131
|
-
|
|
226
|
+
/** Set when the API said this call was rate limited (a 429, or a 403
|
|
227
|
+
* whose headers say the quota is spent): when to retry. */
|
|
228
|
+
readonly rateLimit?: RateLimitInfo;
|
|
229
|
+
|
|
230
|
+
/** `summary` leads the message ("HTTP 404"); the API's message follows it. */
|
|
231
|
+
constructor(summary: string, status: S, body: B, response: ResponseMeta, own?: { message: string; code: string }) {
|
|
232
|
+
const limit = response.rateLimit ?? (response.headers ? rateLimitInfo(status, response.headers) : undefined);
|
|
233
|
+
const detail = errorDetail(body);
|
|
234
|
+
super(
|
|
235
|
+
// An API message usually ends its own sentence; do not double it.
|
|
236
|
+
own?.message ?? (summary + (detail ? ": " + detail : "")).replace(/[.!?]+$/, "") + ". " + (limit ? rateLimitStep(limit) : nextStep(status)),
|
|
237
|
+
own?.code ?? errorCode(body, limit ? "rate_limited" : "http_" + status),
|
|
238
|
+
status, body, response.requestId,
|
|
239
|
+
);
|
|
132
240
|
this.status = status;
|
|
133
241
|
this.body = body;
|
|
134
242
|
this.response = response;
|
|
243
|
+
if (limit) this.rateLimit = limit;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** HTTP 400: the API rejected the request as malformed. Raised for every
|
|
248
|
+
* 400, documented or not; `body` is typed where the operation declares it. */
|
|
249
|
+
export class BadRequestError<B = unknown> extends ApiError<400, B> {
|
|
250
|
+
constructor(body: B, response: ResponseMeta) {
|
|
251
|
+
super("HTTP 400", 400, body, response);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** HTTP 401: the credential is missing, invalid or expired. */
|
|
256
|
+
export class UnauthorizedError<B = unknown> extends ApiError<401, B> {
|
|
257
|
+
constructor(body: B, response: ResponseMeta) {
|
|
258
|
+
super("HTTP 401", 401, body, response);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** HTTP 403: the credential lacks access. A 403 that signals a rate limit
|
|
263
|
+
* raises RateLimitError instead. */
|
|
264
|
+
export class ForbiddenError<B = unknown> extends ApiError<403, B> {
|
|
265
|
+
constructor(body: B, response: ResponseMeta) {
|
|
266
|
+
super("HTTP 403", 403, body, response);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** HTTP 404: the resource or path does not exist. */
|
|
271
|
+
export class NotFoundError<B = unknown> extends ApiError<404, B> {
|
|
272
|
+
constructor(body: B, response: ResponseMeta) {
|
|
273
|
+
super("HTTP 404", 404, body, response);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** HTTP 409: the request conflicts with the resource's current state. */
|
|
278
|
+
export class ConflictError<B = unknown> extends ApiError<409, B> {
|
|
279
|
+
constructor(body: B, response: ResponseMeta) {
|
|
280
|
+
super("HTTP 409", 409, body, response);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** HTTP 422: the request was well formed but failed validation. */
|
|
285
|
+
export class UnprocessableEntityError<B = unknown> extends ApiError<422, B> {
|
|
286
|
+
constructor(body: B, response: ResponseMeta) {
|
|
287
|
+
super("HTTP 422", 422, body, response);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The API rate limited the call: every 429, and a 403 whose headers say
|
|
292
|
+
* the quota is spent. `rateLimit.retryAt` says when to try again. */
|
|
293
|
+
export class RateLimitError<B = unknown> extends ApiError<number, B> {
|
|
294
|
+
constructor(body: B, response: ResponseMeta) {
|
|
295
|
+
super("HTTP " + response.status, response.status, body, response);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Any 5xx: the API failed to handle a valid request. */
|
|
300
|
+
export class ServerError<B = unknown> extends ApiError<number, B> {
|
|
301
|
+
constructor(body: B, response: ResponseMeta) {
|
|
302
|
+
super("HTTP " + response.status, response.status, body, response);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** HTTP 304: a conditional request (`If-None-Match`, `If-Modified-Since`)
|
|
307
|
+
* matched, so the resource has not changed and the response has no body.
|
|
308
|
+
* Raised only when the caller sent a conditional header. `etag` and
|
|
309
|
+
* `lastModified` are the validators to send next time. */
|
|
310
|
+
export class NotModifiedError extends ApiError<304, undefined> {
|
|
311
|
+
/** The response's `ETag` header. */
|
|
312
|
+
readonly etag?: string;
|
|
313
|
+
/** The response's `Last-Modified` header. */
|
|
314
|
+
readonly lastModified?: string;
|
|
315
|
+
constructor(response: ResponseMeta) {
|
|
316
|
+
super("HTTP 304", 304, undefined, response, {
|
|
317
|
+
message: "HTTP 304: Not modified. The resource matches the validator sent in If-None-Match or If-Modified-Since; keep using the copy you have.",
|
|
318
|
+
code: "not_modified",
|
|
319
|
+
});
|
|
320
|
+
if (response.etag) this.etag = response.etag;
|
|
321
|
+
if (response.lastModified) this.lastModified = response.lastModified;
|
|
135
322
|
}
|
|
136
323
|
}
|
|
137
324
|
|
|
138
|
-
/**
|
|
325
|
+
/** The class raised for a status whatever the operation declares. */
|
|
326
|
+
function statusFamily(status: number): ErrorCtor | undefined {
|
|
327
|
+
switch (status) {
|
|
328
|
+
case 400: return BadRequestError;
|
|
329
|
+
case 401: return UnauthorizedError;
|
|
330
|
+
case 403: return ForbiddenError;
|
|
331
|
+
case 404: return NotFoundError;
|
|
332
|
+
case 409: return ConflictError;
|
|
333
|
+
case 422: return UnprocessableEntityError;
|
|
334
|
+
case 429: return RateLimitError;
|
|
335
|
+
}
|
|
336
|
+
return status >= 500 && status < 600 ? ServerError : undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** A 2xx response whose payload reported a failure: a declared envelope
|
|
340
|
+
* flag set to false (`{"ok": false}`, `{"success": false}`), or a GraphQL
|
|
341
|
+
* result that is one of the schema's error types. */
|
|
342
|
+
export class PayloadError extends ApiError<number, unknown> {
|
|
343
|
+
/** The GraphQL error type the result resolved to, for GraphQL operations. */
|
|
344
|
+
readonly typename?: string;
|
|
345
|
+
constructor(body: unknown, response: ResponseMeta, typename?: string) {
|
|
346
|
+
super(typename ? "The operation returned " + typename : "The API reported a failure", response.status, body, response);
|
|
347
|
+
if (typename) this.typename = typename;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** A status with no family class (a 402, 405 or 410, say) that the
|
|
352
|
+
* operation did not document. */
|
|
139
353
|
export class UnexpectedApiError extends ApiError<number, unknown> {
|
|
140
354
|
constructor(status: number, body: unknown, response: ResponseMeta) {
|
|
141
|
-
super("
|
|
355
|
+
super("HTTP " + status, status, body, response);
|
|
142
356
|
}
|
|
143
357
|
}
|
|
144
358
|
|
|
@@ -308,13 +522,35 @@ function transportFailureMessage(method: string, url: string, cause: unknown): s
|
|
|
308
522
|
return method + " " + url + " failed: " + detail;
|
|
309
523
|
}
|
|
310
524
|
|
|
311
|
-
/**
|
|
525
|
+
/** Why a request failed in transit, as TransportError's `code`: the
|
|
526
|
+
* caller's AbortSignal fired ("aborted"), the per-attempt timeout elapsed
|
|
527
|
+
* ("timeout"), or the connection or body read failed ("transport_error"). */
|
|
528
|
+
export type TransportFailure = "aborted" | "timeout" | "transport_error";
|
|
529
|
+
|
|
530
|
+
function transportFailure(cause: unknown, signal?: AbortSignal): TransportFailure {
|
|
531
|
+
if (signal?.aborted) return "aborted";
|
|
532
|
+
let node: unknown = cause;
|
|
533
|
+
for (let depth = 0; depth < 5 && node !== null && typeof node === "object"; depth++) {
|
|
534
|
+
if ((node as { name?: unknown }).name === "TimeoutError") return "timeout";
|
|
535
|
+
node = (node as { cause?: unknown }).cause;
|
|
536
|
+
}
|
|
537
|
+
return "transport_error";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const TRANSPORT_NEXT_STEP: Record<TransportFailure, string> = {
|
|
541
|
+
aborted: ". The caller's AbortSignal cancelled the request.",
|
|
542
|
+
timeout: ". The request timed out; retry, or raise timeoutMs.",
|
|
543
|
+
transport_error: ". Check the connection and retry.",
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body); `code` says which. */
|
|
312
547
|
export class TransportError extends SdkError {
|
|
313
548
|
override readonly cause?: unknown;
|
|
314
549
|
readonly response?: ResponseMeta;
|
|
550
|
+
declare readonly code: TransportFailure;
|
|
315
551
|
|
|
316
|
-
constructor(message: string, cause?: unknown, response?: ResponseMeta) {
|
|
317
|
-
super(message +
|
|
552
|
+
constructor(message: string, cause?: unknown, response?: ResponseMeta, failure: TransportFailure = "transport_error") {
|
|
553
|
+
super(message + TRANSPORT_NEXT_STEP[failure], failure, response?.status ?? null, undefined, response?.requestId);
|
|
318
554
|
this.cause = cause;
|
|
319
555
|
this.response = response;
|
|
320
556
|
}
|
|
@@ -326,10 +562,18 @@ export interface CoreRequest {
|
|
|
326
562
|
security?: Record<string, string[]>[];
|
|
327
563
|
method: string;
|
|
328
564
|
path: string;
|
|
565
|
+
/** A full URL on the API's origin (a next-page link), sent instead of
|
|
566
|
+
* path and query. */
|
|
567
|
+
url?: string;
|
|
329
568
|
query?: Record<string, unknown>;
|
|
330
569
|
headers?: Record<string, string | undefined>;
|
|
331
570
|
body?: unknown;
|
|
332
571
|
bodyKind?: "json" | "form" | "multipart" | "text" | "binary";
|
|
572
|
+
/** Per-field wire encoding (the media type's `encoding`): a multipart
|
|
573
|
+
* part's Content-Type, or the delimiter of an unexploded form array. */
|
|
574
|
+
bodyEncoding?: Record<string, { contentType?: string; delimiter?: string }>;
|
|
575
|
+
/** Multipart fields that must hold a Blob or File (or an array of them). */
|
|
576
|
+
fileFields?: string[];
|
|
333
577
|
/** Status matcher -> generated error class ("404", "4XX", "default"). */
|
|
334
578
|
errors?: Record<string, ErrorCtor>;
|
|
335
579
|
/** Idempotent requests are retried automatically. */
|
|
@@ -339,13 +583,16 @@ export interface CoreRequest {
|
|
|
339
583
|
idempotencyKey?: string;
|
|
340
584
|
/** Key into the schemas table for optional runtime validation. */
|
|
341
585
|
schemaKey?: string;
|
|
342
|
-
/**
|
|
586
|
+
/** The success body's envelope flag (`ok`, `success`): `false` there is a
|
|
587
|
+
* failure reported inside a 2xx response, raised as a PayloadError. */
|
|
588
|
+
failureFlag?: string;
|
|
589
|
+
/** Operation-level retry policy from the API definition, merged over the
|
|
343
590
|
* client-level policy; per-call options.maxRetries still wins. */
|
|
344
591
|
retry?: RetryPolicy;
|
|
345
592
|
options?: RequestOptions;
|
|
346
593
|
}
|
|
347
594
|
|
|
348
|
-
/** Tunable retry behavior (
|
|
595
|
+
/** Tunable retry behavior (defaults preserved when unset). */
|
|
349
596
|
export interface RetryPolicy {
|
|
350
597
|
maxRetries?: number;
|
|
351
598
|
/** Replaces the default retryable set (408, 429, 500, 502, 503, 504). */
|
|
@@ -412,13 +659,18 @@ export interface CoreConfig {
|
|
|
412
659
|
* spec's schemas. Zero-dependency: the validator lives in this file and
|
|
413
660
|
* the schema table in schemas.ts. */
|
|
414
661
|
validate?: { requests: boolean; responses: boolean; mode: "throw" | "warn" };
|
|
415
|
-
/**
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
662
|
+
/** Loads the per-operation schema table, keyed "resource.method", and the
|
|
663
|
+
* shared component definitions it references (see schemas.ts). Called on
|
|
664
|
+
* the first validated request, so the table stays out of the startup path
|
|
665
|
+
* and, in a bundler that splits dynamic imports, out of the main bundle. */
|
|
666
|
+
loadSchemas?: () => Promise<SchemaTables>;
|
|
667
|
+
/** API-wide retry policy from the API definition. */
|
|
420
668
|
retry?: RetryPolicy;
|
|
421
|
-
/**
|
|
669
|
+
/** The longest server-requested wait (Retry-After, x-ratelimit-reset) a
|
|
670
|
+
* retry honors. A longer wait fails the call at once with the reset time
|
|
671
|
+
* instead of sleeping. Default 60000ms. */
|
|
672
|
+
maxRetryWaitMs?: number;
|
|
673
|
+
/** Client-level values for global parameters, by wire name. */
|
|
422
674
|
globals?: Record<string, unknown>;
|
|
423
675
|
/** Header names the spec's API-key schemes use. fetch drops the standard
|
|
424
676
|
* credential headers on a cross-origin redirect but knows nothing of these,
|
|
@@ -465,8 +717,15 @@ function isStreamBody(body: unknown): boolean {
|
|
|
465
717
|
return typeof (body as { getReader?: unknown } | undefined)?.getReader === "function";
|
|
466
718
|
}
|
|
467
719
|
|
|
720
|
+
/** The validation tables schemas.ts exports. */
|
|
721
|
+
export interface SchemaTables {
|
|
722
|
+
SCHEMAS: Record<string, { req?: unknown; res?: unknown }>;
|
|
723
|
+
DEFS: Record<string, unknown>;
|
|
724
|
+
}
|
|
725
|
+
|
|
468
726
|
export class HttpCore {
|
|
469
727
|
readonly config: CoreConfig;
|
|
728
|
+
private schemaTables?: Promise<SchemaTables>;
|
|
470
729
|
/** Lowercased header names dropped on a cross-origin hop. */
|
|
471
730
|
private readonly sensitiveHeaders: string[];
|
|
472
731
|
/** Whether this runtime follows redirects itself instead of letting the
|
|
@@ -487,7 +746,7 @@ export class HttpCore {
|
|
|
487
746
|
this.manualRedirects = custom.length > 0 && (globalThis as { document?: unknown }).document === undefined;
|
|
488
747
|
}
|
|
489
748
|
|
|
490
|
-
/** The client-level value for
|
|
749
|
+
/** The client-level value for a global parameter. */
|
|
491
750
|
globalValue(name: string): unknown {
|
|
492
751
|
return this.config.globals?.[name];
|
|
493
752
|
}
|
|
@@ -514,11 +773,18 @@ export class HttpCore {
|
|
|
514
773
|
? crypto.randomUUID()
|
|
515
774
|
: undefined;
|
|
516
775
|
|
|
517
|
-
const
|
|
776
|
+
const tables = this.config.validate && req.schemaKey && this.config.loadSchemas
|
|
777
|
+
? await (this.schemaTables ??= this.config.loadSchemas())
|
|
778
|
+
: undefined;
|
|
779
|
+
const opSchemas = tables && req.schemaKey ? tables.SCHEMAS[req.schemaKey] : undefined;
|
|
518
780
|
if (opSchemas?.req && this.config.validate!.requests
|
|
519
781
|
&& req.body !== undefined && (req.bodyKind ?? "json") === "json") {
|
|
520
782
|
const violations: Violation[] = [];
|
|
521
|
-
|
|
783
|
+
// A GraphQL body is the {query, variables} envelope; the schema
|
|
784
|
+
// describes the variables.
|
|
785
|
+
let validated: unknown = req.body;
|
|
786
|
+
let label = "body";
|
|
787
|
+
validateAgainstSchema(validated, opSchemas.req, label, violations, tables!.DEFS);
|
|
522
788
|
if (violations.length > 0) {
|
|
523
789
|
const validationError = new ValidationError("request", violations);
|
|
524
790
|
if (this.config.validate!.mode === "warn") {
|
|
@@ -532,6 +798,22 @@ export class HttpCore {
|
|
|
532
798
|
}
|
|
533
799
|
|
|
534
800
|
let lastError: unknown;
|
|
801
|
+
let credentialsRefreshed = false;
|
|
802
|
+
// With an automatic Idempotency-Key, a retry that the API refuses as a
|
|
803
|
+
// duplicate still in progress (409/429) is about our own first attempt.
|
|
804
|
+
// Report that original failure instead, with its request id.
|
|
805
|
+
let original: { error: E; response?: ResponseMeta } | undefined;
|
|
806
|
+
const errorFor = (response: Response, body: unknown, responseMeta: ResponseMeta, limit: RateLimitInfo | undefined): E => {
|
|
807
|
+
// A documented status keeps its own class; a family status (404,
|
|
808
|
+
// 429, 5xx) raises the family class declared or not, so one catch
|
|
809
|
+
// covers it; ranges and default cover the rest.
|
|
810
|
+
const Ctor = limit ? RateLimitError
|
|
811
|
+
: req.errors?.[String(response.status)] ??
|
|
812
|
+
statusFamily(response.status) ??
|
|
813
|
+
req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
|
|
814
|
+
req.errors?.["default"];
|
|
815
|
+
return (Ctor ? new Ctor(body, responseMeta) : new UnexpectedApiError(response.status, body, responseMeta)) as unknown as E;
|
|
816
|
+
};
|
|
535
817
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
536
818
|
let response: Response;
|
|
537
819
|
const attemptStarted = Date.now();
|
|
@@ -541,15 +823,16 @@ export class HttpCore {
|
|
|
541
823
|
status: value.status,
|
|
542
824
|
durationMs: Date.now() - attemptStarted,
|
|
543
825
|
attempt: attempt + 1,
|
|
544
|
-
requestId:
|
|
545
|
-
requestIdFromBody(body) ??
|
|
546
|
-
value.headers.get("request-id") ??
|
|
547
|
-
value.headers.get("x-request-id") ??
|
|
548
|
-
undefined,
|
|
826
|
+
requestId: requestIdFromBody(body) ?? requestIdFromHeaders(value.headers),
|
|
549
827
|
});
|
|
550
828
|
try {
|
|
551
829
|
response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
|
|
552
830
|
} catch (cause) {
|
|
831
|
+
if (cause instanceof CredentialCallbackFailure) {
|
|
832
|
+
const error = cause.error as E;
|
|
833
|
+
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
834
|
+
return { ok: false, error };
|
|
835
|
+
}
|
|
553
836
|
lastError = cause;
|
|
554
837
|
this.config.debug?.({
|
|
555
838
|
method: req.method,
|
|
@@ -559,17 +842,30 @@ export class HttpCore {
|
|
|
559
842
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
560
843
|
});
|
|
561
844
|
if (attempt < maxRetries && retryAllowed && !req.options?.signal?.aborted) {
|
|
845
|
+
if (autoIdempotencyKey) original = { error: new TransportError(transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause), cause, undefined, transportFailure(cause)) as unknown as E };
|
|
562
846
|
await sleep(backoff(attempt, policy));
|
|
563
847
|
continue;
|
|
564
848
|
}
|
|
565
849
|
const error = new TransportError(
|
|
566
850
|
transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause),
|
|
567
851
|
cause,
|
|
852
|
+
undefined,
|
|
853
|
+
transportFailure(cause, req.options?.signal),
|
|
568
854
|
) as unknown as E;
|
|
569
855
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
570
856
|
return { ok: false, error };
|
|
571
857
|
}
|
|
572
858
|
|
|
859
|
+
// A conditional request matched: nothing changed. Raised so a method
|
|
860
|
+
// keeps its non-null type, but not a failure: onError is not called.
|
|
861
|
+
if (response.status === 304) {
|
|
862
|
+
void response.body?.cancel().catch(() => {});
|
|
863
|
+
emitResponseDebug(response);
|
|
864
|
+
const notModified = { ...meta(response), notModified: true };
|
|
865
|
+
req.options?.onResponse?.(notModified);
|
|
866
|
+
return { ok: false, error: new NotModifiedError(notModified) as unknown as E, response: notModified };
|
|
867
|
+
}
|
|
868
|
+
|
|
573
869
|
if (response.ok) {
|
|
574
870
|
let data: T;
|
|
575
871
|
try {
|
|
@@ -577,21 +873,32 @@ export class HttpCore {
|
|
|
577
873
|
} catch (cause) {
|
|
578
874
|
const parseError = cause instanceof ResponseParseError ? cause : undefined;
|
|
579
875
|
emitResponseDebug(response, parseError?.body);
|
|
876
|
+
req.options?.onResponse?.(parseError?.response ?? meta(response));
|
|
580
877
|
const error = (parseError ?? new TransportError(
|
|
581
878
|
"The response body read failed before completing",
|
|
582
879
|
cause,
|
|
583
880
|
meta(response),
|
|
881
|
+
transportFailure(cause, req.options?.signal),
|
|
584
882
|
)) as unknown as E;
|
|
585
883
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
586
884
|
return { ok: false, error, response: parseError?.response ?? meta(response) };
|
|
587
885
|
}
|
|
588
886
|
emitResponseDebug(response, data);
|
|
589
887
|
const responseMeta = meta(response, data);
|
|
888
|
+
req.options?.onResponse?.(responseMeta);
|
|
590
889
|
let responseData: unknown = data;
|
|
890
|
+
// Checked before validation: a failure body rarely matches the
|
|
891
|
+
// success schema, and the failure is the news.
|
|
892
|
+
if (req.failureFlag && data && typeof data === "object" && !Array.isArray(data)
|
|
893
|
+
&& (data as Record<string, unknown>)[req.failureFlag] === false) {
|
|
894
|
+
const payloadError = new PayloadError(data, responseMeta) as unknown as E;
|
|
895
|
+
await this.config.onError?.(payloadError, { method: req.method, path: req.path });
|
|
896
|
+
return { ok: false, error: payloadError, response: responseMeta };
|
|
897
|
+
}
|
|
591
898
|
let shouldValidateResponse = responseData !== undefined;
|
|
592
899
|
if (opSchemas?.res && this.config.validate!.responses && shouldValidateResponse) {
|
|
593
900
|
const violations: Violation[] = [];
|
|
594
|
-
validateAgainstSchema(responseData, opSchemas.res, "response", violations,
|
|
901
|
+
validateAgainstSchema(responseData, opSchemas.res, "response", violations, tables!.DEFS);
|
|
595
902
|
if (violations.length > 0) {
|
|
596
903
|
const validationError = new ValidationError("response", violations);
|
|
597
904
|
if (this.config.validate!.mode === "warn") {
|
|
@@ -607,12 +914,23 @@ export class HttpCore {
|
|
|
607
914
|
}
|
|
608
915
|
|
|
609
916
|
// 429 is safe to retry regardless of idempotency; other retryable
|
|
610
|
-
// statuses only when the verb is idempotent.
|
|
917
|
+
// statuses, and a rate-limited 403, only when the verb is idempotent.
|
|
918
|
+
const limit = rateLimitInfo(response.status, response.headers);
|
|
611
919
|
const retryableStatus =
|
|
612
|
-
retryableStatuses.has(response.status) &&
|
|
920
|
+
(retryableStatuses.has(response.status) || (limit !== undefined && response.status === 403)) &&
|
|
613
921
|
(retryAllowed || response.status === 429);
|
|
614
|
-
|
|
615
|
-
|
|
922
|
+
// A server-requested wait beyond the ceiling fails now, with the reset
|
|
923
|
+
// time in the error, rather than holding the caller.
|
|
924
|
+
const requestedWait = limit?.retryAfterMs ?? retryAfterMs(response);
|
|
925
|
+
const withinCeiling = requestedWait === undefined || requestedWait <= (this.config.maxRetryWaitMs ?? 60_000);
|
|
926
|
+
if (original && (response.status === 409 || response.status === 429)) {
|
|
927
|
+
void response.body?.cancel().catch(() => {});
|
|
928
|
+
emitResponseDebug(response);
|
|
929
|
+
await this.config.onError?.(original.error, { method: req.method, path: req.path });
|
|
930
|
+
return { ok: false, error: original.error, ...(original.response ? { response: original.response } : {}) };
|
|
931
|
+
}
|
|
932
|
+
if (attempt < maxRetries && retryableStatus && withinCeiling) {
|
|
933
|
+
const delay = requestedWait ?? backoff(attempt, policy);
|
|
616
934
|
let retryBody: unknown;
|
|
617
935
|
try {
|
|
618
936
|
retryBody = await parseBody(response, req.method);
|
|
@@ -620,10 +938,24 @@ export class HttpCore {
|
|
|
620
938
|
retryBody = undefined;
|
|
621
939
|
}
|
|
622
940
|
emitResponseDebug(response, retryBody);
|
|
941
|
+
if (autoIdempotencyKey && response.status >= 500) {
|
|
942
|
+
const failedMeta = meta(response, retryBody);
|
|
943
|
+
original = { error: errorFor(response, retryBody, failedMeta, limit), response: failedMeta };
|
|
944
|
+
}
|
|
623
945
|
await sleep(delay);
|
|
624
946
|
continue;
|
|
625
947
|
}
|
|
626
948
|
|
|
949
|
+
// A rejected callback or cached token gets one fresh resolution; the
|
|
950
|
+
// resend does not spend the retry budget. Static credentials do not.
|
|
951
|
+
if (response.status === 401 && !credentialsRefreshed && this.refreshCredentials(req)) {
|
|
952
|
+
credentialsRefreshed = true;
|
|
953
|
+
void response.body?.cancel().catch(() => {});
|
|
954
|
+
emitResponseDebug(response);
|
|
955
|
+
attempt--;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
627
959
|
let body: unknown;
|
|
628
960
|
try {
|
|
629
961
|
body = await parseBody(response, req.method);
|
|
@@ -632,13 +964,10 @@ export class HttpCore {
|
|
|
632
964
|
}
|
|
633
965
|
emitResponseDebug(response, body);
|
|
634
966
|
const responseMeta = meta(response, body);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const error = (Ctor
|
|
640
|
-
? new Ctor(body, responseMeta)
|
|
641
|
-
: new UnexpectedApiError(response.status, body, responseMeta)) as unknown as E;
|
|
967
|
+
req.options?.onResponse?.(responseMeta);
|
|
968
|
+
// A rate limit is its own error whatever the status: a 403 that means
|
|
969
|
+
// "wait" must not read as "your credential lacks access".
|
|
970
|
+
const error = errorFor(response, body, responseMeta, limit);
|
|
642
971
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
643
972
|
return { ok: false, error, response: responseMeta };
|
|
644
973
|
}
|
|
@@ -649,6 +978,19 @@ export class HttpCore {
|
|
|
649
978
|
return { ok: false, error };
|
|
650
979
|
}
|
|
651
980
|
|
|
981
|
+
/** Drop cached tokens behind the credential this request selected. True
|
|
982
|
+
* when a callback or cached token can resolve to a new value. */
|
|
983
|
+
private refreshCredentials(req: CoreRequest): boolean {
|
|
984
|
+
const selected = req.security && this.config.credentials ? selectSecurity(req.security, this.config.credentials) : {};
|
|
985
|
+
let refreshable = false;
|
|
986
|
+
for (const value of [...Object.values(selected.headers ?? {}), ...Object.values(selected.query ?? {})]) {
|
|
987
|
+
if (typeof value !== "function") continue;
|
|
988
|
+
refreshable = true;
|
|
989
|
+
markRejected(value as RefreshableAuth);
|
|
990
|
+
}
|
|
991
|
+
return refreshable;
|
|
992
|
+
}
|
|
993
|
+
|
|
652
994
|
/** SDK-facing call: resolve to the payload or throw its typed error. */
|
|
653
995
|
async requestData<T, E>(req: CoreRequest): Promise<T> {
|
|
654
996
|
return unwrap(await this.request<T, E>(req));
|
|
@@ -768,7 +1110,7 @@ export class HttpCore {
|
|
|
768
1110
|
|
|
769
1111
|
private async buildUrl(req: CoreRequest, authQuery?: Record<string, AuthValue>): Promise<string> {
|
|
770
1112
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
771
|
-
const url = new URL(base + req.path);
|
|
1113
|
+
const url = req.url !== undefined ? new URL(req.url, base) : new URL(base + req.path);
|
|
772
1114
|
for (const [k, v] of Object.entries(req.query ?? {})) {
|
|
773
1115
|
if (v === undefined || v === null) continue;
|
|
774
1116
|
if (Array.isArray(v)) {
|
|
@@ -785,8 +1127,37 @@ export class HttpCore {
|
|
|
785
1127
|
}
|
|
786
1128
|
}
|
|
787
1129
|
|
|
1130
|
+
/** A credential callback failed: surface the caller's own error unchanged
|
|
1131
|
+
* instead of retrying it as a transport failure. */
|
|
1132
|
+
class CredentialCallbackFailure {
|
|
1133
|
+
readonly error: unknown;
|
|
1134
|
+
constructor(error: unknown) { this.error = error; }
|
|
1135
|
+
}
|
|
1136
|
+
|
|
788
1137
|
async function resolveAuthValue(value: AuthValue): Promise<string> {
|
|
789
|
-
|
|
1138
|
+
if (typeof value !== "function") return value;
|
|
1139
|
+
try {
|
|
1140
|
+
return await callCredential(value);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
// The SDK's own token request failures keep the transport retry path.
|
|
1143
|
+
if (error instanceof TransportError) throw error;
|
|
1144
|
+
throw new CredentialCallbackFailure(error);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** A refreshable credential: a callback, or a cached client-credentials
|
|
1149
|
+
* token that can be dropped before one resend after a 401. */
|
|
1150
|
+
type RefreshableAuth = ((context: CredentialContext) => string | Promise<string>) & { invalidate?: () => void };
|
|
1151
|
+
|
|
1152
|
+
/** Callbacks without their own invalidate learn of a 401 on their next call. */
|
|
1153
|
+
const rejectedCallbacks = new WeakSet<object>();
|
|
1154
|
+
function markRejected(value: RefreshableAuth): void {
|
|
1155
|
+
if (value.invalidate) value.invalidate();
|
|
1156
|
+
else rejectedCallbacks.add(value);
|
|
1157
|
+
}
|
|
1158
|
+
function callCredential(value: RefreshableAuth): string | Promise<string> {
|
|
1159
|
+
const rejected = rejectedCallbacks.delete(value);
|
|
1160
|
+
return value({ rejected });
|
|
790
1161
|
}
|
|
791
1162
|
|
|
792
1163
|
|
|
@@ -803,12 +1174,35 @@ export function formatDebugEvent(name: string, event: DebugEvent): string {
|
|
|
803
1174
|
/** Wrap a bearer credential (static or callback) as an Authorization value. */
|
|
804
1175
|
export function bearerAuth(token: AuthValue): AuthValue {
|
|
805
1176
|
if (typeof token === "function") {
|
|
806
|
-
return async () => "Bearer " + (await token());
|
|
1177
|
+
return Object.assign(async () => "Bearer " + (await callCredential(token)), { invalidate: () => markRejected(token) });
|
|
807
1178
|
}
|
|
808
1179
|
return "Bearer " + token;
|
|
809
1180
|
}
|
|
810
1181
|
|
|
811
1182
|
|
|
1183
|
+
/** The media type a local file is uploaded as, from its extension. A
|
|
1184
|
+
* Worker module (.mjs) must arrive as application/javascript+module. */
|
|
1185
|
+
export function mediaTypeForPath(path: string): string {
|
|
1186
|
+
const extension = /\.([A-Za-z0-9]+)$/.exec(path)?.[1]?.toLowerCase() ?? "";
|
|
1187
|
+
const types: Record<string, string> = {
|
|
1188
|
+
mjs: "application/javascript+module", js: "application/javascript", cjs: "application/javascript", wasm: "application/wasm",
|
|
1189
|
+
json: "application/json", jsonl: "application/jsonl", txt: "text/plain", md: "text/markdown", csv: "text/csv", html: "text/html",
|
|
1190
|
+
xml: "application/xml", yaml: "application/yaml", yml: "application/yaml", pdf: "application/pdf", zip: "application/zip",
|
|
1191
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
1192
|
+
mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", flac: "audio/flac", m4a: "audio/mp4", mp4: "video/mp4", webm: "video/webm",
|
|
1193
|
+
};
|
|
1194
|
+
return types[extension] ?? "application/octet-stream";
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Join a query array into one delimited value (`ids=1,2`) for parameters
|
|
1199
|
+
* whose spec says `explode: false`. Other values pass through unchanged.
|
|
1200
|
+
*/
|
|
1201
|
+
export function delimited(value: unknown, separator: string): unknown {
|
|
1202
|
+
if (!Array.isArray(value)) return value;
|
|
1203
|
+
return value.map((item) => (item instanceof Date ? item.toISOString() : String(item))).join(separator);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
812
1206
|
/**
|
|
813
1207
|
* Bracket-style deep encoding shared by query strings and form bodies:
|
|
814
1208
|
* { created: { gte: 5 } } -> created[gte]=5, { items: [{ id: "x" }] } ->
|
|
@@ -827,6 +1221,7 @@ function appendDeep(target: URLSearchParams, key: string, value: unknown): void
|
|
|
827
1221
|
}
|
|
828
1222
|
}
|
|
829
1223
|
|
|
1224
|
+
|
|
830
1225
|
function serializeBody(req: CoreRequest): { body: NonNullable<RequestInit["body"]> | undefined; contentType?: string } {
|
|
831
1226
|
if (req.body === undefined) return { body: undefined };
|
|
832
1227
|
switch (req.bodyKind ?? "json") {
|
|
@@ -870,15 +1265,17 @@ function requestIdFromBody(body: unknown): string | undefined {
|
|
|
870
1265
|
}
|
|
871
1266
|
|
|
872
1267
|
function meta(response: Response, body?: unknown): ResponseMeta {
|
|
1268
|
+
const etag = response.headers.get("etag");
|
|
1269
|
+
const lastModified = response.headers.get("last-modified");
|
|
1270
|
+
const rateLimit = rateLimitInfo(response.status, response.headers);
|
|
873
1271
|
return {
|
|
874
1272
|
status: response.status,
|
|
875
1273
|
headers: response.headers,
|
|
876
1274
|
...(body !== undefined ? { rawBody: body } : {}),
|
|
877
|
-
requestId:
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
undefined,
|
|
1275
|
+
requestId: requestIdFromBody(body) ?? requestIdFromHeaders(response.headers),
|
|
1276
|
+
...(etag ? { etag } : {}),
|
|
1277
|
+
...(lastModified ? { lastModified } : {}),
|
|
1278
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
882
1279
|
};
|
|
883
1280
|
}
|
|
884
1281
|
|
|
@@ -906,13 +1303,14 @@ function composeSignals(signals: AbortSignal[]): AbortSignal {
|
|
|
906
1303
|
return controller.signal;
|
|
907
1304
|
}
|
|
908
1305
|
|
|
1306
|
+
/** Retry-After on a retryable status that is not a rate limit (a 503). */
|
|
909
1307
|
function retryAfterMs(response: Response): number | undefined {
|
|
910
1308
|
const header = response.headers.get("retry-after");
|
|
911
1309
|
if (!header) return undefined;
|
|
912
1310
|
const seconds = Number(header);
|
|
913
|
-
if (Number.isFinite(seconds)) return Math.
|
|
1311
|
+
if (Number.isFinite(seconds)) return Math.max(seconds * 1000, 0);
|
|
914
1312
|
const date = Date.parse(header);
|
|
915
|
-
if (!Number.isNaN(date)) return Math.
|
|
1313
|
+
if (!Number.isNaN(date)) return Math.max(date - Date.now(), 0);
|
|
916
1314
|
return undefined;
|
|
917
1315
|
}
|
|
918
1316
|
|
|
@@ -929,7 +1327,8 @@ function sleep(ms: number): Promise<void> {
|
|
|
929
1327
|
}
|
|
930
1328
|
|
|
931
1329
|
export function toBase64(input: string): string {
|
|
932
|
-
|
|
1330
|
+
// Encode UTF-8 bytes: btoa alone rejects characters outside Latin-1.
|
|
1331
|
+
if (typeof btoa === "function") return btoa(Array.from(new TextEncoder().encode(input), (byte) => String.fromCharCode(byte)).join(""));
|
|
933
1332
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
934
1333
|
return (globalThis as any).Buffer.from(input, "utf-8").toString("base64");
|
|
935
1334
|
}
|