@snutils/snu 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/doctor.d.ts +10 -0
  3. package/dist/cli/doctor.d.ts.map +1 -1
  4. package/dist/cli/doctor.js +25 -0
  5. package/dist/cli/doctor.js.map +1 -1
  6. package/dist/cli/format.d.ts +1 -0
  7. package/dist/cli/format.d.ts.map +1 -1
  8. package/dist/cli/format.js +24 -2
  9. package/dist/cli/format.js.map +1 -1
  10. package/dist/cli/index.d.ts +11 -0
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +41 -1
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/server/authProbe.d.ts +29 -0
  15. package/dist/server/authProbe.d.ts.map +1 -0
  16. package/dist/server/authProbe.js +67 -0
  17. package/dist/server/authProbe.js.map +1 -0
  18. package/dist/server/bridgeIdentity.d.ts +24 -0
  19. package/dist/server/bridgeIdentity.d.ts.map +1 -0
  20. package/dist/server/bridgeIdentity.js +120 -0
  21. package/dist/server/bridgeIdentity.js.map +1 -0
  22. package/dist/server/dispatcher.d.ts +0 -1
  23. package/dist/server/dispatcher.d.ts.map +1 -1
  24. package/dist/server/dispatcher.js +85 -68
  25. package/dist/server/dispatcher.js.map +1 -1
  26. package/dist/server/helperConnection.d.ts +46 -0
  27. package/dist/server/helperConnection.d.ts.map +1 -0
  28. package/dist/server/helperConnection.js +125 -0
  29. package/dist/server/helperConnection.js.map +1 -0
  30. package/dist/server/scopeDiscovery.d.ts +37 -0
  31. package/dist/server/scopeDiscovery.d.ts.map +1 -0
  32. package/dist/server/scopeDiscovery.js +147 -0
  33. package/dist/server/scopeDiscovery.js.map +1 -0
  34. package/dist/server/wsBridge.d.ts +4 -2
  35. package/dist/server/wsBridge.d.ts.map +1 -1
  36. package/dist/server/wsBridge.js +32 -20
  37. package/dist/server/wsBridge.js.map +1 -1
  38. package/package.json +1 -1
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Turns the outcome of the bounded auth probe into a state an agent can act on.
4
+ *
5
+ * The helper tab reports several failures without an HTTP status: no token for
6
+ * the instance, a hostname it has not approved, or a request the browser never
7
+ * got out. All of those used to collapse into AUTH_UNKNOWN with "retry", which
8
+ * sends an agent in circles when the real fix is a /token on the right
9
+ * hostname (reported by Tyler Hodges: an instance reached through both its
10
+ * service-now.com name and a load balancer hostname).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.helperFailureHint = helperFailureHint;
14
+ exports.classifyProbe = classifyProbe;
15
+ const SERVICENOW_HOST = /(^|\.)(service-now\.com|servicenowservices\.com)$/i;
16
+ function hostOf(origin) {
17
+ try {
18
+ return origin ? new URL(origin).hostname : '';
19
+ }
20
+ catch {
21
+ return '';
22
+ }
23
+ }
24
+ /** Extra guidance for helper-tab failures that carry no HTTP status. Empty when there is nothing to add. */
25
+ function helperFailureHint(error, origin) {
26
+ const text = String(error || '');
27
+ const where = origin || 'that instance';
28
+ if (/Missing instance URL or authentication token/i.test(text)) {
29
+ return `No session token is known for ${where}. Open exactly that hostname in the browser and run /token.`;
30
+ }
31
+ if (/unapproved instance URL/i.test(text)) {
32
+ return `The SN Utils helper tab has not approved ${where}. Open exactly that hostname in the browser, run /token and allow the instance when asked.`;
33
+ }
34
+ if (/Failed to fetch|NetworkError|Load failed|net::ERR_/i.test(text)) {
35
+ const host = hostOf(origin);
36
+ return host && !SERVICENOW_HOST.test(host)
37
+ ? `The browser could not reach ${where}. ${host} is not a service-now.com hostname, which the regular SN Utils build cannot connect to: use the instance's service-now.com address, or the SN Utils OnPrem build.`
38
+ : `The browser could not reach ${where}. Check that the instance is reachable from the browser (VPN, network) and that you are logged in on that hostname.`;
39
+ }
40
+ return '';
41
+ }
42
+ function classifyProbe(outcome) {
43
+ const { status, error, origin, hasLiveSession } = outcome;
44
+ if (status === 401) {
45
+ return { state: 'AUTH_EXPIRED', message: 'ServiceNow rejected the session (401). Open the instance in the browser and run /token to refresh it.' };
46
+ }
47
+ if (status === 403 || (status !== undefined && status >= 200 && status < 300)) {
48
+ return { state: 'AUTH_OK', message: 'ServiceNow accepted the session.' };
49
+ }
50
+ const hint = helperFailureHint(error, origin);
51
+ if (/Missing instance URL or authentication token|unapproved instance URL/i.test(String(error || ''))) {
52
+ return { state: 'AUTH_MISSING', message: hint };
53
+ }
54
+ if (hint) {
55
+ return { state: 'AUTH_UNKNOWN', message: hint };
56
+ }
57
+ // Same rule as the VS Code host: a failure with no session ever received
58
+ // for this origin reads as missing, not as unknown.
59
+ if (!hasLiveSession) {
60
+ return {
61
+ state: 'AUTH_MISSING',
62
+ message: `This bridge has not received a session for ${origin || 'this instance'} since it started. Open exactly that hostname in the browser and run /token. Sessions are per hostname: a /token on another address of the same instance does not count.`,
63
+ };
64
+ }
65
+ return { state: 'AUTH_UNKNOWN', message: 'The session check could not complete. This is not a verdict: retry before acting on it.' };
66
+ }
67
+ //# sourceMappingURL=authProbe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authProbe.js","sourceRoot":"","sources":["../../src/server/authProbe.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;AA2BH,8CAgBC;AAED,sCAyBC;AAlDD,MAAM,eAAe,GAAG,oDAAoD,CAAC;AAE7E,SAAS,MAAM,CAAC,MAAsB;IACpC,IAAI,CAAC;QAAC,OAAO,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;AAC7E,CAAC;AAED,4GAA4G;AAC5G,SAAgB,iBAAiB,CAAC,KAAyB,EAAE,MAAsB;IACjF,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,MAAM,IAAI,eAAe,CAAC;IACxC,IAAI,+CAA+C,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/D,OAAO,iCAAiC,KAAK,6DAA6D,CAAC;IAC7G,CAAC;IACD,IAAI,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,4CAA4C,KAAK,4FAA4F,CAAC;IACvJ,CAAC;IACD,IAAI,qDAAqD,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;YACxC,CAAC,CAAC,+BAA+B,KAAK,KAAK,IAAI,mKAAmK;YAClN,CAAC,CAAC,+BAA+B,KAAK,qHAAqH,CAAC;IAChK,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAgB,aAAa,CAAC,OAAqB;IACjD,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IAC1D,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,uGAAuG,EAAE,CAAC;IACrJ,CAAC;IACD,IAAI,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC;QAC9E,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC;IAC3E,CAAC;IAED,MAAM,IAAI,GAAG,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC9C,IAAI,uEAAuE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACtG,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAClD,CAAC;IACD,yEAAyE;IACzE,oDAAoD;IACpD,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,OAAO;YACL,KAAK,EAAE,cAAc;YACrB,OAAO,EAAE,8CAA8C,MAAM,IAAI,eAAe,0KAA0K;SAC3P,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,yFAAyF,EAAE,CAAC;AACvI,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A stable identity for "the ScriptSync bridge of this user on this machine".
3
+ *
4
+ * The SN Utils helper tab re-sends ServiceNow session tokens on its own when it
5
+ * reconnects, but only to a bridge the user has handed a token to before. This
6
+ * id is how it recognises that bridge after a restart: it is created once,
7
+ * kept private to the user under ~/.sn-scriptsync, and shared by the VS Code
8
+ * host and the standalone snu bridge so switching hosts does not reset trust.
9
+ *
10
+ * Returns an empty string when the file cannot be read or created. The helper
11
+ * then never refreshes automatically and a manual /token is needed, which is
12
+ * the behaviour from before this existed.
13
+ */
14
+ export declare function loadBridgeId(dir?: string): string;
15
+ export type BridgeIdStatus = "ok" | "missing" | "invalid" | "not-a-file" | "unreadable";
16
+ /**
17
+ * Read-only look at the identity file, for diagnostics. Unlike loadBridgeId it
18
+ * never creates the file or changes its mode, and it never returns the id.
19
+ */
20
+ export declare function inspectBridgeId(dir?: string): {
21
+ status: BridgeIdStatus;
22
+ private: boolean | null;
23
+ };
24
+ //# sourceMappingURL=bridgeIdentity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgeIdentity.d.ts","sourceRoot":"","sources":["../../src/server/bridgeIdentity.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAkD,GAAG,MAAM,CA6B5F;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,YAAY,CAAC;AAExF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAkD,GAAG;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAW5I"}
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadBridgeId = loadBridgeId;
37
+ exports.inspectBridgeId = inspectBridgeId;
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ /**
43
+ * A stable identity for "the ScriptSync bridge of this user on this machine".
44
+ *
45
+ * The SN Utils helper tab re-sends ServiceNow session tokens on its own when it
46
+ * reconnects, but only to a bridge the user has handed a token to before. This
47
+ * id is how it recognises that bridge after a restart: it is created once,
48
+ * kept private to the user under ~/.sn-scriptsync, and shared by the VS Code
49
+ * host and the standalone snu bridge so switching hosts does not reset trust.
50
+ *
51
+ * Returns an empty string when the file cannot be read or created. The helper
52
+ * then never refreshes automatically and a manual /token is needed, which is
53
+ * the behaviour from before this existed.
54
+ */
55
+ function loadBridgeId(dir = path.join(os.homedir(), ".sn-scriptsync")) {
56
+ const file = path.join(dir, "bridge-id");
57
+ const read = () => {
58
+ const stat = fs.lstatSync(file);
59
+ if (!stat.isFile() || stat.isSymbolicLink())
60
+ return "";
61
+ // Existing files need the same privacy as newly created ones.
62
+ if (process.platform !== "win32")
63
+ fs.chmodSync(file, 0o600);
64
+ const id = fs.readFileSync(file, "utf8").trim();
65
+ return /^[a-f0-9]{64}$/.test(id) ? id : "";
66
+ };
67
+ try {
68
+ return read();
69
+ }
70
+ catch (err) {
71
+ if (err.code !== "ENOENT")
72
+ return "";
73
+ }
74
+ let temporary = "";
75
+ try {
76
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
77
+ const id = crypto.randomBytes(32).toString("hex");
78
+ temporary = path.join(dir, `.bridge-id-${crypto.randomBytes(16).toString("hex")}`);
79
+ fs.writeFileSync(temporary, id + "\n", { mode: 0o600, flag: "wx" });
80
+ // Publish only a complete file, without replacing another host's identity.
81
+ try {
82
+ fs.linkSync(temporary, file);
83
+ }
84
+ catch (err) {
85
+ if (err.code !== "EEXIST")
86
+ return "";
87
+ }
88
+ return read();
89
+ }
90
+ catch {
91
+ return "";
92
+ }
93
+ finally {
94
+ if (temporary) {
95
+ try {
96
+ fs.unlinkSync(temporary);
97
+ }
98
+ catch { /* best effort */ }
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Read-only look at the identity file, for diagnostics. Unlike loadBridgeId it
104
+ * never creates the file or changes its mode, and it never returns the id.
105
+ */
106
+ function inspectBridgeId(dir = path.join(os.homedir(), ".sn-scriptsync")) {
107
+ const file = path.join(dir, "bridge-id");
108
+ try {
109
+ const stat = fs.lstatSync(file);
110
+ if (!stat.isFile() || stat.isSymbolicLink())
111
+ return { status: "not-a-file", private: null };
112
+ const isPrivate = process.platform === "win32" ? null : (stat.mode & 0o077) === 0;
113
+ const id = fs.readFileSync(file, "utf8").trim();
114
+ return { status: /^[a-f0-9]{64}$/.test(id) ? "ok" : "invalid", private: isPrivate };
115
+ }
116
+ catch (err) {
117
+ return { status: err.code === "ENOENT" ? "missing" : "unreadable", private: null };
118
+ }
119
+ }
120
+ //# sourceMappingURL=bridgeIdentity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridgeIdentity.js","sourceRoot":"","sources":["../../src/server/bridgeIdentity.ts"],"names":[],"mappings":"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"}
@@ -40,7 +40,6 @@ export declare class StandaloneDispatcher {
40
40
  private resolveScopeForCreate;
41
41
  listInstanceFolders(): string[];
42
42
  private pullTablePage;
43
- private allCodeTables;
44
43
  private restGet;
45
44
  resolveInstance(requestInstance?: string): {
46
45
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/server/dispatcher.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAA0B,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAoB,aAAa,EAAE,MAAM,aAAa,CAAC;AAiD9D,eAAO,MAAM,uBAAuB,UAInC,CAAC;AAwGF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAoBxE;AAwBD,MAAM,WAAW,2BAA2B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,EAAE,CAAqB;IAC/B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,qBAAqB,CAA6B;IAC1D,OAAO,CAAC,wBAAwB,CAA6B;gBAEjD,IAAI,EAAE,2BAA2B;IAO7C,gBAAgB,IAAI,MAAM;IAI1B,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,GAAG;IAkBxC,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAyDvC,yBAAyB;YAiDzB,sBAAsB;IA4BpC;;;;;;;OAOG;YACW,qBAAqB;IAmCnC,mBAAmB,IAAI,MAAM,EAAE;YAgBjB,aAAa;IAkK3B,OAAO,CAAC,aAAa;YASP,OAAO;IAmBrB,eAAe,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE;IA+E1F,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,SAAc,GAAG,IAAI;IAa/C,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;CAg8B1D"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/server/dispatcher.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAA0B,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAoB,aAAa,EAAE,MAAM,aAAa,CAAC;AAiD9D,eAAO,MAAM,uBAAuB,UAInC,CAAC;AAwGF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAoBxE;AAwBD,MAAM,WAAW,2BAA2B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,EAAE,CAAqB;IAC/B,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,qBAAqB,CAA6B;IAC1D,OAAO,CAAC,wBAAwB,CAA6B;gBAEjD,IAAI,EAAE,2BAA2B;IAO7C,gBAAgB,IAAI,MAAM;IAI1B,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,GAAG;IAkBxC,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;YAyDvC,yBAAyB;YAiDzB,sBAAsB;IA4BpC;;;;;;;OAOG;YACW,qBAAqB;IAmCnC,mBAAmB,IAAI,MAAM,EAAE;YAgBjB,aAAa;YAwKb,OAAO;IAoBrB,eAAe,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,GAAG,CAAA;KAAE;IAuF1F,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,SAAc,GAAG,IAAI;IAa/C,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;CAi8B1D"}
@@ -35,6 +35,8 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.StandaloneDispatcher = exports.HELPER_CONNECT_GUIDANCE = void 0;
37
37
  exports.resolveMappedFileName = resolveMappedFileName;
38
+ const authProbe_js_1 = require("./authProbe.js");
39
+ const scopeDiscovery_js_1 = require("./scopeDiscovery.js");
38
40
  const fs = __importStar(require("fs"));
39
41
  const os = __importStar(require("os"));
40
42
  const path = __importStar(require("path"));
@@ -166,11 +168,11 @@ function resolveTableCodeFields(tableName, cwd) {
166
168
  }
167
169
  return ['script'];
168
170
  }
169
- function resolveFieldExtension(tableName, fieldName, cwd) {
171
+ function resolveFieldExtension(tableName, fieldName, cwd, liveType) {
170
172
  const meta = getMetaDataRelations(cwd);
171
173
  let fieldType = 'script';
172
174
  try {
173
- fieldType = meta?.tableFields?.[tableName]?.codeFields?.[fieldName]?.type || fieldName;
175
+ fieldType = liveType || meta?.tableFields?.[tableName]?.codeFields?.[fieldName]?.type || fieldName;
174
176
  }
175
177
  catch { }
176
178
  let ext = FIELDTYPES[fieldType]?.extension;
@@ -475,7 +477,7 @@ class StandaloneDispatcher {
475
477
  sysparm_offset: String(offset),
476
478
  sysparm_display_value: 'false',
477
479
  sysparm_exclude_reference_link: 'true',
478
- sysparm_no_count: 'true',
480
+ sysparm_no_count: 'false',
479
481
  };
480
482
  if (combinedQuery) {
481
483
  queryParams.sysparm_query = combinedQuery;
@@ -499,6 +501,7 @@ class StandaloneDispatcher {
499
501
  const isFolderRecordTable = FOLDERRECORDTABLES.includes(table);
500
502
  let filesWritten = 0;
501
503
  let skippedEmpty = 0;
504
+ let skippedUnreadable = 0;
502
505
  const warnings = [];
503
506
  const pulledRecordsList = [];
504
507
  for (const rec of matchedRecords) {
@@ -553,7 +556,12 @@ class StandaloneDispatcher {
553
556
  }
554
557
  const recordFiles = [];
555
558
  for (const field of codeFields) {
556
- const ext = resolveFieldExtension(table, field, this.cwd);
559
+ if (!Object.prototype.hasOwnProperty.call(rec, field)) {
560
+ skippedUnreadable++;
561
+ warnings.push(`${table}/${sysId}.${field}: field not returned; local file preserved.`);
562
+ continue;
563
+ }
564
+ const ext = resolveFieldExtension(table, field, this.cwd, opts.fieldDefinitions?.[field]?.type);
557
565
  let targetPath;
558
566
  try {
559
567
  targetPath = isFolderRecordTable
@@ -603,27 +611,18 @@ class StandaloneDispatcher {
603
611
  });
604
612
  }
605
613
  return {
614
+ pagination: res.pagination,
606
615
  table,
607
616
  matchedRecords: matchedRecords.length,
608
617
  pulledRecords: pulledRecordsList.length,
609
618
  filesWritten,
610
619
  skippedEmpty,
620
+ skippedUnreadable,
611
621
  warnings,
612
622
  records: pulledRecordsList,
613
623
  };
614
624
  }
615
- // Which artifact tables snu knows how to write to disk: the ones in
616
- // resources/metaDataRelations.json that declare code fields.
617
- allCodeTables() {
618
- const meta = getMetaDataRelations(this.cwd);
619
- const out = new Set();
620
- for (const [table, def] of Object.entries(meta?.tableFields || {})) {
621
- if (def?.codeFields && typeof def.codeFields === 'object')
622
- out.add(table);
623
- }
624
- return out;
625
- }
626
- async restGet(inst, reqCommand, endpoint, queryParams) {
625
+ async restGet(inst, reqCommand, endpoint, queryParams, withPagination = false) {
627
626
  const correlationId = crypto.randomUUID();
628
627
  const pendingPromise = this.pending.register({ id: correlationId, command: reqCommand, timeoutMs: 70_000 });
629
628
  this.ws.sendToBrowser({
@@ -637,9 +636,10 @@ class StandaloneDispatcher {
637
636
  });
638
637
  const res = await pendingPromise;
639
638
  if (res?.success === false) {
640
- throw Object.assign(new Error(res.error || `REST request failed: ${endpoint}`), { code: res.code || 'E_COMMAND_FAILED' });
639
+ const hint = res?.status ? '' : (0, authProbe_js_1.helperFailureHint)(typeof res?.error === 'string' ? res.error : '', inst.settings?.url);
640
+ throw Object.assign(new Error([res.error || `REST request failed: ${endpoint}`, hint].filter(Boolean).join(' ')), { code: res.code || 'E_COMMAND_FAILED' });
641
641
  }
642
- return res?.data;
642
+ return withPagination ? { rows: res?.data?.result, pagination: res?.pagination } : res?.data;
643
643
  }
644
644
  resolveInstance(requestInstance) {
645
645
  const folders = this.listInstanceFolders();
@@ -680,8 +680,16 @@ class StandaloneDispatcher {
680
680
  if (live) {
681
681
  return { name: live.name, folder: this.cwd, settings: live };
682
682
  }
683
- throw Object.assign(new Error(`No authenticated instance named "${requestInstance}" is connected. Run /token on that ServiceNow instance and retry.`), {
683
+ const known = [
684
+ ...liveInstances.map((candidate) => `${candidate.name} (${candidate.url})`),
685
+ ...folders.map((folder) => path.basename(folder)),
686
+ ];
687
+ const knownText = known.length
688
+ ? ` Known here: ${[...new Set(known)].join(', ')}. Pass one of these as the instance, by name, hostname or origin.`
689
+ : ' This bridge has not received any session since it started.';
690
+ throw Object.assign(new Error(`No authenticated instance named "${requestInstance}" is connected.${knownText} Otherwise open that exact hostname in the browser, run /token and retry.`), {
684
691
  code: 'E_INSTANCE_NOT_FOUND',
692
+ details: { requested: requestInstance, liveInstances: liveInstances.map((candidate) => ({ name: candidate.name, url: candidate.url })) },
685
693
  });
686
694
  }
687
695
  if (folders.length === 1) {
@@ -792,6 +800,18 @@ class StandaloneDispatcher {
792
800
  message: 'The SN Utils helper tab is not connected, so the session cannot be checked.',
793
801
  });
794
802
  }
803
+ const hasLiveSession = !!origin && this.ws.getLiveInstances().some((live) => {
804
+ try {
805
+ return new URL(live.url).origin.toLowerCase() === origin;
806
+ }
807
+ catch {
808
+ return false;
809
+ }
810
+ });
811
+ if (!inst.settings?.g_ck) {
812
+ const verdict = (0, authProbe_js_1.classifyProbe)({ error: 'Missing instance URL or authentication token', origin, hasLiveSession });
813
+ return reply({ ok: false, state: verdict.state, message: verdict.message });
814
+ }
795
815
  const correlationId = crypto.randomUUID();
796
816
  const pendingPromise = this.pending.register({ id: correlationId, command: req.command, timeoutMs: 15_000 });
797
817
  this.ws.sendToBrowser({
@@ -805,32 +825,25 @@ class StandaloneDispatcher {
805
825
  });
806
826
  try {
807
827
  const res = await pendingPromise;
808
- const status = res?.status;
809
828
  // 401 and 403 mean opposite things: the session is gone, versus the
810
- // session authenticated and an ACL refused the row.
811
- const state = res?.success === false
812
- ? status === 401
813
- ? 'AUTH_EXPIRED'
814
- : status === 403
815
- ? 'AUTH_OK'
816
- : 'AUTH_UNKNOWN'
817
- : 'AUTH_OK';
829
+ // session authenticated and an ACL refused the row. Failures without
830
+ // a status are classified from the helper tab's own error text.
831
+ const verdict = res?.success === false
832
+ ? (0, authProbe_js_1.classifyProbe)({ status: res?.status, error: typeof res?.error === 'string' ? res.error : res?.error?.message, origin, hasLiveSession })
833
+ : (0, authProbe_js_1.classifyProbe)({ status: 200, origin, hasLiveSession });
818
834
  return reply({
819
- ok: state === 'AUTH_OK',
820
- state,
821
- message: state === 'AUTH_OK'
822
- ? 'ServiceNow accepted the session.'
823
- : state === 'AUTH_EXPIRED'
824
- ? 'ServiceNow rejected the session (401). Open the instance in the browser and run /token to refresh it.'
825
- : 'The session check could not complete. This is not a verdict — retry before acting on it.',
835
+ ok: verdict.state === 'AUTH_OK',
836
+ state: verdict.state,
837
+ message: verdict.message,
826
838
  detail: res?.error,
827
839
  });
828
840
  }
829
841
  catch (err) {
842
+ const verdict = (0, authProbe_js_1.classifyProbe)({ status: err?.details?.status, error: err?.message || String(err), origin, hasLiveSession });
830
843
  return reply({
831
844
  ok: false,
832
- state: 'AUTH_UNKNOWN',
833
- message: 'The session check could not complete. This is not a verdict — retry before acting on it.',
845
+ state: verdict.state,
846
+ message: verdict.message,
834
847
  detail: err?.message || String(err),
835
848
  });
836
849
  }
@@ -1427,64 +1440,66 @@ class StandaloneDispatcher {
1427
1440
  }
1428
1441
  catch { }
1429
1442
  }
1430
- // Which artifact tables does this application actually use?
1443
+ const fetchPage = (table, query) => this.restGet(inst, req.command, `/api/now/table/${table}`, query, true);
1444
+ const discovery = await (0, scopeDiscovery_js_1.readAllPages)(fetchPage, 'sys_metadata', `sys_scope=${scopeSysId}^sys_class_name!=sys_metadata_delete^sys_update_name!=NULL^ORDERBYsys_id`, 'sys_class_name');
1431
1445
  const classCounts = new Map();
1432
- const metaPage = 1000;
1433
- for (let offset = 0;; offset += metaPage) {
1434
- const data = await this.restGet(inst, req.command, '/api/now/table/sys_metadata', {
1435
- sysparm_query: `sys_scope=${scopeSysId}^sys_class_name!=sys_metadata_delete^sys_update_name!=NULL^ORDERBYsys_id`,
1436
- sysparm_fields: 'sys_class_name',
1437
- sysparm_limit: String(metaPage),
1438
- sysparm_offset: String(offset),
1439
- sysparm_exclude_reference_link: 'true',
1440
- sysparm_no_count: 'true',
1441
- });
1442
- const rows = Array.isArray(data?.result) ? data.result : [];
1443
- for (const r of rows) {
1444
- const cls = String(typeof r.sys_class_name === 'object' ? r.sys_class_name.value : r.sys_class_name || '');
1445
- if (cls)
1446
- classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
1447
- }
1448
- if (rows.length < metaPage)
1449
- break;
1446
+ for (const row of discovery.rows) {
1447
+ const cls = String(typeof row.sys_class_name === 'object' ? row.sys_class_name.value : row.sys_class_name || '');
1448
+ if (/^[a-zA-Z0-9_]+$/.test(cls))
1449
+ classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
1450
1450
  }
1451
- const codeTables = this.allCodeTables();
1452
- const tables = [...classCounts.keys()].filter((t) => codeTables.has(t) && (!tableFilter || tableFilter.has(t))).sort();
1453
- const skippedTables = [...classCounts.keys()].filter((t) => !codeTables.has(t)).sort()
1454
- .map((t) => ({ table: t, records: classCounts.get(t) || 0 }));
1451
+ const schema = await (0, scopeDiscovery_js_1.discoverScopeFields)([...classCounts.keys()], getMetaDataRelations(this.cwd)?.tableFields || {}, fetchPage);
1452
+ const tables = [...classCounts.keys()].filter(t => Object.keys(schema.definitions[t]?.codeFields || {}).length && (!tableFilter || tableFilter.has(t))).sort();
1453
+ const skippedTables = [...classCounts.keys()].filter(t => !Object.keys(schema.definitions[t]?.codeFields || {}).length).sort()
1454
+ .map(table => ({ table, records: classCounts.get(table) || 0, reason: schema.warnings.some(w => w.startsWith(`${table}:`)) ? 'dictionary_unavailable' : 'no_scriptable_fields' }));
1455
1455
  const missingTables = tableFilter ? [...tableFilter].filter((t) => !classCounts.has(t)).sort() : [];
1456
1456
  const perTable = [];
1457
- const warnings = [];
1458
- let totalRecords = 0, totalFiles = 0, totalSkippedEmpty = 0;
1457
+ const warnings = [...discovery.warnings, ...schema.warnings];
1458
+ let complete = discovery.complete && schema.warnings.length === 0;
1459
+ let totalRecords = 0, totalFiles = 0, totalSkippedEmpty = 0, totalSkippedUnreadable = 0;
1459
1460
  for (const table of tables) {
1460
- const codeFields = resolveTableCodeFields(table, this.cwd);
1461
+ const fieldDefinitions = schema.definitions[table].codeFields;
1462
+ const codeFields = Object.keys(fieldDefinitions);
1461
1463
  const query = `sys_scope=${scopeSysId}^sys_class_name=${table}^ORDERBYsys_id`;
1462
- const entry = { table, matchedRecords: 0, pulledRecords: 0, filesWritten: 0, skippedEmpty: 0, truncated: false };
1464
+ const entry = { table, matchedRecords: 0, pulledRecords: 0, filesWritten: 0, skippedEmpty: 0, skippedUnreadable: 0, truncated: false };
1463
1465
  if (includeRecords)
1464
1466
  entry.records = [];
1465
- for (let offset = 0;; offset += PAGE) {
1467
+ let reportedTotal;
1468
+ for (let offset = 0;;) {
1466
1469
  const pageLimit = Math.min(PAGE, limit - entry.matchedRecords);
1467
1470
  if (pageLimit <= 0) {
1468
1471
  entry.truncated = true;
1469
1472
  break;
1470
1473
  }
1471
- const page = await this.pullTablePage(inst, req.command, { table, query, codeFields, limit: pageLimit, offset });
1474
+ const page = await this.pullTablePage(inst, req.command, { table, query, codeFields, fieldDefinitions, limit: pageLimit, offset });
1475
+ if (page.pagination?.totalCount && /^\d+$/.test(page.pagination.totalCount))
1476
+ reportedTotal = Number(page.pagination.totalCount);
1472
1477
  entry.matchedRecords += page.matchedRecords;
1473
1478
  entry.pulledRecords += page.pulledRecords;
1474
1479
  entry.filesWritten += page.filesWritten;
1475
1480
  entry.skippedEmpty += page.skippedEmpty;
1481
+ entry.skippedUnreadable += page.skippedUnreadable;
1476
1482
  if (includeRecords)
1477
1483
  entry.records.push(...page.records);
1478
1484
  for (const w of page.warnings)
1479
1485
  warnings.push(`${table}: ${w}`);
1480
- if (page.matchedRecords < pageLimit)
1486
+ const paging = (0, scopeDiscovery_js_1.pageState)(page.pagination, offset, pageLimit, page.matchedRecords);
1487
+ if (paging.nextOffset === undefined) {
1488
+ complete = complete && paging.complete;
1489
+ if (paging.warning)
1490
+ warnings.push(`${table}: ${paging.warning}`);
1481
1491
  break;
1492
+ }
1493
+ offset = paging.nextOffset;
1482
1494
  }
1495
+ if (!entry.truncated && reportedTotal !== undefined && reportedTotal > entry.matchedRecords)
1496
+ warnings.push(`${table}: ${reportedTotal - entry.matchedRecords} matching records were not returned; access controls or changing records may account for the difference.`);
1483
1497
  if (entry.truncated)
1484
1498
  warnings.push(`${table}: stopped at the per-table limit of ${limit} records; raise "limit" or pull the rest with pull_records.`);
1485
1499
  totalRecords += entry.pulledRecords;
1486
1500
  totalFiles += entry.filesWritten;
1487
1501
  totalSkippedEmpty += entry.skippedEmpty;
1502
+ totalSkippedUnreadable += entry.skippedUnreadable;
1488
1503
  perTable.push(entry);
1489
1504
  }
1490
1505
  for (const t of missingTables)
@@ -1495,10 +1510,12 @@ class StandaloneDispatcher {
1495
1510
  status: 'success',
1496
1511
  timestamp: Date.now(),
1497
1512
  result: {
1513
+ complete: complete && !perTable.some(t => t.truncated) && warnings.length === 0,
1514
+ metadataRecords: discovery.rows.length,
1498
1515
  scope: { name: scopeName, sys_id: scopeSysId },
1499
1516
  folder: path.join(inst.name, scopeName).replace(/\\/g, '/'),
1500
1517
  tables: perTable,
1501
- totals: { tables: perTable.length, records: totalRecords, filesWritten: totalFiles, skippedEmpty: totalSkippedEmpty },
1518
+ totals: { tables: perTable.length, records: totalRecords, filesWritten: totalFiles, skippedEmpty: totalSkippedEmpty, skippedUnreadable: totalSkippedUnreadable },
1502
1519
  skippedTables,
1503
1520
  warnings,
1504
1521
  },