busabase-sdk 0.16.2 → 0.17.1

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/dist/airapp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { B as BusabaseClient } from './client-DN7Ol1g1.js';
1
+ import { B as BusabaseClient } from './client-DIlpYL9z.js';
2
2
  import '@orpc/contract';
3
3
  import '@orpc/shared';
4
4
  import 'zod';
@@ -47,6 +47,11 @@ import 'zod';
47
47
 
48
48
  type NodeChangeRequestInput = Parameters<BusabaseClient["nodes"]["createChangeRequest"]>[0];
49
49
  type NodeOperationInput = NodeChangeRequestInput["operations"][number];
50
+ type FileTreeChangeRequestInput = Parameters<BusabaseClient["fileTrees"]["createChangeRequest"]>[0];
51
+ type FileTreeOperationInput = FileTreeChangeRequestInput["operations"][number];
52
+ type FileTreeCreateOrUpdateOperation = Extract<FileTreeOperationInput, {
53
+ kind: "create" | "update";
54
+ }>;
50
55
  /**
51
56
  * A Base field, as an app declares it.
52
57
  *
@@ -93,15 +98,20 @@ interface AirAppFolderDeclaration {
93
98
  /**
94
99
  * The app's own AirApp node, when it ships one inside its Folder.
95
100
  *
96
- * It is provisioned by publishing the AirApp, not by this module so it is
97
- * never created here, only recognized and stamped. Declaring it matters for a
98
- * second reason: without it, an unstamped Folder holding the app's own AirApp
99
- * would look like it holds an unattributable stranger, and the legacy claim
100
- * would be refused.
101
+ * `inspectProvisionedResources`/`provisionDeclaredResources` never create it
102
+ * an app's Folder and Bases are plain data-schema resources, safe to bring
103
+ * into existence unattended, but an AirApp is a bundle of code the viewer's
104
+ * browser will execute, so bringing it into existence always goes through
105
+ * `publishAirApp`'s separate, always-review-first ChangeRequest instead of
106
+ * riding along on the same `autoMerge: true` request as the data layer.
107
+ * Declaring it here matters for a second reason regardless: without it, an
108
+ * unstamped Folder holding the app's own AirApp would look like it holds an
109
+ * unattributable stranger, and the legacy claim would be refused.
101
110
  */
102
111
  interface AirAppNodeDeclaration {
103
112
  slug: string;
104
113
  name: string;
114
+ description?: string;
105
115
  /** Ownership key written into the node's metadata, e.g. `"airapp"`. */
106
116
  resourceKey: string;
107
117
  }
@@ -153,6 +163,17 @@ interface AirAppResources {
153
163
  missing: AirAppBaseDeclaration[];
154
164
  /** Owned nodes whose ownership stamp is missing or stale. */
155
165
  repairs: AirAppOwnershipRepair[];
166
+ /**
167
+ * The app's own AirApp node (see `AirAppNodeDeclaration`), when `config.airApp`
168
+ * is declared and a matching node exists under the Folder — owned or legacy,
169
+ * stamped or not; a pending stamp repair is reported separately via `repairs`.
170
+ * `null` when not declared, or declared but not found — the latter is what
171
+ * `publishAirApp` treats as "create", not a `missing`-array entry, because
172
+ * unlike a Base it is never auto-created just by finding it absent.
173
+ */
174
+ airApp: {
175
+ nodeId: string;
176
+ } | null;
156
177
  /**
157
178
  * Set when the server is too old for `nodes.updateMetadata`, so ownership was
158
179
  * established by full structural fingerprint instead of a stamp.
@@ -229,5 +250,52 @@ declare function inspectProvisionedResources(client: AirAppProvisioningClient, c
229
250
  * @throws {AirAppSetupError} with a `code` describing which screen to show.
230
251
  */
231
252
  declare function provisionDeclaredResources(client: AirAppProvisioningClient, config: AirAppResourceConfig): Promise<AirAppResources>;
253
+ /** The client surface `publishAirApp` needs, on top of provisioning. */
254
+ type AirAppPublishClient = AirAppProvisioningClient & Pick<BusabaseClient, "fileTrees" | "changeRequests">;
255
+ /** One file of the app's built AirApp bundle, as `publishAirApp` receives it. */
256
+ interface AirAppFileInput {
257
+ path: string;
258
+ content: string;
259
+ mimeType?: string;
260
+ }
261
+ type AirAppPublishResult = {
262
+ status: "created";
263
+ changeRequestId: string;
264
+ } | {
265
+ status: "updated";
266
+ changeRequestId: string;
267
+ } | {
268
+ status: "pending";
269
+ changeRequestId: string;
270
+ };
271
+ /**
272
+ * The create-vs-update operation list for one AirApp publish. Pure — no I/O —
273
+ * so the decision is directly testable: a local path already present on the
274
+ * deployed node updates it, anything else is a new file. Never deletes a
275
+ * remote-only path — a file this bundle stopped shipping is left alone rather
276
+ * than assumed stale, the same conservative choice the rest of this module
277
+ * makes for a Base's fields (`additiveFieldsFor` only ever appends).
278
+ */
279
+ declare function buildAirAppFileOperations(localFiles: AirAppFileInput[], deployedPaths: Iterable<string>): FileTreeCreateOrUpdateOperation[];
280
+ /**
281
+ * Publish the app's own AirApp bundle: create it under the Folder when this
282
+ * Space has never had it, or propose the local files as an update when it
283
+ * already exists. Always a separate, always-review-first ChangeRequest from
284
+ * the data layer's `provisionDeclaredResources` — see the note on
285
+ * `AirAppNodeDeclaration` for why the two must never share a request.
286
+ *
287
+ * Call after `provisionDeclaredResources` has confirmed the Folder exists.
288
+ * Every call proposes the full local file list, even when nothing actually
289
+ * changed — this module has no access to the deployed content hashes
290
+ * (`fileTrees.listFiles` reports paths, not hashes; only a per-file
291
+ * `readFile` does, and fetching one per file to skip a no-op publish is not
292
+ * worth the round trips a normal publish cadence would spend on it). A
293
+ * reviewer sees an empty diff and merges or ignores it; this is a cost in
294
+ * review noise, not correctness.
295
+ *
296
+ * @throws {AirAppSetupError} `SETUP_CONFLICT` when `config` declares no
297
+ * `airApp`; `SETUP_REQUIRED` when the Folder does not exist yet.
298
+ */
299
+ declare function publishAirApp(client: AirAppPublishClient, config: AirAppResourceConfig, files: AirAppFileInput[]): Promise<AirAppPublishResult>;
232
300
 
233
- export { type AirAppBaseDeclaration, type AirAppFieldDeclaration, type AirAppFolderDeclaration, type AirAppNodeDeclaration, type AirAppOwnershipRepair, type AirAppProvisionedBase, type AirAppProvisioningClient, type AirAppResourceConfig, type AirAppResourceOwnership, type AirAppResources, type AirAppSetupCode, AirAppSetupError, buildProvisionOperations, inspectProvisionedResources, isNotFound, provisionDeclaredResources, resolveProvisionedFolder };
301
+ export { type AirAppBaseDeclaration, type AirAppFieldDeclaration, type AirAppFileInput, type AirAppFolderDeclaration, type AirAppNodeDeclaration, type AirAppOwnershipRepair, type AirAppProvisionedBase, type AirAppProvisioningClient, type AirAppPublishClient, type AirAppPublishResult, type AirAppResourceConfig, type AirAppResourceOwnership, type AirAppResources, type AirAppSetupCode, AirAppSetupError, buildAirAppFileOperations, buildProvisionOperations, inspectProvisionedResources, isNotFound, provisionDeclaredResources, publishAirApp, resolveProvisionedFolder };
package/dist/airapp.js CHANGED
@@ -17,8 +17,9 @@ var ownsResource = (node, appId, resourceKey, schemaVersion) => node?.metadata?.
17
17
  var hasResourceIdentity = (node, appId, resourceKey) => node?.metadata?.appId === appId && node?.metadata?.resourceKey === resourceKey;
18
18
  var ownsAppRoot = (node, appId, schemaVersion) => hasResourceIdentity(node, appId, "app-root") && node?.metadata?.schemaVersion === schemaVersion;
19
19
  var hasEmptyMetadata = (node) => Object.keys(node?.metadata ?? {}).length === 0;
20
+ var isUnclaimed = (node) => node?.metadata?.appId === void 0;
20
21
  var matchesDeclaration = (node, declaration, type) => node?.type === type && node?.slug === declaration.slug && node?.name === declaration.name && node?.description === (declaration.description ?? "");
21
- var matchesLegacyAirApp = (node, config) => hasEmptyMetadata(node) && node?.type === "airapp" && node?.slug === config.airApp?.slug && node?.name === config.airApp?.name;
22
+ var matchesLegacyAirApp = (node, config) => isUnclaimed(node) && node?.type === "airapp" && node?.slug === config.airApp?.slug && node?.name === config.airApp?.name;
22
23
  var resourceMetadata = (config, resourceKey) => ({
23
24
  appId: config.appId,
24
25
  resourceKey,
@@ -26,7 +27,7 @@ var resourceMetadata = (config, resourceKey) => ({
26
27
  });
27
28
  function resolveProvisionedFolder(folder, config) {
28
29
  if (!folder) {
29
- return { folder: null, bases: [], missing: [...config.bases], repairs: [] };
30
+ return { folder: null, bases: [], missing: [...config.bases], repairs: [], airApp: null };
30
31
  }
31
32
  if (folder.node?.type !== "folder" || folder.node?.slug !== config.folder.slug) {
32
33
  throw setupError(
@@ -119,7 +120,8 @@ function resolveProvisionedFolder(folder, config) {
119
120
  folder: { ...config.folder, nodeId: folder.node.id },
120
121
  bases,
121
122
  missing,
122
- repairs
123
+ repairs,
124
+ airApp: airAppNode ? { nodeId: airAppNode.id } : null
123
125
  };
124
126
  }
125
127
  function buildProvisionOperations(config, folder, missingBases) {
@@ -359,5 +361,91 @@ function provisionDeclaredResources(client, config) {
359
361
  }
360
362
  return state.inFlight;
361
363
  }
364
+ function buildAirAppFileOperations(localFiles, deployedPaths) {
365
+ const deployed = new Set(deployedPaths);
366
+ return localFiles.map(
367
+ (file) => ({
368
+ kind: deployed.has(file.path) ? "update" : "create",
369
+ path: file.path,
370
+ content: file.content,
371
+ ...file.mimeType ? { mimeType: file.mimeType } : {}
372
+ })
373
+ );
374
+ }
375
+ async function findPendingAirAppCreate(client, slug) {
376
+ let cursor;
377
+ for (let page = 0; page < 10; page += 1) {
378
+ const result = await client.changeRequests.list({
379
+ status: ["in_review"],
380
+ ...cursor ? { cursor } : {}
381
+ });
382
+ for (const changeRequest of result.changeRequests) {
383
+ const matches = (changeRequest.operations ?? []).some((operation) => {
384
+ const payload = operation.headCommit?.payload;
385
+ return payload?.kind === "create" && payload?.nodeType === "airapp" && payload?.slug === slug;
386
+ });
387
+ if (matches) return changeRequest.id;
388
+ }
389
+ if (!result.nextCursor) return null;
390
+ cursor = result.nextCursor;
391
+ }
392
+ return null;
393
+ }
394
+ async function publishAirApp(client, config, files) {
395
+ const airApp = config.airApp;
396
+ if (!airApp) {
397
+ throw setupError("SETUP_CONFLICT", "This app does not declare an airApp to publish");
398
+ }
399
+ const current = await inspectProvisionedResources(client, config);
400
+ if (!current.folder) {
401
+ throw setupError(
402
+ "SETUP_REQUIRED",
403
+ "Provision the Folder and Bases with provisionDeclaredResources before publishing the AirApp"
404
+ );
405
+ }
406
+ if (!current.airApp) {
407
+ const pendingChangeRequestId = await findPendingAirAppCreate(client, airApp.slug);
408
+ if (pendingChangeRequestId) {
409
+ return { status: "pending", changeRequestId: pendingChangeRequestId };
410
+ }
411
+ const changeRequest2 = await client.fileTrees.create({
412
+ type: "airapp",
413
+ parentNodeId: current.folder.nodeId,
414
+ slug: airApp.slug,
415
+ name: airApp.name,
416
+ description: airApp.description ?? "",
417
+ files,
418
+ mergeMode: "replace",
419
+ // Explicit even though this app's write-permission credential would
420
+ // otherwise auto-merge it: executable AirApp code always gets human
421
+ // review before it runs in a viewer's browser, no exceptions.
422
+ autoMerge: false
423
+ });
424
+ if (changeRequest2.materialized) {
425
+ throw setupError(
426
+ "SCHEMA_INCOMPLETE",
427
+ "AirApp create unexpectedly materialized despite autoMerge: false"
428
+ );
429
+ }
430
+ return { status: "created", changeRequestId: changeRequest2.id };
431
+ }
432
+ const deployedFiles = await client.fileTrees.listFiles({
433
+ nodeId: current.airApp.nodeId,
434
+ type: "airapp"
435
+ });
436
+ const operations = buildAirAppFileOperations(
437
+ files,
438
+ deployedFiles.map((file) => file.path)
439
+ );
440
+ const changeRequest = await client.fileTrees.createChangeRequest({
441
+ nodeId: current.airApp.nodeId,
442
+ type: "airapp",
443
+ operations,
444
+ message: `Publish ${config.appName} AirApp`,
445
+ submittedBy: config.appId,
446
+ autoMerge: false
447
+ });
448
+ return { status: "updated", changeRequestId: changeRequest.id };
449
+ }
362
450
 
363
- export { AirAppSetupError, buildProvisionOperations, inspectProvisionedResources, isNotFound, provisionDeclaredResources, resolveProvisionedFolder };
451
+ export { AirAppSetupError, buildAirAppFileOperations, buildProvisionOperations, inspectProvisionedResources, isNotFound, provisionDeclaredResources, publishAirApp, resolveProvisionedFolder };
@@ -9182,6 +9182,7 @@ declare const cloudContract: {
9182
9182
  runLocalNode: _orpc_contract.ContractProcedure<z.ZodObject<{
9183
9183
  nodeId: z.ZodString;
9184
9184
  files: z.ZodRecord<z.ZodString, z.ZodString>;
9185
+ binaryFiles: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
9185
9186
  engine: z.ZodDefault<z.ZodEnum<{
9186
9187
  "local-node": "local-node";
9187
9188
  srt: "srt";
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { z } from 'zod';
5
5
  import * as _orpc_client from '@orpc/client';
6
- import { N as NodeOutput, a as NodeType, O as OperationKind, B as BusabaseClient, R as ResolvedConfig, b as BusabaseConfig } from './client-DN7Ol1g1.js';
7
- export { C as CREATABLE_NODE_TYPES, c as CloudContract, d as CreatableNodeType, D as DEFAULT_BASE_URL, e as cloudContract, f as createBusabaseClient, r as resolveConfig } from './client-DN7Ol1g1.js';
6
+ import { N as NodeOutput, a as NodeType, O as OperationKind, B as BusabaseClient, R as ResolvedConfig, b as BusabaseConfig } from './client-DIlpYL9z.js';
7
+ export { C as CREATABLE_NODE_TYPES, c as CloudContract, d as CreatableNodeType, D as DEFAULT_BASE_URL, e as cloudContract, f as createBusabaseClient, r as resolveConfig } from './client-DIlpYL9z.js';
8
8
  import '@orpc/shared';
9
9
 
10
10
  /**
package/dist/index.js CHANGED
@@ -1357,6 +1357,14 @@ var airAppRunLocalNodeInputSchema = z.object({
1357
1357
  /** Text files to mount into the sandbox workdir before installing, keyed by
1358
1358
  * path (same shape `RunPanel` already assembles for `NodepodRunner.mount`). */
1359
1359
  files: z.record(z.string(), z.string()),
1360
+ /** Binary (asset-backed) files — images, fonts, sample data — keyed by the
1361
+ * same path, base64-encoded because this input crosses a JSON boundary that
1362
+ * `Uint8Array` cannot. Separate from `files` rather than a tagged union so
1363
+ * an older client that sends only `files` keeps working unchanged.
1364
+ *
1365
+ * The in-browser Nodepod engine never uses this field: it hands raw bytes to
1366
+ * `Nodepod.boot({ files })` directly and skips the base64 round trip. */
1367
+ binaryFiles: z.record(z.string(), z.string()).optional().default({}),
1360
1368
  /** Server-side execution mode. `"local-node"` spawns a bare host Node.js
1361
1369
  * process (previewable, data bridge via reverse proxy, NOT OS-isolated);
1362
1370
  * `"srt"` wraps the same commands in the OS sandbox (isolated execution,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busabase-sdk",
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
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",
@@ -63,7 +63,7 @@
63
63
  "tsx": "^4.20.5",
64
64
  "typescript": "^5.9.3",
65
65
  "vitest": "^2.1.8",
66
- "busabase-contract": "0.16.2",
66
+ "busabase-contract": "0.17.1",
67
67
  "open-domains": "0.0.2",
68
68
  "openlib": "0.1.1"
69
69
  },