abap-local-client 1.4.2 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sso-sap-client.mjs +89 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abap-local-client",
3
- "version": "1.4.2",
3
+ "version": "1.4.5",
4
4
  "description": "SAP Local Client — WebSocket bridge with Basic, SPNEGO, X.509, SAML & SNC authentication",
5
5
  "main": "sso-sap-client.mjs",
6
6
  "type": "module",
@@ -269,6 +269,36 @@ async function handleHttpCall(callspec, httpsAgent) {
269
269
  request_url_resolved: callspec.path.startsWith('http') ? callspec.path : `${sysCfg.baseUrl}${callspec.path.split('?')[0]}`, error: null }));
270
270
  }
271
271
 
272
+ // ════════════════════════════════════════════════════════════════════════════
273
+ // ADT URL helper
274
+ // ════════════════════════════════════════════════════════════════════════════
275
+
276
+ const ADT_TYPE_PATHS = {
277
+ CLAS: '/sap/bc/adt/oo/classes',
278
+ CLASS: '/sap/bc/adt/oo/classes',
279
+ INTF: '/sap/bc/adt/oo/interfaces',
280
+ INTERFACE: '/sap/bc/adt/oo/interfaces',
281
+ PROG: '/sap/bc/adt/programs/programs',
282
+ REPORT: '/sap/bc/adt/programs/programs',
283
+ INCL: '/sap/bc/adt/programs/includes',
284
+ INCLUDE: '/sap/bc/adt/programs/includes',
285
+ TABL: '/sap/bc/adt/ddic/tables',
286
+ TABLE: '/sap/bc/adt/ddic/tables',
287
+ STRU: '/sap/bc/adt/ddic/structures',
288
+ DDLS: '/sap/bc/adt/ddic/ddl/sources',
289
+ CDS: '/sap/bc/adt/ddic/ddl/sources',
290
+ BDEF: '/sap/bc/adt/bo/behaviordefinitions',
291
+ SRVD: '/sap/bc/adt/bo/servicedefinitions',
292
+ SRVB: '/sap/bc/adt/bo/servicebindings',
293
+ FUNC: '/sap/bc/adt/functions/groups',
294
+ };
295
+
296
+ function buildAdtUnlockUrl(name, type, lockHandle) {
297
+ const basePath = ADT_TYPE_PATHS[type?.toUpperCase()];
298
+ if (!basePath) return null;
299
+ return `${basePath}/${name.toUpperCase()}?_action=UNLOCK&lockHandle=${encodeURIComponent(lockHandle)}`;
300
+ }
301
+
272
302
  // ════════════════════════════════════════════════════════════════════════════
273
303
  // Lock Manager — IDENTICAL to websocket-client.js
274
304
  // ════════════════════════════════════════════════════════════════════════════
@@ -288,6 +318,7 @@ class LockManager {
288
318
  }
289
319
  clearLock(objectName, objectType) { this.locks.delete(`${objectName.toUpperCase()}:${objectType.toUpperCase()}`); }
290
320
  clearAll() { const c = this.locks.size; this.locks.clear(); if (c > 0) console.log(` 🧹 All locks cleared (${c} lock(s))`); }
321
+ getAll() { return Array.from(this.locks.entries()).map(([key, lock]) => { const [name, type] = key.split(':'); return { name, type, ...lock }; }); }
291
322
  getStats() { return { activeLocks: this.locks.size, locks: Array.from(this.locks.keys()) }; }
292
323
  }
293
324
 
@@ -658,6 +689,7 @@ class WebSocketClient {
658
689
  if (!sys) { this.sendResponse(requestId, false, { error: `Unknown system: ${sid}` }); return; }
659
690
  this.currentSystemId = sid;
660
691
  this.lockManager.clearAll();
692
+ sessions.delete(sid); // force re-create session with fresh config on every switch
661
693
  const ok = await this.initSapClient();
662
694
  if (!ok) { this.sendResponse(requestId, false, { error: `Failed to connect to ${sid}. Check RFC/SNC config and logs.` }); return; }
663
695
  this.sendResponse(requestId, true, [{ success: true, data: { system: sid, authMode: sys.authMode } }]);
@@ -726,6 +758,41 @@ class WebSocketClient {
726
758
  }
727
759
  for (let i = 0; i < callspecs.length; i++) {
728
760
  const cs = callspecs[i];
761
+
762
+ // ── Special client-side commands ────────────────────────────────────
763
+ if (cs.command) {
764
+ if (cs.command === 'unlock_all') {
765
+ console.log(`\n 📋 Step: unlock_all (client-side)`);
766
+ const allLocks = this.lockManager.getAll();
767
+ console.log(` 🔓 Unlocking ${allLocks.length} cached lock(s)...`);
768
+ let unlocked = 0, failed = 0;
769
+ for (const lock of allLocks) {
770
+ const unlockUrl = buildAdtUnlockUrl(lock.name, lock.type, lock.lockHandle);
771
+ if (!unlockUrl) { console.log(` ⚠️ No ADT path for type ${lock.type} — skipping`); failed++; continue; }
772
+ const r = await this.sapClient.executeCallspec({ method: 'DELETE', url: unlockUrl, headers: {} });
773
+ if (r.success) { this.lockManager.clearLock(lock.name, lock.type); unlocked++; console.log(` ✅ Unlocked ${lock.type}:${lock.name}`); }
774
+ else { failed++; console.log(` ⚠️ Failed to unlock ${lock.type}:${lock.name}: ${r.error || r.status}`); }
775
+ }
776
+ results.push({ success: true, status: 200, data: { unlocked, failed, total: allLocks.length } });
777
+ continue;
778
+ }
779
+ if (cs.command === 'force_unlock') {
780
+ const { objectName, objectType, lockHandle } = cs;
781
+ console.log(`\n 📋 Step: force_unlock ${objectType}:${objectName}`);
782
+ const handle = lockHandle || this.lockManager.getLock(objectName, objectType)?.lockHandle || '';
783
+ const unlockUrl = buildAdtUnlockUrl(objectName, objectType, handle);
784
+ if (!unlockUrl) { results.push({ success: false, status: 0, error: `Unknown object type: ${objectType}` }); allSuccessful = false; break; }
785
+ const r = await this.sapClient.executeCallspec({ method: 'DELETE', url: unlockUrl, headers: {} });
786
+ if (r.success) this.lockManager.clearLock(objectName, objectType);
787
+ results.push(r);
788
+ if (!r.success) { allSuccessful = false; break; }
789
+ continue;
790
+ }
791
+ console.log(`\n 📋 Step: unknown command "${cs.command}" — skipping`);
792
+ results.push({ success: true, status: 200, data: { skipped: true, command: cs.command } });
793
+ continue;
794
+ }
795
+
729
796
  console.log(`\n 📋 Step: ${cs.name || cs.step}`);
730
797
  const isLockOp = cs.name === 'LOCK' || cs.url?.includes('_action=LOCK');
731
798
  if (isLockOp && objectName && objectType) {
@@ -751,16 +818,34 @@ class WebSocketClient {
751
818
  console.log(` 🔓 Injected lockHandle into UNLOCK URL from cache: ${cachedLock.lockHandle.substring(0,10)}...`);
752
819
  }
753
820
  }
821
+ const isUnlockOp = pcs.url?.includes('_action=UNLOCK');
754
822
  const result = await this.sapClient.executeCallspec(pcs);
755
823
  results.push(result);
756
- if (result.success && result.data) {
757
- if (result.data.lockHandle) {
824
+ if (result.success) {
825
+ if (result.data?.lockHandle) {
758
826
  extractedValues.lockHandle = result.data.lockHandle;
759
827
  if (isLockOp && objectName && objectType) this.lockManager.saveLock(objectName, objectType, result.data.lockHandle, result.data.transportRequest || '');
760
828
  }
761
- if (result.data.transportRequest) extractedValues.transportRequest = result.data.transportRequest;
829
+ if (result.data?.transportRequest) extractedValues.transportRequest = result.data.transportRequest;
830
+ // After successful UNLOCK, clear the cached handle — it is now invalid on SAP
831
+ if (isUnlockOp && objectName && objectType) {
832
+ this.lockManager.clearLock(objectName, objectType);
833
+ console.log(` 🗑️ Lock handle cleared from cache after successful UNLOCK of ${objectType}:${objectName}`);
834
+ }
835
+ }
836
+ if (!result.success) {
837
+ // 423 with an invalid lock handle means the cached handle is stale — clear it so the
838
+ // next invocation does a fresh LOCK instead of reusing the dead handle
839
+ if (result.status === 423 && objectName && objectType) {
840
+ const stale = this.lockManager.getLock(objectName, objectType);
841
+ if (stale) {
842
+ this.lockManager.clearLock(objectName, objectType);
843
+ console.log(` 🗑️ Stale lock handle cleared from cache after 423 for ${objectType}:${objectName}`);
844
+ }
845
+ }
846
+ allSuccessful = false;
847
+ break;
762
848
  }
763
- if (!result.success) { allSuccessful = false; break; }
764
849
  }
765
850
  return { results, allSuccessful };
766
851
  } else {