busabase-sdk 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ import { BusabaseAirAppCredentialStoreOptions } from './oauth-node.js';
2
+ import './oauth.js';
3
+
4
+ declare const BUSABASE_AIRAPP_GATEWAY_REASONS: {
5
+ readonly authRequired: "AUTH_REQUIRED";
6
+ readonly authUnavailable: "AUTH_UNAVAILABLE";
7
+ readonly connectionRequired: "CONNECTION_REQUIRED";
8
+ readonly oauthCallbackInvalid: "OAUTH_CALLBACK_INVALID";
9
+ readonly spaceNotAllowed: "SPACE_NOT_ALLOWED";
10
+ readonly spaceSelectionRequired: "SPACE_SELECTION_REQUIRED";
11
+ };
12
+ type GatewayReason = (typeof BUSABASE_AIRAPP_GATEWAY_REASONS)[keyof typeof BUSABASE_AIRAPP_GATEWAY_REASONS];
13
+ interface AuthSpace {
14
+ id: string;
15
+ name: string;
16
+ slug?: string | null;
17
+ plan?: string | null;
18
+ }
19
+ interface GatewayTarget {
20
+ baseUrl: string;
21
+ accessToken: string;
22
+ selectedSpace?: AuthSpace;
23
+ source: "airapp-oauth-local" | "environment" | "open-server";
24
+ }
25
+ interface BusabaseAirAppLocalGatewayOptions {
26
+ appId: string;
27
+ cloudBaseUrl?: string;
28
+ clientId?: string;
29
+ environment?: Record<string, string | undefined>;
30
+ fetch?: typeof fetch;
31
+ now?: () => number;
32
+ oauthPendingTtlMs?: number;
33
+ requestTimeoutMs?: number;
34
+ credentialStore?: BusabaseAirAppCredentialStoreOptions;
35
+ successPath?: string;
36
+ errorPath?: string;
37
+ }
38
+ interface BusabaseAirAppAuthStatus {
39
+ connected: boolean;
40
+ cloudBaseUrl: string;
41
+ baseUrl?: string;
42
+ source?: GatewayTarget["source"];
43
+ readiness: "needs_connection" | "needs_auth" | "needs_space" | "ready" | "retry";
44
+ action: "connect" | "reconnect" | "select_space" | "continue" | "retry";
45
+ requiresSpace?: boolean;
46
+ selectedSpace?: AuthSpace | null;
47
+ space?: AuthSpace | null;
48
+ spaces?: AuthSpace[];
49
+ reason?: GatewayReason;
50
+ message?: string;
51
+ }
52
+ declare class BusabaseAirAppLocalGateway {
53
+ #private;
54
+ constructor(options: BusabaseAirAppLocalGatewayOptions);
55
+ get cloudBaseUrl(): string;
56
+ status(): Promise<BusabaseAirAppAuthStatus>;
57
+ statusResponse: () => Promise<Response>;
58
+ start: (request: Request) => Promise<Response>;
59
+ callback: (request: Request) => Promise<Response>;
60
+ selectSpace: (request: Request) => Promise<Response>;
61
+ logout: (request: Request) => Promise<Response>;
62
+ proxy: (request: Request) => Promise<Response>;
63
+ }
64
+ declare const createBusabaseAirAppLocalGateway: (options: BusabaseAirAppLocalGatewayOptions) => BusabaseAirAppLocalGateway;
65
+
66
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, type BusabaseAirAppAuthStatus, BusabaseAirAppLocalGateway, type BusabaseAirAppLocalGatewayOptions, createBusabaseAirAppLocalGateway };
@@ -0,0 +1,408 @@
1
+ import { getBusabaseAirAppAccessToken, storeBusabaseAirAppSelectedSpace, storeBusabaseAirAppOAuthCredential, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential } from './chunk-B2AWPFDI.js';
2
+ import { BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, createBusabaseOAuthRequest, parseBusabaseOAuthCallback, exchangeBusabaseOAuthCode } from './chunk-J2DZKX7A.js';
3
+ import './chunk-5NYQX65A.js';
4
+
5
+ // src/airapp-node.ts
6
+ var DEFAULT_CLOUD_BASE_URL = "https://busabase.com";
7
+ var DEFAULT_PENDING_TTL_MS = 5 * 6e4;
8
+ var DEFAULT_TIMEOUT_MS = 8e3;
9
+ var BUSABASE_AIRAPP_GATEWAY_REASONS = {
10
+ authRequired: "AUTH_REQUIRED",
11
+ authUnavailable: "AUTH_UNAVAILABLE",
12
+ connectionRequired: "CONNECTION_REQUIRED",
13
+ oauthCallbackInvalid: "OAUTH_CALLBACK_INVALID",
14
+ spaceNotAllowed: "SPACE_NOT_ALLOWED",
15
+ spaceSelectionRequired: "SPACE_SELECTION_REQUIRED"
16
+ };
17
+ var jsonError = (status, reason, message, data) => Response.json(
18
+ {
19
+ error: message,
20
+ code: status === 401 ? "UNAUTHORIZED" : status === 403 ? "FORBIDDEN" : status === 409 ? "CONFLICT" : status === 503 ? "SERVICE_UNAVAILABLE" : "BAD_REQUEST",
21
+ data: { reason, ...data }
22
+ },
23
+ { status }
24
+ );
25
+ var normalizeOrigin = (raw, fallback = DEFAULT_CLOUD_BASE_URL) => {
26
+ const withoutApi = String(raw || fallback).trim().replace(/\/+$/, "").replace(/\/api\/v1$/, "");
27
+ let url;
28
+ try {
29
+ url = new URL(withoutApi);
30
+ } catch {
31
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL is invalid");
32
+ }
33
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/" && url.pathname !== "") {
34
+ throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
35
+ }
36
+ const loopback = ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
37
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
38
+ throw new BusabaseOAuthError(
39
+ "invalid_base_url",
40
+ "Busabase requires HTTPS except for a loopback development server"
41
+ );
42
+ }
43
+ return url.origin;
44
+ };
45
+ var requestOrigin = (request) => new URL(request.url).origin;
46
+ var assertSameOrigin = (request) => {
47
+ const origin = request.headers.get("origin");
48
+ if (origin && origin !== requestOrigin(request)) {
49
+ throw new BusabaseOAuthError("origin_mismatch", "Request origin did not match");
50
+ }
51
+ };
52
+ var readInput = async (request) => {
53
+ const contentType = request.headers.get("content-type") || "";
54
+ if (contentType.includes("application/json")) {
55
+ return await request.json().catch(() => ({}));
56
+ }
57
+ const form = await request.formData().catch(() => new FormData());
58
+ return Object.fromEntries(form.entries());
59
+ };
60
+ var safeSpaces = (spaces) => spaces.map(({ id, name, slug, plan }) => ({ id, name, slug, plan }));
61
+ var BusabaseAirAppLocalGateway = class {
62
+ #options;
63
+ #pendingOAuth = /* @__PURE__ */ new Map();
64
+ #environmentSelectedSpace;
65
+ constructor(options) {
66
+ this.#options = {
67
+ ...options,
68
+ appId: options.appId,
69
+ cloudBaseUrl: normalizeOrigin(options.cloudBaseUrl || DEFAULT_CLOUD_BASE_URL),
70
+ clientId: options.clientId || BUSABASE_AIRAPP_CLIENT_ID,
71
+ oauthPendingTtlMs: options.oauthPendingTtlMs ?? DEFAULT_PENDING_TTL_MS,
72
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS,
73
+ successPath: options.successPath || "/",
74
+ errorPath: options.errorPath || "/"
75
+ };
76
+ }
77
+ get cloudBaseUrl() {
78
+ return this.#options.cloudBaseUrl;
79
+ }
80
+ #fetch() {
81
+ return this.#options.fetch ?? fetch;
82
+ }
83
+ #now() {
84
+ return this.#options.now?.() ?? Date.now();
85
+ }
86
+ #environment() {
87
+ return this.#options.environment ?? process.env;
88
+ }
89
+ async #target() {
90
+ const environment = this.#environment();
91
+ if (environment.BUSABASE_BASE_URL) {
92
+ const selectedSpaceId = environment.BUSABASE_SPACE_ID?.trim();
93
+ return {
94
+ baseUrl: normalizeOrigin(environment.BUSABASE_BASE_URL),
95
+ accessToken: environment.BUSABASE_API_KEY || "",
96
+ source: environment.BUSABASE_API_KEY ? "environment" : "open-server",
97
+ ...selectedSpaceId ? { selectedSpace: { id: selectedSpaceId, name: selectedSpaceId } } : this.#environmentSelectedSpace ? { selectedSpace: this.#environmentSelectedSpace } : {}
98
+ };
99
+ }
100
+ const credential = await getBusabaseAirAppAccessToken(
101
+ this.#options.appId,
102
+ this.#options.credentialStore,
103
+ this.#fetch()
104
+ );
105
+ return credential ? {
106
+ baseUrl: credential.baseUrl,
107
+ accessToken: credential.accessToken,
108
+ source: "airapp-oauth-local",
109
+ ...credential.selectedSpace ? { selectedSpace: credential.selectedSpace } : {}
110
+ } : null;
111
+ }
112
+ async #authInfo(target, spaceId = "") {
113
+ const headers = new Headers({ accept: "application/json" });
114
+ if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
115
+ if (spaceId) headers.set("x-busabase-space", spaceId);
116
+ const response = await this.#fetch()(new URL("/api/v1/auth", target.baseUrl), {
117
+ headers,
118
+ signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
119
+ });
120
+ if (!response.ok) {
121
+ throw new BusabaseOAuthError(
122
+ response.status === 401 ? "auth_required" : "auth_verification_failed",
123
+ `Busabase auth verification failed (${response.status})`,
124
+ response.status
125
+ );
126
+ }
127
+ const info = await response.json();
128
+ if (!Array.isArray(info.spaces)) {
129
+ throw new BusabaseOAuthError(
130
+ "invalid_auth_response",
131
+ "Busabase auth response did not include Spaces"
132
+ );
133
+ }
134
+ return info;
135
+ }
136
+ #persistSelectedSpace(target, selectedSpace) {
137
+ if (target.source === "airapp-oauth-local") {
138
+ storeBusabaseAirAppSelectedSpace(
139
+ this.#options.appId,
140
+ selectedSpace ? { id: selectedSpace.id, name: selectedSpace.name } : null,
141
+ this.#options.credentialStore
142
+ );
143
+ } else {
144
+ this.#environmentSelectedSpace = selectedSpace || void 0;
145
+ }
146
+ }
147
+ async status() {
148
+ let target = null;
149
+ try {
150
+ target = await this.#target();
151
+ if (!target) {
152
+ return {
153
+ connected: false,
154
+ cloudBaseUrl: this.cloudBaseUrl,
155
+ readiness: "needs_connection",
156
+ action: "connect",
157
+ reason: BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired
158
+ };
159
+ }
160
+ const info = await this.#authInfo(target);
161
+ const spaces = safeSpaces(info.spaces);
162
+ if (!spaces.length) {
163
+ this.#persistSelectedSpace(target, null);
164
+ return {
165
+ connected: true,
166
+ cloudBaseUrl: this.cloudBaseUrl,
167
+ baseUrl: target.baseUrl,
168
+ source: target.source,
169
+ readiness: "needs_space",
170
+ action: "retry",
171
+ requiresSpace: true,
172
+ selectedSpace: null,
173
+ space: null,
174
+ spaces,
175
+ reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
176
+ message: "This account has no accessible Busabase Space"
177
+ };
178
+ }
179
+ const selectedSpaceId = target.selectedSpace?.id;
180
+ let selected = selectedSpaceId ? spaces.find((space) => space.id === selectedSpaceId) : void 0;
181
+ if (!selected && spaces.length === 1) selected = spaces[0];
182
+ if (target.selectedSpace && !selected) this.#persistSelectedSpace(target, null);
183
+ if (selected && selected.id !== selectedSpaceId) {
184
+ await this.#authInfo(target, selected.id);
185
+ this.#persistSelectedSpace(target, selected);
186
+ }
187
+ return {
188
+ connected: true,
189
+ cloudBaseUrl: this.cloudBaseUrl,
190
+ baseUrl: target.baseUrl,
191
+ source: target.source,
192
+ readiness: selected ? "ready" : "needs_space",
193
+ action: selected ? "continue" : "select_space",
194
+ requiresSpace: !selected,
195
+ selectedSpace: selected || null,
196
+ space: selected || null,
197
+ spaces,
198
+ ...selected ? {} : { reason: BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired }
199
+ };
200
+ } catch (error) {
201
+ const authRequired = error instanceof BusabaseOAuthError && error.status === 401;
202
+ return {
203
+ connected: Boolean(target) && !authRequired,
204
+ cloudBaseUrl: this.cloudBaseUrl,
205
+ ...target ? { baseUrl: target.baseUrl, source: target.source, requiresSpace: true } : {},
206
+ readiness: authRequired ? "needs_auth" : "retry",
207
+ action: authRequired ? "reconnect" : "retry",
208
+ reason: authRequired ? BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired : BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
209
+ message: error instanceof Error ? error.message : "Busabase auth verification failed"
210
+ };
211
+ }
212
+ }
213
+ statusResponse = async () => Response.json(await this.status());
214
+ start = async (request) => {
215
+ try {
216
+ assertSameOrigin(request);
217
+ const body = await readInput(request);
218
+ const baseUrl = normalizeOrigin(String(body.base_url || ""), this.cloudBaseUrl);
219
+ const redirectUri = new URL("/auth/callback", requestOrigin(request)).toString();
220
+ const oauthRequest = await createBusabaseOAuthRequest({
221
+ baseUrl,
222
+ redirectUri,
223
+ clientId: this.#options.clientId
224
+ });
225
+ const probe = await this.#fetch()(oauthRequest.authorizeUrl, {
226
+ headers: { accept: "text/html" },
227
+ redirect: "manual",
228
+ signal: AbortSignal.timeout(this.#options.requestTimeoutMs)
229
+ });
230
+ if (probe.status >= 400) {
231
+ throw new BusabaseOAuthError(
232
+ "oauth_unavailable",
233
+ `Busabase OAuth is unavailable (${probe.status})`,
234
+ probe.status
235
+ );
236
+ }
237
+ this.#pendingOAuth.set(oauthRequest.state, {
238
+ ...oauthRequest,
239
+ expiresAt: this.#now() + this.#options.oauthPendingTtlMs
240
+ });
241
+ return Response.redirect(oauthRequest.authorizeUrl, 303);
242
+ } catch (error) {
243
+ const redirect = new URL(this.#options.errorPath, requestOrigin(request));
244
+ redirect.searchParams.set(
245
+ "oauth_error",
246
+ error instanceof Error ? error.message : "Unable to start Busabase OAuth"
247
+ );
248
+ return Response.redirect(redirect, 303);
249
+ }
250
+ };
251
+ callback = async (request) => {
252
+ const callback = new URL(request.url);
253
+ const state = callback.searchParams.get("state") || "";
254
+ const pending = this.#pendingOAuth.get(state);
255
+ this.#pendingOAuth.delete(state);
256
+ try {
257
+ if (!pending || pending.expiresAt <= this.#now()) {
258
+ throw new BusabaseOAuthError("oauth_request_expired", "OAuth request expired");
259
+ }
260
+ const code = parseBusabaseOAuthCallback(callback.toString(), pending);
261
+ const tokenSet = await exchangeBusabaseOAuthCode(pending, code, this.#fetch());
262
+ storeBusabaseAirAppOAuthCredential(
263
+ {
264
+ appId: this.#options.appId,
265
+ baseUrl: pending.baseUrl,
266
+ clientId: this.#options.clientId,
267
+ tokenSet
268
+ },
269
+ this.#options.credentialStore
270
+ );
271
+ return Response.redirect(new URL(this.#options.successPath, requestOrigin(request)), 303);
272
+ } catch (error) {
273
+ const redirect = new URL(this.#options.errorPath, requestOrigin(request));
274
+ redirect.searchParams.set(
275
+ "oauth_error",
276
+ error instanceof Error ? error.message : "Busabase OAuth callback failed"
277
+ );
278
+ return Response.redirect(redirect, 303);
279
+ }
280
+ };
281
+ selectSpace = async (request) => {
282
+ try {
283
+ assertSameOrigin(request);
284
+ const body = await readInput(request);
285
+ const spaceId = String(body.space_id || "").trim();
286
+ const target = await this.#target();
287
+ if (!target) {
288
+ return jsonError(
289
+ 401,
290
+ BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired,
291
+ "Connect Busabase before selecting a Space"
292
+ );
293
+ }
294
+ const info = await this.#authInfo(target);
295
+ const selected = info.spaces.find((space) => space.id === spaceId);
296
+ if (!selected) {
297
+ return jsonError(
298
+ 403,
299
+ BUSABASE_AIRAPP_GATEWAY_REASONS.spaceNotAllowed,
300
+ "The selected Space is not accessible to this account"
301
+ );
302
+ }
303
+ await this.#authInfo(target, selected.id);
304
+ this.#persistSelectedSpace(target, selected);
305
+ return Response.json({ ok: true, space: { id: selected.id, name: selected.name } });
306
+ } catch (error) {
307
+ return jsonError(
308
+ 400,
309
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
310
+ error instanceof Error ? error.message : "Unable to select Busabase Space"
311
+ );
312
+ }
313
+ };
314
+ logout = async (request) => {
315
+ try {
316
+ assertSameOrigin(request);
317
+ if (loadBusabaseAirAppOAuthCredential(this.#options.appId, this.#options.credentialStore)) {
318
+ await revokeBusabaseAirAppOAuthCredential(
319
+ this.#options.appId,
320
+ this.#options.credentialStore,
321
+ this.#fetch()
322
+ ).catch(() => void 0);
323
+ }
324
+ this.#environmentSelectedSpace = void 0;
325
+ return Response.json({ ok: true });
326
+ } catch (error) {
327
+ return jsonError(
328
+ 400,
329
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
330
+ error instanceof Error ? error.message : "Unable to disconnect Busabase"
331
+ );
332
+ }
333
+ };
334
+ proxy = async (request) => {
335
+ let target;
336
+ try {
337
+ target = await this.#target();
338
+ } catch {
339
+ return jsonError(
340
+ 401,
341
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authRequired,
342
+ "Busabase authentication expired"
343
+ );
344
+ }
345
+ if (!target) {
346
+ return jsonError(
347
+ 401,
348
+ BUSABASE_AIRAPP_GATEWAY_REASONS.connectionRequired,
349
+ "Busabase connection required"
350
+ );
351
+ }
352
+ let selectedSpace = target.selectedSpace;
353
+ if (!selectedSpace) {
354
+ try {
355
+ const info = await this.#authInfo(target);
356
+ if (info.spaces.length === 1) {
357
+ selectedSpace = info.spaces[0];
358
+ this.#persistSelectedSpace(target, selectedSpace);
359
+ }
360
+ } catch {
361
+ return jsonError(
362
+ 503,
363
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
364
+ "Busabase authentication could not be verified"
365
+ );
366
+ }
367
+ }
368
+ if (!selectedSpace) {
369
+ return jsonError(
370
+ 409,
371
+ BUSABASE_AIRAPP_GATEWAY_REASONS.spaceSelectionRequired,
372
+ "Busabase Space selection required"
373
+ );
374
+ }
375
+ const incoming = new URL(request.url);
376
+ const targetUrl = new URL(incoming.pathname + incoming.search, target.baseUrl);
377
+ const headers = new Headers();
378
+ const contentType = request.headers.get("content-type");
379
+ const accept = request.headers.get("accept");
380
+ if (contentType) headers.set("content-type", contentType);
381
+ if (accept) headers.set("accept", accept);
382
+ headers.set("x-busabase-space", selectedSpace.id);
383
+ if (target.accessToken) headers.set("authorization", `Bearer ${target.accessToken}`);
384
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
385
+ let upstream;
386
+ try {
387
+ upstream = await this.#fetch()(targetUrl, {
388
+ method: request.method,
389
+ headers,
390
+ body: hasBody ? await request.arrayBuffer() : void 0,
391
+ redirect: "manual"
392
+ });
393
+ } catch {
394
+ return jsonError(
395
+ 503,
396
+ BUSABASE_AIRAPP_GATEWAY_REASONS.authUnavailable,
397
+ "Busabase API is temporarily unavailable"
398
+ );
399
+ }
400
+ const responseHeaders = new Headers();
401
+ const upstreamType = upstream.headers.get("content-type");
402
+ if (upstreamType) responseHeaders.set("content-type", upstreamType);
403
+ return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
404
+ };
405
+ };
406
+ var createBusabaseAirAppLocalGateway = (options) => new BusabaseAirAppLocalGateway(options);
407
+
408
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, BusabaseAirAppLocalGateway, createBusabaseAirAppLocalGateway };
@@ -0,0 +1,175 @@
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
+ if (item.selectedSpace !== void 0 && (typeof item.selectedSpace !== "object" || item.selectedSpace === null || typeof item.selectedSpace.id !== "string" || typeof item.selectedSpace.name !== "string")) {
43
+ throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
44
+ }
45
+ return item;
46
+ };
47
+ function loadBusabaseAirAppOAuthCredential(appId, options = {}) {
48
+ const path = busabaseAirAppCredentialPath(appId, options);
49
+ try {
50
+ return parseCredential(readFileSync(path, "utf8"), appId);
51
+ } catch (error) {
52
+ if (error.code === "ENOENT") return null;
53
+ throw error;
54
+ }
55
+ }
56
+ function storeBusabaseAirAppOAuthCredential(input, options = {}) {
57
+ if (!input.tokenSet.refreshToken) {
58
+ throw new BusabaseOAuthError(
59
+ "missing_refresh_token",
60
+ "A refresh token is required for a persistent local AirApp login"
61
+ );
62
+ }
63
+ const credential = {
64
+ version: STORE_VERSION,
65
+ appId: assertAppId(input.appId),
66
+ baseUrl: normalizeOrigin(input.baseUrl),
67
+ clientId: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID,
68
+ accessToken: input.tokenSet.accessToken,
69
+ refreshToken: input.tokenSet.refreshToken,
70
+ expiresAt: input.tokenSet.expiresAt,
71
+ scope: input.tokenSet.scope,
72
+ tokenType: input.tokenSet.tokenType,
73
+ ...input.selectedSpace ? { selectedSpace: input.selectedSpace } : {}
74
+ };
75
+ const path = busabaseAirAppCredentialPath(input.appId, options);
76
+ const directory = dirname(path);
77
+ mkdirSync(directory, { recursive: true, mode: 448 });
78
+ try {
79
+ chmodSync(directory, 448);
80
+ } catch {
81
+ }
82
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
83
+ writeFileSync(temporaryPath, `${JSON.stringify(credential, null, 2)}
84
+ `, { mode: 384 });
85
+ try {
86
+ chmodSync(temporaryPath, 384);
87
+ } catch {
88
+ }
89
+ renameSync(temporaryPath, path);
90
+ return credential;
91
+ }
92
+ async function getBusabaseAirAppAccessToken(appId, options = {}, fetchImpl = fetch) {
93
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
94
+ if (!credential) return null;
95
+ const expiresAt = Date.parse(credential.expiresAt);
96
+ if (Number.isFinite(expiresAt) && expiresAt > Date.now() + REFRESH_WINDOW_MS) return credential;
97
+ const credentialPath = busabaseAirAppCredentialPath(appId, options);
98
+ const activeRefresh = refreshesByCredentialPath.get(credentialPath);
99
+ if (activeRefresh) return activeRefresh;
100
+ const refresh = (async () => {
101
+ const tokenSet = await refreshBusabaseOAuthToken(
102
+ {
103
+ baseUrl: credential.baseUrl,
104
+ refreshToken: credential.refreshToken,
105
+ clientId: credential.clientId
106
+ },
107
+ fetchImpl
108
+ );
109
+ return storeBusabaseAirAppOAuthCredential(
110
+ {
111
+ appId,
112
+ baseUrl: credential.baseUrl,
113
+ clientId: credential.clientId,
114
+ tokenSet: {
115
+ ...tokenSet,
116
+ refreshToken: tokenSet.refreshToken ?? credential.refreshToken
117
+ },
118
+ selectedSpace: credential.selectedSpace
119
+ },
120
+ options
121
+ );
122
+ })();
123
+ refreshesByCredentialPath.set(credentialPath, refresh);
124
+ try {
125
+ return await refresh;
126
+ } finally {
127
+ if (refreshesByCredentialPath.get(credentialPath) === refresh) {
128
+ refreshesByCredentialPath.delete(credentialPath);
129
+ }
130
+ }
131
+ }
132
+ function storeBusabaseAirAppSelectedSpace(appId, selectedSpace, options = {}) {
133
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
134
+ if (!credential) {
135
+ throw new BusabaseOAuthError(
136
+ "missing_local_credential",
137
+ "Connect this local AirApp before selecting a Space"
138
+ );
139
+ }
140
+ const next = {
141
+ ...credential,
142
+ ...selectedSpace ? { selectedSpace } : { selectedSpace: void 0 }
143
+ };
144
+ const path = busabaseAirAppCredentialPath(appId, options);
145
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
146
+ writeFileSync(temporaryPath, `${JSON.stringify(next, null, 2)}
147
+ `, { mode: 384 });
148
+ try {
149
+ chmodSync(temporaryPath, 384);
150
+ } catch {
151
+ }
152
+ renameSync(temporaryPath, path);
153
+ return next;
154
+ }
155
+ function clearBusabaseAirAppOAuthCredential(appId, options = {}) {
156
+ rmSync(busabaseAirAppCredentialPath(appId, options), { force: true });
157
+ }
158
+ async function revokeBusabaseAirAppOAuthCredential(appId, options = {}, fetchImpl = fetch) {
159
+ const credential = loadBusabaseAirAppOAuthCredential(appId, options);
160
+ if (!credential) return;
161
+ try {
162
+ await revokeBusabaseOAuthToken(
163
+ {
164
+ baseUrl: credential.baseUrl,
165
+ token: credential.refreshToken,
166
+ clientId: credential.clientId
167
+ },
168
+ fetchImpl
169
+ );
170
+ } finally {
171
+ clearBusabaseAirAppOAuthCredential(appId, options);
172
+ }
173
+ }
174
+
175
+ export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
package/dist/index.d.ts CHANGED
@@ -285,7 +285,7 @@ type OperationKind = GenericOperationKind | RegisteredOperationKind;
285
285
  * of truth). `schema/` is reserved for DB (PO); DTO/VO live here.
286
286
  *
287
287
  * Kernel-generic: `metadata` is an open record (apps layer their own typed
288
- * metadata on top — e.g. `apps/busabase-cloud`'s `AttachmentType` enum).
288
+ * metadata on top — e.g. Busabase Cloud's `AttachmentType` enum).
289
289
  */
290
290
 
291
291
  /**
@@ -662,6 +662,102 @@ declare const FormVOSchema: z.ZodObject<{
662
662
  updatedAt: z.ZodString;
663
663
  }, z.core.$strip>;
664
664
  type FormVO = z.infer<typeof FormVOSchema>;
665
+ declare const ListFormsInputSchema: z.ZodObject<{
666
+ targetBaseId: z.ZodString;
667
+ limit: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
668
+ cursor: z.ZodOptional<z.ZodString>;
669
+ }, z.core.$strip>;
670
+ type ListFormsDTO = z.infer<typeof ListFormsInputSchema>;
671
+ declare const ListFormsVOSchema: z.ZodObject<{
672
+ forms: z.ZodArray<z.ZodObject<{
673
+ id: z.ZodString;
674
+ nodeId: z.ZodString;
675
+ spaceId: z.ZodString;
676
+ targetBaseId: z.ZodString;
677
+ name: z.ZodString;
678
+ description: z.ZodString;
679
+ bindings: z.ZodArray<z.ZodObject<{
680
+ inputName: z.ZodString;
681
+ fieldSlug: z.ZodString;
682
+ required: z.ZodOptional<z.ZodBoolean>;
683
+ label: z.ZodOptional<z.ZodString>;
684
+ help: z.ZodOptional<z.ZodString>;
685
+ }, z.core.$strip>>;
686
+ boundFields: z.ZodDefault<z.ZodArray<z.ZodObject<{
687
+ slug: z.ZodString;
688
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodEnum<{
689
+ en: "en";
690
+ "zh-CN": "zh-CN";
691
+ "zh-TW": "zh-TW";
692
+ ja: "ja";
693
+ ko: "ko";
694
+ de: "de";
695
+ fr: "fr";
696
+ es: "es";
697
+ pt: "pt";
698
+ }> & z.core.$partial, z.ZodString>]>;
699
+ type: z.ZodEnum<{
700
+ number: "number";
701
+ email: "email";
702
+ date: "date";
703
+ text: "text";
704
+ whiteboard: "whiteboard";
705
+ html: "html";
706
+ longtext: "longtext";
707
+ markdown: "markdown";
708
+ attachment: "attachment";
709
+ relation: "relation";
710
+ checkbox: "checkbox";
711
+ select: "select";
712
+ multiselect: "multiselect";
713
+ url: "url";
714
+ embed: "embed";
715
+ phone: "phone";
716
+ created_time: "created_time";
717
+ updated_time: "updated_time";
718
+ created_by: "created_by";
719
+ updated_by: "updated_by";
720
+ auto_number: "auto_number";
721
+ ai_summary: "ai_summary";
722
+ ai_tags: "ai_tags";
723
+ code: "code";
724
+ json: "json";
725
+ yaml: "yaml";
726
+ formula: "formula";
727
+ lookup: "lookup";
728
+ }>;
729
+ choices: z.ZodDefault<z.ZodArray<z.ZodObject<{
730
+ id: z.ZodString;
731
+ name: z.ZodString;
732
+ }, z.core.$strip>>>;
733
+ }, z.core.$strip>>>;
734
+ page: z.ZodObject<{
735
+ code: z.ZodOptional<z.ZodString>;
736
+ theme: z.ZodOptional<z.ZodObject<{
737
+ logoUrl: z.ZodOptional<z.ZodString>;
738
+ coverUrl: z.ZodOptional<z.ZodString>;
739
+ brandColor: z.ZodOptional<z.ZodString>;
740
+ hideBranding: z.ZodOptional<z.ZodBoolean>;
741
+ }, z.core.$strip>>;
742
+ }, z.core.$strip>;
743
+ share: z.ZodObject<{
744
+ isPublic: z.ZodDefault<z.ZodBoolean>;
745
+ anonymousSubmit: z.ZodDefault<z.ZodBoolean>;
746
+ submitLimit: z.ZodOptional<z.ZodNumber>;
747
+ requireCaptcha: z.ZodOptional<z.ZodBoolean>;
748
+ }, z.core.$strip>;
749
+ submissionCount: z.ZodDefault<z.ZodNumber>;
750
+ status: z.ZodEnum<{
751
+ active: "active";
752
+ archived: "archived";
753
+ }>;
754
+ createdBy: z.ZodString;
755
+ createdAt: z.ZodString;
756
+ updatedAt: z.ZodString;
757
+ }, z.core.$strip>>;
758
+ nextCursor: z.ZodNullable<z.ZodString>;
759
+ }, z.core.$strip>;
760
+ type ListFormsVO = z.infer<typeof ListFormsVOSchema>;
665
761
  declare const CreateFormInputSchema: z.ZodObject<{
666
762
  nodeId: z.ZodString;
667
763
  targetBaseId: z.ZodString;
@@ -2529,8 +2625,7 @@ interface UserRefVO {
2529
2625
  role?: string | null;
2530
2626
  }
2531
2627
  /**
2532
- * Cheap, name/slug-only match from `nodes.searchByName` (see
2533
- * `apps/busabase/content/spec/search-quick-jump.md`) — deliberately a much
2628
+ * Cheap, name/slug-only match from `nodes.searchByName` — deliberately a much
2534
2629
  * smaller projection than `NodeVO` (no `description`/`metadata`/tree shape):
2535
2630
  * this backs the dashboard's instant quick-jump palette, not the sidebar tree
2536
2631
  * or a node's own detail view. `path` is the route this node navigates to
@@ -2729,7 +2824,7 @@ interface AuditEventVO {
2729
2824
 
2730
2825
  /**
2731
2826
  * Unified Grep (P2a files+docs, P2b records) — top-level, domain-agnostic
2732
- * schemas for `POST /grep`. See apps/busabase/content/spec/unified-grep.md.
2827
+ * schemas for `POST /grep`.
2733
2828
  *
2734
2829
  * Mirrors how `search`'s schemas live top-level in `contract/schemas.ts`
2735
2830
  * rather than inside a single domain's contract: `grep` composes multiple
@@ -12070,6 +12165,99 @@ declare const cloudContract: {
12070
12165
  }, z.core.$strip>, _orpc_contract.MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
12071
12166
  };
12072
12167
  forms: {
12168
+ list: _orpc_contract.ContractProcedure<z.ZodObject<{
12169
+ targetBaseId: z.ZodString;
12170
+ limit: z.ZodDefault<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
12171
+ cursor: z.ZodOptional<z.ZodString>;
12172
+ }, z.core.$strip>, z.ZodObject<{
12173
+ forms: z.ZodArray<z.ZodObject<{
12174
+ id: z.ZodString;
12175
+ nodeId: z.ZodString;
12176
+ spaceId: z.ZodString;
12177
+ targetBaseId: z.ZodString;
12178
+ name: z.ZodString;
12179
+ description: z.ZodString;
12180
+ bindings: z.ZodArray<z.ZodObject<{
12181
+ inputName: z.ZodString;
12182
+ fieldSlug: z.ZodString;
12183
+ required: z.ZodOptional<z.ZodBoolean>;
12184
+ label: z.ZodOptional<z.ZodString>;
12185
+ help: z.ZodOptional<z.ZodString>;
12186
+ }, z.core.$strip>>;
12187
+ boundFields: z.ZodDefault<z.ZodArray<z.ZodObject<{
12188
+ slug: z.ZodString;
12189
+ name: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodEnum<{
12190
+ en: "en";
12191
+ "zh-CN": "zh-CN";
12192
+ "zh-TW": "zh-TW";
12193
+ ja: "ja";
12194
+ ko: "ko";
12195
+ de: "de";
12196
+ fr: "fr";
12197
+ es: "es";
12198
+ pt: "pt";
12199
+ }> & z.core.$partial, z.ZodString>]>;
12200
+ type: z.ZodEnum<{
12201
+ number: "number";
12202
+ email: "email";
12203
+ date: "date";
12204
+ text: "text";
12205
+ whiteboard: "whiteboard";
12206
+ html: "html";
12207
+ longtext: "longtext";
12208
+ markdown: "markdown";
12209
+ attachment: "attachment";
12210
+ relation: "relation";
12211
+ checkbox: "checkbox";
12212
+ select: "select";
12213
+ multiselect: "multiselect";
12214
+ url: "url";
12215
+ embed: "embed";
12216
+ phone: "phone";
12217
+ created_time: "created_time";
12218
+ updated_time: "updated_time";
12219
+ created_by: "created_by";
12220
+ updated_by: "updated_by";
12221
+ auto_number: "auto_number";
12222
+ ai_summary: "ai_summary";
12223
+ ai_tags: "ai_tags";
12224
+ code: "code";
12225
+ json: "json";
12226
+ yaml: "yaml";
12227
+ formula: "formula";
12228
+ lookup: "lookup";
12229
+ }>;
12230
+ choices: z.ZodDefault<z.ZodArray<z.ZodObject<{
12231
+ id: z.ZodString;
12232
+ name: z.ZodString;
12233
+ }, z.core.$strip>>>;
12234
+ }, z.core.$strip>>>;
12235
+ page: z.ZodObject<{
12236
+ code: z.ZodOptional<z.ZodString>;
12237
+ theme: z.ZodOptional<z.ZodObject<{
12238
+ logoUrl: z.ZodOptional<z.ZodString>;
12239
+ coverUrl: z.ZodOptional<z.ZodString>;
12240
+ brandColor: z.ZodOptional<z.ZodString>;
12241
+ hideBranding: z.ZodOptional<z.ZodBoolean>;
12242
+ }, z.core.$strip>>;
12243
+ }, z.core.$strip>;
12244
+ share: z.ZodObject<{
12245
+ isPublic: z.ZodDefault<z.ZodBoolean>;
12246
+ anonymousSubmit: z.ZodDefault<z.ZodBoolean>;
12247
+ submitLimit: z.ZodOptional<z.ZodNumber>;
12248
+ requireCaptcha: z.ZodOptional<z.ZodBoolean>;
12249
+ }, z.core.$strip>;
12250
+ submissionCount: z.ZodDefault<z.ZodNumber>;
12251
+ status: z.ZodEnum<{
12252
+ active: "active";
12253
+ archived: "archived";
12254
+ }>;
12255
+ createdBy: z.ZodString;
12256
+ createdAt: z.ZodString;
12257
+ updatedAt: z.ZodString;
12258
+ }, z.core.$strip>>;
12259
+ nextCursor: z.ZodNullable<z.ZodString>;
12260
+ }, z.core.$strip>, _orpc_contract.MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
12073
12261
  getByNode: _orpc_contract.ContractProcedure<z.ZodObject<{
12074
12262
  nodeId: z.ZodString;
12075
12263
  }, z.core.$strip>, z.ZodObject<{
@@ -19653,4 +19841,4 @@ declare class Busabase {
19653
19841
  }>>>>;
19654
19842
  }
19655
19843
 
19656
- export { type ActivityItemVO, type AgentTaskVO, type FileTreeFileVO as AirAppFileVO, type FileTreeReadFileVO as AirAppReadFileVO, type FileTreeNodeVO as AirAppVO, type AssetAttachmentRef, type AssetDetailVO, type AssetTextStatus, type AssetUsageVO, type AssetVO, type AttachmentRef, type AuditAction, type AuditEventVO, type BaseFieldVO, type BaseVO, Busabase, type BusabaseAssetsClient, type BusabaseChangeRequestsClient, type BusabaseClient, type BusabaseConfig, type BusabaseRecordsClient, CREATABLE_NODE_TYPES, type ChangeRequestBatchFailureVO, type ChangeRequestCountsVO, type ChangeRequestMergeBatchResultVO, type ChangeRequestReviewBatchResultVO, type ChangeRequestStatus, type ChangeRequestTargetType, type ChangeRequestVO, type CloudContract, type CommentSubjectType, type CommentVO, type CommitVO, type CreatableNodeType, type CreateFormDTO, DEFAULT_BASE_URL, type FileTreeFileVO as DriveFileVO, type FileTreeReadFileVO as DriveReadFileVO, type FileTreeNodeVO as DriveVO, type FieldType, type FileNodeMetadata, type FileNodeVO, type FileTreeFileVO, type FileTreeNodeVO, type FileTreeReadFileVO, type FormBoundFieldVO, type FormFieldBindingVO, type FormPageSourceVO, type FormShareVO, type FormSubmitResultVO, type FormThemeVO, type FormVO, type GalleryCardSize, type GalleryCoverFit, type GanttScale, type LookupRollup, type NodeDetailVO, type NodeSearchResultVO, type NodeType, type NodeVO, type OperationKind, type OperationStatus, type OperationVO, type RecordByFieldInput, type RecordLinkVO, type RecordVO, type ResolvedConfig, type ReviewVO, type ReviewVerdict, type SearchResponseVO, type SearchResultKind, type SearchResultVO, type FileTreeFileVO as SkillFileVO, type FileTreeReadFileVO as SkillReadFileVO, type FileTreeNodeVO as SkillVO, type SubmitFormDTO, type UpdateFormDTO, type UpdateVaultSettingsDTO, type UserRefVO, VIEW_FIELD_MAX_WIDTH, VIEW_FIELD_MIN_WIDTH, type VaultAccessPolicy, type VaultEnvironment, type VaultItemInput, type VaultItemKind, type VaultItemVO, type VaultRuntimeEnv, type VaultScopeType, type VaultSettingsVO, type ViewConfigVO, type ViewFilterOperator, type ViewFilterVO, type ViewSortVO, type ViewType, type ViewVO, cloudContract, createBusabaseClient, getRecordByField, grepAssets, normalizeBaseUrl, resolveConfig, toFilesOnlyGrepResult, toUnifiedFilesGrepInput };
19844
+ export { type ActivityItemVO, type AgentTaskVO, type FileTreeFileVO as AirAppFileVO, type FileTreeReadFileVO as AirAppReadFileVO, type FileTreeNodeVO as AirAppVO, type AssetAttachmentRef, type AssetDetailVO, type AssetTextStatus, type AssetUsageVO, type AssetVO, type AttachmentRef, type AuditAction, type AuditEventVO, type BaseFieldVO, type BaseVO, Busabase, type BusabaseAssetsClient, type BusabaseChangeRequestsClient, type BusabaseClient, type BusabaseConfig, type BusabaseRecordsClient, CREATABLE_NODE_TYPES, type ChangeRequestBatchFailureVO, type ChangeRequestCountsVO, type ChangeRequestMergeBatchResultVO, type ChangeRequestReviewBatchResultVO, type ChangeRequestStatus, type ChangeRequestTargetType, type ChangeRequestVO, type CloudContract, type CommentSubjectType, type CommentVO, type CommitVO, type CreatableNodeType, type CreateFormDTO, DEFAULT_BASE_URL, type FileTreeFileVO as DriveFileVO, type FileTreeReadFileVO as DriveReadFileVO, type FileTreeNodeVO as DriveVO, type FieldType, type FileNodeMetadata, type FileNodeVO, type FileTreeFileVO, type FileTreeNodeVO, type FileTreeReadFileVO, type FormBoundFieldVO, type FormFieldBindingVO, type FormPageSourceVO, type FormShareVO, type FormSubmitResultVO, type FormThemeVO, type FormVO, type GalleryCardSize, type GalleryCoverFit, type GanttScale, type ListFormsDTO, type ListFormsVO, type LookupRollup, type NodeDetailVO, type NodeSearchResultVO, type NodeType, type NodeVO, type OperationKind, type OperationStatus, type OperationVO, type RecordByFieldInput, type RecordLinkVO, type RecordVO, type ResolvedConfig, type ReviewVO, type ReviewVerdict, type SearchResponseVO, type SearchResultKind, type SearchResultVO, type FileTreeFileVO as SkillFileVO, type FileTreeReadFileVO as SkillReadFileVO, type FileTreeNodeVO as SkillVO, type SubmitFormDTO, type UpdateFormDTO, type UpdateVaultSettingsDTO, type UserRefVO, VIEW_FIELD_MAX_WIDTH, VIEW_FIELD_MIN_WIDTH, type VaultAccessPolicy, type VaultEnvironment, type VaultItemInput, type VaultItemKind, type VaultItemVO, type VaultRuntimeEnv, type VaultScopeType, type VaultSettingsVO, type ViewConfigVO, type ViewFilterOperator, type ViewFilterVO, type ViewSortVO, type ViewType, type ViewVO, cloudContract, createBusabaseClient, getRecordByField, grepAssets, normalizeBaseUrl, resolveConfig, toFilesOnlyGrepResult, toUnifiedFilesGrepInput };
package/dist/index.js CHANGED
@@ -2328,6 +2328,16 @@ var FormVOSchema = z.object({
2328
2328
  createdAt: z.string(),
2329
2329
  updatedAt: z.string()
2330
2330
  });
2331
+ var ListFormsInputSchema = z.object({
2332
+ targetBaseId: z.string().min(1),
2333
+ limit: z.coerce.number().int().min(1).max(100).optional().default(50),
2334
+ /** Opaque createdAt/id keyset cursor. */
2335
+ cursor: z.string().optional()
2336
+ });
2337
+ var ListFormsVOSchema = z.object({
2338
+ forms: z.array(FormVOSchema),
2339
+ nextCursor: z.string().nullable()
2340
+ });
2331
2341
  var CreateFormInputSchema = z.object({
2332
2342
  nodeId: z.string().min(1),
2333
2343
  targetBaseId: z.string().min(1),
@@ -2355,6 +2365,13 @@ var FormSubmitResultSchema = z.object({
2355
2365
 
2356
2366
  // ../../packages/busabase-contract/src/domains/form/contract.ts
2357
2367
  var formContract = {
2368
+ list: oc.route({
2369
+ method: "GET",
2370
+ path: "/forms",
2371
+ tags: ["Forms"],
2372
+ summary: "List forms bound to a Base",
2373
+ successDescription: "A newest-first page of forms with a stable opaque cursor (null at the end)."
2374
+ }).input(ListFormsInputSchema).output(ListFormsVOSchema),
2358
2375
  getByNode: oc.route({
2359
2376
  method: "GET",
2360
2377
  path: "/forms/{nodeId}",
@@ -3041,7 +3058,7 @@ var busabaseContractRoutes = {
3041
3058
  path: "/nodes/search",
3042
3059
  tags: ["Nodes", "Search"],
3043
3060
  summary: "Search nodes by name/slug (cheap, name-only quick-jump)",
3044
- successDescription: "Plain ilike match on name/slug across every registered node type, scoped by the same node-visibility ACL as `nodes.list`. No content scan and no full-text ranking \u2014 ordered exact-slug-match first, then by name. Backs the dashboard search dialog's 'Recent' tab cache-miss path (see apps/busabase/content/spec/search-quick-jump.md); the heavier `search` endpoint remains the dedicated full-text content search."
3061
+ successDescription: "Plain ilike match on name/slug across every registered node type, scoped by the same node-visibility ACL as `nodes.list`. No content scan and no full-text ranking \u2014 ordered exact-slug-match first, then by name. Backs the dashboard search dialog's 'Recent' tab cache-miss path; the heavier `search` endpoint remains the dedicated full-text content search."
3045
3062
  }).input(searchNodesByNameInputSchema).output(z.array(nodeSearchResultSchema)),
3046
3063
  isDescendant: oc.route({
3047
3064
  method: "GET",
@@ -3556,9 +3573,9 @@ var cloudExtraRoutes = {
3556
3573
  })
3557
3574
  ).output(AgentTaskDetailSchema)
3558
3575
  },
3559
- // Relative-path twin of `embedLinksContract`
3560
- // (apps/busabase-cloud/src/domains/embed-links/contract.ts, served at the absolute
3561
- // `/api/v1/embed-links` paths) — same schemas imported from `./embed-link-schemas`, just
3576
+ // Relative-path twin of the `embedLinksContract` the Busabase Cloud host
3577
+ // serves at the absolute `/api/v1/embed-links` paths same schemas
3578
+ // imported from `./embed-link-schemas`, just
3562
3579
  // routed relative here so the shared `/api/v1` prefix below lands on the identical real path.
3563
3580
  embedLinks: {
3564
3581
  create: oc.route({
@@ -10,6 +10,11 @@ interface BusabaseAirAppOAuthCredential {
10
10
  expiresAt: string;
11
11
  scope: string[];
12
12
  tokenType: string;
13
+ /** Validated target for this local AirApp. Tokens remain user-scoped. */
14
+ selectedSpace?: {
15
+ id: string;
16
+ name: string;
17
+ };
13
18
  }
14
19
  interface BusabaseAirAppCredentialStoreOptions {
15
20
  /** Override only for tests or an explicitly isolated installation. */
@@ -24,10 +29,16 @@ declare function storeBusabaseAirAppOAuthCredential(input: {
24
29
  baseUrl: string;
25
30
  tokenSet: BusabaseOAuthTokenSet;
26
31
  clientId?: string;
32
+ selectedSpace?: BusabaseAirAppOAuthCredential["selectedSpace"];
27
33
  }, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
28
34
  /** Load a valid access token, rotating and persisting the token set when needed. */
29
35
  declare function getBusabaseAirAppAccessToken(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<BusabaseAirAppOAuthCredential | null>;
36
+ /** Persist a Space only after the caller has verified membership through `/api/v1/auth`. */
37
+ declare function storeBusabaseAirAppSelectedSpace(appId: string, selectedSpace: {
38
+ id: string;
39
+ name: string;
40
+ } | null, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
30
41
  declare function clearBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): void;
31
42
  declare function revokeBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<void>;
32
43
 
33
- export { type BusabaseAirAppCredentialStoreOptions, type BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential };
44
+ export { type BusabaseAirAppCredentialStoreOptions, type BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace };
@@ -1,147 +1,3 @@
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 };
1
+ export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential, storeBusabaseAirAppSelectedSpace } from './chunk-B2AWPFDI.js';
2
+ import './chunk-J2DZKX7A.js';
3
+ import './chunk-5NYQX65A.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -30,6 +30,10 @@
30
30
  "./oauth-node": {
31
31
  "types": "./dist/oauth-node.d.ts",
32
32
  "default": "./dist/oauth-node.js"
33
+ },
34
+ "./airapp-node": {
35
+ "types": "./dist/airapp-node.d.ts",
36
+ "default": "./dist/airapp-node.js"
33
37
  }
34
38
  },
35
39
  "files": [
@@ -49,9 +53,9 @@
49
53
  "tsx": "^4.20.5",
50
54
  "typescript": "^5.9.3",
51
55
  "vitest": "^2.1.8",
52
- "busabase-contract": "0.14.0",
53
- "openlib": "0.1.1",
54
- "open-domains": "0.0.2"
56
+ "open-domains": "0.0.2",
57
+ "busabase-contract": "0.15.0",
58
+ "openlib": "0.1.1"
55
59
  },
56
60
  "engines": {
57
61
  "node": ">=24.18.0"