@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
@@ -83,7 +83,7 @@ In your code:
83
83
 
84
84
  ```typescript
85
85
  import 'dotenv/config'; // or require('dotenv').config();
86
- import { createAbapConnection } from '@mcp-abap-adt/connection';
86
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
87
87
 
88
88
  const config = {
89
89
  url: process.env.SAP_URL!,
@@ -138,7 +138,7 @@ See [JWT_AUTH_TOOLS.md](./JWT_AUTH_TOOLS.md) for detailed CLI documentation.
138
138
  ### Test Installation
139
139
 
140
140
  ```bash
141
- node -e "const { createAbapConnection } = require('@mcp-abap-adt/connection'); console.log('✓ Package loaded successfully');"
141
+ node -e "const { AdtOnPremConnector } = require('@mcp-abap-adt/connection'); console.log('✓ Package loaded successfully');"
142
142
  ```
143
143
 
144
144
  ### Test Connection (Basic Auth)
@@ -146,7 +146,7 @@ node -e "const { createAbapConnection } = require('@mcp-abap-adt/connection'); c
146
146
  Create `test-connection.js`:
147
147
 
148
148
  ```javascript
149
- const { createAbapConnection } = require('@mcp-abap-adt/connection');
149
+ const { AdtOnPremConnector, BasicAuthProvider } = require('@mcp-abap-adt/connection');
150
150
 
151
151
  const config = {
152
152
  url: 'https://your-sap-server.com',
@@ -163,9 +163,15 @@ const logger = {
163
163
  debug: (msg) => console.log('[DEBUG]', msg),
164
164
  };
165
165
 
166
- const connection = createAbapConnection(config, logger, undefined, undefined, {
167
- system: 'onprem', // or 'cloud' — said by you, never detected
168
- });
166
+ const connection = new AdtOnPremConnector(
167
+ config,
168
+ new BasicAuthProvider(config.username!, config.password!),
169
+ new OnPremHttpTransport(() => ({}), logger, {
170
+ client: config.client,
171
+ baseUrl: config.url,
172
+ }),
173
+ logger,
174
+ );
169
175
 
170
176
  connection.connect()
171
177
  .then(() =>
@@ -216,7 +222,13 @@ npm install --save-dev typescript @types/node
216
222
  ### TypeScript Example
217
223
 
218
224
  ```typescript
219
- import { createAbapConnection, SapConfig, ILogger } from '@mcp-abap-adt/connection';
225
+ import {
226
+ AdtOnPremConnector,
227
+ BasicAuthProvider,
228
+ ILogger,
229
+ OnPremHttpTransport,
230
+ SapConfig,
231
+ } from '@mcp-abap-adt/connection';
220
232
 
221
233
  const config: SapConfig = {
222
234
  url: 'https://your-sap-server.com',
@@ -233,9 +245,15 @@ const logger: ILogger = {
233
245
  debug: (msg: string) => console.log(msg),
234
246
  };
235
247
 
236
- const connection = createAbapConnection(config, logger, undefined, undefined, {
237
- system: 'onprem', // or 'cloud' — said by you, never detected
238
- });
248
+ const connection = new AdtOnPremConnector(
249
+ config,
250
+ new BasicAuthProvider(config.username!, config.password!),
251
+ new OnPremHttpTransport(() => ({}), logger, {
252
+ client: config.client,
253
+ baseUrl: config.url,
254
+ }),
255
+ logger,
256
+ );
239
257
  ```
240
258
 
241
259
  ## Troubleshooting
@@ -123,13 +123,29 @@ source .env
123
123
  Or configure `SapConfig` directly:
124
124
 
125
125
  ```ts
126
- import { createAbapConnection } from "@mcp-abap-adt/connection";
127
-
128
- const connection = createAbapConnection({
126
+ import {
127
+ AdtCloudConnector,
128
+ CloudHttpTransport,
129
+ TokenAuthProvider,
130
+ } from "@mcp-abap-adt/connection";
131
+ import type { SapConfig } from "@mcp-abap-adt/connection";
132
+
133
+ const config: SapConfig = {
129
134
  url: process.env.SAP_URL!,
130
135
  authType: "jwt",
131
136
  jwtToken: process.env.SAP_JWT_TOKEN!,
132
- });
137
+ };
138
+ const logger = console;
139
+
140
+ const connection = new AdtCloudConnector(
141
+ config,
142
+ new TokenAuthProvider(config.jwtToken!),
143
+ new CloudHttpTransport(() => ({}), logger, {
144
+ client: config.client,
145
+ baseUrl: config.url,
146
+ }),
147
+ logger,
148
+ );
133
149
 
134
150
  // Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
135
151
  // The refresh token credentials in .env are used by auth-broker, not connection
@@ -0,0 +1,359 @@
1
+ # Migration to 6.0
2
+
3
+ The factory and the six connection classes are gone. What replaces them is not a
4
+ new API so much as the one 5.0 introduced, now the only one: **you state which
5
+ system, which credential, and which wire, and nothing is worked out for you.**
6
+
7
+ **The wire is a required argument.** There is no default to fall back to and
8
+ nothing is derived from the config, because which wire a deployment uses is a
9
+ fact about the deployment. It also carries the two things only you can supply —
10
+ the credential's TLS material and the client — so it arrives built.
11
+
12
+ If you already moved to `AdtOnPremConnector` / `AdtCloudConnector` in 5.0, most
13
+ of this does not apply to you — skip to [Taking the RFC wire](#taking-the-rfc-wire).
14
+
15
+ ## What was removed
16
+
17
+ | removed | take instead |
18
+ |---|---|
19
+ | `createAbapConnection()` | `new AdtOnPremConnector(...)` or `new AdtCloudConnector(...)` |
20
+ | `BaseAbapConnection`, `OnPremAbapConnection` | `AdtOnPremConnector` + `BasicAuthProvider` |
21
+ | `JwtAbapConnection`, `CloudAbapConnection` | `AdtCloudConnector` + `TokenAuthProvider` |
22
+ | `SamlAbapConnection` | `AdtOnPremConnector` + `SamlAuthProvider` |
23
+ | `CertificateAbapConnection` | `AdtOnPremConnector` + `CertificateAuthProvider` |
24
+ | `KerberosAbapConnection` | — see [Kerberos](#kerberos) |
25
+ | `RfcAbapConnection`, `connectionType: 'rfc'` | `AdtOnPremConnector` + `RfcTransport` |
26
+ | `disconnect({ deadlineMs })` | `disconnect()` — see [Teardown](#teardown) |
27
+
28
+ ## Why
29
+
30
+ The factory switched on things the caller had already said. Given
31
+ `options.system` it did no inference at all, and without it, it inferred the
32
+ session mechanism from the credential — which is the arrangement that gets one
33
+ of them wrong: a bearer token against an on-prem system is ordinary, and it does
34
+ not make that system a cloud one.
35
+
36
+ The classes had the same shape of problem one level down. Six subclasses
37
+ distinguished by an argument, and `RfcAbapConnection` was a second translation of
38
+ `SADT_REST_RFC_ENDPOINT` beside `RfcTransport` — two of those drift, and these
39
+ had: the class was missing a default `Accept` header and carried cookie-handling
40
+ code that could never run.
41
+
42
+ ## Basic
43
+
44
+ ```diff
45
+ -import { createAbapConnection } from '@mcp-abap-adt/connection';
46
+ +import {
47
+ + AdtOnPremConnector,
48
+ + BasicAuthProvider,
49
+ +} from '@mcp-abap-adt/connection';
50
+
51
+ -const connection = createAbapConnection(config, logger, undefined, undefined, {
52
+ - system: 'onprem',
53
+ -});
54
+ +const connection = new AdtOnPremConnector(
55
+ + config,
56
+ + new BasicAuthProvider(config.username!, config.password!),
57
+ + new OnPremHttpTransport(() => ({}), logger, {
58
+ + client: config.client,
59
+ + baseUrl: config.url,
60
+ + }),
61
+ + logger,
62
+ +);
63
+ ```
64
+
65
+ `client` is not decoration: SAP answers `sap-usercontext` with the system
66
+ default rather than the client you asked for, and later requests then route to a
67
+ client you never named — on a read-only one, every write comes back `403`.
68
+
69
+ ## JWT / OAuth2
70
+
71
+ ```diff
72
+ -const connection = createAbapConnection(config, logger, undefined, refresher, {
73
+ - system: 'cloud',
74
+ -});
75
+ +const connection = new AdtCloudConnector(
76
+ + config,
77
+ + new TokenAuthProvider(refresher),
78
+ + new CloudHttpTransport(() => ({}), logger, {
79
+ + client: config.client,
80
+ + baseUrl: config.url,
81
+ + }),
82
+ + logger,
83
+ +);
84
+ ```
85
+
86
+ The cloud wire is not the on-prem one with a different name. It asks for a
87
+ session at `/sap/bc/adt/core/http/sessions` and gives it back by `DELETE` on the
88
+ address the server publishes; the on-prem wire has neither, and says goodbye
89
+ through the platform logoff. Handing the wrong one to a connector does not
90
+ compile.
91
+
92
+ The `ITokenRefresher` that used to be a constructor slot **is** the credential
93
+ now. Hand `TokenAuthProvider` a bare string instead and you get a token with
94
+ nothing behind it — fine for a short task, wrong for anything long-lived.
95
+
96
+ One behaviour to know about: **a credential refused during `connect()` now
97
+ surfaces.** The old class renewed and retried inside its own CSRF fetch. It does
98
+ not any more, and deliberately: renewal is the provider's, and the provider does
99
+ it on an expiry it can see, on every call that asks for a header. A token the
100
+ provider still believes in and the server refuses is a credential that needs
101
+ attention, and `connect()` is one call you make — so the refusal is yours to
102
+ answer.
103
+
104
+ The request path changed the same way, and in the same direction: a 401 is no
105
+ longer answered here either. See [A refused credential](#a-refused-credential).
106
+
107
+ ## SAML
108
+
109
+ ```diff
110
+ -const connection = new SamlAbapConnection(config, logger);
111
+ +const connection = new AdtOnPremConnector(
112
+ + config,
113
+ + new SamlAuthProvider(config.sessionCookies!),
114
+ + new OnPremHttpTransport(() => ({}), logger, {
115
+ + client: config.client,
116
+ + baseUrl: config.url,
117
+ + }),
118
+ + logger,
119
+ +);
120
+ ```
121
+
122
+ The cookies are the credential — there is no `Authorization` header at all.
123
+
124
+ ## Certificates
125
+
126
+ A certificate authenticates through the transport rather than through a header,
127
+ so this is the one place the two axes touch — and you wire them, because nobody
128
+ else can:
129
+
130
+ ```diff
131
+ -const connection = new CertificateAbapConnection(config, logger);
132
+ +const credential = new CertificateAuthProvider(config, certLoader);
133
+ +
134
+ +const connection = new AdtOnPremConnector(
135
+ + config,
136
+ + credential,
137
+ + new OnPremHttpTransport(
138
+ + // A thunk, not a value: the material is loaded during connect(), so a
139
+ + // wire that read it at construction would read nothing.
140
+ + () => credential.transportMaterial?.() ?? {},
141
+ + logger,
142
+ + { client: config.client, baseUrl: config.url },
143
+ + ),
144
+ + logger,
145
+ +);
146
+ ```
147
+
148
+ Forget the thunk and mTLS silently does not happen — the connection is built,
149
+ the requests go out, and the server refuses them for a reason that says nothing
150
+ about the certificate.
151
+
152
+ ## Taking the RFC wire
153
+
154
+ `connectionType: 'rfc'` is gone. The wire is an argument now, which is what it
155
+ always was in fact:
156
+
157
+ ```diff
158
+ -const connection = createAbapConnection(
159
+ - { ...config, connectionType: 'rfc' },
160
+ - logger,
161
+ -);
162
+ +import {
163
+ + AdtOnPremConnector,
164
+ + BasicAuthProvider,
165
+ + RfcTransport,
166
+ + rfcConversationFrom,
167
+ +} from '@mcp-abap-adt/connection';
168
+ +
169
+ +const connection = new AdtOnPremConnector(
170
+ + config,
171
+ + new BasicAuthProvider(config.username!, config.password!),
172
+ + new RfcTransport(rfcConversationFrom(config), logger),
173
+ + logger,
174
+ +);
175
+ ```
176
+
177
+ The wire sits where the HTTP one sits, because it is the same argument. There is
178
+ no option to set and no config field to flip.
179
+
180
+ `rfcConversationFrom(config)` does what the class's constructor did: `ashost`
181
+ from the url, `sysnr` from the HTTP port by the SAP convention that `80XX` is
182
+ the ICM port for system `XX`, `SAP_SYSNR` overriding it for a port that follows
183
+ no convention. The SAP NW RFC SDK is loaded when a conversation opens, not when
184
+ this is called, so a machine without it fails at `connect()` with a message
185
+ saying what to install.
186
+
187
+ Everything above the wire is unchanged: `makeAdtRequest`, `setSessionType`,
188
+ `disconnect`, and the session lifecycle. Three things are better than they were
189
+ on the removed class:
190
+
191
+ - a default `Accept` is supplied, so a call that names none is answered rather
192
+ than refused with `400 ExceptionResourceBadRequest`
193
+ - `disconnect()` and `getSessionIdentity()` exist, which they did not
194
+ - the identity is the conversation, and it changes when the conversation does —
195
+ so reconnecting reads as a different session instead of the same one
196
+
197
+ **Where to look for the session.** An HTTP session is an ICF session and appears
198
+ in **SM05**. An RFC conversation appears in **SMGW → Logged on Clients** as
199
+ `NWRFC`, and never in SM05, because there is no ICM in that path.
200
+
201
+ ## Teardown
202
+
203
+ `disconnect()` takes no arguments.
204
+
205
+ ```diff
206
+ -await connection.disconnect({ deadlineMs: 5000 });
207
+ +await connection.disconnect();
208
+ ```
209
+
210
+ It **notifies**: it tells the server the session is finished and does not act on
211
+ the answer — whether and when the session is freed is the server's affair. The
212
+ `deadlineMs` it used to accept bounded a wait for that answer, which bought you
213
+ nothing and was the one thing that could make a teardown unbounded, since the
214
+ goodbye carries no request timeout by design. `SAP_RELEASE_DEADLINE_MS` is gone
215
+ with it.
216
+
217
+ Everything else is unchanged: it resolves rather than throws, always settles,
218
+ and a repeat call performs whatever is still owed.
219
+
220
+ ## Writing your own credential
221
+
222
+ The shipped ones need no change from you — they are handed to a connector and
223
+ that is all. If you wrote your own against `IAuthProvider`, it now states all of
224
+ itself. Five members, and three of them are usually empty:
225
+
226
+ ```diff
227
+ class MyCredential implements IAuthProvider {
228
+ readonly kind = 'mine';
229
+ + async prepare(): Promise<void> {}
230
+ async authorizationHeader(): Promise<string | null> { … }
231
+ + cookies(): string | null { return null; }
232
+ + transportMaterial(): ICertificateMaterial { return {}; }
233
+ }
234
+ ```
235
+
236
+ Empty is not ceremony: "nothing to prepare", "I am not cookies", "I contribute
237
+ no TLS material" are facts about a credential, and a fact is stated rather than
238
+ left for a connection to discover by checking whether a method exists. What it
239
+ buys is that no base class asks anything — every question about a collaborator
240
+ is answered by the type.
241
+
242
+ **One credential still has no home, and this is the honest statement of it.**
243
+ SPNEGO's token is consumed by the first request that carries it, so the exchange
244
+ and the establishing call are the same act — and `establish()` may retry, asking
245
+ `authorizationHeader()` again per attempt, which for that credential means
246
+ presenting something already spent.
247
+
248
+ An earlier cut of this release answered it with an atom the connection narrowed
249
+ to. That is gone: a contract earns its place by something implementing it, and
250
+ nothing here implements that one. What such a credential needs is either an
251
+ exchange it owns end to end, or a signal that the establishing request
252
+ succeeded — neither exists yet, and inventing the seam before there is a
253
+ credential to test it against is how the last one came to be removed.
254
+
255
+ Everything shipping today authenticates by header, cookie or TLS material, and
256
+ those three are what `IAuthProvider` states.
257
+
258
+ Needs `@mcp-abap-adt/interfaces` 21.0.0.
259
+
260
+ ## A refused credential
261
+
262
+ The connector used to answer a `401` for you: it called `renew()` on the
263
+ credential, compared the header with the previous one, and rebuilt the session if
264
+ it had changed. It does not any more, and nothing replaces it — the refusal
265
+ surfaces.
266
+
267
+ Renewal splits in two, and only one half was ever automatic:
268
+
269
+ - a token that **expired** is replaced inside `authorizationHeader()`, which is
270
+ asked per request. Nobody decides anything, and nothing changed here;
271
+ - a token the provider still **believes in** and the server refuses is a
272
+ different thing, and whether that is what happened is a judgement made with
273
+ what you know.
274
+
275
+ So handle it where you can:
276
+
277
+ ```diff
278
+ -await conn.makeAdtRequest(options); // a 401 was retried underneath you
279
+ +try {
280
+ + await conn.makeAdtRequest(options);
281
+ +} catch (error) {
282
+ + if (isUnauthorized(error) && isRenewable(credential)) {
283
+ + await credential.renew();
284
+ + // ... and decide for yourself whether to try again
285
+ + }
286
+ + throw error;
287
+ +}
288
+ ```
289
+
290
+ `isRenewable` is the narrowing the contract recommends, since only some
291
+ credentials have it — a password has nothing behind it to ask again, and a SAML
292
+ session was negotiated elsewhere:
293
+
294
+ ```typescript
295
+ import type { IAuthProvider, IRenewableCredential } from '@mcp-abap-adt/interfaces';
296
+
297
+ function isRenewable(c: IAuthProvider): c is IRenewableCredential {
298
+ return typeof (c as Partial<IRenewableCredential>).renew === 'function';
299
+ }
300
+ ```
301
+
302
+ Needs `@mcp-abap-adt/interfaces` 19.0.0, where `renew()` becomes that atom
303
+ instead of an optional member every credential carried.
304
+
305
+ **The connection is not torn down over it.** A refused credential is not a lost
306
+ session, and discarding one the server never complained about would throw away
307
+ work you can still finish.
308
+
309
+ ## Kerberos
310
+
311
+ `KerberosAbapConnection` is removed without a direct replacement. It was
312
+ single-leg only, and untested against a live KDC — issue #35 says so. If you
313
+ depend on it, stay on 5.x and say so on that issue; a `KerberosAuthProvider`
314
+ belongs on the credential axis and can be added there, but it should be added
315
+ with a system to test it against.
316
+
317
+ ## Types you can now name
318
+
319
+ The seam is public, so a signature can say what it needs instead of taking any
320
+ connection and casting:
321
+
322
+ ```typescript
323
+ import { AdtOnPremConnector, RfcTransport } from '@mcp-abap-adt/connection';
324
+ import type {
325
+ IAdtTransport,
326
+ IAdtTransportRequest,
327
+ IAdtTransportResponse,
328
+ IAdtEstablishContext,
329
+ IAdtSessionContext,
330
+ IOnPremTransport,
331
+ ICloudTransport,
332
+ IRfcConversation,
333
+ RfcConnectionParams,
334
+ } from '@mcp-abap-adt/connection';
335
+
336
+ function overRfc(conn: AdtOnPremConnector<IAuthProvider, RfcTransport>) { /* ... */ }
337
+ ```
338
+
339
+ ### Bringing your own wire
340
+
341
+ The connectors are constrained by a marker, not by the shipped classes, so a
342
+ transport you write is a first-class one. Say which system it is for and it fits
343
+ where the shipped wires fit:
344
+
345
+ ```typescript
346
+ import type { IOnPremTransport } from '@mcp-abap-adt/connection';
347
+
348
+ class RecordingTransport implements IOnPremTransport {
349
+ readonly kind = 'recording';
350
+ readonly system = 'onprem' as const;
351
+ // ... the rest of IAdtTransport
352
+ }
353
+ ```
354
+
355
+ `system` is read by the compiler and never at runtime. Its only job is to stop
356
+ "ABAP Cloud over an on-prem wire" from compiling — which is also why the
357
+ constraint is this marker and not `OnPremHttpTransport`: the shipped classes
358
+ carry private state and compare nominally, so constraining to them would have
359
+ made this impossible while appearing to allow it.
package/docs/SCOPE.md CHANGED
@@ -35,7 +35,7 @@ It forwards every `(name, value)` pair from the JS `Client(params)` object to `R
35
35
 
36
36
  **4. For BTP on-prem scenarios (Cloud Connector), the JWT→x509/SNC swap happens server-side in the CC.** The client-side RFC call still carries classic credentials (x509 + SNC), not the JWT. Supporting that would be adding SNC-over-RFC for on-prem backends behind a CC tunnel — an on-prem extension, not a "cloud RFC" feature.
37
37
 
38
- **Conclusion:** "RFC to cloud" is not a missing feature that would be useful to add; it is a category error. ADT to cloud = HTTP (already supported here). RFC to on-prem = supported here via `connectionType: 'rfc'`. Anything claiming a third combination does not map to what the SAP stack actually exposes.
38
+ **Conclusion:** "RFC to cloud" is not a missing feature that would be useful to add; it is a category error. ADT to cloud = HTTP (already supported here). RFC to on-prem = supported here, by handing `AdtOnPremConnector` an `RfcTransport`. Anything claiming a third combination does not map to what the SAP stack actually exposes.
39
39
 
40
40
  ## See also
41
41
 
@@ -26,19 +26,47 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
26
26
 
27
27
  ## Enabling Stateful Sessions
28
28
 
29
+ Every example below builds on this much, so it is stated once:
30
+
29
31
  ```ts
30
- import { createAbapConnection } from '@mcp-abap-adt/connection';
32
+ import type { SapConfig } from '@mcp-abap-adt/connection';
33
+
34
+ const config: SapConfig = {
35
+ url: 'https://your-sap-server.com',
36
+ authType: 'basic',
37
+ username: 'your-username',
38
+ password: 'your-password',
39
+ client: '100',
40
+ };
41
+ const user = config.username!;
42
+ const pass = config.password!;
43
+ const logger = console;
44
+ ```
31
45
 
32
- const connection = createAbapConnection(config, logger, undefined, undefined, {
33
- system: 'onprem', // or 'cloud' — said by you, never detected
34
- });
46
+ ```ts
47
+ import {
48
+ AdtOnPremConnector,
49
+ BasicAuthProvider,
50
+ OnPremHttpTransport,
51
+ getTimeout,
52
+ } from '@mcp-abap-adt/connection';
53
+
54
+ const connection = new AdtOnPremConnector(
55
+ config,
56
+ new BasicAuthProvider(config.username!, config.password!),
57
+ new OnPremHttpTransport(() => ({}), logger, {
58
+ client: config.client,
59
+ baseUrl: config.url,
60
+ }),
61
+ logger,
62
+ );
35
63
  await connection.connect(); // required before any request
36
64
 
37
65
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
38
66
  connection.setSessionType('stateful');
39
67
 
40
68
  // Now all requests share the same session (cookies, CSRF token)
41
- await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
69
+ await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
42
70
 
43
71
  // Switch back to stateless
44
72
  connection.setSessionType('stateless');
@@ -49,11 +77,11 @@ connection.setSessionType('stateless');
49
77
  ## Knowing Which Session You Are In
50
78
 
51
79
  ```ts
52
- import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
80
+ import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
53
81
 
54
82
  // getSessionIdentity() is on the HTTP connection classes, NOT on the
55
- // IAbapConnection type that createAbapConnection() returns.
56
- const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
83
+ // bare IAbapConnection type a caller may hand you.
84
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), new OnPremHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }), logger);
57
85
  await connection.connect();
58
86
 
59
87
  // Which SAP session this connection is talking to. Changes only when the
@@ -84,11 +112,13 @@ Every ADT request issued through `makeAdtRequest` automatically:
84
112
 
85
113
  This logic is transparent to callers (Builders, handlers, CLI scripts).
86
114
 
87
- On a JWT connection one case is **not** transparent, and cannot be: a 401 that leads to a token
88
- refresh also replaces the SAP session, because the renewed credential cannot keep the old one.
89
- Inside a lock window that surfaces as `ADT_SESSION_REPLACED` rather than a request quietly
90
- continuing on a session your lock is not in. A 403 never does this — it is an authorization
91
- answer, not a credential one, and nothing is torn down for it.
115
+ On a JWT connection a 401 is **not** handled here at all, and that is the point: since 6.0.0 the
116
+ refusal surfaces and the session is left alone. Nothing replaces the credential behind you, so
117
+ nothing replaces the SAP session behind you either — a lock window is not torn down by an
118
+ authentication answer. A 403 never did this: it is an authorization answer, not a credential one.
119
+
120
+ If you decide the refusal meant a stale token, `renew()` and reconnect are yours to call — and a
121
+ reconnect is a NEW session, so do it outside a lock window rather than inside one.
92
122
 
93
123
  ---
94
124
 
@@ -123,6 +153,44 @@ made with the cookies and the answers in hand.
123
153
 
124
154
  ## A Lock Lives In The ABAP Session
125
155
 
156
+ ### Three lifetimes, and a dropped connection ends none of them
157
+
158
+ Before anything else, because conflating these is the mistake that produces
159
+ wrong fixes:
160
+
161
+ | | what it is | what ends it |
162
+ |---|---|---|
163
+ | **the connection** | one TCP socket | you, the network, a timeout firing. **Ends nothing on the server** |
164
+ | **the HTTP session** | the ICF conversation, held by the cookie jar on this side | dropping the cookies, or a logoff. Nothing about it is in doubt |
165
+ | **the ABAP session** | the server-side context named by `SAP_SESSIONID_<SID>_<CLIENT>` — the roll area a stateful request runs in | **only the server.** Its own idle timeout, which this side can neither read nor influence, or an explicit logoff |
166
+ | **the lock** | an enqueue entry, **owned by the ABAP session** | that ABAP session ending. **When it goes, its locks go with it** |
167
+
168
+ The two middle rows are the ones worth keeping apart, because everything that
169
+ goes wrong here goes wrong between them. The lock does not belong to the socket
170
+ and does not belong to the cookies — it belongs to the **ABAP session**, and
171
+ that session is the server's. This side never ends one: it can ask (a logoff),
172
+ and otherwise waits for a timeout it cannot see.
173
+
174
+ A closed socket is just a client that stopped listening. The ABAP session
175
+ neither knows nor cares — which is why `SM04` shows sessions left behind by
176
+ connections that never said they were finished, and why this package sends an
177
+ explicit goodbye at all.
178
+
179
+ And **a lock is never stranded by something dying** — the opposite. If the ABAP
180
+ session ended, the enqueue entry went with it and there is nothing left to
181
+ strand. A lock is stranded when that session **survives** while the caller no
182
+ longer holds the handle needed to unlock it: both sit there, held, until the
183
+ server's own timeout releases them together.
184
+
185
+ This is what makes an aborted request expensive, and it is not what it looks
186
+ like. Aborting costs you **knowledge**, not a session: whether the modification
187
+ was applied becomes unknowable, and the handle `unlock` needs is gone, while the
188
+ lock is still very much held. That — not any teardown — is why
189
+ `beginCriticalSection()` raises the effective timeout to a large ceiling for the
190
+ duration of a `lock → modify → unlock` chain.
191
+
192
+ ### Two sessions, and they are not the same thing
193
+
126
194
  There are two sessions here, and they are not the same thing:
127
195
 
128
196
  - the **HTTP session** — the conversation this client is having. It exists as
@@ -173,17 +241,18 @@ you get by asking — `getSessionIdentity()` for which session you are in, and
173
241
  `disconnect()` followed by `connect()` — there is no local-only discard, because
174
242
  dropping the cookie leaves the ABAP session open on the server. Both live on
175
243
  the HTTP connection classes and are **not** on the `IAbapConnection` type
176
- `createAbapConnection()` returns — reach them through a concrete type:
244
+ a bare `IAbapConnection` carries — reach them through a connector type, or
245
+ through the `ISessionLifecycleAware` atom:
177
246
 
178
247
  ```ts
179
- import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
248
+ import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
180
249
 
181
- const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
250
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), new OnPremHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }), logger);
182
251
  await connection.disconnect(); // ends the session on the server, then clears
183
252
  await connection.connect(); // a new session, explicitly
184
253
  ```
185
254
  - **Session expired**: reauthenticate to obtain a new session.
186
- - **Multiple connections**: each `createAbapConnection` instance maintains its own cookie jar; share the instance if you need continuity.
255
+ - **Multiple connections**: each connector holds its own wire, and the wire holds the cookie jar; share the instance if you need continuity.
187
256
 
188
257
  ---
189
258