@mcp-abap-adt/connection 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +407 -1
  2. package/README.md +239 -53
  3. package/dist/auth/providers.d.ts +124 -0
  4. package/dist/auth/providers.d.ts.map +1 -0
  5. package/dist/auth/providers.js +183 -0
  6. package/dist/connection/AbstractAbapConnection.d.ts +209 -57
  7. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  8. package/dist/connection/AbstractAbapConnection.js +459 -457
  9. package/dist/connection/AdtCloudConnector.d.ts +34 -0
  10. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  11. package/dist/connection/AdtCloudConnector.js +27 -0
  12. package/dist/connection/AdtOnPremConnector.d.ts +43 -0
  13. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtOnPremConnector.js +28 -0
  15. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  16. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  17. package/dist/connection/CloudHttpTransport.js +145 -0
  18. package/dist/connection/CredentialAbapConnection.d.ts +55 -0
  19. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  20. package/dist/connection/CredentialAbapConnection.js +128 -0
  21. package/dist/connection/HttpTransport.d.ts +178 -0
  22. package/dist/connection/HttpTransport.d.ts.map +1 -0
  23. package/dist/connection/HttpTransport.js +402 -0
  24. package/dist/connection/IAdtTransport.d.ts +232 -0
  25. package/dist/connection/IAdtTransport.d.ts.map +1 -0
  26. package/dist/connection/IAdtTransport.js +28 -0
  27. package/dist/connection/LegacyOnPremHttpTransport.d.ts +40 -0
  28. package/dist/connection/LegacyOnPremHttpTransport.d.ts.map +1 -0
  29. package/dist/connection/LegacyOnPremHttpTransport.js +57 -0
  30. package/dist/connection/OnPremHttpTransport.d.ts +45 -0
  31. package/dist/connection/OnPremHttpTransport.d.ts.map +1 -0
  32. package/dist/connection/OnPremHttpTransport.js +91 -0
  33. package/dist/connection/RfcTransport.d.ts +89 -0
  34. package/dist/connection/RfcTransport.d.ts.map +1 -0
  35. package/dist/connection/RfcTransport.js +256 -0
  36. package/dist/connection/rfcConversation.d.ts +44 -0
  37. package/dist/connection/rfcConversation.d.ts.map +1 -0
  38. package/dist/connection/rfcConversation.js +71 -0
  39. package/dist/index.d.ts +10 -7
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +30 -18
  42. package/dist/session/SessionLifecycle.d.ts +17 -2
  43. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  44. package/dist/session/SessionLifecycle.js +17 -2
  45. package/dist/utils/cookies.d.ts +12 -0
  46. package/dist/utils/cookies.d.ts.map +1 -0
  47. package/dist/utils/cookies.js +24 -0
  48. package/dist/utils/timeouts.d.ts +6 -3
  49. package/dist/utils/timeouts.d.ts.map +1 -1
  50. package/dist/utils/timeouts.js +6 -3
  51. package/docs/INDEX.md +5 -2
  52. package/docs/INSTALLATION.md +28 -6
  53. package/docs/JWT_AUTH_TOOLS.md +20 -4
  54. package/docs/MIGRATION-2.0.md +1 -1
  55. package/docs/MIGRATION-5.0.md +116 -0
  56. package/docs/MIGRATION-6.0.md +359 -0
  57. package/docs/SCOPE.md +1 -1
  58. package/docs/STATEFUL_SESSION_GUIDE.md +155 -20
  59. package/docs/USAGE.md +322 -111
  60. package/examples/basic-connection.js +15 -3
  61. package/examples/jwt-with-token-refresh.js +15 -7
  62. package/examples/saml-connection.js +15 -2
  63. package/package.json +12 -10
  64. package/dist/__tests__/helpers/session.d.ts +0 -15
  65. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  66. package/dist/__tests__/helpers/session.js +0 -19
  67. package/dist/connection/BaseAbapConnection.d.ts +0 -23
  68. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  69. package/dist/connection/BaseAbapConnection.js +0 -75
  70. package/dist/connection/CertificateAbapConnection.d.ts +0 -25
  71. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  72. package/dist/connection/CertificateAbapConnection.js +0 -79
  73. package/dist/connection/JwtAbapConnection.d.ts +0 -115
  74. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  75. package/dist/connection/JwtAbapConnection.js +0 -358
  76. package/dist/connection/KerberosAbapConnection.d.ts +0 -24
  77. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  78. package/dist/connection/KerberosAbapConnection.js +0 -120
  79. package/dist/connection/RfcAbapConnection.d.ts +0 -49
  80. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  81. package/dist/connection/RfcAbapConnection.js +0 -331
  82. package/dist/connection/SamlAbapConnection.d.ts +0 -25
  83. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  84. package/dist/connection/SamlAbapConnection.js +0 -75
  85. package/dist/connection/connectionFactory.d.ts +0 -9
  86. package/dist/connection/connectionFactory.d.ts.map +0 -1
  87. package/dist/connection/connectionFactory.js +0 -32
@@ -0,0 +1,34 @@
1
+ /**
2
+ * An ABAP Cloud system.
3
+ *
4
+ * A session there is a **resource**: asked for at
5
+ * `/sap/bc/adt/core/http/sessions` with `x-sap-security-session: create` and
6
+ * `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on the address
7
+ * the server publishes in the answer. Reproduced from Eclipse ADT 3.60 and
8
+ * measured against a BTP trial: without that request the same connection gets
9
+ * `sap-usercontext` and `sap-XSRF_*` and no session for a lock to be bound to,
10
+ * which is how "cloud issues no SAP_SESSIONID" came to be believed.
11
+ *
12
+ * **Taking this class is the consumer stating which system it is dialling**, and
13
+ * it is the only thing that states it: a communication user against ABAP Cloud
14
+ * gets this mechanism because the caller asked for this connector. Nothing is
15
+ * inferred — not from the credential, not from the host name, not from what the
16
+ * server answers.
17
+ */
18
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
19
+ import type { SapConfig } from '../config/sapConfig.js';
20
+ import type { ILogger } from '../logger.js';
21
+ import type { CloudHttpTransport } from './CloudHttpTransport.js';
22
+ import { CredentialAbapConnection } from './CredentialAbapConnection.js';
23
+ import type { ICloudTransport } from './IAdtTransport.js';
24
+ export declare class AdtCloudConnector<TCredential extends IAuthProvider = IAuthProvider,
25
+ /**
26
+ * Constrained to the cloud wire, so "cloud over RFC" does not compile.
27
+ * There is no such deployment, and a connector that accepted the transport
28
+ * would be offering a combination that cannot exist.
29
+ */
30
+ TTransport extends ICloudTransport = CloudHttpTransport> extends CredentialAbapConnection<TCredential> {
31
+ readonly transport: TTransport;
32
+ constructor(config: SapConfig, credential: TCredential, transport: TTransport, logger?: ILogger | null, sessionId?: string);
33
+ }
34
+ //# sourceMappingURL=AdtCloudConnector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdtCloudConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtCloudConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,qBAAa,iBAAiB,CAC5B,WAAW,SAAS,aAAa,GAAG,aAAa;AACjD;;;;GAIG;AACH,UAAU,SAAS,eAAe,GAAG,kBAAkB,CACvD,SAAQ,wBAAwB,CAAC,WAAW,CAAC;IAC7C,SAAiB,SAAS,EAAE,UAAU,CAAC;gBAGrC,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;CAIrB"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * An ABAP Cloud system.
4
+ *
5
+ * A session there is a **resource**: asked for at
6
+ * `/sap/bc/adt/core/http/sessions` with `x-sap-security-session: create` and
7
+ * `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on the address
8
+ * the server publishes in the answer. Reproduced from Eclipse ADT 3.60 and
9
+ * measured against a BTP trial: without that request the same connection gets
10
+ * `sap-usercontext` and `sap-XSRF_*` and no session for a lock to be bound to,
11
+ * which is how "cloud issues no SAP_SESSIONID" came to be believed.
12
+ *
13
+ * **Taking this class is the consumer stating which system it is dialling**, and
14
+ * it is the only thing that states it: a communication user against ABAP Cloud
15
+ * gets this mechanism because the caller asked for this connector. Nothing is
16
+ * inferred — not from the credential, not from the host name, not from what the
17
+ * server answers.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.AdtCloudConnector = void 0;
21
+ const CredentialAbapConnection_js_1 = require("./CredentialAbapConnection.js");
22
+ class AdtCloudConnector extends CredentialAbapConnection_js_1.CredentialAbapConnection {
23
+ constructor(config, credential, transport, logger = null, sessionId) {
24
+ super(config, credential, transport, logger, sessionId);
25
+ }
26
+ }
27
+ exports.AdtCloudConnector = AdtCloudConnector;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * An on-prem ABAP system.
3
+ *
4
+ * The session arrives with the establishing call — the logon *is* that call —
5
+ * and the platform's `/sap/public/bc/icf/logoff` is how it is given back. That
6
+ * is what on-prem has always used here, and it does not change.
7
+ *
8
+ * **Taking this class is the consumer stating which system it is dialling.**
9
+ * Nothing is probed and nothing is inferred: `/sap/bc/adt/core/http/sessions` answers on on-prem too —
10
+ * measured on S/4HANA, publishing both the session resource and the ICF logoff
11
+ * in one document — so asking the server tells you an endpoint exists, not
12
+ * which kind of system you reached.
13
+ *
14
+ * The credential is passed in and decides none of this. A bearer token against
15
+ * an on-prem system is ordinary, and it gets this mechanism because the caller
16
+ * asked for this connector — not because of what is in the header, and not
17
+ * because anything worked out where it was pointing.
18
+ */
19
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
20
+ import type { SapConfig } from '../config/sapConfig.js';
21
+ import type { ILogger } from '../logger.js';
22
+ import { CredentialAbapConnection } from './CredentialAbapConnection.js';
23
+ import type { IOnPremTransport } from './IAdtTransport.js';
24
+ import type { OnPremHttpTransport } from './OnPremHttpTransport.js';
25
+ export declare class AdtOnPremConnector<TCredential extends IAuthProvider = IAuthProvider,
26
+ /**
27
+ * The two wires an on-prem system actually offers. Said by the caller: the
28
+ * same ADT call travels over HTTP, or over RFC on a system where stateful
29
+ * HTTP sessions are not usable.
30
+ */
31
+ TTransport extends IOnPremTransport = OnPremHttpTransport> extends CredentialAbapConnection<TCredential> {
32
+ /**
33
+ * The transport this was built with, in the type.
34
+ *
35
+ * `declare` because the base already assigns it — this only narrows what the
36
+ * caller gets back, which is the whole point of the parameter: a signature
37
+ * can ask for `AdtOnPremConnector<IAuthProvider, RfcTransport>` instead of
38
+ * taking any connection and casting.
39
+ */
40
+ readonly transport: TTransport;
41
+ constructor(config: SapConfig, credential: TCredential, transport: TTransport, logger?: ILogger | null, sessionId?: string);
42
+ }
43
+ //# sourceMappingURL=AdtOnPremConnector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdtOnPremConnector.d.ts","sourceRoot":"","sources":["../../src/connection/AdtOnPremConnector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,qBAAa,kBAAkB,CAC7B,WAAW,SAAS,aAAa,GAAG,aAAa;AACjD;;;;GAIG;AACH,UAAU,SAAS,gBAAgB,GAAG,mBAAmB,CACzD,SAAQ,wBAAwB,CAAC,WAAW,CAAC;IAC7C;;;;;;;OAOG;IACH,SAAiB,SAAS,EAAE,UAAU,CAAC;gBAGrC,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,UAAU,EACrB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;CAIrB"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * An on-prem ABAP system.
4
+ *
5
+ * The session arrives with the establishing call — the logon *is* that call —
6
+ * and the platform's `/sap/public/bc/icf/logoff` is how it is given back. That
7
+ * is what on-prem has always used here, and it does not change.
8
+ *
9
+ * **Taking this class is the consumer stating which system it is dialling.**
10
+ * Nothing is probed and nothing is inferred: `/sap/bc/adt/core/http/sessions` answers on on-prem too —
11
+ * measured on S/4HANA, publishing both the session resource and the ICF logoff
12
+ * in one document — so asking the server tells you an endpoint exists, not
13
+ * which kind of system you reached.
14
+ *
15
+ * The credential is passed in and decides none of this. A bearer token against
16
+ * an on-prem system is ordinary, and it gets this mechanism because the caller
17
+ * asked for this connector — not because of what is in the header, and not
18
+ * because anything worked out where it was pointing.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.AdtOnPremConnector = void 0;
22
+ const CredentialAbapConnection_js_1 = require("./CredentialAbapConnection.js");
23
+ class AdtOnPremConnector extends CredentialAbapConnection_js_1.CredentialAbapConnection {
24
+ constructor(config, credential, transport, logger = null, sessionId) {
25
+ super(config, credential, transport, logger, sessionId);
26
+ }
27
+ }
28
+ exports.AdtOnPremConnector = AdtOnPremConnector;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ABAP Cloud over HTTP: the wire where a session is a resource with an address.
3
+ *
4
+ * Asked for at `/sap/bc/adt/core/http/sessions` with `x-sap-security-session:
5
+ * create` and `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on
6
+ * the address the server publishes in the answer. Reproduced from Eclipse ADT
7
+ * 3.60 and measured against a BTP trial: without that request the same
8
+ * connection gets `sap-usercontext` and `sap-XSRF_*` and no session for a lock
9
+ * to be bound to, which is how "cloud issues no SAP_SESSIONID" came to be
10
+ * believed.
11
+ *
12
+ * **Taking this transport is the consumer saying which system it is dialling.**
13
+ * Nothing here asks the server what it is. The strategy this replaces treated a
14
+ * 404 on the session resource as "then this must be on-prem" and quietly fell
15
+ * back to the platform logoff — an inference, and the only one left in this
16
+ * layer. A consumer that reaches an on-prem system with this transport has
17
+ * taken the wrong one, and finds out rather than being silently downgraded.
18
+ */
19
+ import { HttpTransport } from './HttpTransport.js';
20
+ import type { IAdtSessionContext, ICloudTransport } from './IAdtTransport.js';
21
+ export declare class CloudHttpTransport extends HttpTransport implements ICloudTransport {
22
+ readonly kind = "cloud-http";
23
+ /** Which system this wire is for. Read by the compiler, never at runtime. */
24
+ readonly system: "cloud";
25
+ /** The address this server published for our session — the only close target. */
26
+ private resource;
27
+ open(context: IAdtSessionContext): Promise<void>;
28
+ close(context: IAdtSessionContext): Promise<void>;
29
+ /**
30
+ * The `securitysession` link's href, by its `rel` rather than by position.
31
+ *
32
+ * The document carries three links — the session, the logoff resource and
33
+ * system information — and their order is not a contract.
34
+ */
35
+ private securitySessionHref;
36
+ }
37
+ //# sourceMappingURL=CloudHttpTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloudHttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/CloudHttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAa9E,qBAAa,kBACX,SAAQ,aACR,YAAW,eAAe;IAE1B,SAAkB,IAAI,gBAAgB;IAEtC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAG,OAAO,CAAU;IAEnC,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAuB;IAExB,IAAI,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDhD,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4ChE;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAW5B"}
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * ABAP Cloud over HTTP: the wire where a session is a resource with an address.
4
+ *
5
+ * Asked for at `/sap/bc/adt/core/http/sessions` with `x-sap-security-session:
6
+ * create` and `sap-adt-purpose: preflight_logon`, and given back by `DELETE` on
7
+ * the address the server publishes in the answer. Reproduced from Eclipse ADT
8
+ * 3.60 and measured against a BTP trial: without that request the same
9
+ * connection gets `sap-usercontext` and `sap-XSRF_*` and no session for a lock
10
+ * to be bound to, which is how "cloud issues no SAP_SESSIONID" came to be
11
+ * believed.
12
+ *
13
+ * **Taking this transport is the consumer saying which system it is dialling.**
14
+ * Nothing here asks the server what it is. The strategy this replaces treated a
15
+ * 404 on the session resource as "then this must be on-prem" and quietly fell
16
+ * back to the platform logoff — an inference, and the only one left in this
17
+ * layer. A consumer that reaches an on-prem system with this transport has
18
+ * taken the wrong one, and finds out rather than being silently downgraded.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.CloudHttpTransport = void 0;
22
+ const cookies_js_1 = require("../utils/cookies.js");
23
+ const timeouts_js_1 = require("../utils/timeouts.js");
24
+ const HttpTransport_js_1 = require("./HttpTransport.js");
25
+ const SESSIONS_PATH = '/sap/bc/adt/core/http/sessions';
26
+ /** The versioned media types Eclipse asks this resource for, newest first. */
27
+ const SESSION_ACCEPT = 'application/vnd.sap.adt.core.http.session.v3+xml, ' +
28
+ 'application/vnd.sap.adt.core.http.session.v2+xml, ' +
29
+ 'application/vnd.sap.adt.core.http.session.v1+xml';
30
+ const SECURITY_SESSION_REL = 'http://www.sap.com/adt/categories/core/http/sessions/securitysession';
31
+ class CloudHttpTransport extends HttpTransport_js_1.HttpTransport {
32
+ kind = 'cloud-http';
33
+ /** Which system this wire is for. Read by the compiler, never at runtime. */
34
+ system = 'cloud';
35
+ /** The address this server published for our session — the only close target. */
36
+ resource = null;
37
+ async open(context) {
38
+ try {
39
+ // Read ONCE. The contract lets a provider answer differently each time —
40
+ // a token provider renews behind the call — so two reads can build one
41
+ // request out of two different credentials.
42
+ const auth = await context.authHeaders();
43
+ const response = await this.send({
44
+ method: 'GET',
45
+ // The cache-buster is Eclipse's; kept because this must not be served
46
+ // from anything's cache — a cached session document would hand back an
47
+ // address that belongs to a session somebody else already ended.
48
+ url: `${context.baseUrl}${SESSIONS_PATH}?_=${Date.now()}`,
49
+ // Bounded: connect() waits for this one.
50
+ timeout: (0, timeouts_js_1.getTimeout)('csrf'),
51
+ headers: {
52
+ ...auth,
53
+ ...context.extraHeaders,
54
+ Accept: SESSION_ACCEPT,
55
+ Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, this.cookies() ?? undefined),
56
+ 'sap-adt-purpose': 'preflight_logon',
57
+ 'x-sap-security-session': 'create',
58
+ },
59
+ });
60
+ // Its SAP_SESSIONID is the session, so the answer is folded in before
61
+ // anything is read out of it.
62
+ context.observe(response.headers);
63
+ this.ingest(response.headers);
64
+ const resource = this.securitySessionHref(response.data);
65
+ if (!resource) {
66
+ // Answered, but published no session of its own — so there is no
67
+ // address to give one back by. Said out loud rather than worked
68
+ // around: the establishment that follows reports what a connection
69
+ // without a session means, and this is the fact it will be reporting.
70
+ this.logger?.debug(`Session resource answered ${response.status} but published no securitysession link`);
71
+ return;
72
+ }
73
+ this.resource = resource;
74
+ this.logger?.debug(`Security session opened: ${resource}`);
75
+ }
76
+ catch (error) {
77
+ // Not fatal here. If no session was opened, the establishment that
78
+ // follows says so with the whole picture; an error raised from a
79
+ // 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
+ }
82
+ }
83
+ async close(context) {
84
+ const resource = this.resource;
85
+ this.resource = null;
86
+ // Read SYNCHRONOUSLY, before the first await. A close is dispatched
87
+ // without being awaited, so the teardown's `forgetSession()` runs while
88
+ // this is suspended — and a goodbye assembled afterwards would carry no
89
+ // cookie, which is the one thing that proves the session is ours to end.
90
+ const cookies = this.cookies();
91
+ const csrf = this.csrfToken();
92
+ // Snapshotted for the same reason: the DELETE must reach the server holding
93
+ // this session, not whichever one the wire is pinned to when it goes out.
94
+ const affinity = this.affinityHeaders();
95
+ if (!resource || !cookies) {
96
+ // No address to send it to, or no cookie to prove the session is ours —
97
+ // holding the cookie is the whole permission to end one.
98
+ return;
99
+ }
100
+ try {
101
+ const auth = await context.authHeaders();
102
+ await this.sendDetached({
103
+ method: 'DELETE',
104
+ url: new URL(resource, context.baseUrl).toString(),
105
+ headers: {
106
+ ...affinity,
107
+ ...auth,
108
+ ...context.extraHeaders,
109
+ Cookie: (0, cookies_js_1.mergeCookieHeaders)(auth.Cookie, cookies),
110
+ 'x-sap-security-session': 'use',
111
+ // A state change, so the server wants the token. Without one the
112
+ // request is refused with 403 — which is still just a message the
113
+ // server did not accept, and still not something to raise from a
114
+ // teardown.
115
+ ...(csrf ? { 'x-csrf-token': csrf } : {}),
116
+ },
117
+ });
118
+ this.logger?.debug('Told the server the security session is finished');
119
+ }
120
+ catch (error) {
121
+ this.logger?.debug(`Could not tell the server the session is finished: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ }
124
+ /**
125
+ * The `securitysession` link's href, by its `rel` rather than by position.
126
+ *
127
+ * The document carries three links — the session, the logoff resource and
128
+ * system information — and their order is not a contract.
129
+ */
130
+ securitySessionHref(body) {
131
+ if (typeof body !== 'string')
132
+ return undefined;
133
+ for (const link of body.matchAll(/<[^>]*link\b([^>]*)>/g)) {
134
+ const attrs = link[1];
135
+ const rel = attrs.match(/\brel="([^"]*)"/)?.[1];
136
+ if (rel !== SECURITY_SESSION_REL)
137
+ continue;
138
+ const href = attrs.match(/\bhref="([^"]*)"/)?.[1];
139
+ if (href)
140
+ return href;
141
+ }
142
+ return undefined;
143
+ }
144
+ }
145
+ exports.CloudHttpTransport = CloudHttpTransport;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * A connection whose authentication is passed in.
3
+ *
4
+ * The shared half of the two platform connectors, and the reason the split is
5
+ * possible at all: `establishSession()` was measured to be the same work in
6
+ * every one of the five auth classes — fetch a CSRF token from
7
+ * `/sap/bc/adt/discovery`, keep it, tolerate a failure — with the only
8
+ * per-credential part being a step before it. That step is `prepare()`.
9
+ *
10
+ * So this owns the establishing call, and the credential contributes three
11
+ * things at most: what to prepare, what header to send, and what TLS material
12
+ * to present. Which system this is talking to was stated by the caller when it
13
+ * chose the subclass, and is never worked out here.
14
+ */
15
+ import type { AgentOptions } from 'node:https';
16
+ import type { IAuthProvider } from '@mcp-abap-adt/interfaces';
17
+ import type { SapConfig } from '../config/sapConfig.js';
18
+ import type { ILogger } from '../logger.js';
19
+ import { AbstractAbapConnection } from './AbstractAbapConnection.js';
20
+ import { type IAdtTransport } from './IAdtTransport.js';
21
+ export declare abstract class CredentialAbapConnection<TCredential extends IAuthProvider = IAuthProvider> extends AbstractAbapConnection {
22
+ /** Public because the type is the point: a caller can reach what it gave. */
23
+ readonly credential: TCredential;
24
+ constructor(config: SapConfig,
25
+ /** Public because the type is the point: a caller can reach what it gave. */
26
+ credential: TCredential, transport: IAdtTransport, logger?: ILogger | null, sessionId?: string);
27
+ /**
28
+ * Whatever the credential needs before the first request goes out.
29
+ *
30
+ * Runs before the preflight, not inside the establishing call: building the
31
+ * transport reads the TLS options, and a certificate whose material had not
32
+ * been loaded yet rejected the connect while the argument was still being
33
+ * evaluated — outside every catch, with no request sent.
34
+ */
35
+ /** The header last put on the wire, so a change can be seen. */
36
+ protected prepareCredential(): Promise<void>;
37
+ /**
38
+ * Nothing here: the header is asked for asynchronously in `getAuthHeaders()`,
39
+ * because a provider can renew behind the call and a synchronous read would
40
+ * have to hold what it returned. The base's abstract member is satisfied and
41
+ * unused.
42
+ */
43
+ protected buildAuthorizationHeader(): string;
44
+ /**
45
+ * The credential's cookies travel with its header.
46
+ *
47
+ * On every path, not only on ordinary requests: a SAML session that is not
48
+ * presented to the session preflight and the establishing call is a session
49
+ * the server never sees us in.
50
+ */
51
+ getAuthHeaders(): Promise<Record<string, string>>;
52
+ protected getHttpsAgentOptions(): AgentOptions;
53
+ protected establishSession(): Promise<void>;
54
+ }
55
+ //# sourceMappingURL=CredentialAbapConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CredentialAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/CredentialAbapConnection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAGV,aAAa,EACd,MAAM,0BAA0B,CAAC;AAClC,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;AACrE,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,oBAAoB,CAAC;AASnE,8BAAsB,wBAAwB,CAC5C,WAAW,SAAS,aAAa,GAAG,aAAa,CACjD,SAAQ,sBAAsB;IAG5B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,WAAW;gBAFhC,MAAM,EAAE,SAAS;IACjB,6EAA6E;IACpE,UAAU,EAAE,WAAW,EAChC,SAAS,EAAE,aAAa,EACxB,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,SAAS,CAAC,EAAE,MAAM;IAKpB;;;;;;;OAOG;IACH,gEAAgE;cAEvC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3D;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAI5C;;;;;;OAMG;IACY,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;cAiB7C,oBAAoB,IAAI,YAAY;cAIvC,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CAoDlD"}
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * A connection whose authentication is passed in.
4
+ *
5
+ * The shared half of the two platform connectors, and the reason the split is
6
+ * possible at all: `establishSession()` was measured to be the same work in
7
+ * every one of the five auth classes — fetch a CSRF token from
8
+ * `/sap/bc/adt/discovery`, keep it, tolerate a failure — with the only
9
+ * per-credential part being a step before it. That step is `prepare()`.
10
+ *
11
+ * So this owns the establishing call, and the credential contributes three
12
+ * things at most: what to prepare, what header to send, and what TLS material
13
+ * to present. Which system this is talking to was stated by the caller when it
14
+ * chose the subclass, and is never worked out here.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.CredentialAbapConnection = void 0;
18
+ const AbstractAbapConnection_js_1 = require("./AbstractAbapConnection.js");
19
+ const IAdtTransport_js_1 = require("./IAdtTransport.js");
20
+ /** A 401 from the server, whatever transport shape it arrives in. */
21
+ function isUnauthorized(error) {
22
+ const status = error?.response
23
+ ?.status;
24
+ return status === 401;
25
+ }
26
+ class CredentialAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
27
+ credential;
28
+ constructor(config,
29
+ /** Public because the type is the point: a caller can reach what it gave. */
30
+ credential, transport, logger = null, sessionId) {
31
+ super(config, transport, logger, sessionId);
32
+ this.credential = credential;
33
+ }
34
+ /**
35
+ * Whatever the credential needs before the first request goes out.
36
+ *
37
+ * Runs before the preflight, not inside the establishing call: building the
38
+ * transport reads the TLS options, and a certificate whose material had not
39
+ * been loaded yet rejected the connect while the argument was still being
40
+ * evaluated — outside every catch, with no request sent.
41
+ */
42
+ /** The header last put on the wire, so a change can be seen. */
43
+ async prepareCredential() {
44
+ await this.credential.prepare();
45
+ }
46
+ /**
47
+ * Nothing here: the header is asked for asynchronously in `getAuthHeaders()`,
48
+ * because a provider can renew behind the call and a synchronous read would
49
+ * have to hold what it returned. The base's abstract member is satisfied and
50
+ * unused.
51
+ */
52
+ buildAuthorizationHeader() {
53
+ return '';
54
+ }
55
+ /**
56
+ * The credential's cookies travel with its header.
57
+ *
58
+ * On every path, not only on ordinary requests: a SAML session that is not
59
+ * presented to the session preflight and the establishing call is a session
60
+ * the server never sees us in.
61
+ */
62
+ async getAuthHeaders() {
63
+ const headers = await super.getAuthHeaders();
64
+ // Asked per request, never held: a provider renews behind this call, and a
65
+ // value kept here would be the stale one.
66
+ const authorization = await this.credential.authorizationHeader();
67
+ if (authorization) {
68
+ headers.Authorization = authorization;
69
+ }
70
+ const cookies = this.credential.cookies();
71
+ if (cookies) {
72
+ headers.Cookie = cookies;
73
+ }
74
+ return headers;
75
+ }
76
+ getHttpsAgentOptions() {
77
+ return this.credential.transportMaterial();
78
+ }
79
+ async establishSession() {
80
+ try {
81
+ // The wire establishes itself, always. What that means is the wire's:
82
+ // HTTP earns a CSRF token and the cookies that name the session; an RFC
83
+ // conversation was opened before this and already IS the session, so it
84
+ // does nothing and holds no token. Demanding one here was what made
85
+ // `connect()` impossible over RFC.
86
+ //
87
+ // There is no second path. A credential that wanted to run the exchange
88
+ // itself would need the connection to ask which of the two does the work,
89
+ // and a credential whose way in IS a round trip does not need that: the
90
+ // wire asks `authHeaders()` PER ATTEMPT, so a one-shot token is offered
91
+ // on the establishing call and withheld afterwards by the credential
92
+ // itself, with nobody deciding anything.
93
+ await this.transport.establish({
94
+ baseUrl: await this.getBaseUrl(),
95
+ authHeaders: () => this.getAuthHeaders(),
96
+ extraHeaders: { 'sap-adt-connection-id': this.getSessionId() ?? '' },
97
+ observe: (headers) => this.observeResponse(headers),
98
+ isFatal: (error) => this.isSessionVerdict(error),
99
+ });
100
+ this.logger?.debug('Connected', {
101
+ credential: this.credential.kind,
102
+ hasCsrfToken: !!this.getCsrfToken(),
103
+ hasCookies: !!this.getCookies(),
104
+ });
105
+ }
106
+ catch (error) {
107
+ this.logger?.warn(`Could not establish (${this.credential.kind}): ${error instanceof Error ? error.message : String(error)}`);
108
+ // A rejecting response can still carry the cookies that matter; the wire
109
+ // folds them in as it establishes, so nothing is read out of the error
110
+ // here beyond saying whether any arrived.
111
+ if ((0, IAdtTransport_js_1.refusalOf)(error)?.headers) {
112
+ this.logger?.debug(`Cookies after a failed establishment: ${this.getCookies() ? 'present' : 'none'}`);
113
+ }
114
+ // Rethrow: a resolved connect() must mean a usable session exists.
115
+ //
116
+ // This warned and resolved, on the reasoning that "the first request will
117
+ // retry" — true while establishment could happen lazily, and left behind
118
+ // when connect() became mandatory. Swallowing now leaves a connection
119
+ // that reports success and holds nothing; worse, it skips the debris
120
+ // clearing in establishAndCommit()'s catch, so the Set-Cookie that came
121
+ // with the 401 survives as the session identity. A cookie is proof to
122
+ // every credential that auth is settled, so the NEXT connect() goes out
123
+ // with no credentials at all and fails for an unrelated reason.
124
+ throw error;
125
+ }
126
+ }
127
+ }
128
+ exports.CredentialAbapConnection = CredentialAbapConnection;