@mcp-abap-adt/connection 2.0.0 → 4.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 +133 -1
- package/README.md +26 -11
- package/dist/connection/AbstractAbapConnection.d.ts +32 -16
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +93 -51
- package/dist/connection/BaseAbapConnection.js +1 -1
- package/dist/connection/CertificateAbapConnection.js +1 -1
- package/dist/connection/JwtAbapConnection.d.ts +84 -7
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +246 -80
- package/dist/connection/SamlAbapConnection.js +1 -1
- package/dist/index.js +4 -4
- package/dist/session/SessionLifecycle.d.ts +49 -62
- package/dist/session/SessionLifecycle.d.ts.map +1 -1
- package/dist/session/SessionLifecycle.js +56 -155
- package/docs/INDEX.md +5 -0
- package/docs/MIGRATION-2.0.md +22 -33
- package/docs/MIGRATION-4.0.md +95 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +11 -4
- package/docs/USAGE.md +90 -73
- package/examples/README.md +2 -1
- package/examples/jwt-with-token-refresh.js +11 -3
- package/package.json +2 -2
package/docs/USAGE.md
CHANGED
|
@@ -219,10 +219,9 @@ implied. Four things follow from it.
|
|
|
219
219
|
|
|
220
220
|
> **Availability.** `connect()` is on the shared `IAbapConnection` contract and
|
|
221
221
|
> works on every connection. The rest of this section is on the contract too, but
|
|
222
|
-
> as
|
|
223
|
-
>
|
|
224
|
-
> `
|
|
225
|
-
> (`beginWindow()`, `endWindow()`).
|
|
222
|
+
> as a **capability atom** in `@mcp-abap-adt/interfaces` rather than as methods on
|
|
223
|
+
> `IAbapConnection`: `ISessionLifecycleAware` — `disconnect()`, `isConnected()`,
|
|
224
|
+
> `getSessionIdentity()`.
|
|
226
225
|
>
|
|
227
226
|
> The split is the point. `IAbapConnection` is the minimum every transport can
|
|
228
227
|
> honour, and `RfcAbapConnection` has none of these — on RFC the session *is* the
|
|
@@ -232,50 +231,51 @@ implied. Four things follow from it.
|
|
|
232
231
|
> So ask for the atom you need, not for a concrete class:
|
|
233
232
|
>
|
|
234
233
|
> ```typescript
|
|
235
|
-
> function
|
|
234
|
+
> function tearDownAfter(conn: IAbapConnection & ISessionLifecycleAware) { /* ... */ }
|
|
236
235
|
> ```
|
|
237
236
|
>
|
|
238
237
|
> **How you satisfy that parameter decides whether you get a compile-time
|
|
239
238
|
> guarantee, and there is only one way that does.**
|
|
240
239
|
>
|
|
241
240
|
> Construct the connection through a concrete HTTP class and the compiler knows
|
|
242
|
-
> it implements the
|
|
241
|
+
> it implements the atom, so passing an RFC connection is an error at the call
|
|
243
242
|
> site:
|
|
244
243
|
>
|
|
245
244
|
> ```typescript
|
|
246
245
|
> const conn = new BaseAbapConnection(config, logger);
|
|
247
|
-
>
|
|
248
|
-
>
|
|
246
|
+
> tearDownAfter(conn); // ✅ checked
|
|
247
|
+
> tearDownAfter(new RfcAbapConnection(cfg)); // ✅ compile error, as it should be
|
|
249
248
|
> ```
|
|
250
249
|
>
|
|
251
250
|
> `createAbapConnection()` cannot give you that. It returns `IAbapConnection` for
|
|
252
251
|
> **every** config, RFC included, so the type carries no evidence either way and
|
|
253
252
|
> the compiler rejects its result whatever the transport. Asserting past that —
|
|
254
|
-
> `conn as IAbapConnection &
|
|
255
|
-
> case *and* for RFC, which then fails at runtime
|
|
256
|
-
>
|
|
253
|
+
> `conn as IAbapConnection & ISessionLifecycleAware` — silences the error for the
|
|
254
|
+
> HTTP case *and* for RFC, which then fails at runtime. An assertion is not a
|
|
255
|
+
> check; it is a promise you make to the compiler on your own authority.
|
|
257
256
|
>
|
|
258
257
|
> When you only have an `IAbapConnection` — from the factory, or from a caller —
|
|
259
258
|
> narrow it at runtime with a predicate:
|
|
260
259
|
>
|
|
261
260
|
> ```typescript
|
|
262
|
-
> function
|
|
261
|
+
> function ownsItsSession(
|
|
263
262
|
> conn: IAbapConnection,
|
|
264
|
-
> ): conn is IAbapConnection &
|
|
265
|
-
> const candidate = conn as Partial<
|
|
263
|
+
> ): conn is IAbapConnection & ISessionLifecycleAware {
|
|
264
|
+
> const candidate = conn as Partial<ISessionLifecycleAware>;
|
|
266
265
|
> // EVERY method of the atom. A predicate narrows to the whole interface, so
|
|
267
|
-
> // checking one
|
|
268
|
-
> //
|
|
266
|
+
> // checking one and promising three puts the failure back where this check
|
|
267
|
+
> // was meant to remove it — inside the branch that looked safe.
|
|
269
268
|
> return (
|
|
270
|
-
> typeof candidate.
|
|
271
|
-
> typeof candidate.
|
|
269
|
+
> typeof candidate.disconnect === 'function' &&
|
|
270
|
+
> typeof candidate.isConnected === 'function' &&
|
|
271
|
+
> typeof candidate.getSessionIdentity === 'function'
|
|
272
272
|
> );
|
|
273
273
|
> }
|
|
274
274
|
>
|
|
275
|
-
> if (
|
|
276
|
-
>
|
|
275
|
+
> if (ownsItsSession(conn)) {
|
|
276
|
+
> tearDownAfter(conn); // narrowed by evidence, not by assertion
|
|
277
277
|
> } else {
|
|
278
|
-
> // No
|
|
278
|
+
> // No HTTP session here. On RFC that is expected, not a failure.
|
|
279
279
|
> }
|
|
280
280
|
> ```
|
|
281
281
|
>
|
|
@@ -323,72 +323,68 @@ try {
|
|
|
323
323
|
}
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
-
`
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
`IAbapConnection` that implements neither, so the atom you require is also the
|
|
326
|
+
`AdtSessionErrorCode` comes from the same place, as does the capability interface
|
|
327
|
+
this connection implements — `ISessionLifecycleAware`. Depend on that rather than
|
|
328
|
+
on a concrete connection class where you can: an `RfcAbapConnection` is a valid
|
|
329
|
+
`IAbapConnection` that does not implement it, so the atom you require is also the
|
|
331
330
|
documentation of what your code actually needs.
|
|
332
331
|
|
|
333
|
-
### disconnect()
|
|
332
|
+
### disconnect() waits for nothing
|
|
334
333
|
|
|
335
334
|
```typescript
|
|
336
|
-
|
|
337
|
-
// { abandonedWindows: string[], releasePending: boolean }
|
|
335
|
+
await connection.disconnect(); // Promise<void>
|
|
338
336
|
```
|
|
339
337
|
|
|
340
|
-
It never throws
|
|
341
|
-
in
|
|
342
|
-
|
|
338
|
+
It never throws, and it always settles. **It waits for nothing** — not for
|
|
339
|
+
requests in flight, not for anything you hold. Deciding when to disconnect is
|
|
340
|
+
yours, and so is preparing for it.
|
|
343
341
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
342
|
+
Requests already in flight run to completion untouched; nothing is aborted. What
|
|
343
|
+
the connection guarantees is that their results can no longer reach it: a
|
|
344
|
+
response arriving after the teardown is fenced, so it cannot write its cookies
|
|
345
|
+
over a session established since, and cannot be mistaken for a replacement.
|
|
347
346
|
|
|
348
|
-
|
|
347
|
+
An earlier version waited for in-flight requests before clearing anything. That
|
|
348
|
+
turned out to be unbounded — a request whose caller chose no timeout could hold a
|
|
349
|
+
teardown open forever, and since lifecycle transitions are serialized, every
|
|
350
|
+
later `connect()` queued behind it.
|
|
349
351
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
+
**Over HTTP it does not release locks.** The ABAP session lives on until its
|
|
353
|
+
timeout, along with whatever it held. Unlock first; disconnecting is not a way to
|
|
354
|
+
clean up after yourself.
|
|
352
355
|
|
|
353
|
-
|
|
354
|
-
const token = connection.beginWindow('Class/ZCL_MY_CLASS');
|
|
356
|
+
### Uninterruptible spans
|
|
355
357
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
358
|
+
The connection does **not** track locks — it does not know one exists, what
|
|
359
|
+
object it covers, or what would release it. Pairing every LOCK with its UNLOCK is
|
|
360
|
+
`@mcp-abap-adt/adt-clients`' job, and it does it per object in its lock registry.
|
|
361
|
+
|
|
362
|
+
What this layer owns is the timeout, because a timeout is the server taking too
|
|
363
|
+
long and nothing about it depends on the caller. Aborting a request mid-flight
|
|
364
|
+
leaves an operation whose outcome you cannot determine — a `LOCK` that may have
|
|
365
|
+
succeeded server-side, with a handle you never received:
|
|
364
366
|
|
|
367
|
+
```typescript
|
|
368
|
+
connection.beginCriticalSection();
|
|
365
369
|
try {
|
|
366
|
-
await
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Deliberately NOT closed here. The unlock may have failed, never gone out,
|
|
372
|
-
// or come back with an unknown outcome — in each of those the lock is most
|
|
373
|
-
// likely still held, and a window left open is what makes a teardown wait for
|
|
374
|
-
// it and report it by name instead of walking past it.
|
|
375
|
-
throw error;
|
|
370
|
+
const handle = await lock(connection, 'ZCL_MY_CLASS');
|
|
371
|
+
await update(connection, 'ZCL_MY_CLASS', handle);
|
|
372
|
+
await unlock(connection, 'ZCL_MY_CLASS', handle);
|
|
373
|
+
} finally {
|
|
374
|
+
connection.endCriticalSection();
|
|
376
375
|
}
|
|
377
376
|
```
|
|
378
377
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
378
|
+
While a section is active the effective timeout is raised to
|
|
379
|
+
`SAP_TIMEOUT_CRITICAL` (600s by default) — `Math.max(yours, the ceiling)`, so it
|
|
380
|
+
never shortens a timeout you chose. The pair is reference-counted, so nesting is
|
|
381
|
+
safe.
|
|
383
382
|
|
|
384
|
-
The
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
a new window cannot be opened once a teardown has been requested. A window that
|
|
390
|
-
never closes is given up on after `SAP_TIMEOUT_CRITICAL` and comes back **named**
|
|
391
|
-
in `ITeardownReport.abandonedWindows`.
|
|
383
|
+
The raise is **connection-wide**: while any section is active, every request on
|
|
384
|
+
that connection gets the ceiling, including one that has nothing to do with the
|
|
385
|
+
locked object. Those requests share one ABAP session, and an abort leaves that
|
|
386
|
+
session in the same uncertain state during a span someone declared sensitive
|
|
387
|
+
precisely to avoid it.
|
|
392
388
|
|
|
393
389
|
### When the session is lost
|
|
394
390
|
|
|
@@ -583,7 +579,7 @@ interface AbapConnection {
|
|
|
583
579
|
|
|
584
580
|
Anything beyond that is **not** on the contract. `reset()`, `getSessionMode()`
|
|
585
581
|
and the session lifecycle (`disconnect()`, `isConnected()`,
|
|
586
|
-
`getSessionIdentity()
|
|
582
|
+
`getSessionIdentity()`) live on the HTTP
|
|
587
583
|
connection classes; `RfcAbapConnection` has some of them and not others, so
|
|
588
584
|
reach for them through a concrete type rather than through what
|
|
589
585
|
`createAbapConnection()` returns.
|
|
@@ -680,12 +676,33 @@ For SAP BTP cloud systems. Token refresh is handled by `@mcp-abap-adt/auth-broke
|
|
|
680
676
|
|
|
681
677
|
```typescript
|
|
682
678
|
class JwtAbapConnection extends AbstractAbapConnection {
|
|
683
|
-
constructor(
|
|
679
|
+
constructor(
|
|
680
|
+
config: SapConfig,
|
|
681
|
+
logger?: ILogger | null,
|
|
682
|
+
sessionId?: string,
|
|
683
|
+
tokenRefresher?: ITokenRefresher,
|
|
684
|
+
);
|
|
684
685
|
// Note: refreshToken() and canRefreshToken() methods removed in 0.2.0
|
|
685
|
-
//
|
|
686
|
+
// Token acquisition itself belongs to @mcp-abap-adt/auth-broker
|
|
686
687
|
}
|
|
687
688
|
```
|
|
688
689
|
|
|
690
|
+
**How failures are classified** (4.0.0 — see
|
|
691
|
+
[MIGRATION-4.0.md](./MIGRATION-4.0.md)):
|
|
692
|
+
|
|
693
|
+
| answer | what happens |
|
|
694
|
+
|---|---|
|
|
695
|
+
| **401** | with an injected `ITokenRefresher`, the token is refreshed, the SAP session re-established, and the request retried once. Without one, or when the retry is refused too, the server's error is rethrown |
|
|
696
|
+
| **403** | propagates untouched. The server authenticated the caller and refused the action anyway, so a new token is the same caller — usually the body names the authorization object |
|
|
697
|
+
| anything else | untouched |
|
|
698
|
+
|
|
699
|
+
The connection never replaces the server's error with one of its own: `error.response.status` and
|
|
700
|
+
`error.response.data` are always what SAP sent. Before 4.0.0 both 401 and 403 were reported as
|
|
701
|
+
`JWT token has expired. Please re-authenticate.` with the original error discarded.
|
|
702
|
+
|
|
703
|
+
Concurrent requests that meet the same expired token share **one** renewal — a single token fetch
|
|
704
|
+
and a single session re-establishment between them, not one each.
|
|
705
|
+
|
|
689
706
|
### `createAbapConnection()` Factory
|
|
690
707
|
|
|
691
708
|
Recommended way to create connections:
|
package/examples/README.md
CHANGED
|
@@ -43,8 +43,9 @@ node examples/jwt-with-token-refresh.js
|
|
|
43
43
|
|
|
44
44
|
**What it demonstrates:**
|
|
45
45
|
- Creating connection with token refresher injection
|
|
46
|
-
- Automatic token refresh on 401
|
|
46
|
+
- Automatic token refresh on 401 errors
|
|
47
47
|
- Retry logic with refreshed token
|
|
48
|
+
- A 403 propagating untouched, with its status and body intact
|
|
48
49
|
|
|
49
50
|
### saml-connection.js
|
|
50
51
|
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
* This example demonstrates how to create a JwtAbapConnection with
|
|
5
5
|
* automatic token refresh using ITokenRefresher from auth-broker.
|
|
6
6
|
*
|
|
7
|
-
* When 401
|
|
7
|
+
* When a **401** occurs, the connection automatically:
|
|
8
8
|
* 1. Calls tokenRefresher.refreshToken() to get a new token
|
|
9
9
|
* 2. Updates internal token state
|
|
10
|
-
* 3.
|
|
10
|
+
* 3. Re-establishes the SAP session, which the new credential cannot inherit
|
|
11
|
+
* 4. Retries the failed request
|
|
12
|
+
*
|
|
13
|
+
* A **403** is left alone. It means the server authenticated you and refused
|
|
14
|
+
* the action anyway — an authorization gap, not an expired credential — so it
|
|
15
|
+
* propagates with its status and the server's message, and a new token would
|
|
16
|
+
* change nothing.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
19
|
const { JwtAbapConnection } = require('@mcp-abap-adt/connection');
|
|
@@ -68,7 +74,9 @@ async function main() {
|
|
|
68
74
|
try {
|
|
69
75
|
await connection.connect();
|
|
70
76
|
|
|
71
|
-
// This request will automatically refresh token if 401
|
|
77
|
+
// This request will automatically refresh the token if a 401 occurs. A
|
|
78
|
+
// 403 arrives as-is — read err.response.status and err.response.data to
|
|
79
|
+
// see which authorization object the server named. Note
|
|
72
80
|
// that a refresh replaces the SAP session: with a lock window open the
|
|
73
81
|
// request would fail with ADT_SESSION_REPLACED rather than continue on a
|
|
74
82
|
// session your lock is not in.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/connection",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "ABAP connection layer for MCP ABAP ADT server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"node": ">=18.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@mcp-abap-adt/interfaces": "^
|
|
53
|
+
"@mcp-abap-adt/interfaces": "^12.0.0",
|
|
54
54
|
"axios": "^1.16.0",
|
|
55
55
|
"commander": "^14.0.3",
|
|
56
56
|
"express": "^5.1.0",
|