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.
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,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>
@@ -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"}
@@ -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