Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ CLI2API 是本地网关:不提供账号、额度或官方 API 服务,不做

## 安全

默认只监听 `127.0.0.1:3010`;除 `/health` 和静态前端资源外,所有 API 与控制台数据接口均需要 API Key。不要提交 `.qoder`、Token、Cookie、登录 Blob 或原始抓包;凭证导出是显式敏感操作,请妥善保管导出文件。上游 API 或 CLI 更新可能导致兼容性变化,项目会固定并检查 qodercli 版本。发现安全问题请按 [SECURITY.md](SECURITY.md) 私下报告。
默认只监听 `127.0.0.1:3010`;除 `/health`、静态前端资源和 OpenAI 兼容 `/v1/*` 的 CORS 预检 `OPTIONS` 外,所有 API 与控制台数据接口均需要 API Key。不要提交 `.qoder`、Token、Cookie、登录 Blob 或原始抓包;凭证导出是显式敏感操作,请妥善保管导出文件。上游 API 或 CLI 更新可能导致兼容性变化,项目会固定并检查 qodercli 版本。发现安全问题请按 [SECURITY.md](SECURITY.md) 私下报告。

## 社区

Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ CLI2API is a local gateway: it does not provide accounts, quotas, or an official

## Security

The service binds `127.0.0.1:3010` by default; all APIs and console data endpoints require the API key except `/health` and static frontend assets. Never commit `.qoder`, tokens, cookies, auth blobs, or raw captures; credential export is an explicit sensitive operation — protect exported files. Upstream API or CLI changes may affect compatibility; qodercli is pinned and checked. Please report security issues privately according to [SECURITY.md](SECURITY.md).
The service binds `127.0.0.1:3010` by default; all APIs and console data endpoints require the API key except `/health`, static frontend assets, and CORS preflight `OPTIONS` for the OpenAI-compatible `/v1/*` endpoints. Never commit `.qoder`, tokens, cookies, auth blobs, or raw captures; credential export is an explicit sensitive operation — protect exported files. Upstream API or CLI changes may affect compatibility; qodercli is pinned and checked. Please report security issues privately according to [SECURITY.md](SECURITY.md).

## Community

Expand Down
4 changes: 2 additions & 2 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The API key is generated once and stored in SQLite. There is no environment-vari
| `POST` | `/v1/responses` | OpenAI Responses-compatible API |
| `GET/POST/PATCH/DELETE` | `/api/*` | Console management API |

All console and API routes except `/health` require the API key stored in SQLite.
All console and API routes except `/health` and CORS preflight `OPTIONS` for the OpenAI-compatible `/v1/*` endpoints require the API key stored in SQLite.

## 7. Managed next-version update

Expand Down Expand Up @@ -205,4 +205,4 @@ accepted for staged-update hosts; unknown versions fail closed.
- Published host updater assets cover Linux, macOS, and Windows on `amd64` and `arm64`.
- Run the Windows updater installer as the same signed-in user that runs Docker Desktop; do not install it as `LocalSystem`.
- Keep `deploy/.env` private because Docker Desktop mode stores the updater token there.
- Do not remove `qoder-data` unless you intentionally want to delete SQLite accounts and credentials.
- Do not remove `qoder-data` unless you intentionally want to delete SQLite accounts and credentials.
25 changes: 25 additions & 0 deletions internal/api/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ func TestClassifyCanceledErrorDoesNotBecomeAuth(t *testing.T) {
}
}

func TestCORSHeadersOnUnauthorizedChat(t *testing.T) {
srv := New(config.Config{
Host: "127.0.0.1",
Port: 3010,
ProxyAPIKey: "secret",
QoderHome: t.TempDir(),
})
defer srv.Close()

req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader([]byte("{}")))
req.Header.Set("Origin", "chrome-extension://abc")
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
srv.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("POST without key: got %d want 401 body=%s", rec.Code, rec.Body.String())
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" {
t.Fatalf("allow-origin=%q", got)
}
if got := rec.Header().Get("Access-Control-Expose-Headers"); !strings.Contains(got, "X-Request-Id") {
t.Fatalf("expose-headers=%q", got)
}
}

func TestManagementRoutesRequireAPIKey(t *testing.T) {
srv := New(config.Config{
Host: "127.0.0.1",
Expand Down
4 changes: 2 additions & 2 deletions internal/providers/workbuddy/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ func TestChatRequestFindsStoredReasoningByCanonicalKey(t *testing.T) {
}
}

func TestModelsAcceptsGlobalCLIAgentNamesAndUsesAccountRegion(t *testing.T) {
func TestModelsAcceptsGlobalAgentNamesAndUsesAccountRegion(t *testing.T) {
payload, _ := Credential{AccessToken: "at", UID: "u1", Domain: "codebuddy.cn", ExpiresAt: 4102444800}.Encode()
store := &memStore{items: map[string][]byte{"acc1": payload}, region: "global"}
var origin, requestHost, ideType string
Expand Down Expand Up @@ -602,7 +602,7 @@ func TestModelsAcceptsGlobalCLIAgentNamesAndUsesAccountRegion(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(models) != 1 || models[0].NativeModel != "glm-5.2" {
if len(models) != 2 || models[0].NativeModel != "glm-5.2" || models[1].NativeModel != "web-model" {
t.Fatalf("models=%+v", models)
}
if origin != "https://www.workbuddy.ai" {
Expand Down
Loading