@mcp-abap-adt/connection 5.0.0 → 6.0.1
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 +243 -1
- package/README.md +204 -48
- package/bin/sap-abap-auth.js +101 -13
- 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 +270 -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 -11
- 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
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAbapConnection = createAbapConnection;
|
|
4
|
-
const providers_js_1 = require("../auth/providers.js");
|
|
5
|
-
const AdtCloudConnector_js_1 = require("./AdtCloudConnector.js");
|
|
6
|
-
const AdtOnPremConnector_js_1 = require("./AdtOnPremConnector.js");
|
|
7
|
-
const BaseAbapConnection_js_1 = require("./BaseAbapConnection.js");
|
|
8
|
-
const CertificateAbapConnection_js_1 = require("./CertificateAbapConnection.js");
|
|
9
|
-
const JwtAbapConnection_js_1 = require("./JwtAbapConnection.js");
|
|
10
|
-
const KerberosAbapConnection_js_1 = require("./KerberosAbapConnection.js");
|
|
11
|
-
const RfcAbapConnection_js_1 = require("./RfcAbapConnection.js");
|
|
12
|
-
const SamlAbapConnection_js_1 = require("./SamlAbapConnection.js");
|
|
13
|
-
/**
|
|
14
|
-
* The provider for the credential this config carries.
|
|
15
|
-
*
|
|
16
|
-
* Reading `authType` here is not the thing the split removed: it says HOW to
|
|
17
|
-
* authenticate, which is what it always meant. What it no longer does is decide
|
|
18
|
-
* which system this is — the caller said that.
|
|
19
|
-
*/
|
|
20
|
-
function authProviderFor(config, options, tokenRefresher) {
|
|
21
|
-
switch (config.authType) {
|
|
22
|
-
case 'jwt':
|
|
23
|
-
// No longer refused. It was, while renewal lived only in
|
|
24
|
-
// JwtAbapConnection and this path would have handed back a connection
|
|
25
|
-
// that died on the first expiry. The connector now tells the provider
|
|
26
|
-
// when a token was rejected and rebuilds the session behind it, so a
|
|
27
|
-
// refresher passed here is used for what it is for.
|
|
28
|
-
return new providers_js_1.TokenAuthProvider(tokenRefresher ?? config.jwtToken ?? '');
|
|
29
|
-
case 'saml':
|
|
30
|
-
return new providers_js_1.SamlAuthProvider(config.sessionCookies ?? '');
|
|
31
|
-
case 'certificate':
|
|
32
|
-
if (!options.certLoader) {
|
|
33
|
-
throw new Error('authType "certificate" needs options.certLoader to load the material.');
|
|
34
|
-
}
|
|
35
|
-
return new providers_js_1.CertificateAuthProvider(options.certLoader, config);
|
|
36
|
-
case 'basic':
|
|
37
|
-
return new providers_js_1.BasicAuthProvider(config.username ?? '', config.password ?? '');
|
|
38
|
-
default:
|
|
39
|
-
throw new Error(`authType "${config.authType}" has no auth provider yet; use the deprecated class for it.`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Builds the connection the caller asked for.
|
|
44
|
-
*
|
|
45
|
-
* **Say which system it is** with `options.system`, and that is what you get —
|
|
46
|
-
* `'onprem'` or `'cloud'` — with the credential built from the config and
|
|
47
|
-
* handed in as an auth provider. Nothing about the system is worked out here:
|
|
48
|
-
* not from the credential, not from the host name, not by asking the server.
|
|
49
|
-
*
|
|
50
|
-
* Without it, the old behaviour: a connection chosen from `authType`, which is
|
|
51
|
-
* the arrangement where the credential drags the session mechanism along with
|
|
52
|
-
* it and gets one of them wrong. **Deprecated**, kept so existing callers keep
|
|
53
|
-
* working, and warned about once per call so it is visible rather than
|
|
54
|
-
* inherited by accident.
|
|
55
|
-
*/
|
|
56
|
-
function createAbapConnection(config, logger, sessionId, tokenRefresher, options) {
|
|
57
|
-
// RFC connection type takes priority over auth type
|
|
58
|
-
if (config.connectionType === 'rfc') {
|
|
59
|
-
if (config.authType === 'certificate' || config.authType === 'kerberos') {
|
|
60
|
-
throw new Error(`authType "${config.authType}" is not supported with connectionType "rfc".`);
|
|
61
|
-
}
|
|
62
|
-
return new RfcAbapConnection_js_1.RfcAbapConnection(config, logger);
|
|
63
|
-
}
|
|
64
|
-
if (options?.system) {
|
|
65
|
-
const provider = authProviderFor(config, options, tokenRefresher);
|
|
66
|
-
const Connector = options.system === 'cloud' ? AdtCloudConnector_js_1.AdtCloudConnector : AdtOnPremConnector_js_1.AdtOnPremConnector;
|
|
67
|
-
return new Connector(config, provider, logger ?? null, sessionId, options);
|
|
68
|
-
}
|
|
69
|
-
logger?.warn("createAbapConnection(): no options.system given, so the connection is chosen from authType — which lets the credential decide the session mechanism. Pass system: 'onprem' | 'cloud'.");
|
|
70
|
-
switch (config.authType) {
|
|
71
|
-
case 'basic':
|
|
72
|
-
return new BaseAbapConnection_js_1.BaseAbapConnection(config, logger, sessionId, options);
|
|
73
|
-
case 'jwt':
|
|
74
|
-
return new JwtAbapConnection_js_1.JwtAbapConnection(config, logger, sessionId, tokenRefresher);
|
|
75
|
-
case 'saml':
|
|
76
|
-
return new SamlAbapConnection_js_1.SamlAbapConnection(config, logger, sessionId, options);
|
|
77
|
-
case 'certificate':
|
|
78
|
-
return new CertificateAbapConnection_js_1.CertificateAbapConnection(config, logger, sessionId, options?.certLoader);
|
|
79
|
-
case 'kerberos':
|
|
80
|
-
return new KerberosAbapConnection_js_1.KerberosAbapConnection(config, logger, sessionId);
|
|
81
|
-
default:
|
|
82
|
-
throw new Error(`Unsupported SAP authentication type: ${config.authType}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ABAP Cloud: a session is a resource, asked for and given back by address.
|
|
3
|
-
*
|
|
4
|
-
* Captured from Eclipse ADT 3.60 against a BTP trial and reproduced here with a
|
|
5
|
-
* bearer token, which is what made the difference visible: **the server issues
|
|
6
|
-
* `SAP_SESSIONID` to whoever asks for a session, and we never asked.** The
|
|
7
|
-
* request that asks carries `x-sap-security-session: create` — `use` does not
|
|
8
|
-
* do it — and `sap-adt-purpose: preflight_logon`. The response sets the cookie
|
|
9
|
-
* and publishes the session's own address in a `securitysession` link, which is
|
|
10
|
-
* the address a close goes to.
|
|
11
|
-
*
|
|
12
|
-
* Without that request the same connection gets `sap-usercontext` and
|
|
13
|
-
* `sap-XSRF_*` and nothing else, no `securitysession` link, and no session for
|
|
14
|
-
* a lock to be bound to — which is how "cloud does not issue SAP_SESSIONID"
|
|
15
|
-
* came to be believed.
|
|
16
|
-
*/
|
|
17
|
-
import { type ISessionTransport, SessionStrategy } from './SessionStrategy.js';
|
|
18
|
-
export declare class CloudSecuritySessionStrategy extends SessionStrategy {
|
|
19
|
-
readonly kind: "cloud-security-session";
|
|
20
|
-
/** The address this server published for our session — the only close target. */
|
|
21
|
-
private resource;
|
|
22
|
-
openSession(transport: ISessionTransport): Promise<boolean>;
|
|
23
|
-
closeSession(transport: ISessionTransport): Promise<void>;
|
|
24
|
-
/**
|
|
25
|
-
* The `securitysession` link's href, by its `rel` rather than by position.
|
|
26
|
-
*
|
|
27
|
-
* The document carries three links — the session, the logoff resource and
|
|
28
|
-
* system information — and their order is not a contract.
|
|
29
|
-
*/
|
|
30
|
-
private securitySessionHref;
|
|
31
|
-
}
|
|
32
|
-
//# sourceMappingURL=CloudSecuritySessionStrategy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CloudSecuritySessionStrategy.d.ts","sourceRoot":"","sources":["../../src/session/CloudSecuritySessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,KAAK,iBAAiB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAa/E,qBAAa,4BAA6B,SAAQ,eAAe;IAC/D,QAAQ,CAAC,IAAI,EAAG,wBAAwB,CAAU;IAElD,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAuB;IAEjC,WAAW,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IA6D3D,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC/D;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAW5B"}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* On-prem: the logon is the establishing request, and the platform takes the
|
|
3
|
-
* session back through `/sap/public/bc/icf/logoff`.
|
|
4
|
-
*
|
|
5
|
-
* There is nothing to open. The session arrives as a `SAP_SESSIONID` cookie on
|
|
6
|
-
* the establishing call, which is why on-prem SM04 shows those sessions opened
|
|
7
|
-
* by `P=/sap/bc/adt/discovery` — one per `connect()`, and one left behind per
|
|
8
|
-
* `connect()` that never said it was finished.
|
|
9
|
-
*
|
|
10
|
-
* ADT publishes no session resource here — `/sap/bc/adt/core/http/sessions`
|
|
11
|
-
* does not exist — so this is the only mechanism, and it is the platform's
|
|
12
|
-
* rather than ADT's.
|
|
13
|
-
*/
|
|
14
|
-
import { type ISessionTransport, SessionStrategy } from './SessionStrategy.js';
|
|
15
|
-
export declare class IcfSessionStrategy extends SessionStrategy {
|
|
16
|
-
readonly kind: "icf";
|
|
17
|
-
/**
|
|
18
|
-
* Nothing to ask for: the session comes with the establishing request.
|
|
19
|
-
*
|
|
20
|
-
* `false` is about the session RESOURCE, not about sessions — this system has
|
|
21
|
-
* no address to open or close one by, which is exactly why the close below is
|
|
22
|
-
* the platform's logoff.
|
|
23
|
-
*/
|
|
24
|
-
openSession(_transport: ISessionTransport): Promise<boolean>;
|
|
25
|
-
closeSession(transport: ISessionTransport): Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
//# sourceMappingURL=IcfSessionStrategy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"IcfSessionStrategy.d.ts","sourceRoot":"","sources":["../../src/session/IcfSessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,KAAK,iBAAiB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI/E,qBAAa,kBAAmB,SAAQ,eAAe;IACrD,QAAQ,CAAC,IAAI,EAAG,KAAK,CAAU;IAE/B;;;;;;OAMG;IACG,WAAW,CAAC,UAAU,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI5D,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CA+BhE"}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* On-prem: the logon is the establishing request, and the platform takes the
|
|
4
|
-
* session back through `/sap/public/bc/icf/logoff`.
|
|
5
|
-
*
|
|
6
|
-
* There is nothing to open. The session arrives as a `SAP_SESSIONID` cookie on
|
|
7
|
-
* the establishing call, which is why on-prem SM04 shows those sessions opened
|
|
8
|
-
* by `P=/sap/bc/adt/discovery` — one per `connect()`, and one left behind per
|
|
9
|
-
* `connect()` that never said it was finished.
|
|
10
|
-
*
|
|
11
|
-
* ADT publishes no session resource here — `/sap/bc/adt/core/http/sessions`
|
|
12
|
-
* does not exist — so this is the only mechanism, and it is the platform's
|
|
13
|
-
* rather than ADT's.
|
|
14
|
-
*/
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.IcfSessionStrategy = void 0;
|
|
17
|
-
const cookies_js_1 = require("../utils/cookies.js");
|
|
18
|
-
const SessionStrategy_js_1 = require("./SessionStrategy.js");
|
|
19
|
-
const ICF_LOGOFF_PATH = '/sap/public/bc/icf/logoff';
|
|
20
|
-
class IcfSessionStrategy extends SessionStrategy_js_1.SessionStrategy {
|
|
21
|
-
kind = 'icf';
|
|
22
|
-
/**
|
|
23
|
-
* Nothing to ask for: the session comes with the establishing request.
|
|
24
|
-
*
|
|
25
|
-
* `false` is about the session RESOURCE, not about sessions — this system has
|
|
26
|
-
* no address to open or close one by, which is exactly why the close below is
|
|
27
|
-
* the platform's logoff.
|
|
28
|
-
*/
|
|
29
|
-
async openSession(_transport) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
async closeSession(transport) {
|
|
33
|
-
const cookies = transport.cookies();
|
|
34
|
-
if (!cookies) {
|
|
35
|
-
// Holding the cookie is the whole permission to end a session, and it is
|
|
36
|
-
// why nothing is sent without one. The session limit is per user and the
|
|
37
|
-
// pool is shared — a SAP GUI logon of the same user sits in the same SM04
|
|
38
|
-
// list — so a connector that tidied up sessions it did not open would
|
|
39
|
-
// eventually close somebody's GUI.
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
// Read once: see CloudSecuritySessionStrategy — a provider may answer
|
|
44
|
-
// differently on a second call, and one request must be built from one
|
|
45
|
-
// credential.
|
|
46
|
-
const auth = await transport.authHeaders();
|
|
47
|
-
await transport.send({
|
|
48
|
-
method: 'GET',
|
|
49
|
-
url: `${transport.baseUrl}${ICF_LOGOFF_PATH}`,
|
|
50
|
-
headers: {
|
|
51
|
-
...auth,
|
|
52
|
-
Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, cookies),
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
this.logger?.debug('Told the server the session is finished');
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
this.logger?.debug(`Could not tell the server the session is finished: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
exports.IcfSessionStrategy = IcfSessionStrategy;
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How a session is opened on the server, and how the server is told we are
|
|
3
|
-
* done with it.
|
|
4
|
-
*
|
|
5
|
-
* Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
|
|
6
|
-
* resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
|
|
7
|
-
* response publishes its own address, which is what a `DELETE` is sent to. On
|
|
8
|
-
* on-prem there is no such resource — the logon call is the establishing
|
|
9
|
-
* request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
|
|
10
|
-
* given back.
|
|
11
|
-
*
|
|
12
|
-
* **Both are notifications, not commands.** They say "we have finished with
|
|
13
|
-
* this session". Whether the system frees it now, later, or keeps it to reuse
|
|
14
|
-
* is the system's business, and nothing here checks afterwards or depends on
|
|
15
|
-
* the answer. Measured on a trial: the `DELETE` is answered `200` and the
|
|
16
|
-
* session is still listed a moment later; the logoff is answered `200` and it
|
|
17
|
-
* is not. Neither is a failure — they are two systems deciding differently
|
|
18
|
-
* about a message they both accepted.
|
|
19
|
-
*
|
|
20
|
-
* Split by what the server publishes rather than by which system we think we
|
|
21
|
-
* are talking to: `openSession()` asks, and a system that has no such resource
|
|
22
|
-
* says so. Guessing the platform would be a guess; asking is not.
|
|
23
|
-
*/
|
|
24
|
-
import type { ILogger } from '../logger.js';
|
|
25
|
-
/** What a strategy needs from the connection to talk to the server. */
|
|
26
|
-
export interface ISessionTransport {
|
|
27
|
-
/** Absolute base URL of the system. */
|
|
28
|
-
baseUrl: string;
|
|
29
|
-
/** Auth headers for a request made outside the normal request path. */
|
|
30
|
-
authHeaders(): Promise<Record<string, string>>;
|
|
31
|
-
/** Cookies held for this connection, or null before any have arrived. */
|
|
32
|
-
cookies(): string | null;
|
|
33
|
-
/** The CSRF token held, if one has been fetched. */
|
|
34
|
-
csrfToken(): string | null;
|
|
35
|
-
/** Issue a raw request and hand back status, headers and body. */
|
|
36
|
-
send(request: {
|
|
37
|
-
method: 'GET' | 'DELETE';
|
|
38
|
-
url: string;
|
|
39
|
-
headers: Record<string, string>;
|
|
40
|
-
/**
|
|
41
|
-
* A deadline for the REQUEST, and only where one belongs.
|
|
42
|
-
*
|
|
43
|
-
* The open is a preflight that `connect()` waits for, so it is bounded.
|
|
44
|
-
* The close must not be: a request timeout aborts the socket, which would
|
|
45
|
-
* cancel the very message it was waiting for and leave the session open —
|
|
46
|
-
* the caller's own deadline bounds the WAIT instead, elsewhere.
|
|
47
|
-
*/
|
|
48
|
-
timeoutMs?: number;
|
|
49
|
-
/**
|
|
50
|
-
* Whether the cookies this answers with become the connection's.
|
|
51
|
-
*
|
|
52
|
-
* True for the open — its `SAP_SESSIONID` **is** the session. False for a
|
|
53
|
-
* close: a teardown's response must not feed the session identity, or the
|
|
54
|
-
* cookies it sets read as the session having been replaced under a
|
|
55
|
-
* connection that is on its way out.
|
|
56
|
-
*/
|
|
57
|
-
adoptCookies?: boolean;
|
|
58
|
-
}): Promise<{
|
|
59
|
-
status: number;
|
|
60
|
-
headers: unknown;
|
|
61
|
-
data: unknown;
|
|
62
|
-
}>;
|
|
63
|
-
}
|
|
64
|
-
export declare abstract class SessionStrategy {
|
|
65
|
-
protected readonly logger: ILogger | null;
|
|
66
|
-
constructor(logger: ILogger | null);
|
|
67
|
-
/** A name for logs, so which one ran is never a guess. */
|
|
68
|
-
abstract readonly kind: 'cloud-security-session' | 'icf';
|
|
69
|
-
/**
|
|
70
|
-
* Ask the server to open a session, before anything needs one.
|
|
71
|
-
*
|
|
72
|
-
* Returns whether a session resource was actually opened and its address
|
|
73
|
-
* captured — `false` is not a failure, it is a system saying it has none, and
|
|
74
|
-
* the close then falls to the mechanism that needs no address. Never throws: a system that
|
|
75
|
-
* will not open one is a fact for the establishment that follows to report,
|
|
76
|
-
* with the whole picture, not a transport error raised from a preflight.
|
|
77
|
-
*/
|
|
78
|
-
abstract openSession(transport: ISessionTransport): Promise<boolean>;
|
|
79
|
-
/**
|
|
80
|
-
* Tell the server we are done with the session.
|
|
81
|
-
*
|
|
82
|
-
* Never throws, and says nothing about what the server then did with it.
|
|
83
|
-
*/
|
|
84
|
-
abstract closeSession(transport: ISessionTransport): Promise<void>;
|
|
85
|
-
}
|
|
86
|
-
//# sourceMappingURL=SessionStrategy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SessionStrategy.d.ts","sourceRoot":"","sources":["../../src/session/SessionStrategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,yEAAyE;IACzE,OAAO,IAAI,MAAM,GAAG,IAAI,CAAC;IACzB,oDAAoD;IACpD,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,EAAE,KAAK,GAAG,QAAQ,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC;;;;;;;WAOG;QACH,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB;;;;;;;WAOG;QACH,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAClE;AAED,8BAAsB,eAAe;IACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;gBAAtB,MAAM,EAAE,OAAO,GAAG,IAAI;IAErD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,GAAG,KAAK,CAAC;IAEzD;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAEpE;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,SAAS,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CACnE"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* How a session is opened on the server, and how the server is told we are
|
|
4
|
-
* done with it.
|
|
5
|
-
*
|
|
6
|
-
* Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
|
|
7
|
-
* resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
|
|
8
|
-
* response publishes its own address, which is what a `DELETE` is sent to. On
|
|
9
|
-
* on-prem there is no such resource — the logon call is the establishing
|
|
10
|
-
* request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
|
|
11
|
-
* given back.
|
|
12
|
-
*
|
|
13
|
-
* **Both are notifications, not commands.** They say "we have finished with
|
|
14
|
-
* this session". Whether the system frees it now, later, or keeps it to reuse
|
|
15
|
-
* is the system's business, and nothing here checks afterwards or depends on
|
|
16
|
-
* the answer. Measured on a trial: the `DELETE` is answered `200` and the
|
|
17
|
-
* session is still listed a moment later; the logoff is answered `200` and it
|
|
18
|
-
* is not. Neither is a failure — they are two systems deciding differently
|
|
19
|
-
* about a message they both accepted.
|
|
20
|
-
*
|
|
21
|
-
* Split by what the server publishes rather than by which system we think we
|
|
22
|
-
* are talking to: `openSession()` asks, and a system that has no such resource
|
|
23
|
-
* says so. Guessing the platform would be a guess; asking is not.
|
|
24
|
-
*/
|
|
25
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.SessionStrategy = void 0;
|
|
27
|
-
class SessionStrategy {
|
|
28
|
-
logger;
|
|
29
|
-
constructor(logger) {
|
|
30
|
-
this.logger = logger;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
exports.SessionStrategy = SessionStrategy;
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# Two connectors, one credential contract
|
|
2
|
-
|
|
3
|
-
**Status:** draft, for review
|
|
4
|
-
**Subject:** `AdtOnPremConnector` / `AdtCloudConnector` over a shared base, with authentication passed in rather than inherited.
|
|
5
|
-
|
|
6
|
-
## Why
|
|
7
|
-
|
|
8
|
-
The class you take today states your **credential**, and the session mechanism rides along with it. `JwtAbapConnection` is the only class that overrides `createSessionStrategy()`, so:
|
|
9
|
-
|
|
10
|
-
| the consumer builds | on a **cloud** host | on an **on-prem** host |
|
|
11
|
-
|---|---|---|
|
|
12
|
-
| `authType: 'jwt'` | ADT session resource — correct | ADT session resource — wrong |
|
|
13
|
-
| `authType: 'basic'` | ICF logoff — wrong | ICF logoff — correct |
|
|
14
|
-
| `authType: 'saml'` | ICF logoff — wrong | correct |
|
|
15
|
-
| `authType: 'certificate'`, `'kerberos'` | ICF logoff — wrong | correct |
|
|
16
|
-
|
|
17
|
-
Both wrong rows are reachable. The cloud trial answers `www-authenticate: Basic`, so a communication user against ABAP Cloud is an ordinary setup; SAML is the usual interactive logon on BTP. And the mirror — JWT against an on-prem system — puts that system on a mechanism it does not use.
|
|
18
|
-
|
|
19
|
-
**The consumer knows which system it is dialling.** It should say so by taking the connector for it, and the credential should be a parameter of that choice rather than the thing making it.
|
|
20
|
-
|
|
21
|
-
### Why not detect it
|
|
22
|
-
|
|
23
|
-
Tried, measured, wrong. `/sap/bc/adt/core/http/sessions` was probed to decide the mechanism; **on-prem answers it too** — 200, a `SAP_SESSIONID`, and a document publishing *both* the session resource and `/sap/public/bc/icf/logoff`. The probe never told the two systems apart; it told whether an endpoint exists, and both have it, so on-prem was silently moved onto the cloud mechanism.
|
|
24
|
-
|
|
25
|
-
Nothing here goes back to asking the server which kind of system it is.
|
|
26
|
-
|
|
27
|
-
## The shape
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
AbstractAbapConnection requests, CSRF, cookies, session lifecycle, admission
|
|
31
|
-
│
|
|
32
|
-
├── AdtOnPremConnector session arrives with the establishing call;
|
|
33
|
-
│ given back with the platform's ICF logoff
|
|
34
|
-
│
|
|
35
|
-
└── AdtCloudConnector session opened at /sap/bc/adt/core/http/sessions
|
|
36
|
-
(`x-sap-security-session: create`,
|
|
37
|
-
`sap-adt-purpose: preflight_logon`);
|
|
38
|
-
given back by DELETE on the address it published
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Both take a **credential** object. The two session strategies already exist and do not change; what changes is who chooses them and how authentication arrives.
|
|
42
|
-
|
|
43
|
-
## The credential contract
|
|
44
|
-
|
|
45
|
-
`ITokenRefresher` and `ITokenProvider` already exist in `@mcp-abap-adt/interfaces` — the first says outright *"Created by AuthBroker for a specific destination. Injected into JwtAbapConnection to enable automatic token refresh."* — and `@mcp-abap-adt/auth-providers` ships twelve implementations. That is the seam to join, not to reinvent.
|
|
46
|
-
|
|
47
|
-
But both are about **tokens**, and only one of the five ways in works that way. Measured, every current class overrides exactly two members, plus a few extras:
|
|
48
|
-
|
|
49
|
-
| | `buildAuthorizationHeader` | `establishSession` | other |
|
|
50
|
-
|---|---|---|---|
|
|
51
|
-
| basic | ✓ | ✓ | — |
|
|
52
|
-
| jwt | ✓ | ✓ | `prepareCredential`, `ensureToken` |
|
|
53
|
-
| saml | ✓ | ✓ | — |
|
|
54
|
-
| certificate | ✓ | ✓ | `getHttpsAgentOptions`, `ensureMaterial` |
|
|
55
|
-
| kerberos | ✓ | ✓ | `fetchCsrfToken` |
|
|
56
|
-
|
|
57
|
-
So the contract is small and is **not** "give me a token":
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
interface IAbapCredential {
|
|
61
|
-
/** Prepare before anything is sent — mint, load, unlock. Optional. */
|
|
62
|
-
prepare?(): Promise<void>;
|
|
63
|
-
/** The Authorization header value, or '' when the credential is not a header. */
|
|
64
|
-
authorizationHeader(): string;
|
|
65
|
-
/** TLS material, for credentials that are a certificate rather than a header. */
|
|
66
|
-
httpsAgentOptions?(): AgentOptions;
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
A token provider becomes one implementation of it, wrapping `ITokenRefresher`. Basic wraps a username and password. Certificate returns no header and supplies agent options instead.
|
|
71
|
-
|
|
72
|
-
**Where it lives:** `@mcp-abap-adt/interfaces`, released before the connector consumes it — the standing rule, no local bridge.
|
|
73
|
-
|
|
74
|
-
## The hard part, named
|
|
75
|
-
|
|
76
|
-
`JwtAbapConnection` is 483 lines and **most of it is not authentication.** `renewalInFlight`, the `AsyncLocalStorage` operation scopes, `tokenGeneration` against `recoveredGeneration`, `ensureRecovered`, and the calls to `discardSession()` and `recoverSession()` are about what happens to *the session* when the credential is renewed mid-flight. That is connection business and stays in the base.
|
|
77
|
-
|
|
78
|
-
The split to make:
|
|
79
|
-
|
|
80
|
-
- **credential**: "get me a valid token" → the provider, behind `IAbapCredential`;
|
|
81
|
-
- **connection**: "the credential changed, so the session it built is gone — discard, re-establish, let the request retry once" → the base, unchanged in behaviour.
|
|
82
|
-
|
|
83
|
-
If this line is drawn wrongly the 4.0.0 work is undone, and that work exists because the failures it fixed were subtle: two layered single-flight promises, identity-checked clears, an epoch checked before a retry. **The spec's success condition is that every test from 4.0.0 passes untouched.**
|
|
84
|
-
|
|
85
|
-
## Migration
|
|
86
|
-
|
|
87
|
-
The five current classes stay, deprecated, as thin wrappers: `new JwtAbapConnection(config, logger, sessionId, refresher)` builds `AdtCloudConnector` with a token credential. Nothing published breaks, and `createAbapConnection()` keeps working from `SapConfig` alone.
|
|
88
|
-
|
|
89
|
-
New code takes a connector and hands it a credential.
|
|
90
|
-
|
|
91
|
-
## What this does not do
|
|
92
|
-
|
|
93
|
-
- **No auto-detection**, in any form — see above.
|
|
94
|
-
- **No change to the two session strategies.** They were measured against a live trial and an on-prem system and are not the subject here.
|
|
95
|
-
- **No new auth mechanism.** Only a place for the ones that exist to be passed in.
|
|
96
|
-
- **No dependency on the auth packages.** The connector keeps speaking contracts; consumers supply implementations, as with `IAbapConnection` today.
|
|
97
|
-
|
|
98
|
-
## Open, to settle in review
|
|
99
|
-
|
|
100
|
-
1. **`prepare()` and Kerberos.** SPNEGO deliberately does not prepare early: minting the token sooner changes when the exchange happens, and that path is not production-tested. Does Kerberos keep opting out, or does the contract grow a way to say "prepare late"?
|
|
101
|
-
2. **What `createAbapConnection(config)` does when the config does not say which system it is.** Defaulting to on-prem keeps every existing consumer working and never guesses; refusing is more honest and breaks them. Deprecation-shaped default seems right, but it is a decision.
|
|
102
|
-
3. **Whether `AdtOnPremConnector` should refuse the ADT session resource explicitly** — it exists there and answers, so a future edit could quietly start using it.
|
|
103
|
-
|
|
104
|
-
## How it gets verified
|
|
105
|
-
|
|
106
|
-
- Cloud: from this machine, against the BTP trial — connect, hold a session across stateful requests, disconnect. Already the way the current mechanism was measured.
|
|
107
|
-
- On-prem: not reachable from here. Reviewed and run on the machine that reaches a system, as PR #34 was: strategy `icf`, `lock → PUT` chains, session present, and **no request to the session resource**.
|
|
108
|
-
- Both wrong rows from the table above become tests: a cloud connector with a basic credential must use the ADT session; an on-prem connector with a token credential must not.
|