@mcp-abap-adt/connection 4.0.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +407 -1
- package/README.md +239 -53
- package/dist/auth/providers.d.ts +124 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +183 -0
- package/dist/connection/AbstractAbapConnection.d.ts +209 -57
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +459 -457
- package/dist/connection/AdtCloudConnector.d.ts +34 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +27 -0
- package/dist/connection/AdtOnPremConnector.d.ts +43 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +28 -0
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/connection/CloudHttpTransport.js +145 -0
- package/dist/connection/CredentialAbapConnection.d.ts +55 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +128 -0
- package/dist/connection/HttpTransport.d.ts +178 -0
- package/dist/connection/HttpTransport.d.ts.map +1 -0
- package/dist/connection/HttpTransport.js +402 -0
- package/dist/connection/IAdtTransport.d.ts +232 -0
- package/dist/connection/IAdtTransport.d.ts.map +1 -0
- package/dist/connection/IAdtTransport.js +28 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
- package/dist/connection/OnPremHttpTransport.d.ts +45 -0
- package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/OnPremHttpTransport.js +91 -0
- package/dist/connection/RfcTransport.d.ts +89 -0
- package/dist/connection/RfcTransport.d.ts.map +1 -0
- package/dist/connection/RfcTransport.js +256 -0
- package/dist/connection/rfcConversation.d.ts +44 -0
- package/dist/connection/rfcConversation.d.ts.map +1 -0
- package/dist/connection/rfcConversation.js +71 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -18
- package/dist/session/SessionLifecycle.d.ts +17 -2
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +17 -2
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +24 -0
- package/dist/utils/timeouts.d.ts +6 -3
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -3
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -6
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
- package/docs/USAGE.md +322 -111
- package/examples/basic-connection.js +15 -3
- package/examples/jwt-with-token-refresh.js +15 -7
- package/examples/saml-connection.js +15 -2
- package/package.json +12 -10
- package/dist/__tests__/helpers/session.d.ts +0 -15
- package/dist/__tests__/helpers/session.d.ts.map +0 -1
- package/dist/__tests__/helpers/session.js +0 -19
- package/dist/connection/BaseAbapConnection.d.ts +0 -23
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -75
- package/dist/connection/CertificateAbapConnection.d.ts +0 -25
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -79
- package/dist/connection/JwtAbapConnection.d.ts +0 -115
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -358
- package/dist/connection/KerberosAbapConnection.d.ts +0 -24
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -120
- package/dist/connection/RfcAbapConnection.d.ts +0 -49
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -331
- package/dist/connection/SamlAbapConnection.d.ts +0 -25
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -75
- package/dist/connection/connectionFactory.d.ts +0 -9
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- package/dist/connection/connectionFactory.js +0 -32
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The credentials a connector can be handed.
|
|
4
|
+
*
|
|
5
|
+
* Each is the authentication half of one of the connection classes that used to
|
|
6
|
+
* carry both halves, lifted out unchanged — the header a `BasicAuthProvider`
|
|
7
|
+
* builds is byte for byte what `BaseAbapConnection` built, and the TLS options a
|
|
8
|
+
* `CertificateAuthProvider` returns are what `CertificateAbapConnection`
|
|
9
|
+
* returned. Nothing about how a credential works changed; only who owns it.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.CertificateAuthProvider = exports.SamlAuthProvider = exports.TokenAuthProvider = exports.BasicAuthProvider = void 0;
|
|
13
|
+
/** Username and password, as `Basic base64(user:pass)`. */
|
|
14
|
+
class BasicAuthProvider {
|
|
15
|
+
username;
|
|
16
|
+
password;
|
|
17
|
+
kind = 'basic';
|
|
18
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
19
|
+
async prepare() { }
|
|
20
|
+
/** Not cookies. This one authenticates with a header. */
|
|
21
|
+
cookies() {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
25
|
+
transportMaterial() {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
constructor(username, password) {
|
|
29
|
+
this.username = username;
|
|
30
|
+
this.password = password;
|
|
31
|
+
}
|
|
32
|
+
async authorizationHeader() {
|
|
33
|
+
return `Basic ${Buffer.from(`${this.username ?? ''}:${this.password ?? ''}`).toString('base64')}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.BasicAuthProvider = BasicAuthProvider;
|
|
37
|
+
/**
|
|
38
|
+
* A bearer token, kept current by whoever issued it.
|
|
39
|
+
*
|
|
40
|
+
* Takes an `ITokenRefresher` — the contract `@mcp-abap-adt/auth-broker` already
|
|
41
|
+
* produces and `@mcp-abap-adt/auth-providers` already implements twelve ways.
|
|
42
|
+
* This package depends on neither: it speaks the contract, and the consumer
|
|
43
|
+
* brings the implementation, exactly as it does for `IAbapConnection`.
|
|
44
|
+
*
|
|
45
|
+
* **This carries no session recovery.** What happens to a session when the
|
|
46
|
+
* credential behind it is renewed mid-flight is the connection's business and
|
|
47
|
+
* stays there; see `JwtAbapConnection`, which still owns that machinery.
|
|
48
|
+
*/
|
|
49
|
+
class TokenAuthProvider {
|
|
50
|
+
source;
|
|
51
|
+
kind = 'token';
|
|
52
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
53
|
+
async prepare() { }
|
|
54
|
+
/** Not cookies. This one authenticates with a header. */
|
|
55
|
+
cookies() {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
59
|
+
transportMaterial() {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A token, or something that can produce one.
|
|
64
|
+
*
|
|
65
|
+
* A bare string is a token with no renewal behind it — honest, and fine for a
|
|
66
|
+
* short task. An `ITokenRefresher` or a function is a provider that checks
|
|
67
|
+
* expiry and renews on its own, which is where this belongs in a long-lived
|
|
68
|
+
* process.
|
|
69
|
+
*/
|
|
70
|
+
constructor(source) {
|
|
71
|
+
this.source = source;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Asked every time, and nothing kept.
|
|
75
|
+
*
|
|
76
|
+
* The provider behind this already caches the token, knows when it expires,
|
|
77
|
+
* and renews before handing one back. A second cache here would serve the
|
|
78
|
+
* stale one and hide exactly the renewal the provider exists to do — which
|
|
79
|
+
* is what the first version of this class did.
|
|
80
|
+
*/
|
|
81
|
+
async authorizationHeader() {
|
|
82
|
+
const token = typeof this.source === 'string'
|
|
83
|
+
? this.source
|
|
84
|
+
: typeof this.source === 'function'
|
|
85
|
+
? await this.source()
|
|
86
|
+
: await this.source.getToken();
|
|
87
|
+
return token ? `Bearer ${token}` : '';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The token was refused, so force a new one.
|
|
91
|
+
*
|
|
92
|
+
* Declared through `IRenewableCredential` rather than as an optional member
|
|
93
|
+
* of every credential, so a consumer narrows to it: a password has nothing
|
|
94
|
+
* behind it to ask again, and a SAML session was negotiated elsewhere.
|
|
95
|
+
*
|
|
96
|
+
* **Nothing in this package calls it.** A token that EXPIRED is replaced
|
|
97
|
+
* inside `authorizationHeader()` above, which is asked per request — nobody
|
|
98
|
+
* decides anything. This is the other case, a token the source still believes
|
|
99
|
+
* in and the server refuses, and whether a refusal meant that is a judgement
|
|
100
|
+
* made with what the caller knows. So the refusal surfaces, and this is the
|
|
101
|
+
* seam the caller decides with.
|
|
102
|
+
*
|
|
103
|
+
* `getToken()` is documented to return the cached token while it believes it
|
|
104
|
+
* is still valid — which is exactly the situation after a 401 on a token the
|
|
105
|
+
* provider has not yet noticed is dead. `refreshToken()` is the contract's
|
|
106
|
+
* answer for that, and this is the only place it is called.
|
|
107
|
+
*/
|
|
108
|
+
async renew() {
|
|
109
|
+
if (typeof this.source === 'string' || typeof this.source === 'function') {
|
|
110
|
+
// Nothing to force: a fixed token has no source, and a function is asked
|
|
111
|
+
// afresh every time anyway.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await this.source.refreshToken();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.TokenAuthProvider = TokenAuthProvider;
|
|
118
|
+
/**
|
|
119
|
+
* A SAML session, already negotiated, presented as cookies.
|
|
120
|
+
*
|
|
121
|
+
* No `Authorization` header at all — the cookies are the credential, and they
|
|
122
|
+
* are added by the connection alongside its own.
|
|
123
|
+
*/
|
|
124
|
+
class SamlAuthProvider {
|
|
125
|
+
sessionCookies;
|
|
126
|
+
kind = 'saml';
|
|
127
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
128
|
+
async prepare() { }
|
|
129
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
130
|
+
transportMaterial() {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
constructor(sessionCookies) {
|
|
134
|
+
this.sessionCookies = sessionCookies;
|
|
135
|
+
}
|
|
136
|
+
/** Not a header: this credential authenticates with the cookies below. */
|
|
137
|
+
async authorizationHeader() {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/** The cookies to present. */
|
|
141
|
+
cookies() {
|
|
142
|
+
return this.sessionCookies;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.SamlAuthProvider = SamlAuthProvider;
|
|
146
|
+
/**
|
|
147
|
+
* Client certificate: the credential lives in the TLS handshake.
|
|
148
|
+
*
|
|
149
|
+
* `prepare()` is where the material is read, which is why it exists at all —
|
|
150
|
+
* building an agent before it is loaded is what used to reject a connect
|
|
151
|
+
* before a single request went out.
|
|
152
|
+
*/
|
|
153
|
+
class CertificateAuthProvider {
|
|
154
|
+
loader;
|
|
155
|
+
config;
|
|
156
|
+
kind = 'certificate';
|
|
157
|
+
/** Not cookies. This one authenticates with a header. */
|
|
158
|
+
cookies() {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
material = null;
|
|
162
|
+
constructor(loader, config) {
|
|
163
|
+
this.loader = loader;
|
|
164
|
+
this.config = config;
|
|
165
|
+
}
|
|
166
|
+
async prepare() {
|
|
167
|
+
if (!this.material) {
|
|
168
|
+
this.material = await this.loader.load(this.config);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Not a header: this credential authenticates through TLS. */
|
|
172
|
+
async authorizationHeader() {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
transportMaterial() {
|
|
176
|
+
if (!this.material) {
|
|
177
|
+
throw new Error('CertificateAuthProvider: certificate material not loaded. connect() prepares it; a request before that has nothing to present.');
|
|
178
|
+
}
|
|
179
|
+
const { cert, key, pfx, passphrase } = this.material;
|
|
180
|
+
return { cert, key, pfx, passphrase };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.CertificateAuthProvider = CertificateAuthProvider;
|
|
@@ -3,14 +3,41 @@ import type { SapConfig } from '../config/sapConfig.js';
|
|
|
3
3
|
import type { ILogger } from '../logger.js';
|
|
4
4
|
import { SessionLifecycle } from '../session/SessionLifecycle.js';
|
|
5
5
|
import type { AbapConnection, AbapRequestOptions } from './AbapConnection.js';
|
|
6
|
+
import { type IAdtSessionContext, type IAdtTransport } from './IAdtTransport.js';
|
|
6
7
|
/**
|
|
7
8
|
* Declares the capabilities explicitly rather than satisfying them by accident.
|
|
8
9
|
* `AbapConnection` is the base contract every transport honours; these two are
|
|
9
10
|
* the HTTP session's own, and naming them means a signature that drifts from
|
|
10
11
|
* the published contract fails to compile here instead of at the consumer.
|
|
12
|
+
*
|
|
13
|
+
* **This gives the consumer instruments; it does not decide for it.** `connect()`
|
|
14
|
+
* opens one session and `disconnect()` closes it. How many connections to hold,
|
|
15
|
+
* how long to hold them and when to let go stays with the caller — there are no
|
|
16
|
+
* thresholds here, no pooling and no eviction, because none of that is knowable
|
|
17
|
+
* from inside a single connection. A session this one did not open is not its
|
|
18
|
+
* business: the session limit is per user and the pool is shared, so a SAP GUI
|
|
19
|
+
* logon of the same user sits in the same list.
|
|
20
|
+
*
|
|
21
|
+
* **Nothing the server decides is treated as something to count on.** Whether it
|
|
22
|
+
* issues a session cookie, whether it still holds a session it issued, how many
|
|
23
|
+
* it will tolerate, how fast it answers a logoff — all of that is its own and
|
|
24
|
+
* may differ by system and release. So each is observed and reported, never
|
|
25
|
+
* relied upon: the logoff is best effort under a bound the caller sets, a
|
|
26
|
+
* missing session cookie is a warning rather than a rule, and no code here
|
|
27
|
+
* counts sessions or predicts the next answer from the last one.
|
|
11
28
|
*/
|
|
12
29
|
declare abstract class AbstractAbapConnection implements AbapConnection, ISessionLifecycleAware {
|
|
13
30
|
private readonly config;
|
|
31
|
+
/**
|
|
32
|
+
* Required, and constructed by the caller.
|
|
33
|
+
*
|
|
34
|
+
* There is no default to fall back to and nothing is worked out from the
|
|
35
|
+
* config: the wire is a fact about the deployment, and a library that
|
|
36
|
+
* picked one would be guessing at the thing this design exists to stop
|
|
37
|
+
* guessing at. It also carries what the caller alone can wire — the
|
|
38
|
+
* credential's TLS material, the client — which is why it arrives built.
|
|
39
|
+
*/
|
|
40
|
+
readonly transport: IAdtTransport;
|
|
14
41
|
protected readonly logger: ILogger | null;
|
|
15
42
|
/**
|
|
16
43
|
* Owns session state, admission and teardown ordering. Composed rather than
|
|
@@ -18,40 +45,86 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
18
45
|
* transports share this unit instead of a base class.
|
|
19
46
|
*/
|
|
20
47
|
protected readonly lifecycle: SessionLifecycle;
|
|
21
|
-
private axiosInstance;
|
|
22
|
-
private csrfToken;
|
|
23
|
-
private cookies;
|
|
24
|
-
private cookieStore;
|
|
25
48
|
private baseUrl;
|
|
26
49
|
private sessionId;
|
|
27
50
|
private sessionMode;
|
|
28
|
-
private skipSessionType;
|
|
29
51
|
/**
|
|
30
52
|
* When true, requests are treated as part of an uninterruptible critical
|
|
31
53
|
* section (e.g. a lock → modify → unlock chain). In this state a short
|
|
32
|
-
* per-request timeout must NOT abort the request mid-flight
|
|
33
|
-
*
|
|
34
|
-
*
|
|
54
|
+
* per-request timeout must NOT abort the request mid-flight: the abort ends
|
|
55
|
+
* nothing server-side — only the server ends an ABAP session, on an idle
|
|
56
|
+
* timeout this side cannot see — it ends what this side KNOWS. Whether the
|
|
57
|
+
* change was applied becomes unknowable, and the handle `unlock` needs is
|
|
58
|
+
* lost while the lock lives on in that session. While set, makeAdtRequest
|
|
35
59
|
* raises the effective timeout to CRITICAL_SECTION_TIMEOUT (a large ceiling)
|
|
36
60
|
* so the request runs to completion instead of being interrupted.
|
|
37
61
|
*/
|
|
38
62
|
private inCriticalSection;
|
|
39
63
|
/** Reference count for nested beginCriticalSection()/endCriticalSection() pairs. */
|
|
40
64
|
private criticalSectionDepth;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
/** The default release deadline, validated once at construction. */
|
|
66
|
+
/**
|
|
67
|
+
* The logoff for the session this connection last held, while it is on its
|
|
68
|
+
* way. One, because a connection holds one session: `connect()` opens it and
|
|
69
|
+
* `disconnect()` closes it, and how many connections to run is the caller's
|
|
70
|
+
* business, not something to be tracked here.
|
|
71
|
+
*
|
|
72
|
+
* Carries the session id, not just the promise, so a release still in flight
|
|
73
|
+
* for an EARLIER session is recognised as not being this one's — reusing it
|
|
74
|
+
* was what left the second session of a reconnect never released at all.
|
|
75
|
+
*
|
|
76
|
+
* The id is the `SAP_SESSIONID` value — the ABAP session, the one locks are
|
|
77
|
+
* bound to — never the cookie header: that header also carries `sap-XSRF_*`,
|
|
78
|
+
* which rotates within one and the same session, so comparing headers made a
|
|
79
|
+
* session stop recognising itself after a token refresh.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* How this server opens and gives back a session, decided by asking it rather
|
|
83
|
+
* than by guessing which system it is. Set at establishment; until then the
|
|
84
|
+
* on-prem mechanism, which is the one that needs no resource.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* The application server this session lives on, as the server named it.
|
|
88
|
+
*
|
|
89
|
+
* A session belongs to ONE application server. On a multi-node system a
|
|
90
|
+
* request that lands on another gets another session — and a lock held on the
|
|
91
|
+
* first is then dead through nobody's fault and no inactivity. Eclipse pins
|
|
92
|
+
* itself with these headers; without them every request is a fresh throw of
|
|
93
|
+
* the dice.
|
|
94
|
+
*
|
|
95
|
+
* Learned from `sap-adt-saplb` on a response, sent back as `saplb`. Cleared
|
|
96
|
+
* with the rest of the session state: it names a server for a session that no
|
|
97
|
+
* longer exists.
|
|
98
|
+
*/
|
|
99
|
+
/**
|
|
100
|
+
* Whether the preflight opened a session of its own.
|
|
101
|
+
*
|
|
102
|
+
* Distinct from "there are cookies": a failed establishment often leaves a
|
|
103
|
+
* cookie from the 401 that rejected it, and that is debris, not a session.
|
|
104
|
+
* Only a preflight answered with a session address opened one, and only that
|
|
105
|
+
* is worth saying goodbye to when establishment then fails.
|
|
106
|
+
*/
|
|
107
|
+
protected constructor(config: SapConfig,
|
|
108
|
+
/**
|
|
109
|
+
* Required, and constructed by the caller.
|
|
110
|
+
*
|
|
111
|
+
* There is no default to fall back to and nothing is worked out from the
|
|
112
|
+
* config: the wire is a fact about the deployment, and a library that
|
|
113
|
+
* picked one would be guessing at the thing this design exists to stop
|
|
114
|
+
* guessing at. It also carries what the caller alone can wire — the
|
|
115
|
+
* credential's TLS material, the client — which is why it arrives built.
|
|
116
|
+
*/
|
|
117
|
+
transport: IAdtTransport, logger: ILogger | null, sessionId?: string);
|
|
44
118
|
/**
|
|
45
119
|
* Set session type (stateful or stateless)
|
|
46
120
|
* Controls whether x-sap-adt-sessiontype: stateful header is added to requests
|
|
47
121
|
* - stateful: SAP maintains session state between requests (locks, transactions)
|
|
48
122
|
* - stateless: Each request is independent
|
|
49
123
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* resulting in HTTP 423 on subsequent PUT requests.
|
|
124
|
+
* Whether the header actually goes out is the WIRE's: a system where the
|
|
125
|
+
* stateful header makes the server store locks in session memory instead of
|
|
126
|
+
* the enqueue table — BASIS 7.40 — is a different deployment, and a
|
|
127
|
+
* deployment is a transport, not a flag on the connection.
|
|
55
128
|
*/
|
|
56
129
|
setSessionType(type: 'stateful' | 'stateless'): void;
|
|
57
130
|
/**
|
|
@@ -65,9 +138,11 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
65
138
|
* in a finally, AFTER unlocking). While in a critical section, a short
|
|
66
139
|
* per-request timeout is not applied — makeAdtRequest uses a large ceiling
|
|
67
140
|
* (CRITICAL_SECTION_TIMEOUT, env SAP_TIMEOUT_CRITICAL) instead — so a slow
|
|
68
|
-
* PUT/activate/unlock is not aborted mid-flight.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
141
|
+
* PUT/activate/unlock is not aborted mid-flight. An abort does not end the
|
|
142
|
+
* ABAP session — that session is the server's, and only the server ends it,
|
|
143
|
+
* on an idle timeout this side cannot influence — it strands the caller:
|
|
144
|
+
* the outcome is unknown and the handle `unlock` needs is gone, while the
|
|
145
|
+
* lock survives inside the ABAP session that owns it.
|
|
71
146
|
*
|
|
72
147
|
* Nesting is reference-counted so nested begin/end pairs are safe.
|
|
73
148
|
*/
|
|
@@ -97,6 +172,27 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
97
172
|
* preparation and fetch the CSRF token; connect() owns everything around it.
|
|
98
173
|
*/
|
|
99
174
|
protected abstract establishSession(): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Gets the credential ready before anything is sent.
|
|
177
|
+
*
|
|
178
|
+
* A no-op for the auth types whose credential is already in hand — basic
|
|
179
|
+
* builds a header from the configuration, JWT carries a token it was given.
|
|
180
|
+
* It exists for the ones that have to fetch or load theirs, because the
|
|
181
|
+
* preflight now runs BEFORE `establishSession()` and needs a credential to
|
|
182
|
+
* go out with: a certificate connection reads its material there, and
|
|
183
|
+
* without this the preflight throws `certificate material not loaded` while
|
|
184
|
+
* assembling the transport — before a single request is made, on every
|
|
185
|
+
* system, cloud or on-prem.
|
|
186
|
+
*
|
|
187
|
+
* Must be idempotent: `establishSession()` may prepare the same credential
|
|
188
|
+
* again, and does.
|
|
189
|
+
*
|
|
190
|
+
* Kerberos deliberately does NOT implement it. Minting the SPNEGO token this
|
|
191
|
+
* early changes when the exchange happens, and that connection is not
|
|
192
|
+
* production-tested — its preflight fails the way it already did, is caught
|
|
193
|
+
* inside the strategy, and the connection falls back to ICF as before.
|
|
194
|
+
*/
|
|
195
|
+
protected prepareCredential(): Promise<void>;
|
|
100
196
|
/**
|
|
101
197
|
* Establishes the session, once, under the lifecycle.
|
|
102
198
|
*
|
|
@@ -110,16 +206,37 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
110
206
|
/**
|
|
111
207
|
* Tears the session down. Never throws, and always settles.
|
|
112
208
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
209
|
+
* Tells the server the session is no longer needed, then clears the local
|
|
210
|
+
* state. **When the server actually reclaims it is the server's business** —
|
|
211
|
+
* possibly not until the next connect asks it for one — and nothing here
|
|
212
|
+
* waits for that or depends on it. What matters is that the session stops
|
|
213
|
+
* being counted against the user, which dropping the cookie alone does not
|
|
214
|
+
* achieve: the server keeps it until its own timeout, so a process that
|
|
215
|
+
* connects repeatedly leaves one behind every time. Measured on S/4HANA on-prem, 25 connects in a row: with the logoff,
|
|
216
|
+
* 24-25 of them were given a session; without it, 2. A connection that gets
|
|
217
|
+
* no session still answers `200` to a LOCK and hands back a handle the next
|
|
218
|
+
* request cannot use, so the leak surfaces as a half-written object rather
|
|
219
|
+
* than as anything about sessions.
|
|
220
|
+
*
|
|
221
|
+
* **Nothing is waited for.** The logoff is dispatched, not awaited: this
|
|
222
|
+
* method notifies, and whether the server has acted on the notice is not
|
|
223
|
+
* something it reports. Finishing chains and releasing locks stay the
|
|
224
|
+
* caller's to do BEFORE calling — waiting here on a request that carries no
|
|
225
|
+
* timeout by design is what made a teardown unbounded, which blocks every
|
|
226
|
+
* later transition on the serialized tail.
|
|
117
227
|
*
|
|
118
|
-
* Requests already in flight
|
|
119
|
-
*
|
|
120
|
-
*
|
|
228
|
+
* **Requests already in flight are not waited for, and the logoff ends the
|
|
229
|
+
* session they are running on** — so they will start failing against a
|
|
230
|
+
* session that no longer exists. That is the caller having asked to
|
|
231
|
+
* disconnect, not a race, and it is a change from the version that only
|
|
232
|
+
* dropped the cookie. Generation fencing (see `SessionLifecycle.isCurrent`)
|
|
233
|
+
* keeps their results from reaching this connection either way.
|
|
121
234
|
*
|
|
122
|
-
*
|
|
235
|
+
* **Nothing to wait for.** This TELLS the server the session is finished and
|
|
236
|
+
* does not act on the answer — whether and when the session is freed is the
|
|
237
|
+
* server's affair. Waiting for a reply nobody reads was also the one thing
|
|
238
|
+
* that could make a teardown unbounded, since the goodbye carries no request
|
|
239
|
+
* timeout by design.
|
|
123
240
|
*/
|
|
124
241
|
disconnect(): Promise<void>;
|
|
125
242
|
isConnected(): boolean;
|
|
@@ -131,14 +248,9 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
131
248
|
* session cookie. Use {@link isConnected} for connection state.
|
|
132
249
|
*
|
|
133
250
|
* It follows that null → non-null is not a replacement but an identity being
|
|
134
|
-
* learned; only a CHANGED value means the session
|
|
251
|
+
* learned; only a CHANGED value means the session we had is gone.
|
|
135
252
|
*/
|
|
136
253
|
getSessionIdentity(): string | null;
|
|
137
|
-
/**
|
|
138
|
-
* Discards the session at a caller's request: cancels queued recoveries and
|
|
139
|
-
* queues the cleanup rather than tearing down under a live request.
|
|
140
|
-
*/
|
|
141
|
-
reset(): void;
|
|
142
254
|
/**
|
|
143
255
|
* Re-establishes the session for a request that is recovering from a
|
|
144
256
|
* credential renewal, then lets that request retry.
|
|
@@ -166,9 +278,40 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
166
278
|
* Shared by connect() and recoverSession() rather than written twice —
|
|
167
279
|
* the two drifted apart once already, and a third caller would drift again.
|
|
168
280
|
*/
|
|
281
|
+
/**
|
|
282
|
+
* What a session strategy is given: enough to make one request and to prove
|
|
283
|
+
* the session is ours, and nothing else. It cannot reach session state, so a
|
|
284
|
+
* strategy can neither mark this connection connected nor tear it down.
|
|
285
|
+
*/
|
|
286
|
+
/**
|
|
287
|
+
* Ask the server to open a session before the establishing call needs one.
|
|
288
|
+
*
|
|
289
|
+
* The strategy is chosen by what the server publishes, not by which system we
|
|
290
|
+
* believe it to be: ABAP Cloud offers a session resource and issues
|
|
291
|
+
* `SAP_SESSIONID` to whoever asks for one — with `x-sap-security-session:
|
|
292
|
+
* create` — while on-prem has no such resource and its session arrives with
|
|
293
|
+
* the establishing request. Believing cloud simply "issues no SAP_SESSIONID"
|
|
294
|
+
* is what happens when nobody asks.
|
|
295
|
+
*/
|
|
296
|
+
/**
|
|
297
|
+
* What a wire needs from this connection to get a session, or give one back.
|
|
298
|
+
*
|
|
299
|
+
* A snapshot in the sense that matters: a close is dispatched without being
|
|
300
|
+
* awaited, so nothing here may read the connection again once the teardown
|
|
301
|
+
* has cleared it.
|
|
302
|
+
*/
|
|
303
|
+
protected sessionContext(): IAdtSessionContext;
|
|
169
304
|
private establishAndCommit;
|
|
170
305
|
/** The teardown epoch, for a recovery to capture before it starts. */
|
|
171
306
|
protected get teardownEpoch(): number;
|
|
307
|
+
/**
|
|
308
|
+
* Which session the connection is on now.
|
|
309
|
+
*
|
|
310
|
+
* Moves whenever the session does. A response that comes back carrying an
|
|
311
|
+
* older one belongs to a session that has already been replaced, and must not
|
|
312
|
+
* be acted on as if it said something about the current one.
|
|
313
|
+
*/
|
|
314
|
+
protected get sessionGeneration(): number;
|
|
172
315
|
/**
|
|
173
316
|
* Raises a session-lost teardown from inside request handling.
|
|
174
317
|
*
|
|
@@ -200,7 +343,7 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
200
343
|
* turns "your lock is dead" back into "your request 403'd", which is the very
|
|
201
344
|
* information the caller needs and the only one it cannot recover itself.
|
|
202
345
|
*/
|
|
203
|
-
|
|
346
|
+
protected isSessionVerdict(error: unknown): boolean;
|
|
204
347
|
/**
|
|
205
348
|
* Folds a response into the session state AND acts on what it means, in one
|
|
206
349
|
* step.
|
|
@@ -210,7 +353,7 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
210
353
|
* later check reads `unchanged`. That is one call site forgetting, and it
|
|
211
354
|
* happened — on the error path and on every retry response.
|
|
212
355
|
*/
|
|
213
|
-
|
|
356
|
+
protected observeResponse(headers?: Record<string, unknown>, generation?: number): void;
|
|
214
357
|
/**
|
|
215
358
|
* Acts on what a response said about the session identity.
|
|
216
359
|
*
|
|
@@ -231,7 +374,7 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
231
374
|
/**
|
|
232
375
|
* Whether the server is telling us the session it was given no longer exists.
|
|
233
376
|
*
|
|
234
|
-
*
|
|
377
|
+
* One on-prem system answered HTTP 400 with "Session not found", answered in ~60 ms
|
|
235
378
|
* with the cookie present — which is why identity comparison cannot see this:
|
|
236
379
|
* the cookie, and therefore the fingerprint, is completely unchanged. The
|
|
237
380
|
* exact match is landscape-specific and is one of the live probes this design
|
|
@@ -255,19 +398,17 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
255
398
|
private performRequest;
|
|
256
399
|
protected abstract buildAuthorizationHeader(): string;
|
|
257
400
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
401
|
+
* Ask the wire to establish itself, and hand back what it earned.
|
|
402
|
+
*
|
|
403
|
+
* The exchange itself is the transport's — it is the HTTP wire that has a
|
|
404
|
+
* token to earn and an endpoint to earn it from, and the RFC wire that has
|
|
405
|
+
* neither. What stays here is the part that is about the SESSION rather than
|
|
406
|
+
* the wire: fencing the answer by generation, and letting the identity policy
|
|
407
|
+
* read what it means.
|
|
260
408
|
*/
|
|
261
|
-
protected fetchCsrfToken(
|
|
409
|
+
protected fetchCsrfToken(_url: string, retryCount?: number, retryDelay?: number,
|
|
262
410
|
/** Fences the response effects; omitted during connect(), which has no lease. */
|
|
263
411
|
generation?: number): Promise<string>;
|
|
264
|
-
/**
|
|
265
|
-
* Fetch CSRF token from a specific endpoint with retries
|
|
266
|
-
*/
|
|
267
|
-
private fetchCsrfTokenFromEndpoint;
|
|
268
|
-
/**
|
|
269
|
-
* Get CSRF token (protected for use by subclasses)
|
|
270
|
-
*/
|
|
271
412
|
protected getCsrfToken(): string | null;
|
|
272
413
|
/**
|
|
273
414
|
* Set CSRF token (protected for use by subclasses)
|
|
@@ -277,27 +418,38 @@ declare abstract class AbstractAbapConnection implements AbapConnection, ISessio
|
|
|
277
418
|
* Get cookies (protected for use by subclasses)
|
|
278
419
|
*/
|
|
279
420
|
protected getCookies(): string | null;
|
|
280
|
-
protected setInitialCookies(cookies: string): void;
|
|
281
421
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
422
|
+
* Seed the wire with cookies the caller already holds — a SAML session, which
|
|
423
|
+
* IS the credential rather than something a logon call earns.
|
|
424
|
+
*
|
|
425
|
+
* Handed over as a response would deliver them, because the wire owns the jar
|
|
426
|
+
* and how it stores them is its business, not this class's.
|
|
286
427
|
*/
|
|
287
|
-
|
|
428
|
+
protected setInitialCookies(cookies: string): void;
|
|
288
429
|
/**
|
|
289
|
-
* Subclasses override to inject extra https.Agent options (e.g. mTLS
|
|
290
|
-
* The returned options are merged with the base options
|
|
430
|
+
* Subclasses override to inject extra https.Agent options (e.g. mTLS
|
|
431
|
+
* cert/key/pfx). The returned options are merged with the base options
|
|
432
|
+
* (rejectUnauthorized).
|
|
291
433
|
*/
|
|
292
434
|
protected getHttpsAgentOptions(): import('node:https').AgentOptions;
|
|
293
|
-
|
|
294
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Make sure the wire is ready to carry a mutation.
|
|
437
|
+
*
|
|
438
|
+
* What ready MEANS is the wire's: HTTP holds a CSRF token and returns at once
|
|
439
|
+
* when it already has one; an RFC conversation has nothing to earn and does
|
|
440
|
+
* nothing. Demanding a token back was an HTTP assumption, and over RFC it
|
|
441
|
+
* raised `No CSRF token in response headers` before every write — swallowed
|
|
442
|
+
* by the caller, but logged as an error and repeated on the next one.
|
|
443
|
+
*/
|
|
444
|
+
private ensureWireReady;
|
|
295
445
|
/**
|
|
296
446
|
* Clear SAP-side session state when SAP rejects the cached CSRF token + session
|
|
297
447
|
* cookies (HTTP 401 on a mutation while a cached token exists). This forces the
|
|
298
448
|
* next request path to fetch a fresh token and a fresh SAP_SESSIONID cookie.
|
|
299
449
|
*
|
|
300
|
-
* Distinct from
|
|
450
|
+
* Distinct from disconnect(): this leaves the axios instance and interceptors
|
|
451
|
+
* in place, and tells the server nothing — it is a request-level repair, not a
|
|
452
|
+
* teardown.
|
|
301
453
|
*/
|
|
302
454
|
private invalidateSession;
|
|
303
455
|
private shouldRetryCsrf;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAE5B,MAAM,0BAA0B,CAAC;AAMlC,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;AAExC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
|