@jacebenson/jsn 4.2.3 → 4.3.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/src/cli.js CHANGED
@@ -30,7 +30,7 @@ import { groupMembersCmd } from './commands/groupmembers.js';
30
30
  import { groupRolesCmd } from './commands/grouproles.js';
31
31
  import { ticketsCmd } from './commands/tickets.js';
32
32
  import { transactionsCmd } from './commands/transactions.js';
33
- import { instanceCmd } from './commands/instance.js';
33
+
34
34
  import { platformCmd } from './commands/platform.js';
35
35
  import { codesearchCmd } from './commands/codesearch.js';
36
36
  import { versionCmd } from './commands/version.js';
@@ -51,11 +51,12 @@ import {
51
51
  catalogscriptsCmd, cataloguipoliciesCmd,
52
52
  uipoliciesCmd,
53
53
  scriptactionsCmd, scheduledjobsCmd, asyncrulesCmd, triggersCmd,
54
- decisiontablesCmd, assignmentsCmd,
54
+ assignmentsCmd,
55
55
  emailCmd,
56
56
  restmessageCmd, soapmessagesCmd,
57
57
  uimacrosCmd, uxlistsCmd, uxapplicabilityCmd,
58
58
  } from './commands/dev/_simple.js';
59
+ import { decisiontablesCmd } from './commands/decisiontables.js';
59
60
  import { flowsCmd } from './commands/dev/flows.js';
60
61
  import { formsCmd } from './commands/dev/forms.js';
61
62
  import { listsCmd } from './commands/dev/lists.js';
@@ -69,6 +70,7 @@ import { scrapiCmd } from './commands/dev/scrapi.js';
69
70
  import { b4rulesCmd } from './commands/dev/b4rules.js';
70
71
  import { atfCmd } from './commands/atf.js';
71
72
  import { approvalsCmd } from './commands/approvals.js';
73
+ import { perfCmd } from './commands/perf.js';
72
74
 
73
75
  function wrap(handler) {
74
76
  return async (argv) => {
@@ -167,15 +169,19 @@ export function buildCLI() {
167
169
  // / app._overrideInstance change — no more argv._overrideInstance mirror
168
170
  // or duplicated --instance branches across middlewares.
169
171
  (argv) => {
170
- const session = resolveSession(argv, cfg);
171
- if (session.unknownProfile) {
172
- guardExit(argv, {
173
- code: 'unknown_profile',
174
- message: `Unknown profile "${session.unknownProfile}"`,
175
- hint: 'Run "jsn auth status" to list profiles.',
176
- });
172
+ try {
173
+ const session = resolveSession(argv, cfg);
174
+ if (session.unknownProfile) {
175
+ guardExit(argv, {
176
+ code: 'unknown_profile',
177
+ message: `Unknown profile "${session.unknownProfile}"`,
178
+ hint: 'Run "jsn auth status" to list profiles.',
179
+ });
180
+ }
181
+ applySession(app, session);
182
+ } catch (err) {
183
+ guardExit(argv, { code: err.code || 'usage', message: err.message, hint: err.hint });
177
184
  }
178
- applySession(app, session);
179
185
  },
180
186
  // Guard mutation commands: require an instance, block read-only profiles
181
187
  (argv) => {
@@ -358,7 +364,7 @@ export function buildCLI() {
358
364
  .command(cmdbCmd(wrap))
359
365
  .command(recordsCmd(wrap))
360
366
  .command(transactionsCmd(wrap))
361
- .command(instanceCmd(wrap))
367
+
362
368
  .command(platformCmd(wrap))
363
369
  .command(snippetsCmd(wrap))
364
370
  .command(ticketsCmd(wrap))
@@ -366,6 +372,7 @@ export function buildCLI() {
366
372
  // DEVELOPER
367
373
  .command(atfCmd(wrap))
368
374
  .command(approvalsCmd(wrap))
375
+ .command(perfCmd(wrap))
369
376
  .command(evalCmd(wrap))
370
377
  .command(restCmd(wrap))
371
378
  .command(skillCmd(wrap))
@@ -82,18 +82,51 @@ export function resolveInstanceArg(arg, cfg) {
82
82
  return normalizeInstanceURL(`${trimmed}.service-now.com`);
83
83
  }
84
84
 
85
+ /** Resolve a command target by explicit profile name before instance fallback. */
86
+ export function resolveTargetProfile(argv = {}, cfg = {}, instanceURL = '') {
87
+ if (argv.profile && cfg.profiles?.[argv.profile]) {
88
+ return { name: argv.profile, profile: cfg.profiles[argv.profile] };
89
+ }
90
+ const entry = Object.entries(cfg.profiles || {})
91
+ .find(([, profile]) => normalizeInstanceURL(profile.instance_url) === instanceURL);
92
+ return entry ? { name: entry[0], profile: entry[1] } : { name: null, profile: null };
93
+ }
94
+
85
95
  /**
86
96
  * Select the login method. Explicit Basic Auth wins; otherwise reuse the
87
97
  * method recorded on an existing profile. New instances remain OAuth by
88
98
  * default.
89
99
  */
90
- export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '') {
100
+ export function shouldUseBasicAuth(argv = {}, cfg = {}, instanceURL = '', targetProfile = null) {
91
101
  if (argv.basic || argv.password) return true;
102
+ if (targetProfile) return targetProfile.auth_method === 'basic';
92
103
  return Object.values(cfg.profiles || {}).some(profile =>
93
104
  profile.instance_url === instanceURL && profile.auth_method === 'basic'
94
105
  );
95
106
  }
96
107
 
108
+ export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '', targetProfile = null) {
109
+ if (argv.gck || argv.headers) return true;
110
+ if (targetProfile) return targetProfile.auth_method === 'gck';
111
+ return Object.values(cfg.profiles || {}).some(profile =>
112
+ profile.instance_url === instanceURL && profile.auth_method === 'gck'
113
+ );
114
+ }
115
+
116
+ export function validateLoginSelectors(argv = {}, targetProfile = null) {
117
+ const basic = argv.basic === true || argv.password === true;
118
+ const gck = argv.gck === true || argv.headers !== undefined;
119
+ if (basic && gck) {
120
+ throw errUsage('Basic and browser-session selectors are mutually exclusive; choose --basic/--password or --gck/--headers.');
121
+ }
122
+ if (basic && targetProfile?.auth_method === 'gck') {
123
+ throw errUsage('This profile is configured for gck authentication; use --gck/--headers or change the profile method.');
124
+ }
125
+ if (gck && targetProfile?.auth_method === 'basic') {
126
+ throw errUsage('This profile is configured for basic authentication; use --basic/--password or change the profile method.');
127
+ }
128
+ }
129
+
97
130
  /**
98
131
  * Pick a profile by name — interactive search picker when ambiguous,
99
132
  * auto-selects when there's exactly one, throws when none configured.
@@ -129,6 +162,46 @@ export function resolveWizardInstance(app, argv = {}) {
129
162
  return process.env.SERVICENOW_INSTANCE_URL || app._overrideInstance || argv.instance || '';
130
163
  }
131
164
 
165
+ export async function readBrowserSessionInput() {
166
+ const stdin = process.stdin;
167
+ console.log('Paste the complete cURL command, then press Ctrl+D:');
168
+ if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
169
+ const chunks = [];
170
+ for await (const chunk of stdin) chunks.push(chunk);
171
+ return Buffer.concat(chunks).toString();
172
+ }
173
+
174
+ return new Promise((resolve, reject) => {
175
+ let pasted = '';
176
+ const finish = () => {
177
+ stdin.setRawMode(false);
178
+ stdin.pause();
179
+ stdin.removeListener('data', onData);
180
+ stdin.removeListener('error', onError);
181
+ process.stdout.write('\n');
182
+ };
183
+ const onError = (err) => { finish(); reject(err); };
184
+ const onData = (chunk) => {
185
+ const text = chunk.toString();
186
+ const eof = text.indexOf('\x04');
187
+ if (eof !== -1) {
188
+ pasted += text.slice(0, eof);
189
+ finish();
190
+ resolve(pasted);
191
+ } else if (text.includes('\x03')) {
192
+ finish();
193
+ reject(new Error('Browser session input cancelled'));
194
+ } else {
195
+ pasted += text;
196
+ }
197
+ };
198
+ stdin.setRawMode(true);
199
+ stdin.resume();
200
+ stdin.on('data', onData);
201
+ stdin.on('error', onError);
202
+ });
203
+ }
204
+
132
205
  /**
133
206
  * Interactive first-run wizard (formerly `jsn setup`).
134
207
  *
@@ -139,7 +212,7 @@ export function resolveWizardInstance(app, argv = {}) {
139
212
  */
140
213
  export async function loginWizard(app, argv = {}) {
141
214
  const readline = (await import('node:readline')).default;
142
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
215
+ let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
216
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
144
217
 
145
218
  console.log('Welcome to JSN - ServiceNow CLI');
@@ -174,24 +247,31 @@ export async function loginWizard(app, argv = {}) {
174
247
  profile.skip_confirmations = true;
175
248
  }
176
249
 
177
- const authMethod = await ask('Authentication method (OAuth/Basic) [OAuth]: ');
178
- const useBasic = authMethod.toLowerCase().startsWith('b');
250
+ const authMethod = await ask('Authentication method (OAuth/Basic/Browser session) [OAuth]: ');
251
+ const useGck = authMethod.toLowerCase().startsWith('g') || authMethod.toLowerCase().startsWith('browser');
252
+ const useBasic = !useGck && authMethod.toLowerCase().startsWith('b');
179
253
 
180
254
  let loggedIn = false;
181
255
  if (useBasic) {
182
256
  const username = await ask('Username: ');
183
257
  rl.close(); // Close outer readline before askHidden creates its own
184
- const { loadCredentials, askHidden, saveCredentials } = await import('../auth.js');
185
- const existingCreds = loadCredentials(instance) || loadCredentials(instance, username);
258
+ const { askHidden } = await import('../auth.js');
259
+ const existingCreds = app.auth.loadCredentials(instance) || app.auth.loadCredentials(instance, username);
186
260
  if (existingCreds && existingCreds.auth_method === 'basic') {
187
261
  console.log(`Using existing credentials for ${instance}`);
188
262
  } else {
189
263
  const password = await askHidden('Password: ');
190
- saveCredentials(instance, { auth_method: 'basic', username, password }, username);
264
+ app.auth.saveCredentials(instance, { auth_method: 'basic', username, password }, username);
191
265
  console.log('Basic auth credentials saved');
192
266
  }
193
267
  profile.username = username;
194
268
  profile.auth_method = 'basic';
269
+ } else if (useGck) {
270
+ rl.close();
271
+ const input = await readBrowserSessionInput();
272
+ await app.auth.loginWithGck(instance, input);
273
+ profile.auth_method = 'gck';
274
+ loggedIn = true;
195
275
  } else {
196
276
  profile.auth_method = 'oauth';
197
277
  }
@@ -205,6 +285,11 @@ export async function loginWizard(app, argv = {}) {
205
285
  // so a piped/scripted call never blocks on prompts.
206
286
  const { canPrompt } = await import('../helpers.js');
207
287
  if (canPrompt()) {
288
+ // @inquirer/prompts owns stdin while these confirmations run. Close the
289
+ // older readline interface before handing stdin to Inquirer. This matters
290
+ // on Windows Git Bash: leaving both readers attached can make the later
291
+ // OAuth question see EOF and let Node exit instead of waiting for input.
292
+ rl.close();
208
293
  const { confirm } = await import('@inquirer/prompts');
209
294
  profile.read_only = await confirm({ message: 'Read-only profile (blocks mutation commands)?', default: profile.read_only || false });
210
295
  profile.skip_confirmations = await confirm({ message: 'Skip confirmations (deletes run without prompting)?', default: profile.skip_confirmations || false });
@@ -214,23 +299,22 @@ export async function loginWizard(app, argv = {}) {
214
299
 
215
300
  await saveConfig(app.config);
216
301
 
217
- if (!useBasic) {
302
+ if (!useBasic && !useGck) {
303
+ // Recreate readline after Inquirer has released stdin. Do not reuse the
304
+ // interface that was open before the confirmation prompts.
305
+ rl = readline.createInterface({ input: process.stdin, output: process.stdout });
218
306
  const loginNow = await ask('Login now? [Y/n]: ');
219
307
  rl.close();
220
308
  if (!loginNow || loginNow.toLowerCase() !== 'n') {
221
309
  await app.auth.login(instance);
222
310
  // Re-save credentials under <user>@<instance> after verifying username
223
311
  try {
224
- const { SDKClient } = await import('../sdk.js');
225
- const sdk = new SDKClient(instance, app.auth);
312
+ const sdk = app.getSDKForProfile
313
+ ? app.getSDKForProfile(instance, { authMethod: 'oauth' })
314
+ : app.sdk;
226
315
  const user = await sdk.getCurrentUser();
227
316
  if (user && user.user_name) {
228
- const { loadCredentials, saveCredentials } = await import('../auth.js');
229
- const stored = loadCredentials(instance);
230
- if (stored && stored.access_token) {
231
- stored.username = user.user_name;
232
- saveCredentials(instance, stored, user.user_name);
233
- }
317
+ app.auth.migrateLegacyCredential(instance, user.user_name);
234
318
  profile.username = user.user_name;
235
319
  }
236
320
  } catch {
@@ -242,9 +326,14 @@ export async function loginWizard(app, argv = {}) {
242
326
  // Probe domain separation capability once and stamp the profile, so
243
327
  // `jsn domains` (and its help entry) only appear on instances with it.
244
328
  try {
245
- const { SDKClient } = await import('../sdk.js');
246
329
  const domainsMod = await import('./dev/domains.js');
247
- const sdk = new SDKClient(instance, app.auth);
330
+ const sdk = app.getSDKForProfile
331
+ ? app.getSDKForProfile(instance, {
332
+ profile,
333
+ authMethod: profile.auth_method || 'oauth',
334
+ username: profile.username,
335
+ })
336
+ : app.sdk;
248
337
  profile.domain_separation = await domainsMod.isDomainSeparationInstalled({ sdk });
249
338
  await setProfile(app.config, profileName, profile);
250
339
 
@@ -348,8 +437,9 @@ export async function modifyProfile(app, argv = {}) {
348
437
  return;
349
438
  }
350
439
  const { isDomainSeparationInstalled } = await import('./dev/domains.js');
351
- const { SDKClient } = await import('../sdk.js');
352
- const sdk = new SDKClient(profile.instance_url, app.auth);
440
+ const sdk = app.getSDKForProfile
441
+ ? app.getSDKForProfile(profile.instance_url, { profile, authMethod: profile.auth_method, username: profile.username })
442
+ : app.sdk;
353
443
  if (!(await isDomainSeparationInstalled({ sdk }))) {
354
444
  throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
355
445
  }
@@ -374,8 +464,9 @@ export async function modifyProfile(app, argv = {}) {
374
464
  value: 'domain',
375
465
  });
376
466
  }
377
- const { SDKClient } = await import('../sdk.js');
378
- const sdk = new SDKClient(profile.instance_url, app.auth);
467
+ const sdk = app.getSDKForProfile
468
+ ? app.getSDKForProfile(profile.instance_url, { profile, authMethod: profile.auth_method, username: profile.username })
469
+ : app.sdk;
379
470
  flag = await select({
380
471
  message: `Modify ${name} (${profile.instance_url})`,
381
472
  choices,
@@ -420,22 +511,38 @@ export async function removeProfile(app, name) {
420
511
  if (!app.config.profiles[name]) {
421
512
  throw new Error(`Profile not found: ${name}`);
422
513
  }
423
- const instance = app.config.profiles[name].instance_url;
514
+ const profile = app.config.profiles[name];
515
+ const instance = profile.instance_url;
516
+ const otherBareProfiles = Object.entries(app.config.profiles)
517
+ .filter(([profileName, other]) => profileName !== name
518
+ && other.instance_url === instance
519
+ && !other.username)
520
+ .length;
424
521
  delete app.config.profiles[name];
425
522
  if (app.config.defaultProfile === name) app.config.defaultProfile = '';
426
523
  if (app.config.activeProfile === name) app.config.activeProfile = '';
427
524
  saveConfig(app.config);
428
525
 
429
- // Only clear credentials if no other profile uses this instance URL
430
- const stillInUse = instance && Object.values(app.config.profiles || {})
431
- .some(p => p.instance_url === instance);
432
- if (instance && !stillInUse) {
433
- app.auth.logout(instance);
526
+ // Delete only this profile's credential identity. Other profiles may share
527
+ // the instance URL but must retain their separate credentials.
528
+ if (instance && (profile.username || otherBareProfiles === 0)) {
529
+ app.auth.logout(instance, profile.username || null);
434
530
  }
435
531
 
436
532
  app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
437
533
  }
438
534
 
535
+ /** Bare credentials are safe to clear only when their target is identifiable. */
536
+ function canLogoutBareProfile(app, instance, target, explicitlySelected) {
537
+ if (target?.username) return true;
538
+ const sameInstance = Object.entries(app.config.profiles || {})
539
+ .filter(([, profile]) => profile.instance_url === instance);
540
+ if (explicitlySelected) {
541
+ return sameInstance.filter(([, profile]) => !profile.username).length === 1;
542
+ }
543
+ return sameInstance.length <= 1;
544
+ }
545
+
439
546
  export function authCmd(wrap) {
440
547
  return {
441
548
  command: 'auth [subcommand]',
@@ -455,6 +562,14 @@ export function authCmd(wrap) {
455
562
  describe: 'Authenticate with Basic Auth via env vars (SN_USERNAME/SN_PASSWORD)',
456
563
  type: 'boolean',
457
564
  })
565
+ .option('gck', {
566
+ describe: 'Authenticate with a browser session from pasted request headers',
567
+ type: 'boolean',
568
+ })
569
+ .option('headers', {
570
+ describe: 'Browser request headers or cURL command for browser-session auth',
571
+ type: 'string',
572
+ })
458
573
  .option('print-url', {
459
574
  describe: 'Print the OAuth URL and exit (saves PKCE state for --code)',
460
575
  type: 'boolean',
@@ -479,7 +594,15 @@ export function authCmd(wrap) {
479
594
  }),
480
595
  handler: wrap(async (argv, app) => {
481
596
  let instanceURL;
482
- if (argv.instance) {
597
+ if (argv.profile) {
598
+ const namedProfile = app.config.profiles?.[argv.profile];
599
+ if (!namedProfile?.instance_url) {
600
+ throw errUsage(`Profile "${argv.profile}" is not configured.`);
601
+ }
602
+ // An explicit profile is authoritative, including in an
603
+ // interactive terminal. Never let the picker retarget it.
604
+ instanceURL = normalizeInstanceURL(namedProfile.instance_url);
605
+ } else if (argv.instance) {
483
606
  instanceURL = resolveInstanceArg(argv.instance, app.config);
484
607
  } else if (app.isInteractive()) {
485
608
  // gh-faithful: interactive terminals always get a picker —
@@ -517,18 +640,28 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
517
640
  }
518
641
  }
519
642
 
520
- const targetProfile = Object.values(app.config.profiles || {})
521
- .find(profile => profile.instance_url === instanceURL);
522
- const targetUsername = targetProfile?.username || '';
523
- const useBasic = shouldUseBasicAuth(argv, app.config, instanceURL);
643
+ const target = resolveTargetProfile(argv, app.config, instanceURL);
644
+ const targetProfile = target.profile;
645
+ validateLoginSelectors(argv, targetProfile);
646
+ let targetUsername = targetProfile?.username || '';
647
+ const useBasic = shouldUseBasicAuth(argv, app.config, instanceURL, targetProfile);
648
+ const useGck = shouldUseGckAuth(argv, app.config, instanceURL, targetProfile);
524
649
  // --basic (with --password as a compatibility alias): authenticate
525
650
  // with basic auth from env vars and persist it for this profile.
526
- if (useBasic) {
527
- await app.auth.loginWithPassword(instanceURL, targetUsername || undefined);
651
+ if (useGck) {
652
+ const input = argv.headers || await readBrowserSessionInput();
653
+ const credentials = await app.auth.loginWithGck(instanceURL, input, targetUsername || undefined);
654
+ targetUsername = credentials?.username || targetUsername;
655
+ } else if (useBasic) {
656
+ const credentials = await app.auth.loginWithPassword(instanceURL, targetUsername || undefined);
657
+ // A new profile has no configured username yet. The auth
658
+ // manager saves env/stored credentials under creds.username;
659
+ // carry that identity into verification and profile creation.
660
+ targetUsername = credentials?.username || targetUsername;
528
661
  }
529
662
  // --print-url with --wait-file: print URL and wait for code file
530
663
  else if (argv.printUrl && argv.waitFile) {
531
- await app.auth.buildAuthURL(instanceURL, argv.waitFile);
664
+ await app.auth.buildAuthURL(instanceURL, argv.waitFile, targetUsername || undefined);
532
665
  }
533
666
  // --print-url: just print the URL and exit
534
667
  else if (argv.printUrl) {
@@ -538,21 +671,32 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
538
671
  }
539
672
  // --code: exchange the provided authorization code directly
540
673
  else if (argv.code) {
541
- await app.auth.loginWithCode(instanceURL, argv.code);
674
+ await app.auth.loginWithCode(instanceURL, argv.code, targetUsername || undefined);
542
675
  } else {
543
676
  // Interactive: full OAuth flow with browser + prompt
544
- await app.auth.login(instanceURL);
677
+ await app.auth.login(instanceURL, targetUsername || undefined);
545
678
  }
546
679
 
547
680
  // Verify auth by fetching current user
548
681
  let username = '';
549
682
  try {
550
- if (!app.sdk || targetUsername) {
551
- const { SDKClient } = await import('../sdk.js');
552
- const authProvider = targetUsername
553
- ? { getCredentials: () => app.auth.getCredentialsFor(instanceURL, targetUsername) }
554
- : app.auth;
555
- app.sdk = new SDKClient(instanceURL, authProvider);
683
+ const activeName = app.config.activeProfile || app.config.defaultProfile;
684
+ const activeProfile = app.config.profiles?.[activeName];
685
+ const activeInstance = app.getEffectiveInstance?.() || activeProfile?.instance_url || '';
686
+ const activeUsername = activeProfile?.username || '';
687
+ const activeMethod = activeProfile?.auth_method || '';
688
+ const targetMethod = useGck ? 'gck' : useBasic ? 'basic' : 'oauth';
689
+ const targetDiffers = instanceURL !== activeInstance
690
+ || targetUsername !== activeUsername
691
+ || targetMethod !== activeMethod;
692
+ if (!app.sdk || targetDiffers) {
693
+ app.sdk = app.getSDKForProfile
694
+ ? app.getSDKForProfile(instanceURL, {
695
+ authMethod: targetMethod,
696
+ username: targetUsername,
697
+ profile: targetProfile,
698
+ })
699
+ : app.sdk;
556
700
  }
557
701
  const user = await app.sdk.getCurrentUser();
558
702
  username = user?.user_name || user?.name || '';
@@ -560,17 +704,24 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
560
704
  // Non-fatal — token is saved, just couldn't verify
561
705
  }
562
706
 
707
+ // Basic/GCK auth establishes the target identity before the
708
+ // profile exists; preserve it when verification is unavailable.
709
+ username = username || targetUsername;
710
+
563
711
  // Save to profiles — deduplicate by instance URL
564
712
  if (!app.config.profiles) {
565
713
  app.config.profiles = {};
566
714
  }
567
715
 
568
- // Check if a profile already exists for this instance
569
- let profileName = null;
570
- for (const [existingName, existingProfile] of Object.entries(app.config.profiles)) {
571
- if (existingProfile.instance_url === instanceURL) {
572
- profileName = existingName;
573
- break;
716
+ // Preserve an explicitly selected profile even when another profile
717
+ // shares the same instance URL.
718
+ let profileName = target.name;
719
+ if (!profileName) {
720
+ for (const [existingName, existingProfile] of Object.entries(app.config.profiles)) {
721
+ if (existingProfile.instance_url === instanceURL) {
722
+ profileName = existingName;
723
+ break;
724
+ }
574
725
  }
575
726
  }
576
727
 
@@ -582,7 +733,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
582
733
  app.config.profiles[profileName] = {
583
734
  ...(app.config.profiles[profileName] || {}),
584
735
  instance_url: instanceURL,
585
- auth_method: useBasic ? 'basic' : 'oauth',
736
+ auth_method: useGck ? 'gck' : useBasic ? 'basic' : 'oauth',
586
737
  username: username || undefined,
587
738
  read_only: argv['read-only'] || undefined,
588
739
  skip_confirmations: argv['skip-confirmations'] || undefined,
@@ -594,7 +745,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
594
745
  // The legacy bare-instance key is cleaned up behind the
595
746
  // AuthManager seam (credential-store internals don't leak here).
596
747
  if (!useBasic && username) {
597
- app.auth.migrateLegacyCredential(instanceURL, username);
748
+ app.auth.migrateLegacyCredential?.(instanceURL, username);
598
749
  }
599
750
 
600
751
  // Set as default if this is the first one
@@ -637,40 +788,69 @@ Examples:
637
788
  jsn auth logout https://dev12345.service-now.com`);
638
789
  }
639
790
  }
640
- app.auth.logout(instanceURL);
791
+ const target = resolveTargetProfile(argv, app.config, instanceURL);
792
+ const username = target.profile?.username ?? null;
793
+ if (username || canLogoutBareProfile(app, instanceURL, target.profile, !!argv.profile)) {
794
+ app.auth.logout(instanceURL, username);
795
+ }
641
796
  app.ok({ logged_out: true, instance: instanceURL }, { summary: `✓ Logged out from ${instanceURL}` });
642
797
  }),
643
798
  })
644
799
  .command({
645
800
  command: 'status',
646
801
  describe: 'Show detailed authentication status',
647
- handler: wrap(async (_argv, app) => {
648
- const defaultInstance = getEffectiveInstance(app.config);
802
+ handler: wrap(async (argv, app) => {
803
+ const configuredDefault = app.config.profiles?.[app.config.defaultProfile];
804
+ const defaultInstance = configuredDefault?.instance_url || app.config.instanceURL || '';
649
805
 
650
806
  // Check environment auth
651
807
  const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
652
808
 
653
809
  const profiles = [];
654
- for (const [name, profile] of Object.entries(app.config.profiles || {})) {
810
+ const configuredProfiles = Object.entries(app.config.profiles || {});
811
+ const selectedProfiles = argv.profile
812
+ ? configuredProfiles.filter(([name]) => name === argv.profile)
813
+ : configuredProfiles;
814
+ for (const [name, profile] of selectedProfiles) {
655
815
  const instance = profile.instance_url;
656
- const isAuth = app.auth.isAuthenticatedFor(instance);
657
- const lastSeen = app.auth.getLastSeen(instance);
658
- const authSource = isAuth ? app.auth.getAuthSource(instance) : null;
659
- const legacy = !isAuth && app.auth.hasLegacyCredentials(instance) ? true : undefined;
816
+ const authOptions = { authMethod: profile.auth_method, username: profile.username };
817
+ const identityOptions = { username: profile.username, authMethod: profile.auth_method };
818
+ const isAuth = app.auth.isAuthenticatedFor(instance, identityOptions);
819
+ const lastSeen = app.auth.getLastSeen(instance, identityOptions);
820
+ const authState = app.auth.getAuthState(instance, authOptions);
821
+ const legacy = !isAuth && app.auth.hasLegacyCredentials(instance, identityOptions) ? true : undefined;
822
+ // Keep the established status field contract: unauthenticated
823
+ // profiles omit auth_source, except legacy credentials retain
824
+ // their historical `legacy` marker. getAuthState remains the
825
+ // safe metadata seam for authenticated profiles.
826
+ const authSource = isAuth ? authState.auth_source : (legacy ? 'legacy' : undefined);
660
827
 
661
- // Try live verification
828
+ // Try live verification through AuthManager's read-only probe seam.
662
829
  let verified = null;
663
830
  let verifiedAt = null;
664
- if (isAuth && instance) {
831
+ let probe;
832
+ if (!instance) {
833
+ probe = app.auth.probeUnavailable?.('missing_instance') || { status: 'not_attempted', code: 'missing_instance', classification: 'unavailable' };
834
+ } else {
665
835
  try {
666
- const { SDKClient } = await import('../sdk.js');
667
- const sdk = new SDKClient(instance, app.auth);
668
- const user = await sdk.getCurrentUser();
669
- if (user && user.user_name) {
836
+ const sdk = app.getSDKForProfile
837
+ ? app.getSDKForProfile(instance, { ...authOptions, profile })
838
+ : app.sdk || { getCurrentUser: async () => null };
839
+ let user;
840
+ probe = await app.auth.probeCurrentUser(instance, {
841
+ getCurrentUser: async (options) => {
842
+ user = await sdk.getCurrentUser(options);
843
+ return user;
844
+ },
845
+ }, authOptions);
846
+ if (probe.status === 'succeeded' && user?.user_name) {
670
847
  verified = true;
671
848
  verifiedAt = user.user_name;
849
+ } else if (probe.status !== 'not_attempted') {
850
+ verified = false;
672
851
  }
673
852
  } catch {
853
+ probe = app.auth.probeUnavailable?.('sdk_construction_failed') || { status: 'not_attempted', code: 'sdk_construction_failed', classification: 'configuration_error' };
674
854
  verified = false;
675
855
  }
676
856
  }
@@ -686,16 +866,22 @@ Examples:
686
866
  instance,
687
867
  authenticated: isAuth,
688
868
  legacy,
689
- auth_source: authSource || (legacy ? 'legacy' : undefined),
869
+ auth_source: authSource,
690
870
  verified,
691
871
  verified_as: verifiedAt || undefined,
692
872
  last_seen: lastSeen || undefined,
693
873
  days_since_last_seen: daysSinceLastSeen,
694
874
  stale: daysSinceLastSeen > 7,
695
- default: instance === defaultInstance,
875
+ default: name === app.config.defaultProfile,
696
876
  read_only: profile.read_only || false,
697
877
  skip_confirmations: profile.skip_confirmations || false,
698
878
  include_counts: profile.include_counts !== false,
879
+ diagnostics: {
880
+ auth_method: authState.auth_method,
881
+ auth_source: authState.auth_source,
882
+ state: authState.state,
883
+ probe,
884
+ },
699
885
  });
700
886
  }
701
887
 
@@ -746,17 +932,31 @@ Examples:
746
932
  }
747
933
  }
748
934
 
749
- const creds = await app.auth.getCredentialsFor(instanceURL);
750
- const refreshed = await app.auth.refreshToken(instanceURL, creds);
935
+ const target = resolveTargetProfile(argv, app.config, instanceURL);
936
+ const targetProfile = target.profile;
937
+ const targetAuthOptions = {
938
+ authMethod: targetProfile ? targetProfile.auth_method : 'oauth',
939
+ username: targetProfile?.username,
940
+ };
941
+ const creds = await app.auth.getCredentialsFor(
942
+ instanceURL,
943
+ targetProfile?.username,
944
+ targetAuthOptions
945
+ );
946
+ const refreshed = await app.auth.refreshToken(instanceURL, creds, targetProfile?.username ?? null);
751
947
 
752
948
  // Re-probe domain separation on refresh so the capability flag
753
949
  // stays in sync if the plugin was installed/removed since setup.
754
950
  try {
755
- const { SDKClient } = await import('../sdk.js');
756
951
  const { isDomainSeparationInstalled } = await import('./dev/domains.js');
757
- const sdk = new SDKClient(instanceURL, app.auth);
952
+ const sdk = app.getSDKForProfile
953
+ ? app.getSDKForProfile(instanceURL, {
954
+ ...targetAuthOptions,
955
+ profile: targetProfile,
956
+ })
957
+ : app.sdk;
758
958
  const hasDS = await isDomainSeparationInstalled({ sdk });
759
- const name = app.config.activeProfile || app.config.defaultProfile;
959
+ const name = target.name || app.config.activeProfile || app.config.defaultProfile;
760
960
  if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
761
961
  app.config.profiles[name].domain_separation = hasDS;
762
962
  await saveConfig(app.config);