feat(plugins): install marketplace packages via dbx deep link

This commit is contained in:
t8y2 committed 2026-09-15 17:55:04 +08:00
1 parent 0437f952e0
commit 94bcbc1631
18 files changed
+223 -1

No files matched your search

+1
View File
@@ -420,6 +420,7 @@ export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles");
export const pendingOpenDbFiles = forward("pendingOpenDbFiles");
export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks");
export const pendingOpenAiConfigLinks = forward("pendingOpenAiConfigLinks");
export const pendingOpenPluginInstallLinks = forward("pendingOpenPluginInstallLinks");
export const readExternalSqlFile = forward("readExternalSqlFile");
export const readExternalSqlFileSnapshot = forward("readExternalSqlFileSnapshot");
export const inspectExternalSqlFile = forward("inspectExternalSqlFile");
+4
View File
@@ -2491,6 +2491,10 @@ export async function pendingOpenAiConfigLinks(): Promise<string[]> {
return [];
}
export async function pendingOpenPluginInstallLinks(): Promise<string[]> {
return [];
}
export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise<string> {
throw new Error("Opening external SQL file paths is only available in the desktop app");
}
+4
View File
@@ -1003,6 +1003,10 @@ export async function pendingOpenAiConfigLinks(): Promise<string[]> {
return invoke("pending_open_ai_config_links");
}
export async function pendingOpenPluginInstallLinks(): Promise<string[]> {
return invoke("pending_open_plugin_install_links");
}
export interface ExternalSqlFileSnapshot {
content: string;
version: ExternalSqlFileVersion;
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { parsePluginInstallDeepLink } from "../pluginInstallDeepLink";
const PACKAGE_URL = "https://dl.dbxio.com/plugins/io.dbx.ssh/0.4.73/io.dbx.ssh-0.4.73-darwin-arm64.dbxp";
describe("parsePluginInstallDeepLink", () => {
it("extracts the encoded package url", () => {
const draft = parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(PACKAGE_URL)}`);
expect(draft).toEqual({ url: PACKAGE_URL });
});
it("returns null for non-plugin links and junk", () => {
expect(parsePluginInstallDeepLink("dbx://connection/new?type=mysql")).toBeNull();
expect(parsePluginInstallDeepLink("dbx://plugins/installed?url=https://example.com/a.dbxp")).toBeNull();
expect(parsePluginInstallDeepLink("dbx://plugins/installation?url=https://example.com/a.dbxp")).toBeNull();
expect(parsePluginInstallDeepLink("")).toBeNull();
expect(parsePluginInstallDeepLink("not a url")).toBeNull();
});
it("throws for matching links without a usable url param", () => {
expect(() => parsePluginInstallDeepLink("dbx://plugins/install")).toThrow(/Missing url/);
expect(() => parsePluginInstallDeepLink("dbx://plugins/install?url=")).toThrow(/Missing url/);
expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent("ftp://example.com/a.dbxp")}`)).toThrow(/http/);
expect(() => parsePluginInstallDeepLink(`dbx://plugins/install?url=${encodeURIComponent(`https://example.com/${"a".repeat(2100)}`)}`)).toThrow(/too long/);
});
});
@@ -0,0 +1,31 @@
const PLUGIN_INSTALL_DEEP_LINK_HOST = "plugins";
const PLUGIN_INSTALL_DEEP_LINK_PATH = "/install";
const MAX_DEEP_LINK_LENGTH = 4096;
const MAX_PACKAGE_URL_LENGTH = 2048;
export interface PluginInstallDeepLinkDraft {
url: string;
}
/**
* Parses `dbx://plugins/install?url=<encoded package url>` links.
* Returns null for non-matching links and throws for malformed ones,
* mirroring the AI config deep-link parser contract.
*/
export function parsePluginInstallDeepLink(value: string): PluginInstallDeepLinkDraft | null {
const trimmed = value.trim();
if (!trimmed || trimmed.length > MAX_DEEP_LINK_LENGTH) return null;
if (!trimmed.startsWith("dbx://")) return null;
const url = new URL(trimmed);
if (url.host !== PLUGIN_INSTALL_DEEP_LINK_HOST || url.pathname !== PLUGIN_INSTALL_DEEP_LINK_PATH) return null;
const packageUrl = url.searchParams.get("url")?.trim() ?? "";
if (!packageUrl) throw new Error("Missing url");
if (packageUrl.length > MAX_PACKAGE_URL_LENGTH) throw new Error("Package url is too long");
const protocol = new URL(packageUrl).protocol;
if (protocol !== "http:" && protocol !== "https:") throw new Error("Package url must be http(s)");
return { url: packageUrl };
}