@abapify/adt-client 0.4.0 → 0.4.3

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1553 @@
1
+ import { exception } from "@abapify/adt-schemas";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { Agent, fetch } from "undici";
4
+ import { adtContract, createAdtClient as createAdtClient$1 } from "@abapify/adt-contracts";
5
+ import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+
8
+ //#region src/utils/session.ts
9
+ /**
10
+ * Cookie Store - Manages HTTP cookies for stateful sessions
11
+ */
12
+ var CookieStore = class {
13
+ cookies = /* @__PURE__ */ new Map();
14
+ /**
15
+ * Parse Set-Cookie header and store cookies
16
+ * Handles complex cookie strings with expires dates
17
+ */
18
+ parseCookies(setCookieHeader) {
19
+ const cookieStrings = this.splitCookieHeader(setCookieHeader);
20
+ for (const cookieString of cookieStrings) {
21
+ const [name, value] = cookieString.split(";")[0].trim().split("=", 2);
22
+ if (name && value && !name.includes("expires") && !name.includes("path")) this.cookies.set(name.trim(), value.trim());
23
+ }
24
+ }
25
+ /**
26
+ * Split Set-Cookie header by commas, avoiding splitting on expires dates
27
+ */
28
+ splitCookieHeader(setCookieHeader) {
29
+ const cookies = [];
30
+ let current = "";
31
+ let inExpires = false;
32
+ for (let i = 0; i < setCookieHeader.length; i++) {
33
+ const char = setCookieHeader[i];
34
+ if (char === "," && !inExpires) {
35
+ if (current.trim()) cookies.push(current.trim());
36
+ current = "";
37
+ } else {
38
+ current += char;
39
+ if (current.toLowerCase().includes("expires=")) inExpires = true;
40
+ if (inExpires && (char === ";" || i === setCookieHeader.length - 1)) inExpires = false;
41
+ }
42
+ }
43
+ if (current.trim()) cookies.push(current.trim());
44
+ return cookies;
45
+ }
46
+ /**
47
+ * Get Cookie header value for requests
48
+ */
49
+ getCookieHeader() {
50
+ if (this.cookies.size === 0) return;
51
+ return Array.from(this.cookies.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
52
+ }
53
+ /**
54
+ * Check if we have any cookies
55
+ */
56
+ hasCookies() {
57
+ return this.cookies.size > 0;
58
+ }
59
+ /**
60
+ * Clear all cookies
61
+ */
62
+ clear() {
63
+ this.cookies.clear();
64
+ }
65
+ /**
66
+ * Get all cookies as a map (for testing/debugging)
67
+ */
68
+ getAll() {
69
+ return new Map(this.cookies);
70
+ }
71
+ /**
72
+ * Inject pre-existing cookies (e.g., from SAML authentication)
73
+ * @param cookieString Cookie string in "name=value; name2=value2" format
74
+ */
75
+ injectCookie(cookieString) {
76
+ const cookies = cookieString.split("; ");
77
+ for (const cookie of cookies) {
78
+ const [name, ...valueParts] = cookie.split("=");
79
+ const value = valueParts.join("=");
80
+ if (name && value) this.cookies.set(name.trim(), value.trim());
81
+ }
82
+ }
83
+ };
84
+ /**
85
+ * CSRF Token Manager - Handles CSRF token caching and initialization
86
+ */
87
+ var CsrfTokenManager = class {
88
+ cachedToken;
89
+ /**
90
+ * Extract CSRF token from cookies (SAP stores it there)
91
+ */
92
+ extractFromCookies(cookies) {
93
+ const xsrfEntry = Array.from(cookies.entries()).find(([key]) => key.toLowerCase().includes("xsrf") || key.toLowerCase().includes("csrf"));
94
+ if (!xsrfEntry) return;
95
+ const cookieValue = xsrfEntry[1];
96
+ const decodedToken = decodeURIComponent(cookieValue);
97
+ const tokenMatch = decodedToken.match(/^([A-Za-z0-9+/_-]+=*)/);
98
+ const actualToken = tokenMatch ? tokenMatch[1] : decodedToken;
99
+ if (actualToken && actualToken !== "Required" && actualToken !== "fetch" && actualToken !== "Fetch") return actualToken;
100
+ }
101
+ /**
102
+ * Extract CSRF token from response header
103
+ */
104
+ extractFromHeader(headerValue) {
105
+ if (!headerValue || headerValue === "Required" || headerValue === "fetch" || headerValue === "Fetch") return;
106
+ return headerValue;
107
+ }
108
+ /**
109
+ * Cache a CSRF token
110
+ */
111
+ cache(token) {
112
+ this.cachedToken = token;
113
+ }
114
+ /**
115
+ * Get cached CSRF token
116
+ */
117
+ getCached() {
118
+ return this.cachedToken;
119
+ }
120
+ /**
121
+ * Clear cached CSRF token (e.g., on 403 errors)
122
+ */
123
+ clear() {
124
+ this.cachedToken = void 0;
125
+ }
126
+ /**
127
+ * Check if we have a cached token
128
+ */
129
+ hasCached() {
130
+ return !!this.cachedToken;
131
+ }
132
+ /**
133
+ * Alias for hasCached() - for consistency with SessionManager API
134
+ */
135
+ hasToken() {
136
+ return this.hasCached();
137
+ }
138
+ };
139
+ /**
140
+ * ETag Manager - Tracks ETags for optimistic locking
141
+ */
142
+ var ETagManager = class {
143
+ etags = /* @__PURE__ */ new Map();
144
+ /**
145
+ * Extract and cache ETag from response header
146
+ * @param url The request URL (used as key)
147
+ * @param headerValue The ETag header value
148
+ */
149
+ cacheFromHeader(url, headerValue) {
150
+ if (headerValue) {
151
+ const key = this.normalizeUrl(url);
152
+ this.etags.set(key, headerValue);
153
+ }
154
+ }
155
+ /**
156
+ * Get cached ETag for a URL
157
+ */
158
+ get(url) {
159
+ const key = this.normalizeUrl(url);
160
+ return this.etags.get(key);
161
+ }
162
+ /**
163
+ * Clear cached ETag for a URL
164
+ */
165
+ clear(url) {
166
+ if (url) {
167
+ const key = this.normalizeUrl(url);
168
+ this.etags.delete(key);
169
+ } else this.etags.clear();
170
+ }
171
+ /**
172
+ * Normalize URL to use as cache key.
173
+ * Removes query params, trailing slashes, and lowercases the path.
174
+ * SAP ADT URLs are case-insensitive, so /fmodules/ZAGE_FM and
175
+ * /fmodules/zage_fm must share the same ETag cache entry.
176
+ */
177
+ normalizeUrl(url) {
178
+ try {
179
+ return new URL(url).pathname.replace(/\/$/, "").toLowerCase();
180
+ } catch {
181
+ return url.split("?")[0].replace(/\/$/, "").toLowerCase();
182
+ }
183
+ }
184
+ };
185
+ /**
186
+ * Session Manager - Orchestrates cookies and CSRF tokens
187
+ */
188
+ var SessionManager = class SessionManager {
189
+ logger;
190
+ fetchFn;
191
+ static SESSION_PATH_PREFIX = "/sap/bc/adt/core/http/sessions/";
192
+ cookieStore = new CookieStore();
193
+ csrfManager = new CsrfTokenManager();
194
+ etagManager = new ETagManager();
195
+ securitySessionActive = false;
196
+ constructor(logger, fetchFn = globalThis.fetch) {
197
+ this.logger = logger;
198
+ this.fetchFn = fetchFn;
199
+ }
200
+ /**
201
+ * Process response to update session state
202
+ * Extracts cookies, CSRF tokens, and ETags
203
+ * @param response The HTTP response
204
+ * @param url Optional URL for ETag caching (required for ETag tracking)
205
+ */
206
+ processResponse(response, url) {
207
+ if (url) {
208
+ const etag = response.headers.get("etag");
209
+ if (etag) {
210
+ this.etagManager.cacheFromHeader(url, etag);
211
+ this.logger?.debug(`Session: ETag cached for ${url}: ${etag}`);
212
+ }
213
+ }
214
+ const setCookieHeader = response.headers.get("set-cookie");
215
+ if (setCookieHeader) {
216
+ this.cookieStore.parseCookies(setCookieHeader);
217
+ this.logger?.debug("Session: Cookies updated from response");
218
+ }
219
+ const csrfHeader = response.headers.get("x-csrf-token");
220
+ const csrfToken = this.csrfManager.extractFromHeader(csrfHeader);
221
+ if (csrfToken) {
222
+ this.csrfManager.cache(csrfToken);
223
+ this.logger?.debug("Session: CSRF token cached from header");
224
+ }
225
+ if (!csrfToken && this.cookieStore.hasCookies()) {
226
+ const cookieCsrf = this.csrfManager.extractFromCookies(this.cookieStore.getAll());
227
+ if (cookieCsrf) {
228
+ this.csrfManager.cache(cookieCsrf);
229
+ this.logger?.debug("Session: CSRF token cached from cookies");
230
+ }
231
+ }
232
+ }
233
+ /**
234
+ * Get headers for next request
235
+ * @param method HTTP method
236
+ * @param url Optional URL for ETag lookup (for PUT/PATCH with optimistic locking)
237
+ */
238
+ getRequestHeaders(method, url) {
239
+ const headers = {};
240
+ if (this.securitySessionActive) headers["x-sap-security-session"] = "use";
241
+ const cookieHeader = this.cookieStore.getCookieHeader();
242
+ if (cookieHeader) headers.Cookie = cookieHeader;
243
+ if ([
244
+ "POST",
245
+ "PUT",
246
+ "DELETE",
247
+ "PATCH"
248
+ ].includes(method.toUpperCase())) {
249
+ const cachedToken = this.csrfManager.getCached();
250
+ if (cachedToken) headers["x-csrf-token"] = cachedToken;
251
+ else headers["x-csrf-token"] = "Fetch";
252
+ }
253
+ if (["PUT", "PATCH"].includes(method.toUpperCase()) && url) {
254
+ const etag = this.etagManager.get(url);
255
+ if (etag) {
256
+ headers["If-Match"] = etag;
257
+ this.logger?.debug(`Session: Using cached ETag for If-Match: ${etag}`);
258
+ }
259
+ }
260
+ return headers;
261
+ }
262
+ /**
263
+ * Get cached ETag for a URL
264
+ */
265
+ getETag(url) {
266
+ return this.etagManager.get(url);
267
+ }
268
+ /**
269
+ * Clear cached ETag for a URL
270
+ */
271
+ clearETag(url) {
272
+ this.etagManager.clear(url);
273
+ }
274
+ /**
275
+ * Reject session paths that could send cleanup requests off-target.
276
+ * Only relative paths under the ADT sessions endpoint (or same-origin
277
+ * absolute URLs with that path) are accepted.
278
+ */
279
+ isTrustedSessionPath({ baseUrl, sessionPath }) {
280
+ try {
281
+ const parsed = new URL(sessionPath, baseUrl);
282
+ const base = new URL(baseUrl);
283
+ return parsed.origin === base.origin && parsed.pathname.startsWith(SessionManager.SESSION_PATH_PREFIX);
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+ extractTrustedSessionPath({ baseUrl, body }) {
289
+ const sessionPath = body.match(/href="([^"]*\/sessions\/[^"]*)"/)?.[1];
290
+ if (!sessionPath || !this.isTrustedSessionPath({
291
+ baseUrl,
292
+ sessionPath
293
+ })) {
294
+ this.logger?.warn("Session: Created security session returned an untrusted path");
295
+ return;
296
+ }
297
+ return sessionPath;
298
+ }
299
+ /**
300
+ * Initialize CSRF token using the Eclipse ADT security session flow:
301
+ *
302
+ * 1. GET /sessions + x-sap-security-session: create → create security session
303
+ * 2. GET /sessions + x-sap-security-session: use + x-csrf-token: Fetch → get CSRF token
304
+ * 3. DELETE /sessions/<id> + x-sap-security-session: use + x-csrf-token → destroy session
305
+ *
306
+ * The CSRF token survives the session deletion and remains valid for
307
+ * all subsequent lock/unlock operations. Deleting the session frees
308
+ * the slot — SAP allows only one security session per user.
309
+ */
310
+ resolveInitializeCsrfArgs(...args) {
311
+ if (args.length === 1) {
312
+ const first = args[0];
313
+ if (first !== null) {
314
+ if (typeof first === "object") return first;
315
+ }
316
+ }
317
+ return {
318
+ authHeader: typeof args[0] === "string" ? args[0] : void 0,
319
+ client: typeof args[1] === "string" ? args[1] : void 0,
320
+ language: typeof args[2] === "string" ? args[2] : void 0,
321
+ signal: args[3] instanceof AbortSignal ? args[3] : void 0
322
+ };
323
+ }
324
+ async initializeCsrf(baseUrl, ...args) {
325
+ const { authHeader, client, language, signal } = this.resolveInitializeCsrfArgs(...args);
326
+ signal?.throwIfAborted();
327
+ const sessionsUrl = this.buildSessionsUrl(baseUrl, client, language);
328
+ let sessionPath;
329
+ try {
330
+ sessionPath = await this.createSecuritySession(baseUrl, sessionsUrl, authHeader, signal);
331
+ if (!sessionPath) return false;
332
+ if (!await this.acquireCsrfToken(sessionsUrl, authHeader, signal)) {
333
+ this.deleteSecuritySessionWithFallback({
334
+ sessionPath,
335
+ baseUrl,
336
+ authHeader,
337
+ client,
338
+ language
339
+ }).catch(() => void 0);
340
+ return false;
341
+ }
342
+ const acquiredCsrfToken = this.csrfManager.getCached();
343
+ if (acquiredCsrfToken) await this.deleteSecuritySession({
344
+ sessionPath,
345
+ csrfToken: acquiredCsrfToken,
346
+ baseUrl,
347
+ authHeader,
348
+ client,
349
+ signal
350
+ });
351
+ signal?.throwIfAborted();
352
+ return true;
353
+ } catch (error) {
354
+ return this.handleCsrfInitializationError(error, {
355
+ baseUrl,
356
+ authHeader,
357
+ client,
358
+ language,
359
+ sessionPath,
360
+ signal
361
+ });
362
+ }
363
+ }
364
+ buildSessionsUrl(baseUrl, client, language) {
365
+ const url = new URL("/sap/bc/adt/core/http/sessions", baseUrl);
366
+ if (client) url.searchParams.append("sap-client", client);
367
+ if (language) url.searchParams.append("sap-language", language);
368
+ return url;
369
+ }
370
+ buildSessionHeaders(authHeader) {
371
+ const headers = {
372
+ Accept: "application/vnd.sap.adt.core.http.session.v3+xml",
373
+ "X-sap-adt-sessiontype": "stateful"
374
+ };
375
+ if (authHeader) headers.Authorization = authHeader;
376
+ const cookie = this.cookieStore.getCookieHeader();
377
+ if (cookie) headers.Cookie = cookie;
378
+ return headers;
379
+ }
380
+ async createSecuritySession(baseUrl, sessionsUrl, authHeader, signal) {
381
+ this.logger?.debug("Session: Creating security session");
382
+ const response = await this.fetchFn(sessionsUrl, {
383
+ method: "GET",
384
+ headers: {
385
+ ...this.buildSessionHeaders(authHeader),
386
+ "x-sap-security-session": "create"
387
+ },
388
+ signal
389
+ });
390
+ signal?.throwIfAborted();
391
+ if (!response.ok) {
392
+ this.logger?.warn(`Session: Security session creation failed with status ${response.status}`);
393
+ return;
394
+ }
395
+ this.processResponse(response);
396
+ const body = await response.text();
397
+ signal?.throwIfAborted();
398
+ return this.extractTrustedSessionPath({
399
+ baseUrl,
400
+ body
401
+ });
402
+ }
403
+ async acquireCsrfToken(sessionsUrl, authHeader, signal) {
404
+ this.logger?.debug("Session: Fetching CSRF token");
405
+ const response = await this.fetchFn(sessionsUrl, {
406
+ method: "GET",
407
+ headers: {
408
+ ...this.buildSessionHeaders(authHeader),
409
+ "x-sap-security-session": "use",
410
+ "x-csrf-token": "Fetch"
411
+ },
412
+ signal
413
+ });
414
+ signal?.throwIfAborted();
415
+ if (!response.ok) {
416
+ this.logger?.warn(`Session: CSRF fetch failed with status ${response.status}`);
417
+ return false;
418
+ }
419
+ this.processResponse(response);
420
+ if (!this.csrfManager.hasCached()) {
421
+ this.logger?.warn("Session: CSRF fetch succeeded but no token found in response");
422
+ return false;
423
+ }
424
+ this.securitySessionActive = true;
425
+ this.logger?.debug("Session: CSRF token acquired");
426
+ return true;
427
+ }
428
+ async deleteSecuritySession({ sessionPath, csrfToken, baseUrl, authHeader, client, signal }) {
429
+ if (!this.isTrustedSessionPath({
430
+ baseUrl,
431
+ sessionPath
432
+ })) {
433
+ this.logger?.warn("Session: Refusing to delete security session with untrusted path");
434
+ return;
435
+ }
436
+ const deleteUrl = new URL(sessionPath, baseUrl);
437
+ if (client) deleteUrl.searchParams.append("sap-client", client);
438
+ try {
439
+ this.logger?.debug(`Session: Deleting security session ${sessionPath}`);
440
+ await this.fetchFn(deleteUrl, {
441
+ method: "DELETE",
442
+ headers: {
443
+ ...this.buildSessionHeaders(authHeader),
444
+ "x-sap-security-session": "use",
445
+ "x-csrf-token": csrfToken
446
+ },
447
+ signal: signal ?? AbortSignal.timeout(5e3)
448
+ });
449
+ this.logger?.debug("Session: Security session deleted");
450
+ } catch {
451
+ this.logger?.debug("Session: Failed to delete security session (will expire)");
452
+ }
453
+ }
454
+ async fetchSecuritySessionCsrfToken({ baseUrl, authHeader, client, language, signal }) {
455
+ const sessionsUrl = this.buildSessionsUrl(baseUrl, client, language);
456
+ try {
457
+ this.logger?.debug("Session: Fetching CSRF token for security session cleanup");
458
+ const response = await this.fetchFn(sessionsUrl, {
459
+ method: "GET",
460
+ headers: {
461
+ ...this.buildSessionHeaders(authHeader),
462
+ "x-sap-security-session": "use",
463
+ "x-csrf-token": "Fetch"
464
+ },
465
+ signal: signal ?? AbortSignal.timeout(5e3)
466
+ });
467
+ if (!response.ok) {
468
+ this.logger?.debug(`Session: Cleanup CSRF fetch failed with status ${response.status}`);
469
+ return;
470
+ }
471
+ const setCookieHeader = response.headers.get("set-cookie");
472
+ if (setCookieHeader) this.cookieStore.parseCookies(setCookieHeader);
473
+ return this.csrfManager.extractFromHeader(response.headers.get("x-csrf-token"));
474
+ } catch {
475
+ this.logger?.debug("Session: Failed to fetch cleanup CSRF token");
476
+ return;
477
+ }
478
+ }
479
+ async deleteSecuritySessionWithFallback({ sessionPath, baseUrl, authHeader, client, language, signal }) {
480
+ const token = await this.fetchSecuritySessionCsrfToken({
481
+ baseUrl,
482
+ authHeader,
483
+ client,
484
+ language,
485
+ signal
486
+ });
487
+ if (token) await this.deleteSecuritySession({
488
+ sessionPath,
489
+ csrfToken: token,
490
+ baseUrl,
491
+ authHeader,
492
+ client,
493
+ signal
494
+ });
495
+ }
496
+ handleCsrfInitializationError(error, { baseUrl, authHeader, client, language, sessionPath, signal }) {
497
+ if (sessionPath) this.deleteSecuritySessionWithFallback({
498
+ sessionPath,
499
+ baseUrl,
500
+ authHeader,
501
+ client,
502
+ language
503
+ }).catch(() => void 0);
504
+ if (signal?.aborted) throw error;
505
+ this.logger?.error(`Session: CSRF initialization error: ${error instanceof Error ? error.message : String(error)}`);
506
+ return false;
507
+ }
508
+ /**
509
+ * Clear all session state (cookies and CSRF)
510
+ */
511
+ clear() {
512
+ this.cookieStore.clear();
513
+ this.csrfManager.clear();
514
+ this.securitySessionActive = false;
515
+ this.logger?.debug("Session: Cleared all session state (cookies and CSRF)");
516
+ }
517
+ /**
518
+ * Check if we have a cached CSRF token
519
+ */
520
+ hasCsrfToken() {
521
+ return this.csrfManager.hasCached();
522
+ }
523
+ /**
524
+ * Get Cookie header value for requests
525
+ */
526
+ getCookieHeader() {
527
+ return this.cookieStore.getCookieHeader();
528
+ }
529
+ /**
530
+ * Get session type header value
531
+ * SAP ADT requires 'stateful' for operations that need session persistence
532
+ */
533
+ getSessionTypeHeader() {
534
+ return "stateful";
535
+ }
536
+ /**
537
+ * Inject a pre-existing cookie (e.g., from SAML authentication)
538
+ * @param cookieString Cookie string in "name=value" format
539
+ */
540
+ injectCookie(cookieString) {
541
+ this.cookieStore.injectCookie(cookieString);
542
+ this.logger?.debug(`Session: Injected cookie from external source`);
543
+ }
544
+ };
545
+
546
+ //#endregion
547
+ //#region src/errors.ts
548
+ /**
549
+ * ADT Error Handling
550
+ *
551
+ * SAP ADT returns errors in a standard XML format defined by exception.xsd:
552
+ * <exc:exception xmlns:exc="http://www.sap.com/abapxml/types/communicationframework">
553
+ * <namespace id="..."/>
554
+ * <type id="..."/>
555
+ * <message lang="EN">Human readable error message</message>
556
+ * <localizedMessage lang="EN">Localized message</localizedMessage>
557
+ * <properties>
558
+ * <entry key="key1">value1</entry>
559
+ * </properties>
560
+ * </exc:exception>
561
+ *
562
+ * Uses the generated exception schema from @abapify/adt-schemas for type-safe parsing.
563
+ */
564
+ /**
565
+ * Custom error class for ADT exceptions
566
+ *
567
+ * Provides structured access to SAP ADT error details.
568
+ */
569
+ var AdtError = class AdtError extends Error {
570
+ /** HTTP status code */
571
+ status;
572
+ /** HTTP status text */
573
+ statusText;
574
+ /** Parsed ADT exception data (if available) */
575
+ exception;
576
+ /** Raw response body */
577
+ rawBody;
578
+ /** Request URL */
579
+ url;
580
+ /** HTTP method */
581
+ method;
582
+ constructor(options) {
583
+ const baseMsg = `HTTP ${options.status}: ${options.statusText}`;
584
+ const detailMsg = options.exception?.message || options.exception?.localizedMessage;
585
+ const fullMsg = detailMsg ? `${baseMsg} - ${detailMsg}` : baseMsg;
586
+ super(fullMsg);
587
+ this.name = "AdtError";
588
+ this.status = options.status;
589
+ this.statusText = options.statusText;
590
+ this.url = options.url;
591
+ this.method = options.method;
592
+ this.rawBody = options.rawBody;
593
+ this.exception = options.exception;
594
+ if (Error.captureStackTrace) Error.captureStackTrace(this, AdtError);
595
+ }
596
+ /**
597
+ * Get the most relevant error message
598
+ */
599
+ get detailMessage() {
600
+ return this.exception?.localizedMessage || this.exception?.message || this.statusText;
601
+ }
602
+ /**
603
+ * Check if this is a specific error type
604
+ */
605
+ isType(type) {
606
+ return this.exception?.type === type;
607
+ }
608
+ /**
609
+ * Check if this is from a specific namespace
610
+ */
611
+ isNamespace(namespace) {
612
+ return this.exception?.namespace === namespace;
613
+ }
614
+ /**
615
+ * Get a property value from the exception
616
+ */
617
+ getProperty(key) {
618
+ return this.exception?.properties?.[key];
619
+ }
620
+ };
621
+ /**
622
+ * Parse ADT exception XML into structured data
623
+ *
624
+ * Uses the generated exception schema from @abapify/adt-schemas for type-safe parsing.
625
+ */
626
+ function parseAdtException(xml) {
627
+ if (!xml.includes("exception") || !/xmlns(?::[\w-]+)?="http:\/\/www\.sap\.com\/abapxml\/types\/communicationframework"/.test(xml)) return;
628
+ try {
629
+ const exc = exception.parse(xml).exception;
630
+ if (!exc) return;
631
+ const result = {};
632
+ if (exc.namespace?.id) result.namespace = exc.namespace.id;
633
+ if (exc.type?.id) result.type = exc.type.id;
634
+ if (exc.message?.$value) result.message = exc.message.$value;
635
+ if (exc.localizedMessage?.$value) result.localizedMessage = exc.localizedMessage.$value;
636
+ if (exc.properties?.entry) {
637
+ result.properties = {};
638
+ const entries = Array.isArray(exc.properties.entry) ? exc.properties.entry : [exc.properties.entry];
639
+ for (const entry of entries) if (entry.key && entry.$value) result.properties[entry.key] = entry.$value;
640
+ }
641
+ if (!result.namespace && !result.type && !result.message && !result.localizedMessage) return;
642
+ return result;
643
+ } catch {
644
+ return;
645
+ }
646
+ }
647
+ /**
648
+ * Create an AdtError from HTTP response details
649
+ */
650
+ function createAdtError(status, statusText, url, method, rawBody) {
651
+ const exception = parseAdtException(rawBody);
652
+ return new AdtError({
653
+ status,
654
+ statusText,
655
+ url,
656
+ method,
657
+ rawBody,
658
+ exception
659
+ });
660
+ }
661
+
662
+ //#endregion
663
+ //#region src/cancellation.ts
664
+ const adtAbortSignal = new AsyncLocalStorage();
665
+ /**
666
+ * Runs an ADT operation with an execution-scoped abort signal. Async local
667
+ * storage keeps concurrent clients and invocations isolated without mutating
668
+ * shared client state.
669
+ */
670
+ async function runWithAdtAbortSignal(signal, operation) {
671
+ signal.throwIfAborted();
672
+ return await adtAbortSignal.run(signal, operation);
673
+ }
674
+ function activeAdtAbortSignal() {
675
+ return adtAbortSignal.getStore();
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/adapter.ts
680
+ /**
681
+ * A response exceeded an explicit byte cap before its body was retained.
682
+ * Deliberately carries size metadata only, never response text.
683
+ */
684
+ var AdtResponseTooLargeError = class extends Error {
685
+ maxBytes;
686
+ receivedBytes;
687
+ code = "ADT_RESPONSE_TOO_LARGE";
688
+ constructor(maxBytes, receivedBytes) {
689
+ super(`ADT response exceeds the ${maxBytes}-byte limit.`);
690
+ this.maxBytes = maxBytes;
691
+ this.receivedBytes = receivedBytes;
692
+ this.name = "AdtResponseTooLargeError";
693
+ }
694
+ };
695
+ function contentLengthExceedsLimit(contentLength, maxBytes) {
696
+ if (!contentLength || !/^\d+$/.test(contentLength)) return void 0;
697
+ const parsed = Number(contentLength);
698
+ return Number.isSafeInteger(parsed) && parsed > maxBytes ? parsed : void 0;
699
+ }
700
+ async function abortResponse(response, abortController) {
701
+ abortController.abort();
702
+ try {
703
+ await response.body?.cancel();
704
+ } catch {}
705
+ }
706
+ async function readResponseTextBounded(response, maxBytes, abortController) {
707
+ const declaredBytes = contentLengthExceedsLimit(response.headers.get("content-length"), maxBytes);
708
+ if (declaredBytes !== void 0) {
709
+ await abortResponse(response, abortController);
710
+ throw new AdtResponseTooLargeError(maxBytes, declaredBytes);
711
+ }
712
+ if (!response.body) return "";
713
+ const reader = response.body.getReader();
714
+ const chunks = [];
715
+ let receivedBytes = 0;
716
+ try {
717
+ while (true) {
718
+ const { done, value } = await reader.read();
719
+ if (done) break;
720
+ if (!value) continue;
721
+ const nextReceivedBytes = receivedBytes + value.byteLength;
722
+ if (nextReceivedBytes > maxBytes) {
723
+ abortController.abort();
724
+ try {
725
+ await reader.cancel();
726
+ } catch {}
727
+ throw new AdtResponseTooLargeError(maxBytes, nextReceivedBytes);
728
+ }
729
+ chunks.push(value);
730
+ receivedBytes = nextReceivedBytes;
731
+ }
732
+ } finally {
733
+ reader.releaseLock();
734
+ }
735
+ const combined = new Uint8Array(receivedBytes);
736
+ let offset = 0;
737
+ for (const chunk of chunks) {
738
+ combined.set(chunk, offset);
739
+ offset += chunk.byteLength;
740
+ }
741
+ return new TextDecoder().decode(combined);
742
+ }
743
+ function executionAbortController() {
744
+ const abortController = new AbortController();
745
+ const executionSignal = activeAdtAbortSignal();
746
+ if (!executionSignal) return {
747
+ abortController,
748
+ dispose: () => void 0
749
+ };
750
+ const abortFromExecution = () => abortController.abort(executionSignal.reason);
751
+ if (executionSignal.aborted) abortFromExecution();
752
+ else executionSignal.addEventListener("abort", abortFromExecution, { once: true });
753
+ return {
754
+ abortController,
755
+ dispose: () => executionSignal.removeEventListener("abort", abortFromExecution)
756
+ };
757
+ }
758
+ function createAdtAdapter(config) {
759
+ const { baseUrl, username, password, cookieHeader, authorizationHeader, client, language, logger, plugins = [], onSessionExpired, headersTimeoutMs } = config;
760
+ const dispatcher = headersTimeoutMs === void 0 ? void 0 : new Agent({ headersTimeout: headersTimeoutMs });
761
+ /** Merge the optional Undici dispatcher into a fetch RequestInit. */
762
+ function withDispatcher(init) {
763
+ return dispatcher ? {
764
+ ...init,
765
+ dispatcher
766
+ } : init;
767
+ }
768
+ /** Keep a configured Agent paired with the Undici fetch ABI that created it. */
769
+ async function requestFetch(input, init) {
770
+ if (!dispatcher) return globalThis.fetch(input, init);
771
+ return await fetch(input, init);
772
+ }
773
+ /**
774
+ * Route a session-manager request through the same paired Undici
775
+ * fetch/dispatcher used for the final adapter request, so the
776
+ * configured headersTimeout applies to the entire write flow
777
+ * (CSRF handshake included), not just the last hop.
778
+ */
779
+ async function sessionFetch(input, init) {
780
+ return requestFetch(input.toString(), withDispatcher(init ?? {}));
781
+ }
782
+ /**
783
+ * Resolve `options.url` against the configured `baseUrl` and reject
784
+ * absolute URLs that would send credentials to a different origin.
785
+ */
786
+ function resolveAdtUrl(requestUrl) {
787
+ const resolved = new URL(requestUrl, baseUrl);
788
+ const baseOrigin = new URL(baseUrl).origin;
789
+ if (resolved.origin !== baseOrigin) throw new RangeError(`Refusing to fetch ${resolved.origin} — ADT requests must target the configured base origin ${baseOrigin}`);
790
+ return resolved;
791
+ }
792
+ const isSamlAuth = !!cookieHeader;
793
+ const basicCredentials = `${username}:${password}`;
794
+ const authHeader = authorizationHeader ?? (isSamlAuth ? void 0 : `Basic ${Buffer.from(basicCredentials).toString("base64")}`);
795
+ const sessionManager = new SessionManager(logger, sessionFetch);
796
+ if (cookieHeader) sessionManager.injectCookie(cookieHeader);
797
+ return {
798
+ async request(options) {
799
+ const executionSignal = activeAdtAbortSignal();
800
+ executionSignal?.throwIfAborted();
801
+ logger?.debug("=== ADAPTER REQUEST START ===");
802
+ logger?.debug("options.url:", options.url);
803
+ logger?.debug("options.method:", options.method);
804
+ logger?.debug("options.body:", options.body ? "present" : "undefined");
805
+ logger?.debug("options.bodySchema:", options.bodySchema ? "present" : "undefined");
806
+ const url = resolveAdtUrl(options.url);
807
+ if (options.query) Object.entries(options.query).forEach(([key, value]) => {
808
+ (Array.isArray(value) ? value : [value]).forEach((entry) => {
809
+ if (entry !== void 0) url.searchParams.append(key, String(entry));
810
+ });
811
+ });
812
+ if (client) url.searchParams.append("sap-client", client);
813
+ if (language) url.searchParams.append("sap-language", language);
814
+ if ([
815
+ "POST",
816
+ "PUT",
817
+ "DELETE",
818
+ "PATCH"
819
+ ].includes(options.method.toUpperCase()) && !sessionManager.hasCsrfToken()) {
820
+ logger?.debug("Adapter: Initializing CSRF token before write operation");
821
+ await sessionManager.initializeCsrf(baseUrl, {
822
+ authHeader,
823
+ client,
824
+ language,
825
+ signal: executionSignal
826
+ });
827
+ }
828
+ const headers = {
829
+ "X-sap-adt-sessiontype": sessionManager.getSessionTypeHeader(),
830
+ ...sessionManager.getRequestHeaders(options.method, url.pathname),
831
+ ...options.headers
832
+ };
833
+ if (authHeader) headers.Authorization = authHeader;
834
+ let bodySerializableSchema;
835
+ if (options.bodySchema && typeof options.bodySchema === "object") {
836
+ if ("build" in options.bodySchema && typeof options.bodySchema.build === "function") bodySerializableSchema = options.bodySchema;
837
+ }
838
+ let serializableSchema;
839
+ if (options.responses) {
840
+ const schema200 = options.responses[200];
841
+ logger?.debug("Schema detection:", {
842
+ hasSchema: !!schema200,
843
+ type: typeof schema200,
844
+ keys: schema200 && typeof schema200 === "object" ? Object.keys(schema200) : []
845
+ });
846
+ if (schema200 && typeof schema200 === "object" && "parse" in schema200 && typeof schema200.parse === "function") serializableSchema = schema200;
847
+ }
848
+ let requestBody;
849
+ const body = options.body;
850
+ logger?.debug("Request URL: " + options.url);
851
+ logger?.debug("Request method: " + options.method);
852
+ logger?.debug("options.body: " + (options.body ? JSON.stringify(options.body).substring(0, 500) : "UNDEFINED"));
853
+ logger?.debug("options.bodySchema: " + (options.bodySchema ? "present" : "undefined"));
854
+ logger?.debug("bodySerializableSchema: " + (bodySerializableSchema ? "present" : "undefined"));
855
+ logger?.debug("Body type: " + typeof body);
856
+ logger?.debug("Body value: " + (body ? JSON.stringify(body).substring(0, 500) : "UNDEFINED/NULL"));
857
+ if (body !== void 0 && body !== null) {
858
+ if (typeof body === "string") {
859
+ requestBody = body;
860
+ logger?.debug("Body: using raw string");
861
+ logger?.debug("Body content (first 200 chars):", requestBody.substring(0, 200));
862
+ } else if (bodySerializableSchema) try {
863
+ logger?.debug("Calling bodySerializableSchema.build with body: " + JSON.stringify(body).substring(0, 300));
864
+ requestBody = bodySerializableSchema.build(body);
865
+ logger?.debug("Body: serialized using Serializable.build()");
866
+ logger?.debug("Body output type: " + typeof requestBody);
867
+ logger?.debug("Body output length: " + requestBody?.length);
868
+ logger?.debug("Body content (first 500 chars): " + requestBody?.substring(0, 500));
869
+ } catch (buildError) {
870
+ logger?.error("Schema build() threw error: " + (buildError instanceof Error ? buildError.message : String(buildError)));
871
+ logger?.error("Stack: " + (buildError instanceof Error ? buildError.stack : "N/A"));
872
+ throw buildError;
873
+ }
874
+ else {
875
+ requestBody = JSON.stringify(body);
876
+ logger?.debug("Body: serialized as JSON");
877
+ }
878
+ }
879
+ logger?.debug(`HTTP ${options.method} ${url.toString()}`);
880
+ {
881
+ const headerNames = Object.keys(headers).sort();
882
+ logger?.debug("Request headers (names only): " + JSON.stringify(headerNames));
883
+ }
884
+ const response = await requestFetch(url.toString(), withDispatcher({
885
+ method: options.method,
886
+ headers,
887
+ body: requestBody,
888
+ signal: executionSignal
889
+ }));
890
+ sessionManager.processResponse(response, url.pathname);
891
+ logger?.debug(`Response: ${response.status} ${response.statusText}`);
892
+ logger?.debug("Response headers: " + JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2));
893
+ if (!response.ok) {
894
+ const errorBody = await response.text();
895
+ const adtError = createAdtError(response.status, response.statusText, url.toString(), options.method, errorBody);
896
+ if (response.status === 412) {
897
+ const sentETag = headers["If-Match"] ?? "(none)";
898
+ const diag = `[If-Match: ${sentETag}, URL: ${url.pathname}]`;
899
+ logger?.error(`ETag mismatch: ${options.method} ${url.pathname} sent If-Match: ${sentETag}`);
900
+ Object.defineProperty(adtError, "message", {
901
+ value: `${adtError.message} ${diag}`,
902
+ writable: true,
903
+ configurable: true
904
+ });
905
+ }
906
+ logger?.error(`Request failed - ${adtError.message} (${options.method} ${url.toString()})`);
907
+ if (adtError.exception) logger?.error(`ADT Exception: namespace=${adtError.exception.namespace}, type=${adtError.exception.type}`);
908
+ if (errorBody && !adtError.exception) logger?.error(`Response body: ${errorBody}`);
909
+ if (response.status === 403) {
910
+ if (!(adtError.exception?.type === "ExceptionResourceNoAccess" && (adtError.message.includes("currently editing") || adtError.message.includes("is being edited")))) {
911
+ sessionManager.clear();
912
+ logger?.warn("Session cleared due to 403 Forbidden response");
913
+ } else logger?.warn("Lock conflict (403) — session preserved (object locked by another session)");
914
+ }
915
+ throw adtError;
916
+ }
917
+ const contentType = response.headers.get("content-type") || "";
918
+ const rawText = await response.text();
919
+ if (contentType.includes("text/html") && rawText.includes("SAMLRequest")) {
920
+ sessionManager.clear();
921
+ logger?.warn("SAML redirect detected - session expired");
922
+ if (onSessionExpired) {
923
+ logger?.info("Attempting automatic re-authentication...");
924
+ try {
925
+ const newCookie = await onSessionExpired();
926
+ sessionManager.injectCookie(newCookie);
927
+ logger?.info("Re-authentication successful, retrying request...");
928
+ return this.request(options);
929
+ } catch (refreshError) {
930
+ logger?.error("Re-authentication failed");
931
+ throw new Error("Session expired - re-authentication failed", { cause: refreshError });
932
+ }
933
+ }
934
+ throw new Error("Session expired - SAML re-authentication required");
935
+ }
936
+ let data;
937
+ if (contentType.includes("application/json") || contentType.includes("+json")) data = JSON.parse(rawText);
938
+ else if (contentType.includes("text/") || contentType.includes("xml")) {
939
+ if (serializableSchema && contentType.includes("xml")) data = serializableSchema.parse(rawText);
940
+ else data = rawText;
941
+ } else data = rawText;
942
+ if (plugins.length > 0) {
943
+ const context = {
944
+ rawText,
945
+ parsedData: data,
946
+ url: url.toString(),
947
+ method: options.method,
948
+ contentType
949
+ };
950
+ for (const plugin of plugins) {
951
+ const result = await plugin.process(context);
952
+ if (result !== void 0) {
953
+ data = result;
954
+ context.parsedData = result;
955
+ }
956
+ }
957
+ }
958
+ return data;
959
+ },
960
+ clearETag(url) {
961
+ sessionManager.clearETag(url);
962
+ },
963
+ async readTextBounded(options, maxBytes) {
964
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new RangeError("maxBytes must be a non-negative safe integer.");
965
+ const url = resolveAdtUrl(options.url);
966
+ if (client) url.searchParams.append("sap-client", client);
967
+ if (language) url.searchParams.append("sap-language", language);
968
+ const headers = {
969
+ "X-sap-adt-sessiontype": sessionManager.getSessionTypeHeader(),
970
+ ...sessionManager.getRequestHeaders("GET", url.pathname),
971
+ ...options.headers
972
+ };
973
+ if (authHeader) headers.Authorization = authHeader;
974
+ const { abortController, dispose } = executionAbortController();
975
+ try {
976
+ const response = await requestFetch(url.toString(), withDispatcher({
977
+ method: "GET",
978
+ headers,
979
+ signal: abortController.signal
980
+ }));
981
+ sessionManager.processResponse(response, url.pathname);
982
+ const text = await readResponseTextBounded(response, maxBytes, abortController);
983
+ if (!response.ok) throw createAdtError(response.status, response.statusText, url.toString(), "GET", text);
984
+ return text;
985
+ } finally {
986
+ dispose();
987
+ }
988
+ }
989
+ };
990
+ }
991
+
992
+ //#endregion
993
+ //#region src/services/transports.ts
994
+ /**
995
+ * Transport Service - orchestrates CTS operations
996
+ */
997
+ var TransportService = class {
998
+ adt;
999
+ constructor(adt) {
1000
+ this.adt = adt;
1001
+ }
1002
+ /**
1003
+ * List transport requests
1004
+ */
1005
+ async list(options) {
1006
+ const response = await this.adt.cts.transportrequests.list(options);
1007
+ return this.extractRequests(response).map((req) => this.mapToTransport(req));
1008
+ }
1009
+ /**
1010
+ * Get a single transport by number
1011
+ */
1012
+ async get(trkorr) {
1013
+ const data = await this.adt.cts.transportrequests.get(trkorr);
1014
+ const unwrapped = data?.root && typeof data.root === "object" ? data.root : data;
1015
+ const wanted = trkorr.trim().toUpperCase();
1016
+ const request = this.findUnit(unwrapped?.task, wanted) ?? this.findUnit(unwrapped?.request, wanted) ?? unwrapped?.request ?? unwrapped?.task ?? unwrapped;
1017
+ return this.mapToTransport(request);
1018
+ }
1019
+ /**
1020
+ * Create a new transport request
1021
+ *
1022
+ * Note: Full create implementation requires XML body building.
1023
+ * For now, this is a placeholder that throws a helpful error.
1024
+ * Use ADK's AdkTransportRequest.create() for full functionality.
1025
+ */
1026
+ async create(_options) {
1027
+ throw new Error("Transport creation via service layer not yet implemented. Use ADK's AdkTransportRequest.create() for now.");
1028
+ }
1029
+ /**
1030
+ * Delete a transport request
1031
+ */
1032
+ async delete(trkorr) {
1033
+ await this.adt.cts.transportrequests.delete(trkorr);
1034
+ }
1035
+ /**
1036
+ * Release a transport request
1037
+ *
1038
+ * Note: Release requires POST with specific action parameter.
1039
+ * This is a placeholder for the full implementation.
1040
+ */
1041
+ async release(_trkorr) {
1042
+ throw new Error("Transport release via service layer not yet implemented.");
1043
+ }
1044
+ /**
1045
+ * Find a request/task unit whose number matches the wanted transport id.
1046
+ * Handles both single-object and array shapes from ts-xsd.
1047
+ */
1048
+ findUnit(node, wanted) {
1049
+ if (!node) return void 0;
1050
+ return (Array.isArray(node) ? node : [node]).find((item) => {
1051
+ const u = item;
1052
+ return String(u?.trkorr ?? u?.number ?? "").toUpperCase() === wanted;
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Extract requests array from various response formats
1057
+ */
1058
+ extractRequests(response) {
1059
+ if (!response) return [];
1060
+ const data = response;
1061
+ if (Array.isArray(data.request)) return data.request;
1062
+ if (data.request && typeof data.request === "object") return [data.request];
1063
+ if (Array.isArray(response)) return response;
1064
+ if (data.root && typeof data.root === "object") return this.extractRequests(data.root);
1065
+ return [];
1066
+ }
1067
+ /**
1068
+ * Map raw request data to Transport interface
1069
+ */
1070
+ mapToTransport(req) {
1071
+ const data = req;
1072
+ return {
1073
+ number: String(data.trkorr ?? data.number ?? ""),
1074
+ desc: data.as4text ?? data.desc,
1075
+ owner: data.as4user ?? data.owner,
1076
+ status: data.trstatus ?? data.status,
1077
+ type: data.trfunction ?? data.type,
1078
+ target: data.tarsystem ?? data.target,
1079
+ parent: data.parent,
1080
+ lastChangedTimestamp: data.lastchanged_timestamp ?? data.lastChangedTimestamp,
1081
+ tasks: this.extractTasks(data)
1082
+ };
1083
+ }
1084
+ /**
1085
+ * Extract tasks from transport data
1086
+ */
1087
+ extractTasks(data) {
1088
+ const tasks = data.task ?? data.tasks;
1089
+ if (!tasks) return [];
1090
+ return (Array.isArray(tasks) ? tasks : [tasks]).map((task) => {
1091
+ const t = task;
1092
+ return {
1093
+ number: String(t.trkorr ?? t.number ?? ""),
1094
+ owner: t.as4user ?? t.owner,
1095
+ status: t.trstatus ?? t.status,
1096
+ desc: t.as4text ?? t.desc,
1097
+ type: t.trfunction ?? t.type,
1098
+ parent: t.parent,
1099
+ lastChangedTimestamp: t.lastchanged_timestamp ?? t.lastChangedTimestamp
1100
+ };
1101
+ });
1102
+ }
1103
+ };
1104
+ /**
1105
+ * Create transport service instance
1106
+ */
1107
+ function createTransportService(adt) {
1108
+ return new TransportService(adt);
1109
+ }
1110
+
1111
+ //#endregion
1112
+ //#region src/services/users.ts
1113
+ /** Extract entry array from atomFeed parsed response */
1114
+ function extractEntries(data) {
1115
+ const feedData = data.feed;
1116
+ if (!feedData) return [];
1117
+ const rawEntries = feedData.entry;
1118
+ if (!rawEntries) return [];
1119
+ return Array.isArray(rawEntries) ? rawEntries : [rawEntries];
1120
+ }
1121
+ /** Map raw Atom entries to normalized UserInfo */
1122
+ function toUserInfoList(entries) {
1123
+ return entries.map((e) => ({
1124
+ username: e.id ?? "",
1125
+ fullName: e.title,
1126
+ uri: e.link?.[0]?.href
1127
+ }));
1128
+ }
1129
+ /**
1130
+ * User Service - orchestrates user lookup operations
1131
+ */
1132
+ var UserService = class {
1133
+ adt;
1134
+ constructor(adt) {
1135
+ this.adt = adt;
1136
+ }
1137
+ /** Get the currently authenticated user via systeminformation */
1138
+ async getCurrentUser() {
1139
+ const sysInfo = await this.adt.core.http.systeminformation.getSystemInfo();
1140
+ return {
1141
+ userName: sysInfo.userName ?? "",
1142
+ userFullName: sysInfo.userFullName,
1143
+ systemID: sysInfo.systemID ?? "",
1144
+ client: sysInfo.client ?? ""
1145
+ };
1146
+ }
1147
+ /** Look up a specific user by exact username */
1148
+ async getUserByName(username) {
1149
+ const normalized = username.trim().toUpperCase();
1150
+ if (!normalized) throw new Error("username is required");
1151
+ return toUserInfoList(extractEntries(await this.adt.system.users.get(normalized)));
1152
+ }
1153
+ /** Search users by wildcard query */
1154
+ async searchUsers(query, maxcount = 50) {
1155
+ const normalizedQuery = query.trim();
1156
+ if (!normalizedQuery) throw new Error("query is required");
1157
+ if (!Number.isInteger(maxcount) || maxcount <= 0) throw new Error("maxcount must be a positive integer");
1158
+ return toUserInfoList(extractEntries(await this.adt.system.users.search({
1159
+ querystring: normalizedQuery,
1160
+ maxcount
1161
+ })));
1162
+ }
1163
+ };
1164
+ function createUserService(adt) {
1165
+ return new UserService(adt);
1166
+ }
1167
+
1168
+ //#endregion
1169
+ //#region src/services/source-history.ts
1170
+ var SourceHistoryProtocolError = class extends Error {
1171
+ code;
1172
+ ordinal;
1173
+ constructor(code, message, ordinal) {
1174
+ super(message);
1175
+ this.code = code;
1176
+ this.ordinal = ordinal;
1177
+ this.name = "SourceHistoryProtocolError";
1178
+ }
1179
+ };
1180
+ /**
1181
+ * An immutable source body crossed its configured byte limit.
1182
+ * This error intentionally does not retain the source URI or any source text.
1183
+ */
1184
+ var SourceVersionTooLargeError = class extends Error {
1185
+ maxBytes;
1186
+ receivedBytes;
1187
+ code = "SOURCE_VERSION_TOO_LARGE";
1188
+ constructor(maxBytes, receivedBytes) {
1189
+ super(`Source version exceeds the ${maxBytes}-byte limit.`);
1190
+ this.maxBytes = maxBytes;
1191
+ this.receivedBytes = receivedBytes;
1192
+ this.name = "SourceVersionTooLargeError";
1193
+ }
1194
+ };
1195
+ const ADT_PATH_PREFIX = "/sap/bc/adt/";
1196
+ const TRAVERSAL_SEGMENT = /(?:^|\/)(?:\.{1,2}|%2e(?:%2e)?)(?:\/|$)/i;
1197
+ const TRANSPORT_NUMBER = /^[A-Z0-9]{10}$/;
1198
+ function asOptionalString(value) {
1199
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1200
+ }
1201
+ function asArray(value) {
1202
+ if (value === void 0) return [];
1203
+ return Array.isArray(value) ? value : [value];
1204
+ }
1205
+ /**
1206
+ * Validate a server-relative ADT URI before it reaches the authenticated
1207
+ * adapter. This deliberately rejects absolute and protocol-relative URLs so a
1208
+ * source-history link cannot redirect credentials to another origin.
1209
+ */
1210
+ function isUnsafeAdtUri(uri) {
1211
+ if (uri !== uri.trim()) return true;
1212
+ if (!uri.startsWith(ADT_PATH_PREFIX)) return true;
1213
+ if (uri.startsWith("//")) return true;
1214
+ if (uri.includes("#")) return true;
1215
+ if (uri.includes("\\")) return true;
1216
+ return TRAVERSAL_SEGMENT.test(uri);
1217
+ }
1218
+ function assertAdtUri(uri) {
1219
+ if (isUnsafeAdtUri(uri)) throw new SourceHistoryProtocolError("SOURCE_HISTORY_URI_UNSAFE", "Source-history URI must be a server-relative SAP ADT path.");
1220
+ return uri;
1221
+ }
1222
+ function isUnsafeRelativeHref(href) {
1223
+ if (href.startsWith("//")) return true;
1224
+ if (href.includes("\\")) return true;
1225
+ if (href.includes(":")) return true;
1226
+ return TRAVERSAL_SEGMENT.test(href);
1227
+ }
1228
+ function resolveRelativeAdtUri(href, versionsUri) {
1229
+ const validatedBase = assertAdtUri(versionsUri);
1230
+ const collectionBase = validatedBase.endsWith("/") ? validatedBase : `${validatedBase}/`;
1231
+ const resolved = new URL(href, `https://adt.invalid${collectionBase}`);
1232
+ return assertAdtUri(`${resolved.pathname}${resolved.search}${resolved.hash}`);
1233
+ }
1234
+ function resolveAdtUri(href, versionsUri) {
1235
+ if (href.startsWith("/")) return assertAdtUri(href);
1236
+ if (isUnsafeRelativeHref(href)) throw new SourceHistoryProtocolError("SOURCE_HISTORY_URI_UNSAFE", "Source-history link must stay within the SAP ADT origin.");
1237
+ return resolveRelativeAdtUri(href, versionsUri);
1238
+ }
1239
+ function normalizeLinks(value) {
1240
+ return asArray(value).filter((link) => Boolean(link) && typeof link === "object");
1241
+ }
1242
+ function isContentLink(link) {
1243
+ const rel = asOptionalString(link.rel)?.toLowerCase();
1244
+ return Boolean(rel?.endsWith("/content") || rel === "content");
1245
+ }
1246
+ function isPlainTextContentLink(link) {
1247
+ const contentType = asOptionalString(link.type)?.toLowerCase();
1248
+ return Boolean(contentType?.startsWith("text/plain"));
1249
+ }
1250
+ /**
1251
+ * Some SAP source-version feeds represent the immutable source body with the
1252
+ * standard Atom `content` element rather than an Atom link. Treat it exactly
1253
+ * like a content link, but only when it is explicitly plain text.
1254
+ */
1255
+ function contentElementAsLink(entry) {
1256
+ const content = entry.content;
1257
+ if (!content) return void 0;
1258
+ if (typeof content !== "object") return void 0;
1259
+ if (Array.isArray(content)) return void 0;
1260
+ const src = asOptionalString(content.src);
1261
+ if (!src) return void 0;
1262
+ const type = asOptionalString(content.type);
1263
+ if (!type) return void 0;
1264
+ if (!type.toLowerCase().startsWith("text/plain")) return void 0;
1265
+ return {
1266
+ href: src,
1267
+ type
1268
+ };
1269
+ }
1270
+ function isTransportRel(rel) {
1271
+ return rel.endsWith("/transport") || rel.endsWith("/transport/request");
1272
+ }
1273
+ function transportFromTitle(link) {
1274
+ const title = asOptionalString(link.title)?.toUpperCase();
1275
+ if (!title) return void 0;
1276
+ if (TRANSPORT_NUMBER.test(title)) return title;
1277
+ }
1278
+ function transportFromHref(link) {
1279
+ const href = asOptionalString(link.href);
1280
+ if (!href) return void 0;
1281
+ const lastSegment = (href.split(/[?#]/, 1)[0]?.split("/").filter((segment) => segment !== ""))?.pop();
1282
+ if (!lastSegment) return void 0;
1283
+ try {
1284
+ const candidate = decodeURIComponent(lastSegment).toUpperCase();
1285
+ return TRANSPORT_NUMBER.test(candidate) ? candidate : void 0;
1286
+ } catch {
1287
+ return;
1288
+ }
1289
+ }
1290
+ function transportFromLink(link) {
1291
+ const rel = asOptionalString(link.rel)?.toLowerCase();
1292
+ if (!rel || !isTransportRel(rel)) return void 0;
1293
+ return transportFromTitle(link) ?? transportFromHref(link);
1294
+ }
1295
+ function authorName(value) {
1296
+ return asOptionalString(asArray(value).find((candidate) => Boolean(candidate) && typeof candidate === "object")?.name);
1297
+ }
1298
+ /**
1299
+ * Normalize the parsed Atom versions feed while preserving SAP's feed order.
1300
+ * The result contains references and provenance only; source bodies are fetched
1301
+ * separately and are never retained by this function.
1302
+ */
1303
+ function assertSourceVersionPayload(payload) {
1304
+ if (!payload || typeof payload !== "object") throw new SourceHistoryProtocolError("SOURCE_VERSION_FEED_INVALID", "SAP ADT source-history response is not an Atom feed.");
1305
+ if (!("feed" in payload)) throw new SourceHistoryProtocolError("SOURCE_VERSION_FEED_INVALID", "SAP ADT source-history response is not an Atom feed.");
1306
+ return payload;
1307
+ }
1308
+ function assertSourceVersionFeed(feed) {
1309
+ if (!feed) throw invalidFeedError();
1310
+ if (typeof feed !== "object") throw invalidFeedError();
1311
+ if (Array.isArray(feed)) throw invalidFeedError();
1312
+ return feed;
1313
+ }
1314
+ function invalidFeedError() {
1315
+ return new SourceHistoryProtocolError("SOURCE_VERSION_FEED_INVALID", "SAP ADT source-history feed has an invalid root.");
1316
+ }
1317
+ function readSourceVersionEntries(feed) {
1318
+ if (feed.entry === void 0) return [];
1319
+ const entries = asArray(feed.entry);
1320
+ if (entries.some((entry) => !entry || typeof entry !== "object")) throw new SourceHistoryProtocolError("SOURCE_VERSION_FEED_INVALID", "SAP ADT source-history feed contains an invalid entry.");
1321
+ return entries;
1322
+ }
1323
+ function extractContentLink(entry, ordinal) {
1324
+ const links = normalizeLinks(entry.link);
1325
+ const contentLink = links.find(isContentLink) ?? links.find(isPlainTextContentLink) ?? contentElementAsLink(entry);
1326
+ const href = contentLink && asOptionalString(contentLink.href);
1327
+ if (!contentLink || !href) throw new SourceHistoryProtocolError("SOURCE_VERSION_CONTENT_LINK_MISSING", `Source-history entry ${ordinal} has no immutable content link.`, ordinal);
1328
+ return {
1329
+ ...contentLink,
1330
+ href
1331
+ };
1332
+ }
1333
+ function extractEntryTransports(links) {
1334
+ return [...new Set(links.map(transportFromLink).filter((transport) => Boolean(transport)))];
1335
+ }
1336
+ function sourceVersionRefFromEntry(entry, ordinal, versionsUri) {
1337
+ const id = asOptionalString(entry.id);
1338
+ if (!id) throw new SourceHistoryProtocolError("SOURCE_VERSION_ID_MISSING", `Source-history entry ${ordinal} has no stable version id.`, ordinal);
1339
+ const contentLink = extractContentLink(entry, ordinal);
1340
+ return {
1341
+ id,
1342
+ ordinal,
1343
+ title: asOptionalString(entry.title),
1344
+ sourceUri: resolveAdtUri(contentLink.href, versionsUri),
1345
+ contentType: asOptionalString(contentLink.type),
1346
+ etag: asOptionalString(contentLink.etag),
1347
+ updatedAt: asOptionalString(entry.updated),
1348
+ author: authorName(entry.author),
1349
+ transports: extractEntryTransports(normalizeLinks(entry.link))
1350
+ };
1351
+ }
1352
+ function normalizeSourceVersionFeed(payload, versionsUri) {
1353
+ assertAdtUri(versionsUri);
1354
+ return readSourceVersionEntries(assertSourceVersionFeed(assertSourceVersionPayload(payload).feed)).map((entry, ordinal) => sourceVersionRefFromEntry(entry, ordinal, versionsUri));
1355
+ }
1356
+ var SourceHistoryService = class {
1357
+ adt;
1358
+ boundedSourceReader;
1359
+ constructor(adt, boundedSourceReader) {
1360
+ this.adt = adt;
1361
+ this.boundedSourceReader = boundedSourceReader;
1362
+ }
1363
+ async listVersions(versionsUri) {
1364
+ assertAdtUri(versionsUri);
1365
+ return normalizeSourceVersionFeed(await this.adt.repository.sourceversions.list({ versionsUri }), versionsUri);
1366
+ }
1367
+ async readVersionSource(sourceUri) {
1368
+ assertAdtUri(sourceUri);
1369
+ return this.adt.repository.sourceversions.get({ sourceUri });
1370
+ }
1371
+ async readVersionSourceBounded(sourceUri, maxBytes) {
1372
+ assertAdtUri(sourceUri);
1373
+ if (!this.boundedSourceReader) throw new Error("Bounded source reads are not configured.");
1374
+ try {
1375
+ return await this.boundedSourceReader(sourceUri, maxBytes);
1376
+ } catch (error) {
1377
+ if (error instanceof AdtResponseTooLargeError) throw new SourceVersionTooLargeError(error.maxBytes, error.receivedBytes);
1378
+ throw error;
1379
+ }
1380
+ }
1381
+ };
1382
+ function createSourceHistoryService(adt, boundedSourceReader) {
1383
+ return new SourceHistoryService(adt, boundedSourceReader);
1384
+ }
1385
+
1386
+ //#endregion
1387
+ //#region src/client.ts
1388
+ /**
1389
+ * ADT Client V2 - Contract-Based Architecture
1390
+ *
1391
+ * Provides typed access to SAP ADT REST APIs:
1392
+ * - client.adt.* - Typed ADT REST contracts (speci-generated)
1393
+ * - client.fetch() - Generic HTTP utility for raw requests
1394
+ *
1395
+ * Note: Business logic (transport management, etc.) has moved to @abapify/adk
1396
+ * Use AdkTransportRequest for transport operations.
1397
+ */
1398
+ function createAdtClient(config) {
1399
+ const adapter = createAdtAdapter(config);
1400
+ const adtClient = createAdtClient$1({
1401
+ baseUrl: config.baseUrl,
1402
+ adapter
1403
+ });
1404
+ const fetchFn = async (url, options) => {
1405
+ const requestOptions = {
1406
+ method: options?.method || "GET",
1407
+ url,
1408
+ headers: options?.headers || {},
1409
+ body: options?.body
1410
+ };
1411
+ return adapter.request(requestOptions);
1412
+ };
1413
+ const readTextBounded = async (url, maxBytes, options) => adapter.readTextBounded({
1414
+ url,
1415
+ headers: options?.headers
1416
+ }, maxBytes);
1417
+ return {
1418
+ /**
1419
+ * Typed ADT REST contracts
1420
+ * Direct access to speci-generated client methods
1421
+ */
1422
+ adt: adtClient,
1423
+ /**
1424
+ * Services layer - business logic orchestration
1425
+ * Higher-level operations that combine multiple contract calls
1426
+ */
1427
+ services: {
1428
+ transports: createTransportService(adtClient),
1429
+ sourceHistory: createSourceHistoryService(adtClient, (sourceUri, maxBytes) => readTextBounded(sourceUri, maxBytes, { headers: { Accept: "text/plain" } }))
1430
+ },
1431
+ /**
1432
+ * Generic fetch utility for arbitrary ADT endpoints
1433
+ * Useful for debugging, testing, or accessing undocumented APIs
1434
+ *
1435
+ * @param url - The ADT endpoint path (e.g., '/sap/bc/adt/core/http/sessions')
1436
+ * @param options - Request options (method, headers, body)
1437
+ * @returns Raw response
1438
+ */
1439
+ fetch: fetchFn,
1440
+ /**
1441
+ * Read an authenticated plain-text endpoint while enforcing a byte limit.
1442
+ */
1443
+ readTextBounded,
1444
+ /**
1445
+ * Clear cached ETag for a specific URL, or all ETags if no URL given.
1446
+ * Use after operations that may invalidate server-side ETags.
1447
+ */
1448
+ clearETag: (url) => adapter.clearETag(url)
1449
+ };
1450
+ }
1451
+
1452
+ //#endregion
1453
+ //#region src/plugins/logging.ts
1454
+ /**
1455
+ * Logging plugin - logs requests and responses
1456
+ */
1457
+ var LoggingPlugin = class {
1458
+ logger;
1459
+ name = "logging";
1460
+ constructor(logger = console.log) {
1461
+ this.logger = logger;
1462
+ }
1463
+ process(context) {
1464
+ this.logger(`[${context.method}] ${context.url}`, {
1465
+ contentType: context.contentType,
1466
+ hasParsedData: !!context.parsedData,
1467
+ rawSize: context.rawText.length
1468
+ });
1469
+ return context.parsedData;
1470
+ }
1471
+ };
1472
+
1473
+ //#endregion
1474
+ //#region src/plugins/file-logging.ts
1475
+ /**
1476
+ * File Logging Plugin - Saves HTTP responses to files
1477
+ */
1478
+ /**
1479
+ * Plugin that logs HTTP responses to files
1480
+ * Similar to v1's FileLogger functionality
1481
+ */
1482
+ var FileLoggingPlugin = class {
1483
+ config;
1484
+ name = "file-logging";
1485
+ constructor(config) {
1486
+ this.config = config;
1487
+ }
1488
+ process(context) {
1489
+ try {
1490
+ const filePath = this.generateFilePath(context.url, context.method);
1491
+ const fullPath = join(this.config.outputDir, filePath);
1492
+ mkdirSync(dirname(fullPath), { recursive: true });
1493
+ writeFileSync(fullPath, context.rawText, "utf8");
1494
+ this.config.logger?.trace(`Wrote response to: ${fullPath}`);
1495
+ if (this.config.writeMetadata) {
1496
+ const metadataPath = this.getMetadataPath(fullPath);
1497
+ const metadata = {
1498
+ url: context.url,
1499
+ method: context.method,
1500
+ contentType: context.contentType,
1501
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1502
+ size: context.rawText.length
1503
+ };
1504
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
1505
+ this.config.logger?.trace(`Wrote metadata to: ${metadataPath}`);
1506
+ }
1507
+ } catch (error) {
1508
+ this.config.logger?.error(`Failed to write response file: ${error instanceof Error ? error.message : String(error)}`);
1509
+ }
1510
+ return context.parsedData;
1511
+ }
1512
+ /**
1513
+ * Generate file path for HTTP response
1514
+ * Converts URL to fixture-style path structure
1515
+ */
1516
+ generateFilePath(url, method) {
1517
+ const urlObj = new URL(url);
1518
+ let path = urlObj.pathname;
1519
+ path = path.replace(/^\/sap\/bc\/adt\/?/, "");
1520
+ if (!path || path === "discovery") return "./adt/core/discovery.xml";
1521
+ const [basePath] = path.split("?");
1522
+ const segments = basePath.split("/").filter((s) => s);
1523
+ const sourceTypes = [
1524
+ "main",
1525
+ "definitions",
1526
+ "implementations",
1527
+ "macros",
1528
+ "testclasses"
1529
+ ];
1530
+ const lastSegment = segments[segments.length - 1];
1531
+ if (sourceTypes.includes(lastSegment)) return `./adt/${segments.join("/")}`;
1532
+ const requestId = Date.now().toString() + Math.random().toString(36).slice(2, 7);
1533
+ let dirPath = `./adt/${segments.join("/")}`;
1534
+ if (urlObj.search) {
1535
+ const sanitizedQuery = urlObj.search.slice(1).replaceAll(/[^a-zA-Z0-9_-]/g, "_");
1536
+ dirPath += `/${sanitizedQuery}`;
1537
+ }
1538
+ const methodPrefix = method !== "GET" ? `${method.toLowerCase()}-` : "";
1539
+ return `${dirPath}/${methodPrefix}${requestId}-response.xml`;
1540
+ }
1541
+ /**
1542
+ * Get metadata file path for a response file
1543
+ */
1544
+ getMetadataPath(responsePath) {
1545
+ if (/-response\.xml$/.test(responsePath)) return responsePath.replace(/-response\.xml$/, "-metadata.json");
1546
+ if (/\.xml$/.test(responsePath)) return responsePath.replace(/\.xml$/, ".metadata.json");
1547
+ return `${responsePath}.metadata.json`;
1548
+ }
1549
+ };
1550
+
1551
+ //#endregion
1552
+ export { AdtResponseTooLargeError, CookieStore, CsrfTokenManager, FileLoggingPlugin, LoggingPlugin, SessionManager, SourceHistoryProtocolError, SourceHistoryService, SourceVersionTooLargeError, TransportService, UserService, adtContract, assertAdtUri, createAdtAdapter, createAdtClient, createSourceHistoryService, createTransportService, createUserService, normalizeSourceVersionFeed, runWithAdtAbortSignal };
1553
+ //# sourceMappingURL=index.mjs.map