@automattic/vip 4.0.9 → 4.0.11
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/dist/bin/vip-defensive-mode-configure.js +216 -0
- package/dist/bin/vip-defensive-mode-disable.js +42 -0
- package/dist/bin/vip-defensive-mode-enable.js +45 -0
- package/dist/bin/vip-defensive-mode.js +21 -0
- package/dist/bin/vip.js +6 -1
- package/dist/lib/api/constants.js +7 -0
- package/dist/lib/api/feature-flags.js +17 -4
- package/dist/lib/api/http.js +3 -3
- package/dist/lib/api.js +23 -8
- package/dist/lib/cli/internal-bin-loader.js +4 -0
- package/dist/lib/defensive-mode/api.js +106 -0
- package/dist/lib/defensive-mode/cli-helpers.js +60 -0
- package/dist/lib/dev-environment/dev-environment-configuration-file.js +19 -14
- package/dist/lib/keychain/insecure.js +2 -2
- package/dist/lib/keychain/secure.js +2 -2
- package/dist/lib/keychain.js +20 -15
- package/dist/lib/logout.js +9 -4
- package/dist/lib/rechallenge/client.js +55 -0
- package/dist/lib/rechallenge/errors.js +55 -0
- package/dist/lib/rechallenge/flow.js +145 -0
- package/dist/lib/rechallenge/index.js +27 -0
- package/dist/lib/rechallenge/link.js +167 -0
- package/dist/lib/rechallenge/open-browser.js +21 -0
- package/dist/lib/rechallenge/token-cache.js +113 -0
- package/dist/lib/rechallenge/types.js +15 -0
- package/dist/lib/token.js +18 -14
- package/docs/SETUP.md +7 -6
- package/docs/decisions/0001-operation-scoped-elevated-token-reuse.md +87 -0
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -2
|
@@ -6,7 +6,6 @@ exports.getConfigurationFileOptions = getConfigurationFileOptions;
|
|
|
6
6
|
exports.mergeConfigurationFileOptions = mergeConfigurationFileOptions;
|
|
7
7
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
8
8
|
var _debug = _interopRequireDefault(require("debug"));
|
|
9
|
-
var _ejs = _interopRequireDefault(require("ejs"));
|
|
10
9
|
var _jsYaml = _interopRequireWildcard(require("js-yaml"));
|
|
11
10
|
var _promises = require("node:fs/promises");
|
|
12
11
|
var _nodePath = _interopRequireDefault(require("node:path"));
|
|
@@ -17,6 +16,8 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
17
16
|
const debug = (0, _debug.default)('@automattic/vip:bin:dev-environment');
|
|
18
17
|
const CONFIGURATION_FILE_NAME = exports.CONFIGURATION_FILE_NAME = 'vip-dev-env.yml';
|
|
19
18
|
const CONFIGURATION_TEMPLATE_FILE_NAME = exports.CONFIGURATION_TEMPLATE_FILE_NAME = 'vip-dev-env.yml.ejs';
|
|
19
|
+
const ALLOWED_CONFIG_DIR_TEMPLATE = /<%=\s*configDir\s*%>/g;
|
|
20
|
+
const EJS_TEMPLATE_TAG = /<%[\s\S]*?%>/;
|
|
20
21
|
async function getConfigurationFileOptions() {
|
|
21
22
|
const configurationFile = await findConfigurationFile();
|
|
22
23
|
if (configurationFile === false) {
|
|
@@ -176,26 +177,30 @@ async function findConfigurationFile() {
|
|
|
176
177
|
file,
|
|
177
178
|
template
|
|
178
179
|
} of locations) {
|
|
180
|
+
let contents;
|
|
179
181
|
try {
|
|
180
182
|
// eslint-disable-next-line no-await-in-loop
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
contents = await (0, _promises.readFile)(file, 'utf8');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
186
|
+
debug(`Error reading file ${file}: ${err.message}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (template) {
|
|
190
|
+
const stripped = contents.replace(ALLOWED_CONFIG_DIR_TEMPLATE, '');
|
|
191
|
+
if (EJS_TEMPLATE_TAG.test(stripped)) {
|
|
192
|
+
exit.withError(`EJS JavaScript is not supported in dev-env configuration file ${file}. ` + 'Use plain YAML, or only the supported <%= configDir %> placeholder.');
|
|
190
193
|
}
|
|
194
|
+
const rendered = contents.replace(ALLOWED_CONFIG_DIR_TEMPLATE, () => dir);
|
|
191
195
|
return {
|
|
192
196
|
configurationPath: file,
|
|
193
|
-
configurationContents:
|
|
197
|
+
configurationContents: rendered
|
|
194
198
|
};
|
|
195
|
-
} catch (error) {
|
|
196
|
-
const err = error instanceof Error ? error : new Error('Unknown error');
|
|
197
|
-
debug(`Error reading or rendering file ${file}: ${err.message}`);
|
|
198
199
|
}
|
|
200
|
+
return {
|
|
201
|
+
configurationPath: file,
|
|
202
|
+
configurationContents: contents
|
|
203
|
+
};
|
|
199
204
|
}
|
|
200
205
|
return false;
|
|
201
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
|
-
exports.
|
|
4
|
+
exports.Insecure = void 0;
|
|
5
5
|
var _configstore = _interopRequireDefault(require("configstore"));
|
|
6
6
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
7
|
class Insecure {
|
|
@@ -40,4 +40,4 @@ class Insecure {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
exports.
|
|
43
|
+
exports.Insecure = Insecure;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
|
-
exports.
|
|
4
|
+
exports.Secure = void 0;
|
|
5
5
|
var _keytar = _interopRequireDefault(require("@github/keytar"));
|
|
6
6
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
7
|
class Secure {
|
|
@@ -16,4 +16,4 @@ class Secure {
|
|
|
16
16
|
return _keytar.default.deletePassword(service, service);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
exports.
|
|
19
|
+
exports.Secure = Secure;
|
package/dist/lib/keychain.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
|
-
exports.
|
|
4
|
+
exports.getKeychain = getKeychain;
|
|
5
5
|
var _debug = _interopRequireDefault(require("debug"));
|
|
6
|
-
var _insecure =
|
|
6
|
+
var _insecure = require("./keychain/insecure");
|
|
7
7
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
8
|
let keychain;
|
|
9
9
|
const debug = (0, _debug.default)('@automattic/vip:keychain');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
async function getKeychain() {
|
|
11
|
+
if (typeof keychain === 'undefined') {
|
|
12
|
+
try {
|
|
13
|
+
const {
|
|
14
|
+
Secure
|
|
15
|
+
} = await import('./keychain/secure.js');
|
|
16
|
+
const kc = new Secure();
|
|
17
|
+
// We don't know if the secure keychain is actually usable until we try to communicate with it.
|
|
18
|
+
await kc.getPassword('non-existent-password-fraude-perit-virtus');
|
|
19
|
+
debug('Using Secure keychain');
|
|
20
|
+
keychain = kc;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
debug('Cannot use Secure keychain; falling back to Insecure keychain (Details: %o)', error);
|
|
23
|
+
keychain = new _insecure.Insecure('vip-go-cli');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return keychain;
|
|
27
|
+
}
|
package/dist/lib/logout.js
CHANGED
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _http = _interopRequireDefault(require("../lib/api/http"));
|
|
6
|
+
var _tokenCache = _interopRequireDefault(require("../lib/rechallenge/token-cache"));
|
|
6
7
|
var _token = _interopRequireDefault(require("../lib/token"));
|
|
7
8
|
var _tracker = require("../lib/tracker");
|
|
8
9
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
10
|
var _default = async () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
try {
|
|
12
|
+
await (0, _http.default)('/logout', {
|
|
13
|
+
method: 'post'
|
|
14
|
+
});
|
|
15
|
+
} finally {
|
|
16
|
+
await _token.default.purge();
|
|
17
|
+
await _tokenCache.default.clearAll();
|
|
18
|
+
}
|
|
14
19
|
await (0, _tracker.trackEvent)('logout_command_execute');
|
|
15
20
|
};
|
|
16
21
|
exports.default = _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.createSession = createSession;
|
|
5
|
+
exports.exchange = exchange;
|
|
6
|
+
exports.getSessionStatus = getSessionStatus;
|
|
7
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
8
|
+
var _nodeCrypto = require("node:crypto");
|
|
9
|
+
var _errors = require("./errors");
|
|
10
|
+
var _types = require("./types");
|
|
11
|
+
var _http = _interopRequireDefault(require("../api/http"));
|
|
12
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
// Derived from http() rather than imported from a fetch library, so this module
|
|
14
|
+
// keeps compiling across the node-fetch -> undici migration (trunk #2837).
|
|
15
|
+
|
|
16
|
+
const debug = (0, _debug.default)('@automattic/vip:rechallenge:client');
|
|
17
|
+
function fillTemplate(template, challengeId) {
|
|
18
|
+
return template.replaceAll('{challengeId}', encodeURIComponent(challengeId));
|
|
19
|
+
}
|
|
20
|
+
async function parseOrThrow(response, scope) {
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const text = await response.text();
|
|
23
|
+
throw new _errors.RechallengeHttpError(response.status, text, scope);
|
|
24
|
+
}
|
|
25
|
+
return await response.json();
|
|
26
|
+
}
|
|
27
|
+
async function createSession(opts) {
|
|
28
|
+
debug('createSession scope=%s', opts.requestedOperation);
|
|
29
|
+
const response = await (0, _http.default)(opts.path, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
// New UUID per call — intent is a fresh session per invocation, not request deduplication.
|
|
33
|
+
'Idempotency-Key': (0, _nodeCrypto.randomUUID)()
|
|
34
|
+
},
|
|
35
|
+
body: {
|
|
36
|
+
clientType: _types.CLIENT_TYPE,
|
|
37
|
+
requestedOperation: opts.requestedOperation
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return parseOrThrow(response, opts.requestedOperation);
|
|
41
|
+
}
|
|
42
|
+
async function getSessionStatus(opts) {
|
|
43
|
+
const path = fillTemplate(opts.template, opts.challengeId);
|
|
44
|
+
const response = await (0, _http.default)(path, {
|
|
45
|
+
method: 'GET'
|
|
46
|
+
});
|
|
47
|
+
return parseOrThrow(response, opts.scope ?? '');
|
|
48
|
+
}
|
|
49
|
+
async function exchange(opts) {
|
|
50
|
+
const path = fillTemplate(opts.template, opts.challengeId);
|
|
51
|
+
const response = await (0, _http.default)(path, {
|
|
52
|
+
method: 'POST'
|
|
53
|
+
});
|
|
54
|
+
return parseOrThrow(response, opts.scope ?? '');
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.RechallengeUnsupportedVersionError = exports.RechallengeTerminalError = exports.RechallengeInteractionRequiredError = exports.RechallengeHttpError = exports.RechallengeError = exports.RechallengeAbortedError = void 0;
|
|
5
|
+
var _types = require("./types");
|
|
6
|
+
class RechallengeError extends Error {
|
|
7
|
+
scope;
|
|
8
|
+
constructor(message, scope) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'RechallengeError';
|
|
11
|
+
this.scope = scope;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.RechallengeError = RechallengeError;
|
|
15
|
+
class RechallengeUnsupportedVersionError extends RechallengeError {
|
|
16
|
+
constructor(version, scope) {
|
|
17
|
+
super(`Server requested rechallenge version "${version}" but this CLI only supports ${_types.RECHALLENGE_VERSION}. Update vip-cli.`, scope);
|
|
18
|
+
this.name = 'RechallengeUnsupportedVersionError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.RechallengeUnsupportedVersionError = RechallengeUnsupportedVersionError;
|
|
22
|
+
class RechallengeTerminalError extends RechallengeError {
|
|
23
|
+
status;
|
|
24
|
+
constructor(status, scope, detail) {
|
|
25
|
+
super(`Step-up verification did not complete (status=${status})${detail ? `: ${detail}` : ''}.`, scope);
|
|
26
|
+
this.name = 'RechallengeTerminalError';
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.RechallengeTerminalError = RechallengeTerminalError;
|
|
31
|
+
class RechallengeAbortedError extends RechallengeError {
|
|
32
|
+
constructor(scope) {
|
|
33
|
+
super('Step-up verification was cancelled.', scope);
|
|
34
|
+
this.name = 'RechallengeAbortedError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.RechallengeAbortedError = RechallengeAbortedError;
|
|
38
|
+
class RechallengeHttpError extends RechallengeError {
|
|
39
|
+
statusCode;
|
|
40
|
+
bodyText;
|
|
41
|
+
constructor(statusCode, bodyText, scope) {
|
|
42
|
+
super(`Step-up verification request failed (HTTP ${statusCode}): ${bodyText}`, scope);
|
|
43
|
+
this.name = 'RechallengeHttpError';
|
|
44
|
+
this.statusCode = statusCode;
|
|
45
|
+
this.bodyText = bodyText;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.RechallengeHttpError = RechallengeHttpError;
|
|
49
|
+
class RechallengeInteractionRequiredError extends RechallengeError {
|
|
50
|
+
constructor(scope) {
|
|
51
|
+
super(`Step-up verification is required for ${scope}, but this is a non-interactive session. ` + 'Re-run the command interactively, or pass --rechallenge-wait (or set VIP_RECHALLENGE_WAIT=1) ' + 'to print the verification URL and wait for you to complete it on another device.', scope);
|
|
52
|
+
this.name = 'RechallengeInteractionRequiredError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.RechallengeInteractionRequiredError = RechallengeInteractionRequiredError;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.isInteractiveContext = isInteractiveContext;
|
|
5
|
+
exports.runRechallenge = runRechallenge;
|
|
6
|
+
exports.shouldWaitForRechallenge = shouldWaitForRechallenge;
|
|
7
|
+
var _chalk = _interopRequireDefault(require("chalk"));
|
|
8
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
9
|
+
var _promises = require("node:timers/promises");
|
|
10
|
+
var _tracker = require("../tracker");
|
|
11
|
+
var client = _interopRequireWildcard(require("./client"));
|
|
12
|
+
var _errors = require("./errors");
|
|
13
|
+
var _openBrowser = require("./open-browser");
|
|
14
|
+
var _tokenCache = _interopRequireDefault(require("./token-cache"));
|
|
15
|
+
var _types = require("./types");
|
|
16
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
17
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
|
+
const debug = (0, _debug.default)('@automattic/vip:rechallenge:flow');
|
|
19
|
+
|
|
20
|
+
// Floor for the server-provided poll interval. Guards against a missing/0/NaN
|
|
21
|
+
// value (which would otherwise produce a tight status-poll loop) and clamps
|
|
22
|
+
// implausibly small values so a misbehaving server cannot make us hammer the API.
|
|
23
|
+
const MIN_POLL_INTERVAL_SECONDS = 2;
|
|
24
|
+
const TERMINAL = new Set(['verified', 'expired', 'failed', 'cancelled']);
|
|
25
|
+
async function runRechallenge(opts) {
|
|
26
|
+
const {
|
|
27
|
+
requestedOperation,
|
|
28
|
+
rechallenge,
|
|
29
|
+
interactive,
|
|
30
|
+
wait,
|
|
31
|
+
signal
|
|
32
|
+
} = opts;
|
|
33
|
+
if (rechallenge.version !== _types.RECHALLENGE_VERSION) {
|
|
34
|
+
throw new _errors.RechallengeUnsupportedVersionError(rechallenge.version, requestedOperation);
|
|
35
|
+
}
|
|
36
|
+
await (0, _tracker.trackEvent)('rechallenge_required', {
|
|
37
|
+
scope: requestedOperation,
|
|
38
|
+
clientType: _types.CLIENT_TYPE
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// In a non-interactive session with no opt-in to wait, fail fast instead of
|
|
42
|
+
// creating a verification session no human can complete — automation would
|
|
43
|
+
// otherwise block until the session expires.
|
|
44
|
+
if (!interactive && !wait) {
|
|
45
|
+
await (0, _tracker.trackEvent)('rechallenge_interaction_required', {
|
|
46
|
+
scope: requestedOperation
|
|
47
|
+
});
|
|
48
|
+
throw new _errors.RechallengeInteractionRequiredError(requestedOperation);
|
|
49
|
+
}
|
|
50
|
+
const session = await client.createSession({
|
|
51
|
+
path: rechallenge.createSessionPath,
|
|
52
|
+
requestedOperation
|
|
53
|
+
});
|
|
54
|
+
await (0, _tracker.trackEvent)('rechallenge_session_created', {
|
|
55
|
+
scope: requestedOperation
|
|
56
|
+
});
|
|
57
|
+
const verificationUrl = session.verificationUrl;
|
|
58
|
+
const expiresIso = session.expiresAt;
|
|
59
|
+
if (interactive) {
|
|
60
|
+
await (0, _openBrowser.openBrowser)(verificationUrl);
|
|
61
|
+
console.warn(_chalk.default.yellow('⚠'), `Step-up verification required for ${_chalk.default.bold(requestedOperation)}.`);
|
|
62
|
+
console.warn(` Open ${_chalk.default.cyan(verificationUrl)}`);
|
|
63
|
+
console.warn(` If your browser did not open, copy and paste the URL above. Expires at ${expiresIso}.`);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(`Step-up verification required for ${requestedOperation}. ` + `Complete it at: ${verificationUrl} (expires at ${expiresIso}).`);
|
|
66
|
+
}
|
|
67
|
+
const requestedInterval = Number(session.pollIntervalSeconds);
|
|
68
|
+
const interval = Math.max(Number.isFinite(requestedInterval) ? requestedInterval : MIN_POLL_INTERVAL_SECONDS, MIN_POLL_INTERVAL_SECONDS) * 1000;
|
|
69
|
+
const deadline = Date.parse(session.expiresAt);
|
|
70
|
+
if (Number.isNaN(deadline)) {
|
|
71
|
+
throw new _errors.RechallengeTerminalError('expired', requestedOperation, 'server returned unparseable expiresAt');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* eslint-disable no-await-in-loop -- polling loop; each iteration must complete before the next */
|
|
75
|
+
while (true) {
|
|
76
|
+
if (signal?.aborted) {
|
|
77
|
+
throw new _errors.RechallengeAbortedError(requestedOperation);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await (0, _promises.setTimeout)(interval, undefined, {
|
|
81
|
+
signal
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
throw new _errors.RechallengeAbortedError(requestedOperation);
|
|
85
|
+
}
|
|
86
|
+
if (!Number.isNaN(deadline) && Date.now() > deadline) {
|
|
87
|
+
throw new _errors.RechallengeTerminalError('expired', requestedOperation, 'session window elapsed before completion');
|
|
88
|
+
}
|
|
89
|
+
const status = await client.getSessionStatus({
|
|
90
|
+
template: rechallenge.statusPathTemplate,
|
|
91
|
+
challengeId: session.challengeId,
|
|
92
|
+
scope: requestedOperation
|
|
93
|
+
});
|
|
94
|
+
if (!TERMINAL.has(status.status)) {
|
|
95
|
+
debug('still %s; polling again', status.status);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (status.status === 'verified') {
|
|
99
|
+
await (0, _tracker.trackEvent)('rechallenge_verified', {
|
|
100
|
+
scope: requestedOperation,
|
|
101
|
+
provider: status.provider ?? 'unknown'
|
|
102
|
+
});
|
|
103
|
+
const {
|
|
104
|
+
elevatedToken
|
|
105
|
+
} = await client.exchange({
|
|
106
|
+
template: rechallenge.exchangePathTemplate,
|
|
107
|
+
challengeId: session.challengeId,
|
|
108
|
+
scope: requestedOperation
|
|
109
|
+
});
|
|
110
|
+
await (0, _tracker.trackEvent)('rechallenge_exchanged', {
|
|
111
|
+
scope: requestedOperation
|
|
112
|
+
});
|
|
113
|
+
await _tokenCache.default.set(requestedOperation, {
|
|
114
|
+
...elevatedToken,
|
|
115
|
+
headerName: rechallenge.elevatedHeaderName
|
|
116
|
+
});
|
|
117
|
+
return elevatedToken;
|
|
118
|
+
}
|
|
119
|
+
await (0, _tracker.trackEvent)(`rechallenge_${status.status}`, {
|
|
120
|
+
scope: requestedOperation
|
|
121
|
+
});
|
|
122
|
+
throw new _errors.RechallengeTerminalError(status.status, requestedOperation, status.statusReason?.message);
|
|
123
|
+
}
|
|
124
|
+
/* eslint-enable no-await-in-loop */
|
|
125
|
+
}
|
|
126
|
+
function isInteractiveContext(argvOrFlags = process.argv) {
|
|
127
|
+
if (process.env.VIP_NON_INTERACTIVE === '1') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (argvOrFlags.some(item => item === '--non-interactive' || item.startsWith('--non-interactive=') && !/^--non-interactive=(0|false|no|off)$/i.test(item))) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return Boolean(process.stdout.isTTY);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Opt-in for waiting on step-up in a non-interactive session: the operator can
|
|
137
|
+
// complete browser verification on another device while the command polls.
|
|
138
|
+
// Mirrors isInteractiveContext's argv/env detection because the rechallenge link
|
|
139
|
+
// is instantiated globally and cannot read a command's parsed options.
|
|
140
|
+
function shouldWaitForRechallenge(argvOrFlags = process.argv) {
|
|
141
|
+
if (process.env.VIP_RECHALLENGE_WAIT === '1') {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return argvOrFlags.some(item => item === '--rechallenge-wait' || item.startsWith('--rechallenge-wait=') && !/^--rechallenge-wait=(0|false|no|off)$/i.test(item));
|
|
145
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
var _exportNames = {
|
|
5
|
+
rechallengeLink: true,
|
|
6
|
+
tokenCache: true
|
|
7
|
+
};
|
|
8
|
+
exports.tokenCache = exports.rechallengeLink = void 0;
|
|
9
|
+
var _link = _interopRequireDefault(require("./link"));
|
|
10
|
+
exports.rechallengeLink = _link.default;
|
|
11
|
+
var _tokenCache = _interopRequireDefault(require("./token-cache"));
|
|
12
|
+
exports.tokenCache = _tokenCache.default;
|
|
13
|
+
var _types = require("./types");
|
|
14
|
+
Object.keys(_types).forEach(function (key) {
|
|
15
|
+
if (key === "default" || key === "__esModule") return;
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
17
|
+
if (key in exports && exports[key] === _types[key]) return;
|
|
18
|
+
exports[key] = _types[key];
|
|
19
|
+
});
|
|
20
|
+
var _errors = require("./errors");
|
|
21
|
+
Object.keys(_errors).forEach(function (key) {
|
|
22
|
+
if (key === "default" || key === "__esModule") return;
|
|
23
|
+
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
|
24
|
+
if (key in exports && exports[key] === _errors[key]) return;
|
|
25
|
+
exports[key] = _errors[key];
|
|
26
|
+
});
|
|
27
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = createRechallengeLink;
|
|
5
|
+
var _core = require("@apollo/client/core");
|
|
6
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
7
|
+
var _graphql = require("graphql");
|
|
8
|
+
var _flow = require("./flow");
|
|
9
|
+
var _tokenCache = _interopRequireDefault(require("./token-cache"));
|
|
10
|
+
var _types = require("./types");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
const debug = (0, _debug.default)('@automattic/vip:rechallenge:link');
|
|
13
|
+
function operationDefinition(doc) {
|
|
14
|
+
return doc.definitions.find(def => def.kind === _graphql.Kind.OPERATION_DEFINITION);
|
|
15
|
+
}
|
|
16
|
+
function isMutation(doc) {
|
|
17
|
+
return operationDefinition(doc)?.operation === _graphql.OperationTypeNode.MUTATION;
|
|
18
|
+
}
|
|
19
|
+
function primaryMutationFieldName(doc) {
|
|
20
|
+
const op = operationDefinition(doc);
|
|
21
|
+
if (!op || op.operation !== _graphql.OperationTypeNode.MUTATION) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const first = op.selectionSet.selections.find(sel => sel.kind === _graphql.Kind.FIELD);
|
|
25
|
+
return first?.name.value ?? null;
|
|
26
|
+
}
|
|
27
|
+
function extractElevatedPermission(result) {
|
|
28
|
+
const errors = result.errors ?? [];
|
|
29
|
+
for (const err of errors) {
|
|
30
|
+
const ext = err.extensions ?? {};
|
|
31
|
+
if (ext.code !== _types.ELEVATED_PERMISSION_ERROR_CODE) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const rechallenge = ext.rechallenge;
|
|
35
|
+
if (rechallenge && typeof rechallenge.createSessionPath === 'string' && typeof rechallenge.statusPathTemplate === 'string' && typeof rechallenge.exchangePathTemplate === 'string' && typeof rechallenge.elevatedHeaderName === 'string') {
|
|
36
|
+
return {
|
|
37
|
+
rechallenge
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function attachElevatedHeader(operation, headerName, token) {
|
|
44
|
+
const ctx = operation.getContext();
|
|
45
|
+
const headers = {
|
|
46
|
+
...ctx.headers
|
|
47
|
+
};
|
|
48
|
+
headers[headerName] = token.token;
|
|
49
|
+
operation.setContext({
|
|
50
|
+
...ctx,
|
|
51
|
+
headers
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const DEFAULT_HEADER = 'x-elevated-token';
|
|
55
|
+
function createRechallengeLink() {
|
|
56
|
+
return new _core.ApolloLink((operation, forward) => {
|
|
57
|
+
const scope = primaryMutationFieldName(operation.query);
|
|
58
|
+
const eligible = isMutation(operation.query) && Boolean(scope);
|
|
59
|
+
return new _core.Observable(observer => {
|
|
60
|
+
let retrying = false;
|
|
61
|
+
let cancelled = false;
|
|
62
|
+
let firstSub = null;
|
|
63
|
+
let retrySub = null;
|
|
64
|
+
const abortController = new AbortController();
|
|
65
|
+
const preflight = async () => {
|
|
66
|
+
if (!eligible || !scope) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const cached = await _tokenCache.default.get(scope);
|
|
70
|
+
if (cached) {
|
|
71
|
+
attachElevatedHeader(operation, cached.headerName || DEFAULT_HEADER, cached);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const handleRetry = async (result, elevated) => {
|
|
75
|
+
retrying = true;
|
|
76
|
+
const headerName = elevated.rechallenge.elevatedHeaderName || DEFAULT_HEADER;
|
|
77
|
+
let token;
|
|
78
|
+
try {
|
|
79
|
+
token = await (0, _flow.runRechallenge)({
|
|
80
|
+
requestedOperation: scope,
|
|
81
|
+
rechallenge: elevated.rechallenge,
|
|
82
|
+
interactive: (0, _flow.isInteractiveContext)(),
|
|
83
|
+
wait: (0, _flow.shouldWaitForRechallenge)(),
|
|
84
|
+
signal: abortController.signal
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
debug('rechallenge flow failed: %o', err);
|
|
88
|
+
if (cancelled || observer.closed) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Surface the original elevated-permission error to upstream
|
|
92
|
+
// so errorLink and consumers see it.
|
|
93
|
+
observer.next(result);
|
|
94
|
+
observer.complete();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (cancelled || observer.closed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
attachElevatedHeader(operation, headerName, token);
|
|
101
|
+
try {
|
|
102
|
+
retrySub = forward(operation).subscribe({
|
|
103
|
+
next: res => observer.next(res),
|
|
104
|
+
error: err => observer.error(err),
|
|
105
|
+
complete: () => observer.complete()
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (cancelled || observer.closed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
observer.error(err);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const start = async () => {
|
|
115
|
+
try {
|
|
116
|
+
await preflight();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
debug('preflight error: %o', err);
|
|
119
|
+
}
|
|
120
|
+
if (cancelled || observer.closed) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
firstSub = forward(operation).subscribe({
|
|
125
|
+
next: result => {
|
|
126
|
+
if (retrying || !eligible || !scope) {
|
|
127
|
+
observer.next(result);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const elevated = extractElevatedPermission(result);
|
|
131
|
+
if (!elevated) {
|
|
132
|
+
observer.next(result);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
void handleRetry(result, elevated);
|
|
136
|
+
},
|
|
137
|
+
error: err => observer.error(err),
|
|
138
|
+
complete: () => {
|
|
139
|
+
if (!retrying) {
|
|
140
|
+
observer.complete();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (cancelled || observer.closed) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
observer.error(err);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
void start().catch(err => {
|
|
152
|
+
if (cancelled || observer.closed) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
observer.error(err);
|
|
156
|
+
});
|
|
157
|
+
return () => {
|
|
158
|
+
cancelled = true;
|
|
159
|
+
// Abort any in-flight rechallenge polling so the loop, its tracking
|
|
160
|
+
// events, and the token-cache write stop instead of running to expiry.
|
|
161
|
+
abortController.abort();
|
|
162
|
+
firstSub?.unsubscribe();
|
|
163
|
+
retrySub?.unsubscribe();
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.openBrowser = openBrowser;
|
|
5
|
+
var _debug = _interopRequireDefault(require("debug"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
const debug = (0, _debug.default)('@automattic/vip:rechallenge:open-browser');
|
|
8
|
+
|
|
9
|
+
/** Opens a URL in the default browser. Wraps the ESM-only `open` package. */
|
|
10
|
+
async function openBrowser(url) {
|
|
11
|
+
try {
|
|
12
|
+
const {
|
|
13
|
+
default: open
|
|
14
|
+
} = await import('open');
|
|
15
|
+
await open(url, {
|
|
16
|
+
wait: false
|
|
17
|
+
});
|
|
18
|
+
} catch (err) {
|
|
19
|
+
debug('Failed to open(%s): %o', url, err);
|
|
20
|
+
}
|
|
21
|
+
}
|