diff --git a/.github/workflows/plugin-cli-release.yml b/.github/workflows/plugin-cli-release.yml new file mode 100644 index 0000000000..009319b128 --- /dev/null +++ b/.github/workflows/plugin-cli-release.yml @@ -0,0 +1,361 @@ +name: Plugin CLI Release + +on: + push: + tags: + - "plugin-cli-v*" + workflow_dispatch: + inputs: + version: + description: "Plugin CLI version to publish, for example 0.1.0" + required: true + default: "0.1.0" + npm-tag: + description: "npm distribution tag" + required: true + default: "latest" + type: choice + options: + - latest + - next + +permissions: + attestations: write + contents: write + id-token: write + +concurrency: + group: plugin-cli-release + cancel-in-progress: false + +jobs: + prepare: + name: Validate plugin CLI release + runs-on: ubuntu-24.04 + outputs: + tag: ${{ steps.release.outputs.tag }} + version: ${{ steps.release.outputs.version }} + npm-tag: ${{ steps.release.outputs.npm-tag }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 22.13.0 + registry-url: https://registry.npmjs.org + + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/setup-go@v6 + with: + go-version: "1.22.x" + + - name: Validate release branch and versions + id: release + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_NPM_TAG: ${{ inputs.npm-tag }} + run: | + node <<'NODE' + const fs = require("fs"); + + let version; + let tag; + let npmTag; + if (process.env.GITHUB_EVENT_NAME === "push") { + tag = process.env.GITHUB_REF_NAME; + const match = /^plugin-cli-v(.+)$/.exec(tag); + if (!match) throw new Error(`Unexpected plugin CLI tag: ${tag}`); + version = match[1]; + npmTag = version.includes("-") ? "next" : "latest"; + } else { + if (process.env.GITHUB_REF !== "refs/heads/main") { + throw new Error(`Manual plugin CLI releases must run from main, received ${process.env.GITHUB_REF}.`); + } + version = process.env.INPUT_VERSION.trim(); + tag = `plugin-cli-v${version}`; + npmTag = process.env.INPUT_NPM_TAG; + } + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) { + throw new Error(`Invalid semantic version: ${version}`); + } + if (version.includes("-") && npmTag === "latest") { + throw new Error(`Prerelease ${version} cannot use the npm latest tag.`); + } + + const packagePaths = [ + "packages/plugin-cli/package.json", + "packages/plugin-cli-darwin-arm64/package.json", + "packages/plugin-cli-darwin-x64/package.json", + "packages/plugin-cli-linux-arm64-gnu/package.json", + "packages/plugin-cli-linux-x64-gnu/package.json", + "packages/plugin-cli-win32-arm64/package.json", + "packages/plugin-cli-win32-x64/package.json", + ]; + for (const path of packagePaths) { + const pkg = JSON.parse(fs.readFileSync(path, "utf8")); + if (pkg.version !== version) { + throw new Error(`${path} has version ${pkg.version}; expected ${version}.`); + } + } + + const cliCargo = fs.readFileSync("plugins/sdk/cli/Cargo.toml", "utf8"); + const cliVersion = cliCargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1]; + if (cliVersion !== version) { + throw new Error(`plugins/sdk/cli/Cargo.toml has version ${cliVersion}; expected ${version}.`); + } + + const launcher = JSON.parse(fs.readFileSync(packagePaths[0], "utf8")); + for (const [dependency, dependencyVersion] of Object.entries(launcher.optionalDependencies ?? {})) { + if (dependencyVersion !== version) { + throw new Error(`${dependency} is pinned to ${dependencyVersion}; expected ${version}.`); + } + } + + fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\n`); + fs.appendFileSync(process.env.GITHUB_OUTPUT, `npm-tag=${npmTag}\n`); + NODE + + - name: Check npm token + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -z "${NODE_AUTH_TOKEN}" ]; then + echo "::error::NPM_TOKEN secret is required to publish @dbx-app/plugin-cli." + exit 1 + fi + + - name: Test Rust CLI + run: cargo test --locked --manifest-path plugins/sdk/cli/Cargo.toml + + - name: Test npm launcher + run: npm --prefix packages/plugin-cli test + + - name: Verify packed npm installation + env: + DBX_PLUGIN_CLI_VERIFY_NATIVE: "1" + run: node scripts/verify-plugin-cli-package.mjs + + - name: Create immutable source tag + if: github.event_name == 'workflow_dispatch' + env: + TAG: ${{ steps.release.outputs.tag }} + run: | + git fetch origin --tags --force + if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then + TAG_SHA="$(git rev-list -n 1 "refs/tags/${TAG}")" + HEAD_SHA="$(git rev-parse HEAD)" + if [ "${TAG_SHA}" != "${HEAD_SHA}" ]; then + echo "::error::${TAG} already points to ${TAG_SHA}, not ${HEAD_SHA}." + exit 1 + fi + echo "${TAG} already points to this commit." + else + git tag "${TAG}" HEAD + git push origin "refs/tags/${TAG}:refs/tags/${TAG}" + fi + + publish-platforms: + name: Publish ${{ matrix.package-name }} + needs: prepare + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + target: aarch64-apple-darwin + package-dir: plugin-cli-darwin-arm64 + package-name: "@dbx-app/plugin-cli-darwin-arm64" + binary: dbx-plugin + - runner: macos-15-intel + target: x86_64-apple-darwin + package-dir: plugin-cli-darwin-x64 + package-name: "@dbx-app/plugin-cli-darwin-x64" + binary: dbx-plugin + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + package-dir: plugin-cli-linux-arm64-gnu + package-name: "@dbx-app/plugin-cli-linux-arm64-gnu" + binary: dbx-plugin + - runner: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + package-dir: plugin-cli-linux-x64-gnu + package-name: "@dbx-app/plugin-cli-linux-x64-gnu" + binary: dbx-plugin + - runner: windows-11-arm + target: aarch64-pc-windows-msvc + package-dir: plugin-cli-win32-arm64 + package-name: "@dbx-app/plugin-cli-win32-arm64" + binary: dbx-plugin.exe + - runner: windows-2025 + target: x86_64-pc-windows-msvc + package-dir: plugin-cli-win32-x64 + package-name: "@dbx-app/plugin-cli-win32-x64" + binary: dbx-plugin.exe + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.tag }} + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: actions/setup-python@v5 + if: runner.os == 'Linux' + with: + python-version: "3.x" + + - name: Install Linux compatibility builder + if: runner.os == 'Linux' + run: pip install ziglang==0.14.0 cargo-zigbuild==0.23.0 + + - name: Build precompiled CLI + shell: bash + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/target/plugin-cli + run: | + if [[ "${{ runner.os }}" == "Linux" ]]; then + cargo zigbuild --locked --release --manifest-path plugins/sdk/cli/Cargo.toml --target "${{ matrix.target }}.2.31" + else + cargo build --locked --release --manifest-path plugins/sdk/cli/Cargo.toml --target "${{ matrix.target }}" + fi + + - name: Stage platform package + shell: bash + run: | + mkdir -p "packages/${{ matrix.package-dir }}/bin" + cp "target/plugin-cli/${{ matrix.target }}/release/${{ matrix.binary }}" "packages/${{ matrix.package-dir }}/bin/${{ matrix.binary }}" + if [[ "${{ runner.os }}" != "Windows" ]]; then + chmod +x "packages/${{ matrix.package-dir }}/bin/${{ matrix.binary }}" + fi + + - uses: actions/setup-node@v6 + with: + node-version: 22.13.0 + registry-url: https://registry.npmjs.org + + - name: Publish platform package + shell: bash + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ needs.prepare.outputs.version }} + NPM_TAG: ${{ needs.prepare.outputs.npm-tag }} + run: | + if npm view "${{ matrix.package-name }}@${VERSION}" version >/dev/null 2>&1; then + echo "${{ matrix.package-name }}@${VERSION} already exists; refreshing ${NPM_TAG}." + npm dist-tag add "${{ matrix.package-name }}@${VERSION}" "${NPM_TAG}" + else + npm publish "./packages/${{ matrix.package-dir }}" --access public --provenance --tag "${NPM_TAG}" + fi + + publish-launcher: + name: Publish @dbx-app/plugin-cli + needs: [prepare, publish-platforms] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.prepare.outputs.tag }} + + - uses: actions/setup-node@v6 + with: + node-version: 22.13.0 + registry-url: https://registry.npmjs.org + + - name: Wait for platform packages + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + wait_for_package() { + package="$1" + for attempt in $(seq 1 18); do + if npm view "${package}@${VERSION}" version >/dev/null 2>&1; then + return 0 + fi + echo "Waiting for ${package}@${VERSION} (${attempt}/18)." + sleep 10 + done + return 1 + } + + for package in \ + @dbx-app/plugin-cli-darwin-arm64 \ + @dbx-app/plugin-cli-darwin-x64 \ + @dbx-app/plugin-cli-linux-arm64-gnu \ + @dbx-app/plugin-cli-linux-x64-gnu \ + @dbx-app/plugin-cli-win32-arm64 \ + @dbx-app/plugin-cli-win32-x64; do + wait_for_package "${package}" || { + echo "::error::${package}@${VERSION} is unavailable; refusing to publish the launcher." + exit 1 + } + done + + - name: Publish launcher and bundled SDK + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ needs.prepare.outputs.version }} + NPM_TAG: ${{ needs.prepare.outputs.npm-tag }} + run: | + if npm view "@dbx-app/plugin-cli@${VERSION}" version >/dev/null 2>&1; then + echo "@dbx-app/plugin-cli@${VERSION} already exists; refreshing ${NPM_TAG}." + npm dist-tag add "@dbx-app/plugin-cli@${VERSION}" "${NPM_TAG}" + else + npm publish ./packages/plugin-cli --access public --provenance --tag "${NPM_TAG}" + fi + + verify-published-package: + name: Verify clean npm installation + needs: [prepare, publish-launcher] + runs-on: ubuntu-24.04 + steps: + - uses: actions/setup-node@v6 + with: + node-version: 22.13.0 + registry-url: https://registry.npmjs.org + + - uses: dtolnay/rust-toolchain@stable + + - uses: actions/setup-go@v6 + with: + go-version: "1.22.x" + + - name: Install from npm and package every template + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + work="$(mktemp -d)" + cd "${work}" + npm init -y >/dev/null + + for attempt in $(seq 1 18); do + if npm view "@dbx-app/plugin-cli@${VERSION}" version >/dev/null 2>&1; then + break + fi + if [ "${attempt}" -eq 18 ]; then + echo "::error::@dbx-app/plugin-cli@${VERSION} did not become visible on npm." + exit 1 + fi + sleep 10 + done + + npm install --ignore-scripts --no-audit --no-fund "@dbx-app/plugin-cli@${VERSION}" + ./node_modules/.bin/dbx-plugin --version + + for template in frontend rust go; do + ./node_modules/.bin/dbx-plugin create "${template}-plugin" \ + --template "${template}" \ + --yes \ + --id "com.example.npm-${template}" \ + --name "npm ${template} smoke" \ + --publisher example \ + --description "Published npm package smoke" + ./node_modules/.bin/dbx-plugin package "${template}-plugin" + test "$(find "${template}-plugin/dist" -maxdepth 1 -name '*.dbxp' | wc -l)" -eq 1 + done diff --git a/.github/workflows/plugin-release-reusable.yml b/.github/workflows/plugin-release-reusable.yml new file mode 100644 index 0000000000..0b55e1caf1 --- /dev/null +++ b/.github/workflows/plugin-release-reusable.yml @@ -0,0 +1,216 @@ +name: Reusable DBX plugin release + +on: + workflow_call: + inputs: + release-tag: + description: Existing GitHub Release tag that receives the plugin assets + required: true + type: string + package-command: + description: Command that builds one unsigned candidate .dbxp and one .artifact.json for DBX_PLUGIN_TARGET + required: true + type: string + working-directory: + description: Plugin repository directory containing the package command + default: . + required: false + type: string + package-path: + description: Glob relative to working-directory for built .dbxp files + default: dist/*.dbxp + required: false + type: string + metadata-path: + description: Glob relative to working-directory for built .artifact.json files + default: dist/*.artifact.json + required: false + type: string + node-version: + default: "22" + required: false + type: string + go-version: + default: "1.22.x" + required: false + type: string + rust-toolchain: + default: stable + required: false + type: string + sdk-ref: + description: DBX tag or commit used only when installing the plugin CLI from source + default: plugin-sdk-v1 + required: false + type: string + plugin-cli-version: + description: Published @dbx-app/plugin-cli version used to package the plugin + default: "0.1.0" + required: false + type: string + install-plugin-cli: + description: Install the dbx-plugin CLI before packaging + default: true + required: false + type: boolean + install-plugin-cli-from-source: + description: Build the CLI from sdk-ref instead of installing its precompiled npm package + default: false + required: false + type: boolean + build-matrix: + description: JSON matrix with runner and DBX target pairs; use one universal entry for platform-independent plugins + default: >- + {"include":[{"runner":"ubuntu-24.04","target":"linux-x64"},{"runner":"ubuntu-24.04-arm","target":"linux-arm64"},{"runner":"windows-2022","target":"windows-x64"},{"runner":"macos-15-intel","target":"darwin-x64"},{"runner":"macos-15","target":"darwin-arm64"}]} + required: false + type: string +permissions: + contents: read + +jobs: + build: + name: Build ${{ matrix.target }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(inputs.build-matrix) }} + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ inputs.rust-toolchain }} + + - name: Checkout DBX plugin SDK + if: inputs.install-plugin-cli && inputs.install-plugin-cli-from-source + uses: actions/checkout@v4 + with: + repository: t8y2/dbx + ref: ${{ inputs.sdk-ref }} + path: .dbx-plugin-sdk + + - name: Install precompiled DBX plugin CLI + if: inputs.install-plugin-cli && !inputs.install-plugin-cli-from-source + shell: bash + run: npm install --global "@dbx-app/plugin-cli@${{ inputs.plugin-cli-version }}" + + - name: Install DBX plugin CLI from source + if: inputs.install-plugin-cli && inputs.install-plugin-cli-from-source + shell: bash + run: cargo install --locked --path .dbx-plugin-sdk/plugins/sdk/cli + + - name: Build unsigned plugin candidate + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + DBX_PLUGIN_TARGET: ${{ matrix.target }} + DBX_PLUGIN_SDK_ROOT: ${{ inputs.install-plugin-cli-from-source && format('{0}/.dbx-plugin-sdk', github.workspace) || '' }} + run: ${{ inputs.package-command }} + + - name: Upload target artifacts + uses: actions/upload-artifact@v4 + with: + name: dbx-plugin-${{ matrix.target }} + if-no-files-found: error + retention-days: 1 + path: | + ${{ inputs.working-directory }}/${{ inputs.package-path }} + ${{ inputs.working-directory }}/${{ inputs.metadata-path }} + + publish: + name: Publish plugin release assets + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Download target artifacts + uses: actions/download-artifact@v4 + with: + pattern: dbx-plugin-* + path: release-assets + merge-multiple: true + + - name: Validate and merge candidate metadata + shell: bash + run: | + node <<'NODE' + const childProcess = require("node:child_process"); + const crypto = require("node:crypto"); + const fs = require("node:fs"); + const path = require("node:path"); + + function filesUnder(root) { + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const current = path.join(root, entry.name); + return entry.isDirectory() ? filesUnder(current) : [current]; + }); + } + + const files = filesUnder("release-assets"); + const metadataFiles = files.filter((file) => file.endsWith(".artifact.json")); + const packages = new Map(); + for (const file of files.filter((file) => file.endsWith(".dbxp"))) { + const name = path.basename(file); + if (packages.has(name)) throw new Error(`Duplicate package filename ${name}`); + packages.set(name, file); + } + if (metadataFiles.length === 0 || packages.size === 0) throw new Error("Release produced no DBX plugin artifacts"); + + const targets = new Set(); + let pluginIdentity; + const artifacts = metadataFiles.map((file) => { + const artifact = JSON.parse(fs.readFileSync(file, "utf8")); + if (!/^[a-z0-9-]{1,64}$/.test(artifact.target || "")) throw new Error(`Invalid artifact target in ${file}`); + if (!/^[a-fA-F0-9]{64}$/.test(artifact.sha256 || "")) throw new Error(`Invalid artifact SHA-256 in ${file}`); + if (artifact.signingKeyId !== undefined) throw new Error(`Candidate metadata ${file} must not declare signingKeyId`); + if (!Number.isSafeInteger(artifact.size) || artifact.size < 0) throw new Error(`Invalid artifact size in ${file}`); + const packageName = path.basename(new URL(artifact.url, "https://plugins.invalid/releases/").pathname); + const packageFile = packages.get(packageName); + if (!packageFile) throw new Error(`Artifact metadata ${file} references missing package ${packageName}`); + const packageBytes = fs.readFileSync(packageFile); + const packageSha256 = crypto.createHash("sha256").update(packageBytes).digest("hex"); + if (artifact.size !== packageBytes.length) throw new Error(`Artifact metadata ${file} has the wrong package size`); + if (artifact.sha256.toLowerCase() !== packageSha256) throw new Error(`Artifact metadata ${file} has the wrong package SHA-256`); + const entries = childProcess.execFileSync("unzip", ["-Z1", packageFile], { encoding: "utf8" }).split(/\r?\n/); + if (entries.includes("signature.json")) throw new Error(`Candidate package ${packageName} must be unsigned`); + const manifest = JSON.parse(childProcess.execFileSync("unzip", ["-p", packageFile, "manifest.json"], { encoding: "utf8" })); + const identity = { + id: manifest.id, + name: manifest.name, + description: manifest.description || "", + publisher: manifest.publisher, + version: manifest.version, + permissions: [...(manifest.permissions || [])].sort(), + }; + if (!identity.id || !identity.name || !identity.publisher || !identity.version) throw new Error(`Package ${packageName} has incomplete manifest identity`); + if (pluginIdentity && JSON.stringify(pluginIdentity) !== JSON.stringify(identity)) throw new Error(`Package ${packageName} manifest identity differs from other targets`); + pluginIdentity ||= identity; + if (targets.has(artifact.target)) throw new Error(`Duplicate artifact target ${artifact.target}`); + targets.add(artifact.target); + return artifact; + }).sort((left, right) => left.target.localeCompare(right.target)); + + fs.writeFileSync("release-assets/release-candidates.json", `${JSON.stringify({ plugin: pluginIdentity, artifacts }, null, 2)}\n`); + NODE + + - name: Upload assets to GitHub Release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + mapfile -d '' packages < <(find release-assets -type f -name '*.dbxp' -print0) + if [ "${#packages[@]}" -eq 0 ]; then + echo "No .dbxp packages were downloaded" >&2 + exit 1 + fi + gh release upload "$RELEASE_TAG" "${packages[@]}" release-assets/release-candidates.json --clobber diff --git a/.gitignore b/.gitignore index ba7f012d58..24c04e2e27 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ plugins/jdbc/build/ plugins/jdbc/bin/main/ plugins/jdbc/bin/test/ plugins/jdbc/dist/ +packages/plugin-cli/sdk-root/ docs/.next/ docs/out/ docs/.source/ @@ -65,6 +66,7 @@ Thumbs.db .env .env.* !.env.example +.dbx-plugin-signing-key.env # Temp tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6bf524497..9340b42ce3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,7 @@ For a real local Java agent test, build the target `shadowJar`, back up and repl | `crates/dbx-web/` | Docker / Web HTTP backend | | `packages/cli/` | `@dbx-app/cli` | | `packages/mcp-server/` | `@dbx-app/mcp-server` | +| `packages/plugin-cli/` | Precompiled `@dbx-app/plugin-cli` launcher and bundled plugin SDKs | | `packages/mongo-shell/` | Private MongoDB editor parsing helpers | | `docs/` | Official documentation site | | `examples/` | Sample configs and automation scripts | diff --git a/Cargo.lock b/Cargo.lock index fea02f8576..2ae52b7acc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1916,6 +1916,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto 0.2.9", "rustc_version", "subtle", @@ -2092,9 +2093,11 @@ dependencies = [ "csv", "dbx-sqlite-worker", "deadpool-postgres", + "ed25519-dalek 2.2.0", "encoding_rs", "flate2", "font-kit", + "fs2", "futures", "httpdate", "iana-time-zone", @@ -2126,6 +2129,7 @@ dependencies = [ "rust_decimal", "rustls 0.23.43", "rustls-pemfile 2.2.0", + "semver", "serde", "serde_json", "serde_yaml", @@ -2587,19 +2591,43 @@ dependencies = [ "digest 0.11.3", "elliptic-curve", "rfc6979", - "signature", + "signature 3.0.0", "spki 0.8.0-rc.4", "zeroize", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + [[package]] name = "ed25519" version = "3.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.11.0-rc.11", + "signature 3.0.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", ] [[package]] @@ -2609,11 +2637,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" dependencies = [ "curve25519-dalek 5.0.0-pre.6", - "ed25519", + "ed25519 3.0.0-rc.4", "rand_core 0.10.1", "serde", "sha2 0.11.0", - "signature", + "signature 3.0.0", "subtle", "zeroize", ] @@ -2638,7 +2666,7 @@ dependencies = [ "hybrid-array", "once_cell", "pem-rfc7468 1.0.0", - "pkcs8", + "pkcs8 0.11.0-rc.11", "rand_core 0.10.1", "rustcrypto-ff", "rustcrypto-group", @@ -3035,6 +3063,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -4210,7 +4248,7 @@ dependencies = [ "bcrypt-pbkdf", "crypto-bigint", "ecdsa", - "ed25519-dalek", + "ed25519-dalek 3.0.0-pre.6", "hex", "hmac 0.13.0", "num-bigint-dig", @@ -4222,7 +4260,7 @@ dependencies = [ "sec1", "sha1 0.11.0", "sha2 0.11.0", - "signature", + "signature 3.0.0", "ssh-cipher", "ssh-encoding", "subtle", @@ -6097,6 +6135,16 @@ dependencies = [ "spki 0.8.0-rc.4", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.11.0-rc.11" @@ -7053,10 +7101,10 @@ dependencies = [ "crypto-primes", "digest 0.11.3", "pkcs1", - "pkcs8", + "pkcs8 0.11.0-rc.11", "rand_core 0.10.1", "sha2 0.11.0", - "signature", + "signature 3.0.0", "spki 0.8.0-rc.4", "zeroize", ] @@ -7121,7 +7169,7 @@ dependencies = [ "der 0.8.1", "digest 0.10.7", "ecdsa", - "ed25519-dalek", + "ed25519-dalek 3.0.0-pre.6", "elliptic-curve", "enum_dispatch", "flate2", @@ -7150,7 +7198,7 @@ dependencies = [ "pbkdf2 0.13.0", "pkcs1", "pkcs5", - "pkcs8", + "pkcs8 0.11.0-rc.11", "polyval 0.7.3", "rand 0.10.2", "rand_core 0.10.1", @@ -7165,7 +7213,7 @@ dependencies = [ "sha2 0.10.9", "sha2 0.11.0", "sha3", - "signature", + "signature 3.0.0", "spki 0.8.0-rc.4", "ssh-encoding", "subtle", @@ -8034,6 +8082,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "3.0.0" diff --git a/README.md b/README.md index cd6f290dad..93b42359a4 100644 --- a/README.md +++ b/README.md @@ -458,6 +458,8 @@ make docs The official DBX documentation site lives in `docs/`. If you want to improve the website content or documentation pages, edit the files under `docs/` and run `make docs` to preview the site locally. +Plugin authors should start with [Develop and Submit DBX Plugins](https://dbxio.com/en/docs/plugin-development). Plugin source normally stays in its own repository; Marketplace listing pull requests go to [`t8y2/dbx-store`](https://github.com/t8y2/dbx-store), while plugin host, SDK, and CLI changes go to this repository. + For clean, reproducible local database instances, use the versioned Docker Compose recipes under [`deploy/database/`](deploy/database/README.md): ```bash @@ -495,6 +497,7 @@ The installer will be in `src-tauri/target/release/bundle/`. ## Documentation - [Official docs](https://dbxio.com/en/docs/what-is-dbx) — feature guides and tutorials +- [Plugin development](https://dbxio.com/en/docs/plugin-development) — create plugins and submit Marketplace listings to the correct repository - [Database Test Lab](https://dbxio.com/en/docs/database-lab) — local database recipes for development and verification - [Contributing](CONTRIBUTING.md) — how to pick up issues and open PRs - [Web API reference](docs/content/docs/web-api.mdx) — HTTP API for Docker/Web deployments diff --git a/SECURITY.md b/SECURITY.md index 31abe6da34..aba4865b51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,5 +26,8 @@ Security-sensitive areas include: - AI provider keys and OpenAI-compatible endpoint configuration. - MCP and CLI access to local DBX connections. - Docker web service authentication and data directory handling. +- Plugin package verification, native sidecar execution, sandboxed plugin UI, host bridge permissions, and plugin connection secrets. + +Plugin connection secrets are stored outside ordinary connection JSON. Unencrypted sync snapshots are always redacted. If encrypted secret sync is enabled with a user-provided passphrase, plugin secrets are included only in the encrypted payload and are restored through the normal secret-store path. Please avoid testing against systems you do not own or have explicit permission to assess. diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index a997fa7b33..e898a866f6 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -34,7 +34,7 @@ import { type DriverInstallProgress, } from "@/lib/connection/driverInstallProgressUi"; import { installRegisteredManagedJdbcDriver, isManagedJdbcDriver, managedJdbcDriverRows, uninstallRegisteredManagedJdbcDriver } from "@/lib/database/managedJdbcDrivers"; -import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; +import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDriverInstallHint"; import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection"; import { translateBackendError } from "@/i18n/backend-errors"; import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExportFlow"; @@ -55,7 +55,7 @@ function backendError(e: unknown): string { const props = withDefaults( defineProps<{ updateNotificationsEnabled?: boolean; - activeTab?: "agent" | "jdbc" | "storage" | "runtime"; + activeTab?: DriverStoreTab; focusTarget?: DriverStoreFocus | null; }>(), { @@ -67,12 +67,12 @@ const props = withDefaults( const emit = defineEmits<{ "update-count-change": [count: number]; - "update:activeTab": [tab: "agent" | "jdbc" | "storage" | "runtime"]; + "update:activeTab": [tab: DriverStoreTab]; }>(); const driverStoreTab = computed({ get: () => props.activeTab, - set: (tab: "agent" | "jdbc" | "storage" | "runtime") => emit("update:activeTab", tab), + set: (tab: DriverStoreTab) => emit("update:activeTab", tab), }); // ──────────── Driver store path ──────────── @@ -1444,7 +1444,7 @@ watch(driverStoreTab, (tab) => {
- + {{ t("driverStore.agentDrivers") }} @@ -2263,8 +2263,8 @@ watch(driverStoreTab, (tab) => { .driver-store-tabs { display: grid !important; - width: 360px !important; - grid-template-columns: repeat(3, minmax(0, 1fr)) !important; + width: 480px !important; + grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } .driver-store-tabs-root { diff --git a/apps/desktop/src/components/layout/AppDialogs.vue b/apps/desktop/src/components/layout/AppDialogs.vue index 599fcd0cca..594adc0dd4 100644 --- a/apps/desktop/src/components/layout/AppDialogs.vue +++ b/apps/desktop/src/components/layout/AppDialogs.vue @@ -32,10 +32,12 @@ import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; import type { SqlParameterDescriptor, SqlParameterSyntax } from "@/lib/sql/sqlParameters"; import type { ConfigTab } from "@/components/connection/ConnectionDialog.vue"; import type { DatabaseType } from "@/types/database"; +import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation"; const props = defineProps<{ showConnectionDialog: boolean; connectionPrefill?: ConnectionDeepLinkDraft | null; + connectionPluginProvider?: PluginCenterFocus | null; connectionInitialTab?: ConfigTab; showDangerDialog: boolean; dangerSql: string; @@ -140,7 +142,8 @@ const editConfig = computed(() => { const shouldShowConnectionDialog = computed(() => props.showConnectionDialog || !!editConfig.value); watch(editConfig, (v) => { - if (v) emit("update:showConnectionDialog", true); + if (!v) return; + emit("update:showConnectionDialog", true); }); watch( @@ -167,6 +170,7 @@ watch( :open="shouldShowConnectionDialog" :edit-config="editConfig" :prefill-config="connectionPrefill" + :plugin-provider="connectionPluginProvider" :initial-tab="connectionInitialTab" @update:open="emit('update:showConnectionDialog', $event)" @connect-started="emit('connectStarted', $event)" diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index a2397b346f..0cb8271db9 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -123,6 +123,8 @@ const XuguServerDashboard = defineAsyncComponent(() => import("@/components/admi const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/DamengJobAdmin.vue")); const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue")); const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue")); +const PluginWorkbenchTab = defineAsyncComponent(() => import("@/components/plugins/PluginWorkbenchTab.vue")); +const PluginFilesystemTab = defineAsyncComponent(() => import("@/components/plugins/PluginFilesystemTab.vue")); const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue")); const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue")); import { useQueryStore } from "@/stores/queryStore"; @@ -2501,6 +2503,23 @@ defineExpose({
+ +