@typeship-ax/cli 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/AGENTS.md +12 -8
  2. package/README.md +14 -27
  3. package/api.json +8898 -9026
  4. package/api.md +369 -327
  5. package/dist/arguments.d.ts +47 -0
  6. package/dist/arguments.d.ts.map +1 -0
  7. package/dist/arguments.js +254 -0
  8. package/dist/cli-agent.d.ts +31 -8
  9. package/dist/cli-agent.d.ts.map +1 -1
  10. package/dist/cli-agent.js +146 -28
  11. package/dist/cli.js +483 -237
  12. package/dist/core/http.d.ts +162 -19
  13. package/dist/core/http.d.ts.map +1 -1
  14. package/dist/core/http.js +381 -48
  15. package/dist/core/pagination.d.ts +42 -6
  16. package/dist/core/pagination.d.ts.map +1 -1
  17. package/dist/core/pagination.js +111 -17
  18. package/dist/credential-storage.d.ts +10 -3
  19. package/dist/credential-storage.d.ts.map +1 -1
  20. package/dist/credential-storage.js +15 -6
  21. package/dist/dates.d.ts +1 -1
  22. package/dist/dates.js +1 -1
  23. package/dist/errors.d.ts +20 -84
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +20 -108
  26. package/dist/fields.d.ts +29 -0
  27. package/dist/fields.d.ts.map +1 -0
  28. package/dist/fields.js +101 -0
  29. package/dist/index.d.ts +28 -18
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +35 -25
  32. package/dist/named-credentials.d.ts +19 -0
  33. package/dist/named-credentials.d.ts.map +1 -1
  34. package/dist/named-credentials.js +81 -1
  35. package/dist/oauth-login.d.ts +8 -2
  36. package/dist/oauth-login.d.ts.map +1 -1
  37. package/dist/oauth-login.js +31 -19
  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 +53 -5
  45. package/dist/ops.d.ts.map +1 -1
  46. package/dist/ops.js +49 -40
  47. package/dist/polling-login.d.ts +8 -2
  48. package/dist/polling-login.d.ts.map +1 -1
  49. package/dist/polling-login.js +25 -11
  50. package/dist/resources/api-keys.d.ts +10 -7
  51. package/dist/resources/api-keys.d.ts.map +1 -1
  52. package/dist/resources/api-keys.js +10 -31
  53. package/dist/resources/deliveries.d.ts +88 -4
  54. package/dist/resources/deliveries.d.ts.map +1 -1
  55. package/dist/resources/deliveries.js +95 -18
  56. package/dist/resources/drafts.d.ts +15 -15
  57. package/dist/resources/drafts.d.ts.map +1 -1
  58. package/dist/resources/drafts.js +11 -64
  59. package/dist/resources/files.d.ts +4 -4
  60. package/dist/resources/files.d.ts.map +1 -1
  61. package/dist/resources/files.js +3 -12
  62. package/dist/resources/generations.d.ts +14 -14
  63. package/dist/resources/generations.d.ts.map +1 -1
  64. package/dist/resources/generations.js +21 -45
  65. package/dist/resources/organization.d.ts +4 -4
  66. package/dist/resources/organization.d.ts.map +1 -1
  67. package/dist/resources/organization.js +3 -10
  68. package/dist/resources/{generate.d.ts → packages.d.ts} +16 -16
  69. package/dist/resources/packages.d.ts.map +1 -0
  70. package/dist/resources/{generate.js → packages.js} +13 -29
  71. package/dist/resources/projects.d.ts +50 -50
  72. package/dist/resources/projects.d.ts.map +1 -1
  73. package/dist/resources/projects.js +60 -116
  74. package/dist/resources/releases.d.ts +21 -16
  75. package/dist/resources/releases.d.ts.map +1 -1
  76. package/dist/resources/releases.js +18 -39
  77. package/dist/resources/spec-revisions.d.ts +15 -6
  78. package/dist/resources/spec-revisions.d.ts.map +1 -1
  79. package/dist/resources/spec-revisions.js +6 -28
  80. package/dist/resources/specs.d.ts +7 -7
  81. package/dist/resources/specs.d.ts.map +1 -1
  82. package/dist/resources/specs.js +6 -34
  83. package/dist/resources/targets.d.ts +48 -48
  84. package/dist/resources/targets.d.ts.map +1 -1
  85. package/dist/resources/targets.js +58 -114
  86. package/dist/schemas.d.ts.map +1 -1
  87. package/dist/schemas.js +83 -81
  88. package/dist/search.d.ts +54 -0
  89. package/dist/search.d.ts.map +1 -0
  90. package/dist/search.js +421 -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/package.json +5 -2
  95. package/src/arguments.ts +242 -0
  96. package/src/cli-agent.ts +156 -30
  97. package/src/cli.ts +444 -211
  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 +91 -0
  104. package/src/index.ts +45 -28
  105. package/src/named-credentials.ts +66 -1
  106. package/src/oauth-login.ts +36 -21
  107. package/src/oauth-request.ts +32 -6
  108. package/src/oauth-session.ts +37 -19
  109. package/src/ops.ts +82 -44
  110. package/src/polling-login.ts +24 -11
  111. package/src/resources/api-keys.ts +34 -48
  112. package/src/resources/deliveries.ts +211 -30
  113. package/src/resources/drafts.ts +60 -107
  114. package/src/resources/files.ts +19 -20
  115. package/src/resources/generations.ts +57 -75
  116. package/src/resources/organization.ts +11 -16
  117. package/src/resources/{generate.ts → packages.ts} +43 -51
  118. package/src/resources/projects.ts +145 -200
  119. package/src/resources/releases.ts +48 -65
  120. package/src/resources/spec-revisions.ts +38 -47
  121. package/src/resources/specs.ts +39 -59
  122. package/src/resources/targets.ts +143 -193
  123. package/src/schemas.ts +83 -81
  124. package/src/search.ts +434 -0
  125. package/src/types.ts +538 -357
  126. package/dist/console-login-check.d.ts +0 -21
  127. package/dist/console-login-check.d.ts.map +0 -1
  128. package/dist/console-login-check.js +0 -107
  129. package/dist/console-login-contract.d.ts +0 -45
  130. package/dist/console-login-contract.d.ts.map +0 -1
  131. package/dist/console-login-contract.js +0 -40
  132. package/dist/resources/generate.d.ts.map +0 -1
  133. package/dist/resources/publications.d.ts +0 -47
  134. package/dist/resources/publications.d.ts.map +0 -1
  135. package/dist/resources/publications.js +0 -70
  136. package/src/console-login-check.ts +0 -88
  137. package/src/console-login-contract.ts +0 -65
  138. package/src/resources/publications.ts +0 -140
package/src/core/http.ts CHANGED
@@ -1,5 +1,5 @@
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
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
- if (body && typeof body === "object" && !Array.isArray(body)) {
97
- const value = body as Record<string, unknown>;
98
- const first = Array.isArray(value.errors) ? value.errors[0] as { code?: unknown } | undefined : undefined;
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
- if (!body || typeof body !== "object" || Array.isArray(body)) return "";
107
- const value = body as Record<string, unknown>;
108
- const first = Array.isArray(value.errors) ? value.errors[0] as { message?: unknown } | undefined : undefined;
109
- const detail = value.message ?? value.error ?? value.detail ?? first?.message;
110
- return typeof detail === "string" && detail.trim() ? ": " + detail : "";
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
- constructor(message: string, status: S, body: B, response: ResponseMeta) {
131
- super(message + errorDetail(body) + ". " + nextStep(status), errorCode(body, "http_" + status), status, body, response.requestId);
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
- /** A response status the spec didn't document. */
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("Unexpected HTTP status " + status, status, body, response);
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
- /** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body). */
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 + ". Check the connection and retry.", "transport_error", response?.status ?? null, undefined, response?.requestId);
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
- /** Operation-level retry policy (x-typeship-retries), merged over the
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 (x-typeship-retries; defaults preserved when unset). */
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
- /** Per-operation schema table, keyed "resource.method" (see schemas.ts). */
416
- schemas?: Record<string, { req?: unknown; res?: unknown }>;
417
- /** Shared component definitions the schema table references. */
418
- schemaDefs?: Record<string, unknown>;
419
- /** Root-level retry policy (x-typeship-retries at the document root). */
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
- /** Client-level values for x-typeship-globals parameters, by wire name. */
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 an x-typeship-globals parameter. */
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 opSchemas = this.config.validate && req.schemaKey ? this.config.schemas?.[req.schemaKey] : undefined;
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
- validateAgainstSchema(req.body, opSchemas.req, "body", violations, this.config.schemaDefs);
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, this.config.schemaDefs);
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
- if (attempt < maxRetries && retryableStatus) {
615
- const delay = retryAfterMs(response) ?? backoff(attempt, policy);
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
- const Ctor =
636
- req.errors?.[String(response.status)] ??
637
- req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
638
- req.errors?.["default"];
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
- return typeof value === "function" ? await value() : value;
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
- requestIdFromBody(body) ??
879
- response.headers.get("request-id") ??
880
- response.headers.get("x-request-id") ??
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.min(seconds * 1000, 60_000);
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.min(Math.max(date - Date.now(), 0), 60_000);
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
- if (typeof btoa === "function") return btoa(input);
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
  }