@typeship-ax/mcp 0.6.0 → 0.9.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 (114) hide show
  1. package/AGENTS.md +31 -0
  2. package/README.md +67 -10
  3. package/api.json +6735 -3243
  4. package/api.md +8537 -248
  5. package/dist/api-identity.d.ts +40 -0
  6. package/dist/api-identity.d.ts.map +1 -0
  7. package/dist/api-identity.js +128 -0
  8. package/dist/auth-profiles.d.ts +30 -0
  9. package/dist/auth-profiles.d.ts.map +1 -0
  10. package/dist/auth-profiles.js +138 -0
  11. package/dist/core/http.d.ts +21 -92
  12. package/dist/core/http.d.ts.map +1 -1
  13. package/dist/core/http.js +143 -221
  14. package/dist/core/pagination.d.ts.map +1 -1
  15. package/dist/core/pagination.js +6 -34
  16. package/dist/credential-storage.d.ts +24 -0
  17. package/dist/credential-storage.d.ts.map +1 -0
  18. package/dist/credential-storage.js +207 -0
  19. package/dist/dates.d.ts +0 -2
  20. package/dist/dates.d.ts.map +1 -1
  21. package/dist/dates.js +0 -1
  22. package/dist/docs.d.ts +36 -0
  23. package/dist/docs.d.ts.map +1 -0
  24. package/dist/docs.js +258 -0
  25. package/dist/errors.d.ts +42 -34
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +30 -20
  28. package/dist/index.d.ts +27 -12
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +40 -14
  31. package/dist/mcp-authorization.d.ts +52 -0
  32. package/dist/mcp-authorization.d.ts.map +1 -0
  33. package/dist/mcp-authorization.js +232 -0
  34. package/dist/mcp-protocol.d.ts +69 -25
  35. package/dist/mcp-protocol.d.ts.map +1 -1
  36. package/dist/mcp-protocol.js +386 -138
  37. package/dist/mcp.d.ts +21 -3
  38. package/dist/mcp.d.ts.map +1 -1
  39. package/dist/mcp.js +199 -85
  40. package/dist/named-credentials.d.ts +21 -0
  41. package/dist/named-credentials.d.ts.map +1 -0
  42. package/dist/named-credentials.js +86 -0
  43. package/dist/oauth-request.d.ts +21 -0
  44. package/dist/oauth-request.d.ts.map +1 -0
  45. package/dist/oauth-request.js +119 -0
  46. package/dist/oauth-session.d.ts +106 -0
  47. package/dist/oauth-session.d.ts.map +1 -0
  48. package/dist/oauth-session.js +244 -0
  49. package/dist/ops.d.ts +18 -0
  50. package/dist/ops.d.ts.map +1 -1
  51. package/dist/ops.js +31 -17
  52. package/dist/resources/account.d.ts +4 -4
  53. package/dist/resources/account.d.ts.map +1 -1
  54. package/dist/resources/account.js +1 -0
  55. package/dist/resources/api-keys.d.ts +13 -8
  56. package/dist/resources/api-keys.d.ts.map +1 -1
  57. package/dist/resources/api-keys.js +5 -1
  58. package/dist/resources/definition-revisions.d.ts +58 -0
  59. package/dist/resources/definition-revisions.d.ts.map +1 -0
  60. package/dist/resources/definition-revisions.js +114 -0
  61. package/dist/resources/definitions.d.ts +35 -0
  62. package/dist/resources/definitions.d.ts.map +1 -0
  63. package/dist/resources/definitions.js +60 -0
  64. package/dist/resources/generate.d.ts +18 -7
  65. package/dist/resources/generate.d.ts.map +1 -1
  66. package/dist/resources/generate.js +13 -5
  67. package/dist/resources/generations.d.ts +6 -6
  68. package/dist/resources/generations.d.ts.map +1 -1
  69. package/dist/resources/generations.js +3 -1
  70. package/dist/resources/projects.d.ts +111 -35
  71. package/dist/resources/projects.d.ts.map +1 -1
  72. package/dist/resources/projects.js +125 -15
  73. package/dist/resources/targets.d.ts +97 -0
  74. package/dist/resources/targets.d.ts.map +1 -0
  75. package/dist/resources/targets.js +197 -0
  76. package/dist/schemas.d.ts.map +1 -1
  77. package/dist/schemas.js +135 -62
  78. package/dist/types.d.ts +2072 -267
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js +20 -3
  81. package/dist/worker.js +4 -4
  82. package/package.json +11 -1
  83. package/server.json +42 -0
  84. package/src/api-identity.ts +98 -0
  85. package/src/auth-profiles.ts +114 -0
  86. package/src/core/http.ts +156 -305
  87. package/src/core/pagination.ts +6 -30
  88. package/src/credential-storage.ts +183 -0
  89. package/src/dates.ts +0 -1
  90. package/src/docs.ts +239 -0
  91. package/src/errors.ts +52 -41
  92. package/src/index.ts +49 -14
  93. package/src/mcp-authorization.ts +211 -0
  94. package/src/mcp-protocol.ts +432 -133
  95. package/src/mcp.ts +204 -90
  96. package/src/named-credentials.ts +74 -0
  97. package/src/oauth-request.ts +90 -0
  98. package/src/oauth-session.ts +258 -0
  99. package/src/ops.ts +56 -17
  100. package/src/resources/account.ts +6 -3
  101. package/src/resources/api-keys.ts +27 -7
  102. package/src/resources/definition-revisions.ts +207 -0
  103. package/src/resources/definitions.ts +122 -0
  104. package/src/resources/generate.ts +29 -6
  105. package/src/resources/generations.ts +9 -4
  106. package/src/resources/projects.ts +274 -41
  107. package/src/resources/targets.ts +378 -0
  108. package/src/schemas.ts +135 -62
  109. package/src/types.ts +2273 -322
  110. package/src/worker.ts +4 -4
  111. package/dist/resources/spec-revisions.d.ts +0 -47
  112. package/dist/resources/spec-revisions.d.ts.map +0 -1
  113. package/dist/resources/spec-revisions.js +0 -90
  114. package/src/resources/spec-revisions.ts +0 -150
package/src/core/http.ts CHANGED
@@ -17,7 +17,12 @@ export interface RequestOptions {
17
17
  export interface ResponseMeta {
18
18
  status: number;
19
19
  headers: Headers;
20
- /** x-request-id / request-id header when the server sends one. */
20
+ /** Parsed wire body before it is narrowed to the generated response type.
21
+ * Use this escape hatch for additive fields or variants introduced after
22
+ * this generator edition. */
23
+ rawBody?: unknown;
24
+ /** Request identifier from the JSON response body, or from headers for
25
+ * raw and bodyless responses. */
21
26
  requestId?: string;
22
27
  }
23
28
 
@@ -37,15 +42,6 @@ export interface RequestContext {
37
42
  attempt: number;
38
43
  }
39
44
 
40
- /** One server-sent event from a text/event-stream response. */
41
- export interface SseEvent {
42
- /** The `event:` field; undefined for unnamed events. */
43
- event?: string;
44
- /** The `id:` field, when the server sends one. */
45
- id?: string;
46
- /** Concatenated `data:` lines. Parse as JSON if your API sends JSON. */
47
- data: string;
48
- }
49
45
 
50
46
  /**
51
47
  * Every SDK call returns a discriminated result instead of throwing.
@@ -85,97 +81,23 @@ export class UnexpectedApiError extends ApiError<number, unknown> {
85
81
  }
86
82
  }
87
83
 
88
- /** A 200 response whose GraphQL payload carried errors. */
89
- export class GraphQLRequestError extends Error {
90
- /** The raw errors array from the GraphQL response. */
91
- readonly errors: { message?: string; path?: unknown[]; extensions?: unknown }[];
84
+ /** A successful response declared JSON but carried a body that could not be parsed. */
85
+ export class ResponseParseError extends Error {
86
+ readonly status: number;
87
+ readonly body: string;
92
88
  readonly response: ResponseMeta;
89
+ override readonly cause?: unknown;
93
90
 
94
- constructor(errors: unknown[], response: ResponseMeta) {
95
- const first = (errors[0] as { message?: string } | undefined)?.message;
96
- super(first ?? "GraphQL request returned errors");
97
- this.name = "GraphQLRequestError";
98
- this.errors = errors as GraphQLRequestError["errors"];
91
+ constructor(body: string, response: ResponseMeta, cause?: unknown) {
92
+ super("HTTP " + response.status + " response body was not valid JSON");
93
+ this.name = "ResponseParseError";
94
+ this.status = response.status;
95
+ this.body = body;
99
96
  this.response = response;
97
+ this.cause = cause;
100
98
  }
101
99
  }
102
100
 
103
- // ---------------------------------------------------------------------------
104
- // GraphQL selections — typed field picking over the schema's result types
105
- // ---------------------------------------------------------------------------
106
-
107
- type Primitive = string | number | boolean | bigint | symbol | null | undefined;
108
- type Unwrap<T> = NonNullable<T> extends readonly (infer U)[] ? Unwrap<U> : NonNullable<T>;
109
- type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
110
- type TypeNameOf<T> = T extends { __typename?: infer N } ? Extract<N, string> : never;
111
- type Prettify<T> = { [K in keyof T]: T[K] } & {};
112
-
113
- /**
114
- * A selection over a GraphQL result type `T`: `true` picks a leaf field, a
115
- * nested object picks inside an object field, and `on` picks per concrete
116
- * type when `T` is a union or interface (`{ on: { Transaction: { amount: true } } }`).
117
- * Keys are checked against the schema, so a typo is a compile error.
118
- */
119
- export type Selection<T> = Unwrap<T> extends Primitive ? never : SelectionObject<Unwrap<T>>;
120
-
121
- type FieldSelection<V> = Unwrap<V> extends Primitive ? true : SelectionObject<Unwrap<V>>;
122
-
123
- type SelectionObject<T> = {
124
- [K in Extract<keyof T, string> as K extends "__typename" ? never : K]?: FieldSelection<T[K]>;
125
- } & { __typename?: true } & (IsUnion<T> extends true
126
- ? { on?: { [N in TypeNameOf<T>]?: SelectionObject<Extract<T, { __typename?: N }>> } }
127
- : { on?: never });
128
-
129
- /** The result type a {@link Selection} `S` produces from result type `T`:
130
- * exactly the fields picked, nullability and lists preserved, union members
131
- * narrowed by `__typename`. */
132
- export type Selected<T, S> = T extends readonly (infer U)[]
133
- ? Selected<U, S>[]
134
- : T extends Primitive
135
- ? T
136
- : S extends object
137
- ? Prettify<SelectedMember<T, S>>
138
- : never;
139
-
140
- type SelectedMember<T, S> = (S extends { on?: infer O }
141
- ? O extends object
142
- ? TypeNameOf<T> extends keyof O
143
- ? PickSelected<T, NonNullable<O[TypeNameOf<T>]>>
144
- : {}
145
- : {}
146
- : {}) &
147
- PickSelected<T, S>;
148
-
149
- type PickSelected<T, S> = {
150
- -readonly [K in keyof S as K extends "on" ? never : K extends keyof T ? (S[K] extends true | object ? K : never) : never]-?: K extends "__typename"
151
- ? NonNullable<T[K & keyof T]>
152
- : S[K] extends true
153
- ? Exclude<T[K & keyof T], undefined>
154
- : Selected<Exclude<T[K & keyof T], undefined>, S[K]>;
155
- };
156
-
157
- /** A selection object or a raw selection set (`"{ id name }"`). */
158
- export type SelectionInput = string | Record<string, unknown>;
159
-
160
- /** Serialize a selection object to a GraphQL selection set. Strings pass
161
- * through untouched. */
162
- export function selectionToString(selection: SelectionInput): string {
163
- if (typeof selection === "string") return selection;
164
- const parts: string[] = [];
165
- for (const [key, value] of Object.entries(selection)) {
166
- if (!value) continue;
167
- if (key === "on" && typeof value === "object") {
168
- for (const [typeName, sub] of Object.entries(value as Record<string, unknown>)) {
169
- if (sub && typeof sub === "object") parts.push("... on " + typeName + " " + selectionToString(sub as Record<string, unknown>));
170
- }
171
- } else if (value === true) {
172
- parts.push(key);
173
- } else if (typeof value === "object") {
174
- parts.push(key + " " + selectionToString(value as Record<string, unknown>));
175
- }
176
- }
177
- return "{ " + (parts.length > 0 ? parts.join(" ") : "__typename") + " }";
178
- }
179
101
 
180
102
  // ---------------------------------------------------------------------------
181
103
  // Optional runtime validation — zero-dependency, schema table in schemas.ts
@@ -317,7 +239,7 @@ function transportFailureMessage(method: string, url: string, cause: unknown): s
317
239
  return method + " " + url + " failed: " + detail;
318
240
  }
319
241
 
320
- /** The request never produced an HTTP response (network failure, timeout, abort). */
242
+ /** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body). */
321
243
  export class TransportError extends Error {
322
244
  override readonly cause?: unknown;
323
245
 
@@ -331,6 +253,7 @@ export class TransportError extends Error {
331
253
  type ErrorCtor = new (body: any, response: ResponseMeta) => ApiError<number, unknown>;
332
254
 
333
255
  export interface CoreRequest {
256
+ security?: Record<string, string[]>[];
334
257
  method: string;
335
258
  path: string;
336
259
  query?: Record<string, unknown>;
@@ -341,14 +264,9 @@ export interface CoreRequest {
341
264
  errors?: Record<string, ErrorCtor>;
342
265
  /** Idempotent requests are retried automatically. */
343
266
  idempotent?: boolean;
344
- /** Success body is text/event-stream: yield SseEvents instead of parsing. */
345
- stream?: boolean;
346
267
  /** Header name auto-filled with one UUID per call (stable across retries)
347
268
  * when the caller doesn't supply a value. */
348
269
  idempotencyKey?: string;
349
- /** GraphQL: unwrap body.data[field] and turn body.errors into a
350
- * GraphQLRequestError. */
351
- graphqlField?: string;
352
270
  /** Key into the schemas table for optional runtime validation. */
353
271
  schemaKey?: string;
354
272
  /** Operation-level retry policy (x-typeship-retries), merged over the
@@ -368,7 +286,39 @@ export interface RetryPolicy {
368
286
  retryNonIdempotent?: boolean;
369
287
  }
370
288
 
289
+ export interface SecurityCredential {
290
+ headers?: Record<string, AuthValue>;
291
+ query?: Record<string, AuthValue>;
292
+ }
293
+
294
+ /** Resolve one complete alternative in spec order. Missing or incompatible
295
+ * combinations contribute no partial credentials. Anonymous operations send
296
+ * no generated credentials; explicit headers/hooks remain application owned. */
297
+ function selectSecurity(requirements: Record<string, string[]>[], credentials: Record<string, SecurityCredential>): SecurityCredential {
298
+ for (const requirement of requirements) {
299
+ const names = Object.keys(requirement);
300
+ if (!names.length || names.some((name) => !Object.hasOwn(credentials, name))) continue;
301
+ const selected: SecurityCredential = { headers: Object.create(null), query: Object.create(null) };
302
+ const destinations = new Set<string>();
303
+ let conflict = false;
304
+ for (const name of names) {
305
+ for (const location of ["headers", "query"] as const) {
306
+ for (const [wire, value] of Object.entries(credentials[name]![location] ?? {})) {
307
+ const destination = location + ":" + (location === "headers" ? wire.toLowerCase() : wire);
308
+ if (destinations.has(destination)) conflict = true;
309
+ destinations.add(destination);
310
+ selected[location]![wire] = value;
311
+ }
312
+ }
313
+ }
314
+ if (!conflict) return selected;
315
+ }
316
+ return {};
317
+ }
318
+
371
319
  export interface CoreConfig {
320
+ /** Named credentials are selected using each operation's requirements. */
321
+ credentials?: Record<string, SecurityCredential>;
372
322
  baseUrl: string;
373
323
  headers: Record<string, AuthValue>;
374
324
  /** Auth carried as query parameters (apiKey-in-query schemes). */
@@ -417,7 +367,7 @@ export interface DebugEvent {
417
367
  /** 1-based; >1 means this was a retry */
418
368
  attempt: number;
419
369
  requestId?: string;
420
- /** transport failure message, when there was no response */
370
+ /** transport failure message, when the request failed before a complete response */
421
371
  error?: string;
422
372
  }
423
373
 
@@ -476,8 +426,14 @@ export class HttpCore {
476
426
  const policy: RetryPolicy = { ...this.config.retry, ...req.retry };
477
427
  const maxRetries = req.options?.maxRetries ?? policy.maxRetries ?? this.config.maxRetries;
478
428
  const timeoutMs = req.options?.timeoutMs ?? this.config.timeoutMs;
479
- const retryAllowed = req.idempotent === true || req.method === "GET" || policy.retryNonIdempotent === true;
480
- const retryableStatuses = policy.statuses ? new Set(policy.statuses) : RETRYABLE_STATUSES;
429
+ const retryAllowed =
430
+ req.idempotent === true ||
431
+ req.method === "GET" ||
432
+ req.idempotencyKey !== undefined ||
433
+ policy.retryNonIdempotent === true;
434
+ const retryableStatuses = policy.statuses
435
+ ? new Set(policy.statuses)
436
+ : RETRYABLE_STATUSES;
481
437
 
482
438
  // One key per logical call, reused on every retry — that's the point
483
439
  // of idempotency keys.
@@ -509,16 +465,20 @@ export class HttpCore {
509
465
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
510
466
  let response: Response;
511
467
  const attemptStarted = Date.now();
468
+ const emitResponseDebug = (value: Response, body?: unknown) => this.config.debug?.({
469
+ method: req.method,
470
+ path: req.path,
471
+ status: value.status,
472
+ durationMs: Date.now() - attemptStarted,
473
+ attempt: attempt + 1,
474
+ requestId:
475
+ requestIdFromBody(body) ??
476
+ value.headers.get("request-id") ??
477
+ value.headers.get("x-request-id") ??
478
+ undefined,
479
+ });
512
480
  try {
513
481
  response = await this.send(req, timeoutMs, attempt, autoIdempotencyKey);
514
- this.config.debug?.({
515
- method: req.method,
516
- path: req.path,
517
- status: response.status,
518
- durationMs: Date.now() - attemptStarted,
519
- attempt: attempt + 1,
520
- requestId: response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? undefined,
521
- });
522
482
  } catch (cause) {
523
483
  lastError = cause;
524
484
  this.config.debug?.({
@@ -541,23 +501,26 @@ export class HttpCore {
541
501
  }
542
502
 
543
503
  if (response.ok) {
544
- if (req.stream) {
545
- return { ok: true, data: sseEvents(response) as T, response: meta(response) };
546
- }
547
504
  let data: T;
548
505
  try {
549
506
  data = (await parseBody(response, req.method)) as T;
550
507
  } catch (cause) {
551
- const error = new TransportError(
552
- "The response body read was aborted before completing",
508
+ const parseError = cause instanceof ResponseParseError ? cause : undefined;
509
+ emitResponseDebug(response, parseError?.body);
510
+ const error = (parseError ?? new TransportError(
511
+ "The response body read failed before completing",
553
512
  cause,
554
- ) as unknown as E;
513
+ )) as unknown as E;
555
514
  await this.config.onError?.(error, { method: req.method, path: req.path });
556
- return { ok: false, error, response: meta(response) };
515
+ return { ok: false, error, response: parseError?.response ?? meta(response) };
557
516
  }
558
- if (opSchemas?.res && this.config.validate!.responses && data !== undefined) {
517
+ emitResponseDebug(response, data);
518
+ const responseMeta = meta(response, data);
519
+ let responseData: unknown = data;
520
+ let shouldValidateResponse = responseData !== undefined;
521
+ if (opSchemas?.res && this.config.validate!.responses && shouldValidateResponse) {
559
522
  const violations: Violation[] = [];
560
- validateAgainstSchema(data, opSchemas.res, "response", violations, this.config.schemaDefs);
523
+ validateAgainstSchema(responseData, opSchemas.res, "response", violations, this.config.schemaDefs);
561
524
  if (violations.length > 0) {
562
525
  const validationError = new ValidationError("response", violations);
563
526
  if (this.config.validate!.mode === "warn") {
@@ -565,21 +528,11 @@ export class HttpCore {
565
528
  } else {
566
529
  const error = validationError as unknown as E;
567
530
  await this.config.onError?.(error, { method: req.method, path: req.path });
568
- return { ok: false, error, response: meta(response) };
531
+ return { ok: false, error, response: responseMeta };
569
532
  }
570
533
  }
571
534
  }
572
- if (req.graphqlField) {
573
- const payload = data as { data?: Record<string, unknown>; errors?: unknown[] } | undefined;
574
- const responseMeta = meta(response);
575
- if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
576
- const gqlError = new GraphQLRequestError(payload.errors, responseMeta) as unknown as E;
577
- await this.config.onError?.(gqlError, { method: req.method, path: req.path });
578
- return { ok: false, error: gqlError, response: responseMeta };
579
- }
580
- return { ok: true, data: payload?.data?.[req.graphqlField] as T, response: responseMeta };
581
- }
582
- return { ok: true, data, response: meta(response) };
535
+ return { ok: true, data, response: responseMeta };
583
536
  }
584
537
 
585
538
  // 429 is safe to retry regardless of idempotency; other retryable
@@ -588,7 +541,15 @@ export class HttpCore {
588
541
  retryableStatuses.has(response.status) &&
589
542
  (retryAllowed || response.status === 429);
590
543
  if (attempt < maxRetries && retryableStatus) {
591
- await sleep(retryAfterMs(response) ?? backoff(attempt, policy));
544
+ const delay = retryAfterMs(response) ?? backoff(attempt, policy);
545
+ let retryBody: unknown;
546
+ try {
547
+ retryBody = await parseBody(response, req.method);
548
+ } catch {
549
+ retryBody = undefined;
550
+ }
551
+ emitResponseDebug(response, retryBody);
552
+ await sleep(delay);
592
553
  continue;
593
554
  }
594
555
 
@@ -598,7 +559,8 @@ export class HttpCore {
598
559
  } catch {
599
560
  body = undefined; // error responses keep their status even if the body read aborts
600
561
  }
601
- const responseMeta = meta(response);
562
+ emitResponseDebug(response, body);
563
+ const responseMeta = meta(response, body);
602
564
  const Ctor =
603
565
  req.errors?.[String(response.status)] ??
604
566
  req.errors?.[String(Math.floor(response.status / 100)) + "XX"] ??
@@ -622,8 +584,9 @@ export class HttpCore {
622
584
  attempt: number,
623
585
  autoIdempotencyKey?: string,
624
586
  ): Promise<Response> {
587
+ const selected = req.security && this.config.credentials ? selectSecurity(req.security, this.config.credentials) : {};
625
588
  const headers: Record<string, string> = {};
626
- for (const [k, v] of Object.entries(this.config.headers)) {
589
+ for (const [k, v] of Object.entries({ ...this.config.headers, ...selected.headers })) {
627
590
  headers[k] = await resolveAuthValue(v);
628
591
  }
629
592
  if (req.idempotencyKey && autoIdempotencyKey) {
@@ -639,34 +602,20 @@ export class HttpCore {
639
602
 
640
603
  const context: RequestContext = {
641
604
  method: req.method,
642
- url: await this.buildUrl(req),
605
+ url: await this.buildUrl(req, selected.query),
643
606
  headers,
644
607
  attempt,
645
608
  };
646
609
  await this.config.onRequest?.(context);
647
610
 
648
- // Streaming responses are exempt from the attempt timeout once headers
649
- // arrive (an event stream may stay open far longer than timeoutMs);
650
- // everything else keeps the timeout armed through the body read, so a
651
- // stalled body aborts instead of hanging.
652
611
  const signals: AbortSignal[] = [];
653
- let clearStreamTimeout: (() => void) | undefined;
654
- if (req.stream) {
655
- const headersTimeout = new AbortController();
656
- const timer = setTimeout(
657
- () => headersTimeout.abort(new DOMException("Timed out waiting for response headers", "TimeoutError")),
658
- timeoutMs,
659
- );
660
- (timer as { unref?: () => void }).unref?.();
661
- clearStreamTimeout = () => clearTimeout(timer);
662
- signals.push(headersTimeout.signal);
663
- } else {
612
+ {
664
613
  signals.push(AbortSignal.timeout(timeoutMs));
665
614
  }
666
615
  if (req.options?.signal) signals.push(req.options.signal);
667
616
 
668
617
  try {
669
- const signal = AbortSignal.any(signals);
618
+ const signal = composeSignals(signals);
670
619
  const response = this.manualRedirects
671
620
  ? await this.followRedirects(context, body, signal)
672
621
  : await this.config.fetch(context.url, {
@@ -678,7 +627,6 @@ export class HttpCore {
678
627
  await this.config.onResponse?.(response, context);
679
628
  return response;
680
629
  } finally {
681
- clearStreamTimeout?.();
682
630
  }
683
631
  }
684
632
 
@@ -742,7 +690,7 @@ export class HttpCore {
742
690
  }
743
691
  }
744
692
 
745
- private async buildUrl(req: CoreRequest): Promise<string> {
693
+ private async buildUrl(req: CoreRequest, authQuery?: Record<string, AuthValue>): Promise<string> {
746
694
  const base = this.config.baseUrl.replace(/\/+$/, "");
747
695
  const url = new URL(base + req.path);
748
696
  for (const [k, v] of Object.entries(req.query ?? {})) {
@@ -754,7 +702,7 @@ export class HttpCore {
754
702
  appendDeep(url.searchParams, k, v);
755
703
  }
756
704
  }
757
- for (const [k, v] of Object.entries(this.config.query)) {
705
+ for (const [k, v] of Object.entries({ ...this.config.query, ...authQuery })) {
758
706
  url.searchParams.append(k, await resolveAuthValue(v));
759
707
  }
760
708
  return url.toString();
@@ -765,55 +713,6 @@ async function resolveAuthValue(value: AuthValue): Promise<string> {
765
713
  return typeof value === "function" ? await value() : value;
766
714
  }
767
715
 
768
- /** Parse a text/event-stream body into SseEvents, lazily. */
769
- async function* sseEvents(response: Response): AsyncGenerator<SseEvent, void, undefined> {
770
- if (!response.body) return;
771
- const reader = response.body.getReader();
772
- const decoder = new TextDecoder();
773
- let buffer = "";
774
- let dataLines: string[] = [];
775
- let eventName: string | undefined;
776
- let eventId: string | undefined;
777
-
778
- const flush = (): SseEvent | undefined => {
779
- if (dataLines.length === 0) return undefined;
780
- const event: SseEvent = { data: dataLines.join("\n") };
781
- if (eventName !== undefined) event.event = eventName;
782
- if (eventId !== undefined) event.id = eventId;
783
- dataLines = [];
784
- eventName = undefined;
785
- return event;
786
- };
787
-
788
- try {
789
- while (true) {
790
- const { done, value } = await reader.read();
791
- if (done) break;
792
- buffer += decoder.decode(value, { stream: true });
793
- let newline: number;
794
- while ((newline = buffer.indexOf("\n")) !== -1) {
795
- const hasCr = newline > 0 && buffer[newline - 1] === "\r";
796
- const line = buffer.slice(0, hasCr ? newline - 1 : newline);
797
- buffer = buffer.slice(newline + 1);
798
- if (line === "") {
799
- const event = flush();
800
- if (event) yield event;
801
- } else if (line.startsWith("data:")) {
802
- dataLines.push(line.slice(5).replace(/^ /, ""));
803
- } else if (line.startsWith("event:")) {
804
- eventName = line.slice(6).replace(/^ /, "");
805
- } else if (line.startsWith("id:")) {
806
- eventId = line.slice(3).replace(/^ /, "");
807
- }
808
- // comments (":") and "retry:" are intentionally ignored
809
- }
810
- }
811
- const last = flush();
812
- if (last) yield last;
813
- } finally {
814
- reader.releaseLock();
815
- }
816
- }
817
716
 
818
717
  /** Default rendering for debug events (the boolean debug:true sink). */
819
718
  export function formatDebugEvent(name: string, event: DebugEvent): string {
@@ -833,74 +732,6 @@ export function bearerAuth(token: AuthValue): AuthValue {
833
732
  return "Bearer " + token;
834
733
  }
835
734
 
836
- export interface ClientCredentialsConfig {
837
- clientId: string;
838
- clientSecret: string;
839
- tokenUrl: string;
840
- scopes?: string[];
841
- /** Extra token-request parameters, e.g. the "audience" some
842
- * authorization servers require for API-valid access tokens. */
843
- tokenParams?: Record<string, string>;
844
- /** How credentials reach the token endpoint. Default "post"
845
- * (client_secret_post, form fields); "basic" sends an Authorization
846
- * header (client_secret_basic). */
847
- authMethod?: "post" | "basic";
848
- fetchImpl?: typeof fetch;
849
- }
850
-
851
- /**
852
- * OAuth2 client-credentials token source: fetches from the token URL,
853
- * caches until expiry (60s early refresh), and shares one in-flight
854
- * request across concurrent callers. Returned function plugs in as an
855
- * Authorization AuthValue, resolved before every attempt.
856
- */
857
- export function oauthClientCredentials(config: ClientCredentialsConfig): () => Promise<string> {
858
- let token: string | undefined;
859
- let expiresAt = 0;
860
- let inflight: Promise<string> | undefined;
861
- const fetchImpl = config.fetchImpl ?? fetch;
862
-
863
- async function fetchToken(): Promise<string> {
864
- const params = new URLSearchParams({ grant_type: "client_credentials" });
865
- if (config.scopes !== undefined && config.scopes.length > 0) {
866
- params.set("scope", config.scopes.join(" "));
867
- }
868
- for (const [key, value] of Object.entries(config.tokenParams ?? {})) {
869
- params.set(key, value);
870
- }
871
- const headers: Record<string, string> = {
872
- "Content-Type": "application/x-www-form-urlencoded",
873
- Accept: "application/json",
874
- };
875
- if (config.authMethod === "basic") {
876
- headers["Authorization"] = "Basic " + toBase64(config.clientId + ":" + config.clientSecret);
877
- } else {
878
- params.set("client_id", config.clientId);
879
- params.set("client_secret", config.clientSecret);
880
- }
881
- const response = await fetchImpl(config.tokenUrl, { method: "POST", headers, body: params.toString() });
882
- const body = (await response.json().catch(() => null)) as
883
- | { access_token?: string; expires_in?: number; error?: string }
884
- | null;
885
- if (!response.ok || typeof body?.access_token !== "string") {
886
- throw new TransportError(
887
- "OAuth token request failed (HTTP " + response.status + (body?.error ? ": " + body.error : "") + ")",
888
- body,
889
- );
890
- }
891
- token = body.access_token;
892
- expiresAt = body.expires_in !== undefined
893
- ? Date.now() + body.expires_in * 1000 - 60_000
894
- : Number.MAX_SAFE_INTEGER;
895
- return token;
896
- }
897
-
898
- return async () => {
899
- if (token !== undefined && Date.now() < expiresAt) return "Bearer " + token;
900
- inflight ??= fetchToken().finally(() => { inflight = undefined; });
901
- return "Bearer " + (await inflight);
902
- };
903
- }
904
735
 
905
736
  /**
906
737
  * Bracket-style deep encoding shared by query strings and form bodies:
@@ -925,60 +756,80 @@ function serializeBody(req: CoreRequest): { body: NonNullable<RequestInit["body"
925
756
  switch (req.bodyKind ?? "json") {
926
757
  case "json":
927
758
  return { body: JSON.stringify(req.body), contentType: "application/json" };
928
- case "form": {
929
- const params = new URLSearchParams();
930
- for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
931
- appendDeep(params, k, v);
932
- }
933
- // URLSearchParams sets its own content type with the charset suffix.
934
- return { body: params };
935
- }
936
- case "multipart": {
937
- const form = new FormData();
938
- for (const [k, v] of Object.entries(req.body as Record<string, unknown>)) {
939
- if (v === undefined || v === null) continue;
940
- form.append(
941
- k,
942
- v instanceof Blob ? v : typeof v === "object" ? JSON.stringify(v) : String(v),
943
- );
944
- }
945
- // Let fetch set the boundary header.
946
- return { body: form };
947
- }
948
- case "text":
949
- return { body: String(req.body), contentType: "text/plain" };
950
- case "binary":
951
- return { body: req.body as NonNullable<RequestInit["body"]> };
952
759
  }
760
+ throw new Error("Unsupported request body kind: " + String(req.bodyKind));
953
761
  }
954
762
 
955
763
  async function parseBody(response: Response, method: string): Promise<unknown> {
956
764
  if (method === "HEAD" || response.status === 204 || response.status === 205) return undefined;
957
- const contentType = response.headers.get("content-type") ?? "";
765
+ const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
766
+ const mediaType = contentType.split(";", 1)[0]!.trim();
767
+ const slash = mediaType.indexOf("/");
768
+ const subtype = slash === -1 ? "" : mediaType.slice(slash + 1);
958
769
  try {
959
- if (contentType.includes("json")) return await response.json();
770
+ if (subtype === "json" || subtype.endsWith("+json")) {
771
+ const body = await response.text();
772
+ if (body.length === 0) return undefined;
773
+ try {
774
+ return JSON.parse(body);
775
+ } catch (cause) {
776
+ throw new ResponseParseError(body, meta(response, body), cause);
777
+ }
778
+ }
960
779
  if (contentType.startsWith("text/")) return await response.text();
961
780
  if (response.body === null) return undefined;
962
781
  return await response.blob();
963
782
  } catch (cause) {
964
- // A timed-out or aborted body read is a transport failure, not an
965
- // empty body — surface it instead of faking success.
966
- if (cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError")) throw cause;
967
- return undefined;
783
+ if (cause instanceof ResponseParseError) throw cause;
784
+ // A timed-out, aborted, or prematurely terminated body is a transport
785
+ // failure, not an empty body — surface it instead of faking success.
786
+ throw cause;
968
787
  }
969
788
  }
970
789
 
971
- function meta(response: Response): ResponseMeta {
790
+ function requestIdFromBody(body: unknown): string | undefined {
791
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
792
+ const value = (body as Record<string, unknown>).request_id ?? (body as Record<string, unknown>).requestId;
793
+ return typeof value === "string" && value.length > 0 ? value : undefined;
794
+ }
795
+
796
+ function meta(response: Response, body?: unknown): ResponseMeta {
972
797
  return {
973
798
  status: response.status,
974
799
  headers: response.headers,
800
+ ...(body !== undefined ? { rawBody: body } : {}),
975
801
  requestId:
976
- response.headers.get("x-request-id") ??
802
+ requestIdFromBody(body) ??
977
803
  response.headers.get("request-id") ??
804
+ response.headers.get("x-request-id") ??
978
805
  undefined,
979
806
  };
980
807
  }
981
808
 
809
+ /** AbortSignal.any reached Node in 18.17. Keep the package's Node 18 floor
810
+ * honest for earlier 18.x releases and for web runtimes without it. */
811
+ function composeSignals(signals: AbortSignal[]): AbortSignal {
812
+ const nativeAny = (AbortSignal as typeof AbortSignal & { any?: (items: AbortSignal[]) => AbortSignal }).any;
813
+ if (nativeAny) return nativeAny.call(AbortSignal, signals);
814
+ const controller = new AbortController();
815
+ const listeners = new Map<AbortSignal, () => void>();
816
+ const abortFrom = (signal: AbortSignal) => {
817
+ for (const [item, listener] of listeners) item.removeEventListener("abort", listener);
818
+ listeners.clear();
819
+ controller.abort(signal.reason);
820
+ };
821
+ for (const signal of signals) {
822
+ if (signal.aborted) {
823
+ abortFrom(signal);
824
+ break;
825
+ }
826
+ const listener = () => abortFrom(signal);
827
+ listeners.set(signal, listener);
828
+ signal.addEventListener("abort", listener, { once: true });
829
+ }
830
+ return controller.signal;
831
+ }
832
+
982
833
  function retryAfterMs(response: Response): number | undefined {
983
834
  const header = response.headers.get("retry-after");
984
835
  if (!header) return undefined;