From b234c1f94b4684c747adde1606dcb341c1c44288 Mon Sep 17 00:00:00 2001 From: BL Date: Sun, 13 Sep 2026 05:56:13 +0000 Subject: [PATCH] fix: repair release audit and record registry qualification --- .../003-npm-release-trusted-publishing.md | 33 +++- .../003/qualified-tarball.json | 177 ++++++++++++++++++ .../003/registry-artifact.json | 10 + .../003/registry-consumer.json | 177 ++++++++++++++++++ .github/RELEASE_EVIDENCE/003/review.md | 46 +++++ .github/RELEASING.md | 6 +- .github/workflows/npm-publish.yml | 42 +++-- 7 files changed, 467 insertions(+), 24 deletions(-) create mode 100644 .github/RELEASE_EVIDENCE/003/qualified-tarball.json create mode 100644 .github/RELEASE_EVIDENCE/003/registry-artifact.json create mode 100644 .github/RELEASE_EVIDENCE/003/registry-consumer.json create mode 100644 .github/RELEASE_EVIDENCE/003/review.md diff --git a/.github/ISSUE_SPECS/003-npm-release-trusted-publishing.md b/.github/ISSUE_SPECS/003-npm-release-trusted-publishing.md index aca9831..278db1a 100644 --- a/.github/ISSUE_SPECS/003-npm-release-trusted-publishing.md +++ b/.github/ISSUE_SPECS/003-npm-release-trusted-publishing.md @@ -49,6 +49,33 @@ has exercised an OIDC publish. Any pending authentication remains explicit. ## Current status -The local npm account is authenticated with write access to the organization, -2FA is enabled, and `@misofm/codec` does not yet exist in the registry. Release -workflow implementation, review, publication, and trust configuration are pending. +Release tooling is merged through [PR 4](https://github.com/misofm/codec/pull/4). +The release main commit is `12f54c988a2df319064e6ca3885268d976319cc1`, with +[green CI](https://github.com/misofm/codec/actions/runs/34740760831). Fresh Sol +review signed off `5d7e1a7ceb667276c84934e0a61e95c725e5cae6`; the merge has the +same tree. Review evidence is in `../RELEASE_EVIDENCE/003/review.md`. + +The exact main commit passed the complete ten-combination packed-consumer matrix +under npm 11.19.1. The retained 139,230-byte tarball has SHA-256 +`3c318e0e5ccb861994b5084f5b3aeea857d79c5ae78f5fc412f2e85d8dd10a36` and integrity +`sha512-Oleo8mUbyNzooQIgOsdxfxBYGhBXAJyGvZCna5cTcD8vGdfgcxMTUvzbCSccEOUYQ6vg+87WbI0UYmcEQpAk0w==`. +The consumer and package receipt is in +`../RELEASE_EVIDENCE/003/qualified-tarball.json`. + +`@misofm/codec@0.1.0` was published through the authorized local npm CLI with +`--ignore-scripts --provenance=false`. The registry reports `latest: 0.1.0`. +Downloaded registry bytes match both recorded digests, and the normal registry +package passed the full ten-combination consumer matrix. Receipts are in +`../RELEASE_EVIDENCE/003/registry-artifact.json` and `registry-consumer.json`. + +npm confirmed trusted publisher `26f5b08e-325d-42b8-85e3-bdc68ae84ddb`, provider +GitHub, repository `misofm/codec`, workflow `npm-publish.yml`, with direct publish +and npm's default stage-publish permissions. A separate read-back authentication +is pending. + +The initial [verify workflow](https://github.com/misofm/codec/actions/runs/34741165009) +passed qualification, registry byte comparison, and all registry consumers. Its +final signature audit step failed on shell heredoc indentation. A workflow-only +fix preserves the audit and provenance gates; all fourteen extracted shell blocks +now pass `bash -n`, and actionlint passes. Fresh review and a green verify dispatch +of that correction remain required. Verify mode does not exercise OIDC publication. diff --git a/.github/RELEASE_EVIDENCE/003/qualified-tarball.json b/.github/RELEASE_EVIDENCE/003/qualified-tarball.json new file mode 100644 index 0000000..8480e5d --- /dev/null +++ b/.github/RELEASE_EVIDENCE/003/qualified-tarball.json @@ -0,0 +1,177 @@ +{ + "tarballFilename": "misofm-codec-0.1.0.tgz", + "tarballSha256": "3c318e0e5ccb861994b5084f5b3aeea857d79c5ae78f5fc412f2e85d8dd10a36", + "tarballIntegrity": "sha512-Oleo8mUbyNzooQIgOsdxfxBYGhBXAJyGvZCna5cTcD8vGdfgcxMTUvzbCSccEOUYQ6vg+87WbI0UYmcEQpAk0w==", + "packedBytes": 139230, + "unpackedBytes": 418452, + "files": [ + "LICENSE", + "README.md", + "THIRD_PARTY_NOTICES.md", + "dist/decode.d.ts", + "dist/decode.d.ts.map", + "dist/decode.js", + "dist/decode.js.map", + "dist/decoder-errors.d.ts", + "dist/decoder-errors.d.ts.map", + "dist/decoder-errors.js", + "dist/decoder-errors.js.map", + "dist/decoder-wasm.d.ts", + "dist/decoder-wasm.d.ts.map", + "dist/decoder-wasm.js", + "dist/decoder-wasm.js.map", + "dist/encode.d.ts", + "dist/encode.d.ts.map", + "dist/encode.js", + "dist/encode.js.map", + "dist/errors.d.ts", + "dist/errors.d.ts.map", + "dist/errors.js", + "dist/errors.js.map", + "dist/format.d.ts", + "dist/format.d.ts.map", + "dist/format.js", + "dist/format.js.map", + "dist/index.d.ts", + "dist/index.d.ts.map", + "dist/index.js", + "dist/index.js.map", + "dist/internal/decoder-abi.d.ts", + "dist/internal/decoder-abi.d.ts.map", + "dist/internal/decoder-abi.js", + "dist/internal/decoder-abi.js.map", + "dist/internal/encoder-abi.d.ts", + "dist/internal/encoder-abi.d.ts.map", + "dist/internal/encoder-abi.js", + "dist/internal/encoder-abi.js.map", + "dist/node.d.ts", + "dist/node.d.ts.map", + "dist/node.js", + "dist/node.js.map", + "dist/wasm.d.ts", + "dist/wasm.d.ts.map", + "dist/wasm.js", + "dist/wasm.js.map", + "docs/adapter-integration.md", + "docs/architecture.md", + "package.json", + "vendor/licenses/compiler-rt.txt", + "vendor/licenses/emscripten.txt", + "vendor/licenses/libFLAC.txt", + "vendor/licenses/musl.txt", + "wasm/flac-decoder.manifest.json", + "wasm/flac-decoder.wasm", + "wasm/flac-encoder.manifest.json", + "wasm/flac-encoder.wasm" + ], + "consumers": [ + { + "effect": "4.0.0-rc.112", + "bun": { + "runtime": "Bun 1.4.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "node": { + "runtime": "Node v22.23.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "browser": [ + { + "browser": "chromium", + "version": "151.0.7922.34", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "firefox", + "version": "153.0", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "webkit", + "version": "26.5", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + } + ] + }, + { + "effect": "4.0.0-rc.115", + "bun": { + "runtime": "Bun 1.4.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "node": { + "runtime": "Node v22.23.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "browser": [ + { + "browser": "chromium", + "version": "151.0.7922.34", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "firefox", + "version": "153.0", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "webkit", + "version": "26.5", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + } + ] + } + ] +} diff --git a/.github/RELEASE_EVIDENCE/003/registry-artifact.json b/.github/RELEASE_EVIDENCE/003/registry-artifact.json new file mode 100644 index 0000000..bafac5c --- /dev/null +++ b/.github/RELEASE_EVIDENCE/003/registry-artifact.json @@ -0,0 +1,10 @@ +{ + "package": "@misofm/codec", + "version": "0.1.0", + "releaseCommit": "12f54c988a2df319064e6ca3885268d976319cc1", + "registryTarball": "https://registry.npmjs.org/@misofm/codec/-/codec-0.1.0.tgz", + "tarballSha256": "3c318e0e5ccb861994b5084f5b3aeea857d79c5ae78f5fc412f2e85d8dd10a36", + "tarballIntegrity": "sha512-Oleo8mUbyNzooQIgOsdxfxBYGhBXAJyGvZCna5cTcD8vGdfgcxMTUvzbCSccEOUYQ6vg+87WbI0UYmcEQpAk0w==", + "tarballBytes": 139230, + "provenance": "local bootstrap; no CI attestation" +} diff --git a/.github/RELEASE_EVIDENCE/003/registry-consumer.json b/.github/RELEASE_EVIDENCE/003/registry-consumer.json new file mode 100644 index 0000000..8480e5d --- /dev/null +++ b/.github/RELEASE_EVIDENCE/003/registry-consumer.json @@ -0,0 +1,177 @@ +{ + "tarballFilename": "misofm-codec-0.1.0.tgz", + "tarballSha256": "3c318e0e5ccb861994b5084f5b3aeea857d79c5ae78f5fc412f2e85d8dd10a36", + "tarballIntegrity": "sha512-Oleo8mUbyNzooQIgOsdxfxBYGhBXAJyGvZCna5cTcD8vGdfgcxMTUvzbCSccEOUYQ6vg+87WbI0UYmcEQpAk0w==", + "packedBytes": 139230, + "unpackedBytes": 418452, + "files": [ + "LICENSE", + "README.md", + "THIRD_PARTY_NOTICES.md", + "dist/decode.d.ts", + "dist/decode.d.ts.map", + "dist/decode.js", + "dist/decode.js.map", + "dist/decoder-errors.d.ts", + "dist/decoder-errors.d.ts.map", + "dist/decoder-errors.js", + "dist/decoder-errors.js.map", + "dist/decoder-wasm.d.ts", + "dist/decoder-wasm.d.ts.map", + "dist/decoder-wasm.js", + "dist/decoder-wasm.js.map", + "dist/encode.d.ts", + "dist/encode.d.ts.map", + "dist/encode.js", + "dist/encode.js.map", + "dist/errors.d.ts", + "dist/errors.d.ts.map", + "dist/errors.js", + "dist/errors.js.map", + "dist/format.d.ts", + "dist/format.d.ts.map", + "dist/format.js", + "dist/format.js.map", + "dist/index.d.ts", + "dist/index.d.ts.map", + "dist/index.js", + "dist/index.js.map", + "dist/internal/decoder-abi.d.ts", + "dist/internal/decoder-abi.d.ts.map", + "dist/internal/decoder-abi.js", + "dist/internal/decoder-abi.js.map", + "dist/internal/encoder-abi.d.ts", + "dist/internal/encoder-abi.d.ts.map", + "dist/internal/encoder-abi.js", + "dist/internal/encoder-abi.js.map", + "dist/node.d.ts", + "dist/node.d.ts.map", + "dist/node.js", + "dist/node.js.map", + "dist/wasm.d.ts", + "dist/wasm.d.ts.map", + "dist/wasm.js", + "dist/wasm.js.map", + "docs/adapter-integration.md", + "docs/architecture.md", + "package.json", + "vendor/licenses/compiler-rt.txt", + "vendor/licenses/emscripten.txt", + "vendor/licenses/libFLAC.txt", + "vendor/licenses/musl.txt", + "wasm/flac-decoder.manifest.json", + "wasm/flac-decoder.wasm", + "wasm/flac-encoder.manifest.json", + "wasm/flac-encoder.wasm" + ], + "consumers": [ + { + "effect": "4.0.0-rc.112", + "bun": { + "runtime": "Bun 1.4.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "node": { + "runtime": "Node v22.23.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "browser": [ + { + "browser": "chromium", + "version": "151.0.7922.34", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "firefox", + "version": "153.0", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "webkit", + "version": "26.5", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + } + ] + }, + { + "effect": "4.0.0-rc.115", + "bun": { + "runtime": "Bun 1.4.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "node": { + "runtime": "Node v22.23.2", + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + }, + "browser": [ + { + "browser": "chromium", + "version": "151.0.7922.34", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "firefox", + "version": "153.0", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + }, + { + "browser": "webkit", + "version": "26.5", + "worker": { + "formats": 4, + "verifiedFrames": 32772, + "verifiedBytes": 122895, + "cancellation": true, + "truncation": true + } + } + ] + } + ] +} diff --git a/.github/RELEASE_EVIDENCE/003/review.md b/.github/RELEASE_EVIDENCE/003/review.md new file mode 100644 index 0000000..ec32de5 --- /dev/null +++ b/.github/RELEASE_EVIDENCE/003/review.md @@ -0,0 +1,46 @@ +# Adversarial npm release review + +## Decision + +**Sign-off: PASS for release commit `5d7e1a7ceb667276c84934e0a61e95c725e5cae6`, with no remaining launch blocker in the reviewed release tooling or documentation.** + +Reviewed against base `544aaa969a186ef52e492e580dee34986c72d8bf` and issue 3's release contract. Scope was `.github/workflows/npm-publish.yml`, `.github/RELEASING.md`, `README.md`, and the release extensions to `scripts/pack-check.mjs`; codec implementation was intentionally excluded. + +## Blocker found and resolved + +The original review target `732d6dcb710e641b3aa2c65f5cca9f122bdf7ba7` used `${{ runner.temp }}` in `jobs..env` for both the publish/compare and registry-consumer jobs. GitHub's context-availability rules do not permit the `runner` context at job-level `env`, so the workflow could fail validation before dispatch. + +Commit `5d7e1a7ceb667276c84934e0a61e95c725e5cae6` removes those expressions. Each pinned-npm setup step now derives the two npm config paths from `$RUNNER_TEMP`, exports them for the current step, creates the empty files, and persists the resolved paths through `$GITHUB_ENV`. The npm token variables remain empty for the entire job. The final workflow passes actionlint 1.7.12. + +Reference: + +## Release invariants reviewed + +- Exact-main admission is fail-closed: the dispatch input must be a lowercase 40-character SHA, the dispatch ref and event SHA must match it, checkout HEAD must match it, and the live remote `main` tip must match it. Package name, version, and repository identity are also checked before qualification. +- Version admission is correct for both modes. `publish` requires a registry 404 for the exact version and npm remains the authoritative race-safe final writer; `verify` requires the exact registry version to exist. The live registry returned 404 for `@misofm/codec@0.1.0` during this review. +- OIDC is isolated to `publish-or-compare`. Qualification and registry-consumer jobs have only `contents: read`; the privileged job does not check out or execute repository code and has no npm token fallback. Its publish step is additionally conditional on `mode=publish`. +- Qualification copies the tarball only after the complete packed-consumer matrix succeeds. The workflow then requires one retained `.tgz`, recomputes SHA-256 and SHA-512 integrity, binds them to the receipt, and transfers only the tarball plus receipt as a one-day artifact. +- The publish job rechecks artifact membership, receipt fields, both digests, and the packed manifest before registry admission. It publishes the exact downloaded `.tgz` with lifecycle scripts disabled, then downloads the registry tarball and requires byte-level SHA-256/SHA-512 equality. +- Registry verification reuses the full `pack:check` consumer path against `@misofm/codec@` with the qualification SHA-256. It therefore covers both Effect peers, Bun, Node, and Chromium/Firefox/WebKit using the registry tarball rather than a workspace alias. +- Bootstrap semantics are explicit and correct: the one authorized local 0.1.0 command uses the retained tested tarball, `--ignore-scripts`, and `--provenance=false`, while `publishConfig.provenance=true` stays in the manifest. Trust is configured only after the real package exists. `npm trust github` help under npm 11.19.1 confirms `--file`, `--repo`, and `--allow-publish`; the documented command selects npm 11.19.1 without replacing the global client. +- CI publish mode requires a verified SLSA provenance attestation. Verify mode skips publication and explicitly states that it does not prove an OIDC publish, matching the issue and release guide. +- README publication wording and install command are consistent with the intended public 0.1.0 release. The release guide accurately distinguishes local bootstrap from subsequent OIDC publication. + +## Focused validation evidence + +- `actionlint 1.7.12 .github/workflows/npm-publish.yml`: pass. +- Prettier on all changed release/docs files: pass. +- `node --check scripts/pack-check.mjs`: pass. +- `git diff --check 544aaa9..5d7e1a7`: pass; worktree clean. +- Independently exercised failure paths: malformed expected SHA, destination inside the repository, and receipt inside the retained-artifact directory all exited nonzero with the intended diagnostic before qualification. +- Retained tested tarball: `/tmp/codec-release.265Gqx/misofm-codec-0.1.0.tgz`. +- Receipt: `/tmp/codec-pack-receipt.FsHDl3.json`. +- Independently recomputed SHA-256: `3c318e0e5ccb861994b5084f5b3aeea857d79c5ae78f5fc412f2e85d8dd10a36`, equal to the receipt. +- Independently recomputed SHA-512 integrity equals the receipt: `sha512-Oleo8mUbyNzooQIgOsdxfxBYGhBXAJyGvZCna5cTcD8vGdfgcxMTUvzbCSccEOUYQ6vg+87WbI0UYmcEQpAk0w==`. +- A fresh npm 11.19.1 pack of final commit `5d7e1a7` produced the same SHA-256, confirming that the workflow-only correction did not alter package bytes. +- Existing full qualification receipt contains both advertised Effect peers. The supplied full ten-combination packed matrix passed under npm 11.19.1. PR 4 CI was reported green for package and Wasm jobs; those checks were not redundantly rerun in this release-only review. +- npm 11.19.1 resolves through the documented `npm exec` form, and `--provenance=false` resolves to `false`, overriding the manifest default for the bootstrap command. + +## Operational handoff + +The reviewed tooling is ready to merge and use. Publication/trust setup still must record the resulting main SHA, registry integrity and `latest` dist-tag, saved trusted-publisher configuration, read-only `mode=verify` run URL, and registry-consumer result in issue 3. A verify-only run must remain recorded as read-only evidence rather than as an OIDC publication test. diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 3c3f89d..514e035 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -44,9 +44,13 @@ account and completing npm's own browser/passkey challenge when requested: ```sh npm exec --yes --package npm@11.19.1 -- npm trust github @misofm/codec \ --repo=misofm/codec --file=npm-publish.yml --allow-publish --yes -npm exec --yes --package npm@11.19.1 -- npm trust list @misofm/codec --json +npm exec --yes --package npm@11.19.1 -- npm trust list @misofm/codec ``` +Use interactive text output for trust commands: JSON output can buffer the +browser authentication link while the command waits. npm may require a separate +authentication for the read-back command as well as the settings change. + This does not install or replace the local global npm client. GitHub-hosted publishing uses short-lived OIDC credentials, and npm generates provenance for those public releases. See the official [trusted publishing documentation](https://docs.npmjs.com/trusted-publishers/) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 39a0003..5264f8c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -401,24 +401,26 @@ jobs: npm init --yes >/dev/null npm install --ignore-scripts --save-exact "$EXPECTED_PACKAGE@$DISPATCH_VERSION" >/dev/null npm audit signatures --json --include-attestations > audit-signatures.json - if test "$DISPATCH_MODE" = publish; then - node --input-type=module <<'NODE' - import { readFile } from "node:fs/promises"; - const report = JSON.parse( - await readFile("audit-signatures.json", "utf8"), - ); - const target = report.verified?.find( - ({ name, version }) => - name === process.env.EXPECTED_PACKAGE && - version === process.env.DISPATCH_VERSION, + node --input-type=module <<'NODE' + import { readFile } from "node:fs/promises"; + if (process.env.DISPATCH_MODE !== "publish") { + process.stdout.write( + "Verify mode was read-only and does not claim an OIDC publication.\n", ); - if ( - target?.attestations?.provenance?.predicateType !== - "https://slsa.dev/provenance/v1" - ) { - throw new Error("OIDC publication lacks a verified provenance attestation"); - } - NODE - else - echo "Verify mode was read-only and does not claim an OIDC publication." - fi + process.exit(0); + } + const report = JSON.parse( + await readFile("audit-signatures.json", "utf8"), + ); + const target = report.verified?.find( + ({ name, version }) => + name === process.env.EXPECTED_PACKAGE && + version === process.env.DISPATCH_VERSION, + ); + if ( + target?.attestations?.provenance?.predicateType !== + "https://slsa.dev/provenance/v1" + ) { + throw new Error("OIDC publication lacks a verified provenance attestation"); + } + NODE