diff --git a/packages/sdk/src/areas/plugins.ts b/packages/sdk/src/areas/plugins.ts index 30137c133..5dd21645b 100644 --- a/packages/sdk/src/areas/plugins.ts +++ b/packages/sdk/src/areas/plugins.ts @@ -235,12 +235,20 @@ export interface PluginsArea { ): Promise; } -function pluginSourceSelection(args: PluginInstallArgs): PluginSourceSelection { +/** + * Only a non-root selection goes on the wire. A root install is the server's + * default, and omitting the key keeps the request accepted by servers that + * predate the `selection` field (bb-app <= 0.37.x validate the install body + * strictly and answer 422 on unknown keys, see #1662). + */ +function pluginSourceSelection( + args: PluginInstallArgs, +): PluginSourceSelection | undefined { if (args.subdirectory !== undefined) { return { kind: "subdirectory", path: args.subdirectory }; } if (args.plugin !== undefined) return { kind: "entry", name: args.plugin }; - return { kind: "root" }; + return undefined; } function pluginPath(pluginId: string, suffix = ""): string { @@ -424,10 +432,14 @@ export function createPluginsArea(args: CreateSdkAreaArgs): PluginsArea { "plugin install accepts subdirectory or plugin, not both", ); } - const body = pluginInstallSourceRequestSchema.parse({ - source: input.source, - selection: pluginSourceSelection(input), - }); + const selection = pluginSourceSelection(input); + const body = { + source: z.string().min(1).parse(input.source), + ...(selection === undefined ? {} : { selection }), + }; + // The contract still validates the shape; its root default is the + // server's to fill, so the parsed value is not what gets sent. + pluginInstallSourceRequestSchema.parse(body); const response = await requestParsed( "/api/v1/plugins/install", pluginInstallResponseSchema, diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk/test/sdk.test.ts index 4d27c39ff..ba3d651a2 100644 --- a/packages/sdk/test/sdk.test.ts +++ b/packages/sdk/test/sdk.test.ts @@ -1420,10 +1420,9 @@ describe("@bb/sdk", () => { url: "http://bb.test/api/v1/plugins", }, { - bodyText: JSON.stringify({ - source: "npm:@bb/notes@^1", - selection: { kind: "root" }, - }), + // A root install sends no `selection`: the server fills that default, + // and pre-0.38.0 servers reject the key outright (#1662). + bodyText: JSON.stringify({ source: "npm:@bb/notes@^1" }), method: "POST", url: "http://bb.test/api/v1/plugins/install", },