@air/api-core 0.1.2 → 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/index.cjs CHANGED
@@ -33,6 +33,9 @@ __export(index_exports, {
33
33
  RateLimitError: () => RateLimitError,
34
34
  TimeoutError: () => TimeoutError,
35
35
  VERSION: () => VERSION,
36
+ buildAuthorizationUrl: () => buildAuthorizationUrl,
37
+ exchangeAuthorizationCode: () => exchangeAuthorizationCode,
38
+ generatePKCEChallenge: () => generatePKCEChallenge,
36
39
  getRetryDelay: () => getRetryDelay,
37
40
  isRetryableError: () => isRetryableError,
38
41
  retryWithBackoff: () => retryWithBackoff
@@ -224,6 +227,7 @@ var VERSION = "0.1.0";
224
227
  // src/base-client.ts
225
228
  var AirBase = class {
226
229
  apiKey;
230
+ accessToken;
227
231
  workspaceId;
228
232
  baseURL;
229
233
  maxRetries;
@@ -231,19 +235,41 @@ var AirBase = class {
231
235
  _fetch;
232
236
  _defaultHeaders;
233
237
  constructor(options = {}) {
234
- const apiKey = options.apiKey ?? process.env.AIR_API_KEY;
235
- if (!apiKey) {
238
+ let apiKey;
239
+ let accessToken;
240
+ if (options.apiKey !== void 0 && options.accessToken !== void 0) {
236
241
  throw new Error(
237
- "API key is required. Pass it as `apiKey` option or set the AIR_API_KEY environment variable."
242
+ "Provide either `apiKey` or `accessToken`, not both. API key auth and OAuth bearer auth are mutually exclusive."
243
+ );
244
+ }
245
+ if (options.apiKey !== void 0) {
246
+ apiKey = options.apiKey;
247
+ } else if (options.accessToken !== void 0) {
248
+ accessToken = options.accessToken;
249
+ } else {
250
+ const envApiKey = process.env.AIR_API_KEY;
251
+ const envAccessToken = process.env.AIR_ACCESS_TOKEN;
252
+ if (envApiKey && envAccessToken) {
253
+ throw new Error(
254
+ "Both AIR_API_KEY and AIR_ACCESS_TOKEN are set. Set only one, or pass `apiKey`/`accessToken` explicitly to choose."
255
+ );
256
+ }
257
+ apiKey = envApiKey;
258
+ accessToken = envAccessToken;
259
+ }
260
+ if (!apiKey && !accessToken) {
261
+ throw new Error(
262
+ "Authentication is required. Pass `apiKey` (or set AIR_API_KEY) for API key auth, or `accessToken` (or set AIR_ACCESS_TOKEN) for OAuth bearer auth."
238
263
  );
239
264
  }
240
265
  const workspaceId = options.workspaceId ?? process.env.AIR_WORKSPACE_ID;
241
- if (!workspaceId) {
266
+ if (apiKey && !workspaceId) {
242
267
  throw new Error(
243
- "Workspace ID is required. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
268
+ "Workspace ID is required when using API key authentication. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
244
269
  );
245
270
  }
246
271
  this.apiKey = apiKey;
272
+ this.accessToken = accessToken;
247
273
  this.workspaceId = workspaceId;
248
274
  this.baseURL = (options.baseURL ?? "https://api.air.inc/v1").replace(/\/$/, "");
249
275
  this.maxRetries = options.maxRetries ?? 3;
@@ -282,12 +308,17 @@ var AirBase = class {
282
308
  async _executeRequest(options) {
283
309
  const url = this._buildURL(options.path, options.query);
284
310
  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
311
+ "user-agent": `air-api-sdk/${VERSION}`
290
312
  };
313
+ if (this.apiKey) {
314
+ headers["x-api-key"] = this.apiKey;
315
+ } else if (this.accessToken) {
316
+ headers["authorization"] = `Bearer ${this.accessToken}`;
317
+ }
318
+ if (this.workspaceId) {
319
+ headers["x-air-workspace-id"] = this.workspaceId;
320
+ }
321
+ Object.assign(headers, this._defaultHeaders, options.headers);
291
322
  if (options.body !== void 0) {
292
323
  headers["content-type"] = "application/json";
293
324
  }
@@ -342,6 +373,90 @@ var AirBase = class {
342
373
  return url.toString();
343
374
  }
344
375
  };
376
+
377
+ // src/oauth.ts
378
+ function buildAuthorizationUrl(options) {
379
+ const url = new URL(options.authorizeUrl);
380
+ url.searchParams.set("response_type", "code");
381
+ url.searchParams.set("client_id", options.clientId);
382
+ url.searchParams.set("redirect_uri", options.redirectUri);
383
+ url.searchParams.set("state", options.state);
384
+ url.searchParams.set("code_challenge", options.codeChallenge);
385
+ url.searchParams.set("code_challenge_method", "S256");
386
+ if (options.scopes !== void 0) {
387
+ const scope = Array.isArray(options.scopes) ? options.scopes.join(" ") : options.scopes;
388
+ if (scope.length > 0) {
389
+ url.searchParams.set("scope", scope);
390
+ }
391
+ }
392
+ return url.toString();
393
+ }
394
+ async function exchangeAuthorizationCode(options) {
395
+ const fetchFn = options.fetch ?? globalThis.fetch;
396
+ const body = new URLSearchParams({
397
+ grant_type: "authorization_code",
398
+ code: options.code,
399
+ code_verifier: options.codeVerifier,
400
+ redirect_uri: options.redirectUri
401
+ });
402
+ const headers = {
403
+ "content-type": "application/x-www-form-urlencoded"
404
+ };
405
+ if (options.clientSecret === void 0) {
406
+ body.set("client_id", options.clientId);
407
+ } else {
408
+ applyClientAuth(headers, body, {
409
+ clientId: options.clientId,
410
+ clientSecret: options.clientSecret,
411
+ method: options.clientAuthMethod ?? "basic"
412
+ });
413
+ }
414
+ return executeTokenRequest(fetchFn, options.tokenUrl, headers, body.toString());
415
+ }
416
+ function applyClientAuth(headers, body, args) {
417
+ if (args.method === "basic") {
418
+ headers["authorization"] = `Basic ${base64Encode(`${args.clientId}:${args.clientSecret}`)}`;
419
+ } else {
420
+ body.set("client_id", args.clientId);
421
+ body.set("client_secret", args.clientSecret);
422
+ }
423
+ }
424
+ async function executeTokenRequest(fetchFn, tokenUrl, headers, body) {
425
+ let response;
426
+ try {
427
+ response = await fetchFn(tokenUrl, { method: "POST", headers, body });
428
+ } catch (error) {
429
+ throw new ConnectionError(error instanceof Error ? error.message : "Connection failed");
430
+ }
431
+ if (!response.ok) {
432
+ throw await APIError.fromResponse(response);
433
+ }
434
+ const raw = await response.json();
435
+ return {
436
+ accessToken: raw.access_token,
437
+ tokenType: raw.token_type,
438
+ expiresIn: raw.expires_in,
439
+ scope: raw.scope,
440
+ refreshToken: raw.refresh_token
441
+ };
442
+ }
443
+ function base64Encode(input) {
444
+ if (typeof Buffer !== "undefined") {
445
+ return Buffer.from(input, "utf-8").toString("base64");
446
+ }
447
+ return btoa(input);
448
+ }
449
+
450
+ // src/pkce.ts
451
+ var import_node_crypto = require("crypto");
452
+ function generatePKCEChallenge() {
453
+ const codeVerifier = base64UrlEncode((0, import_node_crypto.randomBytes)(32));
454
+ const codeChallenge = base64UrlEncode((0, import_node_crypto.createHash)("sha256").update(codeVerifier).digest());
455
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
456
+ }
457
+ function base64UrlEncode(input) {
458
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
459
+ }
345
460
  // Annotate the CommonJS export names for ESM import in node:
346
461
  0 && (module.exports = {
347
462
  APIError,
@@ -357,6 +472,9 @@ var AirBase = class {
357
472
  RateLimitError,
358
473
  TimeoutError,
359
474
  VERSION,
475
+ buildAuthorizationUrl,
476
+ exchangeAuthorizationCode,
477
+ generatePKCEChallenge,
360
478
  getRetryDelay,
361
479
  isRetryableError,
362
480
  retryWithBackoff
package/dist/index.d.cts CHANGED
@@ -46,6 +46,7 @@ interface RequestOptions {
46
46
 
47
47
  interface AirBaseOptions {
48
48
  apiKey?: string;
49
+ accessToken?: string;
49
50
  workspaceId?: string;
50
51
  baseURL?: string;
51
52
  maxRetries?: number;
@@ -54,8 +55,9 @@ interface AirBaseOptions {
54
55
  defaultHeaders?: Record<string, string>;
55
56
  }
56
57
  declare class AirBase {
57
- readonly apiKey: string;
58
- workspaceId: string;
58
+ readonly apiKey: string | undefined;
59
+ readonly accessToken: string | undefined;
60
+ workspaceId: string | undefined;
59
61
  readonly baseURL: string;
60
62
  readonly maxRetries: number;
61
63
  readonly timeout: number;
@@ -108,4 +110,72 @@ declare function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, m
108
110
 
109
111
  declare const VERSION = "0.1.0";
110
112
 
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 };
113
+ /**
114
+ * How the client authenticates to the OAuth token endpoint.
115
+ *
116
+ * - `"basic"` (default for confidential clients) — HTTP Basic auth header
117
+ * `Authorization: Basic base64(client_id:client_secret)`. Preferred per
118
+ * RFC 6749 §2.3.1.
119
+ * - `"body"` — credentials sent as `client_id` (+ `client_secret`) form
120
+ * fields in the POST body. Useful when an upstream load balancer strips
121
+ * `Authorization` headers, or for public clients with no secret.
122
+ */
123
+ type OAuthClientAuthMethod = "basic" | "body";
124
+ interface OAuthAccessToken {
125
+ accessToken: string;
126
+ tokenType: string;
127
+ expiresIn: number;
128
+ scope?: string;
129
+ refreshToken?: string;
130
+ }
131
+ interface BuildAuthorizationUrlOptions {
132
+ authorizeUrl: string;
133
+ clientId: string;
134
+ redirectUri: string;
135
+ codeChallenge: string;
136
+ state: string;
137
+ scopes?: string | readonly string[];
138
+ }
139
+ /**
140
+ * Builds the OAuth 2.0 authorization URL for the authorization_code + PKCE
141
+ * flow. The user is redirected to this URL in a browser; after login + consent
142
+ * the IdP redirects back to `redirectUri` with a `code` query parameter.
143
+ */
144
+ declare function buildAuthorizationUrl(options: BuildAuthorizationUrlOptions): string;
145
+ interface ExchangeAuthorizationCodeOptions {
146
+ tokenUrl: string;
147
+ clientId: string;
148
+ clientSecret?: string;
149
+ code: string;
150
+ codeVerifier: string;
151
+ redirectUri: string;
152
+ /**
153
+ * Defaults to `"basic"` for confidential clients (those with a
154
+ * `clientSecret`). Public clients always send `client_id` in the body.
155
+ */
156
+ clientAuthMethod?: OAuthClientAuthMethod;
157
+ fetch?: typeof globalThis.fetch;
158
+ }
159
+ /**
160
+ * Exchanges an authorization code for an access token using PKCE. Confidential
161
+ * clients (`clientSecret` provided) authenticate via the method chosen with
162
+ * `clientAuthMethod`. Public clients always send `client_id` in the body.
163
+ */
164
+ declare function exchangeAuthorizationCode(options: ExchangeAuthorizationCodeOptions): Promise<OAuthAccessToken>;
165
+
166
+ interface PKCEChallenge {
167
+ codeVerifier: string;
168
+ codeChallenge: string;
169
+ codeChallengeMethod: "S256";
170
+ }
171
+ /**
172
+ * Generates a PKCE code verifier and matching S256 challenge per RFC 7636.
173
+ *
174
+ * The verifier is 43 base64url characters (32 random bytes encoded). The
175
+ * challenge is the base64url-encoded SHA-256 hash of the verifier. Use the
176
+ * challenge in the authorization request and the verifier when exchanging
177
+ * the resulting code for a token.
178
+ */
179
+ declare function generatePKCEChallenge(): PKCEChallenge;
180
+
181
+ export { APIError, AirBase, type AirBaseOptions, AuthenticationError, BadRequestError, type BuildAuthorizationUrlOptions, ConnectionError, CursorPage, type CursorPageParams, type CursorPageResponse, type ExchangeAuthorizationCodeOptions, InternalServerError, NotFoundError, type OAuthAccessToken, type OAuthClientAuthMethod, type PKCEChallenge, PagePromise, PermissionError, RateLimitError, type RequestOptions, TimeoutError, VERSION, buildAuthorizationUrl, exchangeAuthorizationCode, generatePKCEChallenge, getRetryDelay, isRetryableError, retryWithBackoff };
package/dist/index.d.ts CHANGED
@@ -46,6 +46,7 @@ interface RequestOptions {
46
46
 
47
47
  interface AirBaseOptions {
48
48
  apiKey?: string;
49
+ accessToken?: string;
49
50
  workspaceId?: string;
50
51
  baseURL?: string;
51
52
  maxRetries?: number;
@@ -54,8 +55,9 @@ interface AirBaseOptions {
54
55
  defaultHeaders?: Record<string, string>;
55
56
  }
56
57
  declare class AirBase {
57
- readonly apiKey: string;
58
- workspaceId: string;
58
+ readonly apiKey: string | undefined;
59
+ readonly accessToken: string | undefined;
60
+ workspaceId: string | undefined;
59
61
  readonly baseURL: string;
60
62
  readonly maxRetries: number;
61
63
  readonly timeout: number;
@@ -108,4 +110,72 @@ declare function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries: number, m
108
110
 
109
111
  declare const VERSION = "0.1.0";
110
112
 
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 };
113
+ /**
114
+ * How the client authenticates to the OAuth token endpoint.
115
+ *
116
+ * - `"basic"` (default for confidential clients) — HTTP Basic auth header
117
+ * `Authorization: Basic base64(client_id:client_secret)`. Preferred per
118
+ * RFC 6749 §2.3.1.
119
+ * - `"body"` — credentials sent as `client_id` (+ `client_secret`) form
120
+ * fields in the POST body. Useful when an upstream load balancer strips
121
+ * `Authorization` headers, or for public clients with no secret.
122
+ */
123
+ type OAuthClientAuthMethod = "basic" | "body";
124
+ interface OAuthAccessToken {
125
+ accessToken: string;
126
+ tokenType: string;
127
+ expiresIn: number;
128
+ scope?: string;
129
+ refreshToken?: string;
130
+ }
131
+ interface BuildAuthorizationUrlOptions {
132
+ authorizeUrl: string;
133
+ clientId: string;
134
+ redirectUri: string;
135
+ codeChallenge: string;
136
+ state: string;
137
+ scopes?: string | readonly string[];
138
+ }
139
+ /**
140
+ * Builds the OAuth 2.0 authorization URL for the authorization_code + PKCE
141
+ * flow. The user is redirected to this URL in a browser; after login + consent
142
+ * the IdP redirects back to `redirectUri` with a `code` query parameter.
143
+ */
144
+ declare function buildAuthorizationUrl(options: BuildAuthorizationUrlOptions): string;
145
+ interface ExchangeAuthorizationCodeOptions {
146
+ tokenUrl: string;
147
+ clientId: string;
148
+ clientSecret?: string;
149
+ code: string;
150
+ codeVerifier: string;
151
+ redirectUri: string;
152
+ /**
153
+ * Defaults to `"basic"` for confidential clients (those with a
154
+ * `clientSecret`). Public clients always send `client_id` in the body.
155
+ */
156
+ clientAuthMethod?: OAuthClientAuthMethod;
157
+ fetch?: typeof globalThis.fetch;
158
+ }
159
+ /**
160
+ * Exchanges an authorization code for an access token using PKCE. Confidential
161
+ * clients (`clientSecret` provided) authenticate via the method chosen with
162
+ * `clientAuthMethod`. Public clients always send `client_id` in the body.
163
+ */
164
+ declare function exchangeAuthorizationCode(options: ExchangeAuthorizationCodeOptions): Promise<OAuthAccessToken>;
165
+
166
+ interface PKCEChallenge {
167
+ codeVerifier: string;
168
+ codeChallenge: string;
169
+ codeChallengeMethod: "S256";
170
+ }
171
+ /**
172
+ * Generates a PKCE code verifier and matching S256 challenge per RFC 7636.
173
+ *
174
+ * The verifier is 43 base64url characters (32 random bytes encoded). The
175
+ * challenge is the base64url-encoded SHA-256 hash of the verifier. Use the
176
+ * challenge in the authorization request and the verifier when exchanging
177
+ * the resulting code for a token.
178
+ */
179
+ declare function generatePKCEChallenge(): PKCEChallenge;
180
+
181
+ export { APIError, AirBase, type AirBaseOptions, AuthenticationError, BadRequestError, type BuildAuthorizationUrlOptions, ConnectionError, CursorPage, type CursorPageParams, type CursorPageResponse, type ExchangeAuthorizationCodeOptions, InternalServerError, NotFoundError, type OAuthAccessToken, type OAuthClientAuthMethod, type PKCEChallenge, PagePromise, PermissionError, RateLimitError, type RequestOptions, TimeoutError, VERSION, buildAuthorizationUrl, exchangeAuthorizationCode, generatePKCEChallenge, getRetryDelay, isRetryableError, retryWithBackoff };
package/dist/index.js CHANGED
@@ -183,6 +183,7 @@ var VERSION = "0.1.0";
183
183
  // src/base-client.ts
184
184
  var AirBase = class {
185
185
  apiKey;
186
+ accessToken;
186
187
  workspaceId;
187
188
  baseURL;
188
189
  maxRetries;
@@ -190,19 +191,41 @@ var AirBase = class {
190
191
  _fetch;
191
192
  _defaultHeaders;
192
193
  constructor(options = {}) {
193
- const apiKey = options.apiKey ?? process.env.AIR_API_KEY;
194
- if (!apiKey) {
194
+ let apiKey;
195
+ let accessToken;
196
+ if (options.apiKey !== void 0 && options.accessToken !== void 0) {
195
197
  throw new Error(
196
- "API key is required. Pass it as `apiKey` option or set the AIR_API_KEY environment variable."
198
+ "Provide either `apiKey` or `accessToken`, not both. API key auth and OAuth bearer auth are mutually exclusive."
199
+ );
200
+ }
201
+ if (options.apiKey !== void 0) {
202
+ apiKey = options.apiKey;
203
+ } else if (options.accessToken !== void 0) {
204
+ accessToken = options.accessToken;
205
+ } else {
206
+ const envApiKey = process.env.AIR_API_KEY;
207
+ const envAccessToken = process.env.AIR_ACCESS_TOKEN;
208
+ if (envApiKey && envAccessToken) {
209
+ throw new Error(
210
+ "Both AIR_API_KEY and AIR_ACCESS_TOKEN are set. Set only one, or pass `apiKey`/`accessToken` explicitly to choose."
211
+ );
212
+ }
213
+ apiKey = envApiKey;
214
+ accessToken = envAccessToken;
215
+ }
216
+ if (!apiKey && !accessToken) {
217
+ throw new Error(
218
+ "Authentication is required. Pass `apiKey` (or set AIR_API_KEY) for API key auth, or `accessToken` (or set AIR_ACCESS_TOKEN) for OAuth bearer auth."
197
219
  );
198
220
  }
199
221
  const workspaceId = options.workspaceId ?? process.env.AIR_WORKSPACE_ID;
200
- if (!workspaceId) {
222
+ if (apiKey && !workspaceId) {
201
223
  throw new Error(
202
- "Workspace ID is required. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
224
+ "Workspace ID is required when using API key authentication. Pass it as `workspaceId` option or set the AIR_WORKSPACE_ID environment variable."
203
225
  );
204
226
  }
205
227
  this.apiKey = apiKey;
228
+ this.accessToken = accessToken;
206
229
  this.workspaceId = workspaceId;
207
230
  this.baseURL = (options.baseURL ?? "https://api.air.inc/v1").replace(/\/$/, "");
208
231
  this.maxRetries = options.maxRetries ?? 3;
@@ -241,12 +264,17 @@ var AirBase = class {
241
264
  async _executeRequest(options) {
242
265
  const url = this._buildURL(options.path, options.query);
243
266
  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
267
+ "user-agent": `air-api-sdk/${VERSION}`
249
268
  };
269
+ if (this.apiKey) {
270
+ headers["x-api-key"] = this.apiKey;
271
+ } else if (this.accessToken) {
272
+ headers["authorization"] = `Bearer ${this.accessToken}`;
273
+ }
274
+ if (this.workspaceId) {
275
+ headers["x-air-workspace-id"] = this.workspaceId;
276
+ }
277
+ Object.assign(headers, this._defaultHeaders, options.headers);
250
278
  if (options.body !== void 0) {
251
279
  headers["content-type"] = "application/json";
252
280
  }
@@ -301,6 +329,90 @@ var AirBase = class {
301
329
  return url.toString();
302
330
  }
303
331
  };
332
+
333
+ // src/oauth.ts
334
+ function buildAuthorizationUrl(options) {
335
+ const url = new URL(options.authorizeUrl);
336
+ url.searchParams.set("response_type", "code");
337
+ url.searchParams.set("client_id", options.clientId);
338
+ url.searchParams.set("redirect_uri", options.redirectUri);
339
+ url.searchParams.set("state", options.state);
340
+ url.searchParams.set("code_challenge", options.codeChallenge);
341
+ url.searchParams.set("code_challenge_method", "S256");
342
+ if (options.scopes !== void 0) {
343
+ const scope = Array.isArray(options.scopes) ? options.scopes.join(" ") : options.scopes;
344
+ if (scope.length > 0) {
345
+ url.searchParams.set("scope", scope);
346
+ }
347
+ }
348
+ return url.toString();
349
+ }
350
+ async function exchangeAuthorizationCode(options) {
351
+ const fetchFn = options.fetch ?? globalThis.fetch;
352
+ const body = new URLSearchParams({
353
+ grant_type: "authorization_code",
354
+ code: options.code,
355
+ code_verifier: options.codeVerifier,
356
+ redirect_uri: options.redirectUri
357
+ });
358
+ const headers = {
359
+ "content-type": "application/x-www-form-urlencoded"
360
+ };
361
+ if (options.clientSecret === void 0) {
362
+ body.set("client_id", options.clientId);
363
+ } else {
364
+ applyClientAuth(headers, body, {
365
+ clientId: options.clientId,
366
+ clientSecret: options.clientSecret,
367
+ method: options.clientAuthMethod ?? "basic"
368
+ });
369
+ }
370
+ return executeTokenRequest(fetchFn, options.tokenUrl, headers, body.toString());
371
+ }
372
+ function applyClientAuth(headers, body, args) {
373
+ if (args.method === "basic") {
374
+ headers["authorization"] = `Basic ${base64Encode(`${args.clientId}:${args.clientSecret}`)}`;
375
+ } else {
376
+ body.set("client_id", args.clientId);
377
+ body.set("client_secret", args.clientSecret);
378
+ }
379
+ }
380
+ async function executeTokenRequest(fetchFn, tokenUrl, headers, body) {
381
+ let response;
382
+ try {
383
+ response = await fetchFn(tokenUrl, { method: "POST", headers, body });
384
+ } catch (error) {
385
+ throw new ConnectionError(error instanceof Error ? error.message : "Connection failed");
386
+ }
387
+ if (!response.ok) {
388
+ throw await APIError.fromResponse(response);
389
+ }
390
+ const raw = await response.json();
391
+ return {
392
+ accessToken: raw.access_token,
393
+ tokenType: raw.token_type,
394
+ expiresIn: raw.expires_in,
395
+ scope: raw.scope,
396
+ refreshToken: raw.refresh_token
397
+ };
398
+ }
399
+ function base64Encode(input) {
400
+ if (typeof Buffer !== "undefined") {
401
+ return Buffer.from(input, "utf-8").toString("base64");
402
+ }
403
+ return btoa(input);
404
+ }
405
+
406
+ // src/pkce.ts
407
+ import { createHash, randomBytes } from "crypto";
408
+ function generatePKCEChallenge() {
409
+ const codeVerifier = base64UrlEncode(randomBytes(32));
410
+ const codeChallenge = base64UrlEncode(createHash("sha256").update(codeVerifier).digest());
411
+ return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
412
+ }
413
+ function base64UrlEncode(input) {
414
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
415
+ }
304
416
  export {
305
417
  APIError,
306
418
  AirBase,
@@ -315,6 +427,9 @@ export {
315
427
  RateLimitError,
316
428
  TimeoutError,
317
429
  VERSION,
430
+ buildAuthorizationUrl,
431
+ exchangeAuthorizationCode,
432
+ generatePKCEChallenge,
318
433
  getRetryDelay,
319
434
  isRetryableError,
320
435
  retryWithBackoff
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@air/api-core",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Core HTTP engine for Air API SDK",
6
6
  "main": "./dist/index.cjs",
@@ -26,7 +26,7 @@
26
26
  "build": "tsup",
27
27
  "test": "vitest run",
28
28
  "typecheck": "tsc -b",
29
- "clean": "rm -rf dist tsconfig.tsbuildinfo"
29
+ "clean": "rm -rf dist .tsc-out tsconfig.tsbuildinfo"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=18"