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

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,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.1.3] - 2026-09-26
11
+
12
+ ### Changed
13
+
14
+ - A `bearerConfirmation` refusal naming several candidates joins them with
15
+ ` | ` instead of `; `, and ends ` | and N more`. The count reason
16
+ `carries N SubjectConfirmationData; exactly one is allowed` contains `; `
17
+ itself, so the old separator read ambiguously. `check` and every reason's
18
+ wording are unchanged.
19
+
20
+ ### Removed
21
+
22
+ - `mcp.json`, an XSUAA service key with a client secret, committed to the
23
+ repository on 2025-12-23 with the since-removed `bin/` CLIs. It was never in
24
+ the npm package. It stays in the git history, so that binding should be
25
+ rotated or deleted. `.gitignore` now keeps `mcp.json` and
26
+ `*service-key*.json` out.
27
+
28
+ ## [4.1.2] - 2026-09-26
29
+
30
+ ### Security
31
+
32
+ - **No token the provider holds reaches a log line.** Every provider logged tokens through
33
+ `BaseTokenProvider.formatToken`. That function returned a token of 50
34
+ characters or fewer whole, and a longer one's first and last 25 characters.
35
+ UAA and XSUAA refresh tokens are opaque and about 34 characters, so they were
36
+ logged in full, at `info`, by `AuthorizationCodeProvider` on creation, on
37
+ refresh (old and new) and by `BaseTokenProvider` on every token update. Access
38
+ tokens leaked 50 characters. A log line now carries only
39
+ `<redacted, N chars>`. Anyone who shipped logs from an earlier version at
40
+ `info` or `debug` should treat the refresh tokens in them as exposed, and
41
+ revoke them.
42
+ - **No token endpoint's error body reaches a log line or an error message
43
+ whole.** The SAML exchange and SAML refresh logged the whole error body, and
44
+ `refreshJwtToken` and `getTokenWithClientCredentials` serialised it into the
45
+ thrown `Error`'s message, which `BaseTokenProvider` then logged. Only the
46
+ body's `error` and `error_description` are kept now, each quoted and capped:
47
+ `error` at 64 characters, and `error_description` at 512, so a server's
48
+ diagnosis survives. Both are redacted first. Every secret the request itself
49
+ sent (refresh token, assertion, client secret), however short, whether it
50
+ comes back as sent or form- or percent-encoded, and anything shaped like a JWT
51
+ becomes `<redacted>`. A server that echoes one of those, in the body or in
52
+ its description, no longer leaks it. **Limit:** an opaque token the request
53
+ did not send cannot be told from an ordinary identifier, so if a server writes
54
+ a new one into `error_description` it passes through, capped at 512
55
+ characters. Scrubbing every long string would also erase the IDs that make a
56
+ refusal diagnosable.
57
+
58
+ ## [4.1.1] - 2026-09-26
59
+
60
+ ### Changed
61
+
62
+ - The `signedNode` refusal for an assertion inside a `ds:Signature` now reads
63
+ "…inside a ds:Signature, which is never accepted". The 4.1.0 wording, "where
64
+ no signature covers it", was untrue when a signed Response also covers that
65
+ signature. `check` and the fragment `inside a ds:Signature` are unchanged.
66
+
67
+ ### Fixed
68
+
69
+ - The tarball no longer ships `dist/__tests__/…`. Test helpers and stand
70
+ fixtures were built into `dist` and published with 4.0.0 and 4.1.0. The build
71
+ now uses `tsconfig.build.json`, which leaves `src/__tests__` out, and the
72
+ tarball drops from 140 to 128 files. Nothing a consumer imports changes.
73
+
74
+ ### Development
75
+
76
+ - `@mcp-abap-adt/auth-stores` devDependency `^1.2.1`, which depends on
77
+ `@mcp-abap-adt/interfaces-auth` `^2.0.1`. The development tree now holds a
78
+ single `interfaces-auth` 2.0.1 instead of a second, nested 1.2.0. Nothing
79
+ published changes.
80
+ - The live authorization tests take tokens from any session file:
81
+ `MCP_ABAP_ADT_SESSION_FILE`, or `session_path` in `tests/test-config.yaml`,
82
+ or the stores' folder `<destination_dir>/sessions/<destination>.env`. The
83
+ folder defaults to `~/.config/mcp-abap-adt` on Unix and
84
+ `Documents/mcp-abap-adt` on Windows. Browser logins run only with
85
+ `interactive_login: true` or `MCP_ABAP_ADT_INTERACTIVE=1`, so a default
86
+ `npm test` never waits for one.
87
+
10
88
  ## [4.1.0] - 2026-09-26
11
89
 
12
90
  Three refusals get stricter; no export, configuration field or error class
@@ -54,7 +132,9 @@ changes. The README's *Upgrading from 4.0 to 4.1* lists what now fails.
54
132
  - `idpInitiated: true` with `authnRequestId` is refused when the provider is
55
133
  constructed — a `ValidationError` with `missingFields: ['idpInitiated']` —
56
134
  not after `authorize()` returns, so before the user has been through the
57
- browser.
135
+ browser. A `Saml2BearerProvider` seeded with a `refreshToken` did work in 4.0
136
+ until that token lapsed, since a refresh never reaches the strategy; it now
137
+ fails at construction.
58
138
  - A malformed `Signature` whose `loadSignature` throws something other than
59
139
  an `Error` is refused at `signature` quoting what was thrown. Before, the
60
140
  refusal carried an empty message, and a thrown `null` or `undefined`
package/README.md CHANGED
@@ -672,7 +672,7 @@ What the table compresses:
672
672
  `xsd:dateTime`; `NotOnOrAfter` has not passed beyond `clockSkewMs`;
673
673
  `NotBefore` has arrived within it. When none qualifies, the refusal names
674
674
  every candidate in document order with the first sub-rule it failed —
675
- `no bearer confirmation qualifies: #1 Recipient is not the ACS; #2
675
+ `no bearer confirmation qualifies: #1 Recipient is not the ACS | #2
676
676
  NotOnOrAfter has passed` — listing at most five, then `and N more`.
677
677
  - **Check 9 is AND across restrictions, OR within one**, as SAML Core §2.5.1.4
678
678
  says: every `AudienceRestriction` must name you; the `Audience` elements
@@ -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` |
@@ -755,14 +755,16 @@ value.
755
755
  | `bearerConfirmation` | `the assertion carries no saml:Subject` |
756
756
  | `bearerConfirmation` | `the assertion carries <n> saml:Subject; exactly one is allowed` |
757
757
  | `bearerConfirmation` | `the saml:Subject holds no SubjectConfirmation` |
758
- | `bearerConfirmation` | `no bearer confirmation qualifies: #1 <reason>; #2 <reason>; …` |
758
+ | `bearerConfirmation` | `no bearer confirmation qualifies: #1 <reason> \| #2 <reason> \| …` |
759
759
  | `destination` | `the response carries no Destination` |
760
760
  | `destination` | `the response is addressed to "…", not to us` |
761
761
  | `replay` | `this assertion has been presented before` |
762
762
 
763
763
  A `bearerConfirmation` refusal naming candidates lists each one's first failed
764
- sub-rule, in document order, joined by `; `; past five candidates it ends
765
- `; and N more`, N being how many were not listed. The eleven reasons:
764
+ sub-rule, in document order, joined by ` | ` — not `; `, which a count reason
765
+ such as `carries 2 SubjectConfirmationData; exactly one is allowed` contains
766
+ itself; past five candidates it ends ` | and N more`, N being how many were
767
+ not listed. The eleven reasons:
766
768
 
767
769
  | # | `<reason>`, in the order a candidate is tested |
768
770
  |---|---|
@@ -1768,7 +1770,7 @@ Example output:
1768
1770
  ```
1769
1771
 
1770
1772
  **Logging Features**:
1771
- - **Token Formatting**: Tokens are logged in truncated format (start...end) for security
1773
+ - **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
1774
  - **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
1773
1775
  - **Browser Information**: Logs browser type and authorization URL for debugging
1774
1776
  - **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');
@@ -506,7 +506,7 @@ function readConfirmation(confirmation, context) {
506
506
  return { notOnOrAfter, notBefore };
507
507
  }
508
508
  /**
509
- * `no bearer confirmation qualifies: #1 …; #2 …`, naming at most
509
+ * `no bearer confirmation qualifies: #1 … | #2 …`, naming at most
510
510
  * LISTED_CANDIDATES candidates so the message stays bounded however many the
511
511
  * document carries.
512
512
  */
@@ -515,5 +515,5 @@ function describeRefusals(reasons) {
515
515
  .slice(0, LISTED_CANDIDATES)
516
516
  .map((reason, index) => `#${index + 1} ${reason}`);
517
517
  const more = reasons.length - listed.length;
518
- return `no bearer confirmation qualifies: ${listed.join('; ')}${more > 0 ? `; and ${more} more` : ''}`;
518
+ return `no bearer confirmation qualifies: ${listed.join(' | ')}${more > 0 ? ` | and ${more} more` : ''}`;
519
519
  }
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.3",
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
- }