Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ GROK_AFFINITY_MAX_ENTRIES=100000

GROK_API_KEYS=key1,key2,key3

# 可选的管理员密钥。设置后启用远程凭证管理接口,并允许在凭证目录为空时启动服务。
# 管理接口仅接受此密钥,不接受上面的普通 API Key。请只通过 HTTPS 暴露管理接口。
GROK_ADMIN_KEY=admin


# 每个账号对应一个 OAuth JSON 凭证文件,并直接放入此目录,支持直接导入CPA格式的凭证
# 文件支持热加载,刷新后的令牌会写回。
Expand Down
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-663399.svg)](LICENSE)
[![Container](https://img.shields.io/badge/GHCR-grokcli2api--go-2496ED?logo=docker&logoColor=white)](https://github.com/Futureppo/grokcli2api-go/pkgs/container/grokcli2api-go)

[快速开始](#快速开始) · [API 兼容性](#api-兼容性) · [配置说明](#配置说明) · [接口一览](#接口一览) · [参与贡献](#开发与贡献)
[一键部署](#一键部署linux) · [快速开始](#快速开始) · [API 兼容性](#api-兼容性) · [配置说明](#配置说明) · [接口一览](#接口一览) · [参与贡献](#开发与贡献)

**简体中文** · [English](README_EN.md)

Expand Down Expand Up @@ -70,6 +70,30 @@ flowchart LR

## 快速开始

### 一键部署(Linux)

服务器已安装 Docker 与 Docker Compose v2 时,可直接运行:

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/Futureppo/grokcli2api-go/main/scripts/deploy.sh)
```

脚本会检查 Docker 环境、下载 Compose 配置、创建受保护的 `.env` 和 `auths/` 目录、生成随机本地 API Key、导入你指定的 OAuth JSON 凭证,并启动及验证服务。交互执行时按提示输入凭证文件路径即可;已有安装会保留 `.env` 与凭证,可用同一条命令完成镜像更新。

无人值守部署可预先传入参数:

```bash
AUTH_FILE=/root/account.json \
GROK_API_KEYS='sk-change-this-to-a-strong-random-key' \
INSTALL_DIR=/opt/grokcli2api-go \
bash <(curl -fsSL https://raw.githubusercontent.com/Futureppo/grokcli2api-go/main/scripts/deploy.sh)
```

可选变量包括 `GROK2API_PORT`(默认 `8088`)、`INSTALL_DIR`(默认 `~/grokcli2api-go`)、`AUTH_FILE` 与 `GROK_API_KEYS`。未提供凭证时,脚本只初始化安全配置而不会启动一个无法工作的服务。

> [!TIP]
> 一键脚本解决的是服务部署,不会替你获取上游凭证。OAuth JSON 属于敏感信息,请只从可信来源导出,并在服务器上以最小权限保存。正式对公网开放前还应配置 HTTPS、反向代理、访问控制和限流。

### 1. 准备项目

运行前需要:
Expand Down Expand Up @@ -241,13 +265,33 @@ X-Grok-Session-ID: conversation-123
| `GROK2API_LOG_LEVEL` | `INFO` | 日志等级:`DEBUG`、`INFO`、`WARN` 或 `ERROR` |
| `GROK_API_KEYS` | 空 | 逗号分隔的本地访问密钥,可为不同客户端分配独立 Key |
| `GROK_API_KEY` | 空 | 单个本地访问密钥的兼容别名 |
| `GROK_ADMIN_KEY` | 空 | 独立的管理员密钥;设置后启用远程凭证管理并允许空凭证池启动 |

启用本地访问保护后,受保护接口接受以下任一种请求头:

- `Authorization: Bearer <key>`
- `x-api-key: <key>`
- `api-key: <key>`

管理员密钥与普通 API Key 相互独立。管理接口接受 `Authorization: Bearer <admin-key>` 或 `X-Admin-Key: <admin-key>`,且只应通过 HTTPS 和受限网络暴露。上传凭证可以直接发送 JSON:

```bash
curl http://localhost:8088/v1/admin/credentials \
-H "Authorization: Bearer $GROK_ADMIN_KEY" \
-H "Content-Type: application/json" \
--data-binary @auth.json
```

也可以使用文件表单:

```bash
curl http://localhost:8088/v1/admin/credentials \
-H "X-Admin-Key: $GROK_ADMIN_KEY" \
-F "file=@auth.json;type=application/json"
```

服务端根据凭证中的稳定账号标识生成脱敏 ID;重复上传同一账号会原子覆盖原凭证。上传后会立即尝试发现模型,临时探测失败不会删除已经保存的凭证。

### 凭证池与调度

| 环境变量 | 未设置时的默认值 | 说明 |
Expand Down Expand Up @@ -302,6 +346,16 @@ go run ./cmd/grok2api -version

“可选”表示仅在配置了本地 API Key 时需要鉴权。

### 管理员凭证接口

仅在设置 `GROK_ADMIN_KEY` 后启用,普通 API Key 无法访问。列表响应不会包含账号标识、文件路径、客户端 ID 或 Token。

| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/v1/admin/credentials` | 列出脱敏的凭证状态和模型目录 |
| `POST` | `/v1/admin/credentials` | 上传或覆盖 JSON 凭证,支持 JSON 请求体和 multipart `file` 字段 |
| `DELETE` | `/v1/admin/credentials/{id}` | 删除凭证并立即从调度池移除 |

### Grok 只读透传接口

| 方法 | 路径 |
Expand All @@ -327,6 +381,7 @@ go run ./cmd/grok2api -version

- 切勿提交或公开 OAuth Token、API Key、认证文件及未脱敏日志。
- 对外提供服务前,务必配置 `GROK_API_KEYS`,并在反向代理层启用 HTTPS、访问控制和限流。
- 启用 `GROK_ADMIN_KEY` 时应使用独立的强随机密钥,并额外限制管理接口的来源地址和请求频率。
- 为凭证目录设置最小必要文件权限,并限制可访问该目录的系统用户。
- 除非处于受控调试环境,否则不要启用 `GROK_TLS_INSECURE_SKIP_VERIFY`。
- 不要把会话 ID、用户邮箱或其他敏感数据直接用作亲和标识。
Expand Down
57 changes: 56 additions & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A lightweight, deployable Go compatibility layer with streaming and multi-accoun
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-663399.svg)](LICENSE)
[![Container](https://img.shields.io/badge/GHCR-grokcli2api--go-2496ED?logo=docker&logoColor=white)](https://github.com/Futureppo/grokcli2api-go/pkgs/container/grokcli2api-go)

[Quick Start](#quick-start) · [API Compatibility](#api-compatibility) · [Configuration](#configuration) · [Endpoints](#endpoints) · [Contributing](#development-and-contributing)
[One-command deployment](#one-command-deployment-linux) · [Quick Start](#quick-start) · [API Compatibility](#api-compatibility) · [Configuration](#configuration) · [Endpoints](#endpoints) · [Contributing](#development-and-contributing)

[简体中文](README.md) · **English**

Expand Down Expand Up @@ -70,6 +70,30 @@ The compatibility layer preserves commonly used request fields, response structu

## Quick Start

### One-command deployment (Linux)

If Docker and Docker Compose v2 are already installed on the server, run:

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/Futureppo/grokcli2api-go/main/scripts/deploy.sh)
```

The script checks Docker, downloads the Compose configuration, creates a protected `.env` file and `auths/` directory, generates a random local API key, imports the OAuth JSON credential you select, and starts and verifies the service. Interactive runs prompt for the credential path. Existing installations retain their `.env` and credentials, so the same command also updates the container image.

For unattended deployment, provide the inputs as environment variables:

```bash
AUTH_FILE=/root/account.json \
GROK_API_KEYS='sk-change-this-to-a-strong-random-key' \
INSTALL_DIR=/opt/grokcli2api-go \
bash <(curl -fsSL https://raw.githubusercontent.com/Futureppo/grokcli2api-go/main/scripts/deploy.sh)
```

Optional variables include `GROK2API_PORT` (default `8088`), `INSTALL_DIR` (default `~/grokcli2api-go`), `AUTH_FILE`, and `GROK_API_KEYS`. Without a credential, the script initializes the protected configuration but does not start a service that cannot handle requests.

> [!TIP]
> The deployment script does not obtain upstream credentials for you. OAuth JSON is sensitive: export it only from a trusted source and store it with least-privilege permissions. Configure HTTPS, a reverse proxy, access controls, and rate limiting before exposing the service publicly.

### 1. Prepare the Project

You will need:
Expand Down Expand Up @@ -241,13 +265,33 @@ The service loads environment variables that are not already set from a `.env` f
| `GROK2API_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARN`, or `ERROR` |
| `GROK_API_KEYS` | empty | Comma-separated local access keys; separate keys may be assigned to different clients |
| `GROK_API_KEY` | empty | Backward-compatible alias for one local access key |
| `GROK_ADMIN_KEY` | empty | Independent administrator key; enables remote credential management and empty-pool startup |

When local access protection is enabled, protected endpoints accept any of these headers:

- `Authorization: Bearer <key>`
- `x-api-key: <key>`
- `api-key: <key>`

The administrator key is independent from normal API keys. Management endpoints accept `Authorization: Bearer <admin-key>` or `X-Admin-Key: <admin-key>` and should only be exposed through HTTPS on a restricted network. Upload a credential as a JSON request body:

```bash
curl http://localhost:8088/v1/admin/credentials \
-H "Authorization: Bearer $GROK_ADMIN_KEY" \
-H "Content-Type: application/json" \
--data-binary @auth.json
```

Or upload it as a file form:

```bash
curl http://localhost:8088/v1/admin/credentials \
-H "X-Admin-Key: $GROK_ADMIN_KEY" \
-F "file=@auth.json;type=application/json"
```

The server derives a redacted ID from the credential's stable account identity. Uploading the same account again atomically replaces its existing credential. Model discovery runs immediately after upload; a temporary discovery failure does not remove the saved credential.

### Credential Pool and Scheduling

| Environment variable | Default when unset | Description |
Expand Down Expand Up @@ -302,6 +346,16 @@ go run ./cmd/grok2api -version

“Optional” means authentication is required only when a local API key has been configured.

### Administrator Credential APIs

These endpoints are registered only when `GROK_ADMIN_KEY` is set, and normal API keys cannot access them. List responses never expose account subjects, file paths, client IDs, or tokens.

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/admin/credentials` | List redacted credential status and model catalogs |
| `POST` | `/v1/admin/credentials` | Upload or replace a JSON credential using a JSON body or multipart `file` field |
| `DELETE` | `/v1/admin/credentials/{id}` | Delete a credential and immediately remove it from scheduling |

### Read-only Grok Passthrough APIs

| Method | Path |
Expand All @@ -327,6 +381,7 @@ Local `.env` and `auths/` data remain external configuration and credential stor

- Never commit or disclose OAuth tokens, API keys, authentication files, or unsanitized logs.
- Configure `GROK_API_KEYS` before exposing the service to a network, and enable HTTPS, access control, and rate limiting at the reverse proxy.
- When enabling `GROK_ADMIN_KEY`, use a separate strong random key and apply additional source-address and rate restrictions to the management endpoints.
- Apply least-privilege file permissions to the credential directory and restrict which system users can access it.
- Do not enable `GROK_TLS_INSECURE_SKIP_VERIFY` outside a controlled debugging environment.
- Do not use session IDs, email addresses, or other sensitive data directly as affinity identifiers.
Expand Down
24 changes: 20 additions & 4 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import (

var ErrNoAuth = errors.New("no usable credentials in auths directory")

var (
ErrInvalidCredentialJSON = errors.New("invalid credential JSON")
ErrInvalidCredential = errors.New("invalid credential")
)

type Session struct {
Token string `json:"-"`
Surface string `json:"surface"`
Expand Down Expand Up @@ -68,15 +73,22 @@ func loadCredential(path, surface string) (*credential, error) {
if err != nil {
return nil, err
}
return parseCredential(b, path, surface)
}

func parseCredential(b []byte, path, surface string) (*credential, error) {
var raw map[string]any
if err := json.Unmarshal(b, &raw); err != nil {
return nil, fmt.Errorf("invalid credential JSON: %w", err)
return nil, fmt.Errorf("%w: %v", ErrInvalidCredentialJSON, err)
}
if raw == nil {
return nil, fmt.Errorf("%w: credential must be a JSON object", ErrInvalidCredential)
}
node := credentialNode(raw)
access := firstString(node, "access_token", "AccessToken", "key", "session_token", "SessionToken")
refresh := firstString(node, "refresh_token", "RefreshToken")
if access == "" && refresh == "" {
return nil, errors.New("credential has neither access_token nor refresh_token")
return nil, fmt.Errorf("%w: credential has neither access_token nor refresh_token", ErrInvalidCredential)
}
accessClaims := jwtClaims(access)
idClaims := jwtClaims(firstString(node, "id_token", "IDToken"))
Expand All @@ -88,7 +100,7 @@ func loadCredential(path, surface string) (*credential, error) {
subject = claimString(idClaims, "sub")
}
if subject == "" {
return nil, errors.New("credential has no stable subject")
return nil, fmt.Errorf("%w: credential has no stable subject", ErrInvalidCredential)
}
clientID := firstString(node, "client_id", "clientId")
if clientID == "" {
Expand Down Expand Up @@ -236,6 +248,10 @@ func writeCredentialAtomic(path string, raw map[string]any) error {
if err != nil {
return err
}
return writeCredentialAtomicMode(path, raw, info.Mode().Perm())
}

func writeCredentialAtomicMode(path string, raw map[string]any, mode os.FileMode) error {
b, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return err
Expand All @@ -247,7 +263,7 @@ func writeCredentialAtomic(path string, raw map[string]any) error {
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if err := tmp.Chmod(info.Mode().Perm()); err != nil {
if err := tmp.Chmod(mode); err != nil {
tmp.Close()
return err
}
Expand Down
Loading
Loading