@mcp-abap-adt/auth-providers 1.1.0 → 2.0.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/CHANGELOG.md +121 -0
- package/README.md +376 -61
- package/bin/auth-authorization-code.ts +5 -3
- package/dist/__tests__/helpers/netHelpers.d.ts +16 -0
- package/dist/__tests__/helpers/netHelpers.d.ts.map +1 -1
- package/dist/__tests__/helpers/netHelpers.js +29 -0
- package/dist/auth/announce.d.ts +11 -0
- package/dist/auth/announce.d.ts.map +1 -0
- package/dist/auth/announce.js +19 -0
- package/dist/auth/browserAuth.d.ts +22 -16
- package/dist/auth/browserAuth.d.ts.map +1 -1
- package/dist/auth/browserAuth.js +101 -536
- package/dist/auth/callbackServer.d.ts +46 -0
- package/dist/auth/callbackServer.d.ts.map +1 -0
- package/dist/auth/callbackServer.js +332 -0
- package/dist/auth/oidcBrowserAuth.d.ts +4 -3
- package/dist/auth/oidcBrowserAuth.d.ts.map +1 -1
- package/dist/auth/oidcBrowserAuth.js +25 -134
- package/dist/auth/saml2Auth.d.ts +2 -2
- package/dist/auth/saml2Auth.d.ts.map +1 -1
- package/dist/auth/saml2Auth.js +21 -141
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/providers/AuthorizationCodeProvider.d.ts +7 -3
- package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
- package/dist/providers/AuthorizationCodeProvider.js +51 -41
- package/dist/providers/DeviceFlowProvider.d.ts +2 -1
- package/dist/providers/DeviceFlowProvider.d.ts.map +1 -1
- package/dist/providers/DeviceFlowProvider.js +13 -10
- package/dist/providers/OidcBrowserProvider.d.ts +4 -7
- package/dist/providers/OidcBrowserProvider.d.ts.map +1 -1
- package/dist/providers/OidcBrowserProvider.js +54 -57
- package/dist/providers/OidcDeviceFlowProvider.d.ts.map +1 -1
- package/dist/providers/OidcDeviceFlowProvider.js +9 -7
- package/dist/providers/Saml2BearerProvider.d.ts +2 -2
- package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
- package/dist/providers/Saml2BearerProvider.js +3 -0
- package/dist/providers/Saml2PureProvider.d.ts +2 -2
- package/dist/providers/Saml2PureProvider.d.ts.map +1 -1
- package/dist/providers/Saml2PureProvider.js +3 -0
- package/dist/providers/saml2Utils.d.ts +12 -12
- package/dist/providers/saml2Utils.d.ts.map +1 -1
- package/dist/providers/saml2Utils.js +46 -26
- package/dist/strategies/BrowserCallbackStrategy.d.ts +64 -0
- package/dist/strategies/BrowserCallbackStrategy.d.ts.map +1 -0
- package/dist/strategies/BrowserCallbackStrategy.js +210 -0
- package/dist/strategies/asOidcResult.d.ts +11 -0
- package/dist/strategies/asOidcResult.d.ts.map +1 -0
- package/dist/strategies/asOidcResult.js +30 -0
- package/dist/strategies/codeStrategies.d.ts +22 -0
- package/dist/strategies/codeStrategies.d.ts.map +1 -0
- package/dist/strategies/codeStrategies.js +39 -0
- package/dist/strategies/index.d.ts +8 -0
- package/dist/strategies/index.d.ts.map +1 -0
- package/dist/strategies/index.js +18 -0
- package/dist/strategies/manualStrategies.d.ts +20 -0
- package/dist/strategies/manualStrategies.d.ts.map +1 -0
- package/dist/strategies/manualStrategies.js +77 -0
- package/package.json +4 -4
- package/dist/auth/manualInput.d.ts +0 -5
- package/dist/auth/manualInput.d.ts.map +0 -1
- package/dist/auth/manualInput.js +0 -19
package/dist/auth/browserAuth.js
CHANGED
|
@@ -40,14 +40,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.extractCode = extractCode;
|
|
43
|
+
exports.getJwtAuthorizationUrl = getJwtAuthorizationUrl;
|
|
43
44
|
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
44
|
-
exports.
|
|
45
|
+
exports.launchBrowser = launchBrowser;
|
|
45
46
|
const child_process = __importStar(require("node:child_process"));
|
|
46
|
-
const http = __importStar(require("node:http"));
|
|
47
|
-
const net = __importStar(require("node:net"));
|
|
48
|
-
const readline = __importStar(require("node:readline"));
|
|
49
47
|
const axios_1 = __importDefault(require("axios"));
|
|
50
|
-
const express_1 = __importDefault(require("express"));
|
|
51
48
|
const BROWSER_MAP = {
|
|
52
49
|
chrome: 'chrome',
|
|
53
50
|
edge: 'msedge',
|
|
@@ -89,12 +86,14 @@ function extractCode(input) {
|
|
|
89
86
|
return trimmed;
|
|
90
87
|
}
|
|
91
88
|
/**
|
|
92
|
-
*
|
|
89
|
+
* Build the OAuth2 authorization URL for a redirect URI that is already known.
|
|
90
|
+
*
|
|
91
|
+
* The URI is a parameter rather than a port because the port may have been
|
|
92
|
+
* chosen by the OS moments earlier — see `ICallbackServerOptions.port`.
|
|
93
93
|
*/
|
|
94
|
-
function getJwtAuthorizationUrl(authConfig,
|
|
94
|
+
function getJwtAuthorizationUrl(authConfig, redirectUri) {
|
|
95
95
|
const oauthUrl = authConfig.uaaUrl;
|
|
96
96
|
const clientid = authConfig.uaaClientId;
|
|
97
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
98
97
|
if (!oauthUrl || !clientid) {
|
|
99
98
|
throw new Error('Authorization config missing UAA URL or client ID');
|
|
100
99
|
}
|
|
@@ -104,10 +103,9 @@ function getJwtAuthorizationUrl(authConfig, port = 3001) {
|
|
|
104
103
|
* Exchange authorization code for tokens
|
|
105
104
|
* @internal - Exported for testing
|
|
106
105
|
*/
|
|
107
|
-
async function exchangeCodeForToken(authConfig, code,
|
|
106
|
+
async function exchangeCodeForToken(authConfig, code, redirectUri, log) {
|
|
108
107
|
const { uaaUrl: url, uaaClientId: clientid, uaaClientSecret: clientsecret, } = authConfig;
|
|
109
108
|
const tokenUrl = `${url}/oauth/token`;
|
|
110
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
111
109
|
const params = new URLSearchParams();
|
|
112
110
|
params.append('grant_type', 'authorization_code');
|
|
113
111
|
params.append('code', code);
|
|
@@ -148,542 +146,109 @@ function _isDebugEnabled() {
|
|
|
148
146
|
process.env.DEBUG?.includes('browser-auth') === true);
|
|
149
147
|
}
|
|
150
148
|
/**
|
|
151
|
-
*
|
|
149
|
+
* Open the authorization URL, or tell the user how to do it.
|
|
150
|
+
*
|
|
151
|
+
* Never awaited on the critical path by the caller: a launcher that hangs must
|
|
152
|
+
* not delay the login timeout or the release of the port. A launcher that fails
|
|
153
|
+
* is reported through the scope's `fail`, which is just another way for the
|
|
154
|
+
* scope to end.
|
|
152
155
|
*/
|
|
153
|
-
function
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
const server = net.createServer();
|
|
156
|
-
server.listen(port, () => {
|
|
157
|
-
server.once('close', () => resolve(true));
|
|
158
|
-
server.close();
|
|
159
|
-
});
|
|
160
|
-
server.on('error', () => resolve(false));
|
|
161
|
-
});
|
|
162
|
-
}
|
|
156
|
+
async function launchBrowser(authorizationUrl, browser, callbackUri, announce, log,
|
|
163
157
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* @param port Port for OAuth callback server (default: 3001)
|
|
169
|
-
* @returns Promise that resolves to tokens
|
|
170
|
-
* @internal - Internal function, not exported from package
|
|
158
|
+
* Extra guidance for 'none'/'headless', supplied only by a flow whose
|
|
159
|
+
* transport really offers another way in. The UAA callback server has a paste
|
|
160
|
+
* form on `/`; the OIDC and SAML ones do not, and promising one there sends
|
|
161
|
+
* the user to a 404.
|
|
171
162
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
process.stderr.write(`${msg}\n`);
|
|
183
|
-
};
|
|
184
|
-
// Check if requested port is available, throw error if not
|
|
185
|
-
const portAvailable = await isPortAvailable(port);
|
|
186
|
-
if (!portAvailable) {
|
|
187
|
-
throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
|
|
163
|
+
remoteHint) {
|
|
164
|
+
const browserApp = BROWSER_MAP[browser];
|
|
165
|
+
// 'none' / 'headless': show the URL and wait. For SSH and remote sessions.
|
|
166
|
+
if (browser === 'none' || browser === 'headless') {
|
|
167
|
+
announce('🔗 Open this URL in your browser to authenticate:');
|
|
168
|
+
announce(` ${authorizationUrl}`);
|
|
169
|
+
announce(` Waiting for callback on ${callbackUri} ...`);
|
|
170
|
+
if (remoteHint)
|
|
171
|
+
announce(remoteHint);
|
|
172
|
+
return;
|
|
188
173
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let stdinReader = null;
|
|
196
|
-
const stopStdin = () => {
|
|
197
|
-
if (stdinReader) {
|
|
198
|
-
stdinReader.close();
|
|
199
|
-
stdinReader = null;
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
const app = (0, express_1.default)();
|
|
203
|
-
const server = http.createServer(app);
|
|
204
|
-
// Disable keep-alive to ensure connections close immediately
|
|
205
|
-
server.keepAliveTimeout = 0;
|
|
206
|
-
server.headersTimeout = 0;
|
|
207
|
-
const PORT = port;
|
|
208
|
-
let serverInstance = null;
|
|
209
|
-
// Cleanup function to ensure server is closed on process termination
|
|
210
|
-
const cleanup = () => {
|
|
211
|
-
if (cleanupDone)
|
|
212
|
-
return;
|
|
213
|
-
cleanupDone = true;
|
|
214
|
-
log?.debug(`Cleaning up OAuth callback server on port ${PORT}`);
|
|
215
|
-
if (timeoutId) {
|
|
216
|
-
clearTimeout(timeoutId);
|
|
217
|
-
timeoutId = null;
|
|
218
|
-
}
|
|
219
|
-
if (finishTimeoutId) {
|
|
220
|
-
clearTimeout(finishTimeoutId);
|
|
221
|
-
finishTimeoutId = null;
|
|
222
|
-
}
|
|
223
|
-
stopStdin();
|
|
224
|
-
if (server) {
|
|
225
|
-
try {
|
|
226
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
227
|
-
server.closeAllConnections();
|
|
228
|
-
}
|
|
229
|
-
server.close(() => {
|
|
230
|
-
log?.debug(`OAuth server closed during cleanup, port ${PORT} freed`);
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
catch (_e) {
|
|
234
|
-
// Ignore errors during cleanup
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
// Remove cleanup listeners to prevent memory leaks
|
|
239
|
-
const removeCleanupListeners = () => {
|
|
240
|
-
process.removeListener('exit', cleanup);
|
|
241
|
-
process.removeListener('SIGTERM', cleanup);
|
|
242
|
-
process.removeListener('SIGINT', cleanup);
|
|
243
|
-
process.removeListener('SIGHUP', cleanup);
|
|
244
|
-
if (process.platform === 'win32') {
|
|
245
|
-
process.removeListener('SIGBREAK', cleanup);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
const resolve = (value) => {
|
|
249
|
-
if (resolved)
|
|
250
|
-
return; // Prevent double resolution
|
|
251
|
-
resolved = true;
|
|
252
|
-
if (timeoutId) {
|
|
253
|
-
clearTimeout(timeoutId);
|
|
254
|
-
timeoutId = null;
|
|
255
|
-
}
|
|
256
|
-
if (finishTimeoutId) {
|
|
257
|
-
clearTimeout(finishTimeoutId);
|
|
258
|
-
finishTimeoutId = null;
|
|
259
|
-
}
|
|
260
|
-
stopStdin();
|
|
261
|
-
removeCleanupListeners();
|
|
262
|
-
originalResolve(value);
|
|
263
|
-
};
|
|
264
|
-
const reject = (reason) => {
|
|
265
|
-
if (timeoutId)
|
|
266
|
-
clearTimeout(timeoutId);
|
|
267
|
-
removeCleanupListeners();
|
|
268
|
-
originalReject(reason);
|
|
269
|
-
};
|
|
270
|
-
// Register cleanup handlers for process termination
|
|
271
|
-
// This ensures port is freed when Cline or other clients kill the process
|
|
272
|
-
process.once('exit', cleanup);
|
|
273
|
-
process.once('SIGTERM', cleanup);
|
|
274
|
-
process.once('SIGINT', cleanup);
|
|
275
|
-
process.once('SIGHUP', cleanup);
|
|
276
|
-
// SIGBREAK is Windows-specific (Ctrl+Break)
|
|
277
|
-
if (process.platform === 'win32') {
|
|
278
|
-
process.once('SIGBREAK', cleanup);
|
|
279
|
-
}
|
|
280
|
-
// Use provided authorization URL or build from authConfig
|
|
281
|
-
const authorizationUrl = authConfig.authorizationUrl ?? getJwtAuthorizationUrl(authConfig, PORT);
|
|
282
|
-
log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
|
|
283
|
-
log?.info(`[browserAuth] Server listening on port: ${PORT}`);
|
|
284
|
-
// Verify port in redirect_uri matches server port
|
|
285
|
-
const redirectUriMatch = authorizationUrl.match(/redirect_uri=([^&]+)/);
|
|
286
|
-
if (redirectUriMatch) {
|
|
287
|
-
const redirectUri = decodeURIComponent(redirectUriMatch[1]);
|
|
288
|
-
const urlPortMatch = redirectUri.match(/localhost:(\d+)/);
|
|
289
|
-
if (urlPortMatch) {
|
|
290
|
-
const urlPort = parseInt(urlPortMatch[1], 10);
|
|
291
|
-
if (urlPort !== PORT) {
|
|
292
|
-
log?.warn(`[browserAuth] WARNING: Port mismatch! URL has port ${urlPort}, but server listens on ${PORT}`);
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
log?.info(`[browserAuth] Port match: URL and server both use port ${PORT}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
174
|
+
if (browser === 'auto') {
|
|
175
|
+
log?.info('🌐 Attempting to open browser for authentication...');
|
|
176
|
+
try {
|
|
177
|
+
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
178
|
+
await openModule.default(authorizationUrl);
|
|
179
|
+
log?.info('✅ Browser opened successfully. Waiting for authentication...');
|
|
298
180
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
</head><body><div class="container"><div class="success-icon">✓</div>
|
|
306
|
-
<h1>Authentication Successful!</h1>
|
|
307
|
-
<p>You have successfully authenticated with SAP BTP. You can close this window.</p>
|
|
308
|
-
</div></body></html>`;
|
|
309
|
-
// Manual paste form (GET /). Used when the automatic localhost callback
|
|
310
|
-
// cannot reach this server (browser on another machine). Accepts a bare
|
|
311
|
-
// code or a full redirected URL; re-renders with a message on a bad paste.
|
|
312
|
-
const pasteFormHtml = (message) => `<!DOCTYPE html>
|
|
313
|
-
<html lang="en"><head><meta charset="UTF-8">
|
|
314
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
315
|
-
<title>SAP BTP Authentication — paste code</title>
|
|
316
|
-
<style>body{font-family:'Segoe UI',Tahoma,sans-serif;text-align:center;padding:50px 20px;background:linear-gradient(135deg,#0070f3,#00d4ff);color:#fff;min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center}.container{background:rgba(255,255,255,.1);border-radius:20px;padding:40px;max-width:560px;width:100%}h1{font-weight:300}input{width:100%;padding:12px;border-radius:8px;border:none;font-size:1rem;box-sizing:border-box;margin:14px 0}button{padding:12px 24px;border-radius:8px;border:none;background:#fff;color:#0070f3;font-size:1rem;cursor:pointer}.msg{color:#fde68a;margin-bottom:10px}</style>
|
|
317
|
-
</head><body><div class="container">
|
|
318
|
-
<h1>Paste authorization code</h1>
|
|
319
|
-
${message ? `<p class="msg">${message}</p>` : ''}
|
|
320
|
-
<p>After signing in, copy the <code>code</code> from your browser's address bar
|
|
321
|
-
(or paste the whole redirected URL) and submit it here.</p>
|
|
322
|
-
<form action="/submit" method="get">
|
|
323
|
-
<input name="input" autofocus placeholder="code=... or http://localhost/callback?code=..." />
|
|
324
|
-
<button type="submit">Submit</button>
|
|
325
|
-
</form></div></body></html>`;
|
|
326
|
-
// Resolve the outer promise once a code has been turned into tokens.
|
|
327
|
-
const finalizeSuccess = (tokens, res) => {
|
|
328
|
-
let serverClosing = false;
|
|
329
|
-
const closeServerAndResolve = () => {
|
|
330
|
-
if (serverClosing)
|
|
331
|
-
return;
|
|
332
|
-
serverClosing = true;
|
|
333
|
-
if (finishTimeoutId) {
|
|
334
|
-
clearTimeout(finishTimeoutId);
|
|
335
|
-
finishTimeoutId = null;
|
|
336
|
-
}
|
|
337
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
338
|
-
server.closeAllConnections();
|
|
339
|
-
}
|
|
340
|
-
server.close(() => {
|
|
341
|
-
log?.info(`[browserAuth] Server closed, port ${PORT} freed`);
|
|
342
|
-
resolve({
|
|
343
|
-
accessToken: tokens.accessToken,
|
|
344
|
-
refreshToken: tokens.refreshToken,
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
};
|
|
348
|
-
if (res) {
|
|
349
|
-
res.send(successHtml);
|
|
350
|
-
res.once('finish', closeServerAndResolve);
|
|
351
|
-
// Fallback if the finish event doesn't fire promptly.
|
|
352
|
-
finishTimeoutId = setTimeout(closeServerAndResolve, 1000);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
closeServerAndResolve();
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
// Exchange a code for tokens and resolve. Throws on exchange failure so the
|
|
359
|
-
// caller decides: reject (auto callback) or keep waiting (manual paste).
|
|
360
|
-
const completeWithCode = async (code, res) => {
|
|
361
|
-
if (resolved)
|
|
362
|
-
return;
|
|
363
|
-
log?.info(`[browserAuth] Exchanging code for token...`);
|
|
364
|
-
const tokens = await exchangeCodeForToken(authConfig, code, PORT, log);
|
|
365
|
-
log?.info(`[browserAuth] Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
|
|
366
|
-
finalizeSuccess(tokens, res);
|
|
367
|
-
};
|
|
368
|
-
// OAuth2 callback handler
|
|
369
|
-
app.get('/callback', async (req, res) => {
|
|
370
|
-
try {
|
|
371
|
-
log?.info(`[browserAuth] Callback received: ${req.url}`);
|
|
372
|
-
log?.debug(`Callback query: ${JSON.stringify(req.query)}`);
|
|
373
|
-
// Check for OAuth2 error parameters
|
|
374
|
-
const { error, error_description, error_uri } = req.query;
|
|
375
|
-
if (error) {
|
|
376
|
-
log?.error(`Callback error: ${error}${error_description ? ` - ${error_description}` : ''}`);
|
|
377
|
-
const errorMsg = error_description
|
|
378
|
-
? `${error}: ${error_description}`
|
|
379
|
-
: String(error);
|
|
380
|
-
const errorHtml = `<!DOCTYPE html>
|
|
381
|
-
<html lang="en">
|
|
382
|
-
<head>
|
|
383
|
-
<meta charset="UTF-8">
|
|
384
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
385
|
-
<title>Authentication Error</title>
|
|
386
|
-
<style>
|
|
387
|
-
body {
|
|
388
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
389
|
-
text-align: center;
|
|
390
|
-
margin: 0;
|
|
391
|
-
padding: 50px 20px;
|
|
392
|
-
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
|
393
|
-
color: white;
|
|
394
|
-
min-height: 100vh;
|
|
395
|
-
display: flex;
|
|
396
|
-
flex-direction: column;
|
|
397
|
-
justify-content: center;
|
|
398
|
-
align-items: center;
|
|
181
|
+
catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
log?.warn(`⚠️ Could not open browser automatically: ${message}`);
|
|
184
|
+
announce('🔗 Please open this URL in your browser to authenticate:');
|
|
185
|
+
announce(` ${authorizationUrl}`);
|
|
186
|
+
announce(` Waiting for callback on ${callbackUri} ...`);
|
|
399
187
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (browserApp === null)
|
|
191
|
+
return;
|
|
192
|
+
// On Linux, ensure DISPLAY is set for X11 applications.
|
|
193
|
+
if (process.platform === 'linux' &&
|
|
194
|
+
!process.env.DISPLAY &&
|
|
195
|
+
!process.env.WAYLAND_DISPLAY) {
|
|
196
|
+
process.env.DISPLAY = ':0';
|
|
197
|
+
log?.debug('DISPLAY not set, using fallback DISPLAY=:0');
|
|
198
|
+
}
|
|
199
|
+
let open = null;
|
|
200
|
+
try {
|
|
201
|
+
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
202
|
+
open = openModule.default;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
open = null;
|
|
206
|
+
}
|
|
207
|
+
if (!open) {
|
|
208
|
+
// Fallback: shell out. Non-blocking by design.
|
|
209
|
+
const platform = process.platform;
|
|
210
|
+
let command;
|
|
211
|
+
if (browserApp === 'chrome') {
|
|
212
|
+
command =
|
|
213
|
+
platform === 'win32'
|
|
214
|
+
? 'cmd /c start "" "chrome"'
|
|
215
|
+
: platform === 'darwin'
|
|
216
|
+
? 'open -a "Google Chrome"'
|
|
217
|
+
: 'google-chrome || chromium || chromium-browser';
|
|
408
218
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
219
|
+
else if (browserApp === 'msedge') {
|
|
220
|
+
command =
|
|
221
|
+
platform === 'win32'
|
|
222
|
+
? 'cmd /c start "" "msedge"'
|
|
223
|
+
: platform === 'darwin'
|
|
224
|
+
? 'open -a "Microsoft Edge"'
|
|
225
|
+
: 'microsoft-edge || microsoft-edge-stable';
|
|
414
226
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
227
|
+
else if (browserApp === 'firefox') {
|
|
228
|
+
command =
|
|
229
|
+
platform === 'win32'
|
|
230
|
+
? 'cmd /c start "" "firefox"'
|
|
231
|
+
: platform === 'darwin'
|
|
232
|
+
? 'open -a Firefox'
|
|
233
|
+
: 'firefox || firefox-esr';
|
|
419
234
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
235
|
+
else {
|
|
236
|
+
command =
|
|
237
|
+
platform === 'win32'
|
|
238
|
+
? 'cmd /c start ""'
|
|
239
|
+
: platform === 'darwin'
|
|
240
|
+
? 'open'
|
|
241
|
+
: 'xdg-open';
|
|
425
242
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
<div class="container">
|
|
430
|
-
<div class="error-icon">✗</div>
|
|
431
|
-
<h1>Authentication Failed</h1>
|
|
432
|
-
<p>${errorMsg}</p>
|
|
433
|
-
<p>Please check your service key configuration and try again.</p>
|
|
434
|
-
</div>
|
|
435
|
-
</body>
|
|
436
|
-
</html>`;
|
|
437
|
-
res.status(400).send(errorHtml);
|
|
438
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
439
|
-
server.closeAllConnections();
|
|
440
|
-
}
|
|
441
|
-
server.close(() => {
|
|
442
|
-
// Server closed on error
|
|
443
|
-
});
|
|
444
|
-
return reject(new Error(`OAuth2 authentication failed: ${errorMsg}${error_uri ? ` (${error_uri})` : ''}`));
|
|
445
|
-
}
|
|
446
|
-
const { code } = req.query;
|
|
447
|
-
log?.info(`[browserAuth] Callback code received: ${code ? 'yes' : 'no'}`);
|
|
448
|
-
log?.debug(`Callback code received: ${code ? 'yes' : 'no'}`);
|
|
449
|
-
if (!code || typeof code !== 'string') {
|
|
450
|
-
log?.error(`[browserAuth] Callback code missing`);
|
|
451
|
-
res.status(400).send('Error: Authorization code missing');
|
|
452
|
-
return reject(new Error('Authorization code missing'));
|
|
453
|
-
}
|
|
454
|
-
// Exchange code for tokens; on failure the auto-callback path rejects.
|
|
455
|
-
try {
|
|
456
|
-
await completeWithCode(code, res);
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
460
|
-
server.closeAllConnections();
|
|
461
|
-
}
|
|
462
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
463
|
-
setTimeout(() => {
|
|
464
|
-
server.close(() => {
|
|
465
|
-
log?.debug(`Server closed on error, port ${PORT} should be freed`);
|
|
466
|
-
});
|
|
467
|
-
}, 100);
|
|
468
|
-
reject(error);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
res.status(500).send('Error processing authentication');
|
|
473
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
474
|
-
server.closeAllConnections();
|
|
475
|
-
}
|
|
476
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
477
|
-
setTimeout(() => {
|
|
478
|
-
server.close(() => {
|
|
479
|
-
// Server closed on error - port should be freed
|
|
480
|
-
log?.debug(`Server closed on error, port ${PORT} should be freed`);
|
|
481
|
-
});
|
|
482
|
-
}, 100);
|
|
483
|
-
reject(error);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
// Manual paste form (served on the same already-listening server).
|
|
487
|
-
app.get('/', (_req, res) => {
|
|
488
|
-
res.send(pasteFormHtml());
|
|
489
|
-
});
|
|
490
|
-
// Manual paste submit: accept a bare code or a full redirected URL.
|
|
491
|
-
app.get('/submit', async (req, res) => {
|
|
492
|
-
const raw = req.query.input ?? req.query.code;
|
|
493
|
-
const code = typeof raw === 'string' ? extractCode(raw) : null;
|
|
494
|
-
if (!code) {
|
|
495
|
-
res
|
|
496
|
-
.status(400)
|
|
497
|
-
.send(pasteFormHtml('Could not read an authorization code from that input. Try again.'));
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
try {
|
|
501
|
-
await completeWithCode(code, res);
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
// Recoverable: a wrong/expired code shouldn't end the whole flow.
|
|
505
|
-
log?.warn(`[browserAuth] Manual code exchange failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
-
res
|
|
507
|
-
.status(400)
|
|
508
|
-
.send(pasteFormHtml('Code exchange failed. Check the code and try again.'));
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
// Handle server errors (e.g., EADDRINUSE)
|
|
512
|
-
server.on('error', (error) => {
|
|
513
|
-
if (error.code === 'EADDRINUSE') {
|
|
514
|
-
log?.error(`Port ${PORT} is already in use. This should not happen after port check.`);
|
|
515
|
-
reject(new Error(`Port ${PORT} is already in use. Please try again or specify a different port.`));
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
log?.error(`Server error: ${error.message}`);
|
|
519
|
-
reject(error);
|
|
243
|
+
child_process.exec(`${command} "${authorizationUrl}"`, (error) => {
|
|
244
|
+
if (error) {
|
|
245
|
+
log?.error(`❌ Failed to open browser: ${error.message}. Please open manually: ${authorizationUrl}`, { error: error.message, url: authorizationUrl });
|
|
520
246
|
}
|
|
521
247
|
});
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (browser === 'none' || browser === 'headless') {
|
|
529
|
-
announce(`🔗 Open this URL in your browser to authenticate:`);
|
|
530
|
-
announce(` ${authorizationUrl}`);
|
|
531
|
-
announce(` Waiting for callback on http://localhost:${PORT}/callback ...`);
|
|
532
|
-
announce(` If your browser is on another machine, after login copy the ` +
|
|
533
|
-
`\`code\` from the address bar and paste it at ` +
|
|
534
|
-
`http://<this-host>:${PORT}/ — or paste it here and press Enter.`);
|
|
535
|
-
// Manual stdin paste — only when attached to an interactive terminal.
|
|
536
|
-
// Under a stdio RPC transport stdin carries the protocol, so we must
|
|
537
|
-
// never consume it; isTTY guards that.
|
|
538
|
-
if (process.stdin.isTTY) {
|
|
539
|
-
stdinReader = readline.createInterface({ input: process.stdin });
|
|
540
|
-
stdinReader.on('line', async (line) => {
|
|
541
|
-
const code = extractCode(line);
|
|
542
|
-
if (!code) {
|
|
543
|
-
process.stderr.write('Could not read an authorization code from that input. Try again.\n');
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
await completeWithCode(code);
|
|
548
|
-
}
|
|
549
|
-
catch (error) {
|
|
550
|
-
process.stderr.write(`Code exchange failed: ${error instanceof Error ? error.message : String(error)}. Try again.\n`);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
// Don't open browser, don't reject - just wait for callback or paste.
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
// Handle 'auto' mode - try to open browser, fallback to showing URL
|
|
558
|
-
if (browser === 'auto') {
|
|
559
|
-
log?.info('🌐 Attempting to open browser for authentication...');
|
|
560
|
-
try {
|
|
561
|
-
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
562
|
-
const open = openModule.default;
|
|
563
|
-
await open(authorizationUrl);
|
|
564
|
-
log?.info('✅ Browser opened successfully. Waiting for authentication...');
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
catch (error) {
|
|
568
|
-
// If browser cannot be opened, show URL and wait
|
|
569
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
570
|
-
log?.warn(`⚠️ Could not open browser automatically: ${errorMessage}`);
|
|
571
|
-
announce(`🔗 Please open this URL in your browser to authenticate:`);
|
|
572
|
-
announce(` ${authorizationUrl}`);
|
|
573
|
-
announce(` Waiting for callback on http://localhost:${PORT}/callback ...`);
|
|
574
|
-
// Don't reject - wait for callback
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// Handle browser opening (system, chrome, edge, firefox)
|
|
579
|
-
if (browser && browserApp !== null) {
|
|
580
|
-
log?.debug('🌐 Opening browser for authentication...');
|
|
581
|
-
// On Linux, ensure DISPLAY is set for X11 applications
|
|
582
|
-
// This helps when running from terminals that don't set DISPLAY automatically
|
|
583
|
-
if (process.platform === 'linux' &&
|
|
584
|
-
!process.env.DISPLAY &&
|
|
585
|
-
!process.env.WAYLAND_DISPLAY) {
|
|
586
|
-
process.env.DISPLAY = ':0';
|
|
587
|
-
log?.debug('DISPLAY not set, using fallback DISPLAY=:0');
|
|
588
|
-
}
|
|
589
|
-
try {
|
|
590
|
-
// Try dynamic import first (for ES modules)
|
|
591
|
-
let open;
|
|
592
|
-
try {
|
|
593
|
-
const openModule = await Promise.resolve().then(() => __importStar(require('open')));
|
|
594
|
-
open = openModule.default;
|
|
595
|
-
}
|
|
596
|
-
catch (_importError) {
|
|
597
|
-
// Fallback: use child_process to open browser if import fails
|
|
598
|
-
// This works in both CommonJS and ES module environments (like Jest)
|
|
599
|
-
const platform = process.platform;
|
|
600
|
-
let command;
|
|
601
|
-
if (browserApp === 'chrome') {
|
|
602
|
-
command =
|
|
603
|
-
platform === 'win32'
|
|
604
|
-
? 'cmd /c start "" "chrome"'
|
|
605
|
-
: platform === 'darwin'
|
|
606
|
-
? 'open -a "Google Chrome"'
|
|
607
|
-
: 'google-chrome || google-chrome-stable || chromium || chromium-browser';
|
|
608
|
-
}
|
|
609
|
-
else if (browserApp === 'edge') {
|
|
610
|
-
command =
|
|
611
|
-
platform === 'win32'
|
|
612
|
-
? 'cmd /c start "" "msedge"'
|
|
613
|
-
: platform === 'darwin'
|
|
614
|
-
? 'open -a "Microsoft Edge"'
|
|
615
|
-
: 'microsoft-edge || microsoft-edge-stable';
|
|
616
|
-
}
|
|
617
|
-
else if (browserApp === 'firefox') {
|
|
618
|
-
command =
|
|
619
|
-
platform === 'win32'
|
|
620
|
-
? 'cmd /c start "" "firefox"'
|
|
621
|
-
: platform === 'darwin'
|
|
622
|
-
? 'open -a Firefox'
|
|
623
|
-
: 'firefox || firefox-esr';
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
// System default
|
|
627
|
-
command =
|
|
628
|
-
platform === 'win32'
|
|
629
|
-
? 'cmd /c start ""'
|
|
630
|
-
: platform === 'darwin'
|
|
631
|
-
? 'open'
|
|
632
|
-
: 'xdg-open';
|
|
633
|
-
}
|
|
634
|
-
// Use child_process as fallback (non-blocking)
|
|
635
|
-
child_process.exec(`${command} "${authorizationUrl}"`, (error) => {
|
|
636
|
-
if (error) {
|
|
637
|
-
log?.error(`❌ Failed to open browser: ${error.message}. Please open manually: ${authorizationUrl}`, { error: error.message, url: authorizationUrl });
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
return; // Exit early since we're using child_process (non-blocking)
|
|
641
|
-
}
|
|
642
|
-
// Use open module if import succeeded
|
|
643
|
-
if (browserApp) {
|
|
644
|
-
await open(authorizationUrl, { app: { name: browserApp } });
|
|
645
|
-
}
|
|
646
|
-
else {
|
|
647
|
-
await open(authorizationUrl);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
catch (error) {
|
|
651
|
-
// If browser cannot be opened, close server and show URL
|
|
652
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
653
|
-
server.closeAllConnections();
|
|
654
|
-
}
|
|
655
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
656
|
-
setTimeout(() => {
|
|
657
|
-
server.close(() => {
|
|
658
|
-
// Server closed on browser open error - port should be freed
|
|
659
|
-
log?.debug(`Server closed on browser open error, port ${PORT} should be freed`);
|
|
660
|
-
});
|
|
661
|
-
}, 100);
|
|
662
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
663
|
-
log?.error(`❌ Failed to open browser: ${errorMessage}. Please open manually: ${authorizationUrl}`, { error: errorMessage, url: authorizationUrl });
|
|
664
|
-
log?.info(`🔗 Open in browser: ${authorizationUrl}`, {
|
|
665
|
-
url: authorizationUrl,
|
|
666
|
-
});
|
|
667
|
-
// Throw error so consumer can distinguish this from "service key missing" error
|
|
668
|
-
reject(new Error(`Browser opening failed for destination authentication. Please open manually: ${authorizationUrl}`));
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
// Timeout after 30 seconds to prevent blocking consumer
|
|
673
|
-
timeoutId = setTimeout(() => {
|
|
674
|
-
if (serverInstance) {
|
|
675
|
-
if (typeof server.closeAllConnections === 'function') {
|
|
676
|
-
server.closeAllConnections();
|
|
677
|
-
}
|
|
678
|
-
// Use setTimeout to ensure connections are closed before server.close()
|
|
679
|
-
setTimeout(() => {
|
|
680
|
-
server.close(() => {
|
|
681
|
-
// Server closed on timeout - port should be freed
|
|
682
|
-
log?.debug(`Server closed on timeout, port ${PORT} should be freed`);
|
|
683
|
-
});
|
|
684
|
-
}, 100);
|
|
685
|
-
reject(new Error('Authentication timeout after 30 seconds. Please try again.'));
|
|
686
|
-
}
|
|
687
|
-
}, 30 * 1000);
|
|
688
|
-
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (browserApp)
|
|
251
|
+
await open(authorizationUrl, { app: { name: browserApp } });
|
|
252
|
+
else
|
|
253
|
+
await open(authorizationUrl);
|
|
689
254
|
}
|