@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/dist/core/http.js
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
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
|
+
/** Response headers that carry a request identifier, most specific first. */
|
|
6
|
+
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"];
|
|
7
|
+
function requestIdFromHeaders(headers) {
|
|
8
|
+
for (const name of REQUEST_ID_HEADERS) {
|
|
9
|
+
const value = headers.get(name);
|
|
10
|
+
if (value)
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether a response is a rate limit, and when to retry. A 429 always is. A
|
|
17
|
+
* 403 is when it says the quota is spent (`x-ratelimit-remaining: 0`) or asks
|
|
18
|
+
* the caller to wait (`Retry-After`), which is how GitHub signals its primary
|
|
19
|
+
* and secondary limits. Undefined for every other response.
|
|
20
|
+
*/
|
|
21
|
+
export function rateLimitInfo(status, headers, now = Date.now()) {
|
|
22
|
+
const retryAfter = headers.get("retry-after");
|
|
23
|
+
const remaining = headers.get("x-ratelimit-remaining") ?? headers.get("ratelimit-remaining");
|
|
24
|
+
const limited = status === 429 || (status === 403 && (retryAfter !== null || (remaining !== null && remaining.trim() === "0")));
|
|
25
|
+
if (!limited)
|
|
26
|
+
return undefined;
|
|
27
|
+
let waitMs;
|
|
28
|
+
if (retryAfter !== null && retryAfter.trim() !== "") {
|
|
29
|
+
const seconds = Number(retryAfter);
|
|
30
|
+
if (Number.isFinite(seconds))
|
|
31
|
+
waitMs = Math.max(seconds, 0) * 1000;
|
|
32
|
+
else if (!Number.isNaN(Date.parse(retryAfter)))
|
|
33
|
+
waitMs = Math.max(Date.parse(retryAfter) - now, 0);
|
|
34
|
+
}
|
|
35
|
+
const reset = headers.get("x-ratelimit-reset") ?? headers.get("ratelimit-reset");
|
|
36
|
+
if (waitMs === undefined && reset !== null && reset.trim() !== "" && Number.isFinite(Number(reset))) {
|
|
37
|
+
const value = Number(reset);
|
|
38
|
+
// An epoch timestamp (GitHub, Twitter) or seconds until the reset (the
|
|
39
|
+
// IETF RateLimit header fields).
|
|
40
|
+
waitMs = Math.max(value > 1e9 ? value * 1000 - now : value * 1000, 0);
|
|
41
|
+
}
|
|
42
|
+
if (waitMs === undefined)
|
|
43
|
+
return {};
|
|
44
|
+
return { retryAt: new Date(now + waitMs), retryAfterMs: waitMs };
|
|
45
|
+
}
|
|
46
|
+
function rateLimitStep(info) {
|
|
47
|
+
if (!info.retryAt)
|
|
48
|
+
return "Rate limited: wait before retrying.";
|
|
49
|
+
// Round up to the second so "wait until" is never early.
|
|
50
|
+
const at = new Date(Math.ceil(info.retryAt.getTime() / 1000) * 1000);
|
|
51
|
+
return "Rate limited: wait until " + at.toISOString().replace(/\.\d{3}Z$/, "Z") + ", then retry.";
|
|
52
|
+
}
|
|
5
53
|
/** Collapse a result into its data, throwing the typed error when !ok. */
|
|
6
54
|
export function unwrap(result) {
|
|
7
55
|
if (result.ok)
|
|
@@ -45,25 +93,46 @@ export class SdkError extends Error {
|
|
|
45
93
|
this.requestId = requestId;
|
|
46
94
|
}
|
|
47
95
|
}
|
|
96
|
+
function errorFields(body) {
|
|
97
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
98
|
+
return {};
|
|
99
|
+
const value = body;
|
|
100
|
+
const nested = value.error && typeof value.error === "object" && !Array.isArray(value.error) ? value.error : undefined;
|
|
101
|
+
const first = Array.isArray(value.errors) && value.errors[0] && typeof value.errors[0] === "object" ? value.errors[0] : undefined;
|
|
102
|
+
return { error: nested, value, first };
|
|
103
|
+
}
|
|
104
|
+
/** The API's own error code: `error.code`, `code`, `error.type`, then
|
|
105
|
+
* `errors[0].code`; numeric codes (Twilio's 20404) as strings. */
|
|
48
106
|
function errorCode(body, fallback) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
const { error, value, first } = errorFields(body);
|
|
108
|
+
if (!value)
|
|
109
|
+
return fallback;
|
|
110
|
+
for (const code of [error?.code, value.code, error?.type, first?.code]) {
|
|
53
111
|
if (typeof code === "string" && code.trim())
|
|
54
112
|
return code;
|
|
113
|
+
if (typeof code === "number" && Number.isFinite(code))
|
|
114
|
+
return String(code);
|
|
55
115
|
}
|
|
116
|
+
// Slack-style `{"ok": false, "error": "invalid_auth"}`: the error is a code.
|
|
117
|
+
if (typeof value.error === "string" && /^[A-Za-z][\w.-]{0,63}$/.test(value.error))
|
|
118
|
+
return value.error;
|
|
56
119
|
return fallback;
|
|
57
120
|
}
|
|
121
|
+
/** The API's own message: `error.message`, `message`, `errors[0].message`,
|
|
122
|
+
* `detail`, `error_description`, then a string `error`. */
|
|
58
123
|
function errorDetail(body) {
|
|
59
|
-
|
|
124
|
+
const { error, value, first } = errorFields(body);
|
|
125
|
+
if (!value)
|
|
60
126
|
return "";
|
|
61
|
-
const value
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
127
|
+
for (const detail of [error?.message, value.message, first?.message, value.detail, value.error_description, value.error]) {
|
|
128
|
+
if (typeof detail === "string" && detail.trim())
|
|
129
|
+
return detail.trim();
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
65
132
|
}
|
|
66
133
|
function nextStep(status) {
|
|
134
|
+
if (status >= 200 && status < 300)
|
|
135
|
+
return "It arrived in a successful HTTP response; the body says why.";
|
|
67
136
|
if (status === 401)
|
|
68
137
|
return "Check the credential and retry.";
|
|
69
138
|
if (status === 403)
|
|
@@ -72,6 +141,8 @@ function nextStep(status) {
|
|
|
72
141
|
return "Check the requested identifier or path.";
|
|
73
142
|
if (status === 409)
|
|
74
143
|
return "Refresh the resource and retry the change.";
|
|
144
|
+
if (status === 413)
|
|
145
|
+
return "Send less data in one request.";
|
|
75
146
|
if (status === 422 || status === 400)
|
|
76
147
|
return "Correct the request and retry.";
|
|
77
148
|
if (status === 429)
|
|
@@ -80,22 +151,131 @@ function nextStep(status) {
|
|
|
80
151
|
return "Retry later; contact the API provider if this continues.";
|
|
81
152
|
return "Inspect the error body and correct the request before retrying.";
|
|
82
153
|
}
|
|
83
|
-
/** Base class for every HTTP error response.
|
|
154
|
+
/** Base class for every HTTP error response. The message leads with the
|
|
155
|
+
* status and the API's own message ("HTTP 404: No such customer"), then the
|
|
156
|
+
* next step. `code` is the API's error code when the body names one. */
|
|
84
157
|
export class ApiError extends SdkError {
|
|
85
158
|
status;
|
|
86
159
|
body;
|
|
87
160
|
response;
|
|
88
|
-
|
|
89
|
-
|
|
161
|
+
/** Set when the API said this call was rate limited (a 429, or a 403
|
|
162
|
+
* whose headers say the quota is spent): when to retry. */
|
|
163
|
+
rateLimit;
|
|
164
|
+
/** `summary` leads the message ("HTTP 404"); the API's message follows it. */
|
|
165
|
+
constructor(summary, status, body, response, own) {
|
|
166
|
+
const limit = response.rateLimit ?? (response.headers ? rateLimitInfo(status, response.headers) : undefined);
|
|
167
|
+
const detail = errorDetail(body);
|
|
168
|
+
super(
|
|
169
|
+
// An API message usually ends its own sentence; do not double it.
|
|
170
|
+
own?.message ?? (summary + (detail ? ": " + detail : "")).replace(/[.!?]+$/, "") + ". " + (limit ? rateLimitStep(limit) : nextStep(status)), own?.code ?? errorCode(body, limit ? "rate_limited" : "http_" + status), status, body, response.requestId);
|
|
90
171
|
this.status = status;
|
|
91
172
|
this.body = body;
|
|
92
173
|
this.response = response;
|
|
174
|
+
if (limit)
|
|
175
|
+
this.rateLimit = limit;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** HTTP 400: the API rejected the request as malformed. Raised for every
|
|
179
|
+
* 400, documented or not; `body` is typed where the operation declares it. */
|
|
180
|
+
export class BadRequestError extends ApiError {
|
|
181
|
+
constructor(body, response) {
|
|
182
|
+
super("HTTP 400", 400, body, response);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** HTTP 401: the credential is missing, invalid or expired. */
|
|
186
|
+
export class UnauthorizedError extends ApiError {
|
|
187
|
+
constructor(body, response) {
|
|
188
|
+
super("HTTP 401", 401, body, response);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** HTTP 403: the credential lacks access. A 403 that signals a rate limit
|
|
192
|
+
* raises RateLimitError instead. */
|
|
193
|
+
export class ForbiddenError extends ApiError {
|
|
194
|
+
constructor(body, response) {
|
|
195
|
+
super("HTTP 403", 403, body, response);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** HTTP 404: the resource or path does not exist. */
|
|
199
|
+
export class NotFoundError extends ApiError {
|
|
200
|
+
constructor(body, response) {
|
|
201
|
+
super("HTTP 404", 404, body, response);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** HTTP 409: the request conflicts with the resource's current state. */
|
|
205
|
+
export class ConflictError extends ApiError {
|
|
206
|
+
constructor(body, response) {
|
|
207
|
+
super("HTTP 409", 409, body, response);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** HTTP 422: the request was well formed but failed validation. */
|
|
211
|
+
export class UnprocessableEntityError extends ApiError {
|
|
212
|
+
constructor(body, response) {
|
|
213
|
+
super("HTTP 422", 422, body, response);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** The API rate limited the call: every 429, and a 403 whose headers say
|
|
217
|
+
* the quota is spent. `rateLimit.retryAt` says when to try again. */
|
|
218
|
+
export class RateLimitError extends ApiError {
|
|
219
|
+
constructor(body, response) {
|
|
220
|
+
super("HTTP " + response.status, response.status, body, response);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Any 5xx: the API failed to handle a valid request. */
|
|
224
|
+
export class ServerError extends ApiError {
|
|
225
|
+
constructor(body, response) {
|
|
226
|
+
super("HTTP " + response.status, response.status, body, response);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** HTTP 304: a conditional request (`If-None-Match`, `If-Modified-Since`)
|
|
230
|
+
* matched, so the resource has not changed and the response has no body.
|
|
231
|
+
* Raised only when the caller sent a conditional header. `etag` and
|
|
232
|
+
* `lastModified` are the validators to send next time. */
|
|
233
|
+
export class NotModifiedError extends ApiError {
|
|
234
|
+
/** The response's `ETag` header. */
|
|
235
|
+
etag;
|
|
236
|
+
/** The response's `Last-Modified` header. */
|
|
237
|
+
lastModified;
|
|
238
|
+
constructor(response) {
|
|
239
|
+
super("HTTP 304", 304, undefined, response, {
|
|
240
|
+
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.",
|
|
241
|
+
code: "not_modified",
|
|
242
|
+
});
|
|
243
|
+
if (response.etag)
|
|
244
|
+
this.etag = response.etag;
|
|
245
|
+
if (response.lastModified)
|
|
246
|
+
this.lastModified = response.lastModified;
|
|
93
247
|
}
|
|
94
248
|
}
|
|
95
|
-
/**
|
|
249
|
+
/** The class raised for a status whatever the operation declares. */
|
|
250
|
+
function statusFamily(status) {
|
|
251
|
+
switch (status) {
|
|
252
|
+
case 400: return BadRequestError;
|
|
253
|
+
case 401: return UnauthorizedError;
|
|
254
|
+
case 403: return ForbiddenError;
|
|
255
|
+
case 404: return NotFoundError;
|
|
256
|
+
case 409: return ConflictError;
|
|
257
|
+
case 422: return UnprocessableEntityError;
|
|
258
|
+
case 429: return RateLimitError;
|
|
259
|
+
}
|
|
260
|
+
return status >= 500 && status < 600 ? ServerError : undefined;
|
|
261
|
+
}
|
|
262
|
+
/** A 2xx response whose payload reported a failure: a declared envelope
|
|
263
|
+
* flag set to false (`{"ok": false}`, `{"success": false}`), or a GraphQL
|
|
264
|
+
* result that is one of the schema's error types. */
|
|
265
|
+
export class PayloadError extends ApiError {
|
|
266
|
+
/** The GraphQL error type the result resolved to, for GraphQL operations. */
|
|
267
|
+
typename;
|
|
268
|
+
constructor(body, response, typename) {
|
|
269
|
+
super(typename ? "The operation returned " + typename : "The API reported a failure", response.status, body, response);
|
|
270
|
+
if (typename)
|
|
271
|
+
this.typename = typename;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** A status with no family class (a 402, 405 or 410, say) that the
|
|
275
|
+
* operation did not document. */
|
|
96
276
|
export class UnexpectedApiError extends ApiError {
|
|
97
277
|
constructor(status, body, response) {
|
|
98
|
-
super("
|
|
278
|
+
super("HTTP " + status, status, body, response);
|
|
99
279
|
}
|
|
100
280
|
}
|
|
101
281
|
/** A successful response declared JSON but carried a body that could not be parsed. */
|
|
@@ -267,12 +447,28 @@ function transportFailureMessage(method, url, cause) {
|
|
|
267
447
|
}
|
|
268
448
|
return method + " " + url + " failed: " + detail;
|
|
269
449
|
}
|
|
270
|
-
|
|
450
|
+
function transportFailure(cause, signal) {
|
|
451
|
+
if (signal?.aborted)
|
|
452
|
+
return "aborted";
|
|
453
|
+
let node = cause;
|
|
454
|
+
for (let depth = 0; depth < 5 && node !== null && typeof node === "object"; depth++) {
|
|
455
|
+
if (node.name === "TimeoutError")
|
|
456
|
+
return "timeout";
|
|
457
|
+
node = node.cause;
|
|
458
|
+
}
|
|
459
|
+
return "transport_error";
|
|
460
|
+
}
|
|
461
|
+
const TRANSPORT_NEXT_STEP = {
|
|
462
|
+
aborted: ". The caller's AbortSignal cancelled the request.",
|
|
463
|
+
timeout: ". The request timed out; retry, or raise timeoutMs.",
|
|
464
|
+
transport_error: ". Check the connection and retry.",
|
|
465
|
+
};
|
|
466
|
+
/** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body); `code` says which. */
|
|
271
467
|
export class TransportError extends SdkError {
|
|
272
468
|
cause;
|
|
273
469
|
response;
|
|
274
|
-
constructor(message, cause, response) {
|
|
275
|
-
super(message +
|
|
470
|
+
constructor(message, cause, response, failure = "transport_error") {
|
|
471
|
+
super(message + TRANSPORT_NEXT_STEP[failure], failure, response?.status ?? null, undefined, response?.requestId);
|
|
276
472
|
this.cause = cause;
|
|
277
473
|
this.response = response;
|
|
278
474
|
}
|
|
@@ -327,6 +523,7 @@ function isStreamBody(body) {
|
|
|
327
523
|
}
|
|
328
524
|
export class HttpCore {
|
|
329
525
|
config;
|
|
526
|
+
schemaTables;
|
|
330
527
|
/** Lowercased header names dropped on a cross-origin hop. */
|
|
331
528
|
sensitiveHeaders;
|
|
332
529
|
/** Whether this runtime follows redirects itself instead of letting the
|
|
@@ -345,7 +542,7 @@ export class HttpCore {
|
|
|
345
542
|
// the target's CORS consent anyway.
|
|
346
543
|
this.manualRedirects = custom.length > 0 && globalThis.document === undefined;
|
|
347
544
|
}
|
|
348
|
-
/** The client-level value for
|
|
545
|
+
/** The client-level value for a global parameter. */
|
|
349
546
|
globalValue(name) {
|
|
350
547
|
return this.config.globals?.[name];
|
|
351
548
|
}
|
|
@@ -367,11 +564,18 @@ export class HttpCore {
|
|
|
367
564
|
req.options?.headers?.[req.idempotencyKey] === undefined
|
|
368
565
|
? crypto.randomUUID()
|
|
369
566
|
: undefined;
|
|
370
|
-
const
|
|
567
|
+
const tables = this.config.validate && req.schemaKey && this.config.loadSchemas
|
|
568
|
+
? await (this.schemaTables ??= this.config.loadSchemas())
|
|
569
|
+
: undefined;
|
|
570
|
+
const opSchemas = tables && req.schemaKey ? tables.SCHEMAS[req.schemaKey] : undefined;
|
|
371
571
|
if (opSchemas?.req && this.config.validate.requests
|
|
372
572
|
&& req.body !== undefined && (req.bodyKind ?? "json") === "json") {
|
|
373
573
|
const violations = [];
|
|
374
|
-
|
|
574
|
+
// A GraphQL body is the {query, variables} envelope; the schema
|
|
575
|
+
// describes the variables.
|
|
576
|
+
let validated = req.body;
|
|
577
|
+
let label = "body";
|
|
578
|
+
validateAgainstSchema(validated, opSchemas.req, label, violations, tables.DEFS);
|
|
375
579
|
if (violations.length > 0) {
|
|
376
580
|
const validationError = new ValidationError("request", violations);
|
|
377
581
|
if (this.config.validate.mode === "warn") {
|
|
@@ -385,6 +589,22 @@ export class HttpCore {
|
|
|
385
589
|
}
|
|
386
590
|
}
|
|
387
591
|
let lastError;
|
|
592
|
+
let credentialsRefreshed = false;
|
|
593
|
+
// With an automatic Idempotency-Key, a retry that the API refuses as a
|
|
594
|
+
// duplicate still in progress (409/429) is about our own first attempt.
|
|
595
|
+
// Report that original failure instead, with its request id.
|
|
596
|
+
let original;
|
|
597
|
+
const errorFor = (response, body, responseMeta, limit) => {
|
|
598
|
+
// A documented status keeps its own class; a family status (404,
|
|
599
|
+
// 429, 5xx) raises the family class declared or not, so one catch
|
|
600
|
+
// covers it; ranges and default cover the rest.
|
|
601
|
+
const Ctor = limit ? RateLimitError
|
|
602
|
+
: req.errors?.[String(response.status)] ??
|
|
603
|
+
statusFamily(response.status) ??
|
|
604
|
+
req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
|
|
605
|
+
req.errors?.["default"];
|
|
606
|
+
return (Ctor ? new Ctor(body, responseMeta) : new UnexpectedApiError(response.status, body, responseMeta));
|
|
607
|
+
};
|
|
388
608
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
389
609
|
let response;
|
|
390
610
|
const attemptStarted = Date.now();
|
|
@@ -394,15 +614,17 @@ export class HttpCore {
|
|
|
394
614
|
status: value.status,
|
|
395
615
|
durationMs: Date.now() - attemptStarted,
|
|
396
616
|
attempt: attempt + 1,
|
|
397
|
-
requestId: requestIdFromBody(body) ??
|
|
398
|
-
value.headers.get("request-id") ??
|
|
399
|
-
value.headers.get("x-request-id") ??
|
|
400
|
-
undefined,
|
|
617
|
+
requestId: requestIdFromBody(body) ?? requestIdFromHeaders(value.headers),
|
|
401
618
|
});
|
|
402
619
|
try {
|
|
403
620
|
response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
|
|
404
621
|
}
|
|
405
622
|
catch (cause) {
|
|
623
|
+
if (cause instanceof CredentialCallbackFailure) {
|
|
624
|
+
const error = cause.error;
|
|
625
|
+
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
626
|
+
return { ok: false, error };
|
|
627
|
+
}
|
|
406
628
|
lastError = cause;
|
|
407
629
|
this.config.debug?.({
|
|
408
630
|
method: req.method,
|
|
@@ -412,13 +634,24 @@ export class HttpCore {
|
|
|
412
634
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
413
635
|
});
|
|
414
636
|
if (attempt < maxRetries && retryAllowed && !req.options?.signal?.aborted) {
|
|
637
|
+
if (autoIdempotencyKey)
|
|
638
|
+
original = { error: new TransportError(transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause), cause, undefined, transportFailure(cause)) };
|
|
415
639
|
await sleep(backoff(attempt, policy));
|
|
416
640
|
continue;
|
|
417
641
|
}
|
|
418
|
-
const error = new TransportError(transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause), cause);
|
|
642
|
+
const error = new TransportError(transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause), cause, undefined, transportFailure(cause, req.options?.signal));
|
|
419
643
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
420
644
|
return { ok: false, error };
|
|
421
645
|
}
|
|
646
|
+
// A conditional request matched: nothing changed. Raised so a method
|
|
647
|
+
// keeps its non-null type, but not a failure: onError is not called.
|
|
648
|
+
if (response.status === 304) {
|
|
649
|
+
void response.body?.cancel().catch(() => { });
|
|
650
|
+
emitResponseDebug(response);
|
|
651
|
+
const notModified = { ...meta(response), notModified: true };
|
|
652
|
+
req.options?.onResponse?.(notModified);
|
|
653
|
+
return { ok: false, error: new NotModifiedError(notModified), response: notModified };
|
|
654
|
+
}
|
|
422
655
|
if (response.ok) {
|
|
423
656
|
let data;
|
|
424
657
|
try {
|
|
@@ -427,17 +660,27 @@ export class HttpCore {
|
|
|
427
660
|
catch (cause) {
|
|
428
661
|
const parseError = cause instanceof ResponseParseError ? cause : undefined;
|
|
429
662
|
emitResponseDebug(response, parseError?.body);
|
|
430
|
-
|
|
663
|
+
req.options?.onResponse?.(parseError?.response ?? meta(response));
|
|
664
|
+
const error = (parseError ?? new TransportError("The response body read failed before completing", cause, meta(response), transportFailure(cause, req.options?.signal)));
|
|
431
665
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
432
666
|
return { ok: false, error, response: parseError?.response ?? meta(response) };
|
|
433
667
|
}
|
|
434
668
|
emitResponseDebug(response, data);
|
|
435
669
|
const responseMeta = meta(response, data);
|
|
670
|
+
req.options?.onResponse?.(responseMeta);
|
|
436
671
|
let responseData = data;
|
|
672
|
+
// Checked before validation: a failure body rarely matches the
|
|
673
|
+
// success schema, and the failure is the news.
|
|
674
|
+
if (req.failureFlag && data && typeof data === "object" && !Array.isArray(data)
|
|
675
|
+
&& data[req.failureFlag] === false) {
|
|
676
|
+
const payloadError = new PayloadError(data, responseMeta);
|
|
677
|
+
await this.config.onError?.(payloadError, { method: req.method, path: req.path });
|
|
678
|
+
return { ok: false, error: payloadError, response: responseMeta };
|
|
679
|
+
}
|
|
437
680
|
let shouldValidateResponse = responseData !== undefined;
|
|
438
681
|
if (opSchemas?.res && this.config.validate.responses && shouldValidateResponse) {
|
|
439
682
|
const violations = [];
|
|
440
|
-
validateAgainstSchema(responseData, opSchemas.res, "response", violations,
|
|
683
|
+
validateAgainstSchema(responseData, opSchemas.res, "response", violations, tables.DEFS);
|
|
441
684
|
if (violations.length > 0) {
|
|
442
685
|
const validationError = new ValidationError("response", violations);
|
|
443
686
|
if (this.config.validate.mode === "warn") {
|
|
@@ -453,11 +696,22 @@ export class HttpCore {
|
|
|
453
696
|
return { ok: true, data, response: responseMeta };
|
|
454
697
|
}
|
|
455
698
|
// 429 is safe to retry regardless of idempotency; other retryable
|
|
456
|
-
// statuses only when the verb is idempotent.
|
|
457
|
-
const
|
|
699
|
+
// statuses, and a rate-limited 403, only when the verb is idempotent.
|
|
700
|
+
const limit = rateLimitInfo(response.status, response.headers);
|
|
701
|
+
const retryableStatus = (retryableStatuses.has(response.status) || (limit !== undefined && response.status === 403)) &&
|
|
458
702
|
(retryAllowed || response.status === 429);
|
|
459
|
-
|
|
460
|
-
|
|
703
|
+
// A server-requested wait beyond the ceiling fails now, with the reset
|
|
704
|
+
// time in the error, rather than holding the caller.
|
|
705
|
+
const requestedWait = limit?.retryAfterMs ?? retryAfterMs(response);
|
|
706
|
+
const withinCeiling = requestedWait === undefined || requestedWait <= (this.config.maxRetryWaitMs ?? 60_000);
|
|
707
|
+
if (original && (response.status === 409 || response.status === 429)) {
|
|
708
|
+
void response.body?.cancel().catch(() => { });
|
|
709
|
+
emitResponseDebug(response);
|
|
710
|
+
await this.config.onError?.(original.error, { method: req.method, path: req.path });
|
|
711
|
+
return { ok: false, error: original.error, ...(original.response ? { response: original.response } : {}) };
|
|
712
|
+
}
|
|
713
|
+
if (attempt < maxRetries && retryableStatus && withinCeiling) {
|
|
714
|
+
const delay = requestedWait ?? backoff(attempt, policy);
|
|
461
715
|
let retryBody;
|
|
462
716
|
try {
|
|
463
717
|
retryBody = await parseBody(response, req.method);
|
|
@@ -466,9 +720,22 @@ export class HttpCore {
|
|
|
466
720
|
retryBody = undefined;
|
|
467
721
|
}
|
|
468
722
|
emitResponseDebug(response, retryBody);
|
|
723
|
+
if (autoIdempotencyKey && response.status >= 500) {
|
|
724
|
+
const failedMeta = meta(response, retryBody);
|
|
725
|
+
original = { error: errorFor(response, retryBody, failedMeta, limit), response: failedMeta };
|
|
726
|
+
}
|
|
469
727
|
await sleep(delay);
|
|
470
728
|
continue;
|
|
471
729
|
}
|
|
730
|
+
// A rejected callback or cached token gets one fresh resolution; the
|
|
731
|
+
// resend does not spend the retry budget. Static credentials do not.
|
|
732
|
+
if (response.status === 401 && !credentialsRefreshed && this.refreshCredentials(req)) {
|
|
733
|
+
credentialsRefreshed = true;
|
|
734
|
+
void response.body?.cancel().catch(() => { });
|
|
735
|
+
emitResponseDebug(response);
|
|
736
|
+
attempt--;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
472
739
|
let body;
|
|
473
740
|
try {
|
|
474
741
|
body = await parseBody(response, req.method);
|
|
@@ -478,12 +745,10 @@ export class HttpCore {
|
|
|
478
745
|
}
|
|
479
746
|
emitResponseDebug(response, body);
|
|
480
747
|
const responseMeta = meta(response, body);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const error = (
|
|
485
|
-
? new Ctor(body, responseMeta)
|
|
486
|
-
: new UnexpectedApiError(response.status, body, responseMeta));
|
|
748
|
+
req.options?.onResponse?.(responseMeta);
|
|
749
|
+
// A rate limit is its own error whatever the status: a 403 that means
|
|
750
|
+
// "wait" must not read as "your credential lacks access".
|
|
751
|
+
const error = errorFor(response, body, responseMeta, limit);
|
|
487
752
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
488
753
|
return { ok: false, error, response: responseMeta };
|
|
489
754
|
}
|
|
@@ -492,6 +757,19 @@ export class HttpCore {
|
|
|
492
757
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
493
758
|
return { ok: false, error };
|
|
494
759
|
}
|
|
760
|
+
/** Drop cached tokens behind the credential this request selected. True
|
|
761
|
+
* when a callback or cached token can resolve to a new value. */
|
|
762
|
+
refreshCredentials(req) {
|
|
763
|
+
const selected = req.security && this.config.credentials ? selectSecurity(req.security, this.config.credentials) : {};
|
|
764
|
+
let refreshable = false;
|
|
765
|
+
for (const value of [...Object.values(selected.headers ?? {}), ...Object.values(selected.query ?? {})]) {
|
|
766
|
+
if (typeof value !== "function")
|
|
767
|
+
continue;
|
|
768
|
+
refreshable = true;
|
|
769
|
+
markRejected(value);
|
|
770
|
+
}
|
|
771
|
+
return refreshable;
|
|
772
|
+
}
|
|
495
773
|
/** SDK-facing call: resolve to the payload or throw its typed error. */
|
|
496
774
|
async requestData(req) {
|
|
497
775
|
return unwrap(await this.request(req));
|
|
@@ -603,7 +881,7 @@ export class HttpCore {
|
|
|
603
881
|
}
|
|
604
882
|
async buildUrl(req, authQuery) {
|
|
605
883
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
606
|
-
const url = new URL(base + req.path);
|
|
884
|
+
const url = req.url !== undefined ? new URL(req.url, base) : new URL(base + req.path);
|
|
607
885
|
for (const [k, v] of Object.entries(req.query ?? {})) {
|
|
608
886
|
if (v === undefined || v === null)
|
|
609
887
|
continue;
|
|
@@ -622,8 +900,36 @@ export class HttpCore {
|
|
|
622
900
|
return url.toString();
|
|
623
901
|
}
|
|
624
902
|
}
|
|
903
|
+
/** A credential callback failed: surface the caller's own error unchanged
|
|
904
|
+
* instead of retrying it as a transport failure. */
|
|
905
|
+
class CredentialCallbackFailure {
|
|
906
|
+
error;
|
|
907
|
+
constructor(error) { this.error = error; }
|
|
908
|
+
}
|
|
625
909
|
async function resolveAuthValue(value) {
|
|
626
|
-
|
|
910
|
+
if (typeof value !== "function")
|
|
911
|
+
return value;
|
|
912
|
+
try {
|
|
913
|
+
return await callCredential(value);
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
// The SDK's own token request failures keep the transport retry path.
|
|
917
|
+
if (error instanceof TransportError)
|
|
918
|
+
throw error;
|
|
919
|
+
throw new CredentialCallbackFailure(error);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/** Callbacks without their own invalidate learn of a 401 on their next call. */
|
|
923
|
+
const rejectedCallbacks = new WeakSet();
|
|
924
|
+
function markRejected(value) {
|
|
925
|
+
if (value.invalidate)
|
|
926
|
+
value.invalidate();
|
|
927
|
+
else
|
|
928
|
+
rejectedCallbacks.add(value);
|
|
929
|
+
}
|
|
930
|
+
function callCredential(value) {
|
|
931
|
+
const rejected = rejectedCallbacks.delete(value);
|
|
932
|
+
return value({ rejected });
|
|
627
933
|
}
|
|
628
934
|
/** Default rendering for debug events (the boolean debug:true sink). */
|
|
629
935
|
export function formatDebugEvent(name, event) {
|
|
@@ -637,10 +943,32 @@ export function formatDebugEvent(name, event) {
|
|
|
637
943
|
/** Wrap a bearer credential (static or callback) as an Authorization value. */
|
|
638
944
|
export function bearerAuth(token) {
|
|
639
945
|
if (typeof token === "function") {
|
|
640
|
-
return async () => "Bearer " + (await token());
|
|
946
|
+
return Object.assign(async () => "Bearer " + (await callCredential(token)), { invalidate: () => markRejected(token) });
|
|
641
947
|
}
|
|
642
948
|
return "Bearer " + token;
|
|
643
949
|
}
|
|
950
|
+
/** The media type a local file is uploaded as, from its extension. A
|
|
951
|
+
* Worker module (.mjs) must arrive as application/javascript+module. */
|
|
952
|
+
export function mediaTypeForPath(path) {
|
|
953
|
+
const extension = /\.([A-Za-z0-9]+)$/.exec(path)?.[1]?.toLowerCase() ?? "";
|
|
954
|
+
const types = {
|
|
955
|
+
mjs: "application/javascript+module", js: "application/javascript", cjs: "application/javascript", wasm: "application/wasm",
|
|
956
|
+
json: "application/json", jsonl: "application/jsonl", txt: "text/plain", md: "text/markdown", csv: "text/csv", html: "text/html",
|
|
957
|
+
xml: "application/xml", yaml: "application/yaml", yml: "application/yaml", pdf: "application/pdf", zip: "application/zip",
|
|
958
|
+
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
959
|
+
mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", flac: "audio/flac", m4a: "audio/mp4", mp4: "video/mp4", webm: "video/webm",
|
|
960
|
+
};
|
|
961
|
+
return types[extension] ?? "application/octet-stream";
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Join a query array into one delimited value (`ids=1,2`) for parameters
|
|
965
|
+
* whose spec says `explode: false`. Other values pass through unchanged.
|
|
966
|
+
*/
|
|
967
|
+
export function delimited(value, separator) {
|
|
968
|
+
if (!Array.isArray(value))
|
|
969
|
+
return value;
|
|
970
|
+
return value.map((item) => (item instanceof Date ? item.toISOString() : String(item))).join(separator);
|
|
971
|
+
}
|
|
644
972
|
/**
|
|
645
973
|
* Bracket-style deep encoding shared by query strings and form bodies:
|
|
646
974
|
* { created: { gte: 5 } } -> created[gte]=5, { items: [{ id: "x" }] } ->
|
|
@@ -710,14 +1038,17 @@ function requestIdFromBody(body) {
|
|
|
710
1038
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
711
1039
|
}
|
|
712
1040
|
function meta(response, body) {
|
|
1041
|
+
const etag = response.headers.get("etag");
|
|
1042
|
+
const lastModified = response.headers.get("last-modified");
|
|
1043
|
+
const rateLimit = rateLimitInfo(response.status, response.headers);
|
|
713
1044
|
return {
|
|
714
1045
|
status: response.status,
|
|
715
1046
|
headers: response.headers,
|
|
716
1047
|
...(body !== undefined ? { rawBody: body } : {}),
|
|
717
|
-
requestId: requestIdFromBody(body) ??
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1048
|
+
requestId: requestIdFromBody(body) ?? requestIdFromHeaders(response.headers),
|
|
1049
|
+
...(etag ? { etag } : {}),
|
|
1050
|
+
...(lastModified ? { lastModified } : {}),
|
|
1051
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
721
1052
|
};
|
|
722
1053
|
}
|
|
723
1054
|
/** AbortSignal.any is missing from some web and edge runtimes; compose the
|
|
@@ -745,16 +1076,17 @@ function composeSignals(signals) {
|
|
|
745
1076
|
}
|
|
746
1077
|
return controller.signal;
|
|
747
1078
|
}
|
|
1079
|
+
/** Retry-After on a retryable status that is not a rate limit (a 503). */
|
|
748
1080
|
function retryAfterMs(response) {
|
|
749
1081
|
const header = response.headers.get("retry-after");
|
|
750
1082
|
if (!header)
|
|
751
1083
|
return undefined;
|
|
752
1084
|
const seconds = Number(header);
|
|
753
1085
|
if (Number.isFinite(seconds))
|
|
754
|
-
return Math.
|
|
1086
|
+
return Math.max(seconds * 1000, 0);
|
|
755
1087
|
const date = Date.parse(header);
|
|
756
1088
|
if (!Number.isNaN(date))
|
|
757
|
-
return Math.
|
|
1089
|
+
return Math.max(date - Date.now(), 0);
|
|
758
1090
|
return undefined;
|
|
759
1091
|
}
|
|
760
1092
|
/** Exponential backoff with full jitter, capped at 10s by default. */
|
|
@@ -768,8 +1100,9 @@ function sleep(ms) {
|
|
|
768
1100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
769
1101
|
}
|
|
770
1102
|
export function toBase64(input) {
|
|
1103
|
+
// Encode UTF-8 bytes: btoa alone rejects characters outside Latin-1.
|
|
771
1104
|
if (typeof btoa === "function")
|
|
772
|
-
return btoa(input);
|
|
1105
|
+
return btoa(Array.from(new TextEncoder().encode(input), (byte) => String.fromCharCode(byte)).join(""));
|
|
773
1106
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
774
1107
|
return globalThis.Buffer.from(input, "utf-8").toString("base64");
|
|
775
1108
|
}
|