@mcp-abap-adt/auth-providers 4.1.0 → 4.1.2

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,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.2] - 2026-09-26
11
+
12
+ ### Security
13
+
14
+ - **No token the provider holds reaches a log line.** Every provider logged tokens through
15
+ `BaseTokenProvider.formatToken`. That function returned a token of 50
16
+ characters or fewer whole, and a longer one's first and last 25 characters.
17
+ UAA and XSUAA refresh tokens are opaque and about 34 characters, so they were
18
+ logged in full, at `info`, by `AuthorizationCodeProvider` on creation, on
19
+ refresh (old and new) and by `BaseTokenProvider` on every token update. Access
20
+ tokens leaked 50 characters. A log line now carries only
21
+ `<redacted, N chars>`. Anyone who shipped logs from an earlier version at
22
+ `info` or `debug` should treat the refresh tokens in them as exposed, and
23
+ revoke them.
24
+ - **No token endpoint's error body reaches a log line or an error message
25
+ whole.** The SAML exchange and SAML refresh logged the whole error body, and
26
+ `refreshJwtToken` and `getTokenWithClientCredentials` serialised it into the
27
+ thrown `Error`'s message, which `BaseTokenProvider` then logged. Only the
28
+ body's `error` and `error_description` are kept now, each quoted and capped:
29
+ `error` at 64 characters, and `error_description` at 512, so a server's
30
+ diagnosis survives. Both are redacted first. Every secret the request itself
31
+ sent (refresh token, assertion, client secret), however short, whether it
32
+ comes back as sent or form- or percent-encoded, and anything shaped like a JWT
33
+ becomes `<redacted>`. A server that echoes one of those, in the body or in
34
+ its description, no longer leaks it. **Limit:** an opaque token the request
35
+ did not send cannot be told from an ordinary identifier, so if a server writes
36
+ a new one into `error_description` it passes through, capped at 512
37
+ characters. Scrubbing every long string would also erase the IDs that make a
38
+ refusal diagnosable.
39
+
40
+ ## [4.1.1] - 2026-09-26
41
+
42
+ ### Changed
43
+
44
+ - The `signedNode` refusal for an assertion inside a `ds:Signature` now reads
45
+ "…inside a ds:Signature, which is never accepted". The 4.1.0 wording, "where
46
+ no signature covers it", was untrue when a signed Response also covers that
47
+ signature. `check` and the fragment `inside a ds:Signature` are unchanged.
48
+
49
+ ### Fixed
50
+
51
+ - The tarball no longer ships `dist/__tests__/…`. Test helpers and stand
52
+ fixtures were built into `dist` and published with 4.0.0 and 4.1.0. The build
53
+ now uses `tsconfig.build.json`, which leaves `src/__tests__` out, and the
54
+ tarball drops from 140 to 128 files. Nothing a consumer imports changes.
55
+
56
+ ### Development
57
+
58
+ - `@mcp-abap-adt/auth-stores` devDependency `^1.2.1`, which depends on
59
+ `@mcp-abap-adt/interfaces-auth` `^2.0.1`. The development tree now holds a
60
+ single `interfaces-auth` 2.0.1 instead of a second, nested 1.2.0. Nothing
61
+ published changes.
62
+ - The live authorization tests take tokens from any session file:
63
+ `MCP_ABAP_ADT_SESSION_FILE`, or `session_path` in `tests/test-config.yaml`,
64
+ or the stores' folder `<destination_dir>/sessions/<destination>.env`. The
65
+ folder defaults to `~/.config/mcp-abap-adt` on Unix and
66
+ `Documents/mcp-abap-adt` on Windows. Browser logins run only with
67
+ `interactive_login: true` or `MCP_ABAP_ADT_INTERACTIVE=1`, so a default
68
+ `npm test` never waits for one.
69
+
10
70
  ## [4.1.0] - 2026-09-26
11
71
 
12
72
  Three refusals get stricter; no export, configuration field or error class
@@ -54,7 +114,9 @@ changes. The README's *Upgrading from 4.0 to 4.1* lists what now fails.
54
114
  - `idpInitiated: true` with `authnRequestId` is refused when the provider is
55
115
  constructed — a `ValidationError` with `missingFields: ['idpInitiated']` —
56
116
  not after `authorize()` returns, so before the user has been through the
57
- browser.
117
+ browser. A `Saml2BearerProvider` seeded with a `refreshToken` did work in 4.0
118
+ until that token lapsed, since a refresh never reaches the strategy; it now
119
+ fails at construction.
58
120
  - A malformed `Signature` whose `loadSignature` throws something other than
59
121
  an `Error` is refused at `signature` quoting what was thrown. Before, the
60
122
  refusal carried an empty message, and a thrown `null` or `undefined`
package/README.md CHANGED
@@ -727,7 +727,7 @@ value.
727
727
  | `signedNode` | `the signature does not cover the samlp:Response this validator requires` |
728
728
  | `signedNode` | `the signature does not cover the saml:Assertion this validator requires` |
729
729
  | `signedNode` | `the document carries an Assertion or EncryptedAssertion, SAML 2.0 or 1.x, outside the one the signature covers` |
730
- | `signedNode` | `the document carries an Assertion or EncryptedAssertion inside a ds:Signature, where no signature covers it` |
730
+ | `signedNode` | `the document carries an Assertion or EncryptedAssertion inside a ds:Signature, which is never accepted` |
731
731
  | `status` | `the response carries no samlp:Status` |
732
732
  | `status` | `the response carries <n> samlp:Status; exactly one is allowed` |
733
733
  | `status` | `the samlp:Status carries no samlp:StatusCode` |
@@ -1768,7 +1768,7 @@ Example output:
1768
1768
  ```
1769
1769
 
1770
1770
  **Logging Features**:
1771
- - **Token Formatting**: Tokens are logged in truncated format (start...end) for security
1771
+ - **No tokens in logs**: a token the provider holds or sent is never logged, not even in part. A log line carries only `<redacted, N chars>` (since 4.1.2; earlier versions logged a short refresh token whole). A token endpoint's error body contributes only `error` and `error_description`, with the request's secrets and anything shaped like a JWT redacted. A new opaque token that a server writes into `error_description` cannot be recognised and passes through, capped at 512 characters.
1772
1772
  - **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
1773
1773
  - **Browser Information**: Logs browser type and authorization URL for debugging
1774
1774
  - **Token Lifecycle**: Detailed logging of token acquisition, validation, and refresh operations
@@ -1 +1 @@
1
- {"version":3,"file":"clientCredentialsAuth.d.ts","sourceRoot":"","sources":["../../src/auth/clientCredentialsAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAmDlC"}
1
+ {"version":3,"file":"clientCredentialsAuth.d.ts","sourceRoot":"","sources":["../../src/auth/clientCredentialsAuth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CACjD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAmDlC"}
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getTokenWithClientCredentials = getTokenWithClientCredentials;
13
13
  const axios_1 = __importDefault(require("axios"));
14
+ const oauthErrorBody_1 = require("./oauthErrorBody");
14
15
  /**
15
16
  * Get access token using client_credentials grant type
16
17
  * @param uaaUrl UAA URL (e.g., https://your-account.authentication.eu10.hana.ondemand.com)
@@ -54,7 +55,7 @@ async function getTokenWithClientCredentials(uaaUrl, clientId, clientSecret) {
54
55
  'status' in error.response &&
55
56
  'data' in error.response) {
56
57
  const axiosError = error;
57
- throw new Error(`Client credentials authentication failed (${axiosError.response.status}): ${JSON.stringify(axiosError.response.data)}`);
58
+ throw new Error(`Client credentials authentication failed (${axiosError.response.status}): ${(0, oauthErrorBody_1.describeOAuthErrorBody)(axiosError.response.data, [clientSecret])}`);
58
59
  }
59
60
  else {
60
61
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What of an OAuth error response may reach a log line or an error message.
3
+ *
4
+ * Only RFC 6749 §5.2's `error` and `error_description`, each quoted and capped.
5
+ * A token endpoint's body is otherwise untrusted: a misbehaving server can echo
6
+ * the request or return tokens in it, so it is never serialised whole.
7
+ * `error_description` is the server's human-readable diagnosis, so it keeps a
8
+ * cap long enough to stay useful (UAA explains assertion refusals in it).
9
+ */
10
+ /**
11
+ * @param knownSecrets what the request sent that must never come back out:
12
+ * the refresh token, the assertion, the client secret.
13
+ */
14
+ export declare function describeOAuthErrorBody(data: unknown, knownSecrets?: readonly (string | undefined)[]): string;
15
+ //# sourceMappingURL=oauthErrorBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthErrorBody.d.ts","sourceRoot":"","sources":["../../src/auth/oauthErrorBody.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsCH;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,EACb,YAAY,GAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAO,GACjD,MAAM,CAaR"}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * What of an OAuth error response may reach a log line or an error message.
4
+ *
5
+ * Only RFC 6749 §5.2's `error` and `error_description`, each quoted and capped.
6
+ * A token endpoint's body is otherwise untrusted: a misbehaving server can echo
7
+ * the request or return tokens in it, so it is never serialised whole.
8
+ * `error_description` is the server's human-readable diagnosis, so it keeps a
9
+ * cap long enough to stay useful (UAA explains assertion refusals in it).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.describeOAuthErrorBody = describeOAuthErrorBody;
13
+ const ERROR_CAP = 64;
14
+ const DESCRIPTION_CAP = 512;
15
+ const quote = (value, cap) => JSON.stringify(value.length > cap ? `${value.slice(0, cap)}…` : value);
16
+ /** Anything shaped like a JWT: three base64url segments, the first a header. */
17
+ const JWT_SHAPE = /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g;
18
+ /**
19
+ * Removes what a server might echo back: every secret the request itself
20
+ * sent (a refresh token, an assertion, a client secret), in each form it may
21
+ * come back in, and any JWT.
22
+ * Every known secret is redacted, however short: nothing guarantees a client
23
+ * secret is long, and dropping a matching word from a diagnosis is the lesser
24
+ * harm.
25
+ */
26
+ function redact(text, secrets) {
27
+ let out = text;
28
+ for (const secret of secrets) {
29
+ if (!secret)
30
+ continue;
31
+ // As sent, as the request body encoded it (URLSearchParams: + / = become
32
+ // %2B %2F %3D), and percent-encoded: a server may echo any of these.
33
+ const forms = new Set([
34
+ secret,
35
+ new URLSearchParams({ s: secret }).toString().slice(2),
36
+ encodeURIComponent(secret),
37
+ ]);
38
+ for (const form of forms)
39
+ out = out.split(form).join('<redacted>');
40
+ }
41
+ return out.replace(JWT_SHAPE, '<redacted jwt>');
42
+ }
43
+ /**
44
+ * @param knownSecrets what the request sent that must never come back out:
45
+ * the refresh token, the assertion, the client secret.
46
+ */
47
+ function describeOAuthErrorBody(data, knownSecrets = []) {
48
+ if (!data || typeof data !== 'object')
49
+ return 'no error given';
50
+ const { error, error_description } = data;
51
+ const parts = [];
52
+ if (typeof error === 'string')
53
+ parts.push(quote(redact(error, knownSecrets), ERROR_CAP));
54
+ if (typeof error_description === 'string') {
55
+ parts.push(quote(redact(error_description, knownSecrets), DESCRIPTION_CAP));
56
+ }
57
+ return parts.length > 0 ? parts.join(': ') : 'no error given';
58
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CAwCrC"}
1
+ {"version":3,"file":"saml2TokenExchange.d.ts","sourceRoot":"","sources":["../../src/auth/saml2TokenExchange.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAI9D,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CA2CrC;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,0BAA0B,CAAC,CA2CrC"}
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.exchangeSamlAssertion = exchangeSamlAssertion;
10
10
  exports.refreshSamlBearerToken = refreshSamlBearerToken;
11
11
  const axios_1 = __importDefault(require("axios"));
12
+ const oauthErrorBody_1 = require("./oauthErrorBody");
12
13
  function toBasicAuth(clientId, clientSecret) {
13
14
  return Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
14
15
  }
@@ -34,7 +35,10 @@ async function exchangeSamlAssertion(samlResponse, tokenUrl, clientId, clientSec
34
35
  if (axios_1.default.isAxiosError(error)) {
35
36
  logger?.error('[SAML] Token exchange failed', {
36
37
  status: error.response?.status,
37
- data: error.response?.data,
38
+ error: (0, oauthErrorBody_1.describeOAuthErrorBody)(error.response?.data, [
39
+ samlResponse,
40
+ clientSecret,
41
+ ]),
38
42
  });
39
43
  }
40
44
  throw error;
@@ -79,7 +83,10 @@ async function refreshSamlBearerToken(refreshToken, tokenUrl, clientId, clientSe
79
83
  if (axios_1.default.isAxiosError(error)) {
80
84
  logger?.error('[SAML] Token refresh failed', {
81
85
  status: error.response?.status,
82
- data: error.response?.data,
86
+ error: (0, oauthErrorBody_1.describeOAuthErrorBody)(error.response?.data, [
87
+ refreshToken,
88
+ clientSecret,
89
+ ]),
83
90
  });
84
91
  }
85
92
  throw error;
@@ -1 +1 @@
1
- {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../../src/auth/tokenRefresher.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAqD7B"}
1
+ {"version":3,"file":"tokenRefresher.d.ts","sourceRoot":"","sources":["../../src/auth/tokenRefresher.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAqD7B"}
@@ -8,6 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.refreshJwtToken = refreshJwtToken;
10
10
  const axios_1 = __importDefault(require("axios"));
11
+ const oauthErrorBody_1 = require("./oauthErrorBody");
11
12
  /**
12
13
  * Refreshes the access token using refresh token
13
14
  * @param refreshToken Refresh token
@@ -53,7 +54,7 @@ async function refreshJwtToken(refreshToken, uaaUrl, clientId, clientSecret) {
53
54
  'status' in error.response &&
54
55
  'data' in error.response) {
55
56
  const axiosError = error;
56
- throw new Error(`Token refresh failed (${axiosError.response.status}): ${JSON.stringify(axiosError.response.data)}`);
57
+ throw new Error(`Token refresh failed (${axiosError.response.status}): ${(0, oauthErrorBody_1.describeOAuthErrorBody)(axiosError.response.data, [refreshToken, clientSecret])}`);
57
58
  }
58
59
  else {
59
60
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -31,7 +31,9 @@ export declare abstract class BaseTokenProvider implements ITokenProvider {
31
31
  */
32
32
  protected formatExpirationDate(timestamp: number): string;
33
33
  /**
34
- * Format token for logging (start...end)
34
+ * What a log line may say about a token: that it is there, and its length.
35
+ * Never any of its characters — a UAA refresh token is about 34 characters,
36
+ * so even "the first and last 25" is the whole secret.
35
37
  */
36
38
  protected formatToken(token?: string): string | undefined;
37
39
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWzD;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMzD;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO;IAyBjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAExD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAE1D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,eAAe;IAEjD;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAyElC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoC3E;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IA0BlD;;;;OAIG;IACH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA0BnE;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAShE"}
1
+ {"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAE9D;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWzD;;;;OAIG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKzD;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO;IAyBjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAExD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAE1D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,eAAe;IAEjD;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAyElC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAoC3E;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IA0BlD;;;;OAIG;IACH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA0BnE;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAShE"}
@@ -41,14 +41,14 @@ class BaseTokenProvider {
41
41
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
42
42
  }
43
43
  /**
44
- * Format token for logging (start...end)
44
+ * What a log line may say about a token: that it is there, and its length.
45
+ * Never any of its characters — a UAA refresh token is about 34 characters,
46
+ * so even "the first and last 25" is the whole secret.
45
47
  */
46
48
  formatToken(token) {
47
49
  if (!token)
48
50
  return undefined;
49
- if (token.length <= 50)
50
- return token;
51
- return `${token.substring(0, 25)}...${token.substring(token.length - 25)}`;
51
+ return `<redacted, ${token.length} chars>`;
52
52
  }
53
53
  /**
54
54
  * Check if current token is valid (not expired)
@@ -173,7 +173,7 @@ function createValidator(require, options) {
173
173
  // whose subtree an enveloped signature leaves unsigned.
174
174
  const place = placeOfAssertions(doc, assertion);
175
175
  if (place === 'inSignature') {
176
- return fail('signedNode', 'the document carries an Assertion or EncryptedAssertion inside a ds:Signature, where no signature covers it');
176
+ return fail('signedNode', 'the document carries an Assertion or EncryptedAssertion inside a ds:Signature, which is never accepted');
177
177
  }
178
178
  if (place === 'outside') {
179
179
  return fail('signedNode', 'the document carries an Assertion or EncryptedAssertion, SAML 2.0 or 1.x, outside the one the signature covers');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-providers",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Token providers for MCP ABAP ADT auth-broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "scripts": {
41
41
  "chrono": "./tools/version-stats.sh",
42
- "clean": "rm -rf dist tsconfig.tsbuildinfo",
42
+ "clean": "rm -rf dist tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo",
43
43
  "lint": "npx biome check --write src",
44
44
  "lint:check": "npx biome check src",
45
45
  "format": "npx biome format --write src",
46
- "build": "npm run --silent clean && npx biome check src --diagnostic-level=error && npx tsc -p tsconfig.json",
47
- "build:fast": "npx tsc -p tsconfig.json",
46
+ "build": "npm run --silent clean && npx biome check src --diagnostic-level=error && npx tsc -p tsconfig.build.json",
47
+ "build:fast": "npx tsc -p tsconfig.build.json",
48
48
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
49
49
  "test:check": "npx tsc --noEmit",
50
50
  "prepublishOnly": "npm run build",
@@ -70,7 +70,7 @@
70
70
  "@biomejs/biome": "^2.3.14",
71
71
  "@jest/globals": "^30.2.0",
72
72
  "@mcp-abap-adt/auth-mocks": "^0.3.0",
73
- "@mcp-abap-adt/auth-stores": "^1.0.1",
73
+ "@mcp-abap-adt/auth-stores": "^1.2.1",
74
74
  "@mcp-abap-adt/logger": "^0.4.0",
75
75
  "@types/express": "^5.0.5",
76
76
  "@types/jest": "^30.0.0",
@@ -1,46 +0,0 @@
1
- /**
2
- * Configuration helpers for auth-providers tests
3
- * Loads test configuration from test-config.yaml
4
- */
5
- export interface TestConfig {
6
- destination?: string;
7
- destination_dir?: string;
8
- service_key_path?: string;
9
- session_path?: string;
10
- }
11
- /**
12
- * Load test configuration from YAML
13
- * Uses test-config.yaml from tests/ directory
14
- */
15
- export declare function loadTestConfig(): TestConfig;
16
- /**
17
- * Check if test config has real values (not placeholders)
18
- */
19
- export declare function hasRealConfigValue(config?: TestConfig): boolean;
20
- /**
21
- * Get destination from config
22
- */
23
- export declare function getDestination(config?: TestConfig): string | null;
24
- /**
25
- * Get service keys directory from config
26
- * Uses base_dir/service-keys or default platform path
27
- */
28
- export declare function getServiceKeysDir(config?: TestConfig): string;
29
- /**
30
- * Get sessions directory from config
31
- * Uses base_dir/sessions or default platform path
32
- */
33
- export declare function getSessionsDir(config?: TestConfig): string;
34
- /**
35
- * Get service key file path
36
- * Returns full path to service key file
37
- */
38
- export declare function getServiceKeyPath(config?: TestConfig): string | null;
39
- /**
40
- * Get session file path
41
- * Returns full path to session file
42
- */
43
- export declare function getSessionPath(config?: TestConfig): string | null;
44
- export declare function getAbapDestination(config?: TestConfig): string | null;
45
- export declare function hasRealConfig(config?: TestConfig, _section?: 'abap' | 'xsuaa'): boolean;
46
- //# sourceMappingURL=configHelpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"configHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/configHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAyD3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAO/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAGjE;AA+BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa1D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcjE;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAErE;AAED,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,UAAU,EACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAC1B,OAAO,CAET"}
@@ -1,232 +0,0 @@
1
- "use strict";
2
- /**
3
- * Configuration helpers for auth-providers tests
4
- * Loads test configuration from test-config.yaml
5
- */
6
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
- if (k2 === undefined) k2 = k;
8
- var desc = Object.getOwnPropertyDescriptor(m, k);
9
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
- desc = { enumerable: true, get: function() { return m[k]; } };
11
- }
12
- Object.defineProperty(o, k2, desc);
13
- }) : (function(o, m, k, k2) {
14
- if (k2 === undefined) k2 = k;
15
- o[k2] = m[k];
16
- }));
17
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
- Object.defineProperty(o, "default", { enumerable: true, value: v });
19
- }) : function(o, v) {
20
- o["default"] = v;
21
- });
22
- var __importStar = (this && this.__importStar) || (function () {
23
- var ownKeys = function(o) {
24
- ownKeys = Object.getOwnPropertyNames || function (o) {
25
- var ar = [];
26
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
- return ar;
28
- };
29
- return ownKeys(o);
30
- };
31
- return function (mod) {
32
- if (mod && mod.__esModule) return mod;
33
- var result = {};
34
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
- __setModuleDefault(result, mod);
36
- return result;
37
- };
38
- })();
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.loadTestConfig = loadTestConfig;
41
- exports.hasRealConfigValue = hasRealConfigValue;
42
- exports.getDestination = getDestination;
43
- exports.getServiceKeysDir = getServiceKeysDir;
44
- exports.getSessionsDir = getSessionsDir;
45
- exports.getServiceKeyPath = getServiceKeyPath;
46
- exports.getSessionPath = getSessionPath;
47
- exports.getAbapDestination = getAbapDestination;
48
- exports.hasRealConfig = hasRealConfig;
49
- const fs = __importStar(require("node:fs"));
50
- const path = __importStar(require("node:path"));
51
- const yaml = __importStar(require("js-yaml"));
52
- let cachedConfig = null;
53
- /**
54
- * Find project root directory by looking for package.json
55
- */
56
- function findProjectRoot() {
57
- let currentDir = __dirname;
58
- while (currentDir !== path.dirname(currentDir)) {
59
- const packageJsonPath = path.join(currentDir, 'package.json');
60
- if (fs.existsSync(packageJsonPath)) {
61
- return currentDir;
62
- }
63
- currentDir = path.dirname(currentDir);
64
- }
65
- // Fallback to process.cwd() if package.json not found
66
- return process.cwd();
67
- }
68
- /**
69
- * Load test configuration from YAML
70
- * Uses test-config.yaml from tests/ directory
71
- */
72
- function loadTestConfig() {
73
- if (cachedConfig) {
74
- return cachedConfig;
75
- }
76
- // Find project root and load from tests/test-config.yaml
77
- const projectRoot = findProjectRoot();
78
- const configPath = path.resolve(projectRoot, 'tests', 'test-config.yaml');
79
- const templatePath = path.resolve(projectRoot, 'tests', 'test-config.yaml.template');
80
- if (process.env.TEST_VERBOSE) {
81
- console.log(`[configHelpers] Project root: ${projectRoot}`);
82
- console.log(`[configHelpers] Config path: ${configPath}`);
83
- console.log(`[configHelpers] Config exists: ${fs.existsSync(configPath)}`);
84
- }
85
- if (fs.existsSync(configPath)) {
86
- try {
87
- const configContent = fs.readFileSync(configPath, 'utf8');
88
- cachedConfig = yaml.load(configContent) || {};
89
- if (process.env.TEST_VERBOSE) {
90
- console.log(`[configHelpers] Loaded config:`, JSON.stringify(cachedConfig, null, 2));
91
- }
92
- return cachedConfig;
93
- }
94
- catch (error) {
95
- console.warn(`Failed to load test config from ${configPath}:`, error);
96
- return {};
97
- }
98
- }
99
- if (fs.existsSync(templatePath)) {
100
- console.warn('⚠️ tests/test-config.yaml not found. Using template (all integration tests will be disabled).');
101
- try {
102
- const templateContent = fs.readFileSync(templatePath, 'utf8');
103
- cachedConfig = yaml.load(templateContent) || {};
104
- return cachedConfig;
105
- }
106
- catch (error) {
107
- console.warn(`Failed to load test config template from ${templatePath}:`, error);
108
- return {};
109
- }
110
- }
111
- console.warn('⚠️ Test configuration files not found.');
112
- console.warn('Please create tests/test-config.yaml with test parameters.');
113
- return {};
114
- }
115
- /**
116
- * Check if test config has real values (not placeholders)
117
- */
118
- function hasRealConfigValue(config) {
119
- const cfg = config || loadTestConfig();
120
- if (!cfg.destination) {
121
- return false;
122
- }
123
- // Check if destination is not a placeholder
124
- return !cfg.destination.includes('<') && !cfg.destination.includes('>');
125
- }
126
- /**
127
- * Get destination from config
128
- */
129
- function getDestination(config) {
130
- const cfg = config || loadTestConfig();
131
- return cfg.destination || null;
132
- }
133
- /**
134
- * Get default destination directory based on platform
135
- */
136
- function getDefaultDestinationDir() {
137
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
138
- if (process.platform === 'win32') {
139
- return path.join(homeDir, 'Documents', 'mcp-abap-adt');
140
- }
141
- return path.join(homeDir, '.config', 'mcp-abap-adt');
142
- }
143
- /**
144
- * Get destination directory from config or use default
145
- */
146
- function getDestinationDir(config) {
147
- const cfg = config || loadTestConfig();
148
- if (cfg.destination_dir) {
149
- // Expand ~ to home directory
150
- if (cfg.destination_dir.startsWith('~')) {
151
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
152
- return cfg.destination_dir.replace('~', homeDir);
153
- }
154
- return cfg.destination_dir;
155
- }
156
- return getDefaultDestinationDir();
157
- }
158
- /**
159
- * Get service keys directory from config
160
- * Uses base_dir/service-keys or default platform path
161
- */
162
- function getServiceKeysDir(config) {
163
- const cfg = config || loadTestConfig();
164
- // If service_key_path is specified, return its directory
165
- if (cfg.service_key_path) {
166
- const projectRoot = findProjectRoot();
167
- const fullPath = path.resolve(projectRoot, cfg.service_key_path);
168
- return path.dirname(fullPath);
169
- }
170
- // Use destination_dir/service-keys
171
- const destinationDir = getDestinationDir(cfg);
172
- return path.join(destinationDir, 'service-keys');
173
- }
174
- /**
175
- * Get sessions directory from config
176
- * Uses base_dir/sessions or default platform path
177
- */
178
- function getSessionsDir(config) {
179
- const cfg = config || loadTestConfig();
180
- // If session_path is specified, return its directory
181
- if (cfg.session_path) {
182
- const projectRoot = findProjectRoot();
183
- const fullPath = path.resolve(projectRoot, cfg.session_path);
184
- return path.dirname(fullPath);
185
- }
186
- // Use destination_dir/sessions
187
- const destinationDir = getDestinationDir(cfg);
188
- return path.join(destinationDir, 'sessions');
189
- }
190
- /**
191
- * Get service key file path
192
- * Returns full path to service key file
193
- */
194
- function getServiceKeyPath(config) {
195
- const cfg = config || loadTestConfig();
196
- const destination = cfg.destination;
197
- if (!destination)
198
- return null;
199
- // If service_key_path is specified, use it
200
- if (cfg.service_key_path) {
201
- const projectRoot = findProjectRoot();
202
- return path.resolve(projectRoot, cfg.service_key_path);
203
- }
204
- // Construct from directory + destination
205
- const serviceKeysDir = getServiceKeysDir(cfg);
206
- return path.join(serviceKeysDir, `${destination}.json`);
207
- }
208
- /**
209
- * Get session file path
210
- * Returns full path to session file
211
- */
212
- function getSessionPath(config) {
213
- const cfg = config || loadTestConfig();
214
- const destination = cfg.destination;
215
- if (!destination)
216
- return null;
217
- // If session_path is specified, use it
218
- if (cfg.session_path) {
219
- const projectRoot = findProjectRoot();
220
- return path.resolve(projectRoot, cfg.session_path);
221
- }
222
- // Construct from directory + destination
223
- const sessionsDir = getSessionsDir(cfg);
224
- return path.join(sessionsDir, `${destination}.env`);
225
- }
226
- // Legacy functions for backward compatibility
227
- function getAbapDestination(config) {
228
- return getDestination(config);
229
- }
230
- function hasRealConfig(config, _section) {
231
- return hasRealConfigValue(config);
232
- }
@@ -1,19 +0,0 @@
1
- export declare function getAvailablePort(): Promise<number>;
2
- export declare function canListenOnLocalhost(): Promise<boolean>;
3
- /**
4
- * Whether a login in this test could own `port` — and therefore whether
5
- * asserting that it released the port means anything.
6
- *
7
- * A release assertion is a claim about our own cleanup. When an unrelated
8
- * process already holds the port, the login fails at the probe, never binds
9
- * anything, and cannot release anything: the port is still held afterwards,
10
- * the code is behaving exactly as it should, and the assertion fails anyway.
11
- * That is a test reporting on what else is running on the machine.
12
- *
13
- * So the gate is placed *before* the login rather than the assertion being
14
- * widened until it always passes — a `toBe(true)` relaxed into "free or held"
15
- * would also pass when our own cleanup leaked, which is the one thing these
16
- * cases exist to catch.
17
- */
18
- export declare function canOwnPort(port: number, context: string): Promise<boolean>;
19
- //# sourceMappingURL=netHelpers.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"netHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/netHelpers.ts"],"names":[],"mappings":"AAEA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAcxD;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAclB"}
@@ -1,92 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.getAvailablePort = getAvailablePort;
37
- exports.canListenOnLocalhost = canListenOnLocalhost;
38
- exports.canOwnPort = canOwnPort;
39
- const net = __importStar(require("node:net"));
40
- async function getAvailablePort() {
41
- return new Promise((resolve, reject) => {
42
- const server = net.createServer();
43
- server.once('error', reject);
44
- server.listen(0, '127.0.0.1', () => {
45
- const address = server.address();
46
- if (typeof address === 'object' && address?.port) {
47
- const port = address.port;
48
- server.close(() => resolve(port));
49
- }
50
- else {
51
- server.close(() => reject(new Error('Failed to acquire a port')));
52
- }
53
- });
54
- });
55
- }
56
- async function canListenOnLocalhost() {
57
- return new Promise((resolve) => {
58
- const server = net.createServer();
59
- server.once('error', () => resolve(false));
60
- server.listen(0, '127.0.0.1', () => {
61
- server.close(() => resolve(true));
62
- });
63
- });
64
- }
65
- /**
66
- * Whether a login in this test could own `port` — and therefore whether
67
- * asserting that it released the port means anything.
68
- *
69
- * A release assertion is a claim about our own cleanup. When an unrelated
70
- * process already holds the port, the login fails at the probe, never binds
71
- * anything, and cannot release anything: the port is still held afterwards,
72
- * the code is behaving exactly as it should, and the assertion fails anyway.
73
- * That is a test reporting on what else is running on the machine.
74
- *
75
- * So the gate is placed *before* the login rather than the assertion being
76
- * widened until it always passes — a `toBe(true)` relaxed into "free or held"
77
- * would also pass when our own cleanup leaked, which is the one thing these
78
- * cases exist to catch.
79
- */
80
- async function canOwnPort(port, context) {
81
- const free = await new Promise((resolve) => {
82
- const probe = net.createServer();
83
- probe.once('error', () => resolve(false));
84
- probe.listen(port, () => probe.close(() => resolve(true)));
85
- });
86
- if (!free) {
87
- console.warn(`⚠️ Port ${port} is held by another process — skipping the port-release ` +
88
- `assertions in "${context}". A login that never bound the socket cannot ` +
89
- 'release it; the remaining assertions in this case still run.');
90
- }
91
- return free;
92
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Test logger with environment variable control
3
- * Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
4
- */
5
- import type { ILogger } from '@mcp-abap-adt/interfaces-utils';
6
- export declare function createTestLogger(prefix?: string): ILogger;
7
- //# sourceMappingURL=testLogger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testLogger.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/testLogger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAG9D,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,MAAe,GAAG,OAAO,CAwCjE"}
@@ -1,45 +0,0 @@
1
- "use strict";
2
- /**
3
- * Test logger with environment variable control
4
- * Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
5
- */
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.createTestLogger = createTestLogger;
8
- const logger_1 = require("@mcp-abap-adt/logger");
9
- function createTestLogger(prefix = 'TEST') {
10
- // Check if logging is enabled
11
- const isEnabled = () => {
12
- return (process.env.DEBUG_PROVIDER === 'true' ||
13
- process.env.DEBUG_AUTH_PROVIDERS === 'true' ||
14
- process.env.DEBUG_BROWSER_AUTH === 'true' ||
15
- process.env.DEBUG === 'true' ||
16
- process.env.DEBUG?.includes('provider') === true ||
17
- process.env.DEBUG?.includes('auth-providers') === true);
18
- };
19
- // Create DefaultLogger with appropriate log level
20
- // getLogLevel respects AUTH_LOG_LEVEL env var and defaults to INFO
21
- const baseLogger = new logger_1.DefaultLogger((0, logger_1.getLogLevel)());
22
- // Return wrapper that checks if logging is enabled
23
- return {
24
- debug: (message, meta) => {
25
- if (isEnabled()) {
26
- baseLogger.debug(`[${prefix}] ${message}`, meta);
27
- }
28
- },
29
- info: (message, meta) => {
30
- if (isEnabled()) {
31
- baseLogger.info(`[${prefix}] ${message}`, meta);
32
- }
33
- },
34
- warn: (message, meta) => {
35
- if (isEnabled()) {
36
- baseLogger.warn(`[${prefix}] ${message}`, meta);
37
- }
38
- },
39
- error: (message, meta) => {
40
- if (isEnabled()) {
41
- baseLogger.error(`[${prefix}] ${message}`, meta);
42
- }
43
- },
44
- };
45
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * Plays the user in an interactive login against a stand server's own login
3
- * page: follows redirects, keeps cookies, finds the form with a password
4
- * field, fills it in with every hidden field it carries (UAA's CSRF token,
5
- * Keycloak's session code), and submits it.
6
- *
7
- * Deliberately small: enough for UAA's and Keycloak's stock login pages, which
8
- * the pinned image versions keep stable. It is a test helper, not a browser.
9
- */
10
- export interface Credentials {
11
- username: string;
12
- password: string;
13
- }
14
- export declare class FormBrowser {
15
- private readonly cookies;
16
- /** GET `url` and follow redirects until `stop(url)` or a page is served. */
17
- open(url: string, stop?: (next: string) => boolean): Promise<{
18
- url: string;
19
- html?: string;
20
- }>;
21
- /**
22
- * Submit the login form on `page`, then follow redirects until `stop` says
23
- * the next location is the one the caller wants — typically the client's
24
- * redirect URI carrying the code — without requesting it.
25
- */
26
- submitLogin(page: {
27
- url: string;
28
- html?: string;
29
- }, credentials: Credentials, stop?: (next: string) => boolean): Promise<{
30
- url: string;
31
- html?: string;
32
- }>;
33
- /**
34
- * Accept a consent page ("Do you grant these access privileges?"): submit
35
- * the form that carries an `accept` button, with its hidden fields.
36
- */
37
- acceptConsent(page: {
38
- url: string;
39
- html?: string;
40
- }): Promise<{
41
- url: string;
42
- html?: string;
43
- }>;
44
- private follow;
45
- private remember;
46
- private cookieHeader;
47
- }
48
- /**
49
- * Log in through the authorization URL and return the redirect URI the server
50
- * sends the browser back to, with its `code` — not requested, since nothing
51
- * listens there.
52
- */
53
- export declare function authorizeByForm(authorizationUrl: string, redirectUri: string, credentials: Credentials): Promise<URL>;
54
- /**
55
- * Approve a device authorization the way a user would: open the verification
56
- * URI (with the user code already in it), log in, and grant access.
57
- */
58
- export declare function approveDevice(verificationUriComplete: string, credentials: Credentials): Promise<void>;
59
- /**
60
- * A SAML login at an identity provider: open the AuthnRequest URL, log in,
61
- * and take the SAMLResponse from the auto-posting form the IdP answers with —
62
- * what a browser would post to the assertion consumer service.
63
- */
64
- export declare function samlResponseByForm(authnRequestUrl: string, credentials: Credentials): Promise<{
65
- samlResponse: string;
66
- acsUrl: string;
67
- }>;
68
- //# sourceMappingURL=formLogin.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"formLogin.d.ts","sourceRoot":"","sources":["../../../../src/__tests__/integration/stand/formLogin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,4EAA4E;IACtE,IAAI,CACR,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAqB,GAC5C,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAI1C;;;;OAIG;IACG,WAAW,CACf,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EACpC,WAAW,EAAE,WAAW,EACxB,IAAI,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAqB,GAC5C,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAmB1C;;;OAGG;IACG,aAAa,CAAC,IAAI,EAAE;QACxB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;YAgC7B,MAAM;IA4BpB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,YAAY;CAGrB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,GAAG,CAAC,CAcd;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,uBAAuB,EAAE,MAAM,EAC/B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBnD"}
@@ -1,194 +0,0 @@
1
- "use strict";
2
- /**
3
- * Plays the user in an interactive login against a stand server's own login
4
- * page: follows redirects, keeps cookies, finds the form with a password
5
- * field, fills it in with every hidden field it carries (UAA's CSRF token,
6
- * Keycloak's session code), and submits it.
7
- *
8
- * Deliberately small: enough for UAA's and Keycloak's stock login pages, which
9
- * the pinned image versions keep stable. It is a test helper, not a browser.
10
- */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.FormBrowser = void 0;
13
- exports.authorizeByForm = authorizeByForm;
14
- exports.approveDevice = approveDevice;
15
- exports.samlResponseByForm = samlResponseByForm;
16
- const MAX_HOPS = 20;
17
- class FormBrowser {
18
- cookies = new Map();
19
- /** GET `url` and follow redirects until `stop(url)` or a page is served. */
20
- async open(url, stop = () => false) {
21
- return this.follow(url, { method: 'GET' }, stop);
22
- }
23
- /**
24
- * Submit the login form on `page`, then follow redirects until `stop` says
25
- * the next location is the one the caller wants — typically the client's
26
- * redirect URI carrying the code — without requesting it.
27
- */
28
- async submitLogin(page, credentials, stop = () => false) {
29
- const form = findPasswordForm(page.html ?? '');
30
- if (!form) {
31
- throw new Error(`no login form on ${page.url}`);
32
- }
33
- const body = new URLSearchParams(form.hidden);
34
- body.set(form.userField, credentials.username);
35
- body.set(form.passwordField, credentials.password);
36
- return this.follow(new URL(form.action, page.url).toString(), {
37
- method: 'POST',
38
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
39
- body: body.toString(),
40
- }, stop);
41
- }
42
- /**
43
- * Accept a consent page ("Do you grant these access privileges?"): submit
44
- * the form that carries an `accept` button, with its hidden fields.
45
- */
46
- async acceptConsent(page) {
47
- for (const match of (page.html ?? '').matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
48
- const formHtml = match[0];
49
- const inputs = [...formHtml.matchAll(/<(?:input|button)\b[^>]*>/gi)].map((m) => m[0]);
50
- const accept = inputs.find((i) => attribute(i, 'name') === 'accept');
51
- if (!accept)
52
- continue;
53
- const body = new URLSearchParams();
54
- for (const input of inputs) {
55
- const name = attribute(input, 'name');
56
- if (name && attribute(input, 'type') === 'hidden') {
57
- body.set(name, attribute(input, 'value') ?? '');
58
- }
59
- }
60
- body.set('accept', attribute(accept, 'value') ?? 'Yes');
61
- const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
62
- return this.follow(new URL(attribute(formTag, 'action') ?? '', page.url).toString(), {
63
- method: 'POST',
64
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
65
- body: body.toString(),
66
- }, () => false);
67
- }
68
- throw new Error(`no consent form on ${page.url}`);
69
- }
70
- async follow(url, init, stop) {
71
- let current = url;
72
- let request = init;
73
- for (let hop = 0; hop < MAX_HOPS; hop++) {
74
- const response = await fetch(current, {
75
- ...request,
76
- redirect: 'manual',
77
- headers: { ...(request.headers ?? {}), Cookie: this.cookieHeader() },
78
- signal: AbortSignal.timeout(15_000),
79
- });
80
- this.remember(response);
81
- const location = response.headers.get('location');
82
- if (response.status >= 300 && response.status < 400 && location) {
83
- const next = new URL(location, current).toString();
84
- if (stop(next))
85
- return { url: next };
86
- current = next;
87
- request = { method: 'GET' };
88
- continue;
89
- }
90
- return { url: current, html: await response.text() };
91
- }
92
- throw new Error(`more than ${MAX_HOPS} redirects from ${url}`);
93
- }
94
- remember(response) {
95
- for (const line of response.headers.getSetCookie()) {
96
- const [pair] = line.split(';');
97
- const eq = pair.indexOf('=');
98
- if (eq > 0) {
99
- this.cookies.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
100
- }
101
- }
102
- }
103
- cookieHeader() {
104
- return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; ');
105
- }
106
- }
107
- exports.FormBrowser = FormBrowser;
108
- /**
109
- * Log in through the authorization URL and return the redirect URI the server
110
- * sends the browser back to, with its `code` — not requested, since nothing
111
- * listens there.
112
- */
113
- async function authorizeByForm(authorizationUrl, redirectUri, credentials) {
114
- const reached = (next) => next.startsWith(redirectUri);
115
- const browser = new FormBrowser();
116
- const page = await browser.open(authorizationUrl, reached);
117
- const done = page.html === undefined
118
- ? page
119
- : await browser.submitLogin(page, credentials, reached);
120
- if (!reached(done.url)) {
121
- throw new Error(`login did not return to ${redirectUri}; ended at ${done.url}`);
122
- }
123
- return new URL(done.url);
124
- }
125
- /**
126
- * Approve a device authorization the way a user would: open the verification
127
- * URI (with the user code already in it), log in, and grant access.
128
- */
129
- async function approveDevice(verificationUriComplete, credentials) {
130
- const browser = new FormBrowser();
131
- const page = await browser.open(verificationUriComplete);
132
- const consent = await browser.submitLogin(page, credentials);
133
- await browser.acceptConsent(consent);
134
- }
135
- /**
136
- * A SAML login at an identity provider: open the AuthnRequest URL, log in,
137
- * and take the SAMLResponse from the auto-posting form the IdP answers with —
138
- * what a browser would post to the assertion consumer service.
139
- */
140
- async function samlResponseByForm(authnRequestUrl, credentials) {
141
- const browser = new FormBrowser();
142
- const page = await browser.submitLogin(await browser.open(authnRequestUrl), credentials);
143
- const html = page.html ?? '';
144
- const input = [...html.matchAll(/<input\b[^>]*>/gi)]
145
- .map((m) => m[0])
146
- .find((i) => attribute(i, 'name') === 'SAMLResponse');
147
- const samlResponse = input ? attribute(input, 'value') : undefined;
148
- const formTag = /<form\b[^>]*>/i.exec(html)?.[0] ?? '';
149
- if (!samlResponse) {
150
- throw new Error(`no SAMLResponse form on ${page.url}`);
151
- }
152
- return { samlResponse, acsUrl: attribute(formTag, 'action') ?? '' };
153
- }
154
- const decode = (value) => value
155
- .replace(/&amp;/g, '&')
156
- .replace(/&quot;/g, '"')
157
- .replace(/&#39;/g, "'")
158
- .replace(/&lt;/g, '<')
159
- .replace(/&gt;/g, '>');
160
- const attribute = (tag, name) => {
161
- const match = new RegExp(`\\s${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i').exec(tag);
162
- return match ? decode(match[2] ?? match[3] ?? '') : undefined;
163
- };
164
- function findPasswordForm(html) {
165
- for (const match of html.matchAll(/<form\b[^>]*>[\s\S]*?<\/form>/gi)) {
166
- const formHtml = match[0];
167
- const inputs = [...formHtml.matchAll(/<input\b[^>]*>/gi)].map((m) => m[0]);
168
- const password = inputs.find((i) => attribute(i, 'type') === 'password');
169
- if (!password)
170
- continue;
171
- const hidden = {};
172
- let userField = 'username';
173
- for (const input of inputs) {
174
- const type = (attribute(input, 'type') ?? 'text').toLowerCase();
175
- const name = attribute(input, 'name');
176
- if (!name)
177
- continue;
178
- if (type === 'hidden')
179
- hidden[name] = attribute(input, 'value') ?? '';
180
- if ((type === 'text' || type === 'email') &&
181
- /user|email|login/i.test(name)) {
182
- userField = name;
183
- }
184
- }
185
- const formTag = /<form\b[^>]*>/i.exec(formHtml)?.[0] ?? '';
186
- return {
187
- action: attribute(formTag, 'action') ?? '',
188
- userField,
189
- passwordField: attribute(password, 'name') ?? 'password',
190
- hidden,
191
- };
192
- }
193
- return undefined;
194
- }