@mcp-abap-adt/connection 3.0.0 → 5.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 +337 -1
- package/README.md +74 -31
- package/dist/auth/IAuthProvider.d.ts +84 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -0
- package/dist/auth/IAuthProvider.js +21 -0
- package/dist/auth/providers.d.ts +95 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +140 -0
- package/dist/connection/AbstractAbapConnection.d.ts +222 -17
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +555 -31
- package/dist/connection/AdtCloudConnector.d.ts +29 -0
- package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
- package/dist/connection/AdtCloudConnector.js +31 -0
- package/dist/connection/AdtOnPremConnector.d.ts +30 -0
- package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
- package/dist/connection/AdtOnPremConnector.js +32 -0
- package/dist/connection/BaseAbapConnection.d.ts +7 -1
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
- package/dist/connection/BaseAbapConnection.js +8 -2
- package/dist/connection/CertificateAbapConnection.d.ts +11 -1
- package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
- package/dist/connection/CertificateAbapConnection.js +14 -2
- package/dist/connection/CredentialAbapConnection.d.ts +88 -0
- package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
- package/dist/connection/CredentialAbapConnection.js +195 -0
- package/dist/connection/JwtAbapConnection.d.ts +102 -9
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +266 -82
- package/dist/connection/KerberosAbapConnection.d.ts +9 -1
- package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
- package/dist/connection/KerberosAbapConnection.js +9 -1
- package/dist/connection/RfcAbapConnection.d.ts +0 -5
- package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
- package/dist/connection/RfcAbapConnection.js +0 -7
- package/dist/connection/SamlAbapConnection.d.ts +7 -1
- package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
- package/dist/connection/SamlAbapConnection.js +8 -2
- package/dist/connection/connectionFactory.d.ts +16 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -1
- package/dist/connection/connectionFactory.js +52 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
- package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
- package/dist/session/CloudSecuritySessionStrategy.js +135 -0
- package/dist/session/IcfSessionStrategy.d.ts +27 -0
- package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
- package/dist/session/IcfSessionStrategy.js +62 -0
- 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/session/SessionStrategy.d.ts +86 -0
- package/dist/session/SessionStrategy.d.ts.map +1 -0
- package/dist/session/SessionStrategy.js +33 -0
- 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 +16 -0
- package/dist/utils/timeouts.d.ts.map +1 -1
- package/dist/utils/timeouts.js +19 -0
- package/docs/INDEX.md +5 -0
- package/docs/INSTALLATION.md +6 -2
- package/docs/MIGRATION-2.0.md +1 -1
- package/docs/MIGRATION-4.0.md +95 -0
- package/docs/MIGRATION-5.0.md +116 -0
- package/docs/STATEFUL_SESSION_GUIDE.md +83 -11
- package/docs/USAGE.md +115 -24
- package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
- package/examples/README.md +2 -1
- package/examples/jwt-with-token-refresh.js +11 -3
- package/package.json +1 -1
|
@@ -291,13 +291,6 @@ class RfcAbapConnection {
|
|
|
291
291
|
throw new Error(`RFC call to SADT_REST_RFC_ENDPOINT failed: ${msg}`);
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Reset the connection — for RFC this closes the session.
|
|
296
|
-
* Provides interface compatibility with HTTP connections.
|
|
297
|
-
*/
|
|
298
|
-
reset() {
|
|
299
|
-
this.close();
|
|
300
|
-
}
|
|
301
294
|
/**
|
|
302
295
|
* Close the RFC connection and release the ABAP session.
|
|
303
296
|
*/
|
|
@@ -2,7 +2,13 @@ import type { SapConfig } from '../config/sapConfig.js';
|
|
|
2
2
|
import type { ILogger } from '../logger.js';
|
|
3
3
|
import { AbstractAbapConnection } from './AbstractAbapConnection.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* @deprecated The class you take should say which SYSTEM you are dialling,
|
|
6
|
+
* not which credential you hold — a session mechanism that rides along with
|
|
7
|
+
* the credential gets one of them wrong. Use `AdtOnPremConnector` with `SamlAuthProvider`:
|
|
8
|
+
*
|
|
9
|
+
* new AdtOnPremConnector(config, new SamlAuthProvider(cookies), logger)
|
|
10
|
+
*
|
|
11
|
+
* Kept working, and not removed here: existing consumers are unaffected.
|
|
6
12
|
*/
|
|
7
13
|
export declare class SamlAbapConnection extends AbstractAbapConnection {
|
|
8
14
|
private sessionCookies;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SamlAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/SamlAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE
|
|
1
|
+
{"version":3,"file":"SamlAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/SamlAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;;;;;;;GAQG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;IAC5D,OAAO,CAAC,cAAc,CAAS;gBAG7B,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAUzC;;;OAGG;IACH;;;OAGG;cACa,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCjD,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAItC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAQvD,OAAO,CAAC,MAAM,CAAC,cAAc;CAW9B"}
|
|
@@ -4,7 +4,13 @@ exports.SamlAbapConnection = void 0;
|
|
|
4
4
|
const axios_1 = require("axios");
|
|
5
5
|
const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* @deprecated The class you take should say which SYSTEM you are dialling,
|
|
8
|
+
* not which credential you hold — a session mechanism that rides along with
|
|
9
|
+
* the credential gets one of them wrong. Use `AdtOnPremConnector` with `SamlAuthProvider`:
|
|
10
|
+
*
|
|
11
|
+
* new AdtOnPremConnector(config, new SamlAuthProvider(cookies), logger)
|
|
12
|
+
*
|
|
13
|
+
* Kept working, and not removed here: existing consumers are unaffected.
|
|
8
14
|
*/
|
|
9
15
|
class SamlAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
|
|
10
16
|
sessionCookies;
|
|
@@ -29,7 +35,7 @@ class SamlAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnect
|
|
|
29
35
|
const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
|
|
30
36
|
this.logger?.debug(`[DEBUG] SamlAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
|
|
31
37
|
try {
|
|
32
|
-
const token = await this.fetchCsrfToken(discoveryUrl
|
|
38
|
+
const token = await this.fetchCsrfToken(discoveryUrl);
|
|
33
39
|
this.setCsrfToken(token);
|
|
34
40
|
this.logger?.debug('Successfully connected to SAP system', {
|
|
35
41
|
hasCsrfToken: !!this.getCsrfToken(),
|
|
@@ -2,8 +2,24 @@ import type { ICertificateMaterialLoader, ITokenRefresher } from '@mcp-abap-adt/
|
|
|
2
2
|
import type { SapConfig } from '../config/sapConfig.js';
|
|
3
3
|
import type { ILogger } from '../logger.js';
|
|
4
4
|
import type { AbapConnection } from './AbapConnection.js';
|
|
5
|
+
/**
|
|
6
|
+
* Builds the connection the caller asked for.
|
|
7
|
+
*
|
|
8
|
+
* **Say which system it is** with `options.system`, and that is what you get —
|
|
9
|
+
* `'onprem'` or `'cloud'` — with the credential built from the config and
|
|
10
|
+
* handed in as an auth provider. Nothing about the system is worked out here:
|
|
11
|
+
* not from the credential, not from the host name, not by asking the server.
|
|
12
|
+
*
|
|
13
|
+
* Without it, the old behaviour: a connection chosen from `authType`, which is
|
|
14
|
+
* the arrangement where the credential drags the session mechanism along with
|
|
15
|
+
* it and gets one of them wrong. **Deprecated**, kept so existing callers keep
|
|
16
|
+
* working, and warned about once per call so it is visible rather than
|
|
17
|
+
* inherited by accident.
|
|
18
|
+
*/
|
|
5
19
|
export declare function createAbapConnection(config: SapConfig, logger?: ILogger | null, sessionId?: string, tokenRefresher?: ITokenRefresher, options?: {
|
|
6
20
|
skipSessionType?: boolean;
|
|
7
21
|
certLoader?: ICertificateMaterialLoader;
|
|
22
|
+
/** Which system this is. Said by the caller; never inferred. */
|
|
23
|
+
system?: 'onprem' | 'cloud';
|
|
8
24
|
}): AbapConnection;
|
|
9
25
|
//# sourceMappingURL=connectionFactory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectionFactory.d.ts","sourceRoot":"","sources":["../../src/connection/connectionFactory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,eAAe,EAChB,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"connectionFactory.d.ts","sourceRoot":"","sources":["../../src/connection/connectionFactory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmD1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,eAAe,EAChC,OAAO,CAAC,EAAE;IACR,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC,gEAAgE;IAChE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC7B,GACA,cAAc,CA2ChB"}
|
|
@@ -1,12 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
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");
|
|
4
7
|
const BaseAbapConnection_js_1 = require("./BaseAbapConnection.js");
|
|
5
8
|
const CertificateAbapConnection_js_1 = require("./CertificateAbapConnection.js");
|
|
6
9
|
const JwtAbapConnection_js_1 = require("./JwtAbapConnection.js");
|
|
7
10
|
const KerberosAbapConnection_js_1 = require("./KerberosAbapConnection.js");
|
|
8
11
|
const RfcAbapConnection_js_1 = require("./RfcAbapConnection.js");
|
|
9
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
|
+
*/
|
|
10
56
|
function createAbapConnection(config, logger, sessionId, tokenRefresher, options) {
|
|
11
57
|
// RFC connection type takes priority over auth type
|
|
12
58
|
if (config.connectionType === 'rfc') {
|
|
@@ -15,6 +61,12 @@ function createAbapConnection(config, logger, sessionId, tokenRefresher, options
|
|
|
15
61
|
}
|
|
16
62
|
return new RfcAbapConnection_js_1.RfcAbapConnection(config, logger);
|
|
17
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'.");
|
|
18
70
|
switch (config.authType) {
|
|
19
71
|
case 'basic':
|
|
20
72
|
return new BaseAbapConnection_js_1.BaseAbapConnection(config, logger, sessionId, options);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export type { IWebSocketCloseInfo, IWebSocketConnectOptions, IWebSocketMessageEnvelope, IWebSocketMessageHandler, IWebSocketTransport, } from '@mcp-abap-adt/interfaces';
|
|
2
2
|
export { FileCertificateMaterialLoader } from './auth/FileCertificateMaterialLoader.js';
|
|
3
|
+
export type { IAuthProvider } from './auth/IAuthProvider.js';
|
|
4
|
+
export { BasicAuthProvider, CertificateAuthProvider, SamlAuthProvider, TokenAuthProvider, } from './auth/providers.js';
|
|
3
5
|
export type { SapAuthType, SapConfig, SapConnectionType, } from './config/sapConfig.js';
|
|
4
6
|
export { sapConfigSignature } from './config/sapConfig.js';
|
|
5
7
|
export type { AbapConnection, AbapRequestOptions, } from './connection/AbapConnection.js';
|
|
8
|
+
export { AdtCloudConnector } from './connection/AdtCloudConnector.js';
|
|
9
|
+
export { AdtOnPremConnector } from './connection/AdtOnPremConnector.js';
|
|
6
10
|
export { BaseAbapConnection, BaseAbapConnection as OnPremAbapConnection, } from './connection/BaseAbapConnection.js';
|
|
7
11
|
export { CertificateAbapConnection } from './connection/CertificateAbapConnection.js';
|
|
8
12
|
export { createAbapConnection } from './connection/connectionFactory.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;AACxF,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;
|
|
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;AACxF,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,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;AAOxE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,IAAI,oBAAoB,GAC3C,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,iBAAiB,EACjB,iBAAiB,IAAI,mBAAmB,GACzC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Types - re-exported from interfaces package with backward compatibility aliases
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.getTimeoutConfig = exports.getTimeout = exports.SamlAbapConnection = exports.RfcAbapConnection = exports.KerberosAbapConnection = exports.CloudAbapConnection = exports.JwtAbapConnection = exports.GenericWebSocketTransport = exports.CSRF_ERROR_MESSAGES = exports.CSRF_CONFIG = exports.createAbapConnection = exports.CertificateAbapConnection = exports.OnPremAbapConnection = exports.BaseAbapConnection = exports.sapConfigSignature = exports.FileCertificateMaterialLoader = void 0;
|
|
4
|
+
exports.getTimeoutConfig = exports.getTimeout = exports.SamlAbapConnection = exports.RfcAbapConnection = exports.KerberosAbapConnection = exports.CloudAbapConnection = exports.JwtAbapConnection = exports.GenericWebSocketTransport = exports.CSRF_ERROR_MESSAGES = exports.CSRF_CONFIG = exports.createAbapConnection = exports.CertificateAbapConnection = exports.OnPremAbapConnection = exports.BaseAbapConnection = exports.AdtOnPremConnector = exports.AdtCloudConnector = exports.sapConfigSignature = exports.TokenAuthProvider = exports.SamlAuthProvider = exports.CertificateAuthProvider = exports.BasicAuthProvider = exports.FileCertificateMaterialLoader = void 0;
|
|
5
5
|
var FileCertificateMaterialLoader_js_1 = require("./auth/FileCertificateMaterialLoader.js");
|
|
6
6
|
Object.defineProperty(exports, "FileCertificateMaterialLoader", { enumerable: true, get: function () { return FileCertificateMaterialLoader_js_1.FileCertificateMaterialLoader; } });
|
|
7
|
+
var providers_js_1 = require("./auth/providers.js");
|
|
8
|
+
Object.defineProperty(exports, "BasicAuthProvider", { enumerable: true, get: function () { return providers_js_1.BasicAuthProvider; } });
|
|
9
|
+
Object.defineProperty(exports, "CertificateAuthProvider", { enumerable: true, get: function () { return providers_js_1.CertificateAuthProvider; } });
|
|
10
|
+
Object.defineProperty(exports, "SamlAuthProvider", { enumerable: true, get: function () { return providers_js_1.SamlAuthProvider; } });
|
|
11
|
+
Object.defineProperty(exports, "TokenAuthProvider", { enumerable: true, get: function () { return providers_js_1.TokenAuthProvider; } });
|
|
7
12
|
// Config utilities
|
|
8
13
|
var sapConfig_js_1 = require("./config/sapConfig.js");
|
|
9
14
|
Object.defineProperty(exports, "sapConfigSignature", { enumerable: true, get: function () { return sapConfig_js_1.sapConfigSignature; } });
|
|
15
|
+
var AdtCloudConnector_js_1 = require("./connection/AdtCloudConnector.js");
|
|
16
|
+
Object.defineProperty(exports, "AdtCloudConnector", { enumerable: true, get: function () { return AdtCloudConnector_js_1.AdtCloudConnector; } });
|
|
17
|
+
var AdtOnPremConnector_js_1 = require("./connection/AdtOnPremConnector.js");
|
|
18
|
+
Object.defineProperty(exports, "AdtOnPremConnector", { enumerable: true, get: function () { return AdtOnPremConnector_js_1.AdtOnPremConnector; } });
|
|
10
19
|
// The session lifecycle vocabulary — ISessionLifecycleAware, ADT_SESSION_ERROR —
|
|
11
20
|
// is deliberately NOT exported here. It lives in @mcp-abap-adt/interfaces, and a
|
|
12
21
|
// consumer imports it from there: re-exporting a contract type gives it two
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ABAP Cloud: a session is a resource, asked for and given back by address.
|
|
4
|
+
*
|
|
5
|
+
* Captured from Eclipse ADT 3.60 against a BTP trial and reproduced here with a
|
|
6
|
+
* bearer token, which is what made the difference visible: **the server issues
|
|
7
|
+
* `SAP_SESSIONID` to whoever asks for a session, and we never asked.** The
|
|
8
|
+
* request that asks carries `x-sap-security-session: create` — `use` does not
|
|
9
|
+
* do it — and `sap-adt-purpose: preflight_logon`. The response sets the cookie
|
|
10
|
+
* and publishes the session's own address in a `securitysession` link, which is
|
|
11
|
+
* the address a close goes to.
|
|
12
|
+
*
|
|
13
|
+
* Without that request the same connection gets `sap-usercontext` and
|
|
14
|
+
* `sap-XSRF_*` and nothing else, no `securitysession` link, and no session for
|
|
15
|
+
* a lock to be bound to — which is how "cloud does not issue SAP_SESSIONID"
|
|
16
|
+
* came to be believed.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.CloudSecuritySessionStrategy = void 0;
|
|
20
|
+
const cookies_js_1 = require("../utils/cookies.js");
|
|
21
|
+
const timeouts_js_1 = require("../utils/timeouts.js");
|
|
22
|
+
const SessionStrategy_js_1 = require("./SessionStrategy.js");
|
|
23
|
+
const SESSIONS_PATH = '/sap/bc/adt/core/http/sessions';
|
|
24
|
+
/** The versioned media types Eclipse asks this resource for, newest first. */
|
|
25
|
+
const SESSION_ACCEPT = 'application/vnd.sap.adt.core.http.session.v3+xml, ' +
|
|
26
|
+
'application/vnd.sap.adt.core.http.session.v2+xml, ' +
|
|
27
|
+
'application/vnd.sap.adt.core.http.session.v1+xml';
|
|
28
|
+
const SECURITY_SESSION_REL = 'http://www.sap.com/adt/categories/core/http/sessions/securitysession';
|
|
29
|
+
class CloudSecuritySessionStrategy extends SessionStrategy_js_1.SessionStrategy {
|
|
30
|
+
kind = 'cloud-security-session';
|
|
31
|
+
/** The address this server published for our session — the only close target. */
|
|
32
|
+
resource = null;
|
|
33
|
+
async openSession(transport) {
|
|
34
|
+
try {
|
|
35
|
+
// Read ONCE. The contract lets a provider answer differently each time —
|
|
36
|
+
// a token provider renews behind the call — so two reads can build one
|
|
37
|
+
// request out of two different credentials, and for a token provider they
|
|
38
|
+
// also double the work.
|
|
39
|
+
const auth = await transport.authHeaders();
|
|
40
|
+
const response = await transport.send({
|
|
41
|
+
method: 'GET',
|
|
42
|
+
// The cache-buster is Eclipse's; kept because this must not be served
|
|
43
|
+
// from anything's cache — a cached session document would hand back an
|
|
44
|
+
// address that belongs to a session somebody else already ended.
|
|
45
|
+
url: `${transport.baseUrl}${SESSIONS_PATH}?_=${Date.now()}`,
|
|
46
|
+
// Bounded: connect() waits for this one.
|
|
47
|
+
timeoutMs: (0, timeouts_js_1.getTimeout)('csrf'),
|
|
48
|
+
// Its SAP_SESSIONID is the session.
|
|
49
|
+
adoptCookies: true,
|
|
50
|
+
headers: {
|
|
51
|
+
...auth,
|
|
52
|
+
Accept: SESSION_ACCEPT,
|
|
53
|
+
Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, transport.cookies() ?? undefined),
|
|
54
|
+
'sap-adt-purpose': 'preflight_logon',
|
|
55
|
+
'x-sap-security-session': 'create',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
// 404 is the answer "this system has no session resource", which is what
|
|
59
|
+
// on-prem says. Not an error, and not something to retry.
|
|
60
|
+
if (response.status === 404) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const resource = this.securitySessionHref(response.data);
|
|
64
|
+
if (!resource) {
|
|
65
|
+
// Answered, but published no session of its own — so there is no
|
|
66
|
+
// address to give one back by, and claiming this mechanism would leave
|
|
67
|
+
// the close with nothing to send. Falls back to the platform's logoff,
|
|
68
|
+
// which needs no address.
|
|
69
|
+
this.logger?.debug(`Session resource answered ${response.status} but published no securitysession link`);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
this.resource = resource;
|
|
73
|
+
this.logger?.debug(`Security session opened: ${resource}`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Never fatal here. If no session was opened, the establishment that
|
|
78
|
+
// follows says so with the whole picture; a transport error raised from
|
|
79
|
+
// a preflight would replace that with something less useful.
|
|
80
|
+
this.logger?.debug(`Could not open a security session: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async closeSession(transport) {
|
|
85
|
+
const resource = this.resource;
|
|
86
|
+
if (!resource || !transport.cookies()) {
|
|
87
|
+
// No address to send it to, or no cookie to prove the session is ours —
|
|
88
|
+
// holding the cookie is the whole permission.
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const csrf = transport.csrfToken();
|
|
92
|
+
try {
|
|
93
|
+
const auth = await transport.authHeaders();
|
|
94
|
+
await transport.send({
|
|
95
|
+
method: 'DELETE',
|
|
96
|
+
url: new URL(resource, transport.baseUrl).toString(),
|
|
97
|
+
headers: {
|
|
98
|
+
...auth,
|
|
99
|
+
Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, transport.cookies() ?? undefined),
|
|
100
|
+
'x-sap-security-session': 'use',
|
|
101
|
+
// A state change, so the server wants the token. Without one the
|
|
102
|
+
// request is refused with 403 — which is still just a message the
|
|
103
|
+
// server did not accept, and still not something to raise from a
|
|
104
|
+
// teardown.
|
|
105
|
+
...(csrf ? { 'x-csrf-token': csrf } : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
this.logger?.debug('Told the server the security session is finished');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.logger?.debug(`Could not tell the server the session is finished: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The `securitysession` link's href, by its `rel` rather than by position.
|
|
116
|
+
*
|
|
117
|
+
* The document carries three links — the session, the logoff resource and
|
|
118
|
+
* system information — and their order is not a contract.
|
|
119
|
+
*/
|
|
120
|
+
securitySessionHref(body) {
|
|
121
|
+
if (typeof body !== 'string')
|
|
122
|
+
return undefined;
|
|
123
|
+
for (const link of body.matchAll(/<[^>]*link\b([^>]*)>/g)) {
|
|
124
|
+
const attrs = link[1];
|
|
125
|
+
const rel = attrs.match(/\brel="([^"]*)"/)?.[1];
|
|
126
|
+
if (rel !== SECURITY_SESSION_REL)
|
|
127
|
+
continue;
|
|
128
|
+
const href = attrs.match(/\bhref="([^"]*)"/)?.[1];
|
|
129
|
+
if (href)
|
|
130
|
+
return href;
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.CloudSecuritySessionStrategy = CloudSecuritySessionStrategy;
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
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;
|
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
* this unit testable without a server, and it is where the hard part lives:
|
|
8
8
|
* ordering, admission, and telling one session from the next.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* What it guarantees, since the design note it used to point at lived in another
|
|
11
|
+
* package and has been deleted with the work it described:
|
|
12
|
+
*
|
|
13
|
+
* - **One transition at a time**, on a serializing tail. Connects join
|
|
14
|
+
* connects and disconnects join disconnects, so concurrent callers share one
|
|
15
|
+
* establishment or one teardown rather than racing to build and demolish the
|
|
16
|
+
* same session.
|
|
17
|
+
* - **Admission is synchronous.** A request is admitted, or refused, in the
|
|
18
|
+
* same step that counts it — so nothing can be admitted and still be
|
|
19
|
+
* invisible to a teardown draining at that instant.
|
|
20
|
+
* - **A teardown waits for nothing.** In-flight requests run to completion
|
|
21
|
+
* untouched; their results are fenced by session generation so a late answer
|
|
22
|
+
* cannot write over a session established since.
|
|
23
|
+
* - **Identity is observed, never assumed.** A changed fingerprint means the
|
|
24
|
+
* session we had is gone and this is a different one, which is fatal to
|
|
25
|
+
* anything held against the old one;
|
|
26
|
+
* an unchanged one says nothing about whether the server still has it.
|
|
12
27
|
*/
|
|
13
28
|
import { type AdtSessionErrorCode } from '@mcp-abap-adt/interfaces';
|
|
14
29
|
export type TransitionKind = 'connect' | 'disconnect' | 'recover' | 'cleanup';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,0FAA0F;IAC1F,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC9B,iFAAiF;IACjF,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,mBAAmB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,KAAK,GAAG;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,CAMvC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAK;IAErB,OAAO,CAAC,IAAI,CAAuC;IACnD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,WAAW,CAAiC;IAIpD,4EAA4E;IAC5E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,0EAA0E;IAC1E,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAM5B;IAED,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;;;;;;;;;;OAaG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED;;;;;OAKG;IACH,aAAa,CAAC,WAAW,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,GAAG,IAAI;IAOzE;;;;;;OAMG;IACH,cAAc,IAAI,IAAI;IAItB,gBAAgB,IAAI,IAAI;IAKxB;;;;;;OAMG;IACH,OAAO,CACL,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACvC,WAAW,GAAG,aAAa,GAAG,UAAU;IAiB3C,YAAY,IAAI,IAAI;IAMpB,4EAA4E;IAC5E,YAAY,IAAI,YAAY;IAiB5B,2FAA2F;IAC3F,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,OAAO;IAM3D,aAAa,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,oBAAoB,GAAG,IAAI;IAelE,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAID;;;;;;;;;OASG;IACH,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAuBvE"}
|
|
@@ -8,8 +8,23 @@
|
|
|
8
8
|
* this unit testable without a server, and it is where the hard part lives:
|
|
9
9
|
* ordering, admission, and telling one session from the next.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* What it guarantees, since the design note it used to point at lived in another
|
|
12
|
+
* package and has been deleted with the work it described:
|
|
13
|
+
*
|
|
14
|
+
* - **One transition at a time**, on a serializing tail. Connects join
|
|
15
|
+
* connects and disconnects join disconnects, so concurrent callers share one
|
|
16
|
+
* establishment or one teardown rather than racing to build and demolish the
|
|
17
|
+
* same session.
|
|
18
|
+
* - **Admission is synchronous.** A request is admitted, or refused, in the
|
|
19
|
+
* same step that counts it — so nothing can be admitted and still be
|
|
20
|
+
* invisible to a teardown draining at that instant.
|
|
21
|
+
* - **A teardown waits for nothing.** In-flight requests run to completion
|
|
22
|
+
* untouched; their results are fenced by session generation so a late answer
|
|
23
|
+
* cannot write over a session established since.
|
|
24
|
+
* - **Identity is observed, never assumed.** A changed fingerprint means the
|
|
25
|
+
* session we had is gone and this is a different one, which is fatal to
|
|
26
|
+
* anything held against the old one;
|
|
27
|
+
* an unchanged one says nothing about whether the server still has it.
|
|
13
28
|
*/
|
|
14
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
30
|
exports.SessionLifecycle = void 0;
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|