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.
@@ -0,0 +1,173 @@
1
+ import { t as normalizeBaseUrl } from "./url-B8GMXalA.js";
2
+ //#region ../../packages/busabase-contract/src/auth/device-authorization.ts
3
+ /** Public client identifier used by local Hono/AirApp development servers. */
4
+ const BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
5
+ //#endregion
6
+ //#region src/oauth.ts
7
+ /** The only scope an AirApp ever requests; bound to the `/api/v1` resource. */
8
+ const AIRAPP_OAUTH_SCOPE = "api";
9
+ var BusabaseOAuthError = class extends Error {
10
+ code;
11
+ status;
12
+ constructor(code, message, status) {
13
+ super(message);
14
+ this.name = "BusabaseOAuthError";
15
+ this.code = code;
16
+ this.status = status;
17
+ }
18
+ };
19
+ const oauthBaseUrl = (raw) => {
20
+ let url;
21
+ try {
22
+ url = new URL(normalizeBaseUrl(raw));
23
+ } catch {
24
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL is invalid");
25
+ }
26
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an HTTP(S) origin without credentials, query, or path");
27
+ return url.origin;
28
+ };
29
+ const randomBase64Url = (byteLength) => {
30
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(byteLength));
31
+ let binary = "";
32
+ for (const byte of bytes) binary += String.fromCharCode(byte);
33
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ };
35
+ const digestBase64Url = async (value) => {
36
+ const encoded = new TextEncoder().encode(value);
37
+ const data = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
38
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
39
+ let binary = "";
40
+ for (const byte of new Uint8Array(digest)) binary += String.fromCharCode(byte);
41
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42
+ };
43
+ /** Register an exact HTTPS callback for a public AirApp OAuth client. */
44
+ async function registerBusabaseAirAppOAuthClient(input, fetchImpl = fetch) {
45
+ const baseUrl = oauthBaseUrl(input.baseUrl);
46
+ const redirectUri = new URL(input.redirectUri).toString();
47
+ const response = await fetchImpl(new URL("/api/oauth/register", baseUrl), {
48
+ method: "POST",
49
+ headers: { "content-type": "application/json" },
50
+ body: JSON.stringify({
51
+ client_name: input.appId,
52
+ client_kind: "airapp",
53
+ scope: "api",
54
+ redirect_uris: [redirectUri],
55
+ grant_types: ["authorization_code", "refresh_token"],
56
+ response_types: ["code"],
57
+ token_endpoint_auth_method: "none"
58
+ })
59
+ });
60
+ const body = await response.json().catch(() => null);
61
+ if (!response.ok) throw new BusabaseOAuthError(typeof body?.error === "string" ? body.error : "client_registration_failed", typeof body?.error_description === "string" ? body.error_description : `Busabase OAuth client registration failed (${response.status})`, response.status);
62
+ if (typeof body?.client_id !== "string" || !Array.isArray(body.redirect_uris) || body.redirect_uris.length !== 1 || body.redirect_uris[0] !== redirectUri) throw new BusabaseOAuthError("invalid_client_registration", "Busabase returned an invalid OAuth client registration");
63
+ if (!(typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : null)?.includes("api")) throw new BusabaseOAuthError("unsupported_airapp_registration", `This Busabase server did not grant the "api" scope to a dynamically registered AirApp. Upgrade Busabase, or run the AirApp on a loopback address to use the shared AirApp client.`);
64
+ return {
65
+ clientId: body.client_id,
66
+ redirectUri
67
+ };
68
+ }
69
+ /** Build a public-client OAuth 2.1 authorization request with PKCE S256. */
70
+ async function createBusabaseOAuthRequest(input) {
71
+ const baseUrl = oauthBaseUrl(input.baseUrl);
72
+ const redirectUri = new URL(input.redirectUri).toString();
73
+ const clientId = input.clientId ?? "busabase-airapp";
74
+ const codeVerifier = randomBase64Url(32);
75
+ const state = input.state ?? randomBase64Url(24);
76
+ const resource = new URL("/api/v1", baseUrl).toString();
77
+ const authorizeUrl = new URL("/api/oauth/authorize", baseUrl);
78
+ authorizeUrl.search = new URLSearchParams({
79
+ response_type: "code",
80
+ client_id: clientId,
81
+ resource,
82
+ scope: "api",
83
+ code_challenge: await digestBase64Url(codeVerifier),
84
+ code_challenge_method: "S256",
85
+ redirect_uri: redirectUri,
86
+ state
87
+ }).toString();
88
+ if (input.prompt) authorizeUrl.searchParams.set("prompt", input.prompt);
89
+ return {
90
+ authorizeUrl: authorizeUrl.toString(),
91
+ baseUrl,
92
+ clientId,
93
+ codeVerifier,
94
+ redirectUri,
95
+ resource,
96
+ state
97
+ };
98
+ }
99
+ /** Validate state and issuer before accepting the authorization code. */
100
+ function parseBusabaseOAuthCallback(callbackUrl, request) {
101
+ const callback = new URL(callbackUrl);
102
+ const error = callback.searchParams.get("error");
103
+ if (error) throw new BusabaseOAuthError(error, callback.searchParams.get("error_description") || "Busabase authorization was denied");
104
+ if (callback.searchParams.get("state") !== request.state) throw new BusabaseOAuthError("state_mismatch", "OAuth callback state did not match");
105
+ const issuer = callback.searchParams.get("iss");
106
+ let issuerMatches = false;
107
+ try {
108
+ issuerMatches = Boolean(issuer && new URL(issuer).origin === new URL(request.baseUrl).origin);
109
+ } catch {
110
+ issuerMatches = false;
111
+ }
112
+ if (!issuerMatches) throw new BusabaseOAuthError("issuer_mismatch", "OAuth callback issuer did not match");
113
+ const code = callback.searchParams.get("code");
114
+ if (!code) throw new BusabaseOAuthError("missing_code", "OAuth callback had no code");
115
+ return code;
116
+ }
117
+ const parseTokenResponse = async (response) => {
118
+ const body = await response.json().catch(() => null);
119
+ if (!response.ok) throw new BusabaseOAuthError(typeof body?.error === "string" ? body.error : "token_request_failed", typeof body?.error_description === "string" ? body.error_description : `Busabase OAuth token request failed (${response.status})`, response.status);
120
+ if (typeof body?.access_token !== "string" || typeof body.expires_in !== "number") throw new BusabaseOAuthError("invalid_token_response", "Busabase returned an invalid token set");
121
+ return {
122
+ accessToken: body.access_token,
123
+ refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
124
+ expiresIn: body.expires_in,
125
+ expiresAt: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
126
+ scope: typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : [],
127
+ tokenType: typeof body.token_type === "string" ? body.token_type : "Bearer",
128
+ user: body.user && typeof body.user === "object" ? body.user : void 0
129
+ };
130
+ };
131
+ async function exchangeBusabaseOAuthCode(request, code, fetchImpl = fetch) {
132
+ const response = await fetchImpl(new URL("/api/oauth/token", request.baseUrl), {
133
+ method: "POST",
134
+ headers: { "content-type": "application/x-www-form-urlencoded" },
135
+ body: new URLSearchParams({
136
+ grant_type: "authorization_code",
137
+ client_id: request.clientId,
138
+ code,
139
+ code_verifier: request.codeVerifier,
140
+ redirect_uri: request.redirectUri,
141
+ resource: request.resource
142
+ })
143
+ });
144
+ return parseTokenResponse(response);
145
+ }
146
+ async function refreshBusabaseOAuthToken(input, fetchImpl = fetch) {
147
+ const baseUrl = oauthBaseUrl(input.baseUrl);
148
+ const response = await fetchImpl(new URL("/api/oauth/token", baseUrl), {
149
+ method: "POST",
150
+ headers: { "content-type": "application/x-www-form-urlencoded" },
151
+ body: new URLSearchParams({
152
+ grant_type: "refresh_token",
153
+ refresh_token: input.refreshToken,
154
+ client_id: input.clientId ?? "busabase-airapp",
155
+ resource: new URL("/api/v1", baseUrl).toString()
156
+ })
157
+ });
158
+ return parseTokenResponse(response);
159
+ }
160
+ async function revokeBusabaseOAuthToken(input, fetchImpl = fetch) {
161
+ const baseUrl = oauthBaseUrl(input.baseUrl);
162
+ const response = await fetchImpl(new URL("/api/oauth/revoke", baseUrl), {
163
+ method: "POST",
164
+ headers: { "content-type": "application/x-www-form-urlencoded" },
165
+ body: new URLSearchParams({
166
+ token: input.token,
167
+ client_id: input.clientId ?? "busabase-airapp"
168
+ })
169
+ });
170
+ if (!response.ok) throw new BusabaseOAuthError("revoke_failed", `Busabase OAuth revocation failed (${response.status})`, response.status);
171
+ }
172
+ //#endregion
173
+ export { parseBusabaseOAuthCallback as a, revokeBusabaseOAuthToken as c, exchangeBusabaseOAuthCode as i, BUSABASE_AIRAPP_CLIENT_ID as l, BusabaseOAuthError as n, refreshBusabaseOAuthToken as o, createBusabaseOAuthRequest as r, registerBusabaseAirAppOAuthClient as s, AIRAPP_OAUTH_SCOPE as t };
@@ -0,0 +1,69 @@
1
+ //#region ../../packages/busabase-contract/src/auth/device-authorization.d.ts
2
+ /** Public client identifier used by local Hono/AirApp development servers. */
3
+ declare const BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
4
+ //#endregion
5
+ //#region src/oauth.d.ts
6
+ /** The only scope an AirApp ever requests; bound to the `/api/v1` resource. */
7
+ declare const AIRAPP_OAUTH_SCOPE = "api";
8
+ interface BusabaseOAuthRequest {
9
+ authorizeUrl: string;
10
+ baseUrl: string;
11
+ clientId: string;
12
+ codeVerifier: string;
13
+ redirectUri: string;
14
+ resource: string;
15
+ state: string;
16
+ }
17
+ interface CreateBusabaseOAuthRequestInput {
18
+ baseUrl: string;
19
+ redirectUri: string;
20
+ clientId?: string;
21
+ state?: string;
22
+ prompt?: "login";
23
+ }
24
+ interface BusabaseOAuthTokenSet {
25
+ accessToken: string;
26
+ refreshToken?: string;
27
+ expiresIn: number;
28
+ expiresAt: string;
29
+ scope: string[];
30
+ tokenType: string;
31
+ user?: {
32
+ id: string;
33
+ name: string;
34
+ email: string;
35
+ image: string | null;
36
+ };
37
+ }
38
+ interface RegisterBusabaseAirAppOAuthClientInput {
39
+ appId: string;
40
+ baseUrl: string;
41
+ redirectUri: string;
42
+ }
43
+ declare class BusabaseOAuthError extends Error {
44
+ readonly code: string;
45
+ readonly status?: number;
46
+ constructor(code: string, message: string, status?: number);
47
+ }
48
+ /** Register an exact HTTPS callback for a public AirApp OAuth client. */
49
+ declare function registerBusabaseAirAppOAuthClient(input: RegisterBusabaseAirAppOAuthClientInput, fetchImpl?: typeof fetch): Promise<{
50
+ clientId: string;
51
+ redirectUri: string;
52
+ }>;
53
+ /** Build a public-client OAuth 2.1 authorization request with PKCE S256. */
54
+ declare function createBusabaseOAuthRequest(input: CreateBusabaseOAuthRequestInput): Promise<BusabaseOAuthRequest>;
55
+ /** Validate state and issuer before accepting the authorization code. */
56
+ declare function parseBusabaseOAuthCallback(callbackUrl: string, request: BusabaseOAuthRequest): string;
57
+ declare function exchangeBusabaseOAuthCode(request: BusabaseOAuthRequest, code: string, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
58
+ declare function refreshBusabaseOAuthToken(input: {
59
+ baseUrl: string;
60
+ refreshToken: string;
61
+ clientId?: string;
62
+ }, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
63
+ declare function revokeBusabaseOAuthToken(input: {
64
+ baseUrl: string;
65
+ token: string;
66
+ clientId?: string;
67
+ }, fetchImpl?: typeof fetch): Promise<void>;
68
+ //#endregion
69
+ export { CreateBusabaseOAuthRequestInput as a, exchangeBusabaseOAuthCode as c, registerBusabaseAirAppOAuthClient as d, revokeBusabaseOAuthToken as f, BusabaseOAuthTokenSet as i, parseBusabaseOAuthCallback as l, BusabaseOAuthError as n, RegisterBusabaseAirAppOAuthClientInput as o, BUSABASE_AIRAPP_CLIENT_ID as p, BusabaseOAuthRequest as r, createBusabaseOAuthRequest as s, AIRAPP_OAUTH_SCOPE as t, refreshBusabaseOAuthToken as u };
@@ -0,0 +1,64 @@
1
+ import { i as BusabaseOAuthTokenSet } from "./oauth-FfuT0EGC.js";
2
+ //#region src/oauth-node.d.ts
3
+ interface BusabaseAirAppOAuthCredential {
4
+ version: 1;
5
+ appId: string;
6
+ baseUrl: string;
7
+ clientId: string;
8
+ accessToken: string;
9
+ refreshToken: string;
10
+ expiresAt: string;
11
+ scope: string[];
12
+ tokenType: string;
13
+ /** Validated target for this local AirApp. Tokens remain user-scoped. */
14
+ selectedSpace?: {
15
+ id: string;
16
+ name: string;
17
+ };
18
+ }
19
+ interface BusabaseAirAppCredentialStoreOptions {
20
+ /** Override only for tests or an explicitly isolated installation. */
21
+ rootDir?: string;
22
+ }
23
+ /** Directory containing one owner-only OAuth registration per local AirApp. */
24
+ declare const busabaseAirAppCredentialsDir: (options?: BusabaseAirAppCredentialStoreOptions) => string;
25
+ declare const busabaseAirAppCredentialPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
26
+ /**
27
+ * Dynamically registered public client ids, keyed by `${baseUrl}|${redirectUri}`.
28
+ *
29
+ * Kept out of the credential file because registration happens before any token exists, and the
30
+ * mapping must outlive both the process and a logout — re-registering on every restart would
31
+ * churn server-side client records and burn the registration rate limit.
32
+ */
33
+ declare const busabaseAirAppDynamicClientsPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
34
+ declare function loadBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential | null;
35
+ declare function storeBusabaseAirAppOAuthCredential(input: {
36
+ appId: string;
37
+ baseUrl: string;
38
+ tokenSet: BusabaseOAuthTokenSet;
39
+ clientId?: string;
40
+ selectedSpace?: BusabaseAirAppOAuthCredential["selectedSpace"];
41
+ }, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
42
+ /** Reuse a previously registered public client for this exact origin and callback. */
43
+ declare function loadBusabaseAirAppDynamicClientId(input: {
44
+ appId: string;
45
+ baseUrl: string;
46
+ redirectUri: string;
47
+ }, options?: BusabaseAirAppCredentialStoreOptions): string | null;
48
+ declare function storeBusabaseAirAppDynamicClientId(input: {
49
+ appId: string;
50
+ baseUrl: string;
51
+ redirectUri: string;
52
+ clientId: string;
53
+ }, options?: BusabaseAirAppCredentialStoreOptions): void;
54
+ /** Load a valid access token, rotating and persisting the token set when needed. */
55
+ declare function getBusabaseAirAppAccessToken(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<BusabaseAirAppOAuthCredential | null>;
56
+ /** Persist a Space only after the caller has verified membership through `/api/v1/auth`. */
57
+ declare function storeBusabaseAirAppSelectedSpace(appId: string, selectedSpace: {
58
+ id: string;
59
+ name: string;
60
+ } | null, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
61
+ declare function clearBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): void;
62
+ declare function revokeBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<void>;
63
+ //#endregion
64
+ export { busabaseAirAppDynamicClientsPath as a, loadBusabaseAirAppDynamicClientId as c, storeBusabaseAirAppDynamicClientId as d, storeBusabaseAirAppOAuthCredential as f, busabaseAirAppCredentialsDir as i, loadBusabaseAirAppOAuthCredential as l, BusabaseAirAppOAuthCredential as n, clearBusabaseAirAppOAuthCredential as o, storeBusabaseAirAppSelectedSpace as p, busabaseAirAppCredentialPath as r, getBusabaseAirAppAccessToken as s, BusabaseAirAppCredentialStoreOptions as t, revokeBusabaseAirAppOAuthCredential as u };
@@ -1,64 +1,2 @@
1
- import { BusabaseOAuthTokenSet } from './oauth.js';
2
-
3
- interface BusabaseAirAppOAuthCredential {
4
- version: 1;
5
- appId: string;
6
- baseUrl: string;
7
- clientId: string;
8
- accessToken: string;
9
- refreshToken: string;
10
- expiresAt: string;
11
- scope: string[];
12
- tokenType: string;
13
- /** Validated target for this local AirApp. Tokens remain user-scoped. */
14
- selectedSpace?: {
15
- id: string;
16
- name: string;
17
- };
18
- }
19
- interface BusabaseAirAppCredentialStoreOptions {
20
- /** Override only for tests or an explicitly isolated installation. */
21
- rootDir?: string;
22
- }
23
- /** Directory containing one owner-only OAuth registration per local AirApp. */
24
- declare const busabaseAirAppCredentialsDir: (options?: BusabaseAirAppCredentialStoreOptions) => string;
25
- declare const busabaseAirAppCredentialPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
26
- /**
27
- * Dynamically registered public client ids, keyed by `${baseUrl}|${redirectUri}`.
28
- *
29
- * Kept out of the credential file because registration happens before any token exists, and the
30
- * mapping must outlive both the process and a logout — re-registering on every restart would
31
- * churn server-side client records and burn the registration rate limit.
32
- */
33
- declare const busabaseAirAppDynamicClientsPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
34
- declare function loadBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential | null;
35
- declare function storeBusabaseAirAppOAuthCredential(input: {
36
- appId: string;
37
- baseUrl: string;
38
- tokenSet: BusabaseOAuthTokenSet;
39
- clientId?: string;
40
- selectedSpace?: BusabaseAirAppOAuthCredential["selectedSpace"];
41
- }, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
42
- /** Reuse a previously registered public client for this exact origin and callback. */
43
- declare function loadBusabaseAirAppDynamicClientId(input: {
44
- appId: string;
45
- baseUrl: string;
46
- redirectUri: string;
47
- }, options?: BusabaseAirAppCredentialStoreOptions): string | null;
48
- declare function storeBusabaseAirAppDynamicClientId(input: {
49
- appId: string;
50
- baseUrl: string;
51
- redirectUri: string;
52
- clientId: string;
53
- }, options?: BusabaseAirAppCredentialStoreOptions): void;
54
- /** Load a valid access token, rotating and persisting the token set when needed. */
55
- declare function getBusabaseAirAppAccessToken(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<BusabaseAirAppOAuthCredential | null>;
56
- /** Persist a Space only after the caller has verified membership through `/api/v1/auth`. */
57
- declare function storeBusabaseAirAppSelectedSpace(appId: string, selectedSpace: {
58
- id: string;
59
- name: string;
60
- } | null, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
61
- declare function clearBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): void;
62
- declare function revokeBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<void>;
63
-
64
- export { type BusabaseAirAppCredentialStoreOptions, type BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, busabaseAirAppDynamicClientsPath, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
1
+ import { a as busabaseAirAppDynamicClientsPath, c as loadBusabaseAirAppDynamicClientId, d as storeBusabaseAirAppDynamicClientId, f as storeBusabaseAirAppOAuthCredential, i as busabaseAirAppCredentialsDir, l as loadBusabaseAirAppOAuthCredential, n as BusabaseAirAppOAuthCredential, o as clearBusabaseAirAppOAuthCredential, p as storeBusabaseAirAppSelectedSpace, r as busabaseAirAppCredentialPath, s as getBusabaseAirAppAccessToken, t as BusabaseAirAppCredentialStoreOptions, u as revokeBusabaseAirAppOAuthCredential } from "./oauth-node-DGiWkh_l.js";
2
+ export { BusabaseAirAppCredentialStoreOptions, BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, busabaseAirAppDynamicClientsPath, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
@@ -1,3 +1,188 @@
1
- export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, busabaseAirAppDynamicClientsPath, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace } from './chunk-C23JVY2Y.js';
2
- import './chunk-WSHJMHUS.js';
3
- import './chunk-5NYQX65A.js';
1
+ import { t as normalizeBaseUrl } from "./url-B8GMXalA.js";
2
+ import { c as revokeBusabaseOAuthToken, n as BusabaseOAuthError, o as refreshBusabaseOAuthToken } from "./oauth-Dg2Z41RP.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ //#region src/oauth-node.ts
8
+ const STORE_VERSION = 1;
9
+ const APP_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10
+ const REFRESH_WINDOW_MS = 6e4;
11
+ const refreshesByCredentialPath = /* @__PURE__ */ new Map();
12
+ const assertAppId = (appId) => {
13
+ if (!APP_ID_RE.test(appId)) throw new BusabaseOAuthError("invalid_airapp_id", "AirApp id must use letters, digits, dot, dash, or underscore");
14
+ return appId;
15
+ };
16
+ const storeRoot = (options = {}) => options.rootDir ?? join(homedir(), ".busabase");
17
+ /** Directory containing one owner-only OAuth registration per local AirApp. */
18
+ const busabaseAirAppCredentialsDir = (options = {}) => join(storeRoot(options), "airapps");
19
+ const busabaseAirAppCredentialPath = (appId, options = {}) => join(busabaseAirAppCredentialsDir(options), `${assertAppId(appId)}.json`);
20
+ /**
21
+ * Dynamically registered public client ids, keyed by `${baseUrl}|${redirectUri}`.
22
+ *
23
+ * Kept out of the credential file because registration happens before any token exists, and the
24
+ * mapping must outlive both the process and a logout — re-registering on every restart would
25
+ * churn server-side client records and burn the registration rate limit.
26
+ */
27
+ const busabaseAirAppDynamicClientsPath = (appId, options = {}) => join(busabaseAirAppCredentialsDir(options), `${assertAppId(appId)}.clients.json`);
28
+ const normalizeOrigin = (raw) => {
29
+ const url = new URL(normalizeBaseUrl(raw));
30
+ if (url.username || url.password || url.search || url.hash) throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
31
+ return url.origin;
32
+ };
33
+ const writeOwnerOnlyJson = (path, value) => {
34
+ const directory = dirname(path);
35
+ mkdirSync(directory, {
36
+ recursive: true,
37
+ mode: 448
38
+ });
39
+ try {
40
+ chmodSync(directory, 448);
41
+ } catch {}
42
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
43
+ writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
44
+ try {
45
+ chmodSync(temporaryPath, 384);
46
+ } catch {}
47
+ renameSync(temporaryPath, path);
48
+ };
49
+ const parseCredential = (raw, expectedAppId) => {
50
+ let value;
51
+ try {
52
+ value = JSON.parse(raw);
53
+ } catch {
54
+ throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
55
+ }
56
+ const item = value;
57
+ 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") throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
58
+ if (item.selectedSpace !== void 0 && (typeof item.selectedSpace !== "object" || item.selectedSpace === null || typeof item.selectedSpace.id !== "string" || typeof item.selectedSpace.name !== "string")) throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
59
+ return item;
60
+ };
61
+ function loadBusabaseAirAppOAuthCredential(appId, options = {}) {
62
+ const path = busabaseAirAppCredentialPath(appId, options);
63
+ try {
64
+ return parseCredential(readFileSync(path, "utf8"), appId);
65
+ } catch (error) {
66
+ if (error.code === "ENOENT") return null;
67
+ throw error;
68
+ }
69
+ }
70
+ function storeBusabaseAirAppOAuthCredential(input, options = {}) {
71
+ if (!input.tokenSet.refreshToken) throw new BusabaseOAuthError("missing_refresh_token", "A refresh token is required for a persistent local AirApp login");
72
+ const credential = {
73
+ version: STORE_VERSION,
74
+ appId: assertAppId(input.appId),
75
+ baseUrl: normalizeOrigin(input.baseUrl),
76
+ clientId: input.clientId ?? "busabase-airapp",
77
+ accessToken: input.tokenSet.accessToken,
78
+ refreshToken: input.tokenSet.refreshToken,
79
+ expiresAt: input.tokenSet.expiresAt,
80
+ scope: input.tokenSet.scope,
81
+ tokenType: input.tokenSet.tokenType,
82
+ ...input.selectedSpace ? { selectedSpace: input.selectedSpace } : {}
83
+ };
84
+ writeOwnerOnlyJson(busabaseAirAppCredentialPath(input.appId, options), credential);
85
+ return credential;
86
+ }
87
+ const dynamicClientKey = (baseUrl, redirectUri) => `${normalizeOrigin(baseUrl)}|${new URL(redirectUri).toString()}`;
88
+ const loadDynamicClientRegistry = (appId, options) => {
89
+ const empty = {
90
+ version: STORE_VERSION,
91
+ appId: assertAppId(appId),
92
+ clients: {}
93
+ };
94
+ let raw;
95
+ try {
96
+ raw = readFileSync(busabaseAirAppDynamicClientsPath(appId, options), "utf8");
97
+ } catch (error) {
98
+ if (error.code === "ENOENT") return empty;
99
+ throw error;
100
+ }
101
+ try {
102
+ const value = JSON.parse(raw);
103
+ if (value.version !== STORE_VERSION || value.appId !== appId || typeof value.clients !== "object" || value.clients === null) return empty;
104
+ const clients = Object.fromEntries(Object.entries(value.clients).filter(([, clientId]) => typeof clientId === "string"));
105
+ return {
106
+ ...empty,
107
+ clients
108
+ };
109
+ } catch {
110
+ return empty;
111
+ }
112
+ };
113
+ /** Reuse a previously registered public client for this exact origin and callback. */
114
+ function loadBusabaseAirAppDynamicClientId(input, options = {}) {
115
+ return loadDynamicClientRegistry(input.appId, options).clients[dynamicClientKey(input.baseUrl, input.redirectUri)] ?? null;
116
+ }
117
+ function storeBusabaseAirAppDynamicClientId(input, options = {}) {
118
+ const registry = loadDynamicClientRegistry(input.appId, options);
119
+ const key = dynamicClientKey(input.baseUrl, input.redirectUri);
120
+ delete registry.clients[key];
121
+ const entries = [...Object.entries(registry.clients), [key, input.clientId]];
122
+ writeOwnerOnlyJson(busabaseAirAppDynamicClientsPath(input.appId, options), {
123
+ ...registry,
124
+ clients: Object.fromEntries(entries.slice(-32))
125
+ });
126
+ }
127
+ /** Load a valid access token, rotating and persisting the token set when needed. */
128
+ async function getBusabaseAirAppAccessToken(appId, options = {}, fetchImpl = fetch) {
129
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
130
+ if (!credential) return null;
131
+ const expiresAt = Date.parse(credential.expiresAt);
132
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now() + REFRESH_WINDOW_MS) return credential;
133
+ const credentialPath = busabaseAirAppCredentialPath(appId, options);
134
+ const activeRefresh = refreshesByCredentialPath.get(credentialPath);
135
+ if (activeRefresh) return activeRefresh;
136
+ const refresh = (async () => {
137
+ const tokenSet = await refreshBusabaseOAuthToken({
138
+ baseUrl: credential.baseUrl,
139
+ refreshToken: credential.refreshToken,
140
+ clientId: credential.clientId
141
+ }, fetchImpl);
142
+ return storeBusabaseAirAppOAuthCredential({
143
+ appId,
144
+ baseUrl: credential.baseUrl,
145
+ clientId: credential.clientId,
146
+ tokenSet: {
147
+ ...tokenSet,
148
+ refreshToken: tokenSet.refreshToken ?? credential.refreshToken
149
+ },
150
+ selectedSpace: credential.selectedSpace
151
+ }, options);
152
+ })();
153
+ refreshesByCredentialPath.set(credentialPath, refresh);
154
+ try {
155
+ return await refresh;
156
+ } finally {
157
+ if (refreshesByCredentialPath.get(credentialPath) === refresh) refreshesByCredentialPath.delete(credentialPath);
158
+ }
159
+ }
160
+ /** Persist a Space only after the caller has verified membership through `/api/v1/auth`. */
161
+ function storeBusabaseAirAppSelectedSpace(appId, selectedSpace, options = {}) {
162
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
163
+ if (!credential) throw new BusabaseOAuthError("missing_local_credential", "Connect this local AirApp before selecting a Space");
164
+ const next = {
165
+ ...credential,
166
+ ...selectedSpace ? { selectedSpace } : { selectedSpace: void 0 }
167
+ };
168
+ writeOwnerOnlyJson(busabaseAirAppCredentialPath(appId, options), next);
169
+ return next;
170
+ }
171
+ function clearBusabaseAirAppOAuthCredential(appId, options = {}) {
172
+ rmSync(busabaseAirAppCredentialPath(appId, options), { force: true });
173
+ }
174
+ async function revokeBusabaseAirAppOAuthCredential(appId, options = {}, fetchImpl = fetch) {
175
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
176
+ if (!credential) return;
177
+ try {
178
+ await revokeBusabaseOAuthToken({
179
+ baseUrl: credential.baseUrl,
180
+ token: credential.refreshToken,
181
+ clientId: credential.clientId
182
+ }, fetchImpl);
183
+ } finally {
184
+ clearBusabaseAirAppOAuthCredential(appId, options);
185
+ }
186
+ }
187
+ //#endregion
188
+ export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, busabaseAirAppDynamicClientsPath, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppDynamicClientId, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppDynamicClientId, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
package/dist/oauth.d.ts CHANGED
@@ -1,67 +1,2 @@
1
- /** Public client identifier used by local Hono/AirApp development servers. */
2
- declare const BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
3
-
4
- /** The only scope an AirApp ever requests; bound to the `/api/v1` resource. */
5
- declare const AIRAPP_OAUTH_SCOPE = "api";
6
- interface BusabaseOAuthRequest {
7
- authorizeUrl: string;
8
- baseUrl: string;
9
- clientId: string;
10
- codeVerifier: string;
11
- redirectUri: string;
12
- resource: string;
13
- state: string;
14
- }
15
- interface CreateBusabaseOAuthRequestInput {
16
- baseUrl: string;
17
- redirectUri: string;
18
- clientId?: string;
19
- state?: string;
20
- prompt?: "login";
21
- }
22
- interface BusabaseOAuthTokenSet {
23
- accessToken: string;
24
- refreshToken?: string;
25
- expiresIn: number;
26
- expiresAt: string;
27
- scope: string[];
28
- tokenType: string;
29
- user?: {
30
- id: string;
31
- name: string;
32
- email: string;
33
- image: string | null;
34
- };
35
- }
36
- interface RegisterBusabaseAirAppOAuthClientInput {
37
- appId: string;
38
- baseUrl: string;
39
- redirectUri: string;
40
- }
41
- declare class BusabaseOAuthError extends Error {
42
- readonly code: string;
43
- readonly status?: number;
44
- constructor(code: string, message: string, status?: number);
45
- }
46
- /** Register an exact HTTPS callback for a public AirApp OAuth client. */
47
- declare function registerBusabaseAirAppOAuthClient(input: RegisterBusabaseAirAppOAuthClientInput, fetchImpl?: typeof fetch): Promise<{
48
- clientId: string;
49
- redirectUri: string;
50
- }>;
51
- /** Build a public-client OAuth 2.1 authorization request with PKCE S256. */
52
- declare function createBusabaseOAuthRequest(input: CreateBusabaseOAuthRequestInput): Promise<BusabaseOAuthRequest>;
53
- /** Validate state and issuer before accepting the authorization code. */
54
- declare function parseBusabaseOAuthCallback(callbackUrl: string, request: BusabaseOAuthRequest): string;
55
- declare function exchangeBusabaseOAuthCode(request: BusabaseOAuthRequest, code: string, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
56
- declare function refreshBusabaseOAuthToken(input: {
57
- baseUrl: string;
58
- refreshToken: string;
59
- clientId?: string;
60
- }, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
61
- declare function revokeBusabaseOAuthToken(input: {
62
- baseUrl: string;
63
- token: string;
64
- clientId?: string;
65
- }, fetchImpl?: typeof fetch): Promise<void>;
66
-
67
- export { AIRAPP_OAUTH_SCOPE, BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, type BusabaseOAuthRequest, type BusabaseOAuthTokenSet, type CreateBusabaseOAuthRequestInput, type RegisterBusabaseAirAppOAuthClientInput, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, registerBusabaseAirAppOAuthClient, revokeBusabaseOAuthToken };
1
+ import { a as CreateBusabaseOAuthRequestInput, c as exchangeBusabaseOAuthCode, d as registerBusabaseAirAppOAuthClient, f as revokeBusabaseOAuthToken, i as BusabaseOAuthTokenSet, l as parseBusabaseOAuthCallback, n as BusabaseOAuthError, o as RegisterBusabaseAirAppOAuthClientInput, p as BUSABASE_AIRAPP_CLIENT_ID, r as BusabaseOAuthRequest, s as createBusabaseOAuthRequest, t as AIRAPP_OAUTH_SCOPE, u as refreshBusabaseOAuthToken } from "./oauth-FfuT0EGC.js";
2
+ export { AIRAPP_OAUTH_SCOPE, BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, BusabaseOAuthRequest, BusabaseOAuthTokenSet, CreateBusabaseOAuthRequestInput, RegisterBusabaseAirAppOAuthClientInput, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, registerBusabaseAirAppOAuthClient, revokeBusabaseOAuthToken };
package/dist/oauth.js CHANGED
@@ -1,2 +1,2 @@
1
- export { AIRAPP_OAUTH_SCOPE, BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, registerBusabaseAirAppOAuthClient, revokeBusabaseOAuthToken } from './chunk-WSHJMHUS.js';
2
- import './chunk-5NYQX65A.js';
1
+ import { a as parseBusabaseOAuthCallback, c as revokeBusabaseOAuthToken, i as exchangeBusabaseOAuthCode, l as BUSABASE_AIRAPP_CLIENT_ID, n as BusabaseOAuthError, o as refreshBusabaseOAuthToken, r as createBusabaseOAuthRequest, s as registerBusabaseAirAppOAuthClient, t as AIRAPP_OAUTH_SCOPE } from "./oauth-Dg2Z41RP.js";
2
+ export { AIRAPP_OAUTH_SCOPE, BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, registerBusabaseAirAppOAuthClient, revokeBusabaseOAuthToken };
@@ -0,0 +1,10 @@
1
+ //#region src/url.ts
2
+ /**
3
+ * Normalize a user-supplied base URL to the server root. The API contract
4
+ * already carries `/api/v1`, so accept either form and strip the suffix.
5
+ */
6
+ function normalizeBaseUrl(raw) {
7
+ return raw.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
8
+ }
9
+ //#endregion
10
+ export { normalizeBaseUrl as t };