busa-sdk 0.14.1 → 0.16.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.
@@ -0,0 +1,66 @@
1
+ import { BusabaseAirAppCredentialStoreOptions } from './oauth-node.js';
2
+ import './oauth.js';
3
+
4
+ declare const BUSABASE_AIRAPP_GATEWAY_REASONS: {
5
+ readonly authRequired: "AUTH_REQUIRED";
6
+ readonly authUnavailable: "AUTH_UNAVAILABLE";
7
+ readonly connectionRequired: "CONNECTION_REQUIRED";
8
+ readonly oauthCallbackInvalid: "OAUTH_CALLBACK_INVALID";
9
+ readonly spaceNotAllowed: "SPACE_NOT_ALLOWED";
10
+ readonly spaceSelectionRequired: "SPACE_SELECTION_REQUIRED";
11
+ };
12
+ type GatewayReason = (typeof BUSABASE_AIRAPP_GATEWAY_REASONS)[keyof typeof BUSABASE_AIRAPP_GATEWAY_REASONS];
13
+ interface AuthSpace {
14
+ id: string;
15
+ name: string;
16
+ slug?: string | null;
17
+ plan?: string | null;
18
+ }
19
+ interface GatewayTarget {
20
+ baseUrl: string;
21
+ accessToken: string;
22
+ selectedSpace?: AuthSpace;
23
+ source: "airapp-oauth-local" | "environment" | "open-server";
24
+ }
25
+ interface BusabaseAirAppLocalGatewayOptions {
26
+ appId: string;
27
+ cloudBaseUrl?: string;
28
+ clientId?: string;
29
+ environment?: Record<string, string | undefined>;
30
+ fetch?: typeof fetch;
31
+ now?: () => number;
32
+ oauthPendingTtlMs?: number;
33
+ requestTimeoutMs?: number;
34
+ credentialStore?: BusabaseAirAppCredentialStoreOptions;
35
+ successPath?: string;
36
+ errorPath?: string;
37
+ }
38
+ interface BusabaseAirAppAuthStatus {
39
+ connected: boolean;
40
+ cloudBaseUrl: string;
41
+ baseUrl?: string;
42
+ source?: GatewayTarget["source"];
43
+ readiness: "needs_connection" | "needs_auth" | "needs_space" | "ready" | "retry";
44
+ action: "connect" | "reconnect" | "select_space" | "continue" | "retry";
45
+ requiresSpace?: boolean;
46
+ selectedSpace?: AuthSpace | null;
47
+ space?: AuthSpace | null;
48
+ spaces?: AuthSpace[];
49
+ reason?: GatewayReason;
50
+ message?: string;
51
+ }
52
+ declare class BusabaseAirAppLocalGateway {
53
+ #private;
54
+ constructor(options: BusabaseAirAppLocalGatewayOptions);
55
+ get cloudBaseUrl(): string;
56
+ status(): Promise<BusabaseAirAppAuthStatus>;
57
+ statusResponse: () => Promise<Response>;
58
+ start: (request: Request) => Promise<Response>;
59
+ callback: (request: Request) => Promise<Response>;
60
+ selectSpace: (request: Request) => Promise<Response>;
61
+ logout: (request: Request) => Promise<Response>;
62
+ proxy: (request: Request) => Promise<Response>;
63
+ }
64
+ declare const createBusabaseAirAppLocalGateway: (options: BusabaseAirAppLocalGatewayOptions) => BusabaseAirAppLocalGateway;
65
+
66
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, type BusabaseAirAppAuthStatus, BusabaseAirAppLocalGateway, type BusabaseAirAppLocalGatewayOptions, createBusabaseAirAppLocalGateway };
@@ -0,0 +1,439 @@
1
+ import { getBusabaseAirAppAccessToken, storeBusabaseAirAppSelectedSpace, loadBusabaseAirAppDynamicClientId, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential } from './chunk-C23JVY2Y.js';
2
+ import { BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, registerBusabaseAirAppOAuthClient, createBusabaseOAuthRequest, parseBusabaseOAuthCallback, exchangeBusabaseOAuthCode } from './chunk-WSHJMHUS.js';
3
+ import './chunk-5NYQX65A.js';
4
+
5
+ // src/airapp-node.ts
6
+ var DEFAULT_CLOUD_BASE_URL = "https://busabase.com";
7
+ var DEFAULT_PENDING_TTL_MS = 5 * 6e4;
8
+ var DEFAULT_TIMEOUT_MS = 8e3;
9
+ var BUSABASE_AIRAPP_GATEWAY_REASONS = {
10
+ authRequired: "AUTH_REQUIRED",
11
+ authUnavailable: "AUTH_UNAVAILABLE",
12
+ connectionRequired: "CONNECTION_REQUIRED",
13
+ oauthCallbackInvalid: "OAUTH_CALLBACK_INVALID",
14
+ spaceNotAllowed: "SPACE_NOT_ALLOWED",
15
+ spaceSelectionRequired: "SPACE_SELECTION_REQUIRED"
16
+ };
17
+ var jsonError = (status, reason, message, data) => Response.json(
18
+ {
19
+ error: message,
20
+ code: status === 401 ? "UNAUTHORIZED" : status === 403 ? "FORBIDDEN" : status === 409 ? "CONFLICT" : status === 503 ? "SERVICE_UNAVAILABLE" : "BAD_REQUEST",
21
+ data: { reason, ...data }
22
+ },
23
+ { status }
24
+ );
25
+ var LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "::1", "[::1]"];
26
+ var normalizeOrigin = (raw, fallback = DEFAULT_CLOUD_BASE_URL) => {
27
+ const withoutApi = String(raw || fallback).trim().replace(/\/+$/, "").replace(/\/api\/v1$/, "");
28
+ let url;
29
+ try {
30
+ url = new URL(withoutApi);
31
+ } catch {
32
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL is invalid");
33
+ }
34
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") {
35
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
36
+ }
37
+ const loopback = LOOPBACK_HOSTNAMES.includes(url.hostname);
38
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
39
+ throw new BusabaseOAuthError(
40
+ "invalid_base_url",
41
+ "Busabase requires HTTPS except for a loopback development server"
42
+ );
43
+ }
44
+ return url.origin;
45
+ };
46
+ var requestOrigin = (request) => new URL(request.url).origin;
47
+ var isLoopbackOrigin = (origin) => {
48
+ const url = new URL(origin);
49
+ return url.protocol === "http:" && LOOPBACK_HOSTNAMES.includes(url.hostname);
50
+ };
51
+ var assertSameOrigin = (request) => {
52
+ const origin = request.headers.get("origin");
53
+ if (origin && origin !== requestOrigin(request)) {
54
+ throw new BusabaseOAuthError("origin_mismatch", "Request origin did not match");
55
+ }
56
+ };
57
+ var readInput = async (request) => {
58
+ const contentType = request.headers.get("content-type") || "";
59
+ if (contentType.includes("application/json")) {
60
+ return await request.json().catch(() => ({}));
61
+ }
62
+ const form = await request.formData().catch(() => new FormData());
63
+ return Object.fromEntries(form.entries());
64
+ };
65
+ var safeSpaces = (spaces) => spaces.map(({ id, name, slug, plan }) => ({ id, name, slug, plan }));
66
+ var BusabaseAirAppLocalGateway = class {
67
+ #options;
68
+ #pendingOAuth = /* @__PURE__ */ new Map();
69
+ #usesDefaultClient;
70
+ #environmentSelectedSpace;
71
+ constructor(options) {
72
+ this.#usesDefaultClient = !options.clientId;
73
+ this.#options = {
74
+ ...options,
75
+ appId: options.appId,
76
+ cloudBaseUrl: normalizeOrigin(options.cloudBaseUrl || DEFAULT_CLOUD_BASE_URL),
77
+ clientId: options.clientId || BUSABASE_AIRAPP_CLIENT_ID,
78
+ oauthPendingTtlMs: options.oauthPendingTtlMs ?? DEFAULT_PENDING_TTL_MS,
79
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS,
80
+ successPath: options.successPath || "/",
81
+ errorPath: options.errorPath || "/"
82
+ };
83
+ }
84
+ get cloudBaseUrl() {
85
+ return this.#options.cloudBaseUrl;
86
+ }
87
+ #fetch() {
88
+ return this.#options.fetch ?? fetch;
89
+ }
90
+ #now() {
91
+ return this.#options.now?.() ?? Date.now();
92
+ }
93
+ #environment() {
94
+ return this.#options.environment ?? process.env;
95
+ }
96
+ async #target() {
97
+ const environment = this.#environment();
98
+ if (environment.BUSABASE_BASE_URL) {
99
+ const selectedSpaceId = environment.BUSABASE_SPACE_ID?.trim();
100
+ return {
101
+ baseUrl: normalizeOrigin(environment.BUSABASE_BASE_URL),
102
+ accessToken: environment.BUSABASE_API_KEY || "",
103
+ source: environment.BUSABASE_API_KEY ? "environment" : "open-server",
104
+ ...selectedSpaceId ? { selectedSpace: { id: selectedSpaceId, name: selectedSpaceId } } : this.#environmentSelectedSpace ? { selectedSpace: this.#environmentSelectedSpace } : {}
105
+ };
106
+ }
107
+ const credential = await getBusabaseAirAppAccessToken(
108
+ this.#options.appId,
109
+ this.#options.credentialStore,
110
+ this.#fetch()
111
+ );
112
+ return credential ? {
113
+ baseUrl: credential.baseUrl,
114
+ accessToken: credential.accessToken,
115
+ source: "airapp-oauth-local",
116
+ ...credential.selectedSpace ? { selectedSpace: credential.selectedSpace } : {}
117
+ } : null;
118
+ }
119
+ async #authInfo(target, spaceId = "") {
120
+ const headers = new Headers({ accept: "application/json" });
121
+ if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
122
+ if (spaceId) headers.set("x-busabase-space", spaceId);
123
+ const response = await this.#fetch()(new URL("/api/v1/auth", target.baseUrl), {
124
+ headers,
125
+ signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
126
+ });
127
+ if (!response.ok) {
128
+ throw new BusabaseOAuthError(
129
+ response.status === 401 ? "auth_required" : "auth_verification_failed",
130
+ `Busabase auth verification failed (${response.status})`,
131
+ response.status
132
+ );
133
+ }
134
+ const info = await response.json();
135
+ if (!Array.isArray(info.spaces)) {
136
+ throw new BusabaseOAuthError(
137
+ "invalid_auth_response",
138
+ "Busabase auth response did not include Spaces"
139
+ );
140
+ }
141
+ return info;
142
+ }
143
+ #persistSelectedSpace(target, selectedSpace) {
144
+ if (target.source === "airapp-oauth-local") {
145
+ storeBusabaseAirAppSelectedSpace(
146
+ this.#options.appId,
147
+ selectedSpace ? { id: selectedSpace.id, name: selectedSpace.name } : null,
148
+ this.#options.credentialStore
149
+ );
150
+ } else {
151
+ this.#environmentSelectedSpace = selectedSpace || void 0;
152
+ }
153
+ }
154
+ async status() {
155
+ let target = null;
156
+ try {
157
+ target = await this.#target();
158
+ if (!target) {
159
+ return {
160
+ connected: false,
161
+ cloudBaseUrl: this.cloudBaseUrl,
162
+ readiness: "needs_connection",
163
+ action: "connect",
164
+ reason: BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired
165
+ };
166
+ }
167
+ const info = await this.#authInfo(target);
168
+ const spaces = safeSpaces(info.spaces);
169
+ if (!spaces.length) {
170
+ this.#persistSelectedSpace(target, null);
171
+ return {
172
+ connected: true,
173
+ cloudBaseUrl: this.cloudBaseUrl,
174
+ baseUrl: target.baseUrl,
175
+ source: target.source,
176
+ readiness: "needs_space",
177
+ action: "retry",
178
+ requiresSpace: true,
179
+ selectedSpace: null,
180
+ space: null,
181
+ spaces,
182
+ reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
183
+ message: "This account has no accessible Busabase Space"
184
+ };
185
+ }
186
+ const selectedSpaceId = target.selectedSpace?.id;
187
+ let selected = selectedSpaceId ? spaces.find((space) => space.id === selectedSpaceId) : void 0;
188
+ if (!selected && spaces.length === 1) selected = spaces[0];
189
+ if (target.selectedSpace && !selected) this.#persistSelectedSpace(target, null);
190
+ if (selected && selected.id !== selectedSpaceId) {
191
+ await this.#authInfo(target, selected.id);
192
+ this.#persistSelectedSpace(target, selected);
193
+ }
194
+ return {
195
+ connected: true,
196
+ cloudBaseUrl: this.cloudBaseUrl,
197
+ baseUrl: target.baseUrl,
198
+ source: target.source,
199
+ readiness: selected ? "ready" : "needs_space",
200
+ action: selected ? "continue" : "select_space",
201
+ requiresSpace: !selected,
202
+ selectedSpace: selected || null,
203
+ space: selected || null,
204
+ spaces,
205
+ ...selected ? {} : { reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired }
206
+ };
207
+ } catch (error) {
208
+ const authRequired = error instanceof BusabaseOAuthError && error.status === 401;
209
+ return {
210
+ connected: Boolean(target) && !authRequired,
211
+ cloudBaseUrl: this.cloudBaseUrl,
212
+ ...target ? { baseUrl: target.baseUrl, source: target.source, requiresSpace: true } : {},
213
+ readiness: authRequired ? "needs_auth" : "retry",
214
+ action: authRequired ? "reconnect" : "retry",
215
+ reason: authRequired ? BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired : BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
216
+ message: error instanceof Error ? error.message : "Busabase auth verification failed"
217
+ };
218
+ }
219
+ }
220
+ statusResponse = async () => Response.json(await this.status());
221
+ /** Resolve the client for this callback, then probe the authorize endpoint with it. */
222
+ async #startAuthorization(target, needsDynamicClient, forceRegistration) {
223
+ let clientId = this.#options.clientId;
224
+ let reusedStoredClient = false;
225
+ if (needsDynamicClient) {
226
+ const stored = forceRegistration ? null : loadBusabaseAirAppDynamicClientId(target, this.#options.credentialStore);
227
+ if (stored) {
228
+ clientId = stored;
229
+ reusedStoredClient = true;
230
+ } else {
231
+ const registration = await registerBusabaseAirAppOAuthClient(target, this.#fetch());
232
+ clientId = registration.clientId;
233
+ storeBusabaseAirAppDynamicClientId({ ...target, clientId }, this.#options.credentialStore);
234
+ }
235
+ }
236
+ const oauthRequest = await createBusabaseOAuthRequest({
237
+ baseUrl: target.baseUrl,
238
+ redirectUri: target.redirectUri,
239
+ clientId
240
+ });
241
+ const probe = await this.#fetch()(oauthRequest.authorizeUrl, {
242
+ headers: { accept: "text/html" },
243
+ redirect: "manual",
244
+ signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
245
+ });
246
+ return { oauthRequest, probe, reusedStoredClient };
247
+ }
248
+ start = async (request) => {
249
+ try {
250
+ assertSameOrigin(request);
251
+ const body = await readInput(request);
252
+ const baseUrl = normalizeOrigin(String(body.base_url || ""), this.cloudBaseUrl);
253
+ const redirectUri = new URL("/auth/callback", requestOrigin(request)).toString();
254
+ const needsDynamicClient = this.#usesDefaultClient && !isLoopbackOrigin(requestOrigin(request));
255
+ const target = { appId: this.#options.appId, baseUrl, redirectUri };
256
+ let attempt = await this.#startAuthorization(target, needsDynamicClient, false);
257
+ if (attempt.probe.status >= 400 && attempt.reusedStoredClient) {
258
+ attempt = await this.#startAuthorization(target, needsDynamicClient, true);
259
+ }
260
+ const { oauthRequest, probe } = attempt;
261
+ if (probe.status >= 400) {
262
+ throw new BusabaseOAuthError(
263
+ "oauth_unavailable",
264
+ `Busabase OAuth is unavailable (${probe.status})`,
265
+ probe.status
266
+ );
267
+ }
268
+ this.#pendingOAuth.set(oauthRequest.state, {
269
+ ...oauthRequest,
270
+ expiresAt: this.#now() + this.#options.oauthPendingTtlMs
271
+ });
272
+ return Response.redirect(oauthRequest.authorizeUrl, 303);
273
+ } catch (error) {
274
+ const redirect = new URL(this.#options.errorPath, requestOrigin(request));
275
+ redirect.searchParams.set(
276
+ "oauth_error",
277
+ error instanceof Error ? error.message : "Unable to start Busabase OAuth"
278
+ );
279
+ return Response.redirect(redirect, 303);
280
+ }
281
+ };
282
+ callback = async (request) => {
283
+ const callback = new URL(request.url);
284
+ const state = callback.searchParams.get("state") || "";
285
+ const pending = this.#pendingOAuth.get(state);
286
+ this.#pendingOAuth.delete(state);
287
+ try {
288
+ if (!pending || pending.expiresAt <= this.#now()) {
289
+ throw new BusabaseOAuthError("oauth_request_expired", "OAuth request expired");
290
+ }
291
+ const code = parseBusabaseOAuthCallback(callback.toString(), pending);
292
+ const tokenSet = await exchangeBusabaseOAuthCode(pending, code, this.#fetch());
293
+ storeBusabaseAirAppOAuthCredential(
294
+ {
295
+ appId: this.#options.appId,
296
+ baseUrl: pending.baseUrl,
297
+ clientId: pending.clientId,
298
+ tokenSet
299
+ },
300
+ this.#options.credentialStore
301
+ );
302
+ return Response.redirect(new URL(this.#options.successPath, requestOrigin(request)), 303);
303
+ } catch (error) {
304
+ const redirect = new URL(this.#options.errorPath, requestOrigin(request));
305
+ redirect.searchParams.set(
306
+ "oauth_error",
307
+ error instanceof Error ? error.message : "Busabase OAuth callback failed"
308
+ );
309
+ return Response.redirect(redirect, 303);
310
+ }
311
+ };
312
+ selectSpace = async (request) => {
313
+ try {
314
+ assertSameOrigin(request);
315
+ const body = await readInput(request);
316
+ const spaceId = String(body.space_id || "").trim();
317
+ const target = await this.#target();
318
+ if (!target) {
319
+ return jsonError(
320
+ 401,
321
+ BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired,
322
+ "Connect Busabase before selecting a Space"
323
+ );
324
+ }
325
+ const info = await this.#authInfo(target);
326
+ const selected = info.spaces.find((space) => space.id === spaceId);
327
+ if (!selected) {
328
+ return jsonError(
329
+ 403,
330
+ BUSABASE_AIRAPP_GATEWAY_REASONS.spaceNotAllowed,
331
+ "The selected Space is not accessible to this account"
332
+ );
333
+ }
334
+ await this.#authInfo(target, selected.id);
335
+ this.#persistSelectedSpace(target, selected);
336
+ return Response.json({ ok: true, space: { id: selected.id, name: selected.name } });
337
+ } catch (error) {
338
+ return jsonError(
339
+ 400,
340
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
341
+ error instanceof Error ? error.message : "Unable to select Busabase Space"
342
+ );
343
+ }
344
+ };
345
+ logout = async (request) => {
346
+ try {
347
+ assertSameOrigin(request);
348
+ if (loadBusabaseAirAppOAuthCredential(this.#options.appId, this.#options.credentialStore)) {
349
+ await revokeBusabaseAirAppOAuthCredential(
350
+ this.#options.appId,
351
+ this.#options.credentialStore,
352
+ this.#fetch()
353
+ ).catch(() => void 0);
354
+ }
355
+ this.#environmentSelectedSpace = void 0;
356
+ return Response.json({ ok: true });
357
+ } catch (error) {
358
+ return jsonError(
359
+ 400,
360
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
361
+ error instanceof Error ? error.message : "Unable to disconnect Busabase"
362
+ );
363
+ }
364
+ };
365
+ proxy = async (request) => {
366
+ let target;
367
+ try {
368
+ target = await this.#target();
369
+ } catch {
370
+ return jsonError(
371
+ 401,
372
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired,
373
+ "Busabase authentication expired"
374
+ );
375
+ }
376
+ if (!target) {
377
+ return jsonError(
378
+ 401,
379
+ BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired,
380
+ "Busabase connection required"
381
+ );
382
+ }
383
+ let selectedSpace = target.selectedSpace;
384
+ if (!selectedSpace) {
385
+ try {
386
+ const info = await this.#authInfo(target);
387
+ if (info.spaces.length === 1) {
388
+ selectedSpace = info.spaces[0];
389
+ this.#persistSelectedSpace(target, selectedSpace);
390
+ }
391
+ } catch {
392
+ return jsonError(
393
+ 503,
394
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
395
+ "Busabase authentication could not be verified"
396
+ );
397
+ }
398
+ }
399
+ if (!selectedSpace) {
400
+ return jsonError(
401
+ 409,
402
+ BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
403
+ "Busabase Space selection required"
404
+ );
405
+ }
406
+ const incoming = new URL(request.url);
407
+ const targetUrl = new URL(incoming.pathname + incoming.search, target.baseUrl);
408
+ const headers = new Headers();
409
+ const contentType = request.headers.get("content-type");
410
+ const accept = request.headers.get("accept");
411
+ if (contentType) headers.set("content-type", contentType);
412
+ if (accept) headers.set("accept", accept);
413
+ headers.set("x-busabase-space", selectedSpace.id);
414
+ if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
415
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
416
+ let upstream;
417
+ try {
418
+ upstream = await this.#fetch()(targetUrl, {
419
+ method: request.method,
420
+ headers,
421
+ body: hasBody ? await request.arrayBuffer() : void 0,
422
+ redirect: "manual"
423
+ });
424
+ } catch {
425
+ return jsonError(
426
+ 503,
427
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
428
+ "Busabase API is temporarily unavailable"
429
+ );
430
+ }
431
+ const responseHeaders = new Headers();
432
+ const upstreamType = upstream.headers.get("content-type");
433
+ if (upstreamType) responseHeaders.set("content-type", upstreamType);
434
+ return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
435
+ };
436
+ };
437
+ var createBusabaseAirAppLocalGateway = (options) => new BusabaseAirAppLocalGateway(options);
438
+
439
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, BusabaseAirAppLocalGateway, createBusabaseAirAppLocalGateway };