busa-sdk 0.17.2 → 0.18.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/airapp-gate.d.ts +74 -114
- package/dist/airapp-gate.js +231 -229
- package/dist/airapp-node.d.ts +97 -52
- package/dist/airapp-node.js +422 -444
- package/dist/airapp.d.ts +102 -147
- package/dist/airapp.js +432 -423
- package/dist/client-CbUceGzy.d.ts +20523 -0
- package/dist/index.d.ts +2807 -2739
- package/dist/index.js +4803 -4029
- package/dist/oauth-Dg2Z41RP.js +173 -0
- package/dist/oauth-FfuT0EGC.d.ts +69 -0
- package/dist/oauth-node-DGiWkh_l.d.ts +64 -0
- package/dist/oauth-node.d.ts +2 -64
- package/dist/oauth-node.js +188 -3
- package/dist/oauth.d.ts +2 -67
- package/dist/oauth.js +2 -2
- package/dist/url-B8GMXalA.js +10 -0
- package/package.json +1 -1
- package/dist/chunk-5NYQX65A.js +0 -6
- package/dist/chunk-C23JVY2Y.js +0 -211
- package/dist/chunk-WSHJMHUS.js +0 -215
- package/dist/client-DIlpYL9z.d.ts +0 -17818
package/dist/airapp-node.js
CHANGED
|
@@ -1,452 +1,430 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { a as parseBusabaseOAuthCallback, i as exchangeBusabaseOAuthCode, n as BusabaseOAuthError, r as createBusabaseOAuthRequest, s as registerBusabaseAirAppOAuthClient } from "./oauth-Dg2Z41RP.js";
|
|
2
|
+
import { getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace } from "./oauth-node.js";
|
|
3
|
+
//#region src/airapp-node.ts
|
|
4
|
+
const DEFAULT_CLOUD_BASE_URL = "https://busabase.com";
|
|
5
|
+
const DEFAULT_PENDING_TTL_MS = 3e5;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 8e3;
|
|
7
|
+
/**
|
|
8
|
+
* The env var Busabase injects into every AirApp process it spawns. Nobody else
|
|
9
|
+
* sets it, which is what makes its **absence** the positive fact "standalone".
|
|
10
|
+
*/
|
|
11
|
+
const BUSABASE_AIRAPP_RUNTIME_ENV = "BUSABASE_AIRAPP_RUNTIME";
|
|
12
|
+
/**
|
|
13
|
+
* Every runtime Busabase hosts an AirApp in, as known to *this* SDK version.
|
|
14
|
+
*
|
|
15
|
+
* **Informational. Not what decides whether an app is hosted.** That separation
|
|
16
|
+
* is the whole point of this constant existing here rather than being pasted
|
|
17
|
+
* into each app, and getting it backwards is a bug we have already shipped once:
|
|
18
|
+
* every generated AirApp used to carry its own hardcoded copy of this list and
|
|
19
|
+
* answer `hosted` from membership in it. When the `local-node` engine was
|
|
20
|
+
* renamed to `local`, all of them started answering "standalone" *inside a
|
|
21
|
+
* hosted preview* — showing their own connection gate, calling `/api/v1` with no
|
|
22
|
+
* credential, and leaving the operator with nothing to act on.
|
|
23
|
+
*
|
|
24
|
+
* Moving the list into the SDK does not by itself fix that. An app pins an SDK
|
|
25
|
+
* version, so a list that gates behaviour would simply rot on a slower clock:
|
|
26
|
+
* add an engine, and every app on an older SDK is wrong again until it upgrades.
|
|
27
|
+
*
|
|
28
|
+
* So the list names runtimes; {@link isBusabaseAirAppHosted} decides hosting,
|
|
29
|
+
* from presence alone, and needs no upgrade to stay correct.
|
|
30
|
+
*/
|
|
31
|
+
const BUSABASE_AIRAPP_RUNTIMES = [
|
|
32
|
+
"nodepod",
|
|
33
|
+
"local",
|
|
34
|
+
"srt",
|
|
35
|
+
"sandock",
|
|
36
|
+
"embed"
|
|
37
|
+
];
|
|
38
|
+
/** Read the injected runtime, normalised. `""` means nobody hosted this process. */
|
|
39
|
+
const readBusabaseAirAppRuntime = (env = process.env) => (env["BUSABASE_AIRAPP_RUNTIME"] || "").trim();
|
|
40
|
+
/**
|
|
41
|
+
* Whether Busabase is hosting this process.
|
|
42
|
+
*
|
|
43
|
+
* Presence, never membership — see {@link BUSABASE_AIRAPP_RUNTIMES}. An engine
|
|
44
|
+
* this SDK has never heard of still means "Busabase spawned me", because only
|
|
45
|
+
* Busabase sets the variable at all.
|
|
46
|
+
*/
|
|
47
|
+
const isBusabaseAirAppHosted = (runtime = readBusabaseAirAppRuntime()) => runtime.trim() !== "";
|
|
48
|
+
/**
|
|
49
|
+
* Narrow a runtime string to one this SDK knows, or `null`.
|
|
50
|
+
*
|
|
51
|
+
* For code that wants to *branch* on a specific engine — an app that renders
|
|
52
|
+
* differently under a public embed, say. Deliberately returns `null` rather than
|
|
53
|
+
* throwing on an unknown value: a newer Busabase naming an engine this SDK
|
|
54
|
+
* predates is normal, not an error, and the caller should fall back to generic
|
|
55
|
+
* behaviour rather than break.
|
|
56
|
+
*/
|
|
57
|
+
const asKnownBusabaseAirAppRuntime = (runtime) => BUSABASE_AIRAPP_RUNTIMES.includes(runtime) ? runtime : null;
|
|
58
|
+
const BUSABASE_AIRAPP_GATEWAY_REASONS = {
|
|
59
|
+
authRequired: "AUTH_REQUIRED",
|
|
60
|
+
authUnavailable: "AUTH_UNAVAILABLE",
|
|
61
|
+
connectionRequired: "CONNECTION_REQUIRED",
|
|
62
|
+
oauthCallbackInvalid: "OAUTH_CALLBACK_INVALID",
|
|
63
|
+
spaceNotAllowed: "SPACE_NOT_ALLOWED",
|
|
64
|
+
spaceSelectionRequired: "SPACE_SELECTION_REQUIRED"
|
|
16
65
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
return url.origin;
|
|
66
|
+
const jsonError = (status, reason, message, data) => Response.json({
|
|
67
|
+
error: message,
|
|
68
|
+
code: status === 401 ? "UNAUTHORIZED" : status === 403 ? "FORBIDDEN" : status === 409 ? "CONFLICT" : status === 503 ? "SERVICE_UNAVAILABLE" : "BAD_REQUEST",
|
|
69
|
+
data: {
|
|
70
|
+
reason,
|
|
71
|
+
...data
|
|
72
|
+
}
|
|
73
|
+
}, { status });
|
|
74
|
+
const LOOPBACK_HOSTNAMES = [
|
|
75
|
+
"localhost",
|
|
76
|
+
"127.0.0.1",
|
|
77
|
+
"::1",
|
|
78
|
+
"[::1]"
|
|
79
|
+
];
|
|
80
|
+
const normalizeOrigin = (raw, fallback = DEFAULT_CLOUD_BASE_URL) => {
|
|
81
|
+
const withoutApi = String(raw || fallback).trim().replace(/\/+$/, "").replace(/\/api\/v1$/, "");
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(withoutApi);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL is invalid");
|
|
87
|
+
}
|
|
88
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
|
|
89
|
+
const loopback = LOOPBACK_HOSTNAMES.includes(url.hostname);
|
|
90
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) throw new BusabaseOAuthError("invalid_base_url", "Busabase requires HTTPS except for a loopback development server");
|
|
91
|
+
return url.origin;
|
|
45
92
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
93
|
+
const requestOrigin = (request) => {
|
|
94
|
+
const directOrigin = new URL(request.url).origin;
|
|
95
|
+
const forwardedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
96
|
+
const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
97
|
+
if (!forwardedHost || !forwardedProto || !["http", "https"].includes(forwardedProto)) return directOrigin;
|
|
98
|
+
try {
|
|
99
|
+
const forwardedOrigin = new URL(`${forwardedProto}://${forwardedHost}`).origin;
|
|
100
|
+
const browserOrigin = request.headers.get("origin");
|
|
101
|
+
return browserOrigin && browserOrigin !== forwardedOrigin ? directOrigin : forwardedOrigin;
|
|
102
|
+
} catch {
|
|
103
|
+
return directOrigin;
|
|
104
|
+
}
|
|
59
105
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
const isLoopbackOrigin = (origin) => {
|
|
107
|
+
const url = new URL(origin);
|
|
108
|
+
return url.protocol === "http:" && LOOPBACK_HOSTNAMES.includes(url.hostname);
|
|
63
109
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new BusabaseOAuthError("origin_mismatch", "Request origin did not match");
|
|
68
|
-
}
|
|
110
|
+
const assertSameOrigin = (request) => {
|
|
111
|
+
const origin = request.headers.get("origin");
|
|
112
|
+
if (origin && origin !== requestOrigin(request)) throw new BusabaseOAuthError("origin_mismatch", "Request origin did not match");
|
|
69
113
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
const form = await request.formData().catch(() => new FormData());
|
|
76
|
-
return Object.fromEntries(form.entries());
|
|
114
|
+
const readInput = async (request) => {
|
|
115
|
+
if ((request.headers.get("content-type") || "").includes("application/json")) return await request.json().catch(() => ({}));
|
|
116
|
+
const form = await request.formData().catch(() => new FormData());
|
|
117
|
+
return Object.fromEntries(form.entries());
|
|
77
118
|
};
|
|
78
|
-
|
|
119
|
+
const safeSpaces = (spaces) => spaces.map(({ id, name, slug, plan }) => ({
|
|
120
|
+
id,
|
|
121
|
+
name,
|
|
122
|
+
slug,
|
|
123
|
+
plan
|
|
124
|
+
}));
|
|
79
125
|
var BusabaseAirAppLocalGateway = class {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
target = await this.#target();
|
|
382
|
-
} catch {
|
|
383
|
-
return jsonError(
|
|
384
|
-
401,
|
|
385
|
-
BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired,
|
|
386
|
-
"Busabase authentication expired"
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
if (!target) {
|
|
390
|
-
return jsonError(
|
|
391
|
-
401,
|
|
392
|
-
BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired,
|
|
393
|
-
"Busabase connection required"
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
let selectedSpace = target.selectedSpace;
|
|
397
|
-
if (!selectedSpace) {
|
|
398
|
-
try {
|
|
399
|
-
const info = await this.#authInfo(target);
|
|
400
|
-
if (info.spaces.length === 1) {
|
|
401
|
-
selectedSpace = info.spaces[0];
|
|
402
|
-
this.#persistSelectedSpace(target, selectedSpace);
|
|
403
|
-
}
|
|
404
|
-
} catch {
|
|
405
|
-
return jsonError(
|
|
406
|
-
503,
|
|
407
|
-
BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
|
|
408
|
-
"Busabase authentication could not be verified"
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
if (!selectedSpace) {
|
|
413
|
-
return jsonError(
|
|
414
|
-
409,
|
|
415
|
-
BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
|
|
416
|
-
"Busabase Space selection required"
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
const incoming = new URL(request.url);
|
|
420
|
-
const targetUrl = new URL(incoming.pathname + incoming.search, target.baseUrl);
|
|
421
|
-
const headers = new Headers();
|
|
422
|
-
const contentType = request.headers.get("content-type");
|
|
423
|
-
const accept = request.headers.get("accept");
|
|
424
|
-
if (contentType) headers.set("content-type", contentType);
|
|
425
|
-
if (accept) headers.set("accept", accept);
|
|
426
|
-
headers.set("x-busabase-space", selectedSpace.id);
|
|
427
|
-
if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
|
|
428
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
429
|
-
let upstream;
|
|
430
|
-
try {
|
|
431
|
-
upstream = await this.#fetch()(targetUrl, {
|
|
432
|
-
method: request.method,
|
|
433
|
-
headers,
|
|
434
|
-
body: hasBody ? await request.arrayBuffer() : void 0,
|
|
435
|
-
redirect: "manual"
|
|
436
|
-
});
|
|
437
|
-
} catch {
|
|
438
|
-
return jsonError(
|
|
439
|
-
503,
|
|
440
|
-
BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
|
|
441
|
-
"Busabase API is temporarily unavailable"
|
|
442
|
-
);
|
|
443
|
-
}
|
|
444
|
-
const responseHeaders = new Headers();
|
|
445
|
-
const upstreamType = upstream.headers.get("content-type");
|
|
446
|
-
if (upstreamType) responseHeaders.set("content-type", upstreamType);
|
|
447
|
-
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
|
448
|
-
};
|
|
126
|
+
#options;
|
|
127
|
+
#pendingOAuth = /* @__PURE__ */ new Map();
|
|
128
|
+
#usesDefaultClient;
|
|
129
|
+
#environmentSelectedSpace;
|
|
130
|
+
constructor(options) {
|
|
131
|
+
this.#usesDefaultClient = !options.clientId;
|
|
132
|
+
this.#options = {
|
|
133
|
+
...options,
|
|
134
|
+
appId: options.appId,
|
|
135
|
+
cloudBaseUrl: normalizeOrigin(options.cloudBaseUrl || DEFAULT_CLOUD_BASE_URL),
|
|
136
|
+
clientId: options.clientId || "busabase-airapp",
|
|
137
|
+
oauthPendingTtlMs: options.oauthPendingTtlMs ?? DEFAULT_PENDING_TTL_MS,
|
|
138
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
139
|
+
successPath: options.successPath || "/",
|
|
140
|
+
errorPath: options.errorPath || "/"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
get cloudBaseUrl() {
|
|
144
|
+
return this.#options.cloudBaseUrl;
|
|
145
|
+
}
|
|
146
|
+
#fetch() {
|
|
147
|
+
return this.#options.fetch ?? fetch;
|
|
148
|
+
}
|
|
149
|
+
#now() {
|
|
150
|
+
return this.#options.now?.() ?? Date.now();
|
|
151
|
+
}
|
|
152
|
+
#environment() {
|
|
153
|
+
return this.#options.environment ?? process.env;
|
|
154
|
+
}
|
|
155
|
+
async #target() {
|
|
156
|
+
const environment = this.#environment();
|
|
157
|
+
if (environment.BUSABASE_BASE_URL) {
|
|
158
|
+
const selectedSpaceId = environment.BUSABASE_SPACE_ID?.trim();
|
|
159
|
+
return {
|
|
160
|
+
baseUrl: normalizeOrigin(environment.BUSABASE_BASE_URL),
|
|
161
|
+
accessToken: environment.BUSABASE_API_KEY || "",
|
|
162
|
+
source: environment.BUSABASE_API_KEY ? "environment" : "open-server",
|
|
163
|
+
...selectedSpaceId ? { selectedSpace: {
|
|
164
|
+
id: selectedSpaceId,
|
|
165
|
+
name: selectedSpaceId
|
|
166
|
+
} } : this.#environmentSelectedSpace ? { selectedSpace: this.#environmentSelectedSpace } : {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const credential = await getBusabaseAirAppAccessToken(this.#options.appId, this.#options.credentialStore, this.#fetch());
|
|
170
|
+
return credential ? {
|
|
171
|
+
baseUrl: credential.baseUrl,
|
|
172
|
+
accessToken: credential.accessToken,
|
|
173
|
+
source: "airapp-oauth-local",
|
|
174
|
+
...credential.selectedSpace ? { selectedSpace: credential.selectedSpace } : {}
|
|
175
|
+
} : null;
|
|
176
|
+
}
|
|
177
|
+
async #authInfo(target, spaceId = "") {
|
|
178
|
+
const headers = new Headers({ accept: "application/json" });
|
|
179
|
+
if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
|
|
180
|
+
if (spaceId) headers.set("x-busabase-space", spaceId);
|
|
181
|
+
const response = await this.#fetch()(new URL("/api/v1/auth", target.baseUrl), {
|
|
182
|
+
headers,
|
|
183
|
+
signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
|
|
184
|
+
});
|
|
185
|
+
if (!response.ok) throw new BusabaseOAuthError(response.status === 401 ? "auth_required" : "auth_verification_failed", `Busabase auth verification failed (${response.status})`, response.status);
|
|
186
|
+
const info = await response.json();
|
|
187
|
+
if (!Array.isArray(info.spaces)) throw new BusabaseOAuthError("invalid_auth_response", "Busabase auth response did not include Spaces");
|
|
188
|
+
return info;
|
|
189
|
+
}
|
|
190
|
+
#persistSelectedSpace(target, selectedSpace) {
|
|
191
|
+
if (target.source === "airapp-oauth-local") storeBusabaseAirAppSelectedSpace(this.#options.appId, selectedSpace ? {
|
|
192
|
+
id: selectedSpace.id,
|
|
193
|
+
name: selectedSpace.name
|
|
194
|
+
} : null, this.#options.credentialStore);
|
|
195
|
+
else this.#environmentSelectedSpace = selectedSpace || void 0;
|
|
196
|
+
}
|
|
197
|
+
async status() {
|
|
198
|
+
let target = null;
|
|
199
|
+
try {
|
|
200
|
+
target = await this.#target();
|
|
201
|
+
if (!target) return {
|
|
202
|
+
connected: false,
|
|
203
|
+
cloudBaseUrl: this.cloudBaseUrl,
|
|
204
|
+
readiness: "needs_connection",
|
|
205
|
+
action: "connect",
|
|
206
|
+
reason: BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired
|
|
207
|
+
};
|
|
208
|
+
const info = await this.#authInfo(target);
|
|
209
|
+
const spaces = safeSpaces(info.spaces);
|
|
210
|
+
if (!spaces.length) {
|
|
211
|
+
this.#persistSelectedSpace(target, null);
|
|
212
|
+
return {
|
|
213
|
+
connected: true,
|
|
214
|
+
cloudBaseUrl: this.cloudBaseUrl,
|
|
215
|
+
baseUrl: target.baseUrl,
|
|
216
|
+
source: target.source,
|
|
217
|
+
readiness: "needs_space",
|
|
218
|
+
action: "retry",
|
|
219
|
+
requiresSpace: true,
|
|
220
|
+
selectedSpace: null,
|
|
221
|
+
space: null,
|
|
222
|
+
spaces,
|
|
223
|
+
reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
|
|
224
|
+
message: "This account has no accessible Busabase Space"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const selectedSpaceId = target.selectedSpace?.id;
|
|
228
|
+
let selected = selectedSpaceId ? spaces.find((space) => space.id === selectedSpaceId) : void 0;
|
|
229
|
+
if (!selected && spaces.length === 1) selected = spaces[0];
|
|
230
|
+
if (target.selectedSpace && !selected) this.#persistSelectedSpace(target, null);
|
|
231
|
+
if (selected && selected.id !== selectedSpaceId) {
|
|
232
|
+
await this.#authInfo(target, selected.id);
|
|
233
|
+
this.#persistSelectedSpace(target, selected);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
connected: true,
|
|
237
|
+
cloudBaseUrl: this.cloudBaseUrl,
|
|
238
|
+
baseUrl: target.baseUrl,
|
|
239
|
+
source: target.source,
|
|
240
|
+
readiness: selected ? "ready" : "needs_space",
|
|
241
|
+
action: selected ? "continue" : "select_space",
|
|
242
|
+
requiresSpace: !selected,
|
|
243
|
+
selectedSpace: selected || null,
|
|
244
|
+
space: selected || null,
|
|
245
|
+
spaces,
|
|
246
|
+
...selected ? {} : { reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired }
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const authRequired = error instanceof BusabaseOAuthError && error.status === 401;
|
|
250
|
+
return {
|
|
251
|
+
connected: Boolean(target) && !authRequired,
|
|
252
|
+
cloudBaseUrl: this.cloudBaseUrl,
|
|
253
|
+
...target ? {
|
|
254
|
+
baseUrl: target.baseUrl,
|
|
255
|
+
source: target.source,
|
|
256
|
+
requiresSpace: true
|
|
257
|
+
} : {},
|
|
258
|
+
readiness: authRequired ? "needs_auth" : "retry",
|
|
259
|
+
action: authRequired ? "reconnect" : "retry",
|
|
260
|
+
reason: authRequired ? BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired : BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
|
|
261
|
+
message: error instanceof Error ? error.message : "Busabase auth verification failed"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
statusResponse = async () => Response.json(await this.status());
|
|
266
|
+
/** Resolve the client for this callback, then probe the authorize endpoint with it. */
|
|
267
|
+
async #startAuthorization(target, needsDynamicClient, forceRegistration) {
|
|
268
|
+
let clientId = this.#options.clientId;
|
|
269
|
+
let reusedStoredClient = false;
|
|
270
|
+
if (needsDynamicClient) {
|
|
271
|
+
const stored = forceRegistration ? null : loadBusabaseAirAppDynamicClientId(target, this.#options.credentialStore);
|
|
272
|
+
if (stored) {
|
|
273
|
+
clientId = stored;
|
|
274
|
+
reusedStoredClient = true;
|
|
275
|
+
} else {
|
|
276
|
+
clientId = (await registerBusabaseAirAppOAuthClient(target, this.#fetch())).clientId;
|
|
277
|
+
storeBusabaseAirAppDynamicClientId({
|
|
278
|
+
...target,
|
|
279
|
+
clientId
|
|
280
|
+
}, this.#options.credentialStore);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const oauthRequest = await createBusabaseOAuthRequest({
|
|
284
|
+
baseUrl: target.baseUrl,
|
|
285
|
+
redirectUri: target.redirectUri,
|
|
286
|
+
clientId
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
oauthRequest,
|
|
290
|
+
probe: await this.#fetch()(oauthRequest.authorizeUrl, {
|
|
291
|
+
headers: { accept: "text/html" },
|
|
292
|
+
redirect: "manual",
|
|
293
|
+
signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
|
|
294
|
+
}),
|
|
295
|
+
reusedStoredClient
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
start = async (request) => {
|
|
299
|
+
try {
|
|
300
|
+
assertSameOrigin(request);
|
|
301
|
+
const body = await readInput(request);
|
|
302
|
+
const baseUrl = normalizeOrigin(String(body.base_url || ""), this.cloudBaseUrl);
|
|
303
|
+
const redirectUri = new URL("/auth/callback", requestOrigin(request)).toString();
|
|
304
|
+
const needsDynamicClient = this.#usesDefaultClient && !isLoopbackOrigin(requestOrigin(request));
|
|
305
|
+
const target = {
|
|
306
|
+
appId: this.#options.appId,
|
|
307
|
+
baseUrl,
|
|
308
|
+
redirectUri
|
|
309
|
+
};
|
|
310
|
+
let attempt = await this.#startAuthorization(target, needsDynamicClient, false);
|
|
311
|
+
if (attempt.probe.status >= 400 && attempt.reusedStoredClient) attempt = await this.#startAuthorization(target, needsDynamicClient, true);
|
|
312
|
+
const { oauthRequest, probe } = attempt;
|
|
313
|
+
if (probe.status >= 400) throw new BusabaseOAuthError("oauth_unavailable", `Busabase OAuth is unavailable (${probe.status})`, probe.status);
|
|
314
|
+
this.#pendingOAuth.set(oauthRequest.state, {
|
|
315
|
+
...oauthRequest,
|
|
316
|
+
expiresAt: this.#now() + this.#options.oauthPendingTtlMs
|
|
317
|
+
});
|
|
318
|
+
return Response.redirect(oauthRequest.authorizeUrl, 303);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const redirect = new URL(this.#options.errorPath, requestOrigin(request));
|
|
321
|
+
redirect.searchParams.set("oauth_error", error instanceof Error ? error.message : "Unable to start Busabase OAuth");
|
|
322
|
+
return Response.redirect(redirect, 303);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
callback = async (request) => {
|
|
326
|
+
const callback = new URL(request.url);
|
|
327
|
+
const state = callback.searchParams.get("state") || "";
|
|
328
|
+
const pending = this.#pendingOAuth.get(state);
|
|
329
|
+
this.#pendingOAuth.delete(state);
|
|
330
|
+
try {
|
|
331
|
+
if (!pending || pending.expiresAt <= this.#now()) throw new BusabaseOAuthError("oauth_request_expired", "OAuth request expired");
|
|
332
|
+
const code = parseBusabaseOAuthCallback(callback.toString(), pending);
|
|
333
|
+
const tokenSet = await exchangeBusabaseOAuthCode(pending, code, this.#fetch());
|
|
334
|
+
storeBusabaseAirAppOAuthCredential({
|
|
335
|
+
appId: this.#options.appId,
|
|
336
|
+
baseUrl: pending.baseUrl,
|
|
337
|
+
clientId: pending.clientId,
|
|
338
|
+
tokenSet
|
|
339
|
+
}, this.#options.credentialStore);
|
|
340
|
+
return Response.redirect(new URL(this.#options.successPath, requestOrigin(request)), 303);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const redirect = new URL(this.#options.errorPath, requestOrigin(request));
|
|
343
|
+
redirect.searchParams.set("oauth_error", error instanceof Error ? error.message : "Busabase OAuth callback failed");
|
|
344
|
+
return Response.redirect(redirect, 303);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
selectSpace = async (request) => {
|
|
348
|
+
try {
|
|
349
|
+
assertSameOrigin(request);
|
|
350
|
+
const body = await readInput(request);
|
|
351
|
+
const spaceId = String(body.space_id || "").trim();
|
|
352
|
+
const target = await this.#target();
|
|
353
|
+
if (!target) return jsonError(401, BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired, "Connect Busabase before selecting a Space");
|
|
354
|
+
const selected = (await this.#authInfo(target)).spaces.find((space) => space.id === spaceId);
|
|
355
|
+
if (!selected) return jsonError(403, BUSABASE_AIRAPP_GATEWAY_REASONS.spaceNotAllowed, "The selected Space is not accessible to this account");
|
|
356
|
+
await this.#authInfo(target, selected.id);
|
|
357
|
+
this.#persistSelectedSpace(target, selected);
|
|
358
|
+
return Response.json({
|
|
359
|
+
ok: true,
|
|
360
|
+
space: {
|
|
361
|
+
id: selected.id,
|
|
362
|
+
name: selected.name
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return jsonError(400, BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable, error instanceof Error ? error.message : "Unable to select Busabase Space");
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
logout = async (request) => {
|
|
370
|
+
try {
|
|
371
|
+
assertSameOrigin(request);
|
|
372
|
+
if (loadBusabaseAirAppOAuthCredential(this.#options.appId, this.#options.credentialStore)) await revokeBusabaseAirAppOAuthCredential(this.#options.appId, this.#options.credentialStore, this.#fetch()).catch(() => void 0);
|
|
373
|
+
this.#environmentSelectedSpace = void 0;
|
|
374
|
+
return Response.json({ ok: true });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return jsonError(400, BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable, error instanceof Error ? error.message : "Unable to disconnect Busabase");
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
proxy = async (request) => {
|
|
380
|
+
let target;
|
|
381
|
+
try {
|
|
382
|
+
target = await this.#target();
|
|
383
|
+
} catch {
|
|
384
|
+
return jsonError(401, BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired, "Busabase authentication expired");
|
|
385
|
+
}
|
|
386
|
+
if (!target) return jsonError(401, BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired, "Busabase connection required");
|
|
387
|
+
let selectedSpace = target.selectedSpace;
|
|
388
|
+
if (!selectedSpace) try {
|
|
389
|
+
const info = await this.#authInfo(target);
|
|
390
|
+
if (info.spaces.length === 1) {
|
|
391
|
+
selectedSpace = info.spaces[0];
|
|
392
|
+
this.#persistSelectedSpace(target, selectedSpace);
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
return jsonError(503, BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable, "Busabase authentication could not be verified");
|
|
396
|
+
}
|
|
397
|
+
if (!selectedSpace) return jsonError(409, BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired, "Busabase Space selection required");
|
|
398
|
+
const incoming = new URL(request.url);
|
|
399
|
+
const targetUrl = new URL(incoming.pathname + incoming.search, target.baseUrl);
|
|
400
|
+
const headers = new Headers();
|
|
401
|
+
const contentType = request.headers.get("content-type");
|
|
402
|
+
const accept = request.headers.get("accept");
|
|
403
|
+
if (contentType) headers.set("content-type", contentType);
|
|
404
|
+
if (accept) headers.set("accept", accept);
|
|
405
|
+
headers.set("x-busabase-space", selectedSpace.id);
|
|
406
|
+
if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
|
|
407
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
408
|
+
let upstream;
|
|
409
|
+
try {
|
|
410
|
+
upstream = await this.#fetch()(targetUrl, {
|
|
411
|
+
method: request.method,
|
|
412
|
+
headers,
|
|
413
|
+
body: hasBody ? await request.arrayBuffer() : void 0,
|
|
414
|
+
redirect: "manual"
|
|
415
|
+
});
|
|
416
|
+
} catch {
|
|
417
|
+
return jsonError(503, BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable, "Busabase API is temporarily unavailable");
|
|
418
|
+
}
|
|
419
|
+
const responseHeaders = new Headers();
|
|
420
|
+
const upstreamType = upstream.headers.get("content-type");
|
|
421
|
+
if (upstreamType) responseHeaders.set("content-type", upstreamType);
|
|
422
|
+
return new Response(upstream.body, {
|
|
423
|
+
status: upstream.status,
|
|
424
|
+
headers: responseHeaders
|
|
425
|
+
});
|
|
426
|
+
};
|
|
449
427
|
};
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
export { BUSABASE_AIRAPP_GATEWAY_REASONS, BusabaseAirAppLocalGateway, createBusabaseAirAppLocalGateway };
|
|
428
|
+
const createBusabaseAirAppLocalGateway = (options) => new BusabaseAirAppLocalGateway(options);
|
|
429
|
+
//#endregion
|
|
430
|
+
export { BUSABASE_AIRAPP_GATEWAY_REASONS, BUSABASE_AIRAPP_RUNTIMES, BUSABASE_AIRAPP_RUNTIME_ENV, BusabaseAirAppLocalGateway, asKnownBusabaseAirAppRuntime, createBusabaseAirAppLocalGateway, isBusabaseAirAppHosted, readBusabaseAirAppRuntime };
|