@jacebenson/jsn 4.3.7-dev.13 → 4.3.7-dev.15
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 +8 -0
- package/package.json +1 -1
- package/src/auth.js +14 -12
- package/src/cli.js +2 -0
- package/src/commands/graphql.js +95 -0
package/README.md
CHANGED
|
@@ -81,6 +81,10 @@ jsn diff record --table incident --sys-id <sys_id> --profile-a dev --profile-b p
|
|
|
81
81
|
jsn eval "gs.info('Hello World')"
|
|
82
82
|
cat script.js | jsn eval --stdin # pipe a script in (no shell escaping)
|
|
83
83
|
|
|
84
|
+
# Now GraphQL (documents may contain mutations; read-only profiles are blocked)
|
|
85
|
+
jsn graphql --query '{ GlideRecord_Query { incident(pagination: {limit: 2}) { _results { number { value } } } } }'
|
|
86
|
+
cat incident.graphql | jsn graphql --stdin --variables '{"limit":2}'
|
|
87
|
+
|
|
84
88
|
# Saved query snippets (stored locally, run against the active profile)
|
|
85
89
|
jsn snippets save open-inc --table incident --query "active=true"
|
|
86
90
|
jsn snippets run open-inc
|
|
@@ -167,6 +171,10 @@ export SERVICENOW_OAUTH_TOKEN="***"
|
|
|
167
171
|
jsn incidents list
|
|
168
172
|
```
|
|
169
173
|
|
|
174
|
+
For the credential identity, auth-method, diagnostics, migration, and concurrency contract, see [`docs/AUTH_LIFECYCLE.md`](docs/AUTH_LIFECYCLE.md).
|
|
175
|
+
|
|
176
|
+
GraphQL uses ServiceNow's `/api/now/graphql` endpoint and sends `{query, variables}` through the authenticated SDK transport. The command preserves GraphQL `data` and `errors`; introspection and available namespaces depend on the target instance.
|
|
177
|
+
|
|
170
178
|
## Local data
|
|
171
179
|
|
|
172
180
|
JSN stores durable local data under `~/.jsn/`. Documentation lives in
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -41,30 +41,32 @@ function credKey(instance, username) {
|
|
|
41
41
|
|
|
42
42
|
// ─── PKCE state persistence (shared with Go version) ───
|
|
43
43
|
|
|
44
|
-
function pkceStatePath(instance) {
|
|
44
|
+
function pkceStatePath(instance, username) {
|
|
45
45
|
const dir = path.join(globalConfigDir(), 'pkce');
|
|
46
46
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
47
|
-
|
|
47
|
+
// A known profile identity gets its own pending-login slot. Keep the bare
|
|
48
|
+
// instance slot for legacy/no-profile flows, where no identity exists yet.
|
|
49
|
+
const filename = credKey(instance, username) + '.json';
|
|
48
50
|
return path.join(dir, filename);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function savePKCEState(instance, pkce) {
|
|
52
|
-
const filePath = pkceStatePath(instance);
|
|
53
|
+
function savePKCEState(instance, pkce, username) {
|
|
54
|
+
const filePath = pkceStatePath(instance, username);
|
|
53
55
|
fs.writeFileSync(filePath, JSON.stringify(pkce, null, 2), { mode: 0o600 });
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function loadPKCEState(instance) {
|
|
58
|
+
function loadPKCEState(instance, username) {
|
|
57
59
|
try {
|
|
58
|
-
const data = fs.readFileSync(pkceStatePath(instance), 'utf-8');
|
|
60
|
+
const data = fs.readFileSync(pkceStatePath(instance, username), 'utf-8');
|
|
59
61
|
return JSON.parse(data);
|
|
60
62
|
} catch {
|
|
61
63
|
return null;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
function removePKCEState(instance) {
|
|
67
|
+
function removePKCEState(instance, username) {
|
|
66
68
|
try {
|
|
67
|
-
fs.unlinkSync(pkceStatePath(instance));
|
|
69
|
+
fs.unlinkSync(pkceStatePath(instance, username));
|
|
68
70
|
} catch {
|
|
69
71
|
// ignore
|
|
70
72
|
}
|
|
@@ -746,7 +748,7 @@ export class AuthManager {
|
|
|
746
748
|
instanceURL = normalizeInstanceURL(instanceURL);
|
|
747
749
|
const clientID = getOAuthClientID();
|
|
748
750
|
const pkce = generatePKCE();
|
|
749
|
-
savePKCEState(instanceURL, pkce);
|
|
751
|
+
savePKCEState(instanceURL, pkce, username);
|
|
750
752
|
const url = buildAuthURL(instanceURL, clientID, pkce);
|
|
751
753
|
if (waitFile) {
|
|
752
754
|
console.log(url);
|
|
@@ -766,7 +768,7 @@ export class AuthManager {
|
|
|
766
768
|
const code = fs.readFileSync(filePath, 'utf-8').trim();
|
|
767
769
|
if (code) {
|
|
768
770
|
console.log(`\nAuthorization code found in ${filePath}`);
|
|
769
|
-
removePKCEState(instanceURL);
|
|
771
|
+
removePKCEState(instanceURL, username);
|
|
770
772
|
const newCreds = await this.exchangeCode(instanceURL, clientID, code, pkce);
|
|
771
773
|
this.saveCredentials(instanceURL, newCreds, username || newCreds.username);
|
|
772
774
|
console.log('Token exchange successful!\n');
|
|
@@ -838,7 +840,7 @@ export class AuthManager {
|
|
|
838
840
|
async loginWithCode(instanceURL, code, username = this._activeUsername()) {
|
|
839
841
|
instanceURL = normalizeInstanceURL(instanceURL);
|
|
840
842
|
const clientID = getOAuthClientID();
|
|
841
|
-
const pkce = loadPKCEState(instanceURL);
|
|
843
|
+
const pkce = loadPKCEState(instanceURL, username);
|
|
842
844
|
if (!pkce) {
|
|
843
845
|
throw errAuth(
|
|
844
846
|
`No pending login session for ${instanceURL}.\n\n` +
|
|
@@ -848,7 +850,7 @@ export class AuthManager {
|
|
|
848
850
|
` jsn auth login ${instanceURL} --code CODE`
|
|
849
851
|
);
|
|
850
852
|
}
|
|
851
|
-
removePKCEState(instanceURL);
|
|
853
|
+
removePKCEState(instanceURL, username);
|
|
852
854
|
|
|
853
855
|
const newCreds = await this.exchangeCode(instanceURL, clientID, code, pkce);
|
|
854
856
|
this.saveCredentials(instanceURL, newCreds, username || newCreds.username);
|
package/src/cli.js
CHANGED
|
@@ -70,6 +70,7 @@ import { atfCmd } from './commands/atf.js';
|
|
|
70
70
|
import { approvalsCmd } from './commands/approvals.js';
|
|
71
71
|
import { perfCmd } from './commands/perf.js';
|
|
72
72
|
import { diffCmd } from './commands/diff.js';
|
|
73
|
+
import { graphqlCmd } from './commands/graphql.js';
|
|
73
74
|
|
|
74
75
|
function wrap(handler) {
|
|
75
76
|
return async (argv) => {
|
|
@@ -373,6 +374,7 @@ export function buildCLI() {
|
|
|
373
374
|
.command(approvalsCmd(wrap))
|
|
374
375
|
.command(perfCmd(wrap))
|
|
375
376
|
.command(diffCmd(wrap))
|
|
377
|
+
.command(graphqlCmd(wrap))
|
|
376
378
|
.command(evalCmd(wrap))
|
|
377
379
|
.command(restCmd(wrap))
|
|
378
380
|
.command(skillCmd(wrap))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { declareCapabilities } from '../capabilities.js';
|
|
3
|
+
|
|
4
|
+
// GraphQL documents may contain mutations. Gate the whole command so normal
|
|
5
|
+
// instance/auth and read-only protections apply without attempting AST parsing.
|
|
6
|
+
declareCapabilities('graphql', { mutationSubcommands: [''] });
|
|
7
|
+
|
|
8
|
+
function readStdin() {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let data = '';
|
|
11
|
+
process.stdin.setEncoding('utf-8');
|
|
12
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
13
|
+
process.stdin.on('end', () => resolve(data));
|
|
14
|
+
process.stdin.on('error', reject);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveGraphQLInput(argv) {
|
|
19
|
+
const sources = [
|
|
20
|
+
argv.document !== undefined,
|
|
21
|
+
argv.query !== undefined,
|
|
22
|
+
argv.queryFile !== undefined,
|
|
23
|
+
argv.stdin === true,
|
|
24
|
+
].filter(Boolean).length;
|
|
25
|
+
if (sources !== 1) {
|
|
26
|
+
throw new Error('Provide exactly one GraphQL document via positional document, --query, --query-file, or --stdin');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let query;
|
|
30
|
+
if (argv.document !== undefined) query = argv.document;
|
|
31
|
+
else if (argv.query !== undefined) query = argv.query;
|
|
32
|
+
else if (argv.queryFile !== undefined) query = fs.readFileSync(argv.queryFile, 'utf-8');
|
|
33
|
+
else query = await readStdin();
|
|
34
|
+
|
|
35
|
+
query = String(query || '').trim();
|
|
36
|
+
if (!query) throw new Error('GraphQL document must not be empty');
|
|
37
|
+
return query;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseGraphQLVariables(value) {
|
|
41
|
+
if (value === undefined) return {};
|
|
42
|
+
let variables;
|
|
43
|
+
try {
|
|
44
|
+
variables = JSON.parse(value);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error('--variables must be valid JSON');
|
|
47
|
+
}
|
|
48
|
+
if (!variables || typeof variables !== 'object' || Array.isArray(variables)) {
|
|
49
|
+
throw new Error('--variables must be a JSON object');
|
|
50
|
+
}
|
|
51
|
+
return variables;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function graphqlCmd(wrap) {
|
|
55
|
+
return {
|
|
56
|
+
command: 'graphql [document]',
|
|
57
|
+
describe: 'Execute a GraphQL document against ServiceNow Now GraphQL',
|
|
58
|
+
builder: (yargs) => yargs
|
|
59
|
+
.positional('document', {
|
|
60
|
+
describe: 'GraphQL document string',
|
|
61
|
+
type: 'string',
|
|
62
|
+
})
|
|
63
|
+
.option('query', {
|
|
64
|
+
type: 'string',
|
|
65
|
+
describe: 'GraphQL document string (alternative to positional)',
|
|
66
|
+
})
|
|
67
|
+
.option('query-file', {
|
|
68
|
+
type: 'string',
|
|
69
|
+
describe: 'Read the GraphQL document from a file',
|
|
70
|
+
})
|
|
71
|
+
.option('variables', {
|
|
72
|
+
type: 'string',
|
|
73
|
+
describe: 'GraphQL variables as a JSON object',
|
|
74
|
+
})
|
|
75
|
+
.option('stdin', {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
describe: 'Read the GraphQL document from stdin',
|
|
78
|
+
}),
|
|
79
|
+
handler: wrap(async (argv, app) => {
|
|
80
|
+
const query = await resolveGraphQLInput(argv);
|
|
81
|
+
const variables = parseGraphQLVariables(argv.variables);
|
|
82
|
+
const endpoint = `${app.getEffectiveInstance()}/api/now/graphql`;
|
|
83
|
+
const result = await app.sdk.request(endpoint, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify({ query, variables }),
|
|
86
|
+
});
|
|
87
|
+
const errorCount = Array.isArray(result?.errors) ? result.errors.length : 0;
|
|
88
|
+
app.ok(result, {
|
|
89
|
+
summary: errorCount
|
|
90
|
+
? `GraphQL returned ${errorCount} error(s)`
|
|
91
|
+
: 'GraphQL request succeeded',
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|