abap-local-client 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/basic-auth.mjs +9 -9
- package/package.json +1 -1
- package/sap-rfc-client.js +40 -8
- package/sso-sap-client.mjs +825 -727
package/sso-sap-client.mjs
CHANGED
|
@@ -1,727 +1,825 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
console.log(
|
|
600
|
-
|
|
601
|
-
ws
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
console.log(
|
|
693
|
-
console.log(
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SSO SAP Client — websocket-client.js with multi-auth
|
|
3
|
+
*
|
|
4
|
+
* Uses the proven websocket-client.js SAPClient, LockManager, executeCallspecs
|
|
5
|
+
* (6 months production), with multi-system auth injected via config-manager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import WebSocket from 'ws';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import https from 'https';
|
|
11
|
+
import { parseSAPResponse } from './xml-utils.js';
|
|
12
|
+
import { BasicSession } from './basic-auth.mjs';
|
|
13
|
+
import { SAPRFCClient } from './sap-rfc-client.js';
|
|
14
|
+
import * as config from './config-manager.mjs';
|
|
15
|
+
|
|
16
|
+
try { process.loadEnvFile?.(); } catch {}
|
|
17
|
+
const env = process.env;
|
|
18
|
+
|
|
19
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// Multi-system auth session cache
|
|
21
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
const sessions = new Map();
|
|
24
|
+
const pendingSessions = new Map();
|
|
25
|
+
let defaultSystemId = null;
|
|
26
|
+
|
|
27
|
+
function getDefaultSystemId() {
|
|
28
|
+
if (defaultSystemId) return defaultSystemId;
|
|
29
|
+
const allSystems = config.listSystems();
|
|
30
|
+
return defaultSystemId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getSessionForSystem(systemId) {
|
|
34
|
+
if (!systemId) throw new Error('No system selected. Use switch_system first.');
|
|
35
|
+
const cacheKey = systemId;
|
|
36
|
+
if (sessions.has(cacheKey)) return sessions.get(cacheKey);
|
|
37
|
+
if (pendingSessions.has(cacheKey)) return await pendingSessions.get(cacheKey);
|
|
38
|
+
|
|
39
|
+
const initPromise = _createSession(systemId);
|
|
40
|
+
pendingSessions.set(cacheKey, initPromise);
|
|
41
|
+
try {
|
|
42
|
+
const entry = await initPromise;
|
|
43
|
+
sessions.set(cacheKey, entry);
|
|
44
|
+
return entry;
|
|
45
|
+
} finally {
|
|
46
|
+
pendingSessions.delete(cacheKey);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function _createSession(resolvedId) {
|
|
51
|
+
const sysConfig = config.getSystem(resolvedId);
|
|
52
|
+
if (!sysConfig) throw new Error(`Unknown system: ${resolvedId}`);
|
|
53
|
+
const conn = sysConfig.connection || {};
|
|
54
|
+
const authMode = sysConfig.authMode || 'basic';
|
|
55
|
+
const hasSaprouter = !!(conn.saprouter);
|
|
56
|
+
|
|
57
|
+
console.log(`\n🔐 Creating auth session for "${resolvedId}" (${authMode})`);
|
|
58
|
+
|
|
59
|
+
const baseUrl = `https://${conn.host}:${conn.port || '44301'}`;
|
|
60
|
+
|
|
61
|
+
let httpSession = null;
|
|
62
|
+
let rfcClient = null;
|
|
63
|
+
let useRfc = (authMode === 'snc' || hasSaprouter);
|
|
64
|
+
|
|
65
|
+
if (useRfc) {
|
|
66
|
+
console.log(` 🛰️ RFC mode (no HTTP)`);
|
|
67
|
+
rfcClient = new SAPRFCClient({
|
|
68
|
+
host: conn.host, sysnr: conn.sysnr || '00',
|
|
69
|
+
client: conn.client || '100', username: conn.username || '',
|
|
70
|
+
password: conn.password || '', language: conn.language || 'EN',
|
|
71
|
+
saprouter: conn.saprouter || '',
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
await rfcClient.connect();
|
|
75
|
+
console.log(` ✅ RFC connection established`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(` ⚠️ RFC connection failed: ${err.message}`);
|
|
78
|
+
rfcClient = null;
|
|
79
|
+
useRfc = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!useRfc && authMode === 'basic') {
|
|
84
|
+
httpSession = new BasicSession(axios, baseUrl, conn.client || '100',
|
|
85
|
+
conn.username || '', conn.password || '',
|
|
86
|
+
{ language: conn.language || 'EN' });
|
|
87
|
+
await httpSession.init();
|
|
88
|
+
} else if (!useRfc && authMode === 'snc') {
|
|
89
|
+
throw new Error(`Auth mode not yet implemented: ${authMode}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { useRfc, rfcClient, httpSession, authMode, sysConfig, resolvedId };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
96
|
+
// SAP Systems Registry — Arcanum
|
|
97
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
98
|
+
|
|
99
|
+
const ARCANUM_ADMIN_URL = 'https://arcanum.mcps.b2rise.com';
|
|
100
|
+
|
|
101
|
+
const sapSystems = new Map();
|
|
102
|
+
|
|
103
|
+
async function fetchSapSystems() {
|
|
104
|
+
try {
|
|
105
|
+
const response = await axios.post(
|
|
106
|
+
`${ARCANUM_ADMIN_URL}/api/sap-environments/by-api-key`,
|
|
107
|
+
{ key: MCP_API_KEY }, { timeout: 15_000 }
|
|
108
|
+
);
|
|
109
|
+
if (!response.data.valid) return;
|
|
110
|
+
for (const [key, cfg] of Object.entries(response.data.systems || {})) {
|
|
111
|
+
sapSystems.set(key, cfg);
|
|
112
|
+
}
|
|
113
|
+
console.log(`🌐 SAP systems loaded from Arcanum: ${sapSystems.size} system(s)`);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn(`⚠️ Could not fetch SAP systems: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
120
|
+
// HTTP Session Manager — cookie jar + CSRF cache (for http_call)
|
|
121
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
122
|
+
|
|
123
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
124
|
+
const SESSION_CLEANUP_MS = 60 * 1000;
|
|
125
|
+
|
|
126
|
+
class SessionManager {
|
|
127
|
+
constructor() {
|
|
128
|
+
this.sessions = new Map();
|
|
129
|
+
this._cleanup = setInterval(() => this._evict(), SESSION_CLEANUP_MS);
|
|
130
|
+
this._cleanup.unref?.();
|
|
131
|
+
}
|
|
132
|
+
_key(sid, sys) { return `${sid}__${sys}`; }
|
|
133
|
+
getOrCreate(sid, sys) {
|
|
134
|
+
const key = this._key(sid, sys);
|
|
135
|
+
if (!this.sessions.has(key))
|
|
136
|
+
this.sessions.set(key, { cookieJar: new Map(), csrfToken: null, system: sys, lastUsedAt: Date.now() });
|
|
137
|
+
const s = this.sessions.get(key);
|
|
138
|
+
s.lastUsedAt = Date.now();
|
|
139
|
+
return s;
|
|
140
|
+
}
|
|
141
|
+
_evict() {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
for (const [k, s] of this.sessions)
|
|
144
|
+
if (now - s.lastUsedAt > SESSION_TTL_MS) this.sessions.delete(k);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const sessionManager = new SessionManager();
|
|
148
|
+
|
|
149
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
150
|
+
// HTTP Call Handler (http_call action)
|
|
151
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
152
|
+
|
|
153
|
+
function mapNetError(err, timeoutMs) {
|
|
154
|
+
const code = err.code || '';
|
|
155
|
+
if (code === 'ECONNABORTED' || err.message?.includes('timeout')) return `timeout after ${timeoutMs}ms`;
|
|
156
|
+
if (code === 'ENOTFOUND') return `DNS resolution failed`;
|
|
157
|
+
if (code === 'ECONNREFUSED') return 'connection refused';
|
|
158
|
+
if (err.message?.toLowerCase().includes('ssl') || err.message?.toLowerCase().includes('certificate'))
|
|
159
|
+
return `SSL handshake failed`;
|
|
160
|
+
return err.message || 'unknown network error';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function makeHttpEnvelope(partial) {
|
|
164
|
+
return { status: 0, status_text: '', headers: {}, body: null, body_raw: '',
|
|
165
|
+
duration_ms: 0, session_id: 'default', csrf_token_cached: false,
|
|
166
|
+
cookies_count: 0, request_url_resolved: '', error: null, ...partial };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function wrapHttpResult(env) {
|
|
170
|
+
return { success: env.error == null || env.status > 0, data: { success: env.error == null || env.status > 0, data: env } };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseResponseBody(res) {
|
|
174
|
+
const ct = (res.headers?.['content-type'] || '').toLowerCase();
|
|
175
|
+
const raw = typeof res.data === 'string' ? res.data : (res.data != null ? JSON.stringify(res.data) : '');
|
|
176
|
+
let parsed = null;
|
|
177
|
+
if (ct.includes('json')) {
|
|
178
|
+
try { parsed = typeof res.data === 'object' ? res.data : JSON.parse(raw); } catch {}
|
|
179
|
+
}
|
|
180
|
+
return { body: parsed, body_raw: raw };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function fetchCsrfForSession(session, sysCfg, httpsAgent) {
|
|
184
|
+
try {
|
|
185
|
+
const res = await axios.get(`${sysCfg.baseUrl}/sap/bc/adt/discovery`, {
|
|
186
|
+
headers: { 'X-CSRF-Token': 'Fetch', 'Accept': 'application/atomsvc+xml', 'sap-client': sysCfg.client,
|
|
187
|
+
'Cookie': [...session.cookieJar.values()].join('; ') },
|
|
188
|
+
auth: { username: sysCfg.username, password: sysCfg.password },
|
|
189
|
+
httpsAgent, timeout: 15_000, validateStatus: () => true
|
|
190
|
+
});
|
|
191
|
+
const token = res.headers['x-csrf-token'];
|
|
192
|
+
if (token && token.toLowerCase() !== 'required') session.csrfToken = token;
|
|
193
|
+
const setCookies = res.headers['set-cookie'];
|
|
194
|
+
if (Array.isArray(setCookies))
|
|
195
|
+
for (const c of setCookies) { const name = c.split('=')[0].trim(); session.cookieJar.set(name, c.split(';')[0].trim()); }
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function doAxiosCall(callspec, sysCfg, session, httpsAgent) {
|
|
200
|
+
const isAbsolute = callspec.path.startsWith('http');
|
|
201
|
+
const url = isAbsolute ? callspec.path : `${sysCfg.baseUrl}${callspec.path}`;
|
|
202
|
+
const reqHeaders = { ...(callspec.headers || {}) };
|
|
203
|
+
delete reqHeaders['Authorization']; delete reqHeaders['authorization'];
|
|
204
|
+
reqHeaders['sap-client'] = sysCfg.client;
|
|
205
|
+
reqHeaders['sap-language'] = sysCfg.language || 'EN';
|
|
206
|
+
if (session.csrfToken) reqHeaders['X-CSRF-Token'] = session.csrfToken;
|
|
207
|
+
if (session.cookieJar.size > 0) reqHeaders['Cookie'] = [...session.cookieJar.values()].join('; ');
|
|
208
|
+
let bodyData = callspec.body ?? null;
|
|
209
|
+
if (bodyData !== null && typeof bodyData === 'object') {
|
|
210
|
+
if (!reqHeaders['Content-Type'] && !reqHeaders['content-type']) reqHeaders['Content-Type'] = 'application/json';
|
|
211
|
+
bodyData = JSON.stringify(bodyData);
|
|
212
|
+
}
|
|
213
|
+
const res = await axios({
|
|
214
|
+
method: callspec.method.toLowerCase(), url, headers: reqHeaders, data: bodyData,
|
|
215
|
+
auth: { username: sysCfg.username, password: sysCfg.password },
|
|
216
|
+
httpsAgent, timeout: callspec.timeout_ms ?? 30_000,
|
|
217
|
+
maxRedirects: callspec.follow_redirects === false ? 0 : 5,
|
|
218
|
+
validateStatus: () => true
|
|
219
|
+
});
|
|
220
|
+
const setCookies = res.headers['set-cookie'];
|
|
221
|
+
if (Array.isArray(setCookies))
|
|
222
|
+
for (const c of setCookies) { const name = c.split('=')[0].trim(); session.cookieJar.set(name, c.split(';')[0].trim()); }
|
|
223
|
+
if (res.headers['x-csrf-token']) session.csrfToken = res.headers['x-csrf-token'];
|
|
224
|
+
return res;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function handleHttpCall(callspec, httpsAgent) {
|
|
228
|
+
const sessionId = callspec.session_id || 'default';
|
|
229
|
+
const systemKey = callspec.system || '';
|
|
230
|
+
const start = Date.now();
|
|
231
|
+
const sysCfg = sapSystems.get(systemKey);
|
|
232
|
+
if (!sysCfg) return wrapHttpResult(makeHttpEnvelope({ status: 0, error: `Unknown system: ${systemKey}`, duration_ms: 0, session_id: sessionId }));
|
|
233
|
+
if (callspec.path.startsWith('http')) {
|
|
234
|
+
try { if (new URL(callspec.path).host !== new URL(sysCfg.baseUrl).host) return wrapHttpResult(makeHttpEnvelope({ status: 0, error: 'forbidden_url', session_id: sessionId, duration_ms: 0 })); }
|
|
235
|
+
catch { return wrapHttpResult(makeHttpEnvelope({ status: 0, error: 'invalid_path', session_id: sessionId, duration_ms: 0 })); }
|
|
236
|
+
} else if (!callspec.path.startsWith('/'))
|
|
237
|
+
return wrapHttpResult(makeHttpEnvelope({ status: 0, error: 'invalid_path', session_id: sessionId, duration_ms: 0 }));
|
|
238
|
+
const session = sessionManager.getOrCreate(sessionId, systemKey);
|
|
239
|
+
const stateChanging = !['GET', 'HEAD', 'OPTIONS'].includes(callspec.method?.toUpperCase());
|
|
240
|
+
if (callspec.auto_csrf !== false && stateChanging && !session.csrfToken) await fetchCsrfForSession(session, sysCfg, httpsAgent);
|
|
241
|
+
let res;
|
|
242
|
+
try { res = await doAxiosCall(callspec, sysCfg, session, httpsAgent); }
|
|
243
|
+
catch (err) { return wrapHttpResult(makeHttpEnvelope({ status: 0, error: mapNetError(err, callspec.timeout_ms ?? 30_000), duration_ms: Date.now() - start, session_id: sessionId })); }
|
|
244
|
+
const csrfHeader = (res.headers['x-csrf-token'] || '').toLowerCase();
|
|
245
|
+
if (res.status === 403 && csrfHeader === 'required' && callspec.auto_csrf !== false) {
|
|
246
|
+
session.csrfToken = null; await fetchCsrfForSession(session, sysCfg, httpsAgent);
|
|
247
|
+
if (!session.csrfToken) return wrapHttpResult(makeHttpEnvelope({ status: 403, error: 'csrf_failed', duration_ms: Date.now() - start, session_id: sessionId }));
|
|
248
|
+
try { res = await doAxiosCall(callspec, sysCfg, session, httpsAgent); }
|
|
249
|
+
catch (err) { return wrapHttpResult(makeHttpEnvelope({ status: 0, error: mapNetError(err, callspec.timeout_ms ?? 30_000), duration_ms: Date.now() - start, session_id: sessionId })); }
|
|
250
|
+
}
|
|
251
|
+
const { body, body_raw } = parseResponseBody(res);
|
|
252
|
+
return wrapHttpResult(makeHttpEnvelope({ status: res.status, status_text: res.statusText || '', headers: res.headers || {},
|
|
253
|
+
body, body_raw, duration_ms: Date.now() - start, session_id: sessionId,
|
|
254
|
+
csrf_token_cached: !!session.csrfToken, cookies_count: session.cookieJar.size,
|
|
255
|
+
request_url_resolved: callspec.path.startsWith('http') ? callspec.path : `${sysCfg.baseUrl}${callspec.path.split('?')[0]}`, error: null }));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
259
|
+
// Lock Manager — IDENTICAL to websocket-client.js
|
|
260
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
261
|
+
|
|
262
|
+
class LockManager {
|
|
263
|
+
constructor() { this.locks = new Map(); this.lockTTL = 30 * 60 * 1000; }
|
|
264
|
+
getLock(objectName, objectType) {
|
|
265
|
+
const key = `${objectName.toUpperCase()}:${objectType.toUpperCase()}`;
|
|
266
|
+
const lock = this.locks.get(key);
|
|
267
|
+
if (!lock) return null;
|
|
268
|
+
if (Date.now() - lock.timestamp >= this.lockTTL) { console.log(` ⏰ Lock expired for ${key}`); this.locks.delete(key); return null; }
|
|
269
|
+
console.log(` 🔓 Found cached lock for ${key}`);
|
|
270
|
+
return lock;
|
|
271
|
+
}
|
|
272
|
+
saveLock(objectName, objectType, lockHandle, transportRequest) {
|
|
273
|
+
this.locks.set(`${objectName.toUpperCase()}:${objectType.toUpperCase()}`, { lockHandle, transportRequest, timestamp: Date.now() });
|
|
274
|
+
}
|
|
275
|
+
clearLock(objectName, objectType) { this.locks.delete(`${objectName.toUpperCase()}:${objectType.toUpperCase()}`); }
|
|
276
|
+
clearAll() { const c = this.locks.size; this.locks.clear(); if (c > 0) console.log(` 🧹 All locks cleared (${c} lock(s))`); }
|
|
277
|
+
getStats() { return { activeLocks: this.locks.size, locks: Array.from(this.locks.keys()) }; }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
281
|
+
// SAP HTTP Client — IDENTICAL to websocket-client.js, but config from session
|
|
282
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
283
|
+
|
|
284
|
+
class SAPClient {
|
|
285
|
+
constructor(sessionEntry) {
|
|
286
|
+
this.session = sessionEntry;
|
|
287
|
+
this.csrfToken = null;
|
|
288
|
+
this.cookies = new Map();
|
|
289
|
+
|
|
290
|
+
const conn = sessionEntry.sysConfig?.connection || {};
|
|
291
|
+
const baseUrl = `https://${conn.host}:${conn.port || '44301'}`;
|
|
292
|
+
|
|
293
|
+
if (sessionEntry.useRfc && sessionEntry.rfcClient) {
|
|
294
|
+
this.rfcClient = sessionEntry.rfcClient;
|
|
295
|
+
console.log('🔀 SAPClient running in RFC mode (SAProuter)');
|
|
296
|
+
} else {
|
|
297
|
+
this.rfcClient = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Keep cookies from HTTP session if available
|
|
301
|
+
const hs = sessionEntry.httpSession;
|
|
302
|
+
if (sessionEntry.httpSession) {
|
|
303
|
+
if (hs.csrfToken) {
|
|
304
|
+
this.csrfToken = hs.csrfToken;
|
|
305
|
+
console.log(` 🔑 CSRF token seeded from HTTP session: ${this.csrfToken.substring(0, 15)}...`);
|
|
306
|
+
}
|
|
307
|
+
if (hs.cookies?.size > 0) {
|
|
308
|
+
// BasicSession stores cookies as Map<name, value> (just the value, no "name=" prefix)
|
|
309
|
+
for (const [name, value] of hs.cookies) {
|
|
310
|
+
// Normalise to "name=value" format (same as what the interceptor stores)
|
|
311
|
+
this.cookies.set(name, value.includes('=') ? value : `${name}=${value}`);
|
|
312
|
+
}
|
|
313
|
+
console.log(` 🍪 ${this.cookies.size} cookie(s) seeded from BasicSession`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.client = axios.create({
|
|
318
|
+
baseURL: baseUrl,
|
|
319
|
+
headers: {
|
|
320
|
+
'sap-client': conn.client || '100',
|
|
321
|
+
'sap-language': conn.language || 'EN',
|
|
322
|
+
'Accept': 'application/xml,text/plain,*/*',
|
|
323
|
+
'X-sap-adt-sessiontype': 'stateful'
|
|
324
|
+
},
|
|
325
|
+
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
|
326
|
+
auth: { username: conn.username || '', password: conn.password || '' }
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
this.client.interceptors.response.use(
|
|
330
|
+
response => {
|
|
331
|
+
if (response.headers['set-cookie']) {
|
|
332
|
+
response.headers['set-cookie'].forEach(cookieString => {
|
|
333
|
+
const cv = cookieString.split(';')[0];
|
|
334
|
+
this.cookies.set(cv.split('=')[0], cv);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (response.headers['x-csrf-token']) {
|
|
338
|
+
this.csrfToken = response.headers['x-csrf-token'];
|
|
339
|
+
console.log(` 🔑 CSRF token obtained: ${this.csrfToken.substring(0, 15)}...`);
|
|
340
|
+
}
|
|
341
|
+
return response;
|
|
342
|
+
},
|
|
343
|
+
error => {
|
|
344
|
+
if (error.response?.status === 401 || error.response?.status === 403) {
|
|
345
|
+
console.error(` ⚠️ Session error (${error.response.status}) - clearing...`);
|
|
346
|
+
this.clearSession();
|
|
347
|
+
}
|
|
348
|
+
return Promise.reject(error);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
this.client.interceptors.request.use(request => {
|
|
353
|
+
if (this.cookies.size > 0)
|
|
354
|
+
request.headers['Cookie'] = Array.from(this.cookies.values()).join('; ');
|
|
355
|
+
return request;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async executeCallspec(callspec) {
|
|
360
|
+
if (this.rfcClient) return this._executeCallspecViaRFC(callspec);
|
|
361
|
+
return this._executeCallspecViaHTTP(callspec);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async _executeCallspecViaRFC(callspec) {
|
|
365
|
+
const { method, url, headers, extract_from_response } = callspec;
|
|
366
|
+
const conn = this.session.sysConfig?.connection || {};
|
|
367
|
+
let finalHeaders = { ...headers };
|
|
368
|
+
finalHeaders['sap-client'] = finalHeaders['sap-client'] || conn.client || '100';
|
|
369
|
+
finalHeaders['sap-language'] = finalHeaders['sap-language'] || conn.language || 'EN';
|
|
370
|
+
if (finalHeaders['X-CSRF-Token'] === '<CSRF_TOKEN>') {
|
|
371
|
+
if (!this.rfcClient.csrfToken) {
|
|
372
|
+
this.rfcClient.csrfToken = this.csrfToken;
|
|
373
|
+
}
|
|
374
|
+
if (!this.rfcClient.csrfToken) {
|
|
375
|
+
await this.rfcClient.fetchCsrfToken();
|
|
376
|
+
}
|
|
377
|
+
if (this.rfcClient.csrfToken) {
|
|
378
|
+
finalHeaders['X-CSRF-Token'] = this.rfcClient.csrfToken;
|
|
379
|
+
console.log(` 🔑 RFC CSRF token: "${finalHeaders['X-CSRF-Token'].substring(0,20)}..."`);
|
|
380
|
+
} else {
|
|
381
|
+
delete finalHeaders['X-CSRF-Token'];
|
|
382
|
+
console.log(` ⚠️ No CSRF token available — removing X-CSRF-Token header (RFC uses Basic Auth)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
delete finalHeaders['Authorization'];
|
|
386
|
+
if (finalHeaders['Cookie'] === '<SESSION_COOKIE>') delete finalHeaders['Cookie'];
|
|
387
|
+
const creds = Buffer.from(`${conn.username || ''}:${conn.password || ''}`).toString('base64');
|
|
388
|
+
finalHeaders['Authorization'] = `Basic ${creds}`;
|
|
389
|
+
|
|
390
|
+
// ── Substitute body placeholders ────────────────────────────────────────
|
|
391
|
+
// abap-mcp-local sends placeholders like <SAP_LANGUAGE>, <SAP_CLIENT> in
|
|
392
|
+
// the XML body. Replace them with real values from the connection config.
|
|
393
|
+
let body = callspec.body ?? null;
|
|
394
|
+
if (typeof body === 'string') {
|
|
395
|
+
const lang = conn.language || 'EN';
|
|
396
|
+
const client = conn.client || '100';
|
|
397
|
+
const user = (conn.username || '').toUpperCase();
|
|
398
|
+
body = body
|
|
399
|
+
.replace(/<SAP_LANGUAGE>/g, lang)
|
|
400
|
+
.replace(/<SAP_CLIENT>/g, client)
|
|
401
|
+
.replace(/<SAP_USER>/g, user)
|
|
402
|
+
.replace(/<SAP_USERNAME>/g, user)
|
|
403
|
+
.replace(/<SAP_RESPONSIBLE>/g, user)
|
|
404
|
+
.replace(/<SAP_MASTER_LANG>/g, lang)
|
|
405
|
+
.replace(/<SAP_MASTER_SYSTEM>/g, conn.systemId || '');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let uriPath = url;
|
|
409
|
+
try { const p = new URL(url); uriPath = p.pathname + (p.search || ''); } catch {}
|
|
410
|
+
|
|
411
|
+
console.log(`\n ╔══════════════════════════════════════════════════════════════`);
|
|
412
|
+
console.log(` ║ 📡 RFC ${method} ${uriPath}`);
|
|
413
|
+
console.log(` ╠══════════════════════════════════════════════════════════════`);
|
|
414
|
+
console.log(` ║ HEADERS:`);
|
|
415
|
+
for (const [k, v] of Object.entries(finalHeaders)) {
|
|
416
|
+
if (k === 'Authorization') console.log(` ║ ${k}: Basic ***`);
|
|
417
|
+
else console.log(` ║ ${k}: ${v}`);
|
|
418
|
+
}
|
|
419
|
+
if (body) {
|
|
420
|
+
console.log(` ╠══════════════════════════════════════════════════════════════`);
|
|
421
|
+
console.log(` ║ BODY (${body.length} chars):`);
|
|
422
|
+
body.split('\n').forEach(line => console.log(` ║ ${line}`));
|
|
423
|
+
}
|
|
424
|
+
console.log(` ╚══════════════════════════════════════════════════════════════\n`);
|
|
425
|
+
try {
|
|
426
|
+
const response = await this.rfcClient.callADT(method, uriPath, finalHeaders, body || null);
|
|
427
|
+
console.log(` ✅ RFC response: ${response.status} ${response.statusText}`);
|
|
428
|
+
if (response.data && response.status >= 400) {
|
|
429
|
+
console.log(` 📄 RFC response body: ${typeof response.data === 'string' ? response.data.substring(0, 500) : JSON.stringify(response.data).substring(0, 500)}`);
|
|
430
|
+
}
|
|
431
|
+
const extractedData = {};
|
|
432
|
+
if (extract_from_response && typeof response.data === 'string') {
|
|
433
|
+
if (extract_from_response.lock_handle) { const m = response.data.match(/<(?:adtcore:)?(?:lock[Hh]andle|LOCK_HANDLE)>(.*?)<\/(?:adtcore:)?(?:lock[Hh]andle|LOCK_HANDLE)>/); if (m) extractedData.lockHandle = m[1]; }
|
|
434
|
+
if (extract_from_response.transport_request) { const m = response.data.match(/<(?:adtcore:)?(?:corrNr|CORRNR)>(.*?)<\/(?:adtcore:)?(?:corrNr|CORRNR)>/); if (m) extractedData.transportRequest = m[1]; }
|
|
435
|
+
if (extract_from_response.function_group) { const m = response.data.match(/PNAME[^>]*>([^<]*)</i); if (m) extractedData.functionGroup = m[1].replace(/^SAPL/, ''); }
|
|
436
|
+
}
|
|
437
|
+
const { hasErrors, errorMessages } = parseSAPResponse(response.data);
|
|
438
|
+
return { success: !hasErrors && response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText, headers: response.headers, data: { raw: response.data, ...extractedData }, ...(errorMessages.length > 0 && { error: `SAP validation failed: ${errorMessages.join('; ')}`, errorDetails: errorMessages }) };
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(` ❌ RFC error: ${err.message}`);
|
|
441
|
+
return { success: false, status: 0, error: err.message, data: null };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async _executeCallspecViaHTTP(callspec) {
|
|
446
|
+
const { method, url, headers, extract_from_response } = callspec;
|
|
447
|
+
let finalUrl = url;
|
|
448
|
+
let finalHeaders = { ...headers };
|
|
449
|
+
|
|
450
|
+
// ── CSRF injection ──────────────────────────────────────────────────────
|
|
451
|
+
// Inject on: (a) explicit <CSRF_TOKEN> placeholder, OR
|
|
452
|
+
// (b) any state-changing request (POST/PUT/PATCH/DELETE) so the
|
|
453
|
+
// client works even when abap-mcp-local omits the placeholder.
|
|
454
|
+
const isStateChanging = !['GET', 'HEAD', 'OPTIONS'].includes((method || '').toUpperCase());
|
|
455
|
+
const hasPlaceholder = finalHeaders['X-CSRF-Token'] === '<CSRF_TOKEN>';
|
|
456
|
+
if (hasPlaceholder || isStateChanging) {
|
|
457
|
+
if (!this.csrfToken) await this.fetchCsrfToken();
|
|
458
|
+
finalHeaders['X-CSRF-Token'] = this.csrfToken;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
delete finalHeaders['Authorization'];
|
|
462
|
+
if (finalHeaders['Cookie'] === '<SESSION_COOKIE>') delete finalHeaders['Cookie'];
|
|
463
|
+
|
|
464
|
+
// ── Substitute body placeholders (same as RFC path) ────────────────────
|
|
465
|
+
let rawBody = callspec.body ?? undefined;
|
|
466
|
+
if (typeof rawBody === 'string') {
|
|
467
|
+
const conn2 = this.session?.sysConfig?.connection || {};
|
|
468
|
+
const lang2 = conn2.language || 'EN';
|
|
469
|
+
const user2 = (conn2.username || '').toUpperCase();
|
|
470
|
+
rawBody = rawBody
|
|
471
|
+
.replace(/<SAP_LANGUAGE>/g, lang2)
|
|
472
|
+
.replace(/<SAP_CLIENT>/g, conn2.client || '100')
|
|
473
|
+
.replace(/<SAP_USER>/g, user2)
|
|
474
|
+
.replace(/<SAP_USERNAME>/g, user2)
|
|
475
|
+
.replace(/<SAP_RESPONSIBLE>/g, user2)
|
|
476
|
+
.replace(/<SAP_MASTER_LANG>/g, lang2)
|
|
477
|
+
.replace(/<SAP_MASTER_SYSTEM>/g, conn2.systemId || '');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── body: use ?? so empty-string bodies (LOCK, ACTIVATE) are preserved ──
|
|
481
|
+
const bodyData = rawBody;
|
|
482
|
+
|
|
483
|
+
// ── Full request dump ───────────────────────────────────────────────────
|
|
484
|
+
console.log(`\n ╔══════════════════════════════════════════════════════════════`);
|
|
485
|
+
console.log(` ║ 📤 REQUEST: ${method} ${finalUrl}`);
|
|
486
|
+
console.log(` ╠══════════════════════════════════════════════════════════════`);
|
|
487
|
+
console.log(` ║ HEADERS SENT:`);
|
|
488
|
+
// Merge: instance defaults + per-request headers (axios will do the same)
|
|
489
|
+
const instanceDefaults = this.client.defaults?.headers || {};
|
|
490
|
+
const merged = {
|
|
491
|
+
...(instanceDefaults.common || {}),
|
|
492
|
+
...((instanceDefaults[(method || '').toLowerCase()]) || {}),
|
|
493
|
+
...finalHeaders,
|
|
494
|
+
};
|
|
495
|
+
for (const [k, v] of Object.entries(merged)) {
|
|
496
|
+
if (k === 'Authorization') console.log(` ║ ${k}: Basic ***`);
|
|
497
|
+
else console.log(` ║ ${k}: ${v}`);
|
|
498
|
+
}
|
|
499
|
+
console.log(` ╠══════════════════════════════════════════════════════════════`);
|
|
500
|
+
console.log(` ║ CSRF token in session: ${this.csrfToken ? this.csrfToken.substring(0, 20) + '...' : 'NONE ⚠️'}`);
|
|
501
|
+
console.log(` ║ Cookies in jar: ${this.cookies.size}`);
|
|
502
|
+
if (this.cookies.size > 0) {
|
|
503
|
+
for (const [k] of this.cookies) console.log(` ║ cookie: ${k}`);
|
|
504
|
+
}
|
|
505
|
+
console.log(` ╠══════════════════════════════════════════════════════════════`);
|
|
506
|
+
if (bodyData !== undefined && bodyData !== null) {
|
|
507
|
+
const bodyStr = typeof bodyData === 'string' ? bodyData : JSON.stringify(bodyData);
|
|
508
|
+
console.log(` ║ BODY (${bodyStr.length} chars, type=${typeof bodyData}):`);
|
|
509
|
+
bodyStr.split('\n').slice(0, 40).forEach(line => console.log(` ║ ${line}`));
|
|
510
|
+
if (bodyStr.split('\n').length > 40) console.log(` ║ ... (truncated)`);
|
|
511
|
+
} else {
|
|
512
|
+
console.log(` ║ BODY: (none)`);
|
|
513
|
+
}
|
|
514
|
+
console.log(` ╚══════════════════════════════════════════════════════════════\n`);
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
const response = await this.client.request({ method, url: finalUrl, headers: finalHeaders, data: bodyData, timeout: 60000 });
|
|
518
|
+
console.log(` ✅ ${response.status} ${response.statusText}`);
|
|
519
|
+
|
|
520
|
+
// Log response headers + body
|
|
521
|
+
console.log(` ║ RESPONSE HEADERS:`);
|
|
522
|
+
for (const [k, v] of Object.entries(response.headers || {})) console.log(` ║ ${k}: ${v}`);
|
|
523
|
+
const resBodyStr = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
|
|
524
|
+
console.log(` ║ RESPONSE BODY (${resBodyStr?.length || 0} chars): ${resBodyStr?.substring(0, 800)}`);
|
|
525
|
+
|
|
526
|
+
const extractedData = {};
|
|
527
|
+
if (extract_from_response && typeof response.data === 'string') {
|
|
528
|
+
if (extract_from_response.lock_handle) { const m = response.data.match(/<(?:adtcore:)?(?:lock[Hh]andle|LOCK_HANDLE)>(.*?)<\/(?:adtcore:)?(?:lock[Hh]andle|LOCK_HANDLE)>/); if (m) extractedData.lockHandle = m[1]; }
|
|
529
|
+
if (extract_from_response.transport_request) { const m = response.data.match(/<(?:adtcore:)?(?:corrNr|CORRNR)>(.*?)<\/(?:adtcore:)?(?:corrNr|CORRNR)>/); if (m) extractedData.transportRequest = m[1]; }
|
|
530
|
+
}
|
|
531
|
+
const { hasErrors, errorMessages } = parseSAPResponse(response.data);
|
|
532
|
+
if (hasErrors) { console.log(` ⚠️ Found ${errorMessages.length} error(s)`); errorMessages.forEach((m, i) => console.log(` ❌ Error ${i + 1}: ${m}`)); }
|
|
533
|
+
return { success: !hasErrors && response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText, headers: response.headers, data: { raw: response.data, ...extractedData }, ...(errorMessages.length > 0 && { error: `SAP validation failed: ${errorMessages.join('; ')}`, errorDetails: errorMessages }) };
|
|
534
|
+
} catch (error) {
|
|
535
|
+
const status = error.response?.status;
|
|
536
|
+
console.error(` ❌ HTTP ${status || 'network error'}: ${error.message}`);
|
|
537
|
+
if (error.response?.headers) {
|
|
538
|
+
console.error(` ❌ RESPONSE HEADERS:`);
|
|
539
|
+
for (const [k, v] of Object.entries(error.response.headers)) console.error(` ❌ ${k}: ${v}`);
|
|
540
|
+
}
|
|
541
|
+
const resBody = typeof error.response?.data === 'string'
|
|
542
|
+
? error.response.data
|
|
543
|
+
: JSON.stringify(error.response?.data);
|
|
544
|
+
if (resBody) console.error(` ❌ RESPONSE BODY:\n${resBody}`);
|
|
545
|
+
return { success: false, status, error: error.message, data: error.response?.data };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async fetchCsrfToken() {
|
|
550
|
+
console.log(' 🔑 Fetching CSRF token...');
|
|
551
|
+
try { await this.client.get('/sap/bc/adt/compatibility/graph', { headers: { 'X-CSRF-Token': 'Fetch' } }); }
|
|
552
|
+
catch (error) { if (error.response?.headers['x-csrf-token']) this.csrfToken = error.response.headers['x-csrf-token']; }
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
clearSession() { this.csrfToken = null; this.cookies.clear(); console.log(' 🧹 Session cleared'); }
|
|
556
|
+
|
|
557
|
+
async ping() {
|
|
558
|
+
try { const r = await this.client.get('/sap/bc/adt/compatibility/graph', { timeout: 5000 }); return { success: true, status: r.status }; }
|
|
559
|
+
catch (e) { return { success: false, error: e.message }; }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
564
|
+
// WebSocket Client — IDENTICAL to websocket-client.js
|
|
565
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
566
|
+
|
|
567
|
+
class WebSocketClient {
|
|
568
|
+
constructor() {
|
|
569
|
+
this.ws = null;
|
|
570
|
+
this.sapClient = null;
|
|
571
|
+
this.reconnectTimer = null;
|
|
572
|
+
this.keepAliveTimer = null;
|
|
573
|
+
this.isReconnecting = false;
|
|
574
|
+
this.lockManager = new LockManager();
|
|
575
|
+
this.currentSystemId = null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async initSapClient() {
|
|
579
|
+
const sid = this.currentSystemId;
|
|
580
|
+
if (!sid) { console.error('❌ No system selected'); return false; }
|
|
581
|
+
try {
|
|
582
|
+
const sessionEntry = await getSessionForSystem(sid);
|
|
583
|
+
this.sapClient = new SAPClient(sessionEntry);
|
|
584
|
+
console.log(`✅ SAPClient initialized for ${sid}`);
|
|
585
|
+
return true;
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error(`❌ Failed to init SAPClient: ${err.message}`);
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
connect() {
|
|
593
|
+
if (this.ws) {
|
|
594
|
+
if (this.ws.readyState === WebSocket.CONNECTING) { console.log('⚠️ Already connecting'); return; }
|
|
595
|
+
if (this.ws.readyState === WebSocket.OPEN) { console.log('⚠️ Already connected'); return; }
|
|
596
|
+
}
|
|
597
|
+
if (this.isReconnecting) { console.log('⚠️ Reconnection already scheduled'); return; }
|
|
598
|
+
console.log(`\n🔌 Connecting to MCP Cloud...`);
|
|
599
|
+
console.log(` URL: ${WS_URL}`);
|
|
600
|
+
this.ws = new WebSocket(WS_URL, { headers: { 'Authorization': `Bearer ${MCP_API_KEY}` } });
|
|
601
|
+
this.ws.on('open', () => this.onOpen());
|
|
602
|
+
this.ws.on('message', (data) => this.onMessage(data));
|
|
603
|
+
this.ws.on('error', (error) => this.onError(error));
|
|
604
|
+
this.ws.on('close', () => this.onClose());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
onOpen() {
|
|
608
|
+
console.log(`✅ Connected to MCP Cloud!\n`);
|
|
609
|
+
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
|
610
|
+
this.isReconnecting = false;
|
|
611
|
+
this.startKeepAlive();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
startKeepAlive() {
|
|
615
|
+
if (this.keepAliveTimer) clearInterval(this.keepAliveTimer);
|
|
616
|
+
this.keepAliveTimer = setInterval(async () => {
|
|
617
|
+
if (!this.sapClient) return;
|
|
618
|
+
const r = await this.sapClient.ping();
|
|
619
|
+
}, parseInt(env.SAP_KEEPALIVE_INTERVAL || '300000', 10));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
stopKeepAlive() { if (this.keepAliveTimer) { clearInterval(this.keepAliveTimer); this.keepAliveTimer = null; } }
|
|
623
|
+
|
|
624
|
+
async onMessage(data) {
|
|
625
|
+
let message;
|
|
626
|
+
try { message = JSON.parse(data.toString()); } catch { return; }
|
|
627
|
+
try {
|
|
628
|
+
if (message.type === 'welcome') { console.log(`👋 Welcome:`, message.message); return; }
|
|
629
|
+
|
|
630
|
+
if (message.type === 'command') {
|
|
631
|
+
if (message.command === 'clear_locks') { this.lockManager.clearAll(); return; }
|
|
632
|
+
if (message.command === 'lock_stats') { const s = this.lockManager.getStats(); console.log(`\n📊 Lock Stats: ${s.activeLocks} active`); return; }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const { requestId, action, callspecs, system } = message;
|
|
636
|
+
if (!requestId || !action) { console.error('❌ Invalid message format'); return; }
|
|
637
|
+
console.log(`\n📥 Received: ${action} (request: ${requestId})`);
|
|
638
|
+
|
|
639
|
+
// ── switch_system ──────────────────────────────────────────────────
|
|
640
|
+
if (action === 'switch_system') {
|
|
641
|
+
const sid = system || (callspecs?.[0]?.system) || '';
|
|
642
|
+
if (!sid) { this.sendResponse(requestId, false, { error: 'system required' }); return; }
|
|
643
|
+
const sys = config.getSystem(sid);
|
|
644
|
+
if (!sys) { this.sendResponse(requestId, false, { error: `Unknown system: ${sid}` }); return; }
|
|
645
|
+
this.currentSystemId = sid;
|
|
646
|
+
this.lockManager.clearAll();
|
|
647
|
+
await this.initSapClient();
|
|
648
|
+
this.sendResponse(requestId, true, [{ success: true, data: { system: sid, authMode: sys.authMode } }]);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── invoke_rfc ─────────────────────────────────────────────────────
|
|
653
|
+
if (action === 'invoke_rfc') {
|
|
654
|
+
if (!this.sapClient?.rfcClient) { this.sendResponse(requestId, false, { error: 'No RFC session' }); return; }
|
|
655
|
+
const spec = Array.isArray(callspecs) ? callspecs[0] : callspecs || {};
|
|
656
|
+
const { fm, importing = {}, tables = [], table_fields = ['LINE'] } = spec;
|
|
657
|
+
try {
|
|
658
|
+
const tr = await this.sapClient.rfcClient.callFM(fm, importing, tables, table_fields);
|
|
659
|
+
const ft = tables[0] || '';
|
|
660
|
+
const lines = (tr[ft] || []).map(l => (l || '').trimEnd());
|
|
661
|
+
this.sendResponse(requestId, true, [{ success: true, data: { raw: lines.join('\n') } }]);
|
|
662
|
+
} catch (err) { this.sendResponse(requestId, false, { error: err.message }); }
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── http_call ──────────────────────────────────────────────────────
|
|
667
|
+
if (action === 'http_call') {
|
|
668
|
+
const specs = Array.isArray(callspecs) ? callspecs : [callspecs];
|
|
669
|
+
const results = [];
|
|
670
|
+
for (const cs of specs) {
|
|
671
|
+
const r = await handleHttpCall(cs, this.sapClient?.client?.defaults?.httpsAgent);
|
|
672
|
+
results.push(r);
|
|
673
|
+
}
|
|
674
|
+
this.sendResponse(requestId, results.every(r => r.success), results);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ── Standard callspec execution ────────────────────────────────────
|
|
679
|
+
if (!this.sapClient) {
|
|
680
|
+
if (!await this.initSapClient()) { this.sendResponse(requestId, false, { error: 'No system selected. Use switch_system first.' }); return; }
|
|
681
|
+
}
|
|
682
|
+
const exec = await this.executeCallspecs(action, callspecs);
|
|
683
|
+
this.sendResponse(requestId, exec.allSuccessful, exec.results);
|
|
684
|
+
console.log(`✅ Done: ${action}\n`);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error(`❌ Error: ${error.message}`);
|
|
687
|
+
if (message?.requestId) this.sendResponse(message.requestId, false, { error: error.message });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async executeCallspecs(action, callspecs) {
|
|
692
|
+
console.log(` Executing ${action} on SAP...`);
|
|
693
|
+
console.log(` 📦 RAW CALLSPECS (first 500): ${JSON.stringify(callspecs).substring(0, 500)}`);
|
|
694
|
+
if (Array.isArray(callspecs)) {
|
|
695
|
+
const results = [];
|
|
696
|
+
const extractedValues = {};
|
|
697
|
+
let allSuccessful = true;
|
|
698
|
+
let objectName = null, objectType = null;
|
|
699
|
+
if (callspecs.length > 0 && callspecs[0].url) {
|
|
700
|
+
const m = callspecs[0].url.match(/\/adt\/(?:ddic\/(?:tables|structures)|oo\/(?:classes|interfaces)|programs\/(?:programs|includes))\/([^\/\?]+)/i);
|
|
701
|
+
if (m) {
|
|
702
|
+
objectName = m[1];
|
|
703
|
+
if (callspecs[0].url.includes('/ddic/tables/')) objectType = 'TABL';
|
|
704
|
+
else if (callspecs[0].url.includes('/ddic/structures/')) objectType = 'STRU';
|
|
705
|
+
else if (callspecs[0].url.includes('/oo/classes/')) objectType = 'CLAS';
|
|
706
|
+
else if (callspecs[0].url.includes('/oo/interfaces/')) objectType = 'INTF';
|
|
707
|
+
else if (callspecs[0].url.includes('/programs/programs/')) objectType = 'PROG';
|
|
708
|
+
else if (callspecs[0].url.includes('/programs/includes/')) objectType = 'INCL';
|
|
709
|
+
console.log(` 🏷️ Detected: ${objectName} (${objectType})`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
for (let i = 0; i < callspecs.length; i++) {
|
|
713
|
+
const cs = callspecs[i];
|
|
714
|
+
console.log(`\n 📋 Step: ${cs.name || cs.step}`);
|
|
715
|
+
const isLockOp = cs.name === 'LOCK' || cs.url?.includes('_action=LOCK');
|
|
716
|
+
if (isLockOp && objectName && objectType) {
|
|
717
|
+
const cached = this.lockManager.getLock(objectName, objectType);
|
|
718
|
+
if (cached) {
|
|
719
|
+
console.log(` ⚡ SKIPPING LOCK - cached!`);
|
|
720
|
+
extractedValues.lockHandle = cached.lockHandle;
|
|
721
|
+
extractedValues.transportRequest = cached.transportRequest;
|
|
722
|
+
results.push({ success: true, status: 200, statusText: 'OK (cached)', data: { lockHandle: cached.lockHandle, transportRequest: cached.transportRequest, cached: true } });
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
let pcs = { ...cs };
|
|
727
|
+
if (pcs.url) {
|
|
728
|
+
if (extractedValues.lockHandle) pcs.url = pcs.url.replace(/<LOCK_HANDLE>/g, extractedValues.lockHandle);
|
|
729
|
+
if (extractedValues.transportRequest) pcs.url = pcs.url.replace(/<TRANSPORT_REQUEST>/g, extractedValues.transportRequest);
|
|
730
|
+
}
|
|
731
|
+
// For UNLOCK: inject lockHandle from cache if not already present in URL
|
|
732
|
+
if (pcs.url?.includes('_action=UNLOCK') && !pcs.url.includes('lockHandle=') && !pcs.url.includes('<LOCK_HANDLE>')) {
|
|
733
|
+
const cachedLock = objectName && objectType ? this.lockManager.getLock(objectName, objectType) : null;
|
|
734
|
+
if (cachedLock?.lockHandle) {
|
|
735
|
+
pcs.url = pcs.url + (pcs.url.includes('?') ? '&' : '?') + 'lockHandle=' + cachedLock.lockHandle;
|
|
736
|
+
console.log(` 🔓 Injected lockHandle into UNLOCK URL from cache: ${cachedLock.lockHandle.substring(0,10)}...`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const result = await this.sapClient.executeCallspec(pcs);
|
|
740
|
+
results.push(result);
|
|
741
|
+
if (result.success && result.data) {
|
|
742
|
+
if (result.data.lockHandle) {
|
|
743
|
+
extractedValues.lockHandle = result.data.lockHandle;
|
|
744
|
+
if (isLockOp && objectName && objectType) this.lockManager.saveLock(objectName, objectType, result.data.lockHandle, result.data.transportRequest || '');
|
|
745
|
+
}
|
|
746
|
+
if (result.data.transportRequest) extractedValues.transportRequest = result.data.transportRequest;
|
|
747
|
+
}
|
|
748
|
+
if (!result.success) { allSuccessful = false; break; }
|
|
749
|
+
}
|
|
750
|
+
return { results, allSuccessful };
|
|
751
|
+
} else {
|
|
752
|
+
const r = await this.sapClient.executeCallspec(callspecs);
|
|
753
|
+
return { results: [r], allSuccessful: r.success };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
sendResponse(requestId, success, data) {
|
|
758
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.error('❌ WS not connected'); return; }
|
|
759
|
+
this.ws.send(JSON.stringify({ type: 'response', requestId, success, data, timestamp: new Date().toISOString() }));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
onError(error) { console.error(`❌ WS error: ${error.message}`); }
|
|
763
|
+
|
|
764
|
+
onClose() {
|
|
765
|
+
console.log(`\n⚠️ Disconnected`);
|
|
766
|
+
this.ws = null; this.stopKeepAlive();
|
|
767
|
+
if (this.sapClient) this.sapClient.clearSession();
|
|
768
|
+
this.lockManager.clearAll();
|
|
769
|
+
if (!this.isReconnecting && !this.reconnectTimer) {
|
|
770
|
+
console.log(` Reconnecting in 5s...`);
|
|
771
|
+
this.isReconnecting = true;
|
|
772
|
+
this.reconnectTimer = setTimeout(() => { this.isReconnecting = false; this.connect(); }, 5000);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
disconnect() {
|
|
777
|
+
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
|
778
|
+
this.isReconnecting = false; this.stopKeepAlive();
|
|
779
|
+
if (this.ws) { this.ws.close(); this.ws = null; }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
784
|
+
// Main
|
|
785
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
786
|
+
|
|
787
|
+
let MCP_API_KEY = '';
|
|
788
|
+
let WS_URL = env.WS_URL || 'ws://localhost:8080';
|
|
789
|
+
|
|
790
|
+
function loadGlobalConfig() {
|
|
791
|
+
if (config.isConfigInitialized()) { MCP_API_KEY = config.getMcpApiKey(); WS_URL = config.getWsUrl(); console.log('🔑 Config from encrypted store.'); }
|
|
792
|
+
else { MCP_API_KEY = env.API_KEY || ''; WS_URL = env.WS_URL || 'ws://localhost:8080'; console.log('⚠️ Using .env fallback.'); }
|
|
793
|
+
if (!MCP_API_KEY) { console.error('❌ No MCP API key'); process.exit(1); }
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const args = process.argv.slice(2);
|
|
797
|
+
if (args[0] === 'setup' || args[0] === 'config' || args[0] === 'add-system' || args[0] === 'set-mcp-key' ||
|
|
798
|
+
args[0] === 'list-systems' || args[0] === 'remove-system' || args[0] === 'enable-system' ||
|
|
799
|
+
args[0] === 'disable-system' || args[0] === 'show-config' || args[0] === 'arcanum-fallback' ||
|
|
800
|
+
args[0] === 'set-ws-url' || args[0] === 'help') {
|
|
801
|
+
await import('./config-cli.mjs'); process.exit(0);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!config.isConfigInitialized()) {
|
|
805
|
+
config.addOrUpdateSystem('S4D', { authMode: 'snc', connection: { host: 'vhlbvs4dci.sap.piracanjuba.com.br', sysnr: '00', client: '100', username: '', sncPartnerName: 'p:CN=svc.ssoS4D', sncQop: '3', language: 'PT' }, enabled: true });
|
|
806
|
+
config.setMcpApiKey('arc_de2fb7965d536a5911ffd7d9ba39d70c');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
console.log('╔═══════════════════════════════════════════════════════════╗');
|
|
810
|
+
console.log('║ SAP Local Client — Multi-System SSO Bridge ║');
|
|
811
|
+
console.log('╚═══════════════════════════════════════════════════════════╝');
|
|
812
|
+
loadGlobalConfig();
|
|
813
|
+
console.log(` WS URL: ${WS_URL}`);
|
|
814
|
+
console.log(` API Key: ${MCP_API_KEY.substring(0, 10)}...`);
|
|
815
|
+
console.log(` Store: ${config.getConfigFilePath()}`);
|
|
816
|
+
const enabled = (config.listSystems() || []).filter(s => s.enabled);
|
|
817
|
+
console.log(` Systems: ${enabled.map(s => `${s.systemId} (${s.authMode})`).join(', ') || 'none'}`);
|
|
818
|
+
console.log(' Use: switch_system <systemId> to select a system');
|
|
819
|
+
|
|
820
|
+
const client = new WebSocketClient();
|
|
821
|
+
await fetchSapSystems();
|
|
822
|
+
client.connect();
|
|
823
|
+
|
|
824
|
+
process.on('SIGINT', () => { client.disconnect(); process.exit(0); });
|
|
825
|
+
process.on('SIGTERM', () => { client.disconnect(); process.exit(0); });
|