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.
- package/.env.example +96 -0
- package/LICENSE +21 -0
- package/README.md +1253 -0
- package/desktop/renderer/dist/apple-touch-icon.png +0 -0
- package/desktop/renderer/dist/assets/index-Bb0ncZQl.css +1 -0
- package/desktop/renderer/dist/assets/index-MlBBSUMZ.js +49 -0
- package/desktop/renderer/dist/favicon-32.png +0 -0
- package/desktop/renderer/dist/favicon.svg +18 -0
- package/desktop/renderer/dist/index.html +18 -0
- package/desktop/serve.js +449 -0
- package/dist/cli/auth.d.ts +14 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +179 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/config-store.d.ts +28 -0
- package/dist/cli/config-store.d.ts.map +1 -0
- package/dist/cli/config-store.js +64 -0
- package/dist/cli/config-store.js.map +1 -0
- package/dist/cli/detect-clients.d.ts +16 -0
- package/dist/cli/detect-clients.d.ts.map +1 -0
- package/dist/cli/detect-clients.js +151 -0
- package/dist/cli/detect-clients.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +193 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +575 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/writers/index.d.ts +9 -0
- package/dist/cli/writers/index.d.ts.map +1 -0
- package/dist/cli/writers/index.js +140 -0
- package/dist/cli/writers/index.js.map +1 -0
- package/dist/prompts/index.d.ts +25 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +38 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/itsm.d.ts +20 -0
- package/dist/prompts/itsm.d.ts.map +1 -0
- package/dist/prompts/itsm.js +110 -0
- package/dist/prompts/itsm.js.map +1 -0
- package/dist/prompts/user-prompts.d.ts +3 -0
- package/dist/prompts/user-prompts.d.ts.map +1 -0
- package/dist/prompts/user-prompts.js +35 -0
- package/dist/prompts/user-prompts.js.map +1 -0
- package/dist/resources/index.d.ts +26 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +99 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +129 -0
- package/dist/server.js.map +1 -0
- package/dist/servicenow/client.d.ts +135 -0
- package/dist/servicenow/client.d.ts.map +1 -0
- package/dist/servicenow/client.js +803 -0
- package/dist/servicenow/client.js.map +1 -0
- package/dist/servicenow/instances.d.ts +28 -0
- package/dist/servicenow/instances.d.ts.map +1 -0
- package/dist/servicenow/instances.js +204 -0
- package/dist/servicenow/instances.js.map +1 -0
- package/dist/servicenow/types.d.ts +574 -0
- package/dist/servicenow/types.d.ts.map +1 -0
- package/dist/servicenow/types.js +3 -0
- package/dist/servicenow/types.js.map +1 -0
- package/dist/tools/agile.d.ts +225 -0
- package/dist/tools/agile.d.ts.map +1 -0
- package/dist/tools/agile.js +205 -0
- package/dist/tools/agile.js.map +1 -0
- package/dist/tools/app-studio.d.ts +139 -0
- package/dist/tools/app-studio.d.ts.map +1 -0
- package/dist/tools/app-studio.js +139 -0
- package/dist/tools/app-studio.js.map +1 -0
- package/dist/tools/atf.d.ts +144 -0
- package/dist/tools/atf.d.ts.map +1 -0
- package/dist/tools/atf.js +186 -0
- package/dist/tools/atf.js.map +1 -0
- package/dist/tools/catalog.d.ts +628 -0
- package/dist/tools/catalog.d.ts.map +1 -0
- package/dist/tools/catalog.js +397 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/change.d.ts +347 -0
- package/dist/tools/change.d.ts.map +1 -0
- package/dist/tools/change.js +213 -0
- package/dist/tools/change.js.map +1 -0
- package/dist/tools/core.d.ts +540 -0
- package/dist/tools/core.d.ts.map +1 -0
- package/dist/tools/core.js +373 -0
- package/dist/tools/core.js.map +1 -0
- package/dist/tools/csm.d.ts +401 -0
- package/dist/tools/csm.d.ts.map +1 -0
- package/dist/tools/csm.js +255 -0
- package/dist/tools/csm.js.map +1 -0
- package/dist/tools/deployment.d.ts +366 -0
- package/dist/tools/deployment.d.ts.map +1 -0
- package/dist/tools/deployment.js +181 -0
- package/dist/tools/deployment.js.map +1 -0
- package/dist/tools/devops.d.ts +236 -0
- package/dist/tools/devops.d.ts.map +1 -0
- package/dist/tools/devops.js +207 -0
- package/dist/tools/devops.js.map +1 -0
- package/dist/tools/flow.d.ts +496 -0
- package/dist/tools/flow.d.ts.map +1 -0
- package/dist/tools/flow.js +348 -0
- package/dist/tools/flow.js.map +1 -0
- package/dist/tools/hrsd.d.ts +789 -0
- package/dist/tools/hrsd.d.ts.map +1 -0
- package/dist/tools/hrsd.js +377 -0
- package/dist/tools/hrsd.js.map +1 -0
- package/dist/tools/incident.d.ts +256 -0
- package/dist/tools/incident.d.ts.map +1 -0
- package/dist/tools/incident.js +163 -0
- package/dist/tools/incident.js.map +1 -0
- package/dist/tools/index.d.ts +11514 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +276 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/integration.d.ts +603 -0
- package/dist/tools/integration.d.ts.map +1 -0
- package/dist/tools/integration.js +510 -0
- package/dist/tools/integration.js.map +1 -0
- package/dist/tools/itam.d.ts +462 -0
- package/dist/tools/itam.d.ts.map +1 -0
- package/dist/tools/itam.js +306 -0
- package/dist/tools/itam.js.map +1 -0
- package/dist/tools/knowledge.d.ts +187 -0
- package/dist/tools/knowledge.d.ts.map +1 -0
- package/dist/tools/knowledge.js +161 -0
- package/dist/tools/knowledge.js.map +1 -0
- package/dist/tools/ml.d.ts +263 -0
- package/dist/tools/ml.d.ts.map +1 -0
- package/dist/tools/ml.js +251 -0
- package/dist/tools/ml.js.map +1 -0
- package/dist/tools/mobile.d.ts +352 -0
- package/dist/tools/mobile.d.ts.map +1 -0
- package/dist/tools/mobile.js +122 -0
- package/dist/tools/mobile.js.map +1 -0
- package/dist/tools/notification.d.ts +590 -0
- package/dist/tools/notification.d.ts.map +1 -0
- package/dist/tools/notification.js +382 -0
- package/dist/tools/notification.js.map +1 -0
- package/dist/tools/now-assist.d.ts +300 -0
- package/dist/tools/now-assist.d.ts.map +1 -0
- package/dist/tools/now-assist.js +227 -0
- package/dist/tools/now-assist.js.map +1 -0
- package/dist/tools/performance.d.ts +447 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +473 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/portal.d.ts +530 -0
- package/dist/tools/portal.d.ts.map +1 -0
- package/dist/tools/portal.js +425 -0
- package/dist/tools/portal.js.map +1 -0
- package/dist/tools/problem.d.ts +110 -0
- package/dist/tools/problem.d.ts.map +1 -0
- package/dist/tools/problem.js +100 -0
- package/dist/tools/problem.js.map +1 -0
- package/dist/tools/reporting.d.ts +825 -0
- package/dist/tools/reporting.d.ts.map +1 -0
- package/dist/tools/reporting.js +460 -0
- package/dist/tools/reporting.js.map +1 -0
- package/dist/tools/script.d.ts +714 -0
- package/dist/tools/script.d.ts.map +1 -0
- package/dist/tools/script.js +629 -0
- package/dist/tools/script.js.map +1 -0
- package/dist/tools/security.d.ts +794 -0
- package/dist/tools/security.d.ts.map +1 -0
- package/dist/tools/security.js +425 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/sys-properties.d.ts +315 -0
- package/dist/tools/sys-properties.d.ts.map +1 -0
- package/dist/tools/sys-properties.js +372 -0
- package/dist/tools/sys-properties.js.map +1 -0
- package/dist/tools/task.d.ts +82 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +96 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/updateset.d.ts +159 -0
- package/dist/tools/updateset.d.ts.map +1 -0
- package/dist/tools/updateset.js +212 -0
- package/dist/tools/updateset.js.map +1 -0
- package/dist/tools/user.d.ts +206 -0
- package/dist/tools/user.d.ts.map +1 -0
- package/dist/tools/user.js +163 -0
- package/dist/tools/user.js.map +1 -0
- package/dist/tools/va.d.ts +217 -0
- package/dist/tools/va.d.ts.map +1 -0
- package/dist/tools/va.js +178 -0
- package/dist/tools/va.js.map +1 -0
- package/dist/tools/workspace.d.ts +565 -0
- package/dist/tools/workspace.d.ts.map +1 -0
- package/dist/tools/workspace.js +201 -0
- package/dist/tools/workspace.js.map +1 -0
- package/dist/tools-manifest.json +7852 -0
- package/dist/utils/errors.d.ts +6 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +11 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logging.d.ts +7 -0
- package/dist/utils/logging.d.ts.map +1 -0
- package/dist/utils/logging.js +15 -0
- package/dist/utils/logging.js.map +1 -0
- package/dist/utils/permissions.d.ts +21 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +54 -0
- package/dist/utils/permissions.js.map +1 -0
- package/instances.example.json +71 -0
- package/package.json +110 -0
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="iconG" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#00F0C0" />
|
|
5
|
+
<stop offset="50%" style="stop-color:#00D4AA" />
|
|
6
|
+
<stop offset="100%" style="stop-color:#0F4C81" />
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<!-- Squircle/rounded shape (rx = 256 for 1024px = same ratio as 24/96) -->
|
|
10
|
+
<rect width="1024" height="1024" rx="256" fill="url(#iconG)"/>
|
|
11
|
+
<!-- N + AI sparkle, scaled to match original proportions (0.82 of 1024) -->
|
|
12
|
+
<g transform="translate(512, 512) scale(18.26) translate(-22, -23)">
|
|
13
|
+
<path d="M5 39V7l15 27V7" stroke="#fff" stroke-width="5.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
14
|
+
<path d="M34 4l2 7 6 2-6 2-2 7-2-7-6-2 6-2z" fill="#fff" opacity="0.9"/>
|
|
15
|
+
<line x1="34" y1="19" x2="34" y2="28.5" stroke="#fff" stroke-width="1.8" opacity="0.5"/>
|
|
16
|
+
<circle cx="34" cy="34" r="4.5" fill="#fff" opacity="0.8"/>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>NowAIKit</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
9
|
+
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
|
|
10
|
+
<!-- CSP: script-src unsafe-inline needed for Vite HMR in dev; style-src unsafe-inline needed for React inline styles -->
|
|
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-MlBBSUMZ.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Bb0ncZQl.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/desktop/serve.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NowAIKit Web Server
|
|
4
|
+
*
|
|
5
|
+
* Zero-dependency Node.js server that:
|
|
6
|
+
* 1. Serves the built renderer/dist/ static files
|
|
7
|
+
* 2. Proxies /api/ai/* requests to AI providers (bypasses CORS)
|
|
8
|
+
*
|
|
9
|
+
* Security:
|
|
10
|
+
* - Binds to localhost only by default (set HOST=0.0.0.0 to expose)
|
|
11
|
+
* - CORS restricted to same-origin (configurable via ALLOWED_ORIGINS)
|
|
12
|
+
* - Requires X-NowAIKit-Proxy header on AI proxy requests (CSRF protection)
|
|
13
|
+
* - Path traversal and null-byte injection protection
|
|
14
|
+
* - Only whitelisted headers forwarded to upstream providers
|
|
15
|
+
* - API keys are never logged
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node serve.js # default port 4175, localhost only
|
|
19
|
+
* PORT=3000 node serve.js # custom port
|
|
20
|
+
* HOST=0.0.0.0 node serve.js # expose to network (use with caution)
|
|
21
|
+
* npm run serve # via package.json script
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const http = require('http');
|
|
25
|
+
const https = require('https');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { URL } = require('url');
|
|
29
|
+
|
|
30
|
+
const PORT = parseInt(process.env.PORT || '4175', 10);
|
|
31
|
+
const HOST = process.env.HOST || '127.0.0.1';
|
|
32
|
+
const STATIC_DIR = path.join(__dirname, 'renderer', 'dist');
|
|
33
|
+
|
|
34
|
+
// Configurable allowed origins (comma-separated)
|
|
35
|
+
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);
|
|
36
|
+
|
|
37
|
+
// ─── MIME types ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const MIME = {
|
|
40
|
+
'.html': 'text/html; charset=utf-8',
|
|
41
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
42
|
+
'.css': 'text/css; charset=utf-8',
|
|
43
|
+
'.json': 'application/json; charset=utf-8',
|
|
44
|
+
'.png': 'image/png',
|
|
45
|
+
'.jpg': 'image/jpeg',
|
|
46
|
+
'.jpeg': 'image/jpeg',
|
|
47
|
+
'.gif': 'image/gif',
|
|
48
|
+
'.svg': 'image/svg+xml',
|
|
49
|
+
'.ico': 'image/x-icon',
|
|
50
|
+
'.woff': 'font/woff',
|
|
51
|
+
'.woff2': 'font/woff2',
|
|
52
|
+
'.ttf': 'font/ttf',
|
|
53
|
+
'.map': 'application/json',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ─── AI provider proxy config ────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const AI_PROXIES = {
|
|
59
|
+
'/api/ai/anthropic': { target: 'https://api.anthropic.com', strip: '/api/ai/anthropic' },
|
|
60
|
+
'/api/ai/openai': { target: 'https://api.openai.com', strip: '/api/ai/openai' },
|
|
61
|
+
'/api/ai/google': { target: 'https://generativelanguage.googleapis.com', strip: '/api/ai/google' },
|
|
62
|
+
'/api/ai/groq': { target: 'https://api.groq.com', strip: '/api/ai/groq' },
|
|
63
|
+
'/api/ai/openrouter': { target: 'https://openrouter.ai', strip: '/api/ai/openrouter' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Headers that are safe to forward to upstream AI providers
|
|
67
|
+
const ALLOWED_PROXY_HEADERS = new Set([
|
|
68
|
+
'content-type', 'authorization', 'x-api-key', 'anthropic-version',
|
|
69
|
+
'accept', 'accept-encoding', 'user-agent',
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Headers safe to forward to ServiceNow instances
|
|
73
|
+
const ALLOWED_SNOW_HEADERS = new Set([
|
|
74
|
+
'content-type', 'authorization', 'accept', 'accept-encoding', 'user-agent',
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// ─── Security helpers ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/** Check if an origin is allowed for CORS */
|
|
80
|
+
function isAllowedOrigin(origin) {
|
|
81
|
+
if (!origin) return true; // Same-origin requests have no Origin header
|
|
82
|
+
// Always allow same-host requests
|
|
83
|
+
if (origin.startsWith(`http://localhost:${PORT}`) || origin.startsWith(`http://127.0.0.1:${PORT}`)) return true;
|
|
84
|
+
// Allow dev server
|
|
85
|
+
if (origin.startsWith('http://localhost:5173') || origin.startsWith('http://127.0.0.1:5173')) return true;
|
|
86
|
+
// Allow custom configured origins
|
|
87
|
+
return ALLOWED_ORIGINS.includes(origin);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Strip potential API key patterns from strings (for safe logging) */
|
|
91
|
+
function sanitizeForLog(str) {
|
|
92
|
+
if (!str) return str;
|
|
93
|
+
// Redact common API key patterns
|
|
94
|
+
return str
|
|
95
|
+
.replace(/sk-ant-[a-zA-Z0-9_-]+/g, 'sk-ant-***REDACTED***')
|
|
96
|
+
.replace(/sk-[a-zA-Z0-9_-]{20,}/g, 'sk-***REDACTED***')
|
|
97
|
+
.replace(/AIza[a-zA-Z0-9_-]+/g, 'AIza***REDACTED***')
|
|
98
|
+
.replace(/gsk_[a-zA-Z0-9_-]+/g, 'gsk_***REDACTED***')
|
|
99
|
+
.replace(/sk-or-[a-zA-Z0-9_-]+/g, 'sk-or-***REDACTED***')
|
|
100
|
+
.replace(/key=[^&\s]+/g, 'key=***REDACTED***');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Rate limiting (simple per-IP) ──────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const rateLimits = new Map(); // IP -> { count, resetAt }
|
|
106
|
+
const RATE_LIMIT = parseInt(process.env.RATE_LIMIT || '60', 10); // requests per minute
|
|
107
|
+
const RATE_WINDOW = 60000; // 1 minute
|
|
108
|
+
|
|
109
|
+
function checkRateLimit(ip) {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
let entry = rateLimits.get(ip);
|
|
112
|
+
if (!entry || now > entry.resetAt) {
|
|
113
|
+
entry = { count: 0, resetAt: now + RATE_WINDOW };
|
|
114
|
+
rateLimits.set(ip, entry);
|
|
115
|
+
}
|
|
116
|
+
entry.count++;
|
|
117
|
+
return entry.count <= RATE_LIMIT;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Periodically clean up stale rate limit entries
|
|
121
|
+
setInterval(() => {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
for (const [ip, entry] of rateLimits) {
|
|
124
|
+
if (now > entry.resetAt) rateLimits.delete(ip);
|
|
125
|
+
}
|
|
126
|
+
}, 60000);
|
|
127
|
+
|
|
128
|
+
// ─── Proxy handler ───────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function proxyRequest(req, res, proxyConfig) {
|
|
131
|
+
const rewritten = req.url.replace(proxyConfig.strip, '') || '/';
|
|
132
|
+
const target = new URL(rewritten, proxyConfig.target);
|
|
133
|
+
|
|
134
|
+
// Collect request body (limit to 10MB)
|
|
135
|
+
const chunks = [];
|
|
136
|
+
let totalSize = 0;
|
|
137
|
+
const MAX_BODY = 10 * 1024 * 1024;
|
|
138
|
+
|
|
139
|
+
req.on('data', chunk => {
|
|
140
|
+
totalSize += chunk.length;
|
|
141
|
+
if (totalSize > MAX_BODY) {
|
|
142
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
144
|
+
req.destroy();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
chunks.push(chunk);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
req.on('end', () => {
|
|
151
|
+
if (totalSize > MAX_BODY) return;
|
|
152
|
+
const body = Buffer.concat(chunks);
|
|
153
|
+
|
|
154
|
+
// Only forward whitelisted headers
|
|
155
|
+
const headers = { 'host': target.hostname };
|
|
156
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
157
|
+
if (ALLOWED_PROXY_HEADERS.has(key.toLowerCase())) {
|
|
158
|
+
headers[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const options = {
|
|
163
|
+
hostname: target.hostname,
|
|
164
|
+
port: 443,
|
|
165
|
+
path: target.pathname + target.search,
|
|
166
|
+
method: req.method,
|
|
167
|
+
headers,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const origin = req.headers.origin;
|
|
171
|
+
const allowedOrigin = isAllowedOrigin(origin) ? (origin || '*') : '';
|
|
172
|
+
|
|
173
|
+
const proxyReq = https.request(options, (proxyRes) => {
|
|
174
|
+
// Set CORS headers for the allowed origin
|
|
175
|
+
if (allowedOrigin) {
|
|
176
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
177
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
178
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, X-NowAIKit-Proxy');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Forward response headers (skip upstream CORS headers)
|
|
182
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
183
|
+
const lower = key.toLowerCase();
|
|
184
|
+
if (lower.startsWith('access-control-')) continue;
|
|
185
|
+
if (value) res.setHeader(key, value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
res.writeHead(proxyRes.statusCode || 500);
|
|
189
|
+
proxyRes.pipe(res);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
proxyReq.on('error', (err) => {
|
|
193
|
+
console.error(`Proxy error -> ${target.hostname}: ${sanitizeForLog(err.message)}`);
|
|
194
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
195
|
+
res.end(JSON.stringify({ error: 'AI provider unreachable. Check your internet connection.' }));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (body.length > 0) proxyReq.write(body);
|
|
199
|
+
proxyReq.end();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── ServiceNow proxy handler ─────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Proxies requests to a ServiceNow instance (bypasses CORS).
|
|
207
|
+
* Route: /api/snow/{base64url-encoded-instance-url}/{rest-of-path}
|
|
208
|
+
* Example: /api/snow/aHR0cHM6Ly9kZXYxMjM0NS5zZXJ2aWNlLW5vdy5jb20/api/now/table/incident?sysparm_limit=5
|
|
209
|
+
*/
|
|
210
|
+
function proxySnowRequest(req, res) {
|
|
211
|
+
// Parse: /api/snow/{encodedBase}/{path...}
|
|
212
|
+
const after = req.url.slice('/api/snow/'.length);
|
|
213
|
+
const slashIdx = after.indexOf('/');
|
|
214
|
+
if (slashIdx < 0) {
|
|
215
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
216
|
+
res.end(JSON.stringify({ error: 'Missing instance URL segment' }));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const encodedBase = after.slice(0, slashIdx);
|
|
220
|
+
const restPath = after.slice(slashIdx); // includes leading /
|
|
221
|
+
|
|
222
|
+
let instanceUrl;
|
|
223
|
+
try {
|
|
224
|
+
instanceUrl = Buffer.from(encodedBase, 'base64url').toString('utf8');
|
|
225
|
+
} catch {
|
|
226
|
+
try { instanceUrl = Buffer.from(encodedBase, 'base64').toString('utf8'); } catch {
|
|
227
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
228
|
+
res.end(JSON.stringify({ error: 'Invalid base64 instance URL' }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate the instance URL
|
|
234
|
+
let parsedInstance;
|
|
235
|
+
try {
|
|
236
|
+
parsedInstance = new URL(instanceUrl);
|
|
237
|
+
if (parsedInstance.protocol !== 'https:' && parsedInstance.protocol !== 'http:') {
|
|
238
|
+
throw new Error('bad protocol');
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({ error: 'Invalid instance URL' }));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const target = new URL(restPath, instanceUrl);
|
|
247
|
+
|
|
248
|
+
// Collect request body
|
|
249
|
+
const chunks = [];
|
|
250
|
+
let totalSize = 0;
|
|
251
|
+
const MAX_BODY = 10 * 1024 * 1024;
|
|
252
|
+
|
|
253
|
+
req.on('data', chunk => {
|
|
254
|
+
totalSize += chunk.length;
|
|
255
|
+
if (totalSize > MAX_BODY) {
|
|
256
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify({ error: 'Request body too large' }));
|
|
258
|
+
req.destroy();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
chunks.push(chunk);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
req.on('end', () => {
|
|
265
|
+
if (totalSize > MAX_BODY) return;
|
|
266
|
+
const body = Buffer.concat(chunks);
|
|
267
|
+
|
|
268
|
+
const headers = { 'host': target.hostname };
|
|
269
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
270
|
+
if (ALLOWED_SNOW_HEADERS.has(key.toLowerCase())) {
|
|
271
|
+
headers[key] = value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const isHttps = target.protocol === 'https:';
|
|
276
|
+
const mod = isHttps ? https : http;
|
|
277
|
+
const options = {
|
|
278
|
+
hostname: target.hostname,
|
|
279
|
+
port: isHttps ? 443 : (parseInt(target.port, 10) || 80),
|
|
280
|
+
path: target.pathname + target.search,
|
|
281
|
+
method: req.method,
|
|
282
|
+
headers,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const origin = req.headers.origin;
|
|
286
|
+
const allowedOrigin = isAllowedOrigin(origin) ? (origin || '*') : '';
|
|
287
|
+
|
|
288
|
+
const proxyReq = mod.request(options, (proxyRes) => {
|
|
289
|
+
if (allowedOrigin) {
|
|
290
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
291
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
292
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-NowAIKit-Proxy');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
296
|
+
const lower = key.toLowerCase();
|
|
297
|
+
if (lower.startsWith('access-control-')) continue;
|
|
298
|
+
if (value) res.setHeader(key, value);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
res.writeHead(proxyRes.statusCode || 500);
|
|
302
|
+
proxyRes.pipe(res);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
proxyReq.on('error', (err) => {
|
|
306
|
+
console.error(`Snow proxy error -> ${target.hostname}: ${err.message}`);
|
|
307
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
308
|
+
res.end(JSON.stringify({ error: 'ServiceNow instance unreachable. Check the URL and your network.' }));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (body.length > 0) proxyReq.write(body);
|
|
312
|
+
proxyReq.end();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ─── Static file handler ─────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
function serveStatic(req, res) {
|
|
319
|
+
// Decode URL and reject null bytes
|
|
320
|
+
let urlPath;
|
|
321
|
+
try {
|
|
322
|
+
urlPath = decodeURIComponent(req.url.split('?')[0]);
|
|
323
|
+
} catch {
|
|
324
|
+
res.writeHead(400);
|
|
325
|
+
res.end('Bad Request');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (urlPath.includes('\0')) {
|
|
330
|
+
res.writeHead(400);
|
|
331
|
+
res.end('Bad Request');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let filePath = path.resolve(STATIC_DIR, urlPath === '/' ? 'index.html' : '.' + urlPath);
|
|
336
|
+
|
|
337
|
+
// Security: prevent directory traversal
|
|
338
|
+
if (!filePath.startsWith(STATIC_DIR)) {
|
|
339
|
+
res.writeHead(403);
|
|
340
|
+
res.end('Forbidden');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fs.stat(filePath, (err, stats) => {
|
|
345
|
+
if (err || !stats.isFile()) {
|
|
346
|
+
// SPA fallback: serve index.html for client-side routes
|
|
347
|
+
filePath = path.join(STATIC_DIR, 'index.html');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fs.readFile(filePath, (readErr, data) => {
|
|
351
|
+
if (readErr) {
|
|
352
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
353
|
+
res.end('Not Found');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
358
|
+
const contentType = MIME[ext] || 'application/octet-stream';
|
|
359
|
+
|
|
360
|
+
// Security headers
|
|
361
|
+
res.writeHead(200, {
|
|
362
|
+
'Content-Type': contentType,
|
|
363
|
+
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
364
|
+
'X-Content-Type-Options': 'nosniff',
|
|
365
|
+
'X-Frame-Options': 'DENY',
|
|
366
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
367
|
+
});
|
|
368
|
+
res.end(data);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── HTTP server ─────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
const server = http.createServer((req, res) => {
|
|
376
|
+
const origin = req.headers.origin;
|
|
377
|
+
const allowed = isAllowedOrigin(origin);
|
|
378
|
+
|
|
379
|
+
// Handle CORS preflight
|
|
380
|
+
if (req.method === 'OPTIONS') {
|
|
381
|
+
if (!allowed) {
|
|
382
|
+
res.writeHead(403);
|
|
383
|
+
res.end('Origin not allowed');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
res.writeHead(204, {
|
|
387
|
+
'Access-Control-Allow-Origin': origin || '*',
|
|
388
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
389
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, X-NowAIKit-Proxy',
|
|
390
|
+
'Access-Control-Max-Age': '86400',
|
|
391
|
+
});
|
|
392
|
+
res.end();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Common proxy security checks
|
|
397
|
+
const proxySecurityCheck = () => {
|
|
398
|
+
if (!req.headers['x-nowaikit-proxy']) {
|
|
399
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
400
|
+
res.end(JSON.stringify({ error: 'Missing X-NowAIKit-Proxy header' }));
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (!allowed) {
|
|
404
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
405
|
+
res.end(JSON.stringify({ error: 'Origin not allowed' }));
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
const ip = req.socket.remoteAddress || 'unknown';
|
|
409
|
+
if (!checkRateLimit(ip)) {
|
|
410
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
411
|
+
res.end(JSON.stringify({ error: 'Rate limit exceeded. Try again in a minute.' }));
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Check if this is an AI proxy request
|
|
418
|
+
for (const [prefix, config] of Object.entries(AI_PROXIES)) {
|
|
419
|
+
if (req.url.startsWith(prefix)) {
|
|
420
|
+
if (!proxySecurityCheck()) return;
|
|
421
|
+
proxyRequest(req, res, config);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if this is a ServiceNow proxy request
|
|
427
|
+
if (req.url.startsWith('/api/snow/')) {
|
|
428
|
+
if (!proxySecurityCheck()) return;
|
|
429
|
+
proxySnowRequest(req, res);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Serve static files
|
|
434
|
+
serveStatic(req, res);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
server.listen(PORT, HOST, () => {
|
|
438
|
+
console.log(`\n NowAIKit Web Server`);
|
|
439
|
+
console.log(` ───────────────────────────────`);
|
|
440
|
+
console.log(` Local: http://localhost:${PORT}`);
|
|
441
|
+
if (HOST !== '127.0.0.1' && HOST !== 'localhost') {
|
|
442
|
+
console.log(` Network: http://${HOST}:${PORT}`);
|
|
443
|
+
}
|
|
444
|
+
console.log(`\n AI proxy: /api/ai/* -> provider APIs (CORS proxied)`);
|
|
445
|
+
console.log(` Snow proxy: /api/snow/* -> ServiceNow instances (CORS proxied)`);
|
|
446
|
+
console.log(` Static: ${STATIC_DIR}`);
|
|
447
|
+
console.log(`\n All AI providers + ServiceNow instances supported.`);
|
|
448
|
+
console.log(` Press Ctrl+C to stop.\n`);
|
|
449
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface UserToken {
|
|
2
|
+
instanceUrl: string;
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
snUser: string;
|
|
7
|
+
snUserSysId: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function authLogin(): Promise<void>;
|
|
10
|
+
export declare function authLogout(instanceUrl?: string): void;
|
|
11
|
+
export declare function authWhoami(): void;
|
|
12
|
+
export declare function getStoredToken(instanceUrl: string): UserToken | undefined;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/cli/auth.ts"],"names":[],"mappings":"AAeA,UAAU,SAAS;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AA8BD,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CA2H/C;AAED,wBAAgB,UAAU,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAgBrD;AAED,wBAAgB,UAAU,IAAI,IAAI,CAYjC;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGzE"}
|
package/dist/cli/auth.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nowaikit auth` subcommands — per-user OAuth / login management.
|
|
3
|
+
*
|
|
4
|
+
* login — opens browser to ServiceNow OAuth consent, stores token
|
|
5
|
+
* logout — removes stored token
|
|
6
|
+
* whoami — show which ServiceNow user is currently authenticated
|
|
7
|
+
*/
|
|
8
|
+
import { input, password, select } from '@inquirer/prompts';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { listInstances } from './config-store.js';
|
|
15
|
+
function tokenPath() {
|
|
16
|
+
const dir = join(homedir(), '.config', 'nowaikit');
|
|
17
|
+
if (!existsSync(dir))
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
return join(dir, 'tokens.json');
|
|
20
|
+
}
|
|
21
|
+
function loadTokens() {
|
|
22
|
+
const p = tokenPath();
|
|
23
|
+
if (!existsSync(p))
|
|
24
|
+
return { tokens: {} };
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { tokens: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function saveTokens(store) {
|
|
33
|
+
writeFileSync(tokenPath(), JSON.stringify(store, null, 2), 'utf8');
|
|
34
|
+
}
|
|
35
|
+
function tokenKey(instanceUrl) {
|
|
36
|
+
return instanceUrl.replace(/https?:\/\//, '').replace(/[^a-z0-9]/gi, '_');
|
|
37
|
+
}
|
|
38
|
+
export async function authLogin() {
|
|
39
|
+
const instances = listInstances();
|
|
40
|
+
if (instances.length === 0) {
|
|
41
|
+
console.log(chalk.yellow('No instances configured. Run `nowaikit setup` first.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const instanceUrl = instances.length === 1
|
|
45
|
+
? instances[0].instanceUrl
|
|
46
|
+
: await select({
|
|
47
|
+
message: 'Choose instance to authenticate against:',
|
|
48
|
+
choices: instances.map(i => ({ name: `${i.name} (${i.instanceUrl})`, value: i.instanceUrl })),
|
|
49
|
+
});
|
|
50
|
+
const instance = instances.find(i => i.instanceUrl === instanceUrl);
|
|
51
|
+
if (!instance)
|
|
52
|
+
return;
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.bold('Per-user OAuth login'));
|
|
55
|
+
console.log(chalk.dim('Your queries will run in your own ServiceNow permission context.'));
|
|
56
|
+
console.log('');
|
|
57
|
+
if (instance.authMethod === 'oauth' && instance.clientId) {
|
|
58
|
+
// OAuth Authorization Code flow — open browser
|
|
59
|
+
const authUrl = `${instanceUrl}/oauth_auth.do` +
|
|
60
|
+
`?response_type=code&client_id=${instance.clientId}` +
|
|
61
|
+
`&redirect_uri=http://localhost:8765/callback`;
|
|
62
|
+
console.log(chalk.cyan('Open this URL in your browser to authenticate:'));
|
|
63
|
+
console.log(chalk.underline(authUrl));
|
|
64
|
+
console.log('');
|
|
65
|
+
const code = await input({
|
|
66
|
+
message: 'Paste the authorization code from the redirect URL:',
|
|
67
|
+
});
|
|
68
|
+
const spinner = ora('Exchanging authorization code for token…').start();
|
|
69
|
+
try {
|
|
70
|
+
const resp = await fetch(`${instanceUrl}/oauth_token.do`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
73
|
+
body: new URLSearchParams({
|
|
74
|
+
grant_type: 'authorization_code',
|
|
75
|
+
client_id: instance.clientId,
|
|
76
|
+
client_secret: instance.clientSecret || '',
|
|
77
|
+
code,
|
|
78
|
+
redirect_uri: 'http://localhost:8765/callback',
|
|
79
|
+
}).toString(),
|
|
80
|
+
});
|
|
81
|
+
if (!resp.ok) {
|
|
82
|
+
spinner.fail(chalk.red(`Token exchange failed: ${resp.status} ${resp.statusText}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const data = await resp.json();
|
|
86
|
+
// Get the ServiceNow user tied to this token
|
|
87
|
+
const meResp = await fetch(`${instanceUrl}/api/now/table/sys_user?sysparm_query=sys_idINSTANCEOF&sysparm_limit=1`, {
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${data.access_token}`,
|
|
90
|
+
Accept: 'application/json',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const meData = await meResp.json();
|
|
94
|
+
const snUserSysId = meData.result?.[0]?.sys_id?.value || '';
|
|
95
|
+
const snUser = meData.result?.[0]?.user_name?.value || 'unknown';
|
|
96
|
+
const store = loadTokens();
|
|
97
|
+
store.tokens[tokenKey(instanceUrl)] = {
|
|
98
|
+
instanceUrl,
|
|
99
|
+
accessToken: data.access_token,
|
|
100
|
+
refreshToken: data.refresh_token,
|
|
101
|
+
expiresAt: Date.now() + data.expires_in * 1000 * 0.9,
|
|
102
|
+
snUser,
|
|
103
|
+
snUserSysId,
|
|
104
|
+
};
|
|
105
|
+
saveTokens(store);
|
|
106
|
+
spinner.succeed(chalk.green(`Authenticated as ${snUser} on ${instanceUrl}`));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
spinner.fail(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Basic auth per-user: prompt credentials, store them
|
|
114
|
+
const username = await input({ message: 'Your ServiceNow username:' });
|
|
115
|
+
const pass = await password({ message: 'Your ServiceNow password:', mask: '•' });
|
|
116
|
+
const spinner = ora('Verifying credentials…').start();
|
|
117
|
+
try {
|
|
118
|
+
const creds = Buffer.from(`${username}:${pass}`).toString('base64');
|
|
119
|
+
const resp = await fetch(`${instanceUrl}/api/now/table/sys_user?sysparm_query=user_name=${encodeURIComponent(username)}&sysparm_limit=1`, { headers: { Authorization: `Basic ${creds}`, Accept: 'application/json' } });
|
|
120
|
+
if (!resp.ok) {
|
|
121
|
+
spinner.fail(chalk.red(`Auth failed: ${resp.status} ${resp.statusText}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const data = await resp.json();
|
|
125
|
+
const snUserSysId = data.result?.[0]?.sys_id?.value || '';
|
|
126
|
+
const store = loadTokens();
|
|
127
|
+
store.tokens[tokenKey(instanceUrl)] = {
|
|
128
|
+
instanceUrl,
|
|
129
|
+
accessToken: creds,
|
|
130
|
+
refreshToken: '',
|
|
131
|
+
expiresAt: Date.now() + 365 * 24 * 60 * 60 * 1000, // basic auth doesn't expire
|
|
132
|
+
snUser: username,
|
|
133
|
+
snUserSysId,
|
|
134
|
+
};
|
|
135
|
+
saveTokens(store);
|
|
136
|
+
spinner.succeed(chalk.green(`Saved credentials for ${username} on ${instanceUrl}`));
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
spinner.fail(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function authLogout(instanceUrl) {
|
|
144
|
+
const store = loadTokens();
|
|
145
|
+
if (instanceUrl) {
|
|
146
|
+
const key = tokenKey(instanceUrl);
|
|
147
|
+
if (store.tokens[key]) {
|
|
148
|
+
delete store.tokens[key];
|
|
149
|
+
saveTokens(store);
|
|
150
|
+
console.log(chalk.green(`Logged out from ${instanceUrl}`));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(chalk.yellow(`No token found for ${instanceUrl}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
store.tokens = {};
|
|
158
|
+
saveTokens(store);
|
|
159
|
+
console.log(chalk.green('Logged out from all instances'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function authWhoami() {
|
|
163
|
+
const store = loadTokens();
|
|
164
|
+
const tokens = Object.values(store.tokens);
|
|
165
|
+
if (tokens.length === 0) {
|
|
166
|
+
console.log(chalk.dim('Not authenticated. Run `nowaikit auth login`'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const t of tokens) {
|
|
170
|
+
const expired = Date.now() > t.expiresAt;
|
|
171
|
+
const status = expired ? chalk.red('(expired)') : chalk.green('(active)');
|
|
172
|
+
console.log(` ${t.instanceUrl} → ${chalk.bold(t.snUser)} ${status}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function getStoredToken(instanceUrl) {
|
|
176
|
+
const store = loadTokens();
|
|
177
|
+
return store.tokens[tokenKey(instanceUrl)];
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=auth.js.map
|