@mcp-abap-adt/connection 1.10.2 → 3.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 (44) hide show
  1. package/CHANGELOG.md +827 -0
  2. package/README.md +44 -11
  3. package/dist/__tests__/helpers/session.d.ts +15 -0
  4. package/dist/__tests__/helpers/session.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/session.js +19 -0
  6. package/dist/auth/ntlm.d.ts +15 -0
  7. package/dist/auth/ntlm.d.ts.map +1 -1
  8. package/dist/auth/ntlm.js +38 -0
  9. package/dist/connection/AbstractAbapConnection.d.ts +180 -12
  10. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
  11. package/dist/connection/AbstractAbapConnection.js +398 -19
  12. package/dist/connection/BaseAbapConnection.d.ts +5 -1
  13. package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
  14. package/dist/connection/BaseAbapConnection.js +11 -1
  15. package/dist/connection/CertificateAbapConnection.d.ts +5 -1
  16. package/dist/connection/CertificateAbapConnection.d.ts.map +1 -1
  17. package/dist/connection/CertificateAbapConnection.js +11 -1
  18. package/dist/connection/JwtAbapConnection.d.ts +3 -2
  19. package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
  20. package/dist/connection/JwtAbapConnection.js +19 -8
  21. package/dist/connection/KerberosAbapConnection.d.ts +5 -1
  22. package/dist/connection/KerberosAbapConnection.d.ts.map +1 -1
  23. package/dist/connection/KerberosAbapConnection.js +54 -3
  24. package/dist/connection/SamlAbapConnection.d.ts +5 -1
  25. package/dist/connection/SamlAbapConnection.d.ts.map +1 -1
  26. package/dist/connection/SamlAbapConnection.js +11 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -0
  29. package/dist/session/SessionLifecycle.d.ts +50 -70
  30. package/dist/session/SessionLifecycle.d.ts.map +1 -1
  31. package/dist/session/SessionLifecycle.js +59 -158
  32. package/docs/INDEX.md +106 -0
  33. package/docs/INSTALLATION.md +304 -0
  34. package/docs/JWT_AUTH_TOOLS.md +142 -0
  35. package/docs/MIGRATION-2.0.md +114 -0
  36. package/docs/SCOPE.md +44 -0
  37. package/docs/STATEFUL_SESSION_GUIDE.md +122 -0
  38. package/docs/USAGE.md +745 -0
  39. package/examples/README.md +112 -0
  40. package/examples/basic-connection.js +55 -0
  41. package/examples/jwt-with-token-refresh.js +87 -0
  42. package/examples/saml-connection.js +52 -0
  43. package/examples/websocket-transport.js +87 -0
  44. package/package.json +11 -4
@@ -1,24 +1,20 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Owns the session lifecycle: what state the session is in, who may use it, and
4
- * when it is safe to tear it down.
4
+ * when it stops being current.
5
5
  *
6
6
  * Deliberately knows nothing about SAP, HTTP, RFC, cookies or ADT. Identities
7
- * are opaque maps of name → value that someone else computed; windows are
8
- * opaque labels. That is what makes this unit testable without a server, and it
9
- * is where the hard part of the design lives: ordering, admission, deadlines.
7
+ * are opaque maps of name → value that someone else computed. That is what makes
8
+ * this unit testable without a server, and it is where the hard part lives:
9
+ * ordering, admission, and telling one session from the next.
10
10
  *
11
- * Design: docs/superpowers/specs/2026-07-27-session-lifecycle-design.md in
11
+ * Design: docs/superpowers/specs/2026-07-31-teardown-policy-design.md in
12
12
  * @mcp-abap-adt/adt-clients.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.SessionLifecycle = exports.ADT_SESSION_ERROR = void 0;
15
+ exports.SessionLifecycle = void 0;
16
16
  exports.sessionError = sessionError;
17
- exports.ADT_SESSION_ERROR = {
18
- NOT_CONNECTED: 'ADT_NOT_CONNECTED',
19
- SESSION_REPLACED: 'ADT_SESSION_REPLACED',
20
- RELEASE_PENDING: 'ADT_RELEASE_PENDING',
21
- };
17
+ const interfaces_1 = require("@mcp-abap-adt/interfaces");
22
18
  function sessionError(code, message) {
23
19
  const error = new Error(message ?? code);
24
20
  error.code = code;
@@ -28,28 +24,14 @@ class SessionLifecycle {
28
24
  state = 'disconnected';
29
25
  fingerprint = new Map();
30
26
  epoch = 0;
27
+ generation = 0;
31
28
  teardownPending = false;
32
- teardownLostSession = false;
33
- teardownAt = null;
34
- /** Set at expiry: admission is shut regardless of open windows. */
35
- admissionForcedShut = false;
36
- windows = new Map();
37
29
  inFlight = 0;
38
30
  tail = Promise.resolve();
39
31
  tailKind = null;
40
32
  tailPromise = null;
41
- waiters = [];
42
- ceilingMs;
43
- now;
44
- constructor(options = {}) {
45
- this.ceilingMs = options.ceilingMs ?? 600_000;
46
- this.now = options.now ?? (() => Date.now());
47
- }
48
33
  // ---------------------------------------------------------------- state ---
49
- /**
50
- * Whether a caller may start work. False throughout a teardown, including
51
- * while a grandfathered window is still finishing: finishing is not starting.
52
- */
34
+ /** Whether a caller may start work. False throughout a pending teardown. */
53
35
  get connected() {
54
36
  return this.state === 'connected' && !this.teardownPending;
55
37
  }
@@ -65,42 +47,55 @@ class SessionLifecycle {
65
47
  get teardownEpoch() {
66
48
  return this.epoch;
67
49
  }
68
- /** One entry per open window, abandoned ones included. */
69
- get openWindows() {
70
- return [...this.windows.values()].map((w) => w.label);
50
+ /**
51
+ * Which session is current, counted from zero.
52
+ *
53
+ * Two counters answer two different questions, and one cannot do both:
54
+ *
55
+ * - `epoch` — did the CALLER ask to stop? It moves only on a caller-initiated
56
+ * teardown, because a recovery must not cancel itself.
57
+ * - `generation` — is this still the session you were issued against? It moves
58
+ * on every change of which session is current, however caused.
59
+ *
60
+ * A fence built on `epoch` misses every internal teardown: after a session
61
+ * loss and a successful recovery, requests from the dead session carry the
62
+ * same epoch as the new one and sail straight through.
63
+ */
64
+ get sessionGeneration() {
65
+ return this.generation;
71
66
  }
72
67
  /**
73
68
  * Publishes a freshly established session, which also ENDS any teardown that
74
69
  * was pending: the flags describe the session being torn down, and this is a
75
70
  * different one. Without this, `connect()` after `disconnect()` — which the
76
71
  * design allows explicitly — leaves the lifecycle permanently unusable.
77
- *
78
- * Windows are dropped for the same reason: they belonged to the old session,
79
- * nothing here can close them, and a teardown that gave up on them has
80
- * already carried their labels out in its report. Keeping them would make
81
- * every future drain re-report locks from a session that no longer exists.
82
72
  */
83
73
  markConnected(fingerprint = new Map()) {
84
74
  this.state = 'connected';
85
75
  this.fingerprint = new Map(fingerprint);
86
76
  this.teardownPending = false;
87
- this.teardownLostSession = false;
88
- this.admissionForcedShut = false;
89
- this.teardownAt = null;
90
- this.windows.clear();
91
- this.wake();
77
+ this.generation += 1;
78
+ }
79
+ /**
80
+ * Drops the tracked identity without touching state or generation.
81
+ *
82
+ * For a session the caller discarded ON PURPOSE — a credential renewal, a
83
+ * cache invalidation. The next fingerprint then reads as `established` rather
84
+ * than `replaced`, which is the truth: nothing was taken from us.
85
+ */
86
+ forgetIdentity() {
87
+ this.fingerprint.clear();
92
88
  }
93
89
  markDisconnected() {
94
90
  this.state = 'disconnected';
95
91
  this.fingerprint.clear();
96
- this.wake();
97
92
  }
98
93
  /**
99
94
  * Classifies a freshly observed fingerprint.
100
95
  *
101
96
  * Additive: a name appearing where none was tracked is `established`, never
102
97
  * `replaced` — otherwise the identifier a LOCK response adds would read as a
103
- * new session and blow up the window it just opened.
98
+ * new session and condemn the operation it just covered.
104
99
  */
105
100
  observe(fingerprint) {
106
101
  let established = false;
@@ -118,23 +113,9 @@ class SessionLifecycle {
118
113
  return established ? 'established' : 'unchanged';
119
114
  }
120
115
  // ------------------------------------------------------------ admission ---
121
- /** True while an already-open window is allowed to finish its work. */
122
- get finishingWindowOpen() {
123
- return (this.teardownPending &&
124
- !this.teardownLostSession &&
125
- !this.admissionForcedShut &&
126
- this.windows.size > 0);
127
- }
128
- get admits() {
129
- if (this.state !== 'connected')
130
- return false;
131
- if (!this.teardownPending)
132
- return true;
133
- return this.finishingWindowOpen;
134
- }
135
116
  assertUsable() {
136
- if (!this.admits) {
137
- throw sessionError(exports.ADT_SESSION_ERROR.NOT_CONNECTED);
117
+ if (this.state !== 'connected' || this.teardownPending) {
118
+ throw sessionError(interfaces_1.ADT_SESSION_ERROR.NOT_CONNECTED);
138
119
  }
139
120
  }
140
121
  /** Asserts usability and counts the request in, in one synchronous step. */
@@ -142,107 +123,52 @@ class SessionLifecycle {
142
123
  this.assertUsable();
143
124
  this.inFlight += 1;
144
125
  const epoch = this.epoch;
126
+ const generation = this.generation;
145
127
  let released = false;
146
128
  return {
147
129
  epoch,
130
+ generation,
148
131
  release: () => {
149
132
  if (released)
150
133
  return;
151
134
  released = true;
152
135
  this.inFlight -= 1;
153
- this.wake();
154
136
  },
155
137
  };
156
138
  }
157
- // -------------------------------------------------------------- windows ---
139
+ /** How many admitted requests have not settled. Diagnostics only — nothing waits on it. */
140
+ get requestsInFlight() {
141
+ return this.inFlight;
142
+ }
158
143
  /**
159
- * A lock outlives the request that takes it, so it is the one thing a pending
160
- * teardown refuses outright.
144
+ * Whether a lease may still touch shared state.
145
+ *
146
+ * A request outliving its session is ordinary now that a teardown does not
147
+ * wait: its response must not write cookies over a newer session's, and must
148
+ * not be read as a replacement — which would raise a session-lost teardown
149
+ * against a session that is perfectly healthy.
161
150
  */
162
- beginWindow(label) {
163
- if (this.state !== 'connected' || this.teardownPending) {
164
- throw sessionError(exports.ADT_SESSION_ERROR.NOT_CONNECTED);
165
- }
166
- const token = Symbol(label);
167
- this.windows.set(token, { label, abandoned: false });
168
- return token;
169
- }
170
- /** A token matching no open window is ignored: double close, foreign token. */
171
- endWindow(token) {
172
- if (!this.windows.delete(token))
173
- return;
174
- this.wake();
151
+ isCurrent(lease) {
152
+ return lease.generation === this.generation;
175
153
  }
176
154
  // ------------------------------------------------------------- teardown ---
177
155
  beginTeardown({ origin, sessionLost }) {
178
- if (!this.teardownPending) {
179
- this.teardownAt = this.now();
180
- }
181
156
  this.teardownPending = true;
182
157
  if (origin === 'caller') {
183
158
  this.epoch += 1;
184
159
  }
160
+ // The moment a session stops being current is the moment leases against it
161
+ // go stale — whether or not a replacement exists yet. Counting only at
162
+ // markConnected() leaves the gap between a teardown and the next connect,
163
+ // where a late response would write into state just cleared.
164
+ this.generation += 1;
185
165
  if (sessionLost) {
186
- this.teardownLostSession = true;
187
- // Nothing can finish over a session that is gone: give up on every window
188
- // now, and drop the identity so a later comparison sees the change.
189
- for (const entry of this.windows.values())
190
- entry.abandoned = true;
191
166
  this.fingerprint.clear();
192
167
  }
193
- this.wake();
194
168
  }
195
169
  get teardownRequested() {
196
170
  return this.teardownPending;
197
171
  }
198
- /**
199
- * Resolves when nothing is in flight and no window is still worth waiting
200
- * for. Bounded by the ceiling measured from the teardown request — absolute,
201
- * never extended by request activity.
202
- *
203
- * On expiry, before resolving and without yielding in between: shuts
204
- * admission, gives up on the remaining windows, then waits once more for the
205
- * already-admitted requests to settle.
206
- */
207
- async drain() {
208
- // `??`, not `||`: a teardown requested at timestamp 0 is a real teardown,
209
- // and treating it as absent would restart the ceiling from drain() — making
210
- // the deadline relative to the wrong event, which is the sliding behaviour
211
- // this design rejects.
212
- const deadline = (this.teardownAt ?? this.now()) + this.ceilingMs;
213
- while (this.inFlight > 0 || this.liveWindows > 0) {
214
- const remaining = deadline - this.now();
215
- if (remaining <= 0)
216
- break;
217
- await this.changed(remaining);
218
- }
219
- if (this.inFlight === 0 && this.liveWindows === 0) {
220
- return { abandonedWindows: this.abandonedLabels() };
221
- }
222
- // Expiry. Synchronous, before any await: no request may enter after this.
223
- this.admissionForcedShut = true;
224
- for (const entry of this.windows.values())
225
- entry.abandoned = true;
226
- while (this.inFlight > 0) {
227
- await this.changed();
228
- }
229
- return { abandonedWindows: this.abandonedLabels() };
230
- }
231
- get liveWindows() {
232
- let live = 0;
233
- for (const entry of this.windows.values())
234
- if (!entry.abandoned)
235
- live += 1;
236
- return live;
237
- }
238
- abandonedLabels() {
239
- const labels = new Set();
240
- for (const entry of this.windows.values()) {
241
- if (entry.abandoned)
242
- labels.add(entry.label);
243
- }
244
- return [...labels];
245
- }
246
172
  // ---------------------------------------------------------- transitions ---
247
173
  /**
248
174
  * Runs a transition on the serializing tail.
@@ -252,8 +178,7 @@ class SessionLifecycle {
252
178
  * when nothing is queued behind it, so a join can never overtake a queued
253
179
  * transition of another kind. `recover` and `cleanup` never join and are
254
180
  * never joined: a recovery carries its own request's baseline, and an
255
- * internal cleanup owes its result to nobody while a caller's teardown owes a
256
- * report.
181
+ * internal cleanup owes its result to nobody.
257
182
  */
258
183
  transition(kind, run) {
259
184
  const joinable = kind === 'connect' || kind === 'disconnect';
@@ -273,29 +198,5 @@ class SessionLifecycle {
273
198
  queued.then(settle, settle);
274
199
  return queued;
275
200
  }
276
- // ----------------------------------------------------------- internals ---
277
- wake() {
278
- const waiters = this.waiters;
279
- this.waiters = [];
280
- for (const resolve of waiters)
281
- resolve();
282
- }
283
- changed(timeoutMs) {
284
- return new Promise((resolve) => {
285
- let done = false;
286
- const finish = () => {
287
- if (done)
288
- return;
289
- done = true;
290
- resolve();
291
- };
292
- this.waiters.push(finish);
293
- if (timeoutMs !== undefined) {
294
- const timer = setTimeout(finish, timeoutMs);
295
- if (typeof timer.unref === 'function')
296
- timer.unref();
297
- }
298
- });
299
- }
300
201
  }
301
202
  exports.SessionLifecycle = SessionLifecycle;
package/docs/INDEX.md ADDED
@@ -0,0 +1,106 @@
1
+ # Documentation Index
2
+
3
+ **Package:** `@mcp-abap-adt/connection`
4
+ **Version:** see [CHANGELOG.md](../CHANGELOG.md) — kept there rather than
5
+ duplicated here, where it went stale by eight minor versions.
6
+
7
+ ## Package Structure
8
+
9
+ ```
10
+ mcp-abap-connection/
11
+ ├── README.md # Main package documentation
12
+ ├── CHANGELOG.md # Version history and changes
13
+ ├── docs/ # Detailed documentation
14
+ │ ├── INDEX.md # This file - documentation overview
15
+ │ ├── INSTALLATION.md # Setup and installation guide
16
+ │ ├── USAGE.md # API documentation and examples
17
+ │ ├── MIGRATION-2.0.md # Moving to the explicit session lifecycle
18
+ │ ├── SCOPE.md # What this package does and does not own
19
+ │ ├── STATEFUL_SESSION_GUIDE.md # Stateful requests and lock windows
20
+ │ └── JWT_AUTH_TOOLS.md # CLI tool for authentication
21
+ ├── examples/ # Working code examples
22
+ │ ├── README.md # Examples overview
23
+ │ └── basic-connection.js # Simple connection example
24
+ ├── bin/ # CLI tools
25
+ │ └── sap-abap-auth.js # JWT authentication CLI
26
+ └── src/ # Source code
27
+ ├── connection/ # Connection classes
28
+ ├── config/ # Configuration utilities
29
+ ├── utils/ # Helper functions
30
+ └── __tests__/ # Unit tests
31
+ ```
32
+
33
+ ## Quick Links
34
+
35
+ ### Getting Started
36
+ - 📦 [Installation Guide](./INSTALLATION.md) - How to install and set up the package
37
+ - 📚 [Usage Guide](./USAGE.md) - Basic usage and comprehensive API documentation
38
+ - 📖 [Main README](../README.md) - Package overview and quick start
39
+ - 🧭 [Scope and Boundaries](./SCOPE.md) - What this package does (and does not), sibling packages, and why there is no RFC to cloud
40
+
41
+ ### Core Features
42
+ - 🔑 [JWT Auth Tools](./JWT_AUTH_TOOLS.md) - CLI tool for browser-based authentication
43
+
44
+ ### Version Information
45
+ - 📋 [CHANGELOG](../CHANGELOG.md) - Complete version history, including what the latest release changed
46
+
47
+ ### Examples
48
+ - 📁 [Examples Overview](../examples/README.md) - All available examples
49
+ - 🔌 [Basic Connection](../examples/basic-connection.js) - Simple connection setup
50
+
51
+ ## Documentation by Topic
52
+
53
+ ### Authentication
54
+ - **Basic Auth**: [USAGE.md - Basic Authentication](./USAGE.md#basic-authentication-on-premise)
55
+ - **JWT/OAuth2**: [USAGE.md - JWT Authentication](./USAGE.md#jwt-authentication-cloudbtp)
56
+ - **Token Refresh**: Handled by `@mcp-abap-adt/auth-broker` package (removed in 0.2.0)
57
+ - **CLI Tool**: [JWT_AUTH_TOOLS.md](./JWT_AUTH_TOOLS.md)
58
+
59
+ ### Session Management
60
+ - **Overview**: [USAGE.md - Session Management](./USAGE.md#session-management)
61
+ - **Stateful Mode**: Use `setSessionType('stateful')` for session headers
62
+ - **Session State Persistence**: Handled by `@mcp-abap-adt/auth-broker` package
63
+ - **API Methods**:
64
+ - `getSessionId()` - Get current session ID (auto-generated UUID)
65
+ - `setSessionType()` - Switch between stateful/stateless modes
66
+
67
+ ### API Reference
68
+ - **Connection Interface**: [USAGE.md - API Reference](./USAGE.md#api-reference)
69
+ - **Configuration Types**: [USAGE.md - Configuration Types](./USAGE.md#configuration-types)
70
+ - **Factory Function**: `createAbapConnection()`
71
+ - **Connection Classes**: `BaseAbapConnection`, `JwtAbapConnection`
72
+
73
+ ## Version Highlights
74
+
75
+ See [CHANGELOG.md](../CHANGELOG.md). This section used to restate it and drifted
76
+ eight minor versions behind — a second copy of a changelog is a changelog that
77
+ is wrong.
78
+
79
+ ## Documentation Standards
80
+
81
+ ### File Organization
82
+ - **README.md** - Package overview, quick start, basic API
83
+ - **CHANGELOG.md** - All changes, following [Keep a Changelog](https://keepachangelog.com/)
84
+ - **docs/** - Detailed documentation, tutorials, guides
85
+ - **examples/** - Working code examples with README
86
+
87
+ ### Naming Conventions
88
+ - `UPPERCASE.md` - Main documentation files (README, CHANGELOG)
89
+ - `PascalCase.md` - Detailed guides in docs/ folder
90
+ - `kebab-case.js` - Example files
91
+
92
+ ### Content Guidelines
93
+ - Keep README concise, link to detailed docs
94
+ - Include working code examples
95
+ - Document environment variables and configuration
96
+ - Provide troubleshooting sections
97
+ - Show both success and error handling
98
+
99
+ ## Contributing Documentation
100
+
101
+ When adding new features:
102
+ 1. Update CHANGELOG.md with changes
103
+ 2. Add usage examples to USAGE.md
104
+ 3. Create working examples in examples/
105
+ 4. Update README.md if API changes
106
+ 5. Add troubleshooting to relevant guide