@air/api-core 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,363 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ APIError: () => APIError,
24
+ AirBase: () => AirBase,
25
+ AuthenticationError: () => AuthenticationError,
26
+ BadRequestError: () => BadRequestError,
27
+ ConnectionError: () => ConnectionError,
28
+ CursorPage: () => CursorPage,
29
+ InternalServerError: () => InternalServerError,
30
+ NotFoundError: () => NotFoundError,
31
+ PagePromise: () => PagePromise,
32
+ PermissionError: () => PermissionError,
33
+ RateLimitError: () => RateLimitError,
34
+ TimeoutError: () => TimeoutError,
35
+ VERSION: () => VERSION,
36
+ getRetryDelay: () => getRetryDelay,
37
+ isRetryableError: () => isRetryableError,
38
+ retryWithBackoff: () => retryWithBackoff
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/errors.ts
43
+ var APIError = class _APIError extends Error {
44
+ status;
45
+ headers;
46
+ body;
47
+ constructor(status, body, message, headers) {
48
+ super(message);
49
+ this.name = "APIError";
50
+ this.status = status;
51
+ this.body = body;
52
+ this.headers = headers;
53
+ }
54
+ static async fromResponse(response) {
55
+ const headers = response.headers;
56
+ let body;
57
+ let message;
58
+ try {
59
+ body = await response.json();
60
+ message = typeof body === "object" && body !== null && "message" in body ? String(body.message) : `Request failed with status ${response.status}`;
61
+ } catch {
62
+ body = await response.text().catch(() => "");
63
+ message = `Request failed with status ${response.status}`;
64
+ }
65
+ const status = response.status;
66
+ switch (status) {
67
+ case 400:
68
+ return new BadRequestError(status, body, message, headers);
69
+ case 401:
70
+ return new AuthenticationError(status, body, message, headers);
71
+ case 403:
72
+ return new PermissionError(status, body, message, headers);
73
+ case 404:
74
+ return new NotFoundError(status, body, message, headers);
75
+ case 429:
76
+ return new RateLimitError(status, body, message, headers);
77
+ default:
78
+ if (status >= 500) {
79
+ return new InternalServerError(status, body, message, headers);
80
+ }
81
+ return new _APIError(status, body, message, headers);
82
+ }
83
+ }
84
+ };
85
+ var BadRequestError = class extends APIError {
86
+ constructor(status, body, message, headers) {
87
+ super(status, body, message, headers);
88
+ this.name = "BadRequestError";
89
+ }
90
+ };
91
+ var AuthenticationError = class extends APIError {
92
+ constructor(status, body, message, headers) {
93
+ super(status, body, message, headers);
94
+ this.name = "AuthenticationError";
95
+ }
96
+ };
97
+ var PermissionError = class extends APIError {
98
+ constructor(status, body, message, headers) {
99
+ super(status, body, message, headers);
100
+ this.name = "PermissionError";
101
+ }
102
+ };
103
+ var NotFoundError = class extends APIError {
104
+ constructor(status, body, message, headers) {
105
+ super(status, body, message, headers);
106
+ this.name = "NotFoundError";
107
+ }
108
+ };
109
+ var RateLimitError = class extends APIError {
110
+ retryAfter;
111
+ constructor(status, body, message, headers) {
112
+ super(status, body, message, headers);
113
+ this.name = "RateLimitError";
114
+ const retryAfterHeader = headers.get("retry-after");
115
+ this.retryAfter = retryAfterHeader ? Number(retryAfterHeader) : null;
116
+ }
117
+ };
118
+ var InternalServerError = class extends APIError {
119
+ constructor(status, body, message, headers) {
120
+ super(status, body, message, headers);
121
+ this.name = "InternalServerError";
122
+ }
123
+ };
124
+ var ConnectionError = class extends Error {
125
+ constructor(message) {
126
+ super(message);
127
+ this.name = "ConnectionError";
128
+ }
129
+ };
130
+ var TimeoutError = class extends ConnectionError {
131
+ constructor(message = "Request timed out") {
132
+ super(message);
133
+ this.name = "TimeoutError";
134
+ }
135
+ };
136
+
137
+ // src/pagination.ts
138
+ var CursorPage = class {
139
+ data;
140
+ pagination;
141
+ total;
142
+ _fetchNextPage;
143
+ constructor(response, fetchNextPage) {
144
+ this.data = response.data;
145
+ this.pagination = response.pagination;
146
+ this.total = response.total;
147
+ this._fetchNextPage = fetchNextPage;
148
+ }
149
+ hasNextPage() {
150
+ return this.pagination.hasMore && this.pagination.cursor !== null;
151
+ }
152
+ async getNextPage() {
153
+ if (!this.hasNextPage() || !this._fetchNextPage) {
154
+ throw new Error("No more pages available");
155
+ }
156
+ return this._fetchNextPage(this.pagination.cursor);
157
+ }
158
+ async *[Symbol.asyncIterator]() {
159
+ let page = this;
160
+ while (true) {
161
+ for (const item of page.data) {
162
+ yield item;
163
+ }
164
+ if (!page.hasNextPage()) break;
165
+ page = await page.getNextPage();
166
+ }
167
+ }
168
+ };
169
+ var PagePromise = class extends Promise {
170
+ async *[Symbol.asyncIterator]() {
171
+ const page = await this;
172
+ yield* page;
173
+ }
174
+ };
175
+
176
+ // src/retry.ts
177
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
178
+ var SERVER_ERROR_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
179
+ function isRetryableError(error, method) {
180
+ if (error instanceof APIError) {
181
+ if (method === "POST" && SERVER_ERROR_CODES.has(error.status)) {
182
+ return false;
183
+ }
184
+ return RETRYABLE_STATUS_CODES.has(error.status);
185
+ }
186
+ if (error instanceof ConnectionError) {
187
+ return true;
188
+ }
189
+ return false;
190
+ }
191
+ function getRetryDelay(error, attempt, baseDelay = 500) {
192
+ if (error instanceof RateLimitError && error.retryAfter !== null) {
193
+ const retryAfterMs = error.retryAfter * 1e3;
194
+ const jitter2 = retryAfterMs * 0.2 * Math.random();
195
+ return retryAfterMs + jitter2;
196
+ }
197
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
198
+ const jitter = exponentialDelay * 0.2 * Math.random();
199
+ return exponentialDelay + jitter;
200
+ }
201
+ async function retryWithBackoff(fn, maxRetries, method) {
202
+ let lastError;
203
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
204
+ try {
205
+ return await fn();
206
+ } catch (error) {
207
+ lastError = error;
208
+ if (attempt === maxRetries || !isRetryableError(error, method)) {
209
+ throw error;
210
+ }
211
+ const delay = getRetryDelay(error, attempt);
212
+ await sleep(delay);
213
+ }
214
+ }
215
+ throw lastError;
216
+ }
217
+ function sleep(ms) {
218
+ return new Promise((resolve) => setTimeout(resolve, ms));
219
+ }
220
+
221
+ // src/version.ts
222
+ var VERSION = "0.1.0";
223
+
224
+ // src/base-client.ts
225
+ var AirBase = class {
226
+ apiKey;
227
+ workspaceId;
228
+ baseURL;
229
+ maxRetries;
230
+ timeout;
231
+ _fetch;
232
+ _defaultHeaders;
233
+ constructor(options = {}) {
234
+ const apiKey = options.apiKey ?? process.env.AIR_API_KEY;
235
+ if (!apiKey) {
236
+ throw new Error(
237
+ "API key is required. Pass it as `apiKey` option or set the AIR_API_KEY environment variable."
238
+ );
239
+ }
240
+ const workspaceId = options.workspaceId ?? process.env.AIR_WORKSPACE_ID;
241
+ if (!workspaceId) {
242
+ throw new Error(
243
+ "Workspace ID is required. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
244
+ );
245
+ }
246
+ this.apiKey = apiKey;
247
+ this.workspaceId = workspaceId;
248
+ this.baseURL = (options.baseURL ?? "https://api.air.inc/v1").replace(/\/$/, "");
249
+ this.maxRetries = options.maxRetries ?? 3;
250
+ this.timeout = options.timeout ?? 6e4;
251
+ this._fetch = options.fetch ?? globalThis.fetch;
252
+ this._defaultHeaders = options.defaultHeaders ?? {};
253
+ }
254
+ get fetchFn() {
255
+ return this._fetch;
256
+ }
257
+ async request(options) {
258
+ return retryWithBackoff(
259
+ () => this._executeRequest(options),
260
+ this.maxRetries,
261
+ options.method
262
+ );
263
+ }
264
+ requestCursorPage(options, query) {
265
+ return new PagePromise((resolve, reject) => {
266
+ this.request(options).then((response) => {
267
+ resolve(
268
+ new CursorPage(
269
+ response,
270
+ (cursor) => this.requestCursorPage(
271
+ {
272
+ ...options,
273
+ query: { ...query, ...options.query, cursor }
274
+ },
275
+ query
276
+ )
277
+ )
278
+ );
279
+ }).catch(reject);
280
+ });
281
+ }
282
+ async _executeRequest(options) {
283
+ const url = this._buildURL(options.path, options.query);
284
+ const headers = {
285
+ "x-api-key": this.apiKey,
286
+ "x-air-workspace-id": this.workspaceId,
287
+ "user-agent": `air-api-sdk/${VERSION}`,
288
+ ...this._defaultHeaders,
289
+ ...options.headers
290
+ };
291
+ if (options.body !== void 0) {
292
+ headers["content-type"] = "application/json";
293
+ }
294
+ const timeout = options.timeout ?? this.timeout;
295
+ const controller = new AbortController();
296
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
297
+ if (options.signal) {
298
+ options.signal.addEventListener("abort", () => controller.abort());
299
+ }
300
+ let response;
301
+ try {
302
+ response = await this._fetch(url, {
303
+ method: options.method,
304
+ headers,
305
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
306
+ signal: controller.signal
307
+ });
308
+ } catch (error) {
309
+ if (error instanceof DOMException && error.name === "AbortError") {
310
+ throw new TimeoutError();
311
+ }
312
+ throw new ConnectionError(error instanceof Error ? error.message : "Connection failed");
313
+ } finally {
314
+ clearTimeout(timeoutId);
315
+ }
316
+ if (!response.ok) {
317
+ throw await APIError.fromResponse(response);
318
+ }
319
+ if (response.status === 204) {
320
+ return void 0;
321
+ }
322
+ const contentType = response.headers.get("content-type");
323
+ if (!contentType?.includes("application/json")) {
324
+ return void 0;
325
+ }
326
+ return await response.json();
327
+ }
328
+ _buildURL(path, query) {
329
+ const url = new URL(`${this.baseURL}${path}`);
330
+ if (query) {
331
+ for (const [key, value] of Object.entries(query)) {
332
+ if (value === void 0) continue;
333
+ if (Array.isArray(value)) {
334
+ for (const v of value) {
335
+ url.searchParams.append(key, v);
336
+ }
337
+ } else {
338
+ url.searchParams.set(key, String(value));
339
+ }
340
+ }
341
+ }
342
+ return url.toString();
343
+ }
344
+ };
345
+ // Annotate the CommonJS export names for ESM import in node:
346
+ 0 && (module.exports = {
347
+ APIError,
348
+ AirBase,
349
+ AuthenticationError,
350
+ BadRequestError,
351
+ ConnectionError,
352
+ CursorPage,
353
+ InternalServerError,
354
+ NotFoundError,
355
+ PagePromise,
356
+ PermissionError,
357
+ RateLimitError,
358
+ TimeoutError,
359
+ VERSION,
360
+ getRetryDelay,
361
+ isRetryableError,
362
+ retryWithBackoff
363
+ });
@@ -0,0 +1,111 @@
1
+ interface CursorPageParams {
2
+ limit?: number;
3
+ cursor?: string;
4
+ }
5
+ interface CursorPageResponse<T> {
6
+ data: T[];
7
+ pagination: {
8
+ hasMore: boolean;
9
+ cursor: string | null;
10
+ };
11
+ total?: number;
12
+ }
13
+
14
+ declare class CursorPage<T> implements AsyncIterable<T> {
15
+ readonly data: T[];
16
+ readonly pagination: {
17
+ hasMore: boolean;
18
+ cursor: string | null;
19
+ };
20
+ readonly total: number | undefined;
21
+ private _fetchNextPage;
22
+ constructor(response: CursorPageResponse<T>, fetchNextPage: ((cursor: string) => Promise<CursorPage<T>>) | null);
23
+ hasNextPage(): boolean;
24
+ getNextPage(): Promise<CursorPage<T>>;
25
+ [Symbol.asyncIterator](): AsyncIterator<T>;
26
+ }
27
+ /**
28
+ * A Promise that resolves to a CursorPage, but is also directly async-iterable.
29
+ * This allows both patterns:
30
+ * - `for await (const item of client.assets.list())` (auto-paginate)
31
+ * - `const page = await client.assets.list()` (manual pagination)
32
+ */
33
+ declare class PagePromise<T> extends Promise<CursorPage<T>> implements AsyncIterable<T> {
34
+ [Symbol.asyncIterator](): AsyncIterator<T>;
35
+ }
36
+
37
+ interface RequestOptions {
38
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
39
+ path: string;
40
+ query?: Record<string, string | string[] | number | boolean | undefined>;
41
+ body?: unknown;
42
+ headers?: Record<string, string>;
43
+ timeout?: number;
44
+ signal?: AbortSignal;
45
+ }
46
+
47
+ interface AirBaseOptions {
48
+ apiKey?: string;
49
+ workspaceId?: string;
50
+ baseURL?: string;
51
+ maxRetries?: number;
52
+ timeout?: number;
53
+ fetch?: typeof globalThis.fetch;
54
+ defaultHeaders?: Record<string, string>;
55
+ }
56
+ declare class AirBase {
57
+ readonly apiKey: string;
58
+ workspaceId: string;
59
+ readonly baseURL: string;
60
+ readonly maxRetries: number;
61
+ readonly timeout: number;
62
+ private _fetch;
63
+ private _defaultHeaders;
64
+ constructor(options?: AirBaseOptions);
65
+ get fetchFn(): typeof globalThis.fetch;
66
+ request<T>(options: RequestOptions): Promise<T>;
67
+ requestCursorPage<T>(options: RequestOptions, query?: Record<string, string | string[] | number | boolean | undefined>): PagePromise<T>;
68
+ private _executeRequest;
69
+ private _buildURL;
70
+ }
71
+
72
+ declare class APIError extends Error {
73
+ readonly status: number;
74
+ readonly headers: Headers;
75
+ readonly body: unknown;
76
+ constructor(status: number, body: unknown, message: string, headers: Headers);
77
+ static fromResponse(response: Response): Promise<APIError>;
78
+ }
79
+ declare class BadRequestError extends APIError {
80
+ constructor(status: number, body: unknown, message: string, headers: Headers);
81
+ }
82
+ declare class AuthenticationError extends APIError {
83
+ constructor(status: number, body: unknown, message: string, headers: Headers);
84
+ }
85
+ declare class PermissionError extends APIError {
86
+ constructor(status: number, body: unknown, message: string, headers: Headers);
87
+ }
88
+ declare class NotFoundError extends APIError {
89
+ constructor(status: number, body: unknown, message: string, headers: Headers);
90
+ }
91
+ declare class RateLimitError extends APIError {
92
+ readonly retryAfter: number | null;
93
+ constructor(status: number, body: unknown, message: string, headers: Headers);
94
+ }
95
+ declare class InternalServerError extends APIError {
96
+ constructor(status: number, body: unknown, message: string, headers: Headers);
97
+ }
98
+ declare class ConnectionError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ declare class TimeoutError extends ConnectionError {
102
+ constructor(message?: string);
103
+ }
104
+
105
+ declare function isRetryableError(error: unknown, method?: string): boolean;
106
+ declare function getRetryDelay(error: unknown, attempt: number, baseDelay?: number): number;
107
+ declare function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, method?: string): Promise<T>;
108
+
109
+ declare const VERSION = "0.1.0";
110
+
111
+ export { APIError, AirBase, type AirBaseOptions, AuthenticationError, BadRequestError, ConnectionError, CursorPage, type CursorPageParams, type CursorPageResponse, InternalServerError, NotFoundError, PagePromise, PermissionError, RateLimitError, type RequestOptions, TimeoutError, VERSION, getRetryDelay, isRetryableError, retryWithBackoff };
@@ -0,0 +1,111 @@
1
+ interface CursorPageParams {
2
+ limit?: number;
3
+ cursor?: string;
4
+ }
5
+ interface CursorPageResponse<T> {
6
+ data: T[];
7
+ pagination: {
8
+ hasMore: boolean;
9
+ cursor: string | null;
10
+ };
11
+ total?: number;
12
+ }
13
+
14
+ declare class CursorPage<T> implements AsyncIterable<T> {
15
+ readonly data: T[];
16
+ readonly pagination: {
17
+ hasMore: boolean;
18
+ cursor: string | null;
19
+ };
20
+ readonly total: number | undefined;
21
+ private _fetchNextPage;
22
+ constructor(response: CursorPageResponse<T>, fetchNextPage: ((cursor: string) => Promise<CursorPage<T>>) | null);
23
+ hasNextPage(): boolean;
24
+ getNextPage(): Promise<CursorPage<T>>;
25
+ [Symbol.asyncIterator](): AsyncIterator<T>;
26
+ }
27
+ /**
28
+ * A Promise that resolves to a CursorPage, but is also directly async-iterable.
29
+ * This allows both patterns:
30
+ * - `for await (const item of client.assets.list())` (auto-paginate)
31
+ * - `const page = await client.assets.list()` (manual pagination)
32
+ */
33
+ declare class PagePromise<T> extends Promise<CursorPage<T>> implements AsyncIterable<T> {
34
+ [Symbol.asyncIterator](): AsyncIterator<T>;
35
+ }
36
+
37
+ interface RequestOptions {
38
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
39
+ path: string;
40
+ query?: Record<string, string | string[] | number | boolean | undefined>;
41
+ body?: unknown;
42
+ headers?: Record<string, string>;
43
+ timeout?: number;
44
+ signal?: AbortSignal;
45
+ }
46
+
47
+ interface AirBaseOptions {
48
+ apiKey?: string;
49
+ workspaceId?: string;
50
+ baseURL?: string;
51
+ maxRetries?: number;
52
+ timeout?: number;
53
+ fetch?: typeof globalThis.fetch;
54
+ defaultHeaders?: Record<string, string>;
55
+ }
56
+ declare class AirBase {
57
+ readonly apiKey: string;
58
+ workspaceId: string;
59
+ readonly baseURL: string;
60
+ readonly maxRetries: number;
61
+ readonly timeout: number;
62
+ private _fetch;
63
+ private _defaultHeaders;
64
+ constructor(options?: AirBaseOptions);
65
+ get fetchFn(): typeof globalThis.fetch;
66
+ request<T>(options: RequestOptions): Promise<T>;
67
+ requestCursorPage<T>(options: RequestOptions, query?: Record<string, string | string[] | number | boolean | undefined>): PagePromise<T>;
68
+ private _executeRequest;
69
+ private _buildURL;
70
+ }
71
+
72
+ declare class APIError extends Error {
73
+ readonly status: number;
74
+ readonly headers: Headers;
75
+ readonly body: unknown;
76
+ constructor(status: number, body: unknown, message: string, headers: Headers);
77
+ static fromResponse(response: Response): Promise<APIError>;
78
+ }
79
+ declare class BadRequestError extends APIError {
80
+ constructor(status: number, body: unknown, message: string, headers: Headers);
81
+ }
82
+ declare class AuthenticationError extends APIError {
83
+ constructor(status: number, body: unknown, message: string, headers: Headers);
84
+ }
85
+ declare class PermissionError extends APIError {
86
+ constructor(status: number, body: unknown, message: string, headers: Headers);
87
+ }
88
+ declare class NotFoundError extends APIError {
89
+ constructor(status: number, body: unknown, message: string, headers: Headers);
90
+ }
91
+ declare class RateLimitError extends APIError {
92
+ readonly retryAfter: number | null;
93
+ constructor(status: number, body: unknown, message: string, headers: Headers);
94
+ }
95
+ declare class InternalServerError extends APIError {
96
+ constructor(status: number, body: unknown, message: string, headers: Headers);
97
+ }
98
+ declare class ConnectionError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ declare class TimeoutError extends ConnectionError {
102
+ constructor(message?: string);
103
+ }
104
+
105
+ declare function isRetryableError(error: unknown, method?: string): boolean;
106
+ declare function getRetryDelay(error: unknown, attempt: number, baseDelay?: number): number;
107
+ declare function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, method?: string): Promise<T>;
108
+
109
+ declare const VERSION = "0.1.0";
110
+
111
+ export { APIError, AirBase, type AirBaseOptions, AuthenticationError, BadRequestError, ConnectionError, CursorPage, type CursorPageParams, type CursorPageResponse, InternalServerError, NotFoundError, PagePromise, PermissionError, RateLimitError, type RequestOptions, TimeoutError, VERSION, getRetryDelay, isRetryableError, retryWithBackoff };
package/dist/index.js ADDED
@@ -0,0 +1,321 @@
1
+ // src/errors.ts
2
+ var APIError = class _APIError extends Error {
3
+ status;
4
+ headers;
5
+ body;
6
+ constructor(status, body, message, headers) {
7
+ super(message);
8
+ this.name = "APIError";
9
+ this.status = status;
10
+ this.body = body;
11
+ this.headers = headers;
12
+ }
13
+ static async fromResponse(response) {
14
+ const headers = response.headers;
15
+ let body;
16
+ let message;
17
+ try {
18
+ body = await response.json();
19
+ message = typeof body === "object" && body !== null && "message" in body ? String(body.message) : `Request failed with status ${response.status}`;
20
+ } catch {
21
+ body = await response.text().catch(() => "");
22
+ message = `Request failed with status ${response.status}`;
23
+ }
24
+ const status = response.status;
25
+ switch (status) {
26
+ case 400:
27
+ return new BadRequestError(status, body, message, headers);
28
+ case 401:
29
+ return new AuthenticationError(status, body, message, headers);
30
+ case 403:
31
+ return new PermissionError(status, body, message, headers);
32
+ case 404:
33
+ return new NotFoundError(status, body, message, headers);
34
+ case 429:
35
+ return new RateLimitError(status, body, message, headers);
36
+ default:
37
+ if (status >= 500) {
38
+ return new InternalServerError(status, body, message, headers);
39
+ }
40
+ return new _APIError(status, body, message, headers);
41
+ }
42
+ }
43
+ };
44
+ var BadRequestError = class extends APIError {
45
+ constructor(status, body, message, headers) {
46
+ super(status, body, message, headers);
47
+ this.name = "BadRequestError";
48
+ }
49
+ };
50
+ var AuthenticationError = class extends APIError {
51
+ constructor(status, body, message, headers) {
52
+ super(status, body, message, headers);
53
+ this.name = "AuthenticationError";
54
+ }
55
+ };
56
+ var PermissionError = class extends APIError {
57
+ constructor(status, body, message, headers) {
58
+ super(status, body, message, headers);
59
+ this.name = "PermissionError";
60
+ }
61
+ };
62
+ var NotFoundError = class extends APIError {
63
+ constructor(status, body, message, headers) {
64
+ super(status, body, message, headers);
65
+ this.name = "NotFoundError";
66
+ }
67
+ };
68
+ var RateLimitError = class extends APIError {
69
+ retryAfter;
70
+ constructor(status, body, message, headers) {
71
+ super(status, body, message, headers);
72
+ this.name = "RateLimitError";
73
+ const retryAfterHeader = headers.get("retry-after");
74
+ this.retryAfter = retryAfterHeader ? Number(retryAfterHeader) : null;
75
+ }
76
+ };
77
+ var InternalServerError = class extends APIError {
78
+ constructor(status, body, message, headers) {
79
+ super(status, body, message, headers);
80
+ this.name = "InternalServerError";
81
+ }
82
+ };
83
+ var ConnectionError = class extends Error {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = "ConnectionError";
87
+ }
88
+ };
89
+ var TimeoutError = class extends ConnectionError {
90
+ constructor(message = "Request timed out") {
91
+ super(message);
92
+ this.name = "TimeoutError";
93
+ }
94
+ };
95
+
96
+ // src/pagination.ts
97
+ var CursorPage = class {
98
+ data;
99
+ pagination;
100
+ total;
101
+ _fetchNextPage;
102
+ constructor(response, fetchNextPage) {
103
+ this.data = response.data;
104
+ this.pagination = response.pagination;
105
+ this.total = response.total;
106
+ this._fetchNextPage = fetchNextPage;
107
+ }
108
+ hasNextPage() {
109
+ return this.pagination.hasMore && this.pagination.cursor !== null;
110
+ }
111
+ async getNextPage() {
112
+ if (!this.hasNextPage() || !this._fetchNextPage) {
113
+ throw new Error("No more pages available");
114
+ }
115
+ return this._fetchNextPage(this.pagination.cursor);
116
+ }
117
+ async *[Symbol.asyncIterator]() {
118
+ let page = this;
119
+ while (true) {
120
+ for (const item of page.data) {
121
+ yield item;
122
+ }
123
+ if (!page.hasNextPage()) break;
124
+ page = await page.getNextPage();
125
+ }
126
+ }
127
+ };
128
+ var PagePromise = class extends Promise {
129
+ async *[Symbol.asyncIterator]() {
130
+ const page = await this;
131
+ yield* page;
132
+ }
133
+ };
134
+
135
+ // src/retry.ts
136
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
137
+ var SERVER_ERROR_CODES = /* @__PURE__ */ new Set([500, 502, 503, 504]);
138
+ function isRetryableError(error, method) {
139
+ if (error instanceof APIError) {
140
+ if (method === "POST" && SERVER_ERROR_CODES.has(error.status)) {
141
+ return false;
142
+ }
143
+ return RETRYABLE_STATUS_CODES.has(error.status);
144
+ }
145
+ if (error instanceof ConnectionError) {
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+ function getRetryDelay(error, attempt, baseDelay = 500) {
151
+ if (error instanceof RateLimitError && error.retryAfter !== null) {
152
+ const retryAfterMs = error.retryAfter * 1e3;
153
+ const jitter2 = retryAfterMs * 0.2 * Math.random();
154
+ return retryAfterMs + jitter2;
155
+ }
156
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
157
+ const jitter = exponentialDelay * 0.2 * Math.random();
158
+ return exponentialDelay + jitter;
159
+ }
160
+ async function retryWithBackoff(fn, maxRetries, method) {
161
+ let lastError;
162
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
163
+ try {
164
+ return await fn();
165
+ } catch (error) {
166
+ lastError = error;
167
+ if (attempt === maxRetries || !isRetryableError(error, method)) {
168
+ throw error;
169
+ }
170
+ const delay = getRetryDelay(error, attempt);
171
+ await sleep(delay);
172
+ }
173
+ }
174
+ throw lastError;
175
+ }
176
+ function sleep(ms) {
177
+ return new Promise((resolve) => setTimeout(resolve, ms));
178
+ }
179
+
180
+ // src/version.ts
181
+ var VERSION = "0.1.0";
182
+
183
+ // src/base-client.ts
184
+ var AirBase = class {
185
+ apiKey;
186
+ workspaceId;
187
+ baseURL;
188
+ maxRetries;
189
+ timeout;
190
+ _fetch;
191
+ _defaultHeaders;
192
+ constructor(options = {}) {
193
+ const apiKey = options.apiKey ?? process.env.AIR_API_KEY;
194
+ if (!apiKey) {
195
+ throw new Error(
196
+ "API key is required. Pass it as `apiKey` option or set the AIR_API_KEY environment variable."
197
+ );
198
+ }
199
+ const workspaceId = options.workspaceId ?? process.env.AIR_WORKSPACE_ID;
200
+ if (!workspaceId) {
201
+ throw new Error(
202
+ "Workspace ID is required. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
203
+ );
204
+ }
205
+ this.apiKey = apiKey;
206
+ this.workspaceId = workspaceId;
207
+ this.baseURL = (options.baseURL ?? "https://api.air.inc/v1").replace(/\/$/, "");
208
+ this.maxRetries = options.maxRetries ?? 3;
209
+ this.timeout = options.timeout ?? 6e4;
210
+ this._fetch = options.fetch ?? globalThis.fetch;
211
+ this._defaultHeaders = options.defaultHeaders ?? {};
212
+ }
213
+ get fetchFn() {
214
+ return this._fetch;
215
+ }
216
+ async request(options) {
217
+ return retryWithBackoff(
218
+ () => this._executeRequest(options),
219
+ this.maxRetries,
220
+ options.method
221
+ );
222
+ }
223
+ requestCursorPage(options, query) {
224
+ return new PagePromise((resolve, reject) => {
225
+ this.request(options).then((response) => {
226
+ resolve(
227
+ new CursorPage(
228
+ response,
229
+ (cursor) => this.requestCursorPage(
230
+ {
231
+ ...options,
232
+ query: { ...query, ...options.query, cursor }
233
+ },
234
+ query
235
+ )
236
+ )
237
+ );
238
+ }).catch(reject);
239
+ });
240
+ }
241
+ async _executeRequest(options) {
242
+ const url = this._buildURL(options.path, options.query);
243
+ const headers = {
244
+ "x-api-key": this.apiKey,
245
+ "x-air-workspace-id": this.workspaceId,
246
+ "user-agent": `air-api-sdk/${VERSION}`,
247
+ ...this._defaultHeaders,
248
+ ...options.headers
249
+ };
250
+ if (options.body !== void 0) {
251
+ headers["content-type"] = "application/json";
252
+ }
253
+ const timeout = options.timeout ?? this.timeout;
254
+ const controller = new AbortController();
255
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
256
+ if (options.signal) {
257
+ options.signal.addEventListener("abort", () => controller.abort());
258
+ }
259
+ let response;
260
+ try {
261
+ response = await this._fetch(url, {
262
+ method: options.method,
263
+ headers,
264
+ body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
265
+ signal: controller.signal
266
+ });
267
+ } catch (error) {
268
+ if (error instanceof DOMException && error.name === "AbortError") {
269
+ throw new TimeoutError();
270
+ }
271
+ throw new ConnectionError(error instanceof Error ? error.message : "Connection failed");
272
+ } finally {
273
+ clearTimeout(timeoutId);
274
+ }
275
+ if (!response.ok) {
276
+ throw await APIError.fromResponse(response);
277
+ }
278
+ if (response.status === 204) {
279
+ return void 0;
280
+ }
281
+ const contentType = response.headers.get("content-type");
282
+ if (!contentType?.includes("application/json")) {
283
+ return void 0;
284
+ }
285
+ return await response.json();
286
+ }
287
+ _buildURL(path, query) {
288
+ const url = new URL(`${this.baseURL}${path}`);
289
+ if (query) {
290
+ for (const [key, value] of Object.entries(query)) {
291
+ if (value === void 0) continue;
292
+ if (Array.isArray(value)) {
293
+ for (const v of value) {
294
+ url.searchParams.append(key, v);
295
+ }
296
+ } else {
297
+ url.searchParams.set(key, String(value));
298
+ }
299
+ }
300
+ }
301
+ return url.toString();
302
+ }
303
+ };
304
+ export {
305
+ APIError,
306
+ AirBase,
307
+ AuthenticationError,
308
+ BadRequestError,
309
+ ConnectionError,
310
+ CursorPage,
311
+ InternalServerError,
312
+ NotFoundError,
313
+ PagePromise,
314
+ PermissionError,
315
+ RateLimitError,
316
+ TimeoutError,
317
+ VERSION,
318
+ getRetryDelay,
319
+ isRetryableError,
320
+ retryWithBackoff
321
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@air/api-core",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Core HTTP engine for Air API SDK",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "prepublishOnly": "npm run build",
26
+ "build": "tsup",
27
+ "test": "vitest run",
28
+ "typecheck": "tsc -b",
29
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/AirLabsTeam/air-api-sdk.git",
40
+ "directory": "packages/core"
41
+ },
42
+ "license": "UNLICENSED"
43
+ }