@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.
- package/CHANGELOG.md +243 -1
- package/README.md +204 -48
- package/bin/sap-abap-auth.js +101 -13
- package/dist/auth/providers.d.ts +38 -9
- package/dist/auth/providers.d.ts.map +1 -1
- package/dist/auth/providers.js +46 -3
- package/dist/connection/AbstractAbapConnection.d.ts +83 -136
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +241 -763
- package/dist/connection/AdtCloudConnector.d.ts +12 -7
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
- package/dist/connection/AdtCloudConnector.js +2 -6
- package/dist/connection/AdtOnPremConnector.d.ts +20 -7
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
- package/dist/connection/AdtOnPremConnector.js +2 -6
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
- package/dist/connection/CredentialAbapConnection.d.ts +8 -41
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
- package/dist/connection/CredentialAbapConnection.js +44 -111
- package/dist/connection/HttpTransport.d.ts +178 -0
- package/dist/connection/HttpTransport.d.ts.map +1 -0
- package/dist/connection/HttpTransport.js +402 -0
- package/dist/connection/IAdtTransport.d.ts +232 -0
- package/dist/connection/IAdtTransport.d.ts.map +1 -0
- package/dist/connection/IAdtTransport.js +28 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
- package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
- package/dist/connection/OnPremHttpTransport.d.ts +45 -0
- package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
- package/dist/connection/OnPremHttpTransport.js +91 -0
- package/dist/connection/RfcTransport.d.ts +89 -0
- package/dist/connection/RfcTransport.d.ts.map +1 -0
- package/dist/connection/RfcTransport.js +270 -0
- package/dist/connection/rfcConversation.d.ts +44 -0
- package/dist/connection/rfcConversation.d.ts.map +1 -0
- package/dist/connection/rfcConversation.js +71 -0
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -18
- package/dist/utils/timeouts.d.ts +6 -19
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -22
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -10
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
- package/docs/USAGE.md +260 -119
- package/examples/basic-connection.js +15 -3
- package/examples/jwt-with-token-refresh.js +15 -7
- package/examples/saml-connection.js +15 -2
- package/package.json +12 -11
- package/dist/__tests__/helpers/session.d.ts +0 -15
- package/dist/__tests__/helpers/session.d.ts.map +0 -1
- package/dist/__tests__/helpers/session.js +0 -19
- package/dist/auth/IAuthProvider.d.ts +0 -84
- package/dist/auth/IAuthProvider.d.ts.map +0 -1
- package/dist/auth/IAuthProvider.js +0 -21
- package/dist/connection/BaseAbapConnection.d.ts +0 -29
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -81
- package/dist/connection/CertificateAbapConnection.d.ts +0 -35
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -91
- package/dist/connection/JwtAbapConnection.d.ts +0 -131
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -376
- package/dist/connection/KerberosAbapConnection.d.ts +0 -32
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -128
- package/dist/connection/RfcAbapConnection.d.ts +0 -44
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -324
- package/dist/connection/SamlAbapConnection.d.ts +0 -31
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -81
- package/dist/connection/connectionFactory.d.ts +0 -25
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- package/dist/connection/connectionFactory.js +0 -84
- package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
- package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
- package/dist/session/IcfSessionStrategy.d.ts +0 -27
- package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
- package/dist/session/IcfSessionStrategy.js +0 -62
- package/dist/session/SessionStrategy.d.ts +0 -86
- package/dist/session/SessionStrategy.d.ts.map +0 -1
- package/dist/session/SessionStrategy.js +0 -33
- 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/
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
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
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
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 {
|
|
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
|
-
|
|
205
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 () =>
|
|
254
|
-
refreshToken: async () =>
|
|
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
|
|
272
|
-
//
|
|
273
|
-
//
|
|
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 {
|
|
405
|
+
import {
|
|
406
|
+
AdtOnPremConnector,
|
|
407
|
+
BasicAuthProvider,
|
|
408
|
+
OnPremHttpTransport,
|
|
409
|
+
getTimeout,
|
|
410
|
+
} from "@mcp-abap-adt/connection";
|
|
301
411
|
|
|
302
|
-
const connection =
|
|
303
|
-
|
|
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 =
|
|
353
|
-
|
|
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
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
that
|
|
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
|
-
#### `
|
|
626
|
+
#### `rfcConversationFrom(config)`
|
|
500
627
|
|
|
501
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
|