@crowdstrike/aidr 1.0.2

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 (51) hide show
  1. package/.editorconfig +9 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/workflows/ci.yml +128 -0
  4. package/.pnpmfile.cjs +17 -0
  5. package/.releaserc.json +21 -0
  6. package/LICENSE.txt +21 -0
  7. package/README.md +3 -0
  8. package/biome.json +67 -0
  9. package/dist/chunk.cjs +34 -0
  10. package/dist/index.cjs +356 -0
  11. package/dist/index.d.cts +2347 -0
  12. package/dist/index.d.mts +2347 -0
  13. package/dist/index.mjs +354 -0
  14. package/dist/schemas/ai-guard.cjs +1000 -0
  15. package/dist/schemas/ai-guard.d.cts +1232 -0
  16. package/dist/schemas/ai-guard.d.mts +1232 -0
  17. package/dist/schemas/ai-guard.mjs +907 -0
  18. package/dist/schemas/index.cjs +7 -0
  19. package/dist/schemas/index.d.cts +64 -0
  20. package/dist/schemas/index.d.mts +64 -0
  21. package/dist/schemas/index.mjs +3 -0
  22. package/dist/schemas.cjs +139 -0
  23. package/dist/schemas.mjs +108 -0
  24. package/flake.lock +59 -0
  25. package/flake.nix +26 -0
  26. package/openapi-ts.config.ts +15 -0
  27. package/package.json +55 -0
  28. package/pnpm-workspace.yaml +3 -0
  29. package/scripts/generate-models +15 -0
  30. package/scripts/test +10 -0
  31. package/specs/ai-guard.openapi.json +3721 -0
  32. package/src/client.ts +441 -0
  33. package/src/core/error.ts +78 -0
  34. package/src/index.ts +2 -0
  35. package/src/internal/builtin-types.ts +18 -0
  36. package/src/internal/errors.ts +34 -0
  37. package/src/internal/headers.ts +100 -0
  38. package/src/internal/parse.ts +30 -0
  39. package/src/internal/request-options.ts +57 -0
  40. package/src/internal/types.ts +3 -0
  41. package/src/internal/utils/sleep.ts +3 -0
  42. package/src/internal/utils/values.ts +38 -0
  43. package/src/schemas/ai-guard.ts +1215 -0
  44. package/src/schemas/index.ts +114 -0
  45. package/src/services/ai-guard.ts +27 -0
  46. package/src/types/ai-guard.ts +2276 -0
  47. package/src/types/index.ts +161 -0
  48. package/tests/ai-guard.test.ts +29 -0
  49. package/tsconfig.json +26 -0
  50. package/tsdown.config.mts +14 -0
  51. package/vitest.config.mts +4 -0
package/src/client.ts ADDED
@@ -0,0 +1,441 @@
1
+ import * as v from 'valibot';
2
+
3
+ import * as Errors from './core/error';
4
+ import type { BodyInit, Fetch, RequestInfo } from './internal/builtin-types';
5
+ import { castToError } from './internal/errors';
6
+ import {
7
+ buildHeaders,
8
+ type HeadersLike,
9
+ type NullableHeaders,
10
+ } from './internal/headers';
11
+ import { type APIResponseProps, defaultParseResponse } from './internal/parse';
12
+ import type {
13
+ FinalRequestOptions,
14
+ RequestOptions,
15
+ } from './internal/request-options';
16
+ import type {
17
+ FinalizedRequestInit,
18
+ HTTPMethod,
19
+ PromiseOrValue,
20
+ } from './internal/types';
21
+ import { sleep } from './internal/utils/sleep';
22
+ import { isAbsoluteURL, stringifyQuery } from './internal/utils/values';
23
+ import { AcceptedResponseSchema } from './schemas';
24
+ import type { AcceptedResponse, MaybeAcceptedResponse } from './types';
25
+
26
+ function isAcceptedResponse(response: unknown): response is AcceptedResponse {
27
+ return v.safeParse(AcceptedResponseSchema, response).success;
28
+ }
29
+
30
+ export interface ClientOptions {
31
+ /** CS AIDR API token.*/
32
+ token: string;
33
+
34
+ /**
35
+ * Template for constructing the base URL for API requests. The placeholder
36
+ * `{SERVICE_NAME}` will be replaced with the service name slug.
37
+ */
38
+ baseURLTemplate: string;
39
+
40
+ /**
41
+ * The maximum number of times that the client will retry a request in case of
42
+ * a temporary failure, like a network error or a 5XX error from the server.
43
+ *
44
+ * @default 2
45
+ */
46
+ maxRetries?: number | undefined;
47
+
48
+ /**
49
+ * The maximum number of times that the client will poll for an async request
50
+ * result when receiving a HTTP/202 response.
51
+ *
52
+ * @default 5
53
+ */
54
+ maxPollingAttempts?: number | undefined;
55
+
56
+ /**
57
+ * The maximum amount of time (in milliseconds) that the client should wait for a response
58
+ * from the server before timing out a single request.
59
+ *
60
+ * @unit milliseconds
61
+ */
62
+ timeout?: number;
63
+
64
+ /**
65
+ * Specify a custom `fetch` function implementation.
66
+ *
67
+ * If not provided, we expect that `fetch` is defined globally.
68
+ */
69
+ fetch?: Fetch | undefined;
70
+
71
+ /**
72
+ * Default headers to include with every request to the API.
73
+ *
74
+ * These can be removed in individual requests by explicitly setting the
75
+ * header to `null` in request options.
76
+ */
77
+ defaultHeaders?: HeadersLike | undefined;
78
+ }
79
+
80
+ export abstract class Client {
81
+ /** CS AIDR API token.*/
82
+ token: string;
83
+
84
+ /**
85
+ * Template for constructing the base URL for API requests. The placeholder
86
+ * `{SERVICE_NAME}` will be replaced with the service name slug.
87
+ */
88
+ baseURLTemplate: string;
89
+ timeout: number;
90
+ maxRetries: number;
91
+ maxPollingAttempts: number;
92
+
93
+ private readonly fetch: Fetch;
94
+ private readonly _options: ClientOptions;
95
+ protected abstract serviceName: string;
96
+
97
+ constructor(options: ClientOptions) {
98
+ if (options.token === undefined) {
99
+ throw new Errors.AIDRError(
100
+ 'Client was instantiated without an API token.'
101
+ );
102
+ }
103
+ if (options.baseURLTemplate === undefined) {
104
+ throw new Errors.AIDRError(
105
+ 'Client was instantiated without a base URL template.'
106
+ );
107
+ }
108
+
109
+ this.baseURLTemplate = options.baseURLTemplate;
110
+ this.fetch = options.fetch ?? fetch;
111
+ this.maxRetries = options.maxRetries ?? 2;
112
+ this.maxPollingAttempts = options.maxPollingAttempts ?? 5;
113
+ this.timeout = options.timeout ?? 60_000;
114
+ this.token = options.token;
115
+
116
+ this._options = options;
117
+ }
118
+
119
+ /**
120
+ * Will retrieve the result, or will return HTTP/202 if the original request
121
+ * is still in progress.
122
+ */
123
+ getAsyncRequest<T>(requestId: string): Promise<MaybeAcceptedResponse<T>> {
124
+ return this.get(`/v1/request/${requestId}`);
125
+ }
126
+
127
+ /**
128
+ * Polls for an async request result with exponential backoff.
129
+ * Continues polling until a success response is received or max attempts are reached.
130
+ */
131
+ private async pollAsyncRequest<T>(
132
+ requestId: string,
133
+ maxAttempts: number
134
+ ): Promise<MaybeAcceptedResponse<T>> {
135
+ let lastResponse: MaybeAcceptedResponse<T> | null = null;
136
+
137
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
138
+ const response = await this.getAsyncRequest<T>(requestId);
139
+
140
+ // If we got a success response, return it immediately
141
+ if (response.status === 'Success') {
142
+ return response;
143
+ }
144
+
145
+ // Store the last response in case we exhaust attempts
146
+ lastResponse = response;
147
+
148
+ // If this is the last attempt, don't sleep
149
+ if (attempt < maxAttempts - 1) {
150
+ const timeoutMillis = this.calculateDefaultRetryTimeoutMillis(
151
+ maxAttempts - attempt - 1,
152
+ maxAttempts
153
+ );
154
+ await sleep(timeoutMillis);
155
+ }
156
+ }
157
+
158
+ // Return the last response (should be AcceptedResponse)
159
+ // lastResponse is guaranteed to be set since maxAttempts > 0
160
+ if (lastResponse === null) {
161
+ throw new Errors.AIDRError('Polling failed: no response received');
162
+ }
163
+ return lastResponse;
164
+ }
165
+
166
+ protected async get<R>(
167
+ path: string,
168
+ opts?: PromiseOrValue<RequestOptions>
169
+ ): Promise<R> {
170
+ return await this.methodRequest('get', path, opts);
171
+ }
172
+
173
+ protected async post<R>(
174
+ path: string,
175
+ opts?: PromiseOrValue<RequestOptions>
176
+ ): Promise<R> {
177
+ return await this.methodRequest('post', path, opts);
178
+ }
179
+
180
+ private async methodRequest<Rsp>(
181
+ method: HTTPMethod,
182
+ path: string,
183
+ opts?: PromiseOrValue<RequestOptions>
184
+ ): Promise<Rsp> {
185
+ return await this.request(
186
+ Promise.resolve(opts).then((opts) => {
187
+ return { method, path, ...opts };
188
+ })
189
+ );
190
+ }
191
+
192
+ private async request<R>(
193
+ options: PromiseOrValue<FinalRequestOptions>,
194
+ remainingRetries: number | null = null
195
+ ): Promise<R> {
196
+ const props = await this.makeRequest(options, remainingRetries);
197
+ const parsed = await defaultParseResponse<R>(props);
198
+
199
+ if (isAcceptedResponse(parsed)) {
200
+ const finalOptions = await Promise.resolve(options);
201
+ const maxPollingAttempts =
202
+ finalOptions.maxPollingAttempts ?? this.maxPollingAttempts;
203
+
204
+ if (maxPollingAttempts <= 0) {
205
+ return parsed;
206
+ }
207
+
208
+ return (await this.pollAsyncRequest<R>(
209
+ parsed.request_id,
210
+ maxPollingAttempts
211
+ )) as R;
212
+ }
213
+
214
+ return parsed;
215
+ }
216
+
217
+ private async makeRequest(
218
+ optionsInput: PromiseOrValue<FinalRequestOptions>,
219
+ retriesRemaining: number | null
220
+ ): Promise<APIResponseProps> {
221
+ const options = await optionsInput;
222
+ const maxRetries = options.maxRetries ?? this.maxRetries;
223
+ if (retriesRemaining == null) {
224
+ retriesRemaining = maxRetries;
225
+ }
226
+
227
+ const { req, url, timeout } = this.buildRequest(options, {
228
+ retryCount: maxRetries - retriesRemaining,
229
+ });
230
+
231
+ if (options.signal?.aborted) {
232
+ throw new Errors.APIUserAbortError();
233
+ }
234
+
235
+ const controller = new AbortController();
236
+ const response = await this.fetchWithTimeout(
237
+ url,
238
+ req,
239
+ timeout,
240
+ controller
241
+ ).catch(castToError);
242
+
243
+ if (response instanceof globalThis.Error) {
244
+ if (options.signal?.aborted) {
245
+ throw new Errors.APIUserAbortError();
246
+ }
247
+
248
+ if (retriesRemaining) {
249
+ return await this.retryRequest(options, retriesRemaining);
250
+ }
251
+
252
+ throw new Errors.APIConnectionError({ cause: response });
253
+ }
254
+
255
+ if (!response.ok) {
256
+ const shouldRetry = this.shouldRetry(response);
257
+
258
+ if (retriesRemaining && shouldRetry) {
259
+ return await this.retryRequest(options, retriesRemaining);
260
+ }
261
+
262
+ // TODO: throw error based on status
263
+ }
264
+
265
+ return { response, options, controller };
266
+ }
267
+
268
+ private shouldRetry(response: Response): boolean {
269
+ return (
270
+ response.status === 408 ||
271
+ response.status === 409 ||
272
+ response.status === 429 ||
273
+ response.status >= 500
274
+ );
275
+ }
276
+
277
+ private async retryRequest(
278
+ options: FinalRequestOptions,
279
+ retriesRemaining: number
280
+ ): Promise<APIResponseProps> {
281
+ const maxRetries = options.maxRetries ?? this.maxRetries;
282
+ const timeoutMillis = this.calculateDefaultRetryTimeoutMillis(
283
+ retriesRemaining,
284
+ maxRetries
285
+ );
286
+
287
+ await sleep(timeoutMillis);
288
+
289
+ return this.makeRequest(options, retriesRemaining - 1);
290
+ }
291
+
292
+ private calculateDefaultRetryTimeoutMillis(
293
+ retriesRemaining: number,
294
+ maxRetries: number
295
+ ): number {
296
+ const initialRetryDelay = 0.5;
297
+ const maxRetryDelay = 8.0;
298
+
299
+ const numRetries = maxRetries - retriesRemaining;
300
+
301
+ // Apply exponential backoff, but not more than the max.
302
+ const sleepSeconds = Math.min(
303
+ initialRetryDelay * 2 ** numRetries,
304
+ maxRetryDelay
305
+ );
306
+
307
+ // Apply some jitter, take up to at most 25 percent of the retry time.
308
+ const jitter = 1 - Math.random() * 0.25;
309
+
310
+ return sleepSeconds * jitter * 1000;
311
+ }
312
+
313
+ private async fetchWithTimeout(
314
+ url: RequestInfo,
315
+ init: RequestInit | undefined,
316
+ ms: number,
317
+ controller: AbortController
318
+ ): Promise<Response> {
319
+ const { signal, method, ...options } = init || {};
320
+ if (signal) {
321
+ signal.addEventListener('abort', () => controller.abort());
322
+ }
323
+
324
+ const timeout = setTimeout(() => controller.abort(), ms);
325
+
326
+ const fetchOptions: RequestInit = {
327
+ signal: controller.signal,
328
+ method: 'GET',
329
+ ...options,
330
+ };
331
+ if (method) {
332
+ fetchOptions.method = method.toUpperCase();
333
+ }
334
+
335
+ try {
336
+ return await this.fetch.call(undefined, url, fetchOptions);
337
+ } finally {
338
+ clearTimeout(timeout);
339
+ }
340
+ }
341
+
342
+ private buildRequest(
343
+ inputOptions: FinalRequestOptions,
344
+ { retryCount = 0 }: { retryCount?: number } = {}
345
+ ): { req: FinalizedRequestInit; url: string; timeout: number } {
346
+ const options = { ...inputOptions };
347
+ const { method, path, query, baseURLTemplate } = options;
348
+
349
+ const url = this.buildURL(
350
+ path,
351
+ query as Record<string, unknown>,
352
+ baseURLTemplate
353
+ );
354
+ options.timeout = options.timeout ?? this.timeout;
355
+ const { bodyHeaders, body } = this.buildBody({ options });
356
+ const reqHeaders = this.buildHeaders({
357
+ options: inputOptions,
358
+ method,
359
+ bodyHeaders,
360
+ retryCount,
361
+ });
362
+
363
+ const req: FinalizedRequestInit = {
364
+ method,
365
+ headers: reqHeaders,
366
+ ...(options.signal && { signal: options.signal }),
367
+ ...(body && { body }),
368
+ };
369
+
370
+ return { req, url, timeout: options.timeout };
371
+ }
372
+
373
+ private buildURL(
374
+ path: string,
375
+ query: Record<string, unknown> | null | undefined,
376
+ baseURLTemplate: string = this.baseURLTemplate
377
+ ): string {
378
+ const url = new URL(
379
+ (isAbsoluteURL(path)
380
+ ? path
381
+ : baseURLTemplate +
382
+ (baseURLTemplate.endsWith('/') && path.startsWith('/')
383
+ ? path.slice(1)
384
+ : path)
385
+ ).replaceAll('{SERVICE_NAME}', this.serviceName)
386
+ );
387
+
388
+ if (typeof query === 'object' && query && !Array.isArray(query)) {
389
+ url.search = stringifyQuery(query as Record<string, unknown>);
390
+ }
391
+
392
+ return url.toString();
393
+ }
394
+
395
+ private buildBody({
396
+ options: { body, headers: _rawHeaders },
397
+ }: {
398
+ options: FinalRequestOptions;
399
+ }): {
400
+ bodyHeaders: HeadersLike;
401
+ body: BodyInit | undefined;
402
+ } {
403
+ if (!body) {
404
+ return { bodyHeaders: undefined, body: undefined };
405
+ }
406
+
407
+ // TODO: other body types.
408
+
409
+ return {
410
+ bodyHeaders: { 'content-type': 'application/json' },
411
+ body: JSON.stringify(body),
412
+ };
413
+ }
414
+
415
+ private buildHeaders({
416
+ options,
417
+ bodyHeaders,
418
+ }: {
419
+ options: FinalRequestOptions;
420
+ method: HTTPMethod;
421
+ bodyHeaders: HeadersLike;
422
+ retryCount: number;
423
+ }): Headers {
424
+ const headers = buildHeaders([
425
+ {
426
+ Accept: 'application/json',
427
+ 'User-Agent': 'aidr-typescript',
428
+ },
429
+ this.authHeaders(),
430
+ this._options.defaultHeaders,
431
+ bodyHeaders,
432
+ options.headers,
433
+ ]);
434
+
435
+ return headers.values;
436
+ }
437
+
438
+ private authHeaders(): NullableHeaders {
439
+ return buildHeaders([{ Authorization: `Bearer ${this.token}` }]);
440
+ }
441
+ }
@@ -0,0 +1,78 @@
1
+ export class AIDRError extends Error {}
2
+
3
+ export class APIError<
4
+ TStatus extends number | undefined = number | undefined,
5
+ THeaders extends Headers | undefined = Headers | undefined,
6
+ TError extends object | undefined = object | undefined,
7
+ > extends AIDRError {
8
+ /** HTTP status for the response that caused the error */
9
+ readonly status: TStatus;
10
+ /** HTTP headers for the response that caused the error */
11
+ readonly headers: THeaders;
12
+ /** JSON body of the response that caused the error */
13
+ readonly error: TError;
14
+
15
+ constructor(
16
+ status: TStatus,
17
+ error: TError,
18
+ message: string | undefined,
19
+ headers: THeaders
20
+ ) {
21
+ super(`${APIError.makeMessage(status, error, message)}`);
22
+ this.status = status;
23
+ this.headers = headers;
24
+ this.error = error;
25
+ }
26
+
27
+ private static makeMessage(
28
+ status: number | undefined,
29
+ // biome-ignore lint/suspicious/noExplicitAny: matches upstream.
30
+ error: any,
31
+ message: string | undefined
32
+ ) {
33
+ const msg = error?.message
34
+ ? typeof error.message === 'string'
35
+ ? error.message
36
+ : JSON.stringify(error.message)
37
+ : error
38
+ ? JSON.stringify(error)
39
+ : message;
40
+
41
+ if (status && msg) {
42
+ return `${status} ${msg}`;
43
+ }
44
+ if (status) {
45
+ return `${status} status code (no body)`;
46
+ }
47
+ if (msg) {
48
+ return msg;
49
+ }
50
+ return '(no status code or body)';
51
+ }
52
+ }
53
+
54
+ export class APIUserAbortError extends APIError<
55
+ undefined,
56
+ undefined,
57
+ undefined
58
+ > {
59
+ constructor({ message }: { message?: string } = {}) {
60
+ super(undefined, undefined, message || 'Request was aborted.', undefined);
61
+ }
62
+ }
63
+
64
+ export class APIConnectionError extends APIError<
65
+ undefined,
66
+ undefined,
67
+ undefined
68
+ > {
69
+ constructor({
70
+ message,
71
+ cause,
72
+ }: { message?: string | undefined; cause?: Error | undefined }) {
73
+ super(undefined, undefined, message || 'Connection error.', undefined);
74
+ if (cause) {
75
+ this.cause = cause;
76
+ }
77
+ }
78
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { AIGuard } from './services/ai-guard';
2
+ export * from './types';
@@ -0,0 +1,18 @@
1
+ export type Fetch = (
2
+ input: string | URL | Request,
3
+ init?: RequestInit
4
+ ) => Promise<Response>;
5
+
6
+ /**
7
+ * The type for constructing `RequestInit` body.
8
+ *
9
+ * https://developer.mozilla.org/docs/Web/API/RequestInit#body
10
+ */
11
+ export type BodyInit = RequestInit['body'];
12
+
13
+ /**
14
+ * The type for the first argument to `fetch`.
15
+ *
16
+ * https://developer.mozilla.org/docs/Web/API/Window/fetch#resource
17
+ */
18
+ export type RequestInfo = Request | URL | string;
@@ -0,0 +1,34 @@
1
+ // biome-ignore lint/suspicious/noExplicitAny: matches upstream.
2
+ export function castToError(err: any): Error {
3
+ if (err instanceof Error) {
4
+ return err;
5
+ }
6
+ if (typeof err === 'object' && err !== null) {
7
+ try {
8
+ if (Object.prototype.toString.call(err) === '[object Error]') {
9
+ const error = new Error(
10
+ err.message,
11
+ err.cause ? { cause: err.cause } : {}
12
+ );
13
+ if (err.stack) {
14
+ error.stack = err.stack;
15
+ }
16
+ if (err.cause && !error.cause) {
17
+ error.cause = err.cause;
18
+ }
19
+ if (err.name) {
20
+ error.name = err.name;
21
+ }
22
+ return error;
23
+ }
24
+ } catch {
25
+ /** no-op */
26
+ }
27
+ try {
28
+ return new Error(JSON.stringify(err));
29
+ } catch {
30
+ /** no-op */
31
+ }
32
+ }
33
+ return new Error(err);
34
+ }
@@ -0,0 +1,100 @@
1
+ import { isReadonlyArray } from './utils/values';
2
+
3
+ type HeaderValue = string | undefined | null;
4
+
5
+ export type NullableHeaders = {
6
+ /** Brand check, prevent users from creating a NullableHeaders. */
7
+ [brand_privateNullableHeaders]: true;
8
+
9
+ /** Parsed headers. */
10
+ values: Headers;
11
+
12
+ /** Set of lowercase header names explicitly set to null. */
13
+ nulls: Set<string>;
14
+ };
15
+
16
+ export type HeadersLike =
17
+ | Headers
18
+ | readonly HeaderValue[][]
19
+ | Record<string, HeaderValue | readonly HeaderValue[]>
20
+ | undefined
21
+ | null
22
+ | NullableHeaders;
23
+
24
+ const brand_privateNullableHeaders = /* @__PURE__ */ Symbol(
25
+ 'brand.privateNullableHeaders'
26
+ );
27
+
28
+ function* iterateHeaders(
29
+ headers: HeadersLike
30
+ ): IterableIterator<readonly [string, string | null]> {
31
+ if (!headers) {
32
+ return;
33
+ }
34
+
35
+ if (brand_privateNullableHeaders in headers) {
36
+ const { values, nulls } = headers;
37
+ yield* values.entries();
38
+ for (const name of nulls) {
39
+ yield [name, null];
40
+ }
41
+ return;
42
+ }
43
+
44
+ let shouldClear = false;
45
+ let iter: Iterable<readonly (HeaderValue | readonly HeaderValue[])[]>;
46
+ if (headers instanceof Headers) {
47
+ iter = headers.entries();
48
+ } else if (isReadonlyArray(headers)) {
49
+ iter = headers;
50
+ } else {
51
+ shouldClear = true;
52
+ iter = Object.entries(headers ?? {});
53
+ }
54
+ for (const row of iter) {
55
+ const name = row[0];
56
+ if (typeof name !== 'string') {
57
+ throw new TypeError('expected header name to be a string');
58
+ }
59
+ const values = isReadonlyArray(row[1]) ? row[1] : [row[1]];
60
+ let didClear = false;
61
+ for (const value of values) {
62
+ if (value === undefined) {
63
+ continue;
64
+ }
65
+
66
+ if (shouldClear && !didClear) {
67
+ didClear = true;
68
+ yield [name, null];
69
+ }
70
+ yield [name, value];
71
+ }
72
+ }
73
+ }
74
+
75
+ export function buildHeaders(newHeaders: HeadersLike[]): NullableHeaders {
76
+ const targetHeaders = new Headers();
77
+ const nullHeaders = new Set<string>();
78
+ for (const headers of newHeaders) {
79
+ const seenHeaders = new Set<string>();
80
+ for (const [name, value] of iterateHeaders(headers)) {
81
+ const lowerName = name.toLowerCase();
82
+ if (!seenHeaders.has(lowerName)) {
83
+ targetHeaders.delete(name);
84
+ seenHeaders.add(lowerName);
85
+ }
86
+ if (value === null) {
87
+ targetHeaders.delete(name);
88
+ nullHeaders.add(lowerName);
89
+ } else {
90
+ targetHeaders.append(name, value);
91
+ nullHeaders.delete(lowerName);
92
+ }
93
+ }
94
+ }
95
+ return {
96
+ [brand_privateNullableHeaders]: true,
97
+ values: targetHeaders,
98
+ nulls: nullHeaders,
99
+ };
100
+ }
@@ -0,0 +1,30 @@
1
+ import type { FinalRequestOptions } from './request-options';
2
+
3
+ export type APIResponseProps = {
4
+ controller: AbortController;
5
+ options: FinalRequestOptions;
6
+ response: Response;
7
+ };
8
+
9
+ export async function defaultParseResponse<T>(
10
+ props: APIResponseProps
11
+ ): Promise<T> {
12
+ const { response } = props;
13
+ return await (async () => {
14
+ if (response.status === 204) {
15
+ return null as T;
16
+ }
17
+
18
+ const contentType = response.headers.get('content-type');
19
+ const mediaType = contentType?.split(';')[0]?.trim();
20
+ const isJSON =
21
+ mediaType?.includes('application/json') || mediaType?.endsWith('+json');
22
+ if (isJSON) {
23
+ const json = await response.json();
24
+ return json as T;
25
+ }
26
+
27
+ const text = await response.text();
28
+ return text as unknown as T;
29
+ })();
30
+ }