Skip to content

S0317-config-1: publish with the workflow's OIDC identity, not a long-lived npm token - #103

Open
NSchatz wants to merge 2 commits into
mainfrom
sdd/S0317-config-1
Open

NSchatz wants to merge 2 commits into
mainfrom
sdd/S0317-config-1

Conversation

@NSchatz

@NSchatz NSchatz commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Spec: pipeline/active/S0317-config-1/spec.md (umbrella), gate verdict verdict-spec-1.md.

No long-lived npm credential can publish a forged @cosyte/* version any more, because the release
workflow's own OIDC identity is what the registry accepts.

What changed

  • release.yml's publish job carries no registry credential. NPM_TOKEN and NODE_AUTH_TOKEN
    are gone from the publish step and from the configuration allow-check step, id-token: write and
    the protected release environment stay exactly where they were, and actions/setup-node is no
    longer given registry-url, so nothing generates a user npmrc whose only purpose is carrying a
    token. The registry itself is pinned by npm-config-allow.json's require rule instead.
  • .github/credential-surface.json declares the authentication under
    publishPath.authentication: the method, the grant it rests on, the runtime evidence a preflight
    can observe, the npm CLI floor, and the four trusted-publisher fields npmjs.com asks for.
    credential-surface.mjs validates that block, checks the grant against the workflow, refuses a
    trusted publisher registered against a different workflow filename or environment, refuses a
    declared registry credential on a tokenless path, and now runs the documentation comparison in
    both directions.
  • The npm CLI on the publish path is pinned to an exact version (NPM_CLI_VERSION, 11.19.1) that
    a reader can determine from the workflow file, and ci.yml's release-dry-run declares the same
    one. scripts/publish-toolchain.mjs (new) asserts both inside the required verify job.
  • scripts/publish-preflight.mjs stops proving a token is set. It proves the declared
    authentication is available in this environment and that the resolved npm meets the declared floor,
    still before the build and without contacting the registry. Presence only: one of those variables
    is a credential and its value is never read, compared or printed.
  • RELEASING.md carries the registration procedure with the exact fields, the post-2026-09-03
    allowed-action default, and the correction that the npm CLI, not pnpm, carries trusted
    publishing on this path.

What this does NOT do

Registering the npm Trusted Publisher and deleting the organization NPM_TOKEN secret are founder
actions on npmjs.com and in GitHub settings. Register the publisher before this lands, or no
publish can succeed until it is registered
(npm accepts OIDC in addition to a token, so registering
first is safe). AC-11 is the operator's and is proved only by the first real publish.

Nothing here changes what any package contains; no package version moves, so there is no changeset.

Evidence

pnpm test:root 840/840 green, credentials:check, install-hardening, readme-check,
drift-check, publish-toolchain, changeset-guard, release:notes, format:check,
check:no-emdash, typecheck and lint all green locally. Every grader added here is run against a
mutation that breaks the property it asserts.

The publish job authenticates with the workflow's own OIDC identity and
nothing else: no NPM_TOKEN or NODE_AUTH_TOKEN reaches any job or step,
id-token: write and the protected release environment are the whole of
its authentication, and setup-node no longer generates a user npmrc whose
only purpose is carrying a token.

The declaration gains publishPath.authentication, which the checker
validates and compares against the workflow: the grant the method rests
on must be on the publish job, the trusted publisher must name this
workflow filename and environment, and a credential carrying registryAuth
is a contradiction under a tokenless method. The documentation check now
runs in both directions, so a rotation procedure for a credential nobody
declares is a refusal too.

scripts/publish-toolchain.mjs pins the npm CLI floor at merge time inside
the required verify job, and refuses a release dry run declaring a
different toolchain.
The preflight stops proving a token is set and starts proving the
authentication the declaration names for the publish path is available
here, still before the build and without contacting the registry. It also
asserts the resolved npm against the declared floor, so an npm that
cannot do the OIDC exchange stops the release before anything is packed.
Presence only: one of those variables is itself a credential and its
value is never read, compared or printed.

Two new suites, one per subject. tokenless-publish grades what the
committed workflow, declaration and allow-set say (AC-1, AC-4, AC-5,
AC-8, AC-12); publish-preflight-oidc grades what the command path does
when it runs (AC-3, AC-6, AC-7, AC-9). Every grader is run against a
mutation that breaks the property it asserts. test/publish-preflight.test.ts
is superseded by the second file, which carries every case of it that is
still true.

The existing credential-surface and npm-config-allow suites had fixtures
written around the token; those are retargeted at credentials that still
exist, so what each case grades is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant