@tenonhq/dovetail-servicenow 0.0.37 → 0.0.39
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 +65 -0
- package/dist/cli.js +263 -3
- package/dist/exportApp.d.ts +102 -0
- package/dist/exportApp.js +306 -0
- package/dist/exportUpdateSet.d.ts +133 -0
- package/dist/exportUpdateSet.js +506 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +43 -1
- package/dist/loadEnv.d.ts +37 -7
- package/dist/loadEnv.js +94 -14
- package/dist/mcp/registry.d.ts +1 -1
- package/dist/mcp/registry.js +69 -0
- package/dist/mcp/schemas.d.ts +58 -2
- package/dist/mcp/schemas.js +25 -1
- package/dist/secrets/secretRules.d.ts +112 -0
- package/dist/secrets/secretRules.js +402 -0
- package/dist/secrets/stripSecrets.d.ts +138 -0
- package/dist/secrets/stripSecrets.js +458 -0
- package/dist/table/formSession.d.ts +8 -0
- package/dist/table/formSession.js +32 -0
- package/dist/table/index.d.ts +1 -1
- package/dist/table/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -555,6 +555,71 @@ Store `appLink`, and the publish's update-set sys_id where the instance reports
|
|
|
555
555
|
one. Exit codes: `0` published or dry-run, `1` bad args/unconfirmed, `2`
|
|
556
556
|
failed/timeout. Programmatic: `publishApp({ app, version, target, confirm })`.
|
|
557
557
|
|
|
558
|
+
### Export an update set (or a whole app) to importable XML
|
|
559
|
+
|
|
560
|
+
Produce the `<unload>` document the implementation team imports on a customer
|
|
561
|
+
instance — with every secret value replaced by `__SET_DURING_INSTALL__`.
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
# An update set. assemble mode is READ-ONLY: nothing on the instance changes.
|
|
565
|
+
npx dove-sn export-update-set --update-set 0123456789abcdef0123456789abcdef \
|
|
566
|
+
--out ./tenon-core.xml
|
|
567
|
+
|
|
568
|
+
# A whole app: publish into a new set, then export it. Dry-run first (default).
|
|
569
|
+
npx dove-sn export-app --app x_cadso_automate --out ./automate.xml
|
|
570
|
+
npx dove-sn export-app --app x_cadso_automate --out ./automate.xml --confirm
|
|
571
|
+
|
|
572
|
+
# A document exported some other way
|
|
573
|
+
npx dove-sn strip-secrets --in ./exported.xml --out ./safe.xml
|
|
574
|
+
npx dove-sn strip-secrets --in ./exported.xml --report # what would be stripped
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Secret stripping is not optional.** There is no `--no-strip` flag on any of
|
|
578
|
+
these verbs, and no field in the MCP schemas that disables it. This matters
|
|
579
|
+
because an unload carries field *values*: on tenonworkstudio, completed update
|
|
580
|
+
sets hold filled-in values for `password2` system properties and
|
|
581
|
+
`oauth_entity.client_secret`, several of them in sets that shipped to a
|
|
582
|
+
customer. A field is exempted only by a reviewed entry in the rules file.
|
|
583
|
+
|
|
584
|
+
The rule is enumerable, in four layers:
|
|
585
|
+
|
|
586
|
+
| Layer | Covers |
|
|
587
|
+
|---|---|
|
|
588
|
+
| L1 type | `password` / `password2` fields, restricted to tables an update set can capture (`update_synch=true`), resolved through `super_class`. Refreshed from the live dictionary per run, with a committed baseline as the fallback. |
|
|
589
|
+
| L2 conditional / explicit | `sys_properties.value` when the property's `type` is a password type; named exceptions such as `x_cadso_core.google_translate_api_key`, which holds an API key in a *string* column. |
|
|
590
|
+
| L3 JSON | secrets nested inside a JSON blob field, stripped in place. |
|
|
591
|
+
| L4 heuristic | a field that merely *looks* secret. **Never stripped silently and never assumed safe** — the run fails and names it, until a human records it in the rules file as a strip rule or as `notSecret` with a reason. |
|
|
592
|
+
|
|
593
|
+
Override the rules with `--rules <file>`; the JSON is merged over the built-ins,
|
|
594
|
+
and the only subtractive key is `notSecret`, which requires a reason:
|
|
595
|
+
|
|
596
|
+
```json
|
|
597
|
+
{
|
|
598
|
+
"notSecret": [
|
|
599
|
+
{ "table": "x_cadso_core_thing", "field": "webhook_token", "reason": "public identifier, not a credential" }
|
|
600
|
+
],
|
|
601
|
+
"fieldRules": [
|
|
602
|
+
{ "id": "thing-signing-key", "table": "x_cadso_core_thing", "field": "signing_key", "reason": "inbound webhook HMAC key" }
|
|
603
|
+
]
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Two more things refuse to produce a file rather than produce a wrong one: a
|
|
608
|
+
record count that does not match the set, and the documented **in-progress
|
|
609
|
+
empty 200** from `export_update_set.do` (the servlet streams a document only for
|
|
610
|
+
a *complete* set, and app-publish leaves the set in progress). After stripping,
|
|
611
|
+
the output is re-read and verified; a secret that somehow survived fails the run.
|
|
612
|
+
|
|
613
|
+
`export-update-set --mode complete` marks the set complete on the instance
|
|
614
|
+
first — a real write, so it needs `--confirm`. `export-app` publishes ~1000+
|
|
615
|
+
records into a new update set and is dry-run by default. Neither is the Store
|
|
616
|
+
publish; that is `publish-app`, which is externally visible.
|
|
617
|
+
|
|
618
|
+
Exit codes: `0` exported or dry-run, `1` bad args/unconfirmed, `2`
|
|
619
|
+
failed/timeout. Programmatic: `exportUpdateSet({ updateSet, mode })`,
|
|
620
|
+
`exportApp({ app, confirm })`, `stripSecrets(xml, rules)`. MCP:
|
|
621
|
+
`update_set_export` (read-only in assemble mode) and `app_export`.
|
|
622
|
+
|
|
558
623
|
`test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
|
|
559
624
|
declared variables?) that never runs the flow; `--execute --confirm` runs it via
|
|
560
625
|
the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
|
package/dist/cli.js
CHANGED
|
@@ -69,6 +69,10 @@ const formLayout_1 = require("./layout/formLayout");
|
|
|
69
69
|
const relatedLists_1 = require("./layout/relatedLists");
|
|
70
70
|
const formatter_2 = require("./layout/formatter");
|
|
71
71
|
const server_1 = require("./mcp/server");
|
|
72
|
+
const exportUpdateSet_1 = require("./exportUpdateSet");
|
|
73
|
+
const exportApp_1 = require("./exportApp");
|
|
74
|
+
const stripSecrets_1 = require("./secrets/stripSecrets");
|
|
75
|
+
const secretRules_1 = require("./secrets/secretRules");
|
|
72
76
|
const schemas_1 = require("./mcp/schemas");
|
|
73
77
|
const buildFlowOrchestrator_1 = require("./flowDesigner/buildFlowOrchestrator");
|
|
74
78
|
const flowDesigner_formatter_1 = require("./flowDesigner-formatter");
|
|
@@ -1048,10 +1052,29 @@ function printHelp() {
|
|
|
1048
1052
|
" [--dry-run] [--json] [--confirm])\n" +
|
|
1049
1053
|
" Store creds: SN_STORE_USERNAME/SN_STORE_PASSWORD in the --env file;\n" +
|
|
1050
1054
|
" the password is never a flag. Repo publish needs the sn_cicd role.\n" +
|
|
1055
|
+
" export-update-set Export an update set to importable <unload> XML, with secret\n" +
|
|
1056
|
+
" values replaced by __SET_DURING_INSTALL__ (no opt-out).\n" +
|
|
1057
|
+
" assemble mode is READ-ONLY; complete mode marks the set\n" +
|
|
1058
|
+
" complete on the instance and needs --confirm.\n" +
|
|
1059
|
+
" (--update-set <sys_id|name> --out <file>\n" +
|
|
1060
|
+
" [--mode assemble|complete]\n" +
|
|
1061
|
+
" [--rules <file>] [--page-size <n>] [--max-rows <n>]\n" +
|
|
1062
|
+
" [--dry-run] [--json] [--confirm])\n" +
|
|
1063
|
+
" export-app Publish a scoped app into a new update set and export it.\n" +
|
|
1064
|
+
" PUBLISHING IS A REAL INSTANCE WRITE (~1000+ records).\n" +
|
|
1065
|
+
" DRY-RUN BY DEFAULT — nothing is published without --confirm\n" +
|
|
1066
|
+
" (--app <scope|sys_id|name> --out <file> [--version <v>]\n" +
|
|
1067
|
+
" [--description <text>] [--include-data] [--rules <file>]\n" +
|
|
1068
|
+
" [--timeout-ms <n>] [--dry-run] [--json] [--confirm])\n" +
|
|
1069
|
+
" strip-secrets Strip secret values from an unload XML exported elsewhere\n" +
|
|
1070
|
+
" (--in <file> [--out <file>] [--rules <file>] [--report] [--json])\n" +
|
|
1051
1071
|
" mcp Run the MCP stdio server (--smoke lists tools and exits)\n" +
|
|
1052
1072
|
"\nGlobal flags:\n" +
|
|
1053
|
-
" --env <path>
|
|
1054
|
-
" or the DOVETAIL_ENV_FILE env var).
|
|
1073
|
+
" --env <name|path> Load credentials from a specific env file (also --env-file,\n" +
|
|
1074
|
+
" or the DOVETAIL_ENV_FILE env var). A bare name like 'prod'\n" +
|
|
1075
|
+
" resolves to .env.prod in the cwd. The file's SN_* connection\n" +
|
|
1076
|
+
" vars replace any already exported; a missing or incomplete\n" +
|
|
1077
|
+
" file is an error (no fallback). Default: .env in the cwd.\n");
|
|
1055
1078
|
}
|
|
1056
1079
|
/** Parse inline `--columns "Label:type:max, Other:choice, ..."` into ColumnSpec[]. */
|
|
1057
1080
|
function parseColumnsInline(input) {
|
|
@@ -1439,7 +1462,9 @@ async function runSetTable(flags, bare) {
|
|
|
1439
1462
|
var stringFlags = ["table", "update-set", "updateSetSysId"];
|
|
1440
1463
|
for (var f = 0; f < stringFlags.length; f += 1) {
|
|
1441
1464
|
if (bare[stringFlags[f]]) {
|
|
1442
|
-
process.stderr.write("set-table: --" +
|
|
1465
|
+
process.stderr.write("set-table: --" +
|
|
1466
|
+
stringFlags[f] +
|
|
1467
|
+
" needs a value (it was given none).\n");
|
|
1443
1468
|
return 1;
|
|
1444
1469
|
}
|
|
1445
1470
|
}
|
|
@@ -1902,6 +1927,232 @@ async function runPublishApp(flags) {
|
|
|
1902
1927
|
}
|
|
1903
1928
|
return exitCode;
|
|
1904
1929
|
}
|
|
1930
|
+
/**
|
|
1931
|
+
* dove-sn export-update-set:
|
|
1932
|
+
* --update-set <sys_id|name> Required. The set to export.
|
|
1933
|
+
* [--mode assemble|complete] assemble (default) is READ-ONLY; complete marks
|
|
1934
|
+
* the set complete on the instance first, which is
|
|
1935
|
+
* a real write and needs --confirm.
|
|
1936
|
+
* [--out <file>] Write the XML here (default: stdout is NOT used —
|
|
1937
|
+
* a document this size belongs in a file).
|
|
1938
|
+
* [--rules <file>] JSON overrides for the secret rules.
|
|
1939
|
+
* [--page-size <n>] [--max-rows <n>]
|
|
1940
|
+
* [--dry-run] [--json] [--confirm]
|
|
1941
|
+
*
|
|
1942
|
+
* Secret values are ALWAYS replaced with __SET_DURING_INSTALL__; there is no
|
|
1943
|
+
* opt-out flag. A field that looks secret and is covered by no rule fails the
|
|
1944
|
+
* run, and nothing is written.
|
|
1945
|
+
* Exit codes: 0 exported/dry-run, 1 bad args/unconfirmed, 2 failed.
|
|
1946
|
+
*/
|
|
1947
|
+
async function runExportUpdateSet(flags) {
|
|
1948
|
+
var selector = flags["update-set"];
|
|
1949
|
+
if (!selector) {
|
|
1950
|
+
process.stderr.write("export-update-set: --update-set <sys_id|name> is required\n");
|
|
1951
|
+
return 1;
|
|
1952
|
+
}
|
|
1953
|
+
var mode = flags.mode || "assemble";
|
|
1954
|
+
if (mode !== "assemble" && mode !== "complete") {
|
|
1955
|
+
process.stderr.write("export-update-set: --mode must be assemble or complete\n");
|
|
1956
|
+
return 1;
|
|
1957
|
+
}
|
|
1958
|
+
var outPath = flags.out;
|
|
1959
|
+
if (!outPath && flags["dry-run"] !== "true") {
|
|
1960
|
+
process.stderr.write("export-update-set: --out <file> is required for a real export\n");
|
|
1961
|
+
return 1;
|
|
1962
|
+
}
|
|
1963
|
+
var pageSize = undefined;
|
|
1964
|
+
if (flags["page-size"]) {
|
|
1965
|
+
pageSize = Number(flags["page-size"]);
|
|
1966
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
1967
|
+
process.stderr.write("export-update-set: --page-size must be a positive integer\n");
|
|
1968
|
+
return 1;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
var maxRows = undefined;
|
|
1972
|
+
if (flags["max-rows"]) {
|
|
1973
|
+
maxRows = Number(flags["max-rows"]);
|
|
1974
|
+
if (!Number.isInteger(maxRows) || maxRows < 1) {
|
|
1975
|
+
process.stderr.write("export-update-set: --max-rows must be a positive integer\n");
|
|
1976
|
+
return 1;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
var result = await (0, exportUpdateSet_1.exportUpdateSet)({
|
|
1980
|
+
updateSet: selector,
|
|
1981
|
+
mode: mode,
|
|
1982
|
+
confirm: flags.confirm === "true",
|
|
1983
|
+
dryRun: flags["dry-run"] === "true",
|
|
1984
|
+
rulesPath: flags.rules,
|
|
1985
|
+
pageSize: pageSize,
|
|
1986
|
+
maxRows: maxRows,
|
|
1987
|
+
});
|
|
1988
|
+
if (result.status === "exported" && result.xml && outPath) {
|
|
1989
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
1990
|
+
}
|
|
1991
|
+
writeExportReceipt(flags, result, outPath);
|
|
1992
|
+
if (result.status === "failed") {
|
|
1993
|
+
return 2;
|
|
1994
|
+
}
|
|
1995
|
+
if (result.status === "dry-run" && flags["dry-run"] !== "true") {
|
|
1996
|
+
return 1;
|
|
1997
|
+
}
|
|
1998
|
+
return 0;
|
|
1999
|
+
}
|
|
2000
|
+
/** Shared receipt for both export verbs. */
|
|
2001
|
+
function writeExportReceipt(flags, result, outPath) {
|
|
2002
|
+
if (flags.json === "true") {
|
|
2003
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
process.stdout.write("[" +
|
|
2007
|
+
result.status +
|
|
2008
|
+
"]" +
|
|
2009
|
+
(outPath && result.status === "exported"
|
|
2010
|
+
? " " + path.resolve(outPath)
|
|
2011
|
+
: "") +
|
|
2012
|
+
"\n" +
|
|
2013
|
+
result.note +
|
|
2014
|
+
"\n");
|
|
2015
|
+
if (result.secretFields.length > 0) {
|
|
2016
|
+
process.stdout.write("Set these after loading the package:\n");
|
|
2017
|
+
for (var i = 0; i < result.secretFields.length; i += 1) {
|
|
2018
|
+
process.stdout.write(" " +
|
|
2019
|
+
result.secretFields[i].table +
|
|
2020
|
+
"." +
|
|
2021
|
+
result.secretFields[i].field +
|
|
2022
|
+
"\n");
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* dove-sn export-app:
|
|
2028
|
+
* --app <scope|sys_id|name> Required. The sys_app to publish and export.
|
|
2029
|
+
* [--version <v>] Publish version (default: the app's current one).
|
|
2030
|
+
* [--description <text>] Recorded on the update set.
|
|
2031
|
+
* [--include-data] Ship table DATA as well as schema (off by default).
|
|
2032
|
+
* --out <file> Where to write the XML.
|
|
2033
|
+
* [--rules <file>] [--timeout-ms <n>]
|
|
2034
|
+
* [--dry-run] [--json] [--confirm]
|
|
2035
|
+
*
|
|
2036
|
+
* PUBLISHING IS A REAL INSTANCE WRITE — a new update set and ~1000+ records.
|
|
2037
|
+
* DRY-RUN BY DEFAULT. Secret values are always stripped before the file lands.
|
|
2038
|
+
* Exit codes: 0 exported/dry-run, 1 bad args/unconfirmed, 2 failed/timeout.
|
|
2039
|
+
*/
|
|
2040
|
+
async function runExportApp(flags) {
|
|
2041
|
+
var app = flags.app;
|
|
2042
|
+
if (!app) {
|
|
2043
|
+
process.stderr.write("export-app: --app <scope|sys_id|name> is required\n");
|
|
2044
|
+
return 1;
|
|
2045
|
+
}
|
|
2046
|
+
var outPath = flags.out;
|
|
2047
|
+
if (!outPath && flags["dry-run"] !== "true") {
|
|
2048
|
+
process.stderr.write("export-app: --out <file> is required for a real export\n");
|
|
2049
|
+
return 1;
|
|
2050
|
+
}
|
|
2051
|
+
var timeoutMs = undefined;
|
|
2052
|
+
if (flags["timeout-ms"]) {
|
|
2053
|
+
timeoutMs = Number(flags["timeout-ms"]);
|
|
2054
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
2055
|
+
process.stderr.write("export-app: --timeout-ms must be a positive integer\n");
|
|
2056
|
+
return 1;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
var result = await (0, exportApp_1.exportApp)({
|
|
2060
|
+
app: app,
|
|
2061
|
+
version: flags.version,
|
|
2062
|
+
description: flags.description,
|
|
2063
|
+
includeData: flags["include-data"] === "true",
|
|
2064
|
+
keepSet: flags["keep-set"] !== "false",
|
|
2065
|
+
confirm: flags.confirm === "true",
|
|
2066
|
+
dryRun: flags["dry-run"] === "true",
|
|
2067
|
+
rulesPath: flags.rules,
|
|
2068
|
+
timeoutMs: timeoutMs,
|
|
2069
|
+
});
|
|
2070
|
+
if (result.status === "exported" && result.xml && outPath) {
|
|
2071
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
2072
|
+
}
|
|
2073
|
+
writeExportReceipt(flags, result, outPath);
|
|
2074
|
+
if (result.status === "failed" || result.status === "timeout") {
|
|
2075
|
+
return 2;
|
|
2076
|
+
}
|
|
2077
|
+
if (result.status === "dry-run" && flags["dry-run"] !== "true") {
|
|
2078
|
+
return 1;
|
|
2079
|
+
}
|
|
2080
|
+
return 0;
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* dove-sn strip-secrets:
|
|
2084
|
+
* --in <file> Required. An unload XML exported earlier.
|
|
2085
|
+
* --out <file> Required unless --report.
|
|
2086
|
+
* [--rules <file>] JSON overrides for the secret rules.
|
|
2087
|
+
* [--report] List what WOULD be stripped and what needs review;
|
|
2088
|
+
* writes nothing.
|
|
2089
|
+
* [--json]
|
|
2090
|
+
*
|
|
2091
|
+
* Exists for documents produced outside these verbs. Exit codes: 0 clean,
|
|
2092
|
+
* 1 bad args, 2 blocked (a field needs review, or a secret survived).
|
|
2093
|
+
*/
|
|
2094
|
+
async function runStripSecrets(flags) {
|
|
2095
|
+
var inPath = flags.in;
|
|
2096
|
+
if (!inPath) {
|
|
2097
|
+
process.stderr.write("strip-secrets: --in <file> is required\n");
|
|
2098
|
+
return 1;
|
|
2099
|
+
}
|
|
2100
|
+
var report = flags.report === "true";
|
|
2101
|
+
var outPath = flags.out;
|
|
2102
|
+
if (!report && !outPath) {
|
|
2103
|
+
process.stderr.write("strip-secrets: --out <file> is required (or pass --report)\n");
|
|
2104
|
+
return 1;
|
|
2105
|
+
}
|
|
2106
|
+
var xml = "";
|
|
2107
|
+
try {
|
|
2108
|
+
xml = fs.readFileSync(path.resolve(inPath), "utf8");
|
|
2109
|
+
}
|
|
2110
|
+
catch (e) {
|
|
2111
|
+
process.stderr.write("strip-secrets: cannot read " + inPath + "\n");
|
|
2112
|
+
return 1;
|
|
2113
|
+
}
|
|
2114
|
+
var rules = (0, secretRules_1.loadSecretRules)(flags.rules);
|
|
2115
|
+
var result;
|
|
2116
|
+
try {
|
|
2117
|
+
result = (0, stripSecrets_1.stripSecrets)(xml, rules, { allowUnreviewed: report });
|
|
2118
|
+
}
|
|
2119
|
+
catch (e) {
|
|
2120
|
+
process.stderr.write((e instanceof Error ? e.message : String(e)) + "\n");
|
|
2121
|
+
return 2;
|
|
2122
|
+
}
|
|
2123
|
+
if (!report && outPath) {
|
|
2124
|
+
fs.writeFileSync(path.resolve(outPath), result.xml, "utf8");
|
|
2125
|
+
}
|
|
2126
|
+
if (flags.json === "true") {
|
|
2127
|
+
process.stdout.write(JSON.stringify({
|
|
2128
|
+
recordsScanned: result.recordsScanned,
|
|
2129
|
+
secretFields: result.secretFields,
|
|
2130
|
+
reviewFindings: result.reviewFindings,
|
|
2131
|
+
written: report ? null : path.resolve(outPath),
|
|
2132
|
+
}, null, 2) + "\n");
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
process.stdout.write("[" +
|
|
2136
|
+
(report ? "report" : "stripped") +
|
|
2137
|
+
"] " +
|
|
2138
|
+
result.recordsScanned +
|
|
2139
|
+
" record(s), " +
|
|
2140
|
+
result.secretFields.length +
|
|
2141
|
+
" secret value(s)" +
|
|
2142
|
+
(report ? "" : " → " + path.resolve(outPath)) +
|
|
2143
|
+
"\n");
|
|
2144
|
+
for (var i = 0; i < result.reviewFindings.length; i += 1) {
|
|
2145
|
+
process.stdout.write(" NEEDS REVIEW " +
|
|
2146
|
+
result.reviewFindings[i].table +
|
|
2147
|
+
"." +
|
|
2148
|
+
result.reviewFindings[i].field +
|
|
2149
|
+
" (matched '" +
|
|
2150
|
+
result.reviewFindings[i].matched +
|
|
2151
|
+
"')\n");
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return report && result.reviewFindings.length > 0 ? 2 : 0;
|
|
2155
|
+
}
|
|
1905
2156
|
async function main() {
|
|
1906
2157
|
var parsed = parseArgs(process.argv.slice(2));
|
|
1907
2158
|
// Load credentials before any command runs. `--env`/`--env-file` (or the
|
|
@@ -1987,6 +2238,15 @@ async function main() {
|
|
|
1987
2238
|
if (parsed.command === "publish-app") {
|
|
1988
2239
|
return await runPublishApp(parsed.flags);
|
|
1989
2240
|
}
|
|
2241
|
+
if (parsed.command === "export-update-set") {
|
|
2242
|
+
return await runExportUpdateSet(parsed.flags);
|
|
2243
|
+
}
|
|
2244
|
+
if (parsed.command === "export-app") {
|
|
2245
|
+
return await runExportApp(parsed.flags);
|
|
2246
|
+
}
|
|
2247
|
+
if (parsed.command === "strip-secrets") {
|
|
2248
|
+
return await runStripSecrets(parsed.flags);
|
|
2249
|
+
}
|
|
1990
2250
|
if (parsed.command === "mcp") {
|
|
1991
2251
|
return await runMcp(parsed.flags);
|
|
1992
2252
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export a scoped application to an importable `<unload>` XML document — the
|
|
3
|
+
* headless equivalent of the classic UI's **Publish to Update Set → Export to
|
|
4
|
+
* XML**, with secret values stripped before the document is returned.
|
|
5
|
+
*
|
|
6
|
+
* Ground truth: `brainstorms/sn-export-app-flow.md` (HAR capture, tenonworkshop
|
|
7
|
+
* 2026-06-08) and the live run recorded there. "Export an app" is two operations
|
|
8
|
+
* chained, with no single button behind it:
|
|
9
|
+
*
|
|
10
|
+
* 1. `POST /xmlhttp.do` com.snc.apps.AppsAjaxProcessor `createUpdateSet` →
|
|
11
|
+
* the new set's sys_id arrives in the answer attribute.
|
|
12
|
+
* 2. `POST /xmlhttp.do` AppsAjaxProcessor `publishToUpdateSet` → a worker id;
|
|
13
|
+
* poll AJAXProgressStatusChecker/getStatus until "Successfully published"
|
|
14
|
+
* (the HAR took 48 polls / ~44s for 1,088 files).
|
|
15
|
+
* 3. export that update set — delegated to exportUpdateSet, which owns the
|
|
16
|
+
* in-progress empty-200 gotcha and the secret stripping.
|
|
17
|
+
*
|
|
18
|
+
* PUBLISHING IS A REAL SHARED-INSTANCE WRITE: it creates an update set and
|
|
19
|
+
* ~1000+ sys_update_xml rows. DRY-RUN BY DEFAULT — without confirm:true this
|
|
20
|
+
* resolves the app and returns the plan, having written nothing.
|
|
21
|
+
*
|
|
22
|
+
* This is NOT the Store publish. That is publishApp, which is externally
|
|
23
|
+
* visible; this one stays inside the instance.
|
|
24
|
+
*
|
|
25
|
+
* ES6 only, no optional chaining.
|
|
26
|
+
*/
|
|
27
|
+
import type { ServiceNowClient } from "./client";
|
|
28
|
+
import type { FormAuth, FormSession, PostResult } from "./table";
|
|
29
|
+
import type { ExportTransport } from "./exportUpdateSet";
|
|
30
|
+
import type { SecretField } from "./secrets/stripSecrets";
|
|
31
|
+
/** Injectable transport so tests never touch the network. */
|
|
32
|
+
export interface ExportAppTransport extends ExportTransport {
|
|
33
|
+
post?: (auth: FormAuth, session: FormSession, path: string, fields: Record<string, string>) => Promise<PostResult>;
|
|
34
|
+
sleep?: (ms: number) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Inputs for exportApp. */
|
|
37
|
+
export interface ExportAppParams {
|
|
38
|
+
/** App sys_id, scope, or name. */
|
|
39
|
+
app: string;
|
|
40
|
+
/** Publish version. Defaults to the app's current version. */
|
|
41
|
+
version?: string;
|
|
42
|
+
/** Description recorded on the update set. */
|
|
43
|
+
description?: string;
|
|
44
|
+
/** Include table DATA as well as schema. Off by default. */
|
|
45
|
+
includeData?: boolean;
|
|
46
|
+
/** Leave the published update set behind. Default true — deleting is not ours to do. */
|
|
47
|
+
keepSet?: boolean;
|
|
48
|
+
client?: ServiceNowClient;
|
|
49
|
+
instance?: string;
|
|
50
|
+
user?: string;
|
|
51
|
+
password?: string;
|
|
52
|
+
/** Required: publishing writes to the instance. */
|
|
53
|
+
confirm?: boolean;
|
|
54
|
+
/** Force a plan-only run. Wins over confirm. */
|
|
55
|
+
dryRun?: boolean;
|
|
56
|
+
/** Optional JSON secret-rules file. */
|
|
57
|
+
rulesPath?: string;
|
|
58
|
+
/** Milliseconds to wait for the publish worker. Default 300000. */
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
transport?: ExportAppTransport;
|
|
61
|
+
}
|
|
62
|
+
/** Outcome of an app export. */
|
|
63
|
+
export interface ExportAppResult {
|
|
64
|
+
status: "dry-run" | "exported" | "failed" | "timeout";
|
|
65
|
+
appName: string;
|
|
66
|
+
appScope: string;
|
|
67
|
+
appSysId: string;
|
|
68
|
+
version: string;
|
|
69
|
+
/** The update set the publish produced. */
|
|
70
|
+
updateSetSysId: string;
|
|
71
|
+
/** getStatus polls performed. */
|
|
72
|
+
polls: number;
|
|
73
|
+
recordCount: number;
|
|
74
|
+
xml?: string;
|
|
75
|
+
secretFields: Array<SecretField>;
|
|
76
|
+
message?: string;
|
|
77
|
+
note: string;
|
|
78
|
+
}
|
|
79
|
+
/** Publishing a large app is slow — the HAR run took ~44s for 1,088 files. */
|
|
80
|
+
export declare var DEFAULT_EXPORT_APP_TIMEOUT_MS: number;
|
|
81
|
+
interface ResolvedApp {
|
|
82
|
+
sysId: string;
|
|
83
|
+
scope: string;
|
|
84
|
+
name: string;
|
|
85
|
+
version: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fields for the AppsAjaxProcessor createUpdateSet call. The UI sends a single
|
|
89
|
+
* space when the description box is empty — mirrored here so the processor sees
|
|
90
|
+
* what it sees from a browser.
|
|
91
|
+
*/
|
|
92
|
+
export declare function buildCreateSetFields(app: ResolvedApp, description: string): Record<string, string>;
|
|
93
|
+
/** Fields for the AppsAjaxProcessor publishToUpdateSet call. */
|
|
94
|
+
export declare function buildPublishFields(app: ResolvedApp, updateSetSysId: string, version: string, description: string, includeData: boolean): Record<string, string>;
|
|
95
|
+
/**
|
|
96
|
+
* Publish an application into a fresh update set and export that set.
|
|
97
|
+
*
|
|
98
|
+
* Remote failures are RETURNED as a failed/timeout result; only caller errors
|
|
99
|
+
* (bad selector, bad timeout) throw.
|
|
100
|
+
*/
|
|
101
|
+
export declare function exportApp(params: ExportAppParams): Promise<ExportAppResult>;
|
|
102
|
+
export {};
|