@tenonhq/dovetail-servicenow 0.0.40 → 0.0.42
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 +89 -1
- package/dist/cli.js +181 -1
- package/dist/client.d.ts +30 -1
- package/dist/client.js +94 -2
- package/dist/createClientFromEnvFile.js +18 -2
- package/dist/flowDesigner/actionTypeApi.d.ts +23 -0
- package/dist/flowDesigner/actionTypeApi.js +47 -0
- package/dist/flowDesigner/buildFlowOrchestrator.d.ts +1 -1
- package/dist/flowDesigner/buildFlowOrchestrator.js +16 -1
- package/dist/flowDesigner/cloneActionType.d.ts +98 -19
- package/dist/flowDesigner/cloneActionType.js +407 -87
- package/dist/flowDesigner/editActionType.d.ts +6 -1
- package/dist/flowDesigner/editActionType.js +5 -5
- package/dist/flowDesigner/index.d.ts +5 -4
- package/dist/flowDesigner/index.js +8 -1
- package/dist/flowDesigner/stepOps.d.ts +21 -0
- package/dist/flowDesigner/stepOps.js +57 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -3
- package/dist/loadEnv.js +7 -0
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +35 -0
- package/dist/mcp/schemas.d.ts +292 -0
- package/dist/mcp/schemas.js +36 -1
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,33 @@ Reads ServiceNow credentials from env vars in this order of precedence:
|
|
|
55
55
|
| User | `SN_USER` | `SN_DEV_USERNAME` | `SN_PROD_USERNAME` |
|
|
56
56
|
| Password | `SN_PASSWORD` | `SN_DEV_PASSWORD` | `SN_PROD_PASSWORD` |
|
|
57
57
|
|
|
58
|
+
### Flow Designer identity (`SN_FLOW_*`)
|
|
59
|
+
|
|
60
|
+
`/api/now/processflow/*` — every Flow Designer authoring call (view/edit/clone/
|
|
61
|
+
publish an action, create/copy/publish a flow) — **cannot carry a REST API access
|
|
62
|
+
policy** on ServiceNow, so under API-key auth (`SN_API_KEY`) those calls 401. They
|
|
63
|
+
authenticate instead with a dedicated basic-auth identity used **only** for
|
|
64
|
+
processflow paths; every other path keeps the main identity:
|
|
65
|
+
|
|
66
|
+
| Field | Preferred | Dev fallback | Prod fallback |
|
|
67
|
+
|----------|--------------------|------------------------|-------------------------|
|
|
68
|
+
| User | `SN_FLOW_USER` | `SN_DEV_FLOW_USER` | `SN_PROD_FLOW_USER` |
|
|
69
|
+
| Password | `SN_FLOW_PASSWORD` | `SN_DEV_FLOW_PASSWORD` | `SN_PROD_FLOW_PASSWORD` |
|
|
70
|
+
|
|
71
|
+
- **Key mode + flow identity** → processflow requests go out as basic auth with the
|
|
72
|
+
flow identity and **no** `x-sn-apikey` header; table/Dovetail requests keep the key.
|
|
73
|
+
- **Basic mode, no flow identity** → unchanged: processflow uses the main `SN_USER`.
|
|
74
|
+
- **Key mode, no flow identity** → a processflow call **throws before sending**, naming
|
|
75
|
+
`SN_FLOW_USER` / `SN_FLOW_PASSWORD` (it would only 401). A half-set pair also throws.
|
|
76
|
+
- Programmatic: `createClient({ apiKey, flowUser, flowPassword })` — explicit config beats
|
|
77
|
+
env. A config that pins the main identity (apiKey or user/password — e.g. one resolved
|
|
78
|
+
from an `--env` file) takes the flow identity from the config only, never from
|
|
79
|
+
`process.env`, so a per-call retarget can't borrow another instance's flow creds.
|
|
80
|
+
- `--env <file>` fully determines both identities: the `SN_FLOW_*` / `SN_DEV_FLOW_*` /
|
|
81
|
+
`SN_PROD_FLOW_*` keys are connection keys, replaced (or cleared) from the file.
|
|
82
|
+
|
|
83
|
+
The flow password is never logged; errors name the variables, never their values.
|
|
84
|
+
|
|
58
85
|
The dev/prod fallbacks match the names documented in the committed
|
|
59
86
|
`Craftsman/.env.example`, so existing developer setups work out of the box.
|
|
60
87
|
Bare instance names (e.g. `TenonWorkStudio`) get `.service-now.com` appended
|
|
@@ -266,8 +293,67 @@ npx dove-sn edit-action --sys-id <id> --scope <scope> --set-script ./script.js \
|
|
|
266
293
|
npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json # dry-run
|
|
267
294
|
npx dove-sn edit-action --sys-id <id> --scope <scope> --from-json ops.json \
|
|
268
295
|
--apply --update-set <id> # publish + verify
|
|
296
|
+
|
|
297
|
+
# Clone a Custom Action Type (every step + its step IO) into a scope and publish it
|
|
298
|
+
npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
|
|
299
|
+
--scope x_cadso_email_spok --ops ops.json # dry-run (plan)
|
|
300
|
+
npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
|
|
301
|
+
--scope x_cadso_email_spok --ops ops.json --update-set <id> --confirm # write + publish + verify
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Cloning an action type (`clone-action` / `action_clone`)
|
|
305
|
+
|
|
306
|
+
`clone-action` copies a Custom Action Type headlessly — **multi-step capable** — and
|
|
307
|
+
publishes the copy:
|
|
308
|
+
|
|
309
|
+
1. **Reads** (Table API only — `sn_build_agent` is never used) the parent
|
|
310
|
+
`sys_hub_action_type_definition`, its `sys_hub_action_input` / `sys_hub_action_output`
|
|
311
|
+
(`model_id` → parent), every `sys_hub_step_instance` (**`action`** → parent), and each
|
|
312
|
+
step's `sys_hub_step_ext_input` / `sys_hub_step_ext_output` (`model_id` → step).
|
|
313
|
+
2. **Plans** fresh sys_ids for every record (old→new step map), the target scope,
|
|
314
|
+
`name` = `--name`, `internal_name` = `--internal-name` or the slug of the name
|
|
315
|
+
(lowercase, non-alphanumerics → `_`), `state = draft`, and strips system/snapshot
|
|
316
|
+
fields (`master_snapshot`, `latest_snapshot`, `sys_update_name`, audit fields, …).
|
|
317
|
+
3. **Writes** the graph through Dovetail `createRecord`, pinned to `--update-set`, scope
|
|
318
|
+
set per record.
|
|
319
|
+
4. **Publishes**: the SOURCE action's steps are fetched from
|
|
320
|
+
`/processflow/action/action_types/{source}/step_instances`, each step's `action` and
|
|
321
|
+
`sys_id` remapped onto the clone, `--ops` applied, then grafted onto the clone's model
|
|
322
|
+
and POSTed to `/snapshot` — no steps fixture needed.
|
|
323
|
+
5. **Verifies** by reading the clone's steps back (script hash + step IO per step, plus
|
|
324
|
+
the step count). A mismatch exits `1`.
|
|
325
|
+
|
|
326
|
+
`--scope` takes a scope **name** (resolved via `sys_scope`) or a 32-hex sys_id. The
|
|
327
|
+
clone is **idempotent** on `(name, scope)`: an existing match returns `unchanged` and
|
|
328
|
+
writes nothing. **Dry-run by default** — without `--confirm` it prints the plan (records
|
|
329
|
+
per table, step summary, the effect of every op) and writes nothing; `--update-set` is
|
|
330
|
+
required with `--confirm`. Exit `0` on success (incl. dry-run / unchanged), `1` on error.
|
|
331
|
+
|
|
332
|
+
`--ops` takes the same step ops as `edit-action --from-json`, plus **`setStepInputs`** —
|
|
333
|
+
set an **existing** step input's value (and its `display_value` when present), e.g. a
|
|
334
|
+
REST step's HTTP method. An unknown input fails with the list of inputs on that step:
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"setStepInputs": [
|
|
339
|
+
{ "step": "REST Step", "input": "http_method", "value": "post" }
|
|
340
|
+
],
|
|
341
|
+
"patchStepScripts": [
|
|
342
|
+
{ "step": "Parse Response", "patchScript": { "find": "v1", "replace": "v2" } }
|
|
343
|
+
],
|
|
344
|
+
"addStepOutputs": [{ "step": "Parse Response", "name": "isRetryable", "type": "boolean" }],
|
|
345
|
+
"addStepInputs": [
|
|
346
|
+
{ "step": "Handle Error", "name": "isRetryable", "type": "boolean",
|
|
347
|
+
"pillFrom": { "step": "Parse Response", "output": "isRetryable" } }
|
|
348
|
+
]
|
|
349
|
+
}
|
|
269
350
|
```
|
|
270
351
|
|
|
352
|
+
The MCP tool **`action_clone`** takes the same inputs — `from`, `name`, `scope`,
|
|
353
|
+
`internalName`, `description`, `updateSetSysId`, `ops` (inline object), `confirm`,
|
|
354
|
+
`dryRun` — with the same dry-run-unless-`confirm:true` gate. `setStepInputs` is also
|
|
355
|
+
accepted by `edit-action` / `action_edit`.
|
|
356
|
+
|
|
271
357
|
### Editing an action type's steps (`--from-json`)
|
|
272
358
|
|
|
273
359
|
The flag form above patches the one auto-detected script. When you need to touch
|
|
@@ -725,7 +811,9 @@ and read-back-verified — `host_assets` (deploy a built dist/), plus the Flow D
|
|
|
725
811
|
tools `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
|
|
726
812
|
type's model), `action_edit` (structurally edit a published action type — per-step
|
|
727
813
|
scripts, step-level inputs/outputs, data-pill wiring — dry-run by default, and the
|
|
728
|
-
publish is read back and verified), `
|
|
814
|
+
publish is read back and verified), `action_clone` (clone an action type — every step
|
|
815
|
+
and its step IO — into a scope and publish + verify it; dry-run by default),
|
|
816
|
+
`flow_publish` (compile a flow/subflow snapshot), `flow_copy`
|
|
729
817
|
(copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
|
|
730
818
|
publish, grafting a template), `flow_test` (validate or run a flow), and
|
|
731
819
|
`flow_edit` (patch a flow), plus `invoke_rest` (invoke an arbitrary authenticated
|
package/dist/cli.js
CHANGED
|
@@ -83,6 +83,7 @@ const copyFlow_1 = require("./flowDesigner/copyFlow");
|
|
|
83
83
|
const createFlow_1 = require("./flowDesigner/createFlow");
|
|
84
84
|
const editFlow_1 = require("./flowDesigner/editFlow");
|
|
85
85
|
const editActionType_1 = require("./flowDesigner/editActionType");
|
|
86
|
+
const cloneActionType_1 = require("./flowDesigner/cloneActionType");
|
|
86
87
|
const testFlow_1 = require("./flowDesigner/testFlow");
|
|
87
88
|
const table_1 = require("./table");
|
|
88
89
|
const setField_1 = require("./setField");
|
|
@@ -929,6 +930,170 @@ async function runEditAction(flags) {
|
|
|
929
930
|
}
|
|
930
931
|
return 0;
|
|
931
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* dove-sn clone-action:
|
|
935
|
+
* --from <sys_id> Required. Source sys_hub_action_type_definition sys_id.
|
|
936
|
+
* --name <name> Required. Display name of the clone (idempotency key with --scope).
|
|
937
|
+
* --scope <name|sys_id> Required. Target scope — a scope name (x_cadso_email_spok) or 32-hex sys_id.
|
|
938
|
+
* --internal-name <name> Optional. Default: slug of --name.
|
|
939
|
+
* --description <text> Optional.
|
|
940
|
+
* --ops <path> Optional. JSON StepOps applied to the cloned steps before publish:
|
|
941
|
+
* patchStepScripts / setStepInputs / addStepOutputs / addStepInputs.
|
|
942
|
+
* --update-set <sys_id> Required with --confirm. Every write + the publish land here.
|
|
943
|
+
* --confirm Execute (write the graph, publish, verify). WITHOUT it: dry-run.
|
|
944
|
+
* --dry-run Force a dry-run even with --confirm.
|
|
945
|
+
* --json Emit the structured CloneActionTypeResult.
|
|
946
|
+
*
|
|
947
|
+
* Clones a Custom Action Type — parent, inputs, outputs, every step instance and
|
|
948
|
+
* its step-level ext inputs/outputs — into the target scope, then publishes it
|
|
949
|
+
* headlessly through the snapshot path (multi-step capable) and reads the steps
|
|
950
|
+
* back to verify. Idempotent on (name, scope). DRY-RUN BY DEFAULT.
|
|
951
|
+
*
|
|
952
|
+
* --ops shape:
|
|
953
|
+
* {
|
|
954
|
+
* "setStepInputs": [{ "step": "REST Step", "input": "http_method", "value": "post" }],
|
|
955
|
+
* "patchStepScripts": [{ "step": "Parse", "patchScript": { "find": "a", "replace": "b" } }],
|
|
956
|
+
* "addStepOutputs": [{ "step": "Parse", "name": "isRetryable", "type": "boolean" }],
|
|
957
|
+
* "addStepInputs": [{ "step": "Handle", "name": "isRetryable", "type": "boolean",
|
|
958
|
+
* "pillFrom": { "step": "Parse", "output": "isRetryable" } }]
|
|
959
|
+
* }
|
|
960
|
+
* `step` is a step cid or label; `scriptFile` (resolved relative to the ops file)
|
|
961
|
+
* is sugar for `setScript` in patchStepScripts.
|
|
962
|
+
*/
|
|
963
|
+
var CLONE_OPS_KEYS = ["patchStepScripts", "setStepInputs", "addStepOutputs", "addStepInputs"];
|
|
964
|
+
async function runCloneAction(flags, bare) {
|
|
965
|
+
var bareErr = bareStringFlagError("clone-action", bare, [
|
|
966
|
+
"from",
|
|
967
|
+
"name",
|
|
968
|
+
"scope",
|
|
969
|
+
"internal-name",
|
|
970
|
+
"description",
|
|
971
|
+
"ops",
|
|
972
|
+
"update-set",
|
|
973
|
+
]);
|
|
974
|
+
if (bareErr) {
|
|
975
|
+
process.stderr.write(bareErr);
|
|
976
|
+
return 1;
|
|
977
|
+
}
|
|
978
|
+
var from = flags.from;
|
|
979
|
+
var name = flags.name;
|
|
980
|
+
var scope = flags.scope;
|
|
981
|
+
if (!from || !name || !scope) {
|
|
982
|
+
process.stderr.write("clone-action: --from <sys_id>, --name <name> and --scope <scope name|sys_id> are required\n");
|
|
983
|
+
return 1;
|
|
984
|
+
}
|
|
985
|
+
var confirm = flags.confirm === "true";
|
|
986
|
+
var dryRun = flags["dry-run"] === "true";
|
|
987
|
+
var updateSet = flags["update-set"] || flags.updateSetSysId;
|
|
988
|
+
if (confirm && !dryRun && !updateSet) {
|
|
989
|
+
process.stderr.write("clone-action: --update-set <sys_id> is required with --confirm\n");
|
|
990
|
+
return 1;
|
|
991
|
+
}
|
|
992
|
+
var stepOps;
|
|
993
|
+
if (flags.ops) {
|
|
994
|
+
var parsedOps = JSON.parse(fs.readFileSync(flags.ops, "utf8"));
|
|
995
|
+
if (!parsedOps || typeof parsedOps !== "object" || Array.isArray(parsedOps)) {
|
|
996
|
+
process.stderr.write("clone-action: --ops must contain a StepOps object\n");
|
|
997
|
+
return 1;
|
|
998
|
+
}
|
|
999
|
+
var opsObj = parsedOps;
|
|
1000
|
+
var opsKeys = Object.keys(opsObj);
|
|
1001
|
+
for (var k = 0; k < opsKeys.length; k += 1) {
|
|
1002
|
+
if (CLONE_OPS_KEYS.indexOf(opsKeys[k]) === -1) {
|
|
1003
|
+
process.stderr.write("clone-action: unknown --ops key '" +
|
|
1004
|
+
opsKeys[k] +
|
|
1005
|
+
"' (allowed: " +
|
|
1006
|
+
CLONE_OPS_KEYS.join(", ") +
|
|
1007
|
+
")\n");
|
|
1008
|
+
return 1;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
resolveScriptFiles(opsObj, flags.ops);
|
|
1012
|
+
stepOps = opsObj;
|
|
1013
|
+
}
|
|
1014
|
+
var result = await (0, cloneActionType_1.cloneActionType)({
|
|
1015
|
+
client: (0, client_1.createClient)({}),
|
|
1016
|
+
sourceSysId: from,
|
|
1017
|
+
newName: name,
|
|
1018
|
+
internalName: flags["internal-name"],
|
|
1019
|
+
newScope: scope,
|
|
1020
|
+
updateSetSysId: updateSet,
|
|
1021
|
+
description: flags.description,
|
|
1022
|
+
stepOps: stepOps,
|
|
1023
|
+
confirm: confirm,
|
|
1024
|
+
dryRun: dryRun,
|
|
1025
|
+
});
|
|
1026
|
+
if (flags.json === "true") {
|
|
1027
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1028
|
+
return result.verify && !result.verify.ok ? 1 : 0;
|
|
1029
|
+
}
|
|
1030
|
+
process.stdout.write("[" + result.action + "] " + name + " (" + result.internalName + ") -> " + result.sysId + "\n");
|
|
1031
|
+
if (result.action === "unchanged") {
|
|
1032
|
+
process.stdout.write(" an action with this name already exists in the target scope — nothing written\n");
|
|
1033
|
+
return 0;
|
|
1034
|
+
}
|
|
1035
|
+
if (result.plan) {
|
|
1036
|
+
process.stdout.write(" scope: " +
|
|
1037
|
+
result.plan.scope.name +
|
|
1038
|
+
" (" +
|
|
1039
|
+
result.plan.scope.sysId +
|
|
1040
|
+
") source scope: " +
|
|
1041
|
+
result.plan.sourceScopeSysId +
|
|
1042
|
+
"\n records: " +
|
|
1043
|
+
result.plan.total +
|
|
1044
|
+
"\n");
|
|
1045
|
+
var tables = Object.keys(result.plan.counts);
|
|
1046
|
+
for (var t = 0; t < tables.length; t += 1) {
|
|
1047
|
+
process.stdout.write(" " + tables[t] + ": " + result.plan.counts[tables[t]] + "\n");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (result.steps) {
|
|
1051
|
+
process.stdout.write("\n--- steps (as published) ---\n");
|
|
1052
|
+
for (var si = 0; si < result.steps.after.length; si += 1) {
|
|
1053
|
+
var step = result.steps.after[si];
|
|
1054
|
+
process.stdout.write(" " +
|
|
1055
|
+
step.label +
|
|
1056
|
+
" (" +
|
|
1057
|
+
step.cid +
|
|
1058
|
+
")" +
|
|
1059
|
+
(step.scriptChars !== null ? " script " + step.scriptChars + " chars" : "") +
|
|
1060
|
+
(step.extendedInputs.length ? " in:" + step.extendedInputs.length : "") +
|
|
1061
|
+
(step.extendedOutputs.length ? " out:" + step.extendedOutputs.length : "") +
|
|
1062
|
+
"\n");
|
|
1063
|
+
}
|
|
1064
|
+
for (var ci = 0; ci < result.steps.changes.length; ci += 1) {
|
|
1065
|
+
process.stdout.write(" + " + result.steps.changes[ci] + "\n");
|
|
1066
|
+
}
|
|
1067
|
+
for (var wi = 0; wi < result.steps.warnings.length; wi += 1) {
|
|
1068
|
+
process.stdout.write(" ! " + result.steps.warnings[wi] + "\n");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (result.action === "planned") {
|
|
1072
|
+
process.stdout.write("\nDRY RUN — nothing written. Re-run with --confirm --update-set <sys_id> to clone + publish.\n");
|
|
1073
|
+
return 0;
|
|
1074
|
+
}
|
|
1075
|
+
process.stdout.write("\nwritten: " +
|
|
1076
|
+
result.written.length +
|
|
1077
|
+
" record(s)" +
|
|
1078
|
+
(result.publish
|
|
1079
|
+
? "; published (HTTP " +
|
|
1080
|
+
result.publish.httpStatus +
|
|
1081
|
+
(result.publish.snapshotSysId ? ", snapshot " + result.publish.snapshotSysId : "") +
|
|
1082
|
+
")"
|
|
1083
|
+
: "") +
|
|
1084
|
+
"\n");
|
|
1085
|
+
if (result.verify) {
|
|
1086
|
+
process.stdout.write("\n--- verify (read back from the instance) ---\n");
|
|
1087
|
+
process.stdout.write(" " + (result.verify.ok ? "OK" : "FAILED") + "\n");
|
|
1088
|
+
for (var vi = 0; vi < result.verify.notes.length; vi += 1) {
|
|
1089
|
+
process.stdout.write(" " + (result.verify.ok ? "+ " : "! ") + result.verify.notes[vi] + "\n");
|
|
1090
|
+
}
|
|
1091
|
+
if (!result.verify.ok) {
|
|
1092
|
+
return 1;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return 0;
|
|
1096
|
+
}
|
|
932
1097
|
async function runMcp(flags) {
|
|
933
1098
|
if (flags.smoke === "true") {
|
|
934
1099
|
await (0, server_1.runSmoke)();
|
|
@@ -1041,6 +1206,15 @@ function printHelp() {
|
|
|
1041
1206
|
" (per-step scripts + step IO + data-pill wiring)\n" +
|
|
1042
1207
|
' | --patch-script "<find>::<replace>" | --set-script <path> | --merge-outputs <path>\n' +
|
|
1043
1208
|
" [--script-input <name>] [--update-set <sys_id>] [--apply] [--json])\n" +
|
|
1209
|
+
" clone-action Clone a Custom Action Type (all steps + step IO) into a scope and publish it\n" +
|
|
1210
|
+
" DRY-RUN BY DEFAULT — nothing is written without --confirm\n" +
|
|
1211
|
+
" (--from <sys_id> --name <n> --scope <scope name|sys_id>\n" +
|
|
1212
|
+
" [--internal-name <n>] [--description <d>]\n" +
|
|
1213
|
+
" [--ops <ops.json>] ops: setStepInputs / patchStepScripts /\n" +
|
|
1214
|
+
" addStepOutputs / addStepInputs\n" +
|
|
1215
|
+
" [--update-set <sys_id> (required with --confirm)] [--confirm] [--dry-run] [--json])\n" +
|
|
1216
|
+
" Idempotent on (name, scope). Publishes via the snapshot path and\n" +
|
|
1217
|
+
" reads the steps back to verify.\n" +
|
|
1044
1218
|
" edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
|
|
1045
1219
|
" (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
|
|
1046
1220
|
" publish-app Publish a scoped app to the ServiceNow Store, the company application\n" +
|
|
@@ -1077,7 +1251,10 @@ function printHelp() {
|
|
|
1077
1251
|
" or the DOVETAIL_ENV_FILE env var). A bare name like 'prod'\n" +
|
|
1078
1252
|
" resolves to .env.prod in the cwd. The file's SN_* connection\n" +
|
|
1079
1253
|
" vars replace any already exported; a missing or incomplete\n" +
|
|
1080
|
-
" file is an error (no fallback). Default: .env in the cwd.\n"
|
|
1254
|
+
" file is an error (no fallback). Default: .env in the cwd.\n" +
|
|
1255
|
+
" Flow Designer auth /api/now/processflow/* can't carry an API access policy, so\n" +
|
|
1256
|
+
" under SN_API_KEY those calls use a dedicated basic-auth identity:\n" +
|
|
1257
|
+
" SN_FLOW_USER / SN_FLOW_PASSWORD (or SN_DEV_FLOW_* / SN_PROD_FLOW_*).\n");
|
|
1081
1258
|
}
|
|
1082
1259
|
/** Parse inline `--columns "Label:type:max, Other:choice, ..."` into ColumnSpec[]. */
|
|
1083
1260
|
function parseColumnsInline(input) {
|
|
@@ -2244,6 +2421,9 @@ async function main() {
|
|
|
2244
2421
|
if (parsed.command === "edit-action") {
|
|
2245
2422
|
return await runEditAction(parsed.flags);
|
|
2246
2423
|
}
|
|
2424
|
+
if (parsed.command === "clone-action") {
|
|
2425
|
+
return await runCloneAction(parsed.flags, parsed.bare);
|
|
2426
|
+
}
|
|
2247
2427
|
if (parsed.command === "create-view") {
|
|
2248
2428
|
await runCreateView(parsed.flags);
|
|
2249
2429
|
return 0;
|
package/dist/client.d.ts
CHANGED
|
@@ -29,6 +29,33 @@ export type ResolvedAuth = {
|
|
|
29
29
|
user: string;
|
|
30
30
|
password: string;
|
|
31
31
|
};
|
|
32
|
+
/** Path prefix of the Flow Designer authoring API — the only paths the flow identity is used for. */
|
|
33
|
+
export declare var PROCESSFLOW_PATH_PREFIX: string;
|
|
34
|
+
/**
|
|
35
|
+
* Resolved Flow Designer (processflow) identity:
|
|
36
|
+
* - "basic" — a complete SN_FLOW_USER / SN_FLOW_PASSWORD pair;
|
|
37
|
+
* - "partial" — only one half was set (reported, never sent);
|
|
38
|
+
* - "none" — no flow identity configured.
|
|
39
|
+
*/
|
|
40
|
+
export type ResolvedFlowAuth = {
|
|
41
|
+
mode: "basic";
|
|
42
|
+
user: string;
|
|
43
|
+
password: string;
|
|
44
|
+
} | {
|
|
45
|
+
mode: "partial";
|
|
46
|
+
missing: string;
|
|
47
|
+
} | {
|
|
48
|
+
mode: "none";
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Explicit config beats env. When the config pins the MAIN identity (apiKey or
|
|
52
|
+
* user/password — e.g. a config resolved from an --env file), the flow identity
|
|
53
|
+
* is taken from the config ONLY, so a per-call retarget can never borrow the
|
|
54
|
+
* process's flow credentials for a different instance.
|
|
55
|
+
*/
|
|
56
|
+
export declare function resolveFlowAuth(cfg: ServiceNowClientConfig): ResolvedFlowAuth;
|
|
57
|
+
/** True when an instance-relative request URL targets the processflow API. */
|
|
58
|
+
export declare function isProcessflowPath(url: unknown): boolean;
|
|
32
59
|
export interface TableQueryOptions {
|
|
33
60
|
limit?: number;
|
|
34
61
|
fields?: string[];
|
|
@@ -137,7 +164,9 @@ export interface ServiceNowClient {
|
|
|
137
164
|
now: {
|
|
138
165
|
/**
|
|
139
166
|
* GET an arbitrary native ServiceNow REST path (e.g. /api/now/processflow/...).
|
|
140
|
-
*
|
|
167
|
+
* Same credentials/retry/throttle as the rest of the client, except that
|
|
168
|
+
* /api/now/processflow/* paths authenticate with the dedicated Flow Designer
|
|
169
|
+
* identity (SN_FLOW_USER / SN_FLOW_PASSWORD) when one is configured.
|
|
141
170
|
* Use for endpoints that aren't the Table API or the Dovetail Scripted REST API
|
|
142
171
|
* — currently the Flow Designer processflow endpoints. Returns the raw response body.
|
|
143
172
|
*/
|
package/dist/client.js
CHANGED
|
@@ -24,6 +24,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
24
24
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
25
|
};
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.PROCESSFLOW_PATH_PREFIX = void 0;
|
|
28
|
+
exports.resolveFlowAuth = resolveFlowAuth;
|
|
29
|
+
exports.isProcessflowPath = isProcessflowPath;
|
|
27
30
|
exports.createClient = createClient;
|
|
28
31
|
const axios_1 = __importDefault(require("axios"));
|
|
29
32
|
/**
|
|
@@ -86,6 +89,46 @@ function resolveAuth(cfg) {
|
|
|
86
89
|
}
|
|
87
90
|
return { mode: "basic", user: user, password: password };
|
|
88
91
|
}
|
|
92
|
+
/** Path prefix of the Flow Designer authoring API — the only paths the flow identity is used for. */
|
|
93
|
+
exports.PROCESSFLOW_PATH_PREFIX = "/api/now/processflow/";
|
|
94
|
+
/**
|
|
95
|
+
* Explicit config beats env. When the config pins the MAIN identity (apiKey or
|
|
96
|
+
* user/password — e.g. a config resolved from an --env file), the flow identity
|
|
97
|
+
* is taken from the config ONLY, so a per-call retarget can never borrow the
|
|
98
|
+
* process's flow credentials for a different instance.
|
|
99
|
+
*/
|
|
100
|
+
function resolveFlowAuth(cfg) {
|
|
101
|
+
var cfgPinsIdentity = Boolean(cfg.apiKey || cfg.user || cfg.password || cfg.flowUser || cfg.flowPassword);
|
|
102
|
+
var user = cfg.flowUser || "";
|
|
103
|
+
var password = cfg.flowPassword || "";
|
|
104
|
+
if (!cfgPinsIdentity) {
|
|
105
|
+
user = process.env.SN_FLOW_USER
|
|
106
|
+
|| process.env.SN_DEV_FLOW_USER
|
|
107
|
+
|| process.env.SN_PROD_FLOW_USER
|
|
108
|
+
|| "";
|
|
109
|
+
password = process.env.SN_FLOW_PASSWORD
|
|
110
|
+
|| process.env.SN_DEV_FLOW_PASSWORD
|
|
111
|
+
|| process.env.SN_PROD_FLOW_PASSWORD
|
|
112
|
+
|| "";
|
|
113
|
+
}
|
|
114
|
+
if (user && password) {
|
|
115
|
+
return { mode: "basic", user: user, password: password };
|
|
116
|
+
}
|
|
117
|
+
if (user) {
|
|
118
|
+
return { mode: "partial", missing: "SN_FLOW_PASSWORD" };
|
|
119
|
+
}
|
|
120
|
+
if (password) {
|
|
121
|
+
return { mode: "partial", missing: "SN_FLOW_USER" };
|
|
122
|
+
}
|
|
123
|
+
return { mode: "none" };
|
|
124
|
+
}
|
|
125
|
+
/** True when an instance-relative request URL targets the processflow API. */
|
|
126
|
+
function isProcessflowPath(url) {
|
|
127
|
+
if (typeof url !== "string") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return url.toLowerCase().indexOf(exports.PROCESSFLOW_PATH_PREFIX) === 0;
|
|
131
|
+
}
|
|
89
132
|
function sleep(ms) {
|
|
90
133
|
return new Promise(function (resolve) {
|
|
91
134
|
setTimeout(resolve, ms);
|
|
@@ -130,6 +173,49 @@ function createClient(config = {}) {
|
|
|
130
173
|
headers: baseHeaders,
|
|
131
174
|
validateStatus: function () { return true; }
|
|
132
175
|
});
|
|
176
|
+
// Flow Designer (processflow) identity. processflow cannot carry a REST API
|
|
177
|
+
// access policy, so under API-key auth those calls must go out as basic auth
|
|
178
|
+
// with a dedicated identity. The transport is created lazily on the first
|
|
179
|
+
// processflow request and deliberately carries NO x-sn-apikey header.
|
|
180
|
+
var flowAuth = resolveFlowAuth(config);
|
|
181
|
+
var flowHttp = null;
|
|
182
|
+
/**
|
|
183
|
+
* Pick the transport for a request. Non-processflow paths always use the
|
|
184
|
+
* main client. A processflow path uses the flow identity when configured,
|
|
185
|
+
* the main client when it is already basic auth, and otherwise throws BEFORE
|
|
186
|
+
* sending — an API-key processflow call is a guaranteed 401.
|
|
187
|
+
*/
|
|
188
|
+
function transportFor(cfg) {
|
|
189
|
+
if (!isProcessflowPath(cfg.url)) {
|
|
190
|
+
return { http: http, flow: false };
|
|
191
|
+
}
|
|
192
|
+
if (flowAuth.mode === "partial") {
|
|
193
|
+
throw new Error("Flow Designer identity is incomplete: " + flowAuth.missing + " is not set. " +
|
|
194
|
+
"Set both SN_FLOW_USER and SN_FLOW_PASSWORD (or SN_DEV_FLOW_* / SN_PROD_FLOW_*) " +
|
|
195
|
+
"to authenticate /api/now/processflow/* requests.");
|
|
196
|
+
}
|
|
197
|
+
if (flowAuth.mode === "basic") {
|
|
198
|
+
if (!flowHttp) {
|
|
199
|
+
flowHttp = axios_1.default.create({
|
|
200
|
+
baseURL: "https://" + host,
|
|
201
|
+
auth: { username: flowAuth.user, password: flowAuth.password },
|
|
202
|
+
headers: {
|
|
203
|
+
accept: "application/json",
|
|
204
|
+
"content-type": "application/json"
|
|
205
|
+
},
|
|
206
|
+
validateStatus: function () { return true; }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return { http: flowHttp, flow: true };
|
|
210
|
+
}
|
|
211
|
+
if (creds.mode === "basic") {
|
|
212
|
+
return { http: http, flow: false };
|
|
213
|
+
}
|
|
214
|
+
throw new Error("Flow Designer requests (/api/now/processflow/*) cannot use API-key auth: ServiceNow " +
|
|
215
|
+
"cannot attach a REST API access policy to processflow, so the call would 401. " +
|
|
216
|
+
"Set SN_FLOW_USER and SN_FLOW_PASSWORD (a dedicated basic-auth identity used ONLY for " +
|
|
217
|
+
"processflow) in your env file, or pass { flowUser, flowPassword } to createClient.");
|
|
218
|
+
}
|
|
133
219
|
var lastAt = 0;
|
|
134
220
|
// Dovetail core Scripted REST API: prefer the Dovetail-app path
|
|
135
221
|
// /api/cadso/dovetail_core/* and fall back to the legacy global-scope path
|
|
@@ -148,6 +234,12 @@ function createClient(config = {}) {
|
|
|
148
234
|
async function requestRaw(cfg, ctx, passThrough) {
|
|
149
235
|
var attempt429 = 0;
|
|
150
236
|
var attempt5xx = 0;
|
|
237
|
+
// Resolved before the first send: a processflow call with no usable
|
|
238
|
+
// identity throws here, never reaching the network.
|
|
239
|
+
var transport = transportFor(cfg);
|
|
240
|
+
var authHint = transport.flow
|
|
241
|
+
? "check SN_FLOW_USER/SN_FLOW_PASSWORD (the Flow Designer identity) and its roles."
|
|
242
|
+
: "check SN_USER/SN_PASSWORD and ACLs.";
|
|
151
243
|
// eslint-disable-next-line no-constant-condition
|
|
152
244
|
while (true) {
|
|
153
245
|
var elapsed = Date.now() - lastAt;
|
|
@@ -157,7 +249,7 @@ function createClient(config = {}) {
|
|
|
157
249
|
lastAt = Date.now();
|
|
158
250
|
var res;
|
|
159
251
|
try {
|
|
160
|
-
res = await http.request(cfg);
|
|
252
|
+
res = await transport.http.request(cfg);
|
|
161
253
|
}
|
|
162
254
|
catch (netErr) {
|
|
163
255
|
if (attempt5xx >= max5xx) {
|
|
@@ -191,7 +283,7 @@ function createClient(config = {}) {
|
|
|
191
283
|
return { status: res.status, data: res.data };
|
|
192
284
|
}
|
|
193
285
|
if (res.status === 401 || res.status === 403) {
|
|
194
|
-
throw new Error("SN auth error " + res.status + " on " + ctx + " —
|
|
286
|
+
throw new Error("SN auth error " + res.status + " on " + ctx + " — " + authHint);
|
|
195
287
|
}
|
|
196
288
|
if (res.status === 404) {
|
|
197
289
|
throw new Error("SN 404 on " + ctx + " — endpoint or record not found.");
|
|
@@ -49,19 +49,35 @@ function resolveConfigFromEnvFile(envPath) {
|
|
|
49
49
|
throw new Error("env file '" + envPath + "' does not define a ServiceNow instance — " +
|
|
50
50
|
"set SN_INSTANCE (preferred) or SN_DEV_INSTANCE / SN_PROD_INSTANCE.");
|
|
51
51
|
}
|
|
52
|
+
// Dedicated Flow Designer (processflow) identity — optional. Carried only
|
|
53
|
+
// when the file sets it, so a file without it resolves exactly as before.
|
|
54
|
+
// Because the returned config pins the main identity, createClient takes the
|
|
55
|
+
// flow identity from this config ONLY: a file without SN_FLOW_* never
|
|
56
|
+
// borrows the host process's flow credentials.
|
|
57
|
+
var flowUser = parsed.SN_FLOW_USER || parsed.SN_DEV_FLOW_USER || parsed.SN_PROD_FLOW_USER || "";
|
|
58
|
+
var flowPassword = parsed.SN_FLOW_PASSWORD || parsed.SN_DEV_FLOW_PASSWORD || parsed.SN_PROD_FLOW_PASSWORD || "";
|
|
59
|
+
var withFlow = function (cfg) {
|
|
60
|
+
if (flowUser) {
|
|
61
|
+
cfg.flowUser = flowUser;
|
|
62
|
+
}
|
|
63
|
+
if (flowPassword) {
|
|
64
|
+
cfg.flowPassword = flowPassword;
|
|
65
|
+
}
|
|
66
|
+
return cfg;
|
|
67
|
+
};
|
|
52
68
|
if (apiKey) {
|
|
53
69
|
// Key is the default auth mode when the file defines one. Returning the
|
|
54
70
|
// key WITHOUT user/password keeps the resolved config fully explicit —
|
|
55
71
|
// resolveAuth treats a config that names user/password as a deliberate
|
|
56
72
|
// basic-auth pin, so leaking them here would flip the mode.
|
|
57
|
-
return { instance: instance, apiKey: apiKey };
|
|
73
|
+
return withFlow({ instance: instance, apiKey: apiKey });
|
|
58
74
|
}
|
|
59
75
|
if (!user || !password) {
|
|
60
76
|
throw new Error("env file '" + envPath + "' is missing ServiceNow credentials — " +
|
|
61
77
|
"set SN_API_KEY (inbound API key, preferred), SN_USER/SN_PASSWORD, " +
|
|
62
78
|
"or SN_DEV_USERNAME/SN_DEV_PASSWORD (or SN_PROD_*).");
|
|
63
79
|
}
|
|
64
|
-
return { instance: instance, user: user, password: password };
|
|
80
|
+
return withFlow({ instance: instance, user: user, password: password });
|
|
65
81
|
}
|
|
66
82
|
/**
|
|
67
83
|
* Construct a ServiceNowClient bound to the instance/credentials defined in a
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared processflow helpers for Custom Action Type authoring — the path
|
|
3
|
+
* builder, the `{ result: ... }` envelope unwrap, and the `/step_instances`
|
|
4
|
+
* read that editActionType and cloneActionType both depend on.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/now/processflow/action/action_types/{id}/step_instances?sysparm_transaction_scope={scope}
|
|
7
|
+
* -> { steps: [...] } (bare or under `result`)
|
|
8
|
+
*
|
|
9
|
+
* The action-type model GET returns `steps: null`; this is where the Designer
|
|
10
|
+
* (and we) get the real step graph.
|
|
11
|
+
*/
|
|
12
|
+
import type { ServiceNowClient } from "../client";
|
|
13
|
+
import type { StepRecord } from "./stepOps";
|
|
14
|
+
/** Build `/api/now/processflow/action/action_types/{sysId}{suffix}?sysparm_transaction_scope={scope}`. */
|
|
15
|
+
export declare function actionTypePath(sysId: string, scopeSysId: string, suffix: string): string;
|
|
16
|
+
/** Normalize the `{ result: ... }` envelope the processflow endpoints sometimes use. */
|
|
17
|
+
export declare function unwrapProcessflow(data: unknown): unknown;
|
|
18
|
+
/**
|
|
19
|
+
* GET an action type's step graph from `/step_instances` and unwrap `{ steps }`.
|
|
20
|
+
* Returns [] when the response carries no steps array — callers decide whether
|
|
21
|
+
* an empty graph is an error.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fetchActionSteps(client: ServiceNowClient, sysId: string, scopeSysId: string): Promise<Array<StepRecord>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared processflow helpers for Custom Action Type authoring — the path
|
|
4
|
+
* builder, the `{ result: ... }` envelope unwrap, and the `/step_instances`
|
|
5
|
+
* read that editActionType and cloneActionType both depend on.
|
|
6
|
+
*
|
|
7
|
+
* GET /api/now/processflow/action/action_types/{id}/step_instances?sysparm_transaction_scope={scope}
|
|
8
|
+
* -> { steps: [...] } (bare or under `result`)
|
|
9
|
+
*
|
|
10
|
+
* The action-type model GET returns `steps: null`; this is where the Designer
|
|
11
|
+
* (and we) get the real step graph.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.actionTypePath = actionTypePath;
|
|
15
|
+
exports.unwrapProcessflow = unwrapProcessflow;
|
|
16
|
+
exports.fetchActionSteps = fetchActionSteps;
|
|
17
|
+
/** Build `/api/now/processflow/action/action_types/{sysId}{suffix}?sysparm_transaction_scope={scope}`. */
|
|
18
|
+
function actionTypePath(sysId, scopeSysId, suffix) {
|
|
19
|
+
return "/api/now/processflow/action/action_types/" + encodeURIComponent(sysId)
|
|
20
|
+
+ suffix
|
|
21
|
+
+ "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
|
|
22
|
+
}
|
|
23
|
+
/** Normalize the `{ result: ... }` envelope the processflow endpoints sometimes use. */
|
|
24
|
+
function unwrapProcessflow(data) {
|
|
25
|
+
if (data && typeof data === "object") {
|
|
26
|
+
var rec = data;
|
|
27
|
+
if (rec.result && typeof rec.result === "object") {
|
|
28
|
+
return rec.result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* GET an action type's step graph from `/step_instances` and unwrap `{ steps }`.
|
|
35
|
+
* Returns [] when the response carries no steps array — callers decide whether
|
|
36
|
+
* an empty graph is an error.
|
|
37
|
+
*/
|
|
38
|
+
async function fetchActionSteps(client, sysId, scopeSysId) {
|
|
39
|
+
var resp = unwrapProcessflow(await client.now.get(actionTypePath(sysId, scopeSysId, "/step_instances")));
|
|
40
|
+
if (resp && typeof resp === "object") {
|
|
41
|
+
var steps = resp.steps;
|
|
42
|
+
if (Array.isArray(steps)) {
|
|
43
|
+
return steps;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
@@ -57,7 +57,7 @@ export interface BuildFlowResult {
|
|
|
57
57
|
/** Filled when clone/create produced a new artifact. */
|
|
58
58
|
artifact?: {
|
|
59
59
|
sysId: string;
|
|
60
|
-
action: "created" | "unchanged";
|
|
60
|
+
action: "created" | "unchanged" | "planned";
|
|
61
61
|
writtenCount: number;
|
|
62
62
|
};
|
|
63
63
|
verify?: VerifyReport;
|