Document npm registry endpoints and auth gotchas
All checks were successful
continuous-integration/drone/push Build is passing
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:
@@ -753,8 +753,13 @@ pipelines</strong>:</p>
|
|||||||
the disaster-recovery / replay path.</li>
|
the disaster-recovery / replay path.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>Secrets: <code>NPM_REPO_URL</code>, <code>NPM_USERNAME</code>, <code>NPM_PASSWORD</code>. Auth is a generated
|
<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
|
<code>.npmrc</code> written from secrets at runtime. It sets a <strong>scope-scoped</strong> registry,
|
||||||
runtime — the committed <code>.npmrc</code> in a repo only points at the registry URL.</p>
|
never a global one — <code>@aetoskia:registry=$NPM_REPO_URL</code> — plus a host-scoped
|
||||||
|
auth line <code>//<host>/:_auth=<base64(user:pass)></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>
|
<ul>
|
||||||
<li><strong>Ship as-is.</strong> Name and version come from <code>package.json</code>, so the same file
|
<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>
|
is reused verbatim across every npm package.</li>
|
||||||
|
|||||||
@@ -635,6 +635,39 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<nav class="md-nav" aria-label="Publishing & 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>
|
||||||
|
|
||||||
<li class="md-nav__item">
|
<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.
|
<code>NPM_PASSWORD</code>) are one and the same account — there is no separate npm user.
|
||||||
Verdaccio requires <strong>bcrypt</strong> htpasswd entries; see
|
Verdaccio requires <strong>bcrypt</strong> htpasswd entries; see
|
||||||
<a href="../90_drone_pipelines/">90 – Drone Pipelines</a> for the CI wiring.</p>
|
<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=<base64(user:password)>
|
||||||
|
</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>
|
<h2 id="notes">Notes</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Hexa</strong> is a full library with its own MCP server on <code>:8008</code>: read
|
<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
@@ -72,8 +72,13 @@ pipelines**:
|
|||||||
the disaster-recovery / replay path.
|
the disaster-recovery / replay path.
|
||||||
|
|
||||||
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
|
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
|
||||||
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
|
`.npmrc` written from secrets at runtime. It sets a **scope-scoped** registry,
|
||||||
runtime — the committed `.npmrc` in a repo only points at the registry URL.
|
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
|
- **Ship as-is.** Name and version come from `package.json`, so the same file
|
||||||
is reused verbatim across every npm package.
|
is reused verbatim across every npm package.
|
||||||
|
|||||||
@@ -64,6 +64,56 @@ so the CI credentials (`PIP_USERNAME` / `PIP_PASSWORD`, `NPM_USERNAME` /
|
|||||||
Verdaccio requires **bcrypt** htpasswd entries; see
|
Verdaccio requires **bcrypt** htpasswd entries; see
|
||||||
[90 – Drone Pipelines](90_drone_pipelines.md) for the CI wiring.
|
[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
|
## Notes
|
||||||
|
|
||||||
- **Hexa** is a full library with its own MCP server on `:8008`: read
|
- **Hexa** is a full library with its own MCP server on `:8008`: read
|
||||||
|
|||||||
Reference in New Issue
Block a user