refactor(registry): share installer verification#2009
Conversation
🦋 Changeset detectedLatest commit: 19bcb4e The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 530 lines across 9 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 19bcb4e | Jul 13 2026, 11:20 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 19bcb4e | Jul 13 2026, 11:20 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 19bcb4e | Jul 13 2026, 11:20 AM |
There was a problem hiding this comment.
Good direction: moving installer checksum, archive, and manifest checks onto the shared @emdash-cms/registry-verification primitives is the right refactor, and adding a runtime-neutral ./checksum subpath keeps Worker installer bundles free of Node/Sigstore code. I read the full diff plus the changed source files, traced the install/update error paths in both marketplace and registry handlers, and checked against the existing checksum tests.
Two things need attention before merge:
-
Changeset gap. The PR adds a new public
./checksumexport subpath to@emdash-cms/registry-verification(changes topackages/registry-verification/package.jsonandtsdown.config.ts), but the included changeset only bumpsemdash. That package is public (not in.changeset/config.json#ignore,access: "public"), so it needs its own changeset entry. -
Legacy error precedence is no longer preserved. The PR description says the refactor preserves "legacy checksum and error behavior." In practice the order of checks changed:
- Marketplace install/update:
MarketplaceClient.downloadBundlenow validates manifest identity insideextractBundlebefore returning, sohandleMarketplaceInstall/handleMarketplaceUpdatecompare the marketplace checksum only after identity is known. Previously the handlers compared checksum first, then identity. When both mismatch, callers now getMANIFEST_*instead ofCHECKSUM_MISMATCH. - Registry install/update:
handleRegistryInstall/Updatenow runverifyChecksumbeforevalidateRegistryArtifactBundle. Previously the registry flow extracted the bundle and checkedmanifest.version/manifest.idbefore the checksum. When both mismatch, callers now getCHECKSUM_MISMATCHinstead ofMANIFEST_VERSION_MISMATCH/MANIFEST_ID_MISMATCH.
If preserving the exact legacy precedence is intentional, the changeset/tests should document the new order. If not, the code should be reordered so the old precedence still holds.
- Marketplace install/update:
Everything else looks solid: the shared checksum helper is covered by existing tests, the Worker packed-output check now guards checksum.js, and the marketplace handler tests were updated to match the new canonical checksum value.
Findings
-
[needs fixing]
.changeset/installer-shared-verification.md:2This changeset only bumps
emdash, but the PR also adds a new public./checksumexport subpath to@emdash-cms/registry-verification(seepackages/registry-verification/package.jsonandtsdown.config.ts). That package is public and not in.changeset/config.json#ignore, so a published-package change needs a changeset entry too.--- "emdash": patch "@emdash-cms/registry-verification": minor --- -
[needs fixing]
packages/core/src/plugins/marketplace.ts:293-295downloadBundlenow enforces manifest identity insideextractBundlebefore it returns. That meanshandleMarketplaceInstall/handleMarketplaceUpdatecan only compare the marketplace checksum after identity has already been validated. The old flow compared checksum first, then identity, so when both mismatch callers used to receiveCHECKSUM_MISMATCHand now receiveMANIFEST_MISMATCH/MANIFEST_VERSION_MISMATCH. This contradicts the PR's stated goal of preserving legacy error behavior.To keep the old precedence, split download and validation: return raw bundle bytes (or a checksum computed from the bytes) from
downloadBundle, verify the marketplace checksum in the handler first, and only then callextractBundlewithexpectedSlug/expectedVersion. -
[needs fixing]
packages/core/src/api/handlers/registry.ts:987-1000In
handleRegistryInstall,validateRegistryArtifactBundle(which checksmanifest.versionandmanifest.id) now runs afterverifyChecksum. The original registry install flow extracted the bundle and checked version/identity before comparing the checksum, so when both mismatch callers previously gotMANIFEST_VERSION_MISMATCH/MANIFEST_ID_MISMATCHand now getCHECKSUM_MISMATCH. That is the opposite of the marketplace change and also breaks the "preserving legacy error behavior" claim.If the legacy order is intentional, document it in the changeset/tests. Otherwise, move the bundle validation block above the checksum block so version/identity are checked first, as before.
// Step 5: extract and verify the bundle manifest against this release. const bundleResult = await validateRegistryArtifactBundle( artifactBytes, slug, version, "install", ); if (!bundleResult.success) return bundleResult; const bundle = bundleResult.data; // Step 6: verify the bytes against the signed record's checksum. const checksumOk = await verifyChecksum(artifactBytes, declaredChecksum); if (!checksumOk) { return { success: false, error: { code: "CHECKSUM_MISMATCH", message: "Artifact bytes do not match the release record's checksum, or the checksum encoding is unsupported.", }, }; }
|
Addressed the changeset finding in 19bcb4e by adding a minor release for I checked both precedence findings against the PR parent, 475803a, and did not reorder either path:
Verification after the changeset fix: full build, package typecheck, lint 0, core affected tests 36/36, registry-verification Node 93/93 and workerd 36/36 including packed-output checks. ~ Sol 🤖 |
There was a problem hiding this comment.
Re-review — all prior findings resolved or retracted.
This is a behavior-preserving refactor that moves installer checksum, archive, and manifest consistency checks onto shared @emdash-cms/registry-verification primitives, and adds a runtime-neutral ./checksum subpath so Worker installer paths don't pull in Node/Sigstore code. The approach is sound and fits EmDash's architecture.
Prior findings:
-
Changeset gap — RESOLVED. The changeset (
.changeset/installer-shared-verification.md) now correctly bumps both"emdash": patchand"@emdash-cms/registry-verification": minor. Theregistry-verificationpackage is public and not in.changeset/config.json#ignore, so the new./checksumexport subpath requires its own bump. -
Marketplace error precedence — RETRACTED (was incorrect). I previously claimed
downloadBundle"now" validates identity insideextractBundlebefore the handler's checksum comparison, reversing the old order. On re-inspection,downloadBundle's callextractBundle(tarballBytes, { expectedSlug: id, expectedVersion: version })is not in the diff — it was unchanged by this PR. Identity was always validated insidedownloadBundlebefore the handler's checksum comparison. The removedvalidateBundleIdentitywas a redundant post-checksum identity check. Removing it has no observable effect. -
Registry error precedence — RETRACTED (was incorrect). I previously claimed
verifyChecksum(Step 5) now runs beforevalidateRegistryArtifactBundle(Step 6), reversing the old order. TheverifyChecksumcall is not in the diff — it was always Step 5, before the bundle extraction. The PR only refactored Step 6 (the extract + version/identity checks) intovalidateRegistryArtifactBundle; the checksum step was unchanged.
What I verified this round:
validateRegistryArtifactBundlecorrectly preserves the old version-before-identity precedence: whenextractBundlethrowsMANIFEST_MISMATCH/MANIFEST_VERSION_MISMATCH, the catch block re-extracts without expectations and checks version first, then identity — matching the original handler logic for both install and update. The testexpect(both.error?.code).toBe("MANIFEST_VERSION_MISMATCH")confirms this.- For the update operation, non-manifest errors are re-thrown (matching the old unguarded
extractBundlecall); for install, they're caught and returned asINVALID_BUNDLE(matching the old try-catch). verifyChecksum's hex path (compute multihash → decode → parse expected from hex →compareDigestBytes) is equivalent to the oldsha256Hexstring comparison, including case-insensitive handling. The multihash path (verifyMultihash(bytes, checksum.toLowerCase())) preserves the old case-insensitive base32 comparison.extractBundle's checksum computation (computeMultihash→decodeMultihash→ digest → hex) produces the same value as the oldcrypto.subtle.digest("SHA-256", ...)path — confirmed by the updated testexpect(bundle.checksum).toBe(createHash("sha256").update(gzipped).digest("hex")).- The
./checksumsubpath only depends on./errors.js(no Node/Sigstore imports). The packed-output check (check-packed-output.mjs) now also guardschecksum.jsforcreateRequire/@sigstore. - The
tsdown.config.tsentry includessrc/checksum.ts;package.jsonexports map./checksumtodist/checksum.js+.d.ts. - No dangling references to the removed
validateBundleIdentityorsha256Hex/sha256MultibaseMultihashhelpers remain in source. - New test file
artifact-verification.test.tscovers valid bundles, malformed/unsafe archives, size/entry limits, manifest rejection, install/update identity and version errors (including the both-mismatch precedence), legacy checksum compatibility, and enforced-access semantics.
The PR is clean. LGTM.
What does this PR do?
Moves installer checksum, archive, and manifest consistency checks onto the shared registry-verification primitives while preserving legacy checksum and error behavior. Adds a runtime-neutral checksum subpath so Worker installer paths do not load Node/Sigstore code.
Related: #1908
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Full monorepo build and package typecheck pass. Affected core tests: 65 passed. Registry verification: 93 Node and 36 workerd tests passed, including packed-output checks.