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.
- package/README.md +77 -0
- package/dist/airapp-gate.css +296 -0
- package/dist/airapp-gate.d.ts +157 -0
- package/dist/airapp-gate.js +238 -0
- package/dist/airapp-node.d.ts +66 -0
- package/dist/airapp-node.js +439 -0
- package/dist/airapp.d.ts +222 -0
- package/dist/airapp.js +359 -0
- package/dist/chunk-C23JVY2Y.js +211 -0
- package/dist/{chunk-J2DZKX7A.js → chunk-WSHJMHUS.js} +42 -2
- package/dist/client-DB7fREZX.d.ts +17112 -0
- package/dist/index.d.ts +4 -16983
- package/dist/index.js +104 -0
- package/dist/oauth-node.d.ts +32 -1
- package/dist/oauth-node.js +3 -147
- package/dist/oauth.d.ts +13 -1
- package/dist/oauth.js +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -70,6 +70,83 @@ storeBusabaseAirAppOAuthCredential({
|
|
|
70
70
|
|
|
71
71
|
The Node-only helper writes `~/.busabase/airapps/<app-id>.json` with an owner-only directory and file (`0700`/`0600`). Use `getBusabaseAirAppAccessToken()` inside Hono when proxying `/api/v1`; it refreshes and persists the token set when needed. Use `revokeBusabaseAirAppOAuthCredential()` on logout. The browser must never receive the `bso_` access token, `bsr_` refresh token, or PKCE verifier through JavaScript-visible storage. This local AirApp registration is separate from the CLI's active `~/.busabase/.env` profile.
|
|
72
72
|
|
|
73
|
+
## Building an AirApp
|
|
74
|
+
|
|
75
|
+
Three entry points cover what every AirApp needs before it can show its first screen. Use them instead of reimplementing any part — the rules they encode (which server, whose Space, whose Folder) are a security boundary, not app preferences.
|
|
76
|
+
|
|
77
|
+
### `busabase-sdk/airapp-node` — the local gateway (server)
|
|
78
|
+
|
|
79
|
+
`createBusabaseAirAppLocalGateway()` owns the whole standalone-run boundary: the pending PKCE request, credential rotation, auth verification, validated Space persistence, logout, and the `/api/v1` proxy. Do not copy those mechanics into each app.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createBusabaseAirAppLocalGateway } from "busabase-sdk/airapp-node";
|
|
83
|
+
|
|
84
|
+
const gateway = createBusabaseAirAppLocalGateway({ appId: "kelly-crm", successPath: "/#/overview" });
|
|
85
|
+
|
|
86
|
+
app.get("/auth/status", (c) => gateway.statusResponse(c.req.raw));
|
|
87
|
+
app.post("/auth/start", (c) => gateway.start(c.req.raw));
|
|
88
|
+
app.get("/auth/callback", (c) => gateway.callback(c.req.raw));
|
|
89
|
+
app.post("/auth/space", (c) => gateway.selectSpace(c.req.raw));
|
|
90
|
+
app.post("/auth/logout", (c) => gateway.logout(c.req.raw));
|
|
91
|
+
app.all("/api/v1/*", (c) => gateway.proxy(c.req.raw));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`proxy()` always sets `x-busabase-space` from its own validated selection and ignores any such header on the incoming request, so a page cannot talk the gateway into reading another Space.
|
|
95
|
+
|
|
96
|
+
### `busabase-sdk/airapp` — resource provisioning (isomorphic)
|
|
97
|
+
|
|
98
|
+
Declare the Folder and Bases the app needs; the SDK claims or creates them as one idempotent ChangeRequest.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { inspectProvisionedResources, provisionDeclaredResources } from "busabase-sdk/airapp";
|
|
102
|
+
|
|
103
|
+
const config = {
|
|
104
|
+
appId: "kelly-crm",
|
|
105
|
+
appName: "Kelly CRM",
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
folder: { slug: "kelly-crm", name: "Kelly CRM", description: "CRM workspace" },
|
|
108
|
+
bases: [{ key: "contacts", slug: "kelly-crm-contacts-v1", name: "Contacts", fields: [/* … */] }],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
let resources = await inspectProvisionedResources(client, config);
|
|
112
|
+
if (!resources.folder || resources.missing.length) {
|
|
113
|
+
resources = await provisionDeclaredResources(client, config);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
An app that ships its own AirApp node inside the Folder declares it too — `airApp: { slug, name, resourceKey }`. It is published, not provisioned, so it is never created here; declaring it is what lets it be stamped, and what stops it from reading as an unattributable stranger that blocks a legacy claim.
|
|
118
|
+
|
|
119
|
+
**An app owns a node only if it stamped it.** Ownership lives in `node.metadata` as `{ appId, resourceKey, schemaVersion }`. An unstamped node is adopted only when its slug, name, description, and full field list still match the declaration exactly; anything else raises `SETUP_CONFLICT` and nothing is mutated. A `schemaVersion` bump re-stamps in place — it never recreates a Base, so data survives. Concurrent callers on the same client share one in-flight submission.
|
|
120
|
+
|
|
121
|
+
**Bases evolve by appending fields.** A live Base whose fields are a strict *prefix* of the declaration is an older schema of yours: the missing suffix is added, one approval-gated `bases.fieldChangeRequest` per field, and anything left unapproved surfaces as `SETUP_PENDING` naming the requests. A field renamed, retyped, reordered, or removed is not an upgrade this can reason about, so it refuses rather than guessing which shape is right.
|
|
122
|
+
|
|
123
|
+
Failures are an `AirAppSetupError` carrying a `code`: `SETUP_REQUIRED` (offer to initialize), `SETUP_PENDING` (submitted, awaiting approval — approval-first working as designed), `SETUP_CONFLICT`, `SETUP_PERMISSION`, `SCHEMA_INCOMPLETE`. `message` keeps the `"CODE: detail"` shape so code that parses the prefix keeps working.
|
|
124
|
+
|
|
125
|
+
### `busabase-sdk/airapp-gate` — the connect UI (browser)
|
|
126
|
+
|
|
127
|
+
The three screens the operator sees before the app mounts: choose a server and connect, choose a Space, and initialize the workspace.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { createAirAppConnectGate } from "busabase-sdk/airapp-gate";
|
|
131
|
+
|
|
132
|
+
const gate = createAirAppConnectGate({
|
|
133
|
+
appName: "Kelly CRM",
|
|
134
|
+
shouldGate: () => !isDemo() && shouldUseLocalGateway(), // see below
|
|
135
|
+
onProvision: () => provisionDeclaredResources(client, config),
|
|
136
|
+
demoHref: "?demo=1",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (await gate.pass({ onReady: start })) start();
|
|
140
|
+
// …and when loading data fails because the workspace is not set up yet:
|
|
141
|
+
gate.renderSetupRequired(error, start);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`pass()` returns `true` when the app may load data.
|
|
145
|
+
|
|
146
|
+
**Pass `shouldGate`.** Where an app runs is a fact its host states: Busabase injects `BUSABASE_AIRAPP_RUNTIME` into the process it spawns, and the app's own server surfaces that to the browser. Never classify it by hostname, iframe nesting, or path — a hosted AirApp can be served from `localhost`, and a standalone run can be reached over a dev tunnel, so both directions of that guess are wrong. Omitted, `pass()` falls back to probing `/auth/status` and treating an unreachable or non-JSON answer as hosted; that works, but it infers something you already know.
|
|
147
|
+
|
|
148
|
+
Import `busabase-sdk/airapp-gate.css` for the default look; every colour, radius, and font is a `--bb-gate-*` custom property, so an app themes it by overriding them rather than forking. To replace the markup entirely, pass `render` — `selectAirAppGateScreen()` and `describeAirAppSetupError()` are exported so a custom renderer reuses the state machine instead of re-deriving it.
|
|
149
|
+
|
|
73
150
|
## Data client entry points
|
|
74
151
|
|
|
75
152
|
**`Busabase` class** — an ergonomic wrapper with namespaced methods (`bb.bases`, `bb.records`, `bb.changeRequests`, `bb.nodes`, `bb.views`, `bb.assets`, `bb.fileTrees`, `bb.files`, `bb.docs`, `bb.comments`, `bb.auditEvents`, `bb.agent`, `bb.agentTasks`, `bb.embedLinks`, `bb.search()`, `bb.grep()`, `bb.health()`, `bb.me()`). Drop to `bb.client` for the raw oRPC client (e.g. `bb.client.system.meta()`).
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default styling for the AirApp connect gate (`busabase-sdk/airapp-gate`).
|
|
3
|
+
*
|
|
4
|
+
* Ported from the stylesheet 62 App-in-Skills carried as a byte-identical copy,
|
|
5
|
+
* minus the rules left over from the retired setup-gate (secret rows, ready
|
|
6
|
+
* pills, prompt blocks) that the gate never rendered.
|
|
7
|
+
*
|
|
8
|
+
* Every value an app is likely to want to change is a custom property, so an
|
|
9
|
+
* app themes the gate by overriding these on `:root` instead of forking the
|
|
10
|
+
* file — that is the whole reason this can live in the SDK without dictating
|
|
11
|
+
* anyone's design system.
|
|
12
|
+
*
|
|
13
|
+
* :root { --bb-gate-accent: #7c3aed; --bb-gate-radius: 14px; }
|
|
14
|
+
*
|
|
15
|
+
* Load it however the app loads CSS:
|
|
16
|
+
* <link rel="stylesheet" href="./vendor/airapp-gate.css">
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--bb-gate-accent: #1570ef;
|
|
21
|
+
--bb-gate-accent-contrast: #fff;
|
|
22
|
+
--bb-gate-accent-wash: #f5f9ff;
|
|
23
|
+
--bb-gate-surface: #fff;
|
|
24
|
+
--bb-gate-surface-muted: #f9fafb;
|
|
25
|
+
--bb-gate-text: #17202a;
|
|
26
|
+
--bb-gate-text-strong: #101828;
|
|
27
|
+
--bb-gate-text-muted: #667085;
|
|
28
|
+
--bb-gate-text-label: #344054;
|
|
29
|
+
--bb-gate-border: #d0d5dd;
|
|
30
|
+
--bb-gate-border-subtle: #eaecf0;
|
|
31
|
+
--bb-gate-danger: #b42318;
|
|
32
|
+
--bb-gate-scrim: color-mix(in srgb, #101828 72%, transparent);
|
|
33
|
+
--bb-gate-radius: 8px;
|
|
34
|
+
--bb-gate-radius-inner: 6px;
|
|
35
|
+
--bb-gate-font: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
html.bb-gate-active,
|
|
39
|
+
html.bb-gate-active body {
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.bb-gate-overlay {
|
|
44
|
+
position: fixed;
|
|
45
|
+
inset: 0;
|
|
46
|
+
/* Below the max z-index so an app's own toast/portal layer can still sit on
|
|
47
|
+
top if it deliberately opts to. */
|
|
48
|
+
z-index: 2147483000;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
padding: 24px;
|
|
53
|
+
background: var(--bb-gate-scrim);
|
|
54
|
+
font-family: var(--bb-gate-font);
|
|
55
|
+
color: var(--bb-gate-text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.bb-gate-panel {
|
|
59
|
+
width: min(720px, 100%);
|
|
60
|
+
max-height: min(760px, calc(100vh - 48px));
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
background: var(--bb-gate-surface);
|
|
65
|
+
border: 1px solid var(--bb-gate-border);
|
|
66
|
+
border-radius: var(--bb-gate-radius);
|
|
67
|
+
box-shadow: 0 24px 64px rgb(16 24 40 / 28%);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.bb-gate-head {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 24px;
|
|
73
|
+
align-items: start;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
padding: 22px 24px 18px;
|
|
76
|
+
border-bottom: 1px solid var(--bb-gate-border-subtle);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.bb-gate-head h1 {
|
|
80
|
+
margin: 0;
|
|
81
|
+
font-size: 22px;
|
|
82
|
+
line-height: 1.25;
|
|
83
|
+
letter-spacing: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.bb-gate-head p {
|
|
87
|
+
margin: 6px 0 0;
|
|
88
|
+
color: var(--bb-gate-text-muted);
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.bb-gate-body {
|
|
93
|
+
overflow: auto;
|
|
94
|
+
padding: 20px 24px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.bb-gate-body h2 {
|
|
98
|
+
margin: 0 0 12px;
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
letter-spacing: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.bb-gate-server-grid {
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
106
|
+
gap: 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.bb-gate-server-card {
|
|
110
|
+
min-height: 104px;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 10px;
|
|
114
|
+
padding: 16px;
|
|
115
|
+
text-align: left;
|
|
116
|
+
color: var(--bb-gate-text-label);
|
|
117
|
+
background: var(--bb-gate-surface);
|
|
118
|
+
border: 1px solid var(--bb-gate-border);
|
|
119
|
+
border-radius: 7px;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.bb-gate-server-card:hover {
|
|
124
|
+
border-color: var(--bb-gate-text-muted);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.bb-gate-server-card.is-selected {
|
|
128
|
+
border-color: var(--bb-gate-accent);
|
|
129
|
+
box-shadow: inset 0 0 0 1px var(--bb-gate-accent);
|
|
130
|
+
background: var(--bb-gate-accent-wash);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.bb-gate-server-card input {
|
|
134
|
+
flex: none;
|
|
135
|
+
margin: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.bb-gate-server-card > span {
|
|
139
|
+
display: grid;
|
|
140
|
+
gap: 2px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.bb-gate-server-card strong {
|
|
144
|
+
font-size: 15px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.bb-gate-server-card span span {
|
|
148
|
+
color: var(--bb-gate-text-muted);
|
|
149
|
+
font-size: 13px;
|
|
150
|
+
line-height: 1.45;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.bb-gate-custom-url {
|
|
154
|
+
display: grid;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
margin-top: 16px;
|
|
157
|
+
padding: 16px;
|
|
158
|
+
border: 1px solid var(--bb-gate-border-subtle);
|
|
159
|
+
border-radius: 7px;
|
|
160
|
+
background: var(--bb-gate-surface-muted);
|
|
161
|
+
color: var(--bb-gate-text-label);
|
|
162
|
+
font-size: 13px;
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.bb-gate-custom-url[hidden] {
|
|
167
|
+
display: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.bb-gate-custom-url input {
|
|
171
|
+
min-width: 0;
|
|
172
|
+
min-height: 38px;
|
|
173
|
+
padding: 8px 10px;
|
|
174
|
+
color: var(--bb-gate-text-strong);
|
|
175
|
+
background: var(--bb-gate-surface);
|
|
176
|
+
border: 1px solid var(--bb-gate-border);
|
|
177
|
+
border-radius: var(--bb-gate-radius-inner);
|
|
178
|
+
font: inherit;
|
|
179
|
+
font-weight: 400;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.bb-gate-space-select {
|
|
183
|
+
display: grid;
|
|
184
|
+
gap: 6px;
|
|
185
|
+
margin-top: 14px;
|
|
186
|
+
color: var(--bb-gate-text-label);
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.bb-gate-space-select select {
|
|
192
|
+
min-height: 38px;
|
|
193
|
+
padding: 8px 10px;
|
|
194
|
+
color: var(--bb-gate-text-strong);
|
|
195
|
+
background: var(--bb-gate-surface);
|
|
196
|
+
border: 1px solid var(--bb-gate-border);
|
|
197
|
+
border-radius: var(--bb-gate-radius-inner);
|
|
198
|
+
font: inherit;
|
|
199
|
+
font-weight: 400;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.bb-gate-error {
|
|
203
|
+
color: var(--bb-gate-danger);
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.bb-gate-note {
|
|
208
|
+
color: var(--bb-gate-text-muted);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.bb-gate-link {
|
|
212
|
+
color: var(--bb-gate-accent);
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
font-weight: 650;
|
|
215
|
+
text-decoration: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.bb-gate-link:hover {
|
|
219
|
+
text-decoration: underline;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.bb-gate-footer {
|
|
223
|
+
display: flex;
|
|
224
|
+
justify-content: space-between;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 10px;
|
|
227
|
+
padding: 14px 24px;
|
|
228
|
+
border-top: 1px solid var(--bb-gate-border-subtle);
|
|
229
|
+
background: var(--bb-gate-surface);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.bb-gate-footer button {
|
|
233
|
+
min-height: 38px;
|
|
234
|
+
padding: 0 14px;
|
|
235
|
+
color: var(--bb-gate-text-label);
|
|
236
|
+
background: var(--bb-gate-surface);
|
|
237
|
+
border: 1px solid var(--bb-gate-border);
|
|
238
|
+
border-radius: var(--bb-gate-radius-inner);
|
|
239
|
+
font-weight: 650;
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.bb-gate-footer .bb-gate-primary {
|
|
244
|
+
color: var(--bb-gate-accent-contrast);
|
|
245
|
+
background: var(--bb-gate-accent);
|
|
246
|
+
border-color: var(--bb-gate-accent);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.bb-gate-footer button:disabled {
|
|
250
|
+
opacity: 0.55;
|
|
251
|
+
cursor: wait;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@media (max-width: 600px) {
|
|
255
|
+
.bb-gate-overlay {
|
|
256
|
+
align-items: stretch;
|
|
257
|
+
padding: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.bb-gate-panel {
|
|
261
|
+
width: 100%;
|
|
262
|
+
max-height: 100vh;
|
|
263
|
+
border: 0;
|
|
264
|
+
border-radius: 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.bb-gate-head {
|
|
268
|
+
padding: 18px 16px 14px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.bb-gate-head h1 {
|
|
272
|
+
font-size: 19px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.bb-gate-head p {
|
|
276
|
+
font-size: 13px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.bb-gate-body {
|
|
280
|
+
padding: 16px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.bb-gate-server-grid {
|
|
284
|
+
grid-template-columns: 1fr;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.bb-gate-footer {
|
|
288
|
+
padding: 12px 16px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.bb-gate-footer button {
|
|
292
|
+
min-width: 0;
|
|
293
|
+
flex: 1;
|
|
294
|
+
padding-inline: 8px;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { BusabaseAirAppAuthStatus } from './airapp-node.js';
|
|
2
|
+
import './oauth-node.js';
|
|
3
|
+
import './oauth.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The AirApp connect gate — the browser half of local onboarding.
|
|
7
|
+
*
|
|
8
|
+
* `busabase-sdk/airapp-node` already owns the *flow* (PKCE, credential
|
|
9
|
+
* rotation, Space validation, the `/api/v1` proxy). This module owns the
|
|
10
|
+
* remaining copy-paste: deciding which of the three screens to show, and
|
|
11
|
+
* drawing them. 62 App-in-Skills carried a 160-line renderer whose only
|
|
12
|
+
* per-app difference was the display name interpolated into an HTML string,
|
|
13
|
+
* plus a byte-identical 290-line stylesheet — while the app-creator template
|
|
14
|
+
* grew a second, differently-classed implementation of the same three screens.
|
|
15
|
+
*
|
|
16
|
+
* Structure, deliberately: every decision and every piece of markup is a pure
|
|
17
|
+
* function of the gateway's status. The DOM is touched only in `mount` /
|
|
18
|
+
* `wire`. That is what makes the escaping (the one security-relevant part —
|
|
19
|
+
* `oauth_error` and Space names both come from outside) testable without a
|
|
20
|
+
* browser, and what lets an app swap the renderer without reimplementing the
|
|
21
|
+
* state machine.
|
|
22
|
+
*
|
|
23
|
+
* Import `busabase-sdk/airapp-gate.css` (or copy it) for the default look; it
|
|
24
|
+
* is themed entirely through custom properties.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { createAirAppConnectGate } from "busabase-sdk/airapp-gate";
|
|
29
|
+
*
|
|
30
|
+
* const gate = createAirAppConnectGate({
|
|
31
|
+
* appName: "Kelly CRM",
|
|
32
|
+
* onProvision: () => provider.provisionResources(),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Before mounting the app shell:
|
|
36
|
+
* if (await gate.pass({ onReady: () => start() })) start();
|
|
37
|
+
*
|
|
38
|
+
* // When loading data fails because the workspace is not set up yet:
|
|
39
|
+
* gate.renderSetupRequired(error, () => start());
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** Which screen the gate owes the operator right now. */
|
|
44
|
+
type AirAppGateScreen = "connect" | "space" | "ready";
|
|
45
|
+
/**
|
|
46
|
+
* Decide the screen from the gateway's status. Headless — a custom renderer
|
|
47
|
+
* should call this rather than re-deriving the rules from `connected`.
|
|
48
|
+
*
|
|
49
|
+
* Driven by `readiness`, with a fallback to the older `connected` /
|
|
50
|
+
* `requiresSpace` pair so an app can upgrade the SDK before its server.
|
|
51
|
+
*/
|
|
52
|
+
declare function selectAirAppGateScreen(status: Partial<BusabaseAirAppAuthStatus> | null | undefined): AirAppGateScreen;
|
|
53
|
+
/** The five setup states, decoded into what the workspace screen needs to know. */
|
|
54
|
+
interface AirAppSetupDescription {
|
|
55
|
+
code: string;
|
|
56
|
+
/** The message with its `CODE: ` prefix stripped. */
|
|
57
|
+
detail: string;
|
|
58
|
+
title: string;
|
|
59
|
+
/** Offer an "Initialize workspace" button. */
|
|
60
|
+
canProvision: boolean;
|
|
61
|
+
/** Offer a "Check again" button (the app cannot act; the operator can retry). */
|
|
62
|
+
canRetry: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Decode a setup failure. Accepts an `AirAppSetupError`, any `Error` whose
|
|
66
|
+
* message uses the historical `"CODE: detail"` shape, or a bare string — the
|
|
67
|
+
* generated apps throw all three spellings.
|
|
68
|
+
*/
|
|
69
|
+
declare function describeAirAppSetupError(error: unknown): AirAppSetupDescription;
|
|
70
|
+
interface AirAppGateOptions {
|
|
71
|
+
/** Shown in the gate's prose, e.g. "Kelly CRM". The one per-app value. */
|
|
72
|
+
appName: string;
|
|
73
|
+
/**
|
|
74
|
+
* Where to mount. A selector, an element, or omitted to create and prepend
|
|
75
|
+
* `#busabaseAirAppGate` to `<body>`.
|
|
76
|
+
*/
|
|
77
|
+
mount?: string | HTMLElement;
|
|
78
|
+
/** Prefix for the gateway routes; `""` means `/auth/status`, `/auth/start`, … */
|
|
79
|
+
authBasePath?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Whether this run owes a gate at all. Return `false` for a Busabase-hosted
|
|
82
|
+
* AirApp (the ambient session owns auth) or for a demo/offline mode.
|
|
83
|
+
*
|
|
84
|
+
* **Pass this.** Omitted, `pass()` falls back to probing `/auth/status` and
|
|
85
|
+
* treating an unreachable or non-JSON answer as "hosted" — which works, but
|
|
86
|
+
* infers something the host already states outright. A Busabase-hosted AirApp
|
|
87
|
+
* is identified by the `BUSABASE_AIRAPP_RUNTIME` env var Busabase injects into
|
|
88
|
+
* the process it spawns, surfaced to the browser by the app's own server;
|
|
89
|
+
* never by hostname, iframe nesting, or path, since a hosted AirApp can be
|
|
90
|
+
* served from localhost and a standalone run can be reached over a dev tunnel.
|
|
91
|
+
*/
|
|
92
|
+
shouldGate?: () => boolean | Promise<boolean>;
|
|
93
|
+
/** Injected for tests. */
|
|
94
|
+
fetch?: typeof globalThis.fetch;
|
|
95
|
+
/** Called by the workspace screen's "Initialize workspace" button. */
|
|
96
|
+
onProvision?: () => Promise<unknown>;
|
|
97
|
+
/** Where the "open the demo" escape hatch points. Omit to hide it. */
|
|
98
|
+
demoHref?: string | null;
|
|
99
|
+
/** Replace the default markup wholesale while keeping the state machine. */
|
|
100
|
+
render?: AirAppGateRenderer;
|
|
101
|
+
}
|
|
102
|
+
interface AirAppGateRenderer {
|
|
103
|
+
connect(view: AirAppConnectView): string;
|
|
104
|
+
space(view: AirAppSpaceView): string;
|
|
105
|
+
workspace(view: AirAppWorkspaceView): string;
|
|
106
|
+
}
|
|
107
|
+
interface AirAppConnectView {
|
|
108
|
+
appName: string;
|
|
109
|
+
cloudBaseUrl: string;
|
|
110
|
+
/** Set when the operator is here because the session lapsed, not first-run. */
|
|
111
|
+
reconnect: boolean;
|
|
112
|
+
/** `?oauth_error=` from the callback redirect. Untrusted. */
|
|
113
|
+
oauthError: string;
|
|
114
|
+
authBasePath: string;
|
|
115
|
+
}
|
|
116
|
+
interface AirAppSpaceView {
|
|
117
|
+
appName: string;
|
|
118
|
+
baseUrl: string;
|
|
119
|
+
spaces: {
|
|
120
|
+
id: string;
|
|
121
|
+
name: string;
|
|
122
|
+
}[];
|
|
123
|
+
}
|
|
124
|
+
interface AirAppWorkspaceView extends AirAppSetupDescription {
|
|
125
|
+
appName: string;
|
|
126
|
+
demoHref: string | null;
|
|
127
|
+
}
|
|
128
|
+
/** HTML-escape. Every interpolation below goes through this. */
|
|
129
|
+
declare const escapeHtml: (value: unknown) => string;
|
|
130
|
+
/** The stock three screens. Pure string builders — no DOM, no fetch. */
|
|
131
|
+
declare const defaultAirAppGateRenderer: AirAppGateRenderer;
|
|
132
|
+
declare const DEFAULT_CLOUD_BASE_URL = "https://busabase.com";
|
|
133
|
+
interface AirAppConnectGate {
|
|
134
|
+
/**
|
|
135
|
+
* Resolve the gate before the app shell mounts. `true` means the app may
|
|
136
|
+
* load data; `false` means a screen is up and waiting on the operator — call
|
|
137
|
+
* again after `onReady` fires.
|
|
138
|
+
*/
|
|
139
|
+
pass(options?: {
|
|
140
|
+
onReady?: () => void;
|
|
141
|
+
}): Promise<boolean>;
|
|
142
|
+
/** Show the workspace screen for a setup error thrown while loading data. */
|
|
143
|
+
renderSetupRequired(error: unknown, onRetry: () => void): void;
|
|
144
|
+
/** Take the gate down and give scrolling back to the page. */
|
|
145
|
+
close(): void;
|
|
146
|
+
/** Fetch the gateway's status without rendering anything. */
|
|
147
|
+
status(): Promise<BusabaseAirAppAuthStatus | null>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build a gate bound to one app.
|
|
151
|
+
*
|
|
152
|
+
* Nothing is rendered until `pass()` decides a screen is owed, so it is safe to
|
|
153
|
+
* construct at module scope.
|
|
154
|
+
*/
|
|
155
|
+
declare function createAirAppConnectGate(options: AirAppGateOptions): AirAppConnectGate;
|
|
156
|
+
|
|
157
|
+
export { type AirAppConnectGate, type AirAppConnectView, type AirAppGateOptions, type AirAppGateRenderer, type AirAppGateScreen, type AirAppSetupDescription, type AirAppSpaceView, type AirAppWorkspaceView, BusabaseAirAppAuthStatus, DEFAULT_CLOUD_BASE_URL, createAirAppConnectGate, defaultAirAppGateRenderer, describeAirAppSetupError, escapeHtml, selectAirAppGateScreen };
|