@mcp-abap-adt/connection 9.3.0 → 9.3.1
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 +50 -1
- package/README.md +14 -10
- package/dist/connection/AbstractAbapConnection.d.ts +3 -1
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +9 -6
- package/dist/connection/HttpTransport.d.ts +16 -0
- package/dist/connection/HttpTransport.d.ts.map +1 -1
- package/dist/connection/HttpTransport.js +93 -5
- package/dist/connection/IAdtTransport.d.ts +12 -0
- package/dist/connection/IAdtTransport.d.ts.map +1 -1
- package/dist/connection/LegacyOnPremHttpTransport.d.ts +4 -1
- package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -1
- package/dist/connection/LegacyOnPremHttpTransport.js +14 -2
- package/dist/connection/RfcTransport.d.ts +6 -3
- package/dist/connection/RfcTransport.d.ts.map +1 -1
- package/dist/connection/RfcTransport.js +6 -3
- package/docs/INDEX.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +38 -5
- package/docs/USAGE.md +9 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [9.3.1] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Over HTTP, a request that is not stateful no longer reaches the stateful
|
|
15
|
+
context.** `sap-contextid` names the context a `LOCK` opens, and SAP routes any
|
|
16
|
+
request that carries it into that context — session header or none. The wire
|
|
17
|
+
sent the whole jar on every request, so a "stateless" `PUT` after a `LOCK` ran
|
|
18
|
+
in the lock's context. Measured 2026-09-27 on one connection, a package locked,
|
|
19
|
+
written and unlocked twice:
|
|
20
|
+
- E19 (BASIS 816): the second `PUT` answered 400 PAK/058 "Package … is already
|
|
21
|
+
locked" — the first save was still in that context's `CL_PACKAGE` buffer, and
|
|
22
|
+
a delete on the same connection was refused the same way;
|
|
23
|
+
- E98 (BASIS 756): every `PUT` answered 423 "Resource … is not locked (invalid
|
|
24
|
+
lock handle)".
|
|
25
|
+
|
|
26
|
+
With the cookie kept to the stateful requests, both systems wrote twice and
|
|
27
|
+
deleted on the same connection. That is Eclipse ADT's split too: its trace
|
|
28
|
+
shows one stateful session for `LOCK` and `UNLOCK` alone and every other
|
|
29
|
+
request, the `PUT` included, outside it. The jar still holds the cookie, so
|
|
30
|
+
the `UNLOCK` that follows reaches the context holding the lock.
|
|
31
|
+
|
|
32
|
+
The cookie is filtered on the header as it finally goes out, not only on
|
|
33
|
+
the jar: the connection's CSRF and 401 retries write the whole jar into the
|
|
34
|
+
request's own `Cookie`, and a caller can spell that header in lower case.
|
|
35
|
+
Either way, a non-stateful request does not carry the context.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **The session type is the wire's to express, not the connection's.**
|
|
40
|
+
`setSessionType('stateful')` now marks each request `stateful: true` on
|
|
41
|
+
`IAdtTransportRequest` instead of writing `x-sap-adt-sessiontype` into its
|
|
42
|
+
headers.
|
|
43
|
+
- `HttpTransport` adds `x-sap-adt-sessiontype: stateful` and `sap-contextid`
|
|
44
|
+
to stateful requests only (overridable through `sessionTypeHeaders()`;
|
|
45
|
+
`LegacyOnPremHttpTransport` sends no header, as before). The legacy wire
|
|
46
|
+
decides stateful before it drops a header the caller wrote, so a caller
|
|
47
|
+
that asks for the session that way — `AdtClassLegacy` — keeps its context
|
|
48
|
+
cookie, and its `UNLOCK` still reaches the lock.
|
|
49
|
+
- `RfcTransport` sends no session header, which is what Eclipse's JCo trace
|
|
50
|
+
shows.
|
|
51
|
+
|
|
52
|
+
A request whose caller wrote the header themselves is still treated as
|
|
53
|
+
stateful.
|
|
54
|
+
|
|
55
|
+
**What a consumer does:** nothing, through the connectors. A custom
|
|
56
|
+
`IAdtTransport` reads `request.stateful` where it used to find the header.
|
|
57
|
+
|
|
10
58
|
## [9.3.0] - 2026-09-26
|
|
11
59
|
|
|
12
60
|
### Changed
|
|
@@ -1783,7 +1831,8 @@ const connection = createAbapConnection(config, logger);
|
|
|
1783
1831
|
- JWT token refresh now properly handles connection errors (401/403 during initial connect)
|
|
1784
1832
|
- Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
|
|
1785
1833
|
- 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.
|
|
1834
|
+
[Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.1...HEAD
|
|
1835
|
+
[9.3.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.3.0...v9.3.1
|
|
1787
1836
|
[9.3.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.2...v9.3.0
|
|
1788
1837
|
[9.2.2]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.1...v9.2.2
|
|
1789
1838
|
[9.2.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v9.2.0...v9.2.1
|
package/README.md
CHANGED
|
@@ -429,7 +429,11 @@ See [MIGRATION-4.0.md](./docs/MIGRATION-4.0.md).
|
|
|
429
429
|
|
|
430
430
|
### Stateful Sessions
|
|
431
431
|
|
|
432
|
-
|
|
432
|
+
Enable stateful mode for requests that need the ABAP context, such as LOCK and
|
|
433
|
+
UNLOCK. Switch to stateless for GET and PUT between them: a lock handle does
|
|
434
|
+
not require the write itself to run in the lock’s context. Authentication
|
|
435
|
+
cookies and the CSRF token remain available in both modes. See the
|
|
436
|
+
[Stateful Session Guide](./docs/STATEFUL_SESSION_GUIDE.md) for transport behavior.
|
|
433
437
|
|
|
434
438
|
```typescript
|
|
435
439
|
import {
|
|
@@ -450,18 +454,18 @@ const connection = new AdtOnPremConnector(
|
|
|
450
454
|
);
|
|
451
455
|
await connection.connect();
|
|
452
456
|
|
|
453
|
-
// Enable stateful
|
|
457
|
+
// Enable the stateful HTTP context (session header and context cookie)
|
|
454
458
|
connection.setSessionType("stateful");
|
|
455
459
|
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
// Send the LOCK request here, then leave its context for GET / PUT.
|
|
461
|
+
connection.setSessionType("stateless");
|
|
462
|
+
|
|
463
|
+
// Send GET / PUT with the lock handle here.
|
|
464
|
+
// Return to the same context when sending UNLOCK.
|
|
465
|
+
connection.setSessionType("stateful");
|
|
466
|
+
// Send UNLOCK here, then restore the default mode.
|
|
467
|
+
connection.setSessionType("stateless");
|
|
463
468
|
|
|
464
|
-
// Note: Session state persistence is handled by @mcp-abap-adt/auth-broker package
|
|
465
469
|
```
|
|
466
470
|
|
|
467
471
|
### 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
|
-
*
|
|
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":"AAEA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACvB,MAAM,yCAAyC,CAAC;AAOjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAEL,gBAAgB,EAEjB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9E,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAC;AAqC5B,uBAAe,sBACb,YACE,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB;IAuGjB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa;IACjC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAhH3C;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,mBAA0B;IAEtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAyC;IAE5D;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,gBAAgB,CAAgC;IACxD;;;;;;;;;;OAUG;IACH;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,OAAO,CAAoC;IAEnD,OAAO,CAAC,iBAAiB,CAAS;IAClC,oFAAoF;IACpF,OAAO,CAAC,oBAAoB,CAAK;IACjC,oEAAoE;IACpE;;;;;;;;;;;;;;OAcG;IACH;;;;OAIG;IACH;;;;;;;;;;;;OAYG;IACH;;;;;;;OAOG;IAEH,SAAS,aACU,MAAM,EAAE,SAAS;IAClC;;;;;;;;OAQG;IACM,SAAS,EAAE,aAAa,EACd,MAAM,EAAE,OAAO,GAAG,IAAI,EACzC,SAAS,CAAC,EAAE,MAAM;IAoBpB
|
|
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
|
-
*
|
|
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
|
-
//
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
|
|
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,15 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* Whether the request runs in the stateful session: marked so by the
|
|
26
|
+
* connection, or — kept for callers that still write it themselves — carrying
|
|
27
|
+
* the session header already.
|
|
28
|
+
*
|
|
29
|
+
* Exported for the wires that rewrite a request before sending it: the
|
|
30
|
+
* verdict has to be taken on the request as the caller made it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isStatefulRequest(request: IAdtTransportRequest): boolean;
|
|
24
33
|
export declare class HttpTransport implements IAdtTransport {
|
|
25
34
|
private readonly agentOptions;
|
|
26
35
|
protected readonly logger: ILogger | null;
|
|
@@ -141,6 +150,13 @@ export declare class HttpTransport implements IAdtTransport {
|
|
|
141
150
|
* looking like it carried a session.
|
|
142
151
|
*/
|
|
143
152
|
private dress;
|
|
153
|
+
/**
|
|
154
|
+
* How this wire asks for the stateful session: `x-sap-adt-sessiontype:
|
|
155
|
+
* stateful`, as Eclipse sends on its `LOCK` and `UNLOCK` over HTTP. A wire
|
|
156
|
+
* whose system is hurt by the header overrides this (BASIS 7.40, see
|
|
157
|
+
* `LegacyOnPremHttpTransport`).
|
|
158
|
+
*/
|
|
159
|
+
protected sessionTypeHeaders(): Record<string, string>;
|
|
144
160
|
/** A path becomes an address; anything already absolute is left alone. */
|
|
145
161
|
private address;
|
|
146
162
|
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;
|
|
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;AAwC5B;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAaxE;AASD,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"}
|
|
@@ -24,10 +24,69 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
26
|
exports.HttpTransport = void 0;
|
|
27
|
+
exports.isStatefulRequest = isStatefulRequest;
|
|
27
28
|
const node_https_1 = require("node:https");
|
|
28
29
|
const axios_1 = __importDefault(require("axios"));
|
|
29
30
|
const cookies_js_1 = require("../utils/cookies.js");
|
|
30
31
|
const csrfConfig_js_1 = require("./csrfConfig.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 header that asks SAP for the stateful session over HTTP. */
|
|
67
|
+
const SESSION_TYPE_HEADER = 'x-sap-adt-sessiontype';
|
|
68
|
+
/**
|
|
69
|
+
* Whether the request runs in the stateful session: marked so by the
|
|
70
|
+
* connection, or — kept for callers that still write it themselves — carrying
|
|
71
|
+
* the session header already.
|
|
72
|
+
*
|
|
73
|
+
* Exported for the wires that rewrite a request before sending it: the
|
|
74
|
+
* verdict has to be taken on the request as the caller made it.
|
|
75
|
+
*/
|
|
76
|
+
function isStatefulRequest(request) {
|
|
77
|
+
if (request.stateful)
|
|
78
|
+
return true;
|
|
79
|
+
const headers = request.headers;
|
|
80
|
+
if (!headers)
|
|
81
|
+
return false;
|
|
82
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
83
|
+
if (name.toLowerCase() === SESSION_TYPE_HEADER &&
|
|
84
|
+
String(value).toLowerCase() === 'stateful') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
31
90
|
/** A 404 there means the system has no such endpoint, not that it is unwell. */
|
|
32
91
|
function absentEndpoint(error) {
|
|
33
92
|
const status = error
|
|
@@ -307,17 +366,46 @@ class HttpTransport {
|
|
|
307
366
|
* cookie, and replacing it would send the request out unauthenticated while
|
|
308
367
|
* looking like it carried a session.
|
|
309
368
|
*/
|
|
310
|
-
dress(
|
|
369
|
+
dress(request) {
|
|
370
|
+
const headers = request.headers;
|
|
371
|
+
const stateful = isStatefulRequest(request);
|
|
311
372
|
const dressed = {
|
|
312
373
|
...this.affinityHeaders(),
|
|
313
374
|
...this.sessionHeaders(),
|
|
375
|
+
...(stateful ? this.sessionTypeHeaders() : {}),
|
|
314
376
|
...headers,
|
|
315
377
|
};
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
378
|
+
// Whatever the caller spelled the header as: HTTP does not tell `Cookie`
|
|
379
|
+
// from `cookie`, and a lowercase one spread in above would bypass the
|
|
380
|
+
// filter below.
|
|
381
|
+
let callerCookie;
|
|
382
|
+
for (const name of Object.keys(dressed)) {
|
|
383
|
+
if (name.toLowerCase() !== 'cookie')
|
|
384
|
+
continue;
|
|
385
|
+
callerCookie = (0, cookies_js_1.mergeCookieHeaders)(callerCookie, dressed[name]);
|
|
386
|
+
delete dressed[name];
|
|
387
|
+
}
|
|
388
|
+
const merged = (0, cookies_js_1.mergeCookieHeaders)(callerCookie, this.combined ?? undefined);
|
|
389
|
+
// Filtered on the merged header, not only on the jar: a caller's own
|
|
390
|
+
// `Cookie` can carry the context too — the connection's CSRF and 401
|
|
391
|
+
// retries put the whole jar there — and a non-stateful request must not
|
|
392
|
+
// reach the context whichever way the cookie arrived.
|
|
393
|
+
const cookie = stateful ? merged : withoutContextCookie(merged);
|
|
394
|
+
if (cookie)
|
|
395
|
+
dressed.Cookie = cookie;
|
|
396
|
+
else
|
|
397
|
+
delete dressed.Cookie;
|
|
319
398
|
return dressed;
|
|
320
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* How this wire asks for the stateful session: `x-sap-adt-sessiontype:
|
|
402
|
+
* stateful`, as Eclipse sends on its `LOCK` and `UNLOCK` over HTTP. A wire
|
|
403
|
+
* whose system is hurt by the header overrides this (BASIS 7.40, see
|
|
404
|
+
* `LegacyOnPremHttpTransport`).
|
|
405
|
+
*/
|
|
406
|
+
sessionTypeHeaders() {
|
|
407
|
+
return { [SESSION_TYPE_HEADER]: 'stateful' };
|
|
408
|
+
}
|
|
321
409
|
/** A path becomes an address; anything already absolute is left alone. */
|
|
322
410
|
address(url) {
|
|
323
411
|
if (!url.startsWith('/') || !this.options.baseUrl)
|
|
@@ -367,7 +455,7 @@ class HttpTransport {
|
|
|
367
455
|
* to add it.
|
|
368
456
|
*/
|
|
369
457
|
async send(request) {
|
|
370
|
-
return this.dispatch(request, this.dress(request
|
|
458
|
+
return this.dispatch(request, this.dress(request));
|
|
371
459
|
}
|
|
372
460
|
/**
|
|
373
461
|
* 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":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;
|
|
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;
|
|
1
|
+
{"version":3,"file":"LegacyOnPremHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/LegacyOnPremHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,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,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"}
|
|
@@ -15,12 +15,18 @@
|
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.LegacyOnPremHttpTransport = void 0;
|
|
18
|
+
const HttpTransport_js_1 = require("./HttpTransport.js");
|
|
18
19
|
const OnPremHttpTransport_js_1 = require("./OnPremHttpTransport.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
|
-
|
|
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, HttpTransport_js_1.isStatefulRequest)(request),
|
|
44
|
+
});
|
|
33
45
|
}
|
|
34
46
|
withoutSessionType(request) {
|
|
35
47
|
if (!request.headers)
|
|
@@ -11,9 +11,12 @@
|
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* **What is NOT here.** Cookies, the CSRF token and the session lifecycle
|
|
15
|
+
* belong to the connection above this seam and are already in
|
|
16
|
+
* `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
|
|
17
|
+
* not sent at all: `request.stateful` is HTTP's to express, and over RFC
|
|
18
|
+
* Eclipse's JCo trace carries no session header — its stateful session is a
|
|
19
|
+
* connection of its own, not a header on the request. A transport that
|
|
17
20
|
* also captured cookies would be doing that work twice, and the two copies
|
|
18
21
|
* would disagree the first time one of them was cleared.
|
|
19
22
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;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"}
|
|
@@ -12,9 +12,12 @@
|
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* **What is NOT here.** Cookies, the CSRF token and the session lifecycle
|
|
16
|
+
* belong to the connection above this seam and are already in
|
|
17
|
+
* `request.headers` by the time `send()` is called. `x-sap-adt-sessiontype` is
|
|
18
|
+
* not sent at all: `request.stateful` is HTTP's to express, and over RFC
|
|
19
|
+
* Eclipse's JCo trace carries no session header — its stateful session is a
|
|
20
|
+
* connection of its own, not a header on the request. A transport that
|
|
18
21
|
* also captured cookies would be doing that work twice, and the two copies
|
|
19
22
|
* would disagree the first time one of them was cleared.
|
|
20
23
|
*/
|
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
|
|
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
|
-
>
|
|
5
|
-
>
|
|
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,39 @@ 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 its RFC conversation; `setSessionType()` adds no HTTP session-type header |
|
|
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
|
+
|
|
27
60
|
## Enabling Stateful Sessions
|
|
28
61
|
|
|
29
62
|
Every example below builds on this much, so it is stated once:
|
|
@@ -62,10 +95,10 @@ const connection = new AdtOnPremConnector(
|
|
|
62
95
|
);
|
|
63
96
|
await connection.connect(); // required before any request
|
|
64
97
|
|
|
65
|
-
// Enable stateful
|
|
98
|
+
// Enable the stateful HTTP context (session header and context cookie)
|
|
66
99
|
connection.setSessionType('stateful');
|
|
67
100
|
|
|
68
|
-
//
|
|
101
|
+
// Following requests use the stateful context until the mode is changed
|
|
69
102
|
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
70
103
|
|
|
71
104
|
// Switch back to stateless
|
|
@@ -178,7 +211,7 @@ wrong fixes:
|
|
|
178
211
|
|---|---|---|
|
|
179
212
|
| **the connection** | one TCP socket | you, the network, a timeout firing. **Ends nothing on the server** |
|
|
180
213
|
| **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
|
|
214
|
+
| **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
215
|
| **the lock** | an enqueue entry, **owned by the ABAP session** | that ABAP session ending. **When it goes, its locks go with it** |
|
|
183
216
|
|
|
184
217
|
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 (
|
|
253
|
+
### Stateful Mode (HTTP Context)
|
|
254
254
|
|
|
255
|
-
|
|
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
|
|
274
|
+
// Enable the stateful HTTP context (session header and context cookie)
|
|
271
275
|
connection.setSessionType('stateful');
|
|
272
276
|
|
|
273
|
-
//
|
|
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
|
|
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
|