@typeship-ax/mcp 0.6.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 (73) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +43 -0
  3. package/api.json +5163 -0
  4. package/api.md +512 -0
  5. package/dist/core/http.d.ts +303 -0
  6. package/dist/core/http.d.ts.map +1 -0
  7. package/dist/core/http.js +770 -0
  8. package/dist/core/pagination.d.ts +51 -0
  9. package/dist/core/pagination.d.ts.map +1 -0
  10. package/dist/core/pagination.js +154 -0
  11. package/dist/dates.d.ts +33 -0
  12. package/dist/dates.d.ts.map +1 -0
  13. package/dist/dates.js +136 -0
  14. package/dist/errors.d.ts +81 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +103 -0
  17. package/dist/index.d.ts +92 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +86 -0
  20. package/dist/mcp-protocol.d.ts +453 -0
  21. package/dist/mcp-protocol.d.ts.map +1 -0
  22. package/dist/mcp-protocol.js +1262 -0
  23. package/dist/mcp.d.ts +5 -0
  24. package/dist/mcp.d.ts.map +1 -0
  25. package/dist/mcp.js +449 -0
  26. package/dist/ops.d.ts +115 -0
  27. package/dist/ops.d.ts.map +1 -0
  28. package/dist/ops.js +79 -0
  29. package/dist/resources/account.d.ts +18 -0
  30. package/dist/resources/account.d.ts.map +1 -0
  31. package/dist/resources/account.js +26 -0
  32. package/dist/resources/api-keys.d.ts +37 -0
  33. package/dist/resources/api-keys.d.ts.map +1 -0
  34. package/dist/resources/api-keys.js +67 -0
  35. package/dist/resources/generate.d.ts +25 -0
  36. package/dist/resources/generate.d.ts.map +1 -0
  37. package/dist/resources/generate.js +41 -0
  38. package/dist/resources/generations.d.ts +31 -0
  39. package/dist/resources/generations.d.ts.map +1 -0
  40. package/dist/resources/generations.js +56 -0
  41. package/dist/resources/projects.d.ts +110 -0
  42. package/dist/resources/projects.d.ts.map +1 -0
  43. package/dist/resources/projects.js +220 -0
  44. package/dist/resources/spec-revisions.d.ts +47 -0
  45. package/dist/resources/spec-revisions.d.ts.map +1 -0
  46. package/dist/resources/spec-revisions.js +90 -0
  47. package/dist/schemas.d.ts +6 -0
  48. package/dist/schemas.d.ts.map +1 -0
  49. package/dist/schemas.js +88 -0
  50. package/dist/types.d.ts +759 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +37 -0
  53. package/dist/worker.d.ts +5 -0
  54. package/dist/worker.d.ts.map +1 -0
  55. package/dist/worker.js +12 -0
  56. package/package.json +45 -0
  57. package/src/core/http.ts +1008 -0
  58. package/src/core/pagination.ts +195 -0
  59. package/src/dates.ts +126 -0
  60. package/src/errors.ts +117 -0
  61. package/src/index.ts +153 -0
  62. package/src/mcp-protocol.ts +1451 -0
  63. package/src/mcp.ts +448 -0
  64. package/src/ops.ts +174 -0
  65. package/src/resources/account.ts +43 -0
  66. package/src/resources/api-keys.ts +105 -0
  67. package/src/resources/generate.ts +69 -0
  68. package/src/resources/generations.ts +100 -0
  69. package/src/resources/projects.ts +391 -0
  70. package/src/resources/spec-revisions.ts +150 -0
  71. package/src/schemas.ts +90 -0
  72. package/src/types.ts +825 -0
  73. package/src/worker.ts +13 -0
@@ -0,0 +1,770 @@
1
+ /**
2
+ * Runtime core. Generated by typeship — https://typeship.dev
3
+ * Zero dependencies: built on the platform fetch API (Node 18+, browsers, edge).
4
+ */
5
+ /** Collapse a result into its data, throwing the typed error when !ok. */
6
+ export function unwrap(result) {
7
+ if (result.ok)
8
+ return result.data;
9
+ if (result.error instanceof Error)
10
+ throw result.error;
11
+ throw new Error(String(result.error));
12
+ }
13
+ /** Base class for every HTTP error response. */
14
+ export class ApiError extends Error {
15
+ status;
16
+ body;
17
+ response;
18
+ constructor(message, status, body, response) {
19
+ super(message);
20
+ this.name = new.target.name;
21
+ this.status = status;
22
+ this.body = body;
23
+ this.response = response;
24
+ }
25
+ }
26
+ /** A response status the spec didn't document. */
27
+ export class UnexpectedApiError extends ApiError {
28
+ constructor(status, body, response) {
29
+ super("Unexpected HTTP status " + status, status, body, response);
30
+ }
31
+ }
32
+ /** A 200 response whose GraphQL payload carried errors. */
33
+ export class GraphQLRequestError extends Error {
34
+ /** The raw errors array from the GraphQL response. */
35
+ errors;
36
+ response;
37
+ constructor(errors, response) {
38
+ const first = errors[0]?.message;
39
+ super(first ?? "GraphQL request returned errors");
40
+ this.name = "GraphQLRequestError";
41
+ this.errors = errors;
42
+ this.response = response;
43
+ }
44
+ }
45
+ /** Serialize a selection object to a GraphQL selection set. Strings pass
46
+ * through untouched. */
47
+ export function selectionToString(selection) {
48
+ if (typeof selection === "string")
49
+ return selection;
50
+ const parts = [];
51
+ for (const [key, value] of Object.entries(selection)) {
52
+ if (!value)
53
+ continue;
54
+ if (key === "on" && typeof value === "object") {
55
+ for (const [typeName, sub] of Object.entries(value)) {
56
+ if (sub && typeof sub === "object")
57
+ parts.push("... on " + typeName + " " + selectionToString(sub));
58
+ }
59
+ }
60
+ else if (value === true) {
61
+ parts.push(key);
62
+ }
63
+ else if (typeof value === "object") {
64
+ parts.push(key + " " + selectionToString(value));
65
+ }
66
+ }
67
+ return "{ " + (parts.length > 0 ? parts.join(" ") : "__typename") + " }";
68
+ }
69
+ /** Request or response data did not match the spec's schema (opt-in via the
70
+ * client's validate option). Never thrown: returned as the error side of
71
+ * ApiResult, like every other failure. */
72
+ export class ValidationError extends Error {
73
+ direction;
74
+ violations;
75
+ constructor(direction, violations) {
76
+ const shown = violations.slice(0, 3).map((v) => v.path + " " + v.message).join("; ");
77
+ super(direction + " body failed schema validation: " + shown
78
+ + (violations.length > 3 ? " (+" + (violations.length - 3) + " more)" : ""));
79
+ this.name = "ValidationError";
80
+ this.direction = direction;
81
+ this.violations = violations;
82
+ }
83
+ }
84
+ function jsonType(value) {
85
+ if (value === null)
86
+ return "null";
87
+ if (Array.isArray(value))
88
+ return "array";
89
+ return typeof value;
90
+ }
91
+ function typeMatches(value, t) {
92
+ if (t === "integer")
93
+ return typeof value === "number" && Number.isInteger(value);
94
+ return jsonType(value) === t;
95
+ }
96
+ const MAX_VIOLATIONS = 50;
97
+ /**
98
+ * Validates against the depth-capped JSON Schema subset emitted in
99
+ * schemas.ts. Constraints outside the subset (formats, multipleOf, not, ...)
100
+ * are ignored: validation can miss drift but never false-alarms.
101
+ */
102
+ export function validateAgainstSchema(value, schema, path, out, defs) {
103
+ if (out.length >= MAX_VIOLATIONS)
104
+ return;
105
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
106
+ return;
107
+ const s = schema;
108
+ // Named components are deduplicated into the DEFS table (see schemas.ts);
109
+ // recursion is bounded by the data's own depth, so cycles terminate.
110
+ if (typeof s.$ref === "string") {
111
+ validateAgainstSchema(value, defs?.[s.$ref], path, out, defs);
112
+ return;
113
+ }
114
+ if (Array.isArray(s.allOf))
115
+ for (const sub of s.allOf)
116
+ validateAgainstSchema(value, sub, path, out, defs);
117
+ const variants = s.anyOf ?? s.oneOf;
118
+ if (Array.isArray(variants) && variants.length > 0) {
119
+ const matched = variants.some((sub) => {
120
+ const scratch = [];
121
+ validateAgainstSchema(value, sub, path, scratch, defs);
122
+ return scratch.length === 0;
123
+ });
124
+ if (!matched)
125
+ out.push({ path, message: "matches none of the allowed variants" });
126
+ }
127
+ if (s.type !== undefined) {
128
+ const allowed = Array.isArray(s.type) ? s.type : [s.type];
129
+ if (s.nullable === true && !allowed.includes("null"))
130
+ allowed.push("null");
131
+ if (!allowed.some((t) => typeMatches(value, t))) {
132
+ out.push({ path, message: "expected " + allowed.join(" | ") + ", got " + jsonType(value) });
133
+ return; // remaining constraints assume the right type
134
+ }
135
+ }
136
+ if (value === null)
137
+ return;
138
+ if (Array.isArray(s.enum) && !s.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
139
+ out.push({ path, message: "not one of the allowed enum values" });
140
+ }
141
+ if (s.const !== undefined && JSON.stringify(s.const) !== JSON.stringify(value)) {
142
+ out.push({ path, message: "does not equal the required constant" });
143
+ }
144
+ if (typeof value === "number") {
145
+ if (typeof s.minimum === "number" && value < s.minimum)
146
+ out.push({ path, message: "below minimum " + s.minimum });
147
+ if (typeof s.maximum === "number" && value > s.maximum)
148
+ out.push({ path, message: "above maximum " + s.maximum });
149
+ }
150
+ if (typeof value === "string") {
151
+ if (typeof s.minLength === "number" && value.length < s.minLength)
152
+ out.push({ path, message: "shorter than minLength " + s.minLength });
153
+ if (typeof s.maxLength === "number" && value.length > s.maxLength)
154
+ out.push({ path, message: "longer than maxLength " + s.maxLength });
155
+ if (typeof s.pattern === "string") {
156
+ try {
157
+ if (!new RegExp(s.pattern).test(value))
158
+ out.push({ path, message: "does not match pattern" });
159
+ }
160
+ catch { /* invalid pattern in the spec: skip, never false-alarm */ }
161
+ }
162
+ }
163
+ if (Array.isArray(value)) {
164
+ if (typeof s.minItems === "number" && value.length < s.minItems)
165
+ out.push({ path, message: "fewer than minItems " + s.minItems });
166
+ if (typeof s.maxItems === "number" && value.length > s.maxItems)
167
+ out.push({ path, message: "more than maxItems " + s.maxItems });
168
+ if (s.items) {
169
+ for (let i = 0; i < value.length && out.length < MAX_VIOLATIONS; i++) {
170
+ validateAgainstSchema(value[i], s.items, path + "[" + i + "]", out, defs);
171
+ }
172
+ }
173
+ }
174
+ if (jsonType(value) === "object") {
175
+ const obj = value;
176
+ if (Array.isArray(s.required)) {
177
+ for (const key of s.required) {
178
+ if (obj[key] === undefined)
179
+ out.push({ path, message: "missing required property " + JSON.stringify(key) });
180
+ }
181
+ }
182
+ if (s.properties && typeof s.properties === "object") {
183
+ for (const [key, sub] of Object.entries(s.properties)) {
184
+ if (obj[key] !== undefined)
185
+ validateAgainstSchema(obj[key], sub, path + "." + key, out, defs);
186
+ }
187
+ if (s.additionalProperties === false) {
188
+ for (const key of Object.keys(obj)) {
189
+ if (!(key in s.properties))
190
+ out.push({ path, message: "unexpected property " + JSON.stringify(key) });
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * "fetch failed" alone is useless in a bug report; surface the request line
198
+ * and the deepest cause message (getaddrinfo ENOTFOUND, ECONNREFUSED, ...)
199
+ * the platform buried in the error chain.
200
+ */
201
+ function transportFailureMessage(method, url, cause) {
202
+ let detail = cause instanceof Error ? cause.message : String(cause ?? "request failed");
203
+ let node = cause;
204
+ for (let depth = 0; depth < 5 && node instanceof Error; depth++) {
205
+ node = Array.isArray(node.errors)
206
+ ? node.errors[0]
207
+ : node.cause;
208
+ if (node instanceof Error && node.message && !detail.includes(node.message)) {
209
+ detail += ": " + node.message;
210
+ }
211
+ }
212
+ return method + " " + url + " failed: " + detail;
213
+ }
214
+ /** The request never produced an HTTP response (network failure, timeout, abort). */
215
+ export class TransportError extends Error {
216
+ cause;
217
+ constructor(message, cause) {
218
+ super(message);
219
+ this.name = "TransportError";
220
+ this.cause = cause;
221
+ }
222
+ }
223
+ const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
224
+ /** Credential-bearing headers fetch itself drops when a redirect crosses to
225
+ * another origin. A spec's own API-key headers join them below. */
226
+ const SENSITIVE_HEADERS = ["authorization", "cookie", "cookie2", "proxy-authorization", "www-authenticate"];
227
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
228
+ /** Dropped along with the body when a redirect rewrites the method to GET. */
229
+ const CONTENT_HEADERS = ["content-encoding", "content-language", "content-location", "content-type", "content-length"];
230
+ /** fetch's own ceiling, so a redirect loop fails the same way it did before. */
231
+ const MAX_REDIRECTS = 20;
232
+ function withoutHeaders(headers, drop) {
233
+ const kept = {};
234
+ for (const [name, value] of Object.entries(headers)) {
235
+ if (!drop.includes(name.toLowerCase()))
236
+ kept[name] = value;
237
+ }
238
+ return kept;
239
+ }
240
+ /** A body that can only be read once, so a redirect cannot replay it. */
241
+ function isStreamBody(body) {
242
+ return typeof body?.getReader === "function";
243
+ }
244
+ export class HttpCore {
245
+ config;
246
+ /** Lowercased header names dropped on a cross-origin hop. */
247
+ sensitiveHeaders;
248
+ /** Whether this runtime follows redirects itself instead of letting the
249
+ * platform do it — see followRedirects(). */
250
+ manualRedirects;
251
+ // no parameter properties: this file also runs under strip-only TS
252
+ constructor(config) {
253
+ this.config = config;
254
+ const declared = (config.authHeaders ?? []).map((name) => name.toLowerCase());
255
+ const custom = declared.filter((name, i) => !SENSITIVE_HEADERS.includes(name) && declared.indexOf(name) === i);
256
+ this.sensitiveHeaders = [...SENSITIVE_HEADERS, ...custom];
257
+ // Only worth taking over from the platform when the spec puts its
258
+ // credential on a header fetch has never heard of. A browser is excluded:
259
+ // there, `redirect: "manual"` yields an opaque response with no Location
260
+ // to read, and a cross-origin hop carrying a non-safelisted header needs
261
+ // the target's CORS consent anyway.
262
+ this.manualRedirects = custom.length > 0 && globalThis.document === undefined;
263
+ }
264
+ /** The client-level value for an x-typeship-globals parameter. */
265
+ globalValue(name) {
266
+ return this.config.globals?.[name];
267
+ }
268
+ async request(req) {
269
+ const policy = { ...this.config.retry, ...req.retry };
270
+ const maxRetries = req.options?.maxRetries ?? policy.maxRetries ?? this.config.maxRetries;
271
+ const timeoutMs = req.options?.timeoutMs ?? this.config.timeoutMs;
272
+ const retryAllowed = req.idempotent === true || req.method === "GET" || policy.retryNonIdempotent === true;
273
+ const retryableStatuses = policy.statuses ? new Set(policy.statuses) : RETRYABLE_STATUSES;
274
+ // One key per logical call, reused on every retry — that's the point
275
+ // of idempotency keys.
276
+ const autoIdempotencyKey = req.idempotencyKey !== undefined &&
277
+ req.headers?.[req.idempotencyKey] === undefined &&
278
+ req.options?.headers?.[req.idempotencyKey] === undefined
279
+ ? crypto.randomUUID()
280
+ : undefined;
281
+ const opSchemas = this.config.validate && req.schemaKey ? this.config.schemas?.[req.schemaKey] : undefined;
282
+ if (opSchemas?.req && this.config.validate.requests
283
+ && req.body !== undefined && (req.bodyKind ?? "json") === "json") {
284
+ const violations = [];
285
+ validateAgainstSchema(req.body, opSchemas.req, "body", violations, this.config.schemaDefs);
286
+ if (violations.length > 0) {
287
+ const validationError = new ValidationError("request", violations);
288
+ if (this.config.validate.mode === "warn") {
289
+ console.warn(req.method + " " + req.path + ": " + validationError.message);
290
+ }
291
+ else {
292
+ const error = validationError;
293
+ await this.config.onError?.(error, { method: req.method, path: req.path });
294
+ return { ok: false, error };
295
+ }
296
+ }
297
+ }
298
+ let lastError;
299
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
300
+ let response;
301
+ const attemptStarted = Date.now();
302
+ try {
303
+ response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
304
+ this.config.debug?.({
305
+ method: req.method,
306
+ path: req.path,
307
+ status: response.status,
308
+ durationMs: Date.now() - attemptStarted,
309
+ attempt: attempt + 1,
310
+ requestId: response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined,
311
+ });
312
+ }
313
+ catch (cause) {
314
+ lastError = cause;
315
+ this.config.debug?.({
316
+ method: req.method,
317
+ path: req.path,
318
+ durationMs: Date.now() - attemptStarted,
319
+ attempt: attempt + 1,
320
+ error: cause instanceof Error ? cause.message : String(cause),
321
+ });
322
+ if (attempt < maxRetries && retryAllowed && !req.options?.signal?.aborted) {
323
+ await sleep(backoff(attempt, policy));
324
+ continue;
325
+ }
326
+ const error = new TransportError(transportFailureMessage(req.method, this.config.baseUrl.replace(/\/+$/, "") + req.path, cause), cause);
327
+ await this.config.onError?.(error, { method: req.method, path: req.path });
328
+ return { ok: false, error };
329
+ }
330
+ if (response.ok) {
331
+ if (req.stream) {
332
+ return { ok: true, data: sseEvents(response), response: meta(response) };
333
+ }
334
+ let data;
335
+ try {
336
+ data = (await parseBody(response, req.method));
337
+ }
338
+ catch (cause) {
339
+ const error = new TransportError("The response body read was aborted before completing", cause);
340
+ await this.config.onError?.(error, { method: req.method, path: req.path });
341
+ return { ok: false, error, response: meta(response) };
342
+ }
343
+ if (opSchemas?.res && this.config.validate.responses && data !== undefined) {
344
+ const violations = [];
345
+ validateAgainstSchema(data, opSchemas.res, "response", violations, this.config.schemaDefs);
346
+ if (violations.length > 0) {
347
+ const validationError = new ValidationError("response", violations);
348
+ if (this.config.validate.mode === "warn") {
349
+ console.warn(req.method + " " + req.path + ": " + validationError.message);
350
+ }
351
+ else {
352
+ const error = validationError;
353
+ await this.config.onError?.(error, { method: req.method, path: req.path });
354
+ return { ok: false, error, response: meta(response) };
355
+ }
356
+ }
357
+ }
358
+ if (req.graphqlField) {
359
+ const payload = data;
360
+ const responseMeta = meta(response);
361
+ if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
362
+ const gqlError = new GraphQLRequestError(payload.errors, responseMeta);
363
+ await this.config.onError?.(gqlError, { method: req.method, path: req.path });
364
+ return { ok: false, error: gqlError, response: responseMeta };
365
+ }
366
+ return { ok: true, data: payload?.data?.[req.graphqlField], response: responseMeta };
367
+ }
368
+ return { ok: true, data, response: meta(response) };
369
+ }
370
+ // 429 is safe to retry regardless of idempotency; other retryable
371
+ // statuses only when the verb is idempotent.
372
+ const retryableStatus = retryableStatuses.has(response.status) &&
373
+ (retryAllowed || response.status === 429);
374
+ if (attempt < maxRetries && retryableStatus) {
375
+ await sleep(retryAfterMs(response) ?? backoff(attempt, policy));
376
+ continue;
377
+ }
378
+ let body;
379
+ try {
380
+ body = await parseBody(response, req.method);
381
+ }
382
+ catch {
383
+ body = undefined; // error responses keep their status even if the body read aborts
384
+ }
385
+ const responseMeta = meta(response);
386
+ const Ctor = req.errors?.[String(response.status)] ??
387
+ req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
388
+ req.errors?.["default"];
389
+ const error = (Ctor
390
+ ? new Ctor(body, responseMeta)
391
+ : new UnexpectedApiError(response.status, body, responseMeta));
392
+ await this.config.onError?.(error, { method: req.method, path: req.path });
393
+ return { ok: false, error, response: responseMeta };
394
+ }
395
+ // Unreachable, but keeps the compiler honest.
396
+ const error = new TransportError("Request failed", lastError);
397
+ await this.config.onError?.(error, { method: req.method, path: req.path });
398
+ return { ok: false, error };
399
+ }
400
+ async send(req, timeoutMs, attempt, autoIdempotencyKey) {
401
+ const headers = {};
402
+ for (const [k, v] of Object.entries(this.config.headers)) {
403
+ headers[k] = await resolveAuthValue(v);
404
+ }
405
+ if (req.idempotencyKey && autoIdempotencyKey) {
406
+ headers[req.idempotencyKey] = autoIdempotencyKey;
407
+ }
408
+ const { body, contentType } = serializeBody(req);
409
+ if (contentType)
410
+ headers["Content-Type"] = contentType;
411
+ for (const source of [req.headers, req.options?.headers]) {
412
+ for (const [k, v] of Object.entries(source ?? {})) {
413
+ if (v !== undefined)
414
+ headers[k] = v;
415
+ }
416
+ }
417
+ const context = {
418
+ method: req.method,
419
+ url: await this.buildUrl(req),
420
+ headers,
421
+ attempt,
422
+ };
423
+ await this.config.onRequest?.(context);
424
+ // Streaming responses are exempt from the attempt timeout once headers
425
+ // arrive (an event stream may stay open far longer than timeoutMs);
426
+ // everything else keeps the timeout armed through the body read, so a
427
+ // stalled body aborts instead of hanging.
428
+ const signals = [];
429
+ let clearStreamTimeout;
430
+ if (req.stream) {
431
+ const headersTimeout = new AbortController();
432
+ const timer = setTimeout(() => headersTimeout.abort(new DOMException("Timed out waiting for response headers", "TimeoutError")), timeoutMs);
433
+ timer.unref?.();
434
+ clearStreamTimeout = () => clearTimeout(timer);
435
+ signals.push(headersTimeout.signal);
436
+ }
437
+ else {
438
+ signals.push(AbortSignal.timeout(timeoutMs));
439
+ }
440
+ if (req.options?.signal)
441
+ signals.push(req.options.signal);
442
+ try {
443
+ const signal = AbortSignal.any(signals);
444
+ const response = this.manualRedirects
445
+ ? await this.followRedirects(context, body, signal)
446
+ : await this.config.fetch(context.url, {
447
+ method: req.method,
448
+ headers: context.headers,
449
+ body,
450
+ signal,
451
+ });
452
+ await this.config.onResponse?.(response, context);
453
+ return response;
454
+ }
455
+ finally {
456
+ clearStreamTimeout?.();
457
+ }
458
+ }
459
+ /**
460
+ * The redirect chain, walked here instead of inside fetch, so the header
461
+ * this spec puts its API key on is dropped when a hop crosses to another
462
+ * origin. fetch drops Authorization and friends for us, but it has never
463
+ * heard of X-Whatever-Key, and an operation that returns a file commonly
464
+ * redirects to object storage.
465
+ *
466
+ * Only reached when the spec declares such a header (see the constructor);
467
+ * every other client stays on the platform's own redirect handling. One
468
+ * chain is one attempt: retries, the per-attempt timeout, the caller's
469
+ * signal, and the onRequest/onResponse hooks all sit outside it and see
470
+ * the chain as the single exchange it replaces.
471
+ */
472
+ async followRedirects(context, body, signal) {
473
+ let url = context.url;
474
+ let origin = new URL(url).origin;
475
+ let method = context.method;
476
+ let headers = context.headers;
477
+ for (let hop = 0;; hop++) {
478
+ const response = await this.config.fetch(url, { method, headers, body, redirect: "manual", signal });
479
+ // A runtime that filters manual redirects (a browser) hands back an
480
+ // opaque response with no Location to read. Refuse rather than let the
481
+ // credential travel blind — the constructor already keeps browsers off
482
+ // this path, so this is a guard, not an expected outcome.
483
+ if (response.type === "opaqueredirect" || response.status === 0) {
484
+ throw new TransportError("This runtime hides redirect targets, so the credential cannot be dropped before the hop");
485
+ }
486
+ const location = response.headers.get("location");
487
+ if (!REDIRECT_STATUSES.has(response.status) || location === null)
488
+ return response;
489
+ if (hop >= MAX_REDIRECTS)
490
+ throw new TransportError("Too many redirects (" + (hop + 1) + ")");
491
+ await response.body?.cancel();
492
+ const target = new URL(location, url);
493
+ headers = { ...headers };
494
+ // Origins are compared exactly — scheme, host, and port — which is
495
+ // fetch's rule and the one the Python runtime applies.
496
+ if (target.origin !== origin)
497
+ headers = withoutHeaders(headers, this.sensitiveHeaders);
498
+ // The method rewrite fetch performs: a 303 continues as a bodyless GET,
499
+ // and so does a 301/302 that answered a POST.
500
+ const toGet = response.status === 303
501
+ ? method !== "HEAD"
502
+ : (response.status === 301 || response.status === 302) && method === "POST";
503
+ if (toGet) {
504
+ method = "GET";
505
+ body = undefined;
506
+ headers = withoutHeaders(headers, CONTENT_HEADERS);
507
+ }
508
+ else if (isStreamBody(body)) {
509
+ // A stream reads once; fetch fails the same way rather than sending
510
+ // an empty body to the new location.
511
+ throw new TransportError("Cannot replay a streaming request body across a redirect");
512
+ }
513
+ url = target.toString();
514
+ origin = target.origin;
515
+ }
516
+ }
517
+ async buildUrl(req) {
518
+ const base = this.config.baseUrl.replace(/\/+$/, "");
519
+ const url = new URL(base + req.path);
520
+ for (const [k, v] of Object.entries(req.query ?? {})) {
521
+ if (v === undefined || v === null)
522
+ continue;
523
+ if (Array.isArray(v)) {
524
+ // top-level arrays repeat the key: expand=a&expand=b
525
+ for (const item of v)
526
+ appendDeep(url.searchParams, k, item);
527
+ }
528
+ else {
529
+ appendDeep(url.searchParams, k, v);
530
+ }
531
+ }
532
+ for (const [k, v] of Object.entries(this.config.query)) {
533
+ url.searchParams.append(k, await resolveAuthValue(v));
534
+ }
535
+ return url.toString();
536
+ }
537
+ }
538
+ async function resolveAuthValue(value) {
539
+ return typeof value === "function" ? await value() : value;
540
+ }
541
+ /** Parse a text/event-stream body into SseEvents, lazily. */
542
+ async function* sseEvents(response) {
543
+ if (!response.body)
544
+ return;
545
+ const reader = response.body.getReader();
546
+ const decoder = new TextDecoder();
547
+ let buffer = "";
548
+ let dataLines = [];
549
+ let eventName;
550
+ let eventId;
551
+ const flush = () => {
552
+ if (dataLines.length === 0)
553
+ return undefined;
554
+ const event = { data: dataLines.join("\n") };
555
+ if (eventName !== undefined)
556
+ event.event = eventName;
557
+ if (eventId !== undefined)
558
+ event.id = eventId;
559
+ dataLines = [];
560
+ eventName = undefined;
561
+ return event;
562
+ };
563
+ try {
564
+ while (true) {
565
+ const { done, value } = await reader.read();
566
+ if (done)
567
+ break;
568
+ buffer += decoder.decode(value, { stream: true });
569
+ let newline;
570
+ while ((newline = buffer.indexOf("\n")) !== -1) {
571
+ const hasCr = newline > 0 && buffer[newline - 1] === "\r";
572
+ const line = buffer.slice(0, hasCr ? newline - 1 : newline);
573
+ buffer = buffer.slice(newline + 1);
574
+ if (line === "") {
575
+ const event = flush();
576
+ if (event)
577
+ yield event;
578
+ }
579
+ else if (line.startsWith("data:")) {
580
+ dataLines.push(line.slice(5).replace(/^ /, ""));
581
+ }
582
+ else if (line.startsWith("event:")) {
583
+ eventName = line.slice(6).replace(/^ /, "");
584
+ }
585
+ else if (line.startsWith("id:")) {
586
+ eventId = line.slice(3).replace(/^ /, "");
587
+ }
588
+ // comments (":") and "retry:" are intentionally ignored
589
+ }
590
+ }
591
+ const last = flush();
592
+ if (last)
593
+ yield last;
594
+ }
595
+ finally {
596
+ reader.releaseLock();
597
+ }
598
+ }
599
+ /** Default rendering for debug events (the boolean debug:true sink). */
600
+ export function formatDebugEvent(name, event) {
601
+ return name + " " + event.method + " " + event.path +
602
+ " -> " + (event.status !== undefined ? String(event.status) : "error") +
603
+ " (" + event.durationMs + "ms)" +
604
+ (event.attempt > 1 ? " attempt " + event.attempt : "") +
605
+ (event.requestId !== undefined ? " " + event.requestId : "") +
606
+ (event.error !== undefined ? ": " + event.error : "");
607
+ }
608
+ /** Wrap a bearer credential (static or callback) as an Authorization value. */
609
+ export function bearerAuth(token) {
610
+ if (typeof token === "function") {
611
+ return async () => "Bearer " + (await token());
612
+ }
613
+ return "Bearer " + token;
614
+ }
615
+ /**
616
+ * OAuth2 client-credentials token source: fetches from the token URL,
617
+ * caches until expiry (60s early refresh), and shares one in-flight
618
+ * request across concurrent callers. Returned function plugs in as an
619
+ * Authorization AuthValue, resolved before every attempt.
620
+ */
621
+ export function oauthClientCredentials(config) {
622
+ let token;
623
+ let expiresAt = 0;
624
+ let inflight;
625
+ const fetchImpl = config.fetchImpl ?? fetch;
626
+ async function fetchToken() {
627
+ const params = new URLSearchParams({ grant_type: "client_credentials" });
628
+ if (config.scopes !== undefined && config.scopes.length > 0) {
629
+ params.set("scope", config.scopes.join(" "));
630
+ }
631
+ for (const [key, value] of Object.entries(config.tokenParams ?? {})) {
632
+ params.set(key, value);
633
+ }
634
+ const headers = {
635
+ "Content-Type": "application/x-www-form-urlencoded",
636
+ Accept: "application/json",
637
+ };
638
+ if (config.authMethod === "basic") {
639
+ headers["Authorization"] = "Basic " + toBase64(config.clientId + ":" + config.clientSecret);
640
+ }
641
+ else {
642
+ params.set("client_id", config.clientId);
643
+ params.set("client_secret", config.clientSecret);
644
+ }
645
+ const response = await fetchImpl(config.tokenUrl, { method: "POST", headers, body: params.toString() });
646
+ const body = (await response.json().catch(() => null));
647
+ if (!response.ok || typeof body?.access_token !== "string") {
648
+ throw new TransportError("OAuth token request failed (HTTP " + response.status + (body?.error ? ": " + body.error : "") + ")", body);
649
+ }
650
+ token = body.access_token;
651
+ expiresAt = body.expires_in !== undefined
652
+ ? Date.now() + body.expires_in * 1000 - 60_000
653
+ : Number.MAX_SAFE_INTEGER;
654
+ return token;
655
+ }
656
+ return async () => {
657
+ if (token !== undefined && Date.now() < expiresAt)
658
+ return "Bearer " + token;
659
+ inflight ??= fetchToken().finally(() => { inflight = undefined; });
660
+ return "Bearer " + (await inflight);
661
+ };
662
+ }
663
+ /**
664
+ * Bracket-style deep encoding shared by query strings and form bodies:
665
+ * { created: { gte: 5 } } -> created[gte]=5, { items: [{ id: "x" }] } ->
666
+ * items[0][id]=x. Scalars append as-is.
667
+ */
668
+ function appendDeep(target, key, value) {
669
+ if (value === undefined || value === null)
670
+ return;
671
+ if (Array.isArray(value)) {
672
+ value.forEach((item, i) => appendDeep(target, key + "[" + i + "]", item));
673
+ }
674
+ else if (typeof value === "object" && !(value instanceof Blob) && !(value instanceof Date)) {
675
+ for (const [k, v] of Object.entries(value)) {
676
+ appendDeep(target, key + "[" + k + "]", v);
677
+ }
678
+ }
679
+ else {
680
+ target.append(key, value instanceof Date ? value.toISOString() : String(value));
681
+ }
682
+ }
683
+ function serializeBody(req) {
684
+ if (req.body === undefined)
685
+ return { body: undefined };
686
+ switch (req.bodyKind ?? "json") {
687
+ case "json":
688
+ return { body: JSON.stringify(req.body), contentType: "application/json" };
689
+ case "form": {
690
+ const params = new URLSearchParams();
691
+ for (const [k, v] of Object.entries(req.body)) {
692
+ appendDeep(params, k, v);
693
+ }
694
+ // URLSearchParams sets its own content type with the charset suffix.
695
+ return { body: params };
696
+ }
697
+ case "multipart": {
698
+ const form = new FormData();
699
+ for (const [k, v] of Object.entries(req.body)) {
700
+ if (v === undefined || v === null)
701
+ continue;
702
+ form.append(k, v instanceof Blob ? v : typeof v === "object" ? JSON.stringify(v) : String(v));
703
+ }
704
+ // Let fetch set the boundary header.
705
+ return { body: form };
706
+ }
707
+ case "text":
708
+ return { body: String(req.body), contentType: "text/plain" };
709
+ case "binary":
710
+ return { body: req.body };
711
+ }
712
+ }
713
+ async function parseBody(response, method) {
714
+ if (method === "HEAD" || response.status === 204 || response.status === 205)
715
+ return undefined;
716
+ const contentType = response.headers.get("content-type") ?? "";
717
+ try {
718
+ if (contentType.includes("json"))
719
+ return await response.json();
720
+ if (contentType.startsWith("text/"))
721
+ return await response.text();
722
+ if (response.body === null)
723
+ return undefined;
724
+ return await response.blob();
725
+ }
726
+ catch (cause) {
727
+ // A timed-out or aborted body read is a transport failure, not an
728
+ // empty body — surface it instead of faking success.
729
+ if (cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError"))
730
+ throw cause;
731
+ return undefined;
732
+ }
733
+ }
734
+ function meta(response) {
735
+ return {
736
+ status: response.status,
737
+ headers: response.headers,
738
+ requestId: response.headers.get("x-request-id") ??
739
+ response.headers.get("request-id") ??
740
+ undefined,
741
+ };
742
+ }
743
+ function retryAfterMs(response) {
744
+ const header = response.headers.get("retry-after");
745
+ if (!header)
746
+ return undefined;
747
+ const seconds = Number(header);
748
+ if (Number.isFinite(seconds))
749
+ return Math.min(seconds * 1000, 60_000);
750
+ const date = Date.parse(header);
751
+ if (!Number.isNaN(date))
752
+ return Math.min(Math.max(date - Date.now(), 0), 60_000);
753
+ return undefined;
754
+ }
755
+ /** Exponential backoff with full jitter, capped at 10s by default. */
756
+ function backoff(attempt, policy) {
757
+ const initial = policy?.initialDelayMs ?? 300;
758
+ const max = policy?.maxDelayMs ?? 10_000;
759
+ const cap = Math.min(initial * 2 ** attempt, max);
760
+ return Math.random() * cap;
761
+ }
762
+ function sleep(ms) {
763
+ return new Promise((resolve) => setTimeout(resolve, ms));
764
+ }
765
+ export function toBase64(input) {
766
+ if (typeof btoa === "function")
767
+ return btoa(input);
768
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
769
+ return globalThis.Buffer.from(input, "utf-8").toString("base64");
770
+ }