@mcp-abap-adt/connection 8.0.0 → 8.1.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 +51 -1
- package/README.md +17 -1
- package/dist/connection/RfcTransport.d.ts +23 -1
- package/dist/connection/RfcTransport.d.ts.map +1 -1
- package/dist/connection/RfcTransport.js +73 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/docs/INDEX.md +2 -0
- package/docs/MIGRATION-8.0.md +171 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +23 -0
- package/docs/USAGE.md +21 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [8.1.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
**The RFC wire can be asked for its payload, and what it logs is safe to
|
|
13
|
+
paste.** `RFC → METHOD URI` did not show a body that goes missing or gets
|
|
14
|
+
mis-serialised before it reaches `SADT_REST_RFC_ENDPOINT`. Nothing existing
|
|
15
|
+
changes: the channel is off until asked for.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `RfcTransport` can be asked to log the wire: a third constructor argument
|
|
20
|
+
`{ logWire?, maxLoggedBodyChars? }`, off by default, adds the request header
|
|
21
|
+
fields, the request body and the response body to the debug channel. This is
|
|
22
|
+
what shows a payload that was mis-serialised before it reached
|
|
23
|
+
`SADT_REST_RFC_ENDPOINT`. Credential header values (`Authorization`, any
|
|
24
|
+
`Cookie`, anything matching `token`, `secret`, `password`, `credential` or an
|
|
25
|
+
API key) are replaced with `[redacted]` — the names are kept — and a body is
|
|
26
|
+
clipped at `maxLoggedBodyChars`, 2000 by default — `0` logs the size alone,
|
|
27
|
+
`Infinity` asks for the whole body, and a negative or `NaN` ceiling falls
|
|
28
|
+
back to the default rather than throwing. Bodies are not redacted, only
|
|
29
|
+
clipped.
|
|
30
|
+
|
|
31
|
+
It is a flag rather than something inferred from the presence of a logger
|
|
32
|
+
because `ILogger` carries no level predicate: `logger?.debug()` cannot tell
|
|
33
|
+
an enabled debug channel from one that discards, so without the flag every
|
|
34
|
+
caller passing a logger would build a copy of every body on the wire and
|
|
35
|
+
throw it away. `HttpTransport` logs no bodies at any setting.
|
|
36
|
+
|
|
37
|
+
## [8.0.1] - 2026-09-08
|
|
38
|
+
|
|
39
|
+
**Documentation only — 7.0.0 and 8.0.0 shipped without their migration note.**
|
|
40
|
+
|
|
41
|
+
Both majors are already on npm, and `docs/` travels in the tarball, so the
|
|
42
|
+
installed package described the pre-7.0.0 connection: no request id, no
|
|
43
|
+
profiling, no `flushGoodbye`, and no word on what a consumer on 6.x must
|
|
44
|
+
change. This release carries the documentation those two releases owed and
|
|
45
|
+
changes no code.
|
|
46
|
+
|
|
47
|
+
### Documentation
|
|
48
|
+
|
|
49
|
+
- `docs/MIGRATION-8.0.md` (new): what a consumer on 6.x does about the
|
|
50
|
+
`IAdtWireResponse` return of `makeAdtRequest`, the capability atoms the
|
|
51
|
+
class now declares, and the two headers every request now carries.
|
|
52
|
+
- `docs/STATEFUL_SESSION_GUIDE.md`: the session type is the connection's —
|
|
53
|
+
`x-sap-adt-sessiontype: stateful` is added by the transport, not by a
|
|
54
|
+
caller's headers — and `flushGoodbye` is how a caller waits for the
|
|
55
|
+
goodbye.
|
|
56
|
+
- `README.md` and `docs/INDEX.md` point at both.
|
|
57
|
+
|
|
10
58
|
## [8.0.0] - 2026-09-08
|
|
11
59
|
|
|
12
60
|
**A consumer holding the contract can now do what the connection could always do.**
|
|
@@ -1513,7 +1561,9 @@ const connection = createAbapConnection(config, logger);
|
|
|
1513
1561
|
- JWT token refresh now properly handles connection errors (401/403 during initial connect)
|
|
1514
1562
|
- Permission errors (403 with "ExceptionResourceNoAccess") no longer trigger JWT refresh loops
|
|
1515
1563
|
- Proper separation: base class handles HTTP/session, concrete classes handle auth-specific errors
|
|
1516
|
-
[Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.
|
|
1564
|
+
[Unreleased]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.1...HEAD
|
|
1565
|
+
[8.1.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.1...v8.1.0
|
|
1566
|
+
[8.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v8.0.0...v8.0.1
|
|
1517
1567
|
[8.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v7.0.0...v8.0.0
|
|
1518
1568
|
[7.0.0]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.1.0...v7.0.0
|
|
1519
1569
|
[6.0.1]: https://github.com/fr0ster/mcp-abap-connection/compare/v6.0.0...v6.0.1
|
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ The package uses a clean separation of concerns:
|
|
|
34
34
|
|
|
35
35
|
- **`AbstractAbapConnection`** (abstract, internal only):
|
|
36
36
|
- Common HTTP request logic
|
|
37
|
-
- Session lifecycle: `connect()` / `disconnect()`, admission, lock windows, teardown draining
|
|
37
|
+
- Session lifecycle: `connect()` / `disconnect()` / `flushGoodbye()`, admission, lock windows, teardown draining
|
|
38
|
+
- Capability atoms a consumer narrows to, rather than casting to a connector class:
|
|
39
|
+
`ISessionLifecycleAware`, `ICriticalSection`, `IRequestProfiling`
|
|
38
40
|
- Session management (cookies, CSRF tokens)
|
|
39
41
|
- CSRF token fetching with retry
|
|
40
42
|
- Auth-agnostic - knows nothing about Basic or JWT
|
|
@@ -654,6 +656,20 @@ const connection = new AdtOnPremConnector(
|
|
|
654
656
|
);
|
|
655
657
|
```
|
|
656
658
|
|
|
659
|
+
A third argument turns on the wire log, which is off by default:
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
new RfcTransport(rfcConversationFrom(config), logger, { logWire: true });
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
It adds three debug lines per request — header fields, request body, response
|
|
666
|
+
body — which is how you see that a payload was mis-serialised before it reached
|
|
667
|
+
`SADT_REST_RFC_ENDPOINT`. Credential header values are replaced with
|
|
668
|
+
`[redacted]` and bodies are clipped at `maxLoggedBodyChars` (2000). The bodies
|
|
669
|
+
themselves are not redacted, so read a captured log before pasting it into an
|
|
670
|
+
issue. `HttpTransport` never logs bodies, so this is the one wire whose debug
|
|
671
|
+
channel can be asked for the payload.
|
|
672
|
+
|
|
657
673
|
#### `CSRF_CONFIG` and `CSRF_ERROR_MESSAGES`
|
|
658
674
|
|
|
659
675
|
**New in 0.1.13+:** Exported constants for consistent CSRF token handling across different connection implementations.
|
|
@@ -26,9 +26,29 @@ export interface IRfcConversation {
|
|
|
26
26
|
call(fm: string, params: Record<string, unknown>): Promise<Record<string, any>>;
|
|
27
27
|
readonly alive: boolean;
|
|
28
28
|
}
|
|
29
|
+
/** What this wire does beyond carrying the request. */
|
|
30
|
+
export interface IRfcTransportOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Whether the debug channel also carries the request headers and both
|
|
33
|
+
* bodies. Off by default, and deliberately not inferred from the presence
|
|
34
|
+
* of a logger: `ILogger` has no level predicate, so `logger?.debug()` cannot
|
|
35
|
+
* tell an enabled debug channel from a discarded one — without this flag
|
|
36
|
+
* every caller who passes a logger at all would pay to build and throw away
|
|
37
|
+
* a copy of every body on the wire.
|
|
38
|
+
*/
|
|
39
|
+
logWire?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Ceiling on a logged body, in characters. Defaults to 2000. `0` logs the
|
|
42
|
+
* size and none of the bytes, `Infinity` asks for the whole body, and a
|
|
43
|
+
* negative or `NaN` value falls back to the default rather than throwing —
|
|
44
|
+
* a debug option is not worth failing a connection over.
|
|
45
|
+
*/
|
|
46
|
+
maxLoggedBodyChars?: number;
|
|
47
|
+
}
|
|
29
48
|
export declare class RfcTransport implements IOnPremTransport {
|
|
30
49
|
private readonly connect;
|
|
31
50
|
private readonly logger;
|
|
51
|
+
private readonly options;
|
|
32
52
|
readonly kind = "rfc";
|
|
33
53
|
/** Which system this wire is for. Read by the compiler, never at runtime. */
|
|
34
54
|
readonly system: "onprem";
|
|
@@ -80,7 +100,9 @@ export declare class RfcTransport implements IOnPremTransport {
|
|
|
80
100
|
* that reached for it in its constructor could not be built at all on a
|
|
81
101
|
* machine without it.
|
|
82
102
|
*/
|
|
83
|
-
constructor(connect: () => IRfcConversation, logger?: ILogger | null);
|
|
103
|
+
constructor(connect: () => IRfcConversation, logger?: ILogger | null, options?: IRfcTransportOptions);
|
|
104
|
+
/** Cut a body down to what a log line may carry. */
|
|
105
|
+
private clipped;
|
|
84
106
|
open(): Promise<void>;
|
|
85
107
|
/** Never throws, and a repeat call finds nothing owed. */
|
|
86
108
|
close(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"RfcTransport.d.ts","sourceRoot":"","sources":["../../src/connection/RfcTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EACV,oBAAoB,EAEpB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAE5B,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CACF,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAgFD,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAKD,qBAAa,YAAa,YAAW,gBAAgB;IA+EjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAhF1B,QAAQ,CAAC,IAAI,SAAS;IAEtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,QAAQ,CAAU;IAEpC,OAAO,CAAC,YAAY,CAAiC;IACrD,kEAAkE;IAClE,OAAO,CAAC,cAAc,CAAM;IAE5B;;;;;;OAMG;IACH,MAAM,IAAI,IAAI;IAEd,2CAA2C;IAC3C,OAAO,IAAI,IAAI;IAIf;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE9D,+BAA+B;IAC/B,SAAS,IAAI,IAAI;IAIjB;;;OAGG;IACH,cAAc,IAAI,IAAI;IAEtB;;;OAGG;IACH,6EAA6E;IAC7E,kBAAkB,IAAI,OAAO;IAI7B,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC,2EAA2E;IAC3E,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAIzC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAErB;;;;;OAKG;gBAEgB,OAAO,EAAE,MAAM,gBAAgB,EAC/B,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,OAAO,GAAE,oBAAyB;IAGrD,oDAAoD;IACpD,OAAO,CAAC,OAAO;IAcT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,0DAA0D;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAkK1E"}
|
|
@@ -48,11 +48,52 @@ function message(e) {
|
|
|
48
48
|
return JSON.stringify(e);
|
|
49
49
|
return String(e);
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Header names whose VALUE must never reach a log.
|
|
53
|
+
*
|
|
54
|
+
* `Authorization` carries the Basic-auth credential in plain (base64, but that
|
|
55
|
+
* is not encryption) form — and it is not the only one. `request.headers`
|
|
56
|
+
* reaches this wire verbatim from the caller, so a consumer's `Cookie`
|
|
57
|
+
* (`SAP_SESSIONID_*`, `MYSAPSSO2`), an `x-csrf-token` or its own `X-Api-Key`
|
|
58
|
+
* arrives here too. A log line advertised as safe to paste into an issue has
|
|
59
|
+
* to be safe for the headers nobody here anticipated, so this matches by
|
|
60
|
+
* pattern rather than by the list we happened to think of.
|
|
61
|
+
*/
|
|
62
|
+
const SECRET_HEADER_PATTERNS = [
|
|
63
|
+
/authorization/i,
|
|
64
|
+
/cookie/i,
|
|
65
|
+
/token/i,
|
|
66
|
+
/secret/i,
|
|
67
|
+
/password/i,
|
|
68
|
+
/credential/i,
|
|
69
|
+
/api[-_]?key/i,
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* The NAME is kept and only the VALUE goes — a redacted header still says it
|
|
73
|
+
* was sent, which is half of what the log is read for.
|
|
74
|
+
*/
|
|
75
|
+
function redactHeaders(fields) {
|
|
76
|
+
return fields.map((field) => SECRET_HEADER_PATTERNS.some((pattern) => pattern.test(field.NAME))
|
|
77
|
+
? { ...field, VALUE: '[redacted]' }
|
|
78
|
+
: field);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* How much of a body a log line carries before it is cut. An ADT payload is
|
|
82
|
+
* an ABAP source or a repository listing: whole ones are megabytes, and a log
|
|
83
|
+
* sink handed a single line that size is a problem of its own.
|
|
84
|
+
*/
|
|
85
|
+
const DEFAULT_MAX_LOGGED_BODY_CHARS = 2000;
|
|
86
|
+
function clip(text, max) {
|
|
87
|
+
return text.length <= max
|
|
88
|
+
? text
|
|
89
|
+
: `${text.slice(0, max)}… (+${text.length - max} more chars)`;
|
|
90
|
+
}
|
|
51
91
|
/** Axios's own default, which the classification above this seam is written against. */
|
|
52
92
|
const admits2xx = (status) => status >= 200 && status < 300;
|
|
53
93
|
class RfcTransport {
|
|
54
94
|
connect;
|
|
55
95
|
logger;
|
|
96
|
+
options;
|
|
56
97
|
kind = 'rfc';
|
|
57
98
|
/** Which system this wire is for. Read by the compiler, never at runtime. */
|
|
58
99
|
system = 'onprem';
|
|
@@ -118,9 +159,22 @@ class RfcTransport {
|
|
|
118
159
|
* that reached for it in its constructor could not be built at all on a
|
|
119
160
|
* machine without it.
|
|
120
161
|
*/
|
|
121
|
-
constructor(connect, logger = null) {
|
|
162
|
+
constructor(connect, logger = null, options = {}) {
|
|
122
163
|
this.connect = connect;
|
|
123
164
|
this.logger = logger;
|
|
165
|
+
this.options = options;
|
|
166
|
+
}
|
|
167
|
+
/** Cut a body down to what a log line may carry. */
|
|
168
|
+
clipped(text) {
|
|
169
|
+
const asked = this.options.maxLoggedBodyChars ?? DEFAULT_MAX_LOGGED_BODY_CHARS;
|
|
170
|
+
// A nonsense ceiling is a typo in a debug option, and a debug option is
|
|
171
|
+
// not worth failing a connection over — but it is worth not honouring. A
|
|
172
|
+
// negative one reaches `slice(0, -n)`, which drops the END of the body
|
|
173
|
+
// while the line still says the rest was merely clipped: a log that lies
|
|
174
|
+
// about what it cut is worse than one that cut too much. `NaN` compares
|
|
175
|
+
// false against every bound, so the test is for the good case.
|
|
176
|
+
const ceiling = asked >= 0 ? Math.floor(asked) : DEFAULT_MAX_LOGGED_BODY_CHARS;
|
|
177
|
+
return clip(text, ceiling);
|
|
124
178
|
}
|
|
125
179
|
async open() {
|
|
126
180
|
if (this.conversation?.alive)
|
|
@@ -187,6 +241,19 @@ class RfcTransport {
|
|
|
187
241
|
});
|
|
188
242
|
}
|
|
189
243
|
this.logger?.debug(`RFC → ${method} ${uri}`);
|
|
244
|
+
// `RFC → METHOD URI` alone was not enough to debug a body that goes
|
|
245
|
+
// missing or gets mis-serialised on the way to `SADT_REST_RFC_ENDPOINT`
|
|
246
|
+
// (found chasing a `superPackage` that disappeared before it reached
|
|
247
|
+
// SAP) — the actual bytes matter, so the debug channel carries them too
|
|
248
|
+
// when the caller asks for it, redacted and clipped so a captured log is
|
|
249
|
+
// safe to paste into an issue and small enough to want to.
|
|
250
|
+
if (this.logger && this.options.logWire) {
|
|
251
|
+
this.logger.debug(`RFC HEADERS: ${JSON.stringify(redactHeaders(headerFields))}`);
|
|
252
|
+
// A GET has no body, and `RFC BODY (0 chars):` says nothing.
|
|
253
|
+
if (body) {
|
|
254
|
+
this.logger.debug(`RFC BODY (${body.length} chars): ${this.clipped(body)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
190
257
|
// `request.timeout` is deliberately not read, and the absence of the word
|
|
191
258
|
// here is what made that look like an oversight (#42).
|
|
192
259
|
//
|
|
@@ -252,6 +319,11 @@ class RfcTransport {
|
|
|
252
319
|
statusText = statusText || 'OK';
|
|
253
320
|
}
|
|
254
321
|
this.logger?.debug(`RFC ← ${status} ${statusText} (${data.length} bytes)`);
|
|
322
|
+
// The line above already carries the size, so an empty body needs no line
|
|
323
|
+
// of its own.
|
|
324
|
+
if (this.logger && this.options.logWire && data) {
|
|
325
|
+
this.logger.debug(`RFC RESPONSE BODY: ${this.clipped(data)}`);
|
|
326
|
+
}
|
|
255
327
|
const response = {
|
|
256
328
|
status,
|
|
257
329
|
statusText,
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { HttpTransport } from './connection/HttpTransport.js';
|
|
|
13
13
|
export type { IAdtEstablishContext, IAdtSessionContext, IAdtTransport, IAdtTransportRequest, IAdtTransportResponse, ICloudTransport, IOnPremTransport, } from './connection/IAdtTransport.js';
|
|
14
14
|
export { LegacyOnPremHttpTransport } from './connection/LegacyOnPremHttpTransport.js';
|
|
15
15
|
export { OnPremHttpTransport } from './connection/OnPremHttpTransport.js';
|
|
16
|
-
export { type IRfcConversation, RfcTransport, } from './connection/RfcTransport.js';
|
|
16
|
+
export { type IRfcConversation, type IRfcTransportOptions, RfcTransport, } from './connection/RfcTransport.js';
|
|
17
17
|
export { type RfcConnectionParams, rfcConversationFrom, rfcParamsFrom, } from './connection/rfcConversation.js';
|
|
18
18
|
export type { ILogger } from './logger.js';
|
|
19
19
|
export { getTimeout, getTimeoutConfig, type TimeoutConfig, } from './utils/timeouts.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,6BAA6B,EAAE,MAAM,yCAAyC,CAAC;AAKxF,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,WAAW,EACX,SAAS,EACT,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAQxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,2CAA2C,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,GACjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EACL,KAAK,gBAAgB,EACrB,YAAY,GACb,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,aAAa,GACd,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,6BAA6B,EAAE,MAAM,yCAAyC,CAAC;AAKxF,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,WAAW,EACX,SAAS,EACT,iBAAiB,GAClB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,YAAY,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAQxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,2CAA2C,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,GACjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,YAAY,GACb,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,KAAK,mBAAmB,EACxB,mBAAmB,EACnB,aAAa,GACd,MAAM,iCAAiC,CAAC;AACzC,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
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-8.0.md # request headers leave the stateful branch; onto interfaces 39; flushGoodbye
|
|
18
19
|
│ ├── MIGRATION-6.0.md # the factory and the per-credential classes go; RFC is a transport
|
|
19
20
|
│ ├── MIGRATION-4.0.md # JWT error classification: 401 refreshes, 403 propagates
|
|
20
21
|
│ ├── SCOPE.md # What this package does and does not own
|
|
@@ -44,6 +45,7 @@ mcp-abap-connection/
|
|
|
44
45
|
- 🔑 [JWT Auth Tools](./JWT_AUTH_TOOLS.md) - CLI tool for browser-based authentication
|
|
45
46
|
|
|
46
47
|
### Upgrading
|
|
48
|
+
- 🚚 [Migrating to 7.0.0 and 8.0.0](./MIGRATION-8.0.md) - `sap-adt-request-id` and `X-sap-adt-profiling` on every request, `x-sap-security-session: use` on cloud, the contracts floor at 39, and `flushGoodbye()`
|
|
47
49
|
- 🚚 [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
|
|
48
50
|
- 🧱 [Migrating to 4.0.0](./MIGRATION-4.0.md) - JWT error classification: a 401 refreshes, a 403 propagates with the server's message
|
|
49
51
|
- 🧱 [Migrating to 2.0.0](./MIGRATION-2.0.md) - The explicit session lifecycle: `connect()` is required
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Migration to 7.0 and 8.0
|
|
2
|
+
|
|
3
|
+
Two majors, and **no code of yours has to change for either**. One alters what
|
|
4
|
+
goes on the wire; the other moves the contracts floor and makes methods you
|
|
5
|
+
already had reachable through the types you already hold.
|
|
6
|
+
|
|
7
|
+
If you build against `@mcp-abap-adt/interfaces` and never cast to a connector
|
|
8
|
+
class, there is nothing to do but install.
|
|
9
|
+
|
|
10
|
+
## 7.0 — headers that belong to the request stop belonging to the session
|
|
11
|
+
|
|
12
|
+
Three headers used to be written together, inside the stateful branch:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
if (this.sessionMode === 'stateful') {
|
|
16
|
+
requestHeaders['x-sap-adt-sessiontype'] = 'stateful';
|
|
17
|
+
requestHeaders['sap-adt-request-id'] = randomUUID().replace(/-/g, '');
|
|
18
|
+
requestHeaders['X-sap-adt-profiling'] = 'server-time';
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Only the first belongs there. A request id identifies the **request**, and
|
|
23
|
+
asking the server to report its own processing time is not a property of the
|
|
24
|
+
session either. Eclipse sends both on everything — measured on ADT 3.60.0, a
|
|
25
|
+
stateless source `PUT` carries them and no session type at all.
|
|
26
|
+
|
|
27
|
+
It stayed invisible while every write ran inside a lock window. Once
|
|
28
|
+
`@mcp-abap-adt/adt-clients` narrowed stateful to the `LOCK` and the `UNLOCK`,
|
|
29
|
+
the writes silently lost two headers: of 792 requests in a full run, 99 carried
|
|
30
|
+
a request id and **693 carried neither**.
|
|
31
|
+
|
|
32
|
+
### What changed for you
|
|
33
|
+
|
|
34
|
+
| | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `sap-adt-request-id` | now on every request, fresh each time |
|
|
37
|
+
| `X-sap-adt-profiling` | now on every request, from a settable default |
|
|
38
|
+
| `x-sap-security-session: use` | **cloud only**, on every request once a session exists |
|
|
39
|
+
| `x-sap-adt-sessiontype` | unchanged — still the only one that varies with the mode |
|
|
40
|
+
|
|
41
|
+
Nothing in the type surface moved. The major is for the wire: every request
|
|
42
|
+
looks different in an SAP trace, and a system that reacts badly to either should
|
|
43
|
+
be findable by version rather than by reading a dump.
|
|
44
|
+
|
|
45
|
+
### Your own headers win
|
|
46
|
+
|
|
47
|
+
Both new headers are **defaults**. A caller who names either in
|
|
48
|
+
`options.headers` keeps their value, matched without case:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await conn.makeAdtRequest({
|
|
52
|
+
url, method: 'GET', timeout: 30_000,
|
|
53
|
+
headers: { 'sap-adt-request-id': myCorrelationId }, // kept, not replaced
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That is not hypothetical: `adt-clients` passes its own id to
|
|
58
|
+
`getDiscovery({ requestId })` so the id it logs is the id on the wire.
|
|
59
|
+
|
|
60
|
+
### Turning the profiling off
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
conn.setProfilingRequest(null); // ask for nothing
|
|
64
|
+
conn.setProfilingRequest('server-time'); // the default, and what Eclipse asks for
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Nothing in this package reads the `server-time=…` that comes back. An
|
|
68
|
+
investigation does, though — the unit is **microseconds**, which a trial's
|
|
69
|
+
unpublish job settled by answering `server-time=132512547` on a request that
|
|
70
|
+
takes about 133 seconds.
|
|
71
|
+
|
|
72
|
+
## 8.0 — onto interfaces 39.0.0, and the atoms are declared
|
|
73
|
+
|
|
74
|
+
### The floor
|
|
75
|
+
|
|
76
|
+
`@mcp-abap-adt/interfaces` moves from `^21.0.0` to `^39.0.0`. Install it
|
|
77
|
+
alongside; a consumer pinned below 39 cannot have both.
|
|
78
|
+
|
|
79
|
+
Seventeen majors, and the whole migration inside this package was eight compiler
|
|
80
|
+
errors in one file — `makeAdtRequest` returning `IAdtWireResponse<T, D>` rather
|
|
81
|
+
than `IAdtResponse<T, D>`, and `isNetworkError` coming home because interfaces
|
|
82
|
+
stopped emitting code in 29.0.0.
|
|
83
|
+
|
|
84
|
+
**If you deduplicate nothing else, deduplicate this.** Two copies of the
|
|
85
|
+
contracts in one graph are structurally identical and do not compare equal, so
|
|
86
|
+
they produce errors that read as impossible.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
npm ls @mcp-abap-adt/interfaces # should print one version, deduped
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `flushGoodbye()` — the half of `disconnect()` that was missing
|
|
93
|
+
|
|
94
|
+
`disconnect()` dispatches the logoff and does not await it, on purpose: a
|
|
95
|
+
goodbye carries no request timeout, and a server that never answers must not
|
|
96
|
+
hold a teardown open.
|
|
97
|
+
|
|
98
|
+
That is right for a teardown and wrong for a **reconnect**:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await conn.disconnect();
|
|
102
|
+
await conn.flushGoodbye(); // give the goodbye its budget to finish first
|
|
103
|
+
await conn.connect();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Without the middle line, the next session opens while the previous one's goodbye
|
|
107
|
+
is still being assembled and the server keeps both. Measured on E19 through a
|
|
108
|
+
test harness that recycled the session after each test: **a new ABAP session
|
|
109
|
+
every one to two seconds for a whole run, none released**, each living to its
|
|
110
|
+
own thirty-minute idle timeout.
|
|
111
|
+
|
|
112
|
+
**The budget bounds the waiting, not the overlap.** If the goodbye finishes in
|
|
113
|
+
time there is no overlap; if it does not, you proceed and it stays outstanding
|
|
114
|
+
for as long as it takes. A return is not a confirmation and not even of
|
|
115
|
+
dispatch.
|
|
116
|
+
|
|
117
|
+
Calling `disconnect()` twice is **not** a substitute — a repeat call does not
|
|
118
|
+
wait either.
|
|
119
|
+
|
|
120
|
+
### Reaching the controls through the contract
|
|
121
|
+
|
|
122
|
+
`beginCriticalSection()`, `setProfilingRequest()` and `flushGoodbye()` all
|
|
123
|
+
existed before. What changed is that a consumer can reach them without a cast:
|
|
124
|
+
`AbapConnection` is `IAbapConnection`, and these now live on capability atoms
|
|
125
|
+
that this connection declares.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import type {
|
|
129
|
+
IAbapConnection,
|
|
130
|
+
ICriticalSection,
|
|
131
|
+
IRequestProfiling,
|
|
132
|
+
ISessionLifecycleAware,
|
|
133
|
+
} from '@mcp-abap-adt/interfaces';
|
|
134
|
+
|
|
135
|
+
function protectTheWindow(conn: IAbapConnection & ICriticalSection) {
|
|
136
|
+
conn.beginCriticalSection();
|
|
137
|
+
try {
|
|
138
|
+
// the ordinary per-request deadline does not apply in here
|
|
139
|
+
} finally {
|
|
140
|
+
conn.endCriticalSection();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`flushGoodbye` is a member of `ISessionLifecycleAware`, not an atom of its own:
|
|
146
|
+
waiting for the goodbye is not a separate capability from sending it.
|
|
147
|
+
|
|
148
|
+
### One thing to know about `implements`
|
|
149
|
+
|
|
150
|
+
TypeScript is structural. Removing an atom from a class's `implements` list
|
|
151
|
+
changes nothing for a consumer — the class still has the methods and narrowing
|
|
152
|
+
still succeeds. What the clause buys is the compiler checking the class *against*
|
|
153
|
+
the contract in the other direction: remove `beginCriticalSection` itself and it
|
|
154
|
+
is `TS2420`.
|
|
155
|
+
|
|
156
|
+
Worth knowing before writing a guard that tests the clause rather than the
|
|
157
|
+
method.
|
|
158
|
+
|
|
159
|
+
### `IRenewableCredential` is an atom now
|
|
160
|
+
|
|
161
|
+
Not this package's change, but it lands with the floor. Renewing is something a
|
|
162
|
+
credential can also do, not a kind of credential, so a guard that narrowed to
|
|
163
|
+
`IRenewableCredential` alone now hands the caller something that renews and
|
|
164
|
+
cannot authenticate:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// before
|
|
168
|
+
function isRenewable(c: IAuthProvider): c is IRenewableCredential
|
|
169
|
+
// after
|
|
170
|
+
function isRenewable(c: IAuthProvider): c is IAuthProvider & IRenewableCredential
|
|
171
|
+
```
|
|
@@ -120,6 +120,22 @@ authentication answer. A 403 never did this: it is an authorization answer, not
|
|
|
120
120
|
If you decide the refusal meant a stale token, `renew()` and reconnect are yours to call — and a
|
|
121
121
|
reconnect is a NEW session, so do it outside a lock window rather than inside one.
|
|
122
122
|
|
|
123
|
+
**Wait for the goodbye before opening the next one.** `disconnect()` dispatches the logoff and does
|
|
124
|
+
not await it, so a reconnect otherwise opens the next session while the previous one's goodbye is
|
|
125
|
+
still being assembled, and the server keeps both:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
await conn.disconnect();
|
|
129
|
+
await conn.flushGoodbye(); // give the goodbye its budget to finish first
|
|
130
|
+
await conn.connect();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Measured on E19 through a harness that recycled the session after each test: a new ABAP session
|
|
134
|
+
every one to two seconds for a whole run, none released, each living to its own thirty-minute idle
|
|
135
|
+
timeout. The budget bounds the waiting, not the overlap — if the goodbye finishes in time there is
|
|
136
|
+
none, and if it does not you proceed while it stays outstanding. Calling `disconnect()` again is not
|
|
137
|
+
a substitute: a repeat call does not wait either.
|
|
138
|
+
|
|
123
139
|
---
|
|
124
140
|
|
|
125
141
|
## Interaction With ADT Clients
|
|
@@ -189,6 +205,13 @@ lock is still very much held. That — not any teardown — is why
|
|
|
189
205
|
`beginCriticalSection()` raises the effective timeout to a large ceiling for the
|
|
190
206
|
duration of a `lock → modify → unlock` chain.
|
|
191
207
|
|
|
208
|
+
Since 8.0.0 you reach it through the contract rather than the class: it is
|
|
209
|
+
`ICriticalSection` in `@mcp-abap-adt/interfaces`, which this connection declares.
|
|
210
|
+
What it promises is narrow and worth stating exactly — inside a section the
|
|
211
|
+
*ordinary* per-request deadline does not apply. Not that no request can be cut
|
|
212
|
+
short: the ceiling is `SAP_TIMEOUT_CRITICAL`, ten minutes by default, and a
|
|
213
|
+
socket ends a request whatever a contract says.
|
|
214
|
+
|
|
192
215
|
### Two sessions, and they are not the same thing
|
|
193
216
|
|
|
194
217
|
There are two sessions here, and they are not the same thing:
|
package/docs/USAGE.md
CHANGED
|
@@ -794,7 +794,7 @@ establishing, and whatever session state it keeps.
|
|
|
794
794
|
|
|
795
795
|
```text
|
|
796
796
|
new HttpTransport(agentOptions?, logger?, { client?, baseUrl? })
|
|
797
|
-
new RfcTransport(connect: () => IRfcConversation, logger?)
|
|
797
|
+
new RfcTransport(connect: () => IRfcConversation, logger?, { logWire?, maxLoggedBodyChars? })
|
|
798
798
|
```
|
|
799
799
|
|
|
800
800
|
`HttpTransport` is the ordinary wire, and you name it because the connector
|
|
@@ -804,6 +804,26 @@ what the connectors' type parameters admit. `RfcTransport`
|
|
|
804
804
|
you build with `rfcConversationFrom(config)`, which derives `ashost` and `sysnr`
|
|
805
805
|
and loads the SAP NW RFC SDK only when a conversation opens.
|
|
806
806
|
|
|
807
|
+
**`logWire` dumps the wire, and is off.** With it on, `RfcTransport` adds three
|
|
808
|
+
debug lines per request — the header fields, the request body and the response
|
|
809
|
+
body — which is what tells you a payload was mis-serialised before it reached
|
|
810
|
+
`SADT_REST_RFC_ENDPOINT`. Credential header values (`Authorization`, any
|
|
811
|
+
`Cookie`, anything matching `token`, `secret`, `password`, `credential` or an
|
|
812
|
+
API key) are replaced with `[redacted]`, the names are kept, and a body is cut
|
|
813
|
+
at `maxLoggedBodyChars` (2000 by default) so a class source does not arrive as
|
|
814
|
+
one multi-megabyte line — `0` logs the size alone, `Infinity` asks for the
|
|
815
|
+
whole body, and a negative or `NaN` ceiling falls back to the default instead
|
|
816
|
+
of throwing. Read what you captured before pasting it anywhere: a
|
|
817
|
+
body is not redacted, only clipped.
|
|
818
|
+
|
|
819
|
+
It is a flag and not something inferred from the logger, because `ILogger` has
|
|
820
|
+
no level predicate — `logger?.debug()` cannot tell an enabled debug channel
|
|
821
|
+
from one that discards. Without the flag, every caller who passes a logger at
|
|
822
|
+
all would pay to build a copy of every body on the wire and throw it away.
|
|
823
|
+
`HttpTransport` logs no bodies at any setting, so "turn debug on" means
|
|
824
|
+
different things on the two wires; this is the only one that can be asked for
|
|
825
|
+
the payload.
|
|
826
|
+
|
|
807
827
|
The two differ in what they have, not in what they are asked:
|
|
808
828
|
|
|
809
829
|
| | HTTP | RFC |
|