v0.13.0
A second full-pass review over src/ plus the build and release tooling. The headline items are a silent org-role demotion, a retry policy that could re-apply writes, and a release script whose "verify" step could fail an already-successful release.
Fixed
npm_org_member_setno longer demotes an existing admin or owner whenroleis omitted. The registry membership spec definesroleas "defaults todeveloperif not given" -- it does not preserve the current value, and the npm CLI never exposes that because it fills the same default itself (lib/commands/org.js:role = role || 'developer'). A bare{ user }body therefore silently demoted the member. The handler now reads the roster and re-sends the existing role explicitly, so "omitroleto keep the current role" is finally true. If the roster read fails it refuses the write rather than risking the demotion.- Write requests (
PUT/POST/DELETEthat mutate) are no longer retried after a network error or timeout. The registry may have applied the mutation and lost the response; retrying re-sent it with the same_rev, which came back 409 and read like a failure on an operation that had actually succeeded. Those requests now surface the ambiguity explicitly. Writes still retry on 429 and 503 -- the statuses that mean "I did not process this" -- but no longer on 502/504, which a gateway can return after the origin applied the change. Read-only auditPOSTs keep the full retry policy. - A malformed JSON body on a 2xx is reported against its real status instead of being caught as a transport failure and retried -- that path re-sent a request the registry had already answered.
Retry-Afteris clamped to 30s. An unbounded value parked a single tool call for minutes per attempt, an order of magnitude past the request timeout.npm_undeprecateretries once on a 409_revconflict, matchingnpm_deprecate. Both do the same read-modify-write against the same revision, so both race the same way.npm_owner_addmatches existing maintainers case-insensitively (asnpm_owner_removealready did). A packument entry whose case differed from the canonical/-/userrecord read as "not an owner" and appended a duplicate maintainer for the same person.npm_dep_treememoization is depth-aware. A node first reached at the depth limit was recorded without expanding its children, and a later arrival at a shallower depth short-circuited on the "already seen" check -- leaving that subtree missing. Because it turned on fetch-latency ordering, the same query could return different trees on different runs.- Handlers no longer assert
res.data!on responses that can legitimately arrive empty. A 2xx with no body (which the client returns asok: truewith nodata) produced a raw "Cannot read properties of undefined" at the MCP boundary; it now returns an actionable 502. Same fix applied topackument.time,versionDoc.dist, and the array-shaped org/trust responses. npm_publish_preflightno longer reports "2FA is enabled and token type cannot be verified" when the profile fetch failed -- a state it never established. That case gets its own summary naming the real uncertainty.- Download and registry-stats handlers validate the
periodbefore interpolating it into the request path. npm_typesdistinguishes "no@typespackage exists" (a 404) from "the lookup failed", via a newtypesLookupReliablefield. Any non-404 failure previously reported a definitive "No TypeScript types available".release.sh: the post-publish npx smoke test ran as a bare subshell underset -e, so exhausting its retries terminated the script before the warn-only branch -- turning a slow registry into[FAIL] Release failedon an already-successful release. It now runs as anifcondition.src/index.tsno longer relies on a top-levelawait import(). TLA cannot be emitted in the CJS format the single-binary build uses, and it only ever compiled because the__VERSION__define let esbuild eliminate that branch first -- making a build-breaking construct load-bearing on constant folding.- The published bundle no longer carries a
sourceMappingURLcomment pointing at a file that was never shipped. The build now emits the sourcemap asexternal, keepingdist/index.js.mapon disk for local debugging while leaving it out of the tarball -- shipping it instead would have taken the packed download from 225 kB to 583 kB on everynpxcold start.
Changed
- Tool annotations:
npm_owner_add,npm_team_create,npm_team_member_add, andnpm_hook_addnow reportdestructiveHint: false. Per the MCP spec the hint means "may perform destructive updates", and these four only add. Previously a test invariant requiredreadOnlyHintanddestructiveHintto be strict opposites, which forced every write to claim destructiveness. MCP hosts that gate confirmation prompts ondestructiveHintwill stop prompting for these four tools. The invariant is now one-directional (read-only implies non-destructive), with explicit pins for both the additive set and the ten irreversible operations. npm_check_authandnpm_publish_preflighttreat 2FA modeauth-onlyas non-blocking for publishing. That mode challenges login, not writes, so those accounts publish headlessly with any valid token -- they were previously reported ascanPublishHeadless: nullwith an EOTP warning.auth-and-writes(and any unrecognized future mode) still gates.npm_ops_playbookno longer claims that period-and-capital deprecation messages trigger a 422. That heuristic was traced to a wildcard version range matching no published versions and removed from validation in v0.10; the playbook had kept propagating it to the agents it exists to advise. It now points at the 1024-character limit and the semver range instead.npm_hook_update's description matches its schema: bothendpointandsecretare required, and the call always rotates the secret because the registry PUT replaces the whole hook config.release.shchecks for a provenance attestation on every publish, not only in CI, and names the cause when one is absent.
Documentation
- README: the
npm_audit_deepexample omitted the requireddependenciesfield and passed the dependency as the project name, so it could not run as written. The publish row of the decision matrix pointed at a CI tag-push workflow that was removed in b2c256c. scripts/build-binary.mjsno longer suggests verifying with adoctor --jsonsubcommand that does not exist, or citing aBINARY_DISTRIBUTION.mdthat is not in the repo.