sap-ai-dev-toolkit 0.8.5 → 0.9.1
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/README.md +2 -0
- 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 +6 -3
- package/patches/vsp-bas-proxy-auth.patch +177 -118
- package/scripts/ensure-go.mjs +15 -7
- package/scripts/install-local.mjs +5 -1
- package/scripts/postinstall.mjs +17 -5
- package/scripts/run-live-s4h.mjs +10 -0
- package/src/harnesses.mjs +3 -1
- package/src/launcher.mjs +14 -3
- package/src/local-sap-gui.mjs +4 -0
- package/src/mcp-proxy.mjs +25 -2
- package/src/windows-sso-adt-proxy.mjs +7 -36
package/README.md
CHANGED
|
@@ -857,6 +857,8 @@ NPM_PUBLISH_TOKEN=npm_...
|
|
|
857
857
|
- To publish a patch bump, run `npm run publish:npm -- --patch`; it updates `package.json` and `package-lock.json` before publishing. If publishing fails after the bump, retry without `--patch`.
|
|
858
858
|
- npm's `prepublishOnly` hook builds the pinned VSP binaries for all platforms (including the Go toolchain if missing) and refuses to ship a tarball whose `dist/checksums.txt` does not list all five assets; it guards `npm run publish:npm` and a plain `npm publish` alike. Pass `--skip-build` to republish an already-built `dist/` without rebuilding.
|
|
859
859
|
- Preview the package without publishing, changing its version, or building with `npm run publish:npm -- --dry-run`.
|
|
860
|
+
- GitHub Actions runs the test suite on pushes to `main` and pull requests. A push to `main` that changes the `package.json` version runs the same checks, builds the VSP binaries, and publishes that version to npm.
|
|
861
|
+
- The publish workflow uses the `NPM_PUBLISH_TOKEN` GitHub repository secret. Verify it with `gh secret list`; the token value is never stored in the repository.
|
|
860
862
|
|
|
861
863
|
The token is not printed or stored in the repository.
|
|
862
864
|
|
package/dist/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
a5b602573f54ce3d03d161aaaeb78559fd73bd51f2ae9a3b7549cee1ddc3fbf6 vsp-linux-x64
|
|
2
|
+
2e336e2999c3654e5eb1e3a8bb42616eb5d5e422ab7ae74ffbff4634be45189d vsp-linux-arm64
|
|
3
|
+
afc7fd0f2e40ae5ebfcd3ddb8ac3cb7e7501b2b66d76ac96e3b6054b09366bb5 vsp-darwin-x64
|
|
4
|
+
2d28a870c4a006900eec245384b4bbc2e84651e0ba911cca6107c132ace73575 vsp-darwin-arm64
|
|
5
|
+
7d0189aa7ff936a145b0e3f15bffce8de5aebc252465a720edc3a9b122bd2a28 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-ai-dev-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
|
|
5
5
|
"author": "Gurkan Yilmaz",
|
|
6
6
|
"type": "module",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"postinstall": "node scripts/postinstall.mjs",
|
|
24
24
|
"install:local": "node scripts/install-local.mjs",
|
|
25
|
-
"test": "node --test test/*.test.mjs",
|
|
26
|
-
"test:live:s4h": "
|
|
25
|
+
"test": "node --test --test-force-exit test/*.test.mjs",
|
|
26
|
+
"test:live:s4h": "node scripts/run-live-s4h.mjs",
|
|
27
27
|
"test:visibility": "node --test test/setup.test.mjs test/terminal-ui.test.mjs",
|
|
28
28
|
"test:mutation": "node scripts/mutation-terminal-ui.mjs",
|
|
29
29
|
"publish:npm": "node scripts/publish-npm.mjs",
|
|
@@ -55,5 +55,8 @@
|
|
|
55
55
|
],
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@lydell/node-pty": "^1.2.0-beta.15"
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
diff --git a/cmd/vsp/main.go b/cmd/vsp/main.go
|
|
2
|
+
index 5007935..b2bc6be 100755
|
|
2
3
|
--- a/cmd/vsp/main.go
|
|
3
4
|
+++ b/cmd/vsp/main.go
|
|
4
|
-
@@ -
|
|
5
|
+
@@ -100,6 +100,7 @@ func init() {
|
|
6
|
+
rootCmd.Flags().StringVar(&cfg.BaseURL, "service", "", "SAP system URL (alias for --url)")
|
|
7
|
+
|
|
5
8
|
// Authentication flags
|
|
6
9
|
+ rootCmd.Flags().BoolVar(&cfg.ProxyAuth, "proxy-auth", false, "Use BAS destination proxy authentication; do not send local credentials")
|
|
7
|
-
|
|
10
|
+
rootCmd.Flags().StringVarP(&cfg.Username, "user", "u", "", "SAP username")
|
|
11
|
+
rootCmd.Flags().StringVarP(&cfg.Password, "password", "p", "", "SAP password")
|
|
12
|
+
rootCmd.Flags().StringVar(&cfg.Password, "pass", "", "SAP password (alias for --password)")
|
|
13
|
+
@@ -170,6 +171,7 @@ func init() {
|
|
14
|
+
rootCmd.PersistentFlags().BoolVarP(&cfg.Verbose, "verbose", "v", false, "Enable verbose output to stderr")
|
|
15
|
+
|
|
8
16
|
// Bind flags to viper for environment variable support
|
|
9
17
|
+ viper.BindPFlag("proxy-auth", rootCmd.Flags().Lookup("proxy-auth"))
|
|
10
|
-
|
|
18
|
+
viper.BindPFlag("url", rootCmd.Flags().Lookup("url"))
|
|
19
|
+
viper.BindPFlag("user", rootCmd.Flags().Lookup("user"))
|
|
20
|
+
viper.BindPFlag("password", rootCmd.Flags().Lookup("password"))
|
|
21
|
+
@@ -233,6 +235,9 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
// Browser-based SSO authentication (must run before processCookieAuth)
|
|
12
25
|
+ if err := validateProxyAuth(cmd); err != nil {
|
|
13
26
|
+ return err
|
|
14
27
|
+ }
|
|
15
|
-
|
|
28
|
+
if err := processBrowserAuth(cmd); err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
@@ -346,6 +351,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
func resolveConfig(cmd *cobra.Command) {
|
|
17
35
|
+ cfg.ProxyAuth = cfg.ProxyAuth || viper.GetBool("PROXY_AUTH")
|
|
18
|
-
|
|
36
|
+
// Check if cookie auth is explicitly requested via CLI flags OR env vars
|
|
37
|
+
// If so, we should NOT load user/password from env/.env to avoid conflicts
|
|
38
|
+
// Cookie auth takes precedence over basic auth since it's more explicit
|
|
39
|
+
@@ -530,6 +536,24 @@ func validateConfig() error {
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
+func validateProxyAuth(cmd *cobra.Command) error {
|
|
20
44
|
+ if !cfg.ProxyAuth {
|
|
21
45
|
+ return nil
|
|
@@ -35,146 +59,68 @@ diff --git a/cmd/vsp/main.go b/cmd/vsp/main.go
|
|
|
35
59
|
+ return nil
|
|
36
60
|
+}
|
|
37
61
|
func processBrowserAuth(cmd *cobra.Command) error {
|
|
38
|
-
|
|
62
|
+
browserAuth, _ := cmd.Flags().GetBool("browser-auth")
|
|
63
|
+
if !browserAuth && !viper.GetBool("BROWSER_AUTH") {
|
|
64
|
+
@@ -599,6 +623,9 @@ func ssoRequested(cmd *cobra.Command) bool {
|
|
65
|
+
// days will outlive any session it starts with, and the point of the exercise
|
|
66
|
+
// is that nobody has to notice when it does.
|
|
39
67
|
func processSSOAuth(cmd *cobra.Command) error {
|
|
40
68
|
+ if cfg.ProxyAuth {
|
|
41
69
|
+ return nil
|
|
42
70
|
+ }
|
|
43
|
-
|
|
71
|
+
if !ssoRequested(cmd) {
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
@@ -657,6 +684,9 @@ func processSSOAuth(cmd *cobra.Command) error {
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
func processSAMLAuth(cmd *cobra.Command) error {
|
|
45
78
|
+ if cfg.ProxyAuth {
|
|
46
79
|
+ return nil
|
|
47
80
|
+ }
|
|
48
|
-
|
|
81
|
+
samlAuth, _ := cmd.Flags().GetBool("saml-auth")
|
|
82
|
+
if !samlAuth && !viper.GetBool("SAML_AUTH") {
|
|
83
|
+
return nil
|
|
84
|
+
@@ -752,6 +782,9 @@ func processSAMLAuth(cmd *cobra.Command) error {
|
|
85
|
+
}
|
|
86
|
+
|
|
49
87
|
func processCookieAuth(cmd *cobra.Command) error {
|
|
50
88
|
+ if cfg.ProxyAuth {
|
|
51
89
|
+ return nil
|
|
52
90
|
+ }
|
|
91
|
+
cookieFile, _ := cmd.Flags().GetString("cookie-file")
|
|
92
|
+
cookieString, _ := cmd.Flags().GetString("cookie-string")
|
|
93
|
+
|
|
53
94
|
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
|
|
95
|
+
index 9337857..11fc940 100644
|
|
54
96
|
--- a/internal/mcp/server.go
|
|
55
97
|
+++ b/internal/mcp/server.go
|
|
56
|
-
@@ -
|
|
98
|
+
@@ -69,6 +69,9 @@ type Config struct {
|
|
99
|
+
InsecureSkipVerify bool
|
|
100
|
+
|
|
57
101
|
// Cookie authentication (alternative to basic auth)
|
|
58
102
|
+ // ProxyAuth delegates identity to a BAS destination proxy. Local credentials are rejected.
|
|
59
103
|
+ ProxyAuth bool
|
|
60
104
|
+
|
|
61
105
|
Cookies map[string]string
|
|
62
|
-
|
|
106
|
+
|
|
107
|
+
// Build names the binary, as "v2.52.0 (commit abc1234, built ...)".
|
|
108
|
+
@@ -151,6 +154,9 @@ func NewServer(cfg *Config) *Server {
|
|
109
|
+
if cfg.InsecureSkipVerify {
|
|
110
|
+
opts = append(opts, adt.WithInsecureSkipVerify())
|
|
111
|
+
}
|
|
63
112
|
+ if cfg.ProxyAuth {
|
|
64
113
|
+ opts = append(opts, adt.WithProxyContextIDGuard())
|
|
65
114
|
+ }
|
|
66
115
|
if len(cfg.Cookies) > 0 {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
+++ b/pkg/adt/client.go
|
|
70
|
-
@@ -1586 +1586 @@
|
|
71
|
-
- Accept: "application/xml",
|
|
72
|
-
+ Accept: "application/atom+xml;type=feed",
|
|
73
|
-
@@ -1592,27 +1592,27 @@
|
|
74
|
-
- type componentXML struct {
|
|
75
|
-
- Name string `xml:"name,attr"`
|
|
76
|
-
- Release string `xml:"release,attr"`
|
|
77
|
-
- SupportPack string `xml:"supportPack,attr"`
|
|
78
|
-
- Description string `xml:"description,attr"`
|
|
79
|
-
- }
|
|
80
|
-
- type componentsXML struct {
|
|
81
|
-
- XMLName xml.Name `xml:"components"`
|
|
82
|
-
- Components []componentXML `xml:"component"`
|
|
83
|
-
- }
|
|
84
|
-
-
|
|
85
|
-
- var comps componentsXML
|
|
86
|
-
- if err := xml.Unmarshal(resp.Body, &comps); err != nil {
|
|
87
|
-
- return nil, fmt.Errorf("parsing components: %w", err)
|
|
88
|
-
- }
|
|
89
|
-
-
|
|
90
|
-
- result := make([]InstalledComponent, len(comps.Components))
|
|
91
|
-
- for i, c := range comps.Components {
|
|
92
|
-
- result[i] = InstalledComponent{
|
|
93
|
-
- Name: c.Name,
|
|
94
|
-
- Release: c.Release,
|
|
95
|
-
- SupportPack: c.SupportPack,
|
|
96
|
-
- Description: c.Description,
|
|
97
|
-
- }
|
|
98
|
-
- }
|
|
99
|
-
-
|
|
100
|
-
- return result, nil
|
|
101
|
-
+ type componentEntry struct {
|
|
102
|
-
+ ID string `xml:"id"`
|
|
103
|
-
+ Title string `xml:"title"`
|
|
104
|
-
+ }
|
|
105
|
-
+ type componentsFeed struct {
|
|
106
|
-
+ Entries []componentEntry `xml:"entry"`
|
|
107
|
-
+ }
|
|
108
|
-
+ var feed componentsFeed
|
|
109
|
-
+ if err := xml.Unmarshal(resp.Body, &feed); err != nil {
|
|
110
|
-
+ return nil, fmt.Errorf("parsing components: %w", err)
|
|
111
|
-
+ }
|
|
112
|
-
+
|
|
113
|
-
+ result := make([]InstalledComponent, 0, len(feed.Entries))
|
|
114
|
-
+ for i, entry := range feed.Entries {
|
|
115
|
-
+ fields := strings.SplitN(entry.Title, ";", 4)
|
|
116
|
-
+ name := strings.TrimSpace(entry.ID)
|
|
117
|
-
+ if name == "" || len(fields) != 4 {
|
|
118
|
-
+ return nil, fmt.Errorf("parsing components: malformed feed entry %d", i+1)
|
|
119
|
-
+ }
|
|
120
|
-
+ result = append(result, InstalledComponent{
|
|
121
|
-
+ Name: name,
|
|
122
|
-
+ Release: strings.TrimSpace(fields[0]),
|
|
123
|
-
+ SupportPack: strings.TrimSpace(fields[2]),
|
|
124
|
-
+ Description: strings.TrimSpace(fields[3]),
|
|
125
|
-
+ })
|
|
126
|
-
+ }
|
|
127
|
-
+ return result, nil
|
|
128
|
-
diff --git a/pkg/adt/client_components_test.go b/pkg/adt/client_components_test.go
|
|
129
|
-
new file mode 100644
|
|
130
|
-
--- /dev/null
|
|
131
|
-
+++ b/pkg/adt/client_components_test.go
|
|
132
|
-
@@ -0,0 +1,40 @@
|
|
133
|
-
+package adt
|
|
134
|
-
+
|
|
135
|
-
+import (
|
|
136
|
-
+ "context"
|
|
137
|
-
+ "io"
|
|
138
|
-
+ "net/http"
|
|
139
|
-
+ "net/http/httptest"
|
|
140
|
-
+ "testing"
|
|
141
|
-
+)
|
|
142
|
-
+
|
|
143
|
-
+func TestGetInstalledComponentsUsesADTFeed(t *testing.T) {
|
|
144
|
-
+ const accept = "application/atom+xml;type=feed"
|
|
145
|
-
+ const response = `<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"><atom:entry><atom:id>SAP_BASIS</atom:id><atom:title>758;SAPK-75802INSAPBASIS;0002;SAP Basis > 2022; patch</atom:title></atom:entry></atom:feed>`
|
|
146
|
-
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
147
|
-
+ if r.URL.Path != "/sap/bc/adt/system/components" {
|
|
148
|
-
+ t.Errorf("unexpected request path: %s", r.URL.Path)
|
|
149
|
-
+ w.WriteHeader(http.StatusNotFound)
|
|
150
|
-
+ return
|
|
151
|
-
+ }
|
|
152
|
-
+ if got := r.Header.Get("Accept"); got != accept {
|
|
153
|
-
+ t.Errorf("Accept = %q, want %q", got, accept)
|
|
154
|
-
+ w.WriteHeader(http.StatusNotAcceptable)
|
|
155
|
-
+ return
|
|
156
|
-
+ }
|
|
157
|
-
+ w.Header().Set("Content-Type", accept)
|
|
158
|
-
+ _, _ = io.WriteString(w, response)
|
|
159
|
-
+ }))
|
|
160
|
-
+ defer server.Close()
|
|
161
|
-
+
|
|
162
|
-
+ components, err := NewClient(server.URL, "user", "pass").GetInstalledComponents(context.Background())
|
|
163
|
-
+ if err != nil {
|
|
164
|
-
+ t.Fatalf("GetInstalledComponents returned an error: %v", err)
|
|
165
|
-
+ }
|
|
166
|
-
+ if len(components) != 1 {
|
|
167
|
-
+ t.Fatalf("got %d components, want 1", len(components))
|
|
168
|
-
+ }
|
|
169
|
-
+ if got := components[0]; got.Name != "SAP_BASIS" || got.Release != "758" || got.SupportPack != "0002" || got.Description != "SAP Basis > 2022; patch" {
|
|
170
|
-
+ t.Errorf("got component %#v", got)
|
|
171
|
-
+ }
|
|
172
|
-
+}
|
|
116
|
+
opts = append(opts, adt.WithCookies(cfg.Cookies))
|
|
117
|
+
}
|
|
173
118
|
diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go
|
|
119
|
+
index 7740e56..b4c2faf 100755
|
|
174
120
|
--- a/internal/mcp/server_test.go
|
|
175
121
|
+++ b/internal/mcp/server_test.go
|
|
176
122
|
@@ -159,3 +159,112 @@ func TestDebuggerGetVariablesSchemaIncludesItems(t *testing.T) {
|
|
177
|
-
t.Fatalf("expected
|
|
123
|
+
t.Fatalf("expected variable_ids.items.type to be 'string', got %v", items["type"])
|
|
178
124
|
}
|
|
179
125
|
}
|
|
180
126
|
+
|
|
@@ -287,6 +233,7 @@ diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go
|
|
|
287
233
|
+ }
|
|
288
234
|
+}
|
|
289
235
|
diff --git a/internal/mcp/tools_register.go b/internal/mcp/tools_register.go
|
|
236
|
+
index dd433d6..5553b7f 100644
|
|
290
237
|
--- a/internal/mcp/tools_register.go
|
|
291
238
|
+++ b/internal/mcp/tools_register.go
|
|
292
239
|
@@ -837,6 +837,19 @@ func (s *Server) registerDevTools(shouldRegister func(string) bool) {
|
|
@@ -309,7 +256,117 @@ diff --git a/internal/mcp/tools_register.go b/internal/mcp/tools_register.go
|
|
|
309
256
|
),
|
|
310
257
|
), s.handleActivateMultiple)
|
|
311
258
|
}
|
|
259
|
+
diff --git a/pkg/adt/client.go b/pkg/adt/client.go
|
|
260
|
+
index 979e908..c7e9501 100755
|
|
261
|
+
--- a/pkg/adt/client.go
|
|
262
|
+
+++ b/pkg/adt/client.go
|
|
263
|
+
@@ -1583,38 +1583,38 @@ type InstalledComponent struct {
|
|
264
|
+
func (c *Client) GetInstalledComponents(ctx context.Context) ([]InstalledComponent, error) {
|
|
265
|
+
resp, err := c.transport.Request(ctx, "/sap/bc/adt/system/components", &RequestOptions{
|
|
266
|
+
Method: http.MethodGet,
|
|
267
|
+
- Accept: "application/xml",
|
|
268
|
+
+ Accept: "application/atom+xml;type=feed",
|
|
269
|
+
})
|
|
270
|
+
if err != nil {
|
|
271
|
+
return nil, fmt.Errorf("getting installed components: %w", err)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
- type componentXML struct {
|
|
275
|
+
- Name string `xml:"name,attr"`
|
|
276
|
+
- Release string `xml:"release,attr"`
|
|
277
|
+
- SupportPack string `xml:"supportPack,attr"`
|
|
278
|
+
- Description string `xml:"description,attr"`
|
|
279
|
+
+ type componentEntry struct {
|
|
280
|
+
+ ID string `xml:"id"`
|
|
281
|
+
+ Title string `xml:"title"`
|
|
282
|
+
}
|
|
283
|
+
- type componentsXML struct {
|
|
284
|
+
- XMLName xml.Name `xml:"components"`
|
|
285
|
+
- Components []componentXML `xml:"component"`
|
|
286
|
+
+ type componentsFeed struct {
|
|
287
|
+
+ Entries []componentEntry `xml:"entry"`
|
|
288
|
+
}
|
|
289
|
+
-
|
|
290
|
+
- var comps componentsXML
|
|
291
|
+
- if err := xml.Unmarshal(resp.Body, &comps); err != nil {
|
|
292
|
+
+ var feed componentsFeed
|
|
293
|
+
+ if err := xml.Unmarshal(resp.Body, &feed); err != nil {
|
|
294
|
+
return nil, fmt.Errorf("parsing components: %w", err)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
- result := make([]InstalledComponent, len(comps.Components))
|
|
298
|
+
- for i, c := range comps.Components {
|
|
299
|
+
- result[i] = InstalledComponent{
|
|
300
|
+
- Name: c.Name,
|
|
301
|
+
- Release: c.Release,
|
|
302
|
+
- SupportPack: c.SupportPack,
|
|
303
|
+
- Description: c.Description,
|
|
304
|
+
+ result := make([]InstalledComponent, 0, len(feed.Entries))
|
|
305
|
+
+ for i, entry := range feed.Entries {
|
|
306
|
+
+ fields := strings.SplitN(entry.Title, ";", 4)
|
|
307
|
+
+ name := strings.TrimSpace(entry.ID)
|
|
308
|
+
+ if name == "" || len(fields) != 4 {
|
|
309
|
+
+ return nil, fmt.Errorf("parsing components: malformed feed entry %d", i+1)
|
|
310
|
+
}
|
|
311
|
+
+ result = append(result, InstalledComponent{
|
|
312
|
+
+ Name: name,
|
|
313
|
+
+ Release: strings.TrimSpace(fields[0]),
|
|
314
|
+
+ SupportPack: strings.TrimSpace(fields[2]),
|
|
315
|
+
+ Description: strings.TrimSpace(fields[3]),
|
|
316
|
+
+ })
|
|
317
|
+
}
|
|
318
|
+
-
|
|
319
|
+
return result, nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
diff --git a/pkg/adt/client_components_test.go b/pkg/adt/client_components_test.go
|
|
323
|
+
new file mode 100644
|
|
324
|
+
index 0000000..1ed3c2b
|
|
325
|
+
--- /dev/null
|
|
326
|
+
+++ b/pkg/adt/client_components_test.go
|
|
327
|
+
@@ -0,0 +1,40 @@
|
|
328
|
+
+package adt
|
|
329
|
+
+
|
|
330
|
+
+import (
|
|
331
|
+
+ "context"
|
|
332
|
+
+ "io"
|
|
333
|
+
+ "net/http"
|
|
334
|
+
+ "net/http/httptest"
|
|
335
|
+
+ "testing"
|
|
336
|
+
+)
|
|
337
|
+
+
|
|
338
|
+
+func TestGetInstalledComponentsUsesADTFeed(t *testing.T) {
|
|
339
|
+
+ const accept = "application/atom+xml;type=feed"
|
|
340
|
+
+ const response = `<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"><atom:entry><atom:id>SAP_BASIS</atom:id><atom:title>758;SAPK-75802INSAPBASIS;0002;SAP Basis > 2022; patch</atom:title></atom:entry></atom:feed>`
|
|
341
|
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
342
|
+
+ if r.URL.Path != "/sap/bc/adt/system/components" {
|
|
343
|
+
+ t.Errorf("unexpected request path: %s", r.URL.Path)
|
|
344
|
+
+ w.WriteHeader(http.StatusNotFound)
|
|
345
|
+
+ return
|
|
346
|
+
+ }
|
|
347
|
+
+ if got := r.Header.Get("Accept"); got != accept {
|
|
348
|
+
+ t.Errorf("Accept = %q, want %q", got, accept)
|
|
349
|
+
+ w.WriteHeader(http.StatusNotAcceptable)
|
|
350
|
+
+ return
|
|
351
|
+
+ }
|
|
352
|
+
+ w.Header().Set("Content-Type", accept)
|
|
353
|
+
+ _, _ = io.WriteString(w, response)
|
|
354
|
+
+ }))
|
|
355
|
+
+ defer server.Close()
|
|
356
|
+
+
|
|
357
|
+
+ components, err := NewClient(server.URL, "user", "pass").GetInstalledComponents(context.Background())
|
|
358
|
+
+ if err != nil {
|
|
359
|
+
+ t.Fatalf("GetInstalledComponents returned an error: %v", err)
|
|
360
|
+
+ }
|
|
361
|
+
+ if len(components) != 1 {
|
|
362
|
+
+ t.Fatalf("got %d components, want 1", len(components))
|
|
363
|
+
+ }
|
|
364
|
+
+ if got := components[0]; got.Name != "SAP_BASIS" || got.Release != "758" || got.SupportPack != "0002" || got.Description != "SAP Basis > 2022; patch" {
|
|
365
|
+
+ t.Errorf("got component %#v", got)
|
|
366
|
+
+ }
|
|
367
|
+
+}
|
|
312
368
|
diff --git a/pkg/adt/http.go b/pkg/adt/http.go
|
|
369
|
+
index 0b4d9f1..3473536 100755
|
|
313
370
|
--- a/pkg/adt/http.go
|
|
314
371
|
+++ b/pkg/adt/http.go
|
|
315
372
|
@@ -499,6 +499,7 @@ func (t *Transport) fetchCSRFTokenFor(ctx context.Context, stateful bool) error
|
|
@@ -375,9 +432,10 @@ diff --git a/pkg/adt/http.go b/pkg/adt/http.go
|
|
|
375
432
|
}
|
|
376
433
|
diff --git a/pkg/adt/http_csrf_discovery_test.go b/pkg/adt/http_csrf_discovery_test.go
|
|
377
434
|
new file mode 100644
|
|
435
|
+
index 0000000..80741da
|
|
378
436
|
--- /dev/null
|
|
379
437
|
+++ b/pkg/adt/http_csrf_discovery_test.go
|
|
380
|
-
@@ -0,0 +1,
|
|
438
|
+
@@ -0,0 +1,56 @@
|
|
381
439
|
+package adt
|
|
382
440
|
+
|
|
383
441
|
+import (
|
|
@@ -433,3 +491,4 @@ new file mode 100644
|
|
|
433
491
|
+ t.Fatalf("expected both endpoint failures, got %v", err)
|
|
434
492
|
+ }
|
|
435
493
|
+}
|
|
494
|
+
+
|
package/scripts/ensure-go.mjs
CHANGED
|
@@ -10,7 +10,8 @@ const exec = promisify(execFile);
|
|
|
10
10
|
export const GO_VERSION = brandedEnvValue(process.env, 'GO_VERSION') || 'go1.27.1';
|
|
11
11
|
const GO_RELEASES = {
|
|
12
12
|
'linux:x64': { archive: `${GO_VERSION}.linux-amd64.tar.gz`, sha256: '63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445' },
|
|
13
|
-
'linux:arm64': { archive: `${GO_VERSION}.linux-arm64.tar.gz`, sha256: '3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec' }
|
|
13
|
+
'linux:arm64': { archive: `${GO_VERSION}.linux-arm64.tar.gz`, sha256: '3450b45a3f9ee8568792736a5c5e70a1f2e9b36c35a8f74958c03e51d7d92bec' },
|
|
14
|
+
'win32:x64': { archive: `${GO_VERSION}.windows-amd64.zip`, sha256: 'a3911b5e0e1b1053f25ed0675f4c1c6aad1e2bfcf253df2b9be4caabd2edd95d' }
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
function localGoPath() {
|
|
@@ -28,7 +29,7 @@ function candidateGo() {
|
|
|
28
29
|
|
|
29
30
|
export async function installGo() {
|
|
30
31
|
const release = GO_RELEASES[`${process.platform}:${process.arch}`];
|
|
31
|
-
if (!release) throw new Error(`Go is missing and automatic installation is supported only for Linux x64/arm64. Install ${GO_VERSION} manually and ensure go is on PATH.`);
|
|
32
|
+
if (!release) throw new Error(`Go is missing and automatic installation is supported only for Linux x64/arm64 and Windows x64. Install ${GO_VERSION} manually and ensure go is on PATH.`);
|
|
32
33
|
const response = await fetch(`https://go.dev/dl/${release.archive}`);
|
|
33
34
|
if (!response.ok) throw new Error(`Go download failed with HTTP ${response.status}; install ${GO_VERSION} manually and ensure go is on PATH.`);
|
|
34
35
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
@@ -39,13 +40,20 @@ export async function installGo() {
|
|
|
39
40
|
await mkdir(root, { recursive: true, mode: 0o755 });
|
|
40
41
|
const archive = join(tmpdir(), `${release.archive}.${process.pid}`);
|
|
41
42
|
await writeFile(archive, bytes, { mode: 0o600 });
|
|
42
|
-
|
|
43
|
+
// bsdtar on Windows extracts zip archives the same way as gzip tarballs.
|
|
44
|
+
// Prefer the OS-supplied C:\Windows\System32\tar.exe: a GNU tar earlier on
|
|
45
|
+
// PATH (e.g. Git for Windows) misreads the drive-letter -C path as a
|
|
46
|
+
// remote host unless --force-local is passed.
|
|
47
|
+
const tarCommand = process.platform === 'win32' ? join(process.env.SystemRoot || 'C:\\WINDOWS', 'System32', 'tar.exe') : 'tar';
|
|
48
|
+
await exec(tarCommand, ['-xzf', archive, '-C', root, '--strip-components=1'], { stdio: 'inherit' });
|
|
43
49
|
await rm(archive, { force: true });
|
|
44
|
-
const goPath = join(root, 'bin', 'go');
|
|
50
|
+
const goPath = join(root, 'bin', process.platform === 'win32' ? 'go.exe' : 'go');
|
|
45
51
|
await chmod(goPath, 0o755);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
if (process.platform !== 'win32') {
|
|
53
|
+
const userBin = join(homedir(), '.local', 'bin');
|
|
54
|
+
await mkdir(userBin, { recursive: true, mode: 0o755 });
|
|
55
|
+
try { await symlink(goPath, join(userBin, 'go')); } catch {}
|
|
56
|
+
}
|
|
49
57
|
return goPath;
|
|
50
58
|
}
|
|
51
59
|
|
|
@@ -61,7 +61,11 @@ async function resolveGlobalBinaryPath() {
|
|
|
61
61
|
async function verify() {
|
|
62
62
|
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
63
63
|
const prefix = await resolveGlobalBinaryPath();
|
|
64
|
-
|
|
64
|
+
// npm's global layout differs per platform: Unix nests modules under
|
|
65
|
+
// <prefix>/lib/node_modules, Windows puts them directly under <prefix>.
|
|
66
|
+
const expected = process.platform === 'win32'
|
|
67
|
+
? resolvePath(prefix, 'node_modules', pkg.name)
|
|
68
|
+
: resolvePath(prefix, 'lib', 'node_modules', pkg.name);
|
|
65
69
|
const serverCommand = process.platform === 'win32'
|
|
66
70
|
? join(prefix, 'sap-ai-dev.cmd')
|
|
67
71
|
: join(prefix, 'bin', 'sap-ai-dev');
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -20,9 +20,16 @@ const runtimeEnv = withBrandedEnvironment(process.env);
|
|
|
20
20
|
|
|
21
21
|
async function announce(message, tone = 'info') {
|
|
22
22
|
const content = message.replace(/^sap-ai-dev-toolkit:\s*/, '');
|
|
23
|
+
// Windows cannot open CONOUT$ as a writable console stream from a
|
|
24
|
+
// piped npm script (libuv normalizes the device name, and the resulting
|
|
25
|
+
// handle drops writes), so output goes through stderr unconditionally.
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
console.error(formatStatus(content, tone, process.stderr));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
23
30
|
let terminal;
|
|
24
31
|
try {
|
|
25
|
-
terminal = await open(
|
|
32
|
+
terminal = await open('/dev/tty', 'w');
|
|
26
33
|
await terminal.write(`${formatStatus(content, tone, true)}\n`);
|
|
27
34
|
} catch {
|
|
28
35
|
console.error(formatStatus(content, tone, process.stderr));
|
|
@@ -32,12 +39,17 @@ async function announce(message, tone = 'info') {
|
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function openControllingTerminal() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
if (process.platform === 'win32') {
|
|
43
|
+
// fs cannot open CONIN$/CONOUT$ (libuv path normalization rejects the
|
|
44
|
+
// device names), so there is no controlling-terminal recovery on
|
|
45
|
+
// Windows: npm-script installs run non-interactively and the printed
|
|
46
|
+
// guidance points at `sap-ai-dev --setup` in a real terminal.
|
|
47
|
+
throw new Error('controlling terminal recovery is unavailable on Windows');
|
|
48
|
+
}
|
|
49
|
+
const inputFd = openSync('/dev/tty', 'r');
|
|
38
50
|
let outputFd;
|
|
39
51
|
try {
|
|
40
|
-
outputFd = openSync(
|
|
52
|
+
outputFd = openSync('/dev/tty', 'w');
|
|
41
53
|
return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
|
|
42
54
|
} catch (error) {
|
|
43
55
|
closeSync(inputFd);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Cross-platform runner for the live S4H suite: Windows cmd cannot parse the
|
|
2
|
+
// Unix `VAR=1 command` prefix used in npm scripts, so the flag is set here
|
|
3
|
+
// and the test runner is spawned directly.
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const result = spawnSync(process.execPath, ['--test', '--test-force-exit', 'test/live-s4h.test.mjs'], {
|
|
7
|
+
stdio: 'inherit',
|
|
8
|
+
env: { ...process.env, SAP_AI_DEV_LIVE_S4H: '1' }
|
|
9
|
+
});
|
|
10
|
+
process.exitCode = result.status ?? 1;
|
package/src/harnesses.mjs
CHANGED
|
@@ -5,7 +5,9 @@ export const HARNESS_ENV_VAR = 'SAP_AI_DEV_TOOLKIT_HARNESSES';
|
|
|
5
5
|
export const DEFAULT_HARNESS_IDS = ['github-copilot', 'claude-code'];
|
|
6
6
|
export const NON_INTERACTIVE_HARNESS_IDS = ['github-copilot'];
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// \r? keeps frontmatter parsing working on Windows checkouts where git's
|
|
9
|
+
// autocrlf converted the bundled agents and skills to CRLF line endings.
|
|
10
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
9
11
|
|
|
10
12
|
function splitFrontmatter(source) {
|
|
11
13
|
const match = FRONTMATTER_PATTERN.exec(String(source));
|
package/src/launcher.mjs
CHANGED
|
@@ -105,7 +105,10 @@ async function runDoctor(destinations) {
|
|
|
105
105
|
const upstreamCount = tools.filter(tool => !localSegments.has(isUpstreamTool(tool))).length;
|
|
106
106
|
checks.push(doctorRow(destination.name, 'MCP tools/list', upstreamCount ? 'passed' : 'failed', `${upstreamCount} VSP tools and ${tools.length} total MCP tools returned; chat-picker binding is host-managed`));
|
|
107
107
|
const systemInfo = tools.find(tool => tool.name === 'get_system_info' || tool.name === `${toolPrefix}get_system_info`);
|
|
108
|
-
|
|
108
|
+
const needsAdtUrl = destination.probe?.status === 'needs-adt-url';
|
|
109
|
+
if (needsAdtUrl) {
|
|
110
|
+
checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'No ADT URL is configured for this SAP GUI system; run sap-ai-dev --setup to add one.'));
|
|
111
|
+
} else if (!systemInfo) {
|
|
109
112
|
checks.push(doctorRow(destination.name, 'SAP system check', 'skipped', 'GetSystemInfo is not exposed by this VSP mode.'));
|
|
110
113
|
} else {
|
|
111
114
|
const inspected = await proxy.handle({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: systemInfo.name, arguments: {} } });
|
|
@@ -316,10 +319,18 @@ async function main() {
|
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
318
321
|
const binary = await binaryOrError();
|
|
319
|
-
|
|
322
|
+
// Windows cannot exec a .js/.mjs file directly; route JavaScript entries
|
|
323
|
+
// (test fixtures) through the current Node binary like MCPProxy does.
|
|
324
|
+
const spawnTarget = /\.(?:mjs|cjs|js)$/i.test(binary)
|
|
325
|
+
? { command: process.execPath, prefixArgs: [binary] }
|
|
326
|
+
: { command: binary, prefixArgs: [] };
|
|
327
|
+
const child = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...process.argv.slice(2)], { env: runtimeEnv, stdio: 'inherit' });
|
|
320
328
|
process.once('SIGINT', () => child.kill('SIGINT'));
|
|
321
329
|
process.once('SIGTERM', () => child.kill('SIGTERM'));
|
|
322
|
-
const [code, signal] = await new Promise(resolve
|
|
330
|
+
const [code, signal] = await new Promise((resolve, reject) => {
|
|
331
|
+
child.once('error', reject);
|
|
332
|
+
child.once('exit', (exitCode, exitSignal) => resolve([exitCode, exitSignal]));
|
|
333
|
+
});
|
|
323
334
|
if (signal) process.kill(process.pid, signal);
|
|
324
335
|
process.exitCode = code ?? 1;
|
|
325
336
|
return;
|
package/src/local-sap-gui.mjs
CHANGED
|
@@ -300,6 +300,10 @@ function registryPathCandidates(value) {
|
|
|
300
300
|
|
|
301
301
|
async function windowsRegistrySapGuiCandidates(env = process.env) {
|
|
302
302
|
if (platform() !== 'win32') return [];
|
|
303
|
+
// Escape hatch for locked-down machines and for tests that must not see
|
|
304
|
+
// the real registry's landscape paths.
|
|
305
|
+
const disabled = String(env.SAP_AI_DEV_TOOLKIT_DISABLE_SAP_GUI_REGISTRY || '').toLowerCase() === 'true';
|
|
306
|
+
if (disabled) return [];
|
|
303
307
|
const candidates = [];
|
|
304
308
|
for (const root of SAP_GUI_REGISTRY_ROOTS) {
|
|
305
309
|
try {
|
package/src/mcp-proxy.mjs
CHANGED
|
@@ -198,6 +198,14 @@ export function childArguments(destination, env = process.env) {
|
|
|
198
198
|
return args;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Windows cannot exec a .js/.mjs file directly (no shebang support), so test
|
|
202
|
+
// fixtures written in JavaScript are launched through the current Node binary.
|
|
203
|
+
// The production VSP binary is a native executable and is unaffected.
|
|
204
|
+
function spawnCommand(binary) {
|
|
205
|
+
if (/\.(?:mjs|cjs|js)$/i.test(binary)) return [process.execPath, binary];
|
|
206
|
+
return [binary];
|
|
207
|
+
}
|
|
208
|
+
|
|
201
209
|
class Child {
|
|
202
210
|
constructor(binary, destination, options) {
|
|
203
211
|
this.destination = destination;
|
|
@@ -208,7 +216,8 @@ class Child {
|
|
|
208
216
|
this.exited = false;
|
|
209
217
|
this.closing = false;
|
|
210
218
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 0;
|
|
211
|
-
|
|
219
|
+
const [command, ...commandPrefixArgs] = spawnCommand(binary);
|
|
220
|
+
this.process = (options.spawn || nodeSpawn)(command, [...commandPrefixArgs, ...(options.args || childArguments(destination, options.env))], {
|
|
212
221
|
env: childEnvironment(destination, options.env),
|
|
213
222
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
214
223
|
});
|
|
@@ -346,8 +355,22 @@ class Child {
|
|
|
346
355
|
if (this.exited) return;
|
|
347
356
|
this.closing = true;
|
|
348
357
|
this.notify('notifications/cancelled', { reason: 'proxy shutdown' });
|
|
349
|
-
|
|
358
|
+
// Give well-behaved MCP children a graceful exit first: stdin EOF lets
|
|
359
|
+
// them finish and log their own shutdown. Windows cannot trap SIGTERM
|
|
360
|
+
// (Node maps it to TerminateProcess), so EOF is the only graceful path —
|
|
361
|
+
// wait briefly for it before escalating.
|
|
362
|
+
this.process.stdin?.end();
|
|
350
363
|
let timeout;
|
|
364
|
+
try {
|
|
365
|
+
await Promise.race([
|
|
366
|
+
once(this.process, 'exit').catch(() => {}),
|
|
367
|
+
new Promise(resolve => { timeout = setTimeout(resolve, 750); })
|
|
368
|
+
]);
|
|
369
|
+
} finally {
|
|
370
|
+
clearTimeout(timeout);
|
|
371
|
+
}
|
|
372
|
+
if (this.exited) return;
|
|
373
|
+
this.process.kill('SIGTERM');
|
|
351
374
|
try {
|
|
352
375
|
await Promise.race([
|
|
353
376
|
once(this.process, 'exit').catch(() => {}),
|
|
@@ -21,37 +21,9 @@ function validateTarget(value) {
|
|
|
21
21
|
return url;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function authChallengeValues(headers) {
|
|
25
|
-
const value = headers?.get?.('www-authenticate');
|
|
26
|
-
if (!value) return [];
|
|
27
|
-
const parts = [];
|
|
28
|
-
let start = 0;
|
|
29
|
-
let quoted = false;
|
|
30
|
-
let escaped = false;
|
|
31
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
32
|
-
const char = value[index];
|
|
33
|
-
if (escaped) {
|
|
34
|
-
escaped = false;
|
|
35
|
-
} else if (char === '\\' && quoted) {
|
|
36
|
-
escaped = true;
|
|
37
|
-
} else if (char === '"') {
|
|
38
|
-
quoted = !quoted;
|
|
39
|
-
} else if (char === ',' && !quoted) {
|
|
40
|
-
parts.push(value.slice(start, index).trim());
|
|
41
|
-
start = index + 1;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
parts.push(value.slice(start).trim());
|
|
45
|
-
return parts.filter(Boolean);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function hasAuthChallenge(headers, scheme) {
|
|
49
|
-
const pattern = new RegExp(`^${scheme}(?:\\s|$)`, 'i');
|
|
50
|
-
return authChallengeValues(headers).some(part => pattern.test(part));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
24
|
function hasNegotiateChallenge(headers) {
|
|
54
|
-
|
|
25
|
+
const value = headers.get?.('www-authenticate') || '';
|
|
26
|
+
return /(?:^|,)\s*Negotiate\b/i.test(value);
|
|
55
27
|
}
|
|
56
28
|
|
|
57
29
|
function headerObject(headers) {
|
|
@@ -170,8 +142,8 @@ async function acquireNegotiateToken({ targetUrl, challenge, env, execFileImpl }
|
|
|
170
142
|
}
|
|
171
143
|
|
|
172
144
|
function basicFallbackCredentials(env) {
|
|
173
|
-
const mode = String(env.SAP_AUTH_FALLBACK_MODE || env.SAP_AI_DEV_TOOLKIT_AUTH_FALLBACK_MODE || '').
|
|
174
|
-
const enabled = mode === 'basic' ||
|
|
145
|
+
const mode = String(env.SAP_AUTH_FALLBACK_MODE || env.SAP_AI_DEV_TOOLKIT_AUTH_FALLBACK_MODE || '').toLowerCase();
|
|
146
|
+
const enabled = mode === 'basic' || env.SAP_AI_DEV_TOOLKIT_SSO_BASIC_FALLBACK === 'true';
|
|
175
147
|
const user = String(env.SAP_USER || env.SAP_USERNAME || '').trim();
|
|
176
148
|
const password = String(env.SAP_PASSWORD || env.SAP_PASS || '');
|
|
177
149
|
if (!enabled || !user || !password) return null;
|
|
@@ -179,7 +151,8 @@ function basicFallbackCredentials(env) {
|
|
|
179
151
|
}
|
|
180
152
|
|
|
181
153
|
function hasBasicChallenge(headers) {
|
|
182
|
-
|
|
154
|
+
const value = headers.get?.('www-authenticate') || '';
|
|
155
|
+
return /(?:^|,)\s*Basic\b/i.test(value);
|
|
183
156
|
}
|
|
184
157
|
|
|
185
158
|
async function retryWithBasicFallback({ req, target, headers, body, fetchImpl, env }) {
|
|
@@ -243,9 +216,7 @@ export async function createWindowsSsoAdtProxy({ destinationUrl, env = process.e
|
|
|
243
216
|
if (cookies) retryHeaders.Cookie = cookies;
|
|
244
217
|
response = await forward(req, target, retryHeaders, body, fetchImpl);
|
|
245
218
|
} catch (error) {
|
|
246
|
-
response =
|
|
247
|
-
? await retryWithBasicFallback({ req, target, headers: { ...headers, ...(cookies ? { Cookie: cookies } : {}) }, body, fetchImpl, env })
|
|
248
|
-
: null;
|
|
219
|
+
response = await retryWithBasicFallback({ req, target, headers: { ...headers, ...(cookies ? { Cookie: cookies } : {}) }, body, fetchImpl, env });
|
|
249
220
|
if (!response) throw error;
|
|
250
221
|
}
|
|
251
222
|
} else if (response.status === 401 && hasBasicChallenge(response.headers) && basicFallbackCredentials(env)) {
|