@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
@@ -38,7 +38,11 @@ class CertificateAbapConnection extends AbstractAbapConnection_js_1.AbstractAbap
38
38
  * Loads certificate material and primes the session. MUST be called before the first
39
39
  * request — the TLS agent is built lazily and needs the client cert present.
40
40
  */
41
- async connect() {
41
+ /**
42
+ * Establishes the session for this auth type. Called by
43
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
44
+ */
45
+ async establishSession() {
42
46
  await this.ensureMaterial();
43
47
  const baseUrl = await this.getBaseUrl();
44
48
  const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
@@ -53,6 +57,12 @@ class CertificateAbapConnection extends AbstractAbapConnection_js_1.AbstractAbap
53
57
  this.logger?.debug(`[DEBUG] CertificateAbapConnection - Cookies extracted from error response during connect (first 100 chars): ${this.getCookies()?.substring(0, 100)}...`);
54
58
  }
55
59
  }
60
+ // Rethrow: a resolved connect() must mean a usable session exists. This
61
+ // used to swallow and resolve anyway, deferring establishment to the
62
+ // first request — coherent only while that lazy path existed. Without it,
63
+ // swallowing would leave a connection that reports success, holds
64
+ // nothing, and refuses every request.
65
+ throw error;
56
66
  }
57
67
  }
58
68
  getHttpsAgentOptions() {
@@ -21,9 +21,10 @@ export declare class JwtAbapConnection extends AbstractAbapConnection {
21
21
  */
22
22
  private tryRefreshToken;
23
23
  /**
24
- * Override connect to handle JWT token refresh on errors
24
+ * Establishes the session for this auth type. Called by
25
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
25
26
  */
26
- connect(): Promise<void>;
27
+ protected establishSession(): Promise<void>;
27
28
  /**
28
29
  * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
29
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,YAAY,CAAS;gBAG3B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe;IAWlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;;OAGG;YACW,eAAe;IA0B7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyD9B;;OAEG;IACG,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAyD9B;;OAEG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,SAAI,EACd,UAAU,SAAO,GAChB,OAAO,CAAC,MAAM,CAAC;IA2ClB,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
1
+ {"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,YAAY,CAAS;gBAG3B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe;IAWlC,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;;OAGG;YACW,eAAe;IA0B7B;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DjD;;OAEG;IACG,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAiE9B;;OAEG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,SAAI,EACd,UAAU,SAAO,GAChB,OAAO,CAAC,MAAM,CAAC;IA2ClB,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
@@ -52,9 +52,10 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
52
52
  }
53
53
  }
54
54
  /**
55
- * Override connect to handle JWT token refresh on errors
55
+ * Establishes the session for this auth type. Called by
56
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
56
57
  */
57
- async connect() {
58
+ async establishSession() {
58
59
  const baseUrl = await this.getBaseUrl();
59
60
  const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
60
61
  this.logger?.debug(`[DEBUG] JwtAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
@@ -85,9 +86,11 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
85
86
  }
86
87
  // Try to refresh token if tokenRefresher is available
87
88
  if (await this.tryRefreshToken()) {
88
- // Retry connect with new token
89
- this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying connect after token refresh...`);
90
- return this.connect();
89
+ // Retry the ESTABLISHMENT, not connect(): connect() runs this as a
90
+ // joinable transition, so a nested call joins the transition already
91
+ // in flight — this one — and waits for itself forever.
92
+ this.logger?.debug(`[DEBUG] JwtAbapConnection - Retrying establishment after token refresh...`);
93
+ return this.establishSession();
91
94
  }
92
95
  throw new Error('JWT token has expired. Please re-authenticate.');
93
96
  }
@@ -99,6 +102,9 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
99
102
  * Override makeAdtRequest to handle JWT auth errors with automatic token refresh
100
103
  */
101
104
  async makeAdtRequest(options) {
105
+ // Captured before the attempt: a recovery asks "has the caller asked to
106
+ // stop since this request began", not since some later bookkeeping step.
107
+ const baselineEpoch = this.teardownEpoch;
102
108
  this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Starting request: ${options.method} ${options.url}`);
103
109
  try {
104
110
  const response = await super.makeAdtRequest(options);
@@ -124,9 +130,14 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
124
130
  }
125
131
  // Try to refresh token if tokenRefresher is available
126
132
  if (await this.tryRefreshToken()) {
127
- // Reset connection state and retry request with new token
128
- this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Retrying request after token refresh...`);
129
- this.reset();
133
+ this.logger?.debug(`[DEBUG] JwtAbapConnection.makeAdtRequest - Recovering session after token refresh...`);
134
+ // The renewed credential cannot keep the old ABAP session, so this is
135
+ // a session-lost teardown — internal, or it would cancel the very
136
+ // recovery it is setting up. reset() would be the caller-origin one.
137
+ this.discardSession();
138
+ // Re-establish before retrying: the retry goes through admission, and
139
+ // a discarded session admits nothing.
140
+ await this.recoverSession(baselineEpoch);
130
141
  return super.makeAdtRequest(options);
131
142
  }
132
143
  throw new Error('JWT token has expired. Please re-authenticate.');
@@ -14,7 +14,11 @@ export declare class KerberosAbapConnection extends AbstractAbapConnection {
14
14
  * request — the first request carries the Negotiate header; SAP then issues a session
15
15
  * cookie which is reused for subsequent requests.
16
16
  */
17
- connect(): Promise<void>;
17
+ /**
18
+ * Establishes the session for this auth type. Called by
19
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
20
+ */
21
+ protected establishSession(): Promise<void>;
18
22
  protected buildAuthorizationHeader(): string;
19
23
  }
20
24
  //# sourceMappingURL=KerberosAbapConnection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"KerberosAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/KerberosAbapConnection.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,uGAAuG;AACvG,qBAAa,sBAAuB,SAAQ,sBAAsB;IAChE,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,YAAY,CAAM;gBAEd,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM;IAQ1E,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B,mDAAmD;cACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC9B,SAAS,CAAC,wBAAwB,IAAI,MAAM;CAS7C"}
1
+ {"version":3,"file":"KerberosAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/KerberosAbapConnection.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,uGAAuG;AACvG,qBAAa,sBAAuB,SAAQ,sBAAsB;IAChE,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,YAAY,CAAM;gBAEd,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM;IAQ1E,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B,mDAAmD;cACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5C;;;;OAIG;IACH;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqFjD,SAAS,CAAC,wBAAwB,IAAI,MAAM;CAS7C"}
@@ -34,16 +34,38 @@ class KerberosAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapCon
34
34
  * request — the first request carries the Negotiate header; SAP then issues a session
35
35
  * cookie which is reused for subsequent requests.
36
36
  */
37
- async connect() {
37
+ /**
38
+ * Establishes the session for this auth type. Called by
39
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
40
+ */
41
+ async establishSession() {
38
42
  await this.ensureToken();
39
43
  const baseUrl = await this.getBaseUrl();
40
44
  const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
41
45
  this.logger?.debug(`[DEBUG] KerberosAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
42
46
  try {
43
- const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
47
+ // ONE attempt, deliberately — the shared default is 3 retries a second
48
+ // apart, and every one of them corrupts this exchange:
49
+ //
50
+ // - A GSS token is one-shot. Replaying it is meaningless at best, and
51
+ // replay protection may answer it differently than the original.
52
+ // - Worse, a 401 that sets a cookie silences buildAuthorizationHeader()
53
+ // (a cookie carries auth after the first round trip), so the retry
54
+ // goes out with NO Authorization at all and draws the server's plain
55
+ // opening challenge.
56
+ //
57
+ // Either way the classifier below would read the LAST response and name
58
+ // the wrong cause. The retry cannot help here anyway: nothing about a
59
+ // rejected token changes by waiting a second. Retrying a transient
60
+ // network failure is the caller's business now that connect() is
61
+ // explicit, and a fresh connect() mints a fresh token.
62
+ const token = await this.fetchCsrfToken(discoveryUrl, 0, 0);
44
63
  this.setCsrfToken(token);
45
64
  }
46
65
  catch (error) {
66
+ // The token went out and was not accepted, so it is spent. Keeping it
67
+ // would make every later connect() replay a token already known to fail.
68
+ this.currentToken = '';
47
69
  if (error instanceof axios_1.AxiosError && error.response?.headers) {
48
70
  const wwwAuth = error.response.headers['www-authenticate'];
49
71
  if ((0, ntlm_js_1.isNtlmChallenge)(wwwAuth)) {
@@ -53,8 +75,37 @@ class KerberosAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapCon
53
75
  if (this.getCookies()) {
54
76
  this.logger?.debug('[DEBUG] KerberosAbapConnection - cookies captured from error response during connect');
55
77
  }
78
+ // A 401 carrying a Negotiate token is a CONTINUATION, not a success.
79
+ //
80
+ // Per RFC 4559 the server is handing back gssapi-data for the client to
81
+ // feed into its GSS context and retry. This client cannot do that:
82
+ // generateSpnegoToken() calls step('') once and discards the context,
83
+ // so there is nothing to continue with. The token it holds is the
84
+ // initial one, and sending it again produces the same 401.
85
+ //
86
+ // So this fails, and says why — resolving here would claim a readiness
87
+ // that does not exist and move the failure to the first real request,
88
+ // where it reads as an unrelated auth error.
89
+ if ((0, ntlm_js_1.isNegotiateContinuation)(wwwAuth)) {
90
+ throw new Error('KerberosAbapConnection: the server continued the SPNEGO exchange ' +
91
+ '(401 with a Negotiate token), which requires feeding that token ' +
92
+ 'back into the GSS context and retrying. This client performs a ' +
93
+ 'single-leg exchange only — the context is discarded after the ' +
94
+ 'first step — so multi-leg SPNEGO is not supported. See ' +
95
+ 'https://www.rfc-editor.org/rfc/rfc4559');
96
+ }
97
+ // A BARE Negotiate is a different problem with a different fix: the
98
+ // initial Authorization has already gone out, so an empty challenge is
99
+ // the server declining it. No token came back, so there is nothing to
100
+ // step even for a client that supports multi-leg.
101
+ if ((0, ntlm_js_1.isBareNegotiateChallenge)(wwwAuth)) {
102
+ throw new Error('KerberosAbapConnection: the server rejected the SPNEGO token ' +
103
+ '(401 with a bare Negotiate challenge, no token returned). The ' +
104
+ 'credentials were not accepted — check the SPN, the ticket ' +
105
+ '(klist / kinit), and that the user is permitted Kerberos logon.');
106
+ }
56
107
  }
57
- this.logger?.warn(`[WARN] KerberosAbapConnection - connect deferred: ${error instanceof Error ? error.message : String(error)}`);
108
+ throw error;
58
109
  }
59
110
  }
60
111
  buildAuthorizationHeader() {
@@ -13,7 +13,11 @@ export declare class SamlAbapConnection extends AbstractAbapConnection {
13
13
  * Connect to SAP system using existing session cookies
14
14
  * Fetches CSRF token to establish session context
15
15
  */
16
- connect(): Promise<void>;
16
+ /**
17
+ * Establishes the session for this auth type. Called by
18
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
19
+ */
20
+ protected establishSession(): Promise<void>;
17
21
  protected buildAuthorizationHeader(): string;
18
22
  getAuthHeaders(): Promise<Record<string, string>>;
19
23
  private static validateConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"SamlAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/SamlAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;IAC5D,OAAO,CAAC,cAAc,CAAS;gBAG7B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAUzC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC9B,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAItC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAQvD,OAAO,CAAC,MAAM,CAAC,cAAc;CAW9B"}
1
+ {"version":3,"file":"SamlAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/SamlAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;IAC5D,OAAO,CAAC,cAAc,CAAS;gBAG7B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAUzC;;;OAGG;IACH;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCjD,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAItC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAQvD,OAAO,CAAC,MAAM,CAAC,cAAc;CAW9B"}
@@ -20,7 +20,11 @@ class SamlAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnect
20
20
  * Connect to SAP system using existing session cookies
21
21
  * Fetches CSRF token to establish session context
22
22
  */
23
- async connect() {
23
+ /**
24
+ * Establishes the session for this auth type. Called by
25
+ * AbstractAbapConnection.connect(), which owns the lifecycle around it.
26
+ */
27
+ async establishSession() {
24
28
  const baseUrl = await this.getBaseUrl();
25
29
  const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
26
30
  this.logger?.debug(`[DEBUG] SamlAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
@@ -41,6 +45,12 @@ class SamlAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnect
41
45
  this.logger?.debug(`[DEBUG] SamlAbapConnection - Cookies extracted from error response during connect (first 100 chars): ${this.getCookies()?.substring(0, 100)}...`);
42
46
  }
43
47
  }
48
+ // Rethrow: a resolved connect() must mean a usable session exists. This
49
+ // used to swallow and resolve anyway, deferring establishment to the
50
+ // first request — coherent only while that lazy path existed. Without it,
51
+ // swallowing would leave a connection that reports success, holds
52
+ // nothing, and refuses every request.
53
+ throw error;
44
54
  }
45
55
  }
46
56
  buildAuthorizationHeader() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,6BAA6B,EAAE,MAAM,yCAAyC,CAAC;AACxF,YAAY,EACV,WAAW,EACX,SAAS,EACT,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,IAAI,oBAAoB,GAC3C,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,iBAAiB,EACjB,iBAAiB,IAAI,mBAAmB,GACzC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,6BAA6B,EAAE,MAAM,yCAAyC,CAAC;AACxF,YAAY,EACV,WAAW,EACX,SAAS,EACT,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AAOxC,OAAO,EACL,kBAAkB,EAClB,kBAAkB,IAAI,oBAAoB,GAC3C,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,iBAAiB,EACjB,iBAAiB,IAAI,mBAAmB,GACzC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ Object.defineProperty(exports, "FileCertificateMaterialLoader", { enumerable: tr
7
7
  // Config utilities
8
8
  var sapConfig_js_1 = require("./config/sapConfig.js");
9
9
  Object.defineProperty(exports, "sapConfigSignature", { enumerable: true, get: function () { return sapConfig_js_1.sapConfigSignature; } });
10
+ // The session lifecycle vocabulary — ISessionLifecycleAware, ADT_SESSION_ERROR —
11
+ // is deliberately NOT exported here. It lives in @mcp-abap-adt/interfaces, and a
12
+ // consumer imports it from there: re-exporting a contract type gives it two
13
+ // names and lets the two drift.
10
14
  // Connection classes - only final implementations
11
15
  // Deprecated aliases for backward compatibility
12
16
  var BaseAbapConnection_js_1 = require("./connection/BaseAbapConnection.js");
@@ -1,125 +1,108 @@
1
1
  /**
2
2
  * Owns the session lifecycle: what state the session is in, who may use it, and
3
- * when it is safe to tear it down.
3
+ * when it stops being current.
4
4
  *
5
5
  * Deliberately knows nothing about SAP, HTTP, RFC, cookies or ADT. Identities
6
- * are opaque maps of name → value that someone else computed; windows are
7
- * opaque labels. That is what makes this unit testable without a server, and it
8
- * is where the hard part of the design lives: ordering, admission, deadlines.
6
+ * are opaque maps of name → value that someone else computed. That is what makes
7
+ * this unit testable without a server, and it is where the hard part lives:
8
+ * ordering, admission, and telling one session from the next.
9
9
  *
10
- * Design: docs/superpowers/specs/2026-07-27-session-lifecycle-design.md in
10
+ * Design: docs/superpowers/specs/2026-07-31-teardown-policy-design.md in
11
11
  * @mcp-abap-adt/adt-clients.
12
12
  */
13
- /** Opaque handle for one open lock window: `Symbol(label)`. */
14
- export type WindowToken = symbol;
13
+ import { type AdtSessionErrorCode } from '@mcp-abap-adt/interfaces';
15
14
  export type TransitionKind = 'connect' | 'disconnect' | 'recover' | 'cleanup';
16
15
  export interface RequestLease {
17
16
  /** Teardown epoch at admission — the baseline a recovery compares against. */
18
17
  readonly epoch: number;
18
+ /**
19
+ * Session generation at admission — the baseline a RESPONSE compares against
20
+ * before touching shared state. Distinct from `epoch` on purpose: see
21
+ * `sessionGeneration`.
22
+ */
23
+ readonly generation: number;
19
24
  /** Call once the request settles. Safe to call twice. */
20
25
  release(): void;
21
26
  }
22
- export interface DrainResult {
23
- /** Labels of windows still open when the wait gave up, deduplicated. */
24
- abandonedWindows: string[];
25
- }
26
27
  export interface BeginTeardownOptions {
27
28
  /** Decides the epoch: a caller's request cancels recoveries, an internal one must not. */
28
29
  origin: 'caller' | 'internal';
29
- /** Decides admission: a lost session cannot let an open window finish. */
30
+ /** A lost session cannot be spoken to again; its identity is dropped at once. */
30
31
  sessionLost: boolean;
31
32
  }
32
- export declare const ADT_SESSION_ERROR: {
33
- readonly NOT_CONNECTED: "ADT_NOT_CONNECTED";
34
- readonly SESSION_REPLACED: "ADT_SESSION_REPLACED";
35
- readonly RELEASE_PENDING: "ADT_RELEASE_PENDING";
36
- };
37
- export type AdtSessionErrorCode = (typeof ADT_SESSION_ERROR)[keyof typeof ADT_SESSION_ERROR];
38
33
  export declare function sessionError(code: AdtSessionErrorCode, message?: string): Error & {
39
34
  code: AdtSessionErrorCode;
40
35
  };
41
- export interface SessionLifecycleOptions {
42
- /** Ceiling for the window wait, from the moment a teardown is requested. */
43
- ceilingMs?: number;
44
- /** Injected clock, so tests need no real time. */
45
- now?: () => number;
46
- }
47
36
  export declare class SessionLifecycle {
48
37
  private state;
49
38
  private fingerprint;
50
39
  private epoch;
40
+ private generation;
51
41
  private teardownPending;
52
- private teardownLostSession;
53
- private teardownAt;
54
- /** Set at expiry: admission is shut regardless of open windows. */
55
- private admissionForcedShut;
56
- private readonly windows;
57
42
  private inFlight;
58
43
  private tail;
59
44
  private tailKind;
60
45
  private tailPromise;
61
- private waiters;
62
- private readonly ceilingMs;
63
- private readonly now;
64
- constructor(options?: SessionLifecycleOptions);
65
- /**
66
- * Whether a caller may start work. False throughout a teardown, including
67
- * while a grandfathered window is still finishing: finishing is not starting.
68
- */
46
+ /** Whether a caller may start work. False throughout a pending teardown. */
69
47
  get connected(): boolean;
70
48
  /** Derived from the tracked fingerprint; null when nothing is tracked. */
71
49
  get identity(): string | null;
72
50
  get teardownEpoch(): number;
73
- /** One entry per open window, abandoned ones included. */
74
- get openWindows(): readonly string[];
51
+ /**
52
+ * Which session is current, counted from zero.
53
+ *
54
+ * Two counters answer two different questions, and one cannot do both:
55
+ *
56
+ * - `epoch` — did the CALLER ask to stop? It moves only on a caller-initiated
57
+ * teardown, because a recovery must not cancel itself.
58
+ * - `generation` — is this still the session you were issued against? It moves
59
+ * on every change of which session is current, however caused.
60
+ *
61
+ * A fence built on `epoch` misses every internal teardown: after a session
62
+ * loss and a successful recovery, requests from the dead session carry the
63
+ * same epoch as the new one and sail straight through.
64
+ */
65
+ get sessionGeneration(): number;
75
66
  /**
76
67
  * Publishes a freshly established session, which also ENDS any teardown that
77
68
  * was pending: the flags describe the session being torn down, and this is a
78
69
  * different one. Without this, `connect()` after `disconnect()` — which the
79
70
  * design allows explicitly — leaves the lifecycle permanently unusable.
80
- *
81
- * Windows are dropped for the same reason: they belonged to the old session,
82
- * nothing here can close them, and a teardown that gave up on them has
83
- * already carried their labels out in its report. Keeping them would make
84
- * every future drain re-report locks from a session that no longer exists.
85
71
  */
86
72
  markConnected(fingerprint?: ReadonlyMap<string, string>): void;
73
+ /**
74
+ * Drops the tracked identity without touching state or generation.
75
+ *
76
+ * For a session the caller discarded ON PURPOSE — a credential renewal, a
77
+ * cache invalidation. The next fingerprint then reads as `established` rather
78
+ * than `replaced`, which is the truth: nothing was taken from us.
79
+ */
80
+ forgetIdentity(): void;
87
81
  markDisconnected(): void;
88
82
  /**
89
83
  * Classifies a freshly observed fingerprint.
90
84
  *
91
85
  * Additive: a name appearing where none was tracked is `established`, never
92
86
  * `replaced` — otherwise the identifier a LOCK response adds would read as a
93
- * new session and blow up the window it just opened.
87
+ * new session and condemn the operation it just covered.
94
88
  */
95
89
  observe(fingerprint: ReadonlyMap<string, string>): 'unchanged' | 'established' | 'replaced';
96
- /** True while an already-open window is allowed to finish its work. */
97
- private get finishingWindowOpen();
98
- private get admits();
99
90
  assertUsable(): void;
100
91
  /** Asserts usability and counts the request in, in one synchronous step. */
101
92
  admitRequest(): RequestLease;
93
+ /** How many admitted requests have not settled. Diagnostics only — nothing waits on it. */
94
+ get requestsInFlight(): number;
102
95
  /**
103
- * A lock outlives the request that takes it, so it is the one thing a pending
104
- * teardown refuses outright.
96
+ * Whether a lease may still touch shared state.
97
+ *
98
+ * A request outliving its session is ordinary now that a teardown does not
99
+ * wait: its response must not write cookies over a newer session's, and must
100
+ * not be read as a replacement — which would raise a session-lost teardown
101
+ * against a session that is perfectly healthy.
105
102
  */
106
- beginWindow(label: string): WindowToken;
107
- /** A token matching no open window is ignored: double close, foreign token. */
108
- endWindow(token: WindowToken): void;
103
+ isCurrent(lease: Pick<RequestLease, 'generation'>): boolean;
109
104
  beginTeardown({ origin, sessionLost }: BeginTeardownOptions): void;
110
105
  get teardownRequested(): boolean;
111
- /**
112
- * Resolves when nothing is in flight and no window is still worth waiting
113
- * for. Bounded by the ceiling measured from the teardown request — absolute,
114
- * never extended by request activity.
115
- *
116
- * On expiry, before resolving and without yielding in between: shuts
117
- * admission, gives up on the remaining windows, then waits once more for the
118
- * already-admitted requests to settle.
119
- */
120
- drain(): Promise<DrainResult>;
121
- private get liveWindows();
122
- private abandonedLabels;
123
106
  /**
124
107
  * Runs a transition on the serializing tail.
125
108
  *
@@ -128,11 +111,8 @@ export declare class SessionLifecycle {
128
111
  * when nothing is queued behind it, so a join can never overtake a queued
129
112
  * transition of another kind. `recover` and `cleanup` never join and are
130
113
  * never joined: a recovery carries its own request's baseline, and an
131
- * internal cleanup owes its result to nobody while a caller's teardown owes a
132
- * report.
114
+ * internal cleanup owes its result to nobody.
133
115
  */
134
- transition(kind: TransitionKind, run: () => Promise<void>): Promise<void>;
135
- private wake;
136
- private changed;
116
+ transition<T>(kind: TransitionKind, run: () => Promise<T>): Promise<T>;
137
117
  }
138
118
  //# sourceMappingURL=SessionLifecycle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,0FAA0F;IAC1F,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC9B,iFAAiF;IACjF,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,mBAAmB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,GAAG;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAMvC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAK;IAErB,OAAO,CAAC,IAAI,CAAuC;IACnD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAAiC;IAIpD,4EAA4E;IAC5E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAM5B;IAED,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;;;;;;;;;OAaG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED;;;;;OAKG;IACH,aAAa,CAAC,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GAAG,IAAI;IAOzE;;;;;;OAMG;IACH,cAAc,IAAI,IAAI;IAItB,gBAAgB,IAAI,IAAI;IAKxB;;;;;;OAMG;IACH,OAAO,CACL,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,WAAW,GAAG,aAAa,GAAG,UAAU;IAiB3C,YAAY,IAAI,IAAI;IAMpB,4EAA4E;IAC5E,YAAY,IAAI,YAAY;IAiB5B,2FAA2F;IAC3F,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO;IAM3D,aAAa,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,oBAAoB,GAAG,IAAI;IAelE,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAID;;;;;;;;;OASG;IACH,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAuBvE"}