Chapter 07Lesson 02165–215 min

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.

Hosted / proxy / groupIsolated .npmrcnpm publishEvidenceToken hygiene

Learning objectives

  • Create a disposable npm hosted/proxy/group topology without changing production repositories.
  • Use an isolated .npmrc and 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?

Why use a fresh npm cache for the consumer proof?

Which state changes when a package is first proxied from registry.npmjs.org?

Why is deleting the isolated npmrc a meaningful credential-hygiene step?

Does --ignore-scripts prove a dependency is safe?

Next lesson

Design choices and tradeoffs

Turn the working lab into an intentional npm repository policy: scope routing, tags, anonymous access, publication lanes, cache behavior, and dependency-confusion defenses.

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.