busabase-sdk 0.14.1 → 0.16.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,238 @@
1
+ // src/airapp-gate.ts
2
+ function selectAirAppGateScreen(status) {
3
+ if (!status) return "connect";
4
+ switch (status.readiness) {
5
+ case "needs_connection":
6
+ case "needs_auth":
7
+ return "connect";
8
+ case "needs_space":
9
+ return "space";
10
+ case "ready":
11
+ return "ready";
12
+ }
13
+ if (!status.connected) return "connect";
14
+ return status.requiresSpace ? "space" : "ready";
15
+ }
16
+ function describeAirAppSetupError(error) {
17
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "";
18
+ const raw = String(
19
+ (typeof error === "object" && error !== null && "message" in error ? error.message : error) ?? "SETUP_REQUIRED"
20
+ );
21
+ const parsed = /^([A-Z_]+):\s*(.*)$/s.exec(raw);
22
+ const resolvedCode = code || parsed?.[1] || raw.trim() || "SETUP_REQUIRED";
23
+ const detail = parsed ? parsed[2] : code ? raw : "";
24
+ const pending = resolvedCode === "SETUP_PENDING";
25
+ const canProvision = resolvedCode === "SETUP_REQUIRED";
26
+ return {
27
+ code: resolvedCode,
28
+ detail,
29
+ title: pending ? "Waiting for workspace approval" : canProvision ? "Initialize the Busabase workspace" : "Workspace not ready",
30
+ canProvision,
31
+ canRetry: resolvedCode === "SETUP_PENDING" || resolvedCode === "SCHEMA_INCOMPLETE"
32
+ };
33
+ }
34
+ var escapeHtml = (value) => String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
35
+ var panel = (labelledBy, head, body, footer) => `<div class="bb-gate-overlay"><section class="bb-gate-panel" role="dialog" aria-modal="true" aria-labelledby="${labelledBy}"><div class="bb-gate-head"><div>${head}</div></div><div class="bb-gate-body">${body}</div><div class="bb-gate-footer">${footer}</div></section></div>`;
36
+ var defaultAirAppGateRenderer = {
37
+ connect(view) {
38
+ const head = `<h1 id="bbGateConnectTitle">Connect Busabase</h1><p>${escapeHtml(view.appName)} reads and writes through your Busabase workspace.</p>`;
39
+ const body = (view.oauthError ? `<p class="bb-gate-error" role="alert">${escapeHtml(view.oauthError)}</p>` : "") + (view.reconnect ? `<p class="bb-gate-note">Your session expired. Reconnect to continue.</p>` : "") + `<h2>Server</h2><div class="bb-gate-server-grid"><label class="bb-gate-server-card is-selected"><input type="radio" name="server_mode" value="cloud" checked><span><strong>Busabase Cloud</strong><span>${escapeHtml(hostOf(view.cloudBaseUrl))}</span></span></label><label class="bb-gate-server-card"><input type="radio" name="server_mode" value="custom"><span><strong>Custom server</strong><span>Self-hosted or enterprise address</span></span></label></div><label class="bb-gate-custom-url" data-custom-url hidden><span>Busabase URL</span><input type="url" name="custom_base_url" inputmode="url" placeholder="https://busabase.example.com" autocomplete="url"></label><input type="hidden" name="base_url" value="${escapeHtml(view.cloudBaseUrl)}">`;
40
+ const footer = `<span class="bb-gate-note">OAuth credentials stay on this machine (~/.busabase/airapps)</span><button class="bb-gate-primary" type="submit">Connect Busabase</button>`;
41
+ return `<form method="post" action="${escapeHtml(`${view.authBasePath}/auth/start`)}" data-connect-form>${panel("bbGateConnectTitle", head, body, footer)}</form>`;
42
+ },
43
+ space(view) {
44
+ const options = view.spaces.map(
45
+ (space) => `<option value="${escapeHtml(space.id)}">${escapeHtml(space.name)} \xB7 ${escapeHtml(space.id)}</option>`
46
+ ).join("");
47
+ const head = `<h1 id="bbGateSpaceTitle">Choose a Busabase Space</h1><p>Signed in to <strong>${escapeHtml(view.baseUrl)}</strong>. Choose where ${escapeHtml(view.appName)}'s data lives.</p>`;
48
+ const body = `<label class="bb-gate-space-select"><span>Space</span><select name="space_id" required>${options}</select></label><p class="bb-gate-error" data-space-error hidden></p>`;
49
+ const footer = `<span class="bb-gate-note">Resources are only checked after you confirm</span><button class="bb-gate-primary" type="submit">Use this Space</button>`;
50
+ return `<form data-space-form>${panel("bbGateSpaceTitle", head, body, footer)}</form>`;
51
+ },
52
+ workspace(view) {
53
+ const head = `<h1 id="bbGateWorkspaceTitle">${escapeHtml(view.title)}</h1>`;
54
+ const body = view.canProvision ? `<p>${escapeHtml(view.appName)} will create its Folder and Bases in the current Space.</p><p>Submitted as one idempotent Busabase ChangeRequest; nothing existing is deleted or repurposed.</p><p class="bb-gate-error" data-workspace-status hidden></p>` : `<p>${escapeHtml(view.detail)}</p><p>${escapeHtml(view.appName)} never asks you to create Nodes or Bases by hand, and never silently falls back to local data.</p><p class="bb-gate-error" data-workspace-status hidden></p>`;
55
+ const footer = (view.demoHref ? `<a class="bb-gate-link" href="${escapeHtml(view.demoHref)}">Open the read-only demo</a>` : "<span></span>") + (view.canProvision ? `<button class="bb-gate-primary" type="button" data-provision>Initialize workspace</button>` : view.canRetry ? `<button class="bb-gate-primary" type="button" data-retry>Check again</button>` : "");
56
+ return panel("bbGateWorkspaceTitle", head, body, footer);
57
+ }
58
+ };
59
+ var hostOf = (baseUrl) => {
60
+ try {
61
+ return new URL(baseUrl).host;
62
+ } catch {
63
+ return baseUrl;
64
+ }
65
+ };
66
+ var DEFAULT_CLOUD_BASE_URL = "https://busabase.com";
67
+ function createAirAppConnectGate(options) {
68
+ const {
69
+ appName,
70
+ authBasePath = "",
71
+ onProvision,
72
+ demoHref = null,
73
+ render = defaultAirAppGateRenderer
74
+ } = options;
75
+ const doFetch = options.fetch ?? globalThis.fetch;
76
+ const root = () => {
77
+ if (options.mount) {
78
+ const element2 = typeof options.mount === "string" ? document.querySelector(options.mount) : options.mount;
79
+ if (!element2) throw new Error(`AirApp gate mount not found: ${String(options.mount)}`);
80
+ document.documentElement.classList.add("bb-gate-active");
81
+ return element2;
82
+ }
83
+ let element = document.querySelector("#busabaseAirAppGate");
84
+ if (!element) {
85
+ element = document.createElement("div");
86
+ element.id = "busabaseAirAppGate";
87
+ document.body.prepend(element);
88
+ }
89
+ document.documentElement.classList.add("bb-gate-active");
90
+ return element;
91
+ };
92
+ const close = () => {
93
+ if (options.mount) {
94
+ const element = typeof options.mount === "string" ? document.querySelector(options.mount) : options.mount;
95
+ if (element) element.innerHTML = "";
96
+ } else {
97
+ document.querySelector("#busabaseAirAppGate")?.remove();
98
+ }
99
+ document.documentElement.classList.remove("bb-gate-active");
100
+ };
101
+ const status = async () => {
102
+ try {
103
+ const response = await doFetch(`${authBasePath}/auth/status`, {
104
+ headers: { accept: "application/json" }
105
+ });
106
+ const type = response.headers.get("content-type") ?? "";
107
+ if (!response.ok || !type.includes("application/json")) return null;
108
+ return await response.json();
109
+ } catch {
110
+ return null;
111
+ }
112
+ };
113
+ const renderConnect = (current) => {
114
+ const element = root();
115
+ const oauthError = new URLSearchParams(window.location.search).get("oauth_error") ?? "";
116
+ element.innerHTML = render.connect({
117
+ appName,
118
+ cloudBaseUrl: current?.cloudBaseUrl || DEFAULT_CLOUD_BASE_URL,
119
+ reconnect: current?.readiness === "needs_auth",
120
+ oauthError,
121
+ authBasePath
122
+ });
123
+ wireConnect(element, current?.cloudBaseUrl || DEFAULT_CLOUD_BASE_URL);
124
+ };
125
+ const renderSpace = (current, onReady) => {
126
+ const element = root();
127
+ element.innerHTML = render.space({
128
+ appName,
129
+ baseUrl: current.baseUrl ?? "",
130
+ spaces: current.spaces ?? []
131
+ });
132
+ wireSpace(element, onReady);
133
+ };
134
+ const wireConnect = (element, cloudBaseUrl) => {
135
+ const form = element.querySelector("[data-connect-form]");
136
+ if (!form) return;
137
+ const customField = form.querySelector("[data-custom-url]");
138
+ const customInput = customField?.querySelector("input") ?? null;
139
+ const hiddenBaseUrl = form.querySelector('input[name="base_url"]');
140
+ for (const radio of form.querySelectorAll('input[name="server_mode"]')) {
141
+ radio.addEventListener("change", () => {
142
+ const custom = radio.value === "custom";
143
+ for (const card of form.querySelectorAll(".bb-gate-server-card")) {
144
+ card.classList.toggle("is-selected", card.querySelector("input")?.checked === true);
145
+ }
146
+ if (customField) customField.hidden = !custom;
147
+ if (customInput) customInput.required = custom;
148
+ if (hiddenBaseUrl) hiddenBaseUrl.value = custom ? customInput?.value ?? "" : cloudBaseUrl;
149
+ if (custom) customInput?.focus();
150
+ });
151
+ }
152
+ customInput?.addEventListener("input", () => {
153
+ if (hiddenBaseUrl) hiddenBaseUrl.value = customInput.value;
154
+ });
155
+ };
156
+ const wireSpace = (element, onReady) => {
157
+ const form = element.querySelector("[data-space-form]");
158
+ form?.addEventListener("submit", async (event) => {
159
+ event.preventDefault();
160
+ const button = form.querySelector("button[type=submit]");
161
+ const error = form.querySelector("[data-space-error]");
162
+ if (button) button.disabled = true;
163
+ if (error) error.hidden = true;
164
+ try {
165
+ const response = await doFetch(`${authBasePath}/auth/space`, {
166
+ method: "POST",
167
+ headers: { "content-type": "application/x-www-form-urlencoded" },
168
+ body: new URLSearchParams(new FormData(form))
169
+ });
170
+ const result = await response.json();
171
+ if (!response.ok) {
172
+ if (error) {
173
+ error.textContent = result.error || "Could not select a Space.";
174
+ error.hidden = false;
175
+ }
176
+ if (button) button.disabled = false;
177
+ return;
178
+ }
179
+ onReady();
180
+ } catch {
181
+ if (error) {
182
+ error.textContent = "Could not reach this app's server.";
183
+ error.hidden = false;
184
+ }
185
+ if (button) button.disabled = false;
186
+ }
187
+ });
188
+ };
189
+ const renderSetupRequired = (error, onRetry) => {
190
+ const element = root();
191
+ element.innerHTML = render.workspace({
192
+ ...describeAirAppSetupError(error),
193
+ appName,
194
+ demoHref
195
+ });
196
+ element.querySelector("[data-retry]")?.addEventListener("click", () => onRetry());
197
+ element.querySelector("[data-provision]")?.addEventListener("click", async (event) => {
198
+ const button = event.currentTarget;
199
+ const line = element.querySelector("[data-workspace-status]");
200
+ button.disabled = true;
201
+ if (line) {
202
+ line.hidden = false;
203
+ line.textContent = "Submitting the workspace structure\u2026";
204
+ }
205
+ try {
206
+ await onProvision?.();
207
+ onRetry();
208
+ } catch (provisionError) {
209
+ renderSetupRequired(provisionError, onRetry);
210
+ }
211
+ });
212
+ };
213
+ const pass = async ({ onReady } = {}) => {
214
+ if (options.shouldGate && !await options.shouldGate()) {
215
+ close();
216
+ return true;
217
+ }
218
+ const current = await status();
219
+ if (!current) return true;
220
+ const screen = selectAirAppGateScreen(current);
221
+ if (screen === "ready") {
222
+ close();
223
+ return true;
224
+ }
225
+ if (screen === "space") {
226
+ renderSpace(current, () => {
227
+ close();
228
+ onReady?.();
229
+ });
230
+ return false;
231
+ }
232
+ renderConnect(current);
233
+ return false;
234
+ };
235
+ return { pass, renderSetupRequired, close, status };
236
+ }
237
+
238
+ export { DEFAULT_CLOUD_BASE_URL, createAirAppConnectGate, defaultAirAppGateRenderer, describeAirAppSetupError, escapeHtml, selectAirAppGateScreen };
@@ -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 };