@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.
- package/CHANGELOG.md +407 -1
- package/README.md +239 -53
- package/dist/auth/providers.d.ts +124 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +183 -0
- package/dist/connection/AbstractAbapConnection.d.ts +209 -57
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +459 -457
- package/dist/connection/AdtCloudConnector.d.ts +34 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +27 -0
- package/dist/connection/AdtOnPremConnector.d.ts +43 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +28 -0
- package/dist/connection/CloudHttpTransport.d.ts +37 -0
- package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
- package/dist/connection/CloudHttpTransport.js +145 -0
- package/dist/connection/CredentialAbapConnection.d.ts +55 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +128 -0
- 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 +256 -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 +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -18
- package/dist/session/SessionLifecycle.d.ts +17 -2
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +17 -2
- package/dist/utils/cookies.d.ts +12 -0
- package/dist/utils/cookies.d.ts.map +1 -0
- package/dist/utils/cookies.js +24 -0
- package/dist/utils/timeouts.d.ts +6 -3
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +6 -3
- package/docs/INDEX.md +5 -2
- package/docs/INSTALLATION.md +28 -6
- package/docs/JWT_AUTH_TOOLS.md +20 -4
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/MIGRATION-6.0.md +359 -0
- package/docs/SCOPE.md +1 -1
- package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
- package/docs/USAGE.md +322 -111
- 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 -10
- 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/connection/BaseAbapConnection.d.ts +0 -23
- package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
- package/dist/connection/BaseAbapConnection.js +0 -75
- package/dist/connection/CertificateAbapConnection.d.ts +0 -25
- package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
- package/dist/connection/CertificateAbapConnection.js +0 -79
- package/dist/connection/JwtAbapConnection.d.ts +0 -115
- package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
- package/dist/connection/JwtAbapConnection.js +0 -358
- package/dist/connection/KerberosAbapConnection.d.ts +0 -24
- package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
- package/dist/connection/KerberosAbapConnection.js +0 -120
- package/dist/connection/RfcAbapConnection.d.ts +0 -49
- package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
- package/dist/connection/RfcAbapConnection.js +0 -331
- package/dist/connection/SamlAbapConnection.d.ts +0 -25
- package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
- package/dist/connection/SamlAbapConnection.js +0 -75
- package/dist/connection/connectionFactory.d.ts +0 -9
- package/dist/connection/connectionFactory.d.ts.map +0 -1
- 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
|
-
- **
|
|
76
|
-
- **
|
|
77
|
+
- **Connectors**: `AdtOnPremConnector`, `AdtCloudConnector`
|
|
78
|
+
- **Credentials**: `BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`, `CertificateAuthProvider`
|
|
79
|
+
- **Transports**: `HttpTransport`, `RfcTransport` (+ `rfcConversationFrom()`)
|
|
77
80
|
|
|
78
81
|
## Version Highlights
|
|
79
82
|
|
package/docs/INSTALLATION.md
CHANGED
|
@@ -83,7 +83,7 @@ In your code:
|
|
|
83
83
|
|
|
84
84
|
```typescript
|
|
85
85
|
import 'dotenv/config'; // or require('dotenv').config();
|
|
86
|
-
import {
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
package/docs/JWT_AUTH_TOOLS.md
CHANGED
|
@@ -123,13 +123,29 @@ source .env
|
|
|
123
123
|
Or configure `SapConfig` directly:
|
|
124
124
|
|
|
125
125
|
```ts
|
|
126
|
-
import {
|
|
127
|
-
|
|
128
|
-
|
|
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
|
package/docs/MIGRATION-2.0.md
CHANGED
|
@@ -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()`
|
|
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
|
|
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
|
|