sap-ai-dev-toolkit 0.9.9 → 0.9.91
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/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/patches/vsp-bas-proxy-auth.patch +53 -0
- package/src/launcher.mjs +8 -1
- package/src/local-sap-gui.mjs +4 -2
- package/src/setup.mjs +22 -10
package/dist/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
02831a47fa28a1807d94f6b8485c21f1221c7b3aa0a2b9b7bf0682ab4fb5618f vsp-linux-x64
|
|
2
|
+
4398dac4c9d304d63e03abbb86523b61e5180b5b355dd1a2ed0d2405454cc8f7 vsp-linux-arm64
|
|
3
|
+
356a10913b7a46a8ed67132e9b15ea7ff92b8adeb1c799111866e9af176e85fe vsp-darwin-x64
|
|
4
|
+
fb5e72a59618578187eb8e4374571b32baab34397766fc6ea8ab328c6cea849b vsp-darwin-arm64
|
|
5
|
+
50d5c74194063bc9bcc641fb580f1c1232cce0c1577cdad6dc503f41efd3344c vsp-win32-x64.exe
|
package/dist/vsp-darwin-arm64
CHANGED
|
Binary file
|
package/dist/vsp-darwin-x64
CHANGED
|
Binary file
|
package/dist/vsp-linux-arm64
CHANGED
|
Binary file
|
package/dist/vsp-linux-x64
CHANGED
|
Binary file
|
package/dist/vsp-win32-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -492,3 +492,56 @@ index 0000000..80741da
|
|
|
492
492
|
+ }
|
|
493
493
|
+}
|
|
494
494
|
+
|
|
495
|
+
diff --git a/pkg/adt/sso.go b/pkg/adt/sso.go
|
|
496
|
+
index d67c4e8..4e9dc45 100644
|
|
497
|
+
--- a/pkg/adt/sso.go
|
|
498
|
+
+++ b/pkg/adt/sso.go
|
|
499
|
+
@@ -155,7 +155,7 @@ func CaptureSSOCookies(ctx context.Context, opts SSOOptions) (map[string]string,
|
|
500
|
+
// below is the real verdict.
|
|
501
|
+
if err := chromedp.Run(timeoutCtx, chromedp.Navigate(opts.TriggerURL)); err != nil {
|
|
502
|
+
if timeoutCtx.Err() != nil {
|
|
503
|
+
- return nil, fmt.Errorf("SSO capture timed out after %s", timeout)
|
|
504
|
+
+ return nil, navigationTimeout(opts.Headless, timeout)
|
|
505
|
+
}
|
|
506
|
+
if opts.Verbose {
|
|
507
|
+
fmt.Fprintf(os.Stderr, "[SSO] navigation reported %v (normal during SSO)\n", err)
|
|
508
|
+
@@ -174,6 +174,17 @@ func CaptureSSOCookies(ctx context.Context, opts SSOOptions) (map[string]string,
|
|
509
|
+
return cookies, nil
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
+// navigationTimeout explains a first page that never finished loading. In a
|
|
513
|
+
+// silent browser that page is waiting on a person — a smart-card certificate
|
|
514
|
+
+// picker or a login prompt headless mode cannot show — so a visible window is
|
|
515
|
+
+// the next step, not a failed start.
|
|
516
|
+
+func navigationTimeout(headless bool, timeout time.Duration) error {
|
|
517
|
+
+ if headless {
|
|
518
|
+
+ return fmt.Errorf("%w: the silent sign-in page did not finish loading within %s", ErrSSOInteractiveRequired, timeout)
|
|
519
|
+
+ }
|
|
520
|
+
+ return fmt.Errorf("SSO capture timed out after %s", timeout)
|
|
521
|
+
+}
|
|
522
|
+
+
|
|
523
|
+
// DefaultSSOProfileDir returns a per-host browser profile directory under the
|
|
524
|
+
// user's cache location. Keeping one profile per host means a silent refresh
|
|
525
|
+
// for one system is never disturbed by a login to another.
|
|
526
|
+
diff --git a/pkg/adt/sso_navigation_test.go b/pkg/adt/sso_navigation_test.go
|
|
527
|
+
new file mode 100644
|
|
528
|
+
index 0000000..c851021
|
|
529
|
+
--- /dev/null
|
|
530
|
+
+++ b/pkg/adt/sso_navigation_test.go
|
|
531
|
+
@@ -0,0 +1,16 @@
|
|
532
|
+
+package adt
|
|
533
|
+
+
|
|
534
|
+
+import (
|
|
535
|
+
+ "errors"
|
|
536
|
+
+ "testing"
|
|
537
|
+
+ "time"
|
|
538
|
+
+)
|
|
539
|
+
+
|
|
540
|
+
+func TestSilentNavigationTimeoutAsksForAPerson(t *testing.T) {
|
|
541
|
+
+ if err := navigationTimeout(true, time.Second); !errors.Is(err, ErrSSOInteractiveRequired) {
|
|
542
|
+
+ t.Fatalf("silent timeout = %v, want ErrSSOInteractiveRequired so a visible window opens", err)
|
|
543
|
+
+ }
|
|
544
|
+
+ if err := navigationTimeout(false, time.Second); errors.Is(err, ErrSSOInteractiveRequired) {
|
|
545
|
+
+ t.Fatalf("headed timeout = %v, must not ask for yet another window", err)
|
|
546
|
+
+ }
|
|
547
|
+
+}
|
package/src/launcher.mjs
CHANGED
|
@@ -222,7 +222,14 @@ async function rememberAdtUrl(configured, found) {
|
|
|
222
222
|
// swept only when the configuration never named one.
|
|
223
223
|
async function resolveLocalAdtUrl(name, configured, client) {
|
|
224
224
|
const log = message => logLine(`[sap-ai-dev] ${name}: ${message}`);
|
|
225
|
-
const detect =
|
|
225
|
+
const detect = async target => {
|
|
226
|
+
const started = Date.now();
|
|
227
|
+
// MCP output channels cannot animate, so a long scan reports that it is still running.
|
|
228
|
+
const heartbeat = setInterval(() => log(`port scan still running on ${new URL(target.url).hostname} (${Math.round((Date.now() - started) / 1000)} s)`), 20_000);
|
|
229
|
+
try { return await detectLocalAdtUrl({ client, ...target }); }
|
|
230
|
+
catch (error) { return { url: '', reason: error.message, failed: true }; }
|
|
231
|
+
finally { clearInterval(heartbeat); }
|
|
232
|
+
};
|
|
226
233
|
const explicit = hasExplicitPort(configured);
|
|
227
234
|
let verdict = { url: '', reason: '' };
|
|
228
235
|
if (explicit) {
|
package/src/local-sap-gui.mjs
CHANGED
|
@@ -461,8 +461,10 @@ export function adtVerdict(result, protocol = 'https:') {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
// A portless URL means 443, which on-premise ICMs rarely serve; VSP's `detect`
|
|
464
|
-
// probes the conventional ADT ports and reports which one answers.
|
|
465
|
-
|
|
464
|
+
// probes the conventional ADT ports and reports which one answers. A host that
|
|
465
|
+
// accepts every port makes each probe wait out its HTTP timeouts, so the full
|
|
466
|
+
// sweep gets minutes.
|
|
467
|
+
export async function detectAdtUrl(binary, { url, client, instance, env = process.env, exhaustive = false, ports = [], timeoutMs = exhaustive ? 600_000 : 120_000 } = {}) {
|
|
466
468
|
let configured;
|
|
467
469
|
try { configured = new URL(url); } catch { return { url: '', reason: `${url} is not a valid URL` }; }
|
|
468
470
|
const host = configured.hostname.replace(/^\[(.*)\]$/, '$1');
|
package/src/setup.mjs
CHANGED
|
@@ -141,30 +141,42 @@ export function adtCandidate(answer, baseUrl) {
|
|
|
141
141
|
// Cheapest evidence first: named ports get one probe, then the host's usual
|
|
142
142
|
// ports, then the message server's application servers, then every port.
|
|
143
143
|
async function findAdtUrl(destination, label, client, { candidates = [], scan = true, output = stdout, env = process.env, detectAdtUrl } = {}) {
|
|
144
|
-
const detect = async (
|
|
145
|
-
print(output, formatStatus(
|
|
146
|
-
const
|
|
144
|
+
const detect = async (line, activity, target) => {
|
|
145
|
+
print(output, formatStatus(line, 'progress', output, 'ADT'));
|
|
146
|
+
const started = Date.now();
|
|
147
|
+
const progress = startProgress(activity, { output, env, label: 'ADT' });
|
|
148
|
+
// A short, ticking spinner: a long line would truncate the animation away and look frozen.
|
|
149
|
+
const ticker = setInterval(() => progress.update(`${activity} (${Math.round((Date.now() - started) / 1000)} s)`), 1000);
|
|
147
150
|
try { return await detectAdtUrl({ client, ...target }); }
|
|
148
151
|
catch (error) { return { url: '', reason: error.message }; }
|
|
149
|
-
finally {
|
|
152
|
+
finally {
|
|
153
|
+
clearInterval(ticker);
|
|
154
|
+
progress();
|
|
155
|
+
}
|
|
150
156
|
};
|
|
151
157
|
let verdict = { url: '', reason: `no port was checked for ${label}` };
|
|
152
158
|
for (const candidate of [...new Set([destination.url, ...candidates].filter(url => url && hasExplicitPort(url)))]) {
|
|
153
|
-
verdict = await detect(`Checking ADT at ${candidate}`, { url: candidate, ports: [adtPort(candidate)] });
|
|
159
|
+
verdict = await detect(`Checking ADT at ${candidate}`, `Checking ${candidate}`, { url: candidate, ports: [adtPort(candidate)] });
|
|
154
160
|
if (verdict.url || verdict.unresolved) return verdict;
|
|
155
161
|
}
|
|
156
162
|
if (!scan) return verdict;
|
|
157
163
|
const host = new URL(destination.url).hostname;
|
|
158
|
-
verdict = await detect(`Port scan in progress on ${host} to find the ADT port for ${label}`, { url: destination.url, ...(destination.instance ? { instance: destination.instance } : {}) });
|
|
164
|
+
verdict = await detect(`Port scan in progress on ${host} to find the ADT port for ${label}`, `Scanning the usual ADT ports on ${host}`, { url: destination.url, ...(destination.instance ? { instance: destination.instance } : {}) });
|
|
159
165
|
if (verdict.url || verdict.unresolved) return verdict;
|
|
160
166
|
if (destination.messageServer) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
const { host: messageHost, httpPort } = destination.messageServer;
|
|
168
|
+
print(output, formatStatus(`Asking the message server ${messageHost}:${httpPort} for the application servers of ${label}`, 'progress', output, 'ADT'));
|
|
169
|
+
const servers = (await messageServerAppServers({ ...destination.messageServer, systemId: destination.systemId })).slice(0, 3);
|
|
170
|
+
print(output, formatStatus(servers.length
|
|
171
|
+
? `The message server lists ${servers.map(server => `${server.host} (instance ${server.instance})`).join(', ')}.`
|
|
172
|
+
: `The message server at ${messageHost}:${httpPort} listed no application servers.`, 'info', output, 'ADT'));
|
|
173
|
+
for (const server of servers) {
|
|
174
|
+
const found = await detect(`Port scan in progress on application server ${server.host} (instance ${server.instance})`, `Scanning ${server.host}`, { url: `${new URL(destination.url).protocol}//${server.host}`, instance: server.instance });
|
|
164
175
|
if (found.url) return found;
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
|
-
|
|
178
|
+
print(output, formatStatus(`No ADT port on the usual ports for ${label}: ${verdict.reason}.`, 'info', output, 'ADT'));
|
|
179
|
+
return detect(`Scanning the remaining conventional SAP ports on ${host} (about 300 ports; this can take a few minutes)`, `Scanning the remaining SAP ports on ${host}`, { url: destination.url, exhaustive: true });
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
// An SSO sign-in can only start where ADT answers, so for SSO setup does not
|