@mcp-abap-adt/connection 5.0.0 → 6.0.1

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 (92) hide show
  1. package/CHANGELOG.md +243 -1
  2. package/README.md +204 -48
  3. package/bin/sap-abap-auth.js +101 -13
  4. package/dist/auth/providers.d.ts +38 -9
  5. package/dist/auth/providers.d.ts.map +1 -1
  6. package/dist/auth/providers.js +46 -3
  7. package/dist/connection/AbstractAbapConnection.d.ts +83 -136
  8. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  9. package/dist/connection/AbstractAbapConnection.js +241 -763
  10. package/dist/connection/AdtCloudConnector.d.ts +12 -7
  11. package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
  12. package/dist/connection/AdtCloudConnector.js +2 -6
  13. package/dist/connection/AdtOnPremConnector.d.ts +20 -7
  14. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
  15. package/dist/connection/AdtOnPremConnector.js +2 -6
  16. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  17. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  18. package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
  19. package/dist/connection/CredentialAbapConnection.d.ts +8 -41
  20. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
  21. package/dist/connection/CredentialAbapConnection.js +44 -111
  22. package/dist/connection/HttpTransport.d.ts +178 -0
  23. package/dist/connection/HttpTransport.d.ts.map +1 -0
  24. package/dist/connection/HttpTransport.js +402 -0
  25. package/dist/connection/IAdtTransport.d.ts +232 -0
  26. package/dist/connection/IAdtTransport.d.ts.map +1 -0
  27. package/dist/connection/IAdtTransport.js +28 -0
  28. package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
  29. package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
  30. package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
  31. package/dist/connection/OnPremHttpTransport.d.ts +45 -0
  32. package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
  33. package/dist/connection/OnPremHttpTransport.js +91 -0
  34. package/dist/connection/RfcTransport.d.ts +89 -0
  35. package/dist/connection/RfcTransport.d.ts.map +1 -0
  36. package/dist/connection/RfcTransport.js +270 -0
  37. package/dist/connection/rfcConversation.d.ts +44 -0
  38. package/dist/connection/rfcConversation.d.ts.map +1 -0
  39. package/dist/connection/rfcConversation.js +71 -0
  40. package/dist/index.d.ts +7 -8
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +21 -18
  43. package/dist/utils/timeouts.d.ts +6 -19
  44. package/dist/utils/timeouts.d.ts.map +1 -1
  45. package/dist/utils/timeouts.js +6 -22
  46. package/docs/INDEX.md +5 -2
  47. package/docs/INSTALLATION.md +28 -10
  48. package/docs/JWT_AUTH_TOOLS.md +20 -4
  49. package/docs/MIGRATION-6.0.md +359 -0
  50. package/docs/SCOPE.md +1 -1
  51. package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
  52. package/docs/USAGE.md +260 -119
  53. package/examples/basic-connection.js +15 -3
  54. package/examples/jwt-with-token-refresh.js +15 -7
  55. package/examples/saml-connection.js +15 -2
  56. package/package.json +12 -11
  57. package/dist/__tests__/helpers/session.d.ts +0 -15
  58. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  59. package/dist/__tests__/helpers/session.js +0 -19
  60. package/dist/auth/IAuthProvider.d.ts +0 -84
  61. package/dist/auth/IAuthProvider.d.ts.map +0 -1
  62. package/dist/auth/IAuthProvider.js +0 -21
  63. package/dist/connection/BaseAbapConnection.d.ts +0 -29
  64. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  65. package/dist/connection/BaseAbapConnection.js +0 -81
  66. package/dist/connection/CertificateAbapConnection.d.ts +0 -35
  67. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  68. package/dist/connection/CertificateAbapConnection.js +0 -91
  69. package/dist/connection/JwtAbapConnection.d.ts +0 -131
  70. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  71. package/dist/connection/JwtAbapConnection.js +0 -376
  72. package/dist/connection/KerberosAbapConnection.d.ts +0 -32
  73. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  74. package/dist/connection/KerberosAbapConnection.js +0 -128
  75. package/dist/connection/RfcAbapConnection.d.ts +0 -44
  76. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  77. package/dist/connection/RfcAbapConnection.js +0 -324
  78. package/dist/connection/SamlAbapConnection.d.ts +0 -31
  79. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  80. package/dist/connection/SamlAbapConnection.js +0 -81
  81. package/dist/connection/connectionFactory.d.ts +0 -25
  82. package/dist/connection/connectionFactory.d.ts.map +0 -1
  83. package/dist/connection/connectionFactory.js +0 -84
  84. package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
  85. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
  86. package/dist/session/IcfSessionStrategy.d.ts +0 -27
  87. package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
  88. package/dist/session/IcfSessionStrategy.js +0 -62
  89. package/dist/session/SessionStrategy.d.ts +0 -86
  90. package/dist/session/SessionStrategy.d.ts.map +0 -1
  91. package/dist/session/SessionStrategy.js +0 -33
  92. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +0 -108
package/CHANGELOG.md CHANGED
@@ -7,6 +7,246 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.0.1] - 2026-08-25
11
+
12
+ ### Removed
13
+
14
+ - **`express` is no longer a dependency.** It was a runtime dependency for one
15
+ thing: the localhost server in `bin/sap-abap-auth.js` that catches the OAuth2
16
+ redirect. The library never touched it, and that file already imported
17
+ `node:http`. Rewritten on `node:http`, which sheds 5.6MB of `node_modules` and
18
+ the transitive tree the Dependabot bumps kept arriving for — `qs`,
19
+ `body-parser` and `follow-redirects` were all express's.
20
+
21
+ Two things express was doing implicitly are now stated: a request is the
22
+ redirect only when it is a `GET` for `/callback`, so a browser fetching
23
+ `/favicon.ico` no longer reaches the handler waiting for an authorization
24
+ code; and the success page declares `text/html`, which `res.send()` used to
25
+ infer. Exercised as a server: `200` with the page, `400` without a code, `404`
26
+ for anything else including a `POST`.
27
+
28
+ Runtime dependencies are now `@mcp-abap-adt/interfaces`, `axios`, `commander`
29
+ and `open`. The end-to-end flow was measured against the BTP trial — see
30
+ *Verified* below.
31
+
32
+ ### Changed
33
+
34
+ - **The browser wait is the caller's to set.** `bin/sap-abap-auth.js` waited a
35
+ fixed five minutes for the redirect, and a real BTP login does not fit in it —
36
+ the identity provider, a password manager and whatever second factor the
37
+ tenant asks for all happen before the redirect is issued. Measured twice, and
38
+ both times the window closed first; the browser then had nowhere to redirect
39
+ to, which reads as the authentication having failed rather than as a timeout.
40
+
41
+ `--timeout <minutes>`, or `SAP_AUTH_TIMEOUT_MS` for a script that would rather
42
+ not pass an argument, with the flag winning when both are given. Anything that
43
+ is not a positive number is refused by name rather than silently becoming a
44
+ default. **The default is now fifteen minutes**, which covered the login this
45
+ was measured against with room to spare and still bounds an abandoned run.
46
+
47
+ ### Verified
48
+
49
+ - **The whole chain, end to end**: `bin/sap-abap-auth.js auth` → browser
50
+ authorization against the tenant's UAA → redirect to `localhost:3001/callback`
51
+ → code exchanged for a token → `.env` written with `SAP_JWT_TOKEN`,
52
+ `SAP_REFRESH_TOKEN` and the UAA fields.
53
+
54
+ - Each seam the express removal touched, measured on the real handler rather
55
+ than a stand-in: `GET /favicon.ico` answers `404` and does not reach the
56
+ handler waiting for a code — routing express used to do; `GET /callback` with
57
+ no code answers `400`; the success page comes back
58
+ `200 text/html; charset=utf-8`, the Content-Type `res.send()` used to infer
59
+ and `node:http` will not.
60
+
61
+ - **The issued token then drove this library** against the trial:
62
+ `AdtCloudConnector` with `CloudHttpTransport` connected, took a session
63
+ (`SAP_SESSIONID_TRL_100`), answered a `GET /sap/bc/adt/discovery` with `200`
64
+ and 444826 bytes of XML, and `disconnect()` left `isConnected()` false.
65
+
66
+ ### Fixed
67
+
68
+ - **The live RFC suite skips a wire the machine did not install.** It asked for
69
+ four environment variables and nothing else, so a checkout with an env file
70
+ and no SAP NW RFC SDK ran the suite anyway and went eleven tests red, all on
71
+ `@mcp-abap-adt/sap-rfc-lite is not available`. The dependency is `optional` —
72
+ `npm ci` leaves it out WITHOUT failing, which is what optional means — so
73
+ nothing warns and the next run is red for a reason unrelated to the change
74
+ under test.
75
+
76
+ Nothing here is configured at build time the way a C package is: the installed
77
+ optional dependency IS the choice of wire, made at install and readable only
78
+ at runtime. `canRun()` now reads it with `require.resolve`, asking whether the
79
+ wire could be taken rather than loading a native addon to find out. Measured
80
+ in both states: SDK hidden — 11 skipped, 2 passed, green; SDK present — 13
81
+ passed.
82
+
83
+ - `RfcTransport` says why it ignores `request.timeout`. No behaviour change: the
84
+ file contained no occurrence of the word, and that silence read as an
85
+ oversight. It is deliberate — the SDK exposes no cancel, its per-call
86
+ `timeout` is parsed into a commented-out branch, and an abandoned call still
87
+ holds the conversation, so the next one would queue behind a call nobody is
88
+ waiting for. A deadline that reports failure while the wire stays busy is
89
+ worse than none, because the error reads as "safe to retry" when it is not.
90
+ The server bounds the call at `rdisp/max_wprun_time`.
91
+
92
+ ## [6.0.0] - 2026-08-24
93
+
94
+ The wire owns what is the wire's, and the factory and per-credential classes are
95
+ gone. See [Migration to 6.0](./docs/MIGRATION-6.0.md).
96
+
97
+ ### Added — tooling
98
+
99
+ - **CI.** Until this release the only workflow ran on a tag, and `Release` went
100
+ from build straight to `npm pack` — so nothing ran the gate before a merge,
101
+ and the artifact a tag published had been tested nowhere but on a laptop.
102
+ `ci.yml` runs `lint:check`, `build`, `jest --no-cache`, `check:docs` and
103
+ `check:pack` on push and pull request, across ubuntu and windows on Node 18
104
+ and 20. Windows is in the matrix because npm is `npm.cmd` there and Node will
105
+ not execute a `.cmd` without a shell. `release.yml` runs the same three checks
106
+ between build and pack, so a tag is not a route around them.
107
+
108
+ - **`check:pack`** — inspects `npm pack --dry-run --json` and refuses a tarball
109
+ carrying a test file, a `.tsbuildinfo`, a `.ts` source, an env file or an
110
+ npmrc. Both defects it was written for had already shipped once:
111
+ `dist/__tests__/` and 120KB of compiler bookkeeping, neither visible from
112
+ `files`, because it is the compiler that decides what `dist` holds.
113
+
114
+ - **`typecheck:scripts`** — `scripts/*.ts` were checked by nothing, which is how
115
+ three live verification scripts came to sit on a constructor signature two
116
+ releases old.
117
+
118
+ ### Changed — packaging
119
+
120
+ - 111 files / 182KB → 95 / 147KB. Test helpers and the compiler's incremental
121
+ state no longer ship.
122
+
123
+ ### Added
124
+
125
+ - **The transport axis is complete and public.** `IAdtTransport` now covers
126
+ everything true of a wire — carrying a request, addressing it, establishing
127
+ itself, and whatever session state it keeps — and both ends are objects:
128
+ `HttpTransport` and `RfcTransport`. `IAdtEstablishContext`, `IRfcConversation`
129
+ and `RfcConnectionParams` are exported, so a caller handed a seam can name it.
130
+
131
+ - **`rfcConversationFrom(config)`** — the front door to the RFC wire. Derives
132
+ `ashost` from the url and `sysnr` from the HTTP port (`80XX` → `XX`, with
133
+ `SAP_SYSNR` overriding), and loads the SAP NW RFC SDK only when a conversation
134
+ opens, so a machine without it fails at `connect()` rather than at
135
+ construction.
136
+
137
+ - **`RfcTransport` supplies a default `Accept`.** axios adds one over HTTP and
138
+ nobody had noticed; ADT refuses a request without it with
139
+ `400 ExceptionResourceBadRequest: Accept header missing`.
140
+
141
+ ### Changed
142
+
143
+ - **The on-prem connector works over RFC.** It did not, at all. Measured against
144
+ a real system, three blockers stood one behind the other, all of them HTTP
145
+ assumptions in the class every connector shares: the CSRF fetch handed
146
+ `SADT_REST_RFC_ENDPOINT` an absolute URL and dumped it with
147
+ `STRING_OFFSET_TOO_LARGE`; that endpoint returns no `x-csrf-token` however it
148
+ is asked, so the exchange could not succeed; and the session fingerprint was a
149
+ scan for a `SAP_SESSIONID` cookie, so a wire that issues none read as a
150
+ connection the server had opened no session for.
151
+
152
+ The base class did not merely check for cookies — it DEFINED a session as one.
153
+
154
+ - **`AbstractAbapConnection` keeps the lifecycle and nothing else** (1989 → ~1620
155
+ lines): the transition queue, teardown epochs, session generations, critical
156
+ sections, stale-request fencing, 401 classification, the identity policy, and
157
+ the promise that `disconnect()` settles. Cookies, the cookie jar, the CSRF
158
+ exchange, affinity headers, axios and addressing all moved to the wire that
159
+ has them. There is no `if (transport is rfc)` anywhere.
160
+
161
+ - **A credential refused surfaces** rather than being renewed behind the caller —
162
+ on `connect()` and on the request path alike. Renewal is the provider's, and it
163
+ happens on an expiry the provider can see, on every call that asks for a header.
164
+ See *Removed*, below: nothing here answers a 401 any more.
165
+
166
+ ### Changed — BREAKING
167
+
168
+ - **The base classes ask nothing.** `AbstractAbapConnection` and
169
+ `CredentialAbapConnection` contain no config-driven conditional, no optional-member
170
+ call, and no dispatch on a type or a shape. What a collaborator can do is stated by
171
+ its type, not discovered at runtime:
172
+
173
+ - `IAdtTransport.open()` / `close()` are required — a wire with nothing to open
174
+ writes an empty method, which is true of it;
175
+ - `IAuthProvider.prepare()`, `cookies()` and `transportMaterial()` are required for
176
+ the same reason (needs `@mcp-abap-adt/interfaces` 20.0.0);
177
+ - `establish()` is the transport's, and the connection delegates to it without
178
+ asking anything first. The credential contributes a header, cookies and TLS
179
+ material; earning a CSRF token is the wire's work, because the wire is what
180
+ holds the session the token is bound to (needs `@mcp-abap-adt/interfaces`
181
+ 21.0.0, where the two credential atoms leave the contract — nothing
182
+ implemented them);
183
+ - `skipSessionType` is gone: it described BASIS 7.40, and a deployment is a wire, so
184
+ it is `LegacyOnPremHttpTransport`;
185
+ - whether a session exists is `IAdtTransport.sessionEstablished()` — a verdict each
186
+ wire gives about itself, instead of the connection reading a fingerprint and a flag
187
+ and deciding for all of them at once.
188
+
189
+ A credential you wrote gains three usually-empty members; see
190
+ [the migration guide](./docs/MIGRATION-6.0.md#writing-your-own-credential).
191
+
192
+ ### Removed
193
+
194
+ - **The connection no longer answers a `401` for you.** It called `renew()` on the
195
+ credential, compared the header against the previous one, and rebuilt the session
196
+ if it had changed — a credential lifetime managed from inside the connection.
197
+ Renewal on an expiry the provider can SEE still happens, inside
198
+ `authorizationHeader()`, which is asked per request; the other case — a token the
199
+ provider still believes in and the server refuses — is a judgement made with what
200
+ the caller knows, so the refusal surfaces. A refused credential is not a lost
201
+ session, so the connection stays usable. `TokenAuthProvider` declares
202
+ `IRenewableCredential` (interfaces 19.0.0), which is what a consumer narrows to
203
+ before calling `renew()` itself.
204
+
205
+ - **`disconnect({ deadlineMs })`** takes no arguments, and `SAP_RELEASE_DEADLINE_MS`
206
+ is gone with it. The parameter bounded a wait for the goodbye to be *answered*,
207
+ and the method does not act on that answer: it tells the server the session is
208
+ finished, and whether and when the session is freed is the server's affair. The
209
+ default was already `0`. Waiting bought a caller nothing while being the one
210
+ thing that could make a teardown unbounded — the goodbye carries no request
211
+ timeout by design, so a server that never answered would have held the teardown
212
+ for the whole deadline. Needs `@mcp-abap-adt/interfaces` 18.0.0, where the
213
+ parameter leaves the contract. Verified against a live BTP trial before the
214
+ contract moved: `disconnect()` returned in 1 ms and the goodbye still went out.
215
+
216
+ - **`SessionStrategy`** and its two implementations. A session mechanism only some
217
+ wires have, described from inside the class every wire shares and driven by the
218
+ connection — a second wire abstraction beside `IAdtTransport`. It is the
219
+ transport's `open()`/`close()` now, which is also what made `connect()` possible
220
+ over RFC at all.
221
+
222
+ - **`createAbapConnection()`** and the connection classes it built:
223
+ `BaseAbapConnection` (`OnPremAbapConnection`), `JwtAbapConnection`
224
+ (`CloudAbapConnection`), `SamlAbapConnection`, `CertificateAbapConnection`,
225
+ `KerberosAbapConnection`, `RfcAbapConnection` — 1597 lines. Take a connector,
226
+ hand it a credential, and hand it a transport — which has no default, because
227
+ which wire you are on is not something to guess.
228
+
229
+ - `adaptTransport()`, which dressed a transport in an axios shape so six call
230
+ sites did not have to be rewritten. They were rewritten.
231
+
232
+ - `connectionType: 'rfc'` as a way to reach the RFC wire. The wire is an
233
+ argument now.
234
+
235
+ **Kerberos has no direct replacement.** It was single-leg only and untested
236
+ against a live KDC (#35); a `KerberosAuthProvider` belongs on the credential
237
+ axis and should be added with a system to test it against.
238
+
239
+ ### Fixed
240
+
241
+ - Credential cookies are merged into the establishing request rather than
242
+ overwritten by the wire's own — a SAML session IS that cookie, and replacing
243
+ it sent the exchange out unauthenticated.
244
+ - The CSRF fallback endpoint is tried only when the primary answers 404. A host
245
+ that is not answering will not answer a different path, and asking doubled the
246
+ wait before the real error surfaced.
247
+ - A CSRF token arriving on a refused response (405, or any refusal carrying the
248
+ header) is kept instead of thrown away by the retry.
249
+
10
250
  ## [5.0.0] - 2026-08-21
11
251
 
12
252
  A connection now says which system it is, closes what it opens, and is handed its
@@ -1120,7 +1360,9 @@ const connection = createAbapConnection(config, logger);
1120
1360
  - JWT token refresh now properly handles connection errors (401/403 during initial connect)
1121
1361
  - Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
1122
1362
  - Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
1123
- [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v5.0.0...HEAD
1363
+ [Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.1...HEAD
1364
+ [6.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.0...v6.0.1
1365
+ [6.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v5.0.0...v6.0.0
1124
1366
  [5.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v4.0.0...v5.0.0
1125
1367
  [4.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v3.0.0...v4.0.0
1126
1368
  [3.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v2.0.0...v3.0.0
package/README.md CHANGED
@@ -57,13 +57,23 @@ The package uses a clean separation of concerns:
57
57
  - **Auth providers** (`BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`,
58
58
  `CertificateAuthProvider`):
59
59
  - What a connection authenticates with, passed in
60
- - A token provider renews on its own; the connector asks it per request and, on a
61
- `401`, tells it the answer was refused before asking again
62
-
63
- - **`BaseAbapConnection`, `JwtAbapConnection`, `SamlAbapConnection`,
64
- `CertificateAbapConnection`, `KerberosAbapConnection`** (deprecated, still exported):
65
- - The previous shape, where the class stated your credential and the session
66
- mechanism came with it. See [Migration to 5.0](./docs/MIGRATION-5.0.md)
60
+ - A token provider renews on its own, and the connector asks it per request — which
61
+ is how a token that expired between two requests is replaced with nobody
62
+ deciding to replace it
63
+ - A `401` **surfaces**. Whether a refusal meant "the token is stale" or "these
64
+ credentials are refused" is a judgement made with what you know, so the
65
+ connector does not answer it for you. A credential that can be told to get a
66
+ new one says so through `IRenewableCredential`, which you narrow to
67
+
68
+ - **Transports** (`OnPremHttpTransport`, `CloudHttpTransport`, `RfcTransport`):
69
+ - What a request travels over, and everything that is true of that wire.
70
+ `HttpTransport` keeps the cookie jar, the CSRF token and the affinity
71
+ headers; `RfcTransport` translates into `SADT_REST_RFC_ENDPOINT` and keeps a
72
+ conversation that IS the session
73
+ - On-prem is where this is a real choice; ABAP Cloud has one wire and its
74
+ connector takes no such parameter
75
+ - `rfcConversationFrom(config)` builds what `RfcTransport` needs, deriving
76
+ `ashost` and `sysnr` and loading the SDK only when a conversation opens
67
77
 
68
78
  - **`GenericWebSocketTransport`** (concrete, exported):
69
79
  - Transport abstraction for realtime WS message flows
@@ -123,6 +133,7 @@ This package interacts with external packages **ONLY through interfaces**:
123
133
 
124
134
  - 📦 **[Installation Guide](./docs/INSTALLATION.md)** - Setup and installation instructions
125
135
  - 📚 **[Usage Guide](./docs/USAGE.md)** - Detailed usage examples and API documentation
136
+ - 🚚 **[Migration to 6.0.0](./docs/MIGRATION-6.0.md)** - the factory and the per-credential classes are removed; RFC is a transport, not a class
126
137
  - 🚚 **[Migration to 4.0.0](./docs/MIGRATION-4.0.md)** - a 401 refreshes the token, a 403 reaches you with the server's message; the synthesised "JWT token has expired" is gone
127
138
  - 🚚 **[Migration: the explicit session lifecycle](./docs/MIGRATION-2.0.md)** - `connect()` is now required; start here if you are coming from 1.x
128
139
  - 💡 **[Examples](./examples/)** - Working code examples
@@ -149,7 +160,13 @@ For detailed installation instructions, see [Installation Guide](./docs/INSTALLA
149
160
  ### Basic Usage (On-Premise)
150
161
 
151
162
  ```typescript
152
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
163
+ import {
164
+ AdtOnPremConnector,
165
+ BasicAuthProvider,
166
+ OnPremHttpTransport,
167
+ SapConfig,
168
+ getTimeout,
169
+ } from "@mcp-abap-adt/connection";
153
170
 
154
171
  const config: SapConfig = {
155
172
  url: "https://your-sap-system.com",
@@ -167,23 +184,37 @@ const logger = {
167
184
  debug: (msg: string, meta?: any) => console.debug(msg, meta),
168
185
  };
169
186
 
170
- // Create connection
171
- const connection = createAbapConnection(config, logger, undefined, undefined, {
172
- system: "onprem", // which SYSTEM this is — said, never detected
173
- });
187
+ // Which system you are dialling is the class you take; which credential it
188
+ // authenticates with is the object you hand it. Neither is detected.
189
+ const connection = new AdtOnPremConnector(
190
+ config,
191
+ new BasicAuthProvider(config.username!, config.password!),
192
+ new OnPremHttpTransport(() => ({}), logger, {
193
+ client: config.client,
194
+ baseUrl: config.url,
195
+ }),
196
+ logger,
197
+ );
174
198
  await connection.connect(); // required before any request
175
199
 
176
200
  // Make ADT request
177
201
  const response = await connection.makeAdtRequest({
178
202
  method: "GET",
179
203
  url: "/sap/bc/adt/programs/programs/your-program",
204
+ timeout: getTimeout("default"),
180
205
  });
181
206
  ```
182
207
 
183
208
  ### Cloud Usage (JWT/OAuth2)
184
209
 
185
210
  ```typescript
186
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
211
+ import {
212
+ AdtCloudConnector,
213
+ CloudHttpTransport,
214
+ SapConfig,
215
+ TokenAuthProvider,
216
+ getTimeout,
217
+ } from "@mcp-abap-adt/connection";
187
218
 
188
219
  // JWT configuration
189
220
  const config: SapConfig = {
@@ -200,23 +231,77 @@ const logger = {
200
231
  debug: (msg: string, meta?: any) => console.debug(msg, meta),
201
232
  };
202
233
 
203
- // Logger is optional - if not provided, no logging output
204
- const connection = createAbapConnection(config, logger, undefined, undefined, {
205
- system: "cloud", // which SYSTEM this is — said, never detected
206
- });
234
+ // Logger is optional - if not provided, no logging output.
235
+ // A bare string is a token with nothing behind it. Hand `TokenAuthProvider` an
236
+ // `ITokenRefresher` instead and it checks expiry and renews on its own, which
237
+ // is what you want in anything long-lived.
238
+ const connection = new AdtCloudConnector(
239
+ config,
240
+ new TokenAuthProvider(config.jwtToken!),
241
+ new CloudHttpTransport(() => ({}), logger, {
242
+ client: config.client,
243
+ baseUrl: config.url,
244
+ }),
245
+ logger,
246
+ );
207
247
  await connection.connect();
208
248
 
209
- // Note: Token refresh is handled by @mcp-abap-adt/auth-broker package
249
+ // Note: obtaining and refreshing tokens is @mcp-abap-adt/auth-broker's job
210
250
  const response = await connection.makeAdtRequest({
211
251
  method: "GET",
212
252
  url: "/sap/bc/adt/programs/programs/your-program",
253
+ timeout: getTimeout("default"),
213
254
  });
214
255
  ```
215
256
 
257
+ ### On-Premise over RFC
258
+
259
+ The same ADT calls, over `SADT_REST_RFC_ENDPOINT` — the function module Eclipse
260
+ ADT itself uses through JCo — instead of over HTTP. Worth taking on a system
261
+ where stateful HTTP sessions are not usable: an RFC conversation is one ABAP
262
+ session for its whole lifetime, which is the way past `423 invalid lock handle`
263
+ on BASIS < 7.50.
264
+
265
+ Needs the SAP NW RFC SDK on the machine and `npm install @mcp-abap-adt/sap-rfc-lite`.
266
+
267
+ ```typescript
268
+ import {
269
+ AdtOnPremConnector,
270
+ BasicAuthProvider,
271
+ RfcTransport,
272
+ rfcConversationFrom,
273
+ } from "@mcp-abap-adt/connection";
274
+
275
+ const connection = new AdtOnPremConnector(
276
+ config,
277
+ new BasicAuthProvider(config.username!, config.password!),
278
+ new RfcTransport(rfcConversationFrom(config), logger),
279
+ logger,
280
+ );
281
+
282
+ await connection.connect();
283
+ // Everything above the wire is the same: makeAdtRequest, setSessionType,
284
+ // disconnect. What differs is where the session lives — see below.
285
+ ```
286
+
287
+ **Where to look for it.** An HTTP session is an ICF session and appears in
288
+ **SM05**. An RFC conversation is a gateway client: it appears in **SMGW → Logged
289
+ on Clients** as `NWRFC`, and never in SM05, because there is no ICM in that
290
+ path. Looking for one in the other monitor and finding nothing is not a fault.
291
+
292
+ There is no cloud equivalent: ABAP Cloud has one wire, and `AdtCloudConnector`
293
+ takes no transport parameter at all.
294
+
216
295
  ### SSO Usage (SAML Session Cookies)
217
296
 
218
297
  ```typescript
219
- import { createAbapConnection, SapConfig } from "@mcp-abap-adt/connection";
298
+ import {
299
+ AdtOnPremConnector,
300
+ OnPremHttpTransport,
301
+ SamlAuthProvider,
302
+ SapConfig,
303
+ getTimeout,
304
+ } from "@mcp-abap-adt/connection";
220
305
 
221
306
  const config: SapConfig = {
222
307
  url: "https://your-sap-system.com",
@@ -224,34 +309,49 @@ const config: SapConfig = {
224
309
  sessionCookies: "MYSAPSSO2=...; SAP_SESSIONID=...",
225
310
  };
226
311
 
227
- const connection = createAbapConnection(config, logger, undefined, undefined, {
228
- system: "onprem", // which SYSTEM this is — said, never detected
229
- });
312
+ // The cookies ARE the credential here — there is no Authorization header at all.
313
+ const connection = new AdtOnPremConnector(
314
+ config,
315
+ new SamlAuthProvider(config.sessionCookies!),
316
+ new OnPremHttpTransport(() => ({}), logger, {
317
+ client: config.client,
318
+ baseUrl: config.url,
319
+ }),
320
+ logger,
321
+ );
230
322
  await connection.connect();
231
323
 
232
324
  const response = await connection.makeAdtRequest({
233
325
  method: "GET",
234
326
  url: "/sap/bc/adt/programs/programs/your-program",
327
+ timeout: getTimeout("default"),
235
328
  });
236
329
  ```
237
330
 
238
331
  ### Cloud Usage with Automatic Token Refresh
239
332
 
240
- For automatic token refresh on **401** errors, inject `ITokenRefresher`:
333
+ Give `TokenAuthProvider` an `ITokenRefresher` and the provider replaces an
334
+ **expired** token on its own — it is asked per request and checks expiry before
335
+ answering, so nobody decides to renew. A token the source still believes in and
336
+ the server refuses is the other half, and that one **surfaces**:
241
337
 
242
338
  ```typescript
243
339
  import {
244
340
  AdtCloudConnector,
245
- TokenAuthProvider,
341
+ CloudHttpTransport,
246
342
  SapConfig,
343
+ TokenAuthProvider,
344
+ getTimeout,
247
345
  } from "@mcp-abap-adt/connection";
248
346
  import type { ITokenRefresher } from "@mcp-abap-adt/interfaces";
249
347
 
250
348
  // Token refresher provides token acquisition and refresh
251
349
  // (created by @mcp-abap-adt/auth-broker or custom implementation)
350
+ const currentAccessToken = 'the access token you already hold';
351
+ const exchangeRefreshToken = async () => 'a freshly exchanged access token';
252
352
  const tokenRefresher: ITokenRefresher = {
253
- getToken: async () => { /* return current token */ },
254
- refreshToken: async () => { /* refresh and return new token */ },
353
+ getToken: async () => currentAccessToken, // the one you hold
354
+ refreshToken: async () => exchangeRefreshToken(), // a new one, and cache it
255
355
  };
256
356
 
257
357
  const config: SapConfig = {
@@ -264,19 +364,24 @@ const config: SapConfig = {
264
364
  const connection = new AdtCloudConnector(
265
365
  config,
266
366
  new TokenAuthProvider(tokenRefresher),
367
+ new CloudHttpTransport(() => ({}), logger, {
368
+ client: config.client,
369
+ baseUrl: config.url,
370
+ }),
267
371
  logger,
268
372
  );
269
373
  await connection.connect();
270
374
 
271
- // On a 401 the connector tells the provider its token was refused, asks again,
272
- // and only if the answer changed rebuilds the session and retries once. An
273
- // unchanged answer means the server refused these credentials, and the 401
375
+ // On a 401 nothing here decides to get a new credential: the refusal reaches
376
+ // you. Whether it meant "stale" is a judgement made with what you know, and
377
+ // `renew()` is the seam you make it with. The session is untouched — a refused
274
378
  // reaches you. A refresh replaces the SAP session, so if a lock window is open
275
379
  // the request fails with ADT_SESSION_REPLACED rather than continuing on a
276
380
  // session your lock is not in.
277
381
  const response = await connection.makeAdtRequest({
278
382
  method: "GET",
279
383
  url: "/sap/bc/adt/programs/programs/your-program",
384
+ timeout: getTimeout("default"),
280
385
  });
281
386
  ```
282
387
 
@@ -297,11 +402,22 @@ See [MIGRATION-4.0.md](./docs/MIGRATION-4.0.md).
297
402
  For operations that require session state (e.g., object modifications), you can enable stateful sessions:
298
403
 
299
404
  ```typescript
300
- import { createAbapConnection } from "@mcp-abap-adt/connection";
405
+ import {
406
+ AdtOnPremConnector,
407
+ BasicAuthProvider,
408
+ OnPremHttpTransport,
409
+ getTimeout,
410
+ } from "@mcp-abap-adt/connection";
301
411
 
302
- const connection = createAbapConnection(config, logger, undefined, undefined, {
303
- system: "onprem", // which SYSTEM this is — said, never detected
304
- });
412
+ const connection = new AdtOnPremConnector(
413
+ config,
414
+ new BasicAuthProvider(config.username!, config.password!),
415
+ new OnPremHttpTransport(() => ({}), logger, {
416
+ client: config.client,
417
+ baseUrl: config.url,
418
+ }),
419
+ logger,
420
+ );
305
421
  await connection.connect();
306
422
 
307
423
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
@@ -312,6 +428,7 @@ await connection.makeAdtRequest({
312
428
  method: "POST",
313
429
  url: "/sap/bc/adt/objects/domains",
314
430
  data: { /* domain data */ },
431
+ timeout: getTimeout("default"),
315
432
  });
316
433
 
317
434
  // Note: Session state persistence is handled by @mcp-abap-adt/auth-broker package
@@ -320,7 +437,7 @@ await connection.makeAdtRequest({
320
437
  ### Custom Logger
321
438
 
322
439
  ```typescript
323
- import { ILogger } from "@mcp-abap-adt/connection";
440
+ import { AdtOnPremConnector, BasicAuthProvider, ILogger, OnPremHttpTransport } from "@mcp-abap-adt/connection";
324
441
 
325
442
  class MyLogger implements ILogger {
326
443
  info(message: string, meta?: any): void {
@@ -349,9 +466,15 @@ class MyLogger implements ILogger {
349
466
  }
350
467
 
351
468
  const logger = new MyLogger();
352
- const connection = createAbapConnection(config, logger, undefined, undefined, {
353
- system: "onprem", // which SYSTEM this is — said, never detected
354
- });
469
+ const connection = new AdtOnPremConnector(
470
+ config,
471
+ new BasicAuthProvider(config.username!, config.password!),
472
+ new OnPremHttpTransport(() => ({}), logger, {
473
+ client: config.client,
474
+ baseUrl: config.url,
475
+ }),
476
+ logger,
477
+ );
355
478
  ```
356
479
 
357
480
  ## CLI Tool
@@ -441,6 +564,8 @@ type SapConfig = {
441
564
  Main interface for ABAP connections.
442
565
 
443
566
  ```typescript
567
+ import { AbapRequestOptions } from '@mcp-abap-adt/connection';
568
+ import type { AxiosResponse } from 'axios';
444
569
  // The shared contract (IAbapConnection), what every connection provides:
445
570
  interface AbapConnection {
446
571
  connect(): Promise<void>; // REQUIRED before any request; rejects on failure
@@ -451,10 +576,12 @@ interface AbapConnection {
451
576
  }
452
577
  ```
453
578
 
454
- The HTTP connection classes carry the rest of the session lifecycle. It is on the
455
- shared contract as a **capability atom** in `@mcp-abap-adt/interfaces` rather than
456
- as methods on `IAbapConnection`, which is why `RfcAbapConnection` — a transport
457
- that owns no HTTP session — is unaffected by its existence:
579
+ The connectors carry the rest of the session lifecycle. It is on the shared
580
+ contract as a **capability atom** in `@mcp-abap-adt/interfaces` rather than as
581
+ methods on `IAbapConnection`, so a consumer that only carries requests is
582
+ unaffected by its existence. Note that a connection over RFC has the whole of it
583
+ — what an RFC conversation has none of is a session RESOURCE to open and close
584
+ by address, which is an empty mechanism, not an absent lifecycle:
458
585
 
459
586
  ```typescript
460
587
  // ISessionLifecycleAware
@@ -496,16 +623,35 @@ interface ILogger {
496
623
 
497
624
  ### Functions
498
625
 
499
- #### `createAbapConnection(config, logger?, sessionId?)`
626
+ #### `rfcConversationFrom(config)`
500
627
 
501
- Factory function to create an ABAP connection instance.
628
+ What `RfcTransport` is constructed with. Derives `ashost` from the url and
629
+ `sysnr` from the HTTP port by the SAP convention that `80XX` is the ICM port for
630
+ system `XX`, which `SAP_SYSNR` overrides for a port that follows no convention.
631
+
632
+ The SAP NW RFC SDK is loaded when a conversation opens, not when this is called,
633
+ so a machine without it fails at `connect()` with a message saying what to
634
+ install rather than at construction.
635
+
636
+ ```text
637
+ function rfcConversationFrom(config: SapConfig): () => IRfcConversation;
638
+ function rfcParamsFrom(config: SapConfig): RfcConnectionParams;
639
+ ```
502
640
 
503
641
  ```typescript
504
- function createAbapConnection(
505
- config: SapConfig,
506
- logger?: ILogger | null,
507
- sessionId?: string
508
- ): AbapConnection;
642
+ import {
643
+ AdtOnPremConnector,
644
+ BasicAuthProvider,
645
+ RfcTransport,
646
+ rfcConversationFrom,
647
+ } from "@mcp-abap-adt/connection";
648
+
649
+ const connection = new AdtOnPremConnector(
650
+ config,
651
+ new BasicAuthProvider(config.username!, config.password!),
652
+ new RfcTransport(rfcConversationFrom(config), logger),
653
+ logger,
654
+ );
509
655
  ```
510
656
 
511
657
  #### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES`
@@ -532,6 +678,12 @@ import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
532
678
  ```typescript
533
679
  import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
534
680
 
681
+ // Whatever HTTP client your own connection class is built on.
682
+ const yourHttpClient = {
683
+ get: async (url: string, config: { headers: Record<string, string> }) =>
684
+ ({ headers: {} as Record<string, string> }),
685
+ };
686
+
535
687
  async function fetchCsrfToken(baseUrl: string): Promise<string> {
536
688
  const csrfUrl = `${baseUrl}${CSRF_CONFIG.ENDPOINT}`;
537
689
 
@@ -563,6 +715,10 @@ async function fetchCsrfToken(baseUrl: string): Promise<string> {
563
715
  await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
564
716
  }
565
717
  }
718
+
719
+ // Unreachable: the last attempt either returns or throws above. Stated so the
720
+ // function has a return type the compiler can agree with.
721
+ throw new Error(CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
566
722
  }
567
723
  ```
568
724