Source Maps Are Leaking Your Code: How to Stop It in Your npm Packages
Source maps with sourcesContent can leak your entire codebase. Learn how to disable them per bundler, audit with npm pack, and automate the check in CI.
On this page
A source map is a small JSON file that maps your minified, bundled production JavaScript back to the original source code you actually wrote. They exist so that when an error happens in production, your error tracker can show you a readable stack trace instead of a pointer into a single 2 MB line of minified code.
The problem is one specific field inside that JSON: sourcesContent. When populated, it contains the complete original source files, every line, every comment, in plain text, embedded directly inside the map. If that map file ends up somewhere public, so does your entire original codebase.
This is not a hypothetical. A widely covered packaging mistake earlier this year made this exact failure mode the talk of the JavaScript ecosystem for a week: a published npm package included a source map with sourcesContent populated, and within hours the original TypeScript source had been fully reconstructed and mirrored publicly. The mechanism was identical to dozens of smaller, less-publicized incidents that happen regularly.
Here is how to make sure it does not happen to your project.
What's Actually in the File
A source map looks roughly like this:
{
"version": 3,
"sources": ["../src/utils/auth.ts"],
"sourcesContent": [
"export function validateToken(token: string) {\n // full original source here...\n}"
],
"mappings": "AAAA,SAASA..."
}The sources array lists original file paths. The sourcesContent array, when present, contains the full text of each of those files. That second array is the part that turns a debugging convenience into a code disclosure risk.
If a .map file with sourcesContent populated ends up publicly accessible (published to npm, deployed to a public web server, left in a public CDN bucket) anyone who finds it can reconstruct the entire original codebase. No exploit required, just a downloaded file.
How the mappings Field Works
The mappings field is a Base64 VLQ-encoded string that records position relationships between the bundled output and the original sources. Each segment maps a specific character position in the minified file to a line, column, source file index, and original position.
On its own, mappings tells a tool where each piece of code came from, but it does not reveal what the original code looked like. That is the job of sourcesContent. Without it, someone would need access to your source files on disk to make the mappings useful. With it, the source map is entirely self-contained, no disk access needed.
This distinction matters because some developers assume that since the mappings field is encoded, it obscures the source. It does not. The encoding is for compression, not confidentiality. And when sourcesContent is present, even the encoding is irrelevant because the raw source sits right there in plain text.
What Can Actually Be Extracted
The damage from a leaked source map goes well beyond someone reading your variable names. Here is a concrete picture of what becomes available when a source map with sourcesContent is publicly accessible.
- Full original source code including TypeScript types, private utility functions, and internal abstractions that were never meant to be public API
- Comments and TODOs that may reference internal systems, upcoming features, or known vulnerabilities you have not patched yet
- File and directory structure via the
sourcesarray, revealing your project layout, module boundaries, and internal naming conventions - Hardcoded credentials if any API keys, tokens, or connection strings were embedded in the source (even if the bundler replaced them in the output, the source map preserves the original)
- Business logic and algorithms including proprietary calculations, pricing rules, validation logic, and any competitive advantage embedded in your code
- Internal API endpoints and data shapes such as admin routes, undocumented parameters, and internal service URLs that should never face the public internet
Extracting this data is trivial. A single command with a tool like source-map (an npm package specifically designed for parsing source maps) can dump every original file to disk:
const { SourceMapConsumer } = require("source-map");
const fs = require("fs");
const raw = fs.readFileSync("dist/index.js.map", "utf8");
const map = JSON.parse(raw);
// Each entry in sourcesContent is the full original file
map.sources.forEach((filePath, i) => {
const content = map.sourcesContent[i];
console.log(`--- ${filePath} ---`);
console.log(content);
});Anyone who downloads your published package can run this script in under a minute. There is no authentication, no rate limiting, and no way to revoke the data once the package version is published.
Disabling Source Maps Per Bundler
The fix depends on your build tool, but the principle is the same everywhere: do not generate sourcesContent in artifacts that leave your build environment.
Webpack
// webpack.prod.js
module.exports = {
mode: 'production',
devtool: false, // No source maps at all
// OR, if you need maps for error tracking but not public exposure:
// devtool: 'hidden-source-map',
}Setting devtool: false is the safest option. If you need source maps for error tracking (covered below), use hidden-source-map instead. It generates the file but omits the //# sourceMappingURL comment that tells browsers where to find it.
One common mistake with Webpack is using nosources-source-map and assuming it is safe. This option generates a source map that includes file paths and line mappings but strips out sourcesContent. While that does remove the raw source, the file paths and position data can still reveal your project structure and help an attacker understand your codebase layout. For npm packages, devtool: false remains the correct choice.
Vite / Rollup
// vite.config.ts
export default defineConfig({
build: {
sourcemap: false, // or 'hidden' to generate without the public reference
},
})When building a library with Vite in lib mode, the sourcemap option works identically. However, double-check your Rollup config if you are using Rollup directly, since Rollup's output.sourcemap option defaults to false but can be overridden by plugins like @rollup/plugin-typescript which may inject their own source map settings.
esbuild
// esbuild.config.js
require("esbuild").buildSync({
entryPoints: ["src/index.ts"],
bundle: true,
outfile: "dist/index.js",
sourcemap: false, // explicitly disable
// For error tracking: sourcemap: "external"
// Then upload and delete the .map file before publishing
})esbuild supports several source map modes: linked (default when enabled), external, inline, and both. For npm packages, set sourcemap: false. If you need maps for error tracking, use external and handle the map files in your CI pipeline before publishing.
TypeScript Compiler
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declarationMap": false
}
}Keep source maps enabled in your development config; disable them only in the production build config. Using two separate tsconfig files for dev vs build keeps this explicit rather than relying on an environment variable check that is easy to forget.
Note that declarationMap is a separate setting from sourceMap. Declaration maps (.d.ts.map files) map your type declarations back to the original TypeScript source. While less risky than full source maps (they only reference type positions), they still expose file paths and can contain sourcesContent in some configurations. Disable both for published packages.
Bun
Bun generates source maps by default. This is the detail that catches people off guard, since most bundlers default to maps-off in production mode. Pin the flag explicitly rather than relying on documented default behavior:
bun build ./src/index.ts --outdir ./dist --sourcemap=noneNext.js
Next.js generates source maps for server-side code by default but not for client-side bundles. If you have enabled client-side source maps (via productionBrowserSourceMaps), make sure you understand the implications:
// next.config.js
module.exports = {
productionBrowserSourceMaps: false, // default, keep it this way
// If you need maps for error tracking:
webpack: (config, { isServer }) => {
if (!isServer) {
config.devtool = 'hidden-source-map';
}
return config;
},
}Setting productionBrowserSourceMaps: true is one of the most common ways source maps accidentally end up on public-facing websites. Once deployed, every visitor's browser can fetch and parse those maps. Keep this at false and use the hidden-source-map pattern with your error tracker instead.
The npm pack Dry-Run Habit
Before every publish, run:
npm pack --dry-runThis prints exactly what would be included in the published tarball, without actually publishing anything. Read the file list. If you see any .map files in there, stop and fix your config before publishing.
npm pack --dry-run
npm notice
npm notice 📦 your-package@2.1.0
npm notice === Tarball Contents ===
npm notice 1.2kB package.json
npm notice 4.5kB dist/index.js
npm notice 89.3kB dist/index.js.map ← this should not be here
npm notice 2.1kB README.mdThis one command, run as a habit before every release, would have caught the high-profile incident mentioned earlier before it ever reached the registry.
For pnpm users, the equivalent is pnpm pack --dry-run. Yarn does not have a direct equivalent, but you can run yarn pack --dry-run in Yarn v1 or inspect the tarball contents after yarn pack in Yarn v3+. Regardless of your package manager, the principle is the same: inspect before you publish.
Explicit Allowlisting in package.json
Relying on .npmignore to exclude .map files is a blocklist approach: it fails open. If someone adds a new build step that generates files in an unexpected location, .npmignore will not know to exclude them unless someone remembers to update it.
An explicit files field in package.json is an allowlist: it fails safe. Only what is explicitly listed gets published, regardless of what else exists in your working directory:
{
"name": "your-package",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"README.md",
"LICENSE"
]
}Notice what is absent from that list: *.map. Anything not explicitly included simply does not ship, no matter what your build process generates. Check your own package.json against these risks instantly with SourceMapCheck.
Common Allowlist Mistakes
Even with a files field, certain patterns can accidentally include source maps. Watch out for these:
// DANGEROUS: this glob matches .map files too
{
"files": ["dist/**"]
}
// SAFE: restrict to specific extensions
{
"files": ["dist/**/*.js", "dist/**/*.d.ts"]
}
// ALSO DANGEROUS: including the src directory
{
"files": ["dist", "src"]
}The first pattern (dist/**) matches every file in the dist directory, including .map files. The third pattern includes src, which ships your raw TypeScript source directly, defeating the purpose of minification entirely. Always use specific file extensions in your allowlist.
Automating the Check in CI
Manual habits get forgotten under deadline pressure. The reliable fix lives in CI, not in memory. Add this as a CI gate using the same workflow pattern you use for other security checks:
# .github/workflows/publish.yml
- name: Audit package contents before publish
run: |
npm pack --dry-run > pack-output.txt 2>&1
if grep -q '\.map' pack-output.txt; then
echo "::error::Source map file detected in package contents."
exit 1
fiAdd this as a required step before any publish job. It fails the build automatically if a .map file would be included, so no human has to remember to check.
A More Thorough CI Script
The basic grep check catches .map files by extension, but a more robust script can also look for inline source maps and sourcesContent embedded in non-map files:
#!/bin/bash
# ci/check-source-maps.sh
set -euo pipefail
echo "Packing tarball for inspection..."
npm pack --dry-run > pack-output.txt 2>&1
# Check for .map files in the package
if grep -q '\.map' pack-output.txt; then
echo "FAIL: .map file detected in package contents"
grep '\.map' pack-output.txt
exit 1
fi
echo "Building package to inspect output files..."
npm run build
# Check for inline source maps in bundled JS files
if grep -rq '//# sourceMappingURL=data:' dist/; then
echo "FAIL: Inline source map detected in dist/ output"
grep -rl '//# sourceMappingURL=data:' dist/
exit 1
fi
# Check for sourcesContent in any JSON files
if grep -rq '"sourcesContent"' dist/; then
echo "FAIL: sourcesContent found in dist/ files"
exit 1
fi
echo "PASS: No source map leaks detected"This extended check catches three categories of leaks: standalone .map files, inline source maps (where the entire map is Base64-encoded inside a data: URL at the end of the JS file), and any stray sourcesContent fields in JSON files within your dist directory.
Monorepo and Workspace Considerations
In a monorepo setup, each publishable package has its own build config and its own package.json. The source map risk multiplies because a single misconfigured package in a workspace can leak code while every other package is correctly configured.
The most common monorepo failure mode is a shared build config that works correctly for applications (which are deployed, not published) but generates source maps for libraries (which are published to npm). Applications and libraries have different threat models, so they need different build settings.
{
"name": "@your-org/shared-lib",
"files": ["dist/**/*.js", "dist/**/*.d.ts"],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --no-sourcemap",
"prepublishOnly": "npm run build && node ../scripts/check-no-maps.js"
}
}The prepublishOnly script runs automatically before npm publish. Pointing it at a shared validation script means every package in the workspace gets the same pre-publish check. If you use a tool like Turborepo or Nx, add the source map check as a pipeline dependency of your publish task so it runs automatically across all packages.
If You Need Source Maps for Error Tracking
There is a legitimate reason to generate source maps in production: readable stack traces in your error tracking tool. The safe pattern is to generate them, upload them privately to your error tracker during the build, then delete them before the final artifact ships:
# Build with source maps
npm run build # generates dist/*.js and dist/*.js.map
# Upload maps to your error tracker (example: Sentry CLI)
sentry-cli sourcemaps upload ./dist
# Delete the maps before deploying
rm dist/*.js.mapThe map files exist briefly during the build pipeline, get uploaded to a private destination, and never appear in the artifact that actually ships. Your error tracker can still de-minify stack traces using its private copy; nothing public-facing ever has access to your original source.
Error Tracker Upload Examples
Each error tracking service has its own CLI and upload pattern. Here are the most common ones:
# Sentry
sentry-cli sourcemaps upload --release=v2.1.0 ./dist
# Datadog
datadog-ci sourcemaps upload ./dist \
--service=my-app \
--release-version=v2.1.0 \
--minified-path-prefix=/static/js
# Bugsnag
bugsnag-source-maps upload-browser \
--api-key=YOUR_KEY \
--app-version=2.1.0 \
--directory=./dist
# After uploading to ANY of these, always delete the maps
rm dist/*.js.mapThe critical step that is easy to forget: <strong>delete the map files after uploading</strong>. If your CI pipeline does not explicitly remove them, they can end up in your deploy artifact or your npm package. Add the deletion as an explicit step rather than relying on a post-build cleanup script.
DepScan audits what you are installing; this guide covers what you might be accidentally publishing. The same build-hygiene discipline that Bumblebee enforces at the machine level applies to your own published packages.
Pre-publish Safety Checklist
Before every npm publish, walk through this checklist. It takes under a minute and covers the most common source map leak vectors:
- Build config sets
sourcemap: false(or equivalent) for production package.jsonhas an explicitfilesfield with specific extensions (not bare directory globs)npm pack --dry-runoutput contains zero.mapfiles- No
//# sourceMappingURL=data:inline maps in bundled JS files tsconfig.build.jsonhas bothsourceMapanddeclarationMapset tofalse- CI pipeline includes an automated source map check that blocks publishing
- If using error tracking uploads, the pipeline deletes
.mapfiles after uploading
What to Do If You Already Published Source Maps
If you discover that a version of your package has already been published with source maps included, the damage control process depends on how quickly you catch it.
- 1
Unpublish or deprecate the affected version
If the version was published less than 72 hours ago, you can unpublish it with
npm unpublish your-package@2.1.0. After 72 hours, npm does not allow unpublishing. In that case, usenpm deprecate your-package@2.1.0 "This version contains a source map leak, upgrade to 2.1.1"to warn users. - 2
Publish a fixed version immediately
Fix your build config and
package.jsonas described in this guide, then publish a patch version. Users who have lockfiles will not automatically get the new version, so a deprecation notice on the old version helps drive upgrades. - 3
Rotate any exposed secrets
If your source code contained any hardcoded API keys, tokens, database connection strings, or other secrets, rotate them immediately. Assume they are compromised. This is true even if you unpublished the package, because npm registry mirrors and caching proxies (like Verdaccio, Artifactory, or Nexus) may have already cached the tarball.
- 4
Audit the scope of the exposure
Check your npm package's download count for the affected version. Review access logs if available. Determine what was exposed: just code structure, or actual secrets. Document the incident for your team so the same mistake is not repeated.
Frequently Asked Questions
What is sourcesContent in a source map?
sourcesContent is an optional array in the source map JSON spec (version 3). Each entry contains the full original text of a source file referenced by the sources array.
- Why it exists: lets debuggers show original source without needing access to the source files on disk
- Why it is risky: embeds your complete, unminified codebase in a single downloadable JSON file
- When it is populated: most bundlers include it by default unless you explicitly disable it
How do I disable source maps in production?
The config depends on your bundler:
// Webpack: devtool: false
// Vite: build.sourcemap: false
// esbuild: sourcemap: false
// Bun: --sourcemap=none
// TSC: "sourceMap": false in tsconfig.build.jsonHow do I stop npm from publishing .map files?
Add an explicit files field to your package.json that only lists the file types you intend to ship:
"files": ["dist/**/*.js", "dist/**/*.d.ts"]This is an allowlist approach. Since *.map is not in the list, source maps are excluded regardless of where they are generated. Verify by running npm pack --dry-run before every publish.
Can source maps leak API keys?
Yes, if your original source code contains hardcoded secrets (API keys, tokens, connection strings), those strings appear verbatim in sourcesContent. Even if the secrets are stripped or replaced during the build, the source map preserves the pre-build version.
Are inline source maps also a risk?
Yes, and they are actually harder to catch. An inline source map embeds the entire map as a Base64-encoded data: URL at the end of the JavaScript file. There is no separate .map file to look for, so a simple file extension check will miss it.
// This line at the end of a bundled file contains the full source map
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLC...Inline maps are commonly generated during development (using Webpack's eval-source-map or inline-source-map devtool settings). The risk is that a development build accidentally gets published instead of a production build. Your CI check should grep for sourceMappingURL=data: in addition to checking for .map files.
I already published a package with source maps. What should I do?
If the version was published less than 72 hours ago, run npm unpublish your-package@version to remove it. Otherwise, deprecate the version with npm deprecate and publish a fixed patch version immediately.
Rotate any secrets that were present in the source code, even if they were not in the final bundle. The source map's sourcesContent preserves the original pre-build source. Assume that any hardcoded credential has been compromised and generate new ones.
Do TypeScript declaration maps (.d.ts.map) also leak source code?
Declaration maps are less risky than full source maps because they only map type declaration positions. However, they can still expose your original file paths and project structure. In some TypeScript configurations, declaration maps may include a sourcesContent field that contains the original TypeScript source.
Set "declarationMap": false in your production tsconfig.build.json alongside "sourceMap": false. Your library consumers do not need declaration maps. They exist primarily for IDE "Go to Definition" navigation, which works fine with just the .d.ts files.
Related Articles
GitHub Actions Security: 7 Misconfigurations to Avoid
The 7 GitHub Actions misconfigurations behind real supply chain attacks: weak GITHUB_TOKEN scope, pull_request_target, unpinned actions, script injection.
Bumblebee Tutorial: Scan Your Dev Machine for Supply Chain Risks
How to install and use Bumblebee, Perplexity's open-source scanner for npm, MCP configs, and extensions. Real commands, scan profiles, and incident response setup.
WordPress CDN Supply Chain Attack 2026: What Happened and How to Check Your Site
The OptinMonster, TrustPulse, and PushEngage supply chain attack (June 2026) hit 1.2M sites. Here's exactly how it worked, how to check if you were compromised, and how to recover.