Checkpoint Lab — npm Repositories, Scoped Packages, Metadata, Tokens, Proxying, and JavaScript Supply Chains
Publish two synthetic scoped npm versions, move a non-identity dist-tag, consume through Nexus from a clean client configuration, prove tarball identity, document scope/token hygiene, and clean up only disposable lab state.
Checkpoint objectives
- Build a disposable internal npm scope and repository topology from a clean client configuration.
- Publish two synthetic versions only to hosted storage and expose them through a group.
-
Move a
candidatedist-tag and prove that the versions/tarballs remain distinct identities. - Consume from a clean client cache through Nexus with lifecycle scripts disabled and collect independent integrity evidence.
- Produce an evidence packet covering routing, authorization, metadata, tarballs, caches, credentials, and cleanup.
Checkpoint scope. This lab is intentionally local
and disposable. Use @learner-example/ch07-checkpoint,
not a real organization scope. Do not publish anything to
registry.npmjs.org. Verify every registry URL before pressing Enter
on a publish command.
1. Scenario and predictions
You are preparing an internal JavaScript library. Developers should
consume from one Nexus group, CI should publish only to an internal
hosted repository, and a mutable candidate tag should
identify the version currently under evaluation without becoming the
release identity.
Write these predictions before changing state:
- Publishing 1.0.0 and 1.1.0 will create two distinct immutable package-version entries/tarballs in hosted storage, while package metadata gains both versions.
-
Moving
candidatefrom 1.0.0 to 1.1.0 will change metadata only; the 1.0.0 tarball digest must remain unchanged. -
A clean consumer configured for the group will resolve
@learner-example/ch07-checkpoint@candidateto 1.1.0 after the move. - No direct request or publish should target registry.npmjs.org for the internal scope.
2. Preflight and lab state
set -eu
export NX_URL="http://127.0.0.1:8081"
export LAB="${TMPDIR:-/tmp}/nexus-ch07-checkpoint"
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"
node --version | tee "$LAB/evidence/node-version.txt"
npm --version | tee "$LAB/evidence/npm-version.txt"
printf '%s
' "$NX_URL" | tee "$LAB/evidence/nexus-url.txt"
In the Nexus UI record the instance version/edition, database mode, default blob store, free disk, and current repository list. This is evidence only; do not inspect or edit H2/PostgreSQL/blob internals directly.
3. Create the checkpoint topology
Create these disposable npm repositories:
| Name | Type | Configuration |
|---|---|---|
academy-ch07cp-hosted |
npm hosted | Internal authoritative package publication; default blob store. |
academy-ch07cp-proxy |
npm proxy |
Remote https://registry.npmjs.org; normal cache
+ negative cache.
|
academy-ch07cp-group |
npm group | Member order: hosted first, proxy second; read endpoint only for Community lab. |
Create disposable publisher/reader
academy-ch07cp-publisher with minimal hosted
browse/read/add/edit privileges plus group browse/read privileges.
If needed, activate the npm Bearer Token Realm after recording the
prior realm state.
4. Authenticate without exposing a password
npm login --auth-type=legacy --registry="$NX_URL/repository/academy-ch07cp-hosted/"
npm login --auth-type=legacy --registry="$NX_URL/repository/academy-ch07cp-group/"
npm config set registry "$NX_URL/repository/academy-ch07cp-group/"
npm config set @learner-example:registry "$NX_URL/repository/academy-ch07cp-group/"
# Record only non-secret routing evidence.
{
echo "registry=$(npm config get registry)"
echo "scope=$(npm config get @learner-example:registry)"
} | tee "$LAB/evidence/routing.txt"
Do not run npm config ls into a shared log without
redaction. The checkpoint proves routing and credential hygiene, not
the credential value.
5. Build and publish version 1.0.0
cat > "$LAB/pkg/package.json" <<JSON
{
"name": "@learner-example/ch07-checkpoint",
"version": "1.0.0",
"description": "Disposable Chapter 07 checkpoint",
"main": "index.js",
"license": "MIT",
"publishConfig": {
"registry": "$NX_URL/repository/academy-ch07cp-hosted/"
}
}
JSON
cat > "$LAB/pkg/index.js" <<'JS'
module.exports = { version: '1.0.0', channel: 'checkpoint' };
JS
cat > "$LAB/pkg/README.md" <<'MD'
# Chapter 07 checkpoint fixture
Disposable. Contains no secrets and no lifecycle scripts.
MD
cd "$LAB/pkg"
npm pack --dry-run --ignore-scripts | tee "$LAB/evidence/v1-pack-dry-run.txt"
npm publish --ignore-scripts --registry="$NX_URL/repository/academy-ch07cp-hosted/" | tee "$LAB/evidence/v1-publish.txt"
6. Point candidate at 1.0.0
npm dist-tag add @learner-example/ch07-checkpoint@1.0.0 candidate --registry="$NX_URL/repository/academy-ch07cp-hosted/"
npm dist-tag ls @learner-example/ch07-checkpoint --registry="$NX_URL/repository/academy-ch07cp-group/" | tee "$LAB/evidence/tags-after-v1.txt"
Record the tag map, then retrieve the package metadata through the group. Save 1.0.0's tarball URL and integrity before publishing the second version.
PKG='%40learner-example%2Fch07-checkpoint'
curl -fsS "$NX_URL/repository/academy-ch07cp-group/$PKG" -o "$LAB/evidence/metadata-before-v2.json"
python - <<'PY2'
import json, os
p=os.environ['LAB']+'/evidence/metadata-before-v2.json'
d=json.load(open(p,encoding='utf-8'))
v=d['versions']['1.0.0']
print('candidate:', d.get('dist-tags',{}).get('candidate'))
print('v1 integrity:', v['dist'].get('integrity'))
print('v1 tarball:', v['dist'].get('tarball'))
PY2
curl -fsS "$NX_URL/repository/academy-ch07cp-group/@learner-example/ch07-checkpoint/-/ch07-checkpoint-1.0.0.tgz" -o "$LAB/evidence/v1.tgz"
sha256sum "$LAB/evidence/v1.tgz" | tee "$LAB/evidence/v1-sha256-before.txt"
7. Publish 1.1.0 as a new identity
python - <<'PY2'
import json, os
p=os.environ['LAB']+'/pkg/package.json'
d=json.load(open(p,encoding='utf-8'))
d['version']='1.1.0'
open(p,'w',encoding='utf-8').write(json.dumps(d,indent=2)+'
')
PY2
cat > "$LAB/pkg/index.js" <<'JS'
module.exports = { version: '1.1.0', channel: 'checkpoint' };
JS
cd "$LAB/pkg"
npm pack --dry-run --ignore-scripts | tee "$LAB/evidence/v2-pack-dry-run.txt"
npm publish --ignore-scripts --registry="$NX_URL/repository/academy-ch07cp-hosted/" | tee "$LAB/evidence/v2-publish.txt"
Do not republish 1.0.0 with changed bytes. npm's version identity model and repository history should preserve the existing name/version.
8. Move candidate to 1.1.0
npm dist-tag add @learner-example/ch07-checkpoint@1.1.0 candidate --registry="$NX_URL/repository/academy-ch07cp-hosted/"
npm dist-tag ls @learner-example/ch07-checkpoint --registry="$NX_URL/repository/academy-ch07cp-group/" | tee "$LAB/evidence/tags-after-v2.txt"
Now re-download 1.0.0 and compare its SHA-256 with the pre-move value. The tag changed, but 1.0.0 bytes should not.
curl -fsS "$NX_URL/repository/academy-ch07cp-group/@learner-example/ch07-checkpoint/-/ch07-checkpoint-1.0.0.tgz" -o "$LAB/evidence/v1-after-tag-move.tgz"
sha256sum "$LAB/evidence/v1-after-tag-move.tgz" | tee "$LAB/evidence/v1-sha256-after.txt"
V1_BEFORE="$(cut -d' ' -f1 "$LAB/evidence/v1-sha256-before.txt")"
V1_AFTER="$(cut -d' ' -f1 "$LAB/evidence/v1-sha256-after.txt")"
test "$V1_BEFORE" = "$V1_AFTER"
printf 'v1 identity preserved: %s
' "$V1_AFTER"
9. Prove clean-client resolution of the mutable channel
rm -rf "$LAB/consumer" "$LAB/consumer-cache"
mkdir -p "$LAB/consumer" "$LAB/consumer-cache"
cd "$LAB/consumer"
cat > package.json <<'JSON'
{"name":"ch07-checkpoint-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-checkpoint@candidate --registry="$NX_URL/repository/academy-ch07cp-group/" | tee "$LAB/evidence/consumer-install.txt"
node - <<'JS' | tee "$LAB/evidence/consumer-runtime.txt"
const p=require('@learner-example/ch07-checkpoint');
console.log(JSON.stringify(p));
if (p.version !== '1.1.0') process.exit(2);
JS
The proof is intentionally from a clean cache. If an earlier cached package satisfied the install, the result would not prove the group and current tag metadata were correct.
10. Prove internal scope routing does not point to public npm
Record the scope mapping and inspect the lockfile/resolved URLs.
Every internal package URL should point at the Nexus group/hosted
path, not registry.npmjs.org. The public proxy may
contact npmjs.org for unrelated public dependencies, but the owned
scope should never require direct public publication.
npm config get @learner-example:registry | tee "$LAB/evidence/internal-scope-registry.txt"
if grep -R "registry.npmjs.org.*learner-example" "$LAB/consumer"; then
echo 'Unexpected public routing for internal scope' >&2
exit 3
else
echo 'No direct public URL for @learner-example found in consumer state.' | tee "$LAB/evidence/no-public-internal-route.txt"
fi
11. Repository and asset evidence
In Nexus Browse/Search, capture the hosted component with both versions and the tarball assets. If your lab user has REST browse permission, query Components/Assets APIs as read-only evidence. Do not expose admin credentials in curl command arguments or captured shell history. The evidence should show that the group is a view while the hosted repository owns the published versions.
12. Credential removal and realm rollback
Create a redacted routing copy, then delete the disposable
npmrc. Delete the disposable publisher user through
Nexus, and restore the previous realm list if you enabled the npm
Bearer Token Realm only for this lab.
grep -Ev '(_auth|_authToken|password|token)' "$LAB/npmrc" > "$LAB/evidence/npmrc-redacted.txt" || true
rm -f "$LAB/npmrc"
unset NPM_CONFIG_USERCONFIG
printf 'Local npm credential state removed.
'
13. Supported Nexus cleanup
Delete academy-ch07cp-group,
academy-ch07cp-proxy, and
academy-ch07cp-hosted through the supported Nexus
UI/API after preserving evidence. Then remove the disposable user.
Do not manually delete blob-store files or database rows. Only after
Nexus cleanup is complete should you remove the local lab directory.
rm -rf "$LAB"
unset NPM_CONFIG_CACHE
printf 'Checkpoint client state removed.
'
14. Required evidence packet
- Nexus edition/version and npm/Node versions.
- Repository topology with hosted-first group order.
- Redacted client routing configuration and proof that the internal scope points to Nexus.
- Publish evidence for 1.0.0 and 1.1.0.
-
Package metadata before and after moving
candidate. - 1.0.0 SHA-256 before/after tag movement, proving versioned bytes did not change.
-
Clean-client install output showing
candidateresolved to 1.1.0. - Component/asset screenshots or safe API output from Nexus.
- Credential-removal and repository-cleanup checklist.
15. Verification checklist
- Both versions existed as separate package identities in hosted storage.
-
candidatemoved to 1.1.0 without changing 1.0.0 bytes. - A clean client consumed through the group with lifecycle scripts disabled.
- No real credential appears in package files, command arguments, evidence, or committed configuration.
-
No direct public URL is used for
@learner-exampleinternal package resolution/publication. - All Nexus cleanup used supported repository/user controls; no database/blob files were edited.
Knowledge check
What exactly changed when candidate moved from 1.0.0 to 1.1.0?
The mutable dist-tag mapping in package metadata changed. The already-published 1.0.0 and 1.1.0 version identities/tarballs remained distinct.
Why compare 1.0.0 SHA-256 before and after the tag move?
To independently prove that moving the channel label did not mutate the old versioned tarball.
Why is the clean consumer cache part of the acceptance criteria?
It prevents local cache from masking registry/group behavior and proves the current Nexus endpoint can resolve the tag/version.
What prevents accidental public publication better than package scope alone?
Scope-to-Nexus routing plus hosted publishConfig, repository/member policy, authorization, and controlled egress. The name alone is not an enforcement boundary.
Why delete the isolated npmrc instead of including it in the evidence packet?
It contains authentication material. Preserve only redacted routing evidence and destroy the transient credential state.
If npm install succeeds through Nexus, what remains unproven?
Trusted provenance, vulnerability safety, correct authorization design, safe lifecycle behavior, and whether the selected package was the intended organizational source all require separate evidence/controls.
16. What Chapter 07 adds to the production operating model
You can now operate npm as a protocol with explicit namespace ownership, metadata/tarball separation, mutable channel tags, registry-scoped credentials, hosted/proxy/group topology, cache layers, and executable lifecycle risk. A production platform can route internal scopes deterministically, publish with least privilege, keep public dependencies behind Nexus, prove exact package-version identity, and diagnose stale or unauthorized behavior without damaging shared state.
Chapter 08 moves to Docker and OCI. There the identity model changes again: manifests, layers, tags, digests, connectors, registry paths, and Docker authentication must be learned on their own terms rather than mapped mechanically from npm.
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.