busa-sdk 0.16.2 → 0.17.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/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,49 @@ 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">;
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
+ /**
269
+ * The create-vs-update operation list for one AirApp publish. Pure — no I/O —
270
+ * so the decision is directly testable: a local path already present on the
271
+ * deployed node updates it, anything else is a new file. Never deletes a
272
+ * remote-only path — a file this bundle stopped shipping is left alone rather
273
+ * than assumed stale, the same conservative choice the rest of this module
274
+ * makes for a Base's fields (`additiveFieldsFor` only ever appends).
275
+ */
276
+ declare function buildAirAppFileOperations(localFiles: AirAppFileInput[], deployedPaths: Iterable<string>): FileTreeCreateOrUpdateOperation[];
277
+ /**
278
+ * Publish the app's own AirApp bundle: create it under the Folder when this
279
+ * Space has never had it, or propose the local files as an update when it
280
+ * already exists. Always a separate, always-review-first ChangeRequest from
281
+ * the data layer's `provisionDeclaredResources` — see the note on
282
+ * `AirAppNodeDeclaration` for why the two must never share a request.
283
+ *
284
+ * Call after `provisionDeclaredResources` has confirmed the Folder exists.
285
+ * Every call proposes the full local file list, even when nothing actually
286
+ * changed — this module has no access to the deployed content hashes
287
+ * (`fileTrees.listFiles` reports paths, not hashes; only a per-file
288
+ * `readFile` does, and fetching one per file to skip a no-op publish is not
289
+ * worth the round trips a normal publish cadence would spend on it). A
290
+ * reviewer sees an empty diff and merges or ignores it; this is a cost in
291
+ * review noise, not correctness.
292
+ *
293
+ * @throws {AirAppSetupError} `SETUP_CONFLICT` when `config` declares no
294
+ * `airApp`; `SETUP_REQUIRED` when the Folder does not exist yet.
295
+ */
296
+ declare function publishAirApp(client: AirAppPublishClient, config: AirAppResourceConfig, files: AirAppFileInput[]): Promise<AirAppPublishResult>;
232
297
 
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 };
298
+ 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,68 @@ 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 publishAirApp(client, config, files) {
376
+ const airApp = config.airApp;
377
+ if (!airApp) {
378
+ throw setupError("SETUP_CONFLICT", "This app does not declare an airApp to publish");
379
+ }
380
+ const current = await inspectProvisionedResources(client, config);
381
+ if (!current.folder) {
382
+ throw setupError(
383
+ "SETUP_REQUIRED",
384
+ "Provision the Folder and Bases with provisionDeclaredResources before publishing the AirApp"
385
+ );
386
+ }
387
+ if (!current.airApp) {
388
+ const changeRequest2 = await client.fileTrees.create({
389
+ type: "airapp",
390
+ parentNodeId: current.folder.nodeId,
391
+ slug: airApp.slug,
392
+ name: airApp.name,
393
+ description: airApp.description ?? "",
394
+ files,
395
+ mergeMode: "replace",
396
+ // Explicit even though this app's write-permission credential would
397
+ // otherwise auto-merge it: executable AirApp code always gets human
398
+ // review before it runs in a viewer's browser, no exceptions.
399
+ autoMerge: false
400
+ });
401
+ if (changeRequest2.materialized) {
402
+ throw setupError(
403
+ "SCHEMA_INCOMPLETE",
404
+ "AirApp create unexpectedly materialized despite autoMerge: false"
405
+ );
406
+ }
407
+ return { status: "created", changeRequestId: changeRequest2.id };
408
+ }
409
+ const deployedFiles = await client.fileTrees.listFiles({
410
+ nodeId: current.airApp.nodeId,
411
+ type: "airapp"
412
+ });
413
+ const operations = buildAirAppFileOperations(
414
+ files,
415
+ deployedFiles.map((file) => file.path)
416
+ );
417
+ const changeRequest = await client.fileTrees.createChangeRequest({
418
+ nodeId: current.airApp.nodeId,
419
+ type: "airapp",
420
+ operations,
421
+ message: `Publish ${config.appName} AirApp`,
422
+ submittedBy: config.appId,
423
+ autoMerge: false
424
+ });
425
+ return { status: "updated", changeRequestId: changeRequest.id };
426
+ }
362
427
 
363
- export { AirAppSetupError, buildProvisionOperations, inspectProvisionedResources, isNotFound, provisionDeclaredResources, resolveProvisionedFolder };
428
+ 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": "busa-sdk",
3
- "version": "0.16.2",
3
+ "version": "0.17.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). Short-name alias for busabase-sdk.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-sdk",