Merge pull request #4039 from router-for-me/plugin

feat(pluginstore): add API URL to ReleaseAsset and update asset download logic
This commit is contained in:
Chén Mù
2026-06-29 09:47:45 +08:00
committed by GitHub
4 changed files with 127 additions and 36 deletions
+4 -11
View File
@@ -82,11 +82,7 @@ func AuthConfigured(auth []AuthConfig, requestURL string, kind string) bool {
case AuthTypeNone:
return false
case AuthTypeBearer, AuthTypeGitHubToken:
envName := item.TokenEnv
if envName == "" && item.Type == AuthTypeGitHubToken {
envName = "GITSTORE_GIT_TOKEN"
}
return strings.TrimSpace(os.Getenv(envName)) != ""
return strings.TrimSpace(os.Getenv(item.TokenEnv)) != ""
case AuthTypeBasic:
return strings.TrimSpace(os.Getenv(item.UsernameEnv)) != "" && strings.TrimSpace(os.Getenv(item.PasswordEnv)) != ""
case AuthTypeHeader:
@@ -162,12 +158,9 @@ func applyPluginStoreAuth(headers http.Header, auth []AuthConfig, requestURL str
}
headers.Set(item.HeaderName, value)
case AuthTypeGitHubToken:
token := strings.TrimSpace(os.Getenv(strings.TrimSpace(item.TokenEnv)))
if token == "" {
token = strings.TrimSpace(os.Getenv("GITSTORE_GIT_TOKEN"))
}
if token == "" {
return fmt.Errorf("plugin store auth missing token-env")
token, errToken := envValueRequired(item.TokenEnv, "token-env")
if errToken != nil {
return errToken
}
headers.Set("Authorization", "Bearer "+token)
default:
+18
View File
@@ -44,6 +44,24 @@ func TestPluginStoreAuthMatchesURLHostAndPathBoundaries(t *testing.T) {
}
}
func TestPluginStoreGitHubTokenUsesExplicitTokenEnv(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
headers := http.Header{}
auth := []AuthConfig{{
Match: "https://api.github.com/repos/author-name/sample-provider/releases/",
ApplyTo: []string{RequestKindArtifact},
Type: AuthTypeGitHubToken,
TokenEnv: "PLUGIN_STORE_TOKEN",
}}
if errAuth := applyPluginStoreAuth(headers, auth, "https://api.github.com/repos/author-name/sample-provider/releases/assets/1", RequestKindArtifact); errAuth != nil {
t.Fatalf("applyPluginStoreAuth() error = %v", errAuth)
}
if gotAuth := headers.Get("Authorization"); gotAuth != "Bearer secret-token" {
t.Fatalf("Authorization = %q, want Bearer secret-token", gotAuth)
}
}
func TestPluginAuthConfiguredCoversInstallRequestKinds(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
+19 -23
View File
@@ -7,7 +7,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/httpfetch"
@@ -33,6 +32,7 @@ type Release struct {
}
type ReleaseAsset struct {
APIURL string `json:"url"`
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
}
@@ -114,10 +114,25 @@ func ReleaseVersion(release Release) (string, error) {
}
func (c Client) DownloadAsset(ctx context.Context, asset ReleaseAsset) ([]byte, error) {
if strings.TrimSpace(asset.BrowserDownloadURL) == "" {
return nil, fmt.Errorf("asset %q missing browser_download_url", asset.Name)
downloadURL := strings.TrimSpace(asset.BrowserDownloadURL)
apiURL := strings.TrimSpace(asset.APIURL)
if downloadURL == "" || c.releaseAssetAPIAuthenticated(apiURL) {
if apiURL != "" {
downloadURL = apiURL
}
}
return c.get(ctx, asset.BrowserDownloadURL, "application/octet-stream", RequestKindArtifact, 0)
if downloadURL == "" {
return nil, fmt.Errorf("asset %q missing download url", asset.Name)
}
return c.get(ctx, downloadURL, "application/octet-stream", RequestKindArtifact, 0)
}
func (c Client) releaseAssetAPIAuthenticated(apiURL string) bool {
apiURL = strings.TrimSpace(apiURL)
if apiURL == "" {
return false
}
return AuthConfigured(c.Auth, apiURL, RequestKindArtifact)
}
func (c Client) get(ctx context.Context, requestURL string, accept string, kind string, maxSize int64) ([]byte, error) {
@@ -133,11 +148,6 @@ func (c Client) get(ctx context.Context, requestURL string, accept string, kind
if errAuth := applyPluginStoreAuth(headers, c.Auth, currentURL, kind); errAuth != nil {
return nil, errAuth
}
if headers.Get("Authorization") == "" {
if token := gitHubAPIToken(currentURL); token != "" {
headers.Set("Authorization", "Bearer "+token)
}
}
resp, errDo := pluginStoreGetNoRedirect(ctx, c.httpClient(), currentURL, headers)
if errDo != nil {
return nil, errDo
@@ -160,20 +170,6 @@ func (c Client) get(ctx context.Context, requestURL string, accept string, kind
}
}
// gitHubAPIToken returns the optional GitHub token for GitHub API requests to
// raise the unauthenticated rate limit, mirroring the management asset updater.
func gitHubAPIToken(requestURL string) string {
parsed, errParse := url.Parse(requestURL)
if errParse != nil || !strings.EqualFold(parsed.Host, "api.github.com") {
return ""
}
gitURL := strings.ToLower(strings.TrimSpace(os.Getenv("GITSTORE_GIT_URL")))
if !strings.Contains(gitURL, "github.com") {
return ""
}
return strings.TrimSpace(os.Getenv("GITSTORE_GIT_TOKEN"))
}
func (c Client) httpClient() HTTPDoer {
if c.HTTPClient != nil {
return c.HTTPClient
+86 -2
View File
@@ -319,8 +319,16 @@ func TestInstallUsesLatestReleaseVersion(t *testing.T) {
"https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/latest": []byte(`{
"tag_name": "v0.2.0",
"assets": [
{"name": "` + archiveName + `", "browser_download_url": "https://downloads.example/` + archiveName + `"},
{"name": "checksums.txt", "browser_download_url": "https://downloads.example/checksums.txt"}
{
"name": "` + archiveName + `",
"url": "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/assets/1",
"browser_download_url": "https://downloads.example/` + archiveName + `"
},
{
"name": "checksums.txt",
"url": "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/assets/2",
"browser_download_url": "https://downloads.example/checksums.txt"
}
]
}`),
"https://downloads.example/" + archiveName: archiveData,
@@ -347,6 +355,82 @@ func TestInstallUsesLatestReleaseVersion(t *testing.T) {
}
}
func TestDownloadAssetFallsBackToReleaseAssetAPIURLWhenBrowserDownloadURLEmpty(t *testing.T) {
apiURL := "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/assets/1"
client := Client{HTTPClient: mapHTTPDoer{
apiURL: []byte("artifact-data"),
}}
data, errDownload := client.DownloadAsset(context.Background(), ReleaseAsset{
Name: "sample-provider_0.2.0_darwin_arm64.zip",
APIURL: apiURL,
})
if errDownload != nil {
t.Fatalf("DownloadAsset() error = %v", errDownload)
}
if string(data) != "artifact-data" {
t.Fatalf("DownloadAsset() = %q, want artifact-data", data)
}
}
func TestDownloadAssetUsesAPIURLWhenAuthMatchesArtifact(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
apiURL := "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/assets/1"
client := Client{
HTTPClient: authCheckingHTTPDoer{
url: apiURL,
wantAuth: "Bearer secret-token",
responseBytes: []byte("artifact-data"),
},
Auth: []AuthConfig{{
Match: "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/",
ApplyTo: []string{RequestKindArtifact},
Type: AuthTypeBearer,
TokenEnv: "PLUGIN_STORE_TOKEN",
}},
}
data, errDownload := client.DownloadAsset(context.Background(), ReleaseAsset{
Name: "sample-provider_0.2.0_darwin_arm64.zip",
APIURL: apiURL,
BrowserDownloadURL: "https://downloads.example/sample-provider.zip",
})
if errDownload != nil {
t.Fatalf("DownloadAsset() error = %v", errDownload)
}
if string(data) != "artifact-data" {
t.Fatalf("DownloadAsset() = %q, want artifact-data", data)
}
}
func TestDownloadAssetUsesBrowserDownloadURLWithUnrelatedAuth(t *testing.T) {
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
browserURL := "https://downloads.example/sample-provider.zip"
client := Client{
HTTPClient: mapHTTPDoer{
browserURL: []byte("artifact-data"),
},
Auth: []AuthConfig{{
Match: "https://registry.example/",
ApplyTo: []string{RequestKindRegistry},
Type: AuthTypeBearer,
TokenEnv: "PLUGIN_STORE_TOKEN",
}},
}
data, errDownload := client.DownloadAsset(context.Background(), ReleaseAsset{
Name: "sample-provider_0.2.0_darwin_arm64.zip",
APIURL: "https://api.github.com/repos/author-name/cliproxy-sample-provider-plugin/releases/assets/1",
BrowserDownloadURL: browserURL,
})
if errDownload != nil {
t.Fatalf("DownloadAsset() error = %v", errDownload)
}
if string(data) != "artifact-data" {
t.Fatalf("DownloadAsset() = %q, want artifact-data", data)
}
}
func TestInstallVersionUsesPinnedReleaseTag(t *testing.T) {
t.Parallel()