@mcp-abap-adt/header-validator 0.1.6 → 0.1.7
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 +29 -0
- package/dist/headerValidator.d.ts +55 -2
- package/dist/headerValidator.js +197 -55
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] - 2025-01-XX
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Header Sets Documentation**: Added comprehensive documentation section on header sets in `USAGE.md`
|
|
14
|
+
- **Set 1: Basic Authentication Set** - Documents that `x-sap-login`, `x-sap-password`, and `x-sap-auth-type: basic` must be provided together
|
|
15
|
+
- **Set 2: UAA Refresh Token Set** - Documents that UAA headers (`x-sap-uaa-url`, `x-sap-uaa-client-id`, `x-sap-uaa-client-secret`) must be provided together when used (optional)
|
|
16
|
+
- **Set 3: Direct JWT Authentication** - Documents minimal set requirements for JWT authentication
|
|
17
|
+
- Includes validation rules, examples, and error scenarios for each set
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Header Constants Migration**: Replaced all hardcoded header strings with constants from `@mcp-abap-adt/interfaces`
|
|
21
|
+
- Updated `@mcp-abap-adt/interfaces` dependency from `^0.1.1` to `^0.1.2`
|
|
22
|
+
- All header references now use constants (e.g., `HEADER_SAP_LOGIN`, `HEADER_SAP_PASSWORD`, `HEADER_SAP_AUTH_TYPE`, etc.)
|
|
23
|
+
- Improved type safety and consistency across packages
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **Basic Auth Validation**: Enhanced validation to ensure `x-sap-login` and `x-sap-password` are provided together as a cohesive set
|
|
27
|
+
- Validation now checks if either header is present, both must be present
|
|
28
|
+
- Improved error messages for missing basic auth headers
|
|
29
|
+
- **UAA Headers Validation**: Enhanced validation for UAA refresh token headers
|
|
30
|
+
- Added warning when UAA headers are partially provided
|
|
31
|
+
- Clarified that UAA headers are optional and only used for token refresh
|
|
32
|
+
- UAA headers do not affect authorization validation
|
|
33
|
+
|
|
34
|
+
### Technical
|
|
35
|
+
- **Test Updates**: Updated all tests to use header constants instead of hardcoded strings
|
|
36
|
+
- **Jest Configuration**: Updated Jest to version 30.2.0 and ts-jest to 29.2.5 (aligned with auth-providers package)
|
|
37
|
+
- **Dependencies**: Added `jest-util@^30.2.0` as dev dependency to resolve Jest compatibility issues
|
|
38
|
+
|
|
10
39
|
## [0.1.6] - 2025-12-05
|
|
11
40
|
|
|
12
41
|
### Changed
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Header validator for MCP ABAP ADT authentication headers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides validation functions for different contexts:
|
|
5
|
+
* - MCP server headers: x-sap-destination, x-mcp-destination, x-sap-url, x-sap-jwt-token, etc.
|
|
6
|
+
* - Proxy headers: x-btp-destination, x-mcp-destination, x-mcp-url
|
|
7
|
+
*
|
|
8
|
+
* MCP authentication headers are validated and prioritized according to:
|
|
9
|
+
* 1. Destination-based auth (x-sap-destination, x-mcp-destination) - highest priority
|
|
6
10
|
* 2. Direct JWT token (x-sap-jwt-token) - medium priority
|
|
7
11
|
* 3. Basic auth (x-sap-login + x-sap-password) - lowest priority
|
|
8
12
|
*/
|
|
@@ -15,3 +19,52 @@ import type { HeaderValidationResult } from './types';
|
|
|
15
19
|
* @returns Validation result with prioritized authentication configuration
|
|
16
20
|
*/
|
|
17
21
|
export declare function validateAuthHeaders(headers?: IncomingHttpHeaders): HeaderValidationResult;
|
|
22
|
+
/**
|
|
23
|
+
* Proxy Header Validation
|
|
24
|
+
*
|
|
25
|
+
* Functions to validate proxy-specific headers (x-btp-destination, x-mcp-destination, x-mcp-url)
|
|
26
|
+
* These are separate from MCP authentication headers and used for proxy routing decisions.
|
|
27
|
+
*/
|
|
28
|
+
export interface ProxyHeaderValidationResult {
|
|
29
|
+
isValid: boolean;
|
|
30
|
+
hasBtpDestination: boolean;
|
|
31
|
+
hasMcpDestination: boolean;
|
|
32
|
+
hasMcpUrl: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate proxy routing headers
|
|
38
|
+
*
|
|
39
|
+
* Checks for proxy-specific headers:
|
|
40
|
+
* - x-btp-destination: BTP Cloud authorization destination
|
|
41
|
+
* - x-mcp-destination: SAP ABAP connection destination
|
|
42
|
+
* - x-mcp-url: Direct MCP server URL
|
|
43
|
+
*
|
|
44
|
+
* @param headers HTTP headers
|
|
45
|
+
* @returns Validation result indicating which proxy headers are present
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateProxyHeaders(headers?: IncomingHttpHeaders): ProxyHeaderValidationResult;
|
|
48
|
+
/**
|
|
49
|
+
* Check if headers indicate a proxy request
|
|
50
|
+
*
|
|
51
|
+
* A request is considered a proxy request if it has at least one of:
|
|
52
|
+
* - x-btp-destination
|
|
53
|
+
* - x-mcp-destination
|
|
54
|
+
* - x-mcp-url
|
|
55
|
+
*
|
|
56
|
+
* @param headers HTTP headers
|
|
57
|
+
* @returns true if headers indicate a proxy request
|
|
58
|
+
*/
|
|
59
|
+
export declare function isProxyRequest(headers?: IncomingHttpHeaders): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Check if headers indicate an MCP server request (not proxy)
|
|
62
|
+
*
|
|
63
|
+
* An MCP server request has MCP authentication headers but no proxy headers:
|
|
64
|
+
* - Has x-sap-destination, x-mcp-destination, x-sap-url, x-sap-jwt-token, etc.
|
|
65
|
+
* - Does NOT have x-btp-destination or x-mcp-url (proxy-specific)
|
|
66
|
+
*
|
|
67
|
+
* @param headers HTTP headers
|
|
68
|
+
* @returns true if headers indicate an MCP server request
|
|
69
|
+
*/
|
|
70
|
+
export declare function isMcpServerRequest(headers?: IncomingHttpHeaders): boolean;
|
package/dist/headerValidator.js
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Header validator for MCP ABAP ADT authentication headers
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Provides validation functions for different contexts:
|
|
6
|
+
* - MCP server headers: x-sap-destination, x-mcp-destination, x-sap-url, x-sap-jwt-token, etc.
|
|
7
|
+
* - Proxy headers: x-btp-destination, x-mcp-destination, x-mcp-url
|
|
8
|
+
*
|
|
9
|
+
* MCP authentication headers are validated and prioritized according to:
|
|
10
|
+
* 1. Destination-based auth (x-sap-destination, x-mcp-destination) - highest priority
|
|
7
11
|
* 2. Direct JWT token (x-sap-jwt-token) - medium priority
|
|
8
12
|
* 3. Basic auth (x-sap-login + x-sap-password) - lowest priority
|
|
9
13
|
*/
|
|
10
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
15
|
exports.validateAuthHeaders = validateAuthHeaders;
|
|
16
|
+
exports.validateProxyHeaders = validateProxyHeaders;
|
|
17
|
+
exports.isProxyRequest = isProxyRequest;
|
|
18
|
+
exports.isMcpServerRequest = isMcpServerRequest;
|
|
12
19
|
const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
13
20
|
/**
|
|
14
21
|
* Extract header value (handles array values)
|
|
@@ -48,47 +55,47 @@ function isValidUrl(url) {
|
|
|
48
55
|
*/
|
|
49
56
|
function validateSapDestinationAuth(headers) {
|
|
50
57
|
// Check both lowercase and original case (Node.js normalizes to lowercase, but check both for safety)
|
|
51
|
-
const destinationRaw = headers[
|
|
58
|
+
const destinationRaw = headers[interfaces_1.HEADER_SAP_DESTINATION_SERVICE.toLowerCase()] || headers[interfaces_1.HEADER_SAP_DESTINATION_SERVICE];
|
|
52
59
|
if (!destinationRaw) {
|
|
53
60
|
return null;
|
|
54
61
|
}
|
|
55
|
-
const destination = getHeaderValue(headers,
|
|
62
|
+
const destination = getHeaderValue(headers, interfaces_1.HEADER_SAP_DESTINATION_SERVICE);
|
|
56
63
|
const errors = [];
|
|
57
64
|
const warnings = [];
|
|
58
65
|
// Validate destination name (check if empty after trim)
|
|
59
66
|
if (!destination || destination.length === 0) {
|
|
60
|
-
errors.push(
|
|
67
|
+
errors.push(`${interfaces_1.HEADER_SAP_DESTINATION_SERVICE} header is empty`);
|
|
61
68
|
return {
|
|
62
69
|
priority: interfaces_1.AuthMethodPriority.NONE,
|
|
63
|
-
authType:
|
|
70
|
+
authType: interfaces_1.AUTH_TYPE_JWT, // SAP destination always uses JWT
|
|
64
71
|
sapUrl: '', // URL will be loaded from destination
|
|
65
72
|
errors,
|
|
66
73
|
warnings,
|
|
67
74
|
};
|
|
68
75
|
}
|
|
69
76
|
// Extract optional SAP client
|
|
70
|
-
const sapClient = getHeaderValue(headers,
|
|
77
|
+
const sapClient = getHeaderValue(headers, interfaces_1.HEADER_SAP_CLIENT);
|
|
71
78
|
// Optional: x-sap-login and x-sap-password (for cloud systems)
|
|
72
|
-
const username = getHeaderValue(headers,
|
|
73
|
-
const password = getHeaderValue(headers,
|
|
79
|
+
const username = getHeaderValue(headers, interfaces_1.HEADER_SAP_LOGIN);
|
|
80
|
+
const password = getHeaderValue(headers, interfaces_1.HEADER_SAP_PASSWORD);
|
|
74
81
|
// Warning if x-sap-url is provided (URL comes from destination, not header)
|
|
75
|
-
const sapUrl = getHeaderValue(headers,
|
|
82
|
+
const sapUrl = getHeaderValue(headers, interfaces_1.HEADER_SAP_URL);
|
|
76
83
|
if (sapUrl) {
|
|
77
|
-
warnings.push(
|
|
84
|
+
warnings.push(`${interfaces_1.HEADER_SAP_URL} is ignored when ${interfaces_1.HEADER_SAP_DESTINATION_SERVICE} is present (URL is loaded from destination service key or .env file)`);
|
|
78
85
|
}
|
|
79
86
|
// Warning if direct JWT token is also provided (destination takes priority)
|
|
80
|
-
const jwtToken = getHeaderValue(headers,
|
|
87
|
+
const jwtToken = getHeaderValue(headers, interfaces_1.HEADER_SAP_JWT_TOKEN);
|
|
81
88
|
if (jwtToken) {
|
|
82
|
-
warnings.push(
|
|
89
|
+
warnings.push(`${interfaces_1.HEADER_SAP_JWT_TOKEN} is ignored when ${interfaces_1.HEADER_SAP_DESTINATION_SERVICE} is present (destination-based auth takes priority)`);
|
|
83
90
|
}
|
|
84
91
|
// Warning if auth-type is provided (not needed for x-sap-destination)
|
|
85
|
-
const authType = getHeaderValue(headers,
|
|
92
|
+
const authType = getHeaderValue(headers, interfaces_1.HEADER_SAP_AUTH_TYPE);
|
|
86
93
|
if (authType) {
|
|
87
|
-
warnings.push(
|
|
94
|
+
warnings.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} is ignored when ${interfaces_1.HEADER_SAP_DESTINATION_SERVICE} is present (always uses JWT)`);
|
|
88
95
|
}
|
|
89
96
|
return {
|
|
90
97
|
priority: interfaces_1.AuthMethodPriority.SAP_DESTINATION,
|
|
91
|
-
authType:
|
|
98
|
+
authType: interfaces_1.AUTH_TYPE_JWT, // Always JWT for x-sap-destination
|
|
92
99
|
sapUrl: '', // URL will be loaded from destination (service key or .env)
|
|
93
100
|
sapClient,
|
|
94
101
|
destination,
|
|
@@ -106,19 +113,19 @@ function validateSapDestinationAuth(headers) {
|
|
|
106
113
|
*/
|
|
107
114
|
function validateMcpDestinationAuth(headers, sapUrl) {
|
|
108
115
|
// Check both lowercase and original case (Node.js normalizes to lowercase, but check both for safety)
|
|
109
|
-
const destinationRaw = headers[
|
|
116
|
+
const destinationRaw = headers[interfaces_1.HEADER_MCP_DESTINATION.toLowerCase()] || headers[interfaces_1.HEADER_MCP_DESTINATION];
|
|
110
117
|
if (!destinationRaw) {
|
|
111
118
|
return null;
|
|
112
119
|
}
|
|
113
|
-
const destination = getHeaderValue(headers,
|
|
120
|
+
const destination = getHeaderValue(headers, interfaces_1.HEADER_MCP_DESTINATION);
|
|
114
121
|
const errors = [];
|
|
115
122
|
const warnings = [];
|
|
116
123
|
// Validate destination name (check if empty after trim)
|
|
117
124
|
if (!destination || destination.length === 0) {
|
|
118
|
-
errors.push(
|
|
125
|
+
errors.push(`${interfaces_1.HEADER_MCP_DESTINATION} header is empty`);
|
|
119
126
|
return {
|
|
120
127
|
priority: interfaces_1.AuthMethodPriority.NONE,
|
|
121
|
-
authType:
|
|
128
|
+
authType: interfaces_1.AUTH_TYPE_JWT, // MCP destination always uses JWT
|
|
122
129
|
sapUrl: '', // URL will be loaded from destination
|
|
123
130
|
errors,
|
|
124
131
|
warnings,
|
|
@@ -126,23 +133,23 @@ function validateMcpDestinationAuth(headers, sapUrl) {
|
|
|
126
133
|
}
|
|
127
134
|
// Warning if x-sap-url is provided (URL comes from destination, not header)
|
|
128
135
|
if (sapUrl) {
|
|
129
|
-
warnings.push(
|
|
136
|
+
warnings.push(`${interfaces_1.HEADER_SAP_URL} is ignored when ${interfaces_1.HEADER_MCP_DESTINATION} is present (URL is loaded from destination service key or .env file)`);
|
|
130
137
|
}
|
|
131
138
|
// Warning if x-sap-auth-type is provided (not needed for x-mcp-destination)
|
|
132
|
-
const authType = getHeaderValue(headers,
|
|
139
|
+
const authType = getHeaderValue(headers, interfaces_1.HEADER_SAP_AUTH_TYPE);
|
|
133
140
|
if (authType) {
|
|
134
|
-
warnings.push(
|
|
141
|
+
warnings.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} is ignored when ${interfaces_1.HEADER_MCP_DESTINATION} is present (always uses JWT)`);
|
|
135
142
|
}
|
|
136
143
|
// Extract optional SAP client
|
|
137
|
-
const sapClient = getHeaderValue(headers,
|
|
144
|
+
const sapClient = getHeaderValue(headers, interfaces_1.HEADER_SAP_CLIENT);
|
|
138
145
|
// Warning if direct JWT token is also provided (destination takes priority)
|
|
139
|
-
const jwtToken = getHeaderValue(headers,
|
|
146
|
+
const jwtToken = getHeaderValue(headers, interfaces_1.HEADER_SAP_JWT_TOKEN);
|
|
140
147
|
if (jwtToken) {
|
|
141
|
-
warnings.push(
|
|
148
|
+
warnings.push(`${interfaces_1.HEADER_SAP_JWT_TOKEN} is ignored when ${interfaces_1.HEADER_MCP_DESTINATION} is present (destination-based auth takes priority)`);
|
|
142
149
|
}
|
|
143
150
|
return {
|
|
144
151
|
priority: interfaces_1.AuthMethodPriority.MCP_DESTINATION,
|
|
145
|
-
authType:
|
|
152
|
+
authType: interfaces_1.AUTH_TYPE_JWT, // Always JWT for x-mcp-destination
|
|
146
153
|
sapUrl: '', // URL will be loaded from destination (service key or .env)
|
|
147
154
|
sapClient,
|
|
148
155
|
destination,
|
|
@@ -152,12 +159,16 @@ function validateMcpDestinationAuth(headers, sapUrl) {
|
|
|
152
159
|
}
|
|
153
160
|
/**
|
|
154
161
|
* Validate direct JWT authentication (medium priority)
|
|
162
|
+
*
|
|
163
|
+
* For authorization, only x-sap-jwt-token is required.
|
|
164
|
+
* UAA headers (x-sap-uaa-url, x-sap-uaa-client-id, x-sap-uaa-client-secret) are optional
|
|
165
|
+
* and only used for token refresh - they are a separate set of headers.
|
|
155
166
|
*/
|
|
156
167
|
function validateDirectJwtAuth(headers, sapUrl, authType) {
|
|
157
|
-
if (authType !==
|
|
168
|
+
if (authType !== interfaces_1.AUTH_TYPE_JWT && authType !== interfaces_1.AUTH_TYPE_XSUAA) {
|
|
158
169
|
return null;
|
|
159
170
|
}
|
|
160
|
-
const jwtToken = getHeaderValue(headers,
|
|
171
|
+
const jwtToken = getHeaderValue(headers, interfaces_1.HEADER_SAP_JWT_TOKEN);
|
|
161
172
|
if (!jwtToken) {
|
|
162
173
|
return null;
|
|
163
174
|
}
|
|
@@ -165,15 +176,23 @@ function validateDirectJwtAuth(headers, sapUrl, authType) {
|
|
|
165
176
|
const warnings = [];
|
|
166
177
|
// Validate JWT token format (basic check - should start with eyJ)
|
|
167
178
|
if (jwtToken.length < 10) {
|
|
168
|
-
errors.push(
|
|
179
|
+
errors.push(`${interfaces_1.HEADER_SAP_JWT_TOKEN} appears to be invalid (too short)`);
|
|
180
|
+
}
|
|
181
|
+
// Extract optional refresh token
|
|
182
|
+
const refreshToken = getHeaderValue(headers, interfaces_1.HEADER_SAP_REFRESH_TOKEN);
|
|
183
|
+
// Extract optional UAA config (for token refresh only - separate set of headers)
|
|
184
|
+
// These are optional and don't affect authorization validation
|
|
185
|
+
const uaaUrl = getHeaderValue(headers, interfaces_1.HEADER_SAP_UAA_URL) || getHeaderValue(headers, interfaces_1.HEADER_UAA_URL);
|
|
186
|
+
const uaaClientId = getHeaderValue(headers, interfaces_1.HEADER_SAP_UAA_CLIENT_ID) || getHeaderValue(headers, interfaces_1.HEADER_UAA_CLIENT_ID);
|
|
187
|
+
const uaaClientSecret = getHeaderValue(headers, interfaces_1.HEADER_SAP_UAA_CLIENT_SECRET) || getHeaderValue(headers, interfaces_1.HEADER_UAA_CLIENT_SECRET);
|
|
188
|
+
// Validate UAA config completeness if any UAA header is present
|
|
189
|
+
if (uaaUrl || uaaClientId || uaaClientSecret) {
|
|
190
|
+
if (!uaaUrl || !uaaClientId || !uaaClientSecret) {
|
|
191
|
+
warnings.push(`UAA headers (${interfaces_1.HEADER_SAP_UAA_URL}, ${interfaces_1.HEADER_SAP_UAA_CLIENT_ID}, ${interfaces_1.HEADER_SAP_UAA_CLIENT_SECRET}) should be provided together for token refresh`);
|
|
192
|
+
}
|
|
169
193
|
}
|
|
170
|
-
// Extract optional tokens and UAA config
|
|
171
|
-
const refreshToken = getHeaderValue(headers, 'x-sap-refresh-token');
|
|
172
|
-
const uaaUrl = getHeaderValue(headers, 'x-sap-uaa-url') || getHeaderValue(headers, 'uaa-url');
|
|
173
|
-
const uaaClientId = getHeaderValue(headers, 'x-sap-uaa-client-id') || getHeaderValue(headers, 'uaa-client-id');
|
|
174
|
-
const uaaClientSecret = getHeaderValue(headers, 'x-sap-uaa-client-secret') || getHeaderValue(headers, 'uaa-client-secret');
|
|
175
194
|
// Extract optional SAP client
|
|
176
|
-
const sapClient = getHeaderValue(headers,
|
|
195
|
+
const sapClient = getHeaderValue(headers, interfaces_1.HEADER_SAP_CLIENT);
|
|
177
196
|
return {
|
|
178
197
|
priority: interfaces_1.AuthMethodPriority.DIRECT_JWT,
|
|
179
198
|
authType,
|
|
@@ -192,24 +211,24 @@ function validateDirectJwtAuth(headers, sapUrl, authType) {
|
|
|
192
211
|
* Validate basic authentication (lowest priority)
|
|
193
212
|
*/
|
|
194
213
|
function validateBasicAuth(headers, sapUrl, authType) {
|
|
195
|
-
if (authType !==
|
|
214
|
+
if (authType !== interfaces_1.AUTH_TYPE_BASIC) {
|
|
196
215
|
return null;
|
|
197
216
|
}
|
|
198
|
-
const usernameRaw = headers[
|
|
199
|
-
const passwordRaw = headers[
|
|
217
|
+
const usernameRaw = headers[interfaces_1.HEADER_SAP_LOGIN.toLowerCase()] || headers[interfaces_1.HEADER_SAP_LOGIN];
|
|
218
|
+
const passwordRaw = headers[interfaces_1.HEADER_SAP_PASSWORD.toLowerCase()] || headers[interfaces_1.HEADER_SAP_PASSWORD];
|
|
200
219
|
if (!usernameRaw || !passwordRaw) {
|
|
201
220
|
return null;
|
|
202
221
|
}
|
|
203
|
-
const username = getHeaderValue(headers,
|
|
204
|
-
const password = getHeaderValue(headers,
|
|
222
|
+
const username = getHeaderValue(headers, interfaces_1.HEADER_SAP_LOGIN);
|
|
223
|
+
const password = getHeaderValue(headers, interfaces_1.HEADER_SAP_PASSWORD);
|
|
205
224
|
const errors = [];
|
|
206
225
|
const warnings = [];
|
|
207
226
|
// Validate username and password (check if empty after trim)
|
|
208
227
|
if (!username || username.length === 0) {
|
|
209
|
-
errors.push(
|
|
228
|
+
errors.push(`${interfaces_1.HEADER_SAP_LOGIN} header is empty`);
|
|
210
229
|
}
|
|
211
230
|
if (!password || password.length === 0) {
|
|
212
|
-
errors.push(
|
|
231
|
+
errors.push(`${interfaces_1.HEADER_SAP_PASSWORD} header is empty`);
|
|
213
232
|
}
|
|
214
233
|
// Return config with errors if validation failed
|
|
215
234
|
if (errors.length > 0) {
|
|
@@ -270,7 +289,7 @@ function validateAuthHeaders(headers) {
|
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
// Check for MCP destination (doesn't require x-sap-url, URL comes from destination)
|
|
273
|
-
const sapUrl = getHeaderValue(headers,
|
|
292
|
+
const sapUrl = getHeaderValue(headers, interfaces_1.HEADER_SAP_URL);
|
|
274
293
|
const mcpDestinationConfig = validateMcpDestinationAuth(headers, sapUrl);
|
|
275
294
|
if (mcpDestinationConfig) {
|
|
276
295
|
// MCP destination found - URL comes from destination, not header
|
|
@@ -302,7 +321,7 @@ function validateAuthHeaders(headers) {
|
|
|
302
321
|
}
|
|
303
322
|
// Validate URL format
|
|
304
323
|
if (!isValidUrl(sapUrl)) {
|
|
305
|
-
errors.push(
|
|
324
|
+
errors.push(`${interfaces_1.HEADER_SAP_URL} is not a valid URL: ${sapUrl}`);
|
|
306
325
|
return {
|
|
307
326
|
isValid: false,
|
|
308
327
|
errors,
|
|
@@ -311,16 +330,32 @@ function validateAuthHeaders(headers) {
|
|
|
311
330
|
}
|
|
312
331
|
// Try to validate authentication methods in priority order
|
|
313
332
|
const configs = [];
|
|
333
|
+
// Check if basic auth headers are present (x-sap-login, x-sap-password)
|
|
334
|
+
// These should come together with x-sap-auth-type: basic
|
|
335
|
+
const hasSapLogin = !!getHeaderValue(headers, interfaces_1.HEADER_SAP_LOGIN);
|
|
336
|
+
const hasSapPassword = !!getHeaderValue(headers, interfaces_1.HEADER_SAP_PASSWORD);
|
|
337
|
+
const hasBasicAuthHeaders = hasSapLogin || hasSapPassword;
|
|
338
|
+
if (hasBasicAuthHeaders && (!hasSapLogin || !hasSapPassword)) {
|
|
339
|
+
errors.push(`${interfaces_1.HEADER_SAP_LOGIN} and ${interfaces_1.HEADER_SAP_PASSWORD} must be provided together`);
|
|
340
|
+
}
|
|
314
341
|
// 3. Other auth methods require x-sap-auth-type
|
|
315
|
-
const sapAuthType = getHeaderValue(headers,
|
|
342
|
+
const sapAuthType = getHeaderValue(headers, interfaces_1.HEADER_SAP_AUTH_TYPE);
|
|
316
343
|
if (sapAuthType) {
|
|
317
344
|
// Validate auth type
|
|
318
|
-
const validAuthTypes = [
|
|
345
|
+
const validAuthTypes = [interfaces_1.AUTH_TYPE_JWT, interfaces_1.AUTH_TYPE_XSUAA, interfaces_1.AUTH_TYPE_BASIC];
|
|
319
346
|
const authType = sapAuthType.toLowerCase();
|
|
320
347
|
if (!validAuthTypes.includes(authType)) {
|
|
321
|
-
errors.push(
|
|
348
|
+
errors.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} must be one of: ${validAuthTypes.join(', ')}, got: ${sapAuthType}`);
|
|
322
349
|
}
|
|
323
350
|
else {
|
|
351
|
+
// Check if basic auth headers are present but auth-type is not basic
|
|
352
|
+
if (hasBasicAuthHeaders && authType !== interfaces_1.AUTH_TYPE_BASIC) {
|
|
353
|
+
warnings.push(`${interfaces_1.HEADER_SAP_LOGIN} and ${interfaces_1.HEADER_SAP_PASSWORD} are present but ${interfaces_1.HEADER_SAP_AUTH_TYPE} is not "${interfaces_1.AUTH_TYPE_BASIC}"`);
|
|
354
|
+
}
|
|
355
|
+
// Check if auth-type is basic but headers are missing
|
|
356
|
+
if (authType === interfaces_1.AUTH_TYPE_BASIC && !hasBasicAuthHeaders) {
|
|
357
|
+
errors.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} is "${interfaces_1.AUTH_TYPE_BASIC}" but ${interfaces_1.HEADER_SAP_LOGIN} and ${interfaces_1.HEADER_SAP_PASSWORD} are missing`);
|
|
358
|
+
}
|
|
324
359
|
// Only validate direct JWT and basic auth if MCP destination is not present
|
|
325
360
|
// (MCP destination already handled above)
|
|
326
361
|
if (!mcpDestinationConfig) {
|
|
@@ -338,20 +373,25 @@ function validateAuthHeaders(headers) {
|
|
|
338
373
|
}
|
|
339
374
|
}
|
|
340
375
|
else {
|
|
341
|
-
// No auth-type provided
|
|
342
|
-
|
|
343
|
-
|
|
376
|
+
// No auth-type provided
|
|
377
|
+
// If basic auth headers are present, auth-type must be basic
|
|
378
|
+
if (hasBasicAuthHeaders) {
|
|
379
|
+
errors.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} must be "${interfaces_1.AUTH_TYPE_BASIC}" when ${interfaces_1.HEADER_SAP_LOGIN} and ${interfaces_1.HEADER_SAP_PASSWORD} are present`);
|
|
380
|
+
}
|
|
381
|
+
else if (!mcpDestinationConfig && !sapDestinationConfig) {
|
|
382
|
+
// No auth-type and no destination - error
|
|
383
|
+
errors.push(`${interfaces_1.HEADER_SAP_AUTH_TYPE} header is required when ${interfaces_1.HEADER_SAP_DESTINATION_SERVICE} and ${interfaces_1.HEADER_MCP_DESTINATION} are not present`);
|
|
344
384
|
}
|
|
345
385
|
}
|
|
346
386
|
// No valid authentication method found
|
|
347
387
|
if (configs.length === 0) {
|
|
348
388
|
if (sapAuthType) {
|
|
349
389
|
const authType = sapAuthType.toLowerCase();
|
|
350
|
-
if (authType ===
|
|
351
|
-
errors.push(
|
|
390
|
+
if (authType === interfaces_1.AUTH_TYPE_JWT || authType === interfaces_1.AUTH_TYPE_XSUAA) {
|
|
391
|
+
errors.push(`JWT authentication requires either ${interfaces_1.HEADER_SAP_DESTINATION_SERVICE}, ${interfaces_1.HEADER_MCP_DESTINATION}, or ${interfaces_1.HEADER_SAP_JWT_TOKEN} header`);
|
|
352
392
|
}
|
|
353
|
-
else if (authType ===
|
|
354
|
-
errors.push(
|
|
393
|
+
else if (authType === interfaces_1.AUTH_TYPE_BASIC) {
|
|
394
|
+
errors.push(`Basic authentication requires ${interfaces_1.HEADER_SAP_LOGIN} and ${interfaces_1.HEADER_SAP_PASSWORD} headers`);
|
|
355
395
|
}
|
|
356
396
|
}
|
|
357
397
|
else {
|
|
@@ -384,3 +424,105 @@ function validateAuthHeaders(headers) {
|
|
|
384
424
|
warnings: allWarnings,
|
|
385
425
|
};
|
|
386
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Validate proxy routing headers
|
|
429
|
+
*
|
|
430
|
+
* Checks for proxy-specific headers:
|
|
431
|
+
* - x-btp-destination: BTP Cloud authorization destination
|
|
432
|
+
* - x-mcp-destination: SAP ABAP connection destination
|
|
433
|
+
* - x-mcp-url: Direct MCP server URL
|
|
434
|
+
*
|
|
435
|
+
* @param headers HTTP headers
|
|
436
|
+
* @returns Validation result indicating which proxy headers are present
|
|
437
|
+
*/
|
|
438
|
+
function validateProxyHeaders(headers) {
|
|
439
|
+
const errors = [];
|
|
440
|
+
const warnings = [];
|
|
441
|
+
if (!headers) {
|
|
442
|
+
return {
|
|
443
|
+
isValid: false,
|
|
444
|
+
hasBtpDestination: false,
|
|
445
|
+
hasMcpDestination: false,
|
|
446
|
+
hasMcpUrl: false,
|
|
447
|
+
errors: [],
|
|
448
|
+
warnings: [],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
const btpDestination = getHeaderValue(headers, interfaces_1.HEADER_BTP_DESTINATION);
|
|
452
|
+
const mcpDestination = getHeaderValue(headers, interfaces_1.HEADER_MCP_DESTINATION);
|
|
453
|
+
const mcpUrl = getHeaderValue(headers, interfaces_1.HEADER_MCP_URL);
|
|
454
|
+
const hasBtpDestination = !!btpDestination;
|
|
455
|
+
const hasMcpDestination = !!mcpDestination;
|
|
456
|
+
const hasMcpUrl = !!mcpUrl;
|
|
457
|
+
// Validate destination names if present
|
|
458
|
+
if (hasBtpDestination && (!btpDestination || btpDestination.trim().length === 0)) {
|
|
459
|
+
errors.push(`${interfaces_1.HEADER_BTP_DESTINATION} header is empty`);
|
|
460
|
+
}
|
|
461
|
+
if (hasMcpDestination && (!mcpDestination || mcpDestination.trim().length === 0)) {
|
|
462
|
+
errors.push(`${interfaces_1.HEADER_MCP_DESTINATION} header is empty`);
|
|
463
|
+
}
|
|
464
|
+
// Validate mcpUrl format if present
|
|
465
|
+
if (hasMcpUrl) {
|
|
466
|
+
if (!isValidUrl(mcpUrl)) {
|
|
467
|
+
errors.push(`${interfaces_1.HEADER_MCP_URL} is not a valid URL: ${mcpUrl}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// At least one proxy header should be present for proxy routing
|
|
471
|
+
const hasAnyProxyHeader = hasBtpDestination || hasMcpDestination || hasMcpUrl;
|
|
472
|
+
if (!hasAnyProxyHeader) {
|
|
473
|
+
warnings.push(`No proxy headers found (${interfaces_1.HEADER_BTP_DESTINATION}, ${interfaces_1.HEADER_MCP_DESTINATION}, or ${interfaces_1.HEADER_MCP_URL})`);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
isValid: errors.length === 0,
|
|
477
|
+
hasBtpDestination,
|
|
478
|
+
hasMcpDestination,
|
|
479
|
+
hasMcpUrl,
|
|
480
|
+
errors,
|
|
481
|
+
warnings,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Check if headers indicate a proxy request
|
|
486
|
+
*
|
|
487
|
+
* A request is considered a proxy request if it has at least one of:
|
|
488
|
+
* - x-btp-destination
|
|
489
|
+
* - x-mcp-destination
|
|
490
|
+
* - x-mcp-url
|
|
491
|
+
*
|
|
492
|
+
* @param headers HTTP headers
|
|
493
|
+
* @returns true if headers indicate a proxy request
|
|
494
|
+
*/
|
|
495
|
+
function isProxyRequest(headers) {
|
|
496
|
+
if (!headers) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
const validation = validateProxyHeaders(headers);
|
|
500
|
+
return validation.hasBtpDestination || validation.hasMcpDestination || validation.hasMcpUrl;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check if headers indicate an MCP server request (not proxy)
|
|
504
|
+
*
|
|
505
|
+
* An MCP server request has MCP authentication headers but no proxy headers:
|
|
506
|
+
* - Has x-sap-destination, x-mcp-destination, x-sap-url, x-sap-jwt-token, etc.
|
|
507
|
+
* - Does NOT have x-btp-destination or x-mcp-url (proxy-specific)
|
|
508
|
+
*
|
|
509
|
+
* @param headers HTTP headers
|
|
510
|
+
* @returns true if headers indicate an MCP server request
|
|
511
|
+
*/
|
|
512
|
+
function isMcpServerRequest(headers) {
|
|
513
|
+
if (!headers) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
// Check for MCP authentication headers
|
|
517
|
+
const hasSapDestination = !!getHeaderValue(headers, interfaces_1.HEADER_SAP_DESTINATION_SERVICE);
|
|
518
|
+
const hasMcpDestination = !!getHeaderValue(headers, interfaces_1.HEADER_MCP_DESTINATION);
|
|
519
|
+
const hasSapUrl = !!getHeaderValue(headers, interfaces_1.HEADER_SAP_URL);
|
|
520
|
+
const hasSapJwtToken = !!getHeaderValue(headers, interfaces_1.HEADER_SAP_JWT_TOKEN);
|
|
521
|
+
// Check for proxy-specific headers
|
|
522
|
+
const hasBtpDestination = !!getHeaderValue(headers, interfaces_1.HEADER_BTP_DESTINATION);
|
|
523
|
+
const hasMcpUrl = !!getHeaderValue(headers, interfaces_1.HEADER_MCP_URL);
|
|
524
|
+
// MCP server request has MCP auth headers but no proxy headers
|
|
525
|
+
const hasMcpAuthHeaders = hasSapDestination || hasMcpDestination || hasSapUrl || hasSapJwtToken;
|
|
526
|
+
const hasProxyHeaders = hasBtpDestination || hasMcpUrl;
|
|
527
|
+
return hasMcpAuthHeaders && !hasProxyHeaders;
|
|
528
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Validates and prioritizes authentication headers for MCP ABAP ADT servers
|
|
5
5
|
*/
|
|
6
|
-
export { validateAuthHeaders } from './headerValidator';
|
|
6
|
+
export { validateAuthHeaders, validateProxyHeaders, isProxyRequest, isMcpServerRequest, type ProxyHeaderValidationResult, } from './headerValidator';
|
|
7
7
|
export * from './types';
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
19
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.validateAuthHeaders = void 0;
|
|
22
|
+
exports.isMcpServerRequest = exports.isProxyRequest = exports.validateProxyHeaders = exports.validateAuthHeaders = void 0;
|
|
23
23
|
var headerValidator_1 = require("./headerValidator");
|
|
24
24
|
Object.defineProperty(exports, "validateAuthHeaders", { enumerable: true, get: function () { return headerValidator_1.validateAuthHeaders; } });
|
|
25
|
+
Object.defineProperty(exports, "validateProxyHeaders", { enumerable: true, get: function () { return headerValidator_1.validateProxyHeaders; } });
|
|
26
|
+
Object.defineProperty(exports, "isProxyRequest", { enumerable: true, get: function () { return headerValidator_1.isProxyRequest; } });
|
|
27
|
+
Object.defineProperty(exports, "isMcpServerRequest", { enumerable: true, get: function () { return headerValidator_1.isMcpServerRequest; } });
|
|
25
28
|
__exportStar(require("./types"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/header-validator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Header validator for MCP ABAP ADT - validates and prioritizes authentication headers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
38
38
|
"build": "npm run clean --silent && npx tsc -p tsconfig.json",
|
|
39
39
|
"build:fast": "npx tsc -p tsconfig.json",
|
|
40
|
-
"test": "jest",
|
|
40
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
41
41
|
"test:check": "npx tsc --noEmit",
|
|
42
42
|
"prepublishOnly": "npm run build"
|
|
43
43
|
},
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
"node": ">=18.0.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@mcp-abap-adt/interfaces": "^0.1.
|
|
48
|
+
"@mcp-abap-adt/interfaces": "^0.1.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/jest": "^30.0.0",
|
|
52
52
|
"@types/node": "^24.2.1",
|
|
53
53
|
"jest": "^30.2.0",
|
|
54
|
+
"jest-util": "^30.2.0",
|
|
54
55
|
"ts-jest": "^29.2.5",
|
|
55
56
|
"typescript": "^5.9.2"
|
|
56
57
|
}
|