busa-sdk 0.19.2 → 0.21.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-kiUJSr_d.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),