@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/README.md +12 -0
- package/package.json +2 -2
- package/src/app.js +9 -0
- package/src/auth.js +411 -96
- package/src/cli.js +18 -11
- package/src/commands/auth.js +273 -73
- 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 +69 -18
- 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,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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
225
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
352
|
-
|
|
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
|
|
378
|
-
|
|
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
|
|
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
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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.
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 (
|
|
527
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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.
|
|
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 (
|
|
648
|
-
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 || '';
|
|
649
805
|
|
|
650
806
|
// Check environment auth
|
|
651
807
|
const envToken = process.env.SERVICENOW_OAUTH_TOKEN || '';
|
|
652
808
|
|
|
653
809
|
const profiles = [];
|
|
654
|
-
|
|
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
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
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);
|
|
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
|
-
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
|
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:
|
|
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
|
|
750
|
-
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);
|
|
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 =
|
|
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);
|