@jacebenson/jsn 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/auth.js +29 -0
- package/src/commands/auth.js +57 -9
- package/src/commands/profiles.js +61 -2
- package/src/output.js +21 -4
- package/src/sdk.js +7 -0
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -180,12 +180,33 @@ function loadCredentials(instance) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function saveCredentials(instance, creds) {
|
|
183
|
+
// Stamp last_seen on every credential save
|
|
184
|
+
creds.last_seen = creds.last_seen || Math.floor(Date.now() / 1000);
|
|
183
185
|
// Try keyring first, fall back to file
|
|
184
186
|
if (!keyringStore(instance, creds)) {
|
|
185
187
|
fs.writeFileSync(credentialsPath(instance), JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Get the last_seen timestamp for an instance, if available.
|
|
193
|
+
*/
|
|
194
|
+
function getLastSeen(instance) {
|
|
195
|
+
const creds = loadCredentials(instance);
|
|
196
|
+
if (!creds || !creds.last_seen) return null;
|
|
197
|
+
return creds.last_seen;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Update the last_seen timestamp for an instance to now.
|
|
202
|
+
*/
|
|
203
|
+
function touchLastSeen(instance) {
|
|
204
|
+
const creds = loadCredentials(instance);
|
|
205
|
+
if (!creds) return;
|
|
206
|
+
creds.last_seen = Math.floor(Date.now() / 1000);
|
|
207
|
+
saveCredentials(instance, creds);
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
function deleteCredentials(instance) {
|
|
190
211
|
keyringDelete(instance);
|
|
191
212
|
try {
|
|
@@ -250,6 +271,14 @@ export class AuthManager {
|
|
|
250
271
|
this.httpClient = { timeout: 30000 };
|
|
251
272
|
}
|
|
252
273
|
|
|
274
|
+
getLastSeen(instance) {
|
|
275
|
+
return getLastSeen(instance);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
touchLastSeen(instance) {
|
|
279
|
+
return touchLastSeen(instance);
|
|
280
|
+
}
|
|
281
|
+
|
|
253
282
|
isAuthenticated() {
|
|
254
283
|
if (process.env.SERVICENOW_OAUTH_TOKEN) return true;
|
|
255
284
|
if (getBasicAuthFromEnv()) return true;
|
package/src/commands/auth.js
CHANGED
|
@@ -105,11 +105,25 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
105
105
|
// Non-fatal — token is saved, just couldn't verify
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// Save to profiles
|
|
109
|
-
const profileName = instanceURL.replace(/https?:\/\//, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
108
|
+
// Save to profiles — deduplicate by instance URL
|
|
110
109
|
if (!app.config.profiles) {
|
|
111
110
|
app.config.profiles = {};
|
|
112
111
|
}
|
|
112
|
+
|
|
113
|
+
// Check if a profile already exists for this instance
|
|
114
|
+
let profileName = null;
|
|
115
|
+
for (const [existingName, existingProfile] of Object.entries(app.config.profiles)) {
|
|
116
|
+
if (existingProfile.instance_url === instanceURL) {
|
|
117
|
+
profileName = existingName;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If no existing profile found, generate a name from the URL
|
|
123
|
+
if (!profileName) {
|
|
124
|
+
profileName = instanceURL.replace(/https?:\/\//, '').replace(/\.service-now\.com.*/, '').replace(/[^a-zA-Z0-9]/g, '-');
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
app.config.profiles[profileName] = {
|
|
114
128
|
instance_url: instanceURL,
|
|
115
129
|
auth_method: 'oauth',
|
|
@@ -130,11 +144,12 @@ Find your instance URL in your browser's address bar when logged into ServiceNow
|
|
|
130
144
|
authenticated: true,
|
|
131
145
|
instance: instanceURL,
|
|
132
146
|
username: username || undefined,
|
|
147
|
+
profile: profileName,
|
|
133
148
|
default: setDefault || undefined,
|
|
134
149
|
};
|
|
135
150
|
const summary = username
|
|
136
|
-
? `✓ Authenticated to ${instanceURL} as ${username}`
|
|
137
|
-
: `✓ Authenticated to ${instanceURL}`;
|
|
151
|
+
? `✓ Authenticated to ${instanceURL} as ${username} (profile: ${profileName})`
|
|
152
|
+
: `✓ Authenticated to ${instanceURL} (profile: ${profileName})`;
|
|
138
153
|
app.ok(result, { summary });
|
|
139
154
|
}),
|
|
140
155
|
})
|
|
@@ -170,21 +185,54 @@ Examples:
|
|
|
170
185
|
|
|
171
186
|
const profiles = [];
|
|
172
187
|
for (const [name, profile] of Object.entries(app.config.profiles || {})) {
|
|
173
|
-
const
|
|
188
|
+
const instance = profile.instance_url;
|
|
189
|
+
const isAuth = app.auth.isAuthenticatedFor(instance);
|
|
190
|
+
const lastSeen = app.auth.getLastSeen(instance);
|
|
191
|
+
|
|
192
|
+
// Try live verification
|
|
193
|
+
let verified = null;
|
|
194
|
+
let verifiedAt = null;
|
|
195
|
+
if (isAuth && instance) {
|
|
196
|
+
try {
|
|
197
|
+
const { SDKClient } = await import('../sdk.js');
|
|
198
|
+
const sdk = new SDKClient(instance, app.auth);
|
|
199
|
+
const user = await sdk.getCurrentUser();
|
|
200
|
+
if (user && user.user_name) {
|
|
201
|
+
verified = true;
|
|
202
|
+
verifiedAt = user.user_name;
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
verified = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Calculate days since last seen
|
|
210
|
+
let daysSinceLastSeen = null;
|
|
211
|
+
if (lastSeen) {
|
|
212
|
+
daysSinceLastSeen = Math.floor((Date.now() / 1000 - lastSeen) / 86400);
|
|
213
|
+
}
|
|
214
|
+
|
|
174
215
|
profiles.push({
|
|
175
216
|
name,
|
|
176
|
-
instance
|
|
217
|
+
instance,
|
|
177
218
|
authenticated: isAuth,
|
|
178
|
-
|
|
219
|
+
verified,
|
|
220
|
+
verified_as: verifiedAt || undefined,
|
|
221
|
+
last_seen: lastSeen || undefined,
|
|
222
|
+
days_since_last_seen: daysSinceLastSeen,
|
|
223
|
+
stale: daysSinceLastSeen > 7,
|
|
224
|
+
default: instance === defaultInstance,
|
|
179
225
|
});
|
|
180
226
|
}
|
|
181
227
|
|
|
182
|
-
|
|
228
|
+
const result = {
|
|
183
229
|
default_instance: defaultInstance,
|
|
184
230
|
authenticated: app.auth.isAuthenticated(),
|
|
185
231
|
environment_auth: envToken ? true : undefined,
|
|
186
232
|
profiles,
|
|
187
|
-
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
app.ok(result, { summary: `${profiles.length} profile(s)` });
|
|
188
236
|
}),
|
|
189
237
|
})
|
|
190
238
|
.command({
|
package/src/commands/profiles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { saveConfig } from '../config.js';
|
|
1
|
+
import { normalizeInstanceURL, saveConfig, setProfile } from '../config.js';
|
|
2
2
|
|
|
3
3
|
export function profilesCmd(wrap) {
|
|
4
4
|
return {
|
|
@@ -7,6 +7,64 @@ export function profilesCmd(wrap) {
|
|
|
7
7
|
describe: 'Manage configuration profiles',
|
|
8
8
|
builder: (yargs) => {
|
|
9
9
|
return yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: 'create <name>',
|
|
12
|
+
describe: 'Create a new profile',
|
|
13
|
+
builder: (sub) => sub
|
|
14
|
+
.positional('name', {
|
|
15
|
+
describe: 'Profile name',
|
|
16
|
+
type: 'string',
|
|
17
|
+
})
|
|
18
|
+
.option('instance', {
|
|
19
|
+
alias: 'i',
|
|
20
|
+
describe: 'ServiceNow instance URL (e.g. dev12345.service-now.com)',
|
|
21
|
+
type: 'string',
|
|
22
|
+
})
|
|
23
|
+
.option('username', {
|
|
24
|
+
alias: 'u',
|
|
25
|
+
describe: 'Username for basic auth',
|
|
26
|
+
type: 'string',
|
|
27
|
+
})
|
|
28
|
+
.option('password', {
|
|
29
|
+
alias: 'p',
|
|
30
|
+
describe: 'Password for basic auth',
|
|
31
|
+
type: 'string',
|
|
32
|
+
}),
|
|
33
|
+
handler: wrap(async (argv, app) => {
|
|
34
|
+
const name = argv.name;
|
|
35
|
+
if (app.config.profiles && app.config.profiles[name]) {
|
|
36
|
+
throw new Error(`Profile "${name}" already exists. Use "jsn profiles update" or remove it first.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let instance = argv.instance;
|
|
40
|
+
if (!instance) {
|
|
41
|
+
throw new Error(`Instance URL is required.\n\n jsn profiles create ${name} --instance https://dev12345.service-now.com`);
|
|
42
|
+
}
|
|
43
|
+
instance = normalizeInstanceURL(instance);
|
|
44
|
+
|
|
45
|
+
const profile = { instance_url: instance };
|
|
46
|
+
|
|
47
|
+
// If username/password provided, save credentials too
|
|
48
|
+
if (argv.username && argv.password) {
|
|
49
|
+
const { saveCredentials } = await import('../auth.js');
|
|
50
|
+
saveCredentials(instance, {
|
|
51
|
+
auth_method: 'basic',
|
|
52
|
+
username: argv.username,
|
|
53
|
+
password: argv.password,
|
|
54
|
+
});
|
|
55
|
+
profile.auth_method = 'basic';
|
|
56
|
+
profile.username = argv.username;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await setProfile(app.config, name, profile);
|
|
60
|
+
|
|
61
|
+
app.ok({
|
|
62
|
+
profile: name,
|
|
63
|
+
instance,
|
|
64
|
+
auth_method: profile.auth_method || 'oauth (not yet logged in)',
|
|
65
|
+
}, { summary: `Profile "${name}" created for ${instance}` });
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
10
68
|
.command({
|
|
11
69
|
command: 'list',
|
|
12
70
|
describe: 'List all profiles',
|
|
@@ -78,8 +136,9 @@ export function profilesCmd(wrap) {
|
|
|
78
136
|
// When no subcommand is given, show help
|
|
79
137
|
if (!argv._[1]) {
|
|
80
138
|
console.log('Manage your ServiceNow instance profiles.\n');
|
|
81
|
-
console.log('Usage: jsn profiles <list|show|use|remove> [options]\n');
|
|
139
|
+
console.log('Usage: jsn profiles <create|list|show|use|remove> [options]\n');
|
|
82
140
|
console.log('Commands:');
|
|
141
|
+
console.log(' create <name> Create a new profile (--instance required)');
|
|
83
142
|
console.log(' list List all profiles');
|
|
84
143
|
console.log(' show [name] Show profile details');
|
|
85
144
|
console.log(' use <name> Switch to a different profile');
|
package/src/output.js
CHANGED
|
@@ -176,11 +176,28 @@ export class OutputWriter {
|
|
|
176
176
|
this.writer.write('\n');
|
|
177
177
|
for (const p of data.profiles) {
|
|
178
178
|
const prefix = p.default ? '* ' : ' ';
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
const authIcon = p.authenticated ? '✓' : '✗';
|
|
180
|
+
|
|
181
|
+
// Show verified status if we got one
|
|
182
|
+
let verifiedStr = '';
|
|
183
|
+
if (p.verified === true) {
|
|
184
|
+
verifiedStr = ' ✅';
|
|
185
|
+
} else if (p.verified === false) {
|
|
186
|
+
verifiedStr = ' ⚠️';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Show stale hint if >7 days since last seen
|
|
190
|
+
let staleStr = '';
|
|
191
|
+
if (p.stale && p.days_since_last_seen) {
|
|
192
|
+
staleStr = ` (${p.days_since_last_seen}d ago — may have been released)`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (p.name) {
|
|
196
|
+
this.writer.write(`${prefix}${authIcon} ${p.name} — ${p.instance}${verifiedStr}${staleStr}\n`);
|
|
197
|
+
} else if (p.username) {
|
|
198
|
+
this.writer.write(`${prefix}${authIcon} ${p.instance} (as ${p.username})${verifiedStr}${staleStr}\n`);
|
|
182
199
|
} else {
|
|
183
|
-
this.writer.write(`${prefix}${
|
|
200
|
+
this.writer.write(`${prefix}${authIcon} ${p.instance}${verifiedStr}${staleStr}\n`);
|
|
184
201
|
}
|
|
185
202
|
}
|
|
186
203
|
}
|
package/src/sdk.js
CHANGED
|
@@ -65,6 +65,13 @@ export class SDKClient {
|
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Stamp last_seen on successful API call
|
|
69
|
+
try {
|
|
70
|
+
if (this.authProvider && typeof this.authProvider.touchLastSeen === 'function') {
|
|
71
|
+
this.authProvider.touchLastSeen(this.baseURL);
|
|
72
|
+
}
|
|
73
|
+
} catch { /* non-fatal */ }
|
|
74
|
+
|
|
68
75
|
return JSON.parse(body);
|
|
69
76
|
} catch (err) {
|
|
70
77
|
if (err.name === 'AbortError') {
|