nowaikit 4.19.0 → 4.21.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.
- package/.env.example +1 -1
- package/README.md +5 -3
- package/desktop/renderer/dist/assets/index-DSLAKhIs.js +55 -0
- package/desktop/renderer/dist/index.html +1 -1
- package/desktop/serve.cjs +161 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +11 -2
- package/dist/server.js.map +1 -1
- package/dist/servicenow/client.d.ts +27 -0
- package/dist/servicenow/client.d.ts.map +1 -1
- package/dist/servicenow/client.js +124 -8
- package/dist/servicenow/client.js.map +1 -1
- package/dist/servicenow/instances.js +1 -1
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +38 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/script.d.ts.map +1 -1
- package/dist/tools/script.js +20 -5
- package/dist/tools/script.js.map +1 -1
- package/dist/utils/request-context.d.ts +19 -1
- package/dist/utils/request-context.d.ts.map +1 -1
- package/dist/utils/request-context.js +33 -2
- package/dist/utils/request-context.js.map +1 -1
- package/package.json +1 -1
- package/desktop/renderer/dist/assets/index-DIXajM0H.js +0 -55
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
|
|
10
10
|
<!-- CSP: script-src unsafe-inline needed for Vite HMR in dev; style-src unsafe-inline needed for React inline styles -->
|
|
11
11
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://*.service-now.com https://*.servicenowservices.com https://generativelanguage.googleapis.com ws://localhost:* http://localhost:*; img-src 'self' data:; frame-ancestors 'none'; form-action 'self';" />
|
|
12
|
-
<script type="module" crossorigin src="./assets/index-
|
|
12
|
+
<script type="module" crossorigin src="./assets/index-DSLAKhIs.js"></script>
|
|
13
13
|
<link rel="stylesheet" crossorigin href="./assets/index-Bb0ncZQl.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
package/desktop/serve.cjs
CHANGED
|
@@ -25,6 +25,8 @@ const http = require('http');
|
|
|
25
25
|
const https = require('https');
|
|
26
26
|
const fs = require('fs');
|
|
27
27
|
const path = require('path');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const { spawn } = require('child_process');
|
|
28
30
|
const { URL } = require('url');
|
|
29
31
|
|
|
30
32
|
const PORT = parseInt(process.env.PORT || '4175', 10);
|
|
@@ -464,6 +466,158 @@ function handleReportGenerate(req, res) {
|
|
|
464
466
|
|
|
465
467
|
// ─── HTTP server ─────────────────────────────────────────────────────────────
|
|
466
468
|
|
|
469
|
+
/** Read a JSON file from ~/.config/nowaikit (returns null if missing/invalid). */
|
|
470
|
+
function readNowaikitJson(file) {
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(fs.readFileSync(path.join(os.homedir(), '.config', 'nowaikit', file), 'utf8'));
|
|
473
|
+
} catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* POST /api/ai/claude-cli
|
|
480
|
+
* Runs the local `claude` CLI (Claude Code subscription) headlessly with the NowAIKit MCP loaded, so
|
|
481
|
+
* it answers with real ServiceNow data using the user's subscription instead of an API key. The MCP
|
|
482
|
+
* server authenticates from the active instance's stored config (basic creds, or an OAuth token it
|
|
483
|
+
* self-refreshes). Returns an Anthropic-Messages-shaped body so the chat UI handles it unchanged.
|
|
484
|
+
* Body: { messages: [{role, content}], system?: string, instanceUrl?: string }
|
|
485
|
+
*/
|
|
486
|
+
function handleClaudeCli(req, res) {
|
|
487
|
+
const chunks = [];
|
|
488
|
+
let total = 0;
|
|
489
|
+
const MAX = 5 * 1024 * 1024;
|
|
490
|
+
req.on('data', (c) => {
|
|
491
|
+
total += c.length;
|
|
492
|
+
if (total > MAX) {
|
|
493
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
494
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
495
|
+
req.destroy();
|
|
496
|
+
} else {
|
|
497
|
+
chunks.push(c);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
req.on('end', () => {
|
|
501
|
+
if (total > MAX) return;
|
|
502
|
+
let body;
|
|
503
|
+
try {
|
|
504
|
+
body = JSON.parse(Buffer.concat(chunks).toString());
|
|
505
|
+
} catch {
|
|
506
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
507
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
512
|
+
const system = typeof body.system === 'string' ? body.system : '';
|
|
513
|
+
const instanceUrl = typeof body.instanceUrl === 'string' ? body.instanceUrl : '';
|
|
514
|
+
|
|
515
|
+
const textOf = (c) =>
|
|
516
|
+
typeof c === 'string' ? c : Array.isArray(c) ? c.map((b) => (b && b.text) || '').join('') : '';
|
|
517
|
+
const prompt = messages
|
|
518
|
+
.map((m) => `${m.role === 'assistant' ? 'Assistant' : 'User'}: ${textOf(m.content)}`)
|
|
519
|
+
.join('\n\n')
|
|
520
|
+
.trim();
|
|
521
|
+
if (!prompt) {
|
|
522
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
523
|
+
res.end(JSON.stringify({ error: 'No prompt to send' }));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Resolve the active instance and build an MCP config so claude has the ServiceNow tools.
|
|
528
|
+
const cfg = readNowaikitJson('instances.json');
|
|
529
|
+
let inst = null;
|
|
530
|
+
if (cfg && cfg.instances) {
|
|
531
|
+
const list = Object.values(cfg.instances);
|
|
532
|
+
inst = list.find((i) => i.instanceUrl === instanceUrl) || list.find((i) => i.name === cfg.defaultInstance) || list[0] || null;
|
|
533
|
+
}
|
|
534
|
+
const mcpEnv = { MCP_TOOL_DISCOVERY: 'lean' };
|
|
535
|
+
if (inst) {
|
|
536
|
+
mcpEnv.SERVICENOW_INSTANCE_URL = inst.instanceUrl;
|
|
537
|
+
mcpEnv.SERVICENOW_AUTH_METHOD = inst.authMethod || 'basic';
|
|
538
|
+
if (inst.authMethod === 'oauth') {
|
|
539
|
+
if (inst.clientId) mcpEnv.SERVICENOW_OAUTH_CLIENT_ID = inst.clientId;
|
|
540
|
+
if (inst.clientSecret) mcpEnv.SERVICENOW_OAUTH_CLIENT_SECRET = inst.clientSecret;
|
|
541
|
+
// The MCP server reads and self-refreshes the stored per-user token from tokens.json.
|
|
542
|
+
} else {
|
|
543
|
+
if (inst.username) mcpEnv.SERVICENOW_BASIC_USERNAME = inst.username;
|
|
544
|
+
if (inst.password) mcpEnv.SERVICENOW_BASIC_PASSWORD = inst.password;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const serverJs = path.resolve(__dirname, '..', 'dist', 'server.js');
|
|
549
|
+
const mcpConfig = { mcpServers: { nowaikit: { command: process.execPath, args: [serverJs], env: mcpEnv } } };
|
|
550
|
+
let mcpConfigPath;
|
|
551
|
+
try {
|
|
552
|
+
mcpConfigPath = path.join(os.tmpdir(), `nowaikit-mcp-${process.pid}-${total}.json`);
|
|
553
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig), { mode: 0o600 });
|
|
554
|
+
} catch (e) {
|
|
555
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
556
|
+
res.end(JSON.stringify({ error: 'Could not prepare MCP config: ' + (e.message || e) }));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const cleanup = () => { try { fs.unlinkSync(mcpConfigPath); } catch { /* ignore */ } };
|
|
560
|
+
|
|
561
|
+
const bin = process.env.NOWAIKIT_CLAUDE_BIN || 'claude';
|
|
562
|
+
// Restrict to the NowAIKit MCP tools and block the built-in filesystem/shell tools, and run in a
|
|
563
|
+
// neutral temp dir, so the web chat's claude only touches ServiceNow, never the local machine.
|
|
564
|
+
const args = [
|
|
565
|
+
'-p', '--output-format', 'json',
|
|
566
|
+
'--mcp-config', mcpConfigPath,
|
|
567
|
+
'--allowedTools', 'mcp__nowaikit',
|
|
568
|
+
'--disallowedTools', 'Bash,Edit,Write,Read,WebFetch,WebSearch',
|
|
569
|
+
];
|
|
570
|
+
if (system) args.push('--append-system-prompt', system);
|
|
571
|
+
|
|
572
|
+
let stdout = '';
|
|
573
|
+
let stderr = '';
|
|
574
|
+
let child;
|
|
575
|
+
try {
|
|
576
|
+
child = spawn(bin, args, { shell: process.platform === 'win32', cwd: os.tmpdir() });
|
|
577
|
+
} catch (e) {
|
|
578
|
+
cleanup();
|
|
579
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
580
|
+
res.end(JSON.stringify({ error: 'Failed to start Claude CLI: ' + (e.message || e) }));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
child.on('error', (e) => {
|
|
584
|
+
cleanup();
|
|
585
|
+
const msg = e.code === 'ENOENT'
|
|
586
|
+
? 'Claude CLI not found. Install Claude Code, run `claude login`, or set NOWAIKIT_CLAUDE_BIN to the claude binary.'
|
|
587
|
+
: 'Claude CLI error: ' + e.message;
|
|
588
|
+
if (!res.headersSent) {
|
|
589
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
590
|
+
res.end(JSON.stringify({ error: msg }));
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
child.stdout.on('data', (d) => { stdout += d; if (stdout.length > MAX) child.kill(); });
|
|
594
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
595
|
+
try { child.stdin.end(prompt); } catch { /* ignore */ }
|
|
596
|
+
|
|
597
|
+
child.on('close', (code) => {
|
|
598
|
+
cleanup();
|
|
599
|
+
if (res.headersSent) return;
|
|
600
|
+
if (code !== 0 && !stdout) {
|
|
601
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
602
|
+
res.end(JSON.stringify({ error: `Claude CLI exited ${code}` + (stderr ? ': ' + stderr.slice(0, 500) : '') }));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
let text = '';
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(stdout);
|
|
608
|
+
text = parsed.result || parsed.text || (parsed.is_error ? 'Error: ' + (parsed.error || 'unknown') : '');
|
|
609
|
+
} catch {
|
|
610
|
+
text = stdout.trim();
|
|
611
|
+
}
|
|
612
|
+
const origin = req.headers.origin;
|
|
613
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
614
|
+
if (isAllowedOrigin(origin)) headers['Access-Control-Allow-Origin'] = origin || '*';
|
|
615
|
+
res.writeHead(200, headers);
|
|
616
|
+
res.end(JSON.stringify({ role: 'assistant', model: 'claude-cli', stop_reason: 'end_turn', content: [{ type: 'text', text: text || '(no output)' }] }));
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
467
621
|
const server = http.createServer((req, res) => {
|
|
468
622
|
const origin = req.headers.origin;
|
|
469
623
|
const allowed = isAllowedOrigin(origin);
|
|
@@ -506,6 +660,13 @@ const server = http.createServer((req, res) => {
|
|
|
506
660
|
return true;
|
|
507
661
|
};
|
|
508
662
|
|
|
663
|
+
// Claude Code subscription (local claude CLI) — spawns claude with the NowAIKit MCP loaded.
|
|
664
|
+
if (req.url === '/api/ai/claude-cli' && req.method === 'POST') {
|
|
665
|
+
if (!proxySecurityCheck()) return;
|
|
666
|
+
handleClaudeCli(req, res);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
509
670
|
// Check if this is an AI proxy request
|
|
510
671
|
for (const [prefix, config] of Object.entries(AI_PROXIES)) {
|
|
511
672
|
if (req.url.startsWith(prefix)) {
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAqDnE,wBAAgB,YAAY,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAqDnE,wBAAgB,YAAY,IAAI,MAAM,CA6MrC"}
|
package/dist/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getPrompts, resolvePromptAsync } from './prompts/index.js';
|
|
|
9
9
|
import { logger } from './utils/logging.js';
|
|
10
10
|
import { ServiceNowError } from './utils/errors.js';
|
|
11
11
|
import { connectTransport } from './transport/index.js';
|
|
12
|
-
import { getDelegatedAuth } from './utils/request-context.js';
|
|
12
|
+
import { getDelegatedAuth, strictDelegationViolation } from './utils/request-context.js';
|
|
13
13
|
import { VERSION, SERVER_NAME } from './utils/version.js';
|
|
14
14
|
// quiet: suppress dotenv v17+ startup banner (keeps MCP stdio + CLI output clean)
|
|
15
15
|
dotenv.config({ quiet: true });
|
|
@@ -64,13 +64,22 @@ export function createServer() {
|
|
|
64
64
|
throw new ServiceNowError(`Unknown tool: ${name}`, 'UNKNOWN_TOOL');
|
|
65
65
|
}
|
|
66
66
|
const instanceName = args?.['instance'];
|
|
67
|
-
const baseClient = instanceManager.getClient(instanceName);
|
|
68
67
|
// DELEGATED_AUTH: run as the per-request identity AND (for multi-tenant
|
|
69
68
|
// hosting) against the caller's own instance, both carried in the delegated
|
|
70
69
|
// context headers. When a token is present we also honour its instanceUrl,
|
|
71
70
|
// so one shared server serves many customers with no per-customer config.
|
|
72
71
|
// No-op in normal single-user mode.
|
|
73
72
|
const delegatedAuth = getDelegatedAuth();
|
|
73
|
+
// Strict delegated-auth: refuse to run without a valid delegated token (never fall back to base
|
|
74
|
+
// credentials), and refuse the instance-manager tools that bypass per-request delegation.
|
|
75
|
+
const violation = strictDelegationViolation(name);
|
|
76
|
+
if (violation === 'DELEGATION_REQUIRED') {
|
|
77
|
+
throw new ServiceNowError('Delegation required: strict delegated-auth mode is on, so every tool call must carry a valid delegated token from the trusted gateway. The server will not fall back to base credentials.', 'DELEGATION_REQUIRED');
|
|
78
|
+
}
|
|
79
|
+
if (violation === 'DELEGATION_INCOMPATIBLE_TOOL') {
|
|
80
|
+
throw new ServiceNowError(`Tool "${name}" is unavailable under strict delegated-auth: it operates on server-configured instances, not the delegated per-request identity.`, 'DELEGATION_INCOMPATIBLE_TOOL');
|
|
81
|
+
}
|
|
82
|
+
const baseClient = instanceManager.getClient(instanceName);
|
|
74
83
|
const client = delegatedAuth?.bearerToken
|
|
75
84
|
? baseClient.withUser({ bearerToken: delegatedAuth.bearerToken, instanceUrl: delegatedAuth.instanceUrl })
|
|
76
85
|
: baseClient;
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,0BAA0B,EAC1B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,11 @@ export declare function basicAuthDiagnostic(): string;
|
|
|
11
11
|
* authorized — for OAuth this is almost always a missing scope/role on the token.
|
|
12
12
|
*/
|
|
13
13
|
export declare function forbiddenDiagnostic(authMethod: 'basic' | 'oauth'): string;
|
|
14
|
+
/**
|
|
15
|
+
* A business-rule or data-policy abort. ServiceNow returns 403 for these, but they are NOT a
|
|
16
|
+
* permissions problem, so we surface them separately and never show the roles/ACL checklist.
|
|
17
|
+
*/
|
|
18
|
+
export declare function businessRuleAbortDiagnostic(): string;
|
|
14
19
|
/** Validate and sanitize ServiceNow encoded query strings */
|
|
15
20
|
export declare function validateQuery(query: string): string;
|
|
16
21
|
export declare class ServiceNowClient {
|
|
@@ -50,6 +55,12 @@ export declare class ServiceNowClient {
|
|
|
50
55
|
get instanceUrl(): string;
|
|
51
56
|
/** Human-facing record URL (opens the form in ServiceNow), not the /api/now REST link. */
|
|
52
57
|
recordUrl(table: string, sysId: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Attach a `record_url` deep-link to a record the API just returned, so create/update tool results
|
|
60
|
+
* always give the user a direct link to open the record. Uses `record_url` (not `url`) so it never
|
|
61
|
+
* clobbers a real ServiceNow `url` column. No-op if there's no sys_id.
|
|
62
|
+
*/
|
|
63
|
+
attachRecordUrl<T extends Record<string, any>>(table: string, record: T): T;
|
|
53
64
|
/**
|
|
54
65
|
* Return the fields that ServiceNow Data Policies make mandatory on a table, so an agent can
|
|
55
66
|
* collect them BEFORE a create/update and avoid the "Data Policy Exception: mandatory fields"
|
|
@@ -101,6 +112,22 @@ export declare class ServiceNowClient {
|
|
|
101
112
|
user_display_name?: string;
|
|
102
113
|
[k: string]: any;
|
|
103
114
|
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a scope argument to a sys_scope sys_id. Accepts "global", a scope name
|
|
117
|
+
* (e.g. "x_myco_app"), an application display name, or a 32-char sys_id. ServiceNow
|
|
118
|
+
* uses the literal value "global" as the sys_id of the global application.
|
|
119
|
+
*/
|
|
120
|
+
resolveScopeSysId(scope: string): Promise<string>;
|
|
121
|
+
/**
|
|
122
|
+
* Run `fn` with the current user's active application temporarily set to `scope`, then restore it.
|
|
123
|
+
* ServiceNow stamps application-file records (script includes, business rules, client scripts, UI
|
|
124
|
+
* actions/policies, ACLs, etc.) with the CALLER'S current application, NOT any sys_scope in the
|
|
125
|
+
* request body. Over the Table API the only reliable way to target a scope is to switch
|
|
126
|
+
* `apps.current_app` (the Studio/Developer app picker) around the create. This best-effort restores
|
|
127
|
+
* the prior value in a finally block. Note: on a shared integration user, concurrent scoped creates
|
|
128
|
+
* can race on this preference; per-user auth (the gateway default) switches only that user's picker.
|
|
129
|
+
*/
|
|
130
|
+
withCurrentApp<T>(scope: string, fn: () => Promise<T>): Promise<T>;
|
|
104
131
|
/**
|
|
105
132
|
* Get table schema/structure
|
|
106
133
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/servicenow/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EAGpB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAKpB;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAkB5C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAuBzE;AAgDD,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBnD;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,WAAW,CAAC,CAA4B;IAChD,OAAO,CAAC,WAAW,CAAC,CAA4B;IAChD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,gBAAgB,CAAS;IAEjC,sEAAsE;IACtE,OAAO,CAAC,oBAAoB,CAAC,CAAS;IACtC,yEAAyE;IACzE,OAAO,CAAC,kBAAkB,CAAC,CAAS;IACpC,oGAAoG;IACpG,OAAO,CAAC,mBAAmB,CAAC,CAAS;IACrC,OAAO,CAAC,kBAAkB,CAAC,CAAS;IACpC,OAAO,CAAC,gBAAgB,CAAC,CAAuC;IAEhE,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAS;gBAEjB,MAAM,EAAE,gBAAgB;IAgBpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAIzB,4FAA4F;YAC9E,mBAAmB;IA6BjC;;;OAGG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,qGAAqG;IACrG,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,0FAA0F;IAC1F,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAI/C;;;;;OAKG;IACG,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAqCrD;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,gBAAgB;IAqCnG;;OAEG;YACW,YAAY;IA+E1B;;;;OAIG;IACH,OAAO,CAAC,aAAa;IA4BrB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;OAEG;YACW,OAAO;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/servicenow/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,6 +46,22 @@ export function forbiddenDiagnostic(authMethod) {
|
|
|
46
46
|
lines.push(` ${authMethod === 'oauth' ? '5' : '3'}. Writes need WRITE_ENABLED=true AND the user\'s write roles; some tools also need scripting/CMDB/ATF flags.`);
|
|
47
47
|
return lines.join('\n');
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A business-rule or data-policy abort. ServiceNow returns 403 for these, but they are NOT a
|
|
51
|
+
* permissions problem, so we surface them separately and never show the roles/ACL checklist.
|
|
52
|
+
*/
|
|
53
|
+
export function businessRuleAbortDiagnostic() {
|
|
54
|
+
return [
|
|
55
|
+
'This was stopped by server-side logic on the instance, not by your permissions. A business rule or',
|
|
56
|
+
'data policy aborted the write, and that runs for everyone (admins included), so roles do not bypass',
|
|
57
|
+
'it. The rule or policy named above is telling you the actual reason. Typically:',
|
|
58
|
+
' 1. The record state or field combination is not allowed by the process (e.g. only one active task',
|
|
59
|
+
' at a time, a required field, or a state transition that is not permitted).',
|
|
60
|
+
' 2. Fix it on the process side (adjust the related records or fields), not by adding roles.',
|
|
61
|
+
' To bypass on purpose, an admin deactivates the rule or runs a fix script with',
|
|
62
|
+
' current.setWorkflow(false). The REST path runs business rules by design and will not skip them.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
|
49
65
|
// ─── Input validation helpers ────────────────────────────────────────────────
|
|
50
66
|
/** Best-effort MIME type from a file extension, for attachments fetched without a content type. */
|
|
51
67
|
function guessContentType(fileName) {
|
|
@@ -131,7 +147,7 @@ export class ServiceNowClient {
|
|
|
131
147
|
this.authMode = config.authMode || 'service-account';
|
|
132
148
|
this.oauthConfig = config.oauth;
|
|
133
149
|
this.basicConfig = config.basic;
|
|
134
|
-
this.maxRetries = config.maxRetries || 3
|
|
150
|
+
this.maxRetries = config.maxRetries ?? 3; // honor an explicit 0 (|| would turn 0 back into 3)
|
|
135
151
|
this.retryDelayMs = config.retryDelayMs || 1000;
|
|
136
152
|
this.requestTimeoutMs = config.requestTimeoutMs || 30000;
|
|
137
153
|
this.impersonateUserSysId = config.impersonateUserSysId;
|
|
@@ -200,6 +216,24 @@ export class ServiceNowClient {
|
|
|
200
216
|
recordUrl(table, sysId) {
|
|
201
217
|
return `${this.baseUrl}/${encodeURIComponent(table)}.do?sys_id=${encodeURIComponent(sysId)}`;
|
|
202
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Attach a `record_url` deep-link to a record the API just returned, so create/update tool results
|
|
221
|
+
* always give the user a direct link to open the record. Uses `record_url` (not `url`) so it never
|
|
222
|
+
* clobbers a real ServiceNow `url` column. No-op if there's no sys_id.
|
|
223
|
+
*/
|
|
224
|
+
attachRecordUrl(table, record) {
|
|
225
|
+
if (!record || typeof record !== 'object')
|
|
226
|
+
return record;
|
|
227
|
+
const raw = record.sys_id;
|
|
228
|
+
const sysId = raw && typeof raw === 'object' ? (raw.value ?? raw.display_value) : raw;
|
|
229
|
+
if (sysId && !record.record_url) {
|
|
230
|
+
try {
|
|
231
|
+
record.record_url = this.recordUrl(table, String(sysId));
|
|
232
|
+
}
|
|
233
|
+
catch { /* ignore */ }
|
|
234
|
+
}
|
|
235
|
+
return record;
|
|
236
|
+
}
|
|
203
237
|
/**
|
|
204
238
|
* Return the fields that ServiceNow Data Policies make mandatory on a table, so an agent can
|
|
205
239
|
* collect them BEFORE a create/update and avoid the "Data Policy Exception: mandatory fields"
|
|
@@ -460,8 +494,17 @@ export class ServiceNowClient {
|
|
|
460
494
|
}
|
|
461
495
|
}
|
|
462
496
|
else if (response.status === 403) {
|
|
463
|
-
|
|
464
|
-
|
|
497
|
+
// A business-rule or data-policy abort also returns 403, but it is NOT a permissions
|
|
498
|
+
// problem (business rules run for everyone, admin included, and roles do not bypass them).
|
|
499
|
+
// Label it as an abort and skip the roles/ACL checklist, which is misleading here.
|
|
500
|
+
if (/aborted by Business Rule|Data Policy Exception/i.test(errorMessage)) {
|
|
501
|
+
errorCode = 'OPERATION_ABORTED';
|
|
502
|
+
errorMessage = `${errorMessage}\n\n${businessRuleAbortDiagnostic()}`;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
errorCode = 'INSUFFICIENT_PRIVILEGES';
|
|
506
|
+
errorMessage = `${errorMessage}\n\n${forbiddenDiagnostic(this.authMethod)}`;
|
|
507
|
+
}
|
|
465
508
|
}
|
|
466
509
|
else if (response.status === 404) {
|
|
467
510
|
errorCode = 'NOT_FOUND';
|
|
@@ -471,8 +514,16 @@ export class ServiceNowClient {
|
|
|
471
514
|
}
|
|
472
515
|
throw new ServiceNowError(errorMessage, errorCode);
|
|
473
516
|
}
|
|
474
|
-
|
|
475
|
-
|
|
517
|
+
// A successful DELETE (and some other 2xx responses) return 204 No Content with an empty
|
|
518
|
+
// body. Calling response.json() on an empty body throws, which previously surfaced a
|
|
519
|
+
// *successful* delete as a bogus "No Record found"/parse error. Treat an empty or
|
|
520
|
+
// no-content body as a clean, result-less success instead.
|
|
521
|
+
if (response.status === 204)
|
|
522
|
+
return undefined;
|
|
523
|
+
const text = await response.text();
|
|
524
|
+
if (!text)
|
|
525
|
+
return undefined;
|
|
526
|
+
return JSON.parse(text);
|
|
476
527
|
}
|
|
477
528
|
catch (error) {
|
|
478
529
|
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
@@ -480,10 +531,18 @@ export class ServiceNowClient {
|
|
|
480
531
|
if (error instanceof ServiceNowError) {
|
|
481
532
|
// 403 is not transient (a role/ACL/data-policy denial won't clear in 4s), so fail fast
|
|
482
533
|
// instead of burning ~7s on retries.
|
|
483
|
-
if (['AUTHENTICATION_FAILED', 'INVALID_REQUEST', 'NOT_FOUND', 'INSUFFICIENT_PRIVILEGES'].includes(error.code)) {
|
|
534
|
+
if (['AUTHENTICATION_FAILED', 'INVALID_REQUEST', 'NOT_FOUND', 'INSUFFICIENT_PRIVILEGES', 'OPERATION_ABORTED'].includes(error.code)) {
|
|
484
535
|
throw error;
|
|
485
536
|
}
|
|
486
537
|
}
|
|
538
|
+
// Never auto-retry a non-idempotent write when we got no HTTP response (network error, timeout
|
|
539
|
+
// or abort). The write may already have committed on the server, so replaying it can duplicate
|
|
540
|
+
// the effect. A ServiceNowError means the server DID respond; a bare Error means no response.
|
|
541
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
542
|
+
const safeToRetry = method === 'GET' || method === 'HEAD' || method === 'OPTIONS';
|
|
543
|
+
if (!safeToRetry && !(error instanceof ServiceNowError)) {
|
|
544
|
+
throw lastError;
|
|
545
|
+
}
|
|
487
546
|
// Retry on network errors or server errors
|
|
488
547
|
if (attempt < this.maxRetries) {
|
|
489
548
|
const delay = this.retryDelayMs * Math.pow(2, attempt); // Exponential backoff
|
|
@@ -579,6 +638,63 @@ export class ServiceNowClient {
|
|
|
579
638
|
const response = await this.request(url);
|
|
580
639
|
return response.result || {};
|
|
581
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Resolve a scope argument to a sys_scope sys_id. Accepts "global", a scope name
|
|
643
|
+
* (e.g. "x_myco_app"), an application display name, or a 32-char sys_id. ServiceNow
|
|
644
|
+
* uses the literal value "global" as the sys_id of the global application.
|
|
645
|
+
*/
|
|
646
|
+
async resolveScopeSysId(scope) {
|
|
647
|
+
const s = String(scope || '').trim();
|
|
648
|
+
if (!s || s.toLowerCase() === 'global')
|
|
649
|
+
return 'global';
|
|
650
|
+
if (/^[0-9a-f]{32}$/i.test(s))
|
|
651
|
+
return s;
|
|
652
|
+
const resp = await this.queryRecords({ table: 'sys_scope', query: `scope=${s}^ORname=${s}`, limit: 1, fields: 'sys_id,scope,name' });
|
|
653
|
+
const rec = resp.records && resp.records[0];
|
|
654
|
+
if (rec && rec.sys_id)
|
|
655
|
+
return String(rec.sys_id);
|
|
656
|
+
throw new ServiceNowError(`Scope not found: "${scope}". Pass "global", a scope name (e.g. x_myco_app), or a sys_scope sys_id.`, 'INVALID_REQUEST');
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Run `fn` with the current user's active application temporarily set to `scope`, then restore it.
|
|
660
|
+
* ServiceNow stamps application-file records (script includes, business rules, client scripts, UI
|
|
661
|
+
* actions/policies, ACLs, etc.) with the CALLER'S current application, NOT any sys_scope in the
|
|
662
|
+
* request body. Over the Table API the only reliable way to target a scope is to switch
|
|
663
|
+
* `apps.current_app` (the Studio/Developer app picker) around the create. This best-effort restores
|
|
664
|
+
* the prior value in a finally block. Note: on a shared integration user, concurrent scoped creates
|
|
665
|
+
* can race on this preference; per-user auth (the gateway default) switches only that user's picker.
|
|
666
|
+
*/
|
|
667
|
+
async withCurrentApp(scope, fn) {
|
|
668
|
+
const target = await this.resolveScopeSysId(scope);
|
|
669
|
+
const me = await this.getCurrentUser();
|
|
670
|
+
const userSysId = me.user_sys_id;
|
|
671
|
+
if (!userSysId)
|
|
672
|
+
throw new ServiceNowError('Cannot target a scope: current user sys_id is unavailable.', 'API_ERROR');
|
|
673
|
+
const prefResp = await this.queryRecords({ table: 'sys_user_preference', query: `name=apps.current_app^user=${userSysId}`, limit: 1, fields: 'sys_id,value' });
|
|
674
|
+
const existing = prefResp.records && prefResp.records[0];
|
|
675
|
+
const prefSysId = existing ? String(existing.sys_id) : undefined;
|
|
676
|
+
const prior = existing ? String(existing.value ?? '') : undefined;
|
|
677
|
+
let createdPrefSysId;
|
|
678
|
+
if (prefSysId) {
|
|
679
|
+
await this.updateRecord('sys_user_preference', prefSysId, { value: target });
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
const created = await this.createRecord('sys_user_preference', { name: 'apps.current_app', user: userSysId, value: target, type: 'string' });
|
|
683
|
+
createdPrefSysId = created && created.sys_id ? String(created.sys_id) : undefined;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
return await fn();
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
try {
|
|
690
|
+
if (prefSysId)
|
|
691
|
+
await this.updateRecord('sys_user_preference', prefSysId, { value: prior ?? 'global' });
|
|
692
|
+
else if (createdPrefSysId)
|
|
693
|
+
await this.deleteRecord('sys_user_preference', createdPrefSysId);
|
|
694
|
+
}
|
|
695
|
+
catch { /* best-effort restore; leaving the picker switched is non-fatal */ }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
582
698
|
/**
|
|
583
699
|
* Get table schema/structure
|
|
584
700
|
*/
|
|
@@ -937,7 +1053,7 @@ export class ServiceNowClient {
|
|
|
937
1053
|
method: 'POST',
|
|
938
1054
|
body: JSON.stringify(data),
|
|
939
1055
|
});
|
|
940
|
-
return response.result;
|
|
1056
|
+
return this.attachRecordUrl(table, response.result);
|
|
941
1057
|
}
|
|
942
1058
|
catch (error) {
|
|
943
1059
|
if (error instanceof ServiceNowError)
|
|
@@ -980,7 +1096,7 @@ export class ServiceNowClient {
|
|
|
980
1096
|
`ACLs or a data policy may have silently discarded them (the Table API still returns 200), ` +
|
|
981
1097
|
`or a business rule transformed them. Verify the change took effect.`;
|
|
982
1098
|
}
|
|
983
|
-
return result;
|
|
1099
|
+
return this.attachRecordUrl(table, result);
|
|
984
1100
|
}
|
|
985
1101
|
catch (error) {
|
|
986
1102
|
if (error instanceof ServiceNowError)
|