@mcp-abap-adt/auth-providers 1.0.5 → 1.2.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 CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-07-28
11
+
12
+ ### Fixed
13
+ - **A callback server could hold its port after the login it was opened for had ended.** Three flows, three shapes of one defect: `browserAuth` leaked the socket when a callback carried neither `code` nor `error` — measured still bound at +5 s, +20 s, +35 s and +45 s, i.e. for the life of the process — while `oidcBrowserAuth` and `saml2Auth` had no timeout at all, so an abandoned login never settled and never released anything. (#11)
14
+ - **A rejected login could report a port that was not yet free.** Five exit paths closed the socket asynchronously *after* the promise had settled, leaving a window in which "already in use" was untrue.
15
+ - **`AuthorizationCodeProvider` raced `startBrowserAuth` against a second 30-second timer** of its own, so which fired was down to scheduling; when the outer one won it rejected while the socket was still bound. That timer was never cleared, keeping a login that succeeded in a second armed for the remaining 29.
16
+ - **A hung browser launcher could delay the timeout and the release.** The launch is no longer awaited on the critical path.
17
+
18
+ ### Changed
19
+ - All three flows now run inside a factory scope implementing `CallbackServerFactory` from `@mcp-abap-adt/interfaces`. The port is released when the scope ends — by success, error, timeout, cancellation, or a body that throws — instead of when a promise happens to settle, and the scope settles only once the socket is free. `timeoutMs` is mandatory and an `AbortSignal` is honoured before, during and after the bind.
20
+ - Shutdown is bounded: stop accepting, wait up to 500 ms for `close`, then force. A timeout can no longer hang on its own cleanup.
21
+ - Requires `@mcp-abap-adt/interfaces` `^11.4.0` (was `^2.3.0`).
22
+ - **`engines.node` is now `>=18.2.0`** (was `>=18.0.0`), for `server.closeAllConnections()`.
23
+ - `browserAuth.ts` is 395 lines, down from 790: the HTTP server, six separate close sites, two timers and the process-signal handlers are gone from it.
24
+
25
+ ### Removed
26
+ - **The callback server no longer installs `SIGTERM` / `SIGINT` / `SIGHUP` / `exit` handlers.** A terminating process releases its listening sockets to the OS regardless — measured at 0-1 ms after the process disappears — and these were part of the cleanup tangle being removed. A client that kills the process mid-login still gets its port back.
27
+ - **The manual paste channel no longer retries a bad code.** It used to re-render the form when the exchange failed; the code is now exchanged after the scope closes, so a wrong or expired paste ends the attempt. That retry loop was the only reason the socket outlived the code.
28
+
29
+ ### Notes
30
+ - Public API is unchanged: `AuthorizationCodeProviderConfig` keeps `browser` and `redirectPort`, still defaulting to 3001.
31
+ - The three factories stay internal. Exposing them so a consumer can supply its own callback receiver is issue #11 and is not part of this release.
32
+ - A callback carrying neither `code` nor `error` still ends the login with an error. Only the leaked socket was fixed, not the termination.
33
+
34
+ ## [1.1.0] - 2026-06-02
35
+
36
+ ### Added
37
+ - `extractCode(input)` helper: pulls an OAuth2 authorization code out of a bare
38
+ code, a `code=...` string, or a full redirected URL.
39
+ - Manual paste authentication for `none`/`headless` browser modes, so login can
40
+ complete when the automatic `localhost` callback can't reach the process
41
+ (browser on another machine / container / SSH). Three racing channels, first
42
+ one wins:
43
+ - the existing automatic `GET /callback?code=...` redirect;
44
+ - an HTML paste form on the same callback server (`GET /` + `GET /submit`);
45
+ - stdin paste, only when `process.stdin.isTTY` (never consumed under stdio
46
+ RPC transports).
47
+
48
+ ### Fixed
49
+ - `none`/`headless` mode now prints the authorization URL even when no logger is
50
+ supplied. The prompt previously rode on `log?.info(...)` and was silently
51
+ dropped without a logger; it now falls back to `stderr` (never stdout, to keep
52
+ stdio RPC transports uncorrupted).
53
+
10
54
  ## [1.0.5] - 2026-02-12
11
55
 
12
56
  ### Fixed
package/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # @mcp-abap-adt/auth-providers
2
+ [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
2
3
 
3
4
  Token providers for MCP ABAP ADT auth-broker.
4
5
 
@@ -99,6 +100,36 @@ const clientCredsBroker = new AuthBroker({
99
100
  }, 'none');
100
101
  ```
101
102
 
103
+ ### Browser modes (`AuthorizationCodeProvider`)
104
+
105
+ The `browser` option controls how the authorization URL is opened:
106
+
107
+ | Mode | Behaviour |
108
+ |------|-----------|
109
+ | `system` (default) | Open the OS default browser |
110
+ | `chrome` / `edge` / `firefox` | Open a specific browser |
111
+ | `auto` | Try to open a browser; on failure, print the URL and wait |
112
+ | `none` / `headless` | Do **not** open a browser — print the URL and wait for the code (SSH / remote / containers) |
113
+
114
+ In `none`/`headless` mode the authorization URL is always shown, **even when no
115
+ logger is supplied** (it falls back to `stderr`, never stdout, so stdio-based
116
+ RPC transports are not corrupted).
117
+
118
+ #### Manual paste (none / headless)
119
+
120
+ Login can complete through any of three channels — whichever finishes first wins:
121
+
122
+ 1. **Automatic callback** — `GET /callback?code=...` on `http://localhost:<redirectPort>`.
123
+ Works when the browser is on the same machine as the process.
124
+ 2. **Paste form** — open `http://<host>:<redirectPort>/` and paste the code (or
125
+ the whole redirected URL). Works when the browser is on a *different* machine,
126
+ since the callback server listens on all interfaces.
127
+ 3. **Terminal paste** — paste the code on stdin and press Enter. Only active when
128
+ `process.stdin.isTTY` (stdin is never consumed under a stdio RPC transport).
129
+
130
+ The exported `extractCode(input)` helper accepts a bare code, `code=...`, or a
131
+ full redirected URL.
132
+
102
133
  ### SSO Providers
103
134
 
104
135
  This package also includes SSO providers for OIDC and SAML2, plus a small factory for DI-friendly creation.
@@ -310,11 +341,15 @@ const result = await provider.getTokens();
310
341
  // result.refreshToken is undefined (client_credentials doesn't provide refresh tokens)
311
342
  ```
312
343
 
313
- **Note**: The `browserAuthPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error will be thrown. You must specify a different port or free the port before starting authentication. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
344
+ **Note**: The `redirectPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error is thrown; specify a different port or free it before starting authentication.
345
+
346
+ **Port lifetime**: the callback port is held for the login and nothing longer. It is bound when the login window opens and released when the login ends — by success, by failure, by timeout, or by cancellation — and the returned promise settles only after the socket is actually free. An error therefore always means the port is already available, and the port is released *before* the authorization code is exchanged for a token, so a slow identity provider cannot hold it either.
347
+
348
+ **Timeout**: an interactive login waits 30 seconds for its callback. This applies to the browser, OIDC and SAML flows alike; before 1.2.0 the OIDC and SAML flows had no timeout at all, so an abandoned login held its port for the life of the process.
314
349
 
315
- **Timeout**: Browser authentication has a 30-second timeout to prevent blocking the consumer. If authentication is not completed within 30 seconds, the operation will fail with a timeout error. This prevents the provider from hanging indefinitely when the user doesn't complete authentication.
350
+ **Cancellation**: `ICallbackServerOptions.signal` accepts an `AbortSignal`, honoured before the bind, during it, and while waiting.
316
351
 
317
- **Process Termination Handling**: The OAuth callback server registers cleanup handlers for `SIGTERM`, `SIGINT`, `SIGHUP`, and `exit` signals. This ensures ports are properly freed even when MCP clients (like Cline) terminate the process before authentication completes. This is especially important for stdio servers where the client may kill the process at any time. On Windows, the `SIGBREAK` signal (Ctrl+Break) is also handled.
352
+ **Process termination**: the callback server no longer installs its own `SIGTERM` / `SIGINT` / `SIGHUP` / `exit` handlers. A terminating process releases its listening sockets to the operating system anyway — measured at 0-1 ms after the process disappears — and the handlers were part of the cleanup tangle this release removes. If a client kills the process mid-login, the port comes back with the process.
318
353
 
319
354
  **Cross-Platform Browser Support**: The browser authentication works across Linux, macOS, and Windows:
320
355
  - **Linux**: Automatically sets `DISPLAY=:0` if neither `DISPLAY` nor `WAYLAND_DISPLAY` environment variables are set. Supports multiple browser executable names (`google-chrome`, `google-chrome-stable`, `chromium`, `chromium-browser` for Chrome; `firefox`, `firefox-esr` for Firefox).
@@ -5,6 +5,18 @@ import type { IAuthorizationConfig, ILogger } from '@mcp-abap-adt/interfaces';
5
5
  type BrowserAuthConfig = IAuthorizationConfig & {
6
6
  authorizationUrl?: string;
7
7
  };
8
+ /**
9
+ * Extract an OAuth2 authorization code from arbitrary pasted input.
10
+ *
11
+ * Accepts:
12
+ * - a bare code: `abc123`
13
+ * - `code=abc123`
14
+ * - a full redirected URL: `http://localhost:7779/callback?code=abc123&state=...`
15
+ *
16
+ * Returns the decoded code, or null if nothing usable was found.
17
+ * @internal - Exported for testing and for manual-paste flows.
18
+ */
19
+ export declare function extractCode(input: string): string | null;
8
20
  /**
9
21
  * Exchange authorization code for tokens
10
22
  * @internal - Exported for testing
@@ -14,15 +26,14 @@ export declare function exchangeCodeForToken(authConfig: IAuthorizationConfig, c
14
26
  refreshToken?: string;
15
27
  }>;
16
28
  /**
17
- * Start browser authentication flow
18
- * @param authConfig Authorization configuration with UAA credentials
19
- * @param browser Browser name (chrome, edge, firefox, system, none)
20
- * @param logger Optional logger instance. If not provided, uses default logger.
21
- * @param port Port for OAuth callback server (default: 3001)
22
- * @returns Promise that resolves to tokens
23
- * @internal - Internal function, not exported from package
29
+ * Interactive browser login for the UAA authorization-code flow.
30
+ *
31
+ * The callback socket is owned by `withBrowserCallbackServer`: it is released
32
+ * when the scope ends, whatever ends it, and before the code is exchanged — so
33
+ * a slow UAA cannot hold the port, and a settled promise always means the port
34
+ * is free.
24
35
  */
25
- export declare function startBrowserAuth(authConfig: BrowserAuthConfig, browser?: string, logger?: ILogger, port?: number): Promise<{
36
+ export declare function startBrowserAuth(authConfig: BrowserAuthConfig, browser?: string, logger?: ILogger, port?: number, timeoutMs?: number): Promise<{
26
37
  accessToken: string;
27
38
  refreshToken?: string;
28
39
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA8BF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqjBzD"}
1
+ {"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAYF;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBxD;AAoBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA8JD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,EACnB,SAAS,GAAE,MAAkB,GAC5B,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2FzD"}