diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c46f4..559c3cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add streamed hashing, richer deterministic ZIP controls, SPDX-like SBOM + inventory and provenance statements with explicit local signing evidence. + ## 0.1.0 - 2026-09-01 - Initial local file/directory SHA-256 and SHA-512 manifest workflow. diff --git a/README.md b/README.md index b50d2f7..93a31cb 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,20 @@ Build, compare and package reproducible file inventories locally in the browser. ## Features - Multi-file and directory selection with normalized, deterministic code-point path ordering. -- SHA-256 and SHA-512 content manifests recording source kind, generator version, algorithms and operation semantics. +- SHA-256 and SHA-512 content manifests recording source kind, generator version, algorithms and operation semantics, with bounded cancellable chunk reads and byte progress. - Reference comparison showing matching, changed, missing and unexpected files. - Stable JSON and spreadsheet-safe CSV reports. - Deterministic ZIP output with sorted entries, fixed ZIP-epoch timestamps and an embedded manifest. +- Streaming ZIP-content manifests and compression-independent comparisons under explicit expansion limits. +- CycloneDX JSON, SPDX JSON and npm-lockfile inspection with portable evidence and an explicitly non-conformant local provenance statement. - Optional P-256 signature envelopes using a fresh non-exportable, memory-only private key. - Offline PWA, responsive shell and light/dark/system themes. ## Limits and interpretation -Files are limited to 64 MiB each, 256 MiB per selection and 10,000 entries. Individual UTF-8 paths are capped at 4 KiB and aggregate path metadata at 4 MiB. Reference manifests are schema-, count-, digest- and path-validated before comparison. In-browser ZIP creation is capped at 128 MiB and can require substantially more temporary memory than the source files. Timestamps are omitted by default; enabling them deliberately makes the manifest different on every run. +Files are limited to 64 MiB each, 256 MiB per selection and 10,000 entries. Individual UTF-8 paths are capped at 4 KiB and aggregate path metadata at 4 MiB. Reference manifests are schema-, count-, digest- and path-validated before comparison. In-browser ZIP creation is capped at 128 MiB and can require substantially more temporary memory than the source files. ZIP inspection is limited to 5,000 entries, 32 MiB per expanded file and 256 MiB expanded in total; entries remain inert browser data and are never written to a filesystem. Web Crypto exposes one-shot SHA finalization, so bounded chunks are retained for each file before the digest call. Timestamps are omitted by default; enabling them deliberately makes the manifest different on every run. -Session signatures prove integrity relative to their public key, not the identity of a person or build system. Authenticate the public key independently. ECDSA signatures are intentionally not byte-reproducible. +Session signatures prove integrity relative to their public key, not the identity of a person or build system. Authenticate the public key independently. ECDSA signatures are intentionally not byte-reproducible. Provenance output is local evidence and deliberately makes no SLSA, attestation-authority or builder-identity claim. ## Development @@ -27,7 +29,7 @@ npm run test:browser npm run package:release -- --force ``` -Node.js 22+ and npm 11 are required. The deterministic release is `release/repro-tools-0.1.0.zip` with its SHA-256 sidecar. +Node.js 22+ and npm 11 are required. The deterministic release is `release/repro-tools-0.2.0.zip` with its SHA-256 sidecar. ## Licence diff --git a/SOURCE.md b/SOURCE.md index cedf3c5..83d0bf2 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,10 +1,10 @@ # Source identity - Project: Repro Tools -- Version: 0.1.0 +- Version: 0.2.0 - Repository: https://git.add-ideas.de/lotobo/repro-tools - Licence: GPL-3.0-or-later - Build: Node.js 22+, npm 11, `npm ci && npm run release:artifact` -- Artifact: `repro-tools-0.1.0.zip` +- Artifact: `repro-tools-0.2.0.zip` Dependencies are exactly pinned by `package-lock.json`; runtime licence texts are bundled under `LICENSES/`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index f3632a7..3bff47b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,3 +1,3 @@ # Third-party notices -Repro Tools is GPL-3.0-or-later. Runtime dependencies are React, React DOM and the add·ideas Toolbox Contract, Shell and Helpers packages. `fflate` is used only in tests to independently read generated ZIP archives. Exact versions and declared licences are in `package-lock.json`; detected runtime licence texts are generated into `LICENSES/npm-runtime-licenses.txt` in releases. +Repro Tools is GPL-3.0-or-later. Runtime dependencies are React, React DOM, `fflate` (MIT), and the add·ideas Toolbox Contract, Shell and Helpers packages. `fflate` streams ZIP contents and independently verifies deterministic ZIP output in tests. Exact versions and declared licences are in `package-lock.json`; detected runtime licence texts are generated into `LICENSES/npm-runtime-licenses.txt` in releases. diff --git a/package-lock.json b/package-lock.json index 44b4dc5..9822bc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,23 @@ { "name": "repro-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "repro-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", + "fflate": "0.8.2", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -29,7 +30,6 @@ "eslint": "10.7.0", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-react-refresh": "0.5.2", - "fflate": "0.8.2", "globals": "17.7.0", "jsdom": "29.1.1", "prettier": "3.9.5", @@ -43,24 +43,25 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.2.3/toolbox-contract-0.2.3.tgz", - "integrity": "sha512-T0PVSuMT40GjTDfQJhEEY3ZawQq8zz1/ry95JdKI6W39CdLacaRXdGnEpDCMHt+jUbf1Jz7Nat/M5dFCgKVM9A==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-helpers/-/0.1.0/toolbox-helpers-0.1.0.tgz", - "integrity": "sha512-UKl1Oxekedf8D2df86VrnVA53AcMhrnh6iUPXY+k8frirBXotb0yd8SGT+IF/3hcqYwcYe/v9WVFuSgKtIYVnw==", - "license": "GPL-3.0-or-later" + "version": "0.2.0", + "license": "GPL-3.0-or-later", + "engines": { + "node": ">=22" + } }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.2.3/toolbox-shell-react-0.2.3.tgz", - "integrity": "sha512-DT5lQDH48BFkFcmFLZnQh7+Cm73JzBPcmp5WzUXypfkUXpEyDYHzaXgmW4kZ0edSwh4RK4sPmx+JPtK0X4aKCQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -68,13 +69,13 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", - "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.2.3/toolbox-testkit-0.2.3.tgz", - "integrity": "sha512-sq1MwhKWfFKen+N+124hl74qQimRSvmQ9sOU7jdcI+2qCKZ67+2B8rWyezeV80uTFu4Jv6deHksfYQ/tKNV6XQ==", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" @@ -2283,7 +2284,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { diff --git a/package.json b/package.json index b97a5b1..909b46c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "repro-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Hash, package and verify reproducible file sets locally in the browser.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,17 +39,17 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-shell-react": "0.3.0", + "fflate": "0.8.2", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", - "fflate": "0.8.2", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", "@testing-library/user-event": "14.6.1", diff --git a/playwright.config.ts b/playwright.config.ts index d345d81..678757c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 92c46f4..559c3cd 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add streamed hashing, richer deterministic ZIP controls, SPDX-like SBOM + inventory and provenance statements with explicit local signing evidence. + ## 0.1.0 - 2026-09-01 - Initial local file/directory SHA-256 and SHA-512 manifest workflow. diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 45a7c95..d1e91be 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -1077,6 +1077,33 @@ Declared licence: Apache-2.0 limitations under the License. +============================================================================== +fflate@0.8.2 +Declared licence: MIT +============================================================================== +--- LICENSE --- +MIT License + +Copyright (c) 2023 Arjun Barrett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ============================================================================== react@19.2.8 Declared licence: MIT diff --git a/public/README.md b/public/README.md index b50d2f7..93a31cb 100644 --- a/public/README.md +++ b/public/README.md @@ -5,18 +5,20 @@ Build, compare and package reproducible file inventories locally in the browser. ## Features - Multi-file and directory selection with normalized, deterministic code-point path ordering. -- SHA-256 and SHA-512 content manifests recording source kind, generator version, algorithms and operation semantics. +- SHA-256 and SHA-512 content manifests recording source kind, generator version, algorithms and operation semantics, with bounded cancellable chunk reads and byte progress. - Reference comparison showing matching, changed, missing and unexpected files. - Stable JSON and spreadsheet-safe CSV reports. - Deterministic ZIP output with sorted entries, fixed ZIP-epoch timestamps and an embedded manifest. +- Streaming ZIP-content manifests and compression-independent comparisons under explicit expansion limits. +- CycloneDX JSON, SPDX JSON and npm-lockfile inspection with portable evidence and an explicitly non-conformant local provenance statement. - Optional P-256 signature envelopes using a fresh non-exportable, memory-only private key. - Offline PWA, responsive shell and light/dark/system themes. ## Limits and interpretation -Files are limited to 64 MiB each, 256 MiB per selection and 10,000 entries. Individual UTF-8 paths are capped at 4 KiB and aggregate path metadata at 4 MiB. Reference manifests are schema-, count-, digest- and path-validated before comparison. In-browser ZIP creation is capped at 128 MiB and can require substantially more temporary memory than the source files. Timestamps are omitted by default; enabling them deliberately makes the manifest different on every run. +Files are limited to 64 MiB each, 256 MiB per selection and 10,000 entries. Individual UTF-8 paths are capped at 4 KiB and aggregate path metadata at 4 MiB. Reference manifests are schema-, count-, digest- and path-validated before comparison. In-browser ZIP creation is capped at 128 MiB and can require substantially more temporary memory than the source files. ZIP inspection is limited to 5,000 entries, 32 MiB per expanded file and 256 MiB expanded in total; entries remain inert browser data and are never written to a filesystem. Web Crypto exposes one-shot SHA finalization, so bounded chunks are retained for each file before the digest call. Timestamps are omitted by default; enabling them deliberately makes the manifest different on every run. -Session signatures prove integrity relative to their public key, not the identity of a person or build system. Authenticate the public key independently. ECDSA signatures are intentionally not byte-reproducible. +Session signatures prove integrity relative to their public key, not the identity of a person or build system. Authenticate the public key independently. ECDSA signatures are intentionally not byte-reproducible. Provenance output is local evidence and deliberately makes no SLSA, attestation-authority or builder-identity claim. ## Development @@ -27,7 +29,7 @@ npm run test:browser npm run package:release -- --force ``` -Node.js 22+ and npm 11 are required. The deterministic release is `release/repro-tools-0.1.0.zip` with its SHA-256 sidecar. +Node.js 22+ and npm 11 are required. The deterministic release is `release/repro-tools-0.2.0.zip` with its SHA-256 sidecar. ## Licence diff --git a/public/SOURCE.md b/public/SOURCE.md index cedf3c5..83d0bf2 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,10 +1,10 @@ # Source identity - Project: Repro Tools -- Version: 0.1.0 +- Version: 0.2.0 - Repository: https://git.add-ideas.de/lotobo/repro-tools - Licence: GPL-3.0-or-later - Build: Node.js 22+, npm 11, `npm ci && npm run release:artifact` -- Artifact: `repro-tools-0.1.0.zip` +- Artifact: `repro-tools-0.2.0.zip` Dependencies are exactly pinned by `package-lock.json`; runtime licence texts are bundled under `LICENSES/`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index f3632a7..3bff47b 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -1,3 +1,3 @@ # Third-party notices -Repro Tools is GPL-3.0-or-later. Runtime dependencies are React, React DOM and the add·ideas Toolbox Contract, Shell and Helpers packages. `fflate` is used only in tests to independently read generated ZIP archives. Exact versions and declared licences are in `package-lock.json`; detected runtime licence texts are generated into `LICENSES/npm-runtime-licenses.txt` in releases. +Repro Tools is GPL-3.0-or-later. Runtime dependencies are React, React DOM, `fflate` (MIT), and the add·ideas Toolbox Contract, Shell and Helpers packages. `fflate` streams ZIP contents and independently verifies deterministic ZIP output in tests. Exact versions and declared licences are in `package-lock.json`; detected runtime licence texts are generated into `LICENSES/npm-runtime-licenses.txt` in releases. diff --git a/public/sw.js b/public/sw.js index 40e68f7..55968b5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "repro-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index c40c5b8..0c4de83 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.repro-tools", "name": "Repro Tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Build and verify reproducible file manifests locally.", "entry": "./", "icon": "./favicon.svg", @@ -21,6 +21,19 @@ "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "*/*", "extensions": [] }, + { "mediaType": "application/zip", "extensions": [".zip"] }, + { "mediaType": "application/json", "extensions": [".json"] } + ], + "produces": [ + { "mediaType": "application/json", "extensions": [".json"] }, + { "mediaType": "text/csv", "extensions": [".csv"] }, + { "mediaType": "application/zip", "extensions": [".zip"] } + ] + }, + "capabilities": { "required": [], "optional": ["web-crypto"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index f446c39..87648cd 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -1,25 +1,38 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { + formatBytes, stableStringify, triggerBlobDownload, } from "@add-ideas/toolbox-helpers"; import { + buildArchiveContentManifest, buildManifest, collectSelection, + compareArchiveContentManifests, compareManifests, createSessionSigner, deterministicZip, manifestCsv, MANIFEST_TEXT_LIMIT, + parseArchiveContentManifest, parseManifest, SIGNATURE_TEXT_LIMIT, + serializeArchiveContentManifest, serializeManifest, verifySignature, + type ArchiveContentManifest, type Comparison, type DigestAlgorithm, type ReproManifest, type SelectedFile, } from "../core/repro"; +import { + createProvenanceStatement, + createReproEvidence, + inspectSbomDocuments, + serializeProvenance, + type SbomEvidence, +} from "../core/evidence"; export function Workbench() { const [selected, setSelected] = useState([]); @@ -32,10 +45,18 @@ export function Workbench() { const [reference, setReference] = useState(""); const [comparison, setComparison] = useState([]); const [signature, setSignature] = useState(""); + const [archiveManifest, setArchiveManifest] = + useState(null); + const [archiveReference, setArchiveReference] = useState(""); + const [archiveComparison, setArchiveComparison] = useState([]); + const [sbomEvidence, setSbomEvidence] = useState([]); + const [provenance, setProvenance] = useState(""); + const [evidence, setEvidence] = useState(""); const [status, setStatus] = useState("Choose files or a directory to begin."); const [busy, setBusy] = useState(false); const fileInput = useRef(null); const directoryInput = useRef(null); + const archiveInput = useRef(null); useEffect(() => { directoryInput.current?.setAttribute("webkitdirectory", ""); @@ -59,6 +80,9 @@ export function Workbench() { setManifest(null); setComparison([]); setSignature(""); + setSbomEvidence([]); + setProvenance(""); + setEvidence(""); setStatus( `${next.length.toLocaleString()} file${next.length === 1 ? "" : "s"} ready.`, ); @@ -189,6 +213,77 @@ export function Workbench() { } } + async function inspectArchive(file: File | undefined): Promise { + if (!file) return; + setBusy(true); + try { + const next = await buildArchiveContentManifest(file, algorithms, { + onProgress: (progress) => + setStatus( + progress.phase === "read" + ? `Reading ZIP: ${formatBytes(progress.archiveBytes)} of ${formatBytes(progress.archiveTotalBytes)}…` + : `${progress.phase === "expand" ? "Expanding" : "Hashing"} ${progress.path ?? "entries"}…`, + ), + }); + setArchiveManifest(next); + setArchiveComparison([]); + setStatus( + `ZIP content manifest ready: ${next.fileCount.toLocaleString()} files, ${formatBytes(next.totalBytes)} expanded.`, + ); + } catch (caught) { + setArchiveManifest(null); + setStatus(caught instanceof Error ? caught.message : String(caught)); + } finally { + setBusy(false); + if (archiveInput.current) archiveInput.current.value = ""; + } + } + + function compareArchive(): void { + if (!archiveManifest) return; + try { + const results = compareArchiveContentManifests( + parseArchiveContentManifest(archiveReference), + archiveManifest, + ); + setArchiveComparison(results); + const changed = results.filter((item) => item.status !== "match").length; + setStatus( + changed === 0 + ? "Archive contents match the reference." + : `Archive comparison found ${changed.toLocaleString()} difference${changed === 1 ? "" : "s"}.`, + ); + } catch (caught) { + setArchiveComparison([]); + setStatus(caught instanceof Error ? caught.message : String(caught)); + } + } + + async function inspectEvidence(): Promise { + if (!manifest) return; + setBusy(true); + try { + setStatus("Inspecting local SBOM and lockfile candidates…"); + const materials = await inspectSbomDocuments(selected); + setSbomEvidence(materials); + setProvenance( + serializeProvenance( + createProvenanceStatement(manifest, materials, { timestamp }), + ), + ); + setEvidence( + serializeProvenance(createReproEvidence(manifest, materials)), + ); + setStatus( + `Provenance evidence ready; ${materials.filter((item) => item.status === "recognized").length.toLocaleString()} structured dependency document${materials.length === 1 ? "" : "s"} recognized.`, + ); + } catch (caught) { + setStatus(caught instanceof Error ? caught.message : String(caught)); + } finally { + setBusy(false); + } + } + return (
@@ -224,6 +319,7 @@ export function Workbench() { ref={fileInput} className="sr-only" type="file" + aria-label="Choose files for reproducibility manifest" multiple onChange={(event) => acceptFiles(event.target.files, "files")} /> @@ -231,6 +327,7 @@ export function Workbench() { ref={directoryInput} className="sr-only" type="file" + aria-label="Choose directory for reproducibility manifest" multiple onChange={(event) => acceptFiles(event.target.files, "directory")} /> @@ -399,6 +496,171 @@ export function Workbench() {
+
+
+
+
+

Expanded-content evidence

+

ZIP content manifest

+
+
+

+ Stream a ZIP through strict entry, per-file and expanded-size + ceilings. The inventory hashes uncompressed content, so it can + compare archives whose compression or metadata differs. +

+
+ + void inspectArchive(event.target.files?.[0])} + /> + +
+ {archiveManifest && ( +