catalyst-relay 0.4.3 → 0.4.4

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.d.mts CHANGED
@@ -64,6 +64,15 @@ interface SsoAuthConfig {
64
64
  * Union of all auth configurations
65
65
  */
66
66
  type AuthConfig = BasicAuthConfig | SamlAuthConfig | SsoAuthConfig;
67
+ /**
68
+ * Auto-refresh configuration for session keepalive
69
+ */
70
+ interface AutoRefreshConfig {
71
+ /** Enable automatic session refresh (default: true) */
72
+ enabled: boolean;
73
+ /** Refresh interval in milliseconds (default: 7200000 = 2 hours) */
74
+ intervalMs?: number;
75
+ }
67
76
  /**
68
77
  * Client configuration for connecting to SAP ADT
69
78
  */
@@ -78,6 +87,8 @@ interface ClientConfig {
78
87
  timeout?: number;
79
88
  /** Skip SSL verification (dev only) */
80
89
  insecure?: boolean;
90
+ /** Auto-refresh configuration for session keepalive (default: enabled with 2-hour interval) */
91
+ autoRefresh?: AutoRefreshConfig;
81
92
  }
82
93
 
83
94
  /**
@@ -187,6 +198,27 @@ interface Session {
187
198
  expiresAt: number;
188
199
  }
189
200
 
201
+ /**
202
+ * Session Refresh via Reentrance Ticket
203
+ *
204
+ * Fetches a reentrance ticket from SAP ADT to keep the session alive.
205
+ * Eclipse ADT uses this mechanism to maintain sessions across extended periods.
206
+ *
207
+ * Endpoint: GET /sap/bc/adt/security/reentranceticket
208
+ * - Returns a base64-encoded SSO ticket
209
+ * - Refreshes server-side session cookies (MYSAPSSO2)
210
+ */
211
+
212
+ /**
213
+ * Result of a session refresh operation
214
+ */
215
+ interface RefreshResult {
216
+ /** Base64-encoded reentrance ticket */
217
+ ticket: string;
218
+ /** Updated session expiration timestamp (ms since epoch) */
219
+ expiresAt: number;
220
+ }
221
+
190
222
  interface ObjectConfig {
191
223
  /** ADT endpoint path (e.g., 'ddic/ddl/sources') */
192
224
  endpoint: string;
@@ -273,16 +305,11 @@ interface FolderNode {
273
305
  displayName: string;
274
306
  numContents: number;
275
307
  }
276
- interface ApiState {
277
- useInCloudDevelopment: boolean;
278
- useInCloudDvlpmntActive: boolean;
279
- useInKeyUserApps: boolean;
280
- }
281
308
  interface ObjectNode {
282
309
  name: string;
283
310
  objectType: string;
284
311
  extension: string;
285
- apiState?: ApiState;
312
+ description?: string;
286
313
  }
287
314
 
288
315
  /**
@@ -487,6 +514,7 @@ interface ADTClient {
487
514
  readonly session: Session | null;
488
515
  login(): AsyncResult<Session>;
489
516
  logout(): AsyncResult<void>;
517
+ refreshSession(): AsyncResult<RefreshResult>;
490
518
  read(objects: ObjectRef[]): AsyncResult<ObjectWithContent[]>;
491
519
  create(object: ObjectContent, packageName: string, transport?: string): AsyncResult<void>;
492
520
  update(object: ObjectContent, transport?: string): AsyncResult<void>;
@@ -509,4 +537,4 @@ interface ADTClient {
509
537
  }
510
538
  declare function createClient(config: ClientConfig): Result<ADTClient, Error>;
511
539
 
512
- export { type ADTClient, type ActivationMessage, type ActivationResult, type Aggregation, type ApiResponse, type ApiState, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type BasicFilter, type BetweenFilter, type ClientConfig, type ColumnInfo, type DataFrame, type DataPreviewQuery, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type FolderNode, type ListFilter, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectNode, type ObjectRef, type ObjectWithContent, type Package, type PackageNode, type Parameter, type PreviewSQL, type QueryFilter, type Result, type SamlAuthConfig, type SearchResult, type Session, type Sorting, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeQuery, type TreeResponse, type UpsertResult, buildSQLQuery, createClient, err, ok };
540
+ export { type ADTClient, type ActivationMessage, type ActivationResult, type Aggregation, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type BasicFilter, type BetweenFilter, type ClientConfig, type ColumnInfo, type DataFrame, type DataPreviewQuery, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type FolderNode, type ListFilter, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectNode, type ObjectRef, type ObjectWithContent, type Package, type PackageNode, type Parameter, type PreviewSQL, type QueryFilter, type Result, type SamlAuthConfig, type SearchResult, type Session, type Sorting, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeQuery, type TreeResponse, type UpsertResult, buildSQLQuery, createClient, err, ok };
package/dist/index.d.ts CHANGED
@@ -64,6 +64,15 @@ interface SsoAuthConfig {
64
64
  * Union of all auth configurations
65
65
  */
66
66
  type AuthConfig = BasicAuthConfig | SamlAuthConfig | SsoAuthConfig;
67
+ /**
68
+ * Auto-refresh configuration for session keepalive
69
+ */
70
+ interface AutoRefreshConfig {
71
+ /** Enable automatic session refresh (default: true) */
72
+ enabled: boolean;
73
+ /** Refresh interval in milliseconds (default: 7200000 = 2 hours) */
74
+ intervalMs?: number;
75
+ }
67
76
  /**
68
77
  * Client configuration for connecting to SAP ADT
69
78
  */
@@ -78,6 +87,8 @@ interface ClientConfig {
78
87
  timeout?: number;
79
88
  /** Skip SSL verification (dev only) */
80
89
  insecure?: boolean;
90
+ /** Auto-refresh configuration for session keepalive (default: enabled with 2-hour interval) */
91
+ autoRefresh?: AutoRefreshConfig;
81
92
  }
82
93
 
83
94
  /**
@@ -187,6 +198,27 @@ interface Session {
187
198
  expiresAt: number;
188
199
  }
189
200
 
201
+ /**
202
+ * Session Refresh via Reentrance Ticket
203
+ *
204
+ * Fetches a reentrance ticket from SAP ADT to keep the session alive.
205
+ * Eclipse ADT uses this mechanism to maintain sessions across extended periods.
206
+ *
207
+ * Endpoint: GET /sap/bc/adt/security/reentranceticket
208
+ * - Returns a base64-encoded SSO ticket
209
+ * - Refreshes server-side session cookies (MYSAPSSO2)
210
+ */
211
+
212
+ /**
213
+ * Result of a session refresh operation
214
+ */
215
+ interface RefreshResult {
216
+ /** Base64-encoded reentrance ticket */
217
+ ticket: string;
218
+ /** Updated session expiration timestamp (ms since epoch) */
219
+ expiresAt: number;
220
+ }
221
+
190
222
  interface ObjectConfig {
191
223
  /** ADT endpoint path (e.g., 'ddic/ddl/sources') */
192
224
  endpoint: string;
@@ -273,16 +305,11 @@ interface FolderNode {
273
305
  displayName: string;
274
306
  numContents: number;
275
307
  }
276
- interface ApiState {
277
- useInCloudDevelopment: boolean;
278
- useInCloudDvlpmntActive: boolean;
279
- useInKeyUserApps: boolean;
280
- }
281
308
  interface ObjectNode {
282
309
  name: string;
283
310
  objectType: string;
284
311
  extension: string;
285
- apiState?: ApiState;
312
+ description?: string;
286
313
  }
287
314
 
288
315
  /**
@@ -487,6 +514,7 @@ interface ADTClient {
487
514
  readonly session: Session | null;
488
515
  login(): AsyncResult<Session>;
489
516
  logout(): AsyncResult<void>;
517
+ refreshSession(): AsyncResult<RefreshResult>;
490
518
  read(objects: ObjectRef[]): AsyncResult<ObjectWithContent[]>;
491
519
  create(object: ObjectContent, packageName: string, transport?: string): AsyncResult<void>;
492
520
  update(object: ObjectContent, transport?: string): AsyncResult<void>;
@@ -509,4 +537,4 @@ interface ADTClient {
509
537
  }
510
538
  declare function createClient(config: ClientConfig): Result<ADTClient, Error>;
511
539
 
512
- export { type ADTClient, type ActivationMessage, type ActivationResult, type Aggregation, type ApiResponse, type ApiState, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type BasicFilter, type BetweenFilter, type ClientConfig, type ColumnInfo, type DataFrame, type DataPreviewQuery, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type FolderNode, type ListFilter, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectNode, type ObjectRef, type ObjectWithContent, type Package, type PackageNode, type Parameter, type PreviewSQL, type QueryFilter, type Result, type SamlAuthConfig, type SearchResult, type Session, type Sorting, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeQuery, type TreeResponse, type UpsertResult, buildSQLQuery, createClient, err, ok };
540
+ export { type ADTClient, type ActivationMessage, type ActivationResult, type Aggregation, type ApiResponse, type AsyncResult, type AuthConfig, type AuthType, type BasicAuthConfig, type BasicFilter, type BetweenFilter, type ClientConfig, type ColumnInfo, type DataFrame, type DataPreviewQuery, type Dependency, type DiffResult, type DistinctResult, type ErrorCode, type ErrorResponse, type FolderNode, type ListFilter, type ObjectConfig, type ObjectContent, type ObjectMetadata, type ObjectNode, type ObjectRef, type ObjectWithContent, type Package, type PackageNode, type Parameter, type PreviewSQL, type QueryFilter, type Result, type SamlAuthConfig, type SearchResult, type Session, type Sorting, type SsoAuthConfig, type SuccessResponse, type Transport, type TransportConfig, type TreeQuery, type TreeResponse, type UpsertResult, buildSQLQuery, createClient, err, ok };
package/dist/index.js CHANGED
@@ -237,7 +237,11 @@ var clientConfigSchema = import_zod.z.object({
237
237
  })
238
238
  ]),
239
239
  timeout: import_zod.z.number().positive().optional(),
240
- insecure: import_zod.z.boolean().optional()
240
+ insecure: import_zod.z.boolean().optional(),
241
+ autoRefresh: import_zod.z.object({
242
+ enabled: import_zod.z.boolean(),
243
+ intervalMs: import_zod.z.number().positive().optional()
244
+ }).optional()
241
245
  });
242
246
 
243
247
  // src/core/session/types.ts
@@ -351,6 +355,33 @@ async function sessionReset(state, request3) {
351
355
  return ok(void 0);
352
356
  }
353
357
 
358
+ // src/core/session/refresh.ts
359
+ var REENTRANCE_TICKET_PATH = "/sap/bc/adt/security/reentranceticket";
360
+ async function refreshSession(state, request3) {
361
+ if (!state.session) {
362
+ return err(new Error("Not logged in"));
363
+ }
364
+ debug("Fetching reentrance ticket to refresh session...");
365
+ const [response, reqErr] = await request3({
366
+ method: "GET",
367
+ path: REENTRANCE_TICKET_PATH,
368
+ headers: { "Accept": "text/plain" }
369
+ });
370
+ if (reqErr) {
371
+ return err(new Error(`Session refresh failed: ${reqErr.message}`));
372
+ }
373
+ if (!response.ok) {
374
+ const text = await response.text();
375
+ return err(new Error(`Session refresh failed (${response.status}): ${text}`));
376
+ }
377
+ const ticket = await response.text();
378
+ debug(`Received reentrance ticket: ${ticket.substring(0, 20)}...`);
379
+ const timeout = getSessionTimeout(state.config.auth.type);
380
+ state.session.expiresAt = Date.now() + timeout;
381
+ debug(`Session refreshed, new expiration: ${new Date(state.session.expiresAt).toISOString()}`);
382
+ return ok({ ticket, expiresAt: state.session.expiresAt });
383
+ }
384
+
354
385
  // src/core/adt/types.ts
355
386
  var OBJECT_CONFIG_MAP = {
356
387
  "asddls": {
@@ -745,14 +776,6 @@ async function getPackages(client, filter = "*") {
745
776
  return ok(packages);
746
777
  }
747
778
 
748
- // src/core/adt/discovery/tree/types.ts
749
- var API_FOLDERS = [
750
- "NOT_RELEASED",
751
- "USE_IN_CLOUD_DEVELOPMENT",
752
- "USE_IN_CLOUD_DVLPMNT_ACTIVE",
753
- "USE_IN_KEY_USER_APPS"
754
- ];
755
-
756
779
  // src/core/adt/discovery/tree/parsers.ts
757
780
  function buildQueryFromPath(packageName, path) {
758
781
  const query = {
@@ -795,13 +818,14 @@ function constructTreeBody(query, searchPattern) {
795
818
  const specifiedXml = Object.entries(specified).map(([facet, name]) => ` <vfs:preselection facet="${facet.toLowerCase()}">
796
819
  <vfs:value>${name}</vfs:value>
797
820
  </vfs:preselection>`).join("\n");
798
- const facetsXml = facets.map((facet) => ` <vfs:facet>${facet.toLowerCase()}</vfs:facet>`).join("\n");
821
+ const atObjectLevel = query.PACKAGE && query.GROUP && query.TYPE;
822
+ const facetorderXml = atObjectLevel || facets.length === 0 ? " <vfs:facetorder/>" : ` <vfs:facetorder>
823
+ ${facets.map((f) => ` <vfs:facet>${f.toLowerCase()}</vfs:facet>`).join("\n")}
824
+ </vfs:facetorder>`;
799
825
  return `<?xml version="1.0" encoding="UTF-8"?>
800
826
  <vfs:virtualFoldersRequest xmlns:vfs="http://www.sap.com/adt/ris/virtualFolders" objectSearchPattern="${searchPattern}">
801
827
  ${specifiedXml}
802
- <vfs:facetorder>
803
- ${facetsXml}
804
- </vfs:facetorder>
828
+ ${facetorderXml}
805
829
  </vfs:virtualFoldersRequest>`;
806
830
  }
807
831
  function parseTreeXml(xml) {
@@ -842,11 +866,14 @@ function parseTreeXml(xml) {
842
866
  if (!name || !type) continue;
843
867
  const config = getConfigByType(type);
844
868
  if (!config) continue;
845
- objects.push({
869
+ const text = obj.getAttribute("text");
870
+ const parsedObj = {
846
871
  name,
847
872
  objectType: config.label,
848
873
  extension: config.extension
849
- });
874
+ };
875
+ if (text) parsedObj.description = text;
876
+ objects.push(parsedObj);
850
877
  }
851
878
  return ok({ folders, objects });
852
879
  }
@@ -870,11 +897,15 @@ function transformToTreeResponse(parsed, queryPackage) {
870
897
  });
871
898
  }
872
899
  }
873
- const objects = parsed.objects.map((obj) => ({
874
- name: obj.name,
875
- objectType: obj.objectType,
876
- extension: obj.extension
877
- }));
900
+ const objects = parsed.objects.map((obj) => {
901
+ const node = {
902
+ name: obj.name,
903
+ objectType: obj.objectType,
904
+ extension: obj.extension
905
+ };
906
+ if (obj.description) node.description = obj.description;
907
+ return node;
908
+ });
878
909
  return { packages, folders, objects };
879
910
  }
880
911
 
@@ -938,58 +969,6 @@ async function fetchVirtualFolders(client, query) {
938
969
  const text = await response.text();
939
970
  return parseTreeXml(text);
940
971
  }
941
- async function fetchObjectsWithApiState(client, packageName, pathSegments, apiFolders) {
942
- const group = pathSegments[0];
943
- const type = pathSegments[1];
944
- if (!group || !type) return ok([]);
945
- const apiQueries = apiFolders.map((apiFolder) => ({
946
- apiFolder,
947
- query: {
948
- PACKAGE: { name: `..${packageName}`, hasChildrenOfSameFacet: false },
949
- GROUP: { name: group, hasChildrenOfSameFacet: false },
950
- TYPE: { name: type, hasChildrenOfSameFacet: false },
951
- API: { name: apiFolder, hasChildrenOfSameFacet: false }
952
- }
953
- }));
954
- const results = await Promise.all(
955
- apiQueries.map(async ({ apiFolder, query }) => {
956
- const [parsed, parseErr] = await fetchVirtualFolders(client, query);
957
- if (parseErr) return { apiFolder, objects: [], error: parseErr };
958
- return { apiFolder, objects: parsed.objects, error: null };
959
- })
960
- );
961
- const errors = results.filter((r) => r.error !== null);
962
- if (errors.length === results.length) {
963
- return err(errors[0].error);
964
- }
965
- const objectMap = /* @__PURE__ */ new Map();
966
- for (const { apiFolder, objects } of results) {
967
- for (const obj of objects) {
968
- let node = objectMap.get(obj.name);
969
- if (!node) {
970
- node = {
971
- name: obj.name,
972
- objectType: obj.objectType,
973
- extension: obj.extension,
974
- apiState: {
975
- useInCloudDevelopment: false,
976
- useInCloudDvlpmntActive: false,
977
- useInKeyUserApps: false
978
- }
979
- };
980
- objectMap.set(obj.name, node);
981
- }
982
- if (apiFolder === "USE_IN_CLOUD_DEVELOPMENT") {
983
- node.apiState.useInCloudDevelopment = true;
984
- } else if (apiFolder === "USE_IN_CLOUD_DVLPMNT_ACTIVE") {
985
- node.apiState.useInCloudDvlpmntActive = true;
986
- } else if (apiFolder === "USE_IN_KEY_USER_APPS") {
987
- node.apiState.useInKeyUserApps = true;
988
- }
989
- }
990
- }
991
- return ok(Array.from(objectMap.values()));
992
- }
993
972
 
994
973
  // src/core/adt/discovery/tree/index.ts
995
974
  async function getTree(client, query = {}) {
@@ -1019,22 +998,6 @@ async function getTree(client, query = {}) {
1019
998
  const internalQuery = buildQueryFromPath(query.package, query.path);
1020
999
  const [parsed, parseErr] = await fetchVirtualFolders(client, internalQuery);
1021
1000
  if (parseErr) return err(parseErr);
1022
- const pathSegments = query.path?.split("/").filter((s) => s.length > 0) ?? [];
1023
- const hasApiFolders = parsed.folders.length > 0 && parsed.folders.every((f) => f.facet === "API");
1024
- if (pathSegments.length >= 2 && hasApiFolders) {
1025
- const [objects, objErr] = await fetchObjectsWithApiState(
1026
- client,
1027
- query.package,
1028
- pathSegments,
1029
- API_FOLDERS
1030
- );
1031
- if (objErr) return err(objErr);
1032
- return ok({
1033
- packages,
1034
- folders: [],
1035
- objects
1036
- });
1037
- }
1038
1001
  const result = transformToTreeResponse(parsed, query.package);
1039
1002
  result.packages = packages;
1040
1003
  return ok(result);
@@ -2321,6 +2284,7 @@ async function httpsRequest2(url, options) {
2321
2284
  req.end();
2322
2285
  });
2323
2286
  }
2287
+ var DEFAULT_REFRESH_INTERVAL = 30 * 60 * 1e3;
2324
2288
  function buildParams(baseParams, clientNum) {
2325
2289
  const params = new URLSearchParams();
2326
2290
  if (baseParams) {
@@ -2345,6 +2309,8 @@ var ADTClientImpl = class {
2345
2309
  requestor;
2346
2310
  // Store SSO certificates for mTLS authentication
2347
2311
  ssoCerts;
2312
+ // Auto-refresh timer handle
2313
+ refreshTimer = null;
2348
2314
  constructor(config) {
2349
2315
  const authOptions = {
2350
2316
  config: config.auth,
@@ -2382,6 +2348,22 @@ var ADTClientImpl = class {
2382
2348
  if (this.state.cookies.size === 0) return null;
2383
2349
  return Array.from(this.state.cookies.entries()).map(([name, value]) => `${name}=${value}`).join("; ");
2384
2350
  }
2351
+ startAutoRefresh(intervalMs) {
2352
+ this.stopAutoRefresh();
2353
+ this.refreshTimer = setInterval(async () => {
2354
+ if (!this.state.session) return;
2355
+ const [, refreshErr] = await this.refreshSession();
2356
+ if (refreshErr) {
2357
+ debug(`Auto-refresh failed: ${refreshErr.message}`);
2358
+ }
2359
+ }, intervalMs);
2360
+ }
2361
+ stopAutoRefresh() {
2362
+ if (this.refreshTimer) {
2363
+ clearInterval(this.refreshTimer);
2364
+ this.refreshTimer = null;
2365
+ }
2366
+ }
2385
2367
  // Core HTTP request function with CSRF token injection and automatic retry on 403 errors
2386
2368
  async request(options) {
2387
2369
  const { method, path, params, headers: customHeaders, body } = options;
@@ -2473,9 +2455,9 @@ var ADTClientImpl = class {
2473
2455
  async login() {
2474
2456
  const { authStrategy } = this.state;
2475
2457
  if (authStrategy.performLogin) {
2476
- const [, loginErr] = await authStrategy.performLogin(fetch);
2477
- if (loginErr) {
2478
- return err(loginErr);
2458
+ const [, loginErr2] = await authStrategy.performLogin(fetch);
2459
+ if (loginErr2) {
2460
+ return err(loginErr2);
2479
2461
  }
2480
2462
  }
2481
2463
  if (authStrategy.type === "saml" && authStrategy.getCookies) {
@@ -2495,11 +2477,28 @@ var ADTClientImpl = class {
2495
2477
  debug("Stored mTLS certificates for SSO authentication");
2496
2478
  }
2497
2479
  }
2498
- return login(this.state, this.request.bind(this));
2480
+ const [session, loginErr] = await login(this.state, this.request.bind(this));
2481
+ if (loginErr) {
2482
+ return err(loginErr);
2483
+ }
2484
+ const autoRefresh = this.state.config.autoRefresh ?? { enabled: true };
2485
+ if (autoRefresh.enabled) {
2486
+ const interval = autoRefresh.intervalMs ?? DEFAULT_REFRESH_INTERVAL;
2487
+ this.startAutoRefresh(interval);
2488
+ debug(`Auto-refresh started with ${interval}ms interval`);
2489
+ }
2490
+ return ok(session);
2499
2491
  }
2500
2492
  async logout() {
2493
+ this.stopAutoRefresh();
2501
2494
  return logout(this.state, this.request.bind(this));
2502
2495
  }
2496
+ async refreshSession() {
2497
+ if (!this.state.session) {
2498
+ return err(new Error("Not logged in"));
2499
+ }
2500
+ return refreshSession(this.state, this.request.bind(this));
2501
+ }
2503
2502
  // --- CRAUD Operations ---
2504
2503
  async read(objects) {
2505
2504
  if (!this.state.session) return err(new Error("Not logged in"));