Chapter 07Lesson 05190–245 min

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 labTwo versionsClean clientDigest evidenceSafe cleanup

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 candidate dist-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:

  1. 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.
  2. Moving candidate from 1.0.0 to 1.1.0 will change metadata only; the 1.0.0 tarball digest must remain unchanged.
  3. A clean consumer configured for the group will resolve @learner-example/ch07-checkpoint@candidate to 1.1.0 after the move.
  4. 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 candidate resolved 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.
  • candidate moved 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-example internal 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?

Why compare 1.0.0 SHA-256 before and after the tag move?

Why is the clean consumer cache part of the acceptance criteria?

What prevents accidental public publication better than package scope alone?

Why delete the isolated npmrc instead of including it in the evidence packet?

If npm install succeeds through Nexus, what remains unproven?

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.

Next chapter

Docker and OCI repository semantics

Apply the same evidence-first repository discipline to image manifests, layers, digests, tags, registry routing, authentication, and Nexus connectors.

Official references and version notes

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.