@mcp-abap-adt/connection 1.10.1 → 2.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 +758 -0
- package/README.md +42 -11
- package/dist/__tests__/helpers/session.d.ts +15 -0
- package/dist/__tests__/helpers/session.d.ts.map +1 -0
- package/dist/__tests__/helpers/session.js +19 -0
- package/dist/auth/ntlm.d.ts +15 -0
- package/dist/auth/ntlm.d.ts.map +1 -1
- package/dist/auth/ntlm.js +38 -0
- package/dist/connection/AbstractAbapConnection.d.ts +163 -11
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +351 -14
- package/dist/connection/BaseAbapConnection.d.ts +5 -1
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
- package/dist/connection/BaseAbapConnection.js +11 -1
- package/dist/connection/CertificateAbapConnection.d.ts +5 -1
- package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
- package/dist/connection/CertificateAbapConnection.js +11 -1
- package/dist/connection/JwtAbapConnection.d.ts +3 -2
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +19 -8
- package/dist/connection/KerberosAbapConnection.d.ts +5 -1
- package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
- package/dist/connection/KerberosAbapConnection.js +54 -3
- package/dist/connection/SamlAbapConnection.d.ts +5 -1
- package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
- package/dist/connection/SamlAbapConnection.js +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/session/SessionLifecycle.d.ts +131 -0
- package/dist/session/SessionLifecycle.d.ts.map +1 -0
- package/dist/session/SessionLifecycle.js +301 -0
- package/docs/INDEX.md +106 -0
- package/docs/INSTALLATION.md +304 -0
- package/docs/JWT_AUTH_TOOLS.md +142 -0
- package/docs/MIGRATION-2.0.md +125 -0
- package/docs/SCOPE.md +44 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +121 -0
- package/docs/USAGE.md +749 -0
- package/examples/README.md +112 -0
- package/examples/basic-connection.js +55 -0
- package/examples/jwt-with-token-refresh.js +87 -0
- package/examples/saml-connection.js +52 -0
- package/examples/websocket-transport.js +87 -0
- package/package.json +11 -4
package/docs/USAGE.md
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
# Usage Guide
|
|
2
|
+
|
|
3
|
+
**Version:** see [CHANGELOG.md](../CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Start](#quick-start)
|
|
8
|
+
- [Authentication Types](#authentication-types)
|
|
9
|
+
- [Making ADT Requests](#making-adt-requests)
|
|
10
|
+
- [Session Management](#session-management)
|
|
11
|
+
- [Advanced Features](#advanced-features)
|
|
12
|
+
- [API Reference](#api-reference)
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Basic Usage with Factory
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { createAbapConnection } from '@mcp-abap-adt/connection';
|
|
20
|
+
import { SapConfig } from '@mcp-abap-adt/connection';
|
|
21
|
+
|
|
22
|
+
const config: SapConfig = {
|
|
23
|
+
url: 'https://your-sap-server.com',
|
|
24
|
+
authType: 'basic',
|
|
25
|
+
username: 'your-username',
|
|
26
|
+
password: 'your-password',
|
|
27
|
+
client: '100',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Logger is optional - if not provided, no logging output
|
|
31
|
+
const logger = {
|
|
32
|
+
info: (msg: string, meta?: any) => console.log(msg, meta),
|
|
33
|
+
error: (msg: string, meta?: any) => console.error(msg, meta),
|
|
34
|
+
warn: (msg: string, meta?: any) => console.warn(msg, meta),
|
|
35
|
+
debug: (msg: string, meta?: any) => console.debug(msg, meta),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Create connection (logger is optional)
|
|
39
|
+
const connection = createAbapConnection(config, logger);
|
|
40
|
+
// Or without logger:
|
|
41
|
+
// const connection = createAbapConnection(config);
|
|
42
|
+
|
|
43
|
+
// Establish the session. This is REQUIRED: a request on a connection that was
|
|
44
|
+
// never connected is refused with ADT_NOT_CONNECTED, and connect() rejects if
|
|
45
|
+
// the session cannot be established — it never resolves over a broken one.
|
|
46
|
+
await connection.connect();
|
|
47
|
+
|
|
48
|
+
const response = await connection.makeAdtRequest({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
url: '/sap/bc/adt/repository/nodestructure',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(response.data);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Tearing the session down is `disconnect()`, but it is not on the
|
|
57
|
+
`IAbapConnection` type this factory returns — see
|
|
58
|
+
[Session Lifecycle](#session-lifecycle) for where it lives and how to reach it.
|
|
59
|
+
|
|
60
|
+
## Authentication Types
|
|
61
|
+
|
|
62
|
+
### Basic Authentication (On-Premise)
|
|
63
|
+
|
|
64
|
+
For on-premise SAP systems using basic authentication:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { BaseAbapConnection } from '@mcp-abap-adt/connection';
|
|
68
|
+
|
|
69
|
+
const config = {
|
|
70
|
+
url: 'https://sap-server.local:8000',
|
|
71
|
+
authType: 'basic' as const,
|
|
72
|
+
username: 'developer',
|
|
73
|
+
password: 'SecurePass123',
|
|
74
|
+
client: '100',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const connection = new BaseAbapConnection(config, logger);
|
|
78
|
+
await connection.connect(); // required: nothing is established implicitly
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### JWT Authentication (Cloud/BTP)
|
|
82
|
+
|
|
83
|
+
For SAP BTP ABAP Environment. Token refresh belongs to
|
|
84
|
+
`@mcp-abap-adt/auth-broker`; this package only carries the token:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { JwtAbapConnection } from '@mcp-abap-adt/connection';
|
|
88
|
+
|
|
89
|
+
const config = {
|
|
90
|
+
url: 'https://tenant.abap.cloud',
|
|
91
|
+
authType: 'jwt' as const,
|
|
92
|
+
jwtToken: 'eyJhbGciOiJSUzI1NiIs...',
|
|
93
|
+
client: '100', // Optional for cloud
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const connection = new JwtAbapConnection(config, logger);
|
|
97
|
+
await connection.connect(); // required: nothing is established implicitly
|
|
98
|
+
|
|
99
|
+
// Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
|
|
100
|
+
// Connection package only handles HTTP communication
|
|
101
|
+
const response = await connection.makeAdtRequest({
|
|
102
|
+
method: 'GET',
|
|
103
|
+
url: '/sap/bc/adt/repository/nodestructure',
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Making ADT Requests
|
|
108
|
+
|
|
109
|
+
All requests are made using the `makeAdtRequest()` method. CSRF token handling
|
|
110
|
+
is automatic; **the connection is not** — the examples below assume
|
|
111
|
+
`await connection.connect()` has already succeeded, and without it every one of
|
|
112
|
+
them is refused with `ADT_NOT_CONNECTED`.
|
|
113
|
+
|
|
114
|
+
### GET Request
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const packages = await connection.makeAdtRequest({
|
|
118
|
+
method: 'GET',
|
|
119
|
+
url: '/sap/bc/adt/repository/nodestructure',
|
|
120
|
+
params: {
|
|
121
|
+
parent_name: 'DEVC/K',
|
|
122
|
+
parent_type: 'DEVC/K',
|
|
123
|
+
withShortDescriptions: 'true',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### POST Request (Create Object)
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const classXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
132
|
+
<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes"
|
|
133
|
+
class:name="ZCL_MY_CLASS">
|
|
134
|
+
<class:description>My Test Class</class:description>
|
|
135
|
+
</class:abapClass>`;
|
|
136
|
+
|
|
137
|
+
const response = await connection.makeAdtRequest({
|
|
138
|
+
method: 'POST',
|
|
139
|
+
url: '/sap/bc/adt/oo/classes',
|
|
140
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
141
|
+
data: classXml,
|
|
142
|
+
params: { package: 'ZTEST' },
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### PUT Request (Update Object)
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
await connection.makeAdtRequest({
|
|
150
|
+
method: 'PUT',
|
|
151
|
+
url: '/sap/bc/adt/oo/classes/zcl_my_class/source/main',
|
|
152
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
153
|
+
data: classSourceCode,
|
|
154
|
+
params: { lockHandle: lockToken },
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### DELETE Request
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
await connection.makeAdtRequest({
|
|
162
|
+
method: 'DELETE',
|
|
163
|
+
url: '/sap/bc/adt/oo/classes/zcl_my_class',
|
|
164
|
+
params: { deleteOption: 'deleteAndLocalVersions' },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Session Management
|
|
169
|
+
|
|
170
|
+
### Stateless Mode (Default)
|
|
171
|
+
|
|
172
|
+
By default, connections are stateless - each request gets fresh cookies and CSRF tokens:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const connection = createAbapConnection(config, logger);
|
|
176
|
+
await connection.connect();
|
|
177
|
+
|
|
178
|
+
// Each request is independent
|
|
179
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Stateful Mode (Session Headers)
|
|
183
|
+
|
|
184
|
+
Enable stateful session mode for operations requiring consistent session state:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const connection = createAbapConnection(config, logger);
|
|
188
|
+
await connection.connect();
|
|
189
|
+
|
|
190
|
+
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
191
|
+
connection.setSessionType('stateful');
|
|
192
|
+
|
|
193
|
+
// Now all requests share the same session (cookies, CSRF token)
|
|
194
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
195
|
+
|
|
196
|
+
// Check session mode
|
|
197
|
+
console.log(connection.getSessionMode()); // 'stateful'
|
|
198
|
+
|
|
199
|
+
// Get session ID (auto-generated UUID)
|
|
200
|
+
console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
|
|
201
|
+
|
|
202
|
+
// Switch back to stateless
|
|
203
|
+
connection.setSessionType('stateless');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Note:** Session state persistence is handled by `@mcp-abap-adt/auth-broker` package. The connection package only manages session headers (cookies, CSRF tokens) for HTTP communication.
|
|
207
|
+
|
|
208
|
+
## Session Lifecycle
|
|
209
|
+
|
|
210
|
+
The connection owns its session, and that ownership is explicit rather than
|
|
211
|
+
implied. Four things follow from it.
|
|
212
|
+
|
|
213
|
+
> **A Kerberos limitation worth knowing.** SPNEGO here is single-leg: one
|
|
214
|
+
> `step('')` produces the token and the GSS context is discarded. If the server
|
|
215
|
+
> continues the exchange — a 401 carrying a `Negotiate` token, which
|
|
216
|
+
> [RFC 4559](https://www.rfc-editor.org/rfc/rfc4559) defines as a continuation —
|
|
217
|
+
> this client cannot feed that token back, so `connect()` fails with an error
|
|
218
|
+
> saying exactly that. Multi-leg SPNEGO is not implemented.
|
|
219
|
+
|
|
220
|
+
> **Availability.** `connect()` is on the shared `IAbapConnection` contract and
|
|
221
|
+
> works on every connection. The rest of this section is on the contract too, but
|
|
222
|
+
> as two **capability atoms** in `@mcp-abap-adt/interfaces` (11.5.0+) rather than
|
|
223
|
+
> as methods on `IAbapConnection`: `ISessionLifecycleAware` (`disconnect()`,
|
|
224
|
+
> `isConnected()`, `getSessionIdentity()`) and `ILockWindowAware`
|
|
225
|
+
> (`beginWindow()`, `endWindow()`).
|
|
226
|
+
>
|
|
227
|
+
> The split is the point. `IAbapConnection` is the minimum every transport can
|
|
228
|
+
> honour, and `RfcAbapConnection` has none of these — on RFC the session *is* the
|
|
229
|
+
> open client, so it needs none. Requiring them of every connection would force a
|
|
230
|
+
> transport with no HTTP session to implement a lie.
|
|
231
|
+
>
|
|
232
|
+
> So ask for the atom you need, not for a concrete class:
|
|
233
|
+
>
|
|
234
|
+
> ```typescript
|
|
235
|
+
> function withLock(conn: IAbapConnection & ILockWindowAware) { /* ... */ }
|
|
236
|
+
> ```
|
|
237
|
+
>
|
|
238
|
+
> **How you satisfy that parameter decides whether you get a compile-time
|
|
239
|
+
> guarantee, and there is only one way that does.**
|
|
240
|
+
>
|
|
241
|
+
> Construct the connection through a concrete HTTP class and the compiler knows
|
|
242
|
+
> it implements the atoms, so passing an RFC connection is an error at the call
|
|
243
|
+
> site:
|
|
244
|
+
>
|
|
245
|
+
> ```typescript
|
|
246
|
+
> const conn = new BaseAbapConnection(config, logger);
|
|
247
|
+
> withLock(conn); // ✅ checked
|
|
248
|
+
> withLock(new RfcAbapConnection(cfg)); // ✅ compile error, as it should be
|
|
249
|
+
> ```
|
|
250
|
+
>
|
|
251
|
+
> `createAbapConnection()` cannot give you that. It returns `IAbapConnection` for
|
|
252
|
+
> **every** config, RFC included, so the type carries no evidence either way and
|
|
253
|
+
> the compiler rejects its result whatever the transport. Asserting past that —
|
|
254
|
+
> `conn as IAbapConnection & ILockWindowAware` — silences the error for the HTTP
|
|
255
|
+
> case *and* for RFC, which then fails at runtime on `beginWindow()`. An assertion
|
|
256
|
+
> is not a check; it is a promise you make to the compiler on your own authority.
|
|
257
|
+
>
|
|
258
|
+
> When you only have an `IAbapConnection` — from the factory, or from a caller —
|
|
259
|
+
> narrow it at runtime with a predicate:
|
|
260
|
+
>
|
|
261
|
+
> ```typescript
|
|
262
|
+
> function supportsLockWindows(
|
|
263
|
+
> conn: IAbapConnection,
|
|
264
|
+
> ): conn is IAbapConnection & ILockWindowAware {
|
|
265
|
+
> const candidate = conn as Partial<ILockWindowAware>;
|
|
266
|
+
> // EVERY method of the atom. A predicate narrows to the whole interface, so
|
|
267
|
+
> // checking one method and promising two puts the failure back where this
|
|
268
|
+
> // check was meant to remove it — inside the branch that looked safe.
|
|
269
|
+
> return (
|
|
270
|
+
> typeof candidate.beginWindow === 'function' &&
|
|
271
|
+
> typeof candidate.endWindow === 'function'
|
|
272
|
+
> );
|
|
273
|
+
> }
|
|
274
|
+
>
|
|
275
|
+
> if (supportsLockWindows(conn)) {
|
|
276
|
+
> withLock(conn); // narrowed by evidence, not by assertion
|
|
277
|
+
> } else {
|
|
278
|
+
> // No lock windows here. On RFC that is expected, not a failure.
|
|
279
|
+
> }
|
|
280
|
+
> ```
|
|
281
|
+
>
|
|
282
|
+
> That is a real check, and two things about it are load-bearing. The predicate
|
|
283
|
+
> covers the atom in full — a partial implementation must fail it, not pass it and
|
|
284
|
+
> break later. And the `else` branch is the honest part: a transport without the
|
|
285
|
+
> capability needs a different plan, not a cast.
|
|
286
|
+
>
|
|
287
|
+
> `ISessionLifecycleAware` takes the same treatment, over all three of
|
|
288
|
+
> `disconnect`, `isConnected` and `getSessionIdentity`.
|
|
289
|
+
|
|
290
|
+
### connect() is required, and it tells the truth
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { BaseAbapConnection } from '@mcp-abap-adt/connection';
|
|
294
|
+
|
|
295
|
+
const connection = new BaseAbapConnection(config, logger);
|
|
296
|
+
|
|
297
|
+
await connection.connect(); // establishes the session, or rejects
|
|
298
|
+
connection.isConnected(); // true only while a usable session exists
|
|
299
|
+
connection.getSessionIdentity(); // which SAP session, or null
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
A resolved `connect()` means a usable session exists — there is no third
|
|
303
|
+
outcome. A request before it, or after a teardown, is refused with
|
|
304
|
+
`ADT_NOT_CONNECTED` and never reaches the server.
|
|
305
|
+
|
|
306
|
+
`connect()` is idempotent and safe to call concurrently: callers share one
|
|
307
|
+
establishment rather than opening a session each.
|
|
308
|
+
|
|
309
|
+
Match on the code rather than the message, so a rename is a compile error on your
|
|
310
|
+
side instead of a condition that silently stops matching. The codes live in
|
|
311
|
+
`@mcp-abap-adt/interfaces` — import them from there, not from this package:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { ADT_SESSION_ERROR } from '@mcp-abap-adt/interfaces';
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await connection.makeAdtRequest(options);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if ((error as { code?: string }).code === ADT_SESSION_ERROR.SESSION_REPLACED) {
|
|
320
|
+
// The SAP session was replaced under us. Anything locked over the old one
|
|
321
|
+
// is orphaned: re-connect, then re-acquire the lock.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`ITeardownReport`, `WindowToken` and `AdtSessionErrorCode` come from the same
|
|
327
|
+
place, as do the two capability interfaces this connection implements —
|
|
328
|
+
`ISessionLifecycleAware` and `ILockWindowAware`. Depend on those rather than on a
|
|
329
|
+
concrete connection class where you can: an `RfcAbapConnection` is a valid
|
|
330
|
+
`IAbapConnection` that implements neither, so the atom you require is also the
|
|
331
|
+
documentation of what your code actually needs.
|
|
332
|
+
|
|
333
|
+
### disconnect() reports what it could not finish
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
const report = await connection.disconnect();
|
|
337
|
+
// { abandonedWindows: string[], releasePending: boolean }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
It never throws — the report says what did not finish instead. It waits for
|
|
341
|
+
in-flight requests to settle before clearing anything, so a teardown never
|
|
342
|
+
pulls the session out from under a request already running.
|
|
343
|
+
|
|
344
|
+
**Over HTTP it does not release locks.** The ABAP session lives on until its
|
|
345
|
+
timeout, along with whatever it held. Unlock first; disconnecting is not a way
|
|
346
|
+
to clean up after yourself.
|
|
347
|
+
|
|
348
|
+
### Lock windows
|
|
349
|
+
|
|
350
|
+
A lock outlives the request that takes it, which makes it the one thing a
|
|
351
|
+
teardown has to know about:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
const token = connection.beginWindow('Class/ZCL_MY_CLASS');
|
|
355
|
+
|
|
356
|
+
let lockHandle: string | undefined;
|
|
357
|
+
try {
|
|
358
|
+
lockHandle = await lock(connection, 'ZCL_MY_CLASS'); // your LOCK call
|
|
359
|
+
} catch (error) {
|
|
360
|
+
// The LOCK is confirmed to have failed, so nothing is held: close the window.
|
|
361
|
+
connection.endWindow(token);
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await update(connection, 'ZCL_MY_CLASS', lockHandle);
|
|
367
|
+
await unlock(connection, 'ZCL_MY_CLASS', lockHandle);
|
|
368
|
+
// The UNLOCK is confirmed: the lock is released, so close the window.
|
|
369
|
+
connection.endWindow(token);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
// Deliberately NOT closed here. The unlock may have failed, never gone out,
|
|
372
|
+
// or come back with an unknown outcome — in each of those the lock is most
|
|
373
|
+
// likely still held, and a window left open is what makes a teardown wait for
|
|
374
|
+
// it and report it by name instead of walking past it.
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Note what the `catch` does **not** do. A `finally { endWindow(token) }` reads
|
|
380
|
+
naturally and is wrong here: it closes the window exactly when the lock is most
|
|
381
|
+
likely still there. The window tracks *"a lock may be held"*, so only proof that
|
|
382
|
+
nothing is held closes it — a confirmed unlock, or a confirmed failure to lock.
|
|
383
|
+
|
|
384
|
+
The cost of forgetting `endWindow()` on the success path is not silence: every
|
|
385
|
+
later teardown waits out `SAP_TIMEOUT_CRITICAL` and then reports the window as
|
|
386
|
+
abandoned.
|
|
387
|
+
|
|
388
|
+
While a window is open, a teardown waits for it rather than abandoning it, and
|
|
389
|
+
a new window cannot be opened once a teardown has been requested. A window that
|
|
390
|
+
never closes is given up on after `SAP_TIMEOUT_CRITICAL` and comes back **named**
|
|
391
|
+
in `ITeardownReport.abandonedWindows`.
|
|
392
|
+
|
|
393
|
+
### When the session is lost
|
|
394
|
+
|
|
395
|
+
Two different things can cost you the session, and they do not behave alike.
|
|
396
|
+
|
|
397
|
+
**The session was replaced** — a renewed credential, or a session cookie that
|
|
398
|
+
changed underneath you. This is fatal **only while a lock window is open**:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// window open → ADT_SESSION_REPLACED, the connection stops being usable
|
|
402
|
+
// no window → transparent; work continues on the new session
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
With nothing held there is nothing to lose, so the connector carries on. The
|
|
406
|
+
error exists to tell you that a lock handle you are carrying is now dead, which
|
|
407
|
+
is the one thing you cannot work out for yourself.
|
|
408
|
+
|
|
409
|
+
**The server says the session is gone** — an answer meaning the session it was
|
|
410
|
+
given no longer exists. This is **always** fatal, window or not: the connection
|
|
411
|
+
raises `ADT_SESSION_REPLACED` and stops being usable. Unlike a replacement,
|
|
412
|
+
nothing here suggests a working session to continue on — the one we had is
|
|
413
|
+
confirmed dead, and re-establishing silently is what let the old connector
|
|
414
|
+
carry on over a session the caller never opened.
|
|
415
|
+
|
|
416
|
+
In both cases the connector does **not** retry the request internally. Retrying
|
|
417
|
+
blindly is what produced further orphaned locks in the field. That decision is
|
|
418
|
+
yours.
|
|
419
|
+
|
|
420
|
+
## Advanced Features
|
|
421
|
+
|
|
422
|
+
### Session ID Management
|
|
423
|
+
|
|
424
|
+
Session IDs are auto-generated (UUID) when connection is created:
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
const connection = createAbapConnection(config, logger);
|
|
428
|
+
console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
|
|
429
|
+
|
|
430
|
+
// Or provide your own when creating connection
|
|
431
|
+
const connection = createAbapConnection(config, logger, 'custom-session-123');
|
|
432
|
+
console.log(connection.getSessionId()); // 'custom-session-123'
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Switching Session Types
|
|
436
|
+
|
|
437
|
+
Dynamically switch between stateful and stateless modes:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// Start in stateless mode (default)
|
|
441
|
+
const connection = createAbapConnection(config, logger);
|
|
442
|
+
await connection.connect();
|
|
443
|
+
|
|
444
|
+
// Enable stateful for a series of operations
|
|
445
|
+
connection.setSessionType('stateful');
|
|
446
|
+
|
|
447
|
+
// Do stateful operations...
|
|
448
|
+
await connection.makeAdtRequest({ method: 'POST', url: '...' });
|
|
449
|
+
|
|
450
|
+
// Switch back to stateless
|
|
451
|
+
connection.setSessionType('stateless');
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Connection Reset
|
|
455
|
+
|
|
456
|
+
Reset connection state (clears cookies, CSRF token):
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
connection.reset();
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Custom Logging
|
|
463
|
+
|
|
464
|
+
### Using Custom Logger
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { ILogger } from '@mcp-abap-adt/connection';
|
|
468
|
+
|
|
469
|
+
class CustomLogger implements ILogger {
|
|
470
|
+
info(message: string, meta?: any) {
|
|
471
|
+
console.log(`[INFO] ${message}`, meta);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
warn(message: string, meta?: any) {
|
|
475
|
+
console.warn(`[WARN] ${message}`, meta);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
error(message: string, meta?: any) {
|
|
479
|
+
console.error(`[ERROR] ${message}`, meta);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
debug(message: string, meta?: any) {
|
|
483
|
+
if (process.env.DEBUG) {
|
|
484
|
+
console.debug(`[DEBUG] ${message}`, meta);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Optional: CSRF-specific logging
|
|
489
|
+
csrfToken?(action: 'fetch' | 'retry' | 'success' | 'error', message: string, meta?: any) {
|
|
490
|
+
console.log(`[CSRF:${action.toUpperCase()}] ${message}`, meta);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Optional: TLS config logging
|
|
494
|
+
tlsConfig?(rejectUnauthorized: boolean) {
|
|
495
|
+
console.log(`[TLS] rejectUnauthorized=${rejectUnauthorized}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const logger = new CustomLogger();
|
|
500
|
+
const connection = createAbapConnection(config, logger);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Error Handling
|
|
504
|
+
|
|
505
|
+
### Basic Error Handling
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
try {
|
|
509
|
+
await connection.makeAdtRequest({
|
|
510
|
+
method: 'GET',
|
|
511
|
+
url: '/sap/bc/adt/invalid/endpoint',
|
|
512
|
+
});
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error.response) {
|
|
515
|
+
console.error(`HTTP ${error.response.status}:`, error.response.data);
|
|
516
|
+
} else {
|
|
517
|
+
console.error('Network error:', error.message);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Network Error Detection
|
|
523
|
+
|
|
524
|
+
The connection automatically detects network-level errors and prevents unnecessary retry attempts. Network errors include:
|
|
525
|
+
|
|
526
|
+
- `ECONNREFUSED` - Connection refused (server not reachable)
|
|
527
|
+
- `ETIMEDOUT` - Connection timeout
|
|
528
|
+
- `ENOTFOUND` - DNS resolution failed (hostname not found)
|
|
529
|
+
- `ECONNRESET` - Connection reset by peer
|
|
530
|
+
- `ENETUNREACH` - Network unreachable
|
|
531
|
+
- `EHOSTUNREACH` - Host unreachable
|
|
532
|
+
|
|
533
|
+
When these errors occur, the connection:
|
|
534
|
+
1. **Does NOT attempt CSRF token retry** - network issues can't be fixed by retrying authentication
|
|
535
|
+
2. **Immediately throws the error** with clear network-related message
|
|
536
|
+
3. **Logs the error** with full context for troubleshooting
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
try {
|
|
540
|
+
await connection.makeAdtRequest({
|
|
541
|
+
method: 'GET',
|
|
542
|
+
url: '/sap/bc/adt/repository/nodestructure',
|
|
543
|
+
});
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// Check for specific network error codes
|
|
546
|
+
if (error.code === 'ECONNREFUSED') {
|
|
547
|
+
console.error('Cannot connect to SAP server - check VPN connection');
|
|
548
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
549
|
+
console.error('Connection timeout - server not responding');
|
|
550
|
+
} else if (error.code === 'ENOTFOUND') {
|
|
551
|
+
console.error('Cannot resolve hostname - check SAP URL');
|
|
552
|
+
} else if (error.response) {
|
|
553
|
+
console.error(`HTTP ${error.response.status}:`, error.response.data);
|
|
554
|
+
} else {
|
|
555
|
+
console.error('Request failed:', error.message);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**Best Practices:**
|
|
561
|
+
- Always handle network errors separately from HTTP errors
|
|
562
|
+
- Network errors indicate infrastructure issues (VPN, DNS, firewall)
|
|
563
|
+
- HTTP errors (401, 403, 404, etc.) indicate application-level issues
|
|
564
|
+
- Use error codes to provide specific user guidance
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
## API Reference
|
|
568
|
+
|
|
569
|
+
### `AbapConnection` Interface
|
|
570
|
+
|
|
571
|
+
`AbapConnection` is an alias for `IAbapConnection` — the contract every
|
|
572
|
+
connection satisfies, including RFC:
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
interface AbapConnection {
|
|
576
|
+
connect(): Promise<void>; // REQUIRED before any request
|
|
577
|
+
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
578
|
+
getBaseUrl(): Promise<string>;
|
|
579
|
+
setSessionType(type: "stateless" | "stateful"): void;
|
|
580
|
+
getSessionId(): string | null; // client-side conversation id
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Anything beyond that is **not** on the contract. `reset()`, `getSessionMode()`
|
|
585
|
+
and the session lifecycle (`disconnect()`, `isConnected()`,
|
|
586
|
+
`getSessionIdentity()`, `beginWindow()`, `endWindow()`) live on the HTTP
|
|
587
|
+
connection classes; `RfcAbapConnection` has some of them and not others, so
|
|
588
|
+
reach for them through a concrete type rather than through what
|
|
589
|
+
`createAbapConnection()` returns.
|
|
590
|
+
|
|
591
|
+
### `BaseAbapConnection` (Basic Auth)
|
|
592
|
+
|
|
593
|
+
For on-premise SAP systems:
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
class BaseAbapConnection extends AbstractAbapConnection {
|
|
597
|
+
constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES` (New in 0.1.13+)
|
|
602
|
+
|
|
603
|
+
Exported constants for consistent CSRF token handling across different connection implementations:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
|
|
607
|
+
|
|
608
|
+
// CSRF_CONFIG structure:
|
|
609
|
+
const config = {
|
|
610
|
+
RETRY_COUNT: 3, // Number of retry attempts
|
|
611
|
+
RETRY_DELAY: 1000, // Delay between retries (ms)
|
|
612
|
+
ENDPOINT: '/sap/bc/adt/core/discovery', // CSRF token endpoint
|
|
613
|
+
REQUIRED_HEADERS: {
|
|
614
|
+
'x-csrf-token': 'fetch',
|
|
615
|
+
'Accept': 'application/atomsvc+xml'
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// CSRF_ERROR_MESSAGES structure:
|
|
620
|
+
const messages = {
|
|
621
|
+
FETCH_FAILED: (attempts: number, cause: string) => string,
|
|
622
|
+
NOT_IN_HEADERS: 'No CSRF token in response headers',
|
|
623
|
+
REQUIRED_FOR_MUTATION: 'CSRF token is required for POST/PUT requests but could not be fetched'
|
|
624
|
+
};
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Use case:** When implementing custom connection classes (e.g., Cloud SDK-based), use these constants to ensure consistent CSRF token handling:
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
|
|
631
|
+
import { executeHttpRequest } from '@sap-cloud-sdk/http-client';
|
|
632
|
+
|
|
633
|
+
export class CloudSdkAbapConnection {
|
|
634
|
+
async fetchCsrfToken(baseUrl: string): Promise<string> {
|
|
635
|
+
const csrfUrl = `${baseUrl}${CSRF_CONFIG.ENDPOINT}`;
|
|
636
|
+
|
|
637
|
+
for (let attempt = 0; attempt <= CSRF_CONFIG.RETRY_COUNT; attempt++) {
|
|
638
|
+
try {
|
|
639
|
+
const response = await executeHttpRequest(
|
|
640
|
+
{ destinationName: this.destination },
|
|
641
|
+
{
|
|
642
|
+
method: 'GET',
|
|
643
|
+
url: csrfUrl,
|
|
644
|
+
headers: CSRF_CONFIG.REQUIRED_HEADERS
|
|
645
|
+
}
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
const token = response.headers['x-csrf-token'];
|
|
649
|
+
if (!token) {
|
|
650
|
+
if (attempt < CSRF_CONFIG.RETRY_COUNT) {
|
|
651
|
+
await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
throw new Error(CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return token;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (attempt >= CSRF_CONFIG.RETRY_COUNT) {
|
|
660
|
+
throw new Error(
|
|
661
|
+
CSRF_ERROR_MESSAGES.FETCH_FAILED(
|
|
662
|
+
CSRF_CONFIG.RETRY_COUNT + 1,
|
|
663
|
+
error instanceof Error ? error.message : String(error)
|
|
664
|
+
)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
throw new Error(CSRF_ERROR_MESSAGES.FETCH_FAILED(CSRF_CONFIG.RETRY_COUNT + 1, 'Unknown error'));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
### `JwtAbapConnection` (JWT/OAuth2)
|
|
678
|
+
|
|
679
|
+
For SAP BTP cloud systems. Token refresh is handled by `@mcp-abap-adt/auth-broker`:
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
class JwtAbapConnection extends AbstractAbapConnection {
|
|
683
|
+
constructor(config: SapConfig, logger?: ILogger | null);
|
|
684
|
+
// Note: refreshToken() and canRefreshToken() methods removed in 0.2.0
|
|
685
|
+
// Use @mcp-abap-adt/auth-broker for token refresh functionality
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### `createAbapConnection()` Factory
|
|
690
|
+
|
|
691
|
+
Recommended way to create connections:
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
function createAbapConnection(
|
|
695
|
+
config: SapConfig,
|
|
696
|
+
logger?: ILogger | null,
|
|
697
|
+
sessionId?: string
|
|
698
|
+
): AbapConnection;
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Auto-detects auth type and returns appropriate connection instance.
|
|
702
|
+
|
|
703
|
+
### Configuration Types
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
type SapConfig = {
|
|
707
|
+
url: string; // SAP system URL
|
|
708
|
+
client?: string; // SAP client (optional for cloud)
|
|
709
|
+
authType: 'basic' | 'jwt'; // Authentication type
|
|
710
|
+
|
|
711
|
+
// For basic auth
|
|
712
|
+
username?: string;
|
|
713
|
+
password?: string;
|
|
714
|
+
|
|
715
|
+
// For JWT auth
|
|
716
|
+
jwtToken?: string;
|
|
717
|
+
|
|
718
|
+
// Note: Token refresh credentials (refreshToken, uaaUrl, etc.) are not used by connection package
|
|
719
|
+
// Token refresh is handled by @mcp-abap-adt/auth-broker package
|
|
720
|
+
};
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## Examples Directory
|
|
724
|
+
|
|
725
|
+
See [examples/](../examples/) for complete working examples:
|
|
726
|
+
|
|
727
|
+
- `basic-connection.js` - Simple connection example
|
|
728
|
+
- `basic-connection.js` - Basic authentication example
|
|
729
|
+
- See [examples/README.md](../examples/README.md) for full list
|
|
730
|
+
|
|
731
|
+
## Best Practices
|
|
732
|
+
|
|
733
|
+
1. **Use Factory Function**: Prefer `createAbapConnection()` over direct instantiation - it auto-detects auth type
|
|
734
|
+
2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for multi-request operations (locks, transactions)
|
|
735
|
+
3. **Token Refresh**: For cloud systems, use `@mcp-abap-adt/auth-broker` for token refresh functionality
|
|
736
|
+
4. **Session State Persistence**: Use `@mcp-abap-adt/auth-broker` for session state persistence
|
|
737
|
+
5. **Handle Errors Gracefully**: Wrap requests in try-catch blocks and check `error.response` for HTTP errors
|
|
738
|
+
6. **Use Proper Logging**: Implement custom logger for production systems with appropriate log levels (logger is optional)
|
|
739
|
+
7. **Session ID Management**: Session IDs are auto-generated (UUID) or can be provided when creating connection
|
|
740
|
+
8. **Switch Session Types**: Use `setSessionType()` to dynamically change between stateful/stateless modes
|
|
741
|
+
|
|
742
|
+
See [CHANGELOG.md](../CHANGELOG.md) for the version history.
|
|
743
|
+
|
|
744
|
+
## Next Steps
|
|
745
|
+
|
|
746
|
+
- Token refresh functionality is now in `@mcp-abap-adt/auth-broker` package
|
|
747
|
+
- Session state persistence is now in `@mcp-abap-adt/auth-broker` package
|
|
748
|
+
- See [JWT_AUTH_TOOLS.md](./JWT_AUTH_TOOLS.md) for CLI authentication tool
|
|
749
|
+
- See [INSTALLATION.md](./INSTALLATION.md) for installation instructions
|