@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/CHANGELOG.md CHANGED
@@ -7,6 +7,410 @@ 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
+
142
+ ## [5.0.0] - 2026-08-21
143
+
144
+ A connection now says which system it is, closes what it opens, and is handed its
145
+ credential instead of being one. See [Migration to 5.0](./docs/MIGRATION-5.0.md).
146
+
147
+ ### Added
148
+
149
+ - **`AdtOnPremConnector` and `AdtCloudConnector`.** The class you take states which SYSTEM you
150
+ are dialling; the auth provider states how you authenticate there. The two are independent, and
151
+ both were reachable combinations that the old shape got wrong: a communication user against
152
+ ABAP Cloud, and a bearer token against an on-prem system.
153
+
154
+ Nothing is detected. `/sap/bc/adt/core/http/sessions` answers on **on-prem too**, publishing
155
+ both the session resource and the ICF logoff in one document, and its `DELETE` there leaves the
156
+ session listed while the logoff removes it — a probe would have chosen the mechanism that
157
+ releases nothing.
158
+
159
+ - **Auth providers** — `BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`,
160
+ `CertificateAuthProvider` — and `IAuthProvider`, the contract they satisfy. A token provider
161
+ renews on its own, so nothing is cached here: the header is asked for per request, and on a
162
+ `401` the provider is told its answer was refused (`refreshToken()`) before being asked again.
163
+
164
+ - **`createAbapConnection(..., { system })`** builds the connector you name, with a provider from
165
+ the config. Without it, the old choice by `authType`, warned about once per call.
166
+
167
+ - **`disconnect({ deadlineMs })`** — the parameter `ISessionLifecycleAware` published and nothing
168
+ implemented. It bounds the **wait**, never the request: handed to axios it would abort the
169
+ socket and cancel the release it was waiting for.
170
+
171
+ ### Changed — BREAKING
172
+
173
+ - **`connect()` fails when the server opened no session**, where it used to warn and hand the
174
+ connection back. A lock is held by the ABAP session, so a connection without one can read but
175
+ can hold nothing, and the failure surfaced a request later as `400 Session not found` with the
176
+ object half-edited. Verified rather than inferred: a connection that received no
177
+ `SAP_SESSIONID` is listed in the server's session list as nothing at all.
178
+
179
+ - **`disconnect()` makes a network call**, telling the server the session is finished. It does
180
+ not wait by default — `SAP_RELEASE_DEADLINE_MS` is `0` — because waiting is for steps whose
181
+ successor needs the server to have caught up, and a teardown has none. Requests still in flight
182
+ are running on the session being released and will start failing; that is the caller having
183
+ asked to disconnect.
184
+
185
+ - **The five auth connection classes are deprecated** and keep working.
186
+
187
+ ### Removed — BREAKING
188
+
189
+ - **`reset()`**, from `AbstractAbapConnection` and `RfcAbapConnection`. There is no local-only
190
+ discard because there is no local-only session: it lives on the server, and dropping the cookie
191
+ leaves it there. `await disconnect()`, then `connect()` again.
192
+
193
+ ### Fixed
194
+
195
+ - **Requests stay on the server the session lives on.** A session belongs to one application
196
+ server, so on a multi-node system a request landing elsewhere gets a different session and any
197
+ lock held on the first dies — no inactivity, nobody at fault. `sap-adt-saplb` is asked for and
198
+ sent back, as Eclipse does.
199
+
200
+ - **A late `401` no longer tears down a healthy session.** The comparison is bound to the session
201
+ the request went out on, so a refusal answered by a session that has since been replaced is
202
+ retried rather than acted on.
203
+
204
+ - **A credential that is cookies reaches the wire.** A SAML provider's cookies are part of the
205
+ contract and merged with the session jar rather than written over by it — one says who we are,
206
+ the other which session we are in.
207
+
208
+ - **A session opened before a failed `connect()` is not abandoned**, and the logoff does not cut
209
+ a lock chain held open with `beginCriticalSection()`.
210
+
211
+ ### Measured
212
+
213
+ - 25 connects in a row on-prem: **24-25** given a session with the logoff, **2** without.
214
+ - The server's session list shows the row appear at the second of the call, and go on
215
+ `disconnect()`; without one it would sit for 30 more minutes.
216
+ - The timeout is **idle-based**: 45 small requests a minute apart held one session straight
217
+ through a 30-minute window, identity unchanged. There is no keepalive timer here on purpose.
218
+ - Cloud: `AdtCloudConnector` with a token provider — session opened through ADT, three stateful
219
+ requests holding it, closed by `DELETE` on the address the server published.
220
+
221
+ ### Fixed
222
+
223
+ - **Every session of a reconnect cycle is released, not only the first.** A release already on
224
+ its way was treated as "the release still owed" whoever it belonged to, so an in-flight logoff
225
+ for a previous session suppressed the current one's entirely: `connect → disconnect → connect →
226
+ disconnect` sent **one** logoff and left the second session open. Not an edge case — the
227
+ default deadline is `0`, so `disconnect()` does not wait for the logoff and a release is
228
+ routinely still in flight when the next `connect()` happens, which made this the normal path on
229
+ any server that does not answer instantly. Releases are now keyed by the session they belong
230
+ to, and both completion handlers clear by that key, so a late answer about one session cannot
231
+ discard what is owed for another. Sessions still owed are kept as a set rather than one slot,
232
+ because more than one can be outstanding and the older was being overwritten.
233
+
234
+ ### Removed — BREAKING
235
+
236
+ - **`reset()` is gone**, from `AbstractAbapConnection` and `RfcAbapConnection`. There is no
237
+ local-only discard, because there is no local-only session: the session lives on the server,
238
+ and dropping the cookie leaves it there. The lifecycle is `connect()` / `disconnect()`,
239
+ repeatable, and both say what they do to the server.
240
+
241
+ It carried nothing `disconnect()` lacks — it cleared the fingerprint at the start of teardown
242
+ rather than at its end, did not join the transition tail, and returned `void`. That last one
243
+ is the point: a teardown that reports nothing cannot tell the caller whether the session was
244
+ released, which is the whole subject of this release.
245
+
246
+ No callers outside tests, and it is in neither `IAbapConnection` nor `ISessionLifecycleAware`,
247
+ so consumers programming against the published contracts are unaffected. `RfcAbapConnection`
248
+ keeps `close()`, which is its own teardown and always was.
249
+
250
+ **Migration:** `conn.reset()` → `await conn.disconnect()`, and `connect()` again to carry on —
251
+ the connection is reusable. A caller that does not want to wait simply does not `await` it,
252
+ which is what `reset()` was really used for.
253
+
254
+ ### Fixed
255
+
256
+ - **`disconnect()` tells the server the session is done, not only the client.** Dropping the cookie
257
+ left the ABAP session alive until its own timeout — default `http/security_session_timeout`,
258
+ 1800 s — so a process that connects repeatedly left one behind every time. Measured on S/4HANA
259
+ on-prem: 25 connects in a row with the logoff, 24–25 of them were given a session; without it,
260
+ 2. The server-side view is unambiguous — SM04 showed 25 HTTP sessions for the same user, one per
261
+ `connect()`, each holding ~12.8 MB, all of them opened by `P=/sap/bc/adt/discovery`, which is the
262
+ establishing call.
263
+
264
+ The logoff says the session is no longer needed; **when the server reclaims it is the
265
+ server's business** — possibly not until the next `connect()` asks for one — and nothing
266
+ here waits on that or depends on it. So `disconnect()` **does not wait by default**:
267
+ waiting is for steps whose successor needs the server to have caught up, and a teardown
268
+ has no successor. A caller that wants a bounded wait passes it —
269
+ `disconnect({ deadlineMs })`, the parameter `ISessionLifecycleAware` has published all
270
+ along and which nothing implemented; the default comes from `SAP_RELEASE_DEADLINE_MS`,
271
+ which is `0`.
272
+
273
+ **The deadline bounds the wait, never the request.** When it expires the waiting stops and
274
+ the logoff carries on to the server — the contract's word is *detach*. Handed to axios as a
275
+ request timeout instead, it would abort the socket and cancel the very release it was waiting
276
+ for: `deadlineMs: 200` against a server answering in 500 ms left the session open, and the
277
+ default of `0` was more reliable than any small positive value.
278
+
279
+ **Each caller waits its own deadline.** Concurrent disconnects join one transition and share
280
+ its promise, so a wait placed inside it was the first caller's wait imposed on everyone — a
281
+ caller passing `0` sat through another's 30-second budget, the one guarantee the parameter
282
+ exists to make. The transition now carries only what must happen once: dispatching the logoff
283
+ and clearing the local state.
284
+
285
+ **A repeat call finishes what is still owed**, as `ISessionLifecycleAware` promises. It could
286
+ not: `clearSessionState()` drops the cookies, so a second call found nothing to send and the
287
+ session lived out its 1800 s. The cookies of an incomplete release are kept aside for exactly
288
+ that retry and dropped as soon as one succeeds; a release already on its way is joined rather
289
+ than duplicated.
290
+
291
+ **The logoff does not cut a lock chain in flight.** It ends the session that chain is running
292
+ on, so a consumer's `finally` firing on shutdown mid-unlock would leave the object locked and
293
+ inactive — the damage this release exists to prevent, caused by the release itself.
294
+ `beginCriticalSection()` is honoured here as it already is for timeouts: the local teardown
295
+ still happens, the session is recorded as still owed, and calling `disconnect()` again once
296
+ the chain has finished releases it.
297
+
298
+ - **A malformed `SAP_RELEASE_DEADLINE_MS` is refused at construction, not at teardown.** It
299
+ reached `parseInt`, came out `NaN`, and threw from **every** `disconnect()` in the process —
300
+ blaming a `deadlineMs` argument nobody had passed. It is a startup fault: the same on every
301
+ call, not the caller's argument, and worth refusing a connection over. `Number()` rather than
302
+ `parseInt()`, which read `"5s"` as `5` and travelled on as a silently wrong bound.
303
+
304
+ And `disconnect()` no longer throws at all, which is what it and the interface both promise.
305
+ Its place is a `finally` — a connection that was connected must be disconnected — and an
306
+ exception raised there replaces the error that sent the caller into it. A nonsense per-call
307
+ `deadlineMs` is reported and the default used instead.
308
+
309
+ It surfaces as anything but a session problem: once the server stops issuing sessions it still
310
+ authenticates every request, so stateless reads and writes keep working and only the
311
+ lock-bound write fails — `200` for the LOCK, a handle, then `400 Session not found` on the next
312
+ request and a half-edited object.
313
+
314
+ ICF rather than ADT because ADT publishes no session-close: its discovery document lists none on
315
+ any reachable system — on-prem, cloud, or legacy — and the ADT logon is the discovery call
316
+ itself. Best effort and never throwing: `disconnect()` must always settle, and a session we
317
+ could not close beats a teardown that hangs.
318
+
319
+ How many sessions a system tolerates is the server's business and is not guessed at here. Using
320
+ few connections, and reusing them, stays the consumer's decision.
321
+
322
+ - **A connection the server gave no session now warns.** `sessionFingerprint()` tracks
323
+ `SAP_SESSIONID*` only, so a server that issued none leaves it empty — and an empty fingerprint
324
+ can never be classified `replaced`: `observe()` returns `established` or `unchanged` forever,
325
+ `applyIdentityPolicy()` never fires, `getSessionIdentity()` names nothing. Refusing to connect
326
+ would be the honest answer and is deliberately not done yet: whether cloud ABAP issues this
327
+ cookie is unverified, and a rule that wrong would break every cloud consumer to fix an on-prem
328
+ fault.
329
+
330
+ ### Documentation
331
+
332
+ - **The guides stop recommending `reset()`**, which this release removes. `USAGE.md` had a
333
+ runnable `connection.reset()` under *Connection Reset*, `STATEFUL_SESSION_GUIDE.md` offered it
334
+ as the remedy for CSRF errors, and `MIGRATION-2.0.md` described its teardown — anyone following
335
+ them got `connection.reset is not a function`. They now say what replaces it and why: starting
336
+ over means telling the server, and dropping a cookie does not.
337
+ - A doc block left dangling by the same removal had `close()` in `RfcAbapConnection` documented as
338
+ "Reset the connection … Provides interface compatibility with HTTP connections" — an API that no
339
+ longer exists.
340
+
341
+ ### Tests
342
+
343
+ - The stub in `sessionComposition.test.ts` answered every route instantly, `/sap/bc/adt/slow`
344
+ included, so *does not wait for an in-flight request* held whenever `disconnect()` performed no
345
+ I/O rather than because the teardown declined to wait. That route now takes 300 ms and the test
346
+ asserts what its name says.
347
+ - `sessionTeardown.test.ts` covers the teardown contract from the caller's side: the logoff goes
348
+ out with the session cookies and without a request timeout at any budget; it is detached rather
349
+ than aborted when a deadline expires; a failing logoff still disconnects; a repeat call re-sends
350
+ what is owed and sends nothing once it succeeded; two concurrent disconnects share one logoff
351
+ and keep separate deadlines; a critical section defers it; and a malformed
352
+ `SAP_RELEASE_DEADLINE_MS` refuses construction.
353
+
354
+ ### Fixed
355
+
356
+ - **`disconnect()` releases the session this connection holds, and nothing else.** What grew
357
+ around that sentence — a map of owed sessions, a map of releases in flight, an attempt counter,
358
+ a give-up rule, a retry across reconnects, and the waiting rules to go with them — is gone. Four
359
+ review rounds found a defect in each round's own fix, every one of them in that machinery, and
360
+ none of it was needed: **a connection holds one session**. `connect()` opens it, `disconnect()`
361
+ closes it, a repeat `connect()` is a NEW session with a new `SAP_SESSIONID`, and an earlier
362
+ session is not this connection's business — its logoff is already on the wire, or the system
363
+ times it out.
364
+
365
+ Nothing retries, counts, limits or keeps a list. How many connections to run, how frugally, and
366
+ what to do when a release did not land are the caller's, and were never knowable from inside a
367
+ single connection.
368
+
369
+ The session a release belongs to is now its `SAP_SESSIONID`, not the cookie header it is sent
370
+ with. The header also carries `sap-XSRF_*`, which rotates within one and the same session, so
371
+ comparing headers made a session stop recognising itself after a token refresh.
372
+
373
+ - **A logoff that cannot even be assembled no longer escapes the teardown.** Building it can throw
374
+ on its own — a certificate connection whose material is not loaded throws while building the
375
+ agent — and `disconnect()` is documented never to throw and is called from a `finally`, where a
376
+ throw replaces the error that sent the caller there.
377
+
378
+ ### Documentation
379
+
380
+ - **The cookies are the session, and a logoff ends it for everyone holding them.** A second
381
+ connection given the same cookie jar works in the same ABAP session and can use the locks taken
382
+ in it; `disconnect()` closes that session for all of them, and no connection can see the copies.
383
+ Written down in `STATEFUL_SESSION_GUIDE.md` and on `disconnect()` itself.
384
+
385
+ ### Changed — BREAKING
386
+
387
+ - **`connect()` fails when the server opened no session**, instead of warning and handing back a
388
+ connection whose first lock would be dead on arrival. Locks are held by the ABAP session, so a
389
+ connection without one can read but can hold nothing; the failure used to surface a request
390
+ later, as `400 Session not found` with the object half-edited.
391
+
392
+ Verified rather than inferred: a connection that received no `SAP_SESSIONID` was held open
393
+ against an on-prem system and the session list showed **nothing** for it, while one that
394
+ received the cookie appeared there. No cookie, no session.
395
+
396
+ Reported, not decided on. The message says what the server did, what still works, what does
397
+ not, the usual cause — sessions are limited per user and shared with every other tool logged on
398
+ as them — and that nothing is retried here, because whether to wait, retry, or release sessions
399
+ the user still holds depends on what only the caller knows.
400
+
401
+ Every transport, not only basic: splitting by authentication type would encode a guess about
402
+ cloud ABAP, whose ADT endpoint would not answer the bearer obtainable here. If a cloud system
403
+ turns out to hold sessions without issuing this cookie, this is the rule to revisit.
404
+
405
+ ### Documentation
406
+
407
+ - **`STATEFUL_SESSION_GUIDE.md` gains "A Lock Lives In The Session".** That a lock dies with the
408
+ session that took it; that the timeout is an idle one, spent by silence rather than by elapsed
409
+ time — one small request a minute kept a session alive for 45 minutes past a 30-minute window,
410
+ identity unchanged; and that any request in the session resets it, which is why this package
411
+ holds no keepalive timer. Holding a session alive holds a scarce shared slot, and that is the
412
+ caller's decision to make.
413
+
10
414
  ## [4.0.0] - 2026-08-16
11
415
 
12
416
  A JWT connection stops answering with an error of its own making. See
@@ -848,7 +1252,9 @@ const connection = createAbapConnection(config, logger);
848
1252
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
849
1253
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
850
1254
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
851
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v4.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
1257
+ [5.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v4.0.0...v5.0.0
852
1258
  [4.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v3.0.0...v4.0.0
853
1259
  [3.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v2.0.0...v3.0.0
854
1260
  [2.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v1.10.2...v2.0.0