# netkingdom Publication-Scope Policy **Owner:** netkingdom (security-infrastructure domain) · **Enforced by:** secrets-engine **Data:** `policies/netkingdom-publication-scope.yaml` · **Code:** `src/secrets_engine/publication_policy.py` ## Idea The scope of a publish token depends on the **maturity** of what is being published. A repo's maturity translates into the maturity tag of the package it ships, and each tag is restricted by the publication scope its token carries: | Package maturity tag | Publication scope (when active) | Token env var | Why | | --- | --- | --- | --- | | `maturity-build` | **gitea-wide** | `NPM_AUTH_GITEA_TOKEN` | low-stakes build artifacts may use a broad, shared token | | `maturity-test` | **org-wide** | `NPM_AUTH_COULOMB_TOKEN` | integration artifacts scoped to the org | | `maturity-prod` | **repo-scoped** | `NPM_AUTH_TOKEN` (default) / `NPM_AUTH_WHYNOTDESIGN` (explicit) | high-stakes production publishes require tight, repo-scoped rights | Scope **narrows as stakes rise** — the production token is the most tightly bound. ## The netkingdom maturity gate (why it's dormant today) The graduated table above only takes effect once **netkingdom itself reaches production grade** (`netkingdom_maturity: maturity-prod`). netkingdom is at `maturity-build` today, so the policy is **dormant** and every lane **clamps to the safest scope (`repo`)**, injected as `NPM_AUTH_TOKEN`. This is fail-safe, not fail-open: until the policy machinery is itself production-grade, we never hand out the broad (gitea/org-wide) tokens the graduated table would otherwise allow. ``` $ secrets-engine policy publication whynot-design-npm-publish netkingdom maturity: maturity-build (production_grade=False) effective publication scope: repo (clamped from graduated table — fail-safe) inject token as env var: NPM_AUTH_TOKEN ``` When netkingdom graduates, flip `netkingdom_maturity` to `maturity-prod` in the policy file; the same lanes then resolve build→gitea, test→org, prod→repo automatically. Nothing else changes. ## Token env-var naming convention `NPM_AUTH[_]_TOKEN`, where the qualifier signals the blast radius: | Effective scope | Env var | Qualifier source | | --- | --- | --- | | repo (default) | `NPM_AUTH_TOKEN` | — (bare default) | | repo (explicit) | `NPM_AUTH_WHYNOTDESIGN` | repo `whynot-design` | | npm-scope / "project" | `NPM_AUTH_WHYNOT_TOKEN` | scope `@whynot` — **defined, not used by the policy** | | org | `NPM_AUTH_COULOMB_TOKEN` | org `coulomb` | | gitea-wide | `NPM_AUTH_GITEA_TOKEN` | — | The name is a **signal**, not the boundary. The real boundary is how the Gitea token is minted (a dedicated bot account per grant is usually needed — Gitea package tokens are user-scoped, not natively scope/org-scoped) plus the OpenBao lane path + consumer policy. The catalog records the *intended* grant via `delivery_config.npm.maturity`; a reviewer sees the intended blast radius even when the backend can only enforce a coarser one. ## Lane configuration ```yaml delivery_config: npm: registry: "https://gitea.coulomb.social/api/packages/coulomb/npm/" scope: "@whynot" package: "@whynot/design" maturity: maturity-build # feeds the policy # token_env: NPM_AUTH_WHYNOTDESIGN # optional explicit override ``` `secrets-engine exec` injects the resolved token under the resolved env-var name into the npm child only; the parent shell never holds it.