@mcp-abap-adt/connection 5.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 +134 -1
- package/README.md +204 -48
- 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 +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 +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 -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/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/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,9 +163,15 @@ const logger = {
|
|
|
163
163
|
debug: (msg) => console.log('[DEBUG]', msg),
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
const connection =
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
);
|
|
169
175
|
|
|
170
176
|
connection.connect()
|
|
171
177
|
.then(() =>
|
|
@@ -216,7 +222,13 @@ npm install --save-dev typescript @types/node
|
|
|
216
222
|
### TypeScript Example
|
|
217
223
|
|
|
218
224
|
```typescript
|
|
219
|
-
import {
|
|
225
|
+
import {
|
|
226
|
+
AdtOnPremConnector,
|
|
227
|
+
BasicAuthProvider,
|
|
228
|
+
ILogger,
|
|
229
|
+
OnPremHttpTransport,
|
|
230
|
+
SapConfig,
|
|
231
|
+
} from '@mcp-abap-adt/connection';
|
|
220
232
|
|
|
221
233
|
const config: SapConfig = {
|
|
222
234
|
url: 'https://your-sap-server.com',
|
|
@@ -233,9 +245,15 @@ const logger: ILogger = {
|
|
|
233
245
|
debug: (msg: string) => console.log(msg),
|
|
234
246
|
};
|
|
235
247
|
|
|
236
|
-
const connection =
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
);
|
|
239
257
|
```
|
|
240
258
|
|
|
241
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
|
|
@@ -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
|
|
|
@@ -26,19 +26,47 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
|
|
|
26
26
|
|
|
27
27
|
## Enabling Stateful Sessions
|
|
28
28
|
|
|
29
|
+
Every example below builds on this much, so it is stated once:
|
|
30
|
+
|
|
29
31
|
```ts
|
|
30
|
-
import {
|
|
32
|
+
import type { SapConfig } from '@mcp-abap-adt/connection';
|
|
33
|
+
|
|
34
|
+
const config: SapConfig = {
|
|
35
|
+
url: 'https://your-sap-server.com',
|
|
36
|
+
authType: 'basic',
|
|
37
|
+
username: 'your-username',
|
|
38
|
+
password: 'your-password',
|
|
39
|
+
client: '100',
|
|
40
|
+
};
|
|
41
|
+
const user = config.username!;
|
|
42
|
+
const pass = config.password!;
|
|
43
|
+
const logger = console;
|
|
44
|
+
```
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
AdtOnPremConnector,
|
|
49
|
+
BasicAuthProvider,
|
|
50
|
+
OnPremHttpTransport,
|
|
51
|
+
getTimeout,
|
|
52
|
+
} from '@mcp-abap-adt/connection';
|
|
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
|
+
);
|
|
35
63
|
await connection.connect(); // required before any request
|
|
36
64
|
|
|
37
65
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
38
66
|
connection.setSessionType('stateful');
|
|
39
67
|
|
|
40
68
|
// Now all requests share the same session (cookies, CSRF token)
|
|
41
|
-
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' });
|
|
69
|
+
await connection.makeAdtRequest({ method: 'GET', url: '/sap/bc/adt/discovery' , timeout: getTimeout('default') });
|
|
42
70
|
|
|
43
71
|
// Switch back to stateless
|
|
44
72
|
connection.setSessionType('stateless');
|
|
@@ -49,11 +77,11 @@ connection.setSessionType('stateless');
|
|
|
49
77
|
## Knowing Which Session You Are In
|
|
50
78
|
|
|
51
79
|
```ts
|
|
52
|
-
import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
|
|
80
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
53
81
|
|
|
54
82
|
// getSessionIdentity() is on the HTTP connection classes, NOT on the
|
|
55
|
-
// IAbapConnection type
|
|
56
|
-
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
|
|
83
|
+
// bare IAbapConnection type a caller may hand you.
|
|
84
|
+
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), new OnPremHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }), logger);
|
|
57
85
|
await connection.connect();
|
|
58
86
|
|
|
59
87
|
// Which SAP session this connection is talking to. Changes only when the
|
|
@@ -84,11 +112,13 @@ Every ADT request issued through `makeAdtRequest` automatically:
|
|
|
84
112
|
|
|
85
113
|
This logic is transparent to callers (Builders, handlers, CLI scripts).
|
|
86
114
|
|
|
87
|
-
On a JWT connection
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
On a JWT connection a 401 is **not** handled here at all, and that is the point: since 6.0.0 the
|
|
116
|
+
refusal surfaces and the session is left alone. Nothing replaces the credential behind you, so
|
|
117
|
+
nothing replaces the SAP session behind you either — a lock window is not torn down by an
|
|
118
|
+
authentication answer. A 403 never did this: it is an authorization answer, not a credential one.
|
|
119
|
+
|
|
120
|
+
If you decide the refusal meant a stale token, `renew()` and reconnect are yours to call — and a
|
|
121
|
+
reconnect is a NEW session, so do it outside a lock window rather than inside one.
|
|
92
122
|
|
|
93
123
|
---
|
|
94
124
|
|
|
@@ -123,6 +153,44 @@ made with the cookies and the answers in hand.
|
|
|
123
153
|
|
|
124
154
|
## A Lock Lives In The ABAP Session
|
|
125
155
|
|
|
156
|
+
### Three lifetimes, and a dropped connection ends none of them
|
|
157
|
+
|
|
158
|
+
Before anything else, because conflating these is the mistake that produces
|
|
159
|
+
wrong fixes:
|
|
160
|
+
|
|
161
|
+
| | what it is | what ends it |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| **the connection** | one TCP socket | you, the network, a timeout firing. **Ends nothing on the server** |
|
|
164
|
+
| **the HTTP session** | the ICF conversation, held by the cookie jar on this side | dropping the cookies, or a logoff. Nothing about it is in doubt |
|
|
165
|
+
| **the ABAP session** | the server-side context named by `SAP_SESSIONID_<SID>_<CLIENT>` — the roll area a stateful request runs in | **only the server.** Its own idle timeout, which this side can neither read nor influence, or an explicit logoff |
|
|
166
|
+
| **the lock** | an enqueue entry, **owned by the ABAP session** | that ABAP session ending. **When it goes, its locks go with it** |
|
|
167
|
+
|
|
168
|
+
The two middle rows are the ones worth keeping apart, because everything that
|
|
169
|
+
goes wrong here goes wrong between them. The lock does not belong to the socket
|
|
170
|
+
and does not belong to the cookies — it belongs to the **ABAP session**, and
|
|
171
|
+
that session is the server's. This side never ends one: it can ask (a logoff),
|
|
172
|
+
and otherwise waits for a timeout it cannot see.
|
|
173
|
+
|
|
174
|
+
A closed socket is just a client that stopped listening. The ABAP session
|
|
175
|
+
neither knows nor cares — which is why `SM04` shows sessions left behind by
|
|
176
|
+
connections that never said they were finished, and why this package sends an
|
|
177
|
+
explicit goodbye at all.
|
|
178
|
+
|
|
179
|
+
And **a lock is never stranded by something dying** — the opposite. If the ABAP
|
|
180
|
+
session ended, the enqueue entry went with it and there is nothing left to
|
|
181
|
+
strand. A lock is stranded when that session **survives** while the caller no
|
|
182
|
+
longer holds the handle needed to unlock it: both sit there, held, until the
|
|
183
|
+
server's own timeout releases them together.
|
|
184
|
+
|
|
185
|
+
This is what makes an aborted request expensive, and it is not what it looks
|
|
186
|
+
like. Aborting costs you **knowledge**, not a session: whether the modification
|
|
187
|
+
was applied becomes unknowable, and the handle `unlock` needs is gone, while the
|
|
188
|
+
lock is still very much held. That — not any teardown — is why
|
|
189
|
+
`beginCriticalSection()` raises the effective timeout to a large ceiling for the
|
|
190
|
+
duration of a `lock → modify → unlock` chain.
|
|
191
|
+
|
|
192
|
+
### Two sessions, and they are not the same thing
|
|
193
|
+
|
|
126
194
|
There are two sessions here, and they are not the same thing:
|
|
127
195
|
|
|
128
196
|
- the **HTTP session** — the conversation this client is having. It exists as
|
|
@@ -173,17 +241,18 @@ you get by asking — `getSessionIdentity()` for which session you are in, and
|
|
|
173
241
|
`disconnect()` followed by `connect()` — there is no local-only discard, because
|
|
174
242
|
dropping the cookie leaves the ABAP session open on the server. Both live on
|
|
175
243
|
the HTTP connection classes and are **not** on the `IAbapConnection` type
|
|
176
|
-
`
|
|
244
|
+
a bare `IAbapConnection` carries — reach them through a connector type, or
|
|
245
|
+
through the `ISessionLifecycleAware` atom:
|
|
177
246
|
|
|
178
247
|
```ts
|
|
179
|
-
|
|
248
|
+
import { AdtOnPremConnector, BasicAuthProvider, OnPremHttpTransport } from '@mcp-abap-adt/connection';
|
|
180
249
|
|
|
181
|
-
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
|
|
250
|
+
const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), new OnPremHttpTransport(() => ({}), logger, { client: config.client, baseUrl: config.url }), logger);
|
|
182
251
|
await connection.disconnect(); // ends the session on the server, then clears
|
|
183
252
|
await connection.connect(); // a new session, explicitly
|
|
184
253
|
```
|
|
185
254
|
- **Session expired**: reauthenticate to obtain a new session.
|
|
186
|
-
- **Multiple connections**: each
|
|
255
|
+
- **Multiple connections**: each connector holds its own wire, and the wire holds the cookie jar; share the instance if you need continuity.
|
|
187
256
|
|
|
188
257
|
---
|
|
189
258
|
|