@jacebenson/jsn 4.3.7 → 4.3.8-dev.46

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.
Files changed (41) hide show
  1. package/README.md +266 -1
  2. package/package.json +1 -1
  3. package/skills/servicenow/SKILL.md +16 -0
  4. package/src/app.js +8 -1
  5. package/src/auth.js +14 -12
  6. package/src/cli-adapter.js +162 -11
  7. package/src/cli.js +121 -13
  8. package/src/commands/_generic.js +3 -2
  9. package/src/commands/_simple.js +1 -1
  10. package/src/commands/actions.js +156 -0
  11. package/src/commands/auth.js +21 -11
  12. package/src/commands/cmdb.js +18 -0
  13. package/src/commands/diff.js +205 -0
  14. package/src/commands/docs/docs.js +18 -16
  15. package/src/commands/flows.js +149 -47
  16. package/src/commands/graphql.js +95 -0
  17. package/src/commands/groupmembers.js +19 -4
  18. package/src/commands/grouproles.js +15 -4
  19. package/src/commands/midservers.js +708 -0
  20. package/src/commands/perf.js +36 -9
  21. package/src/commands/platform.js +202 -0
  22. package/src/commands/records.js +19 -15
  23. package/src/commands/skill.js +5 -2
  24. package/src/commands/transactions.js +78 -0
  25. package/src/commands/updatesets.js +77 -11
  26. package/src/errors.js +71 -6
  27. package/src/flow-create.js +884 -0
  28. package/src/flow-designer-interactive.js +417 -0
  29. package/src/flow-inspection.js +15 -5
  30. package/src/flow-publish.js +32 -0
  31. package/src/flow-test.js +122 -0
  32. package/src/flow-triggers.js +448 -0
  33. package/src/flow-versioning.js +53 -0
  34. package/src/help.js +93 -28
  35. package/src/helpers.js +124 -9
  36. package/src/midserver-direct.js +312 -0
  37. package/src/midserver-install.js +170 -0
  38. package/src/midserver-zip.js +107 -0
  39. package/src/output.js +7 -0
  40. package/src/processflow-actions.js +366 -0
  41. package/src/sdk.js +8 -0
package/README.md CHANGED
@@ -31,6 +31,19 @@ npm install -g @jacebenson/jsn
31
31
 
32
32
  **Note:** git installs build `better-sqlite3` (docs search) from source, so the machine needs build tools (Python, make, a C++ compiler).
33
33
 
34
+ ## Direct MID installation
35
+
36
+ ```sh
37
+ jsn midservers add --install-method direct --name worker1
38
+ ```
39
+
40
+ Official x86-64 ZIPs for Linux and Windows, selected from the instance's
41
+ `mid.version`. Credentials are prompted and validated before installation.
42
+ Linux supports a user-owned background runtime. Windows currently prepares
43
+ private files and offers a foreground console, not native service registration.
44
+ See [direct MID installation](docs/mid-direct-install.md) for security boundaries,
45
+ lifecycle commands and the untested Windows host phases. Docker remains available.
46
+
34
47
  ## Quick Start
35
48
 
36
49
  ```bash
@@ -59,6 +72,7 @@ jsn flows executions --since "2026-08-25 00:00:00" --until "2026-08-26 00:00:00"
59
72
  jsn flows executions --summary # Server totals + sampled duration metrics by flow
60
73
  jsn flows executions --limit 100 # Inspect more rows from the matching population
61
74
  jsn flows executions --record <sys_id> # Executions for one source record
75
+ jsn flows create --data-file flow.manifest.json # Create a draft record-triggered flow
62
76
  jsn rules list --query "collection=incident"
63
77
  jsn updatesets set "My Feature"
64
78
 
@@ -72,10 +86,19 @@ jsn records bulk --table incident --query "priority=1" --set '{"state":"3"}' #
72
86
  jsn records get --table incident --sys-id 8a1234abcd5678 --attachments # record + its files
73
87
  jsn records list --table incident --limit 10 --csv # output CSV
74
88
 
89
+ # Read-only cross-profile record diff
90
+ jsn diff record --table incident --sys-id <sys_id> --profile-a dev --profile-b prod
91
+ jsn diff record --table incident --sys-id <sys_id> --profile-a dev --profile-b prod --ignore state
92
+ jsn diff record --table incident --sys-id <sys_id> --profile-a dev --profile-b prod --no-ignore
93
+
75
94
  # Script execution
76
95
  jsn eval "gs.info('Hello World')"
77
96
  cat script.js | jsn eval --stdin # pipe a script in (no shell escaping)
78
97
 
98
+ # Now GraphQL (documents may contain mutations; read-only profiles are blocked)
99
+ jsn graphql --query '{ GlideRecord_Query { incident(pagination: {limit: 2}) { _results { number { value } } } } }'
100
+ cat incident.graphql | jsn graphql --stdin --variables '{"limit":2}'
101
+
79
102
  # Saved query snippets (stored locally, run against the active profile)
80
103
  jsn snippets save open-inc --table incident --query "active=true"
81
104
  jsn snippets run open-inc
@@ -84,8 +107,223 @@ jsn snippets run open-inc
84
107
  jsn logs follow --level error --tail 10
85
108
  ```
86
109
 
110
+ ### Update set completion
111
+
112
+ ```bash
113
+ jsn updatesets complete # Current set, opens a notes editor in a human terminal
114
+ jsn updatesets complete "My Feature" --notes "Fixed validation" --force --json
115
+ ```
116
+
117
+ `complete [name]` saves concise bullet-list notes in the selected set's Description
118
+ and marks it complete. Omit the name to use the current set. Explicit names
119
+ must be unique; for duplicate names, select the intended set with `updatesets set`
120
+ and complete it without a name. ServiceNow's normal batch completion rules still
121
+ apply: completing a parent can complete its children. Notes are not copied to children.
122
+
123
+ Existing Description text is preserved exactly. The command shows it and asks
124
+ before appending notes, unless `--force` or the profile's `skip_confirmations`
125
+ policy explicitly allows the append. Scripts and JSON output must supply
126
+ nonempty `--notes`; they never open prompts. Separate multiple notes with real
127
+ newlines. Success requires readback of both Description and state. If verification
128
+ fails, inspect the set before retrying because the write may have partially succeeded.
129
+
130
+ ### Flow Designer actions
131
+
132
+ `actions` and `action` are the same command. `list`, `show`, and explicit
133
+ `delete` remain available. In a human terminal, omit the target to open a
134
+ searchable picker. Create and edit guide you through provider schemas and save
135
+ full Process Flow definitions, not ordinary table-field updates.
136
+
137
+ ```bash
138
+ jsn actions create # Guided scope, name, schemas, inputs and script
139
+ jsn actions edit # Pick an action, edit its saved definition
140
+ jsn actions definition # Pick and inspect a complete definition
141
+ jsn actions test --wait # Pick an action, supply saved-schema inputs, confirm
142
+ jsn actions step-types # Pick the exact transaction scope
143
+ ```
144
+
145
+ Script inputs offer a file path or your `$VISUAL`/`$EDITOR`. New script steps can
146
+ declare outputs and map them to action outputs. Variable declarations reuse
147
+ provider schema templates and receive fresh UI identities. Unknown nested
148
+ schema fields are retained. Unsupported input types stop with instructions to
149
+ use `--data-file`; JSN does not silently omit them or invent schema defaults.
150
+ Creation waits for an explicit final confirmation before allocating a parent.
151
+ It saves a draft only and never publishes or runs it automatically.
152
+
153
+ For automation, keep using explicit identifiers and full JSON:
154
+
155
+ ```bash
156
+ jsn actions step-types --scope <scope_sys_id> --get data > step-types.json
157
+ jsn actions create --scope <scope_sys_id> --data-file new-action.json --json
158
+ jsn actions definition <action_sys_id> --get data > action.json
159
+ # Edit action.json, retaining its ID, scope, lifecycle fields and all arrays.
160
+ jsn actions update <action_sys_id> --data-file action.json --json
161
+ jsn action edit <action_sys_id> --data-file action.json --json # same operation
162
+ jsn actions test <action_sys_id> --force --output-map '{"response":"hello"}' --wait --timeout 60 --json
163
+ jsn actions test <action_sys_id> --force --data-file action.json --output-map '{"response":"hello"}' --wait --get data.outputs.response.value
164
+ jsn actions delete <action_sys_id> --force # explicit cleanup, never automatic
165
+ ```
166
+
167
+ Existing targets infer their exact transaction scope from the action record.
168
+ An explicit `--scope` must match that record byte-for-byte and exist in
169
+ `sys_scope`. For Global, use the literal sys_id `global`.
170
+
171
+ This **illustrative create document** shows an input/script/output mapping.
172
+ Replace `<SCRIPT_STEP_TYPE_SYS_ID>` with the target's `step-types` result.
173
+ Start from that step type's full input/output schemas, preserving runtime
174
+ settings, status outputs and any other required fields. The shortened example
175
+ is not a captured or instance-verified template. New steps/variables need their
176
+ own identifiers; do not clone another action's record IDs. Leave new steps'
177
+ `step_id` and `action` blank or omitted. Create binds their `action` to the newly
178
+ allocated parent; persisted step IDs or parent bindings are rejected before creation.
179
+
180
+ ```json
181
+ {
182
+ "name": "Echo response",
183
+ "description": "Return the supplied response",
184
+ "internal_name": "echo_response",
185
+ "inputs": [{"name": "response", "label": "Response", "type": "string", "mandatory": true}],
186
+ "outputs": [{"name": "response", "label": "Response", "type": "string", "value": "{{step[11111111-2222-4333-8444-555555555555].response}}"}],
187
+ "steps": [{
188
+ "cid": "11111111-2222-4333-8444-555555555555",
189
+ "step_type": "SCRIPT",
190
+ "step_type_id": "<SCRIPT_STEP_TYPE_SYS_ID>",
191
+ "label": "Echo script",
192
+ "order": 1,
193
+ "inputs": [
194
+ {"name": "required_run_time", "type": "choice", "value": "instance"},
195
+ {"name": "script", "type": "script", "value": "(function execute(inputs, outputs) { outputs.response = inputs.response; })(inputs, outputs);"}
196
+ ],
197
+ "outputs": [],
198
+ "extended_inputs": [{"name": "response", "type": "string", "value": "{{inputs.response}}", "extended": true}],
199
+ "extended_outputs": [{"name": "response", "type": "string", "extended": true}]
200
+ }]
201
+ }
202
+ ```
203
+
204
+ Create first inserts the parent in `sys_hub_action_type_definition`, reads that
205
+ parent's own Process Flow defaults, then saves the inputs, outputs and nonempty
206
+ steps with the internal `/api/now/processflow/action/action_types/{id}` PUT.
207
+ It retains fresh lifecycle/status metadata instead of copying source snapshot
208
+ or status IDs. A subsequent definition GET plus `/step_instances` GET verifies
209
+ scripts, mappings and schemas. Update requires a complete document from
210
+ `definition`, including target-owned lifecycle fields. Omitted fields are
211
+ rejected before mutation. A PUT timeout triggers readback, not another PUT;
212
+ `persisted_after_timeout` means persistence was verified, not that HTTP success
213
+ was received. Failed creation reports the parent ID and leaves cleanup to you.
214
+
215
+ Test sends `action`, `outputMap`, `runOnThread` and `tracingEnabled` to `/test`.
216
+ Despite its wire name, `--output-map` supplies **action inputs**. Without
217
+ `--wait`, success means dispatch only, with `status: dispatched` and no outputs.
218
+ With `--wait`, JSN requires a `COMPLETE` context, blank `error_message`, and each
219
+ declared user output's actual value from `sys_flow_runtime_value`. Output entries
220
+ have `value`, `displayValue` and `hasValue`; false, zero and empty strings count.
221
+ Failures, denied/missing outputs and timeouts exit nonzero. The wait budget
222
+ starts after dispatch, defaults to 60 seconds, and is capped at 3600 seconds.
223
+ Timeout does not cancel the server execution. Draft actions can be tested without
224
+ publication. Testing runs their scripts and can have side effects. It requires
225
+ interactive confirmation or `--force`, unless the profile explicitly skips
226
+ confirmations. Read-only profiles block create/update/delete/test. No publish
227
+ or snapshot command is added.
228
+ These internal endpoints and runtime-table permissions can vary by instance.
229
+
87
230
  ### Flow execution fields
88
231
 
232
+ ### Flow Designer terminal workflows
233
+
234
+ ```bash
235
+ jsn flows create # Guided record trigger and published action inputs
236
+ jsn flows edit # Pick an inactive Draft, edit condition or existing inputs
237
+ jsn flows show # Pick a flow to inspect
238
+ jsn flows status # Pick a flow to diagnose
239
+ jsn flows test # Pick flow and a record from its actual trigger table, confirm
240
+ jsn flows publish # Pick a flow, explicitly confirm activation
241
+ # Automation must explicitly confirm publication too:
242
+ jsn flows publish <flow_sys_id> --force --json
243
+ ```
244
+
245
+ Wizards and omitted-target pickers require both a prompt-capable terminal and
246
+ `auto` output. `--json`, explicit formats, `--get`, `--data`, `--data-file`,
247
+ `--output-map`, pipes, `CI`, `NONINTERACTIVE` and `JSN_NO_PROMPTS=1` never open a
248
+ wizard. In those modes provide identifiers and required scope/record/data
249
+ explicitly. Tests, publish and deletion require `--force` or the profile's
250
+ existing explicit skip-confirmations setting when prompting is unavailable.
251
+ Read-only profiles still block mutations before authentication or prompting.
252
+ Press Escape in record pickers or Ctrl+C in any wizard to cancel. Refusing a
253
+ final confirmation leaves the instance untouched.
254
+
255
+ Existing action/flow scopes come from saved raw identifiers. New scopes offer
256
+ the active application preference if available, then a searchable scope picker.
257
+ No scope name is guessed. Input pills are passed through unchanged.
258
+
259
+ Flow edits are limited to the existing manifest contract. They do not rename
260
+ flows, add/remove steps, implement Flow Logic, subflows, variables or error
261
+ handling. Flow deletion remains unsupported. Action deletion retains its
262
+ existing explicit parent-record cleanup and absence readback, not a new native
263
+ builder deletion lifecycle. See [command coverage and local proof](docs/flow-designer-interactive.md).
264
+
265
+ ### Flow creation manifest
266
+
267
+ `jsn flows create` accepts `--data-file` or inline `--data`, or guides human
268
+ terminal users without those options. The supported slice
269
+ supports one `record_create_or_update` trigger and published Action types whose
270
+ input schemas are available from the instance. JSN resolves action metadata,
271
+ defaults, and typed inputs from ServiceNow; callers do not provide GraphQL or
272
+ editor-generated identifiers.
273
+
274
+ ```json
275
+ {
276
+ "name": "JSN-STUDY-ticket-log",
277
+ "description": "Log JSN study tickets",
278
+ "scope": "<SCOPE_SYS_ID>",
279
+ "trigger": {
280
+ "type": "record_create_or_update",
281
+ "table": "ticket",
282
+ "condition": "short_descriptionSTARTSWITHJSN-STUDY-"
283
+ },
284
+ "actions": [
285
+ { "type": "Log", "inputs": { "log_level": "info", "log_message": "Ticket received" } }
286
+ ]
287
+ }
288
+ ```
289
+
290
+ `scope` is required; JSN never defaults it to a tenant-specific or captured
291
+ scope. The record-trigger condition currently accepts one simple encoded-query
292
+ predicate (`fieldSTARTSWITHvalue`, `field=value`, `fieldLIKEvalue`, and the
293
+ equivalent single-predicate operators). Compound `^`/`^OR` expressions are
294
+ rejected until their predicate AST mapping is proven against the instance.
295
+
296
+ Creation leaves the flow in Draft and inactive, then creates and reads back an
297
+ Autosave version. `flows publish` remains a separate lifecycle operation: it
298
+ reads the saved ProcessFlow definition, POSTs `/snapshot`, creates an
299
+ `Activate/Publish` version, reads version history, and verifies `active=true`.
300
+ Do not use it in a dry-run: publishing activates the flow.
301
+
302
+ `jsn flows test <identifier> --record <sys_id>` is also a mutating operation.
303
+ It requires confirmation (or `--force`), reads the saved definition, verifies
304
+ that the supplied record exists in the record-trigger table, and POSTs the full
305
+ definition to the ProcessFlow test endpoint. A response with only a context ID
306
+ is reported as accepted, not completed. Test runs execute actions and can have
307
+ side effects; this slice does not execute test flows during development.
308
+
309
+ Flow Logic, subflows, flow-variable lifecycle, and error handling are deferred
310
+ and rejected by this slice rather than silently ignored.
311
+
312
+ `jsn flows update <identifier> --data-file update.json` updates supported fields
313
+ of existing inactive Draft flows. The manifest may contain only `trigger` (with
314
+ `table` and/or `condition`) and `actions`. Each action selects one saved instance
315
+ by its one-based `order` and reusable action `type` (name or sys_id). Its `inputs`
316
+ object may change only inputs already present in that saved Action. The CLI reads
317
+ the saved component identities itself; manifests never carry UI UUIDs. Omitted
318
+ trigger fields, Action inputs, metadata, and other flow components are preserved.
319
+ Names must resolve to exactly one flow, and the protected `jace-test-flow` is
320
+ rejected. Update sends the captured GraphQL `triggerInstances.update` and/or
321
+ `actions.update` shapes, reads the full ProcessFlow definition back, creates an
322
+ Autosave version, and reports update-set captures. It never publishes or
323
+ activates the flow. Unsupported Flow Logic, subflows, variables, error handling,
324
+ new Action inputs, ambiguous selectors, active flows, and non-Draft flows are
325
+ rejected before mutation.
326
+
89
327
  `jsn flows executions` reads `sys_flow_context` and returns both the raw row and a normalized `execution` object. JSN discovers the runtime columns from `sys_dictionary` first, then uses these mappings:
90
328
 
91
329
  - `started`: `started`, `start_time`, `started_at`, then `sys_created_on`
@@ -110,10 +348,31 @@ Run `jsn` for the full grouped list. Commands are organized by ServiceNow domain
110
348
 
111
349
  **User Experience** — `forms`, `lists`, `clientscripts`, `uipolicies`, `uiactions`
112
350
 
113
- **Data** — `records` (list/get/create/update/delete/count/bulk/attachments), `tables`, `columns`, `includes`, `import`, `logs` (list/show/follow), `snippets` (save/run), `users`, `groups`
351
+ **Data** — `records` (list/get/create/update/delete/count/bulk/attachments), `diff record` (cross-profile read-only comparison), `tables`, `columns`, `includes`, `import`, `logs` (list/show/follow), `snippets` (save/run), `users`, `groups`
352
+
353
+ **Platform** — `platform health` (cluster records) and `platform stats` (bounded, read-only `/xmlstats.do` summary; allowlisted metrics only)
114
354
 
115
355
  Every command supports `--json`, `--query`, and `--help`.
116
356
 
357
+ For multiline script fields, `--data-file` parses the file as UTF-8 JSON and
358
+ passes the resulting values through unchanged. On Windows PowerShell, generate
359
+ the payload as JSON text and inspect it before invoking JSN, for example:
360
+
361
+ ```powershell
362
+ $script = [string](Get-Content -Path .\Example.script.js -Raw)
363
+ $payload = @{ script = $script; description = 'Example' } |
364
+ ConvertTo-Json -Depth 5 -Compress
365
+ Set-Content -Path .\Example.update.json -Value $payload -Encoding UTF8
366
+ Get-Content .\Example.update.json -Raw | ConvertFrom-Json | Select-Object -ExpandProperty script
367
+ jsn includes update Example --data-file .\Example.update.json --json
368
+ ```
369
+
370
+ `JSON.parse` preserves a JSON string as a JavaScript string, and the SDK
371
+ serializes update payloads with `JSON.stringify`. If the file already contains
372
+ a `{value=...}` or FileSystem-provider representation, that text is external
373
+ to JSN and must be corrected before the update. Use a read-back command or
374
+ `records update --strict` where applicable to verify persisted fields.
375
+
117
376
  ## Instances
118
377
 
119
378
  Switch between instances without re-authenticating.
@@ -162,6 +421,12 @@ export SERVICENOW_OAUTH_TOKEN="***"
162
421
  jsn incidents list
163
422
  ```
164
423
 
424
+ For the credential identity, auth-method, diagnostics, migration, and concurrency contract, see [`docs/AUTH_LIFECYCLE.md`](docs/AUTH_LIFECYCLE.md).
425
+
426
+ 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.
427
+
428
+ `jsn platform stats` uses a fixed `GET /xmlstats.do` request. It never accepts a processor path, query parameters, or request body, and it does not emit raw XML, cookies, credentials, or full node identifiers. Oversized, HTML/login, permission, unsupported, timeout, and malformed responses are reported with bounded collector statuses; missing metrics remain unavailable rather than being reported as zero.
429
+
165
430
  ## Local data
166
431
 
167
432
  JSN stores durable local data under `~/.jsn/`. Documentation lives in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7",
3
+ "version": "4.3.8-dev.46",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,6 +73,22 @@ Pick the most specific tool for the job. **Never default to eval** — it's the
73
73
  | **Docs** | `docs sync`, `docs status`, `docs search`, `docs serve` |
74
74
  | **Developer** | `eval`, `rest`, `skill`, `version` |
75
75
 
76
+ ### Complete an update set
77
+
78
+ ```bash
79
+ jsn updatesets complete "My Feature" --notes "Fixed validation" --force --json
80
+ ```
81
+
82
+ `complete [name]` defaults to the current set when the name is omitted. Explicit
83
+ names must be unique. Supply nonempty `--notes` in automation; real newlines
84
+ separate bullets. A human terminal opens a notes editor when `--notes` is absent.
85
+ Existing Description text is preserved and appended after confirmation. Use
86
+ `--force` only after approval, or the profile's `skip_confirmations` policy.
87
+ Notes apply only to the selected set. ServiceNow's normal batch rules can complete
88
+ child sets when the parent completes; their descriptions are not replaced.
89
+ Both selected-set Description and state are read back before success. On
90
+ verification failure, inspect the set before retrying.
91
+
76
92
  ### Table and raw REST examples
77
93
 
78
94
  ```bash
package/src/app.js CHANGED
@@ -14,6 +14,7 @@ import { saveConfig } from './config.js';
14
14
  import { resolveSession, applySession, contextFromSession, requireSessionInstance } from './session.js';
15
15
  import { getCurrentUser, getCurrentApplication, getCurrentUpdateSet } from './context.js';
16
16
  import { errUsage, errAuth } from './errors.js';
17
+ import { canPrompt } from './helpers.js';
17
18
  import { isMutationCommand } from './mutations.js';
18
19
  import process from 'node:process';
19
20
 
@@ -190,8 +191,14 @@ export class App {
190
191
  this.output.err(error);
191
192
  }
192
193
 
194
+ /**
195
+ * True only when a human can actually answer a prompt. Delegates to
196
+ * canPrompt() so the check is "both stdio descriptors are TTYs AND no
197
+ * agent/CI/opt-out marker is set" — a PTY allocated for an agent
198
+ * (stdout.isTTY true, JSN_NO_PROMPTS/CI set) is NOT interactive.
199
+ */
193
200
  isInteractive() {
194
- return process.stdout.isTTY === true;
201
+ return canPrompt();
195
202
  }
196
203
 
197
204
  requireInstance() {
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
- const filename = sanitizeKeyPart(instance) + '.json';
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);