@mcp-abap-adt/connection 1.10.2 → 3.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 +827 -0
- package/README.md +44 -11
- package/dist/__tests__/helpers/session.d.ts +15 -0
- package/dist/__tests__/helpers/session.d.ts.map +1 -0
- package/dist/__tests__/helpers/session.js +19 -0
- package/dist/auth/ntlm.d.ts +15 -0
- package/dist/auth/ntlm.d.ts.map +1 -1
- package/dist/auth/ntlm.js +38 -0
- package/dist/connection/AbstractAbapConnection.d.ts +180 -12
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +398 -19
- package/dist/connection/BaseAbapConnection.d.ts +5 -1
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
- package/dist/connection/BaseAbapConnection.js +11 -1
- package/dist/connection/CertificateAbapConnection.d.ts +5 -1
- package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
- package/dist/connection/CertificateAbapConnection.js +11 -1
- package/dist/connection/JwtAbapConnection.d.ts +3 -2
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +19 -8
- package/dist/connection/KerberosAbapConnection.d.ts +5 -1
- package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
- package/dist/connection/KerberosAbapConnection.js +54 -3
- package/dist/connection/SamlAbapConnection.d.ts +5 -1
- package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
- package/dist/connection/SamlAbapConnection.js +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/session/SessionLifecycle.d.ts +50 -70
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +59 -158
- package/docs/INDEX.md +106 -0
- package/docs/INSTALLATION.md +304 -0
- package/docs/JWT_AUTH_TOOLS.md +142 -0
- package/docs/MIGRATION-2.0.md +114 -0
- package/docs/SCOPE.md +44 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +122 -0
- package/docs/USAGE.md +745 -0
- package/examples/README.md +112 -0
- package/examples/basic-connection.js +55 -0
- package/examples/jwt-with-token-refresh.js +87 -0
- package/examples/saml-connection.js +52 -0
- package/examples/websocket-transport.js +87 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -34,18 +34,19 @@ The package uses a clean separation of concerns:
|
|
|
34
34
|
|
|
35
35
|
- **`AbstractAbapConnection`** (abstract, internal only):
|
|
36
36
|
- Common HTTP request logic
|
|
37
|
+
- Session lifecycle: `connect()` / `disconnect()`, admission, lock windows, teardown draining
|
|
37
38
|
- Session management (cookies, CSRF tokens)
|
|
38
39
|
- CSRF token fetching with retry
|
|
39
40
|
- Auth-agnostic - knows nothing about Basic or JWT
|
|
40
41
|
|
|
41
42
|
- **`BaseAbapConnection`** (concrete, exported):
|
|
42
43
|
- Basic Authentication implementation
|
|
43
|
-
-
|
|
44
|
+
- `connect()` establishes the session (required before any request)
|
|
44
45
|
- Suitable for on-premise SAP systems
|
|
45
46
|
|
|
46
47
|
- **`JwtAbapConnection`** (concrete, exported):
|
|
47
48
|
- JWT/OAuth2 Authentication implementation
|
|
48
|
-
-
|
|
49
|
+
- `connect()` establishes the session with the JWT token (required before any request)
|
|
49
50
|
- Suitable for SAP BTP ABAP Environment
|
|
50
51
|
- Token refresh handled by auth-broker package
|
|
51
52
|
|
|
@@ -112,6 +113,7 @@ This package interacts with external packages **ONLY through interfaces**:
|
|
|
112
113
|
|
|
113
114
|
- 📦 **[Installation Guide](./docs/INSTALLATION.md)** - Setup and installation instructions
|
|
114
115
|
- 📚 **[Usage Guide](./docs/USAGE.md)** - Detailed usage examples and API documentation
|
|
116
|
+
- 🚚 **[Migration: the explicit session lifecycle](./docs/MIGRATION-2.0.md)** - `connect()` is now required; start here if you are coming from 1.x
|
|
115
117
|
- 💡 **[Examples](./examples/)** - Working code examples
|
|
116
118
|
|
|
117
119
|
## Features
|
|
@@ -156,6 +158,7 @@ const logger = {
|
|
|
156
158
|
|
|
157
159
|
// Create connection
|
|
158
160
|
const connection = createAbapConnection(config, logger);
|
|
161
|
+
await connection.connect(); // required before any request
|
|
159
162
|
|
|
160
163
|
// Make ADT request
|
|
161
164
|
const response = await connection.makeAdtRequest({
|
|
@@ -186,6 +189,7 @@ const logger = {
|
|
|
186
189
|
|
|
187
190
|
// Logger is optional - if not provided, no logging output
|
|
188
191
|
const connection = createAbapConnection(config, logger);
|
|
192
|
+
await connection.connect();
|
|
189
193
|
|
|
190
194
|
// Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
|
|
191
195
|
const response = await connection.makeAdtRequest({
|
|
@@ -206,6 +210,8 @@ const config: SapConfig = {
|
|
|
206
210
|
};
|
|
207
211
|
|
|
208
212
|
const connection = createAbapConnection(config, logger);
|
|
213
|
+
await connection.connect();
|
|
214
|
+
|
|
209
215
|
const response = await connection.makeAdtRequest({
|
|
210
216
|
method: "GET",
|
|
211
217
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
@@ -236,8 +242,11 @@ const config: SapConfig = {
|
|
|
236
242
|
|
|
237
243
|
// Create connection with token refresher - 401/403 handled automatically
|
|
238
244
|
const connection = new JwtAbapConnection(config, logger, undefined, tokenRefresher);
|
|
245
|
+
await connection.connect();
|
|
239
246
|
|
|
240
|
-
// Requests automatically retry with refreshed token on auth errors
|
|
247
|
+
// Requests automatically retry with refreshed token on auth errors. A refresh
|
|
248
|
+
// replaces the SAP session, so if a lock window is open the request fails with
|
|
249
|
+
// ADT_SESSION_REPLACED instead of continuing on a session your lock is not in.
|
|
241
250
|
const response = await connection.makeAdtRequest({
|
|
242
251
|
method: "GET",
|
|
243
252
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
@@ -252,6 +261,7 @@ For operations that require session state (e.g., object modifications), you can
|
|
|
252
261
|
import { createAbapConnection } from "@mcp-abap-adt/connection";
|
|
253
262
|
|
|
254
263
|
const connection = createAbapConnection(config, logger);
|
|
264
|
+
await connection.connect();
|
|
255
265
|
|
|
256
266
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
257
267
|
connection.setSessionType("stateful");
|
|
@@ -388,19 +398,43 @@ type SapConfig = {
|
|
|
388
398
|
Main interface for ABAP connections.
|
|
389
399
|
|
|
390
400
|
```typescript
|
|
401
|
+
// The shared contract (IAbapConnection), what every connection provides:
|
|
391
402
|
interface AbapConnection {
|
|
403
|
+
connect(): Promise<void>; // REQUIRED before any request; rejects on failure
|
|
392
404
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
393
|
-
|
|
405
|
+
getBaseUrl(): Promise<string>;
|
|
394
406
|
setSessionType(type: "stateless" | "stateful"): void; // Switch session type
|
|
395
|
-
|
|
396
|
-
getSessionId(): string | null; // Get current session ID
|
|
407
|
+
getSessionId(): string | null; // Client-side conversation id
|
|
397
408
|
}
|
|
398
409
|
```
|
|
399
410
|
|
|
411
|
+
The HTTP connection classes carry the rest of the session lifecycle. It is on the
|
|
412
|
+
shared contract as a **capability atom** in `@mcp-abap-adt/interfaces` rather than
|
|
413
|
+
as methods on `IAbapConnection`, which is why `RfcAbapConnection` — a transport
|
|
414
|
+
that owns no HTTP session — is unaffected by its existence:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// ISessionLifecycleAware
|
|
418
|
+
disconnect(): Promise<void>; // never throws, and waits for nothing
|
|
419
|
+
isConnected(): boolean;
|
|
420
|
+
getSessionIdentity(): string | null; // WHICH SAP session; null is not "disconnected"
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Import those names from `@mcp-abap-adt/interfaces`, not from this package: a
|
|
424
|
+
contract type re-exported under a second name is a contract type that can drift.
|
|
425
|
+
|
|
426
|
+
**The connection does not track locks.** Deciding when to disconnect, and
|
|
427
|
+
preparing for it, belongs to the caller; pairing every LOCK with its UNLOCK
|
|
428
|
+
belongs to `@mcp-abap-adt/adt-clients`, which holds the handles. What this layer
|
|
429
|
+
owns is not being interrupted by a timeout mid-operation — see
|
|
430
|
+
`beginCriticalSection()` below.
|
|
431
|
+
|
|
432
|
+
See [docs/USAGE.md — Session Lifecycle](./docs/USAGE.md#session-lifecycle).
|
|
433
|
+
|
|
400
434
|
**Session Management:**
|
|
401
|
-
- `setSessionType(type)`: Programmatically switch between stateful and stateless modes
|
|
402
|
-
- `
|
|
403
|
-
- `
|
|
435
|
+
- `setSessionType(type)`: Programmatically switch between stateful and stateless modes *(on the contract)*
|
|
436
|
+
- `getSessionId()`: Returns the client-side conversation id, an auto-generated UUID *(on the contract)*
|
|
437
|
+
- `getSessionMode()`: Returns current session mode *(HTTP classes only)*
|
|
404
438
|
|
|
405
439
|
#### `ILogger`
|
|
406
440
|
|
|
@@ -489,7 +523,6 @@ async function fetchCsrfToken(baseUrl: string): Promise<string> {
|
|
|
489
523
|
}
|
|
490
524
|
```
|
|
491
525
|
|
|
492
|
-
See [PR Proposal](./PR_PROPOSAL_CSRF_CONFIG.md) for more details.
|
|
493
526
|
|
|
494
527
|
## Requirements
|
|
495
528
|
|
|
@@ -500,7 +533,7 @@ See [PR Proposal](./PR_PROPOSAL_CSRF_CONFIG.md) for more details.
|
|
|
500
533
|
|
|
501
534
|
See [CHANGELOG.md](./CHANGELOG.md) for detailed version history and breaking changes.
|
|
502
535
|
|
|
503
|
-
**
|
|
536
|
+
**Version history:** [CHANGELOG.md](./CHANGELOG.md)
|
|
504
537
|
- Removed token refresh functionality (handled by `@mcp-abap-adt/auth-broker`)
|
|
505
538
|
- Removed session storage functionality (handled by `@mcp-abap-adt/auth-broker`)
|
|
506
539
|
- Logger is now optional
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test precondition: put a connection into the connected state without a
|
|
3
|
+
* network round trip.
|
|
4
|
+
*
|
|
5
|
+
* `connect()` is mandatory now, so a test whose subject is something else —
|
|
6
|
+
* timeouts, headers, retry classification — needs the session to exist before
|
|
7
|
+
* it can say anything. Establishing it for real would mean teaching every such
|
|
8
|
+
* mock to answer a CSRF fetch, which tests the wrong thing and adds retry
|
|
9
|
+
* delays to suites that are not about connecting.
|
|
10
|
+
*
|
|
11
|
+
* Tests that ARE about connecting use a real stub instead; see
|
|
12
|
+
* `connection/sessionComposition.test.ts`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function markConnectedForTest(connection: unknown, fingerprint?: Map<string, string>): void;
|
|
15
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,OAAO,EACnB,WAAW,GAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAA4C,GAC1E,IAAI,CAMN"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.markConnectedForTest = markConnectedForTest;
|
|
4
|
+
/**
|
|
5
|
+
* Test precondition: put a connection into the connected state without a
|
|
6
|
+
* network round trip.
|
|
7
|
+
*
|
|
8
|
+
* `connect()` is mandatory now, so a test whose subject is something else —
|
|
9
|
+
* timeouts, headers, retry classification — needs the session to exist before
|
|
10
|
+
* it can say anything. Establishing it for real would mean teaching every such
|
|
11
|
+
* mock to answer a CSRF fetch, which tests the wrong thing and adds retry
|
|
12
|
+
* delays to suites that are not about connecting.
|
|
13
|
+
*
|
|
14
|
+
* Tests that ARE about connecting use a real stub instead; see
|
|
15
|
+
* `connection/sessionComposition.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
function markConnectedForTest(connection, fingerprint = new Map([['SAP_SESSIONID_T_100', 'S1']])) {
|
|
18
|
+
connection.lifecycle.markConnected(fingerprint);
|
|
19
|
+
}
|
package/dist/auth/ntlm.d.ts
CHANGED
|
@@ -8,4 +8,19 @@
|
|
|
8
8
|
* bytes starting with the NTLM signature "NTLMSSP\0" (base64 prefix "TlRMTVNTUAA").
|
|
9
9
|
*/
|
|
10
10
|
export declare function isNtlmChallenge(wwwAuthenticate: string | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `Negotiate <gssapi-data>` — the server has handed back a token for the client
|
|
13
|
+
* to feed into its GSS context and retry (RFC 4559 continuation).
|
|
14
|
+
*/
|
|
15
|
+
export declare function isNegotiateContinuation(wwwAuthenticate: string | undefined): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A bare `Negotiate`, with no token.
|
|
18
|
+
*
|
|
19
|
+
* Told apart from a continuation deliberately: once the client has ALREADY sent
|
|
20
|
+
* its initial Authorization, a bare challenge is the server declining it — the
|
|
21
|
+
* credentials were not accepted. There is no server token here, so nothing
|
|
22
|
+
* could be stepped even by a client that supports multi-leg. Diagnosing the two
|
|
23
|
+
* alike sends the reader after the wrong problem.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isBareNegotiateChallenge(wwwAuthenticate: string | undefined): boolean;
|
|
11
26
|
//# sourceMappingURL=ntlm.d.ts.map
|
package/dist/auth/ntlm.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ntlm.d.ts","sourceRoot":"","sources":["../../src/auth/ntlm.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAiB5E"}
|
|
1
|
+
{"version":3,"file":"ntlm.d.ts","sourceRoot":"","sources":["../../src/auth/ntlm.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAiB5E;AAcD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,OAAO,CAKT;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,OAAO,CAIT"}
|
package/dist/auth/ntlm.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isNtlmChallenge = isNtlmChallenge;
|
|
4
|
+
exports.isNegotiateContinuation = isNegotiateContinuation;
|
|
5
|
+
exports.isBareNegotiateChallenge = isBareNegotiateChallenge;
|
|
4
6
|
/**
|
|
5
7
|
* Detect whether a WWW-Authenticate header (or a Negotiate challenge token) indicates NTLM.
|
|
6
8
|
* NTLM is rejected: only Kerberos/SPNEGO Negotiate is acceptable.
|
|
@@ -32,3 +34,39 @@ function isNtlmChallenge(wwwAuthenticate) {
|
|
|
32
34
|
}
|
|
33
35
|
return false;
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A `Negotiate` challenge that is not NTLM in disguise. Says nothing about
|
|
39
|
+
* whether it carries a token — callers that care must ask which kind it is.
|
|
40
|
+
*/
|
|
41
|
+
function isNegotiate(wwwAuthenticate) {
|
|
42
|
+
if (!wwwAuthenticate)
|
|
43
|
+
return false;
|
|
44
|
+
if (isNtlmChallenge(wwwAuthenticate))
|
|
45
|
+
return false;
|
|
46
|
+
return wwwAuthenticate
|
|
47
|
+
.split(',')
|
|
48
|
+
.some((part) => /^negotiate\b/i.test(part.trim()));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `Negotiate <gssapi-data>` — the server has handed back a token for the client
|
|
52
|
+
* to feed into its GSS context and retry (RFC 4559 continuation).
|
|
53
|
+
*/
|
|
54
|
+
function isNegotiateContinuation(wwwAuthenticate) {
|
|
55
|
+
if (!isNegotiate(wwwAuthenticate))
|
|
56
|
+
return false;
|
|
57
|
+
return wwwAuthenticate
|
|
58
|
+
.split(',')
|
|
59
|
+
.some((part) => /^negotiate\s+\S/i.test(part.trim()));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A bare `Negotiate`, with no token.
|
|
63
|
+
*
|
|
64
|
+
* Told apart from a continuation deliberately: once the client has ALREADY sent
|
|
65
|
+
* its initial Authorization, a bare challenge is the server declining it — the
|
|
66
|
+
* credentials were not accepted. There is no server token here, so nothing
|
|
67
|
+
* could be stepped even by a client that supports multi-leg. Diagnosing the two
|
|
68
|
+
* alike sends the reader after the wrong problem.
|
|
69
|
+
*/
|
|
70
|
+
function isBareNegotiateChallenge(wwwAuthenticate) {
|
|
71
|
+
return (isNegotiate(wwwAuthenticate) && !isNegotiateContinuation(wwwAuthenticate));
|
|
72
|
+
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { type IAdtResponse } from '@mcp-abap-adt/interfaces';
|
|
1
|
+
import { type IAdtResponse, type ISessionLifecycleAware } from '@mcp-abap-adt/interfaces';
|
|
2
2
|
import type { SapConfig } from '../config/sapConfig.js';
|
|
3
3
|
import type { ILogger } from '../logger.js';
|
|
4
|
+
import { SessionLifecycle } from '../session/SessionLifecycle.js';
|
|
4
5
|
import type { AbapConnection, AbapRequestOptions } from './AbapConnection.js';
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Declares the capabilities explicitly rather than satisfying them by accident.
|
|
8
|
+
* `AbapConnection` is the base contract every transport honours; these two are
|
|
9
|
+
* the HTTP session's own, and naming them means a signature that drifts from
|
|
10
|
+
* the published contract fails to compile here instead of at the consumer.
|
|
11
|
+
*/
|
|
12
|
+
declare abstract class AbstractAbapConnection implements AbapConnection, ISessionLifecycleAware {
|
|
6
13
|
private readonly config;
|
|
7
14
|
protected readonly logger: ILogger | null;
|
|
15
|
+
/**
|
|
16
|
+
* Owns session state, admission and teardown ordering. Composed rather than
|
|
17
|
+
* inherited: RfcAbapConnection implements the interface directly, so the two
|
|
18
|
+
* transports share this unit instead of a base class.
|
|
19
|
+
*/
|
|
20
|
+
protected readonly lifecycle: SessionLifecycle;
|
|
8
21
|
private axiosInstance;
|
|
9
22
|
private csrfToken;
|
|
10
23
|
private cookies;
|
|
@@ -79,26 +92,175 @@ declare abstract class AbstractAbapConnection implements AbapConnection {
|
|
|
79
92
|
*/
|
|
80
93
|
getSessionId(): string | null;
|
|
81
94
|
getConfig(): SapConfig;
|
|
95
|
+
/**
|
|
96
|
+
* Establish the session for this auth type. Implementations do their own auth
|
|
97
|
+
* preparation and fetch the CSRF token; connect() owns everything around it.
|
|
98
|
+
*/
|
|
99
|
+
protected abstract establishSession(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Establishes the session, once, under the lifecycle.
|
|
102
|
+
*
|
|
103
|
+
* Idempotent, and concurrent callers share one establishment: the transition
|
|
104
|
+
* joins the tail of its own kind. A teardown requested while establishment
|
|
105
|
+
* was in flight means the caller asked to stop, so usability is NOT published
|
|
106
|
+
* and what was established is released — otherwise a slow connect would hand
|
|
107
|
+
* back a session someone had already discarded.
|
|
108
|
+
*/
|
|
109
|
+
connect(): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Tears the session down. Never throws, and always settles.
|
|
112
|
+
*
|
|
113
|
+
* It waits for NOTHING. Deciding when to disconnect is the caller's, and so is
|
|
114
|
+
* preparing for it — finishing chains, releasing locks. Waiting here on a
|
|
115
|
+
* request whose caller chose no timeout is what made a teardown unbounded, and
|
|
116
|
+
* an unbounded teardown blocks every later transition on the serialized tail.
|
|
117
|
+
*
|
|
118
|
+
* Requests already in flight run to completion untouched. Generation fencing
|
|
119
|
+
* (see `SessionLifecycle.isCurrent`) keeps their results from reaching this
|
|
120
|
+
* connection afterwards.
|
|
121
|
+
*
|
|
122
|
+
* Sends no ADT session-close — see the design's D2.
|
|
123
|
+
*/
|
|
124
|
+
disconnect(): Promise<void>;
|
|
125
|
+
isConnected(): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Fingerprint of the SAP-side session, or null when none is known.
|
|
128
|
+
*
|
|
129
|
+
* `null` is NOT a statement about the connection. Two situations produce it:
|
|
130
|
+
* no session exists, or the connection is live over a server that issued no
|
|
131
|
+
* session cookie. Use {@link isConnected} for connection state.
|
|
132
|
+
*
|
|
133
|
+
* It follows that null → non-null is not a replacement but an identity being
|
|
134
|
+
* learned; only a CHANGED value means the session was replaced.
|
|
135
|
+
*/
|
|
136
|
+
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
|
+
*/
|
|
82
141
|
reset(): void;
|
|
83
|
-
getBaseUrl(): Promise<string>;
|
|
84
|
-
getAuthHeaders(): Promise<Record<string, string>>;
|
|
85
142
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* proper authentication and session initialization.
|
|
143
|
+
* Re-establishes the session for a request that is recovering from a
|
|
144
|
+
* credential renewal, then lets that request retry.
|
|
89
145
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
146
|
+
* Runs as its own `recover` transition, which never joins another: each
|
|
147
|
+
* recovery carries the baseline of its own request. It yields to a caller's
|
|
148
|
+
* teardown — if the epoch moved since `baselineEpoch`, someone asked to stop
|
|
149
|
+
* while this was being prepared, and a retry must not resurrect a session
|
|
150
|
+
* they discarded.
|
|
151
|
+
*
|
|
152
|
+
* The transition queues behind the cleanup that the renewal itself raised, so
|
|
153
|
+
* it never re-establishes on top of stale transport state.
|
|
154
|
+
*/
|
|
155
|
+
protected recoverSession(baselineEpoch: number): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Establishes a session and publishes it — but only if nobody asked to stop
|
|
158
|
+
* meanwhile.
|
|
159
|
+
*
|
|
160
|
+
* The epoch is checked BEFORE, so a teardown already requested costs no round
|
|
161
|
+
* trip, and AFTER, because establishment takes time and a caller can ask to
|
|
162
|
+
* stop during it. Checking only before is the defect this exists to prevent:
|
|
163
|
+
* markConnected() would then clear the teardown state and hand back a session
|
|
164
|
+
* the caller had already discarded.
|
|
165
|
+
*
|
|
166
|
+
* Shared by connect() and recoverSession() rather than written twice —
|
|
167
|
+
* the two drifted apart once already, and a third caller would drift again.
|
|
93
168
|
*/
|
|
94
|
-
|
|
169
|
+
private establishAndCommit;
|
|
170
|
+
/** The teardown epoch, for a recovery to capture before it starts. */
|
|
171
|
+
protected get teardownEpoch(): number;
|
|
172
|
+
/**
|
|
173
|
+
* Raises a session-lost teardown from inside request handling.
|
|
174
|
+
*
|
|
175
|
+
* There are exactly three things that can cost us the ABAP session, and they
|
|
176
|
+
* were found one at a time precisely because they were written apart. They go
|
|
177
|
+
* through here so a fourth joins the list instead of inventing its own
|
|
178
|
+
* sequence:
|
|
179
|
+
*
|
|
180
|
+
* - the credential was renewed (the injected auth says so);
|
|
181
|
+
* - the server says the session is gone (a dead-session response);
|
|
182
|
+
* - the tracked cookie changed under us while a lock was held.
|
|
183
|
+
*
|
|
184
|
+
* `internal` origin, so it does not cancel the recovery that raised it, and
|
|
185
|
+
* `sessionLost`, so admission shuts at once and the identity is dropped
|
|
186
|
+
* immediately — a later comparison must see the change, and on a dead session
|
|
187
|
+
* the cookie is unchanged, so only the state can tell.
|
|
188
|
+
*
|
|
189
|
+
* Does not await: it is called from inside a request, and the cleanup must not
|
|
190
|
+
* wait for the very request that raised it.
|
|
191
|
+
*/
|
|
192
|
+
protected raiseSessionLost(reason: string): void;
|
|
193
|
+
/** The credential-renewal raiser; see raiseSessionLost(). */
|
|
194
|
+
protected discardSession(): void;
|
|
195
|
+
/**
|
|
196
|
+
* Whether an error is this connection's own verdict about the session rather
|
|
197
|
+
* than something the server said about a request.
|
|
198
|
+
*
|
|
199
|
+
* A retry path that swallows one of these and rethrows the original error
|
|
200
|
+
* turns "your lock is dead" back into "your request 403'd", which is the very
|
|
201
|
+
* information the caller needs and the only one it cannot recover itself.
|
|
202
|
+
*/
|
|
203
|
+
private isSessionVerdict;
|
|
204
|
+
/**
|
|
205
|
+
* Folds a response into the session state AND acts on what it means, in one
|
|
206
|
+
* step.
|
|
207
|
+
*
|
|
208
|
+
* Never call updateCookiesFromResponse() directly: it MUTATES the fingerprint,
|
|
209
|
+
* so discarding its classification absorbs a replacement silently and every
|
|
210
|
+
* later check reads `unchanged`. That is one call site forgetting, and it
|
|
211
|
+
* happened — on the error path and on every retry response.
|
|
212
|
+
*/
|
|
213
|
+
private observeResponse;
|
|
214
|
+
/**
|
|
215
|
+
* Acts on what a response said about the session identity.
|
|
216
|
+
*
|
|
217
|
+
* A replacement is always fatal, and that is a narrowing: an earlier version
|
|
218
|
+
* tolerated it "when no lock is held", deciding from the connection's own
|
|
219
|
+
* lock windows. Those are gone, and rightly — this layer does not know that a
|
|
220
|
+
* lock exists, what object it covers or what would release it. Locks are
|
|
221
|
+
* tracked a layer up, per object, by the code that took them.
|
|
222
|
+
*
|
|
223
|
+
* So the rule is written from what this layer CAN know: the ABAP session we
|
|
224
|
+
* were speaking to is not the one we are speaking to now. Anything the caller
|
|
225
|
+
* held against the old one is dead, and continuing quietly would hand them a
|
|
226
|
+
* session they never opened — the failure this whole design exists to prevent.
|
|
227
|
+
* Being wrong in this direction costs a reconnect; being wrong the other way
|
|
228
|
+
* costs a lock nobody can find.
|
|
229
|
+
*/
|
|
230
|
+
private applyIdentityPolicy;
|
|
231
|
+
/**
|
|
232
|
+
* Whether the server is telling us the session it was given no longer exists.
|
|
233
|
+
*
|
|
234
|
+
* The E19 shape was HTTP 400 with "Session not found", answered in ~60 ms
|
|
235
|
+
* with the cookie present — which is why identity comparison cannot see this:
|
|
236
|
+
* the cookie, and therefore the fingerprint, is completely unchanged. The
|
|
237
|
+
* exact match is landscape-specific and is one of the live probes this design
|
|
238
|
+
* still owes.
|
|
239
|
+
*/
|
|
240
|
+
private isDeadSessionResponse;
|
|
241
|
+
/** Drops everything that described the session. Not a lifecycle transition. */
|
|
242
|
+
private clearSessionState;
|
|
243
|
+
/**
|
|
244
|
+
* The session-bearing cookies, and only those.
|
|
245
|
+
*
|
|
246
|
+
* `sap-XSRF_*` is excluded deliberately: it changes on a token refresh WITHIN
|
|
247
|
+
* the same session, so including it would report an ordinary refresh as a new
|
|
248
|
+
* session and fail exactly where nothing is wrong. `sap-usercontext` is ours,
|
|
249
|
+
* overwritten on every response.
|
|
250
|
+
*/
|
|
251
|
+
protected sessionFingerprint(): Map<string, string>;
|
|
252
|
+
getBaseUrl(): Promise<string>;
|
|
253
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
95
254
|
makeAdtRequest<T = any, D = any>(options: AbapRequestOptions): Promise<IAdtResponse<T, D>>;
|
|
255
|
+
private performRequest;
|
|
96
256
|
protected abstract buildAuthorizationHeader(): string;
|
|
97
257
|
/**
|
|
98
258
|
* Fetch CSRF token from SAP system
|
|
99
259
|
* Protected method for use by concrete implementations in their connect() method
|
|
100
260
|
*/
|
|
101
|
-
protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number
|
|
261
|
+
protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number,
|
|
262
|
+
/** Fences the response effects; omitted during connect(), which has no lease. */
|
|
263
|
+
generation?: number): Promise<string>;
|
|
102
264
|
/**
|
|
103
265
|
* Fetch CSRF token from a specific endpoint with retries
|
|
104
266
|
*/
|
|
@@ -116,6 +278,12 @@ declare abstract class AbstractAbapConnection implements AbapConnection {
|
|
|
116
278
|
*/
|
|
117
279
|
protected getCookies(): string | null;
|
|
118
280
|
protected setInitialCookies(cookies: string): void;
|
|
281
|
+
/**
|
|
282
|
+
* Folds a response's cookies into the jar and classifies what that means for
|
|
283
|
+
* the session identity. Returns the classification rather than acting on it:
|
|
284
|
+
* cookie parsing stays free of policy, and no exception fires in the middle
|
|
285
|
+
* of a state update. The caller decides.
|
|
286
|
+
*/
|
|
119
287
|
private updateCookiesFromResponse;
|
|
120
288
|
/**
|
|
121
289
|
* Subclasses override to inject extra https.Agent options (e.g. mTLS cert/key/pfx).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
|