@mcp-abap-adt/connection 5.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 (91) hide show
  1. package/CHANGELOG.md +134 -1
  2. package/README.md +204 -48
  3. package/dist/auth/providers.d.ts +38 -9
  4. package/dist/auth/providers.d.ts.map +1 -1
  5. package/dist/auth/providers.js +46 -3
  6. package/dist/connection/AbstractAbapConnection.d.ts +83 -136
  7. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  8. package/dist/connection/AbstractAbapConnection.js +241 -763
  9. package/dist/connection/AdtCloudConnector.d.ts +12 -7
  10. package/dist/connection/AdtCloudConnector.d.ts.map +1 -1
  11. package/dist/connection/AdtCloudConnector.js +2 -6
  12. package/dist/connection/AdtOnPremConnector.d.ts +20 -7
  13. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -1
  14. package/dist/connection/AdtOnPremConnector.js +2 -6
  15. package/dist/connection/CloudHttpTransport.d.ts +37 -0
  16. package/dist/connection/CloudHttpTransport.d.ts.map +1 -0
  17. package/dist/{session/CloudSecuritySessionStrategy.js → connection/CloudHttpTransport.js} +59 -49
  18. package/dist/connection/CredentialAbapConnection.d.ts +8 -41
  19. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/CredentialAbapConnection.js +44 -111
  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 +7 -8
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +21 -18
  42. package/dist/utils/timeouts.d.ts +6 -19
  43. package/dist/utils/timeouts.d.ts.map +1 -1
  44. package/dist/utils/timeouts.js +6 -22
  45. package/docs/INDEX.md +5 -2
  46. package/docs/INSTALLATION.md +28 -10
  47. package/docs/JWT_AUTH_TOOLS.md +20 -4
  48. package/docs/MIGRATION-6.0.md +359 -0
  49. package/docs/SCOPE.md +1 -1
  50. package/docs/STATEFUL_SESSION_GUIDE.md +86 -17
  51. package/docs/USAGE.md +260 -119
  52. package/examples/basic-connection.js +15 -3
  53. package/examples/jwt-with-token-refresh.js +15 -7
  54. package/examples/saml-connection.js +15 -2
  55. package/package.json +12 -10
  56. package/dist/__tests__/helpers/session.d.ts +0 -15
  57. package/dist/__tests__/helpers/session.d.ts.map +0 -1
  58. package/dist/__tests__/helpers/session.js +0 -19
  59. package/dist/auth/IAuthProvider.d.ts +0 -84
  60. package/dist/auth/IAuthProvider.d.ts.map +0 -1
  61. package/dist/auth/IAuthProvider.js +0 -21
  62. package/dist/connection/BaseAbapConnection.d.ts +0 -29
  63. package/dist/connection/BaseAbapConnection.d.ts.map +0 -1
  64. package/dist/connection/BaseAbapConnection.js +0 -81
  65. package/dist/connection/CertificateAbapConnection.d.ts +0 -35
  66. package/dist/connection/CertificateAbapConnection.d.ts.map +0 -1
  67. package/dist/connection/CertificateAbapConnection.js +0 -91
  68. package/dist/connection/JwtAbapConnection.d.ts +0 -131
  69. package/dist/connection/JwtAbapConnection.d.ts.map +0 -1
  70. package/dist/connection/JwtAbapConnection.js +0 -376
  71. package/dist/connection/KerberosAbapConnection.d.ts +0 -32
  72. package/dist/connection/KerberosAbapConnection.d.ts.map +0 -1
  73. package/dist/connection/KerberosAbapConnection.js +0 -128
  74. package/dist/connection/RfcAbapConnection.d.ts +0 -44
  75. package/dist/connection/RfcAbapConnection.d.ts.map +0 -1
  76. package/dist/connection/RfcAbapConnection.js +0 -324
  77. package/dist/connection/SamlAbapConnection.d.ts +0 -31
  78. package/dist/connection/SamlAbapConnection.d.ts.map +0 -1
  79. package/dist/connection/SamlAbapConnection.js +0 -81
  80. package/dist/connection/connectionFactory.d.ts +0 -25
  81. package/dist/connection/connectionFactory.d.ts.map +0 -1
  82. package/dist/connection/connectionFactory.js +0 -84
  83. package/dist/session/CloudSecuritySessionStrategy.d.ts +0 -32
  84. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +0 -1
  85. package/dist/session/IcfSessionStrategy.d.ts +0 -27
  86. package/dist/session/IcfSessionStrategy.d.ts.map +0 -1
  87. package/dist/session/IcfSessionStrategy.js +0 -62
  88. package/dist/session/SessionStrategy.d.ts +0 -86
  89. package/dist/session/SessionStrategy.d.ts.map +0 -1
  90. package/dist/session/SessionStrategy.js +0 -33
  91. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +0 -108
@@ -0,0 +1,178 @@
1
+ /**
2
+ * An ADT request carried over HTTP.
3
+ *
4
+ * The obvious one, and until now the one that did not exist as a thing: RFC was
5
+ * an object while HTTP was a branch inside `getAxiosInstance()`. An axis with a
6
+ * code path on one end cannot be named in a type — a default type parameter has
7
+ * nothing to point at — so this is what makes the two ends symmetrical.
8
+ *
9
+ * No `open()` or `close()`. A request opens its own socket and there is no
10
+ * conversation to establish or give back; those members exist for a transport
11
+ * that owns a wire, which is RFC.
12
+ *
13
+ * **Where the two axes touch.** TLS client-certificate material comes from the
14
+ * CREDENTIAL — a certificate authenticates through the transport rather than
15
+ * through a header — and configures this. It is taken as a thunk rather than a
16
+ * value so it is read when the client is first built, by which time the
17
+ * credential has been prepared and knows what it holds. Read in a constructor,
18
+ * it would be whatever was loaded before the connection started, which for a
19
+ * certificate is nothing.
20
+ */
21
+ import { type AgentOptions } from 'node:https';
22
+ import type { ILogger } from '../logger.js';
23
+ import type { IAdtEstablishContext, IAdtSessionContext, IAdtTransport, IAdtTransportRequest, IAdtTransportResponse } from './IAdtTransport.js';
24
+ export declare class HttpTransport implements IAdtTransport {
25
+ private readonly agentOptions;
26
+ protected readonly logger: ILogger | null;
27
+ /**
28
+ * `client` because SAP answers `sap-usercontext` with the system default
29
+ * rather than the client that was asked for, and later requests then route
30
+ * to a client the caller never named — on a read-only one, every write
31
+ * comes back 403.
32
+ */
33
+ private readonly options;
34
+ /**
35
+ * A real wire, usable on its own: it sends, holds a jar, and earns a CSRF
36
+ * token. What the two subclasses add is not the wire but the SESSION
37
+ * MECHANISM — the cloud resource, the platform logoff — which is a different
38
+ * question and the one the consumer answers by taking one of them.
39
+ *
40
+ * A bare one therefore does something well defined: it carries requests and
41
+ * never asks for or gives back a session. Which system a connector is for is
42
+ * enforced by its type parameter, not by this being unusable.
43
+ */
44
+ readonly kind: string;
45
+ private instance;
46
+ /**
47
+ * The wire's own state.
48
+ *
49
+ * Here rather than on the connection because it is what HTTP *is*: a cookie
50
+ * jar, the session those cookies address, and the application server the
51
+ * session lives on. A connection that held these would be holding them for
52
+ * every transport, including one that can never fill them.
53
+ */
54
+ private readonly jar;
55
+ private combined;
56
+ private appServer;
57
+ private token;
58
+ constructor(agentOptions?: () => AgentOptions, logger?: ILogger | null,
59
+ /**
60
+ * `client` because SAP answers `sap-usercontext` with the system default
61
+ * rather than the client that was asked for, and later requests then route
62
+ * to a client the caller never named — on a read-only one, every write
63
+ * comes back 403.
64
+ */
65
+ options?: {
66
+ client?: string;
67
+ baseUrl?: string;
68
+ });
69
+ /**
70
+ * Fold a response into the wire state.
71
+ *
72
+ * Says nothing about what the change MEANS — whether a new session id is an
73
+ * establishment or a replacement is a question about the session's lifetime,
74
+ * which this has no way to answer and no business answering.
75
+ */
76
+ ingest(headers?: Record<string, unknown>): void;
77
+ /** What to put on the `Cookie` header, or nothing if the jar is empty. */
78
+ cookies(): string | null;
79
+ /**
80
+ * Which ABAP session this wire is on.
81
+ *
82
+ * `SAP_SESSIONID` and nothing else: `sap-usercontext` is ours and does not
83
+ * name a session, and a fingerprint that moved when it did would report a
84
+ * replacement every time the client was re-asserted.
85
+ */
86
+ sessionFingerprint(): Map<string, string>;
87
+ /**
88
+ * A session exists when the server named one.
89
+ *
90
+ * `SAP_SESSIONID` is what a lock is bound to, so its absence means there is
91
+ * nothing to hold one — the connection can read and can keep nothing.
92
+ */
93
+ sessionEstablished(): boolean;
94
+ /**
95
+ * Headers that keep this connection on the server its session lives on.
96
+ *
97
+ * `sap-adt-saplb: fetch` asks the server to name itself — it answers on every
98
+ * request, so the binding survives a restart that moves us. `saplb` is that
99
+ * name sent back. `REDISPATCH_ON_SHUTDOWN` is what Eclipse asks for: if the
100
+ * server is going down, send us elsewhere rather than fail.
101
+ */
102
+ affinityHeaders(): Record<string, string>;
103
+ /**
104
+ * Earn a CSRF token, and with it the cookies that name the session.
105
+ *
106
+ * The token and the session are one thing on this wire: SAP binds a lock
107
+ * handle to the `SAP_SESSIONID` the same exchange sets, so a token kept
108
+ * across a new session would be presented against a session it was never
109
+ * issued for.
110
+ */
111
+ establish(context: IAdtEstablishContext): Promise<void>;
112
+ /** The token this wire earned, or nothing if it has not earned one. */
113
+ csrfToken(): string | null;
114
+ adoptCsrfToken(token: string | null): void;
115
+ /** Drop everything the wire was holding. The socket outlives none of it. */
116
+ forgetSession(): void;
117
+ /**
118
+ * Take the application server's name from a response, if it named one. Only
119
+ * ever from the server's own answer — never guessed.
120
+ */
121
+ private rememberAppServer;
122
+ /**
123
+ * Everything this wire adds to a request of its own accord: the cookies it
124
+ * holds, and the headers that keep it on the server its session lives on.
125
+ *
126
+ * Here rather than on the connection because they are HTTP's — a cookie jar
127
+ * and a dispatcher to stay bound to — and a connection that threaded them
128
+ * would be threading them for every wire, including one that has neither.
129
+ *
130
+ * MERGED with whatever the caller set: a SAML session IS the credential's
131
+ * cookie, and replacing it would send the request out unauthenticated while
132
+ * looking like it carried a session.
133
+ */
134
+ private dress;
135
+ /** A path becomes an address; anything already absolute is left alone. */
136
+ private address;
137
+ private client;
138
+ /**
139
+ * Nothing to ask for: an HTTP session arrives with the establishing call.
140
+ *
141
+ * Empty rather than absent. "There is no session resource here" is a fact
142
+ * about this wire, and a fact is stated, not left for a caller to discover by
143
+ * checking whether the method exists.
144
+ */
145
+ open(_context: IAdtSessionContext): Promise<void>;
146
+ /**
147
+ * Nothing to give back at this level.
148
+ *
149
+ * The two concrete wires override it — the cloud one DELETEs the session
150
+ * resource, the on-prem one sends the platform logoff. A bare HTTP wire has
151
+ * neither, and says so by doing nothing.
152
+ */
153
+ close(_context: IAdtSessionContext): Promise<void>;
154
+ /**
155
+ * Throws for a status the request does not admit — by doing nothing, because
156
+ * that is already what axios does, and `AxiosError` already carries
157
+ * `response`. The contract was written to describe this behaviour rather than
158
+ * to add it.
159
+ */
160
+ send(request: IAdtTransportRequest): Promise<IAdtTransportResponse>;
161
+ /**
162
+ * Send EXACTLY these headers, with nothing of the wire's live state mixed in.
163
+ *
164
+ * For a goodbye, and only for a goodbye. `disconnect()` dispatches the logoff
165
+ * without awaiting it, so the request is still being assembled while the
166
+ * connection is already free to `connect()` again — and by the time it goes
167
+ * out, the jar can hold a different session. Dressing it then merges the LIVE
168
+ * `SAP_SESSIONID` over the snapshot, and `mergeCookieHeaders` lets the later
169
+ * value win on a repeated name, so the goodbye for the old session arrives
170
+ * addressed to the new one and closes it.
171
+ *
172
+ * Reading the cookies synchronously is necessary and was not sufficient: the
173
+ * snapshot survived only until the send path put the jar back on top of it.
174
+ */
175
+ protected sendDetached(request: IAdtTransportRequest): Promise<IAdtTransportResponse>;
176
+ private dispatch;
177
+ }
178
+ //# sourceMappingURL=HttpTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpTransport.d.ts","sourceRoot":"","sources":["../../src/connection/HttpTransport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAS5B,qBAAa,aAAc,YAAW,aAAa;IA6B/C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IACzC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO;IApC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAA8B;IAE9C;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,KAAK,CAAuB;gBAGjB,YAAY,GAAE,MAAM,YAAyB,EAC3C,MAAM,GAAE,OAAO,GAAG,IAAW;IAChD;;;;;OAKG;IACc,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAGtE;;;;;;OAMG;IACH,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAgC/C,0EAA0E;IAC1E,OAAO,IAAI,MAAM,GAAG,IAAI;IAIxB;;;;;;OAMG;IACH,kBAAkB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAQzC;;;;;OAKG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;;;;;;OAOG;IACH,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IASzC;;;;;;;OAOG;IACG,SAAS,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG7D,uEAAuE;IACvE,SAAS,IAAI,MAAM,GAAG,IAAI;IAI1B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI1C,4EAA4E;IAC5E,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,KAAK;IAab,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,MAAM;IAuBd;;;;;;OAMG;IACG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAEvD;;;;;;OAMG;IACG,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAExD;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAIzE;;;;;;;;;;;;;OAaG;cACa,YAAY,CAC1B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAInB,QAAQ;CA4BvB"}
@@ -0,0 +1,402 @@
1
+ "use strict";
2
+ /**
3
+ * An ADT request carried over HTTP.
4
+ *
5
+ * The obvious one, and until now the one that did not exist as a thing: RFC was
6
+ * an object while HTTP was a branch inside `getAxiosInstance()`. An axis with a
7
+ * code path on one end cannot be named in a type — a default type parameter has
8
+ * nothing to point at — so this is what makes the two ends symmetrical.
9
+ *
10
+ * No `open()` or `close()`. A request opens its own socket and there is no
11
+ * conversation to establish or give back; those members exist for a transport
12
+ * that owns a wire, which is RFC.
13
+ *
14
+ * **Where the two axes touch.** TLS client-certificate material comes from the
15
+ * CREDENTIAL — a certificate authenticates through the transport rather than
16
+ * through a header — and configures this. It is taken as a thunk rather than a
17
+ * value so it is read when the client is first built, by which time the
18
+ * credential has been prepared and knows what it holds. Read in a constructor,
19
+ * it would be whatever was loaded before the connection started, which for a
20
+ * certificate is nothing.
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.HttpTransport = void 0;
27
+ const node_https_1 = require("node:https");
28
+ const axios_1 = __importDefault(require("axios"));
29
+ const cookies_js_1 = require("../utils/cookies.js");
30
+ const csrfConfig_js_1 = require("./csrfConfig.js");
31
+ /** A 404 there means the system has no such endpoint, not that it is unwell. */
32
+ function absentEndpoint(error) {
33
+ const status = error
34
+ ?.response?.status;
35
+ return status === 404 || status === 501;
36
+ }
37
+ class HttpTransport {
38
+ agentOptions;
39
+ logger;
40
+ options;
41
+ /**
42
+ * A real wire, usable on its own: it sends, holds a jar, and earns a CSRF
43
+ * token. What the two subclasses add is not the wire but the SESSION
44
+ * MECHANISM — the cloud resource, the platform logoff — which is a different
45
+ * question and the one the consumer answers by taking one of them.
46
+ *
47
+ * A bare one therefore does something well defined: it carries requests and
48
+ * never asks for or gives back a session. Which system a connector is for is
49
+ * enforced by its type parameter, not by this being unusable.
50
+ */
51
+ kind = 'http';
52
+ instance = null;
53
+ /**
54
+ * The wire's own state.
55
+ *
56
+ * Here rather than on the connection because it is what HTTP *is*: a cookie
57
+ * jar, the session those cookies address, and the application server the
58
+ * session lives on. A connection that held these would be holding them for
59
+ * every transport, including one that can never fill them.
60
+ */
61
+ jar = new Map();
62
+ combined = null;
63
+ appServer = null;
64
+ token = null;
65
+ constructor(agentOptions = () => ({}), logger = null,
66
+ /**
67
+ * `client` because SAP answers `sap-usercontext` with the system default
68
+ * rather than the client that was asked for, and later requests then route
69
+ * to a client the caller never named — on a read-only one, every write
70
+ * comes back 403.
71
+ */
72
+ options = {}) {
73
+ this.agentOptions = agentOptions;
74
+ this.logger = logger;
75
+ this.options = options;
76
+ }
77
+ /**
78
+ * Fold a response into the wire state.
79
+ *
80
+ * Says nothing about what the change MEANS — whether a new session id is an
81
+ * establishment or a replacement is a question about the session's lifetime,
82
+ * which this has no way to answer and no business answering.
83
+ */
84
+ ingest(headers) {
85
+ if (!headers)
86
+ return;
87
+ this.rememberAppServer(headers);
88
+ const setCookie = headers['set-cookie'];
89
+ // Nothing was set, so nothing is folded in — not even the client, which is
90
+ // an assertion ON the cookies a response brought rather than a cookie of
91
+ // its own. Asserting it here would give a wire that has never been issued
92
+ // anything a `Cookie` header to send, and the code above reads a non-empty
93
+ // jar as "this connection holds something".
94
+ if (!setCookie)
95
+ return;
96
+ for (const entry of Array.isArray(setCookie) ? setCookie : [setCookie]) {
97
+ if (typeof entry !== 'string')
98
+ continue;
99
+ const [nameValue] = entry.split(';');
100
+ if (!nameValue)
101
+ continue;
102
+ const [name, ...rest] = nameValue.split('=');
103
+ const trimmed = name?.trim();
104
+ if (!trimmed)
105
+ continue;
106
+ this.jar.set(trimmed, rest.join('=').trim());
107
+ }
108
+ if (this.options.client) {
109
+ this.jar.set('sap-usercontext', `sap-client=${this.options.client}`);
110
+ }
111
+ if (this.jar.size === 0)
112
+ return;
113
+ const combined = Array.from(this.jar.entries())
114
+ .map(([name, value]) => (value ? `${name}=${value}` : name))
115
+ .join('; ');
116
+ if (combined)
117
+ this.combined = combined;
118
+ }
119
+ /** What to put on the `Cookie` header, or nothing if the jar is empty. */
120
+ cookies() {
121
+ return this.combined;
122
+ }
123
+ /**
124
+ * Which ABAP session this wire is on.
125
+ *
126
+ * `SAP_SESSIONID` and nothing else: `sap-usercontext` is ours and does not
127
+ * name a session, and a fingerprint that moved when it did would report a
128
+ * replacement every time the client was re-asserted.
129
+ */
130
+ sessionFingerprint() {
131
+ const fingerprint = new Map();
132
+ for (const [name, value] of this.jar) {
133
+ if (name.startsWith('SAP_SESSIONID'))
134
+ fingerprint.set(name, value);
135
+ }
136
+ return fingerprint;
137
+ }
138
+ /**
139
+ * A session exists when the server named one.
140
+ *
141
+ * `SAP_SESSIONID` is what a lock is bound to, so its absence means there is
142
+ * nothing to hold one — the connection can read and can keep nothing.
143
+ */
144
+ sessionEstablished() {
145
+ return this.sessionFingerprint().size > 0;
146
+ }
147
+ /**
148
+ * Headers that keep this connection on the server its session lives on.
149
+ *
150
+ * `sap-adt-saplb: fetch` asks the server to name itself — it answers on every
151
+ * request, so the binding survives a restart that moves us. `saplb` is that
152
+ * name sent back. `REDISPATCH_ON_SHUTDOWN` is what Eclipse asks for: if the
153
+ * server is going down, send us elsewhere rather than fail.
154
+ */
155
+ affinityHeaders() {
156
+ return {
157
+ 'sap-adt-saplb': 'fetch',
158
+ ...(this.appServer
159
+ ? { saplb: this.appServer, 'saplb-options': 'REDISPATCH_ON_SHUTDOWN' }
160
+ : {}),
161
+ };
162
+ }
163
+ /**
164
+ * Earn a CSRF token, and with it the cookies that name the session.
165
+ *
166
+ * The token and the session are one thing on this wire: SAP binds a lock
167
+ * handle to the `SAP_SESSIONID` the same exchange sets, so a token kept
168
+ * across a new session would be presented against a session it was never
169
+ * issued for.
170
+ */
171
+ async establish(context) {
172
+ // Idempotent. Asked again before a mutation, a wire that already holds a
173
+ // token must not spend a round trip earning another — SAP binds the lock
174
+ // handle to the session the token came with, so a second exchange would
175
+ // move the session out from under a lock taken against the first.
176
+ if (this.token)
177
+ return;
178
+ const base = context.baseUrl.endsWith('/')
179
+ ? context.baseUrl.slice(0, -1)
180
+ : context.baseUrl;
181
+ const endpoints = [
182
+ `${base}${csrfConfig_js_1.CSRF_CONFIG.ENDPOINT}`,
183
+ // BASIS < 7.52 has no /sap/bc/adt/core/discovery.
184
+ `${base}${csrfConfig_js_1.CSRF_CONFIG.FALLBACK_ENDPOINT}`,
185
+ ];
186
+ const retries = context.retries ?? csrfConfig_js_1.CSRF_CONFIG.RETRY_COUNT;
187
+ const delay = context.retryDelayMs ?? csrfConfig_js_1.CSRF_CONFIG.RETRY_DELAY;
188
+ let last;
189
+ for (const [index, url] of endpoints.entries()) {
190
+ // The fallback exists for a system that HAS no
191
+ // `/sap/bc/adt/core/discovery` — BASIS < 7.52 — which the server says by
192
+ // answering 404 there. Trying it after a refused connection or a timeout
193
+ // asks a host that is not answering to answer a different path, which
194
+ // doubles the wait before the caller is told what is actually wrong.
195
+ if (index > 0 && !absentEndpoint(last))
196
+ break;
197
+ for (let attempt = 0; attempt <= retries; attempt++) {
198
+ try {
199
+ // Read per attempt: a provider may renew behind the call.
200
+ const auth = await context.authHeaders();
201
+ const response = await this.send({
202
+ method: 'GET',
203
+ url,
204
+ headers: {
205
+ ...auth,
206
+ ...context.extraHeaders,
207
+ ...csrfConfig_js_1.CSRF_CONFIG.REQUIRED_HEADERS,
208
+ },
209
+ ...(context.timeoutMs !== undefined
210
+ ? { timeout: context.timeoutMs }
211
+ : {}),
212
+ });
213
+ // Handed up BEFORE the token is read: the cookies are the session,
214
+ // and a fold that only happened on success would lose the session a
215
+ // tokenless answer still opened.
216
+ context.observe(response.headers);
217
+ this.ingest(response.headers);
218
+ const token = response.headers['x-csrf-token'];
219
+ if (token) {
220
+ this.token = token;
221
+ this.logger?.debug('CSRF token obtained');
222
+ return;
223
+ }
224
+ last = new Error(csrfConfig_js_1.CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
225
+ }
226
+ catch (error) {
227
+ // Not a failed exchange: see `isFatal`. Leaves immediately, past the
228
+ // retries and past the fallback endpoint.
229
+ if (context.isFatal?.(error))
230
+ throw error;
231
+ last = error instanceof Error ? error : new Error(String(error));
232
+ const response = error.response;
233
+ if (response?.headers) {
234
+ // A refusal can still carry the cookies that matter.
235
+ context.observe(response.headers);
236
+ this.ingest(response.headers);
237
+ // …and the token itself. SAP answers 405 to a GET on some
238
+ // endpoints and puts the token in the header anyway, and other
239
+ // refusals carry one too. A retry would throw away a token the
240
+ // server already handed over.
241
+ const onError = response.headers['x-csrf-token'];
242
+ if (onError) {
243
+ this.token = onError;
244
+ this.logger?.debug('CSRF token arrived on a refusal, and is kept');
245
+ return;
246
+ }
247
+ }
248
+ }
249
+ if (attempt < retries) {
250
+ await new Promise((resolve) => setTimeout(resolve, delay));
251
+ }
252
+ }
253
+ }
254
+ // The original error, with its `response`, when there was one: the
255
+ // classification above this reads `error.response.status`, and a generic
256
+ // replacement would tell it nothing.
257
+ throw last ?? new Error(csrfConfig_js_1.CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
258
+ }
259
+ /** The token this wire earned, or nothing if it has not earned one. */
260
+ csrfToken() {
261
+ return this.token;
262
+ }
263
+ adoptCsrfToken(token) {
264
+ this.token = token;
265
+ }
266
+ /** Drop everything the wire was holding. The socket outlives none of it. */
267
+ forgetSession() {
268
+ this.jar.clear();
269
+ this.combined = null;
270
+ this.appServer = null;
271
+ // The token was issued INTO the session being dropped. Kept, it would be
272
+ // presented against a session it was never bound to.
273
+ this.token = null;
274
+ }
275
+ /**
276
+ * Take the application server's name from a response, if it named one. Only
277
+ * ever from the server's own answer — never guessed.
278
+ */
279
+ rememberAppServer(headers) {
280
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === 'sap-adt-saplb');
281
+ const value = key ? headers[key] : undefined;
282
+ if (typeof value === 'string' && value && value !== this.appServer) {
283
+ this.appServer = value;
284
+ this.logger?.debug(`Session is on application server ${value}`);
285
+ }
286
+ }
287
+ /**
288
+ * Everything this wire adds to a request of its own accord: the cookies it
289
+ * holds, and the headers that keep it on the server its session lives on.
290
+ *
291
+ * Here rather than on the connection because they are HTTP's — a cookie jar
292
+ * and a dispatcher to stay bound to — and a connection that threaded them
293
+ * would be threading them for every wire, including one that has neither.
294
+ *
295
+ * MERGED with whatever the caller set: a SAML session IS the credential's
296
+ * cookie, and replacing it would send the request out unauthenticated while
297
+ * looking like it carried a session.
298
+ */
299
+ dress(headers) {
300
+ const dressed = {
301
+ ...this.affinityHeaders(),
302
+ ...headers,
303
+ };
304
+ const merged = (0, cookies_js_1.mergeCookieHeaders)(headers?.Cookie, this.combined ?? undefined);
305
+ if (merged)
306
+ dressed.Cookie = merged;
307
+ return dressed;
308
+ }
309
+ /** A path becomes an address; anything already absolute is left alone. */
310
+ address(url) {
311
+ if (!url.startsWith('/') || !this.options.baseUrl)
312
+ return url;
313
+ const base = this.options.baseUrl.endsWith('/')
314
+ ? this.options.baseUrl.slice(0, -1)
315
+ : this.options.baseUrl;
316
+ return `${base}${url}`;
317
+ }
318
+ client() {
319
+ if (!this.instance) {
320
+ // Kept as it was: an explicit opt-IN, so a misread env var cannot quietly
321
+ // turn verification off.
322
+ const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1' ||
323
+ (process.env.TLS_REJECT_UNAUTHORIZED === '1' &&
324
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0');
325
+ this.logger?.debug(`TLS configuration: rejectUnauthorized=${rejectUnauthorized}`);
326
+ this.instance = axios_1.default.create({
327
+ httpsAgent: new node_https_1.Agent({
328
+ rejectUnauthorized,
329
+ ...this.agentOptions(),
330
+ }),
331
+ });
332
+ }
333
+ return this.instance;
334
+ }
335
+ /**
336
+ * Nothing to ask for: an HTTP session arrives with the establishing call.
337
+ *
338
+ * Empty rather than absent. "There is no session resource here" is a fact
339
+ * about this wire, and a fact is stated, not left for a caller to discover by
340
+ * checking whether the method exists.
341
+ */
342
+ async open(_context) { }
343
+ /**
344
+ * Nothing to give back at this level.
345
+ *
346
+ * The two concrete wires override it — the cloud one DELETEs the session
347
+ * resource, the on-prem one sends the platform logoff. A bare HTTP wire has
348
+ * neither, and says so by doing nothing.
349
+ */
350
+ async close(_context) { }
351
+ /**
352
+ * Throws for a status the request does not admit — by doing nothing, because
353
+ * that is already what axios does, and `AxiosError` already carries
354
+ * `response`. The contract was written to describe this behaviour rather than
355
+ * to add it.
356
+ */
357
+ async send(request) {
358
+ return this.dispatch(request, this.dress(request.headers));
359
+ }
360
+ /**
361
+ * Send EXACTLY these headers, with nothing of the wire's live state mixed in.
362
+ *
363
+ * For a goodbye, and only for a goodbye. `disconnect()` dispatches the logoff
364
+ * without awaiting it, so the request is still being assembled while the
365
+ * connection is already free to `connect()` again — and by the time it goes
366
+ * out, the jar can hold a different session. Dressing it then merges the LIVE
367
+ * `SAP_SESSIONID` over the snapshot, and `mergeCookieHeaders` lets the later
368
+ * value win on a repeated name, so the goodbye for the old session arrives
369
+ * addressed to the new one and closes it.
370
+ *
371
+ * Reading the cookies synchronously is necessary and was not sufficient: the
372
+ * snapshot survived only until the send path put the jar back on top of it.
373
+ */
374
+ async sendDetached(request) {
375
+ return this.dispatch(request, { ...request.headers });
376
+ }
377
+ async dispatch(request, headers) {
378
+ const response = await this.client()({
379
+ method: request.method,
380
+ // The connection hands over a PATH; putting a server in front of it is
381
+ // this wire's job. RFC's is to write the same path into the request line
382
+ // as it stands — handed an absolute URL there, SADT_REST_RFC_ENDPOINT
383
+ // dumps with STRING_OFFSET_TOO_LARGE. Neither can be done for both from
384
+ // above, which is why addressing sits here.
385
+ url: this.address(request.url),
386
+ headers,
387
+ ...(request.data !== undefined ? { data: request.data } : {}),
388
+ ...(request.params !== undefined ? { params: request.params } : {}),
389
+ ...(request.timeout !== undefined ? { timeout: request.timeout } : {}),
390
+ ...(request.validateStatus !== undefined
391
+ ? { validateStatus: request.validateStatus }
392
+ : {}),
393
+ });
394
+ return {
395
+ status: response.status,
396
+ statusText: response.statusText,
397
+ headers: response.headers,
398
+ data: response.data,
399
+ };
400
+ }
401
+ }
402
+ exports.HttpTransport = HttpTransport;