busa-sdk 0.20.0 → 0.30.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,117 @@
1
+ //#region src/airapp-check.d.ts
2
+ /**
3
+ * The AirApp runtime contract, as checkable rules.
4
+ *
5
+ * These live in the SDK for one reason: **a rule and the runtime it checks must
6
+ * ship together.** The rules below are about `BUSABASE_AIRAPP_RUNTIME`,
7
+ * `__airapp/runtime`, `createBusabaseClient` and `isBusabaseAirAppHosted` — all
8
+ * of which are defined a few files away. Keeping the rules anywhere else means
9
+ * the two drift, and drift here is not hypothetical: an engine rename from
10
+ * `local-node` to `local` broke 66 shipped apps whose runtime detection carried
11
+ * its own private copy of the truth.
12
+ *
13
+ * It also makes them *reachable*. Every AirApp already depends on `busabase-sdk`
14
+ * at an exact pin, so an app's `scripts/check.mjs` shrinks from dozens of
15
+ * hand-copied assertions to reading its files and calling one function. The
16
+ * previous delivery mechanism was "copy these into your check script", and it
17
+ * reached 4 apps out of 67.
18
+ *
19
+ * Pure by design: this module reads no files, spawns nothing, and touches no
20
+ * network. Callers hand it source text. That keeps it usable from a CLI, from an
21
+ * app's own check script, and from a test, without any of them agreeing on a
22
+ * filesystem layout.
23
+ */
24
+ /** Severity split: an error breaks a user of the app; a warning is a default worth defending. */
25
+ type AirAppFindingSeverity = "error" | "warning";
26
+ interface AirAppFinding {
27
+ severity: AirAppFindingSeverity;
28
+ /** Stable kebab-case id, so a caller can allowlist or group without matching prose. */
29
+ rule: string;
30
+ message: string;
31
+ }
32
+ interface AirAppSources {
33
+ /** Raw `package.json` text. */
34
+ packageJson?: string;
35
+ /**
36
+ * The host's source. **Concatenate the whole server subtree**, not just the entry
37
+ * file — a `server.js` that mounts `server/hono.ts` keeps the runtime route in the
38
+ * module, and passing only the entry reports a correct app as broken. (Observed:
39
+ * two shipped apps failed this way against a collector that read `server.js` alone.)
40
+ */
41
+ server?: string;
42
+ /**
43
+ * Which language the host is written in. Inferred from Python syntax when omitted.
44
+ *
45
+ * NOT the AirApp *runtime* — that is the engine Busabase spawned the process in
46
+ * (`nodepod`, `local`, `sandock`, …), which is a value the app reports at runtime,
47
+ * not a property of its source. Two different things called "runtime" in one
48
+ * domain is how the wrong one ends up being checked.
49
+ */
50
+ serverLanguage?: "node" | "python";
51
+ /**
52
+ * The browser files that carry **logic** — app, config, client, runtime probe,
53
+ * the Busabase provider. The structural rules run over this corpus and no wider.
54
+ *
55
+ * Excluding string tables and demo data is deliberate rather than an oversight:
56
+ * an asset-path or hostname rule false-positives on UI copy that merely *talks*
57
+ * about localhost or shows a path in an error message.
58
+ */
59
+ browserLogic?: string;
60
+ /**
61
+ * **Everything** the browser downloads, copy and `index.html` included.
62
+ *
63
+ * Credentials are scanned over this wider corpus, because a key pasted into a
64
+ * string table ships to the browser exactly like one pasted into `app.js` — and
65
+ * used to pass a gate that only looked at the logic files.
66
+ *
67
+ * **Exclude `app/vendor/`.** A bundled `busabase-sdk` legitimately builds an
68
+ * `Authorization: Bearer …` header from a resolved key, so sweeping the vendor
69
+ * directory in reports every app that bundles the SDK as leaking a credential.
70
+ * List the app's own files; never glob the whole `app/` tree.
71
+ */
72
+ browserDownloads?: string;
73
+ /** `app/js/config.js`, when the app declares its own resources. */
74
+ config?: string;
75
+ /** The slug the package ships this app under, for the `resourceKey` rule. */
76
+ shippedSlug?: string;
77
+ }
78
+ /**
79
+ * The documented interactive page budget. A higher one may be justified — say so in
80
+ * review — so this is a warning rather than a hard bound. A generator checking its
81
+ * own output is free to be stricter.
82
+ */
83
+ declare const AIRAPP_DEFAULT_READ_LIMIT = 50;
84
+ /**
85
+ * Strip comments before pattern-matching source.
86
+ *
87
+ * Load-bearing, not tidiness. A file that explains a rule necessarily *names* the
88
+ * thing the rule forbids, and matching that prose let a server which had genuinely
89
+ * stopped reading `BUSABASE_AIRAPP_RUNTIME` pass its gate — the comment about the
90
+ * variable satisfied the check for the variable. Prose about a rule must never
91
+ * satisfy the rule.
92
+ */
93
+ declare const stripComments: (source: string) => string;
94
+ /**
95
+ * Objects inside a `bases: [ ... ]` literal, brace-matched rather than regexed across
96
+ * the array, so a nested `fields:` entry is never mistaken for a Base.
97
+ *
98
+ * Reading the declaration without evaluating the module is the point: a checker must
99
+ * not execute the app it is checking.
100
+ */
101
+ declare const scanAirAppConfig: (source: string) => {
102
+ bases: {
103
+ key: string | null;
104
+ slug: string | null;
105
+ }[];
106
+ drive: {
107
+ slug: string | null;
108
+ } | null;
109
+ resourceKey: string | null;
110
+ };
111
+ /**
112
+ * Check an AirApp against the runtime contract. Returns findings; never throws for a
113
+ * rule violation, so one caller can report every problem at once instead of the first.
114
+ */
115
+ declare const checkAirApp: (sources: AirAppSources) => AirAppFinding[];
116
+ //#endregion
117
+ export { AIRAPP_DEFAULT_READ_LIMIT, AirAppFinding, AirAppFindingSeverity, AirAppSources, checkAirApp, scanAirAppConfig, stripComments };
@@ -0,0 +1,159 @@
1
+ //#region src/airapp-check.ts
2
+ /** A `busabase-sdk` version must be exact. A range is not the app that was reviewed. */
3
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
4
+ /** A `start` script may start the server and nothing else. */
5
+ const START_IS_A_BUILD = /(?:&&|\|\||;|\bnpm\s+run\s+build\b|\btsc\b|\bvite\b|\bwebpack\b|\bparcel\b)/;
6
+ /** An AirApp is Hono plus vanilla browser code. `esbuild` is how the SDK gets bundled. */
7
+ const FORBIDDEN_DEPENDENCIES = [
8
+ "react",
9
+ "react-dom",
10
+ "preact",
11
+ "vite",
12
+ "@vitejs/plugin-react",
13
+ "next",
14
+ "webpack",
15
+ "parcel"
16
+ ];
17
+ /**
18
+ * The documented interactive page budget. A higher one may be justified — say so in
19
+ * review — so this is a warning rather than a hard bound. A generator checking its
20
+ * own output is free to be stricter.
21
+ */
22
+ const AIRAPP_DEFAULT_READ_LIMIT = 50;
23
+ /**
24
+ * Strip comments before pattern-matching source.
25
+ *
26
+ * Load-bearing, not tidiness. A file that explains a rule necessarily *names* the
27
+ * thing the rule forbids, and matching that prose let a server which had genuinely
28
+ * stopped reading `BUSABASE_AIRAPP_RUNTIME` pass its gate — the comment about the
29
+ * variable satisfied the check for the variable. Prose about a rule must never
30
+ * satisfy the rule.
31
+ */
32
+ const stripComments = (source) => source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:'"`\\])\/\/[^\n]*/g, "$1 ");
33
+ /**
34
+ * Objects inside a `bases: [ ... ]` literal, brace-matched rather than regexed across
35
+ * the array, so a nested `fields:` entry is never mistaken for a Base.
36
+ *
37
+ * Reading the declaration without evaluating the module is the point: a checker must
38
+ * not execute the app it is checking.
39
+ */
40
+ const scanAirAppConfig = (source) => {
41
+ const anchor = /\bbases\s*:\s*\[/.exec(source);
42
+ const blocks = [];
43
+ if (anchor) {
44
+ let depth = 0;
45
+ let start = -1;
46
+ for (let index = anchor.index + anchor[0].length - 1; index < source.length; index += 1) {
47
+ const char = source[index];
48
+ if (char === "[") depth += 1;
49
+ else if (char === "]") {
50
+ depth -= 1;
51
+ if (depth === 0) break;
52
+ } else if (char === "{" && depth === 1) {
53
+ if (start === -1) start = index;
54
+ depth += 1;
55
+ } else if (char === "{") depth += 1;
56
+ else if (char === "}") {
57
+ depth -= 1;
58
+ if (depth === 1 && start !== -1) {
59
+ blocks.push(source.slice(start, index + 1));
60
+ start = -1;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ const literal = (block, key) => new RegExp(`\\b${key}\\s*:\\s*"([^"]*)"`).exec(block)?.[1] ?? null;
66
+ const drive = /\bdrive\s*:\s*\{([\s\S]*?)\}/.exec(source);
67
+ return {
68
+ bases: blocks.map((block) => ({
69
+ key: literal(block, "key"),
70
+ slug: literal(block, "slug")
71
+ })),
72
+ drive: drive ? { slug: literal(drive[1], "slug") } : null,
73
+ resourceKey: /\bairApp\s*:\s*\{[^}]*\bresourceKey\s*:\s*"([^"]*)"/.exec(source)?.[1] ?? null
74
+ };
75
+ };
76
+ /**
77
+ * Check an AirApp against the runtime contract. Returns findings; never throws for a
78
+ * rule violation, so one caller can report every problem at once instead of the first.
79
+ */
80
+ const checkAirApp = (sources) => {
81
+ const findings = [];
82
+ const error = (rule, message) => findings.push({
83
+ severity: "error",
84
+ rule,
85
+ message
86
+ });
87
+ const warn = (rule, message) => findings.push({
88
+ severity: "warning",
89
+ rule,
90
+ message
91
+ });
92
+ if (sources.packageJson !== void 0) {
93
+ let manifest;
94
+ try {
95
+ manifest = JSON.parse(sources.packageJson);
96
+ } catch (cause) {
97
+ error("airapp/package-json", `package.json is not valid JSON (${cause.message}).`);
98
+ }
99
+ if (manifest) {
100
+ const scripts = manifest.scripts ?? {};
101
+ if (typeof scripts.dev !== "string") error("airapp/dev-script", "package.json has no `dev` script. Busabase boots an app with `npm run dev`, so it installs cleanly and then never starts.");
102
+ if (typeof scripts.start === "string" && START_IS_A_BUILD.test(scripts.start)) error("airapp/start-pure", `\`start\` is "${scripts.start}" — a deployed start must only run the server, never build or spawn.`);
103
+ const dependencies = {
104
+ ...manifest.dependencies ?? {},
105
+ ...manifest.devDependencies ?? {}
106
+ };
107
+ const sdk = manifest.dependencies?.["busabase-sdk"];
108
+ if (sdk === void 0) warn("airapp/sdk", "No `busabase-sdk` dependency — the app cannot reach the workspace.");
109
+ else if (!EXACT_VERSION.test(String(sdk))) error("airapp/sdk-pin", `busabase-sdk is "${String(sdk)}". Pin the exact version — a range means the installed app is not the one that was reviewed.`);
110
+ for (const forbidden of FORBIDDEN_DEPENDENCIES) if (dependencies[forbidden] !== void 0) error("airapp/no-framework", `Depends on "${forbidden}". An AirApp is Hono plus vanilla HTML/CSS/JS; its files are reviewed and then run as-is, so nothing may need compiling first.`);
111
+ }
112
+ }
113
+ if (sources.browserLogic !== void 0) {
114
+ const logic = sources.browserLogic;
115
+ const code = stripComments(logic);
116
+ if (!logic.includes("createBusabaseClient")) warn("airapp/sdk-client", "Browser code never calls `createBusabaseClient`.");
117
+ if (logic.includes("__busabase_api__")) error("airapp/legacy-bridge", "The obsolete `/__busabase_api__/` bridge prefix is gone. The API is same-origin `/api/v1`.");
118
+ if (/baseUrl\s*:\s*["'`]https?:\/\//.test(code)) error("airapp/absolute-url", "A hard-coded absolute Busabase URL in browser code. Use `window.location.origin` — an absolute URL is right in exactly one deployment and wrong in the rest.");
119
+ if (/(?:src|href)="\/(?!\/)|from\s+["']\/(?!\/)/.test(code)) error("airapp/absolute-asset", "An absolute asset path. Under the Local Node engine the app is proxied onto a sub-path of Busabase's origin, so a leading slash resolves against Busabase itself and 404s.");
120
+ if (/while\s*\(\s*cursor\s*\)|client\.bases\.list\s*\(/.test(code) || /while\s*\(\s*true\s*\)[\s\S]{0,400}?(?:records\.list|readPage)\s*\(/.test(code)) error("airapp/unbounded-read", "Unbounded loading or runtime Base discovery. Every interactive read gets an explicit budget and fetches one page per user action.");
121
+ if (/\bmax\w*pages?\w*\s*=\s*\d+[\s\S]{0,400}?(?:client\.)?records\.list\s*\(/i.test(code)) error("airapp/eager-multi-page", "A capped loop fetches several pages of records in one function call. A cap bounds the damage but does not fix the shape: this still hides a multi-page scan behind one loading state instead of fetching one page per user action.");
122
+ const branches = /createAirAppConnectGate|getRuntime\s*\(|\bhosted\b/.test(code) || sources.server !== void 0 && /createAirAppConnectGate/.test(sources.server);
123
+ if (/location\s*\.\s*(?:hostname|host)\b/.test(code)) error("airapp/runtime-hostname", "Hostname-based runtime detection. Both directions are wrong: a Busabase-hosted AirApp is served from `localhost` on Desktop, and a standalone `npm run dev` is reached over a LAN IP or a signed dev tunnel. Read the runtime from `__airapp/runtime`.");
124
+ if (/(?:===|!==|==|!=)\s*["'`][^"'`]*(?:localhost|127\.0\.0\.1)|(?:includes|startsWith|endsWith|indexOf|search|match|test)\s*\(\s*\/?["'`]?[^"'`)]*(?:localhost|127\.0\.0\.1)/.test(code)) error("airapp/runtime-loopback", "A loopback host comparison. Runtime detection must not depend on the URL at all.");
125
+ if (branches && !logic.includes("__airapp/runtime")) error("airapp/runtime-probe", "This app branches on where it is running but never probes `__airapp/runtime`, so that branch is deciding on something else.");
126
+ if (/["'`]\/__airapp\/runtime/.test(logic)) error("airapp/runtime-probe-relative", "The runtime probe has a leading slash. It must be relative (`__airapp/runtime`) — a leading slash resolves against Busabase's root under the Local Node sub-path proxy.");
127
+ }
128
+ if (sources.server !== void 0) {
129
+ const server = sources.server;
130
+ const serverCode = stripComments(server);
131
+ const readsRuntimeEnv = (sources.serverLanguage ? sources.serverLanguage === "python" : /^\s*(?:import|from)\s+\w+|def\s+\w+\s*\(/m.test(server)) ? /BUSABASE_AIRAPP_RUNTIME/.test(serverCode) : /(?:read|describe)BusabaseAirAppRuntime\s*\(|process\.env\.BUSABASE_AIRAPP_RUNTIME\b/.test(serverCode);
132
+ const clientBranches = sources.browserLogic !== void 0 && /createAirAppConnectGate|getRuntime\s*\(|\bhosted\b/.test(stripComments(sources.browserLogic));
133
+ if (clientBranches && !readsRuntimeEnv) error("airapp/runtime-env", "The server never reads `BUSABASE_AIRAPP_RUNTIME`, directly or through the SDK, so it has nothing to serve at `__airapp/runtime`.");
134
+ if (/AIRAPP_HOSTED_RUNTIMES\s*=\s*new Set|hosted:\s*\w*RUNTIMES?\w*\.has\s*\(/.test(serverCode)) error("airapp/runtime-engine-list", "Hosting is decided from a hardcoded list of engine names. Use presence, not membership — a private list is what broke 66 apps when `local-node` was renamed `local`.");
135
+ if (clientBranches && !/["'`]\/__airapp\/runtime["'`]/.test(server)) error("airapp/runtime-route", "Browser code branches on the runtime but the server does not serve `/__airapp/runtime`.");
136
+ if (/Bearer\s+(?!\$\{)[A-Za-z0-9_-]{8,}/.test(server)) error("airapp/credential", "A literal Bearer token in the server source.");
137
+ }
138
+ if (sources.browserDownloads !== void 0) {
139
+ const downloads = sources.browserDownloads;
140
+ if (/BUSABASE_API_KEY/i.test(downloads)) error("airapp/credential", "An API key reference in browser source.");
141
+ if (/Bearer\s+(?!\$\{)[A-Za-z0-9_.-]{8,}/.test(downloads)) error("airapp/credential", "A literal Bearer token in browser source.");
142
+ if (/["'`]\s*Bearer\s*\$\{/.test(downloads)) error("airapp/credential", "Browser code builds an Authorization header. A deployed AirApp uses the viewer's ambient same-origin session and needs none.");
143
+ }
144
+ if (sources.config !== void 0) {
145
+ const config = scanAirAppConfig(sources.config);
146
+ if (sources.shippedSlug !== void 0 && config.resourceKey !== null && config.resourceKey !== sources.shippedSlug) error("airapp/resource-key", `Config declares resourceKey "${config.resourceKey}" but the package ships this app as "${sources.shippedSlug}". Install stamps nodes with the shipped slug, so the app would not recognise its own node.`);
147
+ for (const base of config.bases) if (base.slug === null) error("airapp/base-slug", `Config base "${base.key ?? "(unnamed)"}" has no \`slug\`. The SDK needs it to CREATE the Base, so the app's own provisioning fails even though installing from the package succeeds — install reads base.json and never opens that door.`);
148
+ if (config.drive !== null && config.drive.slug === null) error("airapp/base-slug", "Config `drive` has no `slug`; provisioning it will fail.");
149
+ if (/vaultValue|vaultSecret\s*:\s*["'`][^"'`]/.test(sources.config)) error("airapp/vault-value", "A Vault value in config. Config may reference secrets, never carry them.");
150
+ for (const found of sources.config.matchAll(/\breadLimit\s*:\s*(\d+)/g)) {
151
+ const limit = Number(found[1]);
152
+ if (limit < 1) error("airapp/read-budget", `readLimit is ${limit}; it must be a positive integer.`);
153
+ else if (limit > 50) warn("airapp/read-budget", `readLimit is ${limit}, above the 50-record default page budget. Justify it in review, or page.`);
154
+ }
155
+ }
156
+ return findings;
157
+ };
158
+ //#endregion
159
+ export { AIRAPP_DEFAULT_READ_LIMIT, checkAirApp, scanAirAppConfig, stripComments };
@@ -46,6 +46,39 @@ declare const isBusabaseAirAppHosted: (runtime?: string) => boolean;
46
46
  * behaviour rather than break.
47
47
  */
48
48
  declare const asKnownBusabaseAirAppRuntime: (runtime: string) => BusabaseAirAppRuntime | null;
49
+ /** The body every AirApp serves at `__airapp/runtime`. */
50
+ interface BusabaseAirAppRuntimeReport {
51
+ /**
52
+ * The engine name verbatim, or `"standalone"` when nothing hosted this process.
53
+ *
54
+ * Deliberately a plain string and not the enum: a newer Busabase naming an engine
55
+ * this SDK predates must still round-trip, and narrowing here would erase it.
56
+ */
57
+ runtime: string;
58
+ /**
59
+ * The same value narrowed to what this SDK knows, or `null` for an engine it has
60
+ * never heard of. This is the field to branch on when behaviour genuinely differs
61
+ * per engine — and `null` is a normal answer, not an error.
62
+ */
63
+ knownRuntime: BusabaseAirAppRuntime | null;
64
+ /** Presence of the injected variable. Never membership of {@link BUSABASE_AIRAPP_RUNTIMES}. */
65
+ hosted: boolean;
66
+ /** Whether a standalone run is proxying to a Busabase base URL. A separate axis. */
67
+ devProxy: boolean;
68
+ }
69
+ /**
70
+ * Build the `__airapp/runtime` body.
71
+ *
72
+ * This exists because the one line it replaces — `hosted: …` — has now regressed
73
+ * twice. Both times a correct app was copied from an older source and came back
74
+ * deciding hosting from a hardcoded list of engine names, and both times every test
75
+ * stayed green, because nothing asserted the shape of that decision.
76
+ *
77
+ * An app that calls this cannot get it wrong: `hosted` is presence by construction,
78
+ * and the engine name is reported both verbatim (so an unknown one survives) and
79
+ * narrowed (so code that wants to branch on a specific engine has a type).
80
+ */
81
+ declare const describeBusabaseAirAppRuntime: (env?: Record<string, string | undefined>) => BusabaseAirAppRuntimeReport;
49
82
  declare const BUSABASE_AIRAPP_GATEWAY_REASONS: {
50
83
  readonly authRequired: "AUTH_REQUIRED";
51
84
  readonly authUnavailable: "AUTH_UNAVAILABLE";
@@ -108,4 +141,4 @@ declare class BusabaseAirAppLocalGateway {
108
141
  }
109
142
  declare const createBusabaseAirAppLocalGateway: (options: BusabaseAirAppLocalGatewayOptions) => BusabaseAirAppLocalGateway;
110
143
  //#endregion
111
- export { BUSABASE_AIRAPP_GATEWAY_REASONS, BUSABASE_AIRAPP_RUNTIMES, BUSABASE_AIRAPP_RUNTIME_ENV, BusabaseAirAppAuthStatus, BusabaseAirAppLocalGateway, BusabaseAirAppLocalGatewayOptions, BusabaseAirAppRuntime, asKnownBusabaseAirAppRuntime, createBusabaseAirAppLocalGateway, isBusabaseAirAppHosted, readBusabaseAirAppRuntime };
144
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, BUSABASE_AIRAPP_RUNTIMES, BUSABASE_AIRAPP_RUNTIME_ENV, BusabaseAirAppAuthStatus, BusabaseAirAppLocalGateway, BusabaseAirAppLocalGatewayOptions, BusabaseAirAppRuntime, BusabaseAirAppRuntimeReport, asKnownBusabaseAirAppRuntime, createBusabaseAirAppLocalGateway, describeBusabaseAirAppRuntime, isBusabaseAirAppHosted, readBusabaseAirAppRuntime };
@@ -55,6 +55,27 @@ const isBusabaseAirAppHosted = (runtime = readBusabaseAirAppRuntime()) => runtim
55
55
  * behaviour rather than break.
56
56
  */
57
57
  const asKnownBusabaseAirAppRuntime = (runtime) => BUSABASE_AIRAPP_RUNTIMES.includes(runtime) ? runtime : null;
58
+ /**
59
+ * Build the `__airapp/runtime` body.
60
+ *
61
+ * This exists because the one line it replaces — `hosted: …` — has now regressed
62
+ * twice. Both times a correct app was copied from an older source and came back
63
+ * deciding hosting from a hardcoded list of engine names, and both times every test
64
+ * stayed green, because nothing asserted the shape of that decision.
65
+ *
66
+ * An app that calls this cannot get it wrong: `hosted` is presence by construction,
67
+ * and the engine name is reported both verbatim (so an unknown one survives) and
68
+ * narrowed (so code that wants to branch on a specific engine has a type).
69
+ */
70
+ const describeBusabaseAirAppRuntime = (env = process.env) => {
71
+ const runtime = readBusabaseAirAppRuntime(env);
72
+ return {
73
+ runtime: runtime || "standalone",
74
+ knownRuntime: asKnownBusabaseAirAppRuntime(runtime),
75
+ hosted: isBusabaseAirAppHosted(runtime),
76
+ devProxy: (env.BUSABASE_BASE_URL || "").trim() !== ""
77
+ };
78
+ };
58
79
  const BUSABASE_AIRAPP_GATEWAY_REASONS = {
59
80
  authRequired: "AUTH_REQUIRED",
60
81
  authUnavailable: "AUTH_UNAVAILABLE",
@@ -427,4 +448,4 @@ var BusabaseAirAppLocalGateway = class {
427
448
  };
428
449
  const createBusabaseAirAppLocalGateway = (options) => new BusabaseAirAppLocalGateway(options);
429
450
  //#endregion
430
- export { BUSABASE_AIRAPP_GATEWAY_REASONS, BUSABASE_AIRAPP_RUNTIMES, BUSABASE_AIRAPP_RUNTIME_ENV, BusabaseAirAppLocalGateway, asKnownBusabaseAirAppRuntime, createBusabaseAirAppLocalGateway, isBusabaseAirAppHosted, readBusabaseAirAppRuntime };
451
+ export { BUSABASE_AIRAPP_GATEWAY_REASONS, BUSABASE_AIRAPP_RUNTIMES, BUSABASE_AIRAPP_RUNTIME_ENV, BusabaseAirAppLocalGateway, asKnownBusabaseAirAppRuntime, createBusabaseAirAppLocalGateway, describeBusabaseAirAppRuntime, isBusabaseAirAppHosted, readBusabaseAirAppRuntime };
package/dist/airapp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BusabaseClient } from "./client-DF6mrd6D.js";
1
+ import { t as BusabaseClient } from "./client-CVz2pqrF.js";
2
2
  //#region src/airapp.d.ts
3
3
  type NodeChangeRequestInput = Parameters<BusabaseClient["nodes"]["createChangeRequest"]>[0];
4
4
  type NodeOperationInput = NodeChangeRequestInput["operations"][number];
package/dist/airapp.js CHANGED
@@ -1,4 +1,33 @@
1
1
  import { z } from "zod";
2
+ //#region ../../packages/busabase-contract/src/domains/skill/frontmatter.ts
3
+ /**
4
+ * `SKILL.md` frontmatter — the generic shape, independent of Busabase.
5
+ *
6
+ * This used to live inside `domains/package/template.ts`, which encoded an
7
+ * assumption that no longer holds: that a Skill is a *part of* a template. Most
8
+ * Skills are not. A Skill is a directory with a `SKILL.md` an agent reads; it
9
+ * needs no `busabase.json`, ships no `content/`, and never installs into a
10
+ * workspace. Busabase's own `busabase-app-creator` is one.
11
+ *
12
+ * Modelling it only as a template component meant there was nowhere to hang
13
+ * checks for a plain Skill, so there were none. Hence its own file, in the
14
+ * domain it belongs to.
15
+ *
16
+ * `metadata` is deliberately open: a Skill carries whatever its ecosystem
17
+ * defines (categories, tags, risk labels, per-agent hints). The Busabase-specific
18
+ * block that decides template-ness is layered on in `domains/package/template.ts`
19
+ * — the template format knows about Skills, not the other way round.
20
+ */
21
+ const SkillFrontmatterSchema$1 = z.object({
22
+ /** Identity. For a Skill inside a package this must equal the package name. */
23
+ name: z.string().min(1),
24
+ /**
25
+ * How an agent decides whether to reach for this Skill at all — so an empty one
26
+ * is not a cosmetic omission, it is a Skill that never gets picked.
27
+ */
28
+ description: z.string().default(""),
29
+ metadata: z.object({}).passthrough().optional()
30
+ });
2
31
  const TemplateAirAppRefSchema = z.object({
3
32
  /** Slug of the `content/<dir>` holding the AirApp. */
4
33
  slug: z.string().min(1),
@@ -70,11 +99,7 @@ const SkillBusabaseMetadataSchema = z.object({
70
99
  resources: z.array(z.string()).default([]),
71
100
  risk: z.string().optional()
72
101
  });
73
- z.object({
74
- name: z.string().min(1),
75
- description: z.string().default(""),
76
- metadata: z.object({ busabase: SkillBusabaseMetadataSchema.optional() }).passthrough().optional()
77
- });
102
+ SkillFrontmatterSchema$1.extend({ metadata: z.object({ busabase: SkillBusabaseMetadataSchema.optional() }).passthrough().optional() });
78
103
  /** Stamp on every resource node (Base, Drive, AirApp, …) an app owns. */
79
104
  const AppResourceOwnershipSchema = z.object({
80
105
  appId: z.string().min(1),
@@ -1,5 +1,13 @@
1
1
  import { ContractRouterClient } from "@orpc/contract";
2
2
  import { z } from "zod";
3
+ //#region ../../packages/busabase-contract/src/domains/types.d.ts
4
+ /**
5
+ * Shared interfaces for node-type modules. Each node type lives in its own module
6
+ * (./folder, ./base, ./skill, …) and exports a definition that satisfies
7
+ * `NodeTypeDefinition`; the registry composes + registers them.
8
+ */
9
+ type NodePublicAccess = "detail" | "submit" | "runtime" | "no";
10
+ //#endregion
3
11
  //#region ../../packages/busabase-contract/src/domains/registry.d.ts
4
12
  declare const GENERIC_NODE_OPERATIONS: readonly [{
5
13
  readonly kind: "node_create";
@@ -32,6 +40,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
32
40
  readonly container: true;
33
41
  readonly creatable: true;
34
42
  readonly hasDetail: true;
43
+ readonly publicAccess: "detail";
35
44
  };
36
45
  readonly operations: readonly [];
37
46
  }, {
@@ -41,6 +50,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
41
50
  readonly capabilities: {
42
51
  readonly hasDetail: true;
43
52
  readonly creatable: true;
53
+ readonly publicAccess: "detail";
44
54
  };
45
55
  readonly operations: readonly [{
46
56
  readonly kind: "record_create";
@@ -118,6 +128,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
118
128
  readonly capabilities: {
119
129
  readonly hasDetail: true;
120
130
  readonly creatable: true;
131
+ readonly publicAccess: NodePublicAccess;
121
132
  };
122
133
  readonly operations: readonly [{
123
134
  readonly kind: `${string}_file_create`;
@@ -143,6 +154,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
143
154
  readonly capabilities: {
144
155
  readonly hasDetail: true;
145
156
  readonly creatable: true;
157
+ readonly publicAccess: NodePublicAccess;
146
158
  };
147
159
  readonly operations: readonly [{
148
160
  readonly kind: `${string}_file_create`;
@@ -168,6 +180,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
168
180
  readonly capabilities: {
169
181
  readonly hasDetail: true;
170
182
  readonly creatable: true;
183
+ readonly publicAccess: NodePublicAccess;
171
184
  };
172
185
  readonly operations: readonly [{
173
186
  readonly kind: `${string}_file_create`;
@@ -193,6 +206,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
193
206
  readonly capabilities: {
194
207
  readonly hasDetail: true;
195
208
  readonly creatable: true;
209
+ readonly publicAccess: "detail";
196
210
  };
197
211
  readonly operations: readonly [];
198
212
  }, {
@@ -202,6 +216,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
202
216
  readonly capabilities: {
203
217
  readonly hasDetail: true;
204
218
  readonly creatable: true;
219
+ readonly publicAccess: "detail";
205
220
  };
206
221
  readonly operations: readonly [{
207
222
  readonly kind: "doc_update";
@@ -215,6 +230,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
215
230
  readonly capabilities: {
216
231
  readonly hasDetail: true;
217
232
  readonly creatable: true;
233
+ readonly publicAccess: "submit";
218
234
  };
219
235
  readonly operations: readonly [];
220
236
  }, {
@@ -224,6 +240,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
224
240
  readonly capabilities: {
225
241
  readonly hasDetail: true;
226
242
  readonly creatable: true;
243
+ readonly publicAccess: "detail";
227
244
  };
228
245
  readonly operations: readonly [{
229
246
  readonly kind: "whiteboard_document_update";
@@ -237,6 +254,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
237
254
  readonly capabilities: {
238
255
  readonly hasDetail: true;
239
256
  readonly creatable: true;
257
+ readonly publicAccess: "detail";
240
258
  };
241
259
  readonly operations: readonly [{
242
260
  readonly kind: "workflow_document_update";
@@ -250,6 +268,7 @@ declare const BUILTIN_NODE_TYPES: readonly [{
250
268
  readonly capabilities: {
251
269
  readonly hasDetail: true;
252
270
  readonly creatable: true;
271
+ readonly publicAccess: "no";
253
272
  };
254
273
  readonly operations: readonly [{
255
274
  readonly kind: "html_document_update";
@@ -420,12 +439,14 @@ declare const cloudContract: {
420
439
  sources: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodArray<z.ZodEnum<{
421
440
  files: "files";
422
441
  names: "names";
442
+ nodes: "nodes";
423
443
  records: "records";
424
444
  }>>, z.ZodEnum<{
425
445
  files: "files";
426
446
  names: "names";
447
+ nodes: "nodes";
427
448
  records: "records";
428
- }>]>, z.ZodTransform<("files" | "names" | "records")[], "files" | "names" | "records" | ("files" | "names" | "records")[]>>>;
449
+ }>]>, z.ZodTransform<("files" | "names" | "nodes" | "records")[], "files" | "names" | "nodes" | "records" | ("files" | "names" | "nodes" | "records")[]>>>;
429
450
  }, z.core.$strip>, z.ZodObject<{
430
451
  query: z.ZodString;
431
452
  limit: z.ZodNumber;
@@ -437,6 +458,7 @@ declare const cloudContract: {
437
458
  base: "base";
438
459
  change_request: "change_request";
439
460
  file: "file";
461
+ node: "node";
440
462
  record: "record";
441
463
  }>;
442
464
  title: z.ZodString;
@@ -445,6 +467,7 @@ declare const cloudContract: {
445
467
  href: z.ZodString;
446
468
  updatedAt: z.ZodNullable<z.ZodString>;
447
469
  }, z.core.$strip>>;
470
+ contentTruncated: z.ZodDefault<z.ZodBoolean>;
448
471
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
449
472
  grep: import("@orpc/contract").ContractProcedure<z.ZodObject<{
450
473
  pattern: z.ZodString;
@@ -570,6 +593,14 @@ declare const cloudContract: {
570
593
  }, z.core.$strip>, z.ZodObject<{
571
594
  isDescendant: z.ZodBoolean;
572
595
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
596
+ ancestors: import("@orpc/contract").ContractProcedure<z.ZodObject<{
597
+ nodeId: z.ZodString;
598
+ type: z.ZodOptional<z.ZodEnum<{
599
+ [x: string]: string;
600
+ }>>;
601
+ }, z.core.$strip>, z.ZodObject<{
602
+ ancestorIds: z.ZodArray<z.ZodString>;
603
+ }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
573
604
  createChangeRequest: import("@orpc/contract").ContractProcedure<z.ZodObject<{
574
605
  message: z.ZodDefault<z.ZodOptional<z.ZodString>>;
575
606
  submittedBy: z.ZodDefault<z.ZodOptional<z.ZodString>>;
@@ -14425,7 +14456,12 @@ declare const cloudContract: {
14425
14456
  deletedSessionCount: z.ZodNumber;
14426
14457
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
14427
14458
  connections: {
14428
- list: import("@orpc/contract").ContractProcedure<import("@orpc/contract").Schema<unknown, unknown>, z.ZodArray<z.ZodObject<{
14459
+ list: import("@orpc/contract").ContractProcedure<z.ZodObject<{
14460
+ scope: z.ZodDefault<z.ZodEnum<{
14461
+ mine: "mine";
14462
+ space: "space";
14463
+ }>>;
14464
+ }, z.core.$strip>, z.ZodArray<z.ZodObject<{
14429
14465
  slug: z.ZodString;
14430
14466
  agentName: z.ZodString;
14431
14467
  transport: z.ZodEnum<{
@@ -14453,6 +14489,7 @@ declare const cloudContract: {
14453
14489
  lastActivityAt: z.ZodString;
14454
14490
  error: z.ZodDefault<z.ZodNullable<z.ZodString>>;
14455
14491
  }, z.core.$strip>>;
14492
+ ownedByCurrentUser: z.ZodBoolean;
14456
14493
  }, z.core.$strip>>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
14457
14494
  };
14458
14495
  sessions: {
@@ -14504,10 +14541,12 @@ declare const cloudContract: {
14504
14541
  attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
14505
14542
  kind: z.ZodEnum<{
14506
14543
  audio: "audio";
14544
+ file: "file";
14507
14545
  image: "image";
14508
14546
  }>;
14509
14547
  data: z.ZodString;
14510
14548
  mimeType: z.ZodString;
14549
+ filename: z.ZodOptional<z.ZodString>;
14511
14550
  }, z.core.$strip>>>;
14512
14551
  }, z.core.$strip>, z.ZodObject<{
14513
14552
  accepted: z.ZodBoolean;
@@ -15086,7 +15125,9 @@ declare const cloudContract: {
15086
15125
  textContentHash: z.ZodNullable<z.ZodString>;
15087
15126
  byteCount: z.ZodNumber;
15088
15127
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
15089
- importBegin: import("@orpc/contract").ContractProcedure<import("@orpc/contract").Schema<unknown, unknown>, z.ZodObject<{
15128
+ importBegin: import("@orpc/contract").ContractProcedure<z.ZodObject<{
15129
+ sourceSpaceId: z.ZodString;
15130
+ }, z.core.$strip>, z.ZodObject<{
15090
15131
  sessionId: z.ZodString;
15091
15132
  }, z.core.$strip>, import("@orpc/contract").MergedErrorMap<Record<never, never>, Record<never, never>>, Record<never, never>>;
15092
15133
  importTables: import("@orpc/contract").ContractProcedure<z.ZodObject<{
@@ -24581,7 +24622,7 @@ type OperationStatus = "pending" | "merged" | "archived" | "failed";
24581
24622
  type ChangeRequestTargetType = "base" | "node";
24582
24623
  type BusabaseSourceChannel = "web_ui" | "browser" | "openapi" | "sdk" | "cli" | "mcp" | "skill" | "webhook" | "automation" | "import";
24583
24624
  type ReviewVerdict = "approved" | "rejected";
24584
- type SearchResultKind = "record" | "change_request" | "base" | "file";
24625
+ type SearchResultKind = "record" | "change_request" | "base" | "file" | "node";
24585
24626
  type CommentSubjectType = "record" | "change_request" | "operation" | "commit";
24586
24627
  type AuditAction = "record.viewed" | "change_request.created" | "change_request.updated" | "change_request.deleted" | "change_request.reviewed" | "change_request.merged" | "base.created" | "field.created" | "doc.created" | "doc.updated" | "file.created" | "skill.created" | "drive.created" | "airapp.created" | "asset.deleted" | "asset.metadata_updated" | "asset.text_written" | "asset.text_marked_none" | "node.metadata_updated" | "node.purged";
24587
24628
  interface UserRefVO {
@@ -24800,6 +24841,12 @@ interface SearchResponseVO {
24800
24841
  offset: number;
24801
24842
  hasMore: boolean;
24802
24843
  results: SearchResultVO[];
24844
+ /**
24845
+ * True when some in-scope node content was indexed only up to the projection
24846
+ * cap, so this search could not see all of it. Lets a client report an empty
24847
+ * result honestly instead of implying the workspace lacks the phrase.
24848
+ */
24849
+ contentTruncated: boolean;
24803
24850
  }
24804
24851
  interface AuditEventVO {
24805
24852
  id: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as FileTreeReadFileVO, A as SearchResultKind, At as AttachmentRef, B as VaultRuntimeEnv, C as NodeSearchResultVO, Ct as VIEW_FIELD_MIN_WIDTH, D as ReviewVO, Dt as ViewSortVO, E as OperationVO, Et as ViewFilterVO, F as VaultAccessPolicy, Ft as CREATABLE_NODE_TYPES, G as AssetDetailVO, H as VaultSettingsVO, I as VaultEnvironment, It as CreatableNodeType, J as AssetVO, K as AssetTextStatus, L as VaultItemInput, Lt as NodeType, M as SourceAttributionVO, Mt as cloudContract, N as UserRefVO, Nt as NodeIcon, O as ReviewVerdict, Ot as ViewType, P as UpdateVaultSettingsDTO, Pt as NodeIconSchema, Q as FileTreeNodeVO, R as VaultItemKind, Rt as OperationKind, S as LookupRollup, St as VIEW_FIELD_MAX_WIDTH, T as OperationStatus, Tt as ViewFilterOperator, U as FileNodeMetadata, V as VaultScopeType, W as FileNodeVO, X as GrepResultVO, Y as GrepInputDTO, Z as FileTreeFileVO, _ as ChangeRequestVO, _t as GalleryCardSize, a as createBusabaseClient, at as FormPageSourceVO, b as CommitVO, bt as RecordLinkVO, c as AuditAction, ct as FormThemeVO, d as ChangeRequestBatchFailureVO, dt as ListFormsVO, et as NodeDetailVO, f as ChangeRequestCountsVO, ft as SubmitFormDTO, g as ChangeRequestTargetType, gt as BaseVO, h as ChangeRequestStatus, ht as BaseFieldVO, i as ResolvedConfig, it as FormFieldBindingVO, j as SearchResultVO, jt as CloudContract, k as SearchResponseVO, kt as ViewVO, l as AuditEventVO, lt as FormVO, m as ChangeRequestReviewBatchResultVO, mt as AssetAttachmentRef, n as BusabaseConfig, nt as CreateFormDTO, o as resolveConfig, ot as FormShareVO, p as ChangeRequestMergeBatchResultVO, pt as UpdateFormDTO, q as AssetUsageVO, r as DEFAULT_BASE_URL, rt as FormBoundFieldVO, s as AgentTaskVO, st as FormSubmitResultVO, t as BusabaseClient, tt as ActivityItemVO, u as BusabaseSourceChannel, ut as ListFormsDTO, v as CommentSubjectType, vt as GalleryCoverFit, w as NodeVO, wt as ViewConfigVO, x as FieldType, xt as RecordVO, y as CommentVO, yt as GanttScale, z as VaultItemVO } from "./client-DF6mrd6D.js";
1
+ import { $ as FileTreeReadFileVO, A as SearchResultKind, At as AttachmentRef, B as VaultRuntimeEnv, C as NodeSearchResultVO, Ct as VIEW_FIELD_MIN_WIDTH, D as ReviewVO, Dt as ViewSortVO, E as OperationVO, Et as ViewFilterVO, F as VaultAccessPolicy, Ft as CREATABLE_NODE_TYPES, G as AssetDetailVO, H as VaultSettingsVO, I as VaultEnvironment, It as CreatableNodeType, J as AssetVO, K as AssetTextStatus, L as VaultItemInput, Lt as NodeType, M as SourceAttributionVO, Mt as cloudContract, N as UserRefVO, Nt as NodeIcon, O as ReviewVerdict, Ot as ViewType, P as UpdateVaultSettingsDTO, Pt as NodeIconSchema, Q as FileTreeNodeVO, R as VaultItemKind, Rt as OperationKind, S as LookupRollup, St as VIEW_FIELD_MAX_WIDTH, T as OperationStatus, Tt as ViewFilterOperator, U as FileNodeMetadata, V as VaultScopeType, W as FileNodeVO, X as GrepResultVO, Y as GrepInputDTO, Z as FileTreeFileVO, _ as ChangeRequestVO, _t as GalleryCardSize, a as createBusabaseClient, at as FormPageSourceVO, b as CommitVO, bt as RecordLinkVO, c as AuditAction, ct as FormThemeVO, d as ChangeRequestBatchFailureVO, dt as ListFormsVO, et as NodeDetailVO, f as ChangeRequestCountsVO, ft as SubmitFormDTO, g as ChangeRequestTargetType, gt as BaseVO, h as ChangeRequestStatus, ht as BaseFieldVO, i as ResolvedConfig, it as FormFieldBindingVO, j as SearchResultVO, jt as CloudContract, k as SearchResponseVO, kt as ViewVO, l as AuditEventVO, lt as FormVO, m as ChangeRequestReviewBatchResultVO, mt as AssetAttachmentRef, n as BusabaseConfig, nt as CreateFormDTO, o as resolveConfig, ot as FormShareVO, p as ChangeRequestMergeBatchResultVO, pt as UpdateFormDTO, q as AssetUsageVO, r as DEFAULT_BASE_URL, rt as FormBoundFieldVO, s as AgentTaskVO, st as FormSubmitResultVO, t as BusabaseClient, tt as ActivityItemVO, u as BusabaseSourceChannel, ut as ListFormsDTO, v as CommentSubjectType, vt as GalleryCoverFit, w as NodeVO, wt as ViewConfigVO, x as FieldType, xt as RecordVO, y as CommentVO, yt as GanttScale, z as VaultItemVO } from "./client-CVz2pqrF.js";
2
2
  import { z } from "zod";
3
3
  //#region src/url.d.ts
4
4
  /**
@@ -317,13 +317,14 @@ declare class Busabase {
317
317
  hasMore: boolean;
318
318
  results: {
319
319
  id: string;
320
- kind: "base" | "change_request" | "file" | "record";
320
+ kind: "base" | "change_request" | "file" | "node" | "record";
321
321
  title: string;
322
322
  body: string;
323
323
  eyebrow: string;
324
324
  href: string;
325
325
  updatedAt: string | null;
326
326
  }[];
327
+ contentTruncated: boolean;
327
328
  }, Error>;
328
329
  /**
329
330
  * Unified grep — one regex/literal pattern scanned across every in-scope
package/dist/index.js CHANGED
@@ -136,13 +136,15 @@ const AgentSessionVOSchema = z.object({
136
136
  /** Set when status is "failed"; surfaced verbatim to the user. */
137
137
  error: z.string().nullable().default(null)
138
138
  });
139
- /** One connected agent backend in the current space and authenticated user's scope. */
139
+ /** One connected agent backend visible in the requested workspace scope. */
140
140
  const AgentConnectionVOSchema = z.object({
141
141
  slug: z.string(),
142
142
  agentName: z.string(),
143
143
  transport: AgentTransportSchema,
144
144
  sessionCount: z.number().int().nonnegative(),
145
- latest: AgentSessionVOSchema.nullable()
145
+ latest: AgentSessionVOSchema.nullable(),
146
+ /** Controls owner-only actions such as deleting the saved OAuth grant. */
147
+ ownedByCurrentUser: z.boolean()
146
148
  });
147
149
  /**
148
150
  * One streamed event from a session.
@@ -178,17 +180,34 @@ const AgentSessionEventVOSchema = z.object({
178
180
  permissionOptionId: z.string().optional(),
179
181
  at: z.string()
180
182
  });
183
+ const AgentConnectionScopeSchema = z.enum(["mine", "space"]);
184
+ const ListAgentConnectionsInputSchema = z.object({ scope: AgentConnectionScopeSchema.default("mine") });
181
185
  const CreateAgentSessionInputSchema = z.object({
182
186
  /** Must name a catalog entry. Deliberately NOT a command line — see below. */
183
187
  slug: z.string().min(1) });
184
188
  const DisconnectAgentInputSchema = z.object({
185
189
  /** The exact connected-agent slug shown by the sessions/catalog surfaces. */
186
190
  slug: z.string().min(1) });
187
- /** Base64 image/audio the browser attached — ACP's `ImageContent`/`AudioContent` shape verbatim. */
191
+ /**
192
+ * A base64 payload the browser attached.
193
+ *
194
+ * `image`/`audio` map to ACP's `ImageContent`/`AudioContent` verbatim; `file`
195
+ * is everything else (a PDF, a spreadsheet, a Markdown note) and becomes an
196
+ * ACP embedded `resource` block at send time. `data` is base64 for every kind
197
+ * — including textual files — so the browser never has to guess an encoding;
198
+ * whether a file travels to the agent as ACP text or as an ACP blob is decided
199
+ * server-side from the payload itself.
200
+ */
188
201
  const PromptAttachmentInputSchema = z.object({
189
- kind: z.enum(["image", "audio"]),
190
- data: z.string().min(1),
191
- mimeType: z.string().min(1)
202
+ kind: z.enum([
203
+ "image",
204
+ "audio",
205
+ "file"
206
+ ]),
207
+ data: z.string().min(1).max(2e7),
208
+ mimeType: z.string().min(1),
209
+ /** The original filename, carried for `file` so the agent and the transcript can name it. */
210
+ filename: z.string().max(255).optional()
192
211
  });
193
212
  const PromptAgentSessionInputSchema = z.object({
194
213
  sessionId: z.string().min(1),
@@ -219,8 +238,8 @@ const agentsContract = {
219
238
  deletedSessionCount: z.number().int().nonnegative()
220
239
  })),
221
240
  connections: {
222
- /** Connected backends, scoped to the current space and authenticated user. */
223
- list: oc.output(AgentConnectionVOSchema.array()) },
241
+ /** Connected backends in the current user's personal or active-space scope. */
242
+ list: oc.input(ListAgentConnectionsInputSchema).output(AgentConnectionVOSchema.array()) },
224
243
  sessions: {
225
244
  list: oc.output(AgentSessionVOSchema.array()),
226
245
  create: oc.input(CreateAgentSessionInputSchema).output(AgentSessionVOSchema),
@@ -663,7 +682,8 @@ const makeFileTreeNodeType = (config) => ({
663
682
  icon: config.icon,
664
683
  capabilities: {
665
684
  hasDetail: true,
666
- creatable: true
685
+ creatable: true,
686
+ publicAccess: config.publicAccess
667
687
  },
668
688
  operations: fileTreeOperations(config.type)
669
689
  });
@@ -681,7 +701,8 @@ const airappNodeType = makeFileTreeNodeType({
681
701
  icon: "app-window",
682
702
  routeBase: "airapps",
683
703
  tag: "AirApps",
684
- entryFile: "package.json"
704
+ entryFile: "package.json",
705
+ publicAccess: "no"
685
706
  });
686
707
  //#endregion
687
708
  //#region ../../packages/busabase-contract/src/domains/base/definition.ts
@@ -692,7 +713,8 @@ const baseNodeType = {
692
713
  icon: "table",
693
714
  capabilities: {
694
715
  hasDetail: true,
695
- creatable: true
716
+ creatable: true,
717
+ publicAccess: "detail"
696
718
  },
697
719
  operations: [
698
720
  {
@@ -795,7 +817,8 @@ const docNodeType = {
795
817
  icon: "file-text",
796
818
  capabilities: {
797
819
  hasDetail: true,
798
- creatable: true
820
+ creatable: true,
821
+ publicAccess: "detail"
799
822
  },
800
823
  operations: [{
801
824
  kind: "doc_update",
@@ -812,7 +835,8 @@ const driveNodeType = makeFileTreeNodeType({
812
835
  icon: "hard-drive",
813
836
  routeBase: "drives",
814
837
  tag: "Drives",
815
- entryFile: "README.md"
838
+ entryFile: "README.md",
839
+ publicAccess: "no"
816
840
  });
817
841
  //#endregion
818
842
  //#region ../../packages/busabase-contract/src/domains/file-node/definition.ts
@@ -824,7 +848,8 @@ const fileNodeType = {
824
848
  icon: "file",
825
849
  capabilities: {
826
850
  hasDetail: true,
827
- creatable: true
851
+ creatable: true,
852
+ publicAccess: "detail"
828
853
  },
829
854
  operations: []
830
855
  };
@@ -839,7 +864,8 @@ const folderNodeType = {
839
864
  capabilities: {
840
865
  container: true,
841
866
  creatable: true,
842
- hasDetail: true
867
+ hasDetail: true,
868
+ publicAccess: "detail"
843
869
  },
844
870
  operations: []
845
871
  };
@@ -860,7 +886,8 @@ const formNodeType = {
860
886
  icon: "form",
861
887
  capabilities: {
862
888
  hasDetail: true,
863
- creatable: true
889
+ creatable: true,
890
+ publicAccess: "submit"
864
891
  },
865
892
  operations: []
866
893
  };
@@ -879,7 +906,8 @@ const htmlNodeType = {
879
906
  icon: "code-xml",
880
907
  capabilities: {
881
908
  hasDetail: true,
882
- creatable: true
909
+ creatable: true,
910
+ publicAccess: "no"
883
911
  },
884
912
  operations: [{
885
913
  kind: "html_document_update",
@@ -896,7 +924,8 @@ const skillNodeType = makeFileTreeNodeType({
896
924
  icon: "sparkles",
897
925
  routeBase: "skills",
898
926
  tag: "Skills",
899
- entryFile: "SKILL.md"
927
+ entryFile: "SKILL.md",
928
+ publicAccess: "no"
900
929
  });
901
930
  //#endregion
902
931
  //#region ../../packages/busabase-contract/src/domains/whiteboard/definition.ts
@@ -912,7 +941,8 @@ const whiteboardNodeType = {
912
941
  icon: "pen-tool",
913
942
  capabilities: {
914
943
  hasDetail: true,
915
- creatable: true
944
+ creatable: true,
945
+ publicAccess: "detail"
916
946
  },
917
947
  operations: [{
918
948
  kind: "whiteboard_document_update",
@@ -935,7 +965,8 @@ const workflowNodeType = {
935
965
  icon: "workflow",
936
966
  capabilities: {
937
967
  hasDetail: true,
938
- creatable: true
968
+ creatable: true,
969
+ publicAccess: "detail"
939
970
  },
940
971
  operations: [{
941
972
  kind: "workflow_document_update",
@@ -1293,11 +1324,18 @@ const agentTaskSchema = z.object({
1293
1324
  });
1294
1325
  const searchResultSchema = z.object({
1295
1326
  id: z.string(),
1327
+ /**
1328
+ * `node` covers the CONTENT of a content-bearing node (doc / html /
1329
+ * whiteboard / workflow). Purely additive — callers that do not know it can
1330
+ * ignore the kind, and callers that never asked for it (an explicit
1331
+ * `sources` list without `nodes`) never receive it.
1332
+ */
1296
1333
  kind: z.enum([
1297
1334
  "record",
1298
1335
  "change_request",
1299
1336
  "base",
1300
- "file"
1337
+ "file",
1338
+ "node"
1301
1339
  ]),
1302
1340
  title: z.string(),
1303
1341
  body: z.string(),
@@ -1310,7 +1348,17 @@ const searchResponseSchema = z.object({
1310
1348
  limit: z.number(),
1311
1349
  offset: z.number(),
1312
1350
  hasMore: z.boolean(),
1313
- results: z.array(searchResultSchema)
1351
+ results: z.array(searchResultSchema),
1352
+ /**
1353
+ * True when at least one in-scope node's content was indexed only up to the
1354
+ * projection cap, so this search could not see all of it.
1355
+ *
1356
+ * Exists so an empty result can be reported honestly: without it, "no
1357
+ * results" is ambiguous between "the workspace does not contain this" and
1358
+ * "we did not look at all of it". Clients should surface it — and point at
1359
+ * `grep`, which has no such cap — rather than implying absence.
1360
+ */
1361
+ contentTruncated: z.boolean().default(false)
1314
1362
  });
1315
1363
  const liveEventSchema = z.object({
1316
1364
  kind: z.enum([
@@ -1523,7 +1571,8 @@ const inboxSnapshotResponseSchema = listChangeRequestsPageResponseSchema.extend(
1523
1571
  const SEARCH_SOURCES = [
1524
1572
  "records",
1525
1573
  "files",
1526
- "names"
1574
+ "names",
1575
+ "nodes"
1527
1576
  ];
1528
1577
  const searchInputSchema = z.object({
1529
1578
  query: z.string().default(""),
@@ -2791,6 +2840,19 @@ const ExportAssetTextVOSchema = z.object({
2791
2840
  textContentHash: z.string().nullable(),
2792
2841
  byteCount: z.number().int().nonnegative()
2793
2842
  });
2843
+ const ImportBeginInputSchema = z.object({
2844
+ /**
2845
+ * The space id the archive was ORIGINALLY exported from (`manifest.spaceId`
2846
+ * in the `.bbdump`, already integrity-verified before this is called).
2847
+ * `importTableRows`'s "nodes" handling needs this to recognize the
2848
+ * archive's own root-node row deterministically (`rootNodeIdForSpace`) —
2849
+ * scanning each batch's rows for "the one with a null `parentId`" only
2850
+ * works when that row happens to land in the SAME batch as its children,
2851
+ * which cursor pagination (id-ordered, not tree-ordered) does not
2852
+ * guarantee once a space has more nodes than one page. See the matching
2853
+ * comment in `import-logic.ts`.
2854
+ */
2855
+ sourceSpaceId: z.string() });
2794
2856
  const ImportBeginVOSchema = z.object({ sessionId: z.string() });
2795
2857
  /**
2796
2858
  * `docBodies`, `attachmentBlobs` and `assetTextBlobs` are pseudo-tables: doc
@@ -2854,7 +2916,7 @@ const dumpContract = {
2854
2916
  tags: ["Dump"],
2855
2917
  summary: "Begin a full-fidelity import session",
2856
2918
  successDescription: "Created an import session for the current space. Refused unless the space's node tree is empty (full-fidelity import preserves original ids and cannot merge into existing data)."
2857
- }).output(ImportBeginVOSchema),
2919
+ }).input(ImportBeginInputSchema).output(ImportBeginVOSchema),
2858
2920
  importTables: oc.route({
2859
2921
  method: "POST",
2860
2922
  path: "/dump/import/tables",
@@ -4233,6 +4295,14 @@ const NodeDetailVOSchema = z.discriminatedUnion("type", [
4233
4295
  * hint. An ambiguous slug with no `type` is refused rather than silently
4234
4296
  * resolved to whichever row sorts first.
4235
4297
  */
4298
+ /**
4299
+ * `nodes.ancestors` output — the node's ancestor ids, ROOT-FIRST, excluding
4300
+ * the node itself. Ids only, deliberately: the sidebar already has (or can
4301
+ * lazily fetch) each ancestor's own row, and it only needs to know WHICH
4302
+ * folders to expand. Returning full nodes here would duplicate `nodes.list`
4303
+ * and make a cheap navigational lookup expensive.
4304
+ */
4305
+ const nodeAncestorsVOSchema = z.object({ ancestorIds: z.array(z.string()) });
4236
4306
  const getNodeInputSchema = z.object({
4237
4307
  nodeId: z.string().describe("Node id, or a slug that is unique within its type."),
4238
4308
  type: z.enum(NODE_TYPES).optional().describe("Optional disambiguation hint, only needed when `nodeId` is a slug that exists under more than one node type.")
@@ -4355,7 +4425,7 @@ const busabaseContractRoutes = {
4355
4425
  path: "/nodes/search",
4356
4426
  tags: ["Nodes", "Search"],
4357
4427
  summary: "Search nodes by name/slug (cheap, name-only quick-jump)",
4358
- 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 — 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."
4428
+ 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 — ordered exact-slug-match first, then by name. Backs the dashboard search dialog's 'Recent' tab cache-miss path. To search what is written INSIDE nodes, use `search` with the `nodes` source (indexed, paginated) or `grep` (exhaustive, no index)."
4359
4429
  }).input(searchNodesByNameInputSchema).output(z.array(nodeSearchResultSchema)),
4360
4430
  isDescendant: oc.route({
4361
4431
  method: "GET",
@@ -4364,6 +4434,13 @@ const busabaseContractRoutes = {
4364
4434
  summary: "Check whether a node is a descendant of another",
4365
4435
  successDescription: "Server-authoritative parentId-chain walk from nodeId up to potentialAncestorId. Used to gate cross-branch drag-and-drop drops in the sidebar, since the full tree is no longer guaranteed to be loaded client-side (depth-bounded lazy load) — a purely local walk could wrongly allow dropping a folder into its own unloaded descendant."
4366
4436
  }).input(isDescendantInputSchema).output(isDescendantOutputSchema),
4437
+ ancestors: oc.route({
4438
+ method: "GET",
4439
+ path: "/nodes/{nodeId}/ancestors",
4440
+ tags: ["Nodes"],
4441
+ summary: "List a node's ancestor ids",
4442
+ successDescription: "The node's ancestor ids, root-first, excluding the node itself (`[]` directly under the workspace root). `nodeId` accepts an id or a slug, same as `nodes.get`; pass `type` when a slug exists under more than one type. Lets a depth-bounded, lazily-expanded tree open straight to a deep node on a cold load (a refresh, a bookmark, a shared link) without one round trip per level."
4443
+ }).input(getNodeInputSchema).output(nodeAncestorsVOSchema),
4367
4444
  createChangeRequest: oc.route({
4368
4445
  method: "POST",
4369
4446
  path: "/nodes/change-requests",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "busa-sdk",
3
- "version": "0.20.0",
3
+ "version": "0.30.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",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/airapp-node.d.ts",
36
36
  "default": "./dist/airapp-node.js"
37
37
  },
38
+ "./airapp-check": {
39
+ "types": "./dist/airapp-check.d.ts",
40
+ "default": "./dist/airapp-check.js"
41
+ },
38
42
  "./airapp": {
39
43
  "types": "./dist/airapp.d.ts",
40
44
  "default": "./dist/airapp.js"