@typeship-ax/mcp 0.21.0 → 0.23.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.
Files changed (133) hide show
  1. package/AGENTS.md +15 -11
  2. package/README.md +22 -53
  3. package/api.json +9998 -10118
  4. package/api.md +8983 -9120
  5. package/dist/arguments.d.ts +54 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +265 -0
  8. package/dist/core/http.d.ts +162 -19
  9. package/dist/core/http.d.ts.map +1 -1
  10. package/dist/core/http.js +381 -48
  11. package/dist/core/pagination.d.ts +42 -6
  12. package/dist/core/pagination.d.ts.map +1 -1
  13. package/dist/core/pagination.js +111 -17
  14. package/dist/credential-storage.d.ts +10 -3
  15. package/dist/credential-storage.d.ts.map +1 -1
  16. package/dist/credential-storage.js +15 -6
  17. package/dist/dates.d.ts +1 -1
  18. package/dist/dates.js +1 -1
  19. package/dist/errors.d.ts +20 -84
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +20 -108
  22. package/dist/fields.d.ts +36 -0
  23. package/dist/fields.d.ts.map +1 -0
  24. package/dist/fields.js +187 -0
  25. package/dist/index.d.ts +28 -18
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +35 -25
  28. package/dist/mcp-authorization.d.ts.map +1 -1
  29. package/dist/mcp-authorization.js +34 -10
  30. package/dist/mcp-protocol.d.ts +108 -44
  31. package/dist/mcp-protocol.d.ts.map +1 -1
  32. package/dist/mcp-protocol.js +780 -484
  33. package/dist/mcp.d.ts.map +1 -1
  34. package/dist/mcp.js +129 -29
  35. package/dist/named-credentials.d.ts +19 -0
  36. package/dist/named-credentials.d.ts.map +1 -1
  37. package/dist/named-credentials.js +81 -1
  38. package/dist/oauth-request.d.ts +7 -1
  39. package/dist/oauth-request.d.ts.map +1 -1
  40. package/dist/oauth-request.js +26 -4
  41. package/dist/oauth-session.d.ts +13 -1
  42. package/dist/oauth-session.d.ts.map +1 -1
  43. package/dist/oauth-session.js +34 -18
  44. package/dist/ops.d.ts +58 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +110 -41
  47. package/dist/resources/api-keys.d.ts +10 -7
  48. package/dist/resources/api-keys.d.ts.map +1 -1
  49. package/dist/resources/api-keys.js +10 -31
  50. package/dist/resources/deliveries.d.ts +89 -5
  51. package/dist/resources/deliveries.d.ts.map +1 -1
  52. package/dist/resources/deliveries.js +96 -19
  53. package/dist/resources/drafts.d.ts +16 -16
  54. package/dist/resources/drafts.d.ts.map +1 -1
  55. package/dist/resources/drafts.js +12 -65
  56. package/dist/resources/files.d.ts +4 -4
  57. package/dist/resources/files.d.ts.map +1 -1
  58. package/dist/resources/files.js +3 -12
  59. package/dist/resources/generations.d.ts +16 -16
  60. package/dist/resources/generations.d.ts.map +1 -1
  61. package/dist/resources/generations.js +23 -47
  62. package/dist/resources/organization.d.ts +4 -4
  63. package/dist/resources/organization.d.ts.map +1 -1
  64. package/dist/resources/organization.js +3 -10
  65. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  66. package/dist/resources/packages.d.ts.map +1 -0
  67. package/dist/resources/{generate.js → packages.js} +13 -29
  68. package/dist/resources/projects.d.ts +50 -50
  69. package/dist/resources/projects.d.ts.map +1 -1
  70. package/dist/resources/projects.js +60 -116
  71. package/dist/resources/releases.d.ts +22 -17
  72. package/dist/resources/releases.d.ts.map +1 -1
  73. package/dist/resources/releases.js +19 -40
  74. package/dist/resources/spec-revisions.d.ts +16 -7
  75. package/dist/resources/spec-revisions.d.ts.map +1 -1
  76. package/dist/resources/spec-revisions.js +7 -29
  77. package/dist/resources/specs.d.ts +7 -7
  78. package/dist/resources/specs.d.ts.map +1 -1
  79. package/dist/resources/specs.js +6 -34
  80. package/dist/resources/targets.d.ts +49 -49
  81. package/dist/resources/targets.d.ts.map +1 -1
  82. package/dist/resources/targets.js +59 -115
  83. package/dist/schemas.d.ts.map +1 -1
  84. package/dist/schemas.js +78 -76
  85. package/dist/search.d.ts +54 -0
  86. package/dist/search.d.ts.map +1 -0
  87. package/dist/search.js +421 -0
  88. package/dist/type-docs.d.ts +61 -0
  89. package/dist/type-docs.d.ts.map +1 -0
  90. package/dist/type-docs.js +174 -0
  91. package/dist/types.d.ts +499 -339
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js +18 -18
  94. package/dist/worker.js +2 -2
  95. package/package.json +5 -2
  96. package/server.json +5 -5
  97. package/src/arguments.ts +254 -0
  98. package/src/core/http.ts +457 -58
  99. package/src/core/pagination.ts +129 -18
  100. package/src/credential-storage.ts +16 -6
  101. package/src/dates.ts +1 -1
  102. package/src/errors.ts +46 -115
  103. package/src/fields.ts +167 -0
  104. package/src/index.ts +45 -28
  105. package/src/mcp-authorization.ts +29 -9
  106. package/src/mcp-protocol.ts +808 -435
  107. package/src/mcp.ts +115 -27
  108. package/src/named-credentials.ts +66 -1
  109. package/src/oauth-request.ts +32 -6
  110. package/src/oauth-session.ts +37 -19
  111. package/src/ops.ts +146 -45
  112. package/src/resources/api-keys.ts +34 -48
  113. package/src/resources/deliveries.ts +213 -32
  114. package/src/resources/drafts.ts +62 -109
  115. package/src/resources/files.ts +19 -20
  116. package/src/resources/generations.ts +61 -79
  117. package/src/resources/organization.ts +11 -16
  118. package/src/resources/{generate.ts → packages.ts} +43 -51
  119. package/src/resources/projects.ts +145 -200
  120. package/src/resources/releases.ts +50 -67
  121. package/src/resources/spec-revisions.ts +40 -49
  122. package/src/resources/specs.ts +39 -59
  123. package/src/resources/targets.ts +144 -194
  124. package/src/schemas.ts +78 -76
  125. package/src/search.ts +434 -0
  126. package/src/type-docs.ts +205 -0
  127. package/src/types.ts +538 -357
  128. package/src/worker.ts +2 -2
  129. package/dist/resources/generate.d.ts.map +0 -1
  130. package/dist/resources/publications.d.ts +0 -47
  131. package/dist/resources/publications.d.ts.map +0 -1
  132. package/dist/resources/publications.js +0 -70
  133. package/src/resources/publications.ts +0 -140
package/dist/core/http.js CHANGED
@@ -1,7 +1,55 @@
1
1
  /**
2
- * Runtime core. Generated by typeship — https://typeship.dev
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
- if (body && typeof body === "object" && !Array.isArray(body)) {
50
- const value = body;
51
- const first = Array.isArray(value.errors) ? value.errors[0] : undefined;
52
- const code = value.code ?? first?.code;
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
- if (!body || typeof body !== "object" || Array.isArray(body))
124
+ const { error, value, first } = errorFields(body);
125
+ if (!value)
60
126
  return "";
61
- const value = body;
62
- const first = Array.isArray(value.errors) ? value.errors[0] : undefined;
63
- const detail = value.message ?? value.error ?? value.detail ?? first?.message;
64
- return typeof detail === "string" && detail.trim() ? ": " + detail : "";
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
- constructor(message, status, body, response) {
89
- super(message + errorDetail(body) + ". " + nextStep(status), errorCode(body, "http_" + status), status, body, response.requestId);
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
- /** A response status the spec didn't document. */
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("Unexpected HTTP status " + status, status, body, response);
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
- /** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body). */
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 + ". Check the connection and retry.", "transport_error", response?.status ?? null, undefined, response?.requestId);
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 an x-typeship-globals parameter. */
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 opSchemas = this.config.validate && req.schemaKey ? this.config.schemas?.[req.schemaKey] : undefined;
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
- validateAgainstSchema(req.body, opSchemas.req, "body", violations, this.config.schemaDefs);
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
- const error = (parseError ?? new TransportError("The response body read failed before completing", cause, meta(response)));
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, this.config.schemaDefs);
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 retryableStatus = retryableStatuses.has(response.status) &&
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
- if (attempt < maxRetries && retryableStatus) {
460
- const delay = retryAfterMs(response) ?? backoff(attempt, policy);
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
- const Ctor = req.errors?.[String(response.status)] ??
482
- req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
483
- req.errors?.["default"];
484
- const error = (Ctor
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
- return typeof value === "function" ? await value() : value;
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
- response.headers.get("request-id") ??
719
- response.headers.get("x-request-id") ??
720
- undefined,
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.min(seconds * 1000, 60_000);
1086
+ return Math.max(seconds * 1000, 0);
755
1087
  const date = Date.parse(header);
756
1088
  if (!Number.isNaN(date))
757
- return Math.min(Math.max(date - Date.now(), 0), 60_000);
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
  }