@mcp-abap-adt/connection 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +407 -1
  2. package/README.md +239 -53
  3. package/dist/auth/providers.d.ts +124 -0
  4. package/dist/auth/providers.d.ts.map +1 -0
  5. package/dist/auth/providers.js +183 -0
  6. package/dist/connection/AbstractAbapConnection.d.ts +209 -57
  7. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  8. package/dist/connection/AbstractAbapConnection.js +459 -457
  9. package/dist/connection/AdtCloudConnector.d.ts +34 -0
  10. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  11. package/dist/connection/AdtCloudConnector.js +27 -0
  12. package/dist/connection/AdtOnPremConnector.d.ts +43 -0
  13. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtOnPremConnector.js +28 -0
  15. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  16. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  17. package/dist/connection/CloudHttpTransport.js +145 -0
  18. package/dist/connection/CredentialAbapConnection.d.ts +55 -0
  19. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  20. package/dist/connection/CredentialAbapConnection.js +128 -0
  21. package/dist/connection/HttpTransport.d.ts +178 -0
  22. package/dist/connection/HttpTransport.d.ts.map +1 -0
  23. package/dist/connection/HttpTransport.js +402 -0
  24. package/dist/connection/IAdtTransport.d.ts +232 -0
  25. package/dist/connection/IAdtTransport.d.ts.map +1 -0
  26. package/dist/connection/IAdtTransport.js +28 -0
  27. package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
  28. package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
  29. package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
  30. package/dist/connection/OnPremHttpTransport.d.ts +45 -0
  31. package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
  32. package/dist/connection/OnPremHttpTransport.js +91 -0
  33. package/dist/connection/RfcTransport.d.ts +89 -0
  34. package/dist/connection/RfcTransport.d.ts.map +1 -0
  35. package/dist/connection/RfcTransport.js +256 -0
  36. package/dist/connection/rfcConversation.d.ts +44 -0
  37. package/dist/connection/rfcConversation.d.ts.map +1 -0
  38. package/dist/connection/rfcConversation.js +71 -0
  39. package/dist/index.d.ts +10 -7
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +30 -18
  42. package/dist/session/SessionLifecycle.d.ts +17 -2
  43. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  44. package/dist/session/SessionLifecycle.js +17 -2
  45. package/dist/utils/cookies.d.ts +12 -0
  46. package/dist/utils/cookies.d.ts.map +1 -0
  47. package/dist/utils/cookies.js +24 -0
  48. package/dist/utils/timeouts.d.ts +6 -3
  49. package/dist/utils/timeouts.d.ts.map +1 -1
  50. package/dist/utils/timeouts.js +6 -3
  51. package/docs/INDEX.md +5 -2
  52. package/docs/INSTALLATION.md +28 -6
  53. package/docs/JWT_AUTH_TOOLS.md +20 -4
  54. package/docs/MIGRATION-2.0.md +1 -1
  55. package/docs/MIGRATION-5.0.md +116 -0
  56. package/docs/MIGRATION-6.0.md +359 -0
  57. package/docs/SCOPE.md +1 -1
  58. package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
  59. package/docs/USAGE.md +322 -111
  60. package/examples/basic-connection.js +15 -3
  61. package/examples/jwt-with-token-refresh.js +15 -7
  62. package/examples/saml-connection.js +15 -2
  63. package/package.json +12 -10
  64. package/dist/__tests__/helpers/session.d.ts +0 -15
  65. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  66. package/dist/__tests__/helpers/session.js +0 -19
  67. package/dist/connection/BaseAbapConnection.d.ts +0 -23
  68. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  69. package/dist/connection/BaseAbapConnection.js +0 -75
  70. package/dist/connection/CertificateAbapConnection.d.ts +0 -25
  71. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  72. package/dist/connection/CertificateAbapConnection.js +0 -79
  73. package/dist/connection/JwtAbapConnection.d.ts +0 -115
  74. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  75. package/dist/connection/JwtAbapConnection.js +0 -358
  76. package/dist/connection/KerberosAbapConnection.d.ts +0 -24
  77. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  78. package/dist/connection/KerberosAbapConnection.js +0 -120
  79. package/dist/connection/RfcAbapConnection.d.ts +0 -49
  80. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  81. package/dist/connection/RfcAbapConnection.js +0 -331
  82. package/dist/connection/SamlAbapConnection.d.ts +0 -25
  83. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  84. package/dist/connection/SamlAbapConnection.js +0 -75
  85. package/dist/connection/connectionFactory.d.ts +0 -9
  86. package/dist/connection/connectionFactory.d.ts.map +0 -1
  87. package/dist/connection/connectionFactory.js +0 -32
package/docs/INDEX.md CHANGED
@@ -15,6 +15,7 @@ mcp-abap-connection/
15
15
  │ ├── INSTALLATION.md # Setup and installation guide
16
16
  │ ├── USAGE.md # API documentation and examples
17
17
  │ ├── MIGRATION-2.0.md # Moving to the explicit session lifecycle
18
+ │ ├── MIGRATION-6.0.md # the factory and the per-credential classes go; RFC is a transport
18
19
  │ ├── MIGRATION-4.0.md # JWT error classification: 401 refreshes, 403 propagates
19
20
  │ ├── SCOPE.md # What this package does and does not own
20
21
  │ ├── STATEFUL_SESSION_GUIDE.md # Stateful requests and lock windows
@@ -43,6 +44,7 @@ mcp-abap-connection/
43
44
  - 🔑 [JWT Auth Tools](./JWT_AUTH_TOOLS.md) - CLI tool for browser-based authentication
44
45
 
45
46
  ### Upgrading
47
+ - 🚚 [Migrating to 6.0.0](./MIGRATION-6.0.md) - the factory and the per-credential classes are removed; the RFC wire is a transport you hand to the on-prem connector
46
48
  - 🧱 [Migrating to 4.0.0](./MIGRATION-4.0.md) - JWT error classification: a 401 refreshes, a 403 propagates with the server's message
47
49
  - 🧱 [Migrating to 2.0.0](./MIGRATION-2.0.md) - The explicit session lifecycle: `connect()` is required
48
50
 
@@ -72,8 +74,9 @@ mcp-abap-connection/
72
74
  ### API Reference
73
75
  - **Connection Interface**: [USAGE.md - API Reference](./USAGE.md#api-reference)
74
76
  - **Configuration Types**: [USAGE.md - Configuration Types](./USAGE.md#configuration-types)
75
- - **Factory Function**: `createAbapConnection()`
76
- - **Connection Classes**: `BaseAbapConnection`, `JwtAbapConnection`
77
+ - **Connectors**: `AdtOnPremConnector`, `AdtCloudConnector`
78
+ - **Credentials**: `BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`, `CertificateAuthProvider`
79
+ - **Transports**: `HttpTransport`, `RfcTransport` (+ `rfcConversationFrom()`)
77
80
 
78
81
  ## Version Highlights
79
82
 
@@ -83,7 +83,7 @@ In your code:
83
83
 
84
84
  ```typescript
85
85
  import 'dotenv/config'; // or require('dotenv').config();
86
- import { createAbapConnection } from '@mcp-abap-adt/connection';
86
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
87
87
 
88
88
  const config = {
89
89
  url: process.env.SAP_URL!,
@@ -138,7 +138,7 @@ See [JWT_AUTH_TOOLS.md](./JWT_AUTH_TOOLS.md) for detailed CLI documentation.
138
138
  ### Test Installation
139
139
 
140
140
  ```bash
141
- node -e "const { createAbapConnection } = require('@mcp-abap-adt/connection'); console.log('✓ Package loaded successfully');"
141
+ node -e "const { AdtOnPremConnector } = require('@mcp-abap-adt/connection'); console.log('✓ Package loaded successfully');"
142
142
  ```
143
143
 
144
144
  ### Test Connection (Basic Auth)
@@ -146,7 +146,7 @@ node -e "const { createAbapConnection } = require('@mcp-abap-adt/connection'); c
146
146
  Create `test-connection.js`:
147
147
 
148
148
  ```javascript
149
- const { createAbapConnection } = require('@mcp-abap-adt/connection');
149
+ const { AdtOnPremConnector, BasicAuthProvider } = require('@mcp-abap-adt/connection');
150
150
 
151
151
  const config = {
152
152
  url: 'https://your-sap-server.com',
@@ -163,7 +163,15 @@ const logger = {
163
163
  debug: (msg) => console.log('[DEBUG]', msg),
164
164
  };
165
165
 
166
- const connection = createAbapConnection(config, logger);
166
+ const connection = new AdtOnPremConnector(
167
+ config,
168
+ new BasicAuthProvider(config.username!, config.password!),
169
+ new OnPremHttpTransport(() => ({}), logger, {
170
+ client: config.client,
171
+ baseUrl: config.url,
172
+ }),
173
+ logger,
174
+ );
167
175
 
168
176
  connection.connect()
169
177
  .then(() =>
@@ -214,7 +222,13 @@ npm install --save-dev typescript @types/node
214
222
  ### TypeScript Example
215
223
 
216
224
  ```typescript
217
- import { createAbapConnection, SapConfig, ILogger } from '@mcp-abap-adt/connection';
225
+ import {
226
+ AdtOnPremConnector,
227
+ BasicAuthProvider,
228
+ ILogger,
229
+ OnPremHttpTransport,
230
+ SapConfig,
231
+ } from '@mcp-abap-adt/connection';
218
232
 
219
233
  const config: SapConfig = {
220
234
  url: 'https://your-sap-server.com',
@@ -231,7 +245,15 @@ const logger: ILogger = {
231
245
  debug: (msg: string) => console.log(msg),
232
246
  };
233
247
 
234
- const connection = createAbapConnection(config, logger);
248
+ const connection = new AdtOnPremConnector(
249
+ config,
250
+ new BasicAuthProvider(config.username!, config.password!),
251
+ new OnPremHttpTransport(() => ({}), logger, {
252
+ client: config.client,
253
+ baseUrl: config.url,
254
+ }),
255
+ logger,
256
+ );
235
257
  ```
236
258
 
237
259
  ## Troubleshooting
@@ -123,13 +123,29 @@ source .env
123
123
  Or configure `SapConfig` directly:
124
124
 
125
125
  ```ts
126
- import { createAbapConnection } from "@mcp-abap-adt/connection";
127
-
128
- const connection = createAbapConnection({
126
+ import {
127
+ AdtCloudConnector,
128
+ CloudHttpTransport,
129
+ TokenAuthProvider,
130
+ } from "@mcp-abap-adt/connection";
131
+ import type { SapConfig } from "@mcp-abap-adt/connection";
132
+
133
+ const config: SapConfig = {
129
134
  url: process.env.SAP_URL!,
130
135
  authType: "jwt",
131
136
  jwtToken: process.env.SAP_JWT_TOKEN!,
132
- });
137
+ };
138
+ const logger = console;
139
+
140
+ const connection = new AdtCloudConnector(
141
+ config,
142
+ new TokenAuthProvider(config.jwtToken!),
143
+ new CloudHttpTransport(() => ({}), logger, {
144
+ client: config.client,
145
+ baseUrl: config.url,
146
+ }),
147
+ logger,
148
+ );
133
149
 
134
150
  // Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
135
151
  // The refresh token credentials in .env are used by auth-broker, not connection
@@ -46,7 +46,7 @@ later, on the first request, as an unrelated-looking 401.
46
46
 
47
47
  ### 3. Requests are refused after a teardown
48
48
 
49
- `disconnect()` and `reset()` stop the connection from serving requests until the
49
+ `disconnect()` stops the connection from serving requests until the
50
50
  next `connect()`. An in-flight request is not cut off and not waited for either:
51
51
  it runs to completion, and its result is fenced so it cannot touch a session
52
52
  established since.
@@ -0,0 +1,116 @@
1
+ # Migrating to 5.0.0
2
+
3
+ Two things changed, and the second is why the first was possible.
4
+
5
+ **A connection now closes what it opened.** `disconnect()` tells the server the
6
+ session is finished instead of only dropping the cookie, and `connect()` fails
7
+ when the server opened no session rather than handing back a connection whose
8
+ first lock would be dead.
9
+
10
+ **The class you take says which system you are dialling, not which credential
11
+ you hold.** `AdtOnPremConnector` and `AdtCloudConnector` are handed an auth
12
+ provider; the five auth classes still work and are deprecated.
13
+
14
+ ---
15
+
16
+ ## If you use `createAbapConnection()`
17
+
18
+ It still works, unchanged, and now warns once per call that the connection is
19
+ being chosen from `authType`. Say which system instead:
20
+
21
+ ```ts
22
+ // before
23
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher);
24
+
25
+ // after
26
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher, {
27
+ system: 'cloud', // or 'onprem'
28
+ });
29
+ ```
30
+
31
+ Nothing is detected. The two systems do not manage sessions the same way, and
32
+ asking the server which it is does not work: `/sap/bc/adt/core/http/sessions`
33
+ answers on on-prem too, and its `DELETE` there leaves the session open while the
34
+ platform logoff removes it. Only you know where you are pointing.
35
+
36
+ ## If you build a connection class directly
37
+
38
+ ```ts
39
+ // before // after
40
+ new BaseAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new BasicAuthProvider(user, pass), log)
41
+ new JwtAbapConnection(cfg, log, id, refr) new AdtCloudConnector(cfg, new TokenAuthProvider(refr), log)
42
+ new SamlAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new SamlAuthProvider(cookies), log)
43
+ new CertificateAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new CertificateAuthProvider(loader, cfg), log)
44
+ ```
45
+
46
+ `KerberosAbapConnection` has no provider yet — its SPNEGO exchange *is* the CSRF
47
+ fetch — so keep using the class. See #35.
48
+
49
+ The old classes are not removed and not scheduled for removal here.
50
+
51
+ ## `reset()` is gone — BREAKING
52
+
53
+ There is no local-only discard, because there is no local-only session: it lives
54
+ on the server, and dropping the cookie leaves it there.
55
+
56
+ ```ts
57
+ conn.reset(); // before
58
+ await conn.disconnect(); // after — and connect() again to carry on
59
+ ```
60
+
61
+ A caller that does not want to wait simply does not `await` it, which is what
62
+ `reset()` was mostly used for. `RfcAbapConnection` keeps `close()`.
63
+
64
+ ## `connect()` can now fail where it used to warn — BREAKING
65
+
66
+ When the server authenticates the request but opens no session, `connect()`
67
+ rejects with `ADT_NOT_CONNECTED` and a message saying what happened, what still
68
+ works, and the usual cause. It used to log a warning and hand the connection
69
+ back — and the failure then surfaced a request later, as `400 Session not found`
70
+ with the object half-edited.
71
+
72
+ Verified rather than assumed: a connection that received no `SAP_SESSIONID` is
73
+ listed in the server's own session list as **nothing at all**.
74
+
75
+ Nothing is retried on your behalf. Whether to wait, retry, or release sessions
76
+ the user still holds depends on things only you know.
77
+
78
+ ## `disconnect()` now makes a network call
79
+
80
+ It tells the server the session is finished. Two consequences:
81
+
82
+ - **It can wait.** By default it does not: `SAP_RELEASE_DEADLINE_MS` is `0`,
83
+ because waiting is for steps whose successor needs the server to have caught
84
+ up, and a teardown has none. Pass `disconnect({ deadlineMs })` if you want a
85
+ bounded wait — the deadline bounds the **wait**, never the request.
86
+ - **Requests still in flight will start failing.** The session they are running
87
+ on is the one being released. That is you having asked to disconnect, not a
88
+ race — finish or abandon your chains first.
89
+
90
+ It still never throws, including on a nonsense `deadlineMs`: its place is a
91
+ `finally`, where an exception would replace the error that sent you there.
92
+
93
+ ## Token renewal, if you use a provider
94
+
95
+ Nothing to do, but worth knowing what changed. The connector no longer caches
96
+ the token it was given. It asks the provider per request — cheap, because the
97
+ provider caches — and on a `401` it tells the provider its answer was refused
98
+ (`refreshToken()`, per that contract), asks again, and only if the answer
99
+ changed rebuilds the session and retries once. An unchanged answer means the
100
+ server refused those credentials, and the `401` reaches you.
101
+
102
+ So a password does the right thing with no configuration, and a bearer token is
103
+ renewed without a class of its own.
104
+
105
+ ## Sessions are a shared, per-user resource
106
+
107
+ Not an API change, a fact worth having. The limit is per user and the pool is
108
+ shared with everything else logged on as them, a SAP GUI session included. The
109
+ timeout is **idle-based**: a connection that keeps working keeps its session,
110
+ one that goes quiet past the window loses it. There is no keepalive timer here,
111
+ deliberately — how long to hold a session is yours to decide.
112
+
113
+ And the shape of the problem this release addresses, seen from the other end: a
114
+ process that opens a connection per check and never disconnects filled a
115
+ system's session list at one session every ten seconds. Releasing what you open
116
+ is half of it; opening one connection instead of one per check is the other.
@@ -0,0 +1,359 @@
1
+ # Migration to 6.0
2
+
3
+ The factory and the six connection classes are gone. What replaces them is not a
4
+ new API so much as the one 5.0 introduced, now the only one: **you state which
5
+ system, which credential, and which wire, and nothing is worked out for you.**
6
+
7
+ **The wire is a required argument.** There is no default to fall back to and
8
+ nothing is derived from the config, because which wire a deployment uses is a
9
+ fact about the deployment. It also carries the two things only you can supply —
10
+ the credential's TLS material and the client — so it arrives built.
11
+
12
+ If you already moved to `AdtOnPremConnector` / `AdtCloudConnector` in 5.0, most
13
+ of this does not apply to you — skip to [Taking the RFC wire](#taking-the-rfc-wire).
14
+
15
+ ## What was removed
16
+
17
+ | removed | take instead |
18
+ |---|---|
19
+ | `createAbapConnection()` | `new AdtOnPremConnector(...)` or `new AdtCloudConnector(...)` |
20
+ | `BaseAbapConnection`, `OnPremAbapConnection` | `AdtOnPremConnector` + `BasicAuthProvider` |
21
+ | `JwtAbapConnection`, `CloudAbapConnection` | `AdtCloudConnector` + `TokenAuthProvider` |
22
+ | `SamlAbapConnection` | `AdtOnPremConnector` + `SamlAuthProvider` |
23
+ | `CertificateAbapConnection` | `AdtOnPremConnector` + `CertificateAuthProvider` |
24
+ | `KerberosAbapConnection` | — see [Kerberos](#kerberos) |
25
+ | `RfcAbapConnection`, `connectionType: 'rfc'` | `AdtOnPremConnector` + `RfcTransport` |
26
+ | `disconnect({ deadlineMs })` | `disconnect()` — see [Teardown](#teardown) |
27
+
28
+ ## Why
29
+
30
+ The factory switched on things the caller had already said. Given
31
+ `options.system` it did no inference at all, and without it, it inferred the
32
+ session mechanism from the credential — which is the arrangement that gets one
33
+ of them wrong: a bearer token against an on-prem system is ordinary, and it does
34
+ not make that system a cloud one.
35
+
36
+ The classes had the same shape of problem one level down. Six subclasses
37
+ distinguished by an argument, and `RfcAbapConnection` was a second translation of
38
+ `SADT_REST_RFC_ENDPOINT` beside `RfcTransport` — two of those drift, and these
39
+ had: the class was missing a default `Accept` header and carried cookie-handling
40
+ code that could never run.
41
+
42
+ ## Basic
43
+
44
+ ```diff
45
+ -import { createAbapConnection } from '@mcp-abap-adt/connection';
46
+ +import {
47
+ + AdtOnPremConnector,
48
+ + BasicAuthProvider,
49
+ +} from '@mcp-abap-adt/connection';
50
+
51
+ -const connection = createAbapConnection(config, logger, undefined, undefined, {
52
+ - system: 'onprem',
53
+ -});
54
+ +const connection = new AdtOnPremConnector(
55
+ + config,
56
+ + new BasicAuthProvider(config.username!, config.password!),
57
+ + new OnPremHttpTransport(() => ({}), logger, {
58
+ + client: config.client,
59
+ + baseUrl: config.url,
60
+ + }),
61
+ + logger,
62
+ +);
63
+ ```
64
+
65
+ `client` is not decoration: SAP answers `sap-usercontext` with the system
66
+ default rather than the client you asked for, and later requests then route to a
67
+ client you never named — on a read-only one, every write comes back `403`.
68
+
69
+ ## JWT / OAuth2
70
+
71
+ ```diff
72
+ -const connection = createAbapConnection(config, logger, undefined, refresher, {
73
+ - system: 'cloud',
74
+ -});
75
+ +const connection = new AdtCloudConnector(
76
+ + config,
77
+ + new TokenAuthProvider(refresher),
78
+ + new CloudHttpTransport(() => ({}), logger, {
79
+ + client: config.client,
80
+ + baseUrl: config.url,
81
+ + }),
82
+ + logger,
83
+ +);
84
+ ```
85
+
86
+ The cloud wire is not the on-prem one with a different name. It asks for a
87
+ session at `/sap/bc/adt/core/http/sessions` and gives it back by `DELETE` on the
88
+ address the server publishes; the on-prem wire has neither, and says goodbye
89
+ through the platform logoff. Handing the wrong one to a connector does not
90
+ compile.
91
+
92
+ The `ITokenRefresher` that used to be a constructor slot **is** the credential
93
+ now. Hand `TokenAuthProvider` a bare string instead and you get a token with
94
+ nothing behind it — fine for a short task, wrong for anything long-lived.
95
+
96
+ One behaviour to know about: **a credential refused during `connect()` now
97
+ surfaces.** The old class renewed and retried inside its own CSRF fetch. It does
98
+ not any more, and deliberately: renewal is the provider's, and the provider does
99
+ it on an expiry it can see, on every call that asks for a header. A token the
100
+ provider still believes in and the server refuses is a credential that needs
101
+ attention, and `connect()` is one call you make — so the refusal is yours to
102
+ answer.
103
+
104
+ The request path changed the same way, and in the same direction: a 401 is no
105
+ longer answered here either. See [A refused credential](#a-refused-credential).
106
+
107
+ ## SAML
108
+
109
+ ```diff
110
+ -const connection = new SamlAbapConnection(config, logger);
111
+ +const connection = new AdtOnPremConnector(
112
+ + config,
113
+ + new SamlAuthProvider(config.sessionCookies!),
114
+ + new OnPremHttpTransport(() => ({}), logger, {
115
+ + client: config.client,
116
+ + baseUrl: config.url,
117
+ + }),
118
+ + logger,
119
+ +);
120
+ ```
121
+
122
+ The cookies are the credential — there is no `Authorization` header at all.
123
+
124
+ ## Certificates
125
+
126
+ A certificate authenticates through the transport rather than through a header,
127
+ so this is the one place the two axes touch — and you wire them, because nobody
128
+ else can:
129
+
130
+ ```diff
131
+ -const connection = new CertificateAbapConnection(config, logger);
132
+ +const credential = new CertificateAuthProvider(config, certLoader);
133
+ +
134
+ +const connection = new AdtOnPremConnector(
135
+ + config,
136
+ + credential,
137
+ + new OnPremHttpTransport(
138
+ + // A thunk, not a value: the material is loaded during connect(), so a
139
+ + // wire that read it at construction would read nothing.
140
+ + () => credential.transportMaterial?.() ?? {},
141
+ + logger,
142
+ + { client: config.client, baseUrl: config.url },
143
+ + ),
144
+ + logger,
145
+ +);
146
+ ```
147
+
148
+ Forget the thunk and mTLS silently does not happen — the connection is built,
149
+ the requests go out, and the server refuses them for a reason that says nothing
150
+ about the certificate.
151
+
152
+ ## Taking the RFC wire
153
+
154
+ `connectionType: 'rfc'` is gone. The wire is an argument now, which is what it
155
+ always was in fact:
156
+
157
+ ```diff
158
+ -const connection = createAbapConnection(
159
+ - { ...config, connectionType: 'rfc' },
160
+ - logger,
161
+ -);
162
+ +import {
163
+ + AdtOnPremConnector,
164
+ + BasicAuthProvider,
165
+ + RfcTransport,
166
+ + rfcConversationFrom,
167
+ +} from '@mcp-abap-adt/connection';
168
+ +
169
+ +const connection = new AdtOnPremConnector(
170
+ + config,
171
+ + new BasicAuthProvider(config.username!, config.password!),
172
+ + new RfcTransport(rfcConversationFrom(config), logger),
173
+ + logger,
174
+ +);
175
+ ```
176
+
177
+ The wire sits where the HTTP one sits, because it is the same argument. There is
178
+ no option to set and no config field to flip.
179
+
180
+ `rfcConversationFrom(config)` does what the class's constructor did: `ashost`
181
+ from the url, `sysnr` from the HTTP port by the SAP convention that `80XX` is
182
+ the ICM port for system `XX`, `SAP_SYSNR` overriding it for a port that follows
183
+ no convention. The SAP NW RFC SDK is loaded when a conversation opens, not when
184
+ this is called, so a machine without it fails at `connect()` with a message
185
+ saying what to install.
186
+
187
+ Everything above the wire is unchanged: `makeAdtRequest`, `setSessionType`,
188
+ `disconnect`, and the session lifecycle. Three things are better than they were
189
+ on the removed class:
190
+
191
+ - a default `Accept` is supplied, so a call that names none is answered rather
192
+ than refused with `400 ExceptionResourceBadRequest`
193
+ - `disconnect()` and `getSessionIdentity()` exist, which they did not
194
+ - the identity is the conversation, and it changes when the conversation does —
195
+ so reconnecting reads as a different session instead of the same one
196
+
197
+ **Where to look for the session.** An HTTP session is an ICF session and appears
198
+ in **SM05**. An RFC conversation appears in **SMGW → Logged on Clients** as
199
+ `NWRFC`, and never in SM05, because there is no ICM in that path.
200
+
201
+ ## Teardown
202
+
203
+ `disconnect()` takes no arguments.
204
+
205
+ ```diff
206
+ -await connection.disconnect({ deadlineMs: 5000 });
207
+ +await connection.disconnect();
208
+ ```
209
+
210
+ It **notifies**: it tells the server the session is finished and does not act on
211
+ the answer — whether and when the session is freed is the server's affair. The
212
+ `deadlineMs` it used to accept bounded a wait for that answer, which bought you
213
+ nothing and was the one thing that could make a teardown unbounded, since the
214
+ goodbye carries no request timeout by design. `SAP_RELEASE_DEADLINE_MS` is gone
215
+ with it.
216
+
217
+ Everything else is unchanged: it resolves rather than throws, always settles,
218
+ and a repeat call performs whatever is still owed.
219
+
220
+ ## Writing your own credential
221
+
222
+ The shipped ones need no change from you — they are handed to a connector and
223
+ that is all. If you wrote your own against `IAuthProvider`, it now states all of
224
+ itself. Five members, and three of them are usually empty:
225
+
226
+ ```diff
227
+ class MyCredential implements IAuthProvider {
228
+ readonly kind = 'mine';
229
+ + async prepare(): Promise<void> {}
230
+ async authorizationHeader(): Promise<string | null> { … }
231
+ + cookies(): string | null { return null; }
232
+ + transportMaterial(): ICertificateMaterial { return {}; }
233
+ }
234
+ ```
235
+
236
+ Empty is not ceremony: "nothing to prepare", "I am not cookies", "I contribute
237
+ no TLS material" are facts about a credential, and a fact is stated rather than
238
+ left for a connection to discover by checking whether a method exists. What it
239
+ buys is that no base class asks anything — every question about a collaborator
240
+ is answered by the type.
241
+
242
+ **One credential still has no home, and this is the honest statement of it.**
243
+ SPNEGO's token is consumed by the first request that carries it, so the exchange
244
+ and the establishing call are the same act — and `establish()` may retry, asking
245
+ `authorizationHeader()` again per attempt, which for that credential means
246
+ presenting something already spent.
247
+
248
+ An earlier cut of this release answered it with an atom the connection narrowed
249
+ to. That is gone: a contract earns its place by something implementing it, and
250
+ nothing here implements that one. What such a credential needs is either an
251
+ exchange it owns end to end, or a signal that the establishing request
252
+ succeeded — neither exists yet, and inventing the seam before there is a
253
+ credential to test it against is how the last one came to be removed.
254
+
255
+ Everything shipping today authenticates by header, cookie or TLS material, and
256
+ those three are what `IAuthProvider` states.
257
+
258
+ Needs `@mcp-abap-adt/interfaces` 21.0.0.
259
+
260
+ ## A refused credential
261
+
262
+ The connector used to answer a `401` for you: it called `renew()` on the
263
+ credential, compared the header with the previous one, and rebuilt the session if
264
+ it had changed. It does not any more, and nothing replaces it — the refusal
265
+ surfaces.
266
+
267
+ Renewal splits in two, and only one half was ever automatic:
268
+
269
+ - a token that **expired** is replaced inside `authorizationHeader()`, which is
270
+ asked per request. Nobody decides anything, and nothing changed here;
271
+ - a token the provider still **believes in** and the server refuses is a
272
+ different thing, and whether that is what happened is a judgement made with
273
+ what you know.
274
+
275
+ So handle it where you can:
276
+
277
+ ```diff
278
+ -await conn.makeAdtRequest(options); // a 401 was retried underneath you
279
+ +try {
280
+ + await conn.makeAdtRequest(options);
281
+ +} catch (error) {
282
+ + if (isUnauthorized(error) && isRenewable(credential)) {
283
+ + await credential.renew();
284
+ + // ... and decide for yourself whether to try again
285
+ + }
286
+ + throw error;
287
+ +}
288
+ ```
289
+
290
+ `isRenewable` is the narrowing the contract recommends, since only some
291
+ credentials have it — a password has nothing behind it to ask again, and a SAML
292
+ session was negotiated elsewhere:
293
+
294
+ ```typescript
295
+ import type { IAuthProvider, IRenewableCredential } from '@mcp-abap-adt/interfaces';
296
+
297
+ function isRenewable(c: IAuthProvider): c is IRenewableCredential {
298
+ return typeof (c as Partial<IRenewableCredential>).renew === 'function';
299
+ }
300
+ ```
301
+
302
+ Needs `@mcp-abap-adt/interfaces` 19.0.0, where `renew()` becomes that atom
303
+ instead of an optional member every credential carried.
304
+
305
+ **The connection is not torn down over it.** A refused credential is not a lost
306
+ session, and discarding one the server never complained about would throw away
307
+ work you can still finish.
308
+
309
+ ## Kerberos
310
+
311
+ `KerberosAbapConnection` is removed without a direct replacement. It was
312
+ single-leg only, and untested against a live KDC — issue #35 says so. If you
313
+ depend on it, stay on 5.x and say so on that issue; a `KerberosAuthProvider`
314
+ belongs on the credential axis and can be added there, but it should be added
315
+ with a system to test it against.
316
+
317
+ ## Types you can now name
318
+
319
+ The seam is public, so a signature can say what it needs instead of taking any
320
+ connection and casting:
321
+
322
+ ```typescript
323
+ import { AdtOnPremConnector, RfcTransport } from '@mcp-abap-adt/connection';
324
+ import type {
325
+ IAdtTransport,
326
+ IAdtTransportRequest,
327
+ IAdtTransportResponse,
328
+ IAdtEstablishContext,
329
+ IAdtSessionContext,
330
+ IOnPremTransport,
331
+ ICloudTransport,
332
+ IRfcConversation,
333
+ RfcConnectionParams,
334
+ } from '@mcp-abap-adt/connection';
335
+
336
+ function overRfc(conn: AdtOnPremConnector<IAuthProvider, RfcTransport>) { /* ... */ }
337
+ ```
338
+
339
+ ### Bringing your own wire
340
+
341
+ The connectors are constrained by a marker, not by the shipped classes, so a
342
+ transport you write is a first-class one. Say which system it is for and it fits
343
+ where the shipped wires fit:
344
+
345
+ ```typescript
346
+ import type { IOnPremTransport } from '@mcp-abap-adt/connection';
347
+
348
+ class RecordingTransport implements IOnPremTransport {
349
+ readonly kind = 'recording';
350
+ readonly system = 'onprem' as const;
351
+ // ... the rest of IAdtTransport
352
+ }
353
+ ```
354
+
355
+ `system` is read by the compiler and never at runtime. Its only job is to stop
356
+ "ABAP Cloud over an on-prem wire" from compiling — which is also why the
357
+ constraint is this marker and not `OnPremHttpTransport`: the shipped classes
358
+ carry private state and compare nominally, so constraining to them would have
359
+ made this impossible while appearing to allow it.
package/docs/SCOPE.md CHANGED
@@ -35,7 +35,7 @@ It forwards every `(name, value)` pair from the JS `Client(params)` object to `R
35
35
 
36
36
  **4. For BTP on-prem scenarios (Cloud Connector), the JWT→x509/SNC swap happens server-side in the CC.** The client-side RFC call still carries classic credentials (x509 + SNC), not the JWT. Supporting that would be adding SNC-over-RFC for on-prem backends behind a CC tunnel — an on-prem extension, not a "cloud RFC" feature.
37
37
 
38
- **Conclusion:** "RFC to cloud" is not a missing feature that would be useful to add; it is a category error. ADT to cloud = HTTP (already supported here). RFC to on-prem = supported here via `connectionType: 'rfc'`. Anything claiming a third combination does not map to what the SAP stack actually exposes.
38
+ **Conclusion:** "RFC to cloud" is not a missing feature that would be useful to add; it is a category error. ADT to cloud = HTTP (already supported here). RFC to on-prem = supported here, by handing `AdtOnPremConnector` an `RfcTransport`. Anything claiming a third combination does not map to what the SAP stack actually exposes.
39
39
 
40
40
  ## See also
41
41