@babamba2/mcp-abap-connection 1.5.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +522 -0
  3. package/bin/sap-abap-auth.js +665 -0
  4. package/dist/config/sapConfig.d.ts +5 -0
  5. package/dist/config/sapConfig.d.ts.map +1 -0
  6. package/dist/config/sapConfig.js +26 -0
  7. package/dist/connection/AbapConnection.d.ts +4 -0
  8. package/dist/connection/AbapConnection.d.ts.map +1 -0
  9. package/dist/connection/AbapConnection.js +2 -0
  10. package/dist/connection/AbstractAbapConnection.d.ts +89 -0
  11. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
  12. package/dist/connection/AbstractAbapConnection.js +612 -0
  13. package/dist/connection/BaseAbapConnection.d.ts +19 -0
  14. package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
  15. package/dist/connection/BaseAbapConnection.js +65 -0
  16. package/dist/connection/GenericWebSocketTransport.d.ts +46 -0
  17. package/dist/connection/GenericWebSocketTransport.d.ts.map +1 -0
  18. package/dist/connection/GenericWebSocketTransport.js +135 -0
  19. package/dist/connection/JwtAbapConnection.d.ts +37 -0
  20. package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
  21. package/dist/connection/JwtAbapConnection.js +181 -0
  22. package/dist/connection/RfcAbapConnection.d.ts +49 -0
  23. package/dist/connection/RfcAbapConnection.d.ts.map +1 -0
  24. package/dist/connection/RfcAbapConnection.js +322 -0
  25. package/dist/connection/SamlAbapConnection.d.ts +21 -0
  26. package/dist/connection/SamlAbapConnection.d.ts.map +1 -0
  27. package/dist/connection/SamlAbapConnection.js +65 -0
  28. package/dist/connection/connectionFactory.d.ts +8 -0
  29. package/dist/connection/connectionFactory.d.ts.map +1 -0
  30. package/dist/connection/connectionFactory.js +23 -0
  31. package/dist/connection/csrfConfig.d.ts +46 -0
  32. package/dist/connection/csrfConfig.d.ts.map +1 -0
  33. package/dist/connection/csrfConfig.js +48 -0
  34. package/dist/index.d.ts +14 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +32 -0
  37. package/dist/logger.d.ts +3 -0
  38. package/dist/logger.d.ts.map +1 -0
  39. package/dist/logger.js +2 -0
  40. package/dist/utils/timeouts.d.ts +5 -0
  41. package/dist/utils/timeouts.d.ts.map +1 -0
  42. package/dist/utils/timeouts.js +21 -0
  43. package/dist/utils/tokenRefresh.d.ts +15 -0
  44. package/dist/utils/tokenRefresh.d.ts.map +1 -0
  45. package/dist/utils/tokenRefresh.js +55 -0
  46. package/package.json +70 -0
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseAbapConnection = void 0;
4
+ const axios_1 = require("axios");
5
+ const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
6
+ /**
7
+ * Basic Authentication connection for on-premise SAP systems
8
+ */
9
+ class BaseAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
10
+ constructor(config, logger, sessionId, options) {
11
+ BaseAbapConnection.validateConfig(config);
12
+ super(config, logger || null, sessionId, options);
13
+ }
14
+ /**
15
+ * Connect to SAP system with Basic Auth
16
+ * Fetches CSRF token which also establishes session cookies
17
+ */
18
+ async connect() {
19
+ const baseUrl = await this.getBaseUrl();
20
+ const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
21
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
22
+ try {
23
+ // Try to get CSRF token (this will also get cookies)
24
+ const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
25
+ this.setCsrfToken(token);
26
+ this.logger?.debug('Successfully connected to SAP system', {
27
+ hasCsrfToken: !!this.getCsrfToken(),
28
+ hasCookies: !!this.getCookies(),
29
+ cookieLength: this.getCookies()?.length || 0,
30
+ });
31
+ }
32
+ catch (error) {
33
+ // For Basic auth, log warning but don't fail
34
+ // The retry logic in makeAdtRequest will handle transient errors automatically
35
+ const errorMsg = error instanceof Error ? error.message : String(error);
36
+ this.logger?.warn(`[WARN] BaseAbapConnection - Could not connect to SAP system upfront: ${errorMsg}. Will retry on first request.`);
37
+ // Still try to extract cookies from error response if available
38
+ if (error instanceof axios_1.AxiosError && error.response?.headers) {
39
+ // updateCookiesFromResponse is private, but cookies are extracted in fetchCsrfToken
40
+ if (this.getCookies()) {
41
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Cookies extracted from error response during connect (first 100 chars): ${this.getCookies()?.substring(0, 100)}...`);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ buildAuthorizationHeader() {
47
+ const { username, password } = this.getConfig();
48
+ const safeUsername = username ?? '';
49
+ const safePassword = password ?? '';
50
+ const token = Buffer.from(`${safeUsername}:${safePassword}`).toString('base64');
51
+ return `Basic ${token}`;
52
+ }
53
+ static validateConfig(config) {
54
+ if (config.authType !== 'basic') {
55
+ throw new Error(`Basic authentication connection expects authType "basic", got "${config.authType}"`);
56
+ }
57
+ if (!config.username || !config.password) {
58
+ throw new Error('Basic authentication requires both username and password');
59
+ }
60
+ if (!config.client) {
61
+ throw new Error('Basic authentication requires SAP_CLIENT to be provided');
62
+ }
63
+ }
64
+ }
65
+ exports.BaseAbapConnection = BaseAbapConnection;
@@ -0,0 +1,46 @@
1
+ import type { IWebSocketCloseInfo, IWebSocketConnectOptions, IWebSocketMessageEnvelope, IWebSocketMessageHandler, IWebSocketTransport } from '@babamba2/mcp-abap-adt-interfaces';
2
+ /**
3
+ * Minimal WS-like instance contract to avoid hard dependency on a specific WS library.
4
+ */
5
+ interface IWebSocketLike {
6
+ readyState: number;
7
+ send(data: string): void;
8
+ close(code?: number, reason?: string): void;
9
+ onopen: ((event: unknown) => void) | null;
10
+ onmessage: ((event: {
11
+ data: unknown;
12
+ }) => void) | null;
13
+ onerror: ((event: unknown) => void) | null;
14
+ onclose: ((event: {
15
+ code?: number;
16
+ reason?: string;
17
+ wasClean?: boolean;
18
+ }) => void) | null;
19
+ }
20
+ interface IWebSocketFactory {
21
+ create(url: string, protocols?: string | string[], options?: IWebSocketConnectOptions): IWebSocketLike;
22
+ }
23
+ export declare class GenericWebSocketTransport implements IWebSocketTransport {
24
+ private readonly factory;
25
+ private socket;
26
+ private messageHandlers;
27
+ private openHandlers;
28
+ private errorHandlers;
29
+ private closeHandlers;
30
+ constructor(factory: IWebSocketFactory);
31
+ connect(url: string, options?: IWebSocketConnectOptions): Promise<void>;
32
+ disconnect(code?: number, reason?: string): Promise<void>;
33
+ send<T = unknown>(message: IWebSocketMessageEnvelope<T>): Promise<void>;
34
+ onMessage<T = unknown>(handler: IWebSocketMessageHandler<T>): void;
35
+ onOpen(handler: () => void | Promise<void>): void;
36
+ onError(handler: (error: Error) => void | Promise<void>): void;
37
+ onClose(handler: (info: IWebSocketCloseInfo) => void | Promise<void>): void;
38
+ isConnected(): boolean;
39
+ private handleIncomingMessage;
40
+ private normalizeError;
41
+ private emitOpen;
42
+ private emitError;
43
+ private emitClose;
44
+ }
45
+ export type { IWebSocketFactory, IWebSocketLike };
46
+ //# sourceMappingURL=GenericWebSocketTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GenericWebSocketTransport.d.ts","sourceRoot":"","sources":["../../src/connection/GenericWebSocketTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,mCAAmC,CAAC;AAE3C;;GAEG;AACH,UAAU,cAAc;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1C,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACvD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,OAAO,EACH,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC,GACzE,IAAI,CAAC;CACV;AAED,UAAU,iBAAiB;IACzB,MAAM,CACJ,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,EAC7B,OAAO,CAAC,EAAE,wBAAwB,GACjC,cAAc,CAAC;CACnB;AAID,qBAAa,yBAA0B,YAAW,mBAAmB;IASvD,OAAO,CAAC,QAAQ,CAAC,OAAO;IARpC,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,eAAe,CAAgD;IACvE,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,aAAa,CAAqD;IAC1E,OAAO,CAAC,aAAa,CAEd;gBAEsB,OAAO,EAAE,iBAAiB;IAEjD,OAAO,CACX,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,IAAI,CAAC;IAqDV,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBzD,IAAI,CAAC,CAAC,GAAG,OAAO,EACpB,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC;IAQhB,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,IAAI;IAIlE,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIjD,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI9D,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI3E,WAAW,IAAI,OAAO;YAIR,qBAAqB;IAoBnC,OAAO,CAAC,cAAc;YAUR,QAAQ;YAMR,SAAS;YAMT,SAAS;CAKxB;AAED,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GenericWebSocketTransport = void 0;
4
+ const WS_OPEN = 1;
5
+ class GenericWebSocketTransport {
6
+ factory;
7
+ socket = null;
8
+ messageHandlers = [];
9
+ openHandlers = [];
10
+ errorHandlers = [];
11
+ closeHandlers = [];
12
+ constructor(factory) {
13
+ this.factory = factory;
14
+ }
15
+ async connect(url, options) {
16
+ if (this.socket && this.isConnected()) {
17
+ return;
18
+ }
19
+ const connectTimeoutMs = options?.connectTimeoutMs ?? 15000;
20
+ const socket = this.factory.create(url, options?.protocols, options);
21
+ this.socket = socket;
22
+ await new Promise((resolve, reject) => {
23
+ const timeoutId = setTimeout(() => {
24
+ reject(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`));
25
+ }, connectTimeoutMs);
26
+ socket.onopen = () => {
27
+ clearTimeout(timeoutId);
28
+ this.emitOpen().catch(() => {
29
+ // no-op
30
+ });
31
+ resolve();
32
+ };
33
+ socket.onerror = (event) => {
34
+ clearTimeout(timeoutId);
35
+ const err = this.normalizeError(event, 'WebSocket connection error');
36
+ this.emitError(err).catch(() => {
37
+ // no-op
38
+ });
39
+ reject(err);
40
+ };
41
+ socket.onclose = (event) => {
42
+ clearTimeout(timeoutId);
43
+ const info = {
44
+ code: typeof event?.code === 'number' ? event.code : 1006,
45
+ reason: event?.reason,
46
+ wasClean: event?.wasClean,
47
+ };
48
+ this.emitClose(info).catch(() => {
49
+ // no-op
50
+ });
51
+ };
52
+ socket.onmessage = (event) => {
53
+ this.handleIncomingMessage(event?.data).catch(() => {
54
+ // no-op
55
+ });
56
+ };
57
+ });
58
+ }
59
+ async disconnect(code, reason) {
60
+ if (!this.socket) {
61
+ return;
62
+ }
63
+ const socket = this.socket;
64
+ this.socket = null;
65
+ try {
66
+ socket.close(code, reason);
67
+ }
68
+ catch (error) {
69
+ const err = this.normalizeError(error, 'WebSocket close failed');
70
+ await this.emitError(err);
71
+ }
72
+ }
73
+ async send(message) {
74
+ if (!this.socket || this.socket.readyState !== WS_OPEN) {
75
+ throw new Error('WebSocket is not connected');
76
+ }
77
+ this.socket.send(JSON.stringify(message));
78
+ }
79
+ onMessage(handler) {
80
+ this.messageHandlers.push(handler);
81
+ }
82
+ onOpen(handler) {
83
+ this.openHandlers.push(handler);
84
+ }
85
+ onError(handler) {
86
+ this.errorHandlers.push(handler);
87
+ }
88
+ onClose(handler) {
89
+ this.closeHandlers.push(handler);
90
+ }
91
+ isConnected() {
92
+ return !!this.socket && this.socket.readyState === WS_OPEN;
93
+ }
94
+ async handleIncomingMessage(rawData) {
95
+ try {
96
+ const text = typeof rawData === 'string'
97
+ ? rawData
98
+ : rawData instanceof Buffer
99
+ ? rawData.toString('utf8')
100
+ : String(rawData ?? '');
101
+ const parsed = JSON.parse(text);
102
+ for (const handler of this.messageHandlers) {
103
+ await handler(parsed);
104
+ }
105
+ }
106
+ catch (error) {
107
+ await this.emitError(this.normalizeError(error, 'Failed to process WS message'));
108
+ }
109
+ }
110
+ normalizeError(error, fallback) {
111
+ if (error instanceof Error) {
112
+ return error;
113
+ }
114
+ if (typeof error === 'string' && error.trim()) {
115
+ return new Error(error.trim());
116
+ }
117
+ return new Error(fallback);
118
+ }
119
+ async emitOpen() {
120
+ for (const handler of this.openHandlers) {
121
+ await handler();
122
+ }
123
+ }
124
+ async emitError(error) {
125
+ for (const handler of this.errorHandlers) {
126
+ await handler(error);
127
+ }
128
+ }
129
+ async emitClose(info) {
130
+ for (const handler of this.closeHandlers) {
131
+ await handler(info);
132
+ }
133
+ }
134
+ }
135
+ exports.GenericWebSocketTransport = GenericWebSocketTransport;
@@ -0,0 +1,37 @@
1
+ import type { IAdtResponse, ITokenRefresher } from '@babamba2/mcp-abap-adt-interfaces';
2
+ import type { SapConfig } from '../config/sapConfig.js';
3
+ import type { ILogger } from '../logger.js';
4
+ import type { AbapRequestOptions } from './AbapConnection.js';
5
+ import { AbstractAbapConnection } from './AbstractAbapConnection.js';
6
+ /**
7
+ * JWT Authentication connection for SAP BTP Cloud systems
8
+ *
9
+ * Supports automatic token refresh via ITokenRefresher injection:
10
+ * - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh
11
+ * - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior)
12
+ */
13
+ export declare class JwtAbapConnection extends AbstractAbapConnection {
14
+ private tokenRefresher?;
15
+ private currentToken;
16
+ constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string, tokenRefresher?: ITokenRefresher);
17
+ protected buildAuthorizationHeader(): string;
18
+ /**
19
+ * Refresh the JWT token using the injected tokenRefresher
20
+ * @returns true if token was refreshed, false if no refresher available
21
+ */
22
+ private tryRefreshToken;
23
+ /**
24
+ * Override connect to handle JWT token refresh on errors
25
+ */
26
+ connect(): Promise<void>;
27
+ /**
28
+ * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
29
+ */
30
+ makeAdtRequest<T = any, D = any>(options: AbapRequestOptions): Promise<IAdtResponse<T, D>>;
31
+ /**
32
+ * Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
33
+ */
34
+ protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number): Promise<string>;
35
+ private static validateConfig;
36
+ }
37
+ //# sourceMappingURL=JwtAbapConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,YAAY,CAAS;gBAG3B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe;IAWlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;;OAGG;YACW,eAAe;IA0B7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyD9B;;OAEG;IACG,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAyD9B;;OAEG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,SAAI,EACd,UAAU,SAAO,GAChB,OAAO,CAAC,MAAM,CAAC;IA2ClB,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JwtAbapConnection = void 0;
4
+ const axios_1 = require("axios");
5
+ const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
6
+ /**
7
+ * JWT Authentication connection for SAP BTP Cloud systems
8
+ *
9
+ * Supports automatic token refresh via ITokenRefresher injection:
10
+ * - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh
11
+ * - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior)
12
+ */
13
+ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
14
+ tokenRefresher;
15
+ currentToken;
16
+ constructor(config, logger, sessionId, tokenRefresher) {
17
+ JwtAbapConnection.validateConfig(config);
18
+ super(config, logger || null, sessionId);
19
+ this.tokenRefresher = tokenRefresher;
20
+ if (!config.jwtToken) {
21
+ throw new Error('jwtToken is required for JwtAbapConnection');
22
+ }
23
+ this.currentToken = config.jwtToken;
24
+ }
25
+ buildAuthorizationHeader() {
26
+ // Use currentToken which may have been refreshed
27
+ const tokenPreview = this.currentToken
28
+ ? `${this.currentToken.substring(0, 10)}...${this.currentToken.substring(Math.max(0, this.currentToken.length - 4))}`
29
+ : 'null';
30
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.buildAuthorizationHeader - Using token: ${tokenPreview}`);
31
+ return `Bearer ${this.currentToken}`;
32
+ }
33
+ /**
34
+ * Refresh the JWT token using the injected tokenRefresher
35
+ * @returns true if token was refreshed, false if no refresher available
36
+ */
37
+ async tryRefreshToken() {
38
+ if (!this.tokenRefresher) {
39
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - No tokenRefresher available, cannot refresh token`);
40
+ return false;
41
+ }
42
+ try {
43
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Refreshing token via tokenRefresher...`);
44
+ const newToken = await this.tokenRefresher.refreshToken();
45
+ this.currentToken = newToken;
46
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Token refreshed successfully`);
47
+ return true;
48
+ }
49
+ catch (error) {
50
+ this.logger?.error(`[ERROR] JwtAbapConnection - Failed to refresh token: ${error instanceof Error ? error.message : String(error)}`);
51
+ return false;
52
+ }
53
+ }
54
+ /**
55
+ * Override connect to handle JWT token refresh on errors
56
+ */
57
+ async connect() {
58
+ const baseUrl = await this.getBaseUrl();
59
+ const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
60
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
61
+ try {
62
+ // Try to get CSRF token (this will also get cookies)
63
+ const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
64
+ this.setCsrfToken(token);
65
+ this.logger?.debug('Successfully connected to SAP system', {
66
+ hasCsrfToken: !!this.getCsrfToken(),
67
+ hasCookies: !!this.getCookies(),
68
+ cookieLength: this.getCookies()?.length || 0,
69
+ });
70
+ }
71
+ catch (error) {
72
+ // Handle JWT auth errors (401/403) during connect
73
+ if (error instanceof axios_1.AxiosError &&
74
+ (error.response?.status === 401 || error.response?.status === 403)) {
75
+ // Check if this is really an auth error, not a permissions error
76
+ const responseData = error.response?.data;
77
+ const responseText = typeof responseData === 'string'
78
+ ? responseData
79
+ : JSON.stringify(responseData || '');
80
+ // Don't retry on "No Access" errors
81
+ if (responseText.includes('ExceptionResourceNoAccess') ||
82
+ responseText.includes('No authorization') ||
83
+ responseText.includes('Missing authorization')) {
84
+ throw error;
85
+ }
86
+ // Try to refresh token if tokenRefresher is available
87
+ if (await this.tryRefreshToken()) {
88
+ // Retry connect with new token
89
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying connect after token refresh...`);
90
+ return this.connect();
91
+ }
92
+ throw new Error('JWT token has expired. Please re-authenticate.');
93
+ }
94
+ // Re-throw other errors
95
+ throw error;
96
+ }
97
+ }
98
+ /**
99
+ * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
100
+ */
101
+ async makeAdtRequest(options) {
102
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Starting request: ${options.method} ${options.url}`);
103
+ try {
104
+ const response = await super.makeAdtRequest(options);
105
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request succeeded: ${response.status}`);
106
+ return response;
107
+ }
108
+ catch (error) {
109
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request failed: ${error instanceof Error ? error.message : String(error)}`);
110
+ // Handle JWT auth errors (401/403)
111
+ if (error instanceof axios_1.AxiosError &&
112
+ (error.response?.status === 401 || error.response?.status === 403)) {
113
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status}, attempting token refresh...`);
114
+ // Check if this is really an auth error, not a permissions error
115
+ const responseData = error.response?.data;
116
+ const responseText = typeof responseData === 'string'
117
+ ? responseData
118
+ : JSON.stringify(responseData || '');
119
+ // Don't retry on "No Access" errors - these are permission issues, not auth issues
120
+ if (responseText.includes('ExceptionResourceNoAccess') ||
121
+ responseText.includes('No authorization') ||
122
+ responseText.includes('Missing authorization')) {
123
+ throw error;
124
+ }
125
+ // Try to refresh token if tokenRefresher is available
126
+ if (await this.tryRefreshToken()) {
127
+ // Reset connection state and retry request with new token
128
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Retrying request after token refresh...`);
129
+ this.reset();
130
+ return super.makeAdtRequest(options);
131
+ }
132
+ throw new Error('JWT token has expired. Please re-authenticate.');
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+ /**
138
+ * Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
139
+ */
140
+ async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) {
141
+ try {
142
+ // Try to fetch CSRF token using parent implementation
143
+ return await super.fetchCsrfToken(url, retryCount, retryDelay);
144
+ }
145
+ catch (error) {
146
+ // Handle JWT auth errors (401/403) during CSRF token fetch
147
+ if (error instanceof axios_1.AxiosError &&
148
+ (error.response?.status === 401 || error.response?.status === 403)) {
149
+ // Check if this is really an auth error, not a permissions error
150
+ const responseData = error.response?.data;
151
+ const responseText = typeof responseData === 'string'
152
+ ? responseData
153
+ : JSON.stringify(responseData || '');
154
+ // Don't retry on "No Access" errors
155
+ if (responseText.includes('ExceptionResourceNoAccess') ||
156
+ responseText.includes('No authorization') ||
157
+ responseText.includes('Missing authorization')) {
158
+ throw error;
159
+ }
160
+ // Try to refresh token if tokenRefresher is available
161
+ if (await this.tryRefreshToken()) {
162
+ // Retry CSRF token fetch with new token
163
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.fetchCsrfToken - Retrying after token refresh...`);
164
+ return super.fetchCsrfToken(url, retryCount, retryDelay);
165
+ }
166
+ throw new Error('JWT token has expired. Please re-authenticate.');
167
+ }
168
+ // Re-throw other errors
169
+ throw error;
170
+ }
171
+ }
172
+ static validateConfig(config) {
173
+ if (config.authType !== 'jwt') {
174
+ throw new Error(`JWT connection expects authType "jwt", got "${config.authType}"`);
175
+ }
176
+ if (!config.jwtToken) {
177
+ throw new Error('JWT authentication requires SAP_JWT_TOKEN to be provided');
178
+ }
179
+ }
180
+ }
181
+ exports.JwtAbapConnection = JwtAbapConnection;
@@ -0,0 +1,49 @@
1
+ import type { IAbapRequestOptions, IAdtResponse } from '@babamba2/mcp-abap-adt-interfaces';
2
+ import type { SapConfig } from '../config/sapConfig.js';
3
+ import type { ILogger } from '../logger.js';
4
+ import type { AbapConnection } from './AbapConnection.js';
5
+ /**
6
+ * RFC-based connection for on-premise SAP systems.
7
+ *
8
+ * Uses node-rfc to call SADT_REST_RFC_ENDPOINT — the same standard SAP FM
9
+ * that Eclipse ADT uses for all on-premise ADT operations via JCo.
10
+ *
11
+ * RFC connections are inherently stateful: one ABAP session persists for the
12
+ * entire connection lifetime. This solves the HTTP 423 "invalid lock handle"
13
+ * problem on legacy systems (BASIS < 7.50) where HTTP stateful sessions
14
+ * are not supported.
15
+ *
16
+ * Connection parameters are derived from the standard ISapConfig.url field:
17
+ * http://saphost:8000 → ashost=saphost, sysnr=00
18
+ *
19
+ * Prerequisites:
20
+ * - SAP NW RFC SDK installed on the machine
21
+ * - node-rfc package installed: npm install node-rfc
22
+ */
23
+ export declare class RfcAbapConnection implements AbapConnection {
24
+ private readonly config;
25
+ private readonly logger;
26
+ private rfcClient;
27
+ private readonly sessionId;
28
+ private readonly baseUrl;
29
+ private readonly rfcParams;
30
+ private sessionType;
31
+ private sessionCookie;
32
+ constructor(config: SapConfig, logger?: ILogger | null);
33
+ connect(): Promise<void>;
34
+ getBaseUrl(): Promise<string>;
35
+ getSessionId(): string | null;
36
+ setSessionType(type: 'stateful' | 'stateless'): void;
37
+ makeAdtRequest<T = any, D = any>(options: IAbapRequestOptions): Promise<IAdtResponse<T, D>>;
38
+ /**
39
+ * Reset the connection — for RFC this closes the session.
40
+ * Provides interface compatibility with HTTP connections.
41
+ */
42
+ reset(): void;
43
+ /**
44
+ * Close the RFC connection and release the ABAP session.
45
+ */
46
+ close(): Promise<void>;
47
+ private static validateConfig;
48
+ }
49
+ //# sourceMappingURL=RfcAbapConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RfcAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/RfcAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACb,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA0F1D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IASpD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsB;IAChD,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,aAAa,CAAuB;gBAGzB,MAAM,EAAE,SAAS,EACjB,MAAM,GAAE,OAAO,GAAG,IAAW;IAa1C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BxB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAInC,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAU9C,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IA8L9B;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,OAAO,CAAC,MAAM,CAAC,cAAc;CAqB9B"}