@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +376 -61
  3. package/bin/auth-authorization-code.ts +5 -3
  4. package/dist/__tests__/helpers/netHelpers.d.ts +16 -0
  5. package/dist/__tests__/helpers/netHelpers.d.ts.map +1 -1
  6. package/dist/__tests__/helpers/netHelpers.js +29 -0
  7. package/dist/auth/announce.d.ts +11 -0
  8. package/dist/auth/announce.d.ts.map +1 -0
  9. package/dist/auth/announce.js +19 -0
  10. package/dist/auth/browserAuth.d.ts +22 -16
  11. package/dist/auth/browserAuth.d.ts.map +1 -1
  12. package/dist/auth/browserAuth.js +101 -536
  13. package/dist/auth/callbackServer.d.ts +46 -0
  14. package/dist/auth/callbackServer.d.ts.map +1 -0
  15. package/dist/auth/callbackServer.js +332 -0
  16. package/dist/auth/oidcBrowserAuth.d.ts +4 -3
  17. package/dist/auth/oidcBrowserAuth.d.ts.map +1 -1
  18. package/dist/auth/oidcBrowserAuth.js +25 -134
  19. package/dist/auth/saml2Auth.d.ts +2 -2
  20. package/dist/auth/saml2Auth.d.ts.map +1 -1
  21. package/dist/auth/saml2Auth.js +21 -141
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +21 -1
  25. package/dist/providers/AuthorizationCodeProvider.d.ts +7 -3
  26. package/dist/providers/AuthorizationCodeProvider.d.ts.map +1 -1
  27. package/dist/providers/AuthorizationCodeProvider.js +51 -41
  28. package/dist/providers/DeviceFlowProvider.d.ts +2 -1
  29. package/dist/providers/DeviceFlowProvider.d.ts.map +1 -1
  30. package/dist/providers/DeviceFlowProvider.js +13 -10
  31. package/dist/providers/OidcBrowserProvider.d.ts +4 -7
  32. package/dist/providers/OidcBrowserProvider.d.ts.map +1 -1
  33. package/dist/providers/OidcBrowserProvider.js +54 -57
  34. package/dist/providers/OidcDeviceFlowProvider.d.ts.map +1 -1
  35. package/dist/providers/OidcDeviceFlowProvider.js +9 -7
  36. package/dist/providers/Saml2BearerProvider.d.ts +2 -2
  37. package/dist/providers/Saml2BearerProvider.d.ts.map +1 -1
  38. package/dist/providers/Saml2BearerProvider.js +3 -0
  39. package/dist/providers/Saml2PureProvider.d.ts +2 -2
  40. package/dist/providers/Saml2PureProvider.d.ts.map +1 -1
  41. package/dist/providers/Saml2PureProvider.js +3 -0
  42. package/dist/providers/saml2Utils.d.ts +12 -12
  43. package/dist/providers/saml2Utils.d.ts.map +1 -1
  44. package/dist/providers/saml2Utils.js +46 -26
  45. package/dist/strategies/BrowserCallbackStrategy.d.ts +64 -0
  46. package/dist/strategies/BrowserCallbackStrategy.d.ts.map +1 -0
  47. package/dist/strategies/BrowserCallbackStrategy.js +210 -0
  48. package/dist/strategies/asOidcResult.d.ts +11 -0
  49. package/dist/strategies/asOidcResult.d.ts.map +1 -0
  50. package/dist/strategies/asOidcResult.js +30 -0
  51. package/dist/strategies/codeStrategies.d.ts +22 -0
  52. package/dist/strategies/codeStrategies.d.ts.map +1 -0
  53. package/dist/strategies/codeStrategies.js +39 -0
  54. package/dist/strategies/index.d.ts +8 -0
  55. package/dist/strategies/index.d.ts.map +1 -0
  56. package/dist/strategies/index.js +18 -0
  57. package/dist/strategies/manualStrategies.d.ts +20 -0
  58. package/dist/strategies/manualStrategies.d.ts.map +1 -0
  59. package/dist/strategies/manualStrategies.js +77 -0
  60. package/package.json +4 -4
  61. package/dist/auth/manualInput.d.ts +0 -5
  62. package/dist/auth/manualInput.d.ts.map +0 -1
  63. package/dist/auth/manualInput.js +0 -19
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Scoped callback servers for interactive authorization flows.
3
+ *
4
+ * One owner, one release point. The socket belongs to the scope, not to the
5
+ * promise a caller happens to be awaiting: it is released on the first terminal
6
+ * outcome — the body returning or throwing, an explicit failure, the timeout, or
7
+ * an abort — and the factory settles only once it is actually free.
8
+ *
9
+ * See `docs/superpowers/specs/2026-07-28-callback-server-contract-design.md`.
10
+ */
11
+ import type { CallbackServerFactory, ICallbackServerHandle, ICallbackServerOptions } from '@mcp-abap-adt/interfaces';
12
+ import express from 'express';
13
+ /**
14
+ * How a route reports an outcome. Settling is deferred until the response has
15
+ * actually flushed — `res.send()` returning does not mean the bytes have left,
16
+ * and settling earlier races the shutdown against the page being delivered.
17
+ */
18
+ export interface Settle<TResult> {
19
+ /** The callback delivered a result. Does not end the scope by itself. */
20
+ ok(value: TResult, res?: express.Response): void;
21
+ /** The callback reported a failure. Ends the scope. */
22
+ err(error: Error, res?: express.Response): void;
23
+ /**
24
+ * This was not our redirect — a reloaded tab, a prefetch, a port scanner.
25
+ * Answered and counted; the login keeps waiting, bounded as ever by the
26
+ * timeout. Ends nothing.
27
+ */
28
+ ignore(reason: string, res?: express.Response): void;
29
+ }
30
+ export type RouteSetup<TResult> = (app: express.Express, settle: Settle<TResult>) => void;
31
+ /**
32
+ * Owns the socket for the duration of `use`.
33
+ *
34
+ * Every flow registers its routes through `routes` and reports outcomes through
35
+ * `settle`; nothing else touches the internal promise, which is what keeps the
36
+ * "settles exactly once" guarantee in one place.
37
+ */
38
+ export declare function runCallbackScope<TResult, TReturn>(options: ICallbackServerOptions, routes: RouteSetup<TResult>, use: (server: ICallbackServerHandle<TResult>) => Promise<TReturn>): Promise<TReturn>;
39
+ /**
40
+ * Callback server for the UAA authorization-code flow.
41
+ *
42
+ * Delivers the authorization code. The paste form and `/submit` are kept for
43
+ * the case where the browser runs on another machine.
44
+ */
45
+ export declare const withBrowserCallbackServer: CallbackServerFactory<string>;
46
+ //# sourceMappingURL=callbackServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callbackServer.d.ts","sourceRoot":"","sources":["../../src/auth/callbackServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,OAAO,MAAM,SAAS,CAAC;AAS9B;;;;GAIG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO;IAC7B,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IACjD,uDAAuD;IACvD,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CACtD;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,CAChC,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KACpB,IAAI,CAAC;AAqBV;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EACrD,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,EAC3B,GAAG,EAAE,CAAC,MAAM,EAAE,qBAAqB,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GAChE,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAyCD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,qBAAqB,CAAC,MAAM,CA4DjE,CAAC"}
@@ -0,0 +1,332 @@
1
+ "use strict";
2
+ /**
3
+ * Scoped callback servers for interactive authorization flows.
4
+ *
5
+ * One owner, one release point. The socket belongs to the scope, not to the
6
+ * promise a caller happens to be awaiting: it is released on the first terminal
7
+ * outcome — the body returning or throwing, an explicit failure, the timeout, or
8
+ * an abort — and the factory settles only once it is actually free.
9
+ *
10
+ * See `docs/superpowers/specs/2026-07-28-callback-server-contract-design.md`.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.withBrowserCallbackServer = void 0;
50
+ exports.runCallbackScope = runCallbackScope;
51
+ const http = __importStar(require("node:http"));
52
+ const express_1 = __importDefault(require("express"));
53
+ const browserAuth_1 = require("./browserAuth");
54
+ /** Node's `setTimeout` takes a 32-bit signed delay; above this it fires in 1 ms. */
55
+ const MAX_TIMEOUT_MS = 2_147_483_647;
56
+ /** How long shutdown waits for `close` before destroying what is left. */
57
+ const SHUTDOWN_GRACE_MS = 500;
58
+ function validate(options) {
59
+ const { port, timeoutMs } = options;
60
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
61
+ throw new Error(`Invalid callback server port: ${String(port)}. Must be an integer in 0..65535.`);
62
+ }
63
+ if (!Number.isFinite(timeoutMs) ||
64
+ timeoutMs <= 0 ||
65
+ timeoutMs > MAX_TIMEOUT_MS) {
66
+ throw new Error(`Invalid callback server timeoutMs: ${String(timeoutMs)}. ` +
67
+ `Must be finite and within 1..${MAX_TIMEOUT_MS}.`);
68
+ }
69
+ }
70
+ /**
71
+ * Owns the socket for the duration of `use`.
72
+ *
73
+ * Every flow registers its routes through `routes` and reports outcomes through
74
+ * `settle`; nothing else touches the internal promise, which is what keeps the
75
+ * "settles exactly once" guarantee in one place.
76
+ */
77
+ async function runCallbackScope(options, routes, use) {
78
+ validate(options);
79
+ if (options.signal?.aborted) {
80
+ throw new Error('Callback server aborted before it started');
81
+ }
82
+ const app = (0, express_1.default)();
83
+ const server = http.createServer(app);
84
+ const sockets = new Set();
85
+ server.on('connection', (socket) => {
86
+ sockets.add(socket);
87
+ socket.on('close', () => sockets.delete(socket));
88
+ });
89
+ let resultSettled = false;
90
+ let resolveResult;
91
+ let rejectResult;
92
+ const resultPromise = new Promise((res, rej) => {
93
+ resolveResult = res;
94
+ rejectResult = rej;
95
+ });
96
+ // Marked handled at creation: a body may create this promise and walk away,
97
+ // and rejecting it at scope end would otherwise raise unhandledRejection.
98
+ void resultPromise.catch(() => undefined);
99
+ let scopeSettled = false;
100
+ let resolveScope;
101
+ let rejectScope;
102
+ const scopePromise = new Promise((res, rej) => {
103
+ resolveScope = res;
104
+ rejectScope = rej;
105
+ });
106
+ let timer = null;
107
+ let alive = false;
108
+ let ignored = 0;
109
+ const settleResult = (outcome) => {
110
+ if (resultSettled)
111
+ return;
112
+ resultSettled = true;
113
+ if ('value' in outcome)
114
+ resolveResult(outcome.value);
115
+ else
116
+ rejectResult(outcome.error);
117
+ };
118
+ /** The one place a scope ends. Everything after the first call is a no-op. */
119
+ const endScope = (outcome) => {
120
+ if (scopeSettled)
121
+ return;
122
+ scopeSettled = true;
123
+ alive = false;
124
+ if (timer) {
125
+ clearTimeout(timer);
126
+ timer = null;
127
+ }
128
+ options.signal?.removeEventListener('abort', onAbort);
129
+ settleResult({
130
+ error: new Error('Callback server closed before a result arrived'),
131
+ });
132
+ void shutdown().then(() => {
133
+ if ('value' in outcome)
134
+ resolveScope(outcome.value);
135
+ else
136
+ rejectScope(outcome.error);
137
+ });
138
+ };
139
+ function onAbort() {
140
+ endScope({ error: new Error('Callback server aborted') });
141
+ }
142
+ /**
143
+ * Stop accepting, end idle connections, wait for `close` under a grace, then
144
+ * destroy whatever is left. `closeAllConnections()` is deliberately not used
145
+ * in the first step: it destroys active connections too, and the one carrying
146
+ * the success page is active.
147
+ */
148
+ function shutdown() {
149
+ return new Promise((done) => {
150
+ let finished = false;
151
+ const finish = () => {
152
+ if (finished)
153
+ return;
154
+ finished = true;
155
+ done();
156
+ };
157
+ // `close()` alone, first. From Node 19 it ends idle connections itself,
158
+ // and it does so gracefully — the client still reads what was written.
159
+ // `closeIdleConnections()` destroys instead, which cuts the success page
160
+ // off mid-read: `finish` on the response means "handed to the OS", not
161
+ // "read by the client".
162
+ server.close(() => finish());
163
+ setTimeout(() => {
164
+ if (finished)
165
+ return;
166
+ // Grace expired — on Node 18.x `close()` does not end idle connections,
167
+ // and an active one may simply be stuck. Force it, bounded.
168
+ server.closeIdleConnections?.();
169
+ server.closeAllConnections?.();
170
+ for (const socket of sockets)
171
+ socket.destroy();
172
+ finish();
173
+ }, SHUTDOWN_GRACE_MS).unref?.();
174
+ });
175
+ }
176
+ /**
177
+ * Settle only once the response has actually flushed, so shutdown cannot cut
178
+ * it off.
179
+ *
180
+ * The check is `writableFinished`, not `writableEnded`: the latter is true as
181
+ * soon as `end()` has been called and says nothing about the data having
182
+ * left. Measured on Node 25 with a paused client — an 800-byte body reports
183
+ * both flags true at once, but a 20 MB body reports `writableEnded` true and
184
+ * `writableFinished` false, with `finish` arriving 456 ms later. Keying off
185
+ * `writableEnded` therefore made this deferral a no-op on the very path it
186
+ * exists for.
187
+ */
188
+ const afterFlush = (res, then) => {
189
+ if (!res || res.writableFinished) {
190
+ then();
191
+ return;
192
+ }
193
+ res.once('finish', then);
194
+ res.once('close', then);
195
+ };
196
+ const settle = {
197
+ ok(value, res) {
198
+ afterFlush(res, () => settleResult({ value }));
199
+ },
200
+ err(error, res) {
201
+ afterFlush(res, () => {
202
+ settleResult({ error });
203
+ endScope({ error });
204
+ });
205
+ },
206
+ ignore(reason) {
207
+ ignored += 1;
208
+ options.logger?.warn('[callbackServer] ignored an incomplete callback request', { reason, ignored });
209
+ },
210
+ };
211
+ routes(app, settle);
212
+ options.signal?.addEventListener('abort', onAbort, { once: true });
213
+ server.once('error', (error) => {
214
+ endScope({ error });
215
+ });
216
+ server.listen(options.port, () => {
217
+ if (scopeSettled) {
218
+ // Aborted while binding.
219
+ void shutdown();
220
+ return;
221
+ }
222
+ alive = true;
223
+ timer = setTimeout(() => {
224
+ const tally = ignored > 0
225
+ ? ` ${ignored} incomplete request(s) reached /callback and were ignored.`
226
+ : '';
227
+ endScope({
228
+ error: new Error(`Authentication timeout after ${options.timeoutMs / 1000} seconds. Please try again.${tally}`),
229
+ });
230
+ }, options.timeoutMs);
231
+ // The requested port may be 0, in which case only the OS knows the answer.
232
+ const bound = server.address().port;
233
+ const handle = {
234
+ port: bound,
235
+ redirectUri: `http://localhost:${bound}/callback`,
236
+ waitForResult: () => alive
237
+ ? resultPromise
238
+ : Promise.reject(new Error('Callback server scope has ended')),
239
+ // Silent no-op once the scope has ended: this is called fire-and-forget
240
+ // from a browser launcher's .catch(), and a late rejection must not become
241
+ // a fresh unhandled rejection.
242
+ fail: (error) => {
243
+ if (!alive)
244
+ return;
245
+ settleResult({ error });
246
+ endScope({ error });
247
+ },
248
+ };
249
+ void use(handle).then((value) => endScope({ value }), (error) => endScope({ error }));
250
+ });
251
+ return await scopePromise;
252
+ }
253
+ const successHtml = `<!DOCTYPE html>
254
+ <html lang="en"><head><meta charset="UTF-8">
255
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
256
+ <title>SAP BTP Authentication</title>
257
+ <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:500px}.success-icon{font-size:4rem;margin-bottom:20px;color:#4ade80}h1{font-weight:300}</style>
258
+ </head><body><div class="container"><div class="success-icon">✓</div>
259
+ <h1>Authentication Successful!</h1>
260
+ <p>You have successfully authenticated with SAP BTP. You can close this window.</p>
261
+ </div></body></html>`;
262
+ const errorHtml = (message) => `<!DOCTYPE html>
263
+ <html lang="en"><head><meta charset="UTF-8">
264
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
265
+ <title>Authentication Error</title>
266
+ <style>body{font-family:'Segoe UI',Tahoma,sans-serif;text-align:center;padding:50px 20px;background:linear-gradient(135deg,#dc2626,#ef4444);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:500px}.error-icon{font-size:4rem;margin-bottom:20px;color:#fbbf24}h1{font-weight:300}</style>
267
+ </head><body><div class="container"><div class="error-icon">✗</div>
268
+ <h1>Authentication Failed</h1>
269
+ <p>${message}</p>
270
+ <p>Please check your service key configuration and try again.</p>
271
+ </div></body></html>`;
272
+ // Manual paste form (GET /). Used when the automatic localhost callback cannot
273
+ // reach this server (browser on another machine). Accepts a bare code or a full
274
+ // redirected URL; re-renders with a message on a bad paste.
275
+ const pasteFormHtml = (message) => `<!DOCTYPE html>
276
+ <html lang="en"><head><meta charset="UTF-8">
277
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
278
+ <title>SAP BTP Authentication — paste code</title>
279
+ <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>
280
+ </head><body><div class="container">
281
+ <h1>Paste authorization code</h1>
282
+ ${message ? `<p class="msg">${message}</p>` : ''}
283
+ <p>After signing in, copy the <code>code</code> from your browser's address bar
284
+ (or paste the whole redirected URL) and submit it here.</p>
285
+ <form action="/submit" method="get">
286
+ <input name="input" autofocus placeholder="code=... or http://localhost/callback?code=..." />
287
+ <button type="submit">Submit</button>
288
+ </form></div></body></html>`;
289
+ /**
290
+ * Callback server for the UAA authorization-code flow.
291
+ *
292
+ * Delivers the authorization code. The paste form and `/submit` are kept for
293
+ * the case where the browser runs on another machine.
294
+ */
295
+ const withBrowserCallbackServer = (options, use) => runCallbackScope(options, (app, settle) => {
296
+ app.get('/callback', (req, res) => {
297
+ const { error, error_description, error_uri } = req.query;
298
+ if (error) {
299
+ const message = error_description
300
+ ? `${String(error)}: ${String(error_description)}`
301
+ : String(error);
302
+ res.status(400).send(errorHtml(message));
303
+ settle.err(new Error(`OAuth2 authentication failed: ${message}` +
304
+ (error_uri ? ` (${String(error_uri)})` : '')), res);
305
+ return;
306
+ }
307
+ const { code } = req.query;
308
+ if (!code || typeof code !== 'string') {
309
+ res.status(400).send('Error: not an authorization callback');
310
+ settle.ignore('no code and no error in query', res);
311
+ return;
312
+ }
313
+ res.send(successHtml);
314
+ settle.ok(code, res);
315
+ });
316
+ app.get('/', (_req, res) => {
317
+ res.send(pasteFormHtml());
318
+ });
319
+ app.get('/submit', (req, res) => {
320
+ const raw = req.query.input ?? req.query.code;
321
+ const code = typeof raw === 'string' ? (0, browserAuth_1.extractCode)(raw) : null;
322
+ if (!code) {
323
+ res
324
+ .status(400)
325
+ .send(pasteFormHtml('Could not read an authorization code from that input. Try again.'));
326
+ return;
327
+ }
328
+ res.send(successHtml);
329
+ settle.ok(code, res);
330
+ });
331
+ }, use);
332
+ exports.withBrowserCallbackServer = withBrowserCallbackServer;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * OIDC browser authorization code flow (capture code)
3
3
  */
4
- import type { ILogger } from '@mcp-abap-adt/interfaces';
5
- export declare function startOidcBrowserAuth(authorizationUrl: string, browser: string, logger?: ILogger, port?: number): Promise<{
4
+ import type { CallbackServerFactory } from '@mcp-abap-adt/interfaces';
5
+ export interface OidcCallbackResult {
6
6
  code: string;
7
7
  state?: string;
8
- }>;
8
+ }
9
+ export declare const withOidcCallbackServer: CallbackServerFactory<OidcCallbackResult>;
9
10
  //# sourceMappingURL=oidcBrowserAuth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAqExD,wBAAsB,oBAAoB,CACxC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2C3C"}
1
+ {"version":3,"file":"oidcBrowserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/oidcBrowserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,sBAAsB,EAAE,qBAAqB,CACxD,kBAAkB,CA6CjB,CAAC"}
@@ -2,143 +2,34 @@
2
2
  /**
3
3
  * OIDC browser authorization code flow (capture code)
4
4
  */
5
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
- if (k2 === undefined) k2 = k;
7
- var desc = Object.getOwnPropertyDescriptor(m, k);
8
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
- desc = { enumerable: true, get: function() { return m[k]; } };
10
- }
11
- Object.defineProperty(o, k2, desc);
12
- }) : (function(o, m, k, k2) {
13
- if (k2 === undefined) k2 = k;
14
- o[k2] = m[k];
15
- }));
16
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
- Object.defineProperty(o, "default", { enumerable: true, value: v });
18
- }) : function(o, v) {
19
- o["default"] = v;
20
- });
21
- var __importStar = (this && this.__importStar) || (function () {
22
- var ownKeys = function(o) {
23
- ownKeys = Object.getOwnPropertyNames || function (o) {
24
- var ar = [];
25
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
- return ar;
27
- };
28
- return ownKeys(o);
29
- };
30
- return function (mod) {
31
- if (mod && mod.__esModule) return mod;
32
- var result = {};
33
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
- __setModuleDefault(result, mod);
35
- return result;
36
- };
37
- })();
38
- var __importDefault = (this && this.__importDefault) || function (mod) {
39
- return (mod && mod.__esModule) ? mod : { "default": mod };
40
- };
41
5
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.startOidcBrowserAuth = startOidcBrowserAuth;
43
- const http = __importStar(require("node:http"));
44
- const net = __importStar(require("node:net"));
45
- const express_1 = __importDefault(require("express"));
46
- const BROWSER_MAP = {
47
- chrome: 'chrome',
48
- edge: 'msedge',
49
- firefox: 'firefox',
50
- system: undefined,
51
- auto: undefined,
52
- headless: null,
53
- none: null,
54
- };
55
- function isPortAvailable(port) {
56
- return new Promise((resolve) => {
57
- const server = net.createServer();
58
- server.listen(port, () => {
59
- server.once('close', () => resolve(true));
60
- server.close();
61
- });
62
- server.on('error', () => resolve(false));
63
- });
64
- }
65
- async function openBrowserUrl(authorizationUrl, browser, logger) {
66
- const browserApp = BROWSER_MAP[browser];
67
- if (browserApp === null) {
68
- logger?.info('[OIDC] Browser suppressed, open URL manually', {
69
- authorizationUrl,
70
- });
71
- return;
72
- }
73
- if (browser === 'auto') {
74
- try {
75
- const openModule = await Promise.resolve().then(() => __importStar(require('open')));
76
- const open = openModule.default;
77
- await open(authorizationUrl);
78
- logger?.info('[OIDC] Browser opened');
6
+ exports.withOidcCallbackServer = void 0;
7
+ const callbackServer_1 = require("./callbackServer");
8
+ const withOidcCallbackServer = (options, use) => (0, callbackServer_1.runCallbackScope)(options, (app, settle) => {
9
+ app.get('/callback', (req, res) => {
10
+ // An IdP that declines says so explicitly. That is a finished login,
11
+ // not a stray request, and it must not wait for the timeout.
12
+ const { error, error_description, error_uri } = req.query;
13
+ if (error) {
14
+ const message = error_description
15
+ ? `${String(error)}: ${String(error_description)}`
16
+ : String(error);
17
+ res.status(400).send(`Authentication failed: ${message}`);
18
+ settle.err(new Error(`OIDC authentication failed: ${message}` +
19
+ (error_uri ? ` (${String(error_uri)})` : '')), res);
79
20
  return;
80
21
  }
81
- catch (error) {
82
- logger?.warn('[OIDC] Failed to open browser automatically', {
83
- error: error instanceof Error ? error.message : String(error),
84
- });
85
- logger?.info('[OIDC] Open URL manually', { authorizationUrl });
22
+ const code = req.query.code;
23
+ const state = req.query.state;
24
+ if (!code || typeof code !== 'string') {
25
+ res.status(400).send('Error: not an authorization callback');
26
+ settle.ignore('no code and no error in query', res);
86
27
  return;
87
28
  }
88
- }
89
- try {
90
- const openModule = await Promise.resolve().then(() => __importStar(require('open')));
91
- const open = openModule.default;
92
- if (browserApp) {
93
- await open(authorizationUrl, { app: { name: browserApp } });
94
- }
95
- else {
96
- await open(authorizationUrl);
97
- }
98
- }
99
- catch (error) {
100
- logger?.warn('[OIDC] Failed to open browser', {
101
- error: error instanceof Error ? error.message : String(error),
102
- });
103
- logger?.info('[OIDC] Open URL manually', { authorizationUrl });
104
- }
105
- }
106
- async function startOidcBrowserAuth(authorizationUrl, browser, logger, port = 3001) {
107
- const portAvailable = await isPortAvailable(port);
108
- if (!portAvailable) {
109
- throw new Error(`Port ${port} is already in use. Please specify a different port or free the port.`);
110
- }
111
- return new Promise((resolve, reject) => {
112
- const app = (0, express_1.default)();
113
- const server = http.createServer(app);
114
- server.keepAliveTimeout = 0;
115
- server.headersTimeout = 0;
116
- const PORT = port;
117
- let resolved = false;
118
- const cleanup = () => {
119
- if (resolved)
120
- return;
121
- resolved = true;
122
- server.close();
123
- };
124
- app.get('/callback', (req, res) => {
125
- const code = req.query.code;
126
- const state = req.query.state;
127
- if (!code || typeof code !== 'string') {
128
- res.status(400).send('Missing authorization code');
129
- cleanup();
130
- reject(new Error('Missing authorization code'));
131
- return;
132
- }
133
- res
134
- .status(200)
135
- .send('Authentication complete. You can close this window.');
136
- cleanup();
137
- resolve({ code, state: typeof state === 'string' ? state : undefined });
138
- });
139
- server.listen(PORT, async () => {
140
- logger?.info('[OIDC] Callback server listening', { port: PORT });
141
- await openBrowserUrl(authorizationUrl, browser, logger);
142
- });
29
+ res
30
+ .status(200)
31
+ .send('Authentication complete. You can close this window.');
32
+ settle.ok({ code, state: typeof state === 'string' ? state : undefined }, res);
143
33
  });
144
- }
34
+ }, use);
35
+ exports.withOidcCallbackServer = withOidcCallbackServer;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SAML 2.0 auth helpers
3
3
  */
4
- import type { ILogger } from '@mcp-abap-adt/interfaces';
4
+ import type { CallbackServerFactory } from '@mcp-abap-adt/interfaces';
5
5
  export interface Saml2AuthConfig {
6
6
  idpSsoUrl: string;
7
7
  spEntityId: string;
@@ -10,6 +10,6 @@ export interface Saml2AuthConfig {
10
10
  authorizationUrl?: string;
11
11
  }
12
12
  export declare function buildSamlAuthorizationUrl(config: Saml2AuthConfig): string;
13
- export declare function startSamlBrowserAuth(config: Saml2AuthConfig, browser: string, logger?: ILogger, port?: number): Promise<string>;
13
+ export declare const withSamlCallbackServer: CallbackServerFactory<string>;
14
14
  export declare function parseSamlNotOnOrAfter(samlResponse: string): number | undefined;
15
15
  //# sourceMappingURL=saml2Auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"saml2Auth.d.ts","sourceRoot":"","sources":["../../src/auth/saml2Auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAGxD,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AA8CD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAazE;AA+CD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC,MAAM,CAAC,CA4DjB;AAED,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAYpB"}
1
+ {"version":3,"file":"saml2Auth.d.ts","sourceRoot":"","sources":["../../src/auth/saml2Auth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAIlC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAyBD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAazE;AAED,eAAO,MAAM,sBAAsB,EAAE,qBAAqB,CAAC,MAAM,CAgC9D,CAAC;AAEJ,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAYpB"}