npm Repositories, Scoped Packages, Metadata, Tokens, Proxying, and JavaScript Supply Chains: Guided Hands-On Workflow and Core Operations
Build a disposable npm/Nexus topology with isolated client configuration, proxy a harmless public package, publish a synthetic scoped package only to hosted storage, consume through a group, and inspect metadata, tarballs, and credentials safely.
Learning objectives
- Create a disposable npm hosted/proxy/group topology without changing production repositories.
-
Use an isolated
.npmrcand cache so the lab cannot silently depend on global client state. - Proxy a harmless public package, publish a synthetic scoped package to hosted, and consume it through a group.
- Inspect registry metadata, tarball URLs, integrity values, Nexus component/assets, and cache effects as separate evidence.
- Remove transient credential state safely and explain what changed at every layer.
Safety boundary. Use only a disposable local Nexus
instance on loopback. Do not use a corporate Nexus server, real
npmjs credentials, employer scopes, or a globally configured
~/.npmrc. The lab package is
@learner-example/ch07-demo.
1. Preflight and exact assumptions
The workflow assumes Nexus Repository Community Edition 3.95.0, Java
21 on the server, a current npm 12 client, and
http://127.0.0.1:8081. The local Nexus instance may use
embedded H2 for this small non-container learning scenario;
production database design remains a separate concern from npm
protocol behavior.
set -eu
export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch07-npm"
rm -rf "$LAB"
mkdir -p "$LAB/cache" "$LAB/pkg" "$LAB/consumer" "$LAB/evidence"
chmod 700 "$LAB"
export NPM_CONFIG_USERCONFIG="$LAB/npmrc"
export NPM_CONFIG_CACHE="$LAB/cache"
printf 'node: '; node --version | tee "$LAB/evidence/node-version.txt"
printf 'npm: '; npm --version | tee "$LAB/evidence/npm-version.txt"
printf 'Nexus expected: %s
' "$NX_URL"
PowerShell equivalent: create a lab directory beneath
$env:TEMP, set
$env:NPM_CONFIG_USERCONFIG to its
npmrc file and $env:NPM_CONFIG_CACHE to a
lab cache directory. Never reuse a credential-bearing production
profile.
2. Create three disposable repositories
Using the Nexus UI with a disposable local administrator, create:
| Repository | Type | Purpose | Critical settings |
|---|---|---|---|
academy-ch07-hosted |
npm hosted | Authoritative internal publication |
Blob store default; strict content type
validation enabled; online.
|
academy-ch07-proxy |
npm proxy | Managed cache of public npm |
Remote https://registry.npmjs.org; normal
metadata/component cache ages; negative cache enabled.
|
academy-ch07-group |
npm group | Single consumer endpoint |
Members in this order: academy-ch07-hosted,
then academy-ch07-proxy.
|
That member order is intentional. Internal content gets the first opportunity to satisfy a matching package name before the public proxy. Chapter 13 later adds routing-rule depth; here the lesson is to see member order as observable namespace policy rather than decoration.
Do not configure a writable repository on the group for this mandatory lab. Sonatype documents npm group publication as a Pro feature. Community learners publish directly to the hosted URL and consume from the group.
3. Prepare a disposable publisher identity and npm realm
Create a local Nexus user such as
academy-ch07-publisher with only browse/read/add/edit
privileges for academy-ch07-hosted plus browse/read
privileges for academy-ch07-group. Do not use
administrator credentials for routine publication. If the npm Bearer
Token Realm is not active, record the current realm list, activate
it for the lab, and plan to restore the previous realm state during
cleanup.
Then authenticate interactively to the hosted repository. The password is typed at the prompt; it never appears in a shell command or lesson file.
export NPM_CONFIG_USERCONFIG="$LAB/npmrc"
export NPM_CONFIG_CACHE="$LAB/cache"
npm login --auth-type=legacy --registry="$NX_URL/repository/academy-ch07-hosted/"
# Authenticate separately to the group read path; auth is registry-path scoped.
npm login --auth-type=legacy --registry="$NX_URL/repository/academy-ch07-group/"
# Verify only the non-secret routing keys.
npm config get registry
npm config get @learner-example:registry || true
# Never print the credential-bearing line. Inspect only file permissions/size.
ls -l "$NPM_CONFIG_USERCONFIG"
Nexus's npm Security guidance requires
--auth-type=legacy for modern npm clients when using
this realm flow. The resulting registry-scoped bearer credential is
client state in the isolated npmrc; it is not a Nexus
Pro User Token.
4. Configure reads through the group and internal scope routing
npm config set registry "$NX_URL/repository/academy-ch07-group/"
npm config set @learner-example:registry "$NX_URL/repository/academy-ch07-group/"
npm config get registry | tee "$LAB/evidence/registry.txt"
npm config get @learner-example:registry | tee "$LAB/evidence/scope-registry.txt"
The default registry and the internal scope both point to the Nexus
group for consumption. Publication still overrides the registry
explicitly to the hosted endpoint. This makes accidental public
publication harder: the package's own
publishConfig will also pin the hosted URL.
5. Observe first-fetch versus cached public fetch
Use a small harmless package such as is-number@7.0.0.
First ask through the group with a clean npm cache. Then repeat the
metadata request. The first request may require the Nexus proxy to
reach registry.npmjs.org; later requests can be satisfied from Nexus
proxy cache and/or the npm client cache. To isolate Nexus behavior,
use npm view and, when needed, a fresh client cache
rather than deleting shared Nexus cache.
rm -rf "$LAB/cache" && mkdir -p "$LAB/cache"
npm view is-number@7.0.0 version dist.integrity dist.tarball --registry="$NX_URL/repository/academy-ch07-group/" | tee "$LAB/evidence/public-first.txt"
npm view is-number@7.0.0 version dist.integrity dist.tarball --registry="$NX_URL/repository/academy-ch07-group/" | tee "$LAB/evidence/public-second.txt"
In Nexus Browse/Search, the public component should now be visible because the proxy cached it. That is a local cached representation of upstream content, not an internal publication.
6. Create a synthetic scoped package with no lifecycle scripts
cat > "$LAB/pkg/package.json" <<JSON
{
"name": "@learner-example/ch07-demo",
"version": "1.0.0",
"description": "Disposable Nexus Academy npm fixture",
"main": "index.js",
"license": "MIT",
"private": false,
"publishConfig": {
"registry": "$NX_URL/repository/academy-ch07-hosted/"
}
}
JSON
cat > "$LAB/pkg/index.js" <<'JS'
module.exports = function ch07Demo() { return 'academy-ch07-v1'; };
JS
cat > "$LAB/pkg/README.md" <<'MD'
# Disposable Chapter 07 npm fixture
No secrets. No install lifecycle scripts. Delete after the lab.
MD
cd "$LAB/pkg"
npm pack --dry-run --ignore-scripts | tee "$LAB/evidence/pack-dry-run.txt"
npm pack --dry-run is a publication preflight: it shows
which files would enter the tarball. The package intentionally
defines no lifecycle scripts, and the lab adds
--ignore-scripts when possible so repository mechanics
remain the focus.
7. Publish only to hosted
cd "$LAB/pkg"
npm publish --ignore-scripts --registry="$NX_URL/repository/academy-ch07-hosted/" | tee "$LAB/evidence/publish-1.0.0.txt"
Expected causal state changes:
-
npm client: reads the project manifest, creates
the package payload, uses the hosted-registry credential from the
isolated
npmrc. - Nexus hosted configuration: unchanged; it remains the selected write target.
- Nexus database: records the package/component/version/assets and updated package metadata relationships.
- Blob store: stores the published npm content bytes.
- Group: no independent authoritative copy is created; it can expose the hosted content immediately on reads.
8. Inspect metadata and tarball through the group
ENCODED_SCOPE='%40learner-example%2Fch07-demo'
curl -fsS "$NX_URL/repository/academy-ch07-group/$ENCODED_SCOPE" -o "$LAB/evidence/package-metadata.json"
python - <<'PY2'
import json, os
p=os.environ['LAB'] + '/evidence/package-metadata.json'
d=json.load(open(p, encoding='utf-8'))
print('name:', d.get('name'))
print('dist-tags:', d.get('dist-tags'))
v=d.get('versions',{}).get('1.0.0',{})
print('version:', v.get('version'))
print('tarball:', v.get('dist',{}).get('tarball'))
print('integrity:', v.get('dist',{}).get('integrity'))
PY2
The metadata document is mutable registry state because tags and version lists can change. The 1.0.0 tarball is versioned artifact content. Record both, but do not treat the tag map as the artifact digest.
9. Consume from a fresh client state through the group
rm -rf "$LAB/consumer" "$LAB/consumer-cache"
mkdir -p "$LAB/consumer" "$LAB/consumer-cache"
cd "$LAB/consumer"
cat > package.json <<'JSON'
{"name":"ch07-consumer","version":"1.0.0","private":true}
JSON
NPM_CONFIG_CACHE="$LAB/consumer-cache" NPM_CONFIG_USERCONFIG="$LAB/npmrc" npm install --ignore-scripts --save-exact @learner-example/ch07-demo@1.0.0 --registry="$NX_URL/repository/academy-ch07-group/" | tee "$LAB/evidence/install-internal.txt"
node -e "console.log(require('@learner-example/ch07-demo')())" | tee "$LAB/evidence/runtime-output.txt"
Using a fresh cache proves the group can serve the package rather
than letting an earlier local cache satisfy the request.
--ignore-scripts is a deliberate supply-chain control
for this repository-mechanics exercise; later JavaScript policy work
may choose a more nuanced script-approval model.
10. Verify tarball identity independently
Use the tarball URL from the metadata document, download it through the group, and compute SHA-256 as independent lab evidence. npm itself uses registry-provided integrity data (commonly SHA-512 SRI) for package verification; your additional SHA-256 is an operator evidence value, not a replacement for npm integrity semantics.
python - <<'PY2'
import json, os
p=os.environ['LAB'] + '/evidence/package-metadata.json'
d=json.load(open(p, encoding='utf-8'))
u=d['versions']['1.0.0']['dist']['tarball']
# Normalize to the lab group if metadata contains the hosted member URL.
base=os.environ['NX_URL'] + '/repository/academy-ch07-group/'
name='@learner-example/ch07-demo/-/ch07-demo-1.0.0.tgz'
open(os.environ['LAB']+'/evidence/tarball-url.txt','w').write(base+name+'
')
print(base+name)
PY2
TARBALL_URL="$(cat "$LAB/evidence/tarball-url.txt")"
curl -fsS "$TARBALL_URL" -o "$LAB/evidence/ch07-demo-1.0.0.tgz"
sha256sum "$LAB/evidence/ch07-demo-1.0.0.tgz" | tee "$LAB/evidence/tarball-sha256.txt"
11. Remove transient credential state
Do not print the bearer line. After publication work is complete, copy only non-secret evidence, then remove the isolated credential file. If you need to publish again, re-authenticate interactively. That is a simple lab credential-rotation exercise: old client credential state is destroyed rather than copied forward indefinitely.
# Preserve routing evidence first; never copy the auth line.
grep -Ev '(_auth|_authToken|password|token)' "$LAB/npmrc" > "$LAB/evidence/npmrc-redacted.txt" || true
rm -f "$LAB/npmrc"
unset NPM_CONFIG_USERCONFIG
printf 'Disposable npm credential file removed.
'
12. Challenge: choose the correct control
Your team wants all @learner-example/* packages to be
internal, while ordinary open-source packages still resolve through
the Nexus public proxy. Which control is primary?
A good answer combines a scope-to-group registry mapping, internal-hosted-first group order, and a rule/egress design that prevents the internal scope from falling through to public. Creating another npm cache or moving a dist-tag does not solve namespace ownership.
Knowledge check
Why publish directly to hosted in the Community lab instead of the group URL?
Because hosted is the authoritative write target, and Sonatype documents writable npm group publication as a Nexus Repository Pro feature.
Why use a fresh npm cache for the consumer proof?
It prevents previously cached client content from satisfying the install, so the test proves the Nexus group can serve the package.
Which state changes when a package is first proxied from registry.npmjs.org?
Nexus proxy metadata/component records and blob/cache state can be populated; the upstream package itself is not republished by your organization.
Why is deleting the isolated npmrc a meaningful credential-hygiene step?
The bearer credential is client state stored in that file. Removing the disposable file prevents accidental reuse or leakage after the lab.
Does --ignore-scripts prove a dependency is safe?
No. It prevents lifecycle scripts from executing during that command, but it does not establish provenance, vulnerability safety, or correct authorization.
Official references and version notes
- Nexus Repository Download and current 2026 release notes — re-check the current 3.95.x self-hosted baseline before executing the lab.
- Sonatype: npm Registry — hosted, proxy, and group behavior.
- Sonatype: Configuring npm — registry configuration through Nexus.
- Sonatype: Publishing npm Packages — hosted publication and the Pro-only writable-group option.
- Sonatype: npm Security — npm Bearer Token Realm/login and basic-auth alternatives.
- Configurable Repository Fields — npm writable-group, proxy, cache, and repository options.
- npm Registry documentation and .npmrc — scope routing and registry-scoped authentication.
- npm publish and npm dist-tag — version immutability, integrity, and mutable channel labels.
Version-sensitive statements were rechecked against Sonatype and npm
primary documentation on 2026-08-26. The mandatory lab assumes Nexus
Repository Community Edition 3.95.0 and a current npm 12 client;
record npm --version and
node --version locally because npm/Node compatibility
evolves independently of Nexus. Nexus 3.87+ requires Java 21 for
supported self-hosted deployments. Re-check current release/support
pages before executing the lab.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.