Verdaccio (github.com/verdaccio/verdaccio (https://github.com/verdaccio/verdaccio 17,898 ), MIT, version 6.10.4 in September 2026) is a small Node.js 2,131 registry. It stores packages you publish to it and proxies everything else from an uplink such as npmjs, caching each tarball on first use. Its default configuration proxies every name to npmjs, your private scope included; this one closes that gap, which Typosquatting shows attackers using.
storage: /verdaccio/storage/data
auth: { htpasswd: { file: /verdaccio/storage/htpasswd, max_users: 10 } }
uplinks: { npmjs: { url: https://registry.npmjs.org/ } }
packages: # private scope first: never looked up on npmjs
'@booknest/*': { access: $authenticated, publish: $authenticated }
'**': { access: $all, publish: $authenticated, proxy: npmjs }Start it in a container, then publish a tiny shared package, @booknest/price-format. Creating a user is one HTTP request, the same one npm 2,036 adduser sends; the token goes into the package's own .npmrc, never into Git 1,932 .
cd ~/v5-ch1/verdaccio
docker run -d --name l1-verdaccio -p 31873:4873 -v "$PWD/conf:/verdaccio/conf:ro" \
-v l1-verdaccio-storage:/verdaccio/storage verdaccio/verdaccio:6 >/dev/null
sleep 4
cd ~/v5-ch1/price-format # package.json: name @booknest/price-format, version 1.0.0
REG=http://localhost:31873
TOKEN=$(curl -s -X PUT $REG/-/user/org.couchdb.user:dev -H 'Content-Type: application/json' \
-d '{"name":"dev","password":"booknest-demo"}' | jq -r .token)
npm config set --location=project "//localhost:31873/:_authToken" "$TOKEN"
npm publish --registry $REG 2>&1 | grep -E 'Publishing|^\+'npm notice Publishing to http://localhost:31873/ with tag latest and default access + @booknest/price-format@1.0.0
A consumer maps the scope to Verdaccio in its .npmrc and keeps npmjs for everything else.
cd ~/v5-ch1/private-demo && cp ../booknest/package.json .
echo '@booknest:registry=http://localhost:31873/' > .npmrc
cat ../price-format/.npmrc >> .npmrc # the auth token
npm install @booknest/price-format --no-audit --no-fund
node -e 'console.log(require("@booknest/price-format")(24))'
curl -s -o /dev/null -w 'anonymous read: %{http_code}\n' $REG/@booknest%2fprice-format
curl -s -o /dev/null -w 'unpublished private name: %{http_code}\n' \
-H "Authorization: Bearer $TOKEN" $REG/@booknest%2fnot-published
npm view left-pad version --registry $REG # proxied from npmjs, then cachedadded 83 packages in 5s $24.00 anonymous read: 401 unpublished private name: 404 1.3.0
Anonymous reads of the private scope get 401, and an unknown @booknest name gets 404 instead of whatever an attacker might publish under that name on npmjs. Public packages such as left-pad pass through the uplink into storage/data, so builds keep working when npmjs is down. 6 shows the web interface after login.
