@mcp-abap-adt/connection 4.0.0 → 6.0.0

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