From 8033b3bcd0847f75791b79f6f45b521ec2b32d5f Mon Sep 17 00:00:00 2001 From: callumalpass Date: Fri, 30 Oct 2026 23:41:08 +1100 Subject: [PATCH 1/2] ci(release): add the production Connect repin reference --- .github/workflows/deploy-reader.yml | 22 ++-- .github/workflows/sdk-repin.yml | 88 ++++++++++++++++ package.json | 1 + scripts/sdk-repin.mjs | 112 +++++++++++++++++++++ scripts/tests/sdk-repin.test.mjs | 150 ++++++++++++++++++++++++++++ 5 files changed, 365 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/sdk-repin.yml create mode 100644 scripts/sdk-repin.mjs create mode 100644 scripts/tests/sdk-repin.test.mjs diff --git a/.github/workflows/deploy-reader.yml b/.github/workflows/deploy-reader.yml index a70e1c44..35541bcb 100644 --- a/.github/workflows/deploy-reader.yml +++ b/.github/workflows/deploy-reader.yml @@ -1,7 +1,13 @@ name: Deploy Reader -# Lab deploys from CI on every push to main. Staging and production are explicit. +# Dependency merges use the existing production publisher; manual paths remain. on: + push: + branches: [main] + paths: + - "**/package.json" + - package.json + - pnpm-lock.yaml workflow_dispatch: inputs: target: @@ -27,18 +33,18 @@ jobs: deploy: if: inputs.source_sha == '' - name: Deploy Reader to ${{ inputs.target }} + name: Deploy Reader to ${{ inputs.target || 'production' }} needs: ci runs-on: ubuntu-24.04 timeout-minutes: 15 concurrency: - group: deploy-reader-${{ inputs.target }} + group: deploy-reader-${{ inputs.target || 'production' }} cancel-in-progress: false environment: - name: reader-${{ inputs.target }} - url: ${{ inputs.target == 'production' && 'https://reader.mdbase.dev/' || 'https://staging.mdbase-reader.pages.dev/' }} + name: reader-${{ inputs.target || 'production' }} + url: ${{ inputs.target != 'staging' && 'https://reader.mdbase.dev/' || 'https://staging.mdbase-reader.pages.dev/' }} env: - MDBASE_ENV: ${{ inputs.target }} + MDBASE_ENV: ${{ inputs.target || 'production' }} steps: - name: Require main if: github.ref != 'refs/heads/main' @@ -62,7 +68,7 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile # deploy-pages.mjs refuses a dirty tree for staging and production. - - run: pnpm ${{ inputs.target == 'production' && 'deploy:prod' || 'deploy:dev' }} + - run: pnpm ${{ inputs.target != 'staging' && 'deploy:prod' || 'deploy:dev' }} env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -70,7 +76,7 @@ jobs: VITE_MDBASE_FEEDBACK_TURNSTILE_SITE_KEY: ${{ vars.MDBASE_FEEDBACK_TURNSTILE_SITE_KEY }} - name: Verify deployed manifest env: - ORIGIN: ${{ inputs.target == 'production' && 'https://reader.mdbase.dev' || 'https://staging.mdbase-reader.pages.dev' }} + ORIGIN: ${{ inputs.target != 'staging' && 'https://reader.mdbase.dev' || 'https://staging.mdbase-reader.pages.dev' }} run: | for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do homepage=$(curl -fsS "$ORIGIN/.well-known/mdbase-app.json" | jq -r .homepage || true) diff --git a/.github/workflows/sdk-repin.yml b/.github/workflows/sdk-repin.yml new file mode 100644 index 00000000..e57c3c85 --- /dev/null +++ b/.github/workflows/sdk-repin.yml @@ -0,0 +1,88 @@ +name: Repin production Connect packages + +on: + workflow_dispatch: + inputs: + connect_release: + description: Production-verified Connect tag (v0.1.0-beta.N) + required: true + type: string + +permissions: + contents: read + +concurrency: + group: sdk-repin + cancel-in-progress: false + +jobs: + repin: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Mint the release automation App token + id: app + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_AUTOMATION_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_AUTOMATION_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + - name: Mint read-only access to the production record + id: ops + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_AUTOMATION_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_AUTOMATION_APP_PRIVATE_KEY }} + owner: mdbase-dev + repositories: mdbase-cloud-ops + permission-contents: read + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + token: ${{ steps.app.outputs.token }} + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 22 + package-manager-cache: false + - name: Validate production identity and plan exact pins + id: pins + env: + GH_TOKEN: ${{ steps.ops.outputs.token }} + CONNECT_RELEASE: ${{ inputs.connect_release }} + run: node scripts/sdk-repin.mjs + - name: Refresh the existing lockfile + if: steps.pins.outputs.changed == 'true' + run: pnpm install --lockfile-only --ignore-scripts + - name: Open or update the single release PR + if: steps.pins.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + APP_SLUG: ${{ steps.app.outputs.app-slug }} + CONNECT_RELEASE: ${{ inputs.connect_release }} + CONNECT_COMMIT: ${{ steps.pins.outputs.commit }} + run: | + if git diff --quiet; then exit 0; fi + branch=automation/sdk-repin + previous=$(git ls-remote origin "refs/heads/$branch" | awk '{print $1}') + git switch --create "$branch" + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + git ls-files -z -- '*package.json' pnpm-lock.yaml | xargs -0 git add -- + git diff --cached --quiet && exit 0 + git commit -m "Pin production Connect packages to $CONNECT_RELEASE" + git push --force-with-lease="refs/heads/$branch:$previous" origin "HEAD:refs/heads/$branch" + title="Pin production Connect packages to $CONNECT_RELEASE" + body=$(printf 'Connect: %s, commit %s.\n\nTrain run: https://github.com/%s/actions/runs/%s\n\nExact npm pins and pnpm lockfile only. App CI and normal branch protections gate auto-merge. Reader web uses the existing production publisher on dependency merges; extension version/tag/store handoff remains separate.\n' "$CONNECT_RELEASE" "$CONNECT_COMMIT" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID") + pull=$(gh pr list --head "$branch" --base main --state open --json number --jq '.[0].number // empty') + if [ -n "$pull" ]; then + gh pr edit "$pull" --title "$title" --body "$body" + else + gh pr create --head "$branch" --base main --title "$title" --body "$body" + pull=$(gh pr list --head "$branch" --base main --state open --json number --jq '.[0].number') + fi + gh pr merge "$pull" --auto --squash diff --git a/package.json b/package.json index d07f9f79..d5cca35e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test:workspaces": "pnpm -r --if-present test", "test:architecture": "node --test scripts/lib/architecture-check.test.mjs", "test:publisher": "node --test scripts/tests/*.test.mjs", + "test:sdk-repin": "node --test scripts/tests/sdk-repin.test.mjs", "test:publisher:evidence": "node --test scripts/tests/reader-evidence.test.mjs", "test:publisher:review": "node --test scripts/review-tests/*.test.mjs", "test:publisher:offline": "node --test scripts/tests/reader-offline.test.mjs", diff --git a/scripts/sdk-repin.mjs b/scripts/sdk-repin.mjs new file mode 100644 index 00000000..c7f1e2f0 --- /dev/null +++ b/scripts/sdk-repin.mjs @@ -0,0 +1,112 @@ +import { execFileSync } from "node:child_process"; +import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +export function productionIdentity(record, release, tag) { + if (!/^v0\.1\.0-beta\.[1-9][0-9]*$/.test(release)) { + throw new Error("connect_release must be v0.1.0-beta.N"); + } + const recordedRelease = record.match(/^MDBASE_CONNECT_RELEASE=(.+)$/m)?.[1]; + const commit = record.match(/^MDBASE_CONNECT_COMMIT=([0-9a-f]{40})$/m)?.[1]; + if (recordedRelease !== release || !commit || tag !== commit) { + throw new Error("Connect tag does not match ops main's production record"); + } + return commit; +} + +export function repinPlan(manifests, version) { + const packages = new Set(["@mdbase-dev/sdk"]); + const changes = []; + for (const { path, text } of manifests) { + const manifest = JSON.parse(text); + let changed = false; + for (const section of [ + "dependencies", + "devDependencies", + "optionalDependencies", + "peerDependencies", + ]) { + for (const [name, pin] of Object.entries(manifest[section] ?? {})) { + if (!name.startsWith("@mdbase-dev/")) continue; + if (name !== "@mdbase-dev/sdk" && /^(file:|workspace:|link:)/.test(pin)) continue; + if (/^@mdbase-dev\/sdk-/.test(name)) continue; + if (/^(npm:|git|https?:)/.test(pin)) { + throw new Error(`Unsupported runtime pin: ${path}: ${name}`); + } + packages.add(name); + if (pin !== version) { + manifest[section][name] = version; + changed = true; + } + } + } + if (changed) { + changes.push({ path, text: JSON.stringify(manifest, null, 2) + "\n" }); + } + } + return { packages: [...packages].sort(), changes }; +} + +function execute(...args) { + return execFileSync(args[0], args.slice(1), { + encoding: "utf8", + timeout: 60_000, + stdio: ["ignore", "pipe", "pipe"], + }).trim(); +} + +function main() { + const release = process.env.CONNECT_RELEASE ?? ""; + if (!/^v0\.1\.0-beta\.[1-9][0-9]*$/.test(release)) { + throw new Error("connect_release must be v0.1.0-beta.N"); + } + const record = JSON.parse( + execute("gh", "api", "repos/mdbase-dev/mdbase-cloud-ops/contents/render/release.env?ref=main"), + ); + const tag = execute( + "git", + "ls-remote", + "https://github.com/mdbase-dev/mdbase-connect.git", + `refs/tags/${release}^{}`, + ).split(/\s/)[0]; + const commit = productionIdentity( + Buffer.from(record.content, "base64").toString("utf8"), + release, + tag, + ); + const manifests = execute("git", "ls-files", "-z", "*package.json") + .split("\0") + .filter(Boolean) + .map((path) => ({ path, text: readFileSync(path, "utf8") })); + const plan = repinPlan(manifests, release.slice(1)); + for (const name of plan.packages) { + let published; + try { + published = JSON.parse( + execute( + "npm", + "view", + `${name}@${release.slice(1)}`, + "version", + "--json", + "--registry=https://registry.npmjs.org", + "--fetch-retries=0", + ), + ); + } catch { + throw new Error( + `${name}@${release.slice(1)} is unavailable; no partial repin. SDK npm publication requires Connect PR 1076 and a published production release.`, + ); + } + if (published !== release.slice(1)) { + throw new Error(`Registry identity mismatch for ${name}`); + } + } + for (const change of plan.changes) writeFileSync(change.path, change.text); + appendFileSync( + process.env.GITHUB_OUTPUT, + `changed=${plan.changes.length > 0}\ncommit=${commit}\n`, + ); +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) main(); diff --git a/scripts/tests/sdk-repin.test.mjs b/scripts/tests/sdk-repin.test.mjs new file mode 100644 index 00000000..d11c0385 --- /dev/null +++ b/scripts/tests/sdk-repin.test.mjs @@ -0,0 +1,150 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { productionIdentity, repinPlan } from "../sdk-repin.mjs"; + +const commit = "a".repeat(40); +const release = "v0.1.0-beta.148"; +const version = release.slice(1); +const record = `MDBASE_CONNECT_RELEASE=${release}\nMDBASE_CONNECT_COMMIT=${commit}\n`; + +test("only the exact annotated production identity is accepted", () => { + assert.equal(productionIdentity(record, release, commit), commit); + for (const [input, tag] of [ + ["latest", commit], + ["v0.1.0-beta.147", commit], + [release, "b".repeat(40)], + [release, ""], + ]) { + assert.throws(() => productionIdentity(record, input, tag)); + } +}); + +test("all npm pins and production SDK advance together, aliases/originals stay", () => { + const plan = repinPlan( + [ + { + path: "package.json", + text: JSON.stringify({ + dependencies: { + "@mdbase-dev/ui": "^0.1.0-beta.147", + "@mdbase-dev/sdk": "file:vendor/original.tgz", + "@mdbase-dev/sdk-staging": "file:vendor/staging.tgz", + "@mdbase-dev/sdk-lab": "file:vendor/lab.tgz", + "@mdbase-dev/sdk-legacy-729b5391": "file:vendor/legacy.tgz", + "@mdbase-dev/connect-protocol": "file:vendor/protocol.tgz", + "@other/package": "1.0.0", + }, + devDependencies: { "@mdbase-dev/connect-dev": "0.1.0-beta.125" }, + optionalDependencies: { "@mdbase-dev/i18n": "~0.1.0-beta.147" }, + }), + }, + ], + version, + ); + const changed = JSON.parse(plan.changes[0].text); + assert.equal(changed.dependencies["@mdbase-dev/ui"], version); + assert.equal(changed.dependencies["@mdbase-dev/sdk"], version); + assert.equal(changed.devDependencies["@mdbase-dev/connect-dev"], version); + assert.equal(changed.optionalDependencies["@mdbase-dev/i18n"], version); + assert.equal(changed.dependencies["@mdbase-dev/sdk-staging"], "file:vendor/staging.tgz"); + assert.equal(changed.dependencies["@mdbase-dev/connect-protocol"], "file:vendor/protocol.tgz"); + assert.equal(changed.dependencies["@other/package"], "1.0.0"); + assert.deepEqual(plan.packages, [ + "@mdbase-dev/connect-dev", + "@mdbase-dev/i18n", + "@mdbase-dev/sdk", + "@mdbase-dev/ui", + ]); +}); + +test("matching exact pins do not rewrite formatting or create a PR", () => { + const plan = repinPlan( + [ + { + path: "package.json", + text: JSON.stringify({ dependencies: { "@mdbase-dev/sdk": version } }), + }, + ], + version, + ); + assert.deepEqual(plan.changes, []); +}); + +test("unexpected remote pin schemes refuse rather than silently migrate", () => { + assert.throws(() => + repinPlan( + [ + { + path: "package.json", + text: JSON.stringify({ + dependencies: { "@mdbase-dev/ui": "https://example.test/ui.tgz" }, + }), + }, + ], + version, + ), + ); +}); + +test("missing SDK refuses atomically; successful repin and repeat no-op use exact versions", () => { + const directory = mkdtempSync(join(tmpdir(), "reader-sdk-repin-")); + try { + const binaries = join(directory, "bin"); + mkdirSync(binaries); + const original = JSON.stringify({ + dependencies: { + "@mdbase-dev/ui": "0.1.0-beta.147", + "@mdbase-dev/sdk": "file:original-sdk.tgz", + }, + }); + writeFileSync(join(directory, "package.json"), original); + const fake = `#!${process.execPath} +const command = process.argv[1].split('/').pop(); +if (command === 'gh') console.log(JSON.stringify({content: Buffer.from(${JSON.stringify(record)}).toString('base64')})); +else if (command === 'git') console.log(process.argv[2] === 'ls-remote' ? '${commit}\\trefs/tags/${release}^{}' : 'package.json\\0'); +else if (process.argv[3].startsWith('@mdbase-dev/sdk@') && process.env.MISSING_SDK === 'true') process.exit(1); +else console.log(JSON.stringify('${version}')); +`; + for (const name of ["gh", "git", "npm"]) { + writeFileSync(join(binaries, name), fake, { mode: 0o755 }); + } + const output = join(directory, "output"); + const invoke = (missing) => + execFileSync( + process.execPath, + [fileURLToPath(new URL("../sdk-repin.mjs", import.meta.url))], + { + cwd: directory, + env: { + ...process.env, + CONNECT_RELEASE: release, + GITHUB_OUTPUT: output, + MISSING_SDK: String(missing), + PATH: `${binaries}:${process.env.PATH}`, + }, + stdio: "pipe", + }, + ); + assert.throws(() => invoke(true), /no partial repin/); + assert.equal(readFileSync(join(directory, "package.json"), "utf8"), original); + assert.equal(existsSync(output), false); + invoke(false); + assert.equal( + JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).dependencies[ + "@mdbase-dev/sdk" + ], + version, + ); + assert.match(readFileSync(output, "utf8"), /changed=true/); + writeFileSync(output, ""); + invoke(false); + assert.match(readFileSync(output, "utf8"), /changed=false/); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); From 3d0b3351d6fe4500b4ea46aa5960e104e366a511 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 1 Nov 2026 14:59:50 +1100 Subject: [PATCH 2/2] fix(release): verify Reader repin provenance and deployment scope --- .github/workflows/ci.yml | 2 +- .github/workflows/deploy-reader.yml | 17 +- .github/workflows/sdk-repin.yml | 14 +- package.json | 3 +- pnpm-lock.yaml | 353 ++++++++++++++++++++++++++ scripts/lib/npm-provenance.mjs | 111 ++++++++ scripts/sdk-repin.mjs | 46 ++-- scripts/tests/npm-provenance.test.mjs | 191 ++++++++++++++ scripts/tests/sdk-repin.test.mjs | 16 +- 9 files changed, 713 insertions(+), 40 deletions(-) create mode 100644 scripts/lib/npm-provenance.mjs create mode 100644 scripts/tests/npm-provenance.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f801684c..d28943d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: deploy-lab: name: Deploy Reader to lab needs: check - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.workflow == 'CI' && github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-24.04 timeout-minutes: 15 concurrency: diff --git a/.github/workflows/deploy-reader.yml b/.github/workflows/deploy-reader.yml index 35541bcb..7562a807 100644 --- a/.github/workflows/deploy-reader.yml +++ b/.github/workflows/deploy-reader.yml @@ -5,9 +5,20 @@ on: push: branches: [main] paths: - - "**/package.json" - - package.json - - pnpm-lock.yaml + - apps/reader/package.json + - apps/zotero-exporter/package.json + - packages/connect/package.json + - packages/core/package.json + - packages/i18n/package.json + - packages/markdown-editor/package.json + - packages/migration/package.json + - packages/platform/package.json + - packages/reading-surface/package.json + - packages/renderer-epub/package.json + - packages/renderer-html/package.json + - packages/renderer-pdf/package.json + - packages/ui/package.json + - packages/web-capture/package.json workflow_dispatch: inputs: target: diff --git a/.github/workflows/sdk-repin.yml b/.github/workflows/sdk-repin.yml index e57c3c85..da316034 100644 --- a/.github/workflows/sdk-repin.yml +++ b/.github/workflows/sdk-repin.yml @@ -54,10 +54,17 @@ jobs: env: GH_TOKEN: ${{ steps.ops.outputs.token }} CONNECT_RELEASE: ${{ inputs.connect_release }} - run: node scripts/sdk-repin.mjs + REPIN_INTEGRITIES: ${{ runner.temp }}/sdk-repin-integrities.json + run: | + pnpm install --frozen-lockfile --ignore-scripts + node scripts/sdk-repin.mjs - name: Refresh the existing lockfile if: steps.pins.outputs.changed == 'true' - run: pnpm install --lockfile-only --ignore-scripts + env: + REPIN_INTEGRITIES: ${{ runner.temp }}/sdk-repin-integrities.json + run: | + pnpm install --lockfile-only --ignore-scripts + node scripts/sdk-repin.mjs verify-lock - name: Open or update the single release PR if: steps.pins.outputs.changed == 'true' env: @@ -82,7 +89,6 @@ jobs: if [ -n "$pull" ]; then gh pr edit "$pull" --title "$title" --body "$body" else - gh pr create --head "$branch" --base main --title "$title" --body "$body" - pull=$(gh pr list --head "$branch" --base main --state open --json number --jq '.[0].number') + pull=$(gh pr create --head "$branch" --base main --title "$title" --body "$body") fi gh pr merge "$pull" --auto --squash diff --git a/package.json b/package.json index d5cca35e..cfb0cf05 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "test:workspaces": "pnpm -r --if-present test", "test:architecture": "node --test scripts/lib/architecture-check.test.mjs", "test:publisher": "node --test scripts/tests/*.test.mjs", - "test:sdk-repin": "node --test scripts/tests/sdk-repin.test.mjs", + "test:sdk-repin": "node --test scripts/tests/sdk-repin.test.mjs scripts/tests/npm-provenance.test.mjs", "test:publisher:evidence": "node --test scripts/tests/reader-evidence.test.mjs", "test:publisher:review": "node --test scripts/review-tests/*.test.mjs", "test:publisher:offline": "node --test scripts/tests/reader-offline.test.mjs", @@ -64,6 +64,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "globals": "^17.13.0", "prettier": "^3.9.10", + "sigstore": "4.1.1", "typescript": "^6.0.3", "typescript-eslint": "^8.72.1", "vitest": "^5.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0c08278..748a960b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: prettier: specifier: ^3.9.10 version: 3.9.10 + sigstore: + specifier: 4.1.1 + version: 4.1.1 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -1355,6 +1358,10 @@ packages: '@fontsource/azeret-mono@5.3.0': resolution: {integrity: sha512-Ag/+gN67fUry7i1yLlTnk16Rr1nnTQI2YefA/G7tYcCSr/+TC/ayMrE8Km7O0OKRgI/B5Mfel46Gq9IGY50Uaw==} + '@gar/promise-retry@1.0.3': + resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} + engines: {node: ^20.17.0 || >=22.9.0} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1770,6 +1777,18 @@ packages: '@nodable/entities@3.1.0': resolution: {integrity: sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==} + '@npmcli/agent@4.0.2': + resolution: {integrity: sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/fs@5.0.0': + resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@npmcli/redact@4.0.0': + resolution: {integrity: sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==} + engines: {node: ^20.17.0 || >=22.9.0} + '@oxc-project/types@0.155.0': resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} @@ -1912,6 +1931,30 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@sigstore/bundle@4.0.0': + resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/core@3.2.1': + resolution: {integrity: sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/protobuf-specs@0.5.2': + resolution: {integrity: sha512-SQqvFMt4V78fdjcDdYX6HbiVSOR4QK3ZgwCa2KOsopAgPIHy1rU5UDUmzLl02r5oyyaYcYHR1hpwDRk/yUe+Mw==} + engines: {node: ^18.17.0 || >=20.5.0} + + '@sigstore/sign@4.1.1': + resolution: {integrity: sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/tuf@4.0.2': + resolution: {integrity: sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + '@sigstore/verify@3.1.1': + resolution: {integrity: sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==} + engines: {node: ^20.17.0 || >=22.9.0} + '@sinclair/typebox@0.27.12': resolution: {integrity: sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==} @@ -1954,6 +1997,14 @@ packages: '@tanstack/virtual-core@3.19.8': resolution: {integrity: sha512-Oz/SYBo/aVPyQkwtOw7Q23myITUgFIof2glQqydYJAPL6mH2SkJDkvDKZTklB+7gv5jGLZQhyTcd7SO1A1EODQ==} + '@tufjs/canonical-json@2.0.0': + resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} + engines: {node: ^16.14.0 || >=18.0.0} + + '@tufjs/models@4.1.0': + resolution: {integrity: sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==} + engines: {node: ^20.17.0 || >=22.9.0} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2281,6 +2332,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -2439,6 +2494,10 @@ packages: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} + cacache@20.0.4: + resolution: {integrity: sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==} + engines: {node: ^20.17.0 || >=22.9.0} + cacheable@2.5.0: resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} @@ -2526,6 +2585,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2946,6 +3009,10 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3072,6 +3139,17 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + i18next@26.5.11: resolution: {integrity: sha512-Wc0klBPKxqZ2tL2qdAnF8xvvnWgr1IMGSY2ikTiAvaFVbz7xiFARtkoU4RidBDw1SyziGROp0qxu6qnJ7l2Wug==} peerDependencies: @@ -3080,6 +3158,10 @@ packages: typescript: optional: true + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -3106,6 +3188,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.7.5: + resolution: {integrity: sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==} + engines: {node: '>= 12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3455,6 +3541,10 @@ packages: magic-string@1.4.5: resolution: {integrity: sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==} + make-fetch-happen@15.0.6: + resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} + engines: {node: ^20.17.0 || >=22.9.0} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3480,6 +3570,30 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minipass-collect@2.0.1: + resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass-fetch@5.0.2: + resolution: {integrity: sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + minipass-flush@1.0.7: + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -3512,6 +3626,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -3585,6 +3703,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@7.1.1: + resolution: {integrity: sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g==} + engines: {node: '>=18'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3678,6 +3800,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -3763,6 +3889,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.1.4: resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} @@ -3833,6 +3962,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sigstore@4.1.1: + resolution: {integrity: sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==} + engines: {node: ^20.17.0 || >=22.9.0} + simple-plist@1.3.1: resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} @@ -3843,6 +3976,18 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.10: + resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -3861,6 +4006,10 @@ packages: sql.js@1.14.1: resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} + engines: {node: ^20.17.0 || >=22.9.0} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -4010,6 +4159,10 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tuf-js@4.1.0: + resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==} + engines: {node: ^20.17.0 || >=22.9.0} + turndown@7.2.4: resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} engines: {node: '>=18', npm: '>=9'} @@ -4305,6 +4458,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -5290,6 +5446,8 @@ snapshots: '@fontsource/azeret-mono@5.3.0': {} + '@gar/promise-retry@1.0.3': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -5852,6 +6010,22 @@ snapshots: '@nodable/entities@3.1.0': {} + '@npmcli/agent@4.0.2': + dependencies: + agent-base: 7.1.4 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 11.5.3 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + '@npmcli/fs@5.0.0': + dependencies: + semver: 7.8.5 + + '@npmcli/redact@4.0.0': {} + '@oxc-project/types@0.155.0': {} '@pdf-lib/standard-fonts@1.0.0': @@ -5963,6 +6137,38 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@sigstore/bundle@4.0.0': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + + '@sigstore/core@3.2.1': {} + + '@sigstore/protobuf-specs@0.5.2': {} + + '@sigstore/sign@4.1.1': + dependencies: + '@gar/promise-retry': 1.0.3 + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + make-fetch-happen: 15.0.6 + proc-log: 6.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@4.0.2': + dependencies: + '@sigstore/protobuf-specs': 0.5.2 + tuf-js: 4.1.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/verify@3.1.1': + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + '@sinclair/typebox@0.27.12': {} '@sveltejs/acorn-typescript@1.0.13(acorn@8.19.0)': @@ -6004,6 +6210,13 @@ snapshots: '@tanstack/virtual-core@3.19.8': {} + '@tufjs/canonical-json@2.0.0': {} + + '@tufjs/models@4.1.0': + dependencies: + '@tufjs/canonical-json': 2.0.0 + minimatch: 10.2.6 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -6323,6 +6536,8 @@ snapshots: acorn@8.19.0: {} + agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -6477,6 +6692,19 @@ snapshots: dependencies: '@types/node': 26.6.6 + cacache@20.0.4: + dependencies: + '@npmcli/fs': 5.0.0 + fs-minipass: 3.0.3 + glob: 13.0.6 + lru-cache: 11.5.3 + minipass: 7.1.3 + minipass-collect: 2.0.1 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + p-map: 7.1.1 + ssri: 13.0.1 + cacheable@2.5.0: dependencies: '@cacheable/memory': 2.2.0 @@ -6553,6 +6781,8 @@ snapshots: concat-map@0.0.1: {} + content-type@2.1.0: {} + convert-source-map@2.0.0: {} crelt@1.0.7: {} @@ -7085,6 +7315,10 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.3 + fsevents@2.3.3: optional: true @@ -7232,10 +7466,31 @@ snapshots: entities: 7.0.1 optional: true + http-cache-semantics@4.3.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + i18next@26.5.11(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + ignore@5.3.2: {} ignore@7.0.14: {} @@ -7254,6 +7509,8 @@ snapshots: hasown: 2.0.4 side-channel: 1.1.1 + ip-address@10.7.5: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -7580,6 +7837,23 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + make-fetch-happen@15.0.6: + dependencies: + '@gar/promise-retry': 1.0.3 + '@npmcli/agent': 4.0.2 + '@npmcli/redact': 4.0.0 + cacache: 20.0.4 + http-cache-semantics: 4.3.0 + minipass: 7.1.3 + minipass-fetch: 5.0.2 + minipass-flush: 1.0.7 + minipass-pipeline: 1.2.4 + negotiator: 1.1.0 + proc-log: 6.1.0 + ssri: 13.0.1 + transitivePeerDependencies: + - supports-color + math-intrinsics@1.1.0: {} mathml-to-latex@1.8.0: @@ -7603,6 +7877,34 @@ snapshots: dependencies: brace-expansion: 1.1.18 + minipass-collect@2.0.1: + dependencies: + minipass: 7.1.3 + + minipass-fetch@5.0.2: + dependencies: + minipass: 7.1.3 + minipass-sized: 2.0.0 + minizlib: 3.1.0 + optionalDependencies: + iconv-lite: 0.7.3 + + minipass-flush@1.0.7: + dependencies: + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@2.0.0: + dependencies: + minipass: 7.1.3 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + minipass@7.1.3: {} minizlib@3.1.0: @@ -7635,6 +7937,10 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + node-domexception@1.0.0: {} node-fetch@3.3.2: @@ -7726,6 +8032,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@7.1.1: {} + package-json-from-dist@1.0.1: {} pako@1.0.11: {} @@ -7796,6 +8104,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + proc-log@6.1.0: {} + progress@2.0.3: {} prompts@2.4.2: @@ -7906,6 +8216,9 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: + optional: true + sax@1.1.4: {} sax@1.6.1: {} @@ -7985,6 +8298,17 @@ snapshots: signal-exit@4.1.0: {} + sigstore@4.1.1: + dependencies: + '@sigstore/bundle': 4.0.0 + '@sigstore/core': 3.2.1 + '@sigstore/protobuf-specs': 0.5.2 + '@sigstore/sign': 4.1.1 + '@sigstore/tuf': 4.0.2 + '@sigstore/verify': 3.1.1 + transitivePeerDependencies: + - supports-color + simple-plist@1.3.1: dependencies: bplist-creator: 0.1.0 @@ -7999,6 +8323,21 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.10 + transitivePeerDependencies: + - supports-color + + socks@2.8.10: + dependencies: + ip-address: 10.7.5 + smart-buffer: 4.2.0 + source-map-js@1.2.2: {} source-map@0.7.6: {} @@ -8009,6 +8348,10 @@ snapshots: sql.js@1.14.1: {} + ssri@13.0.1: + dependencies: + minipass: 7.1.3 + stable-hash-x@0.2.0: {} std-env@4.3.0: {} @@ -8178,6 +8521,14 @@ snapshots: tslib@2.8.1: {} + tuf-js@4.1.0: + dependencies: + '@tufjs/models': 4.1.0 + debug: 4.4.3 + make-fetch-happen: 15.0.6 + transitivePeerDependencies: + - supports-color + turndown@7.2.4: dependencies: '@mixmark-io/domino': 2.2.0 @@ -8453,6 +8804,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yaml@2.9.1: {} diff --git a/scripts/lib/npm-provenance.mjs b/scripts/lib/npm-provenance.mjs new file mode 100644 index 00000000..c2f754bc --- /dev/null +++ b/scripts/lib/npm-provenance.mjs @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; + +const repository = "https://github.com/mdbase-dev/mdbase-connect"; +const predicateType = "https://slsa.dev/provenance/v1"; + +export function publisherIdentity(name, release) { + const workflow = name === "@mdbase-dev/sdk" ? "publish-sdk-npm.yml" : "publish-npm.yml"; + const path = `.github/workflows/${workflow}`; + return { + path, + certificateIssuer: "https://token.actions.githubusercontent.com", + certificateIdentityURI: `^${`${repository}/${path}@refs/tags/${release}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, + ctLogThreshold: 1, + tlogThreshold: 1, + retry: 0, + }; +} + +export async function verifyProvenance( + bundle, + { name, version, integrity, release, commit }, + verify, +) { + const identity = publisherIdentity(name, release); + await verify(bundle, identity); + assert.equal(bundle.dsseEnvelope.payloadType, "application/vnd.in-toto+json"); + const statement = JSON.parse(Buffer.from(bundle.dsseEnvelope.payload, "base64")); + assert.equal(statement._type, "https://in-toto.io/Statement/v1"); + assert.equal(statement.predicateType, predicateType); + assert.match(integrity, /^sha512-[A-Za-z0-9+/]+={0,2}$/); + assert.deepEqual(statement.subject, [ + { + name: `pkg:npm/${name.replace("@", "%40")}@${version}`, + digest: { sha512: Buffer.from(integrity.slice(7), "base64").toString("hex") }, + }, + ]); + const definition = statement.predicate.buildDefinition; + assert.equal( + definition.buildType, + "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", + ); + assert.deepEqual(definition.externalParameters.workflow, { + ref: `refs/tags/${release}`, + repository, + path: identity.path, + }); + const sources = definition.resolvedDependencies.filter( + (source) => source.uri === `git+${repository}@refs/tags/${release}`, + ); + assert.equal(sources.length, 1, "Missing or ambiguous source provenance"); + assert.equal( + sources[0].digest.gitCommit, + commit, + "Provenance does not match verified production commit", + ); +} + +async function registryResponse(url) { + const parsed = new URL(url); + assert.equal(parsed.origin, "https://registry.npmjs.org", "Untrusted package registry URL"); + assert.equal(parsed.username + parsed.password + parsed.hash, ""); + const response = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(30_000) }); + assert.ok(response.ok, `npm registry returned HTTP ${response.status}`); + return response; +} + +export async function verifiedPackage(name, version, release, commit, verifySignature) { + const manifest = await ( + await registryResponse(`https://registry.npmjs.org/${name.replace("/", "%2F")}/${version}`) + ).json(); + assert.equal(manifest.name, name); + assert.equal(manifest.version, version); + const { integrity, tarball, attestations } = manifest.dist; + assert.equal( + attestations?.provenance?.predicateType, + predicateType, + `Missing provenance for ${name}`, + ); + const envelope = await (await registryResponse(attestations.url)).json(); + const candidates = envelope.attestations.filter((item) => item.predicateType === predicateType); + assert.equal(candidates.length, 1, `Missing or ambiguous provenance for ${name}`); + const verify = verifySignature ?? (await import("sigstore")).verify; + await verifyProvenance( + candidates[0].bundle, + { name, version, integrity, release, commit }, + verify, + ); + const bytes = Buffer.from(await (await registryResponse(tarball)).arrayBuffer()); + assert.equal( + `sha512-${createHash("sha512").update(bytes).digest("base64")}`, + integrity, + `Tarball integrity mismatch for ${name}`, + ); + return { name, version, integrity, tarball }; +} + +export function verifyLockIntegrities(lock, packages) { + for (const pkg of packages) { + const resolution = lock.packages?.[`${pkg.name}@${pkg.version}`]?.resolution; + assert.equal( + resolution?.integrity, + pkg.integrity, + `Lockfile integrity mismatch for ${pkg.name}`, + ); + assert.ok( + !resolution.tarball || resolution.tarball === pkg.tarball, + `Lockfile tarball mismatch for ${pkg.name}`, + ); + } +} diff --git a/scripts/sdk-repin.mjs b/scripts/sdk-repin.mjs index c7f1e2f0..faf8a875 100644 --- a/scripts/sdk-repin.mjs +++ b/scripts/sdk-repin.mjs @@ -1,6 +1,7 @@ import { execFileSync } from "node:child_process"; import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; +import { verifiedPackage, verifyLockIntegrities } from "./lib/npm-provenance.mjs"; export function productionIdentity(record, release, tag) { if (!/^v0\.1\.0-beta\.[1-9][0-9]*$/.test(release)) { @@ -20,12 +21,7 @@ export function repinPlan(manifests, version) { for (const { path, text } of manifests) { const manifest = JSON.parse(text); let changed = false; - for (const section of [ - "dependencies", - "devDependencies", - "optionalDependencies", - "peerDependencies", - ]) { + for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) { for (const [name, pin] of Object.entries(manifest[section] ?? {})) { if (!name.startsWith("@mdbase-dev/")) continue; if (name !== "@mdbase-dev/sdk" && /^(file:|workspace:|link:)/.test(pin)) continue; @@ -55,7 +51,7 @@ function execute(...args) { }).trim(); } -function main() { +export async function repin(verifyPackage = verifiedPackage) { const release = process.env.CONNECT_RELEASE ?? ""; if (!/^v0\.1\.0-beta\.[1-9][0-9]*$/.test(release)) { throw new Error("connect_release must be v0.1.0-beta.N"); @@ -79,29 +75,11 @@ function main() { .filter(Boolean) .map((path) => ({ path, text: readFileSync(path, "utf8") })); const plan = repinPlan(manifests, release.slice(1)); + const verified = []; for (const name of plan.packages) { - let published; - try { - published = JSON.parse( - execute( - "npm", - "view", - `${name}@${release.slice(1)}`, - "version", - "--json", - "--registry=https://registry.npmjs.org", - "--fetch-retries=0", - ), - ); - } catch { - throw new Error( - `${name}@${release.slice(1)} is unavailable; no partial repin. SDK npm publication requires Connect PR 1076 and a published production release.`, - ); - } - if (published !== release.slice(1)) { - throw new Error(`Registry identity mismatch for ${name}`); - } + verified.push(await verifyPackage(name, release.slice(1), release, commit)); } + writeFileSync(process.env.REPIN_INTEGRITIES, JSON.stringify(verified)); for (const change of plan.changes) writeFileSync(change.path, change.text); appendFileSync( process.env.GITHUB_OUTPUT, @@ -109,4 +87,14 @@ function main() { ); } -if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) main(); +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + if (process.argv[2] === "verify-lock") { + const { parse } = await import("yaml"); + verifyLockIntegrities( + parse(readFileSync("pnpm-lock.yaml", "utf8")), + JSON.parse(readFileSync(process.env.REPIN_INTEGRITIES, "utf8")), + ); + } else { + await repin(); + } +} diff --git a/scripts/tests/npm-provenance.test.mjs b/scripts/tests/npm-provenance.test.mjs new file mode 100644 index 00000000..6ca3f5ae --- /dev/null +++ b/scripts/tests/npm-provenance.test.mjs @@ -0,0 +1,191 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { parse } from "yaml"; +import { + publisherIdentity, + verifiedPackage, + verifyProvenance, + verifyLockIntegrities, +} from "../lib/npm-provenance.mjs"; + +const release = "v0.1.0-beta.149"; +const version = release.slice(1); +const commit = "a".repeat(40); +const name = "@mdbase-dev/sdk"; +const bytes = Buffer.from("unit-test archive bytes"); +const integrity = `sha512-${createHash("sha512").update(bytes).digest("base64")}`; +const tarball = "https://registry.npmjs.org/@mdbase-dev/sdk/-/sdk.tgz"; +const options = { name, version, integrity, release, commit }; + +function statement() { + return { + _type: "https://in-toto.io/Statement/v1", + predicateType: "https://slsa.dev/provenance/v1", + subject: [ + { + name: `pkg:npm/%40mdbase-dev/sdk@${version}`, + digest: { sha512: Buffer.from(integrity.slice(7), "base64").toString("hex") }, + }, + ], + predicate: { + buildDefinition: { + buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", + externalParameters: { + workflow: { + repository: "https://github.com/mdbase-dev/mdbase-connect", + path: ".github/workflows/publish-sdk-npm.yml", + ref: `refs/tags/${release}`, + }, + }, + resolvedDependencies: [ + { + uri: `git+https://github.com/mdbase-dev/mdbase-connect@refs/tags/${release}`, + digest: { gitCommit: commit }, + }, + ], + }, + }, + }; +} + +function bundle(payload = statement()) { + return { + dsseEnvelope: { + payloadType: "application/vnd.in-toto+json", + payload: Buffer.from(JSON.stringify(payload)).toString("base64"), + }, + }; +} + +test("Sigstore must authenticate the exact issuer, repository, workflow and tag before payload admission", async () => { + let authenticated = false; + await verifyProvenance(bundle(), options, async (_bundle, policy) => { + authenticated = true; + assert.equal(policy.certificateIssuer, "https://token.actions.githubusercontent.com"); + assert.equal(policy.tlogThreshold, 1); + assert.equal(policy.ctLogThreshold, 1); + const identity = new RegExp(policy.certificateIdentityURI); + const expected = `https://github.com/mdbase-dev/mdbase-connect/.github/workflows/publish-sdk-npm.yml@refs/tags/${release}`; + assert.ok(identity.test(expected)); + assert.ok(!identity.test(`evil${expected}`)); + assert.ok(!identity.test(expected.replace("publish-sdk-npm", "publish-npm"))); + assert.ok(!identity.test(expected.replace(release, "v0.1.0-beta.148"))); + assert.ok(!identity.test(expected.replace("mdbase-dev/", "attacker/"))); + }); + assert.ok(authenticated); + assert.match(publisherIdentity("@mdbase-dev/ui", release).path, /\/publish-npm.yml$/); + await assert.rejects( + verifyProvenance(bundle(), options, async () => { + throw new Error("invalid signature"); + }), + /invalid signature/, + ); +}); + +for (const failure of [ + "repository", + "workflow", + "ref", + "commit", + "digest", + "subject", + "missing source", +]) { + test(`rejects authenticated provenance with wrong ${failure}`, async () => { + const payload = statement(); + const definition = payload.predicate.buildDefinition; + if (failure === "repository") definition.externalParameters.workflow.repository += "-evil"; + if (failure === "workflow") + definition.externalParameters.workflow.path = ".github/workflows/untrusted.yml"; + if (failure === "ref") definition.externalParameters.workflow.ref = "refs/heads/main"; + if (failure === "commit") definition.resolvedDependencies[0].digest.gitCommit = "b".repeat(40); + if (failure === "digest") payload.subject[0].digest.sha512 = "0".repeat(128); + if (failure === "subject") payload.subject[0].name = "pkg:npm/other@1.0.0"; + if (failure === "missing source") definition.resolvedDependencies = []; + await assert.rejects(verifyProvenance(bundle(payload), options, async () => {})); + }); +} + +test("registry provenance is required and downloaded archive bytes must match its signed digest", async (context) => { + let corrupt = false; + let missing = false; + const manifest = () => ({ + name, + version, + dist: { + integrity, + tarball, + attestations: { + url: "https://registry.npmjs.org/attestation", + provenance: { predicateType: statement().predicateType }, + }, + }, + }); + context.mock.method(globalThis, "fetch", async (url) => { + if (url.endsWith("/attestation")) + return Response.json({ + attestations: [{ predicateType: statement().predicateType, bundle: bundle() }], + }); + if (url === tarball) return new Response(corrupt ? "tampered" : bytes); + const metadata = manifest(); + if (missing) delete metadata.dist.attestations; + return Response.json(metadata); + }); + assert.deepEqual(await verifiedPackage(name, version, release, commit, async () => {}), { + name, + version, + integrity, + tarball, + }); + corrupt = true; + await assert.rejects( + verifiedPackage(name, version, release, commit, async () => {}), + /Tarball integrity mismatch/, + ); + missing = true; + await assert.rejects( + verifiedPackage(name, version, release, commit, async () => {}), + /Missing provenance/, + ); +}); + +test("lock refresh cannot replace a verified digest or its tarball", () => { + const packages = [{ name, version, integrity, tarball }]; + const lock = { packages: { [`${name}@${version}`]: { resolution: { integrity } } } }; + verifyLockIntegrities(lock, packages); + lock.packages[`${name}@${version}`].resolution.integrity = "sha512-tampered"; + assert.throws(() => verifyLockIntegrities(lock, packages), /Lockfile integrity mismatch/); + lock.packages[`${name}@${version}`].resolution = { + integrity, + tarball: "https://evil.test/sdk.tgz", + }; + assert.throws(() => verifyLockIntegrities(lock, packages), /Lockfile tarball mismatch/); +}); + +test("production triggers only consumed manifests, standalone CI alone owns automatic LAB deployment", () => { + const deployment = parse(readFileSync(".github/workflows/deploy-reader.yml", "utf8")); + const paths = deployment.on.push.paths; + assert.ok(paths.includes("apps/reader/package.json")); + assert.ok(paths.includes("packages/connect/package.json")); + for (const excluded of [ + "**/package.json", + "package.json", + "pnpm-lock.yaml", + "apps/extension/package.json", + "apps/electron/package.json", + "apps/capacitor/package.json", + "packages/testkit/package.json", + ]) { + assert.ok(!paths.includes(excluded)); + } + const ci = parse(readFileSync(".github/workflows/ci.yml", "utf8")); + assert.match(ci.jobs["deploy-lab"].if, /github.workflow == 'CI'/); + const workflow = parse(readFileSync(".github/workflows/sdk-repin.yml", "utf8")); + const commands = workflow.jobs.repin.steps.find( + (step) => step.name === "Open or update the single release PR", + ).run; + assert.ok(commands.includes("pull=$(gh pr create")); + assert.equal(commands.match(/gh pr list/g).length, 1); +}); diff --git a/scripts/tests/sdk-repin.test.mjs b/scripts/tests/sdk-repin.test.mjs index d11c0385..fae968e7 100644 --- a/scripts/tests/sdk-repin.test.mjs +++ b/scripts/tests/sdk-repin.test.mjs @@ -4,7 +4,6 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { fileURLToPath } from "node:url"; import { productionIdentity, repinPlan } from "../sdk-repin.mjs"; const commit = "a".repeat(40); @@ -41,6 +40,7 @@ test("all npm pins and production SDK advance together, aliases/originals stay", }, devDependencies: { "@mdbase-dev/connect-dev": "0.1.0-beta.125" }, optionalDependencies: { "@mdbase-dev/i18n": "~0.1.0-beta.147" }, + peerDependencies: { "@mdbase-dev/ui": "^0.1.0-beta.140" }, }), }, ], @@ -51,6 +51,7 @@ test("all npm pins and production SDK advance together, aliases/originals stay", assert.equal(changed.dependencies["@mdbase-dev/sdk"], version); assert.equal(changed.devDependencies["@mdbase-dev/connect-dev"], version); assert.equal(changed.optionalDependencies["@mdbase-dev/i18n"], version); + assert.equal(changed.peerDependencies["@mdbase-dev/ui"], "^0.1.0-beta.140"); assert.equal(changed.dependencies["@mdbase-dev/sdk-staging"], "file:vendor/staging.tgz"); assert.equal(changed.dependencies["@mdbase-dev/connect-protocol"], "file:vendor/protocol.tgz"); assert.equal(changed.dependencies["@other/package"], "1.0.0"); @@ -117,13 +118,24 @@ else console.log(JSON.stringify('${version}')); const invoke = (missing) => execFileSync( process.execPath, - [fileURLToPath(new URL("../sdk-repin.mjs", import.meta.url))], + [ + "--input-type=module", + "--eval", + ` + import { repin } from ${JSON.stringify(new URL("../sdk-repin.mjs", import.meta.url).href)}; + await repin(async (name, version) => { + if (name === '@mdbase-dev/sdk' && process.env.MISSING_SDK === 'true') throw new Error('no partial repin'); + return {name, version, integrity: 'fixture'}; + }); + `, + ], { cwd: directory, env: { ...process.env, CONNECT_RELEASE: release, GITHUB_OUTPUT: output, + REPIN_INTEGRITIES: join(directory, "integrities.json"), MISSING_SDK: String(missing), PATH: `${binaries}:${process.env.PATH}`, },