@mcp-abap-adt/connection 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +274 -1
  2. package/README.md +60 -30
  3. package/dist/auth/IAuthProvider.d.ts +84 -0
  4. package/dist/auth/IAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/IAuthProvider.js +21 -0
  6. package/dist/auth/providers.d.ts +95 -0
  7. package/dist/auth/providers.d.ts.map +1 -0
  8. package/dist/auth/providers.js +140 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +222 -17
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +555 -31
  12. package/dist/connection/AdtCloudConnector.d.ts +29 -0
  13. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtCloudConnector.js +31 -0
  15. package/dist/connection/AdtOnPremConnector.d.ts +30 -0
  16. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  17. package/dist/connection/AdtOnPremConnector.js +32 -0
  18. package/dist/connection/BaseAbapConnection.d.ts +7 -1
  19. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/BaseAbapConnection.js +7 -1
  21. package/dist/connection/CertificateAbapConnection.d.ts +11 -1
  22. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/CertificateAbapConnection.js +13 -1
  24. package/dist/connection/CredentialAbapConnection.d.ts +88 -0
  25. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  26. package/dist/connection/CredentialAbapConnection.js +195 -0
  27. package/dist/connection/JwtAbapConnection.d.ts +23 -7
  28. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  29. package/dist/connection/JwtAbapConnection.js +26 -8
  30. package/dist/connection/KerberosAbapConnection.d.ts +9 -1
  31. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  32. package/dist/connection/KerberosAbapConnection.js +9 -1
  33. package/dist/connection/RfcAbapConnection.d.ts +0 -5
  34. package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
  35. package/dist/connection/RfcAbapConnection.js +0 -7
  36. package/dist/connection/SamlAbapConnection.d.ts +7 -1
  37. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  38. package/dist/connection/SamlAbapConnection.js +7 -1
  39. package/dist/connection/connectionFactory.d.ts +16 -0
  40. package/dist/connection/connectionFactory.d.ts.map +1 -1
  41. package/dist/connection/connectionFactory.js +52 -0
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +10 -1
  45. package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
  46. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
  47. package/dist/session/CloudSecuritySessionStrategy.js +135 -0
  48. package/dist/session/IcfSessionStrategy.d.ts +27 -0
  49. package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
  50. package/dist/session/IcfSessionStrategy.js +62 -0
  51. package/dist/session/SessionLifecycle.d.ts +17 -2
  52. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  53. package/dist/session/SessionLifecycle.js +17 -2
  54. package/dist/session/SessionStrategy.d.ts +86 -0
  55. package/dist/session/SessionStrategy.d.ts.map +1 -0
  56. package/dist/session/SessionStrategy.js +33 -0
  57. package/dist/utils/cookies.d.ts +12 -0
  58. package/dist/utils/cookies.d.ts.map +1 -0
  59. package/dist/utils/cookies.js +24 -0
  60. package/dist/utils/timeouts.d.ts +16 -0
  61. package/dist/utils/timeouts.d.ts.map +1 -1
  62. package/dist/utils/timeouts.js +19 -0
  63. package/docs/INSTALLATION.md +6 -2
  64. package/docs/MIGRATION-2.0.md +1 -1
  65. package/docs/MIGRATION-5.0.md +116 -0
  66. package/docs/STATEFUL_SESSION_GUIDE.md +77 -11
  67. package/docs/USAGE.md +92 -22
  68. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
  69. package/package.json +1 -1
package/docs/USAGE.md CHANGED
@@ -36,7 +36,9 @@ const logger = {
36
36
  };
37
37
 
38
38
  // Create connection (logger is optional)
39
- const connection = createAbapConnection(config, logger);
39
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
40
+ system: 'onprem', // or 'cloud' — said by you, never detected
41
+ });
40
42
  // Or without logger:
41
43
  // const connection = createAbapConnection(config);
42
44
 
@@ -57,14 +59,27 @@ Tearing the session down is `disconnect()`, but it is not on the
57
59
  `IAbapConnection` type this factory returns — see
58
60
  [Session Lifecycle](#session-lifecycle) for where it lives and how to reach it.
59
61
 
60
- ## Authentication Types
62
+ ## Which system, and how you authenticate
63
+
64
+ Two independent choices, and they must stay independent. The connector says
65
+ which SYSTEM you are dialling; the auth provider says how you prove who you
66
+ are. A communication user against ABAP Cloud and a bearer token against an
67
+ on-prem system are both ordinary, and a design where the credential picked the
68
+ system's session mechanism got one of them wrong whichever way it guessed.
69
+
70
+ Nothing is detected. `/sap/bc/adt/core/http/sessions` answers on on-prem too,
71
+ and its `DELETE` there leaves the session open while the platform logoff removes
72
+ it — so asking the server would choose the mechanism that releases nothing.
61
73
 
62
74
  ### Basic Authentication (On-Premise)
63
75
 
64
76
  For on-premise SAP systems using basic authentication:
65
77
 
66
78
  ```typescript
67
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
79
+ import {
80
+ AdtOnPremConnector,
81
+ BasicAuthProvider,
82
+ } from '@mcp-abap-adt/connection';
68
83
 
69
84
  const config = {
70
85
  url: 'https://sap-server.local:8000',
@@ -74,7 +89,11 @@ const config = {
74
89
  client: '100',
75
90
  };
76
91
 
77
- const connection = new BaseAbapConnection(config, logger);
92
+ const connection = new AdtOnPremConnector(
93
+ config,
94
+ new BasicAuthProvider(config.username, config.password),
95
+ logger,
96
+ );
78
97
  await connection.connect(); // required: nothing is established implicitly
79
98
  ```
80
99
 
@@ -84,16 +103,25 @@ For SAP BTP ABAP Environment. Token refresh belongs to
84
103
  `@mcp-abap-adt/auth-broker`; this package only carries the token:
85
104
 
86
105
  ```typescript
87
- import { JwtAbapConnection } from '@mcp-abap-adt/connection';
106
+ import {
107
+ AdtCloudConnector,
108
+ TokenAuthProvider,
109
+ } from '@mcp-abap-adt/connection';
88
110
 
89
111
  const config = {
90
112
  url: 'https://tenant.abap.cloud',
91
113
  authType: 'jwt' as const,
92
- jwtToken: 'eyJhbGciOiJSUzI1NiIs...',
93
114
  client: '100', // Optional for cloud
94
115
  };
95
116
 
96
- const connection = new JwtAbapConnection(config, logger);
117
+ // A bare token works and has no renewal behind it. Hand the provider an
118
+ // ITokenRefresher instead — from @mcp-abap-adt/auth-broker or your own — and it
119
+ // checks expiry and refreshes on its own.
120
+ const connection = new AdtCloudConnector(
121
+ config,
122
+ new TokenAuthProvider('eyJhbGciOiJSUzI1NiIs...'),
123
+ logger,
124
+ );
97
125
  await connection.connect(); // required: nothing is established implicitly
98
126
 
99
127
  // Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
@@ -172,7 +200,9 @@ await connection.makeAdtRequest({
172
200
  By default, connections are stateless - each request gets fresh cookies and CSRF tokens:
173
201
 
174
202
  ```typescript
175
- const connection = createAbapConnection(config, logger);
203
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
204
+ system: 'onprem', // or 'cloud' — said by you, never detected
205
+ });
176
206
  await connection.connect();
177
207
 
178
208
  // Each request is independent
@@ -184,7 +214,9 @@ await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' })
184
214
  Enable stateful session mode for operations requiring consistent session state:
185
215
 
186
216
  ```typescript
187
- const connection = createAbapConnection(config, logger);
217
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
218
+ system: 'onprem', // or 'cloud' — said by you, never detected
219
+ });
188
220
  await connection.connect();
189
221
 
190
222
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
@@ -242,7 +274,7 @@ implied. Four things follow from it.
242
274
  > site:
243
275
  >
244
276
  > ```typescript
245
- > const conn = new BaseAbapConnection(config, logger);
277
+ > const conn = new AdtOnPremConnector(config, provider, logger);
246
278
  > tearDownAfter(conn); // ✅ checked
247
279
  > tearDownAfter(new RfcAbapConnection(cfg)); // ✅ compile error, as it should be
248
280
  > ```
@@ -290,9 +322,13 @@ implied. Four things follow from it.
290
322
  ### connect() is required, and it tells the truth
291
323
 
292
324
  ```typescript
293
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
325
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
294
326
 
295
- const connection = new BaseAbapConnection(config, logger);
327
+ const connection = new AdtOnPremConnector(
328
+ config,
329
+ new BasicAuthProvider(user, pass),
330
+ logger,
331
+ );
296
332
 
297
333
  await connection.connect(); // establishes the session, or rejects
298
334
  connection.isConnected(); // true only while a usable session exists
@@ -420,7 +456,9 @@ yours.
420
456
  Session IDs are auto-generated (UUID) when connection is created:
421
457
 
422
458
  ```typescript
423
- const connection = createAbapConnection(config, logger);
459
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
460
+ system: 'onprem', // or 'cloud' — said by you, never detected
461
+ });
424
462
  console.log(connection.getSessionId()); // e.g., '7f3a8b2c-...'
425
463
 
426
464
  // Or provide your own when creating connection
@@ -434,7 +472,9 @@ Dynamically switch between stateful and stateless modes:
434
472
 
435
473
  ```typescript
436
474
  // Start in stateless mode (default)
437
- const connection = createAbapConnection(config, logger);
475
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
476
+ system: 'onprem', // or 'cloud' — said by you, never detected
477
+ });
438
478
  await connection.connect();
439
479
 
440
480
  // Enable stateful for a series of operations
@@ -447,14 +487,20 @@ await connection.makeAdtRequest({ method: 'POST', url: '...' });
447
487
  connection.setSessionType('stateless');
448
488
  ```
449
489
 
450
- ### Connection Reset
490
+ ### Starting Over
451
491
 
452
- Reset connection state (clears cookies, CSRF token):
492
+ There is no local-only reset. Discarding the cookie does not end the ABAP
493
+ session — the server keeps it until its own timeout, and sessions are limited
494
+ per user — so starting over means telling the server, then connecting again:
453
495
 
454
496
  ```typescript
455
- connection.reset();
497
+ await connection.disconnect(); // ends the session on the server too
498
+ await connection.connect(); // a new one, explicitly
456
499
  ```
457
500
 
501
+ A connection that was connected must be disconnected, which is why this belongs
502
+ in a `finally`. `disconnect()` never throws, so it is safe there.
503
+
458
504
  ## Custom Logging
459
505
 
460
506
  ### Using Custom Logger
@@ -493,7 +539,9 @@ class CustomLogger implements ILogger {
493
539
  }
494
540
 
495
541
  const logger = new CustomLogger();
496
- const connection = createAbapConnection(config, logger);
542
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
543
+ system: 'onprem', // or 'cloud' — said by you, never detected
544
+ });
497
545
  ```
498
546
 
499
547
  ## Error Handling
@@ -577,16 +625,38 @@ interface AbapConnection {
577
625
  }
578
626
  ```
579
627
 
580
- Anything beyond that is **not** on the contract. `reset()`, `getSessionMode()`
628
+ Anything beyond that is **not** on the contract. `getSessionMode()`
581
629
  and the session lifecycle (`disconnect()`, `isConnected()`,
582
630
  `getSessionIdentity()`) live on the HTTP
583
631
  connection classes; `RfcAbapConnection` has some of them and not others, so
584
632
  reach for them through a concrete type rather than through what
585
633
  `createAbapConnection()` returns.
586
634
 
587
- ### `BaseAbapConnection` (Basic Auth)
635
+ ### `AdtOnPremConnector` / `AdtCloudConnector`
636
+
637
+ One per system, each handed an auth provider:
638
+
639
+ ```typescript
640
+ class AdtOnPremConnector extends CredentialAbapConnection {
641
+ constructor(
642
+ config: SapConfig,
643
+ provider: IAuthProvider,
644
+ logger?: ILogger | null,
645
+ sessionId?: string,
646
+ );
647
+ }
648
+ // AdtCloudConnector has the same shape.
649
+ ```
650
+
651
+ The difference is what each does with the session: on-prem takes it from the
652
+ establishing call and gives it back with the platform's ICF logoff; cloud opens
653
+ one at `/sap/bc/adt/core/http/sessions` and gives it back by `DELETE` on the
654
+ address the server publishes.
655
+
656
+ ### `BaseAbapConnection` (Basic Auth) — deprecated
588
657
 
589
- For on-premise SAP systems:
658
+ The previous shape, where the class stated the credential. Still works; see
659
+ [Migration to 5.0](./MIGRATION-5.0.md).
590
660
 
591
661
  ```typescript
592
662
  class BaseAbapConnection extends AbstractAbapConnection {
@@ -670,7 +740,7 @@ export class CloudSdkAbapConnection {
670
740
  ```
671
741
 
672
742
 
673
- ### `JwtAbapConnection` (JWT/OAuth2)
743
+ ### `JwtAbapConnection` (JWT/OAuth2) — deprecated
674
744
 
675
745
  For SAP BTP cloud systems. Token refresh is handled by `@mcp-abap-adt/auth-broker`:
676
746
 
@@ -0,0 +1,108 @@
1
+ # Two connectors, one credential contract
2
+
3
+ **Status:** draft, for review
4
+ **Subject:** `AdtOnPremConnector` / `AdtCloudConnector` over a shared base, with authentication passed in rather than inherited.
5
+
6
+ ## Why
7
+
8
+ The class you take today states your **credential**, and the session mechanism rides along with it. `JwtAbapConnection` is the only class that overrides `createSessionStrategy()`, so:
9
+
10
+ | the consumer builds | on a **cloud** host | on an **on-prem** host |
11
+ |---|---|---|
12
+ | `authType: 'jwt'` | ADT session resource — correct | ADT session resource — wrong |
13
+ | `authType: 'basic'` | ICF logoff — wrong | ICF logoff — correct |
14
+ | `authType: 'saml'` | ICF logoff — wrong | correct |
15
+ | `authType: 'certificate'`, `'kerberos'` | ICF logoff — wrong | correct |
16
+
17
+ Both wrong rows are reachable. The cloud trial answers `www-authenticate: Basic`, so a communication user against ABAP Cloud is an ordinary setup; SAML is the usual interactive logon on BTP. And the mirror — JWT against an on-prem system — puts that system on a mechanism it does not use.
18
+
19
+ **The consumer knows which system it is dialling.** It should say so by taking the connector for it, and the credential should be a parameter of that choice rather than the thing making it.
20
+
21
+ ### Why not detect it
22
+
23
+ Tried, measured, wrong. `/sap/bc/adt/core/http/sessions` was probed to decide the mechanism; **on-prem answers it too** — 200, a `SAP_SESSIONID`, and a document publishing *both* the session resource and `/sap/public/bc/icf/logoff`. The probe never told the two systems apart; it told whether an endpoint exists, and both have it, so on-prem was silently moved onto the cloud mechanism.
24
+
25
+ Nothing here goes back to asking the server which kind of system it is.
26
+
27
+ ## The shape
28
+
29
+ ```
30
+ AbstractAbapConnection requests, CSRF, cookies, session lifecycle, admission
31
+ │
32
+ ├── AdtOnPremConnector session arrives with the establishing call;
33
+ │ given back with the platform's ICF logoff
34
+ │
35
+ └── AdtCloudConnector session opened at /sap/bc/adt/core/http/sessions
36
+ (`x-sap-security-session: create`,
37
+ `sap-adt-purpose: preflight_logon`);
38
+ given back by DELETE on the address it published
39
+ ```
40
+
41
+ Both take a **credential** object. The two session strategies already exist and do not change; what changes is who chooses them and how authentication arrives.
42
+
43
+ ## The credential contract
44
+
45
+ `ITokenRefresher` and `ITokenProvider` already exist in `@mcp-abap-adt/interfaces` — the first says outright *"Created by AuthBroker for a specific destination. Injected into JwtAbapConnection to enable automatic token refresh."* — and `@mcp-abap-adt/auth-providers` ships twelve implementations. That is the seam to join, not to reinvent.
46
+
47
+ But both are about **tokens**, and only one of the five ways in works that way. Measured, every current class overrides exactly two members, plus a few extras:
48
+
49
+ | | `buildAuthorizationHeader` | `establishSession` | other |
50
+ |---|---|---|---|
51
+ | basic | ✓ | ✓ | — |
52
+ | jwt | ✓ | ✓ | `prepareCredential`, `ensureToken` |
53
+ | saml | ✓ | ✓ | — |
54
+ | certificate | ✓ | ✓ | `getHttpsAgentOptions`, `ensureMaterial` |
55
+ | kerberos | ✓ | ✓ | `fetchCsrfToken` |
56
+
57
+ So the contract is small and is **not** "give me a token":
58
+
59
+ ```ts
60
+ interface IAbapCredential {
61
+ /** Prepare before anything is sent — mint, load, unlock. Optional. */
62
+ prepare?(): Promise<void>;
63
+ /** The Authorization header value, or '' when the credential is not a header. */
64
+ authorizationHeader(): string;
65
+ /** TLS material, for credentials that are a certificate rather than a header. */
66
+ httpsAgentOptions?(): AgentOptions;
67
+ }
68
+ ```
69
+
70
+ A token provider becomes one implementation of it, wrapping `ITokenRefresher`. Basic wraps a username and password. Certificate returns no header and supplies agent options instead.
71
+
72
+ **Where it lives:** `@mcp-abap-adt/interfaces`, released before the connector consumes it — the standing rule, no local bridge.
73
+
74
+ ## The hard part, named
75
+
76
+ `JwtAbapConnection` is 483 lines and **most of it is not authentication.** `renewalInFlight`, the `AsyncLocalStorage` operation scopes, `tokenGeneration` against `recoveredGeneration`, `ensureRecovered`, and the calls to `discardSession()` and `recoverSession()` are about what happens to *the session* when the credential is renewed mid-flight. That is connection business and stays in the base.
77
+
78
+ The split to make:
79
+
80
+ - **credential**: "get me a valid token" → the provider, behind `IAbapCredential`;
81
+ - **connection**: "the credential changed, so the session it built is gone — discard, re-establish, let the request retry once" → the base, unchanged in behaviour.
82
+
83
+ If this line is drawn wrongly the 4.0.0 work is undone, and that work exists because the failures it fixed were subtle: two layered single-flight promises, identity-checked clears, an epoch checked before a retry. **The spec's success condition is that every test from 4.0.0 passes untouched.**
84
+
85
+ ## Migration
86
+
87
+ The five current classes stay, deprecated, as thin wrappers: `new JwtAbapConnection(config, logger, sessionId, refresher)` builds `AdtCloudConnector` with a token credential. Nothing published breaks, and `createAbapConnection()` keeps working from `SapConfig` alone.
88
+
89
+ New code takes a connector and hands it a credential.
90
+
91
+ ## What this does not do
92
+
93
+ - **No auto-detection**, in any form — see above.
94
+ - **No change to the two session strategies.** They were measured against a live trial and an on-prem system and are not the subject here.
95
+ - **No new auth mechanism.** Only a place for the ones that exist to be passed in.
96
+ - **No dependency on the auth packages.** The connector keeps speaking contracts; consumers supply implementations, as with `IAbapConnection` today.
97
+
98
+ ## Open, to settle in review
99
+
100
+ 1. **`prepare()` and Kerberos.** SPNEGO deliberately does not prepare early: minting the token sooner changes when the exchange happens, and that path is not production-tested. Does Kerberos keep opting out, or does the contract grow a way to say "prepare late"?
101
+ 2. **What `createAbapConnection(config)` does when the config does not say which system it is.** Defaulting to on-prem keeps every existing consumer working and never guesses; refusing is more honest and breaks them. Deprecation-shaped default seems right, but it is a decision.
102
+ 3. **Whether `AdtOnPremConnector` should refuse the ADT session resource explicitly** — it exists there and answers, so a future edit could quietly start using it.
103
+
104
+ ## How it gets verified
105
+
106
+ - Cloud: from this machine, against the BTP trial — connect, hold a session across stateful requests, disconnect. Already the way the current mechanism was measured.
107
+ - On-prem: not reachable from here. Reviewed and run on the machine that reaches a system, as PR #34 was: strategy `icf`, `lock → PUT` chains, session present, and **no request to the session resource**.
108
+ - Both wrong rows from the table above become tests: a cloud connector with a basic credential must use the ADT session; an on-prem connector with a token credential must not.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",