@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +827 -0
  2. package/README.md +44 -11
  3. package/dist/__tests__/helpers/session.d.ts +15 -0
  4. package/dist/__tests__/helpers/session.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/session.js +19 -0
  6. package/dist/auth/ntlm.d.ts +15 -0
  7. package/dist/auth/ntlm.d.ts.map +1 -1
  8. package/dist/auth/ntlm.js +38 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +180 -12
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +398 -19
  12. package/dist/connection/BaseAbapConnection.d.ts +5 -1
  13. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  14. package/dist/connection/BaseAbapConnection.js +11 -1
  15. package/dist/connection/CertificateAbapConnection.d.ts +5 -1
  16. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  17. package/dist/connection/CertificateAbapConnection.js +11 -1
  18. package/dist/connection/JwtAbapConnection.d.ts +3 -2
  19. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/JwtAbapConnection.js +19 -8
  21. package/dist/connection/KerberosAbapConnection.d.ts +5 -1
  22. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/KerberosAbapConnection.js +54 -3
  24. package/dist/connection/SamlAbapConnection.d.ts +5 -1
  25. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  26. package/dist/connection/SamlAbapConnection.js +11 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -0
  29. package/dist/session/SessionLifecycle.d.ts +50 -70
  30. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  31. package/dist/session/SessionLifecycle.js +59 -158
  32. package/docs/INDEX.md +106 -0
  33. package/docs/INSTALLATION.md +304 -0
  34. package/docs/JWT_AUTH_TOOLS.md +142 -0
  35. package/docs/MIGRATION-2.0.md +114 -0
  36. package/docs/SCOPE.md +44 -0
  37. package/docs/STATEFUL_SESSION_GUIDE.md +122 -0
  38. package/docs/USAGE.md +745 -0
  39. package/examples/README.md +112 -0
  40. package/examples/basic-connection.js +55 -0
  41. package/examples/jwt-with-token-refresh.js +87 -0
  42. package/examples/saml-connection.js +52 -0
  43. package/examples/websocket-transport.js +87 -0
  44. 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
- - Simple connect() - fetches CSRF token
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
- - Simple connect() - establishes connection with JWT token
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
- reset(): void;
405
+ getBaseUrl(): Promise<string>;
394
406
  setSessionType(type: "stateless" | "stateful"): void; // Switch session type
395
- getSessionMode(): "stateless" | "stateful"; // Get current session mode
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
- - `getSessionMode()`: Returns current session mode
403
- - `getSessionId()`: Returns the current session ID (auto-generated UUID)
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
- **Latest version: 0.2.0**
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
+ }
@@ -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
@@ -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
- declare abstract class AbstractAbapConnection implements AbapConnection {
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
- * Connect to SAP system and initialize session (get CSRF token and cookies)
87
- * This should be called explicitly before making the first request to ensure
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
- * Concrete implementations must provide auth-specific connection logic:
91
- * - BaseAbapConnection: Basic auth with CSRF token fetch
92
- * - JwtAbapConnection: JWT auth with token refresh on 401/403
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
- abstract connect(): Promise<void>;
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): Promise<string>;
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":"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"}
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;AAG9E;;;;;GAKG;AACH,uBAAe,sBACb,YAAW,cAAc,EAAE,sBAAsB;IA+B/C,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IA9B3C;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,mBAA0B;IAEtD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,eAAe,CAAU;IACjC;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB,CAAS;IAClC,oFAAoF;IACpF,OAAO,CAAC,oBAAoB,CAAK;IAEjC,SAAS,aACU,MAAM,EAAE,SAAS,EACf,MAAM,EAAE,OAAO,GAAG,IAAI,EACzC,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAqBzC;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAUpD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,UAAU;IAI1C;;;;;;;;;;;;OAYG;IACH,oBAAoB,IAAI,IAAI;IAQ5B;;;;OAIG;IACH,kBAAkB,IAAI,IAAI;IAY1B;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKrC;;OAEG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,SAAS,IAAI,SAAS;IAItB;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAEpD;;;;;;;;OAQG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B;;;;;;;;;;;;;OAaG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAWjC,WAAW,IAAI,OAAO;IAItB;;;;;;;;;OASG;IACH,kBAAkB,IAAI,MAAM,GAAG,IAAI;IAInC;;;OAGG;IACH,KAAK,IAAI,IAAI;IAQb;;;;;;;;;;;;OAYG;cACa,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE;;;;;;;;;;;;OAYG;YACW,kBAAkB;IAuDhC,sEAAsE;IACtE,SAAS,KAAK,aAAa,IAAI,MAAM,CAEpC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAShD,6DAA6D;IAC7D,SAAS,CAAC,cAAc,IAAI,IAAI;IAIhC;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IAyBvB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;;;;;;;OAQG;IACH,OAAO,CAAC,qBAAqB;IAa7B,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB;IAYzB;;;;;;;OAOG;IACH,SAAS,CAAC,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAU7C,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAejD,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAchB,cAAc;IA4V5B,SAAS,CAAC,QAAQ,CAAC,wBAAwB,IAAI,MAAM;IAErD;;;OAGG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAgC,EAC5C,UAAU,GAAE,MAAgC;IAC5C,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;IAmDlB;;OAEG;YACW,0BAA0B;IA0LxC;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,MAAM,GAAG,IAAI;IAIvC;;OAEG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIlD;;OAEG;IACH,SAAS,CAAC,UAAU,IAAI,MAAM,GAAG,IAAI;IAIrC,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIlD;;;;;OAKG;IACH,OAAO,CAAC,yBAAyB;IAqEjC;;;OAGG;IACH,SAAS,CAAC,oBAAoB,IAAI,OAAO,YAAY,EAAE,YAAY;IAInE,OAAO,CAAC,gBAAgB;YAsBV,oBAAoB;IAiClC;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAazB,OAAO,CAAC,eAAe;CA+BxB;AAGD,OAAO,EAAE,sBAAsB,EAAE,CAAC"}