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.
This commit is contained in:
2026-10-21 01:31:07 +05:30
parent da1e46a921
commit 322741e00c
7 changed files with 161 additions and 7 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

@@ -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