@mcp-abap-adt/connection 5.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 +134 -1
- package/README.md +204 -48
- package/dist/auth/providers.d.ts +38 -9
- package/dist/auth/providers.d.ts.map +1 -1
- package/dist/auth/providers.js +46 -3
- package/dist/connection/AbstractAbapConnection.d.ts +83 -136
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +241 -763
- package/dist/connection/AdtCloudConnector.d.ts +12 -7
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
- package/dist/connection/AdtCloudConnector.js +2 -6
- package/dist/connection/AdtOnPremConnector.d.ts +20 -7
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
- package/dist/connection/AdtOnPremConnector.js +2 -6
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
- package/dist/connection/CredentialAbapConnection.d.ts +8 -41
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
- package/dist/connection/CredentialAbapConnection.js +44 -111
- 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 +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -18
- package/dist/utils/timeouts.d.ts +6 -19
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -22
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -10
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
- package/docs/USAGE.md +260 -119
- 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/auth/IAuthProvider.d.ts +0 -84
- package/dist/auth/IAuthProvider.d.ts.map +0 -1
- package/dist/auth/IAuthProvider.js +0 -21
- package/dist/connection/BaseAbapConnection.d.ts +0 -29
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -81
- package/dist/connection/CertificateAbapConnection.d.ts +0 -35
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -91
- package/dist/connection/JwtAbapConnection.d.ts +0 -131
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -376
- package/dist/connection/KerberosAbapConnection.d.ts +0 -32
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -128
- package/dist/connection/RfcAbapConnection.d.ts +0 -44
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -324
- package/dist/connection/SamlAbapConnection.d.ts +0 -31
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -81
- package/dist/connection/connectionFactory.d.ts +0 -25
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- package/dist/connection/connectionFactory.js +0 -84
- package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
- package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
- package/dist/session/IcfSessionStrategy.d.ts +0 -27
- package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
- package/dist/session/IcfSessionStrategy.js +0 -62
- package/dist/session/SessionStrategy.d.ts +0 -86
- package/dist/session/SessionStrategy.d.ts.map +0 -1
- package/dist/session/SessionStrategy.js +0 -33
- package/docs/superpowers/specs/2026-08-21-platform-connectors.md +0 -108
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,12 +40,18 @@ const logger = {
|
|
|
35
40
|
debug: (msg: string, meta?: any) => console.debug(msg, meta),
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
);
|
|
44
55
|
|
|
45
56
|
// Establish the session. This is REQUIRED: a request on a connection that was
|
|
46
57
|
// never connected is refused with ADT_NOT_CONNECTED, and connect() rejects if
|
|
@@ -50,13 +61,14 @@ await connection.connect();
|
|
|
50
61
|
const response = await connection.makeAdtRequest({
|
|
51
62
|
method: 'GET',
|
|
52
63
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
64
|
+
timeout: getTimeout('default'),
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
console.log(response.data);
|
|
56
68
|
```
|
|
57
69
|
|
|
58
70
|
Tearing the session down is `disconnect()`, but it is not on the
|
|
59
|
-
`IAbapConnection` type
|
|
71
|
+
`IAbapConnection` type a connector satisfies — see
|
|
60
72
|
[Session Lifecycle](#session-lifecycle) for where it lives and how to reach it.
|
|
61
73
|
|
|
62
74
|
## Which system, and how you authenticate
|
|
@@ -79,6 +91,7 @@ For on-premise SAP systems using basic authentication:
|
|
|
79
91
|
import {
|
|
80
92
|
AdtOnPremConnector,
|
|
81
93
|
BasicAuthProvider,
|
|
94
|
+
OnPremHttpTransport,
|
|
82
95
|
} from '@mcp-abap-adt/connection';
|
|
83
96
|
|
|
84
97
|
const config = {
|
|
@@ -92,6 +105,10 @@ const config = {
|
|
|
92
105
|
const connection = new AdtOnPremConnector(
|
|
93
106
|
config,
|
|
94
107
|
new BasicAuthProvider(config.username, config.password),
|
|
108
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
109
|
+
client: config.client,
|
|
110
|
+
baseUrl: config.url,
|
|
111
|
+
}),
|
|
95
112
|
logger,
|
|
96
113
|
);
|
|
97
114
|
await connection.connect(); // required: nothing is established implicitly
|
|
@@ -105,7 +122,9 @@ For SAP BTP ABAP Environment. Token refresh belongs to
|
|
|
105
122
|
```typescript
|
|
106
123
|
import {
|
|
107
124
|
AdtCloudConnector,
|
|
125
|
+
CloudHttpTransport,
|
|
108
126
|
TokenAuthProvider,
|
|
127
|
+
getTimeout,
|
|
109
128
|
} from '@mcp-abap-adt/connection';
|
|
110
129
|
|
|
111
130
|
const config = {
|
|
@@ -120,6 +139,10 @@ const config = {
|
|
|
120
139
|
const connection = new AdtCloudConnector(
|
|
121
140
|
config,
|
|
122
141
|
new TokenAuthProvider('eyJhbGciOiJSUzI1NiIs...'),
|
|
142
|
+
new CloudHttpTransport(() => ({}), logger, {
|
|
143
|
+
client: config.client,
|
|
144
|
+
baseUrl: config.url,
|
|
145
|
+
}),
|
|
123
146
|
logger,
|
|
124
147
|
);
|
|
125
148
|
await connection.connect(); // required: nothing is established implicitly
|
|
@@ -129,6 +152,7 @@ await connection.connect(); // required: nothing is established implicitly
|
|
|
129
152
|
const response = await connection.makeAdtRequest({
|
|
130
153
|
method: 'GET',
|
|
131
154
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
155
|
+
timeout: getTimeout('default'),
|
|
132
156
|
});
|
|
133
157
|
```
|
|
134
158
|
|
|
@@ -142,6 +166,7 @@ them is refused with `ADT_NOT_CONNECTED`.
|
|
|
142
166
|
### GET Request
|
|
143
167
|
|
|
144
168
|
```typescript
|
|
169
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
145
170
|
const packages = await connection.makeAdtRequest({
|
|
146
171
|
method: 'GET',
|
|
147
172
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
@@ -150,12 +175,14 @@ const packages = await connection.makeAdtRequest({
|
|
|
150
175
|
parent_type: 'DEVC/K',
|
|
151
176
|
withShortDescriptions: 'true',
|
|
152
177
|
},
|
|
178
|
+
timeout: getTimeout('default'),
|
|
153
179
|
});
|
|
154
180
|
```
|
|
155
181
|
|
|
156
182
|
### POST Request (Create Object)
|
|
157
183
|
|
|
158
184
|
```typescript
|
|
185
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
159
186
|
const classXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
160
187
|
<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
|
|
161
188
|
class:name="ZCL_MY_CLASS">
|
|
@@ -168,28 +195,35 @@ const response = await connection.makeAdtRequest({
|
|
|
168
195
|
headers: { 'Content-Type': 'application/xml' },
|
|
169
196
|
data: classXml,
|
|
170
197
|
params: { package: 'ZTEST' },
|
|
198
|
+
timeout: getTimeout('default'),
|
|
171
199
|
});
|
|
172
200
|
```
|
|
173
201
|
|
|
174
202
|
### PUT Request (Update Object)
|
|
175
203
|
|
|
176
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';
|
|
177
208
|
await connection.makeAdtRequest({
|
|
178
209
|
method: 'PUT',
|
|
179
210
|
url: '/sap/bc/adt/oo/classes/zcl_my_class/source/main',
|
|
180
211
|
headers: { 'Content-Type': 'text/plain' },
|
|
181
|
-
data: classSourceCode,
|
|
182
|
-
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'),
|
|
183
215
|
});
|
|
184
216
|
```
|
|
185
217
|
|
|
186
218
|
### DELETE Request
|
|
187
219
|
|
|
188
220
|
```typescript
|
|
221
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
189
222
|
await connection.makeAdtRequest({
|
|
190
223
|
method: 'DELETE',
|
|
191
224
|
url: '/sap/bc/adt/oo/classes/zcl_my_class',
|
|
192
225
|
params: { deleteOption: 'deleteAndLocalVersions' },
|
|
226
|
+
timeout: getTimeout('default'),
|
|
193
227
|
});
|
|
194
228
|
```
|
|
195
229
|
|
|
@@ -200,13 +234,20 @@ await connection.makeAdtRequest({
|
|
|
200
234
|
By default, connections are stateless - each request gets fresh cookies and CSRF tokens:
|
|
201
235
|
|
|
202
236
|
```typescript
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
);
|
|
206
247
|
await connection.connect();
|
|
207
248
|
|
|
208
249
|
// Each request is independent
|
|
209
|
-
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
250
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
210
251
|
```
|
|
211
252
|
|
|
212
253
|
### Stateful Mode (Session Headers)
|
|
@@ -214,16 +255,23 @@ await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' })
|
|
|
214
255
|
Enable stateful session mode for operations requiring consistent session state:
|
|
215
256
|
|
|
216
257
|
```typescript
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
);
|
|
220
268
|
await connection.connect();
|
|
221
269
|
|
|
222
270
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
223
271
|
connection.setSessionType('stateful');
|
|
224
272
|
|
|
225
273
|
// Now all requests share the same session (cookies, CSRF token)
|
|
226
|
-
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
274
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
227
275
|
|
|
228
276
|
// Check session mode
|
|
229
277
|
console.log(connection.getSessionMode()); // 'stateful'
|
|
@@ -255,10 +303,12 @@ implied. Four things follow from it.
|
|
|
255
303
|
> `IAbapConnection`: `ISessionLifecycleAware` — `disconnect()`, `isConnected()`,
|
|
256
304
|
> `getSessionIdentity()`.
|
|
257
305
|
>
|
|
258
|
-
> The split is the point. `IAbapConnection` is the minimum
|
|
259
|
-
> honour
|
|
260
|
-
>
|
|
261
|
-
>
|
|
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.
|
|
262
312
|
>
|
|
263
313
|
> So ask for the atom you need, not for a concrete class:
|
|
264
314
|
>
|
|
@@ -269,24 +319,30 @@ implied. Four things follow from it.
|
|
|
269
319
|
> **How you satisfy that parameter decides whether you get a compile-time
|
|
270
320
|
> guarantee, and there is only one way that does.**
|
|
271
321
|
>
|
|
272
|
-
> Construct
|
|
273
|
-
>
|
|
274
|
-
> site:
|
|
322
|
+
> Construct a connector and the compiler knows it implements the atom, whichever
|
|
323
|
+
> wire you gave it:
|
|
275
324
|
>
|
|
276
325
|
> ```typescript
|
|
277
|
-
> const conn = new AdtOnPremConnector(config, provider, logger);
|
|
278
|
-
> tearDownAfter(conn);
|
|
279
|
-
>
|
|
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
|
|
280
336
|
> ```
|
|
281
337
|
>
|
|
282
|
-
>
|
|
283
|
-
>
|
|
284
|
-
>
|
|
285
|
-
>
|
|
286
|
-
>
|
|
287
|
-
>
|
|
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.
|
|
288
344
|
>
|
|
289
|
-
> When you only have an `IAbapConnection` — from
|
|
345
|
+
> When you only have an `IAbapConnection` — from a caller, or from a registry —
|
|
290
346
|
> narrow it at runtime with a predicate:
|
|
291
347
|
>
|
|
292
348
|
> ```typescript
|
|
@@ -322,11 +378,15 @@ implied. Four things follow from it.
|
|
|
322
378
|
### connect() is required, and it tells the truth
|
|
323
379
|
|
|
324
380
|
```typescript
|
|
325
|
-
import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
|
|
381
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
326
382
|
|
|
327
383
|
const connection = new AdtOnPremConnector(
|
|
328
384
|
config,
|
|
329
|
-
new BasicAuthProvider(
|
|
385
|
+
new BasicAuthProvider(config.username!, config.password!),
|
|
386
|
+
new OnPremHttpTransport(() => ({}), logger, {
|
|
387
|
+
client: config.client,
|
|
388
|
+
baseUrl: config.url,
|
|
389
|
+
}),
|
|
330
390
|
logger,
|
|
331
391
|
);
|
|
332
392
|
|
|
@@ -348,6 +408,13 @@ side instead of a condition that silently stops matching. The codes live in
|
|
|
348
408
|
|
|
349
409
|
```typescript
|
|
350
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
|
+
};
|
|
351
418
|
|
|
352
419
|
try {
|
|
353
420
|
await connection.makeAdtRequest(options);
|
|
@@ -360,9 +427,9 @@ try {
|
|
|
360
427
|
```
|
|
361
428
|
|
|
362
429
|
`AdtSessionErrorCode` comes from the same place, as does the capability interface
|
|
363
|
-
this connection implements — `ISessionLifecycleAware`. Depend on
|
|
364
|
-
on a concrete
|
|
365
|
-
|
|
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
|
|
366
433
|
documentation of what your code actually needs.
|
|
367
434
|
|
|
368
435
|
### disconnect() waits for nothing
|
|
@@ -456,14 +523,29 @@ yours.
|
|
|
456
523
|
Session IDs are auto-generated (UUID) when connection is created:
|
|
457
524
|
|
|
458
525
|
```typescript
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
);
|
|
462
536
|
console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
|
|
463
537
|
|
|
464
538
|
// Or provide your own when creating connection
|
|
465
|
-
const
|
|
466
|
-
|
|
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'
|
|
467
549
|
```
|
|
468
550
|
|
|
469
551
|
### Switching Session Types
|
|
@@ -471,17 +553,24 @@ console.log(connection.getSessionId()); // 'custom-session-123'
|
|
|
471
553
|
Dynamically switch between stateful and stateless modes:
|
|
472
554
|
|
|
473
555
|
```typescript
|
|
556
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport, getTimeout } from '@mcp-abap-adt/connection';
|
|
474
557
|
// Start in stateless mode (default)
|
|
475
|
-
const connection =
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
);
|
|
478
567
|
await connection.connect();
|
|
479
568
|
|
|
480
569
|
// Enable stateful for a series of operations
|
|
481
570
|
connection.setSessionType('stateful');
|
|
482
571
|
|
|
483
572
|
// Do stateful operations...
|
|
484
|
-
await connection.makeAdtRequest({ method: 'POST', url: '...' });
|
|
573
|
+
await connection.makeAdtRequest({ method: 'POST', url: '...' , timeout: getTimeout('default') });
|
|
485
574
|
|
|
486
575
|
// Switch back to stateless
|
|
487
576
|
connection.setSessionType('stateless');
|
|
@@ -506,7 +595,7 @@ in a `finally`. `disconnect()` never throws, so it is safe there.
|
|
|
506
595
|
### Using Custom Logger
|
|
507
596
|
|
|
508
597
|
```typescript
|
|
509
|
-
import { ILogger } from '@mcp-abap-adt/connection';
|
|
598
|
+
import { AdtOnPremConnector, BasicAuthProvider, ILogger, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
510
599
|
|
|
511
600
|
class CustomLogger implements ILogger {
|
|
512
601
|
info(message: string, meta?: any) {
|
|
@@ -539,9 +628,15 @@ class CustomLogger implements ILogger {
|
|
|
539
628
|
}
|
|
540
629
|
|
|
541
630
|
const logger = new CustomLogger();
|
|
542
|
-
const connection =
|
|
543
|
-
|
|
544
|
-
|
|
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
|
+
);
|
|
545
640
|
```
|
|
546
641
|
|
|
547
642
|
## Error Handling
|
|
@@ -549,16 +644,23 @@ const connection = createAbapConnection(config, logger, undefined, undefined, {
|
|
|
549
644
|
### Basic Error Handling
|
|
550
645
|
|
|
551
646
|
```typescript
|
|
647
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
552
648
|
try {
|
|
553
649
|
await connection.makeAdtRequest({
|
|
554
650
|
method: 'GET',
|
|
555
651
|
url: '/sap/bc/adt/invalid/endpoint',
|
|
652
|
+
timeout: getTimeout('default'),
|
|
556
653
|
});
|
|
557
654
|
} catch (error) {
|
|
558
|
-
|
|
559
|
-
|
|
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);
|
|
560
662
|
} else {
|
|
561
|
-
console.error('Network error:',
|
|
663
|
+
console.error('Network error:', failure.message);
|
|
562
664
|
}
|
|
563
665
|
}
|
|
564
666
|
```
|
|
@@ -580,23 +682,30 @@ When these errors occur, the connection:
|
|
|
580
682
|
3. **Logs the error** with full context for troubleshooting
|
|
581
683
|
|
|
582
684
|
```typescript
|
|
685
|
+
import { getTimeout } from '@mcp-abap-adt/connection';
|
|
583
686
|
try {
|
|
584
687
|
await connection.makeAdtRequest({
|
|
585
688
|
method: 'GET',
|
|
586
689
|
url: '/sap/bc/adt/repository/nodestructure',
|
|
690
|
+
timeout: getTimeout('default'),
|
|
587
691
|
});
|
|
588
692
|
} catch (error) {
|
|
693
|
+
const failure = error as {
|
|
694
|
+
code?: string;
|
|
695
|
+
response?: { status: number; data: unknown };
|
|
696
|
+
message?: string;
|
|
697
|
+
};
|
|
589
698
|
// Check for specific network error codes
|
|
590
|
-
if (
|
|
699
|
+
if (failure.code === 'ECONNREFUSED') {
|
|
591
700
|
console.error('Cannot connect to SAP server - check VPN connection');
|
|
592
|
-
} else if (
|
|
701
|
+
} else if (failure.code === 'ETIMEDOUT') {
|
|
593
702
|
console.error('Connection timeout - server not responding');
|
|
594
|
-
} else if (
|
|
703
|
+
} else if (failure.code === 'ENOTFOUND') {
|
|
595
704
|
console.error('Cannot resolve hostname - check SAP URL');
|
|
596
|
-
} else if (
|
|
597
|
-
console.error(`HTTP ${
|
|
705
|
+
} else if (failure.response) {
|
|
706
|
+
console.error(`HTTP ${failure.response.status}:`, failure.response.data);
|
|
598
707
|
} else {
|
|
599
|
-
console.error('Request failed:',
|
|
708
|
+
console.error('Request failed:', failure.message);
|
|
600
709
|
}
|
|
601
710
|
}
|
|
602
711
|
```
|
|
@@ -616,6 +725,8 @@ try {
|
|
|
616
725
|
connection satisfies, including RFC:
|
|
617
726
|
|
|
618
727
|
```typescript
|
|
728
|
+
import { AbapRequestOptions } from '@mcp-abap-adt/connection';
|
|
729
|
+
import type { AxiosResponse } from 'axios';
|
|
619
730
|
interface AbapConnection {
|
|
620
731
|
connect(): Promise<void>; // REQUIRED before any request
|
|
621
732
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
@@ -625,27 +736,42 @@ interface AbapConnection {
|
|
|
625
736
|
}
|
|
626
737
|
```
|
|
627
738
|
|
|
628
|
-
Anything beyond that is **not** on the contract. `getSessionMode()`
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
`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.
|
|
634
744
|
|
|
635
745
|
### `AdtOnPremConnector` / `AdtCloudConnector`
|
|
636
746
|
|
|
637
747
|
One per system, each handed an auth provider:
|
|
638
748
|
|
|
639
|
-
```
|
|
640
|
-
class AdtOnPremConnector
|
|
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
|
+
> {
|
|
641
767
|
constructor(
|
|
642
768
|
config: SapConfig,
|
|
643
|
-
|
|
769
|
+
credential: TCredential,
|
|
770
|
+
transport: TTransport,
|
|
644
771
|
logger?: ILogger | null,
|
|
645
772
|
sessionId?: string,
|
|
646
773
|
);
|
|
647
774
|
}
|
|
648
|
-
// AdtCloudConnector has the same shape.
|
|
649
775
|
```
|
|
650
776
|
|
|
651
777
|
The difference is what each does with the session: on-prem takes it from the
|
|
@@ -653,23 +779,47 @@ establishing call and gives it back with the platform's ICF logoff; cloud opens
|
|
|
653
779
|
one at `/sap/bc/adt/core/http/sessions` and gives it back by `DELETE` on the
|
|
654
780
|
address the server publishes.
|
|
655
781
|
|
|
656
|
-
|
|
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.
|
|
657
789
|
|
|
658
|
-
|
|
659
|
-
[Migration to 5.0](./MIGRATION-5.0.md).
|
|
790
|
+
### `HttpTransport` / `RfcTransport`
|
|
660
791
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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?)
|
|
665
798
|
```
|
|
666
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
|
+
|
|
667
817
|
### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES` (New in 0.1.13+)
|
|
668
818
|
|
|
669
819
|
Exported constants for consistent CSRF token handling across different connection implementations:
|
|
670
820
|
|
|
671
|
-
```
|
|
672
|
-
|
|
821
|
+
```text
|
|
822
|
+
// Imported from '@mcp-abap-adt/connection'; shapes, not runnable code.
|
|
673
823
|
|
|
674
824
|
// CSRF_CONFIG structure:
|
|
675
825
|
const config = {
|
|
@@ -726,7 +876,7 @@ export class CloudSdkAbapConnection {
|
|
|
726
876
|
throw new Error(
|
|
727
877
|
CSRF_ERROR_MESSAGES.FETCH_FAILED(
|
|
728
878
|
CSRF_CONFIG.RETRY_COUNT + 1,
|
|
729
|
-
error instanceof Error ?
|
|
879
|
+
error instanceof Error ? failure.message : String(error)
|
|
730
880
|
)
|
|
731
881
|
);
|
|
732
882
|
}
|
|
@@ -740,52 +890,43 @@ export class CloudSdkAbapConnection {
|
|
|
740
890
|
```
|
|
741
891
|
|
|
742
892
|
|
|
743
|
-
###
|
|
893
|
+
### The credential, and how a refusal is classified
|
|
744
894
|
|
|
745
|
-
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.
|
|
746
900
|
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
);
|
|
755
|
-
// Note: refreshToken() and canRefreshToken() methods removed in 0.2.0
|
|
756
|
-
// Token acquisition itself belongs to @mcp-abap-adt/auth-broker
|
|
757
|
-
}
|
|
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
|
+
);
|
|
758
908
|
```
|
|
759
909
|
|
|
760
|
-
**How failures are classified** (
|
|
761
|
-
[MIGRATION-
|
|
910
|
+
**How failures are classified** (6.0.0 — see
|
|
911
|
+
[MIGRATION-6.0.md](./MIGRATION-6.0.md)):
|
|
762
912
|
|
|
763
913
|
| answer | what happens |
|
|
764
914
|
|---|---|
|
|
765
|
-
| **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 |
|
|
766
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 |
|
|
767
917
|
| anything else | untouched |
|
|
768
918
|
|
|
769
|
-
The connection never replaces the server's error with one of its own:
|
|
770
|
-
`
|
|
771
|
-
`JWT token has expired. Please re-authenticate.` with the original error discarded.
|
|
772
|
-
|
|
773
|
-
Concurrent requests that meet the same expired token share **one** renewal — a single token fetch
|
|
774
|
-
and a single session re-establishment between them, not one each.
|
|
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.
|
|
775
921
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
```typescript
|
|
781
|
-
function createAbapConnection(
|
|
782
|
-
config: SapConfig,
|
|
783
|
-
logger?: ILogger | null,
|
|
784
|
-
sessionId?: string
|
|
785
|
-
): AbapConnection;
|
|
786
|
-
```
|
|
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.
|
|
787
925
|
|
|
788
|
-
|
|
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.
|
|
789
930
|
|
|
790
931
|
### Configuration Types
|
|
791
932
|
|
|
@@ -817,11 +958,11 @@ See [examples/](../examples/) for complete working examples:
|
|
|
817
958
|
|
|
818
959
|
## Best Practices
|
|
819
960
|
|
|
820
|
-
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
|
|
821
962
|
2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for multi-request operations (locks, transactions)
|
|
822
963
|
3. **Token Refresh**: For cloud systems, use `@mcp-abap-adt/auth-broker` for token refresh functionality
|
|
823
964
|
4. **Session State Persistence**: Use `@mcp-abap-adt/auth-broker` for session state persistence
|
|
824
|
-
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
|
|
825
966
|
6. **Use Proper Logging**: Implement custom logger for production systems with appropriate log levels (logger is optional)
|
|
826
967
|
7. **Session ID Management**: Session IDs are auto-generated (UUID) or can be provided when creating connection
|
|
827
968
|
8. **Switch Session Types**: Use `setSessionType()` to dynamically change between stateful/stateless modes
|