diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f817ec..53dbcad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## v0.1.12 - 20/05/2026
+
+### Documentation
+- `README` — document the recommended hardened `NPM_CONFIG_FILE` `.npmrc` (per-line rationale) and the token bootstrap auth setup (`NPM_PACKAGE_REGISTRY_TOKEN` + `NPM_EXTRA_CONFIG` secrets, why `npm publish` is used on that path, migration token → OIDC after first publish). Reaffirms that every npm-publish-related value is a **secret** (encrypted), not a GitHub `var`. Reflects the v0.1.11 token-path hardening.
+
## v0.1.11 - 20/05/2026
### Fixes
diff --git a/README.md b/README.md
index 5820672..5c62d77 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ Section format: `## vX.Y.Z - DD/MM/YYYY`. Idempotent. Reuses an existing hand-cu
## Environment
-Zero inputs on pipelines and on every composite — imposed, not proposed. Configuration flows through `secrets:` and the caller's `vars` context.
+Zero inputs on pipelines and on every composite — imposed, not proposed. Configuration flows through the caller's `secrets:` block. Every npm-publish-related value is a **secret** (encrypted at rest, masked in logs); none of them are GitHub `vars`.
Secrets (caller's secrets: block)
@@ -175,7 +175,7 @@ Zero inputs on pipelines and on every composite — imposed, not proposed. Confi
| `NPM_EXTRA_CONFIG` | | Extra `.npmrc` lines appended after `NPM_CONFIG_FILE`. A **secret** — it lands in `.npmrc`, so it can carry auth material and must stay masked. |
| `NPM_PACKAGE_REGISTRY` | ✔ | npm package registry URL. |
| `NPM_PACKAGE_PROXY_REGISTRY` | | Optional npm proxy registry URL. |
-| `NPM_PACKAGE_REGISTRY_TOKEN` | | Required for token-based publish to private registries. Absent → OIDC. |
+| `NPM_PACKAGE_REGISTRY_TOKEN` | | npm Granular Access Token, scoped to the publishing organization with create-new-package permission. Required only for the token bootstrap (first publish of a new scoped package, before npm Trusted Publisher is bound). Absent → OIDC. |
@@ -233,14 +233,58 @@ pnpm CLI resolved via corepack from `packageManager`. No floating version reache
-Publish — OIDC vs token auth
+Recommended NPM_CONFIG_FILE contents
-Auto-detected by `NPM_PACKAGE_REGISTRY_TOKEN` presence:
+Minimal hardened `.npmrc` for every Coroboros consumer. Stored as a **secret** (encrypted; carries `${VAR}` expansions resolved at install time):
-- **Absent** → `pnpm publish --provenance --no-git-checks` (OIDC Trusted Publisher + provenance attestation, no long-lived token).
-- **Present** → `pnpm publish --no-git-checks` (token-based via the `.npmrc` generated by `javascript/base`).
+```ini
+@coroboros:registry=https:${NPM_PACKAGE_REGISTRY}
+save-exact=true
+fund=false
+audit=false
+ignore-scripts=true
+package-lock=false
+prefer-online=true
+```
+
+| Line | Why |
+| :--- | :--- |
+| `@coroboros:registry=https:${NPM_PACKAGE_REGISTRY}` | Scope-resolved registry — `${NPM_PACKAGE_REGISTRY}` expands from the same-named secret. |
+| `save-exact=true` | Pin exact versions on `add` / `install`. |
+| `fund=false` | Suppress funding noise in CI logs. |
+| `audit=false` | `osv-scanner` (in `security.yml`) covers vulnerability scans natively. |
+| `ignore-scripts=true` | Belt-and-suspenders against postinstall supply-chain attacks — backs up the `--ignore-scripts` flag already passed by `javascript/base` on every `pnpm install`. |
+| `package-lock=false` | Prevent `npm` from emitting a parasitic `package-lock.json` in pnpm repos. |
+| `prefer-online=true` | Re-fetch dep metadata each install — local cache cannot mask a yanked or republished version. |
+
+
+
+
+Publish — OIDC vs token bootstrap
+
+
+
+Auto-detected by `NPM_PACKAGE_REGISTRY_TOKEN` **secret** presence on the consumer repo:
+
+| Token secret | Mode | Command |
+| :--- | :--- | :--- |
+| absent | **OIDC + provenance** (default) | `pnpm publish --provenance --no-git-checks` |
+| present | **Token bootstrap** | `npm publish --ignore-scripts --access public` |
+
+**OIDC + provenance** — no long-lived token in the repo; npm trusts a per-run id-token issued by GitHub Actions for `coroboros//ci.yml`. Requires the npm Trusted Publisher form, which only accepts an existing package — so the very first publish has to take the token bootstrap below.
+
+**Token bootstrap** — publishes the first version of a new scoped package. Set two additional **secrets** on the consumer (encrypted; forwarded via the caller's `secrets:` block):
+
+| Secret | Contents |
+| :--- | :--- |
+| `NPM_PACKAGE_REGISTRY_TOKEN` | npm Granular Access Token scoped to the publishing organization with create-new-package permission. Long-lived; revoke after migrating to OIDC. |
+| `NPM_EXTRA_CONFIG` | `${NPM_PACKAGE_REGISTRY}:_authToken=${NPM_PACKAGE_REGISTRY_TOKEN}` — appended to `.npmrc` by `javascript/base`. Stored as a **secret** because it carries auth expansion. |
+
+`npm publish` is used on the bootstrap path (not `pnpm publish`) because pnpm `>= 11.1.3` in CI auto-attempts the OIDC token exchange and does not fall back to the `.npmrc` token if OIDC fails. `--ignore-scripts --access public` skips publish-time lifecycle hooks (`prepublishOnly` excepted — known `npm` behavior). The published tarball is identical to `pnpm publish`'s.
+
+After the first publish, configure the npm Trusted Publisher form (Publisher type: GitHub Actions; Organization: the publishing org; Repository: consumer repo; Workflow filename: `ci.yml`; Environment: empty), then open a `chore(ci):` PR dropping `NPM_PACKAGE_REGISTRY_TOKEN` + `NPM_EXTRA_CONFIG` from the caller's `secrets:` block. Revoke the npm token. `1.0.1+` publishes via OIDC + provenance.
@@ -306,6 +350,8 @@ jobs:
NPM_CONFIG_FILE: ${{ secrets.NPM_CONFIG_FILE }}
NPM_PACKAGE_REGISTRY: ${{ secrets.NPM_PACKAGE_REGISTRY }}
NPM_PACKAGE_PROXY_REGISTRY: ${{ secrets.NPM_PACKAGE_PROXY_REGISTRY }}
+ # Token bootstrap (drop both after npm Trusted Publisher is wired — see Security):
+ NPM_EXTRA_CONFIG: ${{ secrets.NPM_EXTRA_CONFIG }}
NPM_PACKAGE_REGISTRY_TOKEN: ${{ secrets.NPM_PACKAGE_REGISTRY_TOKEN }}
```
diff --git a/package.json b/package.json
index 0a19b90..0835a9e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@coroboros/ci",
- "version": "0.1.11",
+ "version": "0.1.12",
"private": true,
"description": "Reusable GitHub Actions CI for the Coroboros stack.",
"license": "SEE LICENSE IN LICENSE.md",