@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 +128 -10
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +125 -10
- package/package.json +2 -2
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
|
-
|
|
235
|
-
|
|
238
|
+
let apiKey;
|
|
239
|
+
let accessToken;
|
|
240
|
+
if (options.apiKey !== void 0 && options.accessToken !== void 0) {
|
|
236
241
|
throw new Error(
|
|
237
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
194
|
+
let apiKey;
|
|
195
|
+
let accessToken;
|
|
196
|
+
if (options.apiKey !== void 0 && options.accessToken !== void 0) {
|
|
195
197
|
throw new Error(
|
|
196
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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"
|