@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/README.md
CHANGED
|
@@ -17,8 +17,8 @@ ABAP connection layer for MCP ABAP ADT server. Provides a unified interface for
|
|
|
17
17
|
- Session headers management (cookies, CSRF tokens)
|
|
18
18
|
- Session state persistence is handled by `@mcp-abap-adt/auth-broker` package
|
|
19
19
|
- 🏗️ **Clean Architecture**:
|
|
20
|
-
-
|
|
21
|
-
-
|
|
20
|
+
- One connector per SYSTEM (`AdtOnPremConnector`, `AdtCloudConnector`), handed an auth provider
|
|
21
|
+
- Authentication is a parameter, not a subclass — nothing about the system is inferred from it
|
|
22
22
|
- Proper separation of concerns - no JWT logic in base class
|
|
23
23
|
- 🔌 **Realtime Transport Scaffold**:
|
|
24
24
|
- Generic `GenericWebSocketTransport` with pluggable WS factory
|
|
@@ -39,21 +39,41 @@ The package uses a clean separation of concerns:
|
|
|
39
39
|
- CSRF token fetching with retry
|
|
40
40
|
- Auth-agnostic - knows nothing about Basic or JWT
|
|
41
41
|
|
|
42
|
-
- **`
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
- **`
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
- **`AdtOnPremConnector`** (concrete, exported):
|
|
43
|
+
- An on-prem system: the session arrives with the establishing call, and the platform's
|
|
44
|
+
ICF logoff is how it is given back
|
|
45
|
+
- Takes an auth provider — basic, SAML, certificate, a bearer token, whatever you hold
|
|
46
|
+
|
|
47
|
+
- **`AdtCloudConnector`** (concrete, exported):
|
|
48
|
+
- An ABAP Cloud system: a session is a resource, opened at
|
|
49
|
+
`/sap/bc/adt/core/http/sessions` and given back by `DELETE` on the address it publishes
|
|
50
|
+
- Takes an auth provider, same as above
|
|
51
|
+
|
|
52
|
+
**Which one you take is how you say where you are dialling.** Nothing is probed:
|
|
53
|
+
the session resource answers on on-prem too, and its `DELETE` there leaves the
|
|
54
|
+
session open while the logoff removes it — so asking the server would pick the
|
|
55
|
+
mechanism that releases nothing.
|
|
56
|
+
|
|
57
|
+
- **Auth providers** (`BasicAuthProvider`, `TokenAuthProvider`, `SamlAuthProvider`,
|
|
58
|
+
`CertificateAuthProvider`):
|
|
59
|
+
- What a connection authenticates with, passed in
|
|
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
|
|
57
77
|
|
|
58
78
|
- **`GenericWebSocketTransport`** (concrete, exported):
|
|
59
79
|
- Transport abstraction for realtime WS message flows
|
|
@@ -113,6 +133,7 @@ This package interacts with external packages **ONLY through interfaces**:
|
|
|
113
133
|
|
|
114
134
|
- 📦 **[Installation Guide](./docs/INSTALLATION.md)** - Setup and installation instructions
|
|
115
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
|
|
116
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
|
|
117
138
|
- 🚚 **[Migration: the explicit session lifecycle](./docs/MIGRATION-2.0.md)** - `connect()` is now required; start here if you are coming from 1.x
|
|
118
139
|
- 💡 **[Examples](./examples/)** - Working code examples
|
|
@@ -139,7 +160,13 @@ For detailed installation instructions, see [Installation Guide](./docs/INSTALLA
|
|
|
139
160
|
### Basic Usage (On-Premise)
|
|
140
161
|
|
|
141
162
|
```typescript
|
|
142
|
-
import {
|
|
163
|
+
import {
|
|
164
|
+
AdtOnPremConnector,
|
|
165
|
+
BasicAuthProvider,
|
|
166
|
+
OnPremHttpTransport,
|
|
167
|
+
SapConfig,
|
|
168
|
+
getTimeout,
|
|
169
|
+
} from "@mcp-abap-adt/connection";
|
|
143
170
|
|
|
144
171
|
const config: SapConfig = {
|
|
145
172
|
url: "https://your-sap-system.com",
|
|
@@ -157,21 +184,37 @@ const logger = {
|
|
|
157
184
|
debug: (msg: string, meta?: any) => console.debug(msg, meta),
|
|
158
185
|
};
|
|
159
186
|
|
|
160
|
-
//
|
|
161
|
-
|
|
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
|
+
);
|
|
162
198
|
await connection.connect(); // required before any request
|
|
163
199
|
|
|
164
200
|
// Make ADT request
|
|
165
201
|
const response = await connection.makeAdtRequest({
|
|
166
202
|
method: "GET",
|
|
167
203
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
204
|
+
timeout: getTimeout("default"),
|
|
168
205
|
});
|
|
169
206
|
```
|
|
170
207
|
|
|
171
208
|
### Cloud Usage (JWT/OAuth2)
|
|
172
209
|
|
|
173
210
|
```typescript
|
|
174
|
-
import {
|
|
211
|
+
import {
|
|
212
|
+
AdtCloudConnector,
|
|
213
|
+
CloudHttpTransport,
|
|
214
|
+
SapConfig,
|
|
215
|
+
TokenAuthProvider,
|
|
216
|
+
getTimeout,
|
|
217
|
+
} from "@mcp-abap-adt/connection";
|
|
175
218
|
|
|
176
219
|
// JWT configuration
|
|
177
220
|
const config: SapConfig = {
|
|
@@ -188,21 +231,77 @@ const logger = {
|
|
|
188
231
|
debug: (msg: string, meta?: any) => console.debug(msg, meta),
|
|
189
232
|
};
|
|
190
233
|
|
|
191
|
-
// Logger is optional - if not provided, no logging output
|
|
192
|
-
|
|
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
|
+
);
|
|
193
247
|
await connection.connect();
|
|
194
248
|
|
|
195
|
-
// Note:
|
|
249
|
+
// Note: obtaining and refreshing tokens is @mcp-abap-adt/auth-broker's job
|
|
196
250
|
const response = await connection.makeAdtRequest({
|
|
197
251
|
method: "GET",
|
|
198
252
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
253
|
+
timeout: getTimeout("default"),
|
|
199
254
|
});
|
|
200
255
|
```
|
|
201
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
|
+
|
|
202
295
|
### SSO Usage (SAML Session Cookies)
|
|
203
296
|
|
|
204
297
|
```typescript
|
|
205
|
-
import {
|
|
298
|
+
import {
|
|
299
|
+
AdtOnPremConnector,
|
|
300
|
+
OnPremHttpTransport,
|
|
301
|
+
SamlAuthProvider,
|
|
302
|
+
SapConfig,
|
|
303
|
+
getTimeout,
|
|
304
|
+
} from "@mcp-abap-adt/connection";
|
|
206
305
|
|
|
207
306
|
const config: SapConfig = {
|
|
208
307
|
url: "https://your-sap-system.com",
|
|
@@ -210,47 +309,79 @@ const config: SapConfig = {
|
|
|
210
309
|
sessionCookies: "MYSAPSSO2=...; SAP_SESSIONID=...",
|
|
211
310
|
};
|
|
212
311
|
|
|
213
|
-
|
|
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
|
+
);
|
|
214
322
|
await connection.connect();
|
|
215
323
|
|
|
216
324
|
const response = await connection.makeAdtRequest({
|
|
217
325
|
method: "GET",
|
|
218
326
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
327
|
+
timeout: getTimeout("default"),
|
|
219
328
|
});
|
|
220
329
|
```
|
|
221
330
|
|
|
222
331
|
### Cloud Usage with Automatic Token Refresh
|
|
223
332
|
|
|
224
|
-
|
|
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**:
|
|
225
337
|
|
|
226
338
|
```typescript
|
|
227
|
-
import {
|
|
339
|
+
import {
|
|
340
|
+
AdtCloudConnector,
|
|
341
|
+
CloudHttpTransport,
|
|
342
|
+
SapConfig,
|
|
343
|
+
TokenAuthProvider,
|
|
344
|
+
getTimeout,
|
|
345
|
+
} from "@mcp-abap-adt/connection";
|
|
228
346
|
import type { ITokenRefresher } from "@mcp-abap-adt/interfaces";
|
|
229
347
|
|
|
230
348
|
// Token refresher provides token acquisition and refresh
|
|
231
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';
|
|
232
352
|
const tokenRefresher: ITokenRefresher = {
|
|
233
|
-
getToken: async () =>
|
|
234
|
-
refreshToken: async () =>
|
|
353
|
+
getToken: async () => currentAccessToken, // the one you hold
|
|
354
|
+
refreshToken: async () => exchangeRefreshToken(), // a new one, and cache it
|
|
235
355
|
};
|
|
236
356
|
|
|
237
|
-
// JWT configuration
|
|
238
357
|
const config: SapConfig = {
|
|
239
358
|
url: "https://your-instance.abap.cloud.sap",
|
|
240
359
|
authType: "jwt",
|
|
241
|
-
jwtToken: await tokenRefresher.getToken(), // Get initial token
|
|
242
360
|
};
|
|
243
361
|
|
|
244
|
-
//
|
|
245
|
-
|
|
362
|
+
// The connector says which SYSTEM this is; the provider says how to
|
|
363
|
+
// authenticate. Neither decides the other.
|
|
364
|
+
const connection = new AdtCloudConnector(
|
|
365
|
+
config,
|
|
366
|
+
new TokenAuthProvider(tokenRefresher),
|
|
367
|
+
new CloudHttpTransport(() => ({}), logger, {
|
|
368
|
+
client: config.client,
|
|
369
|
+
baseUrl: config.url,
|
|
370
|
+
}),
|
|
371
|
+
logger,
|
|
372
|
+
);
|
|
246
373
|
await connection.connect();
|
|
247
374
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
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
|
|
378
|
+
// reaches you. A refresh replaces the SAP session, so if a lock window is open
|
|
379
|
+
// the request fails with ADT_SESSION_REPLACED rather than continuing on a
|
|
380
|
+
// session your lock is not in.
|
|
251
381
|
const response = await connection.makeAdtRequest({
|
|
252
382
|
method: "GET",
|
|
253
383
|
url: "/sap/bc/adt/programs/programs/your-program",
|
|
384
|
+
timeout: getTimeout("default"),
|
|
254
385
|
});
|
|
255
386
|
```
|
|
256
387
|
|
|
@@ -271,9 +402,22 @@ See [MIGRATION-4.0.md](./docs/MIGRATION-4.0.md).
|
|
|
271
402
|
For operations that require session state (e.g., object modifications), you can enable stateful sessions:
|
|
272
403
|
|
|
273
404
|
```typescript
|
|
274
|
-
import {
|
|
275
|
-
|
|
276
|
-
|
|
405
|
+
import {
|
|
406
|
+
AdtOnPremConnector,
|
|
407
|
+
BasicAuthProvider,
|
|
408
|
+
OnPremHttpTransport,
|
|
409
|
+
getTimeout,
|
|
410
|
+
} from "@mcp-abap-adt/connection";
|
|
411
|
+
|
|
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
|
+
);
|
|
277
421
|
await connection.connect();
|
|
278
422
|
|
|
279
423
|
// Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
|
|
@@ -284,6 +428,7 @@ await connection.makeAdtRequest({
|
|
|
284
428
|
method: "POST",
|
|
285
429
|
url: "/sap/bc/adt/objects/domains",
|
|
286
430
|
data: { /* domain data */ },
|
|
431
|
+
timeout: getTimeout("default"),
|
|
287
432
|
});
|
|
288
433
|
|
|
289
434
|
// Note: Session state persistence is handled by @mcp-abap-adt/auth-broker package
|
|
@@ -292,7 +437,7 @@ await connection.makeAdtRequest({
|
|
|
292
437
|
### Custom Logger
|
|
293
438
|
|
|
294
439
|
```typescript
|
|
295
|
-
import { ILogger } from "@mcp-abap-adt/connection";
|
|
440
|
+
import { AdtOnPremConnector, BasicAuthProvider, ILogger, OnPremHttpTransport } from "@mcp-abap-adt/connection";
|
|
296
441
|
|
|
297
442
|
class MyLogger implements ILogger {
|
|
298
443
|
info(message: string, meta?: any): void {
|
|
@@ -321,7 +466,15 @@ class MyLogger implements ILogger {
|
|
|
321
466
|
}
|
|
322
467
|
|
|
323
468
|
const logger = new MyLogger();
|
|
324
|
-
const connection =
|
|
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
|
+
);
|
|
325
478
|
```
|
|
326
479
|
|
|
327
480
|
## CLI Tool
|
|
@@ -411,6 +564,8 @@ type SapConfig = {
|
|
|
411
564
|
Main interface for ABAP connections.
|
|
412
565
|
|
|
413
566
|
```typescript
|
|
567
|
+
import { AbapRequestOptions } from '@mcp-abap-adt/connection';
|
|
568
|
+
import type { AxiosResponse } from 'axios';
|
|
414
569
|
// The shared contract (IAbapConnection), what every connection provides:
|
|
415
570
|
interface AbapConnection {
|
|
416
571
|
connect(): Promise<void>; // REQUIRED before any request; rejects on failure
|
|
@@ -421,10 +576,12 @@ interface AbapConnection {
|
|
|
421
576
|
}
|
|
422
577
|
```
|
|
423
578
|
|
|
424
|
-
The
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
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:
|
|
428
585
|
|
|
429
586
|
```typescript
|
|
430
587
|
// ISessionLifecycleAware
|
|
@@ -466,16 +623,35 @@ interface ILogger {
|
|
|
466
623
|
|
|
467
624
|
### Functions
|
|
468
625
|
|
|
469
|
-
#### `
|
|
626
|
+
#### `rfcConversationFrom(config)`
|
|
627
|
+
|
|
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.
|
|
470
631
|
|
|
471
|
-
|
|
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
|
+
```
|
|
472
640
|
|
|
473
641
|
```typescript
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
);
|
|
479
655
|
```
|
|
480
656
|
|
|
481
657
|
#### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES`
|
|
@@ -502,6 +678,12 @@ import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
|
|
|
502
678
|
```typescript
|
|
503
679
|
import { CSRF_CONFIG, CSRF_ERROR_MESSAGES } from '@mcp-abap-adt/connection';
|
|
504
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
|
+
|
|
505
687
|
async function fetchCsrfToken(baseUrl: string): Promise<string> {
|
|
506
688
|
const csrfUrl = `${baseUrl}${CSRF_CONFIG.ENDPOINT}`;
|
|
507
689
|
|
|
@@ -533,6 +715,10 @@ async function fetchCsrfToken(baseUrl: string): Promise<string> {
|
|
|
533
715
|
await new Promise(resolve => setTimeout(resolve, CSRF_CONFIG.RETRY_DELAY));
|
|
534
716
|
}
|
|
535
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);
|
|
536
722
|
}
|
|
537
723
|
```
|
|
538
724
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentials a connector can be handed.
|
|
3
|
+
*
|
|
4
|
+
* Each is the authentication half of one of the connection classes that used to
|
|
5
|
+
* carry both halves, lifted out unchanged — the header a `BasicAuthProvider`
|
|
6
|
+
* builds is byte for byte what `BaseAbapConnection` built, and the TLS options a
|
|
7
|
+
* `CertificateAuthProvider` returns are what `CertificateAbapConnection`
|
|
8
|
+
* returned. Nothing about how a credential works changed; only who owns it.
|
|
9
|
+
*/
|
|
10
|
+
import type { IAuthProvider, ICertificateMaterial, ICertificateMaterialLoader, IRenewableCredential, ISapConfig, ITokenRefresher } from '@mcp-abap-adt/interfaces';
|
|
11
|
+
/** Username and password, as `Basic base64(user:pass)`. */
|
|
12
|
+
export declare class BasicAuthProvider implements IAuthProvider {
|
|
13
|
+
private readonly username;
|
|
14
|
+
private readonly password;
|
|
15
|
+
readonly kind = "basic";
|
|
16
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
17
|
+
prepare(): Promise<void>;
|
|
18
|
+
/** Not cookies. This one authenticates with a header. */
|
|
19
|
+
cookies(): string | null;
|
|
20
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
21
|
+
transportMaterial(): ICertificateMaterial;
|
|
22
|
+
constructor(username: string, password: string);
|
|
23
|
+
authorizationHeader(): Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A bearer token, kept current by whoever issued it.
|
|
27
|
+
*
|
|
28
|
+
* Takes an `ITokenRefresher` — the contract `@mcp-abap-adt/auth-broker` already
|
|
29
|
+
* produces and `@mcp-abap-adt/auth-providers` already implements twelve ways.
|
|
30
|
+
* This package depends on neither: it speaks the contract, and the consumer
|
|
31
|
+
* brings the implementation, exactly as it does for `IAbapConnection`.
|
|
32
|
+
*
|
|
33
|
+
* **This carries no session recovery.** What happens to a session when the
|
|
34
|
+
* credential behind it is renewed mid-flight is the connection's business and
|
|
35
|
+
* stays there; see `JwtAbapConnection`, which still owns that machinery.
|
|
36
|
+
*/
|
|
37
|
+
export declare class TokenAuthProvider implements IRenewableCredential {
|
|
38
|
+
private readonly source;
|
|
39
|
+
readonly kind = "token";
|
|
40
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
41
|
+
prepare(): Promise<void>;
|
|
42
|
+
/** Not cookies. This one authenticates with a header. */
|
|
43
|
+
cookies(): string | null;
|
|
44
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
45
|
+
transportMaterial(): ICertificateMaterial;
|
|
46
|
+
/**
|
|
47
|
+
* A token, or something that can produce one.
|
|
48
|
+
*
|
|
49
|
+
* A bare string is a token with no renewal behind it — honest, and fine for a
|
|
50
|
+
* short task. An `ITokenRefresher` or a function is a provider that checks
|
|
51
|
+
* expiry and renews on its own, which is where this belongs in a long-lived
|
|
52
|
+
* process.
|
|
53
|
+
*/
|
|
54
|
+
constructor(source: string | ITokenRefresher | (() => Promise<string>));
|
|
55
|
+
/**
|
|
56
|
+
* Asked every time, and nothing kept.
|
|
57
|
+
*
|
|
58
|
+
* The provider behind this already caches the token, knows when it expires,
|
|
59
|
+
* and renews before handing one back. A second cache here would serve the
|
|
60
|
+
* stale one and hide exactly the renewal the provider exists to do — which
|
|
61
|
+
* is what the first version of this class did.
|
|
62
|
+
*/
|
|
63
|
+
authorizationHeader(): Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* The token was refused, so force a new one.
|
|
66
|
+
*
|
|
67
|
+
* Declared through `IRenewableCredential` rather than as an optional member
|
|
68
|
+
* of every credential, so a consumer narrows to it: a password has nothing
|
|
69
|
+
* behind it to ask again, and a SAML session was negotiated elsewhere.
|
|
70
|
+
*
|
|
71
|
+
* **Nothing in this package calls it.** A token that EXPIRED is replaced
|
|
72
|
+
* inside `authorizationHeader()` above, which is asked per request — nobody
|
|
73
|
+
* decides anything. This is the other case, a token the source still believes
|
|
74
|
+
* in and the server refuses, and whether a refusal meant that is a judgement
|
|
75
|
+
* made with what the caller knows. So the refusal surfaces, and this is the
|
|
76
|
+
* seam the caller decides with.
|
|
77
|
+
*
|
|
78
|
+
* `getToken()` is documented to return the cached token while it believes it
|
|
79
|
+
* is still valid — which is exactly the situation after a 401 on a token the
|
|
80
|
+
* provider has not yet noticed is dead. `refreshToken()` is the contract's
|
|
81
|
+
* answer for that, and this is the only place it is called.
|
|
82
|
+
*/
|
|
83
|
+
renew(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A SAML session, already negotiated, presented as cookies.
|
|
87
|
+
*
|
|
88
|
+
* No `Authorization` header at all — the cookies are the credential, and they
|
|
89
|
+
* are added by the connection alongside its own.
|
|
90
|
+
*/
|
|
91
|
+
export declare class SamlAuthProvider implements IAuthProvider {
|
|
92
|
+
private readonly sessionCookies;
|
|
93
|
+
readonly kind = "saml";
|
|
94
|
+
/** Nothing to get ready: this credential is complete as constructed. */
|
|
95
|
+
prepare(): Promise<void>;
|
|
96
|
+
/** No TLS material: this credential lives in a header, not in the transport. */
|
|
97
|
+
transportMaterial(): ICertificateMaterial;
|
|
98
|
+
constructor(sessionCookies: string);
|
|
99
|
+
/** Not a header: this credential authenticates with the cookies below. */
|
|
100
|
+
authorizationHeader(): Promise<string | null>;
|
|
101
|
+
/** The cookies to present. */
|
|
102
|
+
cookies(): string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Client certificate: the credential lives in the TLS handshake.
|
|
106
|
+
*
|
|
107
|
+
* `prepare()` is where the material is read, which is why it exists at all —
|
|
108
|
+
* building an agent before it is loaded is what used to reject a connect
|
|
109
|
+
* before a single request went out.
|
|
110
|
+
*/
|
|
111
|
+
export declare class CertificateAuthProvider implements IAuthProvider {
|
|
112
|
+
private readonly loader;
|
|
113
|
+
private readonly config;
|
|
114
|
+
readonly kind = "certificate";
|
|
115
|
+
/** Not cookies. This one authenticates with a header. */
|
|
116
|
+
cookies(): string | null;
|
|
117
|
+
private material;
|
|
118
|
+
constructor(loader: ICertificateMaterialLoader, config: ISapConfig);
|
|
119
|
+
prepare(): Promise<void>;
|
|
120
|
+
/** Not a header: this credential authenticates through TLS. */
|
|
121
|
+
authorizationHeader(): Promise<string | null>;
|
|
122
|
+
transportMaterial(): ICertificateMaterial;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/auth/providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,0BAA0B,EAC1B,oBAAoB,EACpB,UAAU,EACV,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC,2DAA2D;AAC3D,qBAAa,iBAAkB,YAAW,aAAa;IAiBnD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAjB3B,QAAQ,CAAC,IAAI,WAAW;IAExB,wEAAwE;IAClE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAE9B,yDAAyD;IACzD,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB,gFAAgF;IAChF,iBAAiB,IAAI,oBAAoB;gBAKtB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM;IAG7B,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAGpD;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,YAAW,oBAAoB;IAyB1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAxBzB,QAAQ,CAAC,IAAI,WAAW;IAExB,wEAAwE;IAClE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAE9B,yDAAyD;IACzD,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB,gFAAgF;IAChF,iBAAiB,IAAI,oBAAoB;IAIzC;;;;;;;OAOG;gBAEgB,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAG7E;;;;;;;OAOG;IACG,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUnD;;;;;;;;;;;;;;;;;;OAkBG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ7B;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,aAAa;IAWxC,OAAO,CAAC,QAAQ,CAAC,cAAc;IAV3C,QAAQ,CAAC,IAAI,UAAU;IAEvB,wEAAwE;IAClE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAE9B,gFAAgF;IAChF,iBAAiB,IAAI,oBAAoB;gBAIZ,cAAc,EAAE,MAAM;IAEnD,0EAA0E;IACpE,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAInD,8BAA8B;IAC9B,OAAO,IAAI,MAAM;CAGlB;AAED;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IAWzD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAXzB,QAAQ,CAAC,IAAI,iBAAiB;IAE9B,yDAAyD;IACzD,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB,OAAO,CAAC,QAAQ,CAAqC;gBAGlC,MAAM,EAAE,0BAA0B,EAClC,MAAM,EAAE,UAAU;IAG/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,+DAA+D;IACzD,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAInD,iBAAiB,IAAI,oBAAoB;CAS1C"}
|