busabase-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 +6 -6
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busabase-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Typed TypeScript/JavaScript SDK for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server` (or Busabase Cloud).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-sdk",
|
|
@@ -59,20 +59,20 @@
|
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^24.13.2",
|
|
61
61
|
"jsdom": "^27.0.0",
|
|
62
|
-
"
|
|
62
|
+
"tsdown": "^0.22.14",
|
|
63
63
|
"tsx": "^4.20.5",
|
|
64
|
-
"typescript": "^
|
|
64
|
+
"typescript": "^7.0.2",
|
|
65
65
|
"vitest": "^2.1.8",
|
|
66
|
+
"busabase-contract": "0.18.0",
|
|
66
67
|
"open-domains": "0.0.2",
|
|
67
|
-
"busabase-contract": "0.17.2",
|
|
68
68
|
"openlib": "0.1.1"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": ">=24.18.0"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
|
-
"dev": "
|
|
75
|
-
"build": "
|
|
74
|
+
"dev": "tsdown --watch",
|
|
75
|
+
"build": "tsdown",
|
|
76
76
|
"typecheck": "tsc --noEmit",
|
|
77
77
|
"test": "vitest run",
|
|
78
78
|
"lint": "biome format . --write && biome check .",
|
package/dist/chunk-5NYQX65A.js
DELETED
package/dist/chunk-C23JVY2Y.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { BusabaseOAuthError, BUSABASE_AIRAPP_CLIENT_ID, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken } from './chunk-WSHJMHUS.js';
|
|
2
|
-
import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
4
|
-
import { readFileSync, rmSync, mkdirSync, chmodSync, writeFileSync, renameSync } from 'fs';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { join, dirname } from 'path';
|
|
7
|
-
|
|
8
|
-
var STORE_VERSION = 1;
|
|
9
|
-
var APP_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
-
var REFRESH_WINDOW_MS = 6e4;
|
|
11
|
-
var refreshesByCredentialPath = /* @__PURE__ */ new Map();
|
|
12
|
-
var assertAppId = (appId) => {
|
|
13
|
-
if (!APP_ID_RE.test(appId)) {
|
|
14
|
-
throw new BusabaseOAuthError(
|
|
15
|
-
"invalid_airapp_id",
|
|
16
|
-
"AirApp id must use letters, digits, dot, dash, or underscore"
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
return appId;
|
|
20
|
-
};
|
|
21
|
-
var storeRoot = (options = {}) => options.rootDir ?? join(homedir(), ".busabase");
|
|
22
|
-
var busabaseAirAppCredentialsDir = (options = {}) => join(storeRoot(options), "airapps");
|
|
23
|
-
var busabaseAirAppCredentialPath = (appId, options = {}) => join(busabaseAirAppCredentialsDir(options), `${assertAppId(appId)}.json`);
|
|
24
|
-
var busabaseAirAppDynamicClientsPath = (appId, options = {}) => join(busabaseAirAppCredentialsDir(options), `${assertAppId(appId)}.clients.json`);
|
|
25
|
-
var normalizeOrigin = (raw) => {
|
|
26
|
-
const url = new URL(normalizeBaseUrl(raw));
|
|
27
|
-
if (url.username || url.password || url.search || url.hash) {
|
|
28
|
-
throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
|
|
29
|
-
}
|
|
30
|
-
return url.origin;
|
|
31
|
-
};
|
|
32
|
-
var writeOwnerOnlyJson = (path, value) => {
|
|
33
|
-
const directory = dirname(path);
|
|
34
|
-
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
35
|
-
try {
|
|
36
|
-
chmodSync(directory, 448);
|
|
37
|
-
} catch {
|
|
38
|
-
}
|
|
39
|
-
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
40
|
-
writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
41
|
-
`, { mode: 384 });
|
|
42
|
-
try {
|
|
43
|
-
chmodSync(temporaryPath, 384);
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
renameSync(temporaryPath, path);
|
|
47
|
-
};
|
|
48
|
-
var parseCredential = (raw, expectedAppId) => {
|
|
49
|
-
let value;
|
|
50
|
-
try {
|
|
51
|
-
value = JSON.parse(raw);
|
|
52
|
-
} catch {
|
|
53
|
-
throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
|
|
54
|
-
}
|
|
55
|
-
const item = value;
|
|
56
|
-
if (item.version !== STORE_VERSION || item.appId !== expectedAppId || typeof item.baseUrl !== "string" || typeof item.clientId !== "string" || typeof item.accessToken !== "string" || typeof item.refreshToken !== "string" || typeof item.expiresAt !== "string" || !Array.isArray(item.scope) || item.scope.some((scope) => typeof scope !== "string") || typeof item.tokenType !== "string") {
|
|
57
|
-
throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
|
|
58
|
-
}
|
|
59
|
-
if (item.selectedSpace !== void 0 && (typeof item.selectedSpace !== "object" || item.selectedSpace === null || typeof item.selectedSpace.id !== "string" || typeof item.selectedSpace.name !== "string")) {
|
|
60
|
-
throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
|
|
61
|
-
}
|
|
62
|
-
return item;
|
|
63
|
-
};
|
|
64
|
-
function loadBusabaseAirAppOAuthCredential(appId, options = {}) {
|
|
65
|
-
const path = busabaseAirAppCredentialPath(appId, options);
|
|
66
|
-
try {
|
|
67
|
-
return parseCredential(readFileSync(path, "utf8"), appId);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
if (error.code === "ENOENT") return null;
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function storeBusabaseAirAppOAuthCredential(input, options = {}) {
|
|
74
|
-
if (!input.tokenSet.refreshToken) {
|
|
75
|
-
throw new BusabaseOAuthError(
|
|
76
|
-
"missing_refresh_token",
|
|
77
|
-
"A refresh token is required for a persistent local AirApp login"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const credential = {
|
|
81
|
-
version: STORE_VERSION,
|
|
82
|
-
appId: assertAppId(input.appId),
|
|
83
|
-
baseUrl: normalizeOrigin(input.baseUrl),
|
|
84
|
-
clientId: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID,
|
|
85
|
-
accessToken: input.tokenSet.accessToken,
|
|
86
|
-
refreshToken: input.tokenSet.refreshToken,
|
|
87
|
-
expiresAt: input.tokenSet.expiresAt,
|
|
88
|
-
scope: input.tokenSet.scope,
|
|
89
|
-
tokenType: input.tokenSet.tokenType,
|
|
90
|
-
...input.selectedSpace ? { selectedSpace: input.selectedSpace } : {}
|
|
91
|
-
};
|
|
92
|
-
writeOwnerOnlyJson(busabaseAirAppCredentialPath(input.appId, options), credential);
|
|
93
|
-
return credential;
|
|
94
|
-
}
|
|
95
|
-
var dynamicClientKey = (baseUrl, redirectUri) => `${normalizeOrigin(baseUrl)}|${new URL(redirectUri).toString()}`;
|
|
96
|
-
var loadDynamicClientRegistry = (appId, options) => {
|
|
97
|
-
const empty = {
|
|
98
|
-
version: STORE_VERSION,
|
|
99
|
-
appId: assertAppId(appId),
|
|
100
|
-
clients: {}
|
|
101
|
-
};
|
|
102
|
-
let raw;
|
|
103
|
-
try {
|
|
104
|
-
raw = readFileSync(busabaseAirAppDynamicClientsPath(appId, options), "utf8");
|
|
105
|
-
} catch (error) {
|
|
106
|
-
if (error.code === "ENOENT") return empty;
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
const value = JSON.parse(raw);
|
|
111
|
-
if (value.version !== STORE_VERSION || value.appId !== appId || typeof value.clients !== "object" || value.clients === null) {
|
|
112
|
-
return empty;
|
|
113
|
-
}
|
|
114
|
-
const clients = Object.fromEntries(
|
|
115
|
-
Object.entries(value.clients).filter(([, clientId]) => typeof clientId === "string")
|
|
116
|
-
);
|
|
117
|
-
return { ...empty, clients };
|
|
118
|
-
} catch {
|
|
119
|
-
return empty;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
function loadBusabaseAirAppDynamicClientId(input, options = {}) {
|
|
123
|
-
const registry = loadDynamicClientRegistry(input.appId, options);
|
|
124
|
-
return registry.clients[dynamicClientKey(input.baseUrl, input.redirectUri)] ?? null;
|
|
125
|
-
}
|
|
126
|
-
function storeBusabaseAirAppDynamicClientId(input, options = {}) {
|
|
127
|
-
const registry = loadDynamicClientRegistry(input.appId, options);
|
|
128
|
-
const key = dynamicClientKey(input.baseUrl, input.redirectUri);
|
|
129
|
-
delete registry.clients[key];
|
|
130
|
-
const entries = [...Object.entries(registry.clients), [key, input.clientId]];
|
|
131
|
-
writeOwnerOnlyJson(busabaseAirAppDynamicClientsPath(input.appId, options), {
|
|
132
|
-
...registry,
|
|
133
|
-
clients: Object.fromEntries(entries.slice(-32))
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
async function getBusabaseAirAppAccessToken(appId, options = {}, fetchImpl = fetch) {
|
|
137
|
-
const credential = loadBusabaseAirAppOAuthCredential(appId, options);
|
|
138
|
-
if (!credential) return null;
|
|
139
|
-
const expiresAt = Date.parse(credential.expiresAt);
|
|
140
|
-
if (Number.isFinite(expiresAt) && expiresAt > Date.now() + REFRESH_WINDOW_MS) return credential;
|
|
141
|
-
const credentialPath = busabaseAirAppCredentialPath(appId, options);
|
|
142
|
-
const activeRefresh = refreshesByCredentialPath.get(credentialPath);
|
|
143
|
-
if (activeRefresh) return activeRefresh;
|
|
144
|
-
const refresh = (async () => {
|
|
145
|
-
const tokenSet = await refreshBusabaseOAuthToken(
|
|
146
|
-
{
|
|
147
|
-
baseUrl: credential.baseUrl,
|
|
148
|
-
refreshToken: credential.refreshToken,
|
|
149
|
-
clientId: credential.clientId
|
|
150
|
-
},
|
|
151
|
-
fetchImpl
|
|
152
|
-
);
|
|
153
|
-
return storeBusabaseAirAppOAuthCredential(
|
|
154
|
-
{
|
|
155
|
-
appId,
|
|
156
|
-
baseUrl: credential.baseUrl,
|
|
157
|
-
clientId: credential.clientId,
|
|
158
|
-
tokenSet: {
|
|
159
|
-
...tokenSet,
|
|
160
|
-
refreshToken: tokenSet.refreshToken ?? credential.refreshToken
|
|
161
|
-
},
|
|
162
|
-
selectedSpace: credential.selectedSpace
|
|
163
|
-
},
|
|
164
|
-
options
|
|
165
|
-
);
|
|
166
|
-
})();
|
|
167
|
-
refreshesByCredentialPath.set(credentialPath, refresh);
|
|
168
|
-
try {
|
|
169
|
-
return await refresh;
|
|
170
|
-
} finally {
|
|
171
|
-
if (refreshesByCredentialPath.get(credentialPath) === refresh) {
|
|
172
|
-
refreshesByCredentialPath.delete(credentialPath);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
function storeBusabaseAirAppSelectedSpace(appId, selectedSpace, options = {}) {
|
|
177
|
-
const credential = loadBusabaseAirAppOAuthCredential(appId, options);
|
|
178
|
-
if (!credential) {
|
|
179
|
-
throw new BusabaseOAuthError(
|
|
180
|
-
"missing_local_credential",
|
|
181
|
-
"Connect this local AirApp before selecting a Space"
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
const next = {
|
|
185
|
-
...credential,
|
|
186
|
-
...selectedSpace ? { selectedSpace } : { selectedSpace: void 0 }
|
|
187
|
-
};
|
|
188
|
-
writeOwnerOnlyJson(busabaseAirAppCredentialPath(appId, options), next);
|
|
189
|
-
return next;
|
|
190
|
-
}
|
|
191
|
-
function clearBusabaseAirAppOAuthCredential(appId, options = {}) {
|
|
192
|
-
rmSync(busabaseAirAppCredentialPath(appId, options), { force: true });
|
|
193
|
-
}
|
|
194
|
-
async function revokeBusabaseAirAppOAuthCredential(appId, options = {}, fetchImpl = fetch) {
|
|
195
|
-
const credential = loadBusabaseAirAppOAuthCredential(appId, options);
|
|
196
|
-
if (!credential) return;
|
|
197
|
-
try {
|
|
198
|
-
await revokeBusabaseOAuthToken(
|
|
199
|
-
{
|
|
200
|
-
baseUrl: credential.baseUrl,
|
|
201
|
-
token: credential.refreshToken,
|
|
202
|
-
clientId: credential.clientId
|
|
203
|
-
},
|
|
204
|
-
fetchImpl
|
|
205
|
-
);
|
|
206
|
-
} finally {
|
|
207
|
-
clearBusabaseAirAppOAuthCredential(appId, options);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, busabaseAirAppDynamicClientsPath, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
|
package/dist/chunk-WSHJMHUS.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
|
|
2
|
-
|
|
3
|
-
// ../../packages/busabase-contract/src/auth/device-authorization.ts
|
|
4
|
-
var BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
|
|
5
|
-
|
|
6
|
-
// src/oauth.ts
|
|
7
|
-
var AIRAPP_OAUTH_SCOPE = "api";
|
|
8
|
-
var BusabaseOAuthError = class extends Error {
|
|
9
|
-
code;
|
|
10
|
-
status;
|
|
11
|
-
constructor(code, message, status) {
|
|
12
|
-
super(message);
|
|
13
|
-
this.name = "BusabaseOAuthError";
|
|
14
|
-
this.code = code;
|
|
15
|
-
this.status = status;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
var oauthBaseUrl = (raw) => {
|
|
19
|
-
let url;
|
|
20
|
-
try {
|
|
21
|
-
url = new URL(normalizeBaseUrl(raw));
|
|
22
|
-
} catch {
|
|
23
|
-
throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL is invalid");
|
|
24
|
-
}
|
|
25
|
-
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") {
|
|
26
|
-
throw new BusabaseOAuthError(
|
|
27
|
-
"invalid_base_url",
|
|
28
|
-
"Busabase base URL must be an HTTP(S) origin without credentials, query, or path"
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
return url.origin;
|
|
32
|
-
};
|
|
33
|
-
var randomBase64Url = (byteLength) => {
|
|
34
|
-
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(byteLength));
|
|
35
|
-
let binary = "";
|
|
36
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
37
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
38
|
-
};
|
|
39
|
-
var digestBase64Url = async (value) => {
|
|
40
|
-
const encoded = new TextEncoder().encode(value);
|
|
41
|
-
const data = encoded.buffer.slice(
|
|
42
|
-
encoded.byteOffset,
|
|
43
|
-
encoded.byteOffset + encoded.byteLength
|
|
44
|
-
);
|
|
45
|
-
const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
46
|
-
let binary = "";
|
|
47
|
-
for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
|
|
48
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
49
|
-
};
|
|
50
|
-
async function registerBusabaseAirAppOAuthClient(input, fetchImpl = fetch) {
|
|
51
|
-
const baseUrl = oauthBaseUrl(input.baseUrl);
|
|
52
|
-
const redirectUri = new URL(input.redirectUri).toString();
|
|
53
|
-
const response = await fetchImpl(new URL("/api/oauth/register", baseUrl), {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: { "content-type": "application/json" },
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
client_name: input.appId,
|
|
58
|
-
client_kind: "airapp",
|
|
59
|
-
scope: AIRAPP_OAUTH_SCOPE,
|
|
60
|
-
redirect_uris: [redirectUri],
|
|
61
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
62
|
-
response_types: ["code"],
|
|
63
|
-
token_endpoint_auth_method: "none"
|
|
64
|
-
})
|
|
65
|
-
});
|
|
66
|
-
const body = await response.json().catch(() => null);
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
throw new BusabaseOAuthError(
|
|
69
|
-
typeof body?.error === "string" ? body.error : "client_registration_failed",
|
|
70
|
-
typeof body?.error_description === "string" ? body.error_description : `Busabase OAuth client registration failed (${response.status})`,
|
|
71
|
-
response.status
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (typeof body?.client_id !== "string" || !Array.isArray(body.redirect_uris) || body.redirect_uris.length !== 1 || body.redirect_uris[0] !== redirectUri) {
|
|
75
|
-
throw new BusabaseOAuthError(
|
|
76
|
-
"invalid_client_registration",
|
|
77
|
-
"Busabase returned an invalid OAuth client registration"
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const grantedScopes = typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : null;
|
|
81
|
-
if (!grantedScopes?.includes(AIRAPP_OAUTH_SCOPE)) {
|
|
82
|
-
throw new BusabaseOAuthError(
|
|
83
|
-
"unsupported_airapp_registration",
|
|
84
|
-
`This Busabase server did not grant the "${AIRAPP_OAUTH_SCOPE}" scope to a dynamically registered AirApp. Upgrade Busabase, or run the AirApp on a loopback address to use the shared AirApp client.`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return { clientId: body.client_id, redirectUri };
|
|
88
|
-
}
|
|
89
|
-
async function createBusabaseOAuthRequest(input) {
|
|
90
|
-
const baseUrl = oauthBaseUrl(input.baseUrl);
|
|
91
|
-
const redirectUri = new URL(input.redirectUri).toString();
|
|
92
|
-
const clientId = input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID;
|
|
93
|
-
const codeVerifier = randomBase64Url(32);
|
|
94
|
-
const state = input.state ?? randomBase64Url(24);
|
|
95
|
-
const resource = new URL("/api/v1", baseUrl).toString();
|
|
96
|
-
const authorizeUrl = new URL("/api/oauth/authorize", baseUrl);
|
|
97
|
-
authorizeUrl.search = new URLSearchParams({
|
|
98
|
-
response_type: "code",
|
|
99
|
-
client_id: clientId,
|
|
100
|
-
resource,
|
|
101
|
-
scope: AIRAPP_OAUTH_SCOPE,
|
|
102
|
-
code_challenge: await digestBase64Url(codeVerifier),
|
|
103
|
-
code_challenge_method: "S256",
|
|
104
|
-
redirect_uri: redirectUri,
|
|
105
|
-
state
|
|
106
|
-
}).toString();
|
|
107
|
-
if (input.prompt) authorizeUrl.searchParams.set("prompt", input.prompt);
|
|
108
|
-
return {
|
|
109
|
-
authorizeUrl: authorizeUrl.toString(),
|
|
110
|
-
baseUrl,
|
|
111
|
-
clientId,
|
|
112
|
-
codeVerifier,
|
|
113
|
-
redirectUri,
|
|
114
|
-
resource,
|
|
115
|
-
state
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function parseBusabaseOAuthCallback(callbackUrl, request) {
|
|
119
|
-
const callback = new URL(callbackUrl);
|
|
120
|
-
const error = callback.searchParams.get("error");
|
|
121
|
-
if (error) {
|
|
122
|
-
throw new BusabaseOAuthError(
|
|
123
|
-
error,
|
|
124
|
-
callback.searchParams.get("error_description") || "Busabase authorization was denied"
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
if (callback.searchParams.get("state") !== request.state) {
|
|
128
|
-
throw new BusabaseOAuthError("state_mismatch", "OAuth callback state did not match");
|
|
129
|
-
}
|
|
130
|
-
const issuer = callback.searchParams.get("iss");
|
|
131
|
-
let issuerMatches = false;
|
|
132
|
-
try {
|
|
133
|
-
issuerMatches = Boolean(issuer && new URL(issuer).origin === new URL(request.baseUrl).origin);
|
|
134
|
-
} catch {
|
|
135
|
-
issuerMatches = false;
|
|
136
|
-
}
|
|
137
|
-
if (!issuerMatches) {
|
|
138
|
-
throw new BusabaseOAuthError("issuer_mismatch", "OAuth callback issuer did not match");
|
|
139
|
-
}
|
|
140
|
-
const code = callback.searchParams.get("code");
|
|
141
|
-
if (!code) throw new BusabaseOAuthError("missing_code", "OAuth callback had no code");
|
|
142
|
-
return code;
|
|
143
|
-
}
|
|
144
|
-
var parseTokenResponse = async (response) => {
|
|
145
|
-
const body = await response.json().catch(() => null);
|
|
146
|
-
if (!response.ok) {
|
|
147
|
-
const code = typeof body?.error === "string" ? body.error : "token_request_failed";
|
|
148
|
-
const message = typeof body?.error_description === "string" ? body.error_description : `Busabase OAuth token request failed (${response.status})`;
|
|
149
|
-
throw new BusabaseOAuthError(code, message, response.status);
|
|
150
|
-
}
|
|
151
|
-
if (typeof body?.access_token !== "string" || typeof body.expires_in !== "number") {
|
|
152
|
-
throw new BusabaseOAuthError(
|
|
153
|
-
"invalid_token_response",
|
|
154
|
-
"Busabase returned an invalid token set"
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
accessToken: body.access_token,
|
|
159
|
-
refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
160
|
-
expiresIn: body.expires_in,
|
|
161
|
-
expiresAt: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
|
|
162
|
-
scope: typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : [],
|
|
163
|
-
tokenType: typeof body.token_type === "string" ? body.token_type : "Bearer",
|
|
164
|
-
user: body.user && typeof body.user === "object" ? body.user : void 0
|
|
165
|
-
};
|
|
166
|
-
};
|
|
167
|
-
async function exchangeBusabaseOAuthCode(request, code, fetchImpl = fetch) {
|
|
168
|
-
const response = await fetchImpl(new URL("/api/oauth/token", request.baseUrl), {
|
|
169
|
-
method: "POST",
|
|
170
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
171
|
-
body: new URLSearchParams({
|
|
172
|
-
grant_type: "authorization_code",
|
|
173
|
-
client_id: request.clientId,
|
|
174
|
-
code,
|
|
175
|
-
code_verifier: request.codeVerifier,
|
|
176
|
-
redirect_uri: request.redirectUri,
|
|
177
|
-
resource: request.resource
|
|
178
|
-
})
|
|
179
|
-
});
|
|
180
|
-
return parseTokenResponse(response);
|
|
181
|
-
}
|
|
182
|
-
async function refreshBusabaseOAuthToken(input, fetchImpl = fetch) {
|
|
183
|
-
const baseUrl = oauthBaseUrl(input.baseUrl);
|
|
184
|
-
const response = await fetchImpl(new URL("/api/oauth/token", baseUrl), {
|
|
185
|
-
method: "POST",
|
|
186
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
187
|
-
body: new URLSearchParams({
|
|
188
|
-
grant_type: "refresh_token",
|
|
189
|
-
refresh_token: input.refreshToken,
|
|
190
|
-
client_id: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID,
|
|
191
|
-
resource: new URL("/api/v1", baseUrl).toString()
|
|
192
|
-
})
|
|
193
|
-
});
|
|
194
|
-
return parseTokenResponse(response);
|
|
195
|
-
}
|
|
196
|
-
async function revokeBusabaseOAuthToken(input, fetchImpl = fetch) {
|
|
197
|
-
const baseUrl = oauthBaseUrl(input.baseUrl);
|
|
198
|
-
const response = await fetchImpl(new URL("/api/oauth/revoke", baseUrl), {
|
|
199
|
-
method: "POST",
|
|
200
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
201
|
-
body: new URLSearchParams({
|
|
202
|
-
token: input.token,
|
|
203
|
-
client_id: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID
|
|
204
|
-
})
|
|
205
|
-
});
|
|
206
|
-
if (!response.ok) {
|
|
207
|
-
throw new BusabaseOAuthError(
|
|
208
|
-
"revoke_failed",
|
|
209
|
-
`Busabase OAuth revocation failed (${response.status})`,
|
|
210
|
-
response.status
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export { AIRAPP_OAUTH_SCOPE, BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, registerBusabaseAirAppOAuthClient, revokeBusabaseOAuthToken };
|