@mcp-abap-adt/connection 9.3.0 → 9.3.3

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 CHANGED
@@ -7,6 +7,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.3.3] - 2026-09-27
11
+
12
+ ### Changed
13
+
14
+ - **Over RFC, stateless calls reuse a conversation and reset its server
15
+ context when supported.** `@mcp-abap-adt/sap-rfc-lite` 0.2.0 adds
16
+ `resetServerContext()` (`RfcResetServerContext`), so each call gets a clean
17
+ ABAP context without logging in again. Measured on E19, this took about
18
+ 0.24 s per call, compared with 0.67 s when opening a conversation each time.
19
+ Concurrent calls use a separate conversation so they cannot run between a
20
+ previous call and its reset. If reset fails, that conversation is closed.
21
+ With `sap-rfc-lite` 0.1.x, calls continue to use a new conversation each.
22
+ Create, read after create, two lock/write/unlock rounds and delete passed on
23
+ one E19 connection.
24
+
25
+ ## [9.3.2] - 2026-09-27
26
+
27
+ ### Fixed
28
+
29
+ - **Over RFC, a request that is not stateful runs on a conversation of its
30
+ own.** `RfcTransport` carried everything on one conversation, one ABAP
31
+ session, so whatever a program left in it stayed:
32
+ - after a package was created or written, every further write or delete of
33
+ it answered 400 PAK/058 "Package … is already locked" (`CL_PACKAGE`'s
34
+ instance buffer);
35
+ - a read straight after a create answered 400 SADT_RESOURCE/007.
36
+
37
+ Now the conversation `open()` makes carries the requests marked `stateful`
38
+ (the lock and its release), and every other request opens one of its own
39
+ and closes it when it answers. That is Eclipse ADT's split over JCo: one
40
+ stateful enqueue session, every other call in a session of its own.
41
+ Measured on E19, one connection: create, read, two lock/PUT/unlock rounds
42
+ and a delete all pass. The price is a logon per non-stateful call — about
43
+ 0.5 s on E19, against about 0.1 s for the call itself.
44
+
45
+ Stateful is decided the same way on both wires (`isStatefulRequest`): the
46
+ connection's flag, or a session header the caller wrote itself. And a call
47
+ whose own conversation is still logging on when the wire is closed sends
48
+ nothing: it closes that conversation and refuses, rather than delivering a
49
+ `PUT` or `DELETE` on a wire already given back.
50
+
51
+ ## [9.3.1] - 2026-09-27
52
+
53
+ ### Fixed
54
+
55
+ - **Over HTTP, a request that is not stateful no longer reaches the stateful
56
+ context.** `sap-contextid` names the context a `LOCK` opens, and SAP routes any
57
+ request that carries it into that context — session header or none. The wire
58
+ sent the whole jar on every request, so a "stateless" `PUT` after a `LOCK` ran
59
+ in the lock's context. Measured 2026-09-27 on one connection, a package locked,
60
+ written and unlocked twice:
61
+ - E19 (BASIS 816): the second `PUT` answered 400 PAK/058 "Package … is already
62
+ locked" — the first save was still in that context's `CL_PACKAGE` buffer, and
63
+ a delete on the same connection was refused the same way;
64
+ - E98 (BASIS 756): every `PUT` answered 423 "Resource … is not locked (invalid
65
+ lock handle)".
66
+
67
+ With the cookie kept to the stateful requests, both systems wrote twice and
68
+ deleted on the same connection. That is Eclipse ADT's split too: its trace
69
+ shows one stateful session for `LOCK` and `UNLOCK` alone and every other
70
+ request, the `PUT` included, outside it. The jar still holds the cookie, so
71
+ the `UNLOCK` that follows reaches the context holding the lock.
72
+
73
+ The cookie is filtered on the header as it finally goes out, not only on
74
+ the jar: the connection's CSRF and 401 retries write the whole jar into the
75
+ request's own `Cookie`, and a caller can spell that header in lower case.
76
+ Either way, a non-stateful request does not carry the context.
77
+
78
+ ### Changed
79
+
80
+ - **The session type is the wire's to express, not the connection's.**
81
+ `setSessionType('stateful')` now marks each request `stateful: true` on
82
+ `IAdtTransportRequest` instead of writing `x-sap-adt-sessiontype` into its
83
+ headers.
84
+ - `HttpTransport` adds `x-sap-adt-sessiontype: stateful` and `sap-contextid`
85
+ to stateful requests only (overridable through `sessionTypeHeaders()`;
86
+ `LegacyOnPremHttpTransport` sends no header, as before). The legacy wire
87
+ decides stateful before it drops a header the caller wrote, so a caller
88
+ that asks for the session that way — `AdtClassLegacy` — keeps its context
89
+ cookie, and its `UNLOCK` still reaches the lock.
90
+ - `RfcTransport` sends no session header, which is what Eclipse's JCo trace
91
+ shows.
92
+
93
+ A request whose caller wrote the header themselves is still treated as
94
+ stateful.
95
+
96
+ **What a consumer does:** nothing, through the connectors. A custom
97
+ `IAdtTransport` reads `request.stateful` where it used to find the header.
98
+
10
99
  ## [9.3.0] - 2026-09-26
11
100
 
12
101
  ### Changed
@@ -1783,7 +1872,10 @@ const connection = createAbapConnection(config, logger);
1783
1872
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
1784
1873
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
1785
1874
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
1786
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.0...HEAD
1875
+ [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.3...HEAD
1876
+ [9.3.3]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.2...v9.3.3
1877
+ [9.3.2]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.1...v9.3.2
1878
+ [9.3.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.0...v9.3.1
1787
1879
  [9.3.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.2...v9.3.0
1788
1880
  [9.2.2]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.1...v9.2.2
1789
1881
  [9.2.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.0...v9.2.1
package/README.md CHANGED
@@ -292,6 +292,14 @@ where stateful HTTP sessions are not usable: an RFC conversation is one ABAP
292
292
  session for its whole lifetime, which is the way past `423 invalid lock handle`
293
293
  on BASIS < 7.50.
294
294
 
295
+ Since 9.3.2, LOCK and UNLOCK in stateful mode share the persistent RFC
296
+ conversation; stateless requests use a separate ABAP context. With
297
+ `sap-rfc-lite` 0.2.0 or newer, that context is reset between calls on a kept
298
+ conversation; older clients open one conversation per call. This prevents stale
299
+ package state from causing PAK/058 on later writes or SADT_RESOURCE/007 on a
300
+ read after create. Concurrent calls use a separate conversation. See
301
+ [RFC conversation isolation](./docs/STATEFUL_SESSION_GUIDE.md#rfc-conversation-isolation-933).
302
+
295
303
  Needs the SAP NW RFC SDK on the machine and `npm install @mcp-abap-adt/sap-rfc-lite`.
296
304
 
297
305
  ```typescript
@@ -429,7 +437,11 @@ See [MIGRATION-4.0.md](./docs/MIGRATION-4.0.md).
429
437
 
430
438
  ### Stateful Sessions
431
439
 
432
- For operations that require session state (e.g., object modifications), you can enable stateful sessions:
440
+ Enable stateful mode for requests that need the ABAP context, such as LOCK and
441
+ UNLOCK. Switch to stateless for GET and PUT between them: a lock handle does
442
+ not require the write itself to run in the lock’s context. Authentication
443
+ cookies and the CSRF token remain available in both modes. See the
444
+ [Stateful Session Guide](./docs/STATEFUL_SESSION_GUIDE.md) for transport behavior.
433
445
 
434
446
  ```typescript
435
447
  import {
@@ -450,18 +462,18 @@ const connection = new AdtOnPremConnector(
450
462
  );
451
463
  await connection.connect();
452
464
 
453
- // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
465
+ // Enable the stateful HTTP context (session header and context cookie)
454
466
  connection.setSessionType("stateful");
455
467
 
456
- // Make requests - SAP will maintain session state
457
- await connection.makeAdtRequest({
458
- method: "POST",
459
- url: "/sap/bc/adt/objects/domains",
460
- data: { /* domain data */ },
461
- timeout: getTimeout("default"),
462
- });
468
+ // Send the LOCK request here, then leave its context for GET / PUT.
469
+ connection.setSessionType("stateless");
470
+
471
+ // Send GET / PUT with the lock handle here.
472
+ // Return to the same context when sending UNLOCK.
473
+ connection.setSessionType("stateful");
474
+ // Send UNLOCK here, then restore the default mode.
475
+ connection.setSessionType("stateless");
463
476
 
464
- // Note: Session state persistence is handled by @mcp-abap-adt/auth-broker package
465
477
  ```
466
478
 
467
479
  ### Custom Logger
@@ -126,7 +126,9 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
126
126
  transport: IAdtTransport, logger: ILogger | null, sessionId?: string);
127
127
  /**
128
128
  * Set session type (stateful or stateless)
129
- * Controls whether x-sap-adt-sessiontype: stateful header is added to requests
129
+ * Marks the requests that follow as `stateful` for the transport, which says
130
+ * so on its own wire (over HTTP: `x-sap-adt-sessiontype: stateful` and the
131
+ * context cookie; over RFC: nothing)
130
132
  * - stateful: SAP maintains session state between requests (locks, transactions)
131
133
  * - stateless: Each request is independent
132
134
  *
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
@@ -170,7 +170,9 @@ class AbstractAbapConnection {
170
170
  }
171
171
  /**
172
172
  * Set session type (stateful or stateless)
173
- * Controls whether x-sap-adt-sessiontype: stateful header is added to requests
173
+ * Marks the requests that follow as `stateful` for the transport, which says
174
+ * so on its own wire (over HTTP: `x-sap-adt-sessiontype: stateful` and the
175
+ * context cookie; over RFC: nothing)
174
176
  * - stateful: SAP maintains session state between requests (locks, transactions)
175
177
  * - stateless: Each request is independent
176
178
  *
@@ -857,11 +859,11 @@ class AbstractAbapConnection {
857
859
  !hasHeader(requestHeaders, 'x-sap-adt-profiling')) {
858
860
  requestHeaders['X-sap-adt-profiling'] = this.profilingRequest;
859
861
  }
860
- // And this one really is the session's: it says how the server should treat
861
- // the request, which is the only one of the three that varies with the mode.
862
- if (this.sessionMode === 'stateful') {
863
- requestHeaders['x-sap-adt-sessiontype'] = 'stateful';
864
- }
862
+ // The session mode is NOT a header here. How a request asks for the
863
+ // stateful session is the wire's: HTTP says it with
864
+ // `x-sap-adt-sessiontype: stateful` and the context cookie, RFC with
865
+ // neither. It travels as `stateful` on the request below, and each
866
+ // transport expresses it its own way.
865
867
  // Add auth headers (these MUST NOT be overridden)
866
868
  Object.assign(requestHeaders, await this.getAuthHeaders());
867
869
  // Read once: the wire is asked what it holds, and the same value is what
@@ -909,6 +911,7 @@ class AbstractAbapConnection {
909
911
  // `unknown` on the caller's options, a record on the seam: the two
910
912
  // transports serialise a query differently and both need the pairs.
911
913
  params: params,
914
+ stateful: this.sessionMode === 'stateful',
912
915
  };
913
916
  if (data !== undefined) {
914
917
  requestConfig.data = data;
@@ -21,6 +21,7 @@
21
21
  import { type AgentOptions } from 'node:https';
22
22
  import type { ILogger } from '../logger.js';
23
23
  import type { IAdtEstablishContext, IAdtSessionContext, IAdtTransport, IAdtTransportRequest, IAdtTransportResponse } from './IAdtTransport.js';
24
+ export { isStatefulRequest } from './statefulRequest.js';
24
25
  export declare class HttpTransport implements IAdtTransport {
25
26
  private readonly agentOptions;
26
27
  protected readonly logger: ILogger | null;
@@ -141,6 +142,13 @@ export declare class HttpTransport implements IAdtTransport {
141
142
  * looking like it carried a session.
142
143
  */
143
144
  private dress;
145
+ /**
146
+ * How this wire asks for the stateful session: `x-sap-adt-sessiontype:
147
+ * stateful`, as Eclipse sends on its `LOCK` and `UNLOCK` over HTTP. A wire
148
+ * whose system is hurt by the header overrides this (BASIS 7.40, see
149
+ * `LegacyOnPremHttpTransport`).
150
+ */
151
+ protected sessionTypeHeaders(): Record<string, string>;
144
152
  /** A path becomes an address; anything already absolute is left alone. */
145
153
  private address;
146
154
  private client;
@@ -1 +1 @@
1
- {"version":3,"file":"HttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/HttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAS5B,qBAAa,aAAc,YAAW,aAAa;IA6B/C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IACzC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IApC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;gBAGjB,YAAY,GAAE,MAAM,YAAyB,EAC3C,MAAM,GAAE,OAAO,GAAG,IAAW;IAChD;;;;;OAKG;IACc,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAGtE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgC/C,0EAA0E;IAC1E,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB;;;;;;OAMG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;OAOG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASzC;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIlD;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG7D,uEAAuE;IACvE,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI1C,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK;IAcb,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;IAuBd;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAExD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIzE;;;;;;;;;;;;;OAaG;cACa,YAAY,CAC1B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAInB,QAAQ;CA4BvB"}
1
+ {"version":3,"file":"HttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/HttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAuC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AASzD,qBAAa,aAAc,YAAW,aAAa;IA6B/C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IACzC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IApC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;gBAGjB,YAAY,GAAE,MAAM,YAAyB,EAC3C,MAAM,GAAE,OAAO,GAAG,IAAW;IAChD;;;;;OAKG;IACc,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAGtE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgC/C,0EAA0E;IAC1E,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB;;;;;;OAMG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;OAOG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASzC;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIlD;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG7D,uEAAuE;IACvE,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI1C,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK;IA6Bb;;;;;OAKG;IACH,SAAS,CAAC,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;IAuBd;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAExD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIzE;;;;;;;;;;;;;OAaG;cACa,YAAY,CAC1B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAInB,QAAQ;CA4BvB"}
@@ -23,11 +23,49 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.HttpTransport = void 0;
26
+ exports.HttpTransport = exports.isStatefulRequest = void 0;
27
27
  const node_https_1 = require("node:https");
28
28
  const axios_1 = __importDefault(require("axios"));
29
29
  const cookies_js_1 = require("../utils/cookies.js");
30
30
  const csrfConfig_js_1 = require("./csrfConfig.js");
31
+ const statefulRequest_js_1 = require("./statefulRequest.js");
32
+ /** The cookie that names a stateful ABAP context. */
33
+ const CONTEXT_COOKIE = 'sap-contextid';
34
+ /**
35
+ * A `Cookie` header without `sap-contextid`, for a request that is not stateful.
36
+ *
37
+ * `sap-contextid` names the stateful ABAP context a `LOCK` opened, and SAP
38
+ * routes any request carrying it into that context — header or no header. So
39
+ * a "stateless" request that carried it was not stateless: it ran in the
40
+ * lock's context. Measured on E19 (BASIS 816) and E98 (BASIS 756), 2026-09-27,
41
+ * one connection, a package locked, written and unlocked twice:
42
+ *
43
+ * - E19: the second `PUT` answered 400 PAK/058 "Package … is already locked" —
44
+ * the first write's save was still in that context's `CL_PACKAGE` buffer;
45
+ * - E98: every `PUT` answered 423 "Resource … is not locked (invalid lock
46
+ * handle)".
47
+ *
48
+ * Without the cookie on the non-stateful requests, both systems wrote twice
49
+ * and deleted on the same connection. Eclipse ADT does the same thing its
50
+ * own way: its trace shows one stateful session for `LOCK` and `UNLOCK`
51
+ * alone, and every other request — the `PUT` included — in a session of its
52
+ * own.
53
+ *
54
+ * The jar itself keeps the cookie: the stateful requests that follow, the
55
+ * `UNLOCK` above all, still need to reach the context that holds the lock.
56
+ */
57
+ function withoutContextCookie(cookie) {
58
+ if (!cookie)
59
+ return cookie;
60
+ const kept = cookie
61
+ .split(/;\s*/)
62
+ .filter((pair) => pair && pair.split('=')[0]?.trim() !== CONTEXT_COOKIE)
63
+ .join('; ');
64
+ return kept || undefined;
65
+ }
66
+ // The one verdict every wire takes; re-exported for the wires built on this one.
67
+ var statefulRequest_js_2 = require("./statefulRequest.js");
68
+ Object.defineProperty(exports, "isStatefulRequest", { enumerable: true, get: function () { return statefulRequest_js_2.isStatefulRequest; } });
31
69
  /** A 404 there means the system has no such endpoint, not that it is unwell. */
32
70
  function absentEndpoint(error) {
33
71
  const status = error
@@ -307,17 +345,46 @@ class HttpTransport {
307
345
  * cookie, and replacing it would send the request out unauthenticated while
308
346
  * looking like it carried a session.
309
347
  */
310
- dress(headers) {
348
+ dress(request) {
349
+ const headers = request.headers;
350
+ const stateful = (0, statefulRequest_js_1.isStatefulRequest)(request);
311
351
  const dressed = {
312
352
  ...this.affinityHeaders(),
313
353
  ...this.sessionHeaders(),
354
+ ...(stateful ? this.sessionTypeHeaders() : {}),
314
355
  ...headers,
315
356
  };
316
- const merged = (0, cookies_js_1.mergeCookieHeaders)(headers?.Cookie, this.combined ?? undefined);
317
- if (merged)
318
- dressed.Cookie = merged;
357
+ // Whatever the caller spelled the header as: HTTP does not tell `Cookie`
358
+ // from `cookie`, and a lowercase one spread in above would bypass the
359
+ // filter below.
360
+ let callerCookie;
361
+ for (const name of Object.keys(dressed)) {
362
+ if (name.toLowerCase() !== 'cookie')
363
+ continue;
364
+ callerCookie = (0, cookies_js_1.mergeCookieHeaders)(callerCookie, dressed[name]);
365
+ delete dressed[name];
366
+ }
367
+ const merged = (0, cookies_js_1.mergeCookieHeaders)(callerCookie, this.combined ?? undefined);
368
+ // Filtered on the merged header, not only on the jar: a caller's own
369
+ // `Cookie` can carry the context too — the connection's CSRF and 401
370
+ // retries put the whole jar there — and a non-stateful request must not
371
+ // reach the context whichever way the cookie arrived.
372
+ const cookie = stateful ? merged : withoutContextCookie(merged);
373
+ if (cookie)
374
+ dressed.Cookie = cookie;
375
+ else
376
+ delete dressed.Cookie;
319
377
  return dressed;
320
378
  }
379
+ /**
380
+ * How this wire asks for the stateful session: `x-sap-adt-sessiontype:
381
+ * stateful`, as Eclipse sends on its `LOCK` and `UNLOCK` over HTTP. A wire
382
+ * whose system is hurt by the header overrides this (BASIS 7.40, see
383
+ * `LegacyOnPremHttpTransport`).
384
+ */
385
+ sessionTypeHeaders() {
386
+ return { [statefulRequest_js_1.SESSION_TYPE_HEADER]: 'stateful' };
387
+ }
321
388
  /** A path becomes an address; anything already absolute is left alone. */
322
389
  address(url) {
323
390
  if (!url.startsWith('/') || !this.options.baseUrl)
@@ -367,7 +434,7 @@ class HttpTransport {
367
434
  * to add it.
368
435
  */
369
436
  async send(request) {
370
- return this.dispatch(request, this.dress(request.headers));
437
+ return this.dispatch(request, this.dress(request));
371
438
  }
372
439
  /**
373
440
  * Send EXACTLY these headers, with nothing of the wire's live state mixed in.
@@ -37,6 +37,18 @@ export interface IAdtTransportRequest {
37
37
  * this seam is written against.
38
38
  */
39
39
  validateStatus?: (status: number) => boolean;
40
+ /**
41
+ * Whether the request runs in the stateful session — the one a lock is
42
+ * taken and released in.
43
+ *
44
+ * A flag rather than a header, because how a session is asked for is the
45
+ * wire's own business. Over HTTP it is `x-sap-adt-sessiontype: stateful`
46
+ * plus the `sap-contextid` cookie that addresses the context; over RFC it is
47
+ * neither — Eclipse's JCo trace carries no session header at all. A request
48
+ * that is not stateful must not reach that context: see
49
+ * `HttpTransport.dress`.
50
+ */
51
+ stateful?: boolean;
40
52
  }
41
53
  /** What came back, in the terms every transport can produce. */
42
54
  export interface IAdtTransportResponse {
@@ -1 +1 @@
1
- {"version":3,"file":"IAdtTransport.d.ts","sourceRoot":"","sources":["../../src/connection/IAdtTransport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"IAdtTransport.d.ts","sourceRoot":"","sources":["../../src/connection/IAdtTransport.ts"],"names":[],"mappings":"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"}
@@ -16,8 +16,11 @@ import type { IAdtTransportRequest, IAdtTransportResponse } from './IAdtTranspor
16
16
  import { OnPremHttpTransport } from './OnPremHttpTransport.js';
17
17
  export declare class LegacyOnPremHttpTransport extends OnPremHttpTransport {
18
18
  readonly kind = "onprem-http-legacy";
19
+ /** The stateful header is the one this system is hurt by, so it asks with none. */
20
+ protected sessionTypeHeaders(): Record<string, string>;
19
21
  /**
20
- * The header never goes out, whatever the caller set the session type to.
22
+ * The header never goes out, whatever the caller set the session type to —
23
+ * not even when a caller wrote it into the request themselves.
21
24
  *
22
25
  * Dropped here rather than refused above: `setSessionType()` records what the
23
26
  * caller wants, and what actually travels is the wire's business. A caller
@@ -1 +1 @@
1
- {"version":3,"file":"LegacyOnPremHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/LegacyOnPremHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAI/D,qBAAa,yBAA0B,SAAQ,mBAAmB;IAChE,SAAkB,IAAI,wBAAwB;IAE9C;;;;;;;;OAQG;IACY,IAAI,CACjB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,OAAO,CAAC,kBAAkB;IAY1B;;;;;;;OAOG;IACM,kBAAkB,IAAI,OAAO;CAGvC"}
1
+ {"version":3,"file":"LegacyOnPremHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/LegacyOnPremHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAK/D,qBAAa,yBAA0B,SAAQ,mBAAmB;IAChE,SAAkB,IAAI,wBAAwB;IAE9C,mFAAmF;cAChE,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI/D;;;;;;;;;OASG;IACY,IAAI,CACjB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAUjC,OAAO,CAAC,kBAAkB;IAY1B;;;;;;;OAOG;IACM,kBAAkB,IAAI,OAAO;CAGvC"}
@@ -16,11 +16,17 @@
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.LegacyOnPremHttpTransport = void 0;
18
18
  const OnPremHttpTransport_js_1 = require("./OnPremHttpTransport.js");
19
+ const statefulRequest_js_1 = require("./statefulRequest.js");
19
20
  const SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
20
21
  class LegacyOnPremHttpTransport extends OnPremHttpTransport_js_1.OnPremHttpTransport {
21
22
  kind = 'onprem-http-legacy';
23
+ /** The stateful header is the one this system is hurt by, so it asks with none. */
24
+ sessionTypeHeaders() {
25
+ return {};
26
+ }
22
27
  /**
23
- * The header never goes out, whatever the caller set the session type to.
28
+ * The header never goes out, whatever the caller set the session type to —
29
+ * not even when a caller wrote it into the request themselves.
24
30
  *
25
31
  * Dropped here rather than refused above: `setSessionType()` records what the
26
32
  * caller wants, and what actually travels is the wire's business. A caller
@@ -29,7 +35,13 @@ class LegacyOnPremHttpTransport extends OnPremHttpTransport_js_1.OnPremHttpTrans
29
35
  * would move its locks somewhere they cannot be released.
30
36
  */
31
37
  async send(request) {
32
- return super.send(this.withoutSessionType(request));
38
+ // Decided before the header is dropped: a caller that asked for the
39
+ // stateful session by writing the header itself still gets its context
40
+ // cookie, only without the header this system is hurt by.
41
+ return super.send({
42
+ ...this.withoutSessionType(request),
43
+ stateful: (0, statefulRequest_js_1.isStatefulRequest)(request),
44
+ });
33
45
  }
34
46
  withoutSessionType(request) {
35
47
  if (!request.headers)
@@ -11,9 +11,32 @@
11
11
  * "invalid lock handle" on legacy systems (BASIS < 7.50) where stateful HTTP
12
12
  * sessions are not usable.
13
13
  *
14
- * **What is NOT here.** Cookies, the CSRF token, `x-sap-adt-sessiontype` and
15
- * the session lifecycle belong to the connection above this seam and are
16
- * already in `request.headers` by the time `send()` is called. A transport that
14
+ * **Two conversations, the way Eclipse ADT holds them over JCo.** Its trace
15
+ * shows one long-lived stateful session (`JCoEnqueueSystemSession`) for `LOCK`
16
+ * and `UNLOCK`, and every other call — the `PUT` included — in a session of
17
+ * its own. Here:
18
+ *
19
+ * - the persistent conversation, opened by `open()` and kept while this wire
20
+ * lives, carries the requests marked `stateful` — the lock and what must
21
+ * share its session;
22
+ * - every other request opens a conversation of its own, and closes it when
23
+ * it answers.
24
+ *
25
+ * One conversation for everything kept whatever an ABAP program left in the
26
+ * session. `CL_PACKAGE` buffers its instances for the life of the session and
27
+ * a save leaves them changeable, so after a create or a write every further
28
+ * write or delete of that package answered 400 PAK/058 "Package … is already
29
+ * locked"; a read straight after a create answered 400 SADT_RESOURCE/007.
30
+ * Measured on E19 (2026-09-27): LOCK and UNLOCK on one conversation, the PUT
31
+ * on another — two writes in a row fail on a shared second conversation and
32
+ * pass on a fresh one per call. The price is a logon per non-stateful call
33
+ * (~0.5 s on E19, against ~0.1 s for the call itself).
34
+ *
35
+ * **What is NOT here.** Cookies, the CSRF token and the session lifecycle
36
+ * belong to the connection above this seam and are already in
37
+ * `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
38
+ * not sent at all: over RFC `request.stateful` picks the conversation, and
39
+ * Eclipse's JCo trace carries no session header either. A transport that
17
40
  * also captured cookies would be doing that work twice, and the two copies
18
41
  * would disagree the first time one of them was cleared.
19
42
  */
@@ -25,6 +48,13 @@ export interface IRfcConversation {
25
48
  close(): Promise<void>;
26
49
  call(fm: string, params: Record<string, unknown>): Promise<Record<string, any>>;
27
50
  readonly alive: boolean;
51
+ /**
52
+ * Ends the ABAP session context and keeps the connection open
53
+ * (`RfcResetServerContext`, `@mcp-abap-adt/sap-rfc-lite` >= 0.2.0). Where it
54
+ * exists, one conversation serves the non-stateful calls, reset after each;
55
+ * where it does not, each such call opens a conversation of its own.
56
+ */
57
+ resetServerContext?(): Promise<void>;
28
58
  }
29
59
  /** What this wire does beyond carrying the request. */
30
60
  export interface IRfcTransportOptions {
@@ -53,6 +83,23 @@ export declare class RfcTransport implements IOnPremTransport {
53
83
  /** Which system this wire is for. Read by the compiler, never at runtime. */
54
84
  readonly system: "onprem";
55
85
  private conversation;
86
+ /**
87
+ * Bumped by every `close()`. A call that opened a conversation of its own
88
+ * compares it after the logon: the wire may have been closed while that
89
+ * logon was under way, and a write must not go out on a wire the caller has
90
+ * already given back.
91
+ */
92
+ private generation;
93
+ /**
94
+ * The conversation the non-stateful calls share when the client can reset
95
+ * it: opened on the first such call, reset after every call, closed with the
96
+ * wire. `null` until then, or after a reset failed.
97
+ */
98
+ private stateless;
99
+ /** Taken by the call on `stateless` until its reset is done. */
100
+ private statelessBusy;
101
+ /** `false` once a client showed it cannot reset; then every call opens its own. */
102
+ private canReset;
56
103
  /** Names the conversation, and so the ABAP session it carries. */
57
104
  private conversationId;
58
105
  /**
@@ -107,5 +154,31 @@ export declare class RfcTransport implements IOnPremTransport {
107
154
  /** Never throws, and a repeat call finds nothing owed. */
108
155
  close(): Promise<void>;
109
156
  send(request: IAdtTransportRequest): Promise<IAdtTransportResponse>;
157
+ /**
158
+ * A request that is not stateful, in a fresh ABAP session.
159
+ *
160
+ * Where the client can reset (`resetServerContext`), one kept conversation
161
+ * serves these calls and is reset after each: a fresh context without a new
162
+ * logon, ~0.1 s instead of ~0.5 s on E19. Where it cannot, or while that
163
+ * conversation is busy with another call, the call opens a conversation of
164
+ * its own and closes it when it answers.
165
+ *
166
+ * A call and its reset go together: a second call on the kept conversation
167
+ * before the first one's reset would run in the first one's context — a
168
+ * package the first one saved would refuse the second one's write (PAK/058).
169
+ * So a concurrent call takes a conversation of its own instead of waiting.
170
+ */
171
+ private carryOnItsOwn;
172
+ private carryOnKept;
173
+ /** A conversation opened for this one call, closed when it answers. */
174
+ private carryOnThrowaway;
175
+ /**
176
+ * A new conversation, logged on. The logon takes time (~0.5 s on E19); if the
177
+ * wire was closed meanwhile, the conversation is closed again and nothing is
178
+ * sent — not a PUT, not a DELETE — on a wire already given back.
179
+ */
180
+ private openOwn;
181
+ private closeQuietly;
182
+ private carry;
110
183
  }
111
184
  //# sourceMappingURL=RfcTransport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAgFD,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,qBAAa,YAAa,YAAW,gBAAgB;IA+EjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhF1B,QAAQ,CAAC,IAAI,SAAS;IAEtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,YAAY,CAAiC;IACrD,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAM;IAE5B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAEd,2CAA2C;IAC3C,OAAO,IAAI,IAAI;IAIf;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9D,+BAA+B;IAC/B,SAAS,IAAI,IAAI;IAIjB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAEtB;;;OAGG;IACH,6EAA6E;IAC7E,kBAAkB,IAAI,OAAO;IAI7B,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC,2EAA2E;IAC3E,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIzC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAErB;;;;;OAKG;gBAEgB,OAAO,EAAE,MAAM,gBAAgB,EAC/B,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,OAAO,GAAE,oBAAyB;IAGrD,oDAAoD;IACpD,OAAO,CAAC,OAAO;IAcT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,0DAA0D;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAkK1E"}
1
+ {"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAG5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;OAKG;IACH,kBAAkB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAgFD,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,qBAAa,YAAa,YAAW,gBAAgB;IAiGjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAlG1B,QAAQ,CAAC,IAAI,SAAS;IAEtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,YAAY,CAAiC;IACrD;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAK;IAEvB;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAiC;IAClD,gEAAgE;IAChE,OAAO,CAAC,aAAa,CAAS;IAC9B,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAsB;IACtC,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAM;IAE5B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAEd,2CAA2C;IAC3C,OAAO,IAAI,IAAI;IAIf;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9D,+BAA+B;IAC/B,SAAS,IAAI,IAAI;IAIjB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAEtB;;;OAGG;IACH,6EAA6E;IAC7E,kBAAkB,IAAI,OAAO;IAI7B,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC,2EAA2E;IAC3E,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIzC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAErB;;;;;OAKG;gBAEgB,OAAO,EAAE,MAAM,gBAAgB,EAC/B,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,OAAO,GAAE,oBAAyB;IAGrD,oDAAoD;IACpD,OAAO,CAAC,OAAO;IAcT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,0DAA0D;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBtB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAezE;;;;;;;;;;;;;OAaG;YACW,aAAa;YAUb,WAAW;IAqDzB,uEAAuE;YACzD,gBAAgB;IAa9B;;;;OAIG;YACW,OAAO;YAgBP,YAAY;YAQZ,KAAK;CAyKpB"}
@@ -12,15 +12,39 @@
12
12
  * "invalid lock handle" on legacy systems (BASIS < 7.50) where stateful HTTP
13
13
  * sessions are not usable.
14
14
  *
15
- * **What is NOT here.** Cookies, the CSRF token, `x-sap-adt-sessiontype` and
16
- * the session lifecycle belong to the connection above this seam and are
17
- * already in `request.headers` by the time `send()` is called. A transport that
15
+ * **Two conversations, the way Eclipse ADT holds them over JCo.** Its trace
16
+ * shows one long-lived stateful session (`JCoEnqueueSystemSession`) for `LOCK`
17
+ * and `UNLOCK`, and every other call — the `PUT` included — in a session of
18
+ * its own. Here:
19
+ *
20
+ * - the persistent conversation, opened by `open()` and kept while this wire
21
+ * lives, carries the requests marked `stateful` — the lock and what must
22
+ * share its session;
23
+ * - every other request opens a conversation of its own, and closes it when
24
+ * it answers.
25
+ *
26
+ * One conversation for everything kept whatever an ABAP program left in the
27
+ * session. `CL_PACKAGE` buffers its instances for the life of the session and
28
+ * a save leaves them changeable, so after a create or a write every further
29
+ * write or delete of that package answered 400 PAK/058 "Package … is already
30
+ * locked"; a read straight after a create answered 400 SADT_RESOURCE/007.
31
+ * Measured on E19 (2026-09-27): LOCK and UNLOCK on one conversation, the PUT
32
+ * on another — two writes in a row fail on a shared second conversation and
33
+ * pass on a fresh one per call. The price is a logon per non-stateful call
34
+ * (~0.5 s on E19, against ~0.1 s for the call itself).
35
+ *
36
+ * **What is NOT here.** Cookies, the CSRF token and the session lifecycle
37
+ * belong to the connection above this seam and are already in
38
+ * `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
39
+ * not sent at all: over RFC `request.stateful` picks the conversation, and
40
+ * Eclipse's JCo trace carries no session header either. A transport that
18
41
  * also captured cookies would be doing that work twice, and the two copies
19
42
  * would disagree the first time one of them was cleared.
20
43
  */
21
44
  Object.defineProperty(exports, "__esModule", { value: true });
22
45
  exports.RfcTransport = void 0;
23
46
  const node_crypto_1 = require("node:crypto");
47
+ const statefulRequest_js_1 = require("./statefulRequest.js");
24
48
  /**
25
49
  * ADT exception types, mapped to what they would have been over HTTP. Standard
26
50
  * types, returned in `<exc:exception>` XML.
@@ -98,6 +122,23 @@ class RfcTransport {
98
122
  /** Which system this wire is for. Read by the compiler, never at runtime. */
99
123
  system = 'onprem';
100
124
  conversation = null;
125
+ /**
126
+ * Bumped by every `close()`. A call that opened a conversation of its own
127
+ * compares it after the logon: the wire may have been closed while that
128
+ * logon was under way, and a write must not go out on a wire the caller has
129
+ * already given back.
130
+ */
131
+ generation = 0;
132
+ /**
133
+ * The conversation the non-stateful calls share when the client can reset
134
+ * it: opened on the first such call, reset after every call, closed with the
135
+ * wire. `null` until then, or after a reset failed.
136
+ */
137
+ stateless = null;
138
+ /** Taken by the call on `stateless` until its reset is done. */
139
+ statelessBusy = false;
140
+ /** `false` once a client showed it cannot reset; then every call opens its own. */
141
+ canReset;
101
142
  /** Names the conversation, and so the ABAP session it carries. */
102
143
  conversationId = '';
103
144
  /**
@@ -179,6 +220,7 @@ class RfcTransport {
179
220
  async open() {
180
221
  if (this.conversation?.alive)
181
222
  return;
223
+ const generation = this.generation;
182
224
  const conversation = this.connect();
183
225
  try {
184
226
  await conversation.open();
@@ -186,21 +228,39 @@ class RfcTransport {
186
228
  catch (e) {
187
229
  throw new Error(`Failed to open RFC connection: ${message(e)}`);
188
230
  }
231
+ // close() ran while this logged on: the session it would have handed out
232
+ // belongs to a wire already given back, so it is closed, not kept.
233
+ if (generation !== this.generation) {
234
+ await this.closeQuietly(conversation);
235
+ throw new Error('RFC transport was closed while it was being opened.');
236
+ }
189
237
  this.conversation = conversation;
190
238
  // Minted here, where the session begins. A conversation opened after an
191
239
  // earlier one closed is a DIFFERENT ABAP session, and the fingerprint has
192
240
  // to say so — a constant would report `unchanged` across a reconnect and
193
241
  // hide exactly the replacement the identity policy exists to catch.
194
242
  this.conversationId = (0, node_crypto_1.randomUUID)();
195
- this.logger?.debug('RFC conversation opened (stateful by nature)');
243
+ this.logger?.debug('RFC conversation opened (the stateful one)');
196
244
  }
197
245
  /** Never throws, and a repeat call finds nothing owed. */
198
246
  async close() {
247
+ // Everything the wire holds is given up BEFORE the first await. Closing
248
+ // takes time, and a send() that ran meanwhile would otherwise find the
249
+ // old stateful conversation still `alive` — a lock request carried on a
250
+ // session already being given back — or open a stateless one of its own
251
+ // and send a write after close(). With both references gone and the
252
+ // generation moved on, such a send() is refused as "not open", and a
253
+ // logon still under way sees the change and sends nothing.
254
+ this.generation += 1;
255
+ const stateless = this.stateless;
199
256
  const conversation = this.conversation;
200
- if (!conversation)
201
- return;
257
+ this.stateless = null;
202
258
  this.conversation = null;
203
259
  this.conversationId = '';
260
+ if (stateless)
261
+ await this.closeQuietly(stateless);
262
+ if (!conversation)
263
+ return;
204
264
  try {
205
265
  await conversation.close();
206
266
  this.logger?.debug('RFC conversation closed');
@@ -213,6 +273,123 @@ class RfcTransport {
213
273
  if (!this.conversation?.alive) {
214
274
  throw new Error('RFC transport is not open. Call connect() first.');
215
275
  }
276
+ // The same verdict HTTP takes: the flag the connection sets, or a session
277
+ // header the caller wrote itself. A caller that asks for the lock's
278
+ // session either way has to land on the conversation that holds it.
279
+ // Taken once, here: every await below compares against it, and a call
280
+ // that finds the wire closed since it began sends nothing.
281
+ const generation = this.generation;
282
+ return (0, statefulRequest_js_1.isStatefulRequest)(request)
283
+ ? this.carry(request, this.conversation, generation)
284
+ : this.carryOnItsOwn(request, generation);
285
+ }
286
+ /**
287
+ * A request that is not stateful, in a fresh ABAP session.
288
+ *
289
+ * Where the client can reset (`resetServerContext`), one kept conversation
290
+ * serves these calls and is reset after each: a fresh context without a new
291
+ * logon, ~0.1 s instead of ~0.5 s on E19. Where it cannot, or while that
292
+ * conversation is busy with another call, the call opens a conversation of
293
+ * its own and closes it when it answers.
294
+ *
295
+ * A call and its reset go together: a second call on the kept conversation
296
+ * before the first one's reset would run in the first one's context — a
297
+ * package the first one saved would refuse the second one's write (PAK/058).
298
+ * So a concurrent call takes a conversation of its own instead of waiting.
299
+ */
300
+ async carryOnItsOwn(request, generation) {
301
+ if (this.canReset !== false && !this.statelessBusy) {
302
+ return this.carryOnKept(request, generation);
303
+ }
304
+ return this.carryOnThrowaway(request, generation);
305
+ }
306
+ async carryOnKept(request, generation) {
307
+ this.statelessBusy = true;
308
+ try {
309
+ if (!this.stateless?.alive) {
310
+ const opened = await this.openOwn(generation);
311
+ // Checked again after the await: close() may have run since openOwn()
312
+ // checked, and a conversation of a closed wire must not become the
313
+ // kept one of the next.
314
+ if (generation !== this.generation) {
315
+ await this.closeQuietly(opened);
316
+ throw new Error('RFC transport was closed while the call was being opened; nothing was sent.');
317
+ }
318
+ if (typeof opened.resetServerContext !== 'function') {
319
+ // An older client: it cannot be reset, so it is used once and closed,
320
+ // and every later call opens its own.
321
+ this.canReset = false;
322
+ try {
323
+ return await this.carry(request, opened, generation);
324
+ }
325
+ finally {
326
+ await this.closeQuietly(opened);
327
+ }
328
+ }
329
+ this.canReset = true;
330
+ this.stateless = opened;
331
+ this.logger?.debug('RFC stateless conversation opened (reset per call)');
332
+ }
333
+ const kept = this.stateless;
334
+ try {
335
+ return await this.carry(request, kept, generation);
336
+ }
337
+ finally {
338
+ try {
339
+ await kept.resetServerContext?.();
340
+ }
341
+ catch (e) {
342
+ // A context that could not be reset must not serve the next call.
343
+ this.logger?.debug(`RFC reset failed, dropping the conversation: ${message(e)}`);
344
+ if (this.stateless === kept)
345
+ this.stateless = null;
346
+ await this.closeQuietly(kept);
347
+ }
348
+ }
349
+ }
350
+ finally {
351
+ this.statelessBusy = false;
352
+ }
353
+ }
354
+ /** A conversation opened for this one call, closed when it answers. */
355
+ async carryOnThrowaway(request, generation) {
356
+ const own = await this.openOwn(generation);
357
+ this.logger?.debug('RFC conversation opened for one stateless call');
358
+ try {
359
+ return await this.carry(request, own, generation);
360
+ }
361
+ finally {
362
+ await this.closeQuietly(own);
363
+ }
364
+ }
365
+ /**
366
+ * A new conversation, logged on. The logon takes time (~0.5 s on E19); if the
367
+ * wire was closed meanwhile, the conversation is closed again and nothing is
368
+ * sent — not a PUT, not a DELETE — on a wire already given back.
369
+ */
370
+ async openOwn(generation) {
371
+ const own = this.connect();
372
+ try {
373
+ await own.open();
374
+ }
375
+ catch (e) {
376
+ throw new Error(`Failed to open RFC connection: ${message(e)}`);
377
+ }
378
+ if (generation !== this.generation || !this.conversation?.alive) {
379
+ await this.closeQuietly(own);
380
+ throw new Error('RFC transport was closed while the call was being opened; nothing was sent.');
381
+ }
382
+ return own;
383
+ }
384
+ async closeQuietly(conversation) {
385
+ try {
386
+ await conversation.close();
387
+ }
388
+ catch (e) {
389
+ this.logger?.debug(`RFC close error: ${message(e)}`);
390
+ }
391
+ }
392
+ async carry(request, conversation, generation) {
216
393
  const method = request.method.toUpperCase();
217
394
  // HTTP clients serialise `params`; RFC has no such step, so they go into
218
395
  // the URI here or they do not travel at all.
@@ -268,9 +445,14 @@ class RfcTransport {
268
445
  // mid-flight is precisely what the connection emulates over HTTP, where
269
446
  // a critical section raises the caller's timeout to a ten-minute ceiling so
270
447
  // an abort cannot orphan a lock handle. Here that comes free.
448
+ // The last point before anything leaves: nothing awaits between this
449
+ // check and the call, so a close() that ran at any await above stops it.
450
+ if (generation !== this.generation) {
451
+ throw new Error('RFC transport was closed before the call was sent; nothing was sent.');
452
+ }
271
453
  let raw;
272
454
  try {
273
- raw = await this.conversation.call('SADT_REST_RFC_ENDPOINT', {
455
+ raw = await conversation.call('SADT_REST_RFC_ENDPOINT', {
274
456
  REQUEST: {
275
457
  REQUEST_LINE: {
276
458
  METHOD: method,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Whether a request runs in the stateful session — the one a lock is taken and
3
+ * released in.
4
+ *
5
+ * One verdict for every wire. HTTP expresses it as a header and the context
6
+ * cookie, RFC as the conversation it picks; if the two wires decided it
7
+ * differently, a caller that asked for the session one way would keep its lock
8
+ * on one wire and lose it on the other.
9
+ */
10
+ import type { IAdtTransportRequest } from './IAdtTransport.js';
11
+ /** The header that asks SAP for the stateful session over HTTP. */
12
+ export declare const SESSION_TYPE_HEADER = "x-sap-adt-sessiontype";
13
+ /**
14
+ * Marked so by the connection, or — kept for callers that still write it
15
+ * themselves — carrying the session header already. Taken on the request as
16
+ * the caller made it, before a wire rewrites it.
17
+ */
18
+ export declare function isStatefulRequest(request: IAdtTransportRequest): boolean;
19
+ //# sourceMappingURL=statefulRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statefulRequest.d.ts","sourceRoot":"","sources":["../../src/connection/statefulRequest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAE3D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAaxE"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_TYPE_HEADER = void 0;
4
+ exports.isStatefulRequest = isStatefulRequest;
5
+ /** The header that asks SAP for the stateful session over HTTP. */
6
+ exports.SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
7
+ /**
8
+ * Marked so by the connection, or — kept for callers that still write it
9
+ * themselves — carrying the session header already. Taken on the request as
10
+ * the caller made it, before a wire rewrites it.
11
+ */
12
+ function isStatefulRequest(request) {
13
+ if (request.stateful)
14
+ return true;
15
+ const headers = request.headers;
16
+ if (!headers)
17
+ return false;
18
+ for (const [name, value] of Object.entries(headers)) {
19
+ if (name.toLowerCase() === exports.SESSION_TYPE_HEADER &&
20
+ String(value).toLowerCase() === 'stateful') {
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ }
package/docs/INDEX.md CHANGED
@@ -69,7 +69,7 @@ mcp-abap-connection/
69
69
 
70
70
  ### Session Management
71
71
  - **Overview**: [USAGE.md - Session Management](./USAGE.md#session-management)
72
- - **Stateful Mode**: Use `setSessionType('stateful')` for session headers
72
+ - **Stateful Mode**: Use `setSessionType('stateful')` for the LOCK / UNLOCK context; use stateless mode for GET / PUT
73
73
  - **Session State Persistence**: Handled by `@mcp-abap-adt/auth-broker` package
74
74
  - **API Methods**:
75
75
  - `getSessionId()` - Get current session ID (auto-generated UUID)
@@ -1,8 +1,8 @@
1
1
  # Stateful Session Guide (Connection Layer)
2
2
 
3
3
  > **The header is not what tracks your lock.** `setSessionType('stateful')` sets
4
- > a per-request header and nothing more — another handler can flip it back while
5
- > your lock is genuinely open, and a batch never sets it at all. Nothing in this
4
+ > the mode for subsequent requests; HTTP uses it to select the session header
5
+ > and context cookie. Another handler can flip it back while your lock is open. Nothing in this
6
6
  > layer tracks locks: that belongs to `@mcp-abap-adt/adt-clients`, which holds the
7
7
  > handles and pairs each LOCK with its UNLOCK per object. What the connection
8
8
  > offers is `beginCriticalSection()` / `endCriticalSection()`, so a short timeout
@@ -24,6 +24,62 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
24
24
 
25
25
  ---
26
26
 
27
+ ## HTTP context isolation (9.3.1)
28
+
29
+ The authentication session (`SAP_SESSIONID`) and the stateful ABAP context
30
+ (`sap-contextid`) have different roles. HTTP requests keep authentication
31
+ cookies and the CSRF token in either mode. Only stateful requests carry
32
+ `sap-contextid`; the jar retains it while stateless requests run so that a
33
+ later UNLOCK can still reach the context holding the lock.
34
+
35
+ Use stateful mode for LOCK and UNLOCK, and stateless mode for the intervening
36
+ GET and PUT. Passing a lock handle to PUT does not require the context cookie.
37
+ The ADT clients choose these modes; callers using connectors directly must
38
+ choose the mode for each operation themselves.
39
+
40
+ Before 9.3.1, the whole jar went out even in stateless mode. After LOCK, a PUT
41
+ therefore ran in the lock's context. Package updates on E19 (BASIS 816) failed
42
+ on the second write with **400 PAK/058, “Package … is already locked”**, and a
43
+ subsequent delete failed too. On E98 (BASIS 756), PUT failed with **423,
44
+ “Resource … is not locked (invalid lock handle)”**. Filtering the context
45
+ cookie allowed two updates and a delete on one connection on both systems.
46
+ The filter also applies to cookies supplied in request headers and retries.
47
+
48
+ | Transport | Stateful request |
49
+ | --- | --- |
50
+ | HTTP (on-prem / cloud) | Sends `x-sap-adt-sessiontype: stateful` and the context cookie, when held |
51
+ | Legacy on-prem HTTP | Keeps the context cookie but omits the session-type header |
52
+ | RFC | Uses the persistent conversation for stateful requests; since 9.3.3 resets a kept stateless conversation between calls when supported |
53
+
54
+ A caller-supplied stateful header is also recognized, including by the legacy
55
+ HTTP transport before it removes that header. Custom `IAdtTransport`
56
+ implementations must read `request.stateful`; the connection no longer adds
57
+ `x-sap-adt-sessiontype` itself. The cookie fix was verified on E19 and E98;
58
+ BASIS 7.40 and ABAP Cloud were not integration-tested for this change.
59
+
60
+ ## RFC conversation isolation (9.3.2 and 9.3.3)
61
+
62
+ Stateful requests, including LOCK and UNLOCK, share the conversation opened
63
+ by `connect()`. Since 9.3.3, `sap-rfc-lite` 0.2.0 and newer let stateless calls
64
+ reuse a second conversation: the transport resets its ABAP server context after
65
+ each call, keeping the RFC logon open. With older clients (0.1.x), or when a
66
+ resettable conversation is busy, each stateless call gets a separate temporary
67
+ conversation. Both `setSessionType('stateful')` and a caller-supplied stateful
68
+ session header select the persistent conversation.
69
+
70
+ Previously all RFC requests shared one ABAP session. On E19, saving a package
71
+ left state in `CL_PACKAGE`'s instance buffer: later writes or deletes failed
72
+ with **400 PAK/058, “Package … is already locked”**. Reading immediately after
73
+ creation could fail with **400 SADT_RESOURCE/007**. With separate conversations,
74
+ create, read, two lock/PUT/unlock rounds and delete passed on one connection.
75
+
76
+ When using `sap-rfc-lite` 0.1.x, each stateless call pays for an RFC logon:
77
+ about 0.5 s on E19, in addition to about 0.1 s for the call. With 0.2.0+, a
78
+ resettable call took about 0.24 s total on E19. Direct connector users must mark
79
+ LOCK and UNLOCK stateful to retain their conversation. If disconnect occurs
80
+ while a temporary conversation is logging on, it is closed without sending
81
+ the request; reconnect does not revive that pending request.
82
+
27
83
  ## Enabling Stateful Sessions
28
84
 
29
85
  Every example below builds on this much, so it is stated once:
@@ -62,10 +118,10 @@ const connection = new AdtOnPremConnector(
62
118
  );
63
119
  await connection.connect(); // required before any request
64
120
 
65
- // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
121
+ // Enable the stateful HTTP context (session header and context cookie)
66
122
  connection.setSessionType('stateful');
67
123
 
68
- // Now all requests share the same session (cookies, CSRF token)
124
+ // Following requests use the stateful context until the mode is changed
69
125
  await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
70
126
 
71
127
  // Switch back to stateless
@@ -178,7 +234,7 @@ wrong fixes:
178
234
  |---|---|---|
179
235
  | **the connection** | one TCP socket | you, the network, a timeout firing. **Ends nothing on the server** |
180
236
  | **the HTTP session** | the ICF conversation, held by the cookie jar on this side | dropping the cookies, or a logoff. Nothing about it is in doubt |
181
- | **the ABAP session** | the server-side context named by `SAP_SESSIONID_<SID>_<CLIENT>` — the roll area a stateful request runs in | **only the server.** Its own idle timeout, which this side can neither read nor influence, or an explicit logoff |
237
+ | **the ABAP session** | the authentication session named by `SAP_SESSIONID_<SID>_<CLIENT>`; `sap-contextid` separately addresses the stateful ABAP context | **only the server.** Its own idle timeout, which this side can neither read nor influence, or an explicit logoff |
182
238
  | **the lock** | an enqueue entry, **owned by the ABAP session** | that ABAP session ending. **When it goes, its locks go with it** |
183
239
 
184
240
  The two middle rows are the ones worth keeping apart, because everything that
package/docs/USAGE.md CHANGED
@@ -250,9 +250,13 @@ await connection.connect();
250
250
  await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
251
251
  ```
252
252
 
253
- ### Stateful Mode (Session Headers)
253
+ ### Stateful Mode (HTTP Context)
254
254
 
255
- Enable stateful session mode for operations requiring consistent session state:
255
+ Use stateful mode for LOCK and UNLOCK, then stateless mode for GET and PUT
256
+ between them. Since 9.3.1, stateless HTTP requests omit `sap-contextid` while
257
+ retaining authentication cookies and the CSRF token. This prevents package
258
+ writes from running in the lock context and failing with PAK/058 or an invalid
259
+ lock handle. See [HTTP context isolation](./STATEFUL_SESSION_GUIDE.md#http-context-isolation-931).
256
260
 
257
261
  ```typescript
258
262
  import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport, getTimeout } from '@mcp-abap-adt/connection';
@@ -267,10 +271,10 @@ const connection = new AdtOnPremConnector(
267
271
  );
268
272
  await connection.connect();
269
273
 
270
- // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
274
+ // Enable the stateful HTTP context (session header and context cookie)
271
275
  connection.setSessionType('stateful');
272
276
 
273
- // Now all requests share the same session (cookies, CSRF token)
277
+ // Following requests use the stateful context until the mode is changed
274
278
  await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
275
279
 
276
280
  // Check session mode
@@ -972,7 +976,7 @@ See [examples/](../examples/) for complete working examples:
972
976
  ## Best Practices
973
977
 
974
978
  1. **State the three axes**: the connector says which system, the provider says which credential, the transport says which wire. Nothing is detected, and a connection that had to guess would guess wrong on the case that matters
975
- 2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for multi-request operations (locks, transactions)
979
+ 2. **Enable Stateful Mode**: Use `setSessionType('stateful')` for LOCK / UNLOCK and stateless mode for intervening GET / PUT
976
980
  3. **Token Refresh**: For cloud systems, use `@mcp-abap-adt/auth-broker` for token refresh functionality
977
981
  4. **Session State Persistence**: Use `@mcp-abap-adt/auth-broker` for session state persistence
978
982
  5. **Handle Errors Gracefully**: Wrap requests in try-catch blocks and check `failure.response` for HTTP errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "9.3.0",
3
+ "version": "9.3.3",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -63,7 +63,7 @@
63
63
  "open": "^11.0.0"
64
64
  },
65
65
  "optionalDependencies": {
66
- "@mcp-abap-adt/sap-rfc-lite": "^0.1.0"
66
+ "@mcp-abap-adt/sap-rfc-lite": "^0.1.0 || ^0.2.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@biomejs/biome": "^2.3.14",