fix: restore OTP and webauth flows broken by libnpmpublish migration#10834
fix: restore OTP and webauth flows broken by libnpmpublish migration#10834Copilot wants to merge 3 commits into
Conversation
|
@copilot Answer this question: I have noticed that this PR lack integration tests. I wonder if integration tests would be possible. The integration tests of pnpm rely on Does Verdaccio provide OTP protocol? If Verdaccio provides OTP, does If Verdaccio doesn't provide OTP, we probably would have to rely on dependency injection and mocking like #10591 combined with real testing. Important Answer this question only. Do not make change to the code. |
After a thorough search through Verdaccio's source code (both the Since Verdaccio doesn't support OTP, Conclusion: integration tests via Verdaccio are not possible for OTP/webauth flows. The right approach is unit tests with dependency injection, similar to how
Without those injections, the logic can only be exercised end-to-end against a real registry that actually challenges with OTP. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Create a file at This file immitates the dependency injection pattern found in |
Done in commit
A test can inject all dependencies without any registry or real TTY: const context: OtpContext = {
...SHARED_CONTEXT,
isInteractive: true,
publish: jest.fn(async () => { throw Object.assign(new Error('EOTP'), { code: 'EOTP' }) }),
prompt: jest.fn(async () => '123456'),
}
const response = await publishWithOtpHandling({ context, manifest: {}, tarballData, publishOptions }) |
|
@copilot Can you address the comments and questions and suggestions I have just made? |
Working through all the review comments now. For the |
| export const SHARED_CONTEXT: OtpContext = { | ||
| Date, | ||
| delay: (ms) => new Promise<void>(resolve => setTimeout(resolve, ms)), | ||
| enquirer: enquirer as unknown as OtpEnquirer, |
There was a problem hiding this comment.
@copilot What was the actual function signature of the actual enquirer btw? I may be able to suggest you the way to fix this signature without type cast.
Important
This is a question, just answer, don't code.
There was a problem hiding this comment.
The actual enquirer default export is an Enquirer class instance. Its prompt signature is approximately:
prompt<T extends object = object>(questions: PromptOptions | PromptOptions[]): Promise<T>It takes a generic options object and returns Promise<T> where T defaults to object — it never returns undefined. The cast as unknown as OtpEnquirer is needed because:
- The parameter type
PromptOptionsdoesn't overlap with our specificOtpEnquirerOptions - The return type
Promise<T>is incompatible withPromise<OtpEnquirerResponse | undefined>(notably the| undefined)
If we dropped | undefined from OtpEnquirerResponse | undefined, a two-step cast with a compatible intermediate type might be possible, but the PromptOptions vs OtpEnquirerOptions parameter mismatch still requires as unknown as the bridge.
Verdaccio does not support OTP/2FA — there is no OTP-related code in its publish endpoint or authentication. Integration tests via Verdaccio are not possible for OTP/webauth flows. The right approach is unit tests with dependency injection, using the
All comments addressed in |
There was a problem hiding this comment.
Pull request overview
Restores publish-time OTP handling (classic OTP, webauth/passkey flow, and npm-notice messaging) that was lost when migrating from npm publish to libnpmpublish, and centralizes the implementation behind a DI-friendly context to enable unit testing.
Changes:
- Add OTP handling module (
src/otp.ts) with classic OTP prompt retry, webauth URL + doneUrl polling, and npm-notice display (incl. QR codes), plus unit tests. - Consolidate OIDC + OTP shared runtime dependencies into a new
src/utils/shared-context.ts, updating OIDC modules accordingly. - Route
publishPackedPkg()through OTP handling and send npm-style publish headers (npm-auth-type,npm-command) plus corresponding options.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| releasing/plugin-commands-publishing/src/otp.ts | New OTP/webauth handling module (DI-based), QR output, and new error types. |
| releasing/plugin-commands-publishing/test/otp.test.ts | Unit tests covering OTP success, non-interactive behavior, npm-notice, and webauth polling. |
| releasing/plugin-commands-publishing/src/utils/shared-context.ts | New merged shared context providing real implementations for OIDC + OTP flows. |
| releasing/plugin-commands-publishing/src/oidc/utils/shared-context.ts | Removes old OIDC-only shared context file (replaced by merged location). |
| releasing/plugin-commands-publishing/src/oidc/authToken.ts | Updates SHARED_CONTEXT import path to merged shared context. |
| releasing/plugin-commands-publishing/src/oidc/idToken.ts | Updates SHARED_CONTEXT import path to merged shared context. |
| releasing/plugin-commands-publishing/src/oidc/provenance.ts | Updates SHARED_CONTEXT import path to merged shared context. |
| releasing/plugin-commands-publishing/src/publishPackedPkg.ts | Publishes via publishWithOtpHandling() and adds npm publish headers/options. |
| releasing/plugin-commands-publishing/package.json | Adds qrcode-terminal runtime dep and its types. |
| pnpm-workspace.yaml | Adds catalog entries for qrcode-terminal and @types/qrcode-terminal. |
| pnpm-lock.yaml | Locks new qrcode-terminal and @types/qrcode-terminal dependencies. |
| cspell.json | Adds OTP-related words to dictionary. |
| .husky/pre-push | Adds shebang and pnpm presence check (but currently suppresses failures). |
| .husky/commit-msg | Adds shebang and pnpm presence check (but currently suppresses failures). |
| .changeset/fix-webauth-otp-publish.md | Changeset documenting OTP/webauth publish behavior and headers. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Add OTP handling to `pnpm publish` with support for: - Classic OTP prompt (manual code entry) - Web-based authentication flow with QR code display - Configurable npm-auth-type header signaling web auth support Extract OTP logic into a dedicated module with dependency injection for testability. Consolidate shared context for OIDC and OTP flows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3d6a247 to
5bec7c8
Compare
Add OTP handling to `pnpm publish` with support for: - Classic OTP prompt (manual code entry) - Web-based authentication flow with QR code display and doneUrl polling - `npm-auth-type: web` header to signal web auth support to the registry Extract OTP logic into a dedicated `otp.ts` module with dependency injection for testability. Consolidate shared context for OIDC and OTP. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Couldn't rebase this pr properly for some reason. Had to create a new one: #11019 |
* feat(publish): handle OTP and web-based authentication flows (#10834) Add OTP handling to `pnpm publish` with support for: - Classic OTP prompt (manual code entry) - Web-based authentication flow with QR code display and doneUrl polling - `npm-auth-type: web` header to signal web auth support to the registry Extract OTP logic into a dedicated `otp.ts` module with dependency injection for testability. Consolidate shared context for OIDC and OTP. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: KSXGitHub <11488886+KSXGitHub@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Switching from
npm publishtolibnpmpublishin #10591 silently dropped OTP handling —npm publishcaughtEOTPerrors internally, butlibnpmpublishthrows them and pnpm had no handler.Changes
src/otp.ts(new): Extracts all OTP handling into a dedicated module following the dependency injection pattern ofoidc/{authToken,idToken,provenance}.ts:OtpContext— interface with all injectable dependencies:publish(libnpmpublish),fetch(webauth polling withOtpWebAuthFetchOptions),enquirer(OTP input viaOtpEnquirer),globalInfo,Date(timeout),setTimeout(polling interval),process(TTY check)SHARED_CONTEXT— re-exported fromsrc/utils/shared-context.ts(see below)publishWithOtpHandling()— main function,contextdefaults toSHARED_CONTEXT:EOTPwithoutauthUrl/doneUrl): prompts the user viaenquirerand retries by callingcontext.publishdirectly with the OTPEOTPwithauthUrl+doneUrl): prints the auth URL along with a scannable QR code (viaqrcode-terminal), pollsdoneUrlat 1-second intervals with a 5-minute timeout (respectingRetry-Afterheaders), then retries with the returned tokencontext.process.stdin.isTTY && context.process.stdout.isTTY); throwsOtpNonInteractiveErrorif not interactiveEOTPa second time after an OTP was already submitted, throwsOtpSecondChallengeErrorwith a user-facing message describing unexpected registry behaviorOtpWebAuthTimeoutError— exported error class withreadonly endTime,startTime, andtimeoutfieldsOtpNonInteractiveError— exported error class thrown when OTP is required but pnpm is not running in an interactive terminalOtpSecondChallengeError— exported error class thrown when the registry unexpectedly requests OTP a second timeOtpEnquirer/OtpEnquirerOptions/OtpEnquirerResponse— injectable interfaces for theenquirerprompt dependencyOtpWebAuthFetchOptions— fetch options interface (method,retry,timeout) aligned with the OIDC fetch patterngenerateQrCode()— helper that synchronously generates a small QR code string viaqrcode-terminalfor display alongside the auth URLsrc/utils/shared-context.ts(new, replacessrc/oidc/utils/shared-context.ts): Merged shared context file combining OIDC and OTP real implementations. TheSHARED_CONTEXTtype isAuthTokenContext & IdTokenContext & ProvenanceContext & OtpContext. Addsenquirer,publish(with aPublishWithExportedManifesttype alias bridging the outdated@types/libnpmpublishmanifest type), andsetTimeoutto the existing OIDC dependencies. All OIDC files now import from this merged location.src/oidc/utils/shared-context.ts(removed): Replaced bysrc/utils/shared-context.ts.publishPackedPkg.ts: ImportspublishWithOtpHandlingfrom./otp.jsand calls it instead ofpublishdirectly. Removes theManifestFromOutdatedDefinitioncast (now handled by the type alias inshared-context.ts). Adds webauth-enabling publish options:headers(npm-auth-type: web,npm-command: publish),authType: 'web', andnpmCommand: 'publish'.cspell.json: Addedeotp,qrcode, andwebauthto the custom word list..changeset/tangy-pans-pull.md: Updated to document the new OTP prompt and QR code behaviors.qrcode-terminaland@types/qrcode-terminaladded to the catalog andreleasing/commands/package.json.The DI structure makes the OTP flow unit-testable without a real registry — all external dependencies can be mocked via the
contextparameter:Tests
test/publish/otp.test.ts(new): Unit tests covering both classic OTP and webauth flows, including: successful publish without OTP, non-OTP error passthrough, non-interactive terminal rejection, classic OTP prompt and retry, second challenge error, empty/undefined OTP re-throw, webauth polling with token retrieval,Retry-Afterheader handling, fetch error resilience, non-ok response resilience, JSON parse error resilience, and 5-minute timeout enforcement.Note
Verdaccio (used by
@pnpm/registry-mock) does not implement the OTP challenge flow, so integration tests for this feature are not possible via the standard test registry. Unit tests with dependency injection are the appropriate testing strategy.💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.