snow-flow 8.5.7 → 8.5.8
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/CLAUDE.md +52 -4
- package/README.md +99 -30
- package/{OPENCODE-SETUP.md → SNOWCODE-SETUP.md} +47 -21
- package/{OPENCODE-TROUBLESHOOTING.md → SNOWCODE-TROUBLESHOOTING.md} +18 -18
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +526 -280
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +528 -250
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/claude-md-template.d.ts +1 -1
- package/dist/templates/claude-md-template.js +1 -1
- package/dist/templates/readme-template.d.ts +1 -1
- package/dist/templates/readme-template.js +18 -18
- package/dist/utils/artifact-local-sync.d.ts +1 -1
- package/dist/utils/artifact-local-sync.js +4 -4
- package/dist/utils/snow-oauth.d.ts +11 -5
- package/dist/utils/snow-oauth.d.ts.map +1 -1
- package/dist/utils/snow-oauth.js +337 -90
- package/dist/utils/snow-oauth.js.map +1 -1
- package/dist/utils/{opencode-output-interceptor.d.ts → snowcode-output-interceptor.d.ts} +7 -7
- package/dist/utils/{opencode-output-interceptor.d.ts.map → snowcode-output-interceptor.d.ts.map} +1 -1
- package/dist/utils/{opencode-output-interceptor.js → snowcode-output-interceptor.js} +10 -10
- package/dist/utils/{opencode-output-interceptor.js.map → snowcode-output-interceptor.js.map} +1 -1
- package/package.json +20 -9
- package/scripts/{start-opencode.sh → start-snowcode.sh} +28 -28
- package/scripts/verify-snowcode-fork.sh +141 -0
- package/templates/snowcode-package.json +5 -0
- package/THEMES.md +0 -223
- package/bin/opencode +0 -17
- package/dist/mcp/servicenow-mcp-unified/config/tool-definitions.json +0 -3935
- package/dist/mcp/servicenow-mcp-unified/tools/automation/snow_automation_discover.js +0 -164
- package/dist/mcp/servicenow-mcp-unified/tools/deployment/snow_artifact_transfer.js +0 -282
- package/dist/mcp/servicenow-mcp-unified/tools/filters/snow_build_filter.js +0 -171
- package/dist/mcp/servicenow-mcp-unified/tools/formatters/snow_format_value.js +0 -164
- package/dist/mcp/servicenow-mcp-unified/tools/knowledge/index.js.bak +0 -45
- package/dist/mcp/servicenow-mcp-unified/tools/local-sync/snow_artifact_sync.js +0 -172
- package/dist/mcp/servicenow-mcp-unified/tools/system-properties/index.js +0 -36
- package/dist/mcp/servicenow-mcp-unified/tools/ui-builder/snow_discover_uib.js +0 -296
- package/dist/mcp/servicenow-mcp-unified/tools/workspace/snow_create_ux_component.js +0 -292
- package/dist/memory/session-memory.d.ts +0 -80
- package/dist/memory/session-memory.d.ts.map +0 -1
- package/dist/memory/session-memory.js +0 -468
- package/dist/memory/session-memory.js.map +0 -1
- package/dist/templates/opencode-agents-template.d.ts +0 -2
- package/dist/templates/opencode-agents-template.d.ts.map +0 -1
- package/dist/templates/opencode-agents-template.js +0 -469
- package/dist/templates/opencode-agents-template.js.map +0 -1
- package/scripts/bulk-optimize-tools.js +0 -486
- package/scripts/optimize-mcp-tools.ts +0 -410
- package/themes/README.md +0 -83
- package/themes/servicenow.json +0 -117
- /package/{opencode-config.example.json → snowcode-config.example.json} +0 -0
package/dist/utils/snow-oauth.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
* ServiceNow OAuth Authentication Utility with
|
|
5
|
-
* Handles OAuth2 flow for ServiceNow integration
|
|
4
|
+
* ServiceNow OAuth Authentication Utility with Code Paste Flow
|
|
5
|
+
* Handles OAuth2 flow for ServiceNow integration (Claude-style)
|
|
6
6
|
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
7
40
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
42
|
};
|
|
@@ -18,6 +51,7 @@ const axios_1 = __importDefault(require("axios"));
|
|
|
18
51
|
const https_1 = __importDefault(require("https"));
|
|
19
52
|
const net_1 = __importDefault(require("net"));
|
|
20
53
|
const crypto_1 = __importDefault(require("crypto"));
|
|
54
|
+
const prompts = __importStar(require("@clack/prompts"));
|
|
21
55
|
const snow_flow_config_js_1 = require("../config/snow-flow-config.js");
|
|
22
56
|
const unified_auth_store_js_1 = require("./unified-auth-store.js");
|
|
23
57
|
const oauth_html_templates_js_1 = require("./oauth-html-templates.js");
|
|
@@ -44,7 +78,7 @@ class ServiceNowOAuth {
|
|
|
44
78
|
}
|
|
45
79
|
// Check if within rate limit
|
|
46
80
|
if (this.tokenRequestCount >= this.MAX_TOKEN_REQUESTS_PER_WINDOW) {
|
|
47
|
-
|
|
81
|
+
prompts.log.warn('Rate limit exceeded: Too many token requests. Please wait before retrying.');
|
|
48
82
|
return false;
|
|
49
83
|
}
|
|
50
84
|
this.tokenRequestCount++;
|
|
@@ -84,9 +118,6 @@ class ServiceNowOAuth {
|
|
|
84
118
|
});
|
|
85
119
|
});
|
|
86
120
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Initialize OAuth flow - opens browser and handles callback
|
|
89
|
-
*/
|
|
90
121
|
/**
|
|
91
122
|
* 🔧 CRIT-002 FIX: Normalize instance URL to prevent trailing slash 400 errors
|
|
92
123
|
*/
|
|
@@ -104,6 +135,109 @@ class ServiceNowOAuth {
|
|
|
104
135
|
}
|
|
105
136
|
return normalized;
|
|
106
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* 🎯 NEW: Simplified OAuth flow with code paste (Claude-style)
|
|
140
|
+
* No local server required - user manually pastes authorization code
|
|
141
|
+
*/
|
|
142
|
+
async authenticateWithCodePaste(instance, clientId, clientSecret) {
|
|
143
|
+
try {
|
|
144
|
+
// Normalize instance URL
|
|
145
|
+
const normalizedInstance = this.normalizeInstanceUrl(instance);
|
|
146
|
+
// Validate client secret
|
|
147
|
+
const secretValidation = this.validateClientSecret(clientSecret);
|
|
148
|
+
if (!secretValidation.valid) {
|
|
149
|
+
prompts.log.error(`Invalid OAuth Client Secret: ${secretValidation.reason}`);
|
|
150
|
+
prompts.log.info('To get a valid OAuth secret:');
|
|
151
|
+
prompts.log.message(' 1. Log into ServiceNow as admin');
|
|
152
|
+
prompts.log.message(' 2. Navigate to: System OAuth > Application Registry');
|
|
153
|
+
prompts.log.message(' 3. Create a new OAuth application');
|
|
154
|
+
prompts.log.message(' 4. Copy the generated Client Secret (long random string)');
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: secretValidation.reason
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// For code paste flow, we use a special redirect URI that shows the code
|
|
161
|
+
const redirectUri = 'urn:ietf:wg:oauth:2.0:oob'; // Out-of-band redirect for manual code entry
|
|
162
|
+
// Store credentials
|
|
163
|
+
this.credentials = {
|
|
164
|
+
instance: normalizedInstance.replace('https://', '').replace('http://', ''),
|
|
165
|
+
clientId,
|
|
166
|
+
clientSecret,
|
|
167
|
+
redirectUri
|
|
168
|
+
};
|
|
169
|
+
prompts.log.step('Starting ServiceNow OAuth flow');
|
|
170
|
+
prompts.log.info(`Instance: ${normalizedInstance}`);
|
|
171
|
+
prompts.log.info(`Client ID: ${clientId}`);
|
|
172
|
+
// Generate state parameter and PKCE
|
|
173
|
+
this.stateParameter = this.generateState();
|
|
174
|
+
this.generatePKCE();
|
|
175
|
+
// Generate authorization URL
|
|
176
|
+
const authUrl = this.generateAuthUrl(this.credentials.instance, clientId, redirectUri);
|
|
177
|
+
prompts.log.message('');
|
|
178
|
+
prompts.log.step('Authorization URL generated');
|
|
179
|
+
prompts.log.message(`\n${authUrl}\n`);
|
|
180
|
+
prompts.log.warn(`Go to: ${authUrl}`);
|
|
181
|
+
prompts.log.message('');
|
|
182
|
+
const authCode = await prompts.text({
|
|
183
|
+
message: 'Paste the authorization code here',
|
|
184
|
+
placeholder: 'Enter the code from the browser after authorizing',
|
|
185
|
+
validate: (value) => {
|
|
186
|
+
if (!value || value.trim() === '')
|
|
187
|
+
return 'Authorization code is required';
|
|
188
|
+
if (value.length < 10)
|
|
189
|
+
return 'Code seems too short - please paste the full authorization code';
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
if (prompts.isCancel(authCode)) {
|
|
193
|
+
prompts.cancel('Authentication cancelled');
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Authentication cancelled by user'
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Extract code if user pasted full URL
|
|
200
|
+
let code = authCode.trim();
|
|
201
|
+
if (code.includes('code=')) {
|
|
202
|
+
const match = code.match(/code=([^&]+)/);
|
|
203
|
+
if (match) {
|
|
204
|
+
code = match[1];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Exchange code for tokens
|
|
208
|
+
const spinner = prompts.spinner();
|
|
209
|
+
spinner.start('Exchanging authorization code for tokens');
|
|
210
|
+
const tokenResult = await this.exchangeCodeForTokens(code);
|
|
211
|
+
if (tokenResult.success && tokenResult.accessToken) {
|
|
212
|
+
// Save tokens
|
|
213
|
+
await this.saveTokens({
|
|
214
|
+
accessToken: tokenResult.accessToken,
|
|
215
|
+
refreshToken: tokenResult.refreshToken || '',
|
|
216
|
+
expiresIn: tokenResult.expiresIn || 3600,
|
|
217
|
+
instance: this.credentials.instance,
|
|
218
|
+
clientId,
|
|
219
|
+
clientSecret
|
|
220
|
+
});
|
|
221
|
+
spinner.stop('Authentication successful');
|
|
222
|
+
prompts.log.success('Tokens saved securely');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
spinner.stop('Token exchange failed');
|
|
226
|
+
}
|
|
227
|
+
return tokenResult;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
231
|
+
prompts.log.error(`Authentication failed: ${errorMessage}`);
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: errorMessage
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Original OAuth flow with local server (fallback)
|
|
240
|
+
*/
|
|
107
241
|
async authenticate(instance, clientId, clientSecret) {
|
|
108
242
|
try {
|
|
109
243
|
// 🔧 CRIT-002 FIX: Apply URL normalization
|
|
@@ -111,12 +245,12 @@ class ServiceNowOAuth {
|
|
|
111
245
|
// Validate client secret format
|
|
112
246
|
const secretValidation = this.validateClientSecret(clientSecret);
|
|
113
247
|
if (!secretValidation.valid) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
248
|
+
prompts.log.error(`Invalid OAuth Client Secret: ${secretValidation.reason}`);
|
|
249
|
+
prompts.log.info('To get a valid OAuth secret:');
|
|
250
|
+
prompts.log.message(' 1. Log into ServiceNow as admin');
|
|
251
|
+
prompts.log.message(' 2. Navigate to: System OAuth > Application Registry');
|
|
252
|
+
prompts.log.message(' 3. Create a new OAuth application');
|
|
253
|
+
prompts.log.message(' 4. Copy the generated Client Secret (long random string)');
|
|
120
254
|
return {
|
|
121
255
|
success: false,
|
|
122
256
|
error: secretValidation.reason
|
|
@@ -131,8 +265,8 @@ class ServiceNowOAuth {
|
|
|
131
265
|
// Check if port is available
|
|
132
266
|
const isPortAvailable = await this.checkPortAvailable(port);
|
|
133
267
|
if (!isPortAvailable) {
|
|
134
|
-
|
|
135
|
-
|
|
268
|
+
prompts.log.error(`Port ${port} is already in use!`);
|
|
269
|
+
prompts.log.warn(`Please close any application using port ${port} and try again.`);
|
|
136
270
|
return {
|
|
137
271
|
success: false,
|
|
138
272
|
error: `Port ${port} is already in use. Please free up the port and try again.`
|
|
@@ -145,18 +279,20 @@ class ServiceNowOAuth {
|
|
|
145
279
|
clientSecret,
|
|
146
280
|
redirectUri
|
|
147
281
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
282
|
+
prompts.log.step('Starting ServiceNow OAuth flow');
|
|
283
|
+
prompts.log.info(`Instance: ${normalizedInstance}`);
|
|
284
|
+
prompts.log.info(`Client ID: ${clientId}`);
|
|
285
|
+
prompts.log.info(`Redirect URI: ${redirectUri}`);
|
|
152
286
|
// Generate state parameter for CSRF protection
|
|
153
287
|
this.stateParameter = this.generateState();
|
|
154
288
|
// Generate PKCE parameters
|
|
155
289
|
this.generatePKCE();
|
|
156
290
|
// Generate authorization URL
|
|
157
291
|
const authUrl = this.generateAuthUrl(this.credentials.instance, clientId, redirectUri);
|
|
158
|
-
|
|
159
|
-
|
|
292
|
+
prompts.log.step('Authorization URL generated');
|
|
293
|
+
prompts.log.message('');
|
|
294
|
+
prompts.log.message(`\n${authUrl}\n`);
|
|
295
|
+
prompts.log.message('');
|
|
160
296
|
// Start local server to handle callback
|
|
161
297
|
const authResult = await this.startCallbackServer(redirectUri, port);
|
|
162
298
|
if (authResult.success && authResult.accessToken) {
|
|
@@ -169,14 +305,14 @@ class ServiceNowOAuth {
|
|
|
169
305
|
clientId,
|
|
170
306
|
clientSecret
|
|
171
307
|
});
|
|
172
|
-
|
|
173
|
-
|
|
308
|
+
prompts.log.success('Authentication successful');
|
|
309
|
+
prompts.log.success('Tokens saved securely');
|
|
174
310
|
}
|
|
175
311
|
return authResult;
|
|
176
312
|
}
|
|
177
313
|
catch (error) {
|
|
178
314
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
179
|
-
|
|
315
|
+
prompts.log.error(`Authentication failed: ${errorMessage}`);
|
|
180
316
|
return {
|
|
181
317
|
success: false,
|
|
182
318
|
error: errorMessage
|
|
@@ -201,10 +337,14 @@ class ServiceNowOAuth {
|
|
|
201
337
|
}
|
|
202
338
|
/**
|
|
203
339
|
* Start local HTTP server to handle OAuth callback
|
|
340
|
+
* Also supports manual callback URL paste as fallback
|
|
204
341
|
*/
|
|
205
342
|
async startCallbackServer(redirectUri, port) {
|
|
206
|
-
return new Promise((resolve) => {
|
|
343
|
+
return new Promise(async (resolve) => {
|
|
344
|
+
let resolved = false;
|
|
207
345
|
const server = (0, http_1.createServer)(async (req, res) => {
|
|
346
|
+
if (resolved)
|
|
347
|
+
return;
|
|
208
348
|
try {
|
|
209
349
|
const url = new url_1.URL(req.url, `http://${snow_flow_config_js_1.snowFlowConfig.servicenow.oauth.redirectHost}:${port}`);
|
|
210
350
|
if (url.pathname === '/callback') {
|
|
@@ -215,6 +355,7 @@ class ServiceNowOAuth {
|
|
|
215
355
|
if (state !== this.stateParameter) {
|
|
216
356
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
217
357
|
res.end(oauth_html_templates_js_1.OAuthTemplates.securityError);
|
|
358
|
+
resolved = true;
|
|
218
359
|
server.close();
|
|
219
360
|
resolve({
|
|
220
361
|
success: false,
|
|
@@ -225,6 +366,7 @@ class ServiceNowOAuth {
|
|
|
225
366
|
if (error) {
|
|
226
367
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
227
368
|
res.end(oauth_html_templates_js_1.OAuthTemplates.error(error));
|
|
369
|
+
resolved = true;
|
|
228
370
|
server.close();
|
|
229
371
|
resolve({
|
|
230
372
|
success: false,
|
|
@@ -235,6 +377,7 @@ class ServiceNowOAuth {
|
|
|
235
377
|
if (!code) {
|
|
236
378
|
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
237
379
|
res.end(oauth_html_templates_js_1.OAuthTemplates.missingCode);
|
|
380
|
+
resolved = true;
|
|
238
381
|
server.close();
|
|
239
382
|
resolve({
|
|
240
383
|
success: false,
|
|
@@ -243,7 +386,9 @@ class ServiceNowOAuth {
|
|
|
243
386
|
return;
|
|
244
387
|
}
|
|
245
388
|
// Exchange code for tokens
|
|
246
|
-
|
|
389
|
+
resolved = true;
|
|
390
|
+
const spinner = prompts.spinner();
|
|
391
|
+
spinner.start('Exchanging authorization code for tokens');
|
|
247
392
|
const tokenResult = await this.exchangeCodeForTokens(code);
|
|
248
393
|
if (tokenResult.success) {
|
|
249
394
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
@@ -264,9 +409,10 @@ class ServiceNowOAuth {
|
|
|
264
409
|
}
|
|
265
410
|
}
|
|
266
411
|
catch (error) {
|
|
267
|
-
|
|
412
|
+
prompts.log.error(`Callback server error: ${error}`);
|
|
268
413
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
269
414
|
res.end('Internal Server Error');
|
|
415
|
+
resolved = true;
|
|
270
416
|
server.close();
|
|
271
417
|
resolve({
|
|
272
418
|
success: false,
|
|
@@ -275,9 +421,9 @@ class ServiceNowOAuth {
|
|
|
275
421
|
}
|
|
276
422
|
});
|
|
277
423
|
server.listen(port, () => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
424
|
+
prompts.log.step(`OAuth callback server started on http://${snow_flow_config_js_1.snowFlowConfig.servicenow.oauth.redirectHost}:${port}`);
|
|
425
|
+
prompts.log.warn('Please open the authorization URL in your browser');
|
|
426
|
+
prompts.log.info('Waiting for OAuth callback...');
|
|
281
427
|
// Auto-open browser if possible
|
|
282
428
|
// Try to auto-open browser if not in headless environment
|
|
283
429
|
const isCodespaces = process.env.CODESPACES === 'true';
|
|
@@ -310,7 +456,7 @@ class ServiceNowOAuth {
|
|
|
310
456
|
}
|
|
311
457
|
}
|
|
312
458
|
else {
|
|
313
|
-
|
|
459
|
+
prompts.log.warn(`Unknown OS: ${process.platform}`);
|
|
314
460
|
}
|
|
315
461
|
// Prevent the spawn from keeping the process alive
|
|
316
462
|
if (browserProcess && browserProcess.unref) {
|
|
@@ -319,23 +465,92 @@ class ServiceNowOAuth {
|
|
|
319
465
|
}
|
|
320
466
|
catch (err) {
|
|
321
467
|
// Silently fail - user can manually open URL
|
|
322
|
-
|
|
468
|
+
prompts.log.warn('Browser auto-open failed. Please manually copy and open the URL above.');
|
|
323
469
|
}
|
|
324
470
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
471
|
+
// Offer manual callback URL paste as alternative
|
|
472
|
+
prompts.log.message('');
|
|
473
|
+
prompts.log.info('Alternatively, paste the callback URL here after authorizing:');
|
|
474
|
+
// Start prompt for manual URL paste (race with server callback)
|
|
475
|
+
(async () => {
|
|
476
|
+
try {
|
|
477
|
+
const callbackUrl = await prompts.text({
|
|
478
|
+
message: 'Paste callback URL (or press Enter to wait for automatic redirect)',
|
|
479
|
+
placeholder: 'http://localhost:3005/callback?code=...&state=...',
|
|
480
|
+
validate: (value) => {
|
|
481
|
+
// Allow empty (waiting for server callback)
|
|
482
|
+
if (!value || value.trim() === '')
|
|
483
|
+
return undefined;
|
|
484
|
+
// Validate if URL was pasted
|
|
485
|
+
if (!value.includes('callback'))
|
|
486
|
+
return 'Invalid callback URL - must contain "callback"';
|
|
487
|
+
if (!value.includes('code='))
|
|
488
|
+
return 'Invalid callback URL - must contain code parameter';
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
// If user pressed Enter without pasting, just wait for server callback
|
|
492
|
+
if (prompts.isCancel(callbackUrl) || !callbackUrl || callbackUrl.trim() === '') {
|
|
493
|
+
prompts.log.info('Waiting for browser redirect...');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// User pasted a URL - parse it
|
|
497
|
+
if (resolved)
|
|
498
|
+
return; // Server already handled it
|
|
499
|
+
resolved = true;
|
|
500
|
+
try {
|
|
501
|
+
const url = new url_1.URL(callbackUrl);
|
|
502
|
+
const code = url.searchParams.get('code');
|
|
503
|
+
const state = url.searchParams.get('state');
|
|
504
|
+
const error = url.searchParams.get('error');
|
|
505
|
+
// Validate state
|
|
506
|
+
if (state !== this.stateParameter) {
|
|
507
|
+
server.close();
|
|
508
|
+
resolve({
|
|
509
|
+
success: false,
|
|
510
|
+
error: 'Invalid state parameter - security check failed'
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (error) {
|
|
515
|
+
server.close();
|
|
516
|
+
resolve({
|
|
517
|
+
success: false,
|
|
518
|
+
error: `OAuth error: ${error}`
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!code) {
|
|
523
|
+
server.close();
|
|
524
|
+
resolve({
|
|
525
|
+
success: false,
|
|
526
|
+
error: 'No authorization code found in URL'
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
// Exchange code for tokens
|
|
531
|
+
prompts.log.success('Authorization code received from pasted URL');
|
|
532
|
+
const spinner = prompts.spinner();
|
|
533
|
+
spinner.start('Exchanging authorization code for tokens');
|
|
534
|
+
const tokenResult = await this.exchangeCodeForTokens(code);
|
|
535
|
+
spinner.stop(tokenResult.success ? 'Token exchange successful' : 'Token exchange failed');
|
|
536
|
+
server.close();
|
|
537
|
+
resolve(tokenResult);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
server.close();
|
|
541
|
+
resolve({
|
|
542
|
+
success: false,
|
|
543
|
+
error: 'Invalid callback URL format'
|
|
544
|
+
});
|
|
545
|
+
}
|
|
337
546
|
}
|
|
338
|
-
|
|
547
|
+
catch (err) {
|
|
548
|
+
// Prompt was cancelled or errored - just wait for server callback
|
|
549
|
+
if (!resolved) {
|
|
550
|
+
prompts.log.info('Waiting for browser redirect...');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
})();
|
|
339
554
|
});
|
|
340
555
|
});
|
|
341
556
|
}
|
|
@@ -387,9 +602,41 @@ class ServiceNowOAuth {
|
|
|
387
602
|
}
|
|
388
603
|
catch (error) {
|
|
389
604
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
390
|
-
|
|
605
|
+
// Check for invalid redirect_uri error
|
|
391
606
|
if (axios_1.default.isAxiosError(error) && error.response) {
|
|
392
|
-
|
|
607
|
+
const responseData = error.response.data;
|
|
608
|
+
const errorDescription = responseData?.error_description || responseData?.error || '';
|
|
609
|
+
// Special handling for redirect_uri errors
|
|
610
|
+
if (errorDescription.toLowerCase().includes('redirect_uri') ||
|
|
611
|
+
errorDescription.toLowerCase().includes('redirect uri')) {
|
|
612
|
+
prompts.log.error('Invalid redirect_uri configuration');
|
|
613
|
+
prompts.log.message('');
|
|
614
|
+
prompts.log.warn('The OAuth application in ServiceNow is not configured correctly.');
|
|
615
|
+
prompts.log.message('');
|
|
616
|
+
prompts.log.step('Fix this by following these steps:');
|
|
617
|
+
prompts.log.message('');
|
|
618
|
+
prompts.log.info('1. Log into ServiceNow as administrator');
|
|
619
|
+
prompts.log.info(`2. Navigate to: System OAuth → Application Registry`);
|
|
620
|
+
prompts.log.info(`3. Find your OAuth application (Client ID: ${this.credentials.clientId})`);
|
|
621
|
+
prompts.log.info('4. Edit the "Redirect URL" field');
|
|
622
|
+
prompts.log.info('5. Change it to: http://localhost:3005/callback');
|
|
623
|
+
prompts.log.message(' (exactly this - copy/paste to avoid typos!)');
|
|
624
|
+
prompts.log.info('6. Save the application');
|
|
625
|
+
prompts.log.info('7. Run "snow-flow auth login" again');
|
|
626
|
+
prompts.log.message('');
|
|
627
|
+
prompts.log.warn('The redirect URI MUST be exactly: http://localhost:3005/callback');
|
|
628
|
+
prompts.log.message('');
|
|
629
|
+
return {
|
|
630
|
+
success: false,
|
|
631
|
+
error: 'Invalid redirect_uri - OAuth application not configured. See instructions above.'
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
// Log other API errors
|
|
635
|
+
prompts.log.error(`ServiceNow OAuth error: ${errorDescription}`);
|
|
636
|
+
prompts.log.error(`Response data: ${JSON.stringify(responseData)}`);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
prompts.log.error(`Token exchange error: ${errorMessage}`);
|
|
393
640
|
}
|
|
394
641
|
return {
|
|
395
642
|
success: false,
|
|
@@ -414,7 +661,7 @@ class ServiceNowOAuth {
|
|
|
414
661
|
await unified_auth_store_js_1.unifiedAuthStore.bridgeToMCP();
|
|
415
662
|
}
|
|
416
663
|
catch (error) {
|
|
417
|
-
|
|
664
|
+
prompts.log.error(`Failed to save tokens: ${error}`);
|
|
418
665
|
throw error;
|
|
419
666
|
}
|
|
420
667
|
}
|
|
@@ -459,7 +706,7 @@ class ServiceNowOAuth {
|
|
|
459
706
|
const now = new Date();
|
|
460
707
|
if (now >= expiresAt && tokens.refreshToken) {
|
|
461
708
|
// Token expired, try to refresh
|
|
462
|
-
|
|
709
|
+
prompts.log.info('Token expired, refreshing...');
|
|
463
710
|
const refreshResult = await this.refreshAccessToken(tokens);
|
|
464
711
|
if (refreshResult.success && refreshResult.accessToken) {
|
|
465
712
|
// Update saved tokens
|
|
@@ -471,14 +718,14 @@ class ServiceNowOAuth {
|
|
|
471
718
|
return refreshResult.accessToken;
|
|
472
719
|
}
|
|
473
720
|
else {
|
|
474
|
-
|
|
721
|
+
prompts.log.error(`Token refresh failed: ${refreshResult.error}`);
|
|
475
722
|
return null;
|
|
476
723
|
}
|
|
477
724
|
}
|
|
478
725
|
return tokens.accessToken;
|
|
479
726
|
}
|
|
480
727
|
catch (error) {
|
|
481
|
-
|
|
728
|
+
prompts.log.error(`Failed to get access token: ${error}`);
|
|
482
729
|
return null;
|
|
483
730
|
}
|
|
484
731
|
}
|
|
@@ -555,10 +802,10 @@ class ServiceNowOAuth {
|
|
|
555
802
|
async logout() {
|
|
556
803
|
try {
|
|
557
804
|
await fs_1.promises.unlink(this.tokenPath);
|
|
558
|
-
|
|
805
|
+
prompts.log.success('Logged out successfully');
|
|
559
806
|
}
|
|
560
807
|
catch (error) {
|
|
561
|
-
|
|
808
|
+
prompts.log.info('No active session to logout from');
|
|
562
809
|
}
|
|
563
810
|
}
|
|
564
811
|
/**
|
|
@@ -579,15 +826,15 @@ class ServiceNowOAuth {
|
|
|
579
826
|
if (tokens.clientSecret) {
|
|
580
827
|
const secretValidation = this.validateClientSecret(tokens.clientSecret);
|
|
581
828
|
if (!secretValidation.valid) {
|
|
582
|
-
|
|
583
|
-
|
|
829
|
+
prompts.log.warn(`OAuth Configuration Issue: ${secretValidation.reason}`);
|
|
830
|
+
prompts.log.info('Your stored client secret may be incorrect. Re-authenticate with: snow-flow auth login');
|
|
584
831
|
}
|
|
585
832
|
}
|
|
586
833
|
// Check if token is expired
|
|
587
834
|
const expiresAt = new Date(tokens.expiresAt);
|
|
588
835
|
const now = new Date();
|
|
589
836
|
if (now < expiresAt) {
|
|
590
|
-
|
|
837
|
+
prompts.log.success('Using saved OAuth tokens');
|
|
591
838
|
return {
|
|
592
839
|
instance: tokens.instance,
|
|
593
840
|
clientId: tokens.clientId,
|
|
@@ -598,39 +845,39 @@ class ServiceNowOAuth {
|
|
|
598
845
|
};
|
|
599
846
|
}
|
|
600
847
|
else {
|
|
601
|
-
|
|
848
|
+
prompts.log.warn('Saved OAuth token expired, will try refresh...');
|
|
602
849
|
}
|
|
603
850
|
}
|
|
604
851
|
// 🔧 NEW: Fallback to .env file if no valid tokens
|
|
605
|
-
|
|
852
|
+
prompts.log.info('No valid OAuth tokens found, checking .env file...');
|
|
606
853
|
// Load environment variables with dotenv
|
|
607
854
|
try {
|
|
608
855
|
require('dotenv').config();
|
|
609
856
|
}
|
|
610
857
|
catch (err) {
|
|
611
|
-
|
|
858
|
+
prompts.log.message('dotenv not available, using process.env directly');
|
|
612
859
|
}
|
|
613
860
|
const envInstance = process.env.SNOW_INSTANCE;
|
|
614
861
|
const envClientId = process.env.SNOW_CLIENT_ID;
|
|
615
862
|
const envClientSecret = process.env.SNOW_CLIENT_SECRET;
|
|
616
863
|
if (envInstance && envClientId && envClientSecret) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
864
|
+
prompts.log.success('Found ServiceNow credentials in .env file');
|
|
865
|
+
prompts.log.message(` - Instance: ${envInstance}`);
|
|
866
|
+
prompts.log.message(` - Client ID: ${envClientId}`);
|
|
867
|
+
prompts.log.message(' - Client Secret: Present');
|
|
621
868
|
// Validate client secret
|
|
622
869
|
const secretValidation = this.validateClientSecret(envClientSecret);
|
|
623
870
|
if (!secretValidation.valid) {
|
|
624
|
-
|
|
625
|
-
|
|
871
|
+
prompts.log.error(`Invalid OAuth Client Secret in .env file: ${secretValidation.reason}`);
|
|
872
|
+
prompts.log.warn('Please update SNOW_CLIENT_SECRET in .env with proper OAuth secret from ServiceNow');
|
|
626
873
|
return null;
|
|
627
874
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
875
|
+
prompts.log.message('');
|
|
876
|
+
prompts.log.info('OAuth Setup Required:');
|
|
877
|
+
prompts.log.message(' Your .env has OAuth credentials but no active session.');
|
|
878
|
+
prompts.log.message(' Run: snow-flow auth login');
|
|
879
|
+
prompts.log.message(' This will authenticate and create persistent tokens.');
|
|
880
|
+
prompts.log.message('');
|
|
634
881
|
// Return credentials without access token - this will trigger auth flow
|
|
635
882
|
return {
|
|
636
883
|
instance: envInstance.replace(/\/$/, ''),
|
|
@@ -643,33 +890,33 @@ class ServiceNowOAuth {
|
|
|
643
890
|
const envUsername = process.env.SNOW_USERNAME;
|
|
644
891
|
const envPassword = process.env.SNOW_PASSWORD;
|
|
645
892
|
if (envInstance && envUsername && envPassword) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
893
|
+
prompts.log.warn('Found username/password in .env - OAuth is recommended');
|
|
894
|
+
prompts.log.info('For better security, set up OAuth credentials:');
|
|
895
|
+
prompts.log.message(' 1. In ServiceNow: System OAuth > Application Registry > New');
|
|
896
|
+
prompts.log.message(' 2. Update .env with SNOW_CLIENT_ID and SNOW_CLIENT_SECRET');
|
|
897
|
+
prompts.log.message(' 3. Run: snow-flow auth login');
|
|
651
898
|
// Don't return username/password - force OAuth setup
|
|
652
899
|
return null;
|
|
653
900
|
}
|
|
654
901
|
// No credentials found anywhere
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
902
|
+
prompts.log.error('No ServiceNow credentials found!');
|
|
903
|
+
prompts.log.message('');
|
|
904
|
+
prompts.log.info('Setup Instructions:');
|
|
905
|
+
prompts.log.message(' 1. Create .env file with OAuth credentials:');
|
|
906
|
+
prompts.log.message(' SNOW_INSTANCE=your-instance.service-now.com');
|
|
907
|
+
prompts.log.message(' SNOW_CLIENT_ID=your_oauth_client_id');
|
|
908
|
+
prompts.log.message(' SNOW_CLIENT_SECRET=your_oauth_client_secret');
|
|
909
|
+
prompts.log.message(' 2. Run: snow-flow auth login');
|
|
910
|
+
prompts.log.message('');
|
|
911
|
+
prompts.log.info('To get OAuth credentials:');
|
|
912
|
+
prompts.log.message(' • ServiceNow: System OAuth > Application Registry > New OAuth Application');
|
|
913
|
+
prompts.log.message(` • Redirect URI: http://${snow_flow_config_js_1.snowFlowConfig.servicenow.oauth.redirectHost}:${snow_flow_config_js_1.snowFlowConfig.servicenow.oauth.redirectPort}${snow_flow_config_js_1.snowFlowConfig.servicenow.oauth.redirectPath}`);
|
|
914
|
+
prompts.log.message(' • Scopes: useraccount write admin');
|
|
915
|
+
prompts.log.message('');
|
|
669
916
|
return null;
|
|
670
917
|
}
|
|
671
918
|
catch (error) {
|
|
672
|
-
|
|
919
|
+
prompts.log.error(`Error loading credentials: ${error}`);
|
|
673
920
|
return null;
|
|
674
921
|
}
|
|
675
922
|
}
|