@mcp-abap-adt/connection 4.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +274 -1
  2. package/README.md +60 -30
  3. package/dist/auth/IAuthProvider.d.ts +84 -0
  4. package/dist/auth/IAuthProvider.d.ts.map +1 -0
  5. package/dist/auth/IAuthProvider.js +21 -0
  6. package/dist/auth/providers.d.ts +95 -0
  7. package/dist/auth/providers.d.ts.map +1 -0
  8. package/dist/auth/providers.js +140 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +222 -17
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +555 -31
  12. package/dist/connection/AdtCloudConnector.d.ts +29 -0
  13. package/dist/connection/AdtCloudConnector.d.ts.map +1 -0
  14. package/dist/connection/AdtCloudConnector.js +31 -0
  15. package/dist/connection/AdtOnPremConnector.d.ts +30 -0
  16. package/dist/connection/AdtOnPremConnector.d.ts.map +1 -0
  17. package/dist/connection/AdtOnPremConnector.js +32 -0
  18. package/dist/connection/BaseAbapConnection.d.ts +7 -1
  19. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/BaseAbapConnection.js +7 -1
  21. package/dist/connection/CertificateAbapConnection.d.ts +11 -1
  22. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/CertificateAbapConnection.js +13 -1
  24. package/dist/connection/CredentialAbapConnection.d.ts +88 -0
  25. package/dist/connection/CredentialAbapConnection.d.ts.map +1 -0
  26. package/dist/connection/CredentialAbapConnection.js +195 -0
  27. package/dist/connection/JwtAbapConnection.d.ts +23 -7
  28. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  29. package/dist/connection/JwtAbapConnection.js +26 -8
  30. package/dist/connection/KerberosAbapConnection.d.ts +9 -1
  31. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  32. package/dist/connection/KerberosAbapConnection.js +9 -1
  33. package/dist/connection/RfcAbapConnection.d.ts +0 -5
  34. package/dist/connection/RfcAbapConnection.d.ts.map +1 -1
  35. package/dist/connection/RfcAbapConnection.js +0 -7
  36. package/dist/connection/SamlAbapConnection.d.ts +7 -1
  37. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  38. package/dist/connection/SamlAbapConnection.js +7 -1
  39. package/dist/connection/connectionFactory.d.ts +16 -0
  40. package/dist/connection/connectionFactory.d.ts.map +1 -1
  41. package/dist/connection/connectionFactory.js +52 -0
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +10 -1
  45. package/dist/session/CloudSecuritySessionStrategy.d.ts +32 -0
  46. package/dist/session/CloudSecuritySessionStrategy.d.ts.map +1 -0
  47. package/dist/session/CloudSecuritySessionStrategy.js +135 -0
  48. package/dist/session/IcfSessionStrategy.d.ts +27 -0
  49. package/dist/session/IcfSessionStrategy.d.ts.map +1 -0
  50. package/dist/session/IcfSessionStrategy.js +62 -0
  51. package/dist/session/SessionLifecycle.d.ts +17 -2
  52. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  53. package/dist/session/SessionLifecycle.js +17 -2
  54. package/dist/session/SessionStrategy.d.ts +86 -0
  55. package/dist/session/SessionStrategy.d.ts.map +1 -0
  56. package/dist/session/SessionStrategy.js +33 -0
  57. package/dist/utils/cookies.d.ts +12 -0
  58. package/dist/utils/cookies.d.ts.map +1 -0
  59. package/dist/utils/cookies.js +24 -0
  60. package/dist/utils/timeouts.d.ts +16 -0
  61. package/dist/utils/timeouts.d.ts.map +1 -1
  62. package/dist/utils/timeouts.js +19 -0
  63. package/docs/INSTALLATION.md +6 -2
  64. package/docs/MIGRATION-2.0.md +1 -1
  65. package/docs/MIGRATION-5.0.md +116 -0
  66. package/docs/STATEFUL_SESSION_GUIDE.md +77 -11
  67. package/docs/USAGE.md +92 -22
  68. package/docs/superpowers/specs/2026-08-21-platform-connectors.md +108 -0
  69. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;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"}
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
- * Design: docs/superpowers/specs/2026-07-31-teardown-policy-design.md in
12
- * @mcp-abap-adt/adt-clients.
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"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * How a session is opened on the server, and how the server is told we are
4
+ * done with it.
5
+ *
6
+ * Two systems, two mechanisms, one meaning. On ABAP Cloud a session is a
7
+ * resource: it is asked for at `/sap/bc/adt/core/http/sessions` and the
8
+ * response publishes its own address, which is what a `DELETE` is sent to. On
9
+ * on-prem there is no such resource — the logon call is the establishing
10
+ * request itself, and the platform's `/sap/public/bc/icf/logoff` is how it is
11
+ * given back.
12
+ *
13
+ * **Both are notifications, not commands.** They say "we have finished with
14
+ * this session". Whether the system frees it now, later, or keeps it to reuse
15
+ * is the system's business, and nothing here checks afterwards or depends on
16
+ * the answer. Measured on a trial: the `DELETE` is answered `200` and the
17
+ * session is still listed a moment later; the logoff is answered `200` and it
18
+ * is not. Neither is a failure — they are two systems deciding differently
19
+ * about a message they both accepted.
20
+ *
21
+ * Split by what the server publishes rather than by which system we think we
22
+ * are talking to: `openSession()` asks, and a system that has no such resource
23
+ * says so. Guessing the platform would be a guess; asking is not.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SessionStrategy = void 0;
27
+ class SessionStrategy {
28
+ logger;
29
+ constructor(logger) {
30
+ this.logger = logger;
31
+ }
32
+ }
33
+ exports.SessionStrategy = SessionStrategy;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Join two `Cookie` header values, later wins on a repeated name.
3
+ *
4
+ * Used wherever a request already carries cookies and more are added: a
5
+ * credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
6
+ * so writing the session jar over the header drops the very thing the request
7
+ * authenticates with. Both belong on the wire: one says who we are, the other
8
+ * which session we are in. A name in both is the session's, because that is the
9
+ * one the server just issued.
10
+ */
11
+ export declare function mergeCookieHeaders(existing: string | undefined, incoming: string | undefined): string;
12
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/utils/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CASR"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeCookieHeaders = mergeCookieHeaders;
4
+ /**
5
+ * Join two `Cookie` header values, later wins on a repeated name.
6
+ *
7
+ * Used wherever a request already carries cookies and more are added: a
8
+ * credential can BE cookies — a SAML session is handed over as `MYSAPSSO2` —
9
+ * so writing the session jar over the header drops the very thing the request
10
+ * authenticates with. Both belong on the wire: one says who we are, the other
11
+ * which session we are in. A name in both is the session's, because that is the
12
+ * one the server just issued.
13
+ */
14
+ function mergeCookieHeaders(existing, incoming) {
15
+ const parts = [existing, incoming].filter(Boolean).join('; ');
16
+ const byName = new Map();
17
+ for (const part of parts.split(';')) {
18
+ const pair = part.trim();
19
+ if (!pair)
20
+ continue;
21
+ byName.set(pair.slice(0, pair.indexOf('=')), pair);
22
+ }
23
+ return [...byName.values()].join('; ');
24
+ }
@@ -12,4 +12,20 @@ export declare function getTimeout(type?: 'default' | 'csrf' | 'long' | number):
12
12
  * permanently dead socket hanging forever.
13
13
  */
14
14
  export declare function getCriticalSectionTimeout(): number;
15
+ /**
16
+ * How long `disconnect()` may spend telling the server the session is done.
17
+ *
18
+ * **Zero by default: after a disconnect nothing waits on the answer.** Waiting
19
+ * is for steps whose successor depends on the server having caught up — lock,
20
+ * update, unlock, activate, each needing the one before it to have landed.
21
+ * A teardown has no successor: the session is marked unneeded, the server
22
+ * reclaims it whenever it reclaims it, and a caller blocked on that round trip
23
+ * has bought nothing while holding up the serializing tail, where every later
24
+ * connect and disconnect queues behind it.
25
+ *
26
+ * The knob exists for a caller that wants a bounded wait anyway — a test
27
+ * asserting the logoff landed, a script that would rather see the failure —
28
+ * and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
29
+ */
30
+ export declare function getReleaseDeadline(): number;
15
31
  //# sourceMappingURL=timeouts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
1
+ {"version":3,"file":"timeouts.d.ts","sourceRoot":"","sources":["../../src/utils/timeouts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/D,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,wBAAgB,gBAAgB,IAAI,cAAc,CAajD;AAED,wBAAgB,UAAU,CACxB,IAAI,GAAE,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAkB,GACrD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getTimeoutConfig = getTimeoutConfig;
4
4
  exports.getTimeout = getTimeout;
5
5
  exports.getCriticalSectionTimeout = getCriticalSectionTimeout;
6
+ exports.getReleaseDeadline = getReleaseDeadline;
6
7
  function getTimeoutConfig() {
7
8
  const defaultTimeout = parseInt(process.env.SAP_TIMEOUT_DEFAULT || '45000', 10);
8
9
  const csrfTimeout = parseInt(process.env.SAP_TIMEOUT_CSRF || '15000', 10);
@@ -32,3 +33,21 @@ function getTimeout(type = 'default') {
32
33
  function getCriticalSectionTimeout() {
33
34
  return parseInt(process.env.SAP_TIMEOUT_CRITICAL || '600000', 10);
34
35
  }
36
+ /**
37
+ * How long `disconnect()` may spend telling the server the session is done.
38
+ *
39
+ * **Zero by default: after a disconnect nothing waits on the answer.** Waiting
40
+ * is for steps whose successor depends on the server having caught up — lock,
41
+ * update, unlock, activate, each needing the one before it to have landed.
42
+ * A teardown has no successor: the session is marked unneeded, the server
43
+ * reclaims it whenever it reclaims it, and a caller blocked on that round trip
44
+ * has bought nothing while holding up the serializing tail, where every later
45
+ * connect and disconnect queues behind it.
46
+ *
47
+ * The knob exists for a caller that wants a bounded wait anyway — a test
48
+ * asserting the logoff landed, a script that would rather see the failure —
49
+ * and is theirs to set per call via `ISessionLifecycleAware.disconnect`.
50
+ */
51
+ function getReleaseDeadline() {
52
+ return parseInt(process.env.SAP_RELEASE_DEADLINE_MS || '0', 10);
53
+ }
@@ -163,7 +163,9 @@ const logger = {
163
163
  debug: (msg) => console.log('[DEBUG]', msg),
164
164
  };
165
165
 
166
- const connection = createAbapConnection(config, logger);
166
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
167
+ system: 'onprem', // or 'cloud' — said by you, never detected
168
+ });
167
169
 
168
170
  connection.connect()
169
171
  .then(() =>
@@ -231,7 +233,9 @@ const logger: ILogger = {
231
233
  debug: (msg: string) => console.log(msg),
232
234
  };
233
235
 
234
- const connection = createAbapConnection(config, logger);
236
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
237
+ system: 'onprem', // or 'cloud' — said by you, never detected
238
+ });
235
239
  ```
236
240
 
237
241
  ## Troubleshooting
@@ -46,7 +46,7 @@ later, on the first request, as an unrelated-looking 401.
46
46
 
47
47
  ### 3. Requests are refused after a teardown
48
48
 
49
- `disconnect()` and `reset()` stop the connection from serving requests until the
49
+ `disconnect()` stops the connection from serving requests until the
50
50
  next `connect()`. An in-flight request is not cut off and not waited for either:
51
51
  it runs to completion, and its result is fenced so it cannot touch a session
52
52
  established since.
@@ -0,0 +1,116 @@
1
+ # Migrating to 5.0.0
2
+
3
+ Two things changed, and the second is why the first was possible.
4
+
5
+ **A connection now closes what it opened.** `disconnect()` tells the server the
6
+ session is finished instead of only dropping the cookie, and `connect()` fails
7
+ when the server opened no session rather than handing back a connection whose
8
+ first lock would be dead.
9
+
10
+ **The class you take says which system you are dialling, not which credential
11
+ you hold.** `AdtOnPremConnector` and `AdtCloudConnector` are handed an auth
12
+ provider; the five auth classes still work and are deprecated.
13
+
14
+ ---
15
+
16
+ ## If you use `createAbapConnection()`
17
+
18
+ It still works, unchanged, and now warns once per call that the connection is
19
+ being chosen from `authType`. Say which system instead:
20
+
21
+ ```ts
22
+ // before
23
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher);
24
+
25
+ // after
26
+ const conn = createAbapConnection(config, logger, undefined, tokenRefresher, {
27
+ system: 'cloud', // or 'onprem'
28
+ });
29
+ ```
30
+
31
+ Nothing is detected. The two systems do not manage sessions the same way, and
32
+ asking the server which it is does not work: `/sap/bc/adt/core/http/sessions`
33
+ answers on on-prem too, and its `DELETE` there leaves the session open while the
34
+ platform logoff removes it. Only you know where you are pointing.
35
+
36
+ ## If you build a connection class directly
37
+
38
+ ```ts
39
+ // before // after
40
+ new BaseAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new BasicAuthProvider(user, pass), log)
41
+ new JwtAbapConnection(cfg, log, id, refr) new AdtCloudConnector(cfg, new TokenAuthProvider(refr), log)
42
+ new SamlAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new SamlAuthProvider(cookies), log)
43
+ new CertificateAbapConnection(cfg, log) new AdtOnPremConnector(cfg, new CertificateAuthProvider(loader, cfg), log)
44
+ ```
45
+
46
+ `KerberosAbapConnection` has no provider yet — its SPNEGO exchange *is* the CSRF
47
+ fetch — so keep using the class. See #35.
48
+
49
+ The old classes are not removed and not scheduled for removal here.
50
+
51
+ ## `reset()` is gone — BREAKING
52
+
53
+ There is no local-only discard, because there is no local-only session: it lives
54
+ on the server, and dropping the cookie leaves it there.
55
+
56
+ ```ts
57
+ conn.reset(); // before
58
+ await conn.disconnect(); // after — and connect() again to carry on
59
+ ```
60
+
61
+ A caller that does not want to wait simply does not `await` it, which is what
62
+ `reset()` was mostly used for. `RfcAbapConnection` keeps `close()`.
63
+
64
+ ## `connect()` can now fail where it used to warn — BREAKING
65
+
66
+ When the server authenticates the request but opens no session, `connect()`
67
+ rejects with `ADT_NOT_CONNECTED` and a message saying what happened, what still
68
+ works, and the usual cause. It used to log a warning and hand the connection
69
+ back — and the failure then surfaced a request later, as `400 Session not found`
70
+ with the object half-edited.
71
+
72
+ Verified rather than assumed: a connection that received no `SAP_SESSIONID` is
73
+ listed in the server's own session list as **nothing at all**.
74
+
75
+ Nothing is retried on your behalf. Whether to wait, retry, or release sessions
76
+ the user still holds depends on things only you know.
77
+
78
+ ## `disconnect()` now makes a network call
79
+
80
+ It tells the server the session is finished. Two consequences:
81
+
82
+ - **It can wait.** By default it does not: `SAP_RELEASE_DEADLINE_MS` is `0`,
83
+ because waiting is for steps whose successor needs the server to have caught
84
+ up, and a teardown has none. Pass `disconnect({ deadlineMs })` if you want a
85
+ bounded wait — the deadline bounds the **wait**, never the request.
86
+ - **Requests still in flight will start failing.** The session they are running
87
+ on is the one being released. That is you having asked to disconnect, not a
88
+ race — finish or abandon your chains first.
89
+
90
+ It still never throws, including on a nonsense `deadlineMs`: its place is a
91
+ `finally`, where an exception would replace the error that sent you there.
92
+
93
+ ## Token renewal, if you use a provider
94
+
95
+ Nothing to do, but worth knowing what changed. The connector no longer caches
96
+ the token it was given. It asks the provider per request — cheap, because the
97
+ provider caches — and on a `401` it tells the provider its answer was refused
98
+ (`refreshToken()`, per that contract), asks again, and only if the answer
99
+ changed rebuilds the session and retries once. An unchanged answer means the
100
+ server refused those credentials, and the `401` reaches you.
101
+
102
+ So a password does the right thing with no configuration, and a bearer token is
103
+ renewed without a class of its own.
104
+
105
+ ## Sessions are a shared, per-user resource
106
+
107
+ Not an API change, a fact worth having. The limit is per user and the pool is
108
+ shared with everything else logged on as them, a SAP GUI session included. The
109
+ timeout is **idle-based**: a connection that keeps working keeps its session,
110
+ one that goes quiet past the window loses it. There is no keepalive timer here,
111
+ deliberately — how long to hold a session is yours to decide.
112
+
113
+ And the shape of the problem this release addresses, seen from the other end: a
114
+ process that opens a connection per check and never disconnects filled a
115
+ system's session list at one session every ten seconds. Releasing what you open
116
+ is half of it; opening one connection instead of one per check is the other.
@@ -29,7 +29,9 @@ The connection layer **does not** decide when to lock/unlock objects—that logi
29
29
  ```ts
30
30
  import { createAbapConnection } from '@mcp-abap-adt/connection';
31
31
 
32
- const connection = createAbapConnection(config, logger);
32
+ const connection = createAbapConnection(config, logger, undefined, undefined, {
33
+ system: 'onprem', // or 'cloud' — said by you, never detected
34
+ });
33
35
  await connection.connect(); // required before any request
34
36
 
35
37
  // Enable stateful session mode (adds x-sap-adt-sessiontype: stateful header)
@@ -47,11 +49,11 @@ connection.setSessionType('stateless');
47
49
  ## Knowing Which Session You Are In
48
50
 
49
51
  ```ts
50
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
52
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
51
53
 
52
54
  // getSessionIdentity() is on the HTTP connection classes, NOT on the
53
55
  // IAbapConnection type that createAbapConnection() returns.
54
- const connection = new BaseAbapConnection(config, logger);
56
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
55
57
  await connection.connect();
56
58
 
57
59
  // Which SAP session this connection is talking to. Changes only when the
@@ -102,19 +104,83 @@ answer, not a credential one, and nothing is torn down for it.
102
104
 
103
105
  ---
104
106
 
107
+ ## What This Layer Actually Solves
108
+
109
+ The link to the ABAP session is not guaranteed, and that is the whole problem:
110
+
111
+ - the ABAP session can be terminated by the system while this client still
112
+ believes it has one — the next lock-bound request answers
113
+ `400 "Session not found"`;
114
+ - or it is never created at all, because the system will not open another one
115
+ for this user right now.
116
+
117
+ Both are reported, neither is worked around. `connect()` fails with the reason
118
+ when no session was opened; a session lost afterwards surfaces as
119
+ `ADT_SESSION_REPLACED` rather than a request quietly continuing somewhere it
120
+ does not belong. What to do about either — wait, retry, reconnect, release
121
+ sessions the user still holds, carry on read-only — is the caller's decision,
122
+ made with the cookies and the answers in hand.
123
+
124
+ ## A Lock Lives In The ABAP Session
125
+
126
+ There are two sessions here, and they are not the same thing:
127
+
128
+ - the **HTTP session** — the conversation this client is having. It exists as
129
+ long as the cookies do, and nothing about it is in doubt;
130
+ - the **ABAP session** — named by `SAP_SESSIONID_<SID>_<CLIENT>`, and the thing
131
+ locks are bound to.
132
+
133
+ The doubt is only ever about the second. It may not have been created, or it may
134
+ have been terminated while this client still holds the cookies and believes it
135
+ has one — and a lock is dead in either case, whether or not the client noticed.
136
+
137
+ Two ways to lose one, and they are different problems:
138
+
139
+ - **The server never opened one.** No `SAP_SESSIONID` came back, so there is no
140
+ ABAP session known to this connection and nothing a lock could be bound to —
141
+ while the HTTP side is perfectly fine, which is why it does not look like a
142
+ failure at all. `connect()` refuses rather than handing back a connection
143
+ whose first lock would be dead on arrival. Sessions are limited per user and shared with every other tool
144
+ logged on as them, so this says nothing about your code — it says the system
145
+ would not open another one right now.
146
+ - **It timed out while you were quiet.** The timeout is an idle one, and it is
147
+ the *silence* that spends it, not the elapsed time. Measured on an on-prem
148
+ system with a 30-minute window: a small request once a minute kept one session
149
+ alive for 45 minutes with its identity unchanged, straight past the mark.
150
+
151
+ So a long chain under a lock is safe while it is doing something, and at risk
152
+ while it waits. **Any request in the session resets the window** — a poll, a
153
+ read, a status check. There is deliberately no keepalive timer in this package:
154
+ holding a session alive means holding a scarce, shared slot, and deciding to do
155
+ that belongs to the caller who knows why the session is worth keeping.
156
+
157
+ **The cookies are the session.** Nothing else ties a caller to one, so a second
158
+ connection given the same cookie jar works in the same ABAP session and can use
159
+ the locks taken in it — that is what makes handing them over a way to continue
160
+ someone else's work. It cuts both ways: `disconnect()` ends the session for
161
+ everyone holding those cookies, not just for the object it was called on, and no
162
+ connection can see the copies. Deciding who may hold them, and who is allowed to
163
+ close, belongs to whoever passes them around.
164
+
165
+ The server never tells the client how long it has: the session cookie carries no
166
+ expiry and no response header mentions one. The only honest signals are the ones
167
+ you get by asking — `getSessionIdentity()` for which session you are in, and
168
+ `ADT_SESSION_REPLACED` when the one you were in is gone.
169
+
105
170
  ## Troubleshooting
106
171
 
107
- - **CSRF token errors**: discard the session and establish a new one. `reset()`
108
- does that, but it lives on the HTTP connection classes and is **not** on the
109
- `IAbapConnection` type `createAbapConnection()` returns — reach it through a
110
- concrete type:
172
+ - **CSRF token errors**: discard the session and establish a new one. That is
173
+ `disconnect()` followed by `connect()` — there is no local-only discard, because
174
+ dropping the cookie leaves the ABAP session open on the server. Both live on
175
+ the HTTP connection classes and are **not** on the `IAbapConnection` type
176
+ `createAbapConnection()` returns — reach them through a concrete type:
111
177
 
112
178
  ```ts
113
- import { BaseAbapConnection } from '@mcp-abap-adt/connection';
179
+ import { AdtOnPremConnector, BasicAuthProvider } from '@mcp-abap-adt/connection';
114
180
 
115
- const connection = new BaseAbapConnection(config, logger);
116
- connection.reset(); // queues the cleanup; refuses requests meanwhile
117
- await connection.connect(); // a new session, explicitly
181
+ const connection = new AdtOnPremConnector(config, new BasicAuthProvider(user, pass), logger);
182
+ await connection.disconnect(); // ends the session on the server, then clears
183
+ await connection.connect(); // a new session, explicitly
118
184
  ```
119
185
  - **Session expired**: reauthenticate to obtain a new session.
120
186
  - **Multiple connections**: each `createAbapConnection` instance maintains its own cookie jar; share the instance if you need continuity.