@pnpm/network.web-auth 1101.4.3 → 1101.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/withOtpHandling.d.ts +37 -0
- package/lib/withOtpHandling.js +106 -51
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @pnpm/network.web-auth
|
|
2
2
|
|
|
3
|
+
## 1101.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `pnpm unpublish` now completes the two-factor authentication a registry asks for instead of failing with `ERR_PNPM_UNAUTHORIZED` while logged in. A 401 that is an OTP challenge starts the web-based authentication flow, or prompts for a classic one-time password. The obtained password is reused by every request of the run [#14464](https://github.com/pnpm/pnpm/issues/14464).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies:
|
|
12
|
+
- @pnpm/error@1100.1.4
|
|
13
|
+
|
|
14
|
+
## 1101.5.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- `pnpm stage approve` now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. The whole batch is approved with a single one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace, the selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry.
|
|
19
|
+
|
|
3
20
|
## 1101.4.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/lib/index.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export { generateQrCode } from './generateQrCode.js';
|
|
|
3
3
|
export { pollForWebAuthToken, type PollForWebAuthTokenParams, type WebAuthContext, type WebAuthFetchOptions, type WebAuthFetchResponse, type WebAuthFetchResponseBody, type WebAuthFetchResponseBodyReader, type WebAuthFetchResponseHeaders, } from './pollForWebAuthToken.js';
|
|
4
4
|
export { promptBrowserOpen, type PromptBrowserOpenContext, type PromptBrowserOpenParams, type PromptBrowserOpenReadlineInterface, } from './promptBrowserOpen.js';
|
|
5
5
|
export { WebAuthTimeoutError } from './WebAuthTimeoutError.js';
|
|
6
|
-
export { canonicalHttpUrl, isOtpError, type OtpContext, type OtpEnquirer, type OtpHandlingParams, OtpNonInteractiveError, type OtpProcess, OtpSecondChallengeError, SyntheticOtpError, withOtpHandling, } from './withOtpHandling.js';
|
|
6
|
+
export { canonicalHttpUrl, createOtpSession, isOtpError, type OtpContext, type OtpEnquirer, type OtpHandlingParams, OtpNonInteractiveError, type OtpProcess, OtpSecondChallengeError, type OtpSession, type OtpSessionParams, SyntheticOtpError, withOtpHandling, } from './withOtpHandling.js';
|
package/lib/index.js
CHANGED
|
@@ -3,5 +3,5 @@ export { generateQrCode } from './generateQrCode.js';
|
|
|
3
3
|
export { pollForWebAuthToken, } from './pollForWebAuthToken.js';
|
|
4
4
|
export { promptBrowserOpen, } from './promptBrowserOpen.js';
|
|
5
5
|
export { WebAuthTimeoutError } from './WebAuthTimeoutError.js';
|
|
6
|
-
export { canonicalHttpUrl, isOtpError, OtpNonInteractiveError, OtpSecondChallengeError, SyntheticOtpError, withOtpHandling, } from './withOtpHandling.js';
|
|
6
|
+
export { canonicalHttpUrl, createOtpSession, isOtpError, OtpNonInteractiveError, OtpSecondChallengeError, SyntheticOtpError, withOtpHandling, } from './withOtpHandling.js';
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
package/lib/withOtpHandling.d.ts
CHANGED
|
@@ -42,6 +42,33 @@ export interface OtpHandlingParams<T> {
|
|
|
42
42
|
fetchOptions: WebAuthFetchOptions;
|
|
43
43
|
operation: (otp?: string) => Promise<T>;
|
|
44
44
|
}
|
|
45
|
+
export interface OtpSessionParams {
|
|
46
|
+
context: OtpContext;
|
|
47
|
+
fetchOptions: WebAuthFetchOptions;
|
|
48
|
+
}
|
|
49
|
+
export interface OtpSession {
|
|
50
|
+
/**
|
|
51
|
+
* Runs `operation` with the one-time password this session holds, obtaining
|
|
52
|
+
* one on demand.
|
|
53
|
+
*
|
|
54
|
+
* The first operation runs without a password (the caller may still send a
|
|
55
|
+
* configured `--otp`); the password obtained from a challenge is kept and
|
|
56
|
+
* passed to every later operation, so a batch of operations costs one
|
|
57
|
+
* authentication instead of one per operation. When a kept password stops
|
|
58
|
+
* being accepted — a classic OTP expires within a minute — the challenge it
|
|
59
|
+
* triggers obtains a new one and the operation is retried with it.
|
|
60
|
+
*/
|
|
61
|
+
run: <T>(operation: (otp?: string) => Promise<T>) => Promise<T>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates an {@link OtpSession}: OTP challenge handling shared across a series
|
|
65
|
+
* of operations.
|
|
66
|
+
*
|
|
67
|
+
* @throws {@link OtpNonInteractiveError} if OTP is required but the terminal is not interactive.
|
|
68
|
+
* @throws {@link OtpSecondChallengeError} if the registry challenges an operation again right after
|
|
69
|
+
* a freshly obtained one-time password was submitted for it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createOtpSession({ context, fetchOptions }: OtpSessionParams): OtpSession;
|
|
45
72
|
/**
|
|
46
73
|
* Wraps an operation with OTP (one-time password) challenge handling.
|
|
47
74
|
*
|
|
@@ -51,6 +78,9 @@ export interface OtpHandlingParams<T> {
|
|
|
51
78
|
* 2. Falls back to prompting the user for a classic OTP code.
|
|
52
79
|
* 3. Retries the operation with the obtained OTP.
|
|
53
80
|
*
|
|
81
|
+
* Use {@link createOtpSession} instead when several operations authenticate
|
|
82
|
+
* against the same registry in one run, so they share one one-time password.
|
|
83
|
+
*
|
|
54
84
|
* @throws {@link OtpNonInteractiveError} if OTP is required but the terminal is not interactive.
|
|
55
85
|
* @throws {@link OtpSecondChallengeError} if the registry requests OTP a second time after one was submitted.
|
|
56
86
|
* @throws the original error if OTP handling is not applicable.
|
|
@@ -66,6 +96,13 @@ export declare class SyntheticOtpError extends Error implements OtpError {
|
|
|
66
96
|
readonly code = "EOTP";
|
|
67
97
|
readonly body?: OtpErrorBody;
|
|
68
98
|
constructor(body: OtpErrorBody | undefined);
|
|
99
|
+
/**
|
|
100
|
+
* The challenge a `401` response body carries, or `undefined` when the body
|
|
101
|
+
* is a plain authentication failure. A JSON body with both `authUrl` and
|
|
102
|
+
* `doneUrl` is the web-based flow; a body mentioning `one-time pass` (npm's
|
|
103
|
+
* classic wording) is a classic OTP challenge.
|
|
104
|
+
*/
|
|
105
|
+
static fromUnauthorizedBody(body: string): SyntheticOtpError | undefined;
|
|
69
106
|
static fromUnknownBody(globalWarn: OtpContext['globalWarn'], body: unknown): SyntheticOtpError;
|
|
70
107
|
}
|
|
71
108
|
export declare class OtpNonInteractiveError extends PnpmError {
|
package/lib/withOtpHandling.js
CHANGED
|
@@ -6,6 +6,43 @@ export const isOtpError = (error) => error != null &&
|
|
|
6
6
|
typeof error === 'object' &&
|
|
7
7
|
'code' in error &&
|
|
8
8
|
error.code === 'EOTP';
|
|
9
|
+
/**
|
|
10
|
+
* Creates an {@link OtpSession}: OTP challenge handling shared across a series
|
|
11
|
+
* of operations.
|
|
12
|
+
*
|
|
13
|
+
* @throws {@link OtpNonInteractiveError} if OTP is required but the terminal is not interactive.
|
|
14
|
+
* @throws {@link OtpSecondChallengeError} if the registry challenges an operation again right after
|
|
15
|
+
* a freshly obtained one-time password was submitted for it.
|
|
16
|
+
*/
|
|
17
|
+
export function createOtpSession({ context, fetchOptions }) {
|
|
18
|
+
let sessionOtp;
|
|
19
|
+
return {
|
|
20
|
+
async run(operation) {
|
|
21
|
+
let error;
|
|
22
|
+
try {
|
|
23
|
+
return await operation(sessionOtp);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (!isOtpError(err))
|
|
27
|
+
throw err;
|
|
28
|
+
error = err;
|
|
29
|
+
}
|
|
30
|
+
const otp = await resolveOtpChallenge(context, fetchOptions, error);
|
|
31
|
+
if (otp == null)
|
|
32
|
+
throw error;
|
|
33
|
+
sessionOtp = otp;
|
|
34
|
+
try {
|
|
35
|
+
return await operation(otp);
|
|
36
|
+
}
|
|
37
|
+
catch (retryError) {
|
|
38
|
+
if (isOtpError(retryError)) {
|
|
39
|
+
throw new OtpSecondChallengeError();
|
|
40
|
+
}
|
|
41
|
+
throw retryError;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
9
46
|
/**
|
|
10
47
|
* Wraps an operation with OTP (one-time password) challenge handling.
|
|
11
48
|
*
|
|
@@ -15,6 +52,9 @@ export const isOtpError = (error) => error != null &&
|
|
|
15
52
|
* 2. Falls back to prompting the user for a classic OTP code.
|
|
16
53
|
* 3. Retries the operation with the obtained OTP.
|
|
17
54
|
*
|
|
55
|
+
* Use {@link createOtpSession} instead when several operations authenticate
|
|
56
|
+
* against the same registry in one run, so they share one one-time password.
|
|
57
|
+
*
|
|
18
58
|
* @throws {@link OtpNonInteractiveError} if OTP is required but the terminal is not interactive.
|
|
19
59
|
* @throws {@link OtpSecondChallengeError} if the registry requests OTP a second time after one was submitted.
|
|
20
60
|
* @throws the original error if OTP handling is not applicable.
|
|
@@ -22,60 +62,48 @@ export const isOtpError = (error) => error != null &&
|
|
|
22
62
|
* @see https://github.com/npm/cli/blob/7d900c46/lib/utils/otplease.js for npm's implementation.
|
|
23
63
|
*/
|
|
24
64
|
export async function withOtpHandling({ context, fetchOptions, operation, }) {
|
|
25
|
-
|
|
65
|
+
return createOtpSession({ context, fetchOptions }).run(operation);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Satisfies an OTP challenge, either through the web-based authentication flow
|
|
69
|
+
* (when the challenge carries both `authUrl` and `doneUrl`) or by prompting for
|
|
70
|
+
* a classic one-time password.
|
|
71
|
+
*
|
|
72
|
+
* @returns the one-time password, or `undefined` when the user supplied none.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveOtpChallenge(context, fetchOptions, error) {
|
|
75
|
+
const { enquirer, globalInfo, globalWarn, process } = context;
|
|
76
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
77
|
+
throw new OtpNonInteractiveError(error.body);
|
|
78
|
+
}
|
|
79
|
+
const authUrl = canonicalHttpUrl(error.body?.authUrl);
|
|
80
|
+
const doneUrl = canonicalHttpUrl(error.body?.doneUrl);
|
|
81
|
+
if (authUrl != null && doneUrl != null) {
|
|
82
|
+
globalInfo(formatAuthUrlMessage(authUrl, globalWarn));
|
|
83
|
+
const pollPromise = pollForWebAuthToken({
|
|
84
|
+
context,
|
|
85
|
+
doneUrl,
|
|
86
|
+
fetchOptions,
|
|
87
|
+
});
|
|
88
|
+
return promptBrowserOpen({
|
|
89
|
+
authUrl,
|
|
90
|
+
context,
|
|
91
|
+
pollPromise,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
let otp;
|
|
26
95
|
try {
|
|
27
|
-
|
|
96
|
+
otp = await enquirer.input({
|
|
97
|
+
message: 'This operation requires a one-time password.\nEnter OTP:',
|
|
98
|
+
});
|
|
28
99
|
}
|
|
29
|
-
catch (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
let otp;
|
|
36
|
-
const authUrl = canonicalHttpUrl(error.body?.authUrl);
|
|
37
|
-
const doneUrl = canonicalHttpUrl(error.body?.doneUrl);
|
|
38
|
-
if (authUrl != null && doneUrl != null) {
|
|
39
|
-
globalInfo(formatAuthUrlMessage(authUrl, globalWarn));
|
|
40
|
-
const pollPromise = pollForWebAuthToken({
|
|
41
|
-
context,
|
|
42
|
-
doneUrl,
|
|
43
|
-
fetchOptions,
|
|
44
|
-
});
|
|
45
|
-
otp = await promptBrowserOpen({
|
|
46
|
-
authUrl,
|
|
47
|
-
context,
|
|
48
|
-
pollPromise,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
let otpValue;
|
|
53
|
-
try {
|
|
54
|
-
otpValue = await enquirer.input({
|
|
55
|
-
message: 'This operation requires a one-time password.\nEnter OTP:',
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
otp = otpValue || undefined;
|
|
65
|
-
}
|
|
66
|
-
if (otp != null) {
|
|
67
|
-
try {
|
|
68
|
-
return await operation(otp);
|
|
69
|
-
}
|
|
70
|
-
catch (retryError) {
|
|
71
|
-
if (isOtpError(retryError)) {
|
|
72
|
-
throw new OtpSecondChallengeError();
|
|
73
|
-
}
|
|
74
|
-
throw retryError;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
throw error;
|
|
100
|
+
catch (err) {
|
|
101
|
+
// The user aborted the prompt: re-throw the original challenge.
|
|
102
|
+
if (err instanceof Error && err.name === 'ExitPromptError')
|
|
103
|
+
return undefined;
|
|
104
|
+
throw err;
|
|
78
105
|
}
|
|
106
|
+
return otp || undefined;
|
|
79
107
|
}
|
|
80
108
|
/**
|
|
81
109
|
* Synthetic instance of {@link OtpError} meant to be thrown by the callbacks of {@link withOtpHandling}
|
|
@@ -88,6 +116,25 @@ export class SyntheticOtpError extends Error {
|
|
|
88
116
|
super('This error was meant to be caught by `withOtpHandling`, not to propagate to other parts of the code');
|
|
89
117
|
this.body = body;
|
|
90
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The challenge a `401` response body carries, or `undefined` when the body
|
|
121
|
+
* is a plain authentication failure. A JSON body with both `authUrl` and
|
|
122
|
+
* `doneUrl` is the web-based flow; a body mentioning `one-time pass` (npm's
|
|
123
|
+
* classic wording) is a classic OTP challenge.
|
|
124
|
+
*/
|
|
125
|
+
static fromUnauthorizedBody(body) {
|
|
126
|
+
const parsed = tryParseJson(body);
|
|
127
|
+
if (parsed != null && typeof parsed === 'object' && 'authUrl' in parsed && 'doneUrl' in parsed) {
|
|
128
|
+
return new SyntheticOtpError({
|
|
129
|
+
authUrl: typeof parsed.authUrl === 'string' ? parsed.authUrl : undefined,
|
|
130
|
+
doneUrl: typeof parsed.doneUrl === 'string' ? parsed.doneUrl : undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (body.toLowerCase().includes('one-time pass')) {
|
|
134
|
+
return new SyntheticOtpError(undefined);
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
91
138
|
static fromUnknownBody(globalWarn, body) {
|
|
92
139
|
if (body == null || typeof body !== 'object') {
|
|
93
140
|
return new SyntheticOtpError(undefined);
|
|
@@ -113,6 +160,14 @@ export class SyntheticOtpError extends Error {
|
|
|
113
160
|
return new SyntheticOtpError({ authUrl, doneUrl });
|
|
114
161
|
}
|
|
115
162
|
}
|
|
163
|
+
function tryParseJson(body) {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(body);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
116
171
|
export class OtpNonInteractiveError extends PnpmError {
|
|
117
172
|
authUrl;
|
|
118
173
|
doneUrl;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/network.web-auth",
|
|
3
|
-
"version": "1101.
|
|
3
|
+
"version": "1101.6.0",
|
|
4
4
|
"description": "Web-based authentication flow with QR code display and token polling",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"!*.map"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@pnpm/error": "1100.1.
|
|
32
|
-
"open": "^11.0.
|
|
31
|
+
"@pnpm/error": "1100.1.4",
|
|
32
|
+
"open": "^11.0.2",
|
|
33
33
|
"qrcode-terminal": "^0.12.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@jest/globals": "30.4.1",
|
|
37
|
-
"@pnpm/network.web-auth": "1101.
|
|
37
|
+
"@pnpm/network.web-auth": "1101.6.0",
|
|
38
38
|
"@types/qrcode-terminal": "^0.12.2"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|