@mcp-abap-adt/connection 1.10.2 → 3.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 +827 -0
  2. package/README.md +44 -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 +180 -12
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +398 -19
  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 +50 -70
  30. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  31. package/dist/session/SessionLifecycle.js +59 -158
  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 +114 -0
  36. package/docs/SCOPE.md +44 -0
  37. package/docs/STATEFUL_SESSION_GUIDE.md +122 -0
  38. package/docs/USAGE.md +745 -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,745 @@
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 a **capability atom** in `@mcp-abap-adt/interfaces` rather than as methods on
223
+ > `IAbapConnection`: `ISessionLifecycleAware` — `disconnect()`, `isConnected()`,
224
+ > `getSessionIdentity()`.
225
+ >
226
+ > The split is the point. `IAbapConnection` is the minimum every transport can
227
+ > honour, and `RfcAbapConnection` has none of these — on RFC the session *is* the
228
+ > open client, so it needs none. Requiring them of every connection would force a
229
+ > transport with no HTTP session to implement a lie.
230
+ >
231
+ > So ask for the atom you need, not for a concrete class:
232
+ >
233
+ > ```typescript
234
+ > function tearDownAfter(conn: IAbapConnection & ISessionLifecycleAware) { /* ... */ }
235
+ > ```
236
+ >
237
+ > **How you satisfy that parameter decides whether you get a compile-time
238
+ > guarantee, and there is only one way that does.**
239
+ >
240
+ > Construct the connection through a concrete HTTP class and the compiler knows
241
+ > it implements the atom, so passing an RFC connection is an error at the call
242
+ > site:
243
+ >
244
+ > ```typescript
245
+ > const conn = new BaseAbapConnection(config, logger);
246
+ > tearDownAfter(conn); // ✅ checked
247
+ > tearDownAfter(new RfcAbapConnection(cfg)); // ✅ compile error, as it should be
248
+ > ```
249
+ >
250
+ > `createAbapConnection()` cannot give you that. It returns `IAbapConnection` for
251
+ > **every** config, RFC included, so the type carries no evidence either way and
252
+ > the compiler rejects its result whatever the transport. Asserting past that —
253
+ > `conn as IAbapConnection & ISessionLifecycleAware` — silences the error for the
254
+ > HTTP case *and* for RFC, which then fails at runtime. An assertion is not a
255
+ > check; it is a promise you make to the compiler on your own authority.
256
+ >
257
+ > When you only have an `IAbapConnection` — from the factory, or from a caller —
258
+ > narrow it at runtime with a predicate:
259
+ >
260
+ > ```typescript
261
+ > function ownsItsSession(
262
+ > conn: IAbapConnection,
263
+ > ): conn is IAbapConnection & ISessionLifecycleAware {
264
+ > const candidate = conn as Partial<ISessionLifecycleAware>;
265
+ > // EVERY method of the atom. A predicate narrows to the whole interface, so
266
+ > // checking one and promising three puts the failure back where this check
267
+ > // was meant to remove it — inside the branch that looked safe.
268
+ > return (
269
+ > typeof candidate.disconnect === 'function' &&
270
+ > typeof candidate.isConnected === 'function' &&
271
+ > typeof candidate.getSessionIdentity === 'function'
272
+ > );
273
+ > }
274
+ >
275
+ > if (ownsItsSession(conn)) {
276
+ > tearDownAfter(conn); // narrowed by evidence, not by assertion
277
+ > } else {
278
+ > // No HTTP session 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
+ `AdtSessionErrorCode` comes from the same place, as does the capability interface
327
+ this connection implements — `ISessionLifecycleAware`. Depend on that rather than
328
+ on a concrete connection class where you can: an `RfcAbapConnection` is a valid
329
+ `IAbapConnection` that does not implement it, so the atom you require is also the
330
+ documentation of what your code actually needs.
331
+
332
+ ### disconnect() waits for nothing
333
+
334
+ ```typescript
335
+ await connection.disconnect(); // Promise<void>
336
+ ```
337
+
338
+ It never throws, and it always settles. **It waits for nothing** — not for
339
+ requests in flight, not for anything you hold. Deciding when to disconnect is
340
+ yours, and so is preparing for it.
341
+
342
+ Requests already in flight run to completion untouched; nothing is aborted. What
343
+ the connection guarantees is that their results can no longer reach it: a
344
+ response arriving after the teardown is fenced, so it cannot write its cookies
345
+ over a session established since, and cannot be mistaken for a replacement.
346
+
347
+ An earlier version waited for in-flight requests before clearing anything. That
348
+ turned out to be unbounded — a request whose caller chose no timeout could hold a
349
+ teardown open forever, and since lifecycle transitions are serialized, every
350
+ later `connect()` queued behind it.
351
+
352
+ **Over HTTP it does not release locks.** The ABAP session lives on until its
353
+ timeout, along with whatever it held. Unlock first; disconnecting is not a way to
354
+ clean up after yourself.
355
+
356
+ ### Uninterruptible spans
357
+
358
+ The connection does **not** track locks — it does not know one exists, what
359
+ object it covers, or what would release it. Pairing every LOCK with its UNLOCK is
360
+ `@mcp-abap-adt/adt-clients`' job, and it does it per object in its lock registry.
361
+
362
+ What this layer owns is the timeout, because a timeout is the server taking too
363
+ long and nothing about it depends on the caller. Aborting a request mid-flight
364
+ leaves an operation whose outcome you cannot determine — a `LOCK` that may have
365
+ succeeded server-side, with a handle you never received:
366
+
367
+ ```typescript
368
+ connection.beginCriticalSection();
369
+ try {
370
+ const handle = await lock(connection, 'ZCL_MY_CLASS');
371
+ await update(connection, 'ZCL_MY_CLASS', handle);
372
+ await unlock(connection, 'ZCL_MY_CLASS', handle);
373
+ } finally {
374
+ connection.endCriticalSection();
375
+ }
376
+ ```
377
+
378
+ While a section is active the effective timeout is raised to
379
+ `SAP_TIMEOUT_CRITICAL` (600s by default) — `Math.max(yours, the ceiling)`, so it
380
+ never shortens a timeout you chose. The pair is reference-counted, so nesting is
381
+ safe.
382
+
383
+ The raise is **connection-wide**: while any section is active, every request on
384
+ that connection gets the ceiling, including one that has nothing to do with the
385
+ locked object. Those requests share one ABAP session, and an abort leaves that
386
+ session in the same uncertain state during a span someone declared sensitive
387
+ precisely to avoid it.
388
+
389
+ ### When the session is lost
390
+
391
+ Two different things can cost you the session, and they do not behave alike.
392
+
393
+ **The session was replaced** — a renewed credential, or a session cookie that
394
+ changed underneath you. This is fatal **only while a lock window is open**:
395
+
396
+ ```typescript
397
+ // window open → ADT_SESSION_REPLACED, the connection stops being usable
398
+ // no window → transparent; work continues on the new session
399
+ ```
400
+
401
+ With nothing held there is nothing to lose, so the connector carries on. The
402
+ error exists to tell you that a lock handle you are carrying is now dead, which
403
+ is the one thing you cannot work out for yourself.
404
+
405
+ **The server says the session is gone** — an answer meaning the session it was
406
+ given no longer exists. This is **always** fatal, window or not: the connection
407
+ raises `ADT_SESSION_REPLACED` and stops being usable. Unlike a replacement,
408
+ nothing here suggests a working session to continue on — the one we had is
409
+ confirmed dead, and re-establishing silently is what let the old connector
410
+ carry on over a session the caller never opened.
411
+
412
+ In both cases the connector does **not** retry the request internally. Retrying
413
+ blindly is what produced further orphaned locks in the field. That decision is
414
+ yours.
415
+
416
+ ## Advanced Features
417
+
418
+ ### Session ID Management
419
+
420
+ Session IDs are auto-generated (UUID) when connection is created:
421
+
422
+ ```typescript
423
+ const connection = createAbapConnection(config, logger);
424
+ console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
425
+
426
+ // Or provide your own when creating connection
427
+ const connection = createAbapConnection(config, logger, 'custom-session-123');
428
+ console.log(connection.getSessionId()); // 'custom-session-123'
429
+ ```
430
+
431
+ ### Switching Session Types
432
+
433
+ Dynamically switch between stateful and stateless modes:
434
+
435
+ ```typescript
436
+ // Start in stateless mode (default)
437
+ const connection = createAbapConnection(config, logger);
438
+ await connection.connect();
439
+
440
+ // Enable stateful for a series of operations
441
+ connection.setSessionType('stateful');
442
+
443
+ // Do stateful operations...
444
+ await connection.makeAdtRequest({ method: 'POST', url: '...' });
445
+
446
+ // Switch back to stateless
447
+ connection.setSessionType('stateless');
448
+ ```
449
+
450
+ ### Connection Reset
451
+
452
+ Reset connection state (clears cookies, CSRF token):
453
+
454
+ ```typescript
455
+ connection.reset();
456
+ ```
457
+
458
+ ## Custom Logging
459
+
460
+ ### Using Custom Logger
461
+
462
+ ```typescript
463
+ import { ILogger } from '@mcp-abap-adt/connection';
464
+
465
+ class CustomLogger implements ILogger {
466
+ info(message: string, meta?: any) {
467
+ console.log(`[INFO] ${message}`, meta);
468
+ }
469
+
470
+ warn(message: string, meta?: any) {
471
+ console.warn(`[WARN] ${message}`, meta);
472
+ }
473
+
474
+ error(message: string, meta?: any) {
475
+ console.error(`[ERROR] ${message}`, meta);
476
+ }
477
+
478
+ debug(message: string, meta?: any) {
479
+ if (process.env.DEBUG) {
480
+ console.debug(`[DEBUG] ${message}`, meta);
481
+ }
482
+ }
483
+
484
+ // Optional: CSRF-specific logging
485
+ csrfToken?(action: 'fetch' | 'retry' | 'success' | 'error', message: string, meta?: any) {
486
+ console.log(`[CSRF:${action.toUpperCase()}] ${message}`, meta);
487
+ }
488
+
489
+ // Optional: TLS config logging
490
+ tlsConfig?(rejectUnauthorized: boolean) {
491
+ console.log(`[TLS] rejectUnauthorized=${rejectUnauthorized}`);
492
+ }
493
+ }
494
+
495
+ const logger = new CustomLogger();
496
+ const connection = createAbapConnection(config, logger);
497
+ ```
498
+
499
+ ## Error Handling
500
+
501
+ ### Basic Error Handling
502
+
503
+ ```typescript
504
+ try {
505
+ await connection.makeAdtRequest({
506
+ method: 'GET',
507
+ url: '/sap/bc/adt/invalid/endpoint',
508
+ });
509
+ } catch (error) {
510
+ if (error.response) {
511
+ console.error(`HTTP ${error.response.status}:`, error.response.data);
512
+ } else {
513
+ console.error('Network error:', error.message);
514
+ }
515
+ }
516
+ ```
517
+
518
+ ### Network Error Detection
519
+
520
+ The connection automatically detects network-level errors and prevents unnecessary retry attempts. Network errors include:
521
+
522
+ - `ECONNREFUSED` - Connection refused (server not reachable)
523
+ - `ETIMEDOUT` - Connection timeout
524
+ - `ENOTFOUND` - DNS resolution failed (hostname not found)
525
+ - `ECONNRESET` - Connection reset by peer
526
+ - `ENETUNREACH` - Network unreachable
527
+ - `EHOSTUNREACH` - Host unreachable
528
+
529
+ When these errors occur, the connection:
530
+ 1. **Does NOT attempt CSRF token retry** - network issues can't be fixed by retrying authentication
531
+ 2. **Immediately throws the error** with clear network-related message
532
+ 3. **Logs the error** with full context for troubleshooting
533
+
534
+ ```typescript
535
+ try {
536
+ await connection.makeAdtRequest({
537
+ method: 'GET',
538
+ url: '/sap/bc/adt/repository/nodestructure',
539
+ });
540
+ } catch (error) {
541
+ // Check for specific network error codes
542
+ if (error.code === 'ECONNREFUSED') {
543
+ console.error('Cannot connect to SAP server - check VPN connection');
544
+ } else if (error.code === 'ETIMEDOUT') {
545
+ console.error('Connection timeout - server not responding');
546
+ } else if (error.code === 'ENOTFOUND') {
547
+ console.error('Cannot resolve hostname - check SAP URL');
548
+ } else if (error.response) {
549
+ console.error(`HTTP ${error.response.status}:`, error.response.data);
550
+ } else {
551
+ console.error('Request failed:', error.message);
552
+ }
553
+ }
554
+ ```
555
+
556
+ **Best Practices:**
557
+ - Always handle network errors separately from HTTP errors
558
+ - Network errors indicate infrastructure issues (VPN, DNS, firewall)
559
+ - HTTP errors (401, 403, 404, etc.) indicate application-level issues
560
+ - Use error codes to provide specific user guidance
561
+
562
+
563
+ ## API Reference
564
+
565
+ ### `AbapConnection` Interface
566
+
567
+ `AbapConnection` is an alias for `IAbapConnection` — the contract every
568
+ connection satisfies, including RFC:
569
+
570
+ ```typescript
571
+ interface AbapConnection {
572
+ connect(): Promise<void>; // REQUIRED before any request
573
+ makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
574
+ getBaseUrl(): Promise<string>;
575
+ setSessionType(type: "stateless" | "stateful"): void;
576
+ getSessionId(): string | null; // client-side conversation id
577
+ }
578
+ ```
579
+
580
+ Anything beyond that is **not** on the contract. `reset()`, `getSessionMode()`
581
+ and the session lifecycle (`disconnect()`, `isConnected()`,
582
+ `getSessionIdentity()`) live on the HTTP
583
+ connection classes; `RfcAbapConnection` has some of them and not others, so
584
+ reach for them through a concrete type rather than through what
585
+ `createAbapConnection()` returns.
586
+
587
+ ### `BaseAbapConnection` (Basic Auth)
588
+
589
+ For on-premise SAP systems:
590
+
591
+ ```typescript
592
+ class BaseAbapConnection extends AbstractAbapConnection {
593
+ constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string);
594
+ }
595
+ ```
596
+
597
+ ### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES` (New in 0.1.13+)
598
+
599
+ Exported constants for consistent CSRF token handling across different connection implementations:
600
+
601
+ ```typescript
602
+ import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
603
+
604
+ // CSRF_CONFIG structure:
605
+ const config = {
606
+ RETRY_COUNT: 3, // Number of retry attempts
607
+ RETRY_DELAY: 1000, // Delay between retries (ms)
608
+ ENDPOINT: '/sap/bc/adt/core/discovery', // CSRF token endpoint
609
+ REQUIRED_HEADERS: {
610
+ 'x-csrf-token': 'fetch',
611
+ 'Accept': 'application/atomsvc+xml'
612
+ }
613
+ };
614
+
615
+ // CSRF_ERROR_MESSAGES structure:
616
+ const messages = {
617
+ FETCH_FAILED: (attempts: number, cause: string) => string,
618
+ NOT_IN_HEADERS: 'No CSRF token in response headers',
619
+ REQUIRED_FOR_MUTATION: 'CSRF token is required for POST/PUT requests but could not be fetched'
620
+ };
621
+ ```
622
+
623
+ **Use case:** When implementing custom connection classes (e.g., Cloud SDK-based), use these constants to ensure consistent CSRF token handling:
624
+
625
+ ```typescript
626
+ import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
627
+ import { executeHttpRequest } from '@sap-cloud-sdk/http-client';
628
+
629
+ export class CloudSdkAbapConnection {
630
+ async fetchCsrfToken(baseUrl: string): Promise<string> {
631
+ const csrfUrl = `${baseUrl}${CSRF_CONFIG.ENDPOINT}`;
632
+
633
+ for (let attempt = 0; attempt <= CSRF_CONFIG.RETRY_COUNT; attempt++) {
634
+ try {
635
+ const response = await executeHttpRequest(
636
+ { destinationName: this.destination },
637
+ {
638
+ method: 'GET',
639
+ url: csrfUrl,
640
+ headers: CSRF_CONFIG.REQUIRED_HEADERS
641
+ }
642
+ );
643
+
644
+ const token = response.headers['x-csrf-token'];
645
+ if (!token) {
646
+ if (attempt < CSRF_CONFIG.RETRY_COUNT) {
647
+ await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
648
+ continue;
649
+ }
650
+ throw new Error(CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
651
+ }
652
+
653
+ return token;
654
+ } catch (error) {
655
+ if (attempt >= CSRF_CONFIG.RETRY_COUNT) {
656
+ throw new Error(
657
+ CSRF_ERROR_MESSAGES.FETCH_FAILED(
658
+ CSRF_CONFIG.RETRY_COUNT + 1,
659
+ error instanceof Error ? error.message : String(error)
660
+ )
661
+ );
662
+ }
663
+ await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
664
+ }
665
+ }
666
+
667
+ throw new Error(CSRF_ERROR_MESSAGES.FETCH_FAILED(CSRF_CONFIG.RETRY_COUNT + 1, 'Unknown error'));
668
+ }
669
+ }
670
+ ```
671
+
672
+
673
+ ### `JwtAbapConnection` (JWT/OAuth2)
674
+
675
+ For SAP BTP cloud systems. Token refresh is handled by `@mcp-abap-adt/auth-broker`:
676
+
677
+ ```typescript
678
+ class JwtAbapConnection extends AbstractAbapConnection {
679
+ constructor(config: SapConfig, logger?: ILogger | null);
680
+ // Note: refreshToken() and canRefreshToken() methods removed in 0.2.0
681
+ // Use @mcp-abap-adt/auth-broker for token refresh functionality
682
+ }
683
+ ```
684
+
685
+ ### `createAbapConnection()` Factory
686
+
687
+ Recommended way to create connections:
688
+
689
+ ```typescript
690
+ function createAbapConnection(
691
+ config: SapConfig,
692
+ logger?: ILogger | null,
693
+ sessionId?: string
694
+ ): AbapConnection;
695
+ ```
696
+
697
+ Auto-detects auth type and returns appropriate connection instance.
698
+
699
+ ### Configuration Types
700
+
701
+ ```typescript
702
+ type SapConfig = {
703
+ url: string; // SAP system URL
704
+ client?: string; // SAP client (optional for cloud)
705
+ authType: 'basic' | 'jwt'; // Authentication type
706
+
707
+ // For basic auth
708
+ username?: string;
709
+ password?: string;
710
+
711
+ // For JWT auth
712
+ jwtToken?: string;
713
+
714
+ // Note: Token refresh credentials (refreshToken, uaaUrl, etc.) are not used by connection package
715
+ // Token refresh is handled by @mcp-abap-adt/auth-broker package
716
+ };
717
+ ```
718
+
719
+ ## Examples Directory
720
+
721
+ See [examples/](../examples/) for complete working examples:
722
+
723
+ - `basic-connection.js` - Simple connection example
724
+ - `basic-connection.js` - Basic authentication example
725
+ - See [examples/README.md](../examples/README.md) for full list
726
+
727
+ ## Best Practices
728
+
729
+ 1. **Use Factory Function**: Prefer `createAbapConnection()` over direct instantiation - it auto-detects auth type
730
+ 2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for multi-request operations (locks, transactions)
731
+ 3. **Token Refresh**: For cloud systems, use `@mcp-abap-adt/auth-broker` for token refresh functionality
732
+ 4. **Session State Persistence**: Use `@mcp-abap-adt/auth-broker` for session state persistence
733
+ 5. **Handle Errors Gracefully**: Wrap requests in try-catch blocks and check `error.response` for HTTP errors
734
+ 6. **Use Proper Logging**: Implement custom logger for production systems with appropriate log levels (logger is optional)
735
+ 7. **Session ID Management**: Session IDs are auto-generated (UUID) or can be provided when creating connection
736
+ 8. **Switch Session Types**: Use `setSessionType()` to dynamically change between stateful/stateless modes
737
+
738
+ See [CHANGELOG.md](../CHANGELOG.md) for the version history.
739
+
740
+ ## Next Steps
741
+
742
+ - Token refresh functionality is now in `@mcp-abap-adt/auth-broker` package
743
+ - Session state persistence is now in `@mcp-abap-adt/auth-broker` package
744
+ - See [JWT_AUTH_TOOLS.md](./JWT_AUTH_TOOLS.md) for CLI authentication tool
745
+ - See [INSTALLATION.md](./INSTALLATION.md) for installation instructions