Compare commits

...

4 Commits

Author SHA1 Message Date
322741e00c Document npm registry endpoints and auth gotchas
All checks were successful
continuous-integration/drone/push Build is passing
Add the exact pip/npm registry endpoints and secret names, the
scope-scoped (not global) registry requirement given Verdaccio's missing
uplink, the host-scoped _auth rule, the per-item Drone shell caveat, and
the Verdaccio storage uid/permissions gotcha behind the confusing
"404 no such package available" error on publish.
2026-10-21 01:31:07 +05:30
da1e46a921 Scope npm registry in drone template to @aetoskia
The registry was set globally (
egistry=...), which forced all public
transitive deps through the private Verdaccio. Verdaccio has no uplink,
so
pm install 404'd on public packages. Scope the registry to
@aetoskia:registry=... so public deps resolve from npmjs and only
@aetoskia/* go to the private registry.
2026-10-21 01:29:00 +05:30
08a9983984 Fix npm .npmrc: fold NPM_HOST into the same shell as its use
Drone runs each command list item in a separate shell, so a
standalone NPM_HOST= line is empty when the next item references it,
writing //:_auth= (wrong host) and causing a registry 401. Compute
NPM_HOST in the same | block that writes .npmrc.
2026-10-21 01:09:23 +05:30
e1e5daa30a Fix npm registry auth: use scoped //host/:_auth in .npmrc
npm rejects a bare _auth key (ERR_INVALID_AUTH) against a private
registry; derive the host from NPM_REPO_URL and write
//<host>/:_auth=<base64> instead.
2026-10-21 01:03:04 +05:30
9 changed files with 203 additions and 31 deletions

View File

@@ -753,8 +753,13 @@ pipelines</strong>:</p>
the disaster-recovery / replay path.</li>
</ol>
<p>Secrets: <code>NPM_REPO_URL</code>, <code>NPM_USERNAME</code>, <code>NPM_PASSWORD</code>. Auth is a generated
<code>.npmrc</code> (<code>registry=</code> + <code>_auth=</code> base64 of <code>user:pass</code>) written from secrets at
runtime — the committed <code>.npmrc</code> in a repo only points at the registry URL.</p>
<code>.npmrc</code> written from secrets at runtime. It sets a <strong>scope-scoped</strong> registry,
never a global one — <code>@aetoskia:registry=$NPM_REPO_URL</code> — plus a host-scoped
auth line <code>//&lt;host&gt;/:_auth=&lt;base64(user:pass)&gt;</code>. The scope and host are derived
from <code>package.json</code> and <code>NPM_REPO_URL</code> respectively. A global <code>registry=</code> would
push public transitive deps through Verdaccio (which has no uplink) and 404
<code>npm install</code>; a bare <code>_auth=</code> is rejected by modern npm. See
<a href="../93_library_registry/">93 – Library Registry</a>.</p>
<ul>
<li><strong>Ship as-is.</strong> Name and version come from <code>package.json</code>, so the same file
is reused verbatim across every npm package.</li>

View File

@@ -635,6 +635,39 @@
</span>
</a>
<nav class="md-nav" aria-label="Publishing &amp; registry auth">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#endpoints" class="md-nav__link">
<span class="md-ellipsis">
Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#npm-scope-the-registry-never-set-it-globally" class="md-nav__link">
<span class="md-ellipsis">
npm: scope the registry, never set it globally
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#verdaccio-storage-permissions" class="md-nav__link">
<span class="md-ellipsis">
Verdaccio storage permissions
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
@@ -839,6 +872,67 @@ so the CI credentials (<code>PIP_USERNAME</code> / <code>PIP_PASSWORD</code>, <c
<code>NPM_PASSWORD</code>) are one and the same account — there is no separate npm user.
Verdaccio requires <strong>bcrypt</strong> htpasswd entries; see
<a href="../90_drone_pipelines/">90 – Drone Pipelines</a> for the CI wiring.</p>
<h3 id="endpoints">Endpoints</h3>
<table>
<thead>
<tr>
<th>Registry</th>
<th>URL</th>
<th>Backend</th>
<th>CI secret</th>
</tr>
</thead>
<tbody>
<tr>
<td>PyPI</td>
<td><code>https://pip.aetoskia.com</code></td>
<td>pypiserver (<code>server-pi:6007</code>)</td>
<td><code>PIP_REPO_URL</code></td>
</tr>
<tr>
<td>npm</td>
<td><code>https://npm.aetoskia.com</code></td>
<td>Verdaccio (<code>server-pi:6008</code>)</td>
<td><code>NPM_REPO_URL</code></td>
</tr>
</tbody>
</table>
<p>Both are exposed through Traefik on the <code>websecure</code> entrypoint as <strong>bare
origins</strong> — no path, no trailing slash. Set the secret values exactly as above.</p>
<h3 id="npm-scope-the-registry-never-set-it-globally">npm: scope the registry, never set it globally</h3>
<p>Verdaccio here has <strong>no uplink / proxy</strong>, so it can only serve <code>@aetoskia/*</code>
packages. A global <code>registry=https://npm.aetoskia.com</code> in <code>.npmrc</code> would route
<em>every</em> dependency — including public transitive deps such as
<code>zod-to-json-schema</code> (pulled in by <code>@modelcontextprotocol/sdk</code>) — through
Verdaccio, which 404s on anything it does not host. Always scope the registry to
the account:</p>
<div class="language-text highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-0-1">1</a></span>
<span class="normal"><a href="#__codelineno-0-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1"></a>@aetoskia:registry=https://npm.aetoskia.com
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2"></a>//npm.aetoskia.com/:_auth=&lt;base64(user:password)&gt;
</span></code></pre></div></td></tr></table></div>
<p>Public deps then resolve from <code>registry.npmjs.org</code> and only <code>@aetoskia/*</code> hits
the private registry. The auth line must be <strong>scoped to the host</strong>
(<code>//host/:_auth=</code>), not a bare <code>_auth=</code> — modern npm rejects the bare form with
<code>ERR_INVALID_AUTH</code>. The drone template derives both from the package scope and
<code>NPM_REPO_URL</code> automatically; see
<a href="../90_drone_pipelines/">90 – Drone Pipelines</a>.</p>
<blockquote>
<p>The Drone pipeline runs each <code>commands:</code> list item in a <strong>separate shell</strong>, so
shell variables defined in one item do not survive into the next. Keep the
variable definitions and the commands that use them in the same <code>|</code> block.</p>
</blockquote>
<h3 id="verdaccio-storage-permissions">Verdaccio storage permissions</h3>
<p>Verdaccio runs as uid <strong>10001</strong> (<code>verdaccio</code>) inside <code>verdaccio/verdaccio</code>.
The <code>storage</code> directory is bind-mounted from the server
(<code>/mnt/omnissiah-vault/data/verdaccio/storage</code>); if it is owned by <code>root:root</code>
(mode <code>755</code>) the process cannot write to it and every publish fails with a
confusing <code>404 no such package available</code> (<code>NotFoundError</code> from
<code>local-storage</code>). Ensure the directory — and its <code>plugins</code> subdirectory — are
owned by <code>10001:101</code>:</p>
<div class="language-sh highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"><a href="#__codelineno-1-1">1</a></span>
<span class="normal"><a href="#__codelineno-1-2">2</a></span></pre></div></td><td class="code"><div><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1"></a>sudo<span class="w"> </span>chown<span class="w"> </span>-R<span class="w"> </span><span class="m">10001</span>:101<span class="w"> </span>/mnt/omnissiah-vault/data/verdaccio/storage
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2"></a>docker<span class="w"> </span>restart<span class="w"> </span>npm-registry
</span></code></pre></div></td></tr></table></div>
<h2 id="notes">Notes</h2>
<ul>
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,17 +22,18 @@ steps:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache curl
- PACKAGE_NAME=$(node -p "require('./package.json').name")
- VERSION=$(node -p "require('./package.json').version")
- |
PACKAGE_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if [ "$DRONE_TAG" != "$VERSION" ]; then
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
exit 1
fi
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
exit 78
@@ -55,8 +56,10 @@ steps:
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
exit 1
fi
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- npm install
- echo "Running quality gate..."
- npm run quality-gate
@@ -79,8 +82,11 @@ steps:
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- echo "Publishing to private npm at $NPM_REPO_URL ..."
- npm publish --access restricted
@@ -117,8 +123,11 @@ steps:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache git
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- |
set -e

View File

@@ -72,8 +72,13 @@ pipelines**:
the disaster-recovery / replay path.
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
runtime — the committed `.npmrc` in a repo only points at the registry URL.
`.npmrc` written from secrets at runtime. It sets a **scope-scoped** registry,
never a global one — `@aetoskia:registry=$NPM_REPO_URL` — plus a host-scoped
auth line `//<host>/:_auth=<base64(user:pass)>`. The scope and host are derived
from `package.json` and `NPM_REPO_URL` respectively. A global `registry=` would
push public transitive deps through Verdaccio (which has no uplink) and 404
`npm install`; a bare `_auth=` is rejected by modern npm. See
[93 – Library Registry](93_library_registry.md).
- **Ship as-is.** Name and version come from `package.json`, so the same file
is reused verbatim across every npm package.

View File

@@ -64,6 +64,56 @@ so the CI credentials (`PIP_USERNAME` / `PIP_PASSWORD`, `NPM_USERNAME` /
Verdaccio requires **bcrypt** htpasswd entries; see
[90 – Drone Pipelines](90_drone_pipelines.md) for the CI wiring.
### Endpoints
| Registry | URL | Backend | CI secret |
|---|---|---|---|
| PyPI | `https://pip.aetoskia.com` | pypiserver (`server-pi:6007`) | `PIP_REPO_URL` |
| npm | `https://npm.aetoskia.com` | Verdaccio (`server-pi:6008`) | `NPM_REPO_URL` |
Both are exposed through Traefik on the `websecure` entrypoint as **bare
origins** — no path, no trailing slash. Set the secret values exactly as above.
### npm: scope the registry, never set it globally
Verdaccio here has **no uplink / proxy**, so it can only serve `@aetoskia/*`
packages. A global `registry=https://npm.aetoskia.com` in `.npmrc` would route
*every* dependency — including public transitive deps such as
`zod-to-json-schema` (pulled in by `@modelcontextprotocol/sdk`) — through
Verdaccio, which 404s on anything it does not host. Always scope the registry to
the account:
```
@aetoskia:registry=https://npm.aetoskia.com
//npm.aetoskia.com/:_auth=<base64(user:password)>
```
Public deps then resolve from `registry.npmjs.org` and only `@aetoskia/*` hits
the private registry. The auth line must be **scoped to the host**
(`//host/:_auth=`), not a bare `_auth=` — modern npm rejects the bare form with
`ERR_INVALID_AUTH`. The drone template derives both from the package scope and
`NPM_REPO_URL` automatically; see
[90 – Drone Pipelines](90_drone_pipelines.md).
> The Drone pipeline runs each `commands:` list item in a **separate shell**, so
> shell variables defined in one item do not survive into the next. Keep the
> variable definitions and the commands that use them in the same `|` block.
### Verdaccio storage permissions
Verdaccio runs as uid **10001** (`verdaccio`) inside `verdaccio/verdaccio`.
The `storage` directory is bind-mounted from the server
(`/mnt/omnissiah-vault/data/verdaccio/storage`); if it is owned by `root:root`
(mode `755`) the process cannot write to it and every publish fails with a
confusing `404 no such package available` (`NotFoundError` from
`local-storage`). Ensure the directory — and its `plugins` subdirectory — are
owned by `10001:101`:
```sh
sudo chown -R 10001:101 /mnt/omnissiah-vault/data/verdaccio/storage
docker restart npm-registry
```
## Notes
- **Hexa** is a full library with its own MCP server on `:8008`: read

View File

@@ -22,17 +22,18 @@ steps:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache curl
- PACKAGE_NAME=$(node -p "require('./package.json').name")
- VERSION=$(node -p "require('./package.json').version")
- |
PACKAGE_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if [ "$DRONE_TAG" != "$VERSION" ]; then
echo "Tag '$DRONE_TAG' does not match package.json version '$VERSION'; refusing to publish."
exit 1
fi
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
echo "Checking if $PACKAGE_NAME@$VERSION exists on $NPM_REPO_URL ..."
if npm view "$PACKAGE_NAME@$VERSION" version --userconfig /tmp/.npmrc >/dev/null 2>&1; then
echo "$PACKAGE_NAME@$VERSION already exists; skipping build."
exit 78
@@ -55,8 +56,10 @@ steps:
echo "NPM_REPO_URL / NPM_USERNAME / NPM_PASSWORD are not set" >&2
exit 1
fi
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- npm install
- echo "Running quality gate..."
- npm run quality-gate
@@ -79,8 +82,11 @@ steps:
NPM_PASSWORD:
from_secret: NPM_PASSWORD
commands:
- echo "registry=$NPM_REPO_URL" > .npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > .npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> .npmrc
- echo "Publishing to private npm at $NPM_REPO_URL ..."
- npm publish --access restricted
@@ -117,8 +123,11 @@ steps:
from_secret: NPM_PASSWORD
commands:
- apk add --no-cache git
- echo "registry=$NPM_REPO_URL" > /tmp/.npmrc
- echo "_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- |
NPM_HOST=$(echo "$NPM_REPO_URL" | sed -E 's|^[a-z]+://||; s|/*$||')
NPM_SCOPE="@$(node -p "require('./package.json').name.split('/')[0].replace(/^@/,'')")"
echo "$NPM_SCOPE:registry=$NPM_REPO_URL" > /tmp/.npmrc
echo "//$NPM_HOST/:_auth=$(echo -n "$NPM_USERNAME:$NPM_PASSWORD" | base64 | tr -d '\n')" >> /tmp/.npmrc
- |
set -e