@jacebenson/jsn 4.2.3 → 4.2.4
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/package.json +1 -1
- package/src/auth.js +29 -0
- package/src/commands/auth.js +66 -4
- package/src/sdk.js +4 -0
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -271,6 +271,25 @@ function askHidden(promptText) {
|
|
|
271
271
|
return passwordPrompt({ message: promptText });
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Extract the browser session token and cookies from pasted DevTools headers
|
|
276
|
+
* or a "Copy as cURL" command. Only these two values are retained.
|
|
277
|
+
*/
|
|
278
|
+
export function parseBrowserSessionInput(input) {
|
|
279
|
+
const text = String(input || '');
|
|
280
|
+
const tokenMatch = text.match(/(?:x-usertoken|x-user-token)\s*:\s*([^\s'";,]+)/i);
|
|
281
|
+
const cookieArgMatch = text.match(/(?:^|\s)(?:-b|--cookie)\s+(['"])(.*?)\1/s);
|
|
282
|
+
const quotedCookieMatch = text.match(/cookie\s*:\s*(['"])(.*?)\1/i);
|
|
283
|
+
const plainCookieMatch = text.match(/cookie\s*:\s*([^\r\n]+)/i);
|
|
284
|
+
const token = tokenMatch?.[1]?.trim() || '';
|
|
285
|
+
const cookies = (cookieArgMatch?.[2] || quotedCookieMatch?.[2] || plainCookieMatch?.[1] || '')
|
|
286
|
+
.trim()
|
|
287
|
+
.replace(/['"`]\s*(?:\\\s*)?$/, '');
|
|
288
|
+
if (!token) throw errAuth('No X-UserToken header found');
|
|
289
|
+
if (!cookies) throw errAuth('No Cookie header found. Paste the complete request headers or cURL command.');
|
|
290
|
+
return { auth_method: 'gck', access_token: token, cookies, auth_source: 'gck' };
|
|
291
|
+
}
|
|
292
|
+
|
|
274
293
|
export class AuthManager {
|
|
275
294
|
/**
|
|
276
295
|
* @param {object} identity — the resolved-identity provider. Production
|
|
@@ -369,6 +388,9 @@ export class AuthManager {
|
|
|
369
388
|
if (creds.auth_method === 'basic') {
|
|
370
389
|
return Boolean(creds.username && creds.password);
|
|
371
390
|
}
|
|
391
|
+
if (creds.auth_method === 'gck') {
|
|
392
|
+
return Boolean(creds.access_token && creds.cookies);
|
|
393
|
+
}
|
|
372
394
|
if (creds.expires_at && Date.now() >= creds.expires_at * 1000) return false;
|
|
373
395
|
return !!creds.access_token;
|
|
374
396
|
}
|
|
@@ -510,6 +532,13 @@ export class AuthManager {
|
|
|
510
532
|
* Complete login using an authorization code obtained from a prior buildAuthURL() call.
|
|
511
533
|
* The PKCE state must have been saved by an earlier buildAuthURL() call.
|
|
512
534
|
*/
|
|
535
|
+
async loginWithGck(instanceURL, input, username = this._activeUsername()) {
|
|
536
|
+
instanceURL = normalizeInstanceURL(instanceURL);
|
|
537
|
+
const creds = parseBrowserSessionInput(input);
|
|
538
|
+
saveCredentials(instanceURL, { ...creds, username: username || undefined }, username || undefined);
|
|
539
|
+
return { ...creds, username: username || undefined };
|
|
540
|
+
}
|
|
541
|
+
|
|
513
542
|
async loginWithCode(instanceURL, code) {
|
|
514
543
|
instanceURL = normalizeInstanceURL(instanceURL);
|
|
515
544
|
const clientID = getOAuthClientID();
|
package/src/commands/auth.js
CHANGED
|
@@ -94,6 +94,13 @@ export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '') {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '') {
|
|
98
|
+
if (argv.gck || argv.headers) return true;
|
|
99
|
+
return Object.values(cfg.profiles || {}).some(profile =>
|
|
100
|
+
profile.instance_url === instanceURL && profile.auth_method === 'gck'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
/**
|
|
98
105
|
* Pick a profile by name — interactive search picker when ambiguous,
|
|
99
106
|
* auto-selects when there's exactly one, throws when none configured.
|
|
@@ -129,6 +136,46 @@ export function resolveWizardInstance(app, argv = {}) {
|
|
|
129
136
|
return process.env.SERVICENOW_INSTANCE_URL || app._overrideInstance || argv.instance || '';
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
export async function readBrowserSessionInput() {
|
|
140
|
+
const stdin = process.stdin;
|
|
141
|
+
console.log('Paste the complete cURL command, then press Ctrl+D:');
|
|
142
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
143
|
+
const chunks = [];
|
|
144
|
+
for await (const chunk of stdin) chunks.push(chunk);
|
|
145
|
+
return Buffer.concat(chunks).toString();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
let pasted = '';
|
|
150
|
+
const finish = () => {
|
|
151
|
+
stdin.setRawMode(false);
|
|
152
|
+
stdin.pause();
|
|
153
|
+
stdin.removeListener('data', onData);
|
|
154
|
+
stdin.removeListener('error', onError);
|
|
155
|
+
process.stdout.write('\n');
|
|
156
|
+
};
|
|
157
|
+
const onError = (err) => { finish(); reject(err); };
|
|
158
|
+
const onData = (chunk) => {
|
|
159
|
+
const text = chunk.toString();
|
|
160
|
+
const eof = text.indexOf('\x04');
|
|
161
|
+
if (eof !== -1) {
|
|
162
|
+
pasted += text.slice(0, eof);
|
|
163
|
+
finish();
|
|
164
|
+
resolve(pasted);
|
|
165
|
+
} else if (text.includes('\x03')) {
|
|
166
|
+
finish();
|
|
167
|
+
reject(new Error('Browser session input cancelled'));
|
|
168
|
+
} else {
|
|
169
|
+
pasted += text;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
stdin.setRawMode(true);
|
|
173
|
+
stdin.resume();
|
|
174
|
+
stdin.on('data', onData);
|
|
175
|
+
stdin.on('error', onError);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
132
179
|
/**
|
|
133
180
|
* Interactive first-run wizard (formerly `jsn setup`).
|
|
134
181
|
*
|
|
@@ -139,7 +186,7 @@ export function resolveWizardInstance(app, argv = {}) {
|
|
|
139
186
|
*/
|
|
140
187
|
export async function loginWizard(app, argv = {}) {
|
|
141
188
|
const readline = (await import('node:readline')).default;
|
|
142
|
-
|
|
189
|
+
let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
143
190
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
144
191
|
|
|
145
192
|
console.log('Welcome to JSN - ServiceNow CLI');
|
|
@@ -174,8 +221,9 @@ export async function loginWizard(app, argv = {}) {
|
|
|
174
221
|
profile.skip_confirmations = true;
|
|
175
222
|
}
|
|
176
223
|
|
|
177
|
-
const authMethod = await ask('Authentication method (OAuth/Basic) [OAuth]: ');
|
|
178
|
-
const
|
|
224
|
+
const authMethod = await ask('Authentication method (OAuth/Basic/Browser session) [OAuth]: ');
|
|
225
|
+
const useGck = authMethod.toLowerCase().startsWith('g') || authMethod.toLowerCase().startsWith('browser');
|
|
226
|
+
const useBasic = !useGck && authMethod.toLowerCase().startsWith('b');
|
|
179
227
|
|
|
180
228
|
let loggedIn = false;
|
|
181
229
|
if (useBasic) {
|
|
@@ -192,6 +240,12 @@ export async function loginWizard(app, argv = {}) {
|
|
|
192
240
|
}
|
|
193
241
|
profile.username = username;
|
|
194
242
|
profile.auth_method = 'basic';
|
|
243
|
+
} else if (useGck) {
|
|
244
|
+
rl.close();
|
|
245
|
+
const input = await readBrowserSessionInput();
|
|
246
|
+
await app.auth.loginWithGck(instance, input);
|
|
247
|
+
profile.auth_method = 'gck';
|
|
248
|
+
loggedIn = true;
|
|
195
249
|
} else {
|
|
196
250
|
profile.auth_method = 'oauth';
|
|
197
251
|
}
|
|
@@ -205,6 +259,11 @@ export async function loginWizard(app, argv = {}) {
|
|
|
205
259
|
// so a piped/scripted call never blocks on prompts.
|
|
206
260
|
const { canPrompt } = await import('../helpers.js');
|
|
207
261
|
if (canPrompt()) {
|
|
262
|
+
// @inquirer/prompts owns stdin while these confirmations run. Close the
|
|
263
|
+
// older readline interface before handing stdin to Inquirer. This matters
|
|
264
|
+
// on Windows Git Bash: leaving both readers attached can make the later
|
|
265
|
+
// OAuth question see EOF and let Node exit instead of waiting for input.
|
|
266
|
+
rl.close();
|
|
208
267
|
const { confirm } = await import('@inquirer/prompts');
|
|
209
268
|
profile.read_only = await confirm({ message: 'Read-only profile (blocks mutation commands)?', default: profile.read_only || false });
|
|
210
269
|
profile.skip_confirmations = await confirm({ message: 'Skip confirmations (deletes run without prompting)?', default: profile.skip_confirmations || false });
|
|
@@ -214,7 +273,10 @@ export async function loginWizard(app, argv = {}) {
|
|
|
214
273
|
|
|
215
274
|
await saveConfig(app.config);
|
|
216
275
|
|
|
217
|
-
if (!useBasic) {
|
|
276
|
+
if (!useBasic && !useGck) {
|
|
277
|
+
// Recreate readline after Inquirer has released stdin. Do not reuse the
|
|
278
|
+
// interface that was open before the confirmation prompts.
|
|
279
|
+
rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
218
280
|
const loginNow = await ask('Login now? [Y/n]: ');
|
|
219
281
|
rl.close();
|
|
220
282
|
if (!loginNow || loginNow.toLowerCase() !== 'n') {
|
package/src/sdk.js
CHANGED
|
@@ -47,6 +47,10 @@ export class SDKClient {
|
|
|
47
47
|
case 'oauth':
|
|
48
48
|
req.headers.set('Authorization', `Bearer ${creds.access_token}`);
|
|
49
49
|
break;
|
|
50
|
+
case 'gck':
|
|
51
|
+
req.headers.set('X-UserToken', creds.access_token);
|
|
52
|
+
if (creds.cookies) req.headers.set('Cookie', creds.cookies);
|
|
53
|
+
break;
|
|
50
54
|
default:
|
|
51
55
|
if (creds.username && creds.password) {
|
|
52
56
|
req.headers.set('Authorization', 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64'));
|