Using the Import Map integrity Key
Permalink to "Using the Import Map integrity Key"Part of ES Modules & Import Map Integrity, this page covers the top-level integrity section of an import map: the JSON shape, how it composes with imports and scopes, what browsers that do not implement it do instead, and how to prove enforcement in DevTools.
Quick Reference
Permalink to "Quick Reference"| Item | Value | Notes |
|---|---|---|
| Where it lives | Top-level key of the import map JSON | Sibling of imports and scopes |
| Key type | A URL string | Resolved against the document base URL; bare names are rejected |
| Value type | Integrity metadata | Same grammar as the integrity attribute, e.g. sha384-… |
| Algorithms | sha256, sha384, sha512 |
Prefer SHA-384; multiple space-separated digests allowed |
| Covers | Entry module, static imports, dynamic import() |
Any module fetch whose URL matches a key |
| Precedence | Element integrity attribute wins |
The map fills in where no attribute exists |
| Delivery | Inline <script type="importmap"> only |
External import map files are not supported |
| CSP | Needs a nonce or a 'sha256-…' source |
It is an inline script like any other |
| Ordering | Before the first module resolution | Emit it in <head> ahead of every module tag |
| Unsupported browsers | Section ignored, map still applied | Modules load unverified — a fail-open default |
The mental model
Permalink to "The mental model"An import map has always been a resolution table: it rewrites the specifier text a module asks for into a URL the browser can fetch. The integrity section adds a second, independent table to the same JSON document — a verification table keyed by the URLs that resolution produces. Nothing joins the two tables except the URL string itself. That separation is the single most important thing to internalise, because it explains almost every mistake people make with the feature. You do not write "lodash-es": "sha384-…"; you write the fully resolved URL that "lodash-es" maps to, and you write it exactly as the browser will construct it.
The consequence is that the verification table also covers URLs that never appear in imports at all. A relative import inside a dependency — import "./chunk-4f2a.js" from a module served off a CDN — resolves through ordinary URL resolution rather than through the map, but the resulting URL can still be listed in the integrity section and it will still be checked. Import maps are not required for the integrity section to do useful work on a page; the map is simply the vehicle that delivers the URL-to-digest table to the browser.
The table is an allow-list of known digests, not a requirement that every module carry one. A module fetched from a URL that has no entry loads exactly as it would have without the map. That is deliberate — it is what makes the feature deployable incrementally — but it means an attacker who can add a new script URL to your page is unaffected by the integrity section. Requiring integrity for scripts is a job for policy, which is covered separately in Configuring Content Security Policy with SRI.
Canonical example: a complete map with both sections
Permalink to "Canonical example: a complete map with both sections"The block below is a full document head. The import map carries all three sections, the inline map is allowed by a per-request nonce, and the entry module is additionally preloaded. Digests are illustrative — generate real ones for your own files before shipping.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Checkout</title>
<script type="importmap" nonce="r4nd0mPerRequestValue">
{
"imports": {
"lodash-es": "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js",
"app/": "/static/app/"
},
"scopes": {
"/static/app/legacy/": {
"lodash-es": "/static/vendor/lodash-4.17.21.js"
}
},
"integrity": {
"https://cdn.jsdelivr.net/npm/[email protected]/lodash.js": "sha384-+W0dS3YpzbsMDp+Chfci3NQWZGBEUBSo9WUfjtxDUh9YlXNsRXCfehx/aQgDyVPw",
"/static/vendor/lodash-4.17.21.js": "sha384-wLdBA5jE9CBAjEDa1E7npl/guxlhLagxqsNim6Hh0YjeTiZqiTVIDislIA1xPIRa",
"/static/app/main.js": "sha384-km62dHbI7ytLMmbWMnh8uqikpKJSDRejTWsE6vSBRr6AEK+u94i8T4mvH9JdU/An",
"/static/app/checkout.js": "sha384-ICUnXL+NOJEEHYQIJMcy+nAYY5erB1kp6RQh+VOAvp7vJRG2KbZcRyURSUniCkvX"
}
}
</script>
<link rel="modulepreload"
href="/static/app/main.js"
integrity="sha384-km62dHbI7ytLMmbWMnh8uqikpKJSDRejTWsE6vSBRr6AEK+u94i8T4mvH9JdU/An"
crossorigin="anonymous">
</head>
<body>
<script type="module" src="/static/app/main.js" crossorigin="anonymous"></script>
</body>
</html>
Read the three sections in the order the browser uses them. imports turns lodash-es into a jsDelivr URL and turns the app/ prefix into /static/app/. scopes overrides that first table for modules whose own URL sits under /static/app/legacy/, pointing them at a self-hosted copy instead. integrity then supplies a digest for four concrete URLs, two of which are the two different lodash builds the previous two sections can produce. Because scopes can steer the same specifier to different files, every reachable target needs its own entry — this is the most common source of gaps in a hand-written map.
/static/app/checkout.js never appears in imports. It is there because main.js contains await import('app/checkout.js') behind a user interaction, and the whole point of the integrity section is that a lazily fetched module gets the same treatment as an eagerly fetched one. The same reasoning applies to build-emitted chunk files, which is the subject of SRI for Lazy-Loaded Chunks.
Two mechanical requirements come with this block. First, <script type="importmap"> is an inline script, so a policy of script-src 'nonce-r4nd0mPerRequestValue' https://cdn.jsdelivr.net is what makes it run at all; the mechanics of minting that value per response are in Generating Per-Request CSP Nonces. Second, the map must be registered before any module resolution begins, which is why it precedes both the modulepreload link and the module script.
Variants
Permalink to "Variants"Generate the map at build time
Permalink to "Generate the map at build time"Hand-maintaining digests is untenable the moment a bundler renames files. Emit the map from the same build step that writes the assets, reading each file off disk:
// scripts/build-import-map.mjs
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
const digest = (file) =>
'sha384-' + createHash('sha384').update(readFileSync(file)).digest('base64');
const integrity = {};
for (const name of readdirSync('dist/static/app')) {
if (name.endsWith('.js')) {
integrity[`/static/app/${name}`] = digest(`dist/static/app/${name}`);
}
}
writeFileSync(
'dist/import-map.json',
JSON.stringify({ imports: { 'app/': '/static/app/' }, integrity }, null, 2)
);
The template then inlines dist/import-map.json into the response body and stamps the nonce. Keep the digest command identical to the one your CI uses so the two never disagree; the equivalent shell one-liners are in Generating SRI Hashes with OpenSSL and shasum.
Put the hash on <link rel="modulepreload"> instead
Permalink to "Put the hash on <link rel="modulepreload"> instead" A modulepreload link accepts an integrity attribute directly, and for a small page that can be enough:
<link rel="modulepreload"
href="https://cdn.jsdelivr.net/npm/[email protected]/lodash.js"
integrity="sha384-+W0dS3YpzbsMDp+Chfci3NQWZGBEUBSo9WUfjtxDUh9YlXNsRXCfehx/aQgDyVPw"
crossorigin="anonymous">
The difference is coverage, not strength. A preload link verifies the one response it fetches, so a module the page never preloads — anything reached by a dynamic import() after a user action, or a chunk name that only exists in one build — is fetched with no check at all. The integrity section attaches metadata to the URL rather than to a tag, so it applies whenever that URL is requested during module loading. The two also interact: if both are present and disagree, the preloaded response cannot satisfy the later import and the browser refetches or fails outright, so keep them generated from one source.
Keep a self-hosted target for the same specifier
Permalink to "Keep a self-hosted target for the same specifier"Because scopes can point one specifier at two different files, the integrity section is a natural place to register both a CDN build and a local build of the same library. Adding the local URL costs one line and makes a switch to self-hosting a configuration change rather than a code change. Deciding when to make that switch, and what it costs, is covered in Serving Local Fallback Bundles.
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
A blocked import map fails as a resolution error, not a security error. If your policy lacks a nonce or hash for the inline map, the map never runs and the browser reports
Uncaught TypeError: Failed to resolve module specifier "lodash-es"— which looks like a bundler problem and sends people to the wrong file. Check the console for a CSP violation on an inline script before touching the map’s contents. -
Omitting
crossoriginon amodulepreloadlink wastes the preload. Module scripts are always fetched in CORS mode with same-origin credentials, so a preload withoutcrossorigin="anonymous"can end up in a different cache partition than the import that follows it, and the file is downloaded twice — once verified, once not reused. Write the attribute on every preload that carriesintegrity. The credentials-mode rules behind this are in How CORS and crossorigin Affect SRI. -
The URL must match the fetch exactly. A query string, a trailing
?v=3, an extra/./segment, or an origin that redirects to another host will all produce a fetch URL that does not equal your key, and the module then loads unchecked with no error anywhere. Copy keys from the Network panel’s request URL column rather than from your source. -
Late maps are ignored. Injecting
<script type="importmap">after the first module has begun resolving produces a console message on the order of an import map is added after module script load was triggered, and the map does nothing. Server-render the map; do not build it from client-side JavaScript. -
HTMLScriptElement.supports('importmap')does not tell you about the integrity section. It returnstrueon every browser that understands import maps at all, including ones that ignore the integrity key. There is no dedicated feature-detection API for the section, so treat it as defence in depth rather than as a control you can prove is active on an arbitrary visitor’s browser.
Verification Steps
Permalink to "Verification Steps"1. Confirm the JSON parses and the digests are right
Permalink to "1. Confirm the JSON parses and the digests are right"node -e 'const m=require("./dist/import-map.json");console.log(Object.keys(m.integrity).length,"entries")'
openssl dgst -sha384 -binary dist/static/app/main.js | openssl base64 -A
The printed base64 must equal the value stored under /static/app/main.js, minus the sha384- prefix. A mismatch here means the map was generated from a different build than the one you are about to deploy.
2. Confirm the map is applied, not blocked
Permalink to "2. Confirm the map is applied, not blocked"Load the page and run this in the DevTools console:
JSON.parse(document.querySelector('script[type="importmap"]').textContent).integrity
An object printing your URL-to-digest pairs proves the element reached the DOM with its content intact. If the console also shows a Refused to execute inline script violation, the CSP nonce is missing or stale and the map was never registered — fix that before reading anything else.
3. Prove enforcement with a deliberately wrong digest
Permalink to "3. Prove enforcement with a deliberately wrong digest"On a staging copy, change one character in one digest and reload with the Network panel open.
Failed to find a valid digest in the 'integrity' attribute for resource
'https://cdn.jsdelivr.net/npm/[email protected]/lodash.js' with computed
SHA-384 integrity '+W0dS3Ypz...'. The resource has been blocked.
The request appears in the Network panel with the response received and then the module fails: a static import kills the graph, and a dynamic one rejects with TypeError: Failed to fetch dynamically imported module. Seeing that pair is the only reliable evidence the section is being enforced in the browser you are testing. If the module loads normally instead, this browser is ignoring the integrity key — the degradation path. Interpreting the failure text itself is covered in Debugging SRI Hash Mismatch Errors.
4. Confirm coverage of the lazy path
Permalink to "4. Confirm coverage of the lazy path"Restore the correct digest, then trigger the interaction that runs import('app/checkout.js') and confirm the chunk request appears in the Network panel after the click, not during initial load. Corrupt that entry too and repeat step 3 — this is the case a preload-based approach silently misses.
Frequently Asked Questions
Permalink to "Frequently Asked Questions"Which browsers support the import map integrity key?
Chromium-based browsers shipped it first, in Chrome and Edge 127. Support in other engines has arrived later and unevenly, so treat the current MDN and caniuse entries as the authority rather than any date written in a guide. Import maps themselves are supported far more broadly than the integrity section is, which is exactly why the degradation behaviour matters.
What happens in a browser that does not support the integrity section?
The map still works. Import map parsing ignores unrecognised top-level keys and reports a console warning, so imports and scopes continue to drive module resolution and the modules load normally without being hash-checked. That is a fail-open outcome, which means the integrity section hardens a page but cannot be the only control you rely on.
Can I use a bare specifier as a key in the integrity section?
No. Keys in the integrity section are parsed as URLs, resolved against the document base URL, and matched against the URL a module is actually fetched from. A bare name such as lodash-es is not a valid URL, so the entry is discarded with a console warning. Use the same absolute or root-relative URL that appears on the right-hand side of your imports map.
Can a page have more than one import map?
The portable assumption is one map, registered before any module resolution happens. Newer Chromium versions accept several maps and merge them in document order, but that behaviour is not available everywhere yet. If you build a page from independent fragments, merge their maps server-side into a single block rather than emitting one per fragment.
Does an integrity attribute on the script tag override the import map entry?
Yes. When a script or link element carries its own integrity attribute, that metadata is used for that element’s fetch and the map is not consulted for it. The integrity section supplies metadata for fetches that have no attribute of their own, which is every static and dynamic import inside the module graph.
Related
Permalink to "Related"- Layering CSP Nonces, SRI and Trusted Types — how the nonce that admits the inline map fits into a whole-page script policy
- SRI for ES Module Imports — what the
integrityattribute can and cannot reach once a module graph starts loading - Configuring SRI for jsDelivr and unpkg — pinning the CDN URLs you put on the right-hand side of an import map