@mcp-abap-adt/connection 1.10.0 → 1.10.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,0BAA0B,CAAC;AAM7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9E,uBAAe,sBAAuB,YAAW,cAAc;IAuB3D,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAvB3C,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,eAAe,CAAU;IACjC;;;;;;;;OAQG;IACH,OAAO,CAAC,iBAAiB,CAAS;IAClC,oFAAoF;IACpF,OAAO,CAAC,oBAAoB,CAAK;IAEjC,SAAS,aACU,MAAM,EAAE,SAAS,EACf,MAAM,EAAE,OAAO,GAAG,IAAI,EACzC,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAqBzC;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI;IAUpD;;OAEG;IACH,cAAc,IAAI,WAAW,GAAG,UAAU;IAI1C;;;;;;;;;;;;OAYG;IACH,oBAAoB,IAAI,IAAI;IAQ5B;;;;OAIG;IACH,kBAAkB,IAAI,IAAI;IAY1B;;OAEG;IACH,mBAAmB,IAAI,OAAO;IAI9B;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKrC;;OAEG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI;IAI7B,SAAS,IAAI,SAAS;IAItB,KAAK,IAAI,IAAI;IAYP,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IAI7B,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAevD;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAE3B,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,GAAG,EACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAwS9B,SAAS,CAAC,QAAQ,CAAC,wBAAwB,IAAI,MAAM;IAErD;;;OAGG;cACa,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAgC,EAC5C,UAAU,GAAE,MAAgC,GAC3C,OAAO,CAAC,MAAM,CAAC;IA2ClB;;OAEG;YACW,0BAA0B;IAsKxC;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,MAAM,GAAG,IAAI;IAIvC;;OAEG;IACH,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIlD;;OAEG;IACH,SAAS,CAAC,UAAU,IAAI,MAAM,GAAG,IAAI;IAIrC,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIlD,OAAO,CAAC,yBAAyB;IAkEjC;;;OAGG;IACH,SAAS,CAAC,oBAAoB,IAAI,OAAO,YAAY,EAAE,YAAY;IAInE,OAAO,CAAC,gBAAgB;YAsBV,oBAAoB;IAiClC;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,eAAe;CA+BxB;AAGD,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"AbstractAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/AbstractAbapConnection.ts"],"names":[],"mappings":"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"}
@@ -456,6 +456,15 @@ class AbstractAbapConnection {
456
456
  ...authHeaders,
457
457
  ...csrfConfig_js_1.CSRF_CONFIG.REQUIRED_HEADERS,
458
458
  };
459
+ // The token fetch belongs to the same ADT conversation as every other
460
+ // request this connection makes. Without the connection id the server
461
+ // sees a caller that merely happens to present our cookies, so a fetch
462
+ // issued while a lock is held reads as a stranger reaching into the
463
+ // session. makeAdtRequest sends this header for all session types; this
464
+ // path must not be the exception.
465
+ if (this.sessionId) {
466
+ headers['sap-adt-connection-id'] = this.sessionId;
467
+ }
459
468
  // Always add cookies if available - they are needed for session continuity
460
469
  // Even on first attempt, if we have cookies from previous session or error response, use them
461
470
  if (this.cookies) {
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Owns the session lifecycle: what state the session is in, who may use it, and
3
+ * when it is safe to tear it down.
4
+ *
5
+ * Deliberately knows nothing about SAP, HTTP, RFC, cookies or ADT. Identities
6
+ * are opaque maps of name → value that someone else computed; windows are
7
+ * opaque labels. That is what makes this unit testable without a server, and it
8
+ * is where the hard part of the design lives: ordering, admission, deadlines.
9
+ *
10
+ * Design: docs/superpowers/specs/2026-07-27-session-lifecycle-design.md in
11
+ * @mcp-abap-adt/adt-clients.
12
+ */
13
+ /** Opaque handle for one open lock window: `Symbol(label)`. */
14
+ export type WindowToken = symbol;
15
+ export type TransitionKind = 'connect' | 'disconnect' | 'recover' | 'cleanup';
16
+ export interface RequestLease {
17
+ /** Teardown epoch at admission — the baseline a recovery compares against. */
18
+ readonly epoch: number;
19
+ /** Call once the request settles. Safe to call twice. */
20
+ release(): void;
21
+ }
22
+ export interface DrainResult {
23
+ /** Labels of windows still open when the wait gave up, deduplicated. */
24
+ abandonedWindows: string[];
25
+ }
26
+ export interface BeginTeardownOptions {
27
+ /** Decides the epoch: a caller's request cancels recoveries, an internal one must not. */
28
+ origin: 'caller' | 'internal';
29
+ /** Decides admission: a lost session cannot let an open window finish. */
30
+ sessionLost: boolean;
31
+ }
32
+ export declare const ADT_SESSION_ERROR: {
33
+ readonly NOT_CONNECTED: "ADT_NOT_CONNECTED";
34
+ readonly SESSION_REPLACED: "ADT_SESSION_REPLACED";
35
+ readonly RELEASE_PENDING: "ADT_RELEASE_PENDING";
36
+ };
37
+ export type AdtSessionErrorCode = (typeof ADT_SESSION_ERROR)[keyof typeof ADT_SESSION_ERROR];
38
+ export declare function sessionError(code: AdtSessionErrorCode, message?: string): Error & {
39
+ code: AdtSessionErrorCode;
40
+ };
41
+ export interface SessionLifecycleOptions {
42
+ /** Ceiling for the window wait, from the moment a teardown is requested. */
43
+ ceilingMs?: number;
44
+ /** Injected clock, so tests need no real time. */
45
+ now?: () => number;
46
+ }
47
+ export declare class SessionLifecycle {
48
+ private state;
49
+ private fingerprint;
50
+ private epoch;
51
+ private teardownPending;
52
+ private teardownLostSession;
53
+ private teardownAt;
54
+ /** Set at expiry: admission is shut regardless of open windows. */
55
+ private admissionForcedShut;
56
+ private readonly windows;
57
+ private inFlight;
58
+ private tail;
59
+ private tailKind;
60
+ private tailPromise;
61
+ private waiters;
62
+ private readonly ceilingMs;
63
+ private readonly now;
64
+ constructor(options?: SessionLifecycleOptions);
65
+ /**
66
+ * Whether a caller may start work. False throughout a teardown, including
67
+ * while a grandfathered window is still finishing: finishing is not starting.
68
+ */
69
+ get connected(): boolean;
70
+ /** Derived from the tracked fingerprint; null when nothing is tracked. */
71
+ get identity(): string | null;
72
+ get teardownEpoch(): number;
73
+ /** One entry per open window, abandoned ones included. */
74
+ get openWindows(): readonly string[];
75
+ /**
76
+ * Publishes a freshly established session, which also ENDS any teardown that
77
+ * was pending: the flags describe the session being torn down, and this is a
78
+ * different one. Without this, `connect()` after `disconnect()` — which the
79
+ * design allows explicitly — leaves the lifecycle permanently unusable.
80
+ *
81
+ * Windows are dropped for the same reason: they belonged to the old session,
82
+ * nothing here can close them, and a teardown that gave up on them has
83
+ * already carried their labels out in its report. Keeping them would make
84
+ * every future drain re-report locks from a session that no longer exists.
85
+ */
86
+ markConnected(fingerprint?: ReadonlyMap<string, string>): void;
87
+ markDisconnected(): void;
88
+ /**
89
+ * Classifies a freshly observed fingerprint.
90
+ *
91
+ * Additive: a name appearing where none was tracked is `established`, never
92
+ * `replaced` — otherwise the identifier a LOCK response adds would read as a
93
+ * new session and blow up the window it just opened.
94
+ */
95
+ observe(fingerprint: ReadonlyMap<string, string>): 'unchanged' | 'established' | 'replaced';
96
+ /** True while an already-open window is allowed to finish its work. */
97
+ private get finishingWindowOpen();
98
+ private get admits();
99
+ assertUsable(): void;
100
+ /** Asserts usability and counts the request in, in one synchronous step. */
101
+ admitRequest(): RequestLease;
102
+ /**
103
+ * A lock outlives the request that takes it, so it is the one thing a pending
104
+ * teardown refuses outright.
105
+ */
106
+ beginWindow(label: string): WindowToken;
107
+ /** A token matching no open window is ignored: double close, foreign token. */
108
+ endWindow(token: WindowToken): void;
109
+ beginTeardown({ origin, sessionLost }: BeginTeardownOptions): void;
110
+ get teardownRequested(): boolean;
111
+ /**
112
+ * Resolves when nothing is in flight and no window is still worth waiting
113
+ * for. Bounded by the ceiling measured from the teardown request — absolute,
114
+ * never extended by request activity.
115
+ *
116
+ * On expiry, before resolving and without yielding in between: shuts
117
+ * admission, gives up on the remaining windows, then waits once more for the
118
+ * already-admitted requests to settle.
119
+ */
120
+ drain(): Promise<DrainResult>;
121
+ private get liveWindows();
122
+ private abandonedLabels;
123
+ /**
124
+ * Runs a transition on the serializing tail.
125
+ *
126
+ * `connect` and `disconnect` join the tail of their own kind — two callers
127
+ * wanting the same thing get one execution and the same answer — but only
128
+ * when nothing is queued behind it, so a join can never overtake a queued
129
+ * transition of another kind. `recover` and `cleanup` never join and are
130
+ * never joined: a recovery carries its own request's baseline, and an
131
+ * internal cleanup owes its result to nobody while a caller's teardown owes a
132
+ * report.
133
+ */
134
+ transition(kind: TransitionKind, run: () => Promise<void>): Promise<void>;
135
+ private wake;
136
+ private changed;
137
+ }
138
+ //# sourceMappingURL=SessionLifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionLifecycle.d.ts","sourceRoot":"","sources":["../../src/session/SessionLifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
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.
5
+ *
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.
10
+ *
11
+ * Design: docs/superpowers/specs/2026-07-27-session-lifecycle-design.md in
12
+ * @mcp-abap-adt/adt-clients.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SessionLifecycle = exports.ADT_SESSION_ERROR = void 0;
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
+ };
22
+ function sessionError(code, message) {
23
+ const error = new Error(message ?? code);
24
+ error.code = code;
25
+ return error;
26
+ }
27
+ class SessionLifecycle {
28
+ state = 'disconnected';
29
+ fingerprint = new Map();
30
+ epoch = 0;
31
+ 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
+ inFlight = 0;
38
+ tail = Promise.resolve();
39
+ tailKind = null;
40
+ 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
+ // ---------------------------------------------------------------- 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
+ */
53
+ get connected() {
54
+ return this.state === 'connected' && !this.teardownPending;
55
+ }
56
+ /** Derived from the tracked fingerprint; null when nothing is tracked. */
57
+ get identity() {
58
+ if (this.fingerprint.size === 0)
59
+ return null;
60
+ return [...this.fingerprint.entries()]
61
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
62
+ .map(([name, value]) => `${name}=${value}`)
63
+ .join('; ');
64
+ }
65
+ get teardownEpoch() {
66
+ return this.epoch;
67
+ }
68
+ /** One entry per open window, abandoned ones included. */
69
+ get openWindows() {
70
+ return [...this.windows.values()].map((w) => w.label);
71
+ }
72
+ /**
73
+ * Publishes a freshly established session, which also ENDS any teardown that
74
+ * was pending: the flags describe the session being torn down, and this is a
75
+ * different one. Without this, `connect()` after `disconnect()` — which the
76
+ * 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
+ */
83
+ markConnected(fingerprint = new Map()) {
84
+ this.state = 'connected';
85
+ this.fingerprint = new Map(fingerprint);
86
+ this.teardownPending = false;
87
+ this.teardownLostSession = false;
88
+ this.admissionForcedShut = false;
89
+ this.teardownAt = null;
90
+ this.windows.clear();
91
+ this.wake();
92
+ }
93
+ markDisconnected() {
94
+ this.state = 'disconnected';
95
+ this.fingerprint.clear();
96
+ this.wake();
97
+ }
98
+ /**
99
+ * Classifies a freshly observed fingerprint.
100
+ *
101
+ * Additive: a name appearing where none was tracked is `established`, never
102
+ * `replaced` — otherwise the identifier a LOCK response adds would read as a
103
+ * new session and blow up the window it just opened.
104
+ */
105
+ observe(fingerprint) {
106
+ let established = false;
107
+ for (const [name, value] of fingerprint) {
108
+ const known = this.fingerprint.get(name);
109
+ if (known === undefined) {
110
+ established = true;
111
+ this.fingerprint.set(name, value);
112
+ }
113
+ else if (known !== value) {
114
+ this.fingerprint.set(name, value);
115
+ return 'replaced';
116
+ }
117
+ }
118
+ return established ? 'established' : 'unchanged';
119
+ }
120
+ // ------------------------------------------------------------ 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
+ assertUsable() {
136
+ if (!this.admits) {
137
+ throw sessionError(exports.ADT_SESSION_ERROR.NOT_CONNECTED);
138
+ }
139
+ }
140
+ /** Asserts usability and counts the request in, in one synchronous step. */
141
+ admitRequest() {
142
+ this.assertUsable();
143
+ this.inFlight += 1;
144
+ const epoch = this.epoch;
145
+ let released = false;
146
+ return {
147
+ epoch,
148
+ release: () => {
149
+ if (released)
150
+ return;
151
+ released = true;
152
+ this.inFlight -= 1;
153
+ this.wake();
154
+ },
155
+ };
156
+ }
157
+ // -------------------------------------------------------------- windows ---
158
+ /**
159
+ * A lock outlives the request that takes it, so it is the one thing a pending
160
+ * teardown refuses outright.
161
+ */
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();
175
+ }
176
+ // ------------------------------------------------------------- teardown ---
177
+ beginTeardown({ origin, sessionLost }) {
178
+ if (!this.teardownPending) {
179
+ this.teardownAt = this.now();
180
+ }
181
+ this.teardownPending = true;
182
+ if (origin === 'caller') {
183
+ this.epoch += 1;
184
+ }
185
+ 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
+ this.fingerprint.clear();
192
+ }
193
+ this.wake();
194
+ }
195
+ get teardownRequested() {
196
+ return this.teardownPending;
197
+ }
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
+ // ---------------------------------------------------------- transitions ---
247
+ /**
248
+ * Runs a transition on the serializing tail.
249
+ *
250
+ * `connect` and `disconnect` join the tail of their own kind — two callers
251
+ * wanting the same thing get one execution and the same answer — but only
252
+ * when nothing is queued behind it, so a join can never overtake a queued
253
+ * transition of another kind. `recover` and `cleanup` never join and are
254
+ * 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.
257
+ */
258
+ transition(kind, run) {
259
+ const joinable = kind === 'connect' || kind === 'disconnect';
260
+ if (joinable && this.tailKind === kind && this.tailPromise) {
261
+ return this.tailPromise;
262
+ }
263
+ const queued = this.tail.then(run, run);
264
+ this.tail = queued.then(() => undefined, () => undefined);
265
+ this.tailKind = kind;
266
+ this.tailPromise = queued;
267
+ const settle = () => {
268
+ if (this.tailPromise === queued) {
269
+ this.tailKind = null;
270
+ this.tailPromise = null;
271
+ }
272
+ };
273
+ queued.then(settle, settle);
274
+ return queued;
275
+ }
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
+ }
301
+ exports.SessionLifecycle = SessionLifecycle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",