@mcp-abap-adt/connection 0.1.0
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 +80 -0
- package/bin/sap-abap-auth.js +600 -0
- package/dist/config/sapConfig.d.ts +43 -0
- package/dist/config/sapConfig.d.ts.map +1 -0
- package/dist/config/sapConfig.js +202 -0
- package/dist/connection/AbapConnection.d.ts +22 -0
- package/dist/connection/AbapConnection.d.ts.map +1 -0
- package/dist/connection/AbapConnection.js +2 -0
- package/dist/connection/AbstractAbapConnection.d.ts +115 -0
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
- package/dist/connection/AbstractAbapConnection.js +716 -0
- package/dist/connection/BaseAbapConnection.d.ts +17 -0
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
- package/dist/connection/BaseAbapConnection.js +68 -0
- package/dist/connection/JwtAbapConnection.d.ts +33 -0
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
- package/dist/connection/JwtAbapConnection.js +305 -0
- package/dist/connection/connectionFactory.d.ts +5 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -0
- package/dist/connection/connectionFactory.js +15 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/logger.d.ts +67 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +2 -0
- package/dist/utils/FileSessionStorage.d.ts +73 -0
- package/dist/utils/FileSessionStorage.d.ts.map +1 -0
- package/dist/utils/FileSessionStorage.js +191 -0
- package/dist/utils/timeouts.d.ts +8 -0
- package/dist/utils/timeouts.d.ts.map +1 -0
- package/dist/utils/timeouts.js +21 -0
- package/dist/utils/tokenRefresh.d.ts +17 -0
- package/dist/utils/tokenRefresh.d.ts.map +1 -0
- package/dist/utils/tokenRefresh.js +53 -0
- package/package.json +63 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SapConfig } from "../config/sapConfig.js";
|
|
2
|
+
import { AbstractAbapConnection } from "./AbstractAbapConnection.js";
|
|
3
|
+
import { ILogger, ISessionStorage } from "../logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Basic Authentication connection for on-premise SAP systems
|
|
6
|
+
*/
|
|
7
|
+
export declare class BaseAbapConnection extends AbstractAbapConnection {
|
|
8
|
+
constructor(config: SapConfig, logger: ILogger, sessionStorage?: ISessionStorage, sessionId?: string);
|
|
9
|
+
/**
|
|
10
|
+
* Connect to SAP system with Basic Auth
|
|
11
|
+
* Fetches CSRF token which also establishes session cookies
|
|
12
|
+
*/
|
|
13
|
+
connect(): Promise<void>;
|
|
14
|
+
protected buildAuthorizationHeader(): string;
|
|
15
|
+
private static validateConfig;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=BaseAbapConnection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/BaseAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGxD;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;gBAE1D,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM;IAMpB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC9B,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAQ5C,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseAbapConnection = void 0;
|
|
4
|
+
const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
|
|
5
|
+
const axios_1 = require("axios");
|
|
6
|
+
/**
|
|
7
|
+
* Basic Authentication connection for on-premise SAP systems
|
|
8
|
+
*/
|
|
9
|
+
class BaseAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
|
|
10
|
+
constructor(config, logger, sessionStorage, sessionId) {
|
|
11
|
+
BaseAbapConnection.validateConfig(config);
|
|
12
|
+
super(config, logger, sessionStorage, sessionId);
|
|
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
|
+
// Save session state after successful connection
|
|
27
|
+
await this.saveSessionState();
|
|
28
|
+
this.logger.debug("Successfully connected to SAP system", {
|
|
29
|
+
hasCsrfToken: !!this.getCsrfToken(),
|
|
30
|
+
hasCookies: !!this.getCookies(),
|
|
31
|
+
cookieLength: this.getCookies()?.length || 0
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// For Basic auth, log warning but don't fail
|
|
36
|
+
// The retry logic in makeAdtRequest will handle transient errors automatically
|
|
37
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
38
|
+
this.logger.warn(`[WARN] BaseAbapConnection - Could not connect to SAP system upfront: ${errorMsg}. Will retry on first request.`);
|
|
39
|
+
// Still try to extract cookies from error response if available
|
|
40
|
+
if (error instanceof axios_1.AxiosError && error.response?.headers) {
|
|
41
|
+
// updateCookiesFromResponse is private, but cookies are extracted in fetchCsrfToken
|
|
42
|
+
if (this.getCookies()) {
|
|
43
|
+
this.logger.debug(`[DEBUG] BaseAbapConnection - Cookies extracted from error response during connect (first 100 chars): ${this.getCookies().substring(0, 100)}...`);
|
|
44
|
+
await this.saveSessionState();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
buildAuthorizationHeader() {
|
|
50
|
+
const { username, password } = this.getConfig();
|
|
51
|
+
const safeUsername = username ?? "";
|
|
52
|
+
const safePassword = password ?? "";
|
|
53
|
+
const token = Buffer.from(`${safeUsername}:${safePassword}`).toString("base64");
|
|
54
|
+
return `Basic ${token}`;
|
|
55
|
+
}
|
|
56
|
+
static validateConfig(config) {
|
|
57
|
+
if (config.authType !== "basic") {
|
|
58
|
+
throw new Error(`Basic authentication connection expects authType "basic", got "${config.authType}"`);
|
|
59
|
+
}
|
|
60
|
+
if (!config.username || !config.password) {
|
|
61
|
+
throw new Error("Basic authentication requires both username and password");
|
|
62
|
+
}
|
|
63
|
+
if (!config.client) {
|
|
64
|
+
throw new Error("Basic authentication requires SAP_CLIENT to be provided");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.BaseAbapConnection = BaseAbapConnection;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SapConfig } from "../config/sapConfig.js";
|
|
2
|
+
import { AbstractAbapConnection } from "./AbstractAbapConnection.js";
|
|
3
|
+
import { AbapRequestOptions } from "./AbapConnection.js";
|
|
4
|
+
import { ILogger, ISessionStorage } from "../logger.js";
|
|
5
|
+
import { AxiosResponse } from "axios";
|
|
6
|
+
/**
|
|
7
|
+
* JWT Authentication connection for SAP BTP Cloud systems
|
|
8
|
+
* Supports automatic token refresh using OAuth2 refresh tokens
|
|
9
|
+
*/
|
|
10
|
+
export declare class JwtAbapConnection extends AbstractAbapConnection {
|
|
11
|
+
private tokenRefreshInProgress;
|
|
12
|
+
constructor(config: SapConfig, logger: ILogger, sessionStorage?: ISessionStorage, sessionId?: string);
|
|
13
|
+
protected buildAuthorizationHeader(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Refresh JWT token using refresh token
|
|
16
|
+
* @returns Promise that resolves when token is refreshed
|
|
17
|
+
*/
|
|
18
|
+
refreshToken(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Check if token refresh is possible
|
|
21
|
+
*/
|
|
22
|
+
canRefreshToken(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Override connect to handle JWT token refresh on errors
|
|
25
|
+
*/
|
|
26
|
+
connect(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Override makeAdtRequest to handle JWT token refresh on 401/403
|
|
29
|
+
*/
|
|
30
|
+
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
31
|
+
private static validateConfig;
|
|
32
|
+
}
|
|
33
|
+
//# 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,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,eAAe,EAAgB,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;AAElD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,sBAAsB,CAAkB;gBAG9C,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM;IAMpB,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAS5C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6EnC;;OAEG;IACH,eAAe,IAAI,OAAO;IAU1B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2G9B;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgGzE,OAAO,CAAC,MAAM,CAAC,cAAc;CAS9B"}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JwtAbapConnection = void 0;
|
|
4
|
+
const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
|
|
5
|
+
const tokenRefresh_js_1 = require("../utils/tokenRefresh.js");
|
|
6
|
+
const axios_1 = require("axios");
|
|
7
|
+
/**
|
|
8
|
+
* JWT Authentication connection for SAP BTP Cloud systems
|
|
9
|
+
* Supports automatic token refresh using OAuth2 refresh tokens
|
|
10
|
+
*/
|
|
11
|
+
class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
|
|
12
|
+
tokenRefreshInProgress = false;
|
|
13
|
+
constructor(config, logger, sessionStorage, sessionId) {
|
|
14
|
+
JwtAbapConnection.validateConfig(config);
|
|
15
|
+
super(config, logger, sessionStorage, sessionId);
|
|
16
|
+
}
|
|
17
|
+
buildAuthorizationHeader() {
|
|
18
|
+
const config = this.getConfig();
|
|
19
|
+
const { jwtToken } = config;
|
|
20
|
+
// Log token preview for debugging (first 10 and last 4 chars)
|
|
21
|
+
const tokenPreview = jwtToken ? `${jwtToken.substring(0, 10)}...${jwtToken.substring(Math.max(0, jwtToken.length - 4))}` : 'null';
|
|
22
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.buildAuthorizationHeader - Using token: ${tokenPreview}`);
|
|
23
|
+
return `Bearer ${jwtToken}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Refresh JWT token using refresh token
|
|
27
|
+
* @returns Promise that resolves when token is refreshed
|
|
28
|
+
*/
|
|
29
|
+
async refreshToken() {
|
|
30
|
+
const config = this.getConfig();
|
|
31
|
+
if (!config.refreshToken) {
|
|
32
|
+
throw new Error("Refresh token is not available. Please re-authenticate.");
|
|
33
|
+
}
|
|
34
|
+
if (!config.uaaUrl || !config.uaaClientId || !config.uaaClientSecret) {
|
|
35
|
+
throw new Error("UAA credentials are not available for token refresh. " +
|
|
36
|
+
"Please provide UAA_URL, UAA_CLIENT_ID, and UAA_CLIENT_SECRET in configuration or re-authenticate.");
|
|
37
|
+
}
|
|
38
|
+
// Prevent concurrent refresh attempts
|
|
39
|
+
if (this.tokenRefreshInProgress) {
|
|
40
|
+
this.logger.debug("Token refresh already in progress, waiting...");
|
|
41
|
+
// Wait for ongoing refresh to complete
|
|
42
|
+
while (this.tokenRefreshInProgress) {
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.tokenRefreshInProgress = true;
|
|
48
|
+
try {
|
|
49
|
+
this.logger.debug("Refreshing JWT token...");
|
|
50
|
+
const tokens = await (0, tokenRefresh_js_1.refreshJwtToken)(config.refreshToken, config.uaaUrl, config.uaaClientId, config.uaaClientSecret);
|
|
51
|
+
// Update config with new tokens
|
|
52
|
+
// NOTE: This updates the config object directly, which is shared with the connection cache
|
|
53
|
+
// The connection cache will be invalidated on next getManagedConnection() call because
|
|
54
|
+
// sapConfigSignature includes token preview, so signature will change
|
|
55
|
+
const oldTokenPreview = config.jwtToken ? `${config.jwtToken.substring(0, 10)}...${config.jwtToken.substring(Math.max(0, config.jwtToken.length - 4))}` : 'null';
|
|
56
|
+
config.jwtToken = tokens.accessToken;
|
|
57
|
+
if (tokens.refreshToken) {
|
|
58
|
+
config.refreshToken = tokens.refreshToken;
|
|
59
|
+
}
|
|
60
|
+
const newTokenPreview = config.jwtToken ? `${config.jwtToken.substring(0, 10)}...${config.jwtToken.substring(Math.max(0, config.jwtToken.length - 4))}` : 'null';
|
|
61
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.refreshToken - Token updated in config: ${oldTokenPreview} -> ${newTokenPreview}`);
|
|
62
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.refreshToken - Config object reference check: ${config === this.getConfig() ? 'same object ✓' : 'different object ✗'}`);
|
|
63
|
+
// Clear CSRF token and cookies to force new session with new token
|
|
64
|
+
// IMPORTANT: After token refresh, we must clear saved session state because
|
|
65
|
+
// old cookies/CSRF token are tied to the old JWT token and won't work with new token
|
|
66
|
+
this.reset();
|
|
67
|
+
// Also clear saved session state from storage if using stateful session
|
|
68
|
+
// This prevents reloading old cookies/CSRF token that are tied to old JWT token
|
|
69
|
+
const sessionStorage = this.getSessionStorage();
|
|
70
|
+
const sessionId = this.getSessionId();
|
|
71
|
+
if (sessionStorage && sessionId) {
|
|
72
|
+
try {
|
|
73
|
+
await this.clearSessionState();
|
|
74
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.refreshToken - Cleared saved session state from storage`);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.logger.warn(`[DEBUG] JwtAbapConnection.refreshToken - Failed to clear session state: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.logger.debug("JWT token refreshed successfully");
|
|
81
|
+
this.logger.debug("NOTE: Connection cache will be invalidated on next getManagedConnection() call due to changed token signature");
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.logger.error(`Failed to refresh JWT token: ${error.message}`);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.tokenRefreshInProgress = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if token refresh is possible
|
|
93
|
+
*/
|
|
94
|
+
canRefreshToken() {
|
|
95
|
+
const config = this.getConfig();
|
|
96
|
+
return !!(config.refreshToken &&
|
|
97
|
+
config.uaaUrl &&
|
|
98
|
+
config.uaaClientId &&
|
|
99
|
+
config.uaaClientSecret);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Override connect to handle JWT token refresh on errors
|
|
103
|
+
*/
|
|
104
|
+
async connect() {
|
|
105
|
+
const baseUrl = await this.getBaseUrl();
|
|
106
|
+
const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
|
|
107
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
|
|
108
|
+
// If we have saved session state, load it first to compare later
|
|
109
|
+
const sessionStorage = this.getSessionStorage();
|
|
110
|
+
const sessionId = this.getSessionId();
|
|
111
|
+
let savedState = null;
|
|
112
|
+
if (sessionStorage && sessionId) {
|
|
113
|
+
try {
|
|
114
|
+
savedState = await sessionStorage.load(sessionId);
|
|
115
|
+
if (savedState) {
|
|
116
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.connect - Loaded saved session state for comparison`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.connect - No saved session state found or failed to load`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
// Try to get CSRF token (this will also get cookies)
|
|
125
|
+
const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
|
|
126
|
+
this.setCsrfToken(token);
|
|
127
|
+
// Compare new session state with saved state
|
|
128
|
+
const newState = {
|
|
129
|
+
cookies: this.getCookies(),
|
|
130
|
+
csrfToken: this.getCsrfToken(),
|
|
131
|
+
cookieStore: Object.fromEntries(this.cookieStore || new Map())
|
|
132
|
+
};
|
|
133
|
+
// Only save if session state changed
|
|
134
|
+
if (savedState) {
|
|
135
|
+
const cookiesChanged = savedState.cookies !== newState.cookies;
|
|
136
|
+
const csrfTokenChanged = savedState.csrfToken !== newState.csrfToken;
|
|
137
|
+
const cookieStoreChanged = JSON.stringify(savedState.cookieStore) !== JSON.stringify(newState.cookieStore);
|
|
138
|
+
if (cookiesChanged || csrfTokenChanged || cookieStoreChanged) {
|
|
139
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.connect - Session state changed, saving new state`, {
|
|
140
|
+
cookiesChanged,
|
|
141
|
+
csrfTokenChanged,
|
|
142
|
+
cookieStoreChanged
|
|
143
|
+
});
|
|
144
|
+
await this.saveSessionState();
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.connect - Session state unchanged, not saving`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// No saved state, save new one
|
|
152
|
+
await this.saveSessionState();
|
|
153
|
+
}
|
|
154
|
+
this.logger.debug("Successfully connected to SAP system", {
|
|
155
|
+
hasCsrfToken: !!this.getCsrfToken(),
|
|
156
|
+
hasCookies: !!this.getCookies(),
|
|
157
|
+
cookieLength: this.getCookies()?.length || 0
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// Handle JWT auth errors (401/403) during connect
|
|
162
|
+
if (error instanceof axios_1.AxiosError &&
|
|
163
|
+
(error.response?.status === 401 || error.response?.status === 403)) {
|
|
164
|
+
// Check if this is really an auth error, not a permissions error
|
|
165
|
+
const responseData = error.response?.data;
|
|
166
|
+
const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
|
|
167
|
+
// Don't retry on "No Access" errors
|
|
168
|
+
if (responseText.includes("ExceptionResourceNoAccess") ||
|
|
169
|
+
responseText.includes("No authorization") ||
|
|
170
|
+
responseText.includes("Missing authorization")) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
// Try token refresh if possible
|
|
174
|
+
if (this.canRefreshToken()) {
|
|
175
|
+
try {
|
|
176
|
+
this.logger.debug(`Received ${error.response.status} during connect, attempting JWT token refresh...`);
|
|
177
|
+
await this.refreshToken();
|
|
178
|
+
this.logger.debug(`✓ Token refreshed successfully, retrying connect...`);
|
|
179
|
+
// Retry CSRF token fetch with new JWT token
|
|
180
|
+
const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
|
|
181
|
+
this.setCsrfToken(token);
|
|
182
|
+
await this.saveSessionState();
|
|
183
|
+
this.logger.debug("Successfully connected after JWT refresh", {
|
|
184
|
+
hasCsrfToken: !!this.getCsrfToken(),
|
|
185
|
+
hasCookies: !!this.getCookies()
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
catch (refreshError) {
|
|
190
|
+
this.logger.error(`❌ Token refresh failed during connect: ${refreshError.message}`);
|
|
191
|
+
throw new Error("JWT token has expired and refresh failed. Please re-authenticate.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw new Error("JWT token has expired. Please refresh your authentication token.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Re-throw other errors
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Override makeAdtRequest to handle JWT token refresh on 401/403
|
|
204
|
+
*/
|
|
205
|
+
async makeAdtRequest(options) {
|
|
206
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Starting request: ${options.method} ${options.url}`);
|
|
207
|
+
try {
|
|
208
|
+
const response = await super.makeAdtRequest(options);
|
|
209
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request succeeded: ${response.status}`);
|
|
210
|
+
return response;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
214
|
+
// Handle JWT auth errors (401/403)
|
|
215
|
+
if (error instanceof axios_1.AxiosError &&
|
|
216
|
+
(error.response?.status === 401 || error.response?.status === 403)) {
|
|
217
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status}, checking if refresh is possible...`);
|
|
218
|
+
// Check if this is really an auth error, not a permissions error
|
|
219
|
+
const responseData = error.response?.data;
|
|
220
|
+
const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
|
|
221
|
+
// Don't retry on "No Access" errors - these are permission issues, not auth issues
|
|
222
|
+
if (responseText.includes("ExceptionResourceNoAccess") ||
|
|
223
|
+
responseText.includes("No authorization") ||
|
|
224
|
+
responseText.includes("Missing authorization")) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
// Try token refresh if possible
|
|
228
|
+
const canRefresh = this.canRefreshToken();
|
|
229
|
+
const config = this.getConfig();
|
|
230
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - canRefreshToken: ${canRefresh}`, {
|
|
231
|
+
hasRefreshToken: !!config.refreshToken,
|
|
232
|
+
hasUaaUrl: !!config.uaaUrl,
|
|
233
|
+
hasUaaClientId: !!config.uaaClientId,
|
|
234
|
+
hasUaaClientSecret: !!config.uaaClientSecret
|
|
235
|
+
});
|
|
236
|
+
if (canRefresh) {
|
|
237
|
+
// Step 1: Refresh token
|
|
238
|
+
try {
|
|
239
|
+
this.logger.debug(`Received ${error.response.status}, attempting JWT token refresh...`);
|
|
240
|
+
await this.refreshToken();
|
|
241
|
+
this.logger.debug(`✓ Token refreshed successfully, reconnecting to get new CSRF token...`);
|
|
242
|
+
}
|
|
243
|
+
catch (refreshError) {
|
|
244
|
+
// Only catch errors from refreshToken()
|
|
245
|
+
this.logger.error(`❌ Token refresh failed: ${refreshError.message}`);
|
|
246
|
+
throw new Error("JWT token has expired and refresh failed. Please re-authenticate.");
|
|
247
|
+
}
|
|
248
|
+
// Step 2: Reconnect to get new CSRF token and cookies
|
|
249
|
+
try {
|
|
250
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection - Calling connect() after token refresh to get CSRF token...`);
|
|
251
|
+
await this.connect();
|
|
252
|
+
const hasCsrf = !!this.getCsrfToken();
|
|
253
|
+
const hasCookies = !!this.getCookies();
|
|
254
|
+
this.logger.debug(`✓ Reconnected successfully after token refresh`, {
|
|
255
|
+
hasCsrfToken: hasCsrf,
|
|
256
|
+
hasCookies: hasCookies,
|
|
257
|
+
csrfTokenLength: this.getCsrfToken()?.length || 0,
|
|
258
|
+
cookiesLength: this.getCookies()?.length || 0
|
|
259
|
+
});
|
|
260
|
+
if (!hasCsrf) {
|
|
261
|
+
this.logger.error(`❌ CRITICAL: CSRF token not obtained after reconnect! Request may fail.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (connectError) {
|
|
265
|
+
this.logger.error(`❌ Failed to reconnect after token refresh: ${connectError.message}`);
|
|
266
|
+
this.logger.error(`❌ This means CSRF token will not be available for POST/PUT/DELETE requests`);
|
|
267
|
+
// Continue anyway - ensureFreshCsrfToken will try to get CSRF token during request retry
|
|
268
|
+
// But this is likely to fail if connect() failed
|
|
269
|
+
}
|
|
270
|
+
// Step 3: Retry the request with new token
|
|
271
|
+
// ensureFreshCsrfToken will be called automatically if CSRF token is missing
|
|
272
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection - Retrying ADT request after token refresh...`);
|
|
273
|
+
try {
|
|
274
|
+
return await super.makeAdtRequest(options);
|
|
275
|
+
}
|
|
276
|
+
catch (retryError) {
|
|
277
|
+
// If retry fails with 401/403, it means token refresh didn't help - re-throw as auth error
|
|
278
|
+
if (retryError instanceof axios_1.AxiosError &&
|
|
279
|
+
(retryError.response?.status === 401 || retryError.response?.status === 403)) {
|
|
280
|
+
this.logger.error(`❌ Token refresh didn't help - still getting ${retryError.response.status}`);
|
|
281
|
+
throw new Error("JWT token has expired and refresh failed. Please re-authenticate.");
|
|
282
|
+
}
|
|
283
|
+
// For other errors (400, 500, etc.), re-throw the original error
|
|
284
|
+
// These are not auth errors, so they should be handled by the caller
|
|
285
|
+
this.logger.debug(`[DEBUG] JwtAbapConnection - Retry request failed with non-auth error: ${retryError.response?.status || 'unknown'}`);
|
|
286
|
+
throw retryError;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw new Error("JWT token has expired. Please refresh your authentication token.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
static validateConfig(config) {
|
|
297
|
+
if (config.authType !== "jwt") {
|
|
298
|
+
throw new Error(`JWT connection expects authType "jwt", got "${config.authType}"`);
|
|
299
|
+
}
|
|
300
|
+
if (!config.jwtToken) {
|
|
301
|
+
throw new Error("JWT authentication requires SAP_JWT_TOKEN to be provided");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.JwtAbapConnection = JwtAbapConnection;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SapConfig } from "../config/sapConfig.js";
|
|
2
|
+
import { AbapConnection } from "./AbapConnection.js";
|
|
3
|
+
import { ILogger, ISessionStorage } from "../logger.js";
|
|
4
|
+
export declare function createAbapConnection(config: SapConfig, logger: ILogger, sessionStorage?: ISessionStorage, sessionId?: string): AbapConnection;
|
|
5
|
+
//# sourceMappingURL=connectionFactory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connectionFactory.d.ts","sourceRoot":"","sources":["../../src/connection/connectionFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM,GACjB,cAAc,CAShB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAbapConnection = createAbapConnection;
|
|
4
|
+
const JwtAbapConnection_js_1 = require("./JwtAbapConnection.js");
|
|
5
|
+
const BaseAbapConnection_js_1 = require("./BaseAbapConnection.js");
|
|
6
|
+
function createAbapConnection(config, logger, sessionStorage, sessionId) {
|
|
7
|
+
switch (config.authType) {
|
|
8
|
+
case "basic":
|
|
9
|
+
return new BaseAbapConnection_js_1.BaseAbapConnection(config, logger, sessionStorage, sessionId);
|
|
10
|
+
case "jwt":
|
|
11
|
+
return new JwtAbapConnection_js_1.JwtAbapConnection(config, logger, sessionStorage, sessionId);
|
|
12
|
+
default:
|
|
13
|
+
throw new Error(`Unsupported SAP authentication type: ${config.authType}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { SapConfig, SapAuthType, } from "./config/sapConfig.js";
|
|
2
|
+
export type { AbapRequestOptions } from "./connection/AbapConnection.js";
|
|
3
|
+
export { type AbapConnection } from "./connection/AbapConnection.js";
|
|
4
|
+
export type { ILogger, SessionState, ISessionStorage } from "./logger.js";
|
|
5
|
+
export { FileSessionStorage, type FileSessionStorageOptions } from "./utils/FileSessionStorage.js";
|
|
6
|
+
export { BaseAbapConnection } from "./connection/BaseAbapConnection.js";
|
|
7
|
+
export { JwtAbapConnection } from "./connection/JwtAbapConnection.js";
|
|
8
|
+
export { BaseAbapConnection as OnPremAbapConnection } from "./connection/BaseAbapConnection.js";
|
|
9
|
+
export { JwtAbapConnection as CloudAbapConnection } from "./connection/JwtAbapConnection.js";
|
|
10
|
+
export { createAbapConnection } from "./connection/connectionFactory.js";
|
|
11
|
+
export { sapConfigSignature, getConfigFromEnv, loadEnvFile, loadConfigFromEnvFile } from "./config/sapConfig.js";
|
|
12
|
+
export { getTimeout, getTimeoutConfig, type TimeoutConfig } from "./utils/timeouts.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,SAAS,EACT,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG1E,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAGnG,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAGtE,OAAO,EAAE,kBAAkB,IAAI,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAChG,OAAO,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAG7F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAGjH,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTimeoutConfig = exports.getTimeout = exports.loadConfigFromEnvFile = exports.loadEnvFile = exports.getConfigFromEnv = exports.sapConfigSignature = exports.createAbapConnection = exports.CloudAbapConnection = exports.OnPremAbapConnection = exports.JwtAbapConnection = exports.BaseAbapConnection = exports.FileSessionStorage = void 0;
|
|
4
|
+
// Session storage implementations
|
|
5
|
+
var FileSessionStorage_js_1 = require("./utils/FileSessionStorage.js");
|
|
6
|
+
Object.defineProperty(exports, "FileSessionStorage", { enumerable: true, get: function () { return FileSessionStorage_js_1.FileSessionStorage; } });
|
|
7
|
+
// Connection classes - only final implementations
|
|
8
|
+
var BaseAbapConnection_js_1 = require("./connection/BaseAbapConnection.js");
|
|
9
|
+
Object.defineProperty(exports, "BaseAbapConnection", { enumerable: true, get: function () { return BaseAbapConnection_js_1.BaseAbapConnection; } });
|
|
10
|
+
var JwtAbapConnection_js_1 = require("./connection/JwtAbapConnection.js");
|
|
11
|
+
Object.defineProperty(exports, "JwtAbapConnection", { enumerable: true, get: function () { return JwtAbapConnection_js_1.JwtAbapConnection; } });
|
|
12
|
+
// Deprecated aliases for backward compatibility
|
|
13
|
+
var BaseAbapConnection_js_2 = require("./connection/BaseAbapConnection.js");
|
|
14
|
+
Object.defineProperty(exports, "OnPremAbapConnection", { enumerable: true, get: function () { return BaseAbapConnection_js_2.BaseAbapConnection; } });
|
|
15
|
+
var JwtAbapConnection_js_2 = require("./connection/JwtAbapConnection.js");
|
|
16
|
+
Object.defineProperty(exports, "CloudAbapConnection", { enumerable: true, get: function () { return JwtAbapConnection_js_2.JwtAbapConnection; } });
|
|
17
|
+
// Factory
|
|
18
|
+
var connectionFactory_js_1 = require("./connection/connectionFactory.js");
|
|
19
|
+
Object.defineProperty(exports, "createAbapConnection", { enumerable: true, get: function () { return connectionFactory_js_1.createAbapConnection; } });
|
|
20
|
+
// Config utilities
|
|
21
|
+
var sapConfig_js_1 = require("./config/sapConfig.js");
|
|
22
|
+
Object.defineProperty(exports, "sapConfigSignature", { enumerable: true, get: function () { return sapConfig_js_1.sapConfigSignature; } });
|
|
23
|
+
Object.defineProperty(exports, "getConfigFromEnv", { enumerable: true, get: function () { return sapConfig_js_1.getConfigFromEnv; } });
|
|
24
|
+
Object.defineProperty(exports, "loadEnvFile", { enumerable: true, get: function () { return sapConfig_js_1.loadEnvFile; } });
|
|
25
|
+
Object.defineProperty(exports, "loadConfigFromEnvFile", { enumerable: true, get: function () { return sapConfig_js_1.loadConfigFromEnvFile; } });
|
|
26
|
+
// Timeouts
|
|
27
|
+
var timeouts_js_1 = require("./utils/timeouts.js");
|
|
28
|
+
Object.defineProperty(exports, "getTimeout", { enumerable: true, get: function () { return timeouts_js_1.getTimeout; } });
|
|
29
|
+
Object.defineProperty(exports, "getTimeoutConfig", { enumerable: true, get: function () { return timeouts_js_1.getTimeoutConfig; } });
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger interface for connection layer
|
|
3
|
+
* Allows connection layer to be independent of specific logger implementation
|
|
4
|
+
*/
|
|
5
|
+
export interface ILogger {
|
|
6
|
+
/**
|
|
7
|
+
* Log informational message
|
|
8
|
+
*/
|
|
9
|
+
info(message: string, meta?: any): void;
|
|
10
|
+
/**
|
|
11
|
+
* Log error message
|
|
12
|
+
*/
|
|
13
|
+
error(message: string, meta?: any): void;
|
|
14
|
+
/**
|
|
15
|
+
* Log warning message
|
|
16
|
+
*/
|
|
17
|
+
warn(message: string, meta?: any): void;
|
|
18
|
+
/**
|
|
19
|
+
* Log debug message
|
|
20
|
+
*/
|
|
21
|
+
debug(message: string, meta?: any): void;
|
|
22
|
+
/**
|
|
23
|
+
* Log CSRF token operations
|
|
24
|
+
* @param action - Type of CSRF operation: "fetch", "retry", "success", or "error"
|
|
25
|
+
* @param message - Log message
|
|
26
|
+
* @param meta - Additional metadata
|
|
27
|
+
*/
|
|
28
|
+
csrfToken?(action: "fetch" | "retry" | "success" | "error", message: string, meta?: any): void;
|
|
29
|
+
/**
|
|
30
|
+
* Log TLS configuration
|
|
31
|
+
* @param rejectUnauthorized - Whether TLS certificate validation is enabled
|
|
32
|
+
*/
|
|
33
|
+
tlsConfig?(rejectUnauthorized: boolean): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Session state interface for stateful connections
|
|
37
|
+
* Contains cookies and CSRF token that need to be preserved across requests
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionState {
|
|
40
|
+
cookies: string | null;
|
|
41
|
+
csrfToken: string | null;
|
|
42
|
+
cookieStore: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Interface for storing and retrieving session state
|
|
46
|
+
* Allows connection layer to persist session state (cookies, CSRF token) externally
|
|
47
|
+
*/
|
|
48
|
+
export interface ISessionStorage {
|
|
49
|
+
/**
|
|
50
|
+
* Save session state for a given session ID
|
|
51
|
+
* @param sessionId - Unique session identifier
|
|
52
|
+
* @param state - Session state to save
|
|
53
|
+
*/
|
|
54
|
+
save(sessionId: string, state: SessionState): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Load session state for a given session ID
|
|
57
|
+
* @param sessionId - Unique session identifier
|
|
58
|
+
* @returns Session state or null if not found
|
|
59
|
+
*/
|
|
60
|
+
load(sessionId: string): Promise<SessionState | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Delete session state for a given session ID
|
|
63
|
+
* @param sessionId - Unique session identifier
|
|
64
|
+
*/
|
|
65
|
+
delete(sessionId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAExC;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAEzC;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAExC;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;IAEzC;;;;;OAKG;IACH,SAAS,CAAC,CACR,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,EAC/C,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,GAAG,GACT,IAAI,CAAC;IAER;;;OAGG;IACH,SAAS,CAAC,CAAC,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5D;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAEtD;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
|
package/dist/logger.js
ADDED