@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +758 -0
  2. package/README.md +42 -11
  3. package/dist/__tests__/helpers/session.d.ts +15 -0
  4. package/dist/__tests__/helpers/session.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/session.js +19 -0
  6. package/dist/auth/ntlm.d.ts +15 -0
  7. package/dist/auth/ntlm.d.ts.map +1 -1
  8. package/dist/auth/ntlm.js +38 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +163 -11
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +351 -14
  12. package/dist/connection/BaseAbapConnection.d.ts +5 -1
  13. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  14. package/dist/connection/BaseAbapConnection.js +11 -1
  15. package/dist/connection/CertificateAbapConnection.d.ts +5 -1
  16. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  17. package/dist/connection/CertificateAbapConnection.js +11 -1
  18. package/dist/connection/JwtAbapConnection.d.ts +3 -2
  19. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/JwtAbapConnection.js +19 -8
  21. package/dist/connection/KerberosAbapConnection.d.ts +5 -1
  22. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/KerberosAbapConnection.js +54 -3
  24. package/dist/connection/SamlAbapConnection.d.ts +5 -1
  25. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  26. package/dist/connection/SamlAbapConnection.js +11 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -0
  29. package/dist/session/SessionLifecycle.d.ts +131 -0
  30. package/dist/session/SessionLifecycle.d.ts.map +1 -0
  31. package/dist/session/SessionLifecycle.js +301 -0
  32. package/docs/INDEX.md +106 -0
  33. package/docs/INSTALLATION.md +304 -0
  34. package/docs/JWT_AUTH_TOOLS.md +142 -0
  35. package/docs/MIGRATION-2.0.md +125 -0
  36. package/docs/SCOPE.md +44 -0
  37. package/docs/STATEFUL_SESSION_GUIDE.md +121 -0
  38. package/docs/USAGE.md +749 -0
  39. package/examples/README.md +112 -0
  40. package/examples/basic-connection.js +55 -0
  41. package/examples/jwt-with-token-refresh.js +87 -0
  42. package/examples/saml-connection.js +52 -0
  43. package/examples/websocket-transport.js +87 -0
  44. 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