nowaikit 2.5.0

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.

Potentially problematic release.


This version of nowaikit might be problematic. Click here for more details.

Files changed (209) hide show
  1. package/.env.example +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1253 -0
  4. package/desktop/renderer/dist/apple-touch-icon.png +0 -0
  5. package/desktop/renderer/dist/assets/index-Bb0ncZQl.css +1 -0
  6. package/desktop/renderer/dist/assets/index-MlBBSUMZ.js +49 -0
  7. package/desktop/renderer/dist/favicon-32.png +0 -0
  8. package/desktop/renderer/dist/favicon.svg +18 -0
  9. package/desktop/renderer/dist/index.html +18 -0
  10. package/desktop/serve.js +449 -0
  11. package/dist/cli/auth.d.ts +14 -0
  12. package/dist/cli/auth.d.ts.map +1 -0
  13. package/dist/cli/auth.js +179 -0
  14. package/dist/cli/auth.js.map +1 -0
  15. package/dist/cli/config-store.d.ts +28 -0
  16. package/dist/cli/config-store.d.ts.map +1 -0
  17. package/dist/cli/config-store.js +64 -0
  18. package/dist/cli/config-store.js.map +1 -0
  19. package/dist/cli/detect-clients.d.ts +16 -0
  20. package/dist/cli/detect-clients.d.ts.map +1 -0
  21. package/dist/cli/detect-clients.js +151 -0
  22. package/dist/cli/detect-clients.js.map +1 -0
  23. package/dist/cli/index.d.ts +3 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +193 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/cli/setup.d.ts +4 -0
  28. package/dist/cli/setup.d.ts.map +1 -0
  29. package/dist/cli/setup.js +575 -0
  30. package/dist/cli/setup.js.map +1 -0
  31. package/dist/cli/writers/index.d.ts +9 -0
  32. package/dist/cli/writers/index.d.ts.map +1 -0
  33. package/dist/cli/writers/index.js +140 -0
  34. package/dist/cli/writers/index.js.map +1 -0
  35. package/dist/prompts/index.d.ts +25 -0
  36. package/dist/prompts/index.d.ts.map +1 -0
  37. package/dist/prompts/index.js +38 -0
  38. package/dist/prompts/index.js.map +1 -0
  39. package/dist/prompts/itsm.d.ts +20 -0
  40. package/dist/prompts/itsm.d.ts.map +1 -0
  41. package/dist/prompts/itsm.js +110 -0
  42. package/dist/prompts/itsm.js.map +1 -0
  43. package/dist/prompts/user-prompts.d.ts +3 -0
  44. package/dist/prompts/user-prompts.d.ts.map +1 -0
  45. package/dist/prompts/user-prompts.js +35 -0
  46. package/dist/prompts/user-prompts.js.map +1 -0
  47. package/dist/resources/index.d.ts +26 -0
  48. package/dist/resources/index.d.ts.map +1 -0
  49. package/dist/resources/index.js +99 -0
  50. package/dist/resources/index.js.map +1 -0
  51. package/dist/server.d.ts +3 -0
  52. package/dist/server.d.ts.map +1 -0
  53. package/dist/server.js +129 -0
  54. package/dist/server.js.map +1 -0
  55. package/dist/servicenow/client.d.ts +135 -0
  56. package/dist/servicenow/client.d.ts.map +1 -0
  57. package/dist/servicenow/client.js +803 -0
  58. package/dist/servicenow/client.js.map +1 -0
  59. package/dist/servicenow/instances.d.ts +28 -0
  60. package/dist/servicenow/instances.d.ts.map +1 -0
  61. package/dist/servicenow/instances.js +204 -0
  62. package/dist/servicenow/instances.js.map +1 -0
  63. package/dist/servicenow/types.d.ts +574 -0
  64. package/dist/servicenow/types.d.ts.map +1 -0
  65. package/dist/servicenow/types.js +3 -0
  66. package/dist/servicenow/types.js.map +1 -0
  67. package/dist/tools/agile.d.ts +225 -0
  68. package/dist/tools/agile.d.ts.map +1 -0
  69. package/dist/tools/agile.js +205 -0
  70. package/dist/tools/agile.js.map +1 -0
  71. package/dist/tools/app-studio.d.ts +139 -0
  72. package/dist/tools/app-studio.d.ts.map +1 -0
  73. package/dist/tools/app-studio.js +139 -0
  74. package/dist/tools/app-studio.js.map +1 -0
  75. package/dist/tools/atf.d.ts +144 -0
  76. package/dist/tools/atf.d.ts.map +1 -0
  77. package/dist/tools/atf.js +186 -0
  78. package/dist/tools/atf.js.map +1 -0
  79. package/dist/tools/catalog.d.ts +628 -0
  80. package/dist/tools/catalog.d.ts.map +1 -0
  81. package/dist/tools/catalog.js +397 -0
  82. package/dist/tools/catalog.js.map +1 -0
  83. package/dist/tools/change.d.ts +347 -0
  84. package/dist/tools/change.d.ts.map +1 -0
  85. package/dist/tools/change.js +213 -0
  86. package/dist/tools/change.js.map +1 -0
  87. package/dist/tools/core.d.ts +540 -0
  88. package/dist/tools/core.d.ts.map +1 -0
  89. package/dist/tools/core.js +373 -0
  90. package/dist/tools/core.js.map +1 -0
  91. package/dist/tools/csm.d.ts +401 -0
  92. package/dist/tools/csm.d.ts.map +1 -0
  93. package/dist/tools/csm.js +255 -0
  94. package/dist/tools/csm.js.map +1 -0
  95. package/dist/tools/deployment.d.ts +366 -0
  96. package/dist/tools/deployment.d.ts.map +1 -0
  97. package/dist/tools/deployment.js +181 -0
  98. package/dist/tools/deployment.js.map +1 -0
  99. package/dist/tools/devops.d.ts +236 -0
  100. package/dist/tools/devops.d.ts.map +1 -0
  101. package/dist/tools/devops.js +207 -0
  102. package/dist/tools/devops.js.map +1 -0
  103. package/dist/tools/flow.d.ts +496 -0
  104. package/dist/tools/flow.d.ts.map +1 -0
  105. package/dist/tools/flow.js +348 -0
  106. package/dist/tools/flow.js.map +1 -0
  107. package/dist/tools/hrsd.d.ts +789 -0
  108. package/dist/tools/hrsd.d.ts.map +1 -0
  109. package/dist/tools/hrsd.js +377 -0
  110. package/dist/tools/hrsd.js.map +1 -0
  111. package/dist/tools/incident.d.ts +256 -0
  112. package/dist/tools/incident.d.ts.map +1 -0
  113. package/dist/tools/incident.js +163 -0
  114. package/dist/tools/incident.js.map +1 -0
  115. package/dist/tools/index.d.ts +11514 -0
  116. package/dist/tools/index.d.ts.map +1 -0
  117. package/dist/tools/index.js +276 -0
  118. package/dist/tools/index.js.map +1 -0
  119. package/dist/tools/integration.d.ts +603 -0
  120. package/dist/tools/integration.d.ts.map +1 -0
  121. package/dist/tools/integration.js +510 -0
  122. package/dist/tools/integration.js.map +1 -0
  123. package/dist/tools/itam.d.ts +462 -0
  124. package/dist/tools/itam.d.ts.map +1 -0
  125. package/dist/tools/itam.js +306 -0
  126. package/dist/tools/itam.js.map +1 -0
  127. package/dist/tools/knowledge.d.ts +187 -0
  128. package/dist/tools/knowledge.d.ts.map +1 -0
  129. package/dist/tools/knowledge.js +161 -0
  130. package/dist/tools/knowledge.js.map +1 -0
  131. package/dist/tools/ml.d.ts +263 -0
  132. package/dist/tools/ml.d.ts.map +1 -0
  133. package/dist/tools/ml.js +251 -0
  134. package/dist/tools/ml.js.map +1 -0
  135. package/dist/tools/mobile.d.ts +352 -0
  136. package/dist/tools/mobile.d.ts.map +1 -0
  137. package/dist/tools/mobile.js +122 -0
  138. package/dist/tools/mobile.js.map +1 -0
  139. package/dist/tools/notification.d.ts +590 -0
  140. package/dist/tools/notification.d.ts.map +1 -0
  141. package/dist/tools/notification.js +382 -0
  142. package/dist/tools/notification.js.map +1 -0
  143. package/dist/tools/now-assist.d.ts +300 -0
  144. package/dist/tools/now-assist.d.ts.map +1 -0
  145. package/dist/tools/now-assist.js +227 -0
  146. package/dist/tools/now-assist.js.map +1 -0
  147. package/dist/tools/performance.d.ts +447 -0
  148. package/dist/tools/performance.d.ts.map +1 -0
  149. package/dist/tools/performance.js +473 -0
  150. package/dist/tools/performance.js.map +1 -0
  151. package/dist/tools/portal.d.ts +530 -0
  152. package/dist/tools/portal.d.ts.map +1 -0
  153. package/dist/tools/portal.js +425 -0
  154. package/dist/tools/portal.js.map +1 -0
  155. package/dist/tools/problem.d.ts +110 -0
  156. package/dist/tools/problem.d.ts.map +1 -0
  157. package/dist/tools/problem.js +100 -0
  158. package/dist/tools/problem.js.map +1 -0
  159. package/dist/tools/reporting.d.ts +825 -0
  160. package/dist/tools/reporting.d.ts.map +1 -0
  161. package/dist/tools/reporting.js +460 -0
  162. package/dist/tools/reporting.js.map +1 -0
  163. package/dist/tools/script.d.ts +714 -0
  164. package/dist/tools/script.d.ts.map +1 -0
  165. package/dist/tools/script.js +629 -0
  166. package/dist/tools/script.js.map +1 -0
  167. package/dist/tools/security.d.ts +794 -0
  168. package/dist/tools/security.d.ts.map +1 -0
  169. package/dist/tools/security.js +425 -0
  170. package/dist/tools/security.js.map +1 -0
  171. package/dist/tools/sys-properties.d.ts +315 -0
  172. package/dist/tools/sys-properties.d.ts.map +1 -0
  173. package/dist/tools/sys-properties.js +372 -0
  174. package/dist/tools/sys-properties.js.map +1 -0
  175. package/dist/tools/task.d.ts +82 -0
  176. package/dist/tools/task.d.ts.map +1 -0
  177. package/dist/tools/task.js +96 -0
  178. package/dist/tools/task.js.map +1 -0
  179. package/dist/tools/updateset.d.ts +159 -0
  180. package/dist/tools/updateset.d.ts.map +1 -0
  181. package/dist/tools/updateset.js +212 -0
  182. package/dist/tools/updateset.js.map +1 -0
  183. package/dist/tools/user.d.ts +206 -0
  184. package/dist/tools/user.d.ts.map +1 -0
  185. package/dist/tools/user.js +163 -0
  186. package/dist/tools/user.js.map +1 -0
  187. package/dist/tools/va.d.ts +217 -0
  188. package/dist/tools/va.d.ts.map +1 -0
  189. package/dist/tools/va.js +178 -0
  190. package/dist/tools/va.js.map +1 -0
  191. package/dist/tools/workspace.d.ts +565 -0
  192. package/dist/tools/workspace.d.ts.map +1 -0
  193. package/dist/tools/workspace.js +201 -0
  194. package/dist/tools/workspace.js.map +1 -0
  195. package/dist/tools-manifest.json +7852 -0
  196. package/dist/utils/errors.d.ts +6 -0
  197. package/dist/utils/errors.d.ts.map +1 -0
  198. package/dist/utils/errors.js +11 -0
  199. package/dist/utils/errors.js.map +1 -0
  200. package/dist/utils/logging.d.ts +7 -0
  201. package/dist/utils/logging.d.ts.map +1 -0
  202. package/dist/utils/logging.js +15 -0
  203. package/dist/utils/logging.js.map +1 -0
  204. package/dist/utils/permissions.d.ts +21 -0
  205. package/dist/utils/permissions.d.ts.map +1 -0
  206. package/dist/utils/permissions.js +54 -0
  207. package/dist/utils/permissions.js.map +1 -0
  208. package/instances.example.json +71 -0
  209. package/package.json +110 -0
@@ -0,0 +1,803 @@
1
+ import { ServiceNowError } from '../utils/errors.js';
2
+ import { logger } from '../utils/logging.js';
3
+ // ─── Input validation helpers ────────────────────────────────────────────────
4
+ /** Validate and sanitize ServiceNow table names (alphanumeric + underscores only) */
5
+ function validateTableName(table) {
6
+ if (!table || !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(table)) {
7
+ throw new ServiceNowError(`Invalid table name: "${table}". Must contain only letters, numbers, and underscores.`, 'VALIDATION_ERROR');
8
+ }
9
+ return table;
10
+ }
11
+ /** Validate ServiceNow sys_id format (32-char hex string) */
12
+ function validateSysId(sysId) {
13
+ if (!sysId || !/^[0-9a-f]{32}$/i.test(sysId)) {
14
+ throw new ServiceNowError(`Invalid sys_id: "${sysId}". Must be a 32-character hex string.`, 'VALIDATION_ERROR');
15
+ }
16
+ return sysId;
17
+ }
18
+ /** Allowlist of safe GlideSystem functions permitted in javascript: query expressions */
19
+ const SAFE_GS_PATTERN = /^javascript:gs\.(getUserID|beginningOfToday|endOfToday|beginningOfYesterday|endOfYesterday|beginningOfLastMonth|endOfLastMonth|beginningOfThisMonth|endOfThisMonth|beginningOfThisQuarter|endOfThisQuarter|beginningOfThisYear|endOfThisYear|beginningOfNextMonth|endOfNextMonth|beginningOfLast7Days|endOfLast7Days|beginningOfLastYear|endOfLastYear|daysAgo|hoursAgo|minutesAgo|monthsAgo|quartersAgo|yearsAgo|now|dateGenerate)\([\d,\s'":-]*\)$/i;
20
+ /** Validate and sanitize ServiceNow encoded query strings */
21
+ function validateQuery(query) {
22
+ if (!query)
23
+ return query;
24
+ // Validate javascript: expressions against safe GlideSystem function allowlist
25
+ const jsMatches = query.match(/javascript:[^@^]*/gi);
26
+ if (jsMatches) {
27
+ for (const match of jsMatches) {
28
+ if (!SAFE_GS_PATTERN.test(match.trim())) {
29
+ throw new ServiceNowError(`Query contains unsafe JavaScript expression: "${match.substring(0, 60)}…". Only standard GlideSystem date/user functions are allowed.`, 'VALIDATION_ERROR');
30
+ }
31
+ }
32
+ }
33
+ // Enforce max query length
34
+ if (query.length > 4096) {
35
+ throw new ServiceNowError('Query string exceeds maximum length of 4096 characters.', 'VALIDATION_ERROR');
36
+ }
37
+ return query;
38
+ }
39
+ export class ServiceNowClient {
40
+ baseUrl;
41
+ authMethod;
42
+ authMode;
43
+ oauthConfig;
44
+ basicConfig;
45
+ maxRetries;
46
+ retryDelayMs;
47
+ requestTimeoutMs;
48
+ /** For impersonation mode: user sys_id to pass in X-Sn-Impersonate */
49
+ impersonateUserSysId;
50
+ /** For per-user mode: pre-loaded token overrides service-account auth */
51
+ perUserBearerToken;
52
+ accessToken;
53
+ tokenExpiry;
54
+ constructor(config) {
55
+ this.baseUrl = config.instanceUrl.replace(/\/$/, ''); // Remove trailing slash
56
+ this.authMethod = config.authMethod;
57
+ this.authMode = config.authMode || 'service-account';
58
+ this.oauthConfig = config.oauth;
59
+ this.basicConfig = config.basic;
60
+ this.maxRetries = config.maxRetries || 3;
61
+ this.retryDelayMs = config.retryDelayMs || 1000;
62
+ this.requestTimeoutMs = config.requestTimeoutMs || 30000;
63
+ this.impersonateUserSysId = config.impersonateUserSysId;
64
+ this.perUserBearerToken = config.perUserBearerToken;
65
+ }
66
+ /**
67
+ * Return a copy of this client configured to run as a specific user.
68
+ * Used for per-request user context switching without mutating the shared client.
69
+ */
70
+ withUser(options) {
71
+ const copy = Object.create(Object.getPrototypeOf(this));
72
+ Object.assign(copy, this);
73
+ if (options.sysId) {
74
+ copy.authMode = 'impersonation';
75
+ copy.impersonateUserSysId = options.sysId;
76
+ }
77
+ if (options.bearerToken) {
78
+ copy.authMode = 'per-user';
79
+ copy.perUserBearerToken = options.bearerToken;
80
+ }
81
+ return copy;
82
+ }
83
+ /**
84
+ * Authenticate with ServiceNow using OAuth or Basic Auth
85
+ */
86
+ async authenticate() {
87
+ if (this.authMethod === 'basic') {
88
+ // Basic auth doesn't require token acquisition
89
+ return;
90
+ }
91
+ // Check if we have a valid token
92
+ if (this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry) {
93
+ return; // Token still valid
94
+ }
95
+ // Acquire OAuth token
96
+ if (!this.oauthConfig?.clientId || !this.oauthConfig?.clientSecret) {
97
+ throw new ServiceNowError('OAuth client ID and secret are required for OAuth authentication', 'AUTHENTICATION_FAILED');
98
+ }
99
+ if (!this.oauthConfig?.username || !this.oauthConfig?.password) {
100
+ throw new ServiceNowError('Username and password are required for OAuth password grant', 'AUTHENTICATION_FAILED');
101
+ }
102
+ const tokenUrl = `${this.baseUrl}/oauth_token.do`;
103
+ const body = new URLSearchParams({
104
+ grant_type: 'password',
105
+ client_id: this.oauthConfig.clientId,
106
+ client_secret: this.oauthConfig.clientSecret,
107
+ username: this.oauthConfig.username,
108
+ password: this.oauthConfig.password,
109
+ });
110
+ try {
111
+ const response = await fetch(tokenUrl, {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/x-www-form-urlencoded',
115
+ },
116
+ body: body.toString(),
117
+ });
118
+ if (!response.ok) {
119
+ throw new ServiceNowError(`OAuth authentication failed: ${response.status} ${response.statusText}`, 'AUTHENTICATION_FAILED');
120
+ }
121
+ const tokenData = await response.json();
122
+ this.accessToken = tokenData.access_token;
123
+ // Set expiry to 90% of actual expiry time for safety margin
124
+ this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000 * 0.9);
125
+ logger.debug('OAuth token acquired successfully');
126
+ }
127
+ catch (error) {
128
+ if (error instanceof ServiceNowError) {
129
+ throw error;
130
+ }
131
+ throw new ServiceNowError(`OAuth authentication error: ${error instanceof Error ? error.message : 'Unknown error'}`, 'AUTHENTICATION_FAILED');
132
+ }
133
+ }
134
+ /**
135
+ * Get authorization header for requests.
136
+ * Per-user mode returns the user's own Bearer token directly.
137
+ * Impersonation and service-account modes use the configured service account.
138
+ */
139
+ getAuthHeader() {
140
+ // Per-user: use the individual user's token (highest precedence)
141
+ if (this.authMode === 'per-user' && this.perUserBearerToken) {
142
+ return `Bearer ${this.perUserBearerToken}`;
143
+ }
144
+ if (this.authMethod === 'basic') {
145
+ if (!this.basicConfig?.username || !this.basicConfig?.password) {
146
+ throw new ServiceNowError('Username and password are required for Basic authentication', 'AUTHENTICATION_FAILED');
147
+ }
148
+ const credentials = Buffer.from(`${this.basicConfig.username}:${this.basicConfig.password}`).toString('base64');
149
+ return `Basic ${credentials}`;
150
+ }
151
+ else {
152
+ if (!this.accessToken) {
153
+ throw new ServiceNowError('OAuth token not available. Call authenticate() first.', 'AUTHENTICATION_FAILED');
154
+ }
155
+ return `Bearer ${this.accessToken}`;
156
+ }
157
+ }
158
+ /**
159
+ * Returns the X-Sn-Impersonate header value if impersonation mode is active.
160
+ * ServiceNow executes the request in the context of the named user's roles/ACLs.
161
+ */
162
+ getImpersonateHeader() {
163
+ if (this.authMode === 'impersonation' && this.impersonateUserSysId) {
164
+ return this.impersonateUserSysId;
165
+ }
166
+ return undefined;
167
+ }
168
+ /**
169
+ * Make HTTP request with retry logic
170
+ */
171
+ async request(url, options = {}) {
172
+ let lastError;
173
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
174
+ try {
175
+ const controller = new AbortController();
176
+ const timeout = setTimeout(() => controller.abort(), this.requestTimeoutMs);
177
+ const extraHeaders = {};
178
+ const impersonateHeader = this.getImpersonateHeader();
179
+ if (impersonateHeader) {
180
+ extraHeaders['X-Sn-Impersonate'] = impersonateHeader;
181
+ }
182
+ const response = await fetch(url, {
183
+ ...options,
184
+ signal: controller.signal,
185
+ headers: {
186
+ 'Accept': 'application/json',
187
+ 'Content-Type': 'application/json',
188
+ 'Authorization': this.getAuthHeader(),
189
+ ...extraHeaders,
190
+ ...options.headers,
191
+ },
192
+ });
193
+ clearTimeout(timeout);
194
+ // Handle HTTP errors
195
+ if (!response.ok) {
196
+ const errorText = await response.text();
197
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
198
+ try {
199
+ const errorJson = JSON.parse(errorText);
200
+ if (errorJson.error?.message) {
201
+ errorMessage = errorJson.error.message;
202
+ }
203
+ }
204
+ catch {
205
+ // Error response wasn't JSON, use status text
206
+ }
207
+ // Map HTTP status to error codes
208
+ let errorCode = 'API_ERROR';
209
+ if (response.status === 401) {
210
+ errorCode = 'AUTHENTICATION_FAILED';
211
+ }
212
+ else if (response.status === 403) {
213
+ errorCode = 'INSUFFICIENT_PRIVILEGES';
214
+ }
215
+ else if (response.status === 404) {
216
+ errorCode = 'NOT_FOUND';
217
+ }
218
+ else if (response.status === 400) {
219
+ errorCode = 'INVALID_REQUEST';
220
+ }
221
+ throw new ServiceNowError(errorMessage, errorCode);
222
+ }
223
+ const data = await response.json();
224
+ return data;
225
+ }
226
+ catch (error) {
227
+ lastError = error instanceof Error ? error : new Error('Unknown error');
228
+ // Don't retry on auth errors or invalid requests
229
+ if (error instanceof ServiceNowError) {
230
+ if (['AUTHENTICATION_FAILED', 'INVALID_REQUEST', 'NOT_FOUND'].includes(error.code)) {
231
+ throw error;
232
+ }
233
+ }
234
+ // Retry on network errors or server errors
235
+ if (attempt < this.maxRetries) {
236
+ const delay = this.retryDelayMs * Math.pow(2, attempt); // Exponential backoff
237
+ logger.warn(`Request failed, retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
238
+ await new Promise(resolve => setTimeout(resolve, delay));
239
+ continue;
240
+ }
241
+ }
242
+ }
243
+ // Surface the real cause from Node.js fetch failures
244
+ if (lastError) {
245
+ const cause = lastError.cause;
246
+ if (cause) {
247
+ throw new ServiceNowError(`Failed to query records: ${cause.message}`, cause.code || 'NETWORK_ERROR');
248
+ }
249
+ throw lastError;
250
+ }
251
+ throw new Error('Request failed after retries');
252
+ }
253
+ /**
254
+ * Query records from a ServiceNow table
255
+ */
256
+ async queryRecords(params) {
257
+ // Validate inputs
258
+ validateTableName(params.table);
259
+ if (params.query)
260
+ validateQuery(params.query);
261
+ // Authenticate before making API calls
262
+ await this.authenticate();
263
+ // Build query parameters
264
+ const queryParams = new URLSearchParams();
265
+ if (params.query) {
266
+ queryParams.set('sysparm_query', params.query);
267
+ }
268
+ if (params.fields) {
269
+ queryParams.set('sysparm_fields', params.fields);
270
+ }
271
+ if (params.limit !== undefined) {
272
+ queryParams.set('sysparm_limit', Math.min(params.limit, 1000).toString());
273
+ }
274
+ else {
275
+ queryParams.set('sysparm_limit', '10'); // Default limit
276
+ }
277
+ if (params.offset !== undefined) {
278
+ queryParams.set('sysparm_offset', params.offset.toString());
279
+ }
280
+ if (params.orderBy) {
281
+ // Handle descending sort (prefix with "-")
282
+ if (params.orderBy.startsWith('-')) {
283
+ const field = params.orderBy.substring(1);
284
+ queryParams.set('sysparm_query', params.query
285
+ ? `${params.query}^ORDERBY${field}^ORDERBYDESC`
286
+ : `ORDERBY${field}^ORDERBYDESC`);
287
+ }
288
+ else {
289
+ queryParams.set('sysparm_query', params.query
290
+ ? `${params.query}^ORDERBY${params.orderBy}`
291
+ : `ORDERBY${params.orderBy}`);
292
+ }
293
+ }
294
+ const url = `${this.baseUrl}/api/now/table/${params.table}?${queryParams.toString()}`;
295
+ logger.info(`Querying ServiceNow table: ${params.table}`);
296
+ logger.debug(`Query: ${params.query || 'none'}`);
297
+ try {
298
+ const response = await this.request(url);
299
+ return {
300
+ count: response.result.length,
301
+ records: response.result,
302
+ };
303
+ }
304
+ catch (error) {
305
+ if (error instanceof ServiceNowError) {
306
+ throw error;
307
+ }
308
+ throw new ServiceNowError(`Failed to query records: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
309
+ }
310
+ }
311
+ /**
312
+ * Get table schema/structure
313
+ */
314
+ async getTableSchema(tableName) {
315
+ await this.authenticate();
316
+ const url = `${this.baseUrl}/api/now/table/${tableName}?sysparm_exclude_reference_link=true&sysparm_limit=1`;
317
+ logger.info(`Getting schema for table: ${tableName}`);
318
+ try {
319
+ // Get table structure by querying with limit=1
320
+ const response = await this.request(url);
321
+ // Extract field names and types from the result
322
+ if (response.result && response.result.length > 0) {
323
+ const sample = response.result[0];
324
+ const columns = Object.keys(sample).map(key => ({
325
+ element: key,
326
+ value_sample: sample[key],
327
+ }));
328
+ return {
329
+ table: tableName,
330
+ columns,
331
+ };
332
+ }
333
+ return {
334
+ table: tableName,
335
+ columns: [],
336
+ };
337
+ }
338
+ catch (error) {
339
+ if (error instanceof ServiceNowError) {
340
+ throw error;
341
+ }
342
+ throw new ServiceNowError(`Failed to get table schema: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
343
+ }
344
+ }
345
+ /**
346
+ * Get a single record by sys_id
347
+ */
348
+ async getRecord(table, sysId, fields) {
349
+ validateTableName(table);
350
+ validateSysId(sysId);
351
+ await this.authenticate();
352
+ const queryParams = new URLSearchParams();
353
+ if (fields) {
354
+ queryParams.set('sysparm_fields', fields);
355
+ }
356
+ const url = `${this.baseUrl}/api/now/table/${table}/${sysId}${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
357
+ logger.info(`Getting record from ${table}: ${sysId}`);
358
+ try {
359
+ const response = await this.request(url);
360
+ return response.result;
361
+ }
362
+ catch (error) {
363
+ if (error instanceof ServiceNowError) {
364
+ throw error;
365
+ }
366
+ throw new ServiceNowError(`Failed to get record: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
367
+ }
368
+ }
369
+ /**
370
+ * Get user details by email or username
371
+ */
372
+ async getUser(userIdentifier) {
373
+ await this.authenticate();
374
+ // Try user_name, email, or sys_id
375
+ if (/^[0-9a-f]{32}$/i.test(userIdentifier)) {
376
+ return await this.getRecord('sys_user', userIdentifier);
377
+ }
378
+ const query = `user_name=${userIdentifier}^ORemail=${userIdentifier}`;
379
+ const url = `${this.baseUrl}/api/now/table/sys_user?sysparm_query=${query}&sysparm_limit=1`;
380
+ logger.info(`Looking up user: ${userIdentifier}`);
381
+ try {
382
+ const response = await this.request(url);
383
+ if (response.result.length === 0) {
384
+ throw new ServiceNowError(`User not found: ${userIdentifier}`, 'NOT_FOUND');
385
+ }
386
+ return response.result[0];
387
+ }
388
+ catch (error) {
389
+ if (error instanceof ServiceNowError) {
390
+ throw error;
391
+ }
392
+ throw new ServiceNowError(`Failed to get user: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
393
+ }
394
+ }
395
+ /**
396
+ * Get group details by name or sys_id
397
+ */
398
+ async getGroup(groupIdentifier) {
399
+ await this.authenticate();
400
+ // Check if it's a sys_id (32 hex chars) or name
401
+ const isSysId = /^[0-9a-f]{32}$/i.test(groupIdentifier);
402
+ const query = isSysId ? `sys_id=${groupIdentifier}` : `name=${groupIdentifier}`;
403
+ const url = `${this.baseUrl}/api/now/table/sys_user_group?sysparm_query=${query}&sysparm_limit=1`;
404
+ logger.info(`Looking up group: ${groupIdentifier}`);
405
+ try {
406
+ const response = await this.request(url);
407
+ if (response.result.length === 0) {
408
+ throw new ServiceNowError(`Group not found: ${groupIdentifier}`, 'NOT_FOUND');
409
+ }
410
+ return response.result[0];
411
+ }
412
+ catch (error) {
413
+ if (error instanceof ServiceNowError) {
414
+ throw error;
415
+ }
416
+ throw new ServiceNowError(`Failed to get group: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
417
+ }
418
+ }
419
+ /**
420
+ * Search CMDB configuration items
421
+ */
422
+ async searchCmdbCi(query, limit = 10) {
423
+ await this.authenticate();
424
+ const queryParams = new URLSearchParams();
425
+ if (query) {
426
+ queryParams.set('sysparm_query', query);
427
+ }
428
+ queryParams.set('sysparm_limit', Math.min(limit, 100).toString());
429
+ const url = `${this.baseUrl}/api/now/table/cmdb_ci?${queryParams.toString()}`;
430
+ logger.info('Searching CMDB CIs');
431
+ try {
432
+ const response = await this.request(url);
433
+ return {
434
+ count: response.result.length,
435
+ records: response.result,
436
+ };
437
+ }
438
+ catch (error) {
439
+ if (error instanceof ServiceNowError) {
440
+ throw error;
441
+ }
442
+ throw new ServiceNowError(`Failed to search CMDB CIs: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
443
+ }
444
+ }
445
+ /**
446
+ * Get a specific CMDB configuration item
447
+ */
448
+ async getCmdbCi(ciSysId, fields) {
449
+ return this.getRecord('cmdb_ci', ciSysId, fields);
450
+ }
451
+ /**
452
+ * List relationships for a CI
453
+ */
454
+ async listRelationships(ciSysId) {
455
+ await this.authenticate();
456
+ const query = `parent=${ciSysId}^ORchild=${ciSysId}`;
457
+ const url = `${this.baseUrl}/api/now/table/cmdb_rel_ci?sysparm_query=${query}`;
458
+ logger.info(`Listing relationships for CI: ${ciSysId}`);
459
+ try {
460
+ const response = await this.request(url);
461
+ return {
462
+ count: response.result.length,
463
+ relationships: response.result,
464
+ };
465
+ }
466
+ catch (error) {
467
+ if (error instanceof ServiceNowError) {
468
+ throw error;
469
+ }
470
+ throw new ServiceNowError(`Failed to list relationships: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
471
+ }
472
+ }
473
+ /**
474
+ * List discovery schedules
475
+ */
476
+ async listDiscoverySchedules(activeOnly = false) {
477
+ await this.authenticate();
478
+ const query = activeOnly ? 'active=true' : '';
479
+ const url = `${this.baseUrl}/api/now/table/discovery_schedule${query ? '?sysparm_query=' + query : ''}`;
480
+ logger.info('Listing discovery schedules');
481
+ try {
482
+ const response = await this.request(url);
483
+ return {
484
+ count: response.result.length,
485
+ schedules: response.result,
486
+ };
487
+ }
488
+ catch (error) {
489
+ if (error instanceof ServiceNowError) {
490
+ throw error;
491
+ }
492
+ throw new ServiceNowError(`Failed to list discovery schedules: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
493
+ }
494
+ }
495
+ /**
496
+ * List MID servers
497
+ */
498
+ async listMidServers(activeOnly = false) {
499
+ await this.authenticate();
500
+ const query = activeOnly ? 'status=Up' : '';
501
+ const url = `${this.baseUrl}/api/now/table/ecc_agent${query ? '?sysparm_query=' + query : ''}`;
502
+ logger.info('Listing MID servers');
503
+ try {
504
+ const response = await this.request(url);
505
+ return {
506
+ count: response.result.length,
507
+ mid_servers: response.result,
508
+ };
509
+ }
510
+ catch (error) {
511
+ if (error instanceof ServiceNowError) {
512
+ throw error;
513
+ }
514
+ throw new ServiceNowError(`Failed to list MID servers: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
515
+ }
516
+ }
517
+ /**
518
+ * List active events
519
+ */
520
+ async listActiveEvents(query, limit = 10) {
521
+ await this.authenticate();
522
+ const queryParams = new URLSearchParams();
523
+ if (query) {
524
+ queryParams.set('sysparm_query', query);
525
+ }
526
+ queryParams.set('sysparm_limit', limit.toString());
527
+ const url = `${this.baseUrl}/api/now/table/em_event?${queryParams.toString()}`;
528
+ logger.info('Listing active events');
529
+ try {
530
+ const response = await this.request(url);
531
+ return {
532
+ count: response.result.length,
533
+ records: response.result,
534
+ };
535
+ }
536
+ catch (error) {
537
+ if (error instanceof ServiceNowError) {
538
+ throw error;
539
+ }
540
+ throw new ServiceNowError(`Failed to list events: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
541
+ }
542
+ }
543
+ /**
544
+ * Get CMDB health dashboard metrics
545
+ */
546
+ async cmdbHealthDashboard() {
547
+ await this.authenticate();
548
+ logger.info('Getting CMDB health metrics');
549
+ try {
550
+ // Get server metrics
551
+ const serversUrl = `${this.baseUrl}/api/now/table/cmdb_ci_server?sysparm_fields=sys_id,ip_address,os,serial_number`;
552
+ const serversResponse = await this.request(serversUrl);
553
+ const servers = serversResponse.result;
554
+ const serversWithIp = servers.filter(s => s.ip_address).length;
555
+ const serversWithOs = servers.filter(s => s.os).length;
556
+ const serversWithSerial = servers.filter(s => s.serial_number).length;
557
+ // Get network device metrics
558
+ const networkUrl = `${this.baseUrl}/api/now/table/cmdb_ci_network_adapter?sysparm_fields=sys_id,ip_address,mac_address&sysparm_limit=100`;
559
+ const networkResponse = await this.request(networkUrl);
560
+ const network = networkResponse.result;
561
+ const networkWithIp = network.filter(n => n.ip_address).length;
562
+ const networkWithMac = network.filter(n => n.mac_address).length;
563
+ return {
564
+ server_metrics: {
565
+ total: servers.length,
566
+ with_ip: serversWithIp,
567
+ with_os: serversWithOs,
568
+ with_serial: serversWithSerial,
569
+ ip_completeness: servers.length > 0 ? ((serversWithIp / servers.length) * 100).toFixed(2) : '0',
570
+ os_completeness: servers.length > 0 ? ((serversWithOs / servers.length) * 100).toFixed(2) : '0',
571
+ },
572
+ network_metrics: {
573
+ total: network.length,
574
+ with_ip: networkWithIp,
575
+ with_mac: networkWithMac,
576
+ ip_completeness: network.length > 0 ? ((networkWithIp / network.length) * 100).toFixed(2) : '0',
577
+ mac_completeness: network.length > 0 ? ((networkWithMac / network.length) * 100).toFixed(2) : '0',
578
+ },
579
+ };
580
+ }
581
+ catch (error) {
582
+ if (error instanceof ServiceNowError) {
583
+ throw error;
584
+ }
585
+ throw new ServiceNowError(`Failed to get CMDB health: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
586
+ }
587
+ }
588
+ /**
589
+ * Get service mapping summary
590
+ */
591
+ async serviceMappingSummary(serviceSysId) {
592
+ await this.authenticate();
593
+ logger.info(`Getting service mapping summary for: ${serviceSysId}`);
594
+ try {
595
+ // Get service details
596
+ const serviceUrl = `${this.baseUrl}/api/now/table/cmdb_ci_service/${serviceSysId}`;
597
+ const serviceResponse = await this.request(serviceUrl);
598
+ // Get related CIs
599
+ const relatedUrl = `${this.baseUrl}/api/now/table/cmdb_rel_ci?sysparm_query=parent=${serviceSysId}^ORchild=${serviceSysId}`;
600
+ const relatedResponse = await this.request(relatedUrl);
601
+ return {
602
+ service: serviceResponse.result,
603
+ related_cis_count: relatedResponse.result.length,
604
+ related_cis: relatedResponse.result,
605
+ };
606
+ }
607
+ catch (error) {
608
+ if (error instanceof ServiceNowError) {
609
+ throw error;
610
+ }
611
+ throw new ServiceNowError(`Failed to get service mapping: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
612
+ }
613
+ }
614
+ /**
615
+ * Create a change request
616
+ */
617
+ async createChangeRequest(params) {
618
+ await this.authenticate();
619
+ logger.info('Creating change request');
620
+ const url = `${this.baseUrl}/api/now/table/change_request`;
621
+ try {
622
+ const response = await this.request(url, {
623
+ method: 'POST',
624
+ body: JSON.stringify(params),
625
+ });
626
+ return response.result;
627
+ }
628
+ catch (error) {
629
+ if (error instanceof ServiceNowError) {
630
+ throw error;
631
+ }
632
+ throw new ServiceNowError(`Failed to create change request: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
633
+ }
634
+ }
635
+ /**
636
+ * Create a record in any ServiceNow table
637
+ */
638
+ async createRecord(table, data) {
639
+ validateTableName(table);
640
+ await this.authenticate();
641
+ logger.info(`Creating record in ${table}`);
642
+ const url = `${this.baseUrl}/api/now/table/${table}`;
643
+ try {
644
+ const response = await this.request(url, {
645
+ method: 'POST',
646
+ body: JSON.stringify(data),
647
+ });
648
+ return response.result;
649
+ }
650
+ catch (error) {
651
+ if (error instanceof ServiceNowError)
652
+ throw error;
653
+ throw new ServiceNowError(`Failed to create record in ${table}: ${error instanceof Error ? error.message : 'Unknown error'}`, 'CREATE_FAILED');
654
+ }
655
+ }
656
+ /**
657
+ * Update a record in any ServiceNow table
658
+ */
659
+ async updateRecord(table, sysId, data) {
660
+ validateTableName(table);
661
+ validateSysId(sysId);
662
+ await this.authenticate();
663
+ logger.info(`Updating record ${sysId} in ${table}`);
664
+ const url = `${this.baseUrl}/api/now/table/${table}/${sysId}`;
665
+ try {
666
+ const response = await this.request(url, {
667
+ method: 'PATCH',
668
+ body: JSON.stringify(data),
669
+ });
670
+ return response.result;
671
+ }
672
+ catch (error) {
673
+ if (error instanceof ServiceNowError)
674
+ throw error;
675
+ throw new ServiceNowError(`Failed to update record ${sysId} in ${table}: ${error instanceof Error ? error.message : 'Unknown error'}`, 'UPDATE_FAILED');
676
+ }
677
+ }
678
+ /**
679
+ * Delete a record from any ServiceNow table
680
+ */
681
+ async deleteRecord(table, sysId) {
682
+ validateTableName(table);
683
+ validateSysId(sysId);
684
+ await this.authenticate();
685
+ logger.info(`Deleting record ${sysId} from ${table}`);
686
+ const url = `${this.baseUrl}/api/now/table/${table}/${sysId}`;
687
+ try {
688
+ await this.request(url, { method: 'DELETE' });
689
+ }
690
+ catch (error) {
691
+ if (error instanceof ServiceNowError)
692
+ throw error;
693
+ throw new ServiceNowError(`Failed to delete record ${sysId} from ${table}: ${error instanceof Error ? error.message : 'Unknown error'}`, 'DELETE_FAILED');
694
+ }
695
+ }
696
+ /**
697
+ * Call Now Assist / Generative AI endpoints (latest release)
698
+ */
699
+ async callNowAssist(endpoint, payload) {
700
+ await this.authenticate();
701
+ logger.info(`Calling Now Assist endpoint: ${endpoint}`);
702
+ const url = `${this.baseUrl}${endpoint}`;
703
+ try {
704
+ const response = await this.request(url, {
705
+ method: 'POST',
706
+ body: JSON.stringify(payload),
707
+ });
708
+ return response;
709
+ }
710
+ catch (error) {
711
+ if (error instanceof ServiceNowError)
712
+ throw error;
713
+ throw new ServiceNowError(`Now Assist call failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NOW_ASSIST_ERROR');
714
+ }
715
+ }
716
+ /**
717
+ * Run aggregate/stats query on a table (ServiceNow Reporting API)
718
+ */
719
+ async runAggregateQuery(table, groupBy, _aggregate = 'COUNT', query) {
720
+ await this.authenticate();
721
+ const params = new URLSearchParams();
722
+ params.set('sysparm_group_by', groupBy);
723
+ if (query)
724
+ params.set('sysparm_query', query);
725
+ params.set('sysparm_count', 'true');
726
+ const url = `${this.baseUrl}/api/now/stats/${table}?${params.toString()}`;
727
+ try {
728
+ const response = await this.request(url);
729
+ return response.result;
730
+ }
731
+ catch (error) {
732
+ if (error instanceof ServiceNowError)
733
+ throw error;
734
+ throw new ServiceNowError(`Aggregate query failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED');
735
+ }
736
+ }
737
+ /**
738
+ * Natural language search (simplified implementation)
739
+ */
740
+ async naturalLanguageSearch(query, limit = 10) {
741
+ // For now, search across incidents - in a full implementation,
742
+ // this would use NLP to determine the table and build the query
743
+ logger.info(`Natural language search: ${query}`);
744
+ const searchQuery = `short_descriptionLIKE${query}^ORdescriptionLIKE${query}`;
745
+ return this.queryRecords({
746
+ table: 'incident',
747
+ query: searchQuery,
748
+ limit,
749
+ });
750
+ }
751
+ /**
752
+ * Upload a file attachment to a ServiceNow record via the Attachment API.
753
+ * Accepts base64-encoded content and uploads it as a multipart form.
754
+ */
755
+ async uploadAttachment(table, recordSysId, fileName, contentType, contentBase64) {
756
+ await this.authenticate();
757
+ const url = `${this.baseUrl}/api/now/attachment/file?table_name=${encodeURIComponent(table)}&table_sys_id=${encodeURIComponent(recordSysId)}&file_name=${encodeURIComponent(fileName)}`;
758
+ logger.info(`Uploading attachment "${fileName}" to ${table}:${recordSysId}`);
759
+ try {
760
+ // Decode base64 to binary
761
+ const binary = Buffer.from(contentBase64, 'base64');
762
+ const response = await fetch(url, {
763
+ method: 'POST',
764
+ headers: {
765
+ 'Content-Type': contentType,
766
+ 'Authorization': this.getAuthHeader(),
767
+ 'Accept': 'application/json',
768
+ },
769
+ body: binary,
770
+ });
771
+ if (!response.ok) {
772
+ const errorText = await response.text();
773
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
774
+ try {
775
+ const errorJson = JSON.parse(errorText);
776
+ if (errorJson.error?.message)
777
+ errorMessage = errorJson.error.message;
778
+ }
779
+ catch {
780
+ // ignore parse error
781
+ }
782
+ throw new ServiceNowError(errorMessage, 'ATTACHMENT_UPLOAD_FAILED');
783
+ }
784
+ const data = await response.json();
785
+ return data.result ?? data;
786
+ }
787
+ catch (error) {
788
+ if (error instanceof ServiceNowError)
789
+ throw error;
790
+ throw new ServiceNowError(`Failed to upload attachment: ${error instanceof Error ? error.message : 'Unknown error'}`, 'ATTACHMENT_UPLOAD_FAILED');
791
+ }
792
+ }
793
+ /**
794
+ * Natural language update (simplified implementation)
795
+ */
796
+ async naturalLanguageUpdate(_instruction, _table) {
797
+ // This is a simplified implementation - a full version would parse
798
+ // the instruction to extract record identifier and field updates
799
+ logger.warn('Natural language update is experimental and requires manual parsing');
800
+ throw new ServiceNowError('Natural language update requires custom parsing logic - not yet implemented', 'NOT_IMPLEMENTED');
801
+ }
802
+ }
803
+ //# sourceMappingURL=client.js.map