busabase-sdk 0.10.2 → 0.11.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,33 @@
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
+ }
14
+ interface BusabaseAirAppCredentialStoreOptions {
15
+ /** Override only for tests or an explicitly isolated installation. */
16
+ rootDir?: string;
17
+ }
18
+ /** Directory containing one owner-only OAuth registration per local AirApp. */
19
+ declare const busabaseAirAppCredentialsDir: (options?: BusabaseAirAppCredentialStoreOptions) => string;
20
+ declare const busabaseAirAppCredentialPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
21
+ declare function loadBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential | null;
22
+ declare function storeBusabaseAirAppOAuthCredential(input: {
23
+ appId: string;
24
+ baseUrl: string;
25
+ tokenSet: BusabaseOAuthTokenSet;
26
+ clientId?: string;
27
+ }, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
28
+ /** Load a valid access token, rotating and persisting the token set when needed. */
29
+ declare function getBusabaseAirAppAccessToken(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<BusabaseAirAppOAuthCredential | null>;
30
+ declare function clearBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): void;
31
+ declare function revokeBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<void>;
32
+
33
+ export { type BusabaseAirAppCredentialStoreOptions, type BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential };
@@ -0,0 +1,147 @@
1
+ import { BusabaseOAuthError, BUSABASE_AIRAPP_CLIENT_ID, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken } from './chunk-J2DZKX7A.js';
2
+ import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
3
+ import { randomUUID } from 'crypto';
4
+ import { readFileSync, mkdirSync, chmodSync, writeFileSync, renameSync, rmSync } 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 normalizeOrigin = (raw) => {
25
+ const url = new URL(normalizeBaseUrl(raw));
26
+ if (url.username || url.password || url.search || url.hash) {
27
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
28
+ }
29
+ return url.origin;
30
+ };
31
+ var parseCredential = (raw, expectedAppId) => {
32
+ let value;
33
+ try {
34
+ value = JSON.parse(raw);
35
+ } catch {
36
+ throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
37
+ }
38
+ const item = value;
39
+ 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") {
40
+ throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
41
+ }
42
+ return item;
43
+ };
44
+ function loadBusabaseAirAppOAuthCredential(appId, options = {}) {
45
+ const path = busabaseAirAppCredentialPath(appId, options);
46
+ try {
47
+ return parseCredential(readFileSync(path, "utf8"), appId);
48
+ } catch (error) {
49
+ if (error.code === "ENOENT") return null;
50
+ throw error;
51
+ }
52
+ }
53
+ function storeBusabaseAirAppOAuthCredential(input, options = {}) {
54
+ if (!input.tokenSet.refreshToken) {
55
+ throw new BusabaseOAuthError(
56
+ "missing_refresh_token",
57
+ "A refresh token is required for a persistent local AirApp login"
58
+ );
59
+ }
60
+ const credential = {
61
+ version: STORE_VERSION,
62
+ appId: assertAppId(input.appId),
63
+ baseUrl: normalizeOrigin(input.baseUrl),
64
+ clientId: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID,
65
+ accessToken: input.tokenSet.accessToken,
66
+ refreshToken: input.tokenSet.refreshToken,
67
+ expiresAt: input.tokenSet.expiresAt,
68
+ scope: input.tokenSet.scope,
69
+ tokenType: input.tokenSet.tokenType
70
+ };
71
+ const path = busabaseAirAppCredentialPath(input.appId, options);
72
+ const directory = dirname(path);
73
+ mkdirSync(directory, { recursive: true, mode: 448 });
74
+ try {
75
+ chmodSync(directory, 448);
76
+ } catch {
77
+ }
78
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
79
+ writeFileSync(temporaryPath, `${JSON.stringify(credential, null, 2)}
80
+ `, { mode: 384 });
81
+ try {
82
+ chmodSync(temporaryPath, 384);
83
+ } catch {
84
+ }
85
+ renameSync(temporaryPath, path);
86
+ return credential;
87
+ }
88
+ async function getBusabaseAirAppAccessToken(appId, options = {}, fetchImpl = fetch) {
89
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
90
+ if (!credential) return null;
91
+ const expiresAt = Date.parse(credential.expiresAt);
92
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now() + REFRESH_WINDOW_MS) return credential;
93
+ const credentialPath = busabaseAirAppCredentialPath(appId, options);
94
+ const activeRefresh = refreshesByCredentialPath.get(credentialPath);
95
+ if (activeRefresh) return activeRefresh;
96
+ const refresh = (async () => {
97
+ const tokenSet = await refreshBusabaseOAuthToken(
98
+ {
99
+ baseUrl: credential.baseUrl,
100
+ refreshToken: credential.refreshToken,
101
+ clientId: credential.clientId
102
+ },
103
+ fetchImpl
104
+ );
105
+ return storeBusabaseAirAppOAuthCredential(
106
+ {
107
+ appId,
108
+ baseUrl: credential.baseUrl,
109
+ clientId: credential.clientId,
110
+ tokenSet: {
111
+ ...tokenSet,
112
+ refreshToken: tokenSet.refreshToken ?? credential.refreshToken
113
+ }
114
+ },
115
+ options
116
+ );
117
+ })();
118
+ refreshesByCredentialPath.set(credentialPath, refresh);
119
+ try {
120
+ return await refresh;
121
+ } finally {
122
+ if (refreshesByCredentialPath.get(credentialPath) === refresh) {
123
+ refreshesByCredentialPath.delete(credentialPath);
124
+ }
125
+ }
126
+ }
127
+ function clearBusabaseAirAppOAuthCredential(appId, options = {}) {
128
+ rmSync(busabaseAirAppCredentialPath(appId, options), { force: true });
129
+ }
130
+ async function revokeBusabaseAirAppOAuthCredential(appId, options = {}, fetchImpl = fetch) {
131
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
132
+ if (!credential) return;
133
+ try {
134
+ await revokeBusabaseOAuthToken(
135
+ {
136
+ baseUrl: credential.baseUrl,
137
+ token: credential.refreshToken,
138
+ clientId: credential.clientId
139
+ },
140
+ fetchImpl
141
+ );
142
+ } finally {
143
+ clearBusabaseAirAppOAuthCredential(appId, options);
144
+ }
145
+ }
146
+
147
+ export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential };
@@ -0,0 +1,55 @@
1
+ /** Public client identifier used by local Hono/AirApp development servers. */
2
+ declare const BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
3
+
4
+ interface BusabaseOAuthRequest {
5
+ authorizeUrl: string;
6
+ baseUrl: string;
7
+ clientId: string;
8
+ codeVerifier: string;
9
+ redirectUri: string;
10
+ resource: string;
11
+ state: string;
12
+ }
13
+ interface CreateBusabaseOAuthRequestInput {
14
+ baseUrl: string;
15
+ redirectUri: string;
16
+ clientId?: string;
17
+ state?: string;
18
+ prompt?: "login";
19
+ }
20
+ interface BusabaseOAuthTokenSet {
21
+ accessToken: string;
22
+ refreshToken?: string;
23
+ expiresIn: number;
24
+ expiresAt: string;
25
+ scope: string[];
26
+ tokenType: string;
27
+ user?: {
28
+ id: string;
29
+ name: string;
30
+ email: string;
31
+ image: string | null;
32
+ };
33
+ }
34
+ declare class BusabaseOAuthError extends Error {
35
+ readonly code: string;
36
+ readonly status?: number;
37
+ constructor(code: string, message: string, status?: number);
38
+ }
39
+ /** Build a public-client OAuth 2.1 authorization request with PKCE S256. */
40
+ declare function createBusabaseOAuthRequest(input: CreateBusabaseOAuthRequestInput): Promise<BusabaseOAuthRequest>;
41
+ /** Validate state and issuer before accepting the authorization code. */
42
+ declare function parseBusabaseOAuthCallback(callbackUrl: string, request: BusabaseOAuthRequest): string;
43
+ declare function exchangeBusabaseOAuthCode(request: BusabaseOAuthRequest, code: string, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
44
+ declare function refreshBusabaseOAuthToken(input: {
45
+ baseUrl: string;
46
+ refreshToken: string;
47
+ clientId?: string;
48
+ }, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
49
+ declare function revokeBusabaseOAuthToken(input: {
50
+ baseUrl: string;
51
+ token: string;
52
+ clientId?: string;
53
+ }, fetchImpl?: typeof fetch): Promise<void>;
54
+
55
+ export { BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, type BusabaseOAuthRequest, type BusabaseOAuthTokenSet, type CreateBusabaseOAuthRequestInput, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken };
package/dist/oauth.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken } from './chunk-J2DZKX7A.js';
2
+ import './chunk-5NYQX65A.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-sdk",
3
- "version": "0.10.2",
3
+ "version": "0.11.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",
@@ -22,6 +22,14 @@
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
24
24
  "default": "./dist/index.js"
25
+ },
26
+ "./oauth": {
27
+ "types": "./dist/oauth.d.ts",
28
+ "default": "./dist/oauth.js"
29
+ },
30
+ "./oauth-node": {
31
+ "types": "./dist/oauth-node.d.ts",
32
+ "default": "./dist/oauth-node.js"
25
33
  }
26
34
  },
27
35
  "files": [
@@ -41,9 +49,9 @@
41
49
  "tsx": "^4.20.5",
42
50
  "typescript": "^5.9.3",
43
51
  "vitest": "^2.1.8",
44
- "busabase-contract": "0.10.2",
45
- "open-domains": "0.0.2",
46
- "openlib": "0.1.1"
52
+ "busabase-contract": "0.11.0",
53
+ "openlib": "0.1.1",
54
+ "open-domains": "0.0.2"
47
55
  },
48
56
  "engines": {
49
57
  "node": ">=24.18.0"