@nohead/sdk 0.0.0 → 0.2.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.
package/dist/core.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { APIPromise, PagePromise } from "./api-promise.ts";
2
+ import { NoheadError } from "./errors.ts";
3
+ import { type OperationId, type Operations } from "./generated/operations.gen.ts";
4
+ import type { ListMeta } from "./generated/types.gen.ts";
5
+ export declare const DEFAULT_BASE_URL = "https://api.nohead.io";
6
+ export interface ClientOptions {
7
+ /** A project API key (`sk_live_…`). Defaults to `NOHEAD_API_KEY`. */
8
+ apiKey?: string;
9
+ /** Defaults to `NOHEAD_API_URL`, else `https://api.nohead.io`. */
10
+ baseUrl?: string;
11
+ /** The key's project. Looked up once with `GET /v1/me` when omitted. */
12
+ projectId?: string;
13
+ /** Retries of failed requests (see README, "Retries"). Default 2. */
14
+ maxRetries?: number;
15
+ /** Milliseconds per attempt. Default 60 000. */
16
+ timeout?: number;
17
+ /** A `fetch` implementation, for proxies, instrumentation or tests. */
18
+ fetch?: typeof fetch;
19
+ /** Headers added to every request. */
20
+ headers?: Record<string, string>;
21
+ /** Log deprecation and plan usage warnings (once each). Default true. */
22
+ warnings?: boolean;
23
+ /** Called before each retry, for example to report it. */
24
+ onRetry?: (retry: RetryEvent) => void;
25
+ }
26
+ /** A retry about to happen (`onRetry`). */
27
+ export interface RetryEvent {
28
+ /** 1 for the first retry of the request, 2 for the second… */
29
+ attempt: number;
30
+ /** Seconds the client waits before it. */
31
+ delay: number;
32
+ /** The URL being retried. */
33
+ url: string;
34
+ /** The status that failed, or undefined when the connection did. */
35
+ status?: number;
36
+ /** What failed: an `APIError` or a `ConnectionError` (none for storage). */
37
+ error?: NoheadError;
38
+ }
39
+ export interface RequestOptions {
40
+ /** Sent as `Idempotency-Key` on writes. Generated when omitted. */
41
+ idempotencyKey?: string;
42
+ /** A reason for the change, shown in history (`Nohead-Change-Note`). */
43
+ changeNote?: string;
44
+ signal?: AbortSignal;
45
+ /** Milliseconds per attempt, overriding the client's. */
46
+ timeout?: number;
47
+ /** Retries, overriding the client's. */
48
+ maxRetries?: number;
49
+ headers?: Record<string, string>;
50
+ }
51
+ /** Options of record writes that accept `If-Match`. */
52
+ export interface ConditionalRequestOptions extends RequestOptions {
53
+ /**
54
+ * Only write if the record is still at this revision; otherwise the API
55
+ * answers `412` (`PreconditionFailedError`). A record works too.
56
+ */
57
+ ifMatch?: number | {
58
+ revision: number;
59
+ };
60
+ }
61
+ export interface RequestInput {
62
+ path?: Record<string, string | number | undefined>;
63
+ query?: object;
64
+ body?: unknown;
65
+ }
66
+ type AllOptions = ConditionalRequestOptions;
67
+ export declare class Core {
68
+ #private;
69
+ constructor(options: ClientOptions);
70
+ /** The API key's project, from `GET /v1/me` the first time. */
71
+ projectId(): Promise<string>;
72
+ request<Op extends OperationId>(operation: Op, input?: RequestInput, options?: AllOptions): APIPromise<Operations[Op]["response"]>;
73
+ /** A list operation as pages of `Item`. */
74
+ paginate<Item, Meta extends ListMeta = ListMeta>(operation: OperationId, input: RequestInput, options?: RequestOptions): PagePromise<Item, Meta>;
75
+ /** `fetch` with this client's timeout and retries, for presigned URLs. */
76
+ fetchWithRetries(url: string, init: RequestInit, options: RequestOptions & {
77
+ retry: boolean;
78
+ }): Promise<Response>;
79
+ }
80
+ /**
81
+ * Query parameters as the API reads them: objects become `key[sub]=…`
82
+ * (`filter[status]=published`), arrays are comma-separated
83
+ * (`expand=author,tags`), dates are ISO 8601.
84
+ */
85
+ export declare function appendQuery(params: URLSearchParams, value: unknown, key?: string): void;
86
+ export {};
package/dist/core.js ADDED
@@ -0,0 +1,299 @@
1
+ import { APIPromise, Page, PagePromise, } from "./api-promise.js";
2
+ import { AbortError, ConnectionError, NoheadError, TimeoutError, apiError, retryAfterSeconds, } from "./errors.js";
3
+ import { OPERATIONS, } from "./generated/operations.gen.js";
4
+ import { VERSION } from "./version.js";
5
+ export const DEFAULT_BASE_URL = "https://api.nohead.io";
6
+ const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
7
+ const MAX_RETRY_AFTER_SECONDS = 60;
8
+ const warnedDeprecations = new Set();
9
+ let warnedUsage = false;
10
+ export class Core {
11
+ #apiKey;
12
+ #baseUrl;
13
+ #fetch;
14
+ #headers;
15
+ #maxRetries;
16
+ #timeout;
17
+ #warnings;
18
+ #onRetry;
19
+ #projectId;
20
+ constructor(options) {
21
+ const env = environment();
22
+ // Empty environment variables count as unset.
23
+ const apiKey = options.apiKey ?? (env.NOHEAD_API_KEY || undefined);
24
+ if (!apiKey) {
25
+ throw new NoheadError("Missing API key: pass `apiKey` or set NOHEAD_API_KEY");
26
+ }
27
+ this.#apiKey = apiKey;
28
+ this.#baseUrl = (options.baseUrl ??
29
+ (env.NOHEAD_API_URL || DEFAULT_BASE_URL)).replace(/\/+$/, "");
30
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
31
+ this.#headers = options.headers ?? {};
32
+ this.#maxRetries = options.maxRetries ?? 2;
33
+ this.#timeout = options.timeout ?? 60_000;
34
+ this.#warnings = options.warnings ?? true;
35
+ this.#onRetry = options.onRetry;
36
+ if (options.projectId)
37
+ this.#projectId = Promise.resolve(options.projectId);
38
+ }
39
+ /** The API key's project, from `GET /v1/me` the first time. */
40
+ projectId() {
41
+ this.#projectId ??= this.request("me_get", {}).then((me) => {
42
+ if (!me.api_key) {
43
+ throw new NoheadError("The credentials are not a project API key");
44
+ }
45
+ return me.api_key.project_id;
46
+ }, (error) => {
47
+ this.#projectId = undefined;
48
+ throw error;
49
+ });
50
+ return this.#projectId;
51
+ }
52
+ request(operation, input = {}, options = {}) {
53
+ return new APIPromise(this.#send(operation, input, options));
54
+ }
55
+ /** A list operation as pages of `Item`. */
56
+ paginate(operation, input, options = {}) {
57
+ const fetchPage = async (cursor) => {
58
+ const query = { ...input.query, ...(cursor ? { cursor } : {}) };
59
+ const result = await this.#send(operation, { ...input, query }, options);
60
+ const list = result.data;
61
+ const page = new Page(list.data, list.meta, (next) => fetchPage(next).then((r) => r.data));
62
+ return { ...result, data: page };
63
+ };
64
+ const first = input.query?.cursor;
65
+ return new PagePromise(fetchPage(first));
66
+ }
67
+ /** `fetch` with this client's timeout and retries, for presigned URLs. */
68
+ async fetchWithRetries(url, init, options) {
69
+ const maxRetries = options.retry
70
+ ? (options.maxRetries ?? this.#maxRetries)
71
+ : 0;
72
+ for (let attempt = 0;; attempt++) {
73
+ let failed;
74
+ try {
75
+ const { response } = await this.#attempt(url, init, options, false);
76
+ if (!RETRYABLE_STATUSES.has(response.status) || attempt >= maxRetries) {
77
+ return response;
78
+ }
79
+ failed = { status: response.status };
80
+ }
81
+ catch (error) {
82
+ if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
83
+ throw error;
84
+ }
85
+ failed = { error };
86
+ }
87
+ await this.#retry(url, attempt, backoff(attempt), failed, options.signal);
88
+ }
89
+ }
90
+ async #send(operation, input, options) {
91
+ const { method, path } = OPERATIONS[operation];
92
+ const url = new URL(this.#baseUrl + (await this.#path(path, input.path)));
93
+ appendQuery(url.searchParams, input.query);
94
+ const headers = {
95
+ Accept: "application/json",
96
+ Authorization: `Bearer ${this.#apiKey}`,
97
+ "Nohead-Client": `sdk-typescript/${VERSION}`,
98
+ ...USER_AGENT,
99
+ ...this.#headers,
100
+ };
101
+ if (input.body !== undefined)
102
+ headers["Content-Type"] = "application/json";
103
+ if (method !== "GET") {
104
+ headers["Idempotency-Key"] = options.idempotencyKey ?? crypto.randomUUID();
105
+ }
106
+ if (options.ifMatch !== undefined) {
107
+ const revision = typeof options.ifMatch === "object"
108
+ ? options.ifMatch.revision
109
+ : options.ifMatch;
110
+ headers["If-Match"] = `"${revision}"`;
111
+ }
112
+ if (options.changeNote)
113
+ headers["Nohead-Change-Note"] = options.changeNote;
114
+ Object.assign(headers, options.headers);
115
+ const init = {
116
+ method,
117
+ headers,
118
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
119
+ };
120
+ const maxRetries = options.maxRetries ?? this.#maxRetries;
121
+ for (let attempt = 0;; attempt++) {
122
+ let result;
123
+ try {
124
+ result = await this.#attempt(url.toString(), init, options, true);
125
+ }
126
+ catch (error) {
127
+ if (!(error instanceof ConnectionError) || attempt >= maxRetries) {
128
+ throw error;
129
+ }
130
+ await this.#retry(url.toString(), attempt, backoff(attempt), { error }, options.signal);
131
+ continue;
132
+ }
133
+ const { response, body } = result;
134
+ this.#warn(operation, response);
135
+ const requestId = response.headers.get("x-request-id") ?? undefined;
136
+ if (response.ok)
137
+ return { data: body, response, requestId };
138
+ const error = apiError(response.status, body, response.headers);
139
+ const delay = retryDelay(error, attempt, maxRetries);
140
+ if (delay === undefined)
141
+ throw error;
142
+ await this.#retry(url.toString(), attempt, delay, { status: response.status, error }, options.signal);
143
+ }
144
+ }
145
+ #retry(url, attempt, delay, failed, signal) {
146
+ this.#onRetry?.({ attempt: attempt + 1, delay, url, ...failed });
147
+ return sleep(delay, signal);
148
+ }
149
+ async #attempt(url, init, options, readBody) {
150
+ const timeout = options.timeout ?? this.#timeout;
151
+ const timer = AbortSignal.timeout(timeout);
152
+ const signal = options.signal
153
+ ? AbortSignal.any([options.signal, timer])
154
+ : timer;
155
+ try {
156
+ const response = await this.#fetch(url, { ...init, signal });
157
+ const body = readBody ? await parseBody(response) : undefined;
158
+ return { response, body };
159
+ }
160
+ catch (error) {
161
+ if (options.signal?.aborted) {
162
+ throw new AbortError("The request was aborted", {
163
+ cause: options.signal.reason,
164
+ });
165
+ }
166
+ if (timer.aborted) {
167
+ throw new TimeoutError(`The request timed out after ${timeout} ms`);
168
+ }
169
+ throw new ConnectionError(`Could not reach ${new URL(url).host}: ${error.message}`, { cause: error });
170
+ }
171
+ }
172
+ async #path(template, values = {}) {
173
+ let path = template;
174
+ for (const [, name] of template.matchAll(/\{(\w+)\}/g)) {
175
+ let value = values[name];
176
+ if (value === undefined && name === "project_id") {
177
+ value = await this.projectId();
178
+ }
179
+ if (value === undefined || value === "") {
180
+ throw new NoheadError(`Missing ${name}`);
181
+ }
182
+ path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
183
+ }
184
+ return path;
185
+ }
186
+ #warn(operation, response) {
187
+ if (!this.#warnings)
188
+ return;
189
+ const deprecation = response.headers.get("deprecation");
190
+ if (deprecation && !warnedDeprecations.has(operation)) {
191
+ warnedDeprecations.add(operation);
192
+ const sunset = response.headers.get("sunset");
193
+ const link = response.headers.get("link")?.match(/<([^>]+)>/)?.[1];
194
+ console.warn(`[nohead] ${operation} is deprecated` +
195
+ (sunset ? ` and will be removed after ${sunset}` : "") +
196
+ (link ? `. See ${link}` : ""));
197
+ }
198
+ const usage = response.headers.get("nohead-usage-warning");
199
+ if (usage && !warnedUsage) {
200
+ warnedUsage = true;
201
+ console.warn(`[nohead] Over a plan limit: ${usage}`);
202
+ }
203
+ }
204
+ }
205
+ /** Seconds to wait before retrying `error`, or undefined to throw it. */
206
+ function retryDelay(error, attempt, maxRetries) {
207
+ if (attempt >= maxRetries)
208
+ return undefined;
209
+ const inProgress = error.status === 409 && error.details.some((d) => d.code === "in_progress");
210
+ if (!inProgress && !RETRYABLE_STATUSES.has(error.status))
211
+ return undefined;
212
+ const retryAfter = retryAfterSeconds(error.headers);
213
+ if (retryAfter === undefined)
214
+ return backoff(attempt);
215
+ return retryAfter <= MAX_RETRY_AFTER_SECONDS ? retryAfter : undefined;
216
+ }
217
+ /** Exponential backoff with jitter: about 0.5 s, 1 s, 2 s… up to 8 s. */
218
+ function backoff(attempt) {
219
+ const base = Math.min(0.5 * 2 ** attempt, 8);
220
+ return base * (1 - Math.random() * 0.25);
221
+ }
222
+ function sleep(seconds, signal) {
223
+ return new Promise((resolve, reject) => {
224
+ if (signal?.aborted) {
225
+ reject(new AbortError("The request was aborted", { cause: signal.reason }));
226
+ return;
227
+ }
228
+ const timer = setTimeout(done, seconds * 1000);
229
+ function done() {
230
+ signal?.removeEventListener("abort", aborted);
231
+ resolve();
232
+ }
233
+ function aborted() {
234
+ clearTimeout(timer);
235
+ reject(new AbortError("The request was aborted", { cause: signal.reason }));
236
+ }
237
+ signal?.addEventListener("abort", aborted, { once: true });
238
+ });
239
+ }
240
+ async function parseBody(response) {
241
+ const text = await response.text();
242
+ if (!text)
243
+ return undefined;
244
+ if (!response.headers.get("content-type")?.includes("json"))
245
+ return text;
246
+ try {
247
+ return JSON.parse(text);
248
+ }
249
+ catch {
250
+ return text;
251
+ }
252
+ }
253
+ /**
254
+ * Query parameters as the API reads them: objects become `key[sub]=…`
255
+ * (`filter[status]=published`), arrays are comma-separated
256
+ * (`expand=author,tags`), dates are ISO 8601.
257
+ */
258
+ export function appendQuery(params, value, key) {
259
+ if (value === undefined || value === null)
260
+ return;
261
+ if (value instanceof Date) {
262
+ params.append(key, value.toISOString());
263
+ }
264
+ else if (Array.isArray(value)) {
265
+ if (value.length > 0)
266
+ params.append(key, value.map(scalar).join(","));
267
+ }
268
+ else if (typeof value === "object") {
269
+ for (const [name, inner] of Object.entries(value)) {
270
+ appendQuery(params, inner, key ? `${key}[${name}]` : name);
271
+ }
272
+ }
273
+ else {
274
+ params.append(key, scalar(value));
275
+ }
276
+ }
277
+ function scalar(value) {
278
+ return value instanceof Date ? value.toISOString() : String(value);
279
+ }
280
+ // `nohead-typescript/0.1.0 node/26.9.0`, or none in a browser, which sends its
281
+ // own (Nohead-Client still names the SDK there).
282
+ const RUNTIME = runtimeVersion();
283
+ const USER_AGENT = {};
284
+ if (RUNTIME)
285
+ USER_AGENT["User-Agent"] = `nohead-typescript/${VERSION} ${RUNTIME}`;
286
+ function runtimeVersion() {
287
+ const g = globalThis;
288
+ if (g.Bun?.version)
289
+ return `bun/${g.Bun.version}`;
290
+ if (g.Deno?.version?.deno)
291
+ return `deno/${g.Deno.version.deno}`;
292
+ if (g.process?.versions?.node)
293
+ return `node/${g.process.versions.node}`;
294
+ return undefined;
295
+ }
296
+ function environment() {
297
+ const process = globalThis.process;
298
+ return process?.env ?? {};
299
+ }
@@ -0,0 +1,85 @@
1
+ import type { ErrorDetail, ErrorType } from "./generated/types.gen.ts";
2
+ /** The base class of every error this SDK throws. */
3
+ export declare class NoheadError extends Error {
4
+ name: string;
5
+ }
6
+ /** The API answered with an error status. Subclasses follow the error `type`. */
7
+ export declare class APIError extends NoheadError {
8
+ name: string;
9
+ /** The HTTP status. */
10
+ readonly status: number;
11
+ /** The error `type`, e.g. `validation_error`; undefined if the body had none. */
12
+ readonly type: ErrorType | undefined;
13
+ /** The request's ID (`req_…`), for support requests and logs. */
14
+ readonly requestId: string | undefined;
15
+ /** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
16
+ readonly details: ErrorDetail[];
17
+ readonly headers: Headers;
18
+ /** The parsed response body, or its text when it was not JSON. */
19
+ readonly body: unknown;
20
+ constructor(status: number, body: unknown, headers: Headers);
21
+ }
22
+ export declare class InvalidRequestError extends APIError {
23
+ name: string;
24
+ }
25
+ export declare class AuthenticationError extends APIError {
26
+ name: string;
27
+ }
28
+ export declare class PlanLimitExceededError extends APIError {
29
+ name: string;
30
+ }
31
+ export declare class AuthorizationError extends APIError {
32
+ name: string;
33
+ }
34
+ export declare class NotFoundError extends APIError {
35
+ name: string;
36
+ }
37
+ export declare class ConflictError extends APIError {
38
+ name: string;
39
+ }
40
+ /** An `ifMatch` revision was not the current one: reload and try again. */
41
+ export declare class PreconditionFailedError extends APIError {
42
+ name: string;
43
+ /** The resource's current revision, when the API reports it. */
44
+ get currentRevision(): number | undefined;
45
+ }
46
+ export declare class ValidationError extends APIError {
47
+ name: string;
48
+ }
49
+ export declare class RateLimitError extends APIError {
50
+ name: string;
51
+ /** Seconds to wait before retrying, from `Retry-After`. */
52
+ get retryAfter(): number | undefined;
53
+ }
54
+ export declare class InternalServerError extends APIError {
55
+ name: string;
56
+ }
57
+ export declare class ServiceUnavailableError extends APIError {
58
+ name: string;
59
+ }
60
+ /** The request never got a response: DNS, TLS, a reset connection. */
61
+ export declare class ConnectionError extends NoheadError {
62
+ name: string;
63
+ }
64
+ /** An attempt took longer than the `timeout` option. */
65
+ export declare class TimeoutError extends ConnectionError {
66
+ name: string;
67
+ }
68
+ /** The caller's `signal` aborted the request. */
69
+ export declare class AbortError extends NoheadError {
70
+ name: string;
71
+ }
72
+ /** A presigned upload to storage failed (`assets.upload`). */
73
+ export declare class UploadError extends NoheadError {
74
+ name: string;
75
+ /** Storage's HTTP status, when it answered. */
76
+ readonly status: number | undefined;
77
+ constructor(message: string, status?: number, options?: ErrorOptions);
78
+ }
79
+ /** A webhook request's signature or timestamp did not check out. */
80
+ export declare class WebhookVerificationError extends NoheadError {
81
+ name: string;
82
+ }
83
+ /** The error for a response, by its `type`, or by status when it has none. */
84
+ export declare function apiError(status: number, body: unknown, headers: Headers): APIError;
85
+ export declare function retryAfterSeconds(headers: Headers): number | undefined;
package/dist/errors.js ADDED
@@ -0,0 +1,144 @@
1
+ /** The base class of every error this SDK throws. */
2
+ export class NoheadError extends Error {
3
+ name = "NoheadError";
4
+ }
5
+ /** The API answered with an error status. Subclasses follow the error `type`. */
6
+ export class APIError extends NoheadError {
7
+ name = "APIError";
8
+ /** The HTTP status. */
9
+ status;
10
+ /** The error `type`, e.g. `validation_error`; undefined if the body had none. */
11
+ type;
12
+ /** The request's ID (`req_…`), for support requests and logs. */
13
+ requestId;
14
+ /** Field-level details, e.g. `[{ field: "title", code: "required", … }]`. */
15
+ details;
16
+ headers;
17
+ /** The parsed response body, or its text when it was not JSON. */
18
+ body;
19
+ constructor(status, body, headers) {
20
+ const error = errorEnvelope(body);
21
+ super(error?.message ?? `Request failed with status ${status}`);
22
+ this.status = status;
23
+ this.type = error?.type;
24
+ this.requestId =
25
+ error?.request_id ?? headers.get("x-request-id") ?? undefined;
26
+ this.details = error?.details ?? [];
27
+ this.headers = headers;
28
+ this.body = body;
29
+ }
30
+ }
31
+ export class InvalidRequestError extends APIError {
32
+ name = "InvalidRequestError";
33
+ }
34
+ export class AuthenticationError extends APIError {
35
+ name = "AuthenticationError";
36
+ }
37
+ export class PlanLimitExceededError extends APIError {
38
+ name = "PlanLimitExceededError";
39
+ }
40
+ export class AuthorizationError extends APIError {
41
+ name = "AuthorizationError";
42
+ }
43
+ export class NotFoundError extends APIError {
44
+ name = "NotFoundError";
45
+ }
46
+ export class ConflictError extends APIError {
47
+ name = "ConflictError";
48
+ }
49
+ /** An `ifMatch` revision was not the current one: reload and try again. */
50
+ export class PreconditionFailedError extends APIError {
51
+ name = "PreconditionFailedError";
52
+ /** The resource's current revision, when the API reports it. */
53
+ get currentRevision() {
54
+ return this.details.find((d) => d.code === "revision_mismatch")
55
+ ?.current_revision;
56
+ }
57
+ }
58
+ export class ValidationError extends APIError {
59
+ name = "ValidationError";
60
+ }
61
+ export class RateLimitError extends APIError {
62
+ name = "RateLimitError";
63
+ /** Seconds to wait before retrying, from `Retry-After`. */
64
+ get retryAfter() {
65
+ return retryAfterSeconds(this.headers);
66
+ }
67
+ }
68
+ export class InternalServerError extends APIError {
69
+ name = "InternalServerError";
70
+ }
71
+ export class ServiceUnavailableError extends APIError {
72
+ name = "ServiceUnavailableError";
73
+ }
74
+ /** The request never got a response: DNS, TLS, a reset connection. */
75
+ export class ConnectionError extends NoheadError {
76
+ name = "ConnectionError";
77
+ }
78
+ /** An attempt took longer than the `timeout` option. */
79
+ export class TimeoutError extends ConnectionError {
80
+ name = "TimeoutError";
81
+ }
82
+ /** The caller's `signal` aborted the request. */
83
+ export class AbortError extends NoheadError {
84
+ name = "AbortError";
85
+ }
86
+ /** A presigned upload to storage failed (`assets.upload`). */
87
+ export class UploadError extends NoheadError {
88
+ name = "UploadError";
89
+ /** Storage's HTTP status, when it answered. */
90
+ status;
91
+ constructor(message, status, options) {
92
+ super(message, options);
93
+ this.status = status;
94
+ }
95
+ }
96
+ /** A webhook request's signature or timestamp did not check out. */
97
+ export class WebhookVerificationError extends NoheadError {
98
+ name = "WebhookVerificationError";
99
+ }
100
+ const CLASSES = {
101
+ invalid_request: InvalidRequestError,
102
+ authentication_error: AuthenticationError,
103
+ plan_limit_exceeded: PlanLimitExceededError,
104
+ authorization_error: AuthorizationError,
105
+ not_found: NotFoundError,
106
+ conflict: ConflictError,
107
+ precondition_failed: PreconditionFailedError,
108
+ validation_error: ValidationError,
109
+ rate_limited: RateLimitError,
110
+ internal_error: InternalServerError,
111
+ service_unavailable: ServiceUnavailableError,
112
+ };
113
+ /** The error for a response, by its `type`, or by status when it has none. */
114
+ export function apiError(status, body, headers) {
115
+ const type = errorEnvelope(body)?.type;
116
+ const Class = (type && CLASSES[type]) ||
117
+ (status === 503
118
+ ? ServiceUnavailableError
119
+ : status >= 500
120
+ ? InternalServerError
121
+ : APIError);
122
+ return new Class(status, body, headers);
123
+ }
124
+ export function retryAfterSeconds(headers) {
125
+ const value = headers.get("retry-after");
126
+ if (!value)
127
+ return undefined;
128
+ const seconds = Number(value);
129
+ if (Number.isFinite(seconds))
130
+ return Math.max(0, seconds);
131
+ const date = Date.parse(value);
132
+ return Number.isNaN(date)
133
+ ? undefined
134
+ : Math.max(0, (date - Date.now()) / 1000);
135
+ }
136
+ function errorEnvelope(body) {
137
+ if (typeof body !== "object" || body === null || !("error" in body)) {
138
+ return undefined;
139
+ }
140
+ const error = body.error;
141
+ return typeof error === "object" && error !== null
142
+ ? error
143
+ : undefined;
144
+ }