npmjs.com sits behind bot management. The fleet's npm browser tools work with
it, not around it: a human signs in once in plain Chrome, automation only reuses
that seeded session, and when a write triggers a human-verification challenge the
run PAUSES for a person to solve it. That rhythm lives in one place —
scripts/fleet/publish-infra/npm/browser-session.mts, the runChallengeAware
helper — so no tool re-derives it.
npm's bot management DROPS a login performed in a CDP / devtools-driven browser:
the sign-in and OTP succeed, then the site bounces straight back to the
signed-out landing page. The CDP wire itself is the tell, so no launch-flag
tuning fixes it. browser-sign-in.mts therefore launches real, CDP-free Chrome
on the shared profile for the one human sign-in. Every automation launch
(openNpmBrowserSession) only ever REUSES the session cookie that sign-in
seeded — it never types a credential.
An existing session cookie is honored for reads and navigation: the tools read
/-/whoami, the staged-packages view, and a package's access page through the
signed-in page session. Sensitive WRITE operations — changing Trusted Publisher
settings, publishing — can trigger a human-verification challenge even on a
valid session.
A challenge is a person's job. The run brings the Chrome window to the front, prints an elapsed / remaining countdown, and waits for the operator to solve it. It is NEVER retried on a backoff ladder: a retry into a live challenge earns a rate limit, and that rate limit then masquerades as a broken session — the exact false trail that turns a five-minute pause into an hour of debugging. Nothing is written while a challenge is outstanding.
- A write triggers a challenge. The operator solves ONE challenge in the window.
- The driver ticks npm's per-IP cooldown opt-in (the
didOptForCooldowncheckbox,COOLDOWN_OPTIN_SELECTOR). - With the opt-in ticked, subsequent trust / publish operations skip re-challenge for about five minutes.
- Batch the rest of the work into that window — the first package in a batch pays the pause, the rest ride the cooldown.
- When the window lapses, the next operation draws a fresh challenge. Solve it, repeat.
runChallengeAware (in browser-session.mts) owns the whole loop: it runs a
caller's operation, and each time the operation reports a challenge it calls
pauseForChallenge (visible countdown, cooldown opt-in, budget enforcement) and
re-attempts — bounded by CHALLENGE_BUDGET_MS, never a blind retry. When the
budget is spent it throws a What / Where / Saw-vs-wanted / Fix block that says it
stopped rather than retried into a rate limit.
The operation owns what it does and how it classifies its own result into a
finished value, a challenge, or a transient race retry; the helper owns only the
pause-then-retry orchestration. Both consumers — the Trusted Publisher settings
driver (trusted-publisher-page.mts) and the staged-tarball read
(staged-browser-read.mts) — call runChallengeAware, so the anti-bot rhythm
exists once.