@mcp-abap-adt/connection 0.2.2 → 0.2.4
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/README.md +33 -0
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +8 -0
- package/dist/connection/JwtAbapConnection.d.ts +15 -7
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +55 -19
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ This package interacts with external packages **ONLY through interfaces**:
|
|
|
105
105
|
- 📝 **Custom Logging**: Pluggable logger interface for integration with any logging system (optional)
|
|
106
106
|
- 📦 **TypeScript**: Full TypeScript support with type definitions included
|
|
107
107
|
- ⚡ **Timeout Management**: Configurable timeouts for different operation types
|
|
108
|
+
- 🌐 **Network Error Detection**: Automatic detection and proper handling of network-level errors (connection refused, timeout, DNS failures)
|
|
108
109
|
|
|
109
110
|
## Installation
|
|
110
111
|
|
|
@@ -177,6 +178,38 @@ const response = await connection.makeAdtRequest({
|
|
|
177
178
|
});
|
|
178
179
|
```
|
|
179
180
|
|
|
181
|
+
### Cloud Usage with Automatic Token Refresh
|
|
182
|
+
|
|
183
|
+
For automatic token refresh on 401/403 errors, inject `ITokenRefresher`:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { JwtAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
|
|
187
|
+
import type { ITokenRefresher } from "@mcp-abap-adt/interfaces";
|
|
188
|
+
|
|
189
|
+
// Token refresher provides token acquisition and refresh
|
|
190
|
+
// (created by @mcp-abap-adt/auth-broker or custom implementation)
|
|
191
|
+
const tokenRefresher: ITokenRefresher = {
|
|
192
|
+
getToken: async () => { /* return current token */ },
|
|
193
|
+
refreshToken: async () => { /* refresh and return new token */ },
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// JWT configuration
|
|
197
|
+
const config: SapConfig = {
|
|
198
|
+
url: "https://your-instance.abap.cloud.sap",
|
|
199
|
+
authType: "jwt",
|
|
200
|
+
jwtToken: await tokenRefresher.getToken(), // Get initial token
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Create connection with token refresher - 401/403 handled automatically
|
|
204
|
+
const connection = new JwtAbapConnection(config, logger, undefined, tokenRefresher);
|
|
205
|
+
|
|
206
|
+
// Requests automatically retry with refreshed token on auth errors
|
|
207
|
+
const response = await connection.makeAdtRequest({
|
|
208
|
+
method: "GET",
|
|
209
|
+
url: "/sap/bc/adt/programs/programs/your-program",
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
180
213
|
### Stateful Sessions
|
|
181
214
|
|
|
182
215
|
For operations that require session state (e.g., object modifications), you can enable stateful sessions:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAc,EAAiD,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAc,EAAiD,aAAa,EAAE,MAAM,OAAO,CAAC;AAI5F,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGzE,uBAAe,sBAAuB,YAAW,cAAc;IAU3D,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAV3C,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAyC;IAE5D,SAAS,aACU,MAAM,EAAE,SAAS,EACf,MAAM,EAAE,OAAO,GAAG,IAAI,EACzC,SAAS,CAAC,EAAE,MAAM;IAgBpB;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAOpD;;;;OAIG;IACH,qBAAqB,IAAI,IAAI;IAI7B;;;OAGG;IACH,sBAAsB,IAAI,IAAI;IAU9B;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,UAAU;IAI1C;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKrC;;OAEG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI;IAK7B,SAAS,IAAI,SAAS;IAItB,KAAK,IAAI,IAAI;IAYP,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAevD;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAE3B,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAoNzE,SAAS,CAAC,QAAQ,CAAC,wBAAwB,IAAI,MAAM;IAErD;;;OAGG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAgC,EAC5C,UAAU,GAAE,MAAgC,GAC3C,OAAO,CAAC,MAAM,CAAC;IAkKlB;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,MAAM,GAAG,IAAI;IAIvC;;OAEG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIlD;;OAEG;IACH,SAAS,CAAC,UAAU,IAAI,MAAM,GAAG,IAAI;IAIrC,OAAO,CAAC,yBAAyB;IAuDjC,OAAO,CAAC,gBAAgB;YAmBV,oBAAoB;IAwBlC,OAAO,CAAC,eAAe;CAyBxB;AAGD,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
|
|
@@ -37,6 +37,7 @@ exports.AbstractAbapConnection = void 0;
|
|
|
37
37
|
const axios_1 = __importStar(require("axios"));
|
|
38
38
|
const https_1 = require("https");
|
|
39
39
|
const crypto_1 = require("crypto");
|
|
40
|
+
const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
40
41
|
const timeouts_js_1 = require("../utils/timeouts.js");
|
|
41
42
|
const csrfConfig_js_1 = require("./csrfConfig.js");
|
|
42
43
|
class AbstractAbapConnection {
|
|
@@ -247,6 +248,13 @@ class AbstractAbapConnection {
|
|
|
247
248
|
: JSON.stringify(error.response.data).slice(0, 200);
|
|
248
249
|
this.updateCookiesFromResponse(error.response.headers);
|
|
249
250
|
}
|
|
251
|
+
// Check if this is a network error (connection refused, timeout, DNS, etc.)
|
|
252
|
+
// Don't retry for network errors - these indicate infrastructure/VPN issues
|
|
253
|
+
const networkError = (0, interfaces_1.isNetworkError)(error);
|
|
254
|
+
if (networkError) {
|
|
255
|
+
this.logger?.error(`Network error - cannot connect to SAP system: ${errorDetails.message}`, errorDetails);
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
250
258
|
// Log 404 as debug (common for existence checks), other errors as error
|
|
251
259
|
if (errorDetails.status === 404) {
|
|
252
260
|
this.logger?.debug(errorDetails.message, errorDetails);
|
|
@@ -3,26 +3,34 @@ import { AbstractAbapConnection } from "./AbstractAbapConnection.js";
|
|
|
3
3
|
import { AbapRequestOptions } from "./AbapConnection.js";
|
|
4
4
|
import { ILogger } from "../logger.js";
|
|
5
5
|
import { AxiosResponse } from "axios";
|
|
6
|
+
import type { ITokenRefresher } from "@mcp-abap-adt/interfaces";
|
|
6
7
|
/**
|
|
7
8
|
* JWT Authentication connection for SAP BTP Cloud systems
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* Supports automatic token refresh via ITokenRefresher injection:
|
|
11
|
+
* - If tokenRefresher is provided, 401/403 errors trigger automatic token refresh
|
|
12
|
+
* - If tokenRefresher is not provided, 401/403 errors throw an error (legacy behavior)
|
|
10
13
|
*/
|
|
11
14
|
export declare class JwtAbapConnection extends AbstractAbapConnection {
|
|
12
|
-
|
|
15
|
+
private tokenRefresher?;
|
|
16
|
+
private currentToken;
|
|
17
|
+
constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string, tokenRefresher?: ITokenRefresher);
|
|
13
18
|
protected buildAuthorizationHeader(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Refresh the JWT token using the injected tokenRefresher
|
|
21
|
+
* @returns true if token was refreshed, false if no refresher available
|
|
22
|
+
*/
|
|
23
|
+
private tryRefreshToken;
|
|
14
24
|
/**
|
|
15
25
|
* Override connect to handle JWT token refresh on errors
|
|
16
26
|
*/
|
|
17
27
|
connect(): Promise<void>;
|
|
18
28
|
/**
|
|
19
|
-
* Override makeAdtRequest to handle JWT auth errors
|
|
20
|
-
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
29
|
+
* Override makeAdtRequest to handle JWT auth errors with automatic token refresh
|
|
21
30
|
*/
|
|
22
31
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
23
32
|
/**
|
|
24
|
-
* Override fetchCsrfToken to handle JWT auth errors
|
|
25
|
-
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
33
|
+
* Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
|
|
26
34
|
*/
|
|
27
35
|
protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number): Promise<string>;
|
|
28
36
|
private static validateConfig;
|
|
@@ -1 +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,MAAM,cAAc,CAAC;AACvC,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
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,MAAM,cAAc,CAAC;AACvC,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE;;;;;;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;IAQlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAO5C;;;OAGG;YACW,eAAe;IAmB7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgD9B;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAwCzE;;OAEG;cACa,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAI,EAAE,UAAU,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAmC/F,OAAO,CAAC,MAAM,CAAC,cAAc;CAS9B"}
|
|
@@ -5,21 +5,46 @@ const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
|
|
|
5
5
|
const axios_1 = require("axios");
|
|
6
6
|
/**
|
|
7
7
|
* JWT Authentication connection for SAP BTP Cloud systems
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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)
|
|
10
12
|
*/
|
|
11
13
|
class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
|
|
12
|
-
|
|
14
|
+
tokenRefresher;
|
|
15
|
+
currentToken;
|
|
16
|
+
constructor(config, logger, sessionId, tokenRefresher) {
|
|
13
17
|
JwtAbapConnection.validateConfig(config);
|
|
14
18
|
super(config, logger || null, sessionId);
|
|
19
|
+
this.tokenRefresher = tokenRefresher;
|
|
20
|
+
this.currentToken = config.jwtToken;
|
|
15
21
|
}
|
|
16
22
|
buildAuthorizationHeader() {
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
// Log token preview for debugging (first 10 and last 4 chars)
|
|
20
|
-
const tokenPreview = jwtToken ? `${jwtToken.substring(0, 10)}...${jwtToken.substring(Math.max(0, jwtToken.length - 4))}` : 'null';
|
|
23
|
+
// Use currentToken which may have been refreshed
|
|
24
|
+
const tokenPreview = this.currentToken ? `${this.currentToken.substring(0, 10)}...${this.currentToken.substring(Math.max(0, this.currentToken.length - 4))}` : 'null';
|
|
21
25
|
this.logger?.debug(`[DEBUG] JwtAbapConnection.buildAuthorizationHeader - Using token: ${tokenPreview}`);
|
|
22
|
-
return `Bearer ${
|
|
26
|
+
return `Bearer ${this.currentToken}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Refresh the JWT token using the injected tokenRefresher
|
|
30
|
+
* @returns true if token was refreshed, false if no refresher available
|
|
31
|
+
*/
|
|
32
|
+
async tryRefreshToken() {
|
|
33
|
+
if (!this.tokenRefresher) {
|
|
34
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection - No tokenRefresher available, cannot refresh token`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection - Refreshing token via tokenRefresher...`);
|
|
39
|
+
const newToken = await this.tokenRefresher.refreshToken();
|
|
40
|
+
this.currentToken = newToken;
|
|
41
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection - Token refreshed successfully`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
this.logger?.error(`[ERROR] JwtAbapConnection - Failed to refresh token: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
23
48
|
}
|
|
24
49
|
/**
|
|
25
50
|
* Override connect to handle JWT token refresh on errors
|
|
@@ -51,8 +76,12 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
51
76
|
responseText.includes("Missing authorization")) {
|
|
52
77
|
throw error;
|
|
53
78
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
79
|
+
// Try to refresh token if tokenRefresher is available
|
|
80
|
+
if (await this.tryRefreshToken()) {
|
|
81
|
+
// Retry connect with new token
|
|
82
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying connect after token refresh...`);
|
|
83
|
+
return this.connect();
|
|
84
|
+
}
|
|
56
85
|
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
57
86
|
}
|
|
58
87
|
// Re-throw other errors
|
|
@@ -60,8 +89,7 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
60
89
|
}
|
|
61
90
|
}
|
|
62
91
|
/**
|
|
63
|
-
* Override makeAdtRequest to handle JWT auth errors
|
|
64
|
-
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
92
|
+
* Override makeAdtRequest to handle JWT auth errors with automatic token refresh
|
|
65
93
|
*/
|
|
66
94
|
async makeAdtRequest(options) {
|
|
67
95
|
this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Starting request: ${options.method} ${options.url}`);
|
|
@@ -75,7 +103,7 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
75
103
|
// Handle JWT auth errors (401/403)
|
|
76
104
|
if (error instanceof axios_1.AxiosError &&
|
|
77
105
|
(error.response?.status === 401 || error.response?.status === 403)) {
|
|
78
|
-
this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status},
|
|
106
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Got ${error.response.status}, attempting token refresh...`);
|
|
79
107
|
// Check if this is really an auth error, not a permissions error
|
|
80
108
|
const responseData = error.response?.data;
|
|
81
109
|
const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
|
|
@@ -85,16 +113,20 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
85
113
|
responseText.includes("Missing authorization")) {
|
|
86
114
|
throw error;
|
|
87
115
|
}
|
|
88
|
-
//
|
|
89
|
-
|
|
116
|
+
// Try to refresh token if tokenRefresher is available
|
|
117
|
+
if (await this.tryRefreshToken()) {
|
|
118
|
+
// Reset connection state and retry request with new token
|
|
119
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Retrying request after token refresh...`);
|
|
120
|
+
this.reset();
|
|
121
|
+
return super.makeAdtRequest(options);
|
|
122
|
+
}
|
|
90
123
|
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
91
124
|
}
|
|
92
125
|
throw error;
|
|
93
126
|
}
|
|
94
127
|
}
|
|
95
128
|
/**
|
|
96
|
-
* Override fetchCsrfToken to handle JWT auth errors
|
|
97
|
-
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
129
|
+
* Override fetchCsrfToken to handle JWT auth errors with automatic token refresh
|
|
98
130
|
*/
|
|
99
131
|
async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) {
|
|
100
132
|
try {
|
|
@@ -114,8 +146,12 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
114
146
|
responseText.includes("Missing authorization")) {
|
|
115
147
|
throw error;
|
|
116
148
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
149
|
+
// Try to refresh token if tokenRefresher is available
|
|
150
|
+
if (await this.tryRefreshToken()) {
|
|
151
|
+
// Retry CSRF token fetch with new token
|
|
152
|
+
this.logger?.debug(`[DEBUG] JwtAbapConnection.fetchCsrfToken - Retrying after token refresh...`);
|
|
153
|
+
return super.fetchCsrfToken(url, retryCount, retryDelay);
|
|
154
|
+
}
|
|
119
155
|
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
120
156
|
}
|
|
121
157
|
// Re-throw other errors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/connection",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "ABAP connection layer for MCP ABAP ADT server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@mcp-abap-adt/interfaces": "^0.
|
|
45
|
+
"@mcp-abap-adt/interfaces": "^0.2.5",
|
|
46
46
|
"axios": "^1.11.0",
|
|
47
47
|
"commander": "^14.0.2",
|
|
48
48
|
"express": "^5.1.0",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@types/jest": "^30.0.0",
|
|
53
53
|
"@types/node": "^24.2.1",
|
|
54
54
|
"jest": "^30.2.0",
|
|
55
|
+
"jest-util": "^30.2.0",
|
|
55
56
|
"ts-jest": "^29.2.5",
|
|
56
57
|
"typescript": "^5.9.2"
|
|
57
58
|
}
|