@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.2.3",
3
+ "version": "4.2.4",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
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();
@@ -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
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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 useBasic = authMethod.toLowerCase().startsWith('b');
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'));