@jacebenson/jsn 0.0.10 โ 1.0.2
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 +7 -49
- package/bin/jsn.js +57 -2
- package/package.json +28 -32
- package/scripts/install.sh +227 -0
- package/scripts/npm-install.js +235 -0
- package/scripts/pre-commit-check.sh +61 -0
- package/src/app.js +0 -157
- package/src/auth.js +0 -283
- package/src/cli.js +0 -144
- package/src/commands/_ticket.js +0 -256
- package/src/commands/auth.js +0 -62
- package/src/commands/changes.js +0 -7
- package/src/commands/dev/_generic.js +0 -223
- package/src/commands/dev/_simple.js +0 -89
- package/src/commands/dev/eval.js +0 -17
- package/src/commands/dev/flows.js +0 -528
- package/src/commands/dev/forms.js +0 -313
- package/src/commands/dev/lists.js +0 -233
- package/src/commands/dev/logs.js +0 -51
- package/src/commands/dev/rest.js +0 -64
- package/src/commands/dev/scopes.js +0 -96
- package/src/commands/dev/updatesets.js +0 -97
- package/src/commands/dev.js +0 -53
- package/src/commands/groupmembers.js +0 -39
- package/src/commands/grouproles.js +0 -39
- package/src/commands/groups.js +0 -57
- package/src/commands/incidents.js +0 -7
- package/src/commands/profiles.js +0 -79
- package/src/commands/records.js +0 -137
- package/src/commands/requests.js +0 -7
- package/src/commands/setup.js +0 -39
- package/src/commands/tasks.js +0 -7
- package/src/commands/tickets.js +0 -121
- package/src/commands/users.js +0 -57
- package/src/commands/version.js +0 -25
- package/src/config.js +0 -154
- package/src/context.js +0 -62
- package/src/errors.js +0 -101
- package/src/helpers.js +0 -60
- package/src/output.js +0 -410
- package/src/sdk.js +0 -357
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# pre-commit-check.sh - Run this before committing to check architecture compliance
|
|
3
|
+
|
|
4
|
+
echo "๐ Checking architecture patterns..."
|
|
5
|
+
|
|
6
|
+
# Check for forbidden SDK patterns
|
|
7
|
+
FORBIDDEN_PATTERNS=(
|
|
8
|
+
"func.*Client.*ListForm"
|
|
9
|
+
"func.*Client.*ListList"
|
|
10
|
+
"func.*Client.*GetSP"
|
|
11
|
+
"func.*Client.*ListSP"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
ERRORS=0
|
|
15
|
+
|
|
16
|
+
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
|
17
|
+
matches=$(grep -r "$pattern" internal/sdk/*.go 2>/dev/null | grep -v "_test.go" | grep -v "^Binary")
|
|
18
|
+
if [ ! -z "$matches" ]; then
|
|
19
|
+
echo "โ Found forbidden SDK pattern: $pattern"
|
|
20
|
+
echo "$matches"
|
|
21
|
+
ERRORS=$((ERRORS + 1))
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
# Check that commands don't import old SDK types
|
|
26
|
+
BAD_IMPORTS=(
|
|
27
|
+
"sdk.FormSection"
|
|
28
|
+
"sdk.FormElement"
|
|
29
|
+
"sdk.ListLayout"
|
|
30
|
+
"sdk.ListElement"
|
|
31
|
+
"sdk.SPPage"
|
|
32
|
+
"sdk.SPWidgetInstance"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for pattern in "${BAD_IMPORTS[@]}"; do
|
|
36
|
+
matches=$(grep -r "$pattern" internal/commands/**/*.go 2>/dev/null)
|
|
37
|
+
if [ ! -z "$matches" ]; then
|
|
38
|
+
echo "โ Command using SDK type instead of local type: $pattern"
|
|
39
|
+
echo "$matches"
|
|
40
|
+
ERRORS=$((ERRORS + 1))
|
|
41
|
+
fi
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
# Run architecture tests
|
|
45
|
+
echo "๐งช Running architecture tests..."
|
|
46
|
+
if ! go test ./internal/sdk -run TestNoSDKHelperMethods -run TestCommandsUseDirectSDKList > /dev/null 2>&1; then
|
|
47
|
+
echo "โ Architecture tests failed"
|
|
48
|
+
go test ./internal/sdk -v -run "TestNoSDKHelperMethods|TestCommandsUseDirectSDKList"
|
|
49
|
+
ERRORS=$((ERRORS + 1))
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if [ $ERRORS -eq 0 ]; then
|
|
53
|
+
echo "โ
All architecture checks passed!"
|
|
54
|
+
exit 0
|
|
55
|
+
else
|
|
56
|
+
echo ""
|
|
57
|
+
echo "โ ๏ธ Architecture violations found!"
|
|
58
|
+
echo " Remember: Commands should call app.SDK.List() directly with local types."
|
|
59
|
+
echo " See internal/commands/dev/forms.go for the correct pattern."
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
package/src/app.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// App context: bundles config, auth, SDK, output, and runtime context
|
|
2
|
-
|
|
3
|
-
import { AuthManager } from './auth.js';
|
|
4
|
-
import { SDKClient } from './sdk.js';
|
|
5
|
-
import { OutputWriter, FormatAuto, FormatJSON, FormatMarkdown, FormatQuiet, FormatStyled } from './output.js';
|
|
6
|
-
import { getEffectiveInstance } from './config.js';
|
|
7
|
-
import { extractProfileName } from './helpers.js';
|
|
8
|
-
import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
|
|
9
|
-
import { errUsage, errAuth } from './errors.js';
|
|
10
|
-
import process from 'node:process';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export class App {
|
|
14
|
-
constructor(cfg) {
|
|
15
|
-
this.config = cfg;
|
|
16
|
-
this.auth = new AuthManager(this);
|
|
17
|
-
this.output = new OutputWriter({ format: resolveFormat(cfg.format) });
|
|
18
|
-
this.sdk = null;
|
|
19
|
-
|
|
20
|
-
const instance = getEffectiveInstance(cfg);
|
|
21
|
-
if (instance) {
|
|
22
|
-
this.sdk = new SDKClient(instance, this.auth);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
this.context = {
|
|
26
|
-
profileName: '',
|
|
27
|
-
username: '',
|
|
28
|
-
scope: '',
|
|
29
|
-
updateSet: '',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
this.loadContext();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
loadContext() {
|
|
36
|
-
const instance = getEffectiveInstance(this.config);
|
|
37
|
-
if (!instance) return;
|
|
38
|
-
this.context.profileName = extractProfileName(instance);
|
|
39
|
-
for (const [name, profile] of Object.entries(this.config.profiles || {})) {
|
|
40
|
-
if (profile.instance_url === instance) {
|
|
41
|
-
this.context.profileName = name;
|
|
42
|
-
this.context.username = profile.username || '';
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
getEffectiveInstance() {
|
|
49
|
-
return getEffectiveInstance(this.config);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async printContextHeader() {
|
|
53
|
-
if (!this.getEffectiveInstance() || !this.sdk) return;
|
|
54
|
-
if (process.env.JSN_NO_HEADER) return;
|
|
55
|
-
if (this.output.getFormat() === FormatJSON || this.output.getFormat() === FormatQuiet) return;
|
|
56
|
-
|
|
57
|
-
let userDisplayName = 'Unknown';
|
|
58
|
-
let userSysID = '';
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const user = await getCurrentUser(this.sdk);
|
|
62
|
-
if (user) {
|
|
63
|
-
userDisplayName = user.name || user.user_name;
|
|
64
|
-
userSysID = user.sys_id;
|
|
65
|
-
this.context.username = userDisplayName;
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let displayUserName = userDisplayName;
|
|
72
|
-
if (displayUserName.length > 10) {
|
|
73
|
-
displayUserName = displayUserName.slice(0, 6) + '...';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let scope = 'global';
|
|
77
|
-
if (userSysID) {
|
|
78
|
-
try {
|
|
79
|
-
const app = await getCurrentApplication(this.sdk, userSysID);
|
|
80
|
-
if (app && app.scope) scope = app.scope;
|
|
81
|
-
} catch {
|
|
82
|
-
// ignore
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
this.context.scope = scope;
|
|
86
|
-
|
|
87
|
-
let updateSet = 'Default';
|
|
88
|
-
let updateSetSysID = '';
|
|
89
|
-
if (userSysID) {
|
|
90
|
-
try {
|
|
91
|
-
const us = await getCurrentUpdateSet(this.sdk, userSysID);
|
|
92
|
-
if (us && us.name && us.name !== '-') {
|
|
93
|
-
updateSet = us.name;
|
|
94
|
-
updateSetSysID = us.sys_id;
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// ignore
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
this.context.updateSet = updateSet;
|
|
101
|
-
|
|
102
|
-
const instance = this.getEffectiveInstance();
|
|
103
|
-
const instanceLink = instance;
|
|
104
|
-
const userLink = `${instance}/sys_user_list.do?sysparm_query=sys_id=${userSysID}`;
|
|
105
|
-
const scopeLink = `${instance}/sys_scope.do?sysparm_query=scope=${scope}`;
|
|
106
|
-
const updateSetLink = updateSetSysID
|
|
107
|
-
? `${instance}/sys_update_set.do?sys_id=${updateSetSysID}`
|
|
108
|
-
: `${instance}/sys_update_set_list.do`;
|
|
109
|
-
|
|
110
|
-
const scopeFormatted = `[${scope}]`;
|
|
111
|
-
|
|
112
|
-
process.stderr.write('# Use `jsn updateset use` or `jsn scope use` to change scope/updateset\n');
|
|
113
|
-
process.stderr.write('PROFILE USER [SCOPE] UPDATE SET\n');
|
|
114
|
-
|
|
115
|
-
const profileStr = `]8;;${instanceLink}\x07${String(this.context.profileName).padEnd(9)}]8;;\x07`;
|
|
116
|
-
const userStr = `]8;;${userLink}\x07${String(displayUserName).padEnd(9)}]8;;\x07`;
|
|
117
|
-
const scopeStr = `]8;;${scopeLink}\x07${String(scopeFormatted).padEnd(17)}]8;;\x07`;
|
|
118
|
-
const updateSetStr = `]8;;${updateSetLink}\x07${updateSet}]8;;\x07`;
|
|
119
|
-
|
|
120
|
-
process.stderr.write(`${profileStr} ${userStr} ${scopeStr} ${updateSetStr}\n\n`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
ok(data, opts = {}) {
|
|
124
|
-
this.output.ok(data, opts);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
err(error) {
|
|
128
|
-
this.output.err(error);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
isInteractive() {
|
|
132
|
-
return process.stdout.isTTY === true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
requireInstance() {
|
|
136
|
-
if (!this.getEffectiveInstance()) {
|
|
137
|
-
throw errUsage('Instance URL required. Set via --instance flag, SERVICENOW_INSTANCE_URL env, or config file.');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
requireAuth() {
|
|
142
|
-
if (!this.auth.isAuthenticated()) {
|
|
143
|
-
throw errAuth('Not authenticated');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function resolveFormat(fmt) {
|
|
149
|
-
switch (fmt) {
|
|
150
|
-
case 'json': return FormatJSON;
|
|
151
|
-
case 'markdown':
|
|
152
|
-
case 'md': return FormatMarkdown;
|
|
153
|
-
case 'quiet': return FormatQuiet;
|
|
154
|
-
case 'styled': return FormatStyled;
|
|
155
|
-
default: return FormatAuto;
|
|
156
|
-
}
|
|
157
|
-
}
|
package/src/auth.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
// OAuth 2.0 with PKCE authentication
|
|
2
|
-
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import readline from 'node:readline';
|
|
7
|
-
import { globalConfigDir, normalizeInstanceURL } from './config.js';
|
|
8
|
-
import { errAuth } from './errors.js';
|
|
9
|
-
|
|
10
|
-
const DEFAULT_OAUTH_CLIENT_ID = '543e5655f77746a28228c6009a599dfb';
|
|
11
|
-
const REDIRECT_URI = '/sdk-oauth.do';
|
|
12
|
-
|
|
13
|
-
function credentialsPath(instance) {
|
|
14
|
-
const dir = path.join(globalConfigDir(), 'credentials');
|
|
15
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
16
|
-
const filename = Buffer.from(instance).toString('base64url') + '.json';
|
|
17
|
-
return path.join(dir, filename);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getOAuthClientID() {
|
|
21
|
-
return process.env.SERVICENOW_OAUTH_CLIENT_ID || DEFAULT_OAUTH_CLIENT_ID;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function generatePKCE() {
|
|
25
|
-
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
26
|
-
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
27
|
-
const state = crypto.randomBytes(16).toString('base64url');
|
|
28
|
-
return { code_verifier: verifier, code_challenge: challenge, state };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function buildAuthURL(instanceURL, clientID, pkce) {
|
|
32
|
-
const u = new URL('/oauth_auth.do', instanceURL);
|
|
33
|
-
u.searchParams.set('response_type', 'code');
|
|
34
|
-
u.searchParams.set('client_id', clientID);
|
|
35
|
-
u.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
36
|
-
u.searchParams.set('state', pkce.state);
|
|
37
|
-
u.searchParams.set('code_challenge', pkce.code_challenge);
|
|
38
|
-
u.searchParams.set('code_challenge_method', 'S256');
|
|
39
|
-
u.searchParams.set('scope', 'openid');
|
|
40
|
-
return u.toString();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function loadCredentials(instance) {
|
|
44
|
-
try {
|
|
45
|
-
const data = fs.readFileSync(credentialsPath(instance), 'utf-8');
|
|
46
|
-
return JSON.parse(data);
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function saveCredentials(instance, creds) {
|
|
53
|
-
fs.writeFileSync(credentialsPath(instance), JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function deleteCredentials(instance) {
|
|
57
|
-
try {
|
|
58
|
-
fs.unlinkSync(credentialsPath(instance));
|
|
59
|
-
} catch {
|
|
60
|
-
// ignore
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function askHidden(promptText) {
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
const rl = readline.createInterface({
|
|
67
|
-
input: process.stdin,
|
|
68
|
-
output: process.stdout,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const stdin = process.stdin;
|
|
72
|
-
const stdout = process.stdout;
|
|
73
|
-
|
|
74
|
-
if (!stdin.isTTY) {
|
|
75
|
-
rl.question(promptText, (answer) => {
|
|
76
|
-
rl.close();
|
|
77
|
-
resolve(answer.trim());
|
|
78
|
-
});
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
stdout.write(promptText);
|
|
83
|
-
|
|
84
|
-
stdin.setRawMode(true);
|
|
85
|
-
stdin.resume();
|
|
86
|
-
stdin.setEncoding('utf-8');
|
|
87
|
-
|
|
88
|
-
let input = '';
|
|
89
|
-
const onData = (key) => {
|
|
90
|
-
if (key === '\r' || key === '\n') {
|
|
91
|
-
stdin.removeListener('data', onData);
|
|
92
|
-
stdin.setRawMode(false);
|
|
93
|
-
stdin.pause();
|
|
94
|
-
stdout.write('\n');
|
|
95
|
-
rl.close();
|
|
96
|
-
resolve(input);
|
|
97
|
-
} else if (key === '\u0003') {
|
|
98
|
-
process.exit();
|
|
99
|
-
} else if (key === '\u007f') {
|
|
100
|
-
if (input.length > 0) {
|
|
101
|
-
input = input.slice(0, -1);
|
|
102
|
-
stdout.write('\b \b');
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
input += key;
|
|
106
|
-
stdout.write('*');
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
stdin.on('data', onData);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export class AuthManager {
|
|
114
|
-
constructor(configProvider) {
|
|
115
|
-
this.configProvider = configProvider;
|
|
116
|
-
this.httpClient = { timeout: 30000 };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
isAuthenticated() {
|
|
120
|
-
if (process.env.SERVICENOW_OAUTH_TOKEN) return true;
|
|
121
|
-
const instance = this.configProvider.getEffectiveInstance();
|
|
122
|
-
if (!instance) return false;
|
|
123
|
-
try {
|
|
124
|
-
this.getCredentialsFor(instance);
|
|
125
|
-
return true;
|
|
126
|
-
} catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
isAuthenticatedFor(instance) {
|
|
132
|
-
if (!instance) return false;
|
|
133
|
-
const creds = loadCredentials(instance);
|
|
134
|
-
if (!creds) return false;
|
|
135
|
-
if (creds.expires_at && Date.now() >= creds.expires_at * 1000) return false;
|
|
136
|
-
return !!creds.access_token;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async getCredentials() {
|
|
140
|
-
if (process.env.SERVICENOW_OAUTH_TOKEN) {
|
|
141
|
-
return { auth_method: 'oauth', access_token: process.env.SERVICENOW_OAUTH_TOKEN };
|
|
142
|
-
}
|
|
143
|
-
const instance = this.configProvider.getEffectiveInstance();
|
|
144
|
-
if (!instance) {
|
|
145
|
-
throw errAuth('No instance configured');
|
|
146
|
-
}
|
|
147
|
-
return this.getCredentialsFor(instance);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
getCredentialsFor(instance) {
|
|
151
|
-
const creds = loadCredentials(instance);
|
|
152
|
-
if (!creds) {
|
|
153
|
-
throw errAuth(`Not authenticated for ${instance}`);
|
|
154
|
-
}
|
|
155
|
-
// Check expiry โ refresh if less than 5 minutes remaining
|
|
156
|
-
if (creds.expires_at && Date.now() >= (creds.expires_at - 300) * 1000) {
|
|
157
|
-
if (creds.refresh_token) {
|
|
158
|
-
return this.refreshToken(instance, creds);
|
|
159
|
-
}
|
|
160
|
-
throw errAuth('Token expired, please login again');
|
|
161
|
-
}
|
|
162
|
-
return creds;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async login(instanceURL) {
|
|
166
|
-
instanceURL = normalizeInstanceURL(instanceURL);
|
|
167
|
-
const clientID = getOAuthClientID();
|
|
168
|
-
const pkce = generatePKCE();
|
|
169
|
-
const authURL = buildAuthURL(instanceURL, clientID, pkce);
|
|
170
|
-
|
|
171
|
-
console.log();
|
|
172
|
-
console.log('Opening browser for OAuth authentication...');
|
|
173
|
-
console.log('If the browser does not open automatically, visit:');
|
|
174
|
-
console.log(authURL);
|
|
175
|
-
console.log();
|
|
176
|
-
|
|
177
|
-
// Try to open browser
|
|
178
|
-
const open = (await import('node:child_process')).spawn;
|
|
179
|
-
const platform = process.platform;
|
|
180
|
-
let cmd, args;
|
|
181
|
-
if (platform === 'darwin') {
|
|
182
|
-
cmd = 'open';
|
|
183
|
-
args = [authURL];
|
|
184
|
-
} else if (platform === 'win32') {
|
|
185
|
-
cmd = 'cmd';
|
|
186
|
-
args = ['/c', 'start', authURL];
|
|
187
|
-
} else {
|
|
188
|
-
cmd = 'xdg-open';
|
|
189
|
-
args = [authURL];
|
|
190
|
-
}
|
|
191
|
-
const child = open(cmd, args, { detached: true, stdio: 'ignore' });
|
|
192
|
-
child.on('error', () => {
|
|
193
|
-
// Browser open command not available โ user will open the URL manually
|
|
194
|
-
});
|
|
195
|
-
child.unref();
|
|
196
|
-
|
|
197
|
-
console.log('After authenticating in the browser, copy the authorization code shown on the page.');
|
|
198
|
-
console.log('(input is hidden for security โ just paste and press Enter)');
|
|
199
|
-
console.log();
|
|
200
|
-
|
|
201
|
-
const authCode = await askHidden('Authorization code (hidden on paste for security): ');
|
|
202
|
-
const code = authCode.trim();
|
|
203
|
-
if (!code) {
|
|
204
|
-
throw errAuth('Authorization code is required');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
console.log('\nExchanging authorization code for tokens...');
|
|
208
|
-
const newCreds = await this.exchangeCode(instanceURL, clientID, code, pkce);
|
|
209
|
-
saveCredentials(instanceURL, newCreds);
|
|
210
|
-
return newCreds;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async exchangeCode(instanceURL, clientID, code, pkce) {
|
|
214
|
-
const tokenURL = `${instanceURL.replace(/\/$/, '')}/oauth_token.do`;
|
|
215
|
-
const body = new URLSearchParams();
|
|
216
|
-
body.set('grant_type', 'authorization_code');
|
|
217
|
-
body.set('client_id', clientID);
|
|
218
|
-
body.set('code', code);
|
|
219
|
-
body.set('redirect_uri', REDIRECT_URI);
|
|
220
|
-
body.set('code_verifier', pkce.code_verifier);
|
|
221
|
-
|
|
222
|
-
const resp = await fetch(tokenURL, {
|
|
223
|
-
method: 'POST',
|
|
224
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
225
|
-
body: body.toString(),
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const text = await resp.text();
|
|
229
|
-
if (!resp.ok) {
|
|
230
|
-
throw errAuth(`Token exchange failed (status ${resp.status}): ${text}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const tokenResp = JSON.parse(text);
|
|
234
|
-
const expiresAt = tokenResp.expires_in ? Math.floor(Date.now() / 1000) + tokenResp.expires_in : 0;
|
|
235
|
-
return {
|
|
236
|
-
auth_method: 'oauth',
|
|
237
|
-
access_token: tokenResp.access_token,
|
|
238
|
-
refresh_token: tokenResp.refresh_token,
|
|
239
|
-
expires_at: expiresAt,
|
|
240
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async refreshToken(instance, creds) {
|
|
245
|
-
const tokenURL = `${instance.replace(/\/$/, '')}/oauth_token.do`;
|
|
246
|
-
const clientID = getOAuthClientID();
|
|
247
|
-
const body = new URLSearchParams();
|
|
248
|
-
body.set('grant_type', 'refresh_token');
|
|
249
|
-
body.set('client_id', clientID);
|
|
250
|
-
body.set('refresh_token', creds.refresh_token);
|
|
251
|
-
|
|
252
|
-
const resp = await fetch(tokenURL, {
|
|
253
|
-
method: 'POST',
|
|
254
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
255
|
-
body: body.toString(),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
if (!resp.ok) {
|
|
259
|
-
const text = await resp.text();
|
|
260
|
-
throw errAuth(`Token refresh failed: ${text}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const tokenResp = await resp.json();
|
|
264
|
-
const newCreds = {
|
|
265
|
-
auth_method: 'oauth',
|
|
266
|
-
access_token: tokenResp.access_token,
|
|
267
|
-
refresh_token: tokenResp.refresh_token,
|
|
268
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
269
|
-
};
|
|
270
|
-
if (tokenResp.expires_in) {
|
|
271
|
-
newCreds.expires_at = Math.floor(Date.now() / 1000) + tokenResp.expires_in;
|
|
272
|
-
}
|
|
273
|
-
saveCredentials(instance, newCreds);
|
|
274
|
-
return newCreds;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
logout(instance) {
|
|
278
|
-
if (!instance) {
|
|
279
|
-
throw errAuth('No instance specified');
|
|
280
|
-
}
|
|
281
|
-
deleteCredentials(instance);
|
|
282
|
-
}
|
|
283
|
-
}
|
package/src/cli.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// Root CLI using yargs
|
|
2
|
-
|
|
3
|
-
import yargs from 'yargs';
|
|
4
|
-
import { hideBin } from 'yargs/helpers';
|
|
5
|
-
import process from 'node:process';
|
|
6
|
-
import { loadConfig, getEffectiveInstance } from './config.js';
|
|
7
|
-
import { App } from './app.js';
|
|
8
|
-
|
|
9
|
-
// Command modules
|
|
10
|
-
import { setupCmd } from './commands/setup.js';
|
|
11
|
-
import { authCmd } from './commands/auth.js';
|
|
12
|
-
import { profilesCmd } from './commands/profiles.js';
|
|
13
|
-
import { recordsCmd } from './commands/records.js';
|
|
14
|
-
import { incidentsCmd } from './commands/incidents.js';
|
|
15
|
-
import { changesCmd } from './commands/changes.js';
|
|
16
|
-
import { requestsCmd } from './commands/requests.js';
|
|
17
|
-
import { tasksCmd } from './commands/tasks.js';
|
|
18
|
-
import { usersCmd } from './commands/users.js';
|
|
19
|
-
import { groupsCmd } from './commands/groups.js';
|
|
20
|
-
import { groupMembersCmd } from './commands/groupmembers.js';
|
|
21
|
-
import { groupRolesCmd } from './commands/grouproles.js';
|
|
22
|
-
import { ticketsCmd } from './commands/tickets.js';
|
|
23
|
-
import { versionCmd } from './commands/version.js';
|
|
24
|
-
import { devCmd } from './commands/dev.js';
|
|
25
|
-
|
|
26
|
-
function wrap(handler) {
|
|
27
|
-
return async (argv) => {
|
|
28
|
-
try {
|
|
29
|
-
const app = argv.app;
|
|
30
|
-
if (!app) {
|
|
31
|
-
process.stderr.write('Error: App context not initialized.\n');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
await handler(argv, app);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
const app = argv.app;
|
|
37
|
-
if (app) {
|
|
38
|
-
app.err(err);
|
|
39
|
-
} else {
|
|
40
|
-
process.stderr.write(`Error: ${err.message || err}\n`);
|
|
41
|
-
}
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const cli = yargs(hideBin(process.argv))
|
|
48
|
-
.scriptName('jsn')
|
|
49
|
-
.usage('Usage: $0 <command> [options]')
|
|
50
|
-
.option('instance', {
|
|
51
|
-
describe: 'ServiceNow instance URL (e.g., https://dev12345.service-now.com)',
|
|
52
|
-
type: 'string',
|
|
53
|
-
global: true,
|
|
54
|
-
})
|
|
55
|
-
.option('profile', {
|
|
56
|
-
alias: 'p',
|
|
57
|
-
describe: 'Configuration profile to use',
|
|
58
|
-
type: 'string',
|
|
59
|
-
global: true,
|
|
60
|
-
})
|
|
61
|
-
.option('format', {
|
|
62
|
-
describe: 'Output format: auto, json, markdown, styled, quiet',
|
|
63
|
-
type: 'string',
|
|
64
|
-
global: true,
|
|
65
|
-
})
|
|
66
|
-
.option('json', {
|
|
67
|
-
describe: 'Output in JSON format',
|
|
68
|
-
type: 'boolean',
|
|
69
|
-
global: true,
|
|
70
|
-
})
|
|
71
|
-
.option('quiet', {
|
|
72
|
-
alias: 'q',
|
|
73
|
-
describe: 'Output only data, no envelope',
|
|
74
|
-
type: 'boolean',
|
|
75
|
-
global: true,
|
|
76
|
-
})
|
|
77
|
-
.option('styled', {
|
|
78
|
-
describe: 'Force styled output',
|
|
79
|
-
type: 'boolean',
|
|
80
|
-
global: true,
|
|
81
|
-
})
|
|
82
|
-
.option('markdown', {
|
|
83
|
-
describe: 'Output in Markdown format',
|
|
84
|
-
type: 'boolean',
|
|
85
|
-
global: true,
|
|
86
|
-
})
|
|
87
|
-
.middleware((argv) => {
|
|
88
|
-
// Determine format from flags
|
|
89
|
-
let format = 'auto';
|
|
90
|
-
if (argv.json) format = 'json';
|
|
91
|
-
else if (argv.quiet) format = 'quiet';
|
|
92
|
-
else if (argv.styled) format = 'styled';
|
|
93
|
-
else if (argv.markdown) format = 'markdown';
|
|
94
|
-
else if (argv.format) format = argv.format;
|
|
95
|
-
|
|
96
|
-
const cfg = loadConfig({
|
|
97
|
-
instance: argv.instance,
|
|
98
|
-
profile: argv.profile,
|
|
99
|
-
format,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
argv.app = new App(cfg);
|
|
103
|
-
|
|
104
|
-
// Check auth for non-auth commands
|
|
105
|
-
const cmd = argv._[0];
|
|
106
|
-
const skipAuth = ['help', 'version', 'setup', 'auth', undefined].includes(cmd);
|
|
107
|
-
if (!skipAuth) {
|
|
108
|
-
const instance = getEffectiveInstance(cfg);
|
|
109
|
-
if (!argv.app.auth.isAuthenticated() && instance) {
|
|
110
|
-
process.stderr.write(`\nโ ๏ธ Not authenticated to ${instance}\n\n`);
|
|
111
|
-
process.stderr.write('To get started, run:\n');
|
|
112
|
-
process.stderr.write(' jsn setup # Interactive setup\n');
|
|
113
|
-
process.stderr.write(` jsn auth login ${instance} # Login to instance\n\n`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Print context header for interactive terminals
|
|
118
|
-
if (!['help', 'version', 'completion'].includes(cmd)) {
|
|
119
|
-
argv.app.printContextHeader();
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
.command(setupCmd(wrap))
|
|
123
|
-
.command(authCmd(wrap))
|
|
124
|
-
.command(profilesCmd(wrap))
|
|
125
|
-
.command(recordsCmd(wrap))
|
|
126
|
-
.command(incidentsCmd(wrap))
|
|
127
|
-
.command(changesCmd(wrap))
|
|
128
|
-
.command(requestsCmd(wrap))
|
|
129
|
-
.command(tasksCmd(wrap))
|
|
130
|
-
.command(usersCmd(wrap))
|
|
131
|
-
.command(groupsCmd(wrap))
|
|
132
|
-
.command(groupMembersCmd(wrap))
|
|
133
|
-
.command(groupRolesCmd(wrap))
|
|
134
|
-
.command(ticketsCmd(wrap))
|
|
135
|
-
.command(devCmd(wrap))
|
|
136
|
-
.command(versionCmd(wrap))
|
|
137
|
-
.demandCommand(1, 'You must specify a command')
|
|
138
|
-
.help('help', 'Show help')
|
|
139
|
-
.version(false)
|
|
140
|
-
.strictCommands()
|
|
141
|
-
.strictOptions(false)
|
|
142
|
-
.epilogue('TIPS\n'
|
|
143
|
-
+ ' --query is available on every list command (e.g. "incidents list --query priority=1")\n'
|
|
144
|
-
+ ' Use "jsn <command> --help" for details, or "jsn <command> list --help" for list options');
|