@mcp-abap-adt/connection 4.0.0 → 6.0.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/CHANGELOG.md +407 -1
- package/README.md +239 -53
- package/dist/auth/providers.d.ts +124 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +183 -0
- package/dist/connection/AbstractAbapConnection.d.ts +209 -57
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +459 -457
- package/dist/connection/AdtCloudConnector.d.ts +34 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +27 -0
- package/dist/connection/AdtOnPremConnector.d.ts +43 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +28 -0
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/connection/CloudHttpTransport.js +145 -0
- package/dist/connection/CredentialAbapConnection.d.ts +55 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +128 -0
- package/dist/connection/HttpTransport.d.ts +178 -0
- package/dist/connection/HttpTransport.d.ts.map +1 -0
- package/dist/connection/HttpTransport.js +402 -0
- package/dist/connection/IAdtTransport.d.ts +232 -0
- package/dist/connection/IAdtTransport.d.ts.map +1 -0
- package/dist/connection/IAdtTransport.js +28 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
- package/dist/connection/OnPremHttpTransport.d.ts +45 -0
- package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/OnPremHttpTransport.js +91 -0
- package/dist/connection/RfcTransport.d.ts +89 -0
- package/dist/connection/RfcTransport.d.ts.map +1 -0
- package/dist/connection/RfcTransport.js +256 -0
- package/dist/connection/rfcConversation.d.ts +44 -0
- package/dist/connection/rfcConversation.d.ts.map +1 -0
- package/dist/connection/rfcConversation.js +71 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -18
- package/dist/session/SessionLifecycle.d.ts +17 -2
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +17 -2
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +24 -0
- package/dist/utils/timeouts.d.ts +6 -3
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -3
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -6
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
- package/docs/USAGE.md +322 -111
- package/examples/basic-connection.js +15 -3
- package/examples/jwt-with-token-refresh.js +15 -7
- package/examples/saml-connection.js +15 -2
- package/package.json +12 -10
- package/dist/__tests__/helpers/session.d.ts +0 -15
- package/dist/__tests__/helpers/session.d.ts.map +0 -1
- package/dist/__tests__/helpers/session.js +0 -19
- package/dist/connection/BaseAbapConnection.d.ts +0 -23
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -75
- package/dist/connection/CertificateAbapConnection.d.ts +0 -25
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -79
- package/dist/connection/JwtAbapConnection.d.ts +0 -115
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -358
- package/dist/connection/KerberosAbapConnection.d.ts +0 -24
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -120
- package/dist/connection/RfcAbapConnection.d.ts +0 -49
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -331
- package/dist/connection/SamlAbapConnection.d.ts +0 -25
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -75
- package/dist/connection/connectionFactory.d.ts +0 -9
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- package/dist/connection/connectionFactory.js +0 -32
package/docs/USAGE.md
CHANGED
|
@@ -13,10 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
|
-
### Basic Usage
|
|
16
|
+
### Basic Usage
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
AdtOnPremConnector,
|
|
21
|
+
BasicAuthProvider,
|
|
22
|
+
OnPremHttpTransport,
|
|
23
|
+
getTimeout,
|
|
24
|
+
} from '@mcp-abap-adt/connection';
|
|
20
25
|
import { SapConfig } from '@mcp-abap-adt/connection';
|
|
21
26
|
|
|
22
27
|
const config: SapConfig = {
|
|
@@ -35,10 +40,18 @@ const logger = {
|
|
|
35
40
|
debug: (msg: string, meta?: any) => console.debug(msg, meta),
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
43
|
+
// Three things, all stated by you and none of them detected: which system
|
|
44
|
+
// (the class), which credential (the object), which wire (the transport, which
|
|
45
|
+
// has no default — you build it and hand it over).
|
|
46
|
+
const connection = new AdtOnPremConnector(
|
|
47
|
+
config,
|
|
48
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
49
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
50
|
+
client: config.client,
|
|
51
|
+
baseUrl: config.url,
|
|
52
|
+
}),
|
|
53
|
+
logger, // optional; omit or pass null for no logging
|
|
54
|
+
);
|
|
42
55
|
|
|
43
56
|
// Establish the session. This is REQUIRED: a request on a connection that was
|
|
44
57
|
// never connected is refused with ADT_NOT_CONNECTED, and connect() rejects if
|
|
@@ -48,23 +61,38 @@ await connection.connect();
|
|
|
48
61
|
const response = await connection.makeAdtRequest({
|
|
49
62
|
method: 'GET',
|
|
50
63
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
64
|
+
timeout: getTimeout('default'),
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
console.log(response.data);
|
|
54
68
|
```
|
|
55
69
|
|
|
56
70
|
Tearing the session down is `disconnect()`, but it is not on the
|
|
57
|
-
`IAbapConnection` type
|
|
71
|
+
`IAbapConnection` type a connector satisfies — see
|
|
58
72
|
[Session Lifecycle](#session-lifecycle) for where it lives and how to reach it.
|
|
59
73
|
|
|
60
|
-
##
|
|
74
|
+
## Which system, and how you authenticate
|
|
75
|
+
|
|
76
|
+
Two independent choices, and they must stay independent. The connector says
|
|
77
|
+
which SYSTEM you are dialling; the auth provider says how you prove who you
|
|
78
|
+
are. A communication user against ABAP Cloud and a bearer token against an
|
|
79
|
+
on-prem system are both ordinary, and a design where the credential picked the
|
|
80
|
+
system's session mechanism got one of them wrong whichever way it guessed.
|
|
81
|
+
|
|
82
|
+
Nothing is detected. `/sap/bc/adt/core/http/sessions` answers on on-prem too,
|
|
83
|
+
and its `DELETE` there leaves the session open while the platform logoff removes
|
|
84
|
+
it — so asking the server would choose the mechanism that releases nothing.
|
|
61
85
|
|
|
62
86
|
### Basic Authentication (On-Premise)
|
|
63
87
|
|
|
64
88
|
For on-premise SAP systems using basic authentication:
|
|
65
89
|
|
|
66
90
|
```typescript
|
|
67
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
AdtOnPremConnector,
|
|
93
|
+
BasicAuthProvider,
|
|
94
|
+
OnPremHttpTransport,
|
|
95
|
+
} from '@mcp-abap-adt/connection';
|
|
68
96
|
|
|
69
97
|
const config = {
|
|
70
98
|
url: 'https://sap-server.local:8000',
|
|
@@ -74,7 +102,15 @@ const config = {
|
|
|
74
102
|
client: '100',
|
|
75
103
|
};
|
|
76
104
|
|
|
77
|
-
const connection = new
|
|
105
|
+
const connection = new AdtOnPremConnector(
|
|
106
|
+
config,
|
|
107
|
+
new BasicAuthProvider(config.username, config.password),
|
|
108
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
109
|
+
client: config.client,
|
|
110
|
+
baseUrl: config.url,
|
|
111
|
+
}),
|
|
112
|
+
logger,
|
|
113
|
+
);
|
|
78
114
|
await connection.connect(); // required: nothing is established implicitly
|
|
79
115
|
```
|
|
80
116
|
|
|
@@ -84,16 +120,31 @@ For SAP BTP ABAP Environment. Token refresh belongs to
|
|
|
84
120
|
`@mcp-abap-adt/auth-broker`; this package only carries the token:
|
|
85
121
|
|
|
86
122
|
```typescript
|
|
87
|
-
import {
|
|
123
|
+
import {
|
|
124
|
+
AdtCloudConnector,
|
|
125
|
+
CloudHttpTransport,
|
|
126
|
+
TokenAuthProvider,
|
|
127
|
+
getTimeout,
|
|
128
|
+
} from '@mcp-abap-adt/connection';
|
|
88
129
|
|
|
89
130
|
const config = {
|
|
90
131
|
url: 'https://tenant.abap.cloud',
|
|
91
132
|
authType: 'jwt' as const,
|
|
92
|
-
jwtToken: 'eyJhbGciOiJSUzI1NiIs...',
|
|
93
133
|
client: '100', // Optional for cloud
|
|
94
134
|
};
|
|
95
135
|
|
|
96
|
-
|
|
136
|
+
// A bare token works and has no renewal behind it. Hand the provider an
|
|
137
|
+
// ITokenRefresher instead — from @mcp-abap-adt/auth-broker or your own — and it
|
|
138
|
+
// checks expiry and refreshes on its own.
|
|
139
|
+
const connection = new AdtCloudConnector(
|
|
140
|
+
config,
|
|
141
|
+
new TokenAuthProvider('eyJhbGciOiJSUzI1NiIs...'),
|
|
142
|
+
new CloudHttpTransport(() => ({}), logger, {
|
|
143
|
+
client: config.client,
|
|
144
|
+
baseUrl: config.url,
|
|
145
|
+
}),
|
|
146
|
+
logger,
|
|
147
|
+
);
|
|
97
148
|
await connection.connect(); // required: nothing is established implicitly
|
|
98
149
|
|
|
99
150
|
// Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
|
|
@@ -101,6 +152,7 @@ await connection.connect(); // required: nothing is established implicitly
|
|
|
101
152
|
const response = await connection.makeAdtRequest({
|
|
102
153
|
method: 'GET',
|
|
103
154
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
155
|
+
timeout: getTimeout('default'),
|
|
104
156
|
});
|
|
105
157
|
```
|
|
106
158
|
|
|
@@ -114,6 +166,7 @@ them is refused with `ADT_NOT_CONNECTED`.
|
|
|
114
166
|
### GET Request
|
|
115
167
|
|
|
116
168
|
```typescript
|
|
169
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
117
170
|
const packages = await connection.makeAdtRequest({
|
|
118
171
|
method: 'GET',
|
|
119
172
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
@@ -122,12 +175,14 @@ const packages = await connection.makeAdtRequest({
|
|
|
122
175
|
parent_type: 'DEVC/K',
|
|
123
176
|
withShortDescriptions: 'true',
|
|
124
177
|
},
|
|
178
|
+
timeout: getTimeout('default'),
|
|
125
179
|
});
|
|
126
180
|
```
|
|
127
181
|
|
|
128
182
|
### POST Request (Create Object)
|
|
129
183
|
|
|
130
184
|
```typescript
|
|
185
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
131
186
|
const classXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
132
187
|
<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
|
|
133
188
|
class:name="ZCL_MY_CLASS">
|
|
@@ -140,28 +195,35 @@ const response = await connection.makeAdtRequest({
|
|
|
140
195
|
headers: { 'Content-Type': 'application/xml' },
|
|
141
196
|
data: classXml,
|
|
142
197
|
params: { package: 'ZTEST' },
|
|
198
|
+
timeout: getTimeout('default'),
|
|
143
199
|
});
|
|
144
200
|
```
|
|
145
201
|
|
|
146
202
|
### PUT Request (Update Object)
|
|
147
203
|
|
|
148
204
|
```typescript
|
|
205
|
+
const classSourceCode = 'CLASS zcl_my_class DEFINITION ...';
|
|
206
|
+
const lockToken = 'the handle the lock call returned';
|
|
207
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
149
208
|
await connection.makeAdtRequest({
|
|
150
209
|
method: 'PUT',
|
|
151
210
|
url: '/sap/bc/adt/oo/classes/zcl_my_class/source/main',
|
|
152
211
|
headers: { 'Content-Type': 'text/plain' },
|
|
153
|
-
data: classSourceCode,
|
|
154
|
-
params: { lockHandle: lockToken },
|
|
212
|
+
data: classSourceCode, // the new source, as a string
|
|
213
|
+
params: { lockHandle: lockToken }, // from the lock you took first
|
|
214
|
+
timeout: getTimeout('default'),
|
|
155
215
|
});
|
|
156
216
|
```
|
|
157
217
|
|
|
158
218
|
### DELETE Request
|
|
159
219
|
|
|
160
220
|
```typescript
|
|
221
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
161
222
|
await connection.makeAdtRequest({
|
|
162
223
|
method: 'DELETE',
|
|
163
224
|
url: '/sap/bc/adt/oo/classes/zcl_my_class',
|
|
164
225
|
params: { deleteOption: 'deleteAndLocalVersions' },
|
|
226
|
+
timeout: getTimeout('default'),
|
|
165
227
|
});
|
|
166
228
|
```
|
|
167
229
|
|
|
@@ -172,11 +234,20 @@ await connection.makeAdtRequest({
|
|
|
172
234
|
By default, connections are stateless - each request gets fresh cookies and CSRF tokens:
|
|
173
235
|
|
|
174
236
|
```typescript
|
|
175
|
-
|
|
237
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport, getTimeout } from '@mcp-abap-adt/connection';
|
|
238
|
+
const connection = new AdtOnPremConnector(
|
|
239
|
+
config,
|
|
240
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
241
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
242
|
+
client: config.client,
|
|
243
|
+
baseUrl: config.url,
|
|
244
|
+
}),
|
|
245
|
+
logger,
|
|
246
|
+
);
|
|
176
247
|
await connection.connect();
|
|
177
248
|
|
|
178
249
|
// Each request is independent
|
|
179
|
-
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
250
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
180
251
|
```
|
|
181
252
|
|
|
182
253
|
### Stateful Mode (Session Headers)
|
|
@@ -184,14 +255,23 @@ await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' })
|
|
|
184
255
|
Enable stateful session mode for operations requiring consistent session state:
|
|
185
256
|
|
|
186
257
|
```typescript
|
|
187
|
-
|
|
258
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport, getTimeout } from '@mcp-abap-adt/connection';
|
|
259
|
+
const connection = new AdtOnPremConnector(
|
|
260
|
+
config,
|
|
261
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
262
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
263
|
+
client: config.client,
|
|
264
|
+
baseUrl: config.url,
|
|
265
|
+
}),
|
|
266
|
+
logger,
|
|
267
|
+
);
|
|
188
268
|
await connection.connect();
|
|
189
269
|
|
|
190
270
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
191
271
|
connection.setSessionType('stateful');
|
|
192
272
|
|
|
193
273
|
// Now all requests share the same session (cookies, CSRF token)
|
|
194
|
-
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
274
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
195
275
|
|
|
196
276
|
// Check session mode
|
|
197
277
|
console.log(connection.getSessionMode()); // 'stateful'
|
|
@@ -223,10 +303,12 @@ implied. Four things follow from it.
|
|
|
223
303
|
> `IAbapConnection`: `ISessionLifecycleAware` — `disconnect()`, `isConnected()`,
|
|
224
304
|
> `getSessionIdentity()`.
|
|
225
305
|
>
|
|
226
|
-
> The split is the point. `IAbapConnection` is the minimum
|
|
227
|
-
> honour
|
|
228
|
-
>
|
|
229
|
-
>
|
|
306
|
+
> The split is the point. `IAbapConnection` is the minimum any consumer of ADT
|
|
307
|
+
> can honour — a caller that only issues requests should not have to implement a
|
|
308
|
+
> teardown it never performs. Both connectors implement the atom, over either
|
|
309
|
+
> wire: an RFC conversation has the whole lifecycle, and what it has none of is a
|
|
310
|
+
> session RESOURCE to open and close by address, which is an empty mechanism
|
|
311
|
+
> rather than an absent lifecycle.
|
|
230
312
|
>
|
|
231
313
|
> So ask for the atom you need, not for a concrete class:
|
|
232
314
|
>
|
|
@@ -237,24 +319,30 @@ implied. Four things follow from it.
|
|
|
237
319
|
> **How you satisfy that parameter decides whether you get a compile-time
|
|
238
320
|
> guarantee, and there is only one way that does.**
|
|
239
321
|
>
|
|
240
|
-
> Construct
|
|
241
|
-
>
|
|
242
|
-
> site:
|
|
322
|
+
> Construct a connector and the compiler knows it implements the atom, whichever
|
|
323
|
+
> wire you gave it:
|
|
243
324
|
>
|
|
244
325
|
> ```typescript
|
|
245
|
-
> const conn = new
|
|
246
|
-
> tearDownAfter(conn);
|
|
247
|
-
>
|
|
326
|
+
> const conn = new AdtOnPremConnector(config, provider, new OnPremHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }), logger);
|
|
327
|
+
> tearDownAfter(conn); // ✅ checked
|
|
328
|
+
>
|
|
329
|
+
> const overRfc = new AdtOnPremConnector(
|
|
330
|
+
> config,
|
|
331
|
+
> provider,
|
|
332
|
+
> new RfcTransport(rfcConversationFrom(config), logger),
|
|
333
|
+
> logger,
|
|
334
|
+
> );
|
|
335
|
+
> tearDownAfter(overRfc); // ✅ also checked — same class, different wire
|
|
248
336
|
> ```
|
|
249
337
|
>
|
|
250
|
-
>
|
|
251
|
-
>
|
|
252
|
-
>
|
|
253
|
-
>
|
|
254
|
-
>
|
|
255
|
-
>
|
|
338
|
+
> What does NOT give you that is a bare `IAbapConnection` handed to you by
|
|
339
|
+
> somebody else: the type carries no evidence either way, and the compiler
|
|
340
|
+
> rejects it. Asserting past that — `conn as IAbapConnection &
|
|
341
|
+
> ISessionLifecycleAware` — silences the error whether or not the object has the
|
|
342
|
+
> methods. An assertion is not a check; it is a promise you make to the compiler
|
|
343
|
+
> on your own authority.
|
|
256
344
|
>
|
|
257
|
-
> When you only have an `IAbapConnection` — from
|
|
345
|
+
> When you only have an `IAbapConnection` — from a caller, or from a registry —
|
|
258
346
|
> narrow it at runtime with a predicate:
|
|
259
347
|
>
|
|
260
348
|
> ```typescript
|
|
@@ -290,9 +378,17 @@ implied. Four things follow from it.
|
|
|
290
378
|
### connect() is required, and it tells the truth
|
|
291
379
|
|
|
292
380
|
```typescript
|
|
293
|
-
import {
|
|
294
|
-
|
|
295
|
-
const connection = new
|
|
381
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
382
|
+
|
|
383
|
+
const connection = new AdtOnPremConnector(
|
|
384
|
+
config,
|
|
385
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
386
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
387
|
+
client: config.client,
|
|
388
|
+
baseUrl: config.url,
|
|
389
|
+
}),
|
|
390
|
+
logger,
|
|
391
|
+
);
|
|
296
392
|
|
|
297
393
|
await connection.connect(); // establishes the session, or rejects
|
|
298
394
|
connection.isConnected(); // true only while a usable session exists
|
|
@@ -312,6 +408,13 @@ side instead of a condition that silently stops matching. The codes live in
|
|
|
312
408
|
|
|
313
409
|
```typescript
|
|
314
410
|
import { ADT_SESSION_ERROR } from '@mcp-abap-adt/interfaces';
|
|
411
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
412
|
+
|
|
413
|
+
const options = {
|
|
414
|
+
method: 'GET',
|
|
415
|
+
url: '/sap/bc/adt/repository/nodestructure',
|
|
416
|
+
timeout: getTimeout('default'),
|
|
417
|
+
};
|
|
315
418
|
|
|
316
419
|
try {
|
|
317
420
|
await connection.makeAdtRequest(options);
|
|
@@ -324,9 +427,9 @@ try {
|
|
|
324
427
|
```
|
|
325
428
|
|
|
326
429
|
`AdtSessionErrorCode` comes from the same place, as does the capability interface
|
|
327
|
-
this connection implements — `ISessionLifecycleAware`. Depend on
|
|
328
|
-
on a concrete
|
|
329
|
-
|
|
430
|
+
this connection implements — `ISessionLifecycleAware`. Depend on the atom rather
|
|
431
|
+
than on a concrete class where you can: an `IAbapConnection` may come from
|
|
432
|
+
somewhere that implements only the minimum, so the atom you require is also the
|
|
330
433
|
documentation of what your code actually needs.
|
|
331
434
|
|
|
332
435
|
### disconnect() waits for nothing
|
|
@@ -420,12 +523,29 @@ yours.
|
|
|
420
523
|
Session IDs are auto-generated (UUID) when connection is created:
|
|
421
524
|
|
|
422
525
|
```typescript
|
|
423
|
-
|
|
526
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
527
|
+
const connection = new AdtOnPremConnector(
|
|
528
|
+
config,
|
|
529
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
530
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
531
|
+
client: config.client,
|
|
532
|
+
baseUrl: config.url,
|
|
533
|
+
}),
|
|
534
|
+
logger,
|
|
535
|
+
);
|
|
424
536
|
console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
|
|
425
537
|
|
|
426
538
|
// Or provide your own when creating connection
|
|
427
|
-
const
|
|
428
|
-
|
|
539
|
+
const connectionWithOwnId = new AdtOnPremConnector(
|
|
540
|
+
config,
|
|
541
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
542
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
543
|
+
client: config.client,
|
|
544
|
+
baseUrl: config.url,
|
|
545
|
+
}),
|
|
546
|
+
logger,
|
|
547
|
+
'custom-session-123');
|
|
548
|
+
console.log(connectionWithOwnId.getSessionId()); // 'custom-session-123'
|
|
429
549
|
```
|
|
430
550
|
|
|
431
551
|
### Switching Session Types
|
|
@@ -433,34 +553,49 @@ console.log(connection.getSessionId()); // 'custom-session-123'
|
|
|
433
553
|
Dynamically switch between stateful and stateless modes:
|
|
434
554
|
|
|
435
555
|
```typescript
|
|
556
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport, getTimeout } from '@mcp-abap-adt/connection';
|
|
436
557
|
// Start in stateless mode (default)
|
|
437
|
-
const connection =
|
|
558
|
+
const connection = new AdtOnPremConnector(
|
|
559
|
+
config,
|
|
560
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
561
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
562
|
+
client: config.client,
|
|
563
|
+
baseUrl: config.url,
|
|
564
|
+
}),
|
|
565
|
+
logger,
|
|
566
|
+
);
|
|
438
567
|
await connection.connect();
|
|
439
568
|
|
|
440
569
|
// Enable stateful for a series of operations
|
|
441
570
|
connection.setSessionType('stateful');
|
|
442
571
|
|
|
443
572
|
// Do stateful operations...
|
|
444
|
-
await connection.makeAdtRequest({ method: 'POST', url: '...' });
|
|
573
|
+
await connection.makeAdtRequest({ method: 'POST', url: '...' , timeout: getTimeout('default') });
|
|
445
574
|
|
|
446
575
|
// Switch back to stateless
|
|
447
576
|
connection.setSessionType('stateless');
|
|
448
577
|
```
|
|
449
578
|
|
|
450
|
-
###
|
|
579
|
+
### Starting Over
|
|
451
580
|
|
|
452
|
-
|
|
581
|
+
There is no local-only reset. Discarding the cookie does not end the ABAP
|
|
582
|
+
session — the server keeps it until its own timeout, and sessions are limited
|
|
583
|
+
per user — so starting over means telling the server, then connecting again:
|
|
453
584
|
|
|
454
585
|
```typescript
|
|
455
|
-
connection.
|
|
586
|
+
await connection.disconnect(); // ends the session on the server too
|
|
587
|
+
await connection.connect(); // a new one, explicitly
|
|
456
588
|
```
|
|
457
589
|
|
|
590
|
+
A connection that was connected must be disconnected, which is why this belongs
|
|
591
|
+
in a `finally`. `disconnect()` never throws, so it is safe there.
|
|
592
|
+
|
|
458
593
|
## Custom Logging
|
|
459
594
|
|
|
460
595
|
### Using Custom Logger
|
|
461
596
|
|
|
462
597
|
```typescript
|
|
463
|
-
import { ILogger } from '@mcp-abap-adt/connection';
|
|
598
|
+
import { AdtOnPremConnector, BasicAuthProvider, ILogger, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
464
599
|
|
|
465
600
|
class CustomLogger implements ILogger {
|
|
466
601
|
info(message: string, meta?: any) {
|
|
@@ -493,7 +628,15 @@ class CustomLogger implements ILogger {
|
|
|
493
628
|
}
|
|
494
629
|
|
|
495
630
|
const logger = new CustomLogger();
|
|
496
|
-
const connection =
|
|
631
|
+
const connection = new AdtOnPremConnector(
|
|
632
|
+
config,
|
|
633
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
634
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
635
|
+
client: config.client,
|
|
636
|
+
baseUrl: config.url,
|
|
637
|
+
}),
|
|
638
|
+
logger,
|
|
639
|
+
);
|
|
497
640
|
```
|
|
498
641
|
|
|
499
642
|
## Error Handling
|
|
@@ -501,16 +644,23 @@ const connection = createAbapConnection(config, logger);
|
|
|
501
644
|
### Basic Error Handling
|
|
502
645
|
|
|
503
646
|
```typescript
|
|
647
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
504
648
|
try {
|
|
505
649
|
await connection.makeAdtRequest({
|
|
506
650
|
method: 'GET',
|
|
507
651
|
url: '/sap/bc/adt/invalid/endpoint',
|
|
652
|
+
timeout: getTimeout('default'),
|
|
508
653
|
});
|
|
509
654
|
} catch (error) {
|
|
510
|
-
|
|
511
|
-
|
|
655
|
+
const failure = error as {
|
|
656
|
+
code?: string;
|
|
657
|
+
response?: { status: number; data: unknown };
|
|
658
|
+
message?: string;
|
|
659
|
+
};
|
|
660
|
+
if (failure.response) {
|
|
661
|
+
console.error(`HTTP ${failure.response.status}:`, failure.response.data);
|
|
512
662
|
} else {
|
|
513
|
-
console.error('Network error:',
|
|
663
|
+
console.error('Network error:', failure.message);
|
|
514
664
|
}
|
|
515
665
|
}
|
|
516
666
|
```
|
|
@@ -532,23 +682,30 @@ When these errors occur, the connection:
|
|
|
532
682
|
3. **Logs the error** with full context for troubleshooting
|
|
533
683
|
|
|
534
684
|
```typescript
|
|
685
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
535
686
|
try {
|
|
536
687
|
await connection.makeAdtRequest({
|
|
537
688
|
method: 'GET',
|
|
538
689
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
690
|
+
timeout: getTimeout('default'),
|
|
539
691
|
});
|
|
540
692
|
} catch (error) {
|
|
693
|
+
const failure = error as {
|
|
694
|
+
code?: string;
|
|
695
|
+
response?: { status: number; data: unknown };
|
|
696
|
+
message?: string;
|
|
697
|
+
};
|
|
541
698
|
// Check for specific network error codes
|
|
542
|
-
if (
|
|
699
|
+
if (failure.code === 'ECONNREFUSED') {
|
|
543
700
|
console.error('Cannot connect to SAP server - check VPN connection');
|
|
544
|
-
} else if (
|
|
701
|
+
} else if (failure.code === 'ETIMEDOUT') {
|
|
545
702
|
console.error('Connection timeout - server not responding');
|
|
546
|
-
} else if (
|
|
703
|
+
} else if (failure.code === 'ENOTFOUND') {
|
|
547
704
|
console.error('Cannot resolve hostname - check SAP URL');
|
|
548
|
-
} else if (
|
|
549
|
-
console.error(`HTTP ${
|
|
705
|
+
} else if (failure.response) {
|
|
706
|
+
console.error(`HTTP ${failure.response.status}:`, failure.response.data);
|
|
550
707
|
} else {
|
|
551
|
-
console.error('Request failed:',
|
|
708
|
+
console.error('Request failed:', failure.message);
|
|
552
709
|
}
|
|
553
710
|
}
|
|
554
711
|
```
|
|
@@ -568,6 +725,8 @@ try {
|
|
|
568
725
|
connection satisfies, including RFC:
|
|
569
726
|
|
|
570
727
|
```typescript
|
|
728
|
+
import { AbapRequestOptions } from '@mcp-abap-adt/connection';
|
|
729
|
+
import type { AxiosResponse } from 'axios';
|
|
571
730
|
interface AbapConnection {
|
|
572
731
|
connect(): Promise<void>; // REQUIRED before any request
|
|
573
732
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
@@ -577,29 +736,90 @@ interface AbapConnection {
|
|
|
577
736
|
}
|
|
578
737
|
```
|
|
579
738
|
|
|
580
|
-
Anything beyond that is **not** on the contract. `
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
`createAbapConnection()` returns.
|
|
739
|
+
Anything beyond that is **not** on the contract. `getSessionMode()` and the
|
|
740
|
+
session lifecycle (`disconnect()`, `isConnected()`, `getSessionIdentity()`) live
|
|
741
|
+
on the connectors — both of them, over either wire — so reach for them through a
|
|
742
|
+
connector type or through the `ISessionLifecycleAware` atom, not through a bare
|
|
743
|
+
`IAbapConnection` somebody handed you.
|
|
586
744
|
|
|
587
|
-
### `
|
|
745
|
+
### `AdtOnPremConnector` / `AdtCloudConnector`
|
|
588
746
|
|
|
589
|
-
|
|
747
|
+
One per system, each handed an auth provider:
|
|
590
748
|
|
|
591
|
-
```
|
|
592
|
-
class
|
|
593
|
-
|
|
749
|
+
```text
|
|
750
|
+
class AdtOnPremConnector<
|
|
751
|
+
TCredential extends IAuthProvider = IAuthProvider,
|
|
752
|
+
TTransport extends IOnPremTransport = OnPremHttpTransport,
|
|
753
|
+
> {
|
|
754
|
+
constructor(
|
|
755
|
+
config: SapConfig,
|
|
756
|
+
credential: TCredential,
|
|
757
|
+
transport: TTransport,
|
|
758
|
+
logger?: ILogger | null,
|
|
759
|
+
sessionId?: string,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
class AdtCloudConnector<
|
|
764
|
+
TCredential extends IAuthProvider = IAuthProvider,
|
|
765
|
+
TTransport extends ICloudTransport = CloudHttpTransport,
|
|
766
|
+
> {
|
|
767
|
+
constructor(
|
|
768
|
+
config: SapConfig,
|
|
769
|
+
credential: TCredential,
|
|
770
|
+
transport: TTransport,
|
|
771
|
+
logger?: ILogger | null,
|
|
772
|
+
sessionId?: string,
|
|
773
|
+
);
|
|
594
774
|
}
|
|
595
775
|
```
|
|
596
776
|
|
|
777
|
+
The difference is what each does with the session: on-prem takes it from the
|
|
778
|
+
establishing call and gives it back with the platform's ICF logoff; cloud opens
|
|
779
|
+
one at `/sap/bc/adt/core/http/sessions` and gives it back by `DELETE` on the
|
|
780
|
+
address the server publishes.
|
|
781
|
+
|
|
782
|
+
**Both take a transport, and neither defaults one.** What differs is the CHOICE,
|
|
783
|
+
and the type parameter is where that is said: `TTransport` is bound to
|
|
784
|
+
`IOnPremTransport` on one and to `ICloudTransport` on the other, so on-prem
|
|
785
|
+
admits HTTP or RFC while "cloud over RFC" does not compile — there is no such
|
|
786
|
+
deployment. The parameter also records what a connection was built with, so a
|
|
787
|
+
signature can ask for `AdtOnPremConnector<IAuthProvider, RfcTransport>` and be
|
|
788
|
+
given one, instead of taking any connection and casting.
|
|
789
|
+
|
|
790
|
+
### `HttpTransport` / `RfcTransport`
|
|
791
|
+
|
|
792
|
+
What a request travels over, and everything true of that wire: addressing,
|
|
793
|
+
establishing, and whatever session state it keeps.
|
|
794
|
+
|
|
795
|
+
```text
|
|
796
|
+
new HttpTransport(agentOptions?, logger?, { client?, baseUrl? })
|
|
797
|
+
new RfcTransport(connect: () => IRfcConversation, logger?)
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
`HttpTransport` is the ordinary wire, and you name it because the connector
|
|
801
|
+
takes no default. In practice you name one of its two subclasses — the session
|
|
802
|
+
mechanism is what differs, and `OnPremHttpTransport` / `CloudHttpTransport` are
|
|
803
|
+
what the connectors' type parameters admit. `RfcTransport`
|
|
804
|
+
you build with `rfcConversationFrom(config)`, which derives `ashost` and `sysnr`
|
|
805
|
+
and loads the SAP NW RFC SDK only when a conversation opens.
|
|
806
|
+
|
|
807
|
+
The two differ in what they have, not in what they are asked:
|
|
808
|
+
|
|
809
|
+
| | HTTP | RFC |
|
|
810
|
+
|---|---|---|
|
|
811
|
+
| session is | an ICF session, addressed by `SAP_SESSIONID` | the conversation itself |
|
|
812
|
+
| `establish()` | earns a CSRF token, and the cookies with it | nothing to earn |
|
|
813
|
+
| cookies | a jar, replayed on every request | none, ever |
|
|
814
|
+
| affinity | `sap-adt-saplb`, to stay on one app server | none |
|
|
815
|
+
| visible in | SM05 | SMGW → Logged on Clients |
|
|
816
|
+
|
|
597
817
|
### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES` (New in 0.1.13+)
|
|
598
818
|
|
|
599
819
|
Exported constants for consistent CSRF token handling across different connection implementations:
|
|
600
820
|
|
|
601
|
-
```
|
|
602
|
-
|
|
821
|
+
```text
|
|
822
|
+
// Imported from '@mcp-abap-adt/connection'; shapes, not runnable code.
|
|
603
823
|
|
|
604
824
|
// CSRF_CONFIG structure:
|
|
605
825
|
const config = {
|
|
@@ -656,7 +876,7 @@ export class CloudSdkAbapConnection {
|
|
|
656
876
|
throw new Error(
|
|
657
877
|
CSRF_ERROR_MESSAGES.FETCH_FAILED(
|
|
658
878
|
CSRF_CONFIG.RETRY_COUNT + 1,
|
|
659
|
-
error instanceof Error ?
|
|
879
|
+
error instanceof Error ? failure.message : String(error)
|
|
660
880
|
)
|
|
661
881
|
);
|
|
662
882
|
}
|
|
@@ -670,52 +890,43 @@ export class CloudSdkAbapConnection {
|
|
|
670
890
|
```
|
|
671
891
|
|
|
672
892
|
|
|
673
|
-
###
|
|
893
|
+
### The credential, and how a refusal is classified
|
|
674
894
|
|
|
675
|
-
For SAP BTP cloud systems
|
|
895
|
+
For SAP BTP cloud systems, hand `AdtCloudConnector` a `TokenAuthProvider`. A bare
|
|
896
|
+
string is a token with nothing behind it; an `ITokenRefresher` is a provider that
|
|
897
|
+
checks expiry and renews on its own, which is what you want in anything
|
|
898
|
+
long-lived. Obtaining tokens in the first place is `@mcp-abap-adt/auth-broker`'s
|
|
899
|
+
job, not this package's.
|
|
676
900
|
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
);
|
|
685
|
-
// Note: refreshToken() and canRefreshToken() methods removed in 0.2.0
|
|
686
|
-
// Token acquisition itself belongs to @mcp-abap-adt/auth-broker
|
|
687
|
-
}
|
|
901
|
+
```text
|
|
902
|
+
new AdtCloudConnector(
|
|
903
|
+
config,
|
|
904
|
+
new TokenAuthProvider(refresher), // or a bare token string
|
|
905
|
+
new CloudHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }),
|
|
906
|
+
logger,
|
|
907
|
+
);
|
|
688
908
|
```
|
|
689
909
|
|
|
690
|
-
**How failures are classified** (
|
|
691
|
-
[MIGRATION-
|
|
910
|
+
**How failures are classified** (6.0.0 — see
|
|
911
|
+
[MIGRATION-6.0.md](./MIGRATION-6.0.md)):
|
|
692
912
|
|
|
693
913
|
| answer | what happens |
|
|
694
914
|
|---|---|
|
|
695
|
-
| **401** |
|
|
915
|
+
| **401** | **surfaces.** Nothing here decides to get a new credential. An EXPIRED token is already replaced without anyone deciding — the provider is asked per request and checks expiry before answering — so a 401 is the other case: a credential the source still believes in and the server refuses. Whether that means "stale" is a judgement made with what you know, and `renew()` on an `IRenewableCredential` is the seam you make it with. The session is untouched: a refused credential is not a lost session |
|
|
696
916
|
| **403** | propagates untouched. The server authenticated the caller and refused the action anyway, so a new token is the same caller — usually the body names the authorization object |
|
|
697
917
|
| anything else | untouched |
|
|
698
918
|
|
|
699
|
-
The connection never replaces the server's error with one of its own:
|
|
700
|
-
`
|
|
701
|
-
`JWT token has expired. Please re-authenticate.` with the original error discarded.
|
|
919
|
+
The connection never replaces the server's error with one of its own:
|
|
920
|
+
`failure.response.status` and `failure.response.data` are always what SAP sent.
|
|
702
921
|
|
|
703
|
-
Concurrent requests that meet the same expired token share **one** renewal — a
|
|
704
|
-
and a single session re-establishment between them, not one
|
|
705
|
-
|
|
706
|
-
### `createAbapConnection()` Factory
|
|
707
|
-
|
|
708
|
-
Recommended way to create connections:
|
|
709
|
-
|
|
710
|
-
```typescript
|
|
711
|
-
function createAbapConnection(
|
|
712
|
-
config: SapConfig,
|
|
713
|
-
logger?: ILogger | null,
|
|
714
|
-
sessionId?: string
|
|
715
|
-
): AbapConnection;
|
|
716
|
-
```
|
|
922
|
+
Concurrent requests that meet the same expired token share **one** renewal — a
|
|
923
|
+
single token fetch and a single session re-establishment between them, not one
|
|
924
|
+
each.
|
|
717
925
|
|
|
718
|
-
|
|
926
|
+
**A refusal during `connect()` surfaces.** Nothing is renewed behind you there:
|
|
927
|
+
the provider already renews on expiry it can see, every time it is asked for a
|
|
928
|
+
header, so a refusal at establishment means the credential needs attention that
|
|
929
|
+
this library cannot give it.
|
|
719
930
|
|
|
720
931
|
### Configuration Types
|
|
721
932
|
|
|
@@ -747,11 +958,11 @@ See [examples/](../examples/) for complete working examples:
|
|
|
747
958
|
|
|
748
959
|
## Best Practices
|
|
749
960
|
|
|
750
|
-
1. **
|
|
961
|
+
1. **State the three axes**: the connector says which system, the provider says which credential, the transport says which wire. Nothing is detected, and a connection that had to guess would guess wrong on the case that matters
|
|
751
962
|
2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for multi-request operations (locks, transactions)
|
|
752
963
|
3. **Token Refresh**: For cloud systems, use `@mcp-abap-adt/auth-broker` for token refresh functionality
|
|
753
964
|
4. **Session State Persistence**: Use `@mcp-abap-adt/auth-broker` for session state persistence
|
|
754
|
-
5. **Handle Errors Gracefully**: Wrap requests in try-catch blocks and check `
|
|
965
|
+
5. **Handle Errors Gracefully**: Wrap requests in try-catch blocks and check `failure.response` for HTTP errors
|
|
755
966
|
6. **Use Proper Logging**: Implement custom logger for production systems with appropriate log levels (logger is optional)
|
|
756
967
|
7. **Session ID Management**: Session IDs are auto-generated (UUID) or can be provided when creating connection
|
|
757
968
|
8. **Switch Session Types**: Use `setSessionType()` to dynamically change between stateful/stateless modes
|