@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 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), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
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
- * Basic auth, same credentials/retry/throttle as the rest of the client.
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 + " — check SN_USER/SN_PASSWORD and ACLs.");
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;