@jacebenson/jsn 4.2.4 → 4.3.1
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/README.md +12 -0
- package/package.json +2 -2
- package/src/app.js +9 -0
- package/src/auth.js +386 -100
- package/src/cli.js +18 -11
- package/src/commands/auth.js +208 -70
- package/src/commands/decisiontables.js +178 -0
- package/src/commands/dev/_simple.js +1 -2
- package/src/commands/dev/flows.js +92 -838
- package/src/commands/docs/db.js +60 -2
- package/src/commands/perf.js +143 -0
- package/src/decision-table-crud.js +266 -0
- package/src/decision-table-inspection.js +260 -0
- package/src/errors.js +1 -1
- package/src/flow-inspection.js +844 -0
- package/src/flow-publish.js +357 -0
- package/src/helpers.js +7 -1
- package/src/perf-capture.js +112 -0
- package/src/perf.js +335 -0
- package/src/sdk.js +120 -25
- package/src/session.js +14 -0
- package/src/commands/instance.js +0 -362
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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))
|
package/src/commands/auth.js
CHANGED
|
@@ -82,25 +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
|
|
|
97
|
-
export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '') {
|
|
108
|
+
export function shouldUseGckAuth(argv = {}, cfg = {}, instanceURL = '', targetProfile = null) {
|
|
98
109
|
if (argv.gck || argv.headers) return true;
|
|
110
|
+
if (targetProfile) return targetProfile.auth_method === 'gck';
|
|
99
111
|
return Object.values(cfg.profiles || {}).some(profile =>
|
|
100
112
|
profile.instance_url === instanceURL && profile.auth_method === 'gck'
|
|
101
113
|
);
|
|
102
114
|
}
|
|
103
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
|
+
|
|
104
130
|
/**
|
|
105
131
|
* Pick a profile by name — interactive search picker when ambiguous,
|
|
106
132
|
* auto-selects when there's exactly one, throws when none configured.
|
|
@@ -229,13 +255,13 @@ export async function loginWizard(app, argv = {}) {
|
|
|
229
255
|
if (useBasic) {
|
|
230
256
|
const username = await ask('Username: ');
|
|
231
257
|
rl.close(); // Close outer readline before askHidden creates its own
|
|
232
|
-
const {
|
|
233
|
-
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);
|
|
234
260
|
if (existingCreds && existingCreds.auth_method === 'basic') {
|
|
235
261
|
console.log(`Using existing credentials for ${instance}`);
|
|
236
262
|
} else {
|
|
237
263
|
const password = await askHidden('Password: ');
|
|
238
|
-
saveCredentials(instance, { auth_method: 'basic', username, password }, username);
|
|
264
|
+
app.auth.saveCredentials(instance, { auth_method: 'basic', username, password }, username);
|
|
239
265
|
console.log('Basic auth credentials saved');
|
|
240
266
|
}
|
|
241
267
|
profile.username = username;
|
|
@@ -283,16 +309,12 @@ export async function loginWizard(app, argv = {}) {
|
|
|
283
309
|
await app.auth.login(instance);
|
|
284
310
|
// Re-save credentials under <user>@<instance> after verifying username
|
|
285
311
|
try {
|
|
286
|
-
const
|
|
287
|
-
|
|
312
|
+
const sdk = app.getSDKForProfile
|
|
313
|
+
? app.getSDKForProfile(instance, { authMethod: 'oauth' })
|
|
314
|
+
: app.sdk;
|
|
288
315
|
const user = await sdk.getCurrentUser();
|
|
289
316
|
if (user && user.user_name) {
|
|
290
|
-
|
|
291
|
-
const stored = loadCredentials(instance);
|
|
292
|
-
if (stored && stored.access_token) {
|
|
293
|
-
stored.username = user.user_name;
|
|
294
|
-
saveCredentials(instance, stored, user.user_name);
|
|
295
|
-
}
|
|
317
|
+
app.auth.migrateLegacyCredential(instance, user.user_name);
|
|
296
318
|
profile.username = user.user_name;
|
|
297
319
|
}
|
|
298
320
|
} catch {
|
|
@@ -304,9 +326,14 @@ export async function loginWizard(app, argv = {}) {
|
|
|
304
326
|
// Probe domain separation capability once and stamp the profile, so
|
|
305
327
|
// `jsn domains` (and its help entry) only appear on instances with it.
|
|
306
328
|
try {
|
|
307
|
-
const { SDKClient } = await import('../sdk.js');
|
|
308
329
|
const domainsMod = await import('./dev/domains.js');
|
|
309
|
-
const sdk =
|
|
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;
|
|
310
337
|
profile.domain_separation = await domainsMod.isDomainSeparationInstalled({ sdk });
|
|
311
338
|
await setProfile(app.config, profileName, profile);
|
|
312
339
|
|
|
@@ -410,8 +437,9 @@ export async function modifyProfile(app, argv = {}) {
|
|
|
410
437
|
return;
|
|
411
438
|
}
|
|
412
439
|
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
413
|
-
const
|
|
414
|
-
|
|
440
|
+
const sdk = app.getSDKForProfile
|
|
441
|
+
? app.getSDKForProfile(profile.instance_url, { profile, authMethod: profile.auth_method, username: profile.username })
|
|
442
|
+
: app.sdk;
|
|
415
443
|
if (!(await isDomainSeparationInstalled({ sdk }))) {
|
|
416
444
|
throw new Error(`Domain separation is not installed on ${profile.instance_url}`);
|
|
417
445
|
}
|
|
@@ -436,8 +464,9 @@ export async function modifyProfile(app, argv = {}) {
|
|
|
436
464
|
value: 'domain',
|
|
437
465
|
});
|
|
438
466
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
467
|
+
const sdk = app.getSDKForProfile
|
|
468
|
+
? app.getSDKForProfile(profile.instance_url, { profile, authMethod: profile.auth_method, username: profile.username })
|
|
469
|
+
: app.sdk;
|
|
441
470
|
flag = await select({
|
|
442
471
|
message: `Modify ${name} (${profile.instance_url})`,
|
|
443
472
|
choices,
|
|
@@ -482,22 +511,38 @@ export async function removeProfile(app, name) {
|
|
|
482
511
|
if (!app.config.profiles[name]) {
|
|
483
512
|
throw new Error(`Profile not found: ${name}`);
|
|
484
513
|
}
|
|
485
|
-
const
|
|
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;
|
|
486
521
|
delete app.config.profiles[name];
|
|
487
522
|
if (app.config.defaultProfile === name) app.config.defaultProfile = '';
|
|
488
523
|
if (app.config.activeProfile === name) app.config.activeProfile = '';
|
|
489
524
|
saveConfig(app.config);
|
|
490
525
|
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
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);
|
|
496
530
|
}
|
|
497
531
|
|
|
498
532
|
app.ok({ removed: name }, { summary: `Removed profile: ${name}` });
|
|
499
533
|
}
|
|
500
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
|
+
|
|
501
546
|
export function authCmd(wrap) {
|
|
502
547
|
return {
|
|
503
548
|
command: 'auth [subcommand]',
|
|
@@ -517,6 +562,14 @@ export function authCmd(wrap) {
|
|
|
517
562
|
describe: 'Authenticate with Basic Auth via env vars (SN_USERNAME/SN_PASSWORD)',
|
|
518
563
|
type: 'boolean',
|
|
519
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
|
+
})
|
|
520
573
|
.option('print-url', {
|
|
521
574
|
describe: 'Print the OAuth URL and exit (saves PKCE state for --code)',
|
|
522
575
|
type: 'boolean',
|
|
@@ -541,7 +594,15 @@ export function authCmd(wrap) {
|
|
|
541
594
|
}),
|
|
542
595
|
handler: wrap(async (argv, app) => {
|
|
543
596
|
let instanceURL;
|
|
544
|
-
if (argv.
|
|
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) {
|
|
545
606
|
instanceURL = resolveInstanceArg(argv.instance, app.config);
|
|
546
607
|
} else if (app.isInteractive()) {
|
|
547
608
|
// gh-faithful: interactive terminals always get a picker —
|
|
@@ -579,18 +640,28 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
579
640
|
}
|
|
580
641
|
}
|
|
581
642
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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);
|
|
586
649
|
// --basic (with --password as a compatibility alias): authenticate
|
|
587
650
|
// with basic auth from env vars and persist it for this profile.
|
|
588
|
-
if (
|
|
589
|
-
|
|
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;
|
|
590
661
|
}
|
|
591
662
|
// --print-url with --wait-file: print URL and wait for code file
|
|
592
663
|
else if (argv.printUrl && argv.waitFile) {
|
|
593
|
-
await app.auth.buildAuthURL(instanceURL, argv.waitFile);
|
|
664
|
+
await app.auth.buildAuthURL(instanceURL, argv.waitFile, targetUsername || undefined);
|
|
594
665
|
}
|
|
595
666
|
// --print-url: just print the URL and exit
|
|
596
667
|
else if (argv.printUrl) {
|
|
@@ -600,21 +671,32 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
600
671
|
}
|
|
601
672
|
// --code: exchange the provided authorization code directly
|
|
602
673
|
else if (argv.code) {
|
|
603
|
-
await app.auth.loginWithCode(instanceURL, argv.code);
|
|
674
|
+
await app.auth.loginWithCode(instanceURL, argv.code, targetUsername || undefined);
|
|
604
675
|
} else {
|
|
605
676
|
// Interactive: full OAuth flow with browser + prompt
|
|
606
|
-
await app.auth.login(instanceURL);
|
|
677
|
+
await app.auth.login(instanceURL, targetUsername || undefined);
|
|
607
678
|
}
|
|
608
679
|
|
|
609
680
|
// Verify auth by fetching current user
|
|
610
681
|
let username = '';
|
|
611
682
|
try {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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;
|
|
618
700
|
}
|
|
619
701
|
const user = await app.sdk.getCurrentUser();
|
|
620
702
|
username = user?.user_name || user?.name || '';
|
|
@@ -622,17 +704,24 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
622
704
|
// Non-fatal — token is saved, just couldn't verify
|
|
623
705
|
}
|
|
624
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
|
+
|
|
625
711
|
// Save to profiles — deduplicate by instance URL
|
|
626
712
|
if (!app.config.profiles) {
|
|
627
713
|
app.config.profiles = {};
|
|
628
714
|
}
|
|
629
715
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
+
}
|
|
636
725
|
}
|
|
637
726
|
}
|
|
638
727
|
|
|
@@ -644,7 +733,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
644
733
|
app.config.profiles[profileName] = {
|
|
645
734
|
...(app.config.profiles[profileName] || {}),
|
|
646
735
|
instance_url: instanceURL,
|
|
647
|
-
auth_method: useBasic ? 'basic' : 'oauth',
|
|
736
|
+
auth_method: useGck ? 'gck' : useBasic ? 'basic' : 'oauth',
|
|
648
737
|
username: username || undefined,
|
|
649
738
|
read_only: argv['read-only'] || undefined,
|
|
650
739
|
skip_confirmations: argv['skip-confirmations'] || undefined,
|
|
@@ -656,7 +745,7 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
656
745
|
// The legacy bare-instance key is cleaned up behind the
|
|
657
746
|
// AuthManager seam (credential-store internals don't leak here).
|
|
658
747
|
if (!useBasic && username) {
|
|
659
|
-
app.auth.migrateLegacyCredential(instanceURL, username);
|
|
748
|
+
app.auth.migrateLegacyCredential?.(instanceURL, username);
|
|
660
749
|
}
|
|
661
750
|
|
|
662
751
|
// Set as default if this is the first one
|
|
@@ -699,40 +788,69 @@ Examples:
|
|
|
699
788
|
jsn auth logout https://dev12345.service-now.com`);
|
|
700
789
|
}
|
|
701
790
|
}
|
|
702
|
-
app.
|
|
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
|
+
}
|
|
703
796
|
app.ok({ logged_out: true, instance: instanceURL }, { summary: `✓ Logged out from ${instanceURL}` });
|
|
704
797
|
}),
|
|
705
798
|
})
|
|
706
799
|
.command({
|
|
707
800
|
command: 'status',
|
|
708
801
|
describe: 'Show detailed authentication status',
|
|
709
|
-
handler: wrap(async (
|
|
710
|
-
const
|
|
802
|
+
handler: wrap(async (argv, app) => {
|
|
803
|
+
const configuredDefault = app.config.profiles?.[app.config.defaultProfile];
|
|
804
|
+
const defaultInstance = configuredDefault?.instance_url || app.config.instanceURL || '';
|
|
711
805
|
|
|
712
806
|
// Check environment auth
|
|
713
807
|
const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
|
|
714
808
|
|
|
715
809
|
const profiles = [];
|
|
716
|
-
|
|
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) {
|
|
717
815
|
const instance = profile.instance_url;
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
const
|
|
721
|
-
const
|
|
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);
|
|
722
827
|
|
|
723
|
-
// Try live verification
|
|
828
|
+
// Try live verification through AuthManager's read-only probe seam.
|
|
724
829
|
let verified = null;
|
|
725
830
|
let verifiedAt = null;
|
|
726
|
-
|
|
831
|
+
let probe;
|
|
832
|
+
if (!instance) {
|
|
833
|
+
probe = app.auth.probeUnavailable?.('missing_instance') || { status: 'not_attempted', code: 'missing_instance', classification: 'unavailable' };
|
|
834
|
+
} else {
|
|
727
835
|
try {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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) {
|
|
732
847
|
verified = true;
|
|
733
848
|
verifiedAt = user.user_name;
|
|
849
|
+
} else if (probe.status !== 'not_attempted') {
|
|
850
|
+
verified = false;
|
|
734
851
|
}
|
|
735
852
|
} catch {
|
|
853
|
+
probe = app.auth.probeUnavailable?.('sdk_construction_failed') || { status: 'not_attempted', code: 'sdk_construction_failed', classification: 'configuration_error' };
|
|
736
854
|
verified = false;
|
|
737
855
|
}
|
|
738
856
|
}
|
|
@@ -748,16 +866,22 @@ Examples:
|
|
|
748
866
|
instance,
|
|
749
867
|
authenticated: isAuth,
|
|
750
868
|
legacy,
|
|
751
|
-
auth_source: authSource
|
|
869
|
+
auth_source: authSource,
|
|
752
870
|
verified,
|
|
753
871
|
verified_as: verifiedAt || undefined,
|
|
754
872
|
last_seen: lastSeen || undefined,
|
|
755
873
|
days_since_last_seen: daysSinceLastSeen,
|
|
756
874
|
stale: daysSinceLastSeen > 7,
|
|
757
|
-
default:
|
|
875
|
+
default: name === app.config.defaultProfile,
|
|
758
876
|
read_only: profile.read_only || false,
|
|
759
877
|
skip_confirmations: profile.skip_confirmations || false,
|
|
760
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
|
+
},
|
|
761
885
|
});
|
|
762
886
|
}
|
|
763
887
|
|
|
@@ -808,17 +932,31 @@ Examples:
|
|
|
808
932
|
}
|
|
809
933
|
}
|
|
810
934
|
|
|
811
|
-
const
|
|
812
|
-
const
|
|
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);
|
|
813
947
|
|
|
814
948
|
// Re-probe domain separation on refresh so the capability flag
|
|
815
949
|
// stays in sync if the plugin was installed/removed since setup.
|
|
816
950
|
try {
|
|
817
|
-
const { SDKClient } = await import('../sdk.js');
|
|
818
951
|
const { isDomainSeparationInstalled } = await import('./dev/domains.js');
|
|
819
|
-
const sdk =
|
|
952
|
+
const sdk = app.getSDKForProfile
|
|
953
|
+
? app.getSDKForProfile(instanceURL, {
|
|
954
|
+
...targetAuthOptions,
|
|
955
|
+
profile: targetProfile,
|
|
956
|
+
})
|
|
957
|
+
: app.sdk;
|
|
820
958
|
const hasDS = await isDomainSeparationInstalled({ sdk });
|
|
821
|
-
const name = app.config.activeProfile || app.config.defaultProfile;
|
|
959
|
+
const name = target.name || app.config.activeProfile || app.config.defaultProfile;
|
|
822
960
|
if (name && app.config.profiles[name] && app.config.profiles[name].instance_url === instanceURL) {
|
|
823
961
|
app.config.profiles[name].domain_separation = hasDS;
|
|
824
962
|
await saveConfig(app.config);
|