@mcp-abap-adt/connection 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +274 -1
- package/README.md +60 -30
- package/dist/auth/IAuthProvider.d.ts +84 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -0
- package/dist/auth/IAuthProvider.js +21 -0
- package/dist/auth/providers.d.ts +95 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +140 -0
- package/dist/connection/AbstractAbapConnection.d.ts +222 -17
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +555 -31
- package/dist/connection/AdtCloudConnector.d.ts +29 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +31 -0
- package/dist/connection/AdtOnPremConnector.d.ts +30 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +32 -0
- package/dist/connection/BaseAbapConnection.d.ts +7 -1
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
- package/dist/connection/BaseAbapConnection.js +7 -1
- package/dist/connection/CertificateAbapConnection.d.ts +11 -1
- package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
- package/dist/connection/CertificateAbapConnection.js +13 -1
- package/dist/connection/CredentialAbapConnection.d.ts +88 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +195 -0
- package/dist/connection/JwtAbapConnection.d.ts +23 -7
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +26 -8
- package/dist/connection/KerberosAbapConnection.d.ts +9 -1
- package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
- package/dist/connection/KerberosAbapConnection.js +9 -1
- package/dist/connection/RfcAbapConnection.d.ts +0 -5
- package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
- package/dist/connection/RfcAbapConnection.js +0 -7
- package/dist/connection/SamlAbapConnection.d.ts +7 -1
- package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
- package/dist/connection/SamlAbapConnection.js +7 -1
- package/dist/connection/connectionFactory.d.ts +16 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -1
- package/dist/connection/connectionFactory.js +52 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
- package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
- package/dist/session/CloudSecuritySessionStrategy.js +135 -0
- package/dist/session/IcfSessionStrategy.d.ts +27 -0
- package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
- package/dist/session/IcfSessionStrategy.js +62 -0
- package/dist/session/SessionLifecycle.d.ts +17 -2
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +17 -2
- package/dist/session/SessionStrategy.d.ts +86 -0
- package/dist/session/SessionStrategy.d.ts.map +1 -0
- package/dist/session/SessionStrategy.js +33 -0
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +24 -0
- package/dist/utils/timeouts.d.ts +16 -0
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +19 -0
- package/docs/INSTALLATION.md +6 -2
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +77 -11
- package/docs/USAGE.md +92 -22
- package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,0FAA0F;IAC1F,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC9B,iFAAiF;IACjF,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,mBAAmB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,GAAG;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAMvC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAK;IAErB,OAAO,CAAC,IAAI,CAAuC;IACnD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAAiC;IAIpD,4EAA4E;IAC5E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAM5B;IAED,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;;;;;;;;;OAaG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED;;;;;OAKG;IACH,aAAa,CAAC,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GAAG,IAAI;IAOzE;;;;;;OAMG;IACH,cAAc,IAAI,IAAI;IAItB,gBAAgB,IAAI,IAAI;IAKxB;;;;;;OAMG;IACH,OAAO,CACL,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,WAAW,GAAG,aAAa,GAAG,UAAU;IAiB3C,YAAY,IAAI,IAAI;IAMpB,4EAA4E;IAC5E,YAAY,IAAI,YAAY;IAiB5B,2FAA2F;IAC3F,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO;IAM3D,aAAa,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,oBAAoB,GAAG,IAAI;IAelE,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAID;;;;;;;;;OASG;IACH,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAuBvE"}
|
|
@@ -8,8 +8,23 @@
|
|
|
8
8
|
* this unit testable without a server, and it is where the hard part lives:
|
|
9
9
|
* ordering, admission, and telling one session from the next.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* What it guarantees, since the design note it used to point at lived in another
|
|
12
|
+
* package and has been deleted with the work it described:
|
|
13
|
+
*
|
|
14
|
+
* - **One transition at a time**, on a serializing tail. Connects join
|
|
15
|
+
* connects and disconnects join disconnects, so concurrent callers share one
|
|
16
|
+
* establishment or one teardown rather than racing to build and demolish the
|
|
17
|
+
* same session.
|
|
18
|
+
* - **Admission is synchronous.** A request is admitted, or refused, in the
|
|
19
|
+
* same step that counts it — so nothing can be admitted and still be
|
|
20
|
+
* invisible to a teardown draining at that instant.
|
|
21
|
+
* - **A teardown waits for nothing.** In-flight requests run to completion
|
|
22
|
+
* untouched; their results are fenced by session generation so a late answer
|
|
23
|
+
* cannot write over a session established since.
|
|
24
|
+
* - **Identity is observed, never assumed.** A changed fingerprint means the
|
|
25
|
+
* session we had is gone and this is a different one, which is fatal to
|
|
26
|
+
* anything held against the old one;
|
|
27
|
+
* an unchanged one says nothing about whether the server still has it.
|
|
13
28
|
*/
|
|
14
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
30
|
exports.SessionLifecycle = void 0;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a session is opened on the server, and how the server is told we are
|
|
3
|
+
* done with it.
|
|
4
|
+
*
|
|
5
|
+
* Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
|
|
6
|
+
* resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
|
|
7
|
+
* response publishes its own address, which is what a `DELETE` is sent to. On
|
|
8
|
+
* on-prem there is no such resource — the logon call is the establishing
|
|
9
|
+
* request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
|
|
10
|
+
* given back.
|
|
11
|
+
*
|
|
12
|
+
* **Both are notifications, not commands.** They say "we have finished with
|
|
13
|
+
* this session". Whether the system frees it now, later, or keeps it to reuse
|
|
14
|
+
* is the system's business, and nothing here checks afterwards or depends on
|
|
15
|
+
* the answer. Measured on a trial: the `DELETE` is answered `200` and the
|
|
16
|
+
* session is still listed a moment later; the logoff is answered `200` and it
|
|
17
|
+
* is not. Neither is a failure — they are two systems deciding differently
|
|
18
|
+
* about a message they both accepted.
|
|
19
|
+
*
|
|
20
|
+
* Split by what the server publishes rather than by which system we think we
|
|
21
|
+
* are talking to: `openSession()` asks, and a system that has no such resource
|
|
22
|
+
* says so. Guessing the platform would be a guess; asking is not.
|
|
23
|
+
*/
|
|
24
|
+
import type { ILogger } from '../logger.js';
|
|
25
|
+
/** What a strategy needs from the connection to talk to the server. */
|
|
26
|
+
export interface ISessionTransport {
|
|
27
|
+
/** Absolute base URL of the system. */
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
/** Auth headers for a request made outside the normal request path. */
|
|
30
|
+
authHeaders(): Promise<Record<string, string>>;
|
|
31
|
+
/** Cookies held for this connection, or null before any have arrived. */
|
|
32
|
+
cookies(): string | null;
|
|
33
|
+
/** The CSRF token held, if one has been fetched. */
|
|
34
|
+
csrfToken(): string | null;
|
|
35
|
+
/** Issue a raw request and hand back status, headers and body. */
|
|
36
|
+
send(request: {
|
|
37
|
+
method: 'GET' | 'DELETE';
|
|
38
|
+
url: string;
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* A deadline for the REQUEST, and only where one belongs.
|
|
42
|
+
*
|
|
43
|
+
* The open is a preflight that `connect()` waits for, so it is bounded.
|
|
44
|
+
* The close must not be: a request timeout aborts the socket, which would
|
|
45
|
+
* cancel the very message it was waiting for and leave the session open —
|
|
46
|
+
* the caller's own deadline bounds the WAIT instead, elsewhere.
|
|
47
|
+
*/
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the cookies this answers with become the connection's.
|
|
51
|
+
*
|
|
52
|
+
* True for the open — its `SAP_SESSIONID` **is** the session. False for a
|
|
53
|
+
* close: a teardown's response must not feed the session identity, or the
|
|
54
|
+
* cookies it sets read as the session having been replaced under a
|
|
55
|
+
* connection that is on its way out.
|
|
56
|
+
*/
|
|
57
|
+
adoptCookies?: boolean;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
status: number;
|
|
60
|
+
headers: unknown;
|
|
61
|
+
data: unknown;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export declare abstract class SessionStrategy {
|
|
65
|
+
protected readonly logger: ILogger | null;
|
|
66
|
+
constructor(logger: ILogger | null);
|
|
67
|
+
/** A name for logs, so which one ran is never a guess. */
|
|
68
|
+
abstract readonly kind: 'cloud-security-session' | 'icf';
|
|
69
|
+
/**
|
|
70
|
+
* Ask the server to open a session, before anything needs one.
|
|
71
|
+
*
|
|
72
|
+
* Returns whether a session resource was actually opened and its address
|
|
73
|
+
* captured — `false` is not a failure, it is a system saying it has none, and
|
|
74
|
+
* the close then falls to the mechanism that needs no address. Never throws: a system that
|
|
75
|
+
* will not open one is a fact for the establishment that follows to report,
|
|
76
|
+
* with the whole picture, not a transport error raised from a preflight.
|
|
77
|
+
*/
|
|
78
|
+
abstract openSession(transport: ISessionTransport): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Tell the server we are done with the session.
|
|
81
|
+
*
|
|
82
|
+
* Never throws, and says nothing about what the server then did with it.
|
|
83
|
+
*/
|
|
84
|
+
abstract closeSession(transport: ISessionTransport): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=SessionStrategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionStrategy.d.ts","sourceRoot":"","sources":["../../src/session/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,yEAAyE;IACzE,OAAO,IAAI,MAAM,GAAG,IAAI,CAAC;IACzB,oDAAoD;IACpD,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,EAAE,KAAK,GAAG,QAAQ,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC;;;;;;;WAOG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB;;;;;;;WAOG;QACH,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,8BAAsB,eAAe;IACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;gBAAtB,MAAM,EAAE,OAAO,GAAG,IAAI;IAErD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,GAAG,KAAK,CAAC;IAEzD;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAEpE;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CACnE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* How a session is opened on the server, and how the server is told we are
|
|
4
|
+
* done with it.
|
|
5
|
+
*
|
|
6
|
+
* Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
|
|
7
|
+
* resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
|
|
8
|
+
* response publishes its own address, which is what a `DELETE` is sent to. On
|
|
9
|
+
* on-prem there is no such resource — the logon call is the establishing
|
|
10
|
+
* request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
|
|
11
|
+
* given back.
|
|
12
|
+
*
|
|
13
|
+
* **Both are notifications, not commands.** They say "we have finished with
|
|
14
|
+
* this session". Whether the system frees it now, later, or keeps it to reuse
|
|
15
|
+
* is the system's business, and nothing here checks afterwards or depends on
|
|
16
|
+
* the answer. Measured on a trial: the `DELETE` is answered `200` and the
|
|
17
|
+
* session is still listed a moment later; the logoff is answered `200` and it
|
|
18
|
+
* is not. Neither is a failure — they are two systems deciding differently
|
|
19
|
+
* about a message they both accepted.
|
|
20
|
+
*
|
|
21
|
+
* Split by what the server publishes rather than by which system we think we
|
|
22
|
+
* are talking to: `openSession()` asks, and a system that has no such resource
|
|
23
|
+
* says so. Guessing the platform would be a guess; asking is not.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.SessionStrategy = void 0;
|
|
27
|
+
class SessionStrategy {
|
|
28
|
+
logger;
|
|
29
|
+
constructor(logger) {
|
|
30
|
+
this.logger = logger;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.SessionStrategy = SessionStrategy;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join two `Cookie` header values, later wins on a repeated name.
|
|
3
|
+
*
|
|
4
|
+
* Used wherever a request already carries cookies and more are added: a
|
|
5
|
+
* credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
|
|
6
|
+
* so writing the session jar over the header drops the very thing the request
|
|
7
|
+
* authenticates with. Both belong on the wire: one says who we are, the other
|
|
8
|
+
* which session we are in. A name in both is the session's, because that is the
|
|
9
|
+
* one the server just issued.
|
|
10
|
+
*/
|
|
11
|
+
export declare function mergeCookieHeaders(existing: string | undefined, incoming: string | undefined): string;
|
|
12
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/utils/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CASR"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeCookieHeaders = mergeCookieHeaders;
|
|
4
|
+
/**
|
|
5
|
+
* Join two `Cookie` header values, later wins on a repeated name.
|
|
6
|
+
*
|
|
7
|
+
* Used wherever a request already carries cookies and more are added: a
|
|
8
|
+
* credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
|
|
9
|
+
* so writing the session jar over the header drops the very thing the request
|
|
10
|
+
* authenticates with. Both belong on the wire: one says who we are, the other
|
|
11
|
+
* which session we are in. A name in both is the session's, because that is the
|
|
12
|
+
* one the server just issued.
|
|
13
|
+
*/
|
|
14
|
+
function mergeCookieHeaders(existing, incoming) {
|
|
15
|
+
const parts = [existing, incoming].filter(Boolean).join('; ');
|
|
16
|
+
const byName = new Map();
|
|
17
|
+
for (const part of parts.split(';')) {
|
|
18
|
+
const pair = part.trim();
|
|
19
|
+
if (!pair)
|
|
20
|
+
continue;
|
|
21
|
+
byName.set(pair.slice(0, pair.indexOf('=')), pair);
|
|
22
|
+
}
|
|
23
|
+
return [...byName.values()].join('; ');
|
|
24
|
+
}
|
package/dist/utils/timeouts.d.ts
CHANGED
|
@@ -12,4 +12,20 @@ export declare function getTimeout(type?: 'default' | 'csrf' | 'long' | number):
|
|
|
12
12
|
* permanently dead socket hanging forever.
|
|
13
13
|
*/
|
|
14
14
|
export declare function getCriticalSectionTimeout(): number;
|
|
15
|
+
/**
|
|
16
|
+
* How long `disconnect()` may spend telling the server the session is done.
|
|
17
|
+
*
|
|
18
|
+
* **Zero by default: after a disconnect nothing waits on the answer.** Waiting
|
|
19
|
+
* is for steps whose successor depends on the server having caught up — lock,
|
|
20
|
+
* update, unlock, activate, each needing the one before it to have landed.
|
|
21
|
+
* A teardown has no successor: the session is marked unneeded, the server
|
|
22
|
+
* reclaims it whenever it reclaims it, and a caller blocked on that round trip
|
|
23
|
+
* has bought nothing while holding up the serializing tail, where every later
|
|
24
|
+
* connect and disconnect queues behind it.
|
|
25
|
+
*
|
|
26
|
+
* The knob exists for a caller that wants a bounded wait anyway — a test
|
|
27
|
+
* asserting the logoff landed, a script that would rather see the failure —
|
|
28
|
+
* and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getReleaseDeadline(): number;
|
|
15
31
|
//# sourceMappingURL=timeouts.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
|
|
1
|
+
{"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
|
package/dist/utils/timeouts.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getTimeoutConfig = getTimeoutConfig;
|
|
4
4
|
exports.getTimeout = getTimeout;
|
|
5
5
|
exports.getCriticalSectionTimeout = getCriticalSectionTimeout;
|
|
6
|
+
exports.getReleaseDeadline = getReleaseDeadline;
|
|
6
7
|
function getTimeoutConfig() {
|
|
7
8
|
const defaultTimeout = parseInt(process.env.SAP_TIMEOUT_DEFAULT || '45000', 10);
|
|
8
9
|
const csrfTimeout = parseInt(process.env.SAP_TIMEOUT_CSRF || '15000', 10);
|
|
@@ -32,3 +33,21 @@ function getTimeout(type = 'default') {
|
|
|
32
33
|
function getCriticalSectionTimeout() {
|
|
33
34
|
return parseInt(process.env.SAP_TIMEOUT_CRITICAL || '600000', 10);
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* How long `disconnect()` may spend telling the server the session is done.
|
|
38
|
+
*
|
|
39
|
+
* **Zero by default: after a disconnect nothing waits on the answer.** Waiting
|
|
40
|
+
* is for steps whose successor depends on the server having caught up — lock,
|
|
41
|
+
* update, unlock, activate, each needing the one before it to have landed.
|
|
42
|
+
* A teardown has no successor: the session is marked unneeded, the server
|
|
43
|
+
* reclaims it whenever it reclaims it, and a caller blocked on that round trip
|
|
44
|
+
* has bought nothing while holding up the serializing tail, where every later
|
|
45
|
+
* connect and disconnect queues behind it.
|
|
46
|
+
*
|
|
47
|
+
* The knob exists for a caller that wants a bounded wait anyway — a test
|
|
48
|
+
* asserting the logoff landed, a script that would rather see the failure —
|
|
49
|
+
* and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
|
|
50
|
+
*/
|
|
51
|
+
function getReleaseDeadline() {
|
|
52
|
+
return parseInt(process.env.SAP_RELEASE_DEADLINE_MS || '0', 10);
|
|
53
|
+
}
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -163,7 +163,9 @@ const logger = {
|
|
|
163
163
|
debug: (msg) => console.log('[DEBUG]', msg),
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
const connection = createAbapConnection(config, logger
|
|
166
|
+
const connection = createAbapConnection(config, logger, undefined, undefined, {
|
|
167
|
+
system: 'onprem', // or 'cloud' — said by you, never detected
|
|
168
|
+
});
|
|
167
169
|
|
|
168
170
|
connection.connect()
|
|
169
171
|
.then(() =>
|
|
@@ -231,7 +233,9 @@ const logger: ILogger = {
|
|
|
231
233
|
debug: (msg: string) => console.log(msg),
|
|
232
234
|
};
|
|
233
235
|
|
|
234
|
-
const connection = createAbapConnection(config, logger
|
|
236
|
+
const connection = createAbapConnection(config, logger, undefined, undefined, {
|
|
237
|
+
system: 'onprem', // or 'cloud' — said by you, never detected
|
|
238
|
+
});
|
|
235
239
|
```
|
|
236
240
|
|
|
237
241
|
## Troubleshooting
|
package/docs/MIGRATION-2.0.md
CHANGED
|
@@ -46,7 +46,7 @@ later, on the first request, as an unrelated-looking 401.
|
|
|
46
46
|
|
|
47
47
|
### 3. Requests are refused after a teardown
|
|
48
48
|
|
|
49
|
-
`disconnect()`
|
|
49
|
+
`disconnect()` stops the connection from serving requests until the
|
|
50
50
|
next `connect()`. An in-flight request is not cut off and not waited for either:
|
|
51
51
|
it runs to completion, and its result is fenced so it cannot touch a session
|
|
52
52
|
established since.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Migrating to 5.0.0
|
|
2
|
+
|
|
3
|
+
Two things changed, and the second is why the first was possible.
|
|
4
|
+
|
|
5
|
+
**A connection now closes what it opened.** `disconnect()` tells the server the
|
|
6
|
+
session is finished instead of only dropping the cookie, and `connect()` fails
|
|
7
|
+
when the server opened no session rather than handing back a connection whose
|
|
8
|
+
first lock would be dead.
|
|
9
|
+
|
|
10
|
+
**The class you take says which system you are dialling, not which credential
|
|
11
|
+
you hold.** `AdtOnPremConnector` and `AdtCloudConnector` are handed an auth
|
|
12
|
+
provider; the five auth classes still work and are deprecated.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## If you use `createAbapConnection()`
|
|
17
|
+
|
|
18
|
+
It still works, unchanged, and now warns once per call that the connection is
|
|
19
|
+
being chosen from `authType`. Say which system instead:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// before
|
|
23
|
+
const conn = createAbapConnection(config, logger, undefined, tokenRefresher);
|
|
24
|
+
|
|
25
|
+
// after
|
|
26
|
+
const conn = createAbapConnection(config, logger, undefined, tokenRefresher, {
|
|
27
|
+
system: 'cloud', // or 'onprem'
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Nothing is detected. The two systems do not manage sessions the same way, and
|
|
32
|
+
asking the server which it is does not work: `/sap/bc/adt/core/http/sessions`
|
|
33
|
+
answers on on-prem too, and its `DELETE` there leaves the session open while the
|
|
34
|
+
platform logoff removes it. Only you know where you are pointing.
|
|
35
|
+
|
|
36
|
+
## If you build a connection class directly
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// before // after
|
|
40
|
+
new BaseAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new BasicAuthProvider(user, pass), log)
|
|
41
|
+
new JwtAbapConnection(cfg, log, id, refr) new AdtCloudConnector(cfg, new TokenAuthProvider(refr), log)
|
|
42
|
+
new SamlAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new SamlAuthProvider(cookies), log)
|
|
43
|
+
new CertificateAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new CertificateAuthProvider(loader, cfg), log)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`KerberosAbapConnection` has no provider yet — its SPNEGO exchange *is* the CSRF
|
|
47
|
+
fetch — so keep using the class. See #35.
|
|
48
|
+
|
|
49
|
+
The old classes are not removed and not scheduled for removal here.
|
|
50
|
+
|
|
51
|
+
## `reset()` is gone — BREAKING
|
|
52
|
+
|
|
53
|
+
There is no local-only discard, because there is no local-only session: it lives
|
|
54
|
+
on the server, and dropping the cookie leaves it there.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
conn.reset(); // before
|
|
58
|
+
await conn.disconnect(); // after — and connect() again to carry on
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A caller that does not want to wait simply does not `await` it, which is what
|
|
62
|
+
`reset()` was mostly used for. `RfcAbapConnection` keeps `close()`.
|
|
63
|
+
|
|
64
|
+
## `connect()` can now fail where it used to warn — BREAKING
|
|
65
|
+
|
|
66
|
+
When the server authenticates the request but opens no session, `connect()`
|
|
67
|
+
rejects with `ADT_NOT_CONNECTED` and a message saying what happened, what still
|
|
68
|
+
works, and the usual cause. It used to log a warning and hand the connection
|
|
69
|
+
back — and the failure then surfaced a request later, as `400 Session not found`
|
|
70
|
+
with the object half-edited.
|
|
71
|
+
|
|
72
|
+
Verified rather than assumed: a connection that received no `SAP_SESSIONID` is
|
|
73
|
+
listed in the server's own session list as **nothing at all**.
|
|
74
|
+
|
|
75
|
+
Nothing is retried on your behalf. Whether to wait, retry, or release sessions
|
|
76
|
+
the user still holds depends on things only you know.
|
|
77
|
+
|
|
78
|
+
## `disconnect()` now makes a network call
|
|
79
|
+
|
|
80
|
+
It tells the server the session is finished. Two consequences:
|
|
81
|
+
|
|
82
|
+
- **It can wait.** By default it does not: `SAP_RELEASE_DEADLINE_MS` is `0`,
|
|
83
|
+
because waiting is for steps whose successor needs the server to have caught
|
|
84
|
+
up, and a teardown has none. Pass `disconnect({ deadlineMs })` if you want a
|
|
85
|
+
bounded wait — the deadline bounds the **wait**, never the request.
|
|
86
|
+
- **Requests still in flight will start failing.** The session they are running
|
|
87
|
+
on is the one being released. That is you having asked to disconnect, not a
|
|
88
|
+
race — finish or abandon your chains first.
|
|
89
|
+
|
|
90
|
+
It still never throws, including on a nonsense `deadlineMs`: its place is a
|
|
91
|
+
`finally`, where an exception would replace the error that sent you there.
|
|
92
|
+
|
|
93
|
+
## Token renewal, if you use a provider
|
|
94
|
+
|
|
95
|
+
Nothing to do, but worth knowing what changed. The connector no longer caches
|
|
96
|
+
the token it was given. It asks the provider per request — cheap, because the
|
|
97
|
+
provider caches — and on a `401` it tells the provider its answer was refused
|
|
98
|
+
(`refreshToken()`, per that contract), asks again, and only if the answer
|
|
99
|
+
changed rebuilds the session and retries once. An unchanged answer means the
|
|
100
|
+
server refused those credentials, and the `401` reaches you.
|
|
101
|
+
|
|
102
|
+
So a password does the right thing with no configuration, and a bearer token is
|
|
103
|
+
renewed without a class of its own.
|
|
104
|
+
|
|
105
|
+
## Sessions are a shared, per-user resource
|
|
106
|
+
|
|
107
|
+
Not an API change, a fact worth having. The limit is per user and the pool is
|
|
108
|
+
shared with everything else logged on as them, a SAP GUI session included. The
|
|
109
|
+
timeout is **idle-based**: a connection that keeps working keeps its session,
|
|
110
|
+
one that goes quiet past the window loses it. There is no keepalive timer here,
|
|
111
|
+
deliberately — how long to hold a session is yours to decide.
|
|
112
|
+
|
|
113
|
+
And the shape of the problem this release addresses, seen from the other end: a
|
|
114
|
+
process that opens a connection per check and never disconnects filled a
|
|
115
|
+
system's session list at one session every ten seconds. Releasing what you open
|
|
116
|
+
is half of it; opening one connection instead of one per check is the other.
|
|
@@ -29,7 +29,9 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
|
|
|
29
29
|
```ts
|
|
30
30
|
import { createAbapConnection } from '@mcp-abap-adt/connection';
|
|
31
31
|
|
|
32
|
-
const connection = createAbapConnection(config, logger
|
|
32
|
+
const connection = createAbapConnection(config, logger, undefined, undefined, {
|
|
33
|
+
system: 'onprem', // or 'cloud' — said by you, never detected
|
|
34
|
+
});
|
|
33
35
|
await connection.connect(); // required before any request
|
|
34
36
|
|
|
35
37
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
@@ -47,11 +49,11 @@ connection.setSessionType('stateless');
|
|
|
47
49
|
## Knowing Which Session You Are In
|
|
48
50
|
|
|
49
51
|
```ts
|
|
50
|
-
import {
|
|
52
|
+
import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
|
|
51
53
|
|
|
52
54
|
// getSessionIdentity() is on the HTTP connection classes, NOT on the
|
|
53
55
|
// IAbapConnection type that createAbapConnection() returns.
|
|
54
|
-
const connection = new
|
|
56
|
+
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
|
|
55
57
|
await connection.connect();
|
|
56
58
|
|
|
57
59
|
// Which SAP session this connection is talking to. Changes only when the
|
|
@@ -102,19 +104,83 @@ answer, not a credential one, and nothing is torn down for it.
|
|
|
102
104
|
|
|
103
105
|
---
|
|
104
106
|
|
|
107
|
+
## What This Layer Actually Solves
|
|
108
|
+
|
|
109
|
+
The link to the ABAP session is not guaranteed, and that is the whole problem:
|
|
110
|
+
|
|
111
|
+
- the ABAP session can be terminated by the system while this client still
|
|
112
|
+
believes it has one — the next lock-bound request answers
|
|
113
|
+
`400 "Session not found"`;
|
|
114
|
+
- or it is never created at all, because the system will not open another one
|
|
115
|
+
for this user right now.
|
|
116
|
+
|
|
117
|
+
Both are reported, neither is worked around. `connect()` fails with the reason
|
|
118
|
+
when no session was opened; a session lost afterwards surfaces as
|
|
119
|
+
`ADT_SESSION_REPLACED` rather than a request quietly continuing somewhere it
|
|
120
|
+
does not belong. What to do about either — wait, retry, reconnect, release
|
|
121
|
+
sessions the user still holds, carry on read-only — is the caller's decision,
|
|
122
|
+
made with the cookies and the answers in hand.
|
|
123
|
+
|
|
124
|
+
## A Lock Lives In The ABAP Session
|
|
125
|
+
|
|
126
|
+
There are two sessions here, and they are not the same thing:
|
|
127
|
+
|
|
128
|
+
- the **HTTP session** — the conversation this client is having. It exists as
|
|
129
|
+
long as the cookies do, and nothing about it is in doubt;
|
|
130
|
+
- the **ABAP session** — named by `SAP_SESSIONID_<SID>_<CLIENT>`, and the thing
|
|
131
|
+
locks are bound to.
|
|
132
|
+
|
|
133
|
+
The doubt is only ever about the second. It may not have been created, or it may
|
|
134
|
+
have been terminated while this client still holds the cookies and believes it
|
|
135
|
+
has one — and a lock is dead in either case, whether or not the client noticed.
|
|
136
|
+
|
|
137
|
+
Two ways to lose one, and they are different problems:
|
|
138
|
+
|
|
139
|
+
- **The server never opened one.** No `SAP_SESSIONID` came back, so there is no
|
|
140
|
+
ABAP session known to this connection and nothing a lock could be bound to —
|
|
141
|
+
while the HTTP side is perfectly fine, which is why it does not look like a
|
|
142
|
+
failure at all. `connect()` refuses rather than handing back a connection
|
|
143
|
+
whose first lock would be dead on arrival. Sessions are limited per user and shared with every other tool
|
|
144
|
+
logged on as them, so this says nothing about your code — it says the system
|
|
145
|
+
would not open another one right now.
|
|
146
|
+
- **It timed out while you were quiet.** The timeout is an idle one, and it is
|
|
147
|
+
the *silence* that spends it, not the elapsed time. Measured on an on-prem
|
|
148
|
+
system with a 30-minute window: a small request once a minute kept one session
|
|
149
|
+
alive for 45 minutes with its identity unchanged, straight past the mark.
|
|
150
|
+
|
|
151
|
+
So a long chain under a lock is safe while it is doing something, and at risk
|
|
152
|
+
while it waits. **Any request in the session resets the window** — a poll, a
|
|
153
|
+
read, a status check. There is deliberately no keepalive timer in this package:
|
|
154
|
+
holding a session alive means holding a scarce, shared slot, and deciding to do
|
|
155
|
+
that belongs to the caller who knows why the session is worth keeping.
|
|
156
|
+
|
|
157
|
+
**The cookies are the session.** Nothing else ties a caller to one, so a second
|
|
158
|
+
connection given the same cookie jar works in the same ABAP session and can use
|
|
159
|
+
the locks taken in it — that is what makes handing them over a way to continue
|
|
160
|
+
someone else's work. It cuts both ways: `disconnect()` ends the session for
|
|
161
|
+
everyone holding those cookies, not just for the object it was called on, and no
|
|
162
|
+
connection can see the copies. Deciding who may hold them, and who is allowed to
|
|
163
|
+
close, belongs to whoever passes them around.
|
|
164
|
+
|
|
165
|
+
The server never tells the client how long it has: the session cookie carries no
|
|
166
|
+
expiry and no response header mentions one. The only honest signals are the ones
|
|
167
|
+
you get by asking — `getSessionIdentity()` for which session you are in, and
|
|
168
|
+
`ADT_SESSION_REPLACED` when the one you were in is gone.
|
|
169
|
+
|
|
105
170
|
## Troubleshooting
|
|
106
171
|
|
|
107
|
-
- **CSRF token errors**: discard the session and establish a new one.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
- **CSRF token errors**: discard the session and establish a new one. That is
|
|
173
|
+
`disconnect()` followed by `connect()` — there is no local-only discard, because
|
|
174
|
+
dropping the cookie leaves the ABAP session open on the server. Both live on
|
|
175
|
+
the HTTP connection classes and are **not** on the `IAbapConnection` type
|
|
176
|
+
`createAbapConnection()` returns — reach them through a concrete type:
|
|
111
177
|
|
|
112
178
|
```ts
|
|
113
|
-
import {
|
|
179
|
+
import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
|
|
114
180
|
|
|
115
|
-
const connection = new
|
|
116
|
-
connection.
|
|
117
|
-
await connection.connect();
|
|
181
|
+
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
|
|
182
|
+
await connection.disconnect(); // ends the session on the server, then clears
|
|
183
|
+
await connection.connect(); // a new session, explicitly
|
|
118
184
|
```
|
|
119
185
|
- **Session expired**: reauthenticate to obtain a new session.
|
|
120
186
|
- **Multiple connections**: each `createAbapConnection` instance maintains its own cookie jar; share the instance if you need continuity.
|