@mcp-abap-adt/connection 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +134 -1
  2. package/README.md +204 -48
  3. package/dist/auth/providers.d.ts +38 -9
  4. package/dist/auth/providers.d.ts.map +1 -1
  5. package/dist/auth/providers.js +46 -3
  6. package/dist/connection/AbstractAbapConnection.d.ts +83 -136
  7. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  8. package/dist/connection/AbstractAbapConnection.js +241 -763
  9. package/dist/connection/AdtCloudConnector.d.ts +12 -7
  10. package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
  11. package/dist/connection/AdtCloudConnector.js +2 -6
  12. package/dist/connection/AdtOnPremConnector.d.ts +20 -7
  13. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
  14. package/dist/connection/AdtOnPremConnector.js +2 -6
  15. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  16. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  17. package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
  18. package/dist/connection/CredentialAbapConnection.d.ts +8 -41
  19. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/CredentialAbapConnection.js +44 -111
  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 +7 -8
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +21 -18
  42. package/dist/utils/timeouts.d.ts +6 -19
  43. package/dist/utils/timeouts.d.ts.map +1 -1
  44. package/dist/utils/timeouts.js +6 -22
  45. package/docs/INDEX.md +5 -2
  46. package/docs/INSTALLATION.md +28 -10
  47. package/docs/JWT_AUTH_TOOLS.md +20 -4
  48. package/docs/MIGRATION-6.0.md +359 -0
  49. package/docs/SCOPE.md +1 -1
  50. package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
  51. package/docs/USAGE.md +260 -119
  52. package/examples/basic-connection.js +15 -3
  53. package/examples/jwt-with-token-refresh.js +15 -7
  54. package/examples/saml-connection.js +15 -2
  55. package/package.json +12 -10
  56. package/dist/__tests__/helpers/session.d.ts +0 -15
  57. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  58. package/dist/__tests__/helpers/session.js +0 -19
  59. package/dist/auth/IAuthProvider.d.ts +0 -84
  60. package/dist/auth/IAuthProvider.d.ts.map +0 -1
  61. package/dist/auth/IAuthProvider.js +0 -21
  62. package/dist/connection/BaseAbapConnection.d.ts +0 -29
  63. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  64. package/dist/connection/BaseAbapConnection.js +0 -81
  65. package/dist/connection/CertificateAbapConnection.d.ts +0 -35
  66. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  67. package/dist/connection/CertificateAbapConnection.js +0 -91
  68. package/dist/connection/JwtAbapConnection.d.ts +0 -131
  69. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  70. package/dist/connection/JwtAbapConnection.js +0 -376
  71. package/dist/connection/KerberosAbapConnection.d.ts +0 -32
  72. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  73. package/dist/connection/KerberosAbapConnection.js +0 -128
  74. package/dist/connection/RfcAbapConnection.d.ts +0 -44
  75. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  76. package/dist/connection/RfcAbapConnection.js +0 -324
  77. package/dist/connection/SamlAbapConnection.d.ts +0 -31
  78. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  79. package/dist/connection/SamlAbapConnection.js +0 -81
  80. package/dist/connection/connectionFactory.d.ts +0 -25
  81. package/dist/connection/connectionFactory.d.ts.map +0 -1
  82. package/dist/connection/connectionFactory.js +0 -84
  83. package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
  84. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
  85. package/dist/session/IcfSessionStrategy.d.ts +0 -27
  86. package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
  87. package/dist/session/IcfSessionStrategy.js +0 -62
  88. package/dist/session/SessionStrategy.d.ts +0 -86
  89. package/dist/session/SessionStrategy.d.ts.map +0 -1
  90. package/dist/session/SessionStrategy.js +0 -33
  91. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +0 -108
package/CHANGELOG.md CHANGED
@@ -7,6 +7,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.0.0] - 2026-08-24
11
+
12
+ The wire owns what is the wire's, and the factory and per-credential classes are
13
+ gone. See [Migration to 6.0](./docs/MIGRATION-6.0.md).
14
+
15
+ ### Added
16
+
17
+ - **The transport axis is complete and public.** `IAdtTransport` now covers
18
+ everything true of a wire — carrying a request, addressing it, establishing
19
+ itself, and whatever session state it keeps — and both ends are objects:
20
+ `HttpTransport` and `RfcTransport`. `IAdtEstablishContext`, `IRfcConversation`
21
+ and `RfcConnectionParams` are exported, so a caller handed a seam can name it.
22
+
23
+ - **`rfcConversationFrom(config)`** — the front door to the RFC wire. Derives
24
+ `ashost` from the url and `sysnr` from the HTTP port (`80XX` → `XX`, with
25
+ `SAP_SYSNR` overriding), and loads the SAP NW RFC SDK only when a conversation
26
+ opens, so a machine without it fails at `connect()` rather than at
27
+ construction.
28
+
29
+ - **`RfcTransport` supplies a default `Accept`.** axios adds one over HTTP and
30
+ nobody had noticed; ADT refuses a request without it with
31
+ `400 ExceptionResourceBadRequest: Accept header missing`.
32
+
33
+ ### Changed
34
+
35
+ - **The on-prem connector works over RFC.** It did not, at all. Measured against
36
+ a real system, three blockers stood one behind the other, all of them HTTP
37
+ assumptions in the class every connector shares: the CSRF fetch handed
38
+ `SADT_REST_RFC_ENDPOINT` an absolute URL and dumped it with
39
+ `STRING_OFFSET_TOO_LARGE`; that endpoint returns no `x-csrf-token` however it
40
+ is asked, so the exchange could not succeed; and the session fingerprint was a
41
+ scan for a `SAP_SESSIONID` cookie, so a wire that issues none read as a
42
+ connection the server had opened no session for.
43
+
44
+ The base class did not merely check for cookies — it DEFINED a session as one.
45
+
46
+ - **`AbstractAbapConnection` keeps the lifecycle and nothing else** (1989 → ~1620
47
+ lines): the transition queue, teardown epochs, session generations, critical
48
+ sections, stale-request fencing, 401 classification, the identity policy, and
49
+ the promise that `disconnect()` settles. Cookies, the cookie jar, the CSRF
50
+ exchange, affinity headers, axios and addressing all moved to the wire that
51
+ has them. There is no `if (transport is rfc)` anywhere.
52
+
53
+ - **A credential refused surfaces** rather than being renewed behind the caller —
54
+ on `connect()` and on the request path alike. Renewal is the provider's, and it
55
+ happens on an expiry the provider can see, on every call that asks for a header.
56
+ See *Removed*, below: nothing here answers a 401 any more.
57
+
58
+ ### Changed — BREAKING
59
+
60
+ - **The base classes ask nothing.** `AbstractAbapConnection` and
61
+ `CredentialAbapConnection` contain no config-driven conditional, no optional-member
62
+ call, and no dispatch on a type or a shape. What a collaborator can do is stated by
63
+ its type, not discovered at runtime:
64
+
65
+ - `IAdtTransport.open()` / `close()` are required — a wire with nothing to open
66
+ writes an empty method, which is true of it;
67
+ - `IAuthProvider.prepare()`, `cookies()` and `transportMaterial()` are required for
68
+ the same reason (needs `@mcp-abap-adt/interfaces` 20.0.0);
69
+ - `establish()` is the transport's, and the connection delegates to it without
70
+ asking anything first. The credential contributes a header, cookies and TLS
71
+ material; earning a CSRF token is the wire's work, because the wire is what
72
+ holds the session the token is bound to (needs `@mcp-abap-adt/interfaces`
73
+ 21.0.0, where the two credential atoms leave the contract — nothing
74
+ implemented them);
75
+ - `skipSessionType` is gone: it described BASIS 7.40, and a deployment is a wire, so
76
+ it is `LegacyOnPremHttpTransport`;
77
+ - whether a session exists is `IAdtTransport.sessionEstablished()` — a verdict each
78
+ wire gives about itself, instead of the connection reading a fingerprint and a flag
79
+ and deciding for all of them at once.
80
+
81
+ A credential you wrote gains three usually-empty members; see
82
+ [the migration guide](./docs/MIGRATION-6.0.md#writing-your-own-credential).
83
+
84
+ ### Removed
85
+
86
+ - **The connection no longer answers a `401` for you.** It called `renew()` on the
87
+ credential, compared the header against the previous one, and rebuilt the session
88
+ if it had changed — a credential lifetime managed from inside the connection.
89
+ Renewal on an expiry the provider can SEE still happens, inside
90
+ `authorizationHeader()`, which is asked per request; the other case — a token the
91
+ provider still believes in and the server refuses — is a judgement made with what
92
+ the caller knows, so the refusal surfaces. A refused credential is not a lost
93
+ session, so the connection stays usable. `TokenAuthProvider` declares
94
+ `IRenewableCredential` (interfaces 19.0.0), which is what a consumer narrows to
95
+ before calling `renew()` itself.
96
+
97
+ - **`disconnect({ deadlineMs })`** takes no arguments, and `SAP_RELEASE_DEADLINE_MS`
98
+ is gone with it. The parameter bounded a wait for the goodbye to be *answered*,
99
+ and the method does not act on that answer: it tells the server the session is
100
+ finished, and whether and when the session is freed is the server's affair. The
101
+ default was already `0`. Waiting bought a caller nothing while being the one
102
+ thing that could make a teardown unbounded — the goodbye carries no request
103
+ timeout by design, so a server that never answered would have held the teardown
104
+ for the whole deadline. Needs `@mcp-abap-adt/interfaces` 18.0.0, where the
105
+ parameter leaves the contract. Verified against a live BTP trial before the
106
+ contract moved: `disconnect()` returned in 1 ms and the goodbye still went out.
107
+
108
+ - **`SessionStrategy`** and its two implementations. A session mechanism only some
109
+ wires have, described from inside the class every wire shares and driven by the
110
+ connection — a second wire abstraction beside `IAdtTransport`. It is the
111
+ transport's `open()`/`close()` now, which is also what made `connect()` possible
112
+ over RFC at all.
113
+
114
+ - **`createAbapConnection()`** and the connection classes it built:
115
+ `BaseAbapConnection` (`OnPremAbapConnection`), `JwtAbapConnection`
116
+ (`CloudAbapConnection`), `SamlAbapConnection`, `CertificateAbapConnection`,
117
+ `KerberosAbapConnection`, `RfcAbapConnection` — 1597 lines. Take a connector,
118
+ hand it a credential, and hand it a transport — which has no default, because
119
+ which wire you are on is not something to guess.
120
+
121
+ - `adaptTransport()`, which dressed a transport in an axios shape so six call
122
+ sites did not have to be rewritten. They were rewritten.
123
+
124
+ - `connectionType: 'rfc'` as a way to reach the RFC wire. The wire is an
125
+ argument now.
126
+
127
+ **Kerberos has no direct replacement.** It was single-leg only and untested
128
+ against a live KDC (#35); a `KerberosAuthProvider` belongs on the credential
129
+ axis and should be added with a system to test it against.
130
+
131
+ ### Fixed
132
+
133
+ - Credential cookies are merged into the establishing request rather than
134
+ overwritten by the wire's own — a SAML session IS that cookie, and replacing
135
+ it sent the exchange out unauthenticated.
136
+ - The CSRF fallback endpoint is tried only when the primary answers 404. A host
137
+ that is not answering will not answer a different path, and asking doubled the
138
+ wait before the real error surfaced.
139
+ - A CSRF token arriving on a refused response (405, or any refusal carrying the
140
+ header) is kept instead of thrown away by the retry.
141
+
10
142
  ## [5.0.0] - 2026-08-21
11
143
 
12
144
  A connection now says which system it is, closes what it opens, and is handed its
@@ -1120,7 +1252,8 @@ const connection = createAbapConnection(config, logger);
1120
1252
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
1121
1253
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
1122
1254
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
1123
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v5.0.0...HEAD
1255
+ [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.0...HEAD
1256
+ [6.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v5.0.0...v6.0.0
1124
1257
  [5.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v4.0.0...v5.0.0
1125
1258
  [4.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v3.0.0...v4.0.0
1126
1259
  [3.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v2.0.0...v3.0.0
package/README.md CHANGED
@@ -57,13 +57,23 @@ The package uses a clean separation of concerns:
57
57
  - **Auth providers** (`BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`,
58
58
  `CertificateAuthProvider`):
59
59
  - What a connection authenticates with, passed in
60
- - A token provider renews on its own; the connector asks it per request and, on a
61
- `401`, tells it the answer was refused before asking again
62
-
63
- - **`BaseAbapConnection`, `JwtAbapConnection`, `SamlAbapConnection`,
64
- `CertificateAbapConnection`, `KerberosAbapConnection`** (deprecated, still exported):
65
- - The previous shape, where the class stated your credential and the session
66
- mechanism came with it. See [Migration to 5.0](./docs/MIGRATION-5.0.md)
60
+ - A token provider renews on its own, and the connector asks it per request — which
61
+ is how a token that expired between two requests is replaced with nobody
62
+ deciding to replace it
63
+ - A `401` **surfaces**. Whether a refusal meant "the token is stale" or "these
64
+ credentials are refused" is a judgement made with what you know, so the
65
+ connector does not answer it for you. A credential that can be told to get a
66
+ new one says so through `IRenewableCredential`, which you narrow to
67
+
68
+ - **Transports** (`OnPremHttpTransport`, `CloudHttpTransport`, `RfcTransport`):
69
+ - What a request travels over, and everything that is true of that wire.
70
+ `HttpTransport` keeps the cookie jar, the CSRF token and the affinity
71
+ headers; `RfcTransport` translates into `SADT_REST_RFC_ENDPOINT` and keeps a
72
+ conversation that IS the session
73
+ - On-prem is where this is a real choice; ABAP Cloud has one wire and its
74
+ connector takes no such parameter
75
+ - `rfcConversationFrom(config)` builds what `RfcTransport` needs, deriving
76
+ `ashost` and `sysnr` and loading the SDK only when a conversation opens
67
77
 
68
78
  - **`GenericWebSocketTransport`** (concrete, exported):
69
79
  - Transport abstraction for realtime WS message flows
@@ -123,6 +133,7 @@ This package interacts with external packages **ONLY through interfaces**:
123
133
 
124
134
  - 📦 **[Installation Guide](./docs/INSTALLATION.md)** - Setup and installation instructions
125
135
  - 📚 **[Usage Guide](./docs/USAGE.md)** - Detailed usage examples and API documentation
136
+ - 🚚 **[Migration to 6.0.0](./docs/MIGRATION-6.0.md)** - the factory and the per-credential classes are removed; RFC is a transport, not a class
126
137
  - 🚚 **[Migration to 4.0.0](./docs/MIGRATION-4.0.md)** - a 401 refreshes the token, a 403 reaches you with the server's message; the synthesised "JWT token has expired" is gone
127
138
  - 🚚 **[Migration: the explicit session lifecycle](./docs/MIGRATION-2.0.md)** - `connect()` is now required; start here if you are coming from 1.x
128
139
  - 💡 **[Examples](./examples/)** - Working code examples
@@ -149,7 +160,13 @@ For detailed installation instructions, see [Installation Guide](./docs/INSTALLA
149
160
  ### Basic Usage (On-Premise)
150
161
 
151
162
  ```typescript
152
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
163
+ import {
164
+ AdtOnPremConnector,
165
+ BasicAuthProvider,
166
+ OnPremHttpTransport,
167
+ SapConfig,
168
+ getTimeout,
169
+ } from "@mcp-abap-adt/connection";
153
170
 
154
171
  const config: SapConfig = {
155
172
  url: "https://your-sap-system.com",
@@ -167,23 +184,37 @@ const logger = {
167
184
  debug: (msg: string, meta?: any) => console.debug(msg, meta),
168
185
  };
169
186
 
170
- // Create connection
171
- const connection = createAbapConnection(config, logger, undefined, undefined, {
172
- system: "onprem", // which SYSTEM this is — said, never detected
173
- });
187
+ // Which system you are dialling is the class you take; which credential it
188
+ // authenticates with is the object you hand it. Neither is detected.
189
+ const connection = new AdtOnPremConnector(
190
+ config,
191
+ new BasicAuthProvider(config.username!, config.password!),
192
+ new OnPremHttpTransport(() => ({}), logger, {
193
+ client: config.client,
194
+ baseUrl: config.url,
195
+ }),
196
+ logger,
197
+ );
174
198
  await connection.connect(); // required before any request
175
199
 
176
200
  // Make ADT request
177
201
  const response = await connection.makeAdtRequest({
178
202
  method: "GET",
179
203
  url: "/sap/bc/adt/programs/programs/your-program",
204
+ timeout: getTimeout("default"),
180
205
  });
181
206
  ```
182
207
 
183
208
  ### Cloud Usage (JWT/OAuth2)
184
209
 
185
210
  ```typescript
186
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
211
+ import {
212
+ AdtCloudConnector,
213
+ CloudHttpTransport,
214
+ SapConfig,
215
+ TokenAuthProvider,
216
+ getTimeout,
217
+ } from "@mcp-abap-adt/connection";
187
218
 
188
219
  // JWT configuration
189
220
  const config: SapConfig = {
@@ -200,23 +231,77 @@ const logger = {
200
231
  debug: (msg: string, meta?: any) => console.debug(msg, meta),
201
232
  };
202
233
 
203
- // Logger is optional - if not provided, no logging output
204
- const connection = createAbapConnection(config, logger, undefined, undefined, {
205
- system: "cloud", // which SYSTEM this is — said, never detected
206
- });
234
+ // Logger is optional - if not provided, no logging output.
235
+ // A bare string is a token with nothing behind it. Hand `TokenAuthProvider` an
236
+ // `ITokenRefresher` instead and it checks expiry and renews on its own, which
237
+ // is what you want in anything long-lived.
238
+ const connection = new AdtCloudConnector(
239
+ config,
240
+ new TokenAuthProvider(config.jwtToken!),
241
+ new CloudHttpTransport(() => ({}), logger, {
242
+ client: config.client,
243
+ baseUrl: config.url,
244
+ }),
245
+ logger,
246
+ );
207
247
  await connection.connect();
208
248
 
209
- // Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
249
+ // Note: obtaining and refreshing tokens is @mcp-abap-adt/auth-broker's job
210
250
  const response = await connection.makeAdtRequest({
211
251
  method: "GET",
212
252
  url: "/sap/bc/adt/programs/programs/your-program",
253
+ timeout: getTimeout("default"),
213
254
  });
214
255
  ```
215
256
 
257
+ ### On-Premise over RFC
258
+
259
+ The same ADT calls, over `SADT_REST_RFC_ENDPOINT` — the function module Eclipse
260
+ ADT itself uses through JCo — instead of over HTTP. Worth taking on a system
261
+ where stateful HTTP sessions are not usable: an RFC conversation is one ABAP
262
+ session for its whole lifetime, which is the way past `423 invalid lock handle`
263
+ on BASIS < 7.50.
264
+
265
+ Needs the SAP NW RFC SDK on the machine and `npm install @mcp-abap-adt/sap-rfc-lite`.
266
+
267
+ ```typescript
268
+ import {
269
+ AdtOnPremConnector,
270
+ BasicAuthProvider,
271
+ RfcTransport,
272
+ rfcConversationFrom,
273
+ } from "@mcp-abap-adt/connection";
274
+
275
+ const connection = new AdtOnPremConnector(
276
+ config,
277
+ new BasicAuthProvider(config.username!, config.password!),
278
+ new RfcTransport(rfcConversationFrom(config), logger),
279
+ logger,
280
+ );
281
+
282
+ await connection.connect();
283
+ // Everything above the wire is the same: makeAdtRequest, setSessionType,
284
+ // disconnect. What differs is where the session lives — see below.
285
+ ```
286
+
287
+ **Where to look for it.** An HTTP session is an ICF session and appears in
288
+ **SM05**. An RFC conversation is a gateway client: it appears in **SMGW → Logged
289
+ on Clients** as `NWRFC`, and never in SM05, because there is no ICM in that
290
+ path. Looking for one in the other monitor and finding nothing is not a fault.
291
+
292
+ There is no cloud equivalent: ABAP Cloud has one wire, and `AdtCloudConnector`
293
+ takes no transport parameter at all.
294
+
216
295
  ### SSO Usage (SAML Session Cookies)
217
296
 
218
297
  ```typescript
219
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
298
+ import {
299
+ AdtOnPremConnector,
300
+ OnPremHttpTransport,
301
+ SamlAuthProvider,
302
+ SapConfig,
303
+ getTimeout,
304
+ } from "@mcp-abap-adt/connection";
220
305
 
221
306
  const config: SapConfig = {
222
307
  url: "https://your-sap-system.com",
@@ -224,34 +309,49 @@ const config: SapConfig = {
224
309
  sessionCookies: "MYSAPSSO2=...; SAP_SESSIONID=...",
225
310
  };
226
311
 
227
- const connection = createAbapConnection(config, logger, undefined, undefined, {
228
- system: "onprem", // which SYSTEM this is — said, never detected
229
- });
312
+ // The cookies ARE the credential here — there is no Authorization header at all.
313
+ const connection = new AdtOnPremConnector(
314
+ config,
315
+ new SamlAuthProvider(config.sessionCookies!),
316
+ new OnPremHttpTransport(() => ({}), logger, {
317
+ client: config.client,
318
+ baseUrl: config.url,
319
+ }),
320
+ logger,
321
+ );
230
322
  await connection.connect();
231
323
 
232
324
  const response = await connection.makeAdtRequest({
233
325
  method: "GET",
234
326
  url: "/sap/bc/adt/programs/programs/your-program",
327
+ timeout: getTimeout("default"),
235
328
  });
236
329
  ```
237
330
 
238
331
  ### Cloud Usage with Automatic Token Refresh
239
332
 
240
- For automatic token refresh on **401** errors, inject `ITokenRefresher`:
333
+ Give `TokenAuthProvider` an `ITokenRefresher` and the provider replaces an
334
+ **expired** token on its own — it is asked per request and checks expiry before
335
+ answering, so nobody decides to renew. A token the source still believes in and
336
+ the server refuses is the other half, and that one **surfaces**:
241
337
 
242
338
  ```typescript
243
339
  import {
244
340
  AdtCloudConnector,
245
- TokenAuthProvider,
341
+ CloudHttpTransport,
246
342
  SapConfig,
343
+ TokenAuthProvider,
344
+ getTimeout,
247
345
  } from "@mcp-abap-adt/connection";
248
346
  import type { ITokenRefresher } from "@mcp-abap-adt/interfaces";
249
347
 
250
348
  // Token refresher provides token acquisition and refresh
251
349
  // (created by @mcp-abap-adt/auth-broker or custom implementation)
350
+ const currentAccessToken = 'the access token you already hold';
351
+ const exchangeRefreshToken = async () => 'a freshly exchanged access token';
252
352
  const tokenRefresher: ITokenRefresher = {
253
- getToken: async () => { /* return current token */ },
254
- refreshToken: async () => { /* refresh and return new token */ },
353
+ getToken: async () => currentAccessToken, // the one you hold
354
+ refreshToken: async () => exchangeRefreshToken(), // a new one, and cache it
255
355
  };
256
356
 
257
357
  const config: SapConfig = {
@@ -264,19 +364,24 @@ const config: SapConfig = {
264
364
  const connection = new AdtCloudConnector(
265
365
  config,
266
366
  new TokenAuthProvider(tokenRefresher),
367
+ new CloudHttpTransport(() => ({}), logger, {
368
+ client: config.client,
369
+ baseUrl: config.url,
370
+ }),
267
371
  logger,
268
372
  );
269
373
  await connection.connect();
270
374
 
271
- // On a 401 the connector tells the provider its token was refused, asks again,
272
- // and only if the answer changed rebuilds the session and retries once. An
273
- // unchanged answer means the server refused these credentials, and the 401
375
+ // On a 401 nothing here decides to get a new credential: the refusal reaches
376
+ // you. Whether it meant "stale" is a judgement made with what you know, and
377
+ // `renew()` is the seam you make it with. The session is untouched — a refused
274
378
  // reaches you. A refresh replaces the SAP session, so if a lock window is open
275
379
  // the request fails with ADT_SESSION_REPLACED rather than continuing on a
276
380
  // session your lock is not in.
277
381
  const response = await connection.makeAdtRequest({
278
382
  method: "GET",
279
383
  url: "/sap/bc/adt/programs/programs/your-program",
384
+ timeout: getTimeout("default"),
280
385
  });
281
386
  ```
282
387
 
@@ -297,11 +402,22 @@ See [MIGRATION-4.0.md](./docs/MIGRATION-4.0.md).
297
402
  For operations that require session state (e.g., object modifications), you can enable stateful sessions:
298
403
 
299
404
  ```typescript
300
- import { createAbapConnection } from "@mcp-abap-adt/connection";
405
+ import {
406
+ AdtOnPremConnector,
407
+ BasicAuthProvider,
408
+ OnPremHttpTransport,
409
+ getTimeout,
410
+ } from "@mcp-abap-adt/connection";
301
411
 
302
- const connection = createAbapConnection(config, logger, undefined, undefined, {
303
- system: "onprem", // which SYSTEM this is — said, never detected
304
- });
412
+ const connection = new AdtOnPremConnector(
413
+ config,
414
+ new BasicAuthProvider(config.username!, config.password!),
415
+ new OnPremHttpTransport(() => ({}), logger, {
416
+ client: config.client,
417
+ baseUrl: config.url,
418
+ }),
419
+ logger,
420
+ );
305
421
  await connection.connect();
306
422
 
307
423
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
@@ -312,6 +428,7 @@ await connection.makeAdtRequest({
312
428
  method: "POST",
313
429
  url: "/sap/bc/adt/objects/domains",
314
430
  data: { /* domain data */ },
431
+ timeout: getTimeout("default"),
315
432
  });
316
433
 
317
434
  // Note: Session state persistence is handled by @mcp-abap-adt/auth-broker package
@@ -320,7 +437,7 @@ await connection.makeAdtRequest({
320
437
  ### Custom Logger
321
438
 
322
439
  ```typescript
323
- import { ILogger } from "@mcp-abap-adt/connection";
440
+ import { AdtOnPremConnector, BasicAuthProvider, ILogger, OnPremHttpTransport } from "@mcp-abap-adt/connection";
324
441
 
325
442
  class MyLogger implements ILogger {
326
443
  info(message: string, meta?: any): void {
@@ -349,9 +466,15 @@ class MyLogger implements ILogger {
349
466
  }
350
467
 
351
468
  const logger = new MyLogger();
352
- const connection = createAbapConnection(config, logger, undefined, undefined, {
353
- system: "onprem", // which SYSTEM this is — said, never detected
354
- });
469
+ const connection = new AdtOnPremConnector(
470
+ config,
471
+ new BasicAuthProvider(config.username!, config.password!),
472
+ new OnPremHttpTransport(() => ({}), logger, {
473
+ client: config.client,
474
+ baseUrl: config.url,
475
+ }),
476
+ logger,
477
+ );
355
478
  ```
356
479
 
357
480
  ## CLI Tool
@@ -441,6 +564,8 @@ type SapConfig = {
441
564
  Main interface for ABAP connections.
442
565
 
443
566
  ```typescript
567
+ import { AbapRequestOptions } from '@mcp-abap-adt/connection';
568
+ import type { AxiosResponse } from 'axios';
444
569
  // The shared contract (IAbapConnection), what every connection provides:
445
570
  interface AbapConnection {
446
571
  connect(): Promise<void>; // REQUIRED before any request; rejects on failure
@@ -451,10 +576,12 @@ interface AbapConnection {
451
576
  }
452
577
  ```
453
578
 
454
- The HTTP connection classes carry the rest of the session lifecycle. It is on the
455
- shared contract as a **capability atom** in `@mcp-abap-adt/interfaces` rather than
456
- as methods on `IAbapConnection`, which is why `RfcAbapConnection` — a transport
457
- that owns no HTTP session — is unaffected by its existence:
579
+ The connectors carry the rest of the session lifecycle. It is on the shared
580
+ contract as a **capability atom** in `@mcp-abap-adt/interfaces` rather than as
581
+ methods on `IAbapConnection`, so a consumer that only carries requests is
582
+ unaffected by its existence. Note that a connection over RFC has the whole of it
583
+ — what an RFC conversation has none of is a session RESOURCE to open and close
584
+ by address, which is an empty mechanism, not an absent lifecycle:
458
585
 
459
586
  ```typescript
460
587
  // ISessionLifecycleAware
@@ -496,16 +623,35 @@ interface ILogger {
496
623
 
497
624
  ### Functions
498
625
 
499
- #### `createAbapConnection(config, logger?, sessionId?)`
626
+ #### `rfcConversationFrom(config)`
500
627
 
501
- Factory function to create an ABAP connection instance.
628
+ What `RfcTransport` is constructed with. Derives `ashost` from the url and
629
+ `sysnr` from the HTTP port by the SAP convention that `80XX` is the ICM port for
630
+ system `XX`, which `SAP_SYSNR` overrides for a port that follows no convention.
631
+
632
+ The SAP NW RFC SDK is loaded when a conversation opens, not when this is called,
633
+ so a machine without it fails at `connect()` with a message saying what to
634
+ install rather than at construction.
635
+
636
+ ```text
637
+ function rfcConversationFrom(config: SapConfig): () => IRfcConversation;
638
+ function rfcParamsFrom(config: SapConfig): RfcConnectionParams;
639
+ ```
502
640
 
503
641
  ```typescript
504
- function createAbapConnection(
505
- config: SapConfig,
506
- logger?: ILogger | null,
507
- sessionId?: string
508
- ): AbapConnection;
642
+ import {
643
+ AdtOnPremConnector,
644
+ BasicAuthProvider,
645
+ RfcTransport,
646
+ rfcConversationFrom,
647
+ } from "@mcp-abap-adt/connection";
648
+
649
+ const connection = new AdtOnPremConnector(
650
+ config,
651
+ new BasicAuthProvider(config.username!, config.password!),
652
+ new RfcTransport(rfcConversationFrom(config), logger),
653
+ logger,
654
+ );
509
655
  ```
510
656
 
511
657
  #### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES`
@@ -532,6 +678,12 @@ import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
532
678
  ```typescript
533
679
  import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
534
680
 
681
+ // Whatever HTTP client your own connection class is built on.
682
+ const yourHttpClient = {
683
+ get: async (url: string, config: { headers: Record<string, string> }) =>
684
+ ({ headers: {} as Record<string, string> }),
685
+ };
686
+
535
687
  async function fetchCsrfToken(baseUrl: string): Promise<string> {
536
688
  const csrfUrl = `${baseUrl}${CSRF_CONFIG.ENDPOINT}`;
537
689
 
@@ -563,6 +715,10 @@ async function fetchCsrfToken(baseUrl: string): Promise<string> {
563
715
  await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
564
716
  }
565
717
  }
718
+
719
+ // Unreachable: the last attempt either returns or throws above. Stated so the
720
+ // function has a return type the compiler can agree with.
721
+ throw new Error(CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
566
722
  }
567
723
  ```
568
724
 
@@ -7,16 +7,20 @@
7
7
  * `CertificateAuthProvider` returns are what `CertificateAbapConnection`
8
8
  * returned. Nothing about how a credential works changed; only who owns it.
9
9
  */
10
- import type { AgentOptions } from 'node:https';
11
- import type { ICertificateMaterialLoader, ISapConfig, ITokenRefresher } from '@mcp-abap-adt/interfaces';
12
- import type { IAuthProvider } from './IAuthProvider.js';
10
+ import type { IAuthProvider, ICertificateMaterial, ICertificateMaterialLoader, IRenewableCredential, ISapConfig, ITokenRefresher } from '@mcp-abap-adt/interfaces';
13
11
  /** Username and password, as `Basic base64(user:pass)`. */
14
12
  export declare class BasicAuthProvider implements IAuthProvider {
15
13
  private readonly username;
16
14
  private readonly password;
17
15
  readonly kind = "basic";
16
+ /** Nothing to get ready: this credential is complete as constructed. */
17
+ prepare(): Promise<void>;
18
+ /** Not cookies. This one authenticates with a header. */
19
+ cookies(): string | null;
20
+ /** No TLS material: this credential lives in a header, not in the transport. */
21
+ transportMaterial(): ICertificateMaterial;
18
22
  constructor(username: string, password: string);
19
- authorizationHeader(): Promise<string>;
23
+ authorizationHeader(): Promise<string | null>;
20
24
  }
21
25
  /**
22
26
  * A bearer token, kept current by whoever issued it.
@@ -30,9 +34,15 @@ export declare class BasicAuthProvider implements IAuthProvider {
30
34
  * credential behind it is renewed mid-flight is the connection's business and
31
35
  * stays there; see `JwtAbapConnection`, which still owns that machinery.
32
36
  */
33
- export declare class TokenAuthProvider implements IAuthProvider {
37
+ export declare class TokenAuthProvider implements IRenewableCredential {
34
38
  private readonly source;
35
39
  readonly kind = "token";
40
+ /** Nothing to get ready: this credential is complete as constructed. */
41
+ prepare(): Promise<void>;
42
+ /** Not cookies. This one authenticates with a header. */
43
+ cookies(): string | null;
44
+ /** No TLS material: this credential lives in a header, not in the transport. */
45
+ transportMaterial(): ICertificateMaterial;
36
46
  /**
37
47
  * A token, or something that can produce one.
38
48
  *
@@ -50,10 +60,21 @@ export declare class TokenAuthProvider implements IAuthProvider {
50
60
  * stale one and hide exactly the renewal the provider exists to do — which
51
61
  * is what the first version of this class did.
52
62
  */
53
- authorizationHeader(): Promise<string>;
63
+ authorizationHeader(): Promise<string | null>;
54
64
  /**
55
65
  * The token was refused, so force a new one.
56
66
  *
67
+ * Declared through `IRenewableCredential` rather than as an optional member
68
+ * of every credential, so a consumer narrows to it: a password has nothing
69
+ * behind it to ask again, and a SAML session was negotiated elsewhere.
70
+ *
71
+ * **Nothing in this package calls it.** A token that EXPIRED is replaced
72
+ * inside `authorizationHeader()` above, which is asked per request — nobody
73
+ * decides anything. This is the other case, a token the source still believes
74
+ * in and the server refuses, and whether a refusal meant that is a judgement
75
+ * made with what the caller knows. So the refusal surfaces, and this is the
76
+ * seam the caller decides with.
77
+ *
57
78
  * `getToken()` is documented to return the cached token while it believes it
58
79
  * is still valid — which is exactly the situation after a 401 on a token the
59
80
  * provider has not yet noticed is dead. `refreshToken()` is the contract's
@@ -70,8 +91,13 @@ export declare class TokenAuthProvider implements IAuthProvider {
70
91
  export declare class SamlAuthProvider implements IAuthProvider {
71
92
  private readonly sessionCookies;
72
93
  readonly kind = "saml";
94
+ /** Nothing to get ready: this credential is complete as constructed. */
95
+ prepare(): Promise<void>;
96
+ /** No TLS material: this credential lives in a header, not in the transport. */
97
+ transportMaterial(): ICertificateMaterial;
73
98
  constructor(sessionCookies: string);
74
- authorizationHeader(): Promise<string>;
99
+ /** Not a header: this credential authenticates with the cookies below. */
100
+ authorizationHeader(): Promise<string | null>;
75
101
  /** The cookies to present. */
76
102
  cookies(): string;
77
103
  }
@@ -86,10 +112,13 @@ export declare class CertificateAuthProvider implements IAuthProvider {
86
112
  private readonly loader;
87
113
  private readonly config;
88
114
  readonly kind = "certificate";
115
+ /** Not cookies. This one authenticates with a header. */
116
+ cookies(): string | null;
89
117
  private material;
90
118
  constructor(loader: ICertificateMaterialLoader, config: ISapConfig);
91
119
  prepare(): Promise<void>;
92
- authorizationHeader(): Promise<string>;
93
- httpsAgentOptions(): AgentOptions;
120
+ /** Not a header: this credential authenticates through TLS. */
121
+ authorizationHeader(): Promise<string | null>;
122
+ transportMaterial(): ICertificateMaterial;
94
123
  }
95
124
  //# sourceMappingURL=providers.d.ts.map