Skip to content

fix(bundle): content-address bundled script filenames for stable SRI#725

Merged
harlan-zw merged 4 commits intomainfrom
fix/bundle-sri-stable-filename
Apr 23, 2026
Merged

fix(bundle): content-address bundled script filenames for stable SRI#725
harlan-zw merged 4 commits intomainfrom
fix/bundle-sri-stable-filename

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Resolves #724

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Bundled script public URLs were hashed from the upstream src, so the filename stayed constant across deploys even when the fetched content (or proxy rewrites) changed. Long-cached JS at the unchanged URL then got served against a new integrity hash in fresh HTML, breaking SRI on the second deploy (notably reproducible with AdSense + bundle + proxy).

The bundler now derives the public filename from a sha256 of the final bundled bytes, so any content change flips the URL and keeps SRI consistent. Cached integrity hashes are dropped from bundle metadata since integrity is now computed fresh from cached content on each build.

Bundled script public URLs were hashed from the upstream src, so the
filename stayed constant across deploys even when the fetched content
(or proxy rewrites) changed. Long-cached JS then got served against a
new integrity hash in fresh HTML, breaking SRI on the second deploy.

Derive the public filename from a sha256 of the final bundled bytes so
content changes always flip the URL, and drop the cached integrity hash
(it is now computed fresh from cached content each build).

Resolves #724
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Apr 23, 2026 6:01am

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@725

commit: ede57c7

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a15c48d9-0c27-4f69-a25a-0dd43009669c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fcdabc and ede57c7.

📒 Files selected for processing (1)
  • test/unit/bundle-two-deploy-repro.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/unit/bundle-two-deploy-repro.test.ts

📝 Walkthrough

Walkthrough

Adds centralized helpers to sanitize hash-derived filenames and build asset URLs from Nuxt cdnURL/baseURL plus assetsBaseURL. Refactors script normalization to use these helpers. downloadScript now returns a content-addressed public filename/URL (SHA-256, sanitized), defers persisting rendered results until content and SRI are finalized, and computes integrity after any proxy/AST rewrites. The transformer uses the returned publicUrl for subsequent AST updates. Tests and snapshots updated to expect content-addressed asset filenames and to validate integrity computation and caching behavior across deployments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: converting bundled script filenames to be content-addressed for stable SRI across deployments.
Description check ✅ Passed The description is directly related to the changeset, explaining the bug (unchanged filenames across deploys causing SRI mismatches) and the solution (content-addressed filenames).
Linked Issues check ✅ Passed The PR resolves issue #724 by implementing content-addressed filenames derived from final bundled bytes (sha256) instead of upstream source, ensuring filenames change when content changes and integrity hashes stay consistent across deployments.
Out of Scope Changes check ✅ Passed All changes directly address the linked issue #724: core logic refactoring in transform.ts, comprehensive test updates reflecting new content-addressed filenames, and e2e snapshot updates—no unrelated modifications present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/bundle-sri-stable-filename

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/unit/transform.test.ts (1)

683-720: ⚠️ Potential issue | 🟡 Minor

Reset cache mocks after this scenario to avoid order-dependent tests.

This test leaves the storage mocks configured as a fresh cache hit with cachedContent. The following top-level tests then inherit that state and their 951c324253eef4b3.js snapshots depend on this test running first.

🧪 Proposed isolation fix
-    beforeEach(() => {
+    const resetBundleStorageMocks = () => {
       // Reset all mocks for bundleStorage
       mockBundleStorage.getItem.mockReset()
       mockBundleStorage.setItem.mockReset()
       mockBundleStorage.getItemRaw.mockReset()
       mockBundleStorage.setItemRaw.mockReset()
       mockBundleStorage.hasItem.mockReset()
       vi.clearAllMocks()
-    })
+    }
+
+    beforeEach(resetBundleStorageMocks)
+    afterEach(resetBundleStorageMocks)

Also add afterEach to the Vitest import.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/transform.test.ts` around lines 683 - 720, This test leaves shared
mocks in a "fresh cache" state and causes order-dependent failures; add an
afterEach cleanup that resets all Vitest mocks and clears any shared
renderedScript state. Specifically, import afterEach from Vitest alongside vi,
then add afterEach(() => { vi.resetAllMocks();
mockBundleStorage.hasItem.mockReset?.();
mockBundleStorage.getItem.mockReset?.();
mockBundleStorage.getItemRaw.mockReset?.(); renderedScript?.clear?.(); }); so
mockBundleStorage methods and the renderedScript Map are reset between tests and
no test order dependency remains.
🧹 Nitpick comments (2)
test/unit/transform.test.ts (1)

1190-1240: Assert the exact hashes, not just the hash shape.

These tests would still pass if the filename/integrity were any 16-hex value or any sha384-* string. Since this PR is specifically about deriving both from final bytes, compare against the digest of cachedContent/scriptContent.

🧪 Proposed stronger assertions
-      // Integrity is computed fresh from cached content, yielding a deterministic SRI hash.
-      expect(code).toMatch(/integrity: 'sha384-[A-Za-z0-9+/=]+'/)
+      const expectedIntegrity = createHash('sha384').update(cachedContent).digest('base64')
+      expect(code).toContain(`integrity: 'sha384-${expectedIntegrity}'`)
@@
-      expect(code).not.toContain('/_scripts/assets/beacon.min.js')
-      expect(code).toMatch(/\/_scripts\/assets\/[a-f0-9]{16}\.js/)
+      const expectedFilename = createHash('sha256').update(scriptContent).digest('hex').slice(0, 16)
+      expect(code).not.toContain('/_scripts/assets/beacon.min.js')
+      expect(code).toContain(`/_scripts/assets/${expectedFilename}.js`)

Add this import near the top of the test file:

+import { createHash } from 'node:crypto'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/transform.test.ts` around lines 1190 - 1240, The tests currently
only assert hash shape; instead compute the actual content digests from the
final bytes and assert exact values: for the first case derive the expected SRI
(sha384) from cachedContent (the Buffer assigned to cachedContent) and assert
transform(...) output contains that exact integrity string; for the second case
compute the expected 16-hex content filename from scriptContent (the Buffer
assigned to scriptContent) and assert transform(...) output contains that exact
filename pattern rather than any 16-hex placeholder. Locate uses of transform,
cachedContent, scriptContent, mockBundleStorage and the mocked fetch to obtain
the bytes to hash and replace the loose regex assertions with exact equality
checks against the computed digests.
packages/script/src/plugins/transform.ts (1)

142-176: Include all rewrite inputs in the cache identity.

cacheKey only hashes proxyRewrites, but the cached bytes also depend on sdkPatches, skipApiRewrites, and neutralizeCanvas at Line 166. Changing any of those while the cache is fresh can reuse stale rewritten JS. The shared bundle-meta:${filename} timestamp also lets one variant refresh another variant’s expiry.

♻️ Proposed cache identity adjustment
-  const proxyRewritesHash = proxyRewrites?.length ? `-${ohash(proxyRewrites)}` : ''
-  const cacheKey = proxyRewrites?.length ? `bundle-proxy:${filename.replace('.js', `${proxyRewritesHash}.js`)}` : `bundle:${filename}`
-  const shouldUseCache = !forceDownload && await storage.hasItem(cacheKey) && !(await isCacheExpired(storage, filename, cacheMaxAge))
+  const rewriteOptionsHash = proxyRewrites?.length
+    ? `-${ohash({ proxyRewrites, sdkPatches, skipApiRewrites, neutralizeCanvas })}`
+    : ''
+  const cacheKey = proxyRewrites?.length ? `bundle-proxy:${filename.replace(/\.js$/, `${rewriteOptionsHash}.js`)}` : `bundle:${filename}`
+  const metaFilename = cacheKey.replace(/^bundle(?:-proxy)?:/, '')
+  const shouldUseCache = !forceDownload && await storage.hasItem(cacheKey) && !(await isCacheExpired(storage, metaFilename, cacheMaxAge))
@@
-    await storage.setItem(`bundle-meta:${filename}`, {
+    await storage.setItem(`bundle-meta:${metaFilename}`, {
       timestamp: Date.now(),
       src,
-      filename,
+      filename: metaFilename,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/script/src/plugins/transform.ts` around lines 142 - 176, The cache
key currently only includes proxyRewritesHash (proxyRewritesHash, cacheKey)
while the stored bundle bytes also depend on sdkPatches, skipApiRewrites, and
neutralizeCanvas used by rewriteScriptUrlsAST; update the cache identity to
incorporate a hash of proxyRewrites + sdkPatches + skipApiRewrites +
neutralizeCanvas (or a combined optionsHash) so cacheKey uniquely represents all
rewrite inputs, and ensure metadata is stored per-variant (use the same
variant-aware key instead of bundle-meta:${filename}) so timestamp/expiry is
tied to the exact cacheKey.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@test/unit/transform.test.ts`:
- Around line 683-720: This test leaves shared mocks in a "fresh cache" state
and causes order-dependent failures; add an afterEach cleanup that resets all
Vitest mocks and clears any shared renderedScript state. Specifically, import
afterEach from Vitest alongside vi, then add afterEach(() => {
vi.resetAllMocks(); mockBundleStorage.hasItem.mockReset?.();
mockBundleStorage.getItem.mockReset?.();
mockBundleStorage.getItemRaw.mockReset?.(); renderedScript?.clear?.(); }); so
mockBundleStorage methods and the renderedScript Map are reset between tests and
no test order dependency remains.

---

Nitpick comments:
In `@packages/script/src/plugins/transform.ts`:
- Around line 142-176: The cache key currently only includes proxyRewritesHash
(proxyRewritesHash, cacheKey) while the stored bundle bytes also depend on
sdkPatches, skipApiRewrites, and neutralizeCanvas used by rewriteScriptUrlsAST;
update the cache identity to incorporate a hash of proxyRewrites + sdkPatches +
skipApiRewrites + neutralizeCanvas (or a combined optionsHash) so cacheKey
uniquely represents all rewrite inputs, and ensure metadata is stored
per-variant (use the same variant-aware key instead of bundle-meta:${filename})
so timestamp/expiry is tied to the exact cacheKey.

In `@test/unit/transform.test.ts`:
- Around line 1190-1240: The tests currently only assert hash shape; instead
compute the actual content digests from the final bytes and assert exact values:
for the first case derive the expected SRI (sha384) from cachedContent (the
Buffer assigned to cachedContent) and assert transform(...) output contains that
exact integrity string; for the second case compute the expected 16-hex content
filename from scriptContent (the Buffer assigned to scriptContent) and assert
transform(...) output contains that exact filename pattern rather than any
16-hex placeholder. Locate uses of transform, cachedContent, scriptContent,
mockBundleStorage and the mocked fetch to obtain the bytes to hash and replace
the loose regex assertions with exact equality checks against the computed
digests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 47cbfdcb-a74b-49d9-9d18-9ba07a066c42

📥 Commits

Reviewing files that changed from the base of the PR and between 9a77830 and c83af87.

📒 Files selected for processing (2)
  • packages/script/src/plugins/transform.ts
  • test/unit/transform.test.ts

Guards against regression of URL-hash-only filenames producing SRI
mismatches when upstream content changes between deployments.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/unit/bundle-two-deploy-repro.test.ts`:
- Around line 4-8: The test currently only checks that an integrity string
starting with "sha384-" was emitted; instead compute the actual SHA-384 SRI of
the served bundle bytes and assert exact equality. In the test that uses
NuxtScriptBundleTransformer (the block around the existing integrity check), get
the emitted script content (the response body or the bundle string used to
generate the tag), compute the base64 SHA-384 digest (e.g., using Node's crypto:
createHash('sha384').update(content).digest('base64')) and compare that value
prefixed with "sha384-" to the integrity attribute parsed from the generated tag
(the variable currently asserted for presence). Replace the loose
regex/assertion with an equality assertion between the computed
"sha384-<base64>" and the integrity attribute so the hash matches the actual
bytes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3963638f-c0c3-48dd-b86d-2f98a29a63b1

📥 Commits

Reviewing files that changed from the base of the PR and between c83af87 and 184120b.

📒 Files selected for processing (1)
  • test/unit/bundle-two-deploy-repro.test.ts

Comment thread test/unit/bundle-two-deploy-repro.test.ts
@harlan-zw harlan-zw merged commit f979ede into main Apr 23, 2026
17 checks passed
@harlan-zw harlan-zw deleted the fix/bundle-sri-stable-filename branch April 23, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrity hash mismatch occurs on subsequent deployments when using AdSense (bundle + proxy)

1 participant