@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.
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/bin/sap-abap-auth.js +665 -0
- package/dist/config/sapConfig.d.ts +5 -0
- package/dist/config/sapConfig.d.ts.map +1 -0
- package/dist/config/sapConfig.js +26 -0
- package/dist/connection/AbapConnection.d.ts +4 -0
- package/dist/connection/AbapConnection.d.ts.map +1 -0
- package/dist/connection/AbapConnection.js +2 -0
- package/dist/connection/AbstractAbapConnection.d.ts +89 -0
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
- package/dist/connection/AbstractAbapConnection.js +612 -0
- package/dist/connection/BaseAbapConnection.d.ts +19 -0
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
- package/dist/connection/BaseAbapConnection.js +65 -0
- package/dist/connection/GenericWebSocketTransport.d.ts +46 -0
- package/dist/connection/GenericWebSocketTransport.d.ts.map +1 -0
- package/dist/connection/GenericWebSocketTransport.js +135 -0
- package/dist/connection/JwtAbapConnection.d.ts +37 -0
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
- package/dist/connection/JwtAbapConnection.js +181 -0
- package/dist/connection/RfcAbapConnection.d.ts +49 -0
- package/dist/connection/RfcAbapConnection.d.ts.map +1 -0
- package/dist/connection/RfcAbapConnection.js +322 -0
- package/dist/connection/SamlAbapConnection.d.ts +21 -0
- package/dist/connection/SamlAbapConnection.d.ts.map +1 -0
- package/dist/connection/SamlAbapConnection.js +65 -0
- package/dist/connection/connectionFactory.d.ts +8 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -0
- package/dist/connection/connectionFactory.js +23 -0
- package/dist/connection/csrfConfig.d.ts +46 -0
- package/dist/connection/csrfConfig.d.ts.map +1 -0
- package/dist/connection/csrfConfig.js +48 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +2 -0
- package/dist/utils/timeouts.d.ts +5 -0
- package/dist/utils/timeouts.d.ts.map +1 -0
- package/dist/utils/timeouts.js +21 -0
- package/dist/utils/tokenRefresh.d.ts +15 -0
- package/dist/utils/tokenRefresh.d.ts.map +1 -0
- package/dist/utils/tokenRefresh.js +55 -0
- 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"}
|