@mcp-abap-adt/connection 3.0.0 → 5.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 (73) hide show
  1. package/CHANGELOG.md +337 -1
  2. package/README.md +74 -31
  3. package/dist/auth/IAuthProvider.d.ts +84 -0
  4. package/dist/auth/IAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/IAuthProvider.js +21 -0
  6. package/dist/auth/providers.d.ts +95 -0
  7. package/dist/auth/providers.d.ts.map +1 -0
  8. package/dist/auth/providers.js +140 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +222 -17
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +555 -31
  12. package/dist/connection/AdtCloudConnector.d.ts +29 -0
  13. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtCloudConnector.js +31 -0
  15. package/dist/connection/AdtOnPremConnector.d.ts +30 -0
  16. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  17. package/dist/connection/AdtOnPremConnector.js +32 -0
  18. package/dist/connection/BaseAbapConnection.d.ts +7 -1
  19. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/BaseAbapConnection.js +8 -2
  21. package/dist/connection/CertificateAbapConnection.d.ts +11 -1
  22. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/CertificateAbapConnection.js +14 -2
  24. package/dist/connection/CredentialAbapConnection.d.ts +88 -0
  25. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  26. package/dist/connection/CredentialAbapConnection.js +195 -0
  27. package/dist/connection/JwtAbapConnection.d.ts +102 -9
  28. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  29. package/dist/connection/JwtAbapConnection.js +266 -82
  30. package/dist/connection/KerberosAbapConnection.d.ts +9 -1
  31. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  32. package/dist/connection/KerberosAbapConnection.js +9 -1
  33. package/dist/connection/RfcAbapConnection.d.ts +0 -5
  34. package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
  35. package/dist/connection/RfcAbapConnection.js +0 -7
  36. package/dist/connection/SamlAbapConnection.d.ts +7 -1
  37. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  38. package/dist/connection/SamlAbapConnection.js +8 -2
  39. package/dist/connection/connectionFactory.d.ts +16 -0
  40. package/dist/connection/connectionFactory.d.ts.map +1 -1
  41. package/dist/connection/connectionFactory.js +52 -0
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +10 -1
  45. package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
  46. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
  47. package/dist/session/CloudSecuritySessionStrategy.js +135 -0
  48. package/dist/session/IcfSessionStrategy.d.ts +27 -0
  49. package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
  50. package/dist/session/IcfSessionStrategy.js +62 -0
  51. package/dist/session/SessionLifecycle.d.ts +17 -2
  52. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  53. package/dist/session/SessionLifecycle.js +17 -2
  54. package/dist/session/SessionStrategy.d.ts +86 -0
  55. package/dist/session/SessionStrategy.d.ts.map +1 -0
  56. package/dist/session/SessionStrategy.js +33 -0
  57. package/dist/utils/cookies.d.ts +12 -0
  58. package/dist/utils/cookies.d.ts.map +1 -0
  59. package/dist/utils/cookies.js +24 -0
  60. package/dist/utils/timeouts.d.ts +16 -0
  61. package/dist/utils/timeouts.d.ts.map +1 -1
  62. package/dist/utils/timeouts.js +19 -0
  63. package/docs/INDEX.md +5 -0
  64. package/docs/INSTALLATION.md +6 -2
  65. package/docs/MIGRATION-2.0.md +1 -1
  66. package/docs/MIGRATION-4.0.md +95 -0
  67. package/docs/MIGRATION-5.0.md +116 -0
  68. package/docs/STATEFUL_SESSION_GUIDE.md +83 -11
  69. package/docs/USAGE.md +115 -24
  70. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
  71. package/examples/README.md +2 -1
  72. package/examples/jwt-with-token-refresh.js +11 -3
  73. package/package.json +1 -1
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * How a session is opened on the server, and how the server is told we are
4
+ * done with it.
5
+ *
6
+ * Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
7
+ * resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
8
+ * response publishes its own address, which is what a `DELETE` is sent to. On
9
+ * on-prem there is no such resource — the logon call is the establishing
10
+ * request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
11
+ * given back.
12
+ *
13
+ * **Both are notifications, not commands.** They say "we have finished with
14
+ * this session". Whether the system frees it now, later, or keeps it to reuse
15
+ * is the system's business, and nothing here checks afterwards or depends on
16
+ * the answer. Measured on a trial: the `DELETE` is answered `200` and the
17
+ * session is still listed a moment later; the logoff is answered `200` and it
18
+ * is not. Neither is a failure — they are two systems deciding differently
19
+ * about a message they both accepted.
20
+ *
21
+ * Split by what the server publishes rather than by which system we think we
22
+ * are talking to: `openSession()` asks, and a system that has no such resource
23
+ * says so. Guessing the platform would be a guess; asking is not.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SessionStrategy = void 0;
27
+ class SessionStrategy {
28
+ logger;
29
+ constructor(logger) {
30
+ this.logger = logger;
31
+ }
32
+ }
33
+ exports.SessionStrategy = SessionStrategy;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Join two `Cookie` header values, later wins on a repeated name.
3
+ *
4
+ * Used wherever a request already carries cookies and more are added: a
5
+ * credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
6
+ * so writing the session jar over the header drops the very thing the request
7
+ * authenticates with. Both belong on the wire: one says who we are, the other
8
+ * which session we are in. A name in both is the session's, because that is the
9
+ * one the server just issued.
10
+ */
11
+ export declare function mergeCookieHeaders(existing: string | undefined, incoming: string | undefined): string;
12
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/utils/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CASR"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeCookieHeaders = mergeCookieHeaders;
4
+ /**
5
+ * Join two `Cookie` header values, later wins on a repeated name.
6
+ *
7
+ * Used wherever a request already carries cookies and more are added: a
8
+ * credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
9
+ * so writing the session jar over the header drops the very thing the request
10
+ * authenticates with. Both belong on the wire: one says who we are, the other
11
+ * which session we are in. A name in both is the session's, because that is the
12
+ * one the server just issued.
13
+ */
14
+ function mergeCookieHeaders(existing, incoming) {
15
+ const parts = [existing, incoming].filter(Boolean).join('; ');
16
+ const byName = new Map();
17
+ for (const part of parts.split(';')) {
18
+ const pair = part.trim();
19
+ if (!pair)
20
+ continue;
21
+ byName.set(pair.slice(0, pair.indexOf('=')), pair);
22
+ }
23
+ return [...byName.values()].join('; ');
24
+ }
@@ -12,4 +12,20 @@ export declare function getTimeout(type?: 'default' | 'csrf' | 'long' | number):
12
12
  * permanently dead socket hanging forever.
13
13
  */
14
14
  export declare function getCriticalSectionTimeout(): number;
15
+ /**
16
+ * How long `disconnect()` may spend telling the server the session is done.
17
+ *
18
+ * **Zero by default: after a disconnect nothing waits on the answer.** Waiting
19
+ * is for steps whose successor depends on the server having caught up — lock,
20
+ * update, unlock, activate, each needing the one before it to have landed.
21
+ * A teardown has no successor: the session is marked unneeded, the server
22
+ * reclaims it whenever it reclaims it, and a caller blocked on that round trip
23
+ * has bought nothing while holding up the serializing tail, where every later
24
+ * connect and disconnect queues behind it.
25
+ *
26
+ * The knob exists for a caller that wants a bounded wait anyway — a test
27
+ * asserting the logoff landed, a script that would rather see the failure —
28
+ * and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
29
+ */
30
+ export declare function getReleaseDeadline(): number;
15
31
  //# sourceMappingURL=timeouts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
1
+ {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getTimeoutConfig = getTimeoutConfig;
4
4
  exports.getTimeout = getTimeout;
5
5
  exports.getCriticalSectionTimeout = getCriticalSectionTimeout;
6
+ exports.getReleaseDeadline = getReleaseDeadline;
6
7
  function getTimeoutConfig() {
7
8
  const defaultTimeout = parseInt(process.env.SAP_TIMEOUT_DEFAULT || '45000', 10);
8
9
  const csrfTimeout = parseInt(process.env.SAP_TIMEOUT_CSRF || '15000', 10);
@@ -32,3 +33,21 @@ function getTimeout(type = 'default') {
32
33
  function getCriticalSectionTimeout() {
33
34
  return parseInt(process.env.SAP_TIMEOUT_CRITICAL || '600000', 10);
34
35
  }
36
+ /**
37
+ * How long `disconnect()` may spend telling the server the session is done.
38
+ *
39
+ * **Zero by default: after a disconnect nothing waits on the answer.** Waiting
40
+ * is for steps whose successor depends on the server having caught up — lock,
41
+ * update, unlock, activate, each needing the one before it to have landed.
42
+ * A teardown has no successor: the session is marked unneeded, the server
43
+ * reclaims it whenever it reclaims it, and a caller blocked on that round trip
44
+ * has bought nothing while holding up the serializing tail, where every later
45
+ * connect and disconnect queues behind it.
46
+ *
47
+ * The knob exists for a caller that wants a bounded wait anyway — a test
48
+ * asserting the logoff landed, a script that would rather see the failure —
49
+ * and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
50
+ */
51
+ function getReleaseDeadline() {
52
+ return parseInt(process.env.SAP_RELEASE_DEADLINE_MS || '0', 10);
53
+ }
package/docs/INDEX.md CHANGED
@@ -15,6 +15,7 @@ mcp-abap-connection/
15
15
  │ ├── INSTALLATION.md # Setup and installation guide
16
16
  │ ├── USAGE.md # API documentation and examples
17
17
  │ ├── MIGRATION-2.0.md # Moving to the explicit session lifecycle
18
+ │ ├── MIGRATION-4.0.md # JWT error classification: 401 refreshes, 403 propagates
18
19
  │ ├── SCOPE.md # What this package does and does not own
19
20
  │ ├── STATEFUL_SESSION_GUIDE.md # Stateful requests and lock windows
20
21
  │ └── JWT_AUTH_TOOLS.md # CLI tool for authentication
@@ -41,6 +42,10 @@ mcp-abap-connection/
41
42
  ### Core Features
42
43
  - 🔑 [JWT Auth Tools](./JWT_AUTH_TOOLS.md) - CLI tool for browser-based authentication
43
44
 
45
+ ### Upgrading
46
+ - 🧱 [Migrating to 4.0.0](./MIGRATION-4.0.md) - JWT error classification: a 401 refreshes, a 403 propagates with the server's message
47
+ - 🧱 [Migrating to 2.0.0](./MIGRATION-2.0.md) - The explicit session lifecycle: `connect()` is required
48
+
44
49
  ### Version Information
45
50
  - 📋 [CHANGELOG](../CHANGELOG.md) - Complete version history, including what the latest release changed
46
51
 
@@ -163,7 +163,9 @@ const logger = {
163
163
  debug: (msg) => console.log('[DEBUG]', msg),
164
164
  };
165
165
 
166
- const connection = createAbapConnection(config, logger);
166
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
167
+ system: 'onprem', // or 'cloud' — said by you, never detected
168
+ });
167
169
 
168
170
  connection.connect()
169
171
  .then(() =>
@@ -231,7 +233,9 @@ const logger: ILogger = {
231
233
  debug: (msg: string) => console.log(msg),
232
234
  };
233
235
 
234
- const connection = createAbapConnection(config, logger);
236
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
237
+ system: 'onprem', // or 'cloud' — said by you, never detected
238
+ });
235
239
  ```
236
240
 
237
241
  ## Troubleshooting
@@ -46,7 +46,7 @@ later, on the first request, as an unrelated-looking 401.
46
46
 
47
47
  ### 3. Requests are refused after a teardown
48
48
 
49
- `disconnect()` and `reset()` stop the connection from serving requests until the
49
+ `disconnect()` stops the connection from serving requests until the
50
50
  next `connect()`. An in-flight request is not cut off and not waited for either:
51
51
  it runs to completion, and its result is fenced so it cannot touch a session
52
52
  established since.
@@ -0,0 +1,95 @@
1
+ # Migrating to 4.0.0: the server's error reaches you
2
+
3
+ A JWT connection used to answer some failures with an error of its own making:
4
+
5
+ ```
6
+ Error: JWT token has expired. Please re-authenticate.
7
+ ```
8
+
9
+ It said that for a **401** and for a **403** alike, and it threw the original `AxiosError` away
10
+ along with the status and the body. Now the server's error reaches the caller unchanged, and only
11
+ a 401 is treated as a credential problem.
12
+
13
+ ## What breaks, and what to do
14
+
15
+ ### 1. That message is gone
16
+
17
+ If anything matches on it, it will stop matching.
18
+
19
+ ```typescript
20
+ // before
21
+ catch (error) {
22
+ if (error.message.includes('JWT token has expired')) {
23
+ await reauthenticate();
24
+ }
25
+ }
26
+
27
+ // after
28
+ catch (error) {
29
+ if (error.response?.status === 401) {
30
+ await reauthenticate();
31
+ }
32
+ }
33
+ ```
34
+
35
+ `error.response.status` and `error.response.data` are available now — they were not before,
36
+ because the replacement error carried neither.
37
+
38
+ **Why it changed.** The message was wrong about half the cases it was used for. A 403 is not an
39
+ expired token, and telling a caller to re-authenticate for one sends them to fix the single thing
40
+ that is not broken.
41
+
42
+ ### 2. A 403 no longer triggers a token refresh
43
+
44
+ It propagates as it arrived. If your code relied on a refresh happening after a 403 — for
45
+ instance to recover from an authorization gap that a *different* principal would not have — that
46
+ no longer happens, and it never worked for the reason you may have assumed: a refreshed token
47
+ belongs to the same caller and cannot change a permissions answer.
48
+
49
+ What a 403 usually carries is worth reading:
50
+
51
+ ```xml
52
+ <exc:exception>
53
+ <type id="ExceptionResourceNoAuthorization"/>
54
+ <message lang="EN">You are not authorized to make changes (authorization object S_DEVELOP)</message>
55
+ </exc:exception>
56
+ ```
57
+
58
+ The authorization object is named. That is actionable; "please re-authenticate" was not.
59
+
60
+ ### 3. A 401 still refreshes, and now says so when it does not help
61
+
62
+ The happy path is unchanged: a 401 with an injected `ITokenRefresher` fetches a new token,
63
+ re-establishes the session and retries. What changed is the unhappy one — when the retry comes
64
+ back 401 as well, the original error is rethrown and an `ERROR`-level log line explains that the
65
+ credential may genuinely need re-authentication.
66
+
67
+ ## What did not change
68
+
69
+ - `ITokenRefresher` and how it is injected;
70
+ - that a credential renewal replaces the SAP session — with a lock window open, a request still
71
+ fails with `ADT_SESSION_REPLACED` rather than continuing on a session your lock is not in (see
72
+ [STATEFUL_SESSION_GUIDE.md](./STATEFUL_SESSION_GUIDE.md));
73
+ - every other authentication type. `BaseAbapConnection`, `SamlAbapConnection`,
74
+ `CertificateAbapConnection` and `KerberosAbapConnection` never refreshed tokens and never
75
+ synthesised an error in place of the server's.
76
+
77
+ ## Under the hood, if you are debugging
78
+
79
+ Concurrent requests that hit the same expired token now share **one** renewal — one token fetch
80
+ and one session re-establishment between them — rather than each running its own teardown and
81
+ recovery in sequence. If you have log-line counts or timing expectations built around the old
82
+ behaviour, they will change; the observable contract does not.
83
+
84
+ ## Where this came from
85
+
86
+ Issue [#30](https://github.com/fr0ster/mcp-abap-connection/issues/30), found while probing ATC on
87
+ a cloud trial: creating a classic program was reported as an expired token on three consecutive
88
+ runs, while the same session was answering other requests with 200. It took bypassing the
89
+ connector on the same credential to see the 403 and the `S_DEVELOP` message underneath.
90
+
91
+ One question is deliberately left open and tracked in
92
+ [#32](https://github.com/fr0ster/mcp-abap-connection/issues/32): whether some BTP setup answers an
93
+ *invalid* token with 403 rather than 401. Nothing captured shows that it does, and preserving the
94
+ original error is what makes the assumption safe to be wrong about — a caller would then see the
95
+ server's own 403 instead of a misleading message.
@@ -0,0 +1,116 @@
1
+ # Migrating to 5.0.0
2
+
3
+ Two things changed, and the second is why the first was possible.
4
+
5
+ **A connection now closes what it opened.** `disconnect()` tells the server the
6
+ session is finished instead of only dropping the cookie, and `connect()` fails
7
+ when the server opened no session rather than handing back a connection whose
8
+ first lock would be dead.
9
+
10
+ **The class you take says which system you are dialling, not which credential
11
+ you hold.** `AdtOnPremConnector` and `AdtCloudConnector` are handed an auth
12
+ provider; the five auth classes still work and are deprecated.
13
+
14
+ ---
15
+
16
+ ## If you use `createAbapConnection()`
17
+
18
+ It still works, unchanged, and now warns once per call that the connection is
19
+ being chosen from `authType`. Say which system instead:
20
+
21
+ ```ts
22
+ // before
23
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher);
24
+
25
+ // after
26
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher, {
27
+ system: 'cloud', // or 'onprem'
28
+ });
29
+ ```
30
+
31
+ Nothing is detected. The two systems do not manage sessions the same way, and
32
+ asking the server which it is does not work: `/sap/bc/adt/core/http/sessions`
33
+ answers on on-prem too, and its `DELETE` there leaves the session open while the
34
+ platform logoff removes it. Only you know where you are pointing.
35
+
36
+ ## If you build a connection class directly
37
+
38
+ ```ts
39
+ // before // after
40
+ new BaseAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new BasicAuthProvider(user, pass), log)
41
+ new JwtAbapConnection(cfg, log, id, refr) new AdtCloudConnector(cfg, new TokenAuthProvider(refr), log)
42
+ new SamlAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new SamlAuthProvider(cookies), log)
43
+ new CertificateAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new CertificateAuthProvider(loader, cfg), log)
44
+ ```
45
+
46
+ `KerberosAbapConnection` has no provider yet — its SPNEGO exchange *is* the CSRF
47
+ fetch — so keep using the class. See #35.
48
+
49
+ The old classes are not removed and not scheduled for removal here.
50
+
51
+ ## `reset()` is gone — BREAKING
52
+
53
+ There is no local-only discard, because there is no local-only session: it lives
54
+ on the server, and dropping the cookie leaves it there.
55
+
56
+ ```ts
57
+ conn.reset(); // before
58
+ await conn.disconnect(); // after — and connect() again to carry on
59
+ ```
60
+
61
+ A caller that does not want to wait simply does not `await` it, which is what
62
+ `reset()` was mostly used for. `RfcAbapConnection` keeps `close()`.
63
+
64
+ ## `connect()` can now fail where it used to warn — BREAKING
65
+
66
+ When the server authenticates the request but opens no session, `connect()`
67
+ rejects with `ADT_NOT_CONNECTED` and a message saying what happened, what still
68
+ works, and the usual cause. It used to log a warning and hand the connection
69
+ back — and the failure then surfaced a request later, as `400 Session not found`
70
+ with the object half-edited.
71
+
72
+ Verified rather than assumed: a connection that received no `SAP_SESSIONID` is
73
+ listed in the server's own session list as **nothing at all**.
74
+
75
+ Nothing is retried on your behalf. Whether to wait, retry, or release sessions
76
+ the user still holds depends on things only you know.
77
+
78
+ ## `disconnect()` now makes a network call
79
+
80
+ It tells the server the session is finished. Two consequences:
81
+
82
+ - **It can wait.** By default it does not: `SAP_RELEASE_DEADLINE_MS` is `0`,
83
+ because waiting is for steps whose successor needs the server to have caught
84
+ up, and a teardown has none. Pass `disconnect({ deadlineMs })` if you want a
85
+ bounded wait — the deadline bounds the **wait**, never the request.
86
+ - **Requests still in flight will start failing.** The session they are running
87
+ on is the one being released. That is you having asked to disconnect, not a
88
+ race — finish or abandon your chains first.
89
+
90
+ It still never throws, including on a nonsense `deadlineMs`: its place is a
91
+ `finally`, where an exception would replace the error that sent you there.
92
+
93
+ ## Token renewal, if you use a provider
94
+
95
+ Nothing to do, but worth knowing what changed. The connector no longer caches
96
+ the token it was given. It asks the provider per request — cheap, because the
97
+ provider caches — and on a `401` it tells the provider its answer was refused
98
+ (`refreshToken()`, per that contract), asks again, and only if the answer
99
+ changed rebuilds the session and retries once. An unchanged answer means the
100
+ server refused those credentials, and the `401` reaches you.
101
+
102
+ So a password does the right thing with no configuration, and a bearer token is
103
+ renewed without a class of its own.
104
+
105
+ ## Sessions are a shared, per-user resource
106
+
107
+ Not an API change, a fact worth having. The limit is per user and the pool is
108
+ shared with everything else logged on as them, a SAP GUI session included. The
109
+ timeout is **idle-based**: a connection that keeps working keeps its session,
110
+ one that goes quiet past the window loses it. There is no keepalive timer here,
111
+ deliberately — how long to hold a session is yours to decide.
112
+
113
+ And the shape of the problem this release addresses, seen from the other end: a
114
+ process that opens a connection per check and never disconnects filled a
115
+ system's session list at one session every ten seconds. Releasing what you open
116
+ is half of it; opening one connection instead of one per check is the other.
@@ -29,7 +29,9 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
29
29
  ```ts
30
30
  import { createAbapConnection } from '@mcp-abap-adt/connection';
31
31
 
32
- const connection = createAbapConnection(config, logger);
32
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
33
+ system: 'onprem', // or 'cloud' — said by you, never detected
34
+ });
33
35
  await connection.connect(); // required before any request
34
36
 
35
37
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
@@ -47,11 +49,11 @@ connection.setSessionType('stateless');
47
49
  ## Knowing Which Session You Are In
48
50
 
49
51
  ```ts
50
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
52
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
51
53
 
52
54
  // getSessionIdentity() is on the HTTP connection classes, NOT on the
53
55
  // IAbapConnection type that createAbapConnection() returns.
54
- const connection = new BaseAbapConnection(config, logger);
56
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
55
57
  await connection.connect();
56
58
 
57
59
  // Which SAP session this connection is talking to. Changes only when the
@@ -82,6 +84,12 @@ Every ADT request issued through `makeAdtRequest` automatically:
82
84
 
83
85
  This logic is transparent to callers (Builders, handlers, CLI scripts).
84
86
 
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.
92
+
85
93
  ---
86
94
 
87
95
  ## Interaction With ADT Clients
@@ -96,19 +104,83 @@ This logic is transparent to callers (Builders, handlers, CLI scripts).
96
104
 
97
105
  ---
98
106
 
107
+ ## What This Layer Actually Solves
108
+
109
+ The link to the ABAP session is not guaranteed, and that is the whole problem:
110
+
111
+ - the ABAP session can be terminated by the system while this client still
112
+ believes it has one — the next lock-bound request answers
113
+ `400 "Session not found"`;
114
+ - or it is never created at all, because the system will not open another one
115
+ for this user right now.
116
+
117
+ Both are reported, neither is worked around. `connect()` fails with the reason
118
+ when no session was opened; a session lost afterwards surfaces as
119
+ `ADT_SESSION_REPLACED` rather than a request quietly continuing somewhere it
120
+ does not belong. What to do about either — wait, retry, reconnect, release
121
+ sessions the user still holds, carry on read-only — is the caller's decision,
122
+ made with the cookies and the answers in hand.
123
+
124
+ ## A Lock Lives In The ABAP Session
125
+
126
+ There are two sessions here, and they are not the same thing:
127
+
128
+ - the **HTTP session** — the conversation this client is having. It exists as
129
+ long as the cookies do, and nothing about it is in doubt;
130
+ - the **ABAP session** — named by `SAP_SESSIONID_<SID>_<CLIENT>`, and the thing
131
+ locks are bound to.
132
+
133
+ The doubt is only ever about the second. It may not have been created, or it may
134
+ have been terminated while this client still holds the cookies and believes it
135
+ has one — and a lock is dead in either case, whether or not the client noticed.
136
+
137
+ Two ways to lose one, and they are different problems:
138
+
139
+ - **The server never opened one.** No `SAP_SESSIONID` came back, so there is no
140
+ ABAP session known to this connection and nothing a lock could be bound to —
141
+ while the HTTP side is perfectly fine, which is why it does not look like a
142
+ failure at all. `connect()` refuses rather than handing back a connection
143
+ whose first lock would be dead on arrival. Sessions are limited per user and shared with every other tool
144
+ logged on as them, so this says nothing about your code — it says the system
145
+ would not open another one right now.
146
+ - **It timed out while you were quiet.** The timeout is an idle one, and it is
147
+ the *silence* that spends it, not the elapsed time. Measured on an on-prem
148
+ system with a 30-minute window: a small request once a minute kept one session
149
+ alive for 45 minutes with its identity unchanged, straight past the mark.
150
+
151
+ So a long chain under a lock is safe while it is doing something, and at risk
152
+ while it waits. **Any request in the session resets the window** — a poll, a
153
+ read, a status check. There is deliberately no keepalive timer in this package:
154
+ holding a session alive means holding a scarce, shared slot, and deciding to do
155
+ that belongs to the caller who knows why the session is worth keeping.
156
+
157
+ **The cookies are the session.** Nothing else ties a caller to one, so a second
158
+ connection given the same cookie jar works in the same ABAP session and can use
159
+ the locks taken in it — that is what makes handing them over a way to continue
160
+ someone else's work. It cuts both ways: `disconnect()` ends the session for
161
+ everyone holding those cookies, not just for the object it was called on, and no
162
+ connection can see the copies. Deciding who may hold them, and who is allowed to
163
+ close, belongs to whoever passes them around.
164
+
165
+ The server never tells the client how long it has: the session cookie carries no
166
+ expiry and no response header mentions one. The only honest signals are the ones
167
+ you get by asking — `getSessionIdentity()` for which session you are in, and
168
+ `ADT_SESSION_REPLACED` when the one you were in is gone.
169
+
99
170
  ## Troubleshooting
100
171
 
101
- - **CSRF token errors**: discard the session and establish a new one. `reset()`
102
- does that, but it lives on the HTTP connection classes and is **not** on the
103
- `IAbapConnection` type `createAbapConnection()` returns — reach it through a
104
- concrete type:
172
+ - **CSRF token errors**: discard the session and establish a new one. That is
173
+ `disconnect()` followed by `connect()` — there is no local-only discard, because
174
+ dropping the cookie leaves the ABAP session open on the server. Both live on
175
+ the HTTP connection classes and are **not** on the `IAbapConnection` type
176
+ `createAbapConnection()` returns — reach them through a concrete type:
105
177
 
106
178
  ```ts
107
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
179
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
108
180
 
109
- const connection = new BaseAbapConnection(config, logger);
110
- connection.reset(); // queues the cleanup; refuses requests meanwhile
111
- await connection.connect(); // a new session, explicitly
181
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
182
+ await connection.disconnect(); // ends the session on the server, then clears
183
+ await connection.connect(); // a new session, explicitly
112
184
  ```
113
185
  - **Session expired**: reauthenticate to obtain a new session.
114
186
  - **Multiple connections**: each `createAbapConnection` instance maintains its own cookie jar; share the instance if you need continuity.