@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.
- package/CHANGELOG.md +407 -1
- package/README.md +239 -53
- package/dist/auth/providers.d.ts +124 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +183 -0
- package/dist/connection/AbstractAbapConnection.d.ts +209 -57
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +459 -457
- package/dist/connection/AdtCloudConnector.d.ts +34 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +27 -0
- package/dist/connection/AdtOnPremConnector.d.ts +43 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +28 -0
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/connection/CloudHttpTransport.js +145 -0
- package/dist/connection/CredentialAbapConnection.d.ts +55 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +128 -0
- package/dist/connection/HttpTransport.d.ts +178 -0
- package/dist/connection/HttpTransport.d.ts.map +1 -0
- package/dist/connection/HttpTransport.js +402 -0
- package/dist/connection/IAdtTransport.d.ts +232 -0
- package/dist/connection/IAdtTransport.d.ts.map +1 -0
- package/dist/connection/IAdtTransport.js +28 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
- package/dist/connection/OnPremHttpTransport.d.ts +45 -0
- package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/OnPremHttpTransport.js +91 -0
- package/dist/connection/RfcTransport.d.ts +89 -0
- package/dist/connection/RfcTransport.d.ts.map +1 -0
- package/dist/connection/RfcTransport.js +256 -0
- package/dist/connection/rfcConversation.d.ts +44 -0
- package/dist/connection/rfcConversation.d.ts.map +1 -0
- package/dist/connection/rfcConversation.js +71 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -18
- package/dist/session/SessionLifecycle.d.ts +17 -2
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +17 -2
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +24 -0
- package/dist/utils/timeouts.d.ts +6 -3
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -3
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -6
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
- package/docs/USAGE.md +322 -111
- package/examples/basic-connection.js +15 -3
- package/examples/jwt-with-token-refresh.js +15 -7
- package/examples/saml-connection.js +15 -2
- package/package.json +12 -10
- package/dist/__tests__/helpers/session.d.ts +0 -15
- package/dist/__tests__/helpers/session.d.ts.map +0 -1
- package/dist/__tests__/helpers/session.js +0 -19
- package/dist/connection/BaseAbapConnection.d.ts +0 -23
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -75
- package/dist/connection/CertificateAbapConnection.d.ts +0 -25
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -79
- package/dist/connection/JwtAbapConnection.d.ts +0 -115
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -358
- package/dist/connection/KerberosAbapConnection.d.ts +0 -24
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -120
- package/dist/connection/RfcAbapConnection.d.ts +0 -49
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -331
- package/dist/connection/SamlAbapConnection.d.ts +0 -25
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -75
- package/dist/connection/connectionFactory.d.ts +0 -9
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- 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/
|
|
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
|