diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index 881c6ae6d848..8202aeb91d95 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -32,7 +32,7 @@ const (
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
-const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
+const DefaultCSPPolicy = "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://127.0.0.1:43110 http://127.0.0.1:43111 http://127.0.0.1:43112 http://127.0.0.1:43113 http://127.0.0.1:43114 http://127.0.0.1:43115 http://127.0.0.1:43116 http://127.0.0.1:43117 http://127.0.0.1:43118 http://127.0.0.1:43119 https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
// UMQ(用户消息队列)模式常量
const (
diff --git a/backend/internal/server/middleware/security_headers.go b/backend/internal/server/middleware/security_headers.go
index 203f0e4609ab..3f904cac7035 100644
--- a/backend/internal/server/middleware/security_headers.go
+++ b/backend/internal/server/middleware/security_headers.go
@@ -94,6 +94,17 @@ var requiredCSPDirectiveValues = []struct {
{"style-src", AirwallexDemoStaticDomain},
{"style-src", AirwallexDemoCheckoutDomain},
{"frame-src", AirwallexDemoCheckoutDomain},
+ // @project-doc docs/interfaces/tf_cli_web_import.md#browser_security_headers
+ {"connect-src", "http://127.0.0.1:43110"},
+ {"connect-src", "http://127.0.0.1:43111"},
+ {"connect-src", "http://127.0.0.1:43112"},
+ {"connect-src", "http://127.0.0.1:43113"},
+ {"connect-src", "http://127.0.0.1:43114"},
+ {"connect-src", "http://127.0.0.1:43115"},
+ {"connect-src", "http://127.0.0.1:43116"},
+ {"connect-src", "http://127.0.0.1:43117"},
+ {"connect-src", "http://127.0.0.1:43118"},
+ {"connect-src", "http://127.0.0.1:43119"},
}
// GenerateNonce generates a cryptographically secure random nonce.
diff --git a/backend/internal/server/middleware/security_headers_test.go b/backend/internal/server/middleware/security_headers_test.go
index f337462c71ae..70588a32926f 100644
--- a/backend/internal/server/middleware/security_headers_test.go
+++ b/backend/internal/server/middleware/security_headers_test.go
@@ -2,6 +2,7 @@ package middleware
import (
"encoding/base64"
+ "fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -346,7 +347,19 @@ func TestEnhanceCSPPolicy(t *testing.T) {
assert.Equal(t, 1, countDirectiveValue(enhanced, "worker-src", TencentCaptchaWorkerSource))
})
- t.Run("default_policy_already_carries_tencent_captcha_domains", func(t *testing.T) {
+ t.Run("adds_only_the_tf_cli_loopback_ports", func(t *testing.T) {
+ policy := "default-src 'self'; connect-src 'self'"
+ enhanced := enhanceCSPPolicy(policy)
+
+ for port := 43110; port <= 43119; port++ {
+ origin := fmt.Sprintf("http://127.0.0.1:%d", port)
+ assert.Equal(t, 1, countDirectiveValue(enhanced, "connect-src", origin))
+ }
+ assert.NotContains(t, enhanced, "http://127.0.0.1:*")
+ assert.NotContains(t, enhanced, "http://localhost")
+ })
+
+ t.Run("default_policy_already_carries_required_domains", func(t *testing.T) {
// 默认策略与中间件强制注入表必须同形,否则 config.example.yaml 会误导自建用户
for _, required := range requiredCSPDirectiveValues {
assert.Equal(t, 1, countDirectiveValue(config.DefaultCSPPolicy, required.directive, required.value),
diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml
index 7401f015eb3f..555d346a0acc 100644
--- a/deploy/config.example.yaml
+++ b/deploy/config.example.yaml
@@ -176,7 +176,7 @@ security:
# 默认 CSP 策略(如果静态资源托管在其他域名,请自行覆盖)
# Note: __CSP_NONCE__ will be replaced with 'nonce-xxx' at request time for inline script security
# 注意:__CSP_NONCE__ 会在请求时被替换为 'nonce-xxx',用于内联脚本安全
- policy: "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
+ policy: "default-src 'self'; worker-src 'self' blob:; script-src 'self' __CSP_NONCE__ https://accounts.google.com/gsi/client https://challenges.cloudflare.com https://*.alicdn.com https://static.cloudflareinsights.com https://turing.captcha.qcloud.com https://turing.captcha.gtimg.com https://ca.turing.captcha.qcloud.com https://global.turing.captcha.gtimg.com https://www.tycaptcha.com https://cloudcache.tencentcs.com https://*.stripe.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; style-src 'self' 'unsafe-inline' https://*.captcha.gtimg.com https://accounts.google.com/gsi/style https://fonts.googleapis.com https://*.alicdn.com https://static.airwallex.com https://checkout.airwallex.com https://static-demo.airwallex.com https://checkout-demo.airwallex.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' http://127.0.0.1:43110 http://127.0.0.1:43111 http://127.0.0.1:43112 http://127.0.0.1:43113 http://127.0.0.1:43114 http://127.0.0.1:43115 http://127.0.0.1:43116 http://127.0.0.1:43117 http://127.0.0.1:43118 http://127.0.0.1:43119 https://accounts.google.com/gsi/ https://turing.captcha.qcloud.com https://www.tycaptcha.com https://rce.tencentrio.com https:; frame-src https://accounts.google.com/gsi/ https://challenges.cloudflare.com https://turing.captcha.qcloud.com https://ca.turing.captcha.qcloud.com https://www.tycaptcha.com https://*.stripe.com https://checkout.airwallex.com https://checkout-demo.airwallex.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
proxy_probe:
# Allow skipping TLS verification for proxy probe (debug only)
# 允许代理探测时跳过 TLS 证书验证(仅用于调试)
diff --git a/docs/index.md b/docs/index.md
index 9b1e7667ab00..eec4846083a1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,5 +14,5 @@
- [架构](architecture/index.md):系统组成、进程启动、网关请求链路、账号调度和缓存一致性。读取时机:修改应用装配、服务边界、请求转发、调度快照或故障转移时读取。
- [领域](domains/index.md):身份租户、网关策略、计费额度、支付推广、内容审核和后台任务规则。读取时机:修改核心实体、业务不变量、状态转换、风险策略或结算规则时读取。
-- [接口](interfaces/index.md):HTTP 路由、配置来源、账号能力矩阵、模型目录和六类上游平台适配契约。读取时机:修改外部 API、配置项、协议转换、错误响应或第三方集成时读取。
+- [接口](interfaces/index.md):HTTP 路由、配置来源、浏览器回环导入、账号能力矩阵、模型目录和上游平台适配契约。读取时机:修改外部 API、配置项、协议转换、错误响应或第三方集成时读取。
- [运维](operations/index.md):部署迁移、账号/传输维护、监控告警、数据共享、开发流程和入口安全。读取时机:构建、发布、迁移、排障、数据维护或同步上游时读取。
diff --git a/docs/interfaces/configuration.md b/docs/interfaces/configuration.md
index 4a12936e7239..5279dcb717f1 100644
--- a/docs/interfaces/configuration.md
+++ b/docs/interfaces/configuration.md
@@ -97,7 +97,7 @@ Grok 文本转发有三项数据库运行时设置:`grok_default_text_model`
`gateway.grok` 属于启动时进程配置。`password_auth_enabled` 默认关闭并控制邮箱密码到 SSO/OAuth 的敏感入口;Free OAuth 本地软门禁由 `free_quota_soft_gate_enabled`、`free_quota_token_limit`、`free_quota_soft_gate_percent`、`free_quota_window_hours` 和 `free_quota_stats_cache_seconds` 控制。所有数值在启动时校验,修改后需要重启;统计缓存 miss 或查询故障按 fail-open 处理,但不能放宽 OAuth state 一次性消费、凭据持久化或 URL 信任边界。
-验证码同样属于数据库运行时设置。Turnstile、腾讯天御与阿里云验证码 2.0 三者互斥。腾讯天御启用时必须同时具备正整数 `CaptchaAppId`、`AppSecretKey`、腾讯云 `SecretId` 和 `SecretKey`,并选择 `cn` 中国站或 `intl` 国际站;站点决定前端 SDK、构造函数形式、控制台入口和服务端票据校验 endpoint,`CaptchaAppId` 与云密钥必须来自同一站点,缺失或非法站点按 `cn` 回退。阿里云启用时必须具备 Scene ID、Prefix、AccessKey ID、AccessKey Secret 及 `cn` 或 `sgp` 地域。公开设置只返回各提供方的启用状态、站点和渲染所需的非敏感参数;管理响应只返回 secret 的“已配置”标记,空白更新保留原值,审计仅记录字段发生写入而不记录内容。腾讯与阿里云 Web SDK 所需的脚本、连接、iframe、worker 和样式来源由默认 CSP 与运行时 CSP 补全逻辑共同维护,覆盖自定义旧策略时也不能遗漏,其中阿里云静态资源允许 `https://*.alicdn.com`。Google GIS 同样由默认策略与旧自定义策略增强共同允许:`script-src` 仅加入 `https://accounts.google.com/gsi/client`,`frame-src`/`connect-src` 加入 `https://accounts.google.com/gsi/`,`style-src` 加入 `https://accounts.google.com/gsi/style`。
+验证码同样属于数据库运行时设置。Turnstile、腾讯天御与阿里云验证码 2.0 三者互斥。腾讯天御启用时必须同时具备正整数 `CaptchaAppId`、`AppSecretKey`、腾讯云 `SecretId` 和 `SecretKey`,并选择 `cn` 中国站或 `intl` 国际站;站点决定前端 SDK、构造函数形式、控制台入口和服务端票据校验 endpoint,`CaptchaAppId` 与云密钥必须来自同一站点,缺失或非法站点按 `cn` 回退。阿里云启用时必须具备 Scene ID、Prefix、AccessKey ID、AccessKey Secret 及 `cn` 或 `sgp` 地域。公开设置只返回各提供方的启用状态、站点和渲染所需的非敏感参数;管理响应只返回 secret 的“已配置”标记,空白更新保留原值,审计仅记录字段发生写入而不记录内容。腾讯与阿里云 Web SDK 所需的脚本、连接、iframe、worker 和样式来源由默认 CSP 与运行时 CSP 补全逻辑共同维护,覆盖自定义旧策略时也不能遗漏,其中阿里云静态资源允许 `https://*.alicdn.com`。Google GIS 同样由默认策略与旧自定义策略增强共同允许:`script-src` 仅加入 `https://accounts.google.com/gsi/client`,`frame-src`/`connect-src` 加入 `https://accounts.google.com/gsi/`,`style-src` 加入 `https://accounts.google.com/gsi/style`。tf CLI 网页导入在 `connect-src` 中只允许 `http://127.0.0.1:43110` 到 `43119` 十个精确 Origin;代码默认策略、旧自定义策略增强和 `deploy/config.example.yaml` 必须同步,不能扩大为端口或局域网通配符。完整边界见 [tf CLI 网页导入](tf_cli_web_import.md)。
SMTP 的测试连接与实际发送共用同一建连路径和超时。`smtp_use_tls=true` 先按隐式 TLS 连接;仅当服务端以明文 SMTP 问候响应时改用强制 STARTTLS,服务端不支持升级时直接失败,不能明文发送认证。`smtp_use_tls=false` 保留机会式 STARTTLS,并在服务端不提供扩展时允许现有明文语义。两条路径都在认证成功后忽略非标准 QUIT 响应,因此后台连接测试与实际发信能力保持一致。
diff --git a/docs/interfaces/index.md b/docs/interfaces/index.md
index 0ea0181fb31a..da732dc59351 100644
--- a/docs/interfaces/index.md
+++ b/docs/interfaces/index.md
@@ -10,6 +10,7 @@
- [HTTP 接口边界](http_api.md):公共、用户、管理员、支付和网关路由族及认证/错误边界。读取时机:新增或移动路由、调整中间件、认证方式或公共响应语义时读取。
- [配置边界](configuration.md):默认值、YAML、环境变量、数据库运行时设置和首次初始化之间的边界。读取时机:新增配置项、修改加载优先级、设置页面或部署变量时读取。
+- [tf CLI 网页导入](tf_cli_web_import.md):Keys 页、本机回环协议、会话证明、双重确认和浏览器安全头。读取时机:修改 Keys 导入入口、URL fragment、localhost fetch、CSP 或 tf-cli 协议时读取。
- [上游账号能力矩阵](upstream_account_matrix.md):九个平台、七类账号和全部公开网关协议的正式支持、兼容保留与不支持边界。读取时机:新增平台/账号类型、修改创建导入校验、路由分派或能力承诺时读取。
- [API Key 上游用量查询](upstream_usage.md):API Key 账号的适配器、管理员查询接口、归一化结果和浏览器缓存边界。读取时机:修改 API Key 用量查询、适配器协议、账号用量展示或查询安全策略时读取。
- [Anthropic 上游](anthropic_upstream.md):OAuth、Setup Token、API Key、Bedrock、Vertex,以及 Messages/OpenAI 兼容转换和缓存/限流契约。读取时机:修改 Anthropic 认证、协议、beta、thinking、缓存或错误分类时读取。
diff --git a/docs/interfaces/tf_cli_web_import.md b/docs/interfaces/tf_cli_web_import.md
new file mode 100644
index 000000000000..9a207ac77719
--- /dev/null
+++ b/docs/interfaces/tf_cli_web_import.md
@@ -0,0 +1,53 @@
+# tf CLI 网页导入
+
+本文只记录 TokenRouter 对 [tf-cli 网页导入协议](https://github.com/TokenFlux/tf-cli/blob/main/docs/integrations/web-import.md) 的接入责任。端口、字段、错误码和签名格式以上游文档为准。
+
+TokenRouter 后端不接收导入请求,也不新增授权接口。Keys 页把当前用户已加载到内存的 API Key 直接发给本机 `tf`;Key 不进入 URL、浏览器持久化、日志或分析事件。
+
+
+## 会话片段
+
+`tf login --from-web` 打开 `/keys#tf=1..`。`frontend/src/router/index.ts` 在 Router 创建和登录守卫运行前调用 `initializeTfCliImportSession()`:
+
+- 任何 `#tf=` fragment 都立即通过 `history.replaceState` 删除,畸形值也删除。
+- 只接受 Keys 路径、`43110` 至 `43119` 端口和 16 字节规范无 padding base64url secret。
+- session 只在模块内存保留十分钟;过期、终端取消或请求被终端接受后清零。
+- 同一 SPA 登录跳转可继续使用 session;刷新和外部认证整页跳转会丢失证明,之后只能走未验证兼容路径。
+
+
+## 回环协议
+
+页面只访问 `http://127.0.0.1:43110` 至 `43119`。发现使用 `GET /ping`;有 session 时携带随机 challenge,并用 Web Crypto 校验 tf 返回的 HMAC。导入使用 `POST /import`,只在发现证明有效时发送 `X-TF-Session-Proof`。
+
+页面只称“已验证当前 tf 会话”。proof 不验证网页、API Key、本机程序来源或后续网关结果,也不提供 freshness 或防重放。没有 session、proof 不匹配或 Web Crypto 不可用时仍可导入,但发送前必须显示未验证警告。若已验证后导入 proof 过期或计算失败,本次不发送 Key;页面先降级为未验证状态,再要求用户确认。
+
+回环 fetch 固定使用 CORS、`credentials: omit`、`cache: no-store`、`redirect: error`、`referrerPolicy: no-referrer` 和 `targetAddressSpace: loopback`。发现总预算为 30 秒。浏览器自动完成 `OPTIONS` 预检,前端不手动发送。
+
+## 字段映射
+
+- `host` 使用页面 Origin。tf 校验同源后会恢复 CLI 自己的完整服务地址。
+- `key_name` 取 Keys 页名称。tf 会保存该来源元数据;未显式指定本地名称时,合法值还会成为终端命名候选。
+- 普通 Key 发送其 `group_id` 和 `group_name`。
+- 复合 Key 不发送单一分组元数据;实际能力由 tf 查询模型目录识别。
+
+HTTP `202 Accepted` 只表示终端已确认。tf 之后才校验网关,并在未显式指定名称时让用户选择自动识别、网页名称或自订名称;页面只能提示最终结果以终端为准,不能显示“导入成功”或“Key 已保存”。
+
+
+## 页面交互
+
+Keys 行的更多菜单提供“导入 TF CLI”,不增加第二套入口或 Key 选择状态。弹窗先发现服务,再显示已验证状态或未验证警告;用户点击“发送到 TF CLI”后才发送 Key。POST 等待期间提示用户在终端核对来源并确认。网页确认决定是否发送 Key,终端确认决定 tf 是否继续处理,两者不能互相替代。
+
+
+## 浏览器安全头
+
+默认 CSP、旧自定义 CSP 的运行时补全和 `deploy/config.example.yaml` 都在 `connect-src` 中精确列出十个回环 Origin。不得扩大为端口通配符、`localhost` 或局域网地址。
+
+TokenRouter 默认不限制 `local-network-access`。若反向代理自行设置 `Permissions-Policy`,必须允许顶层页面访问本地网络。现代 Chromium 首次访问可能请求本地网络权限;拒绝时页面显示未找到本机会话并允许重试。
+
+## 验证
+
+- `tfCliImport.spec.ts`:fragment、过期、固定 HMAC 向量、验证降级、请求选项和 proof。
+- `TfCliImportDialog.spec.ts`、`KeysView.spec.ts`、`KeyActionMenu.spec.ts`:发送确认、状态文案、字段映射和菜单入口。
+- `security_headers_test.go`:十个精确 CSP Origin 及默认策略同步。
+
+相关文档:[接口目录](index.md)、[配置边界](configuration.md)、[复合 API Key](../domains/composite_api_keys.md)。
diff --git a/frontend/src/components/keys/KeyActionMenu.vue b/frontend/src/components/keys/KeyActionMenu.vue
index 3174fe8be103..061dade85b1c 100644
--- a/frontend/src/components/keys/KeyActionMenu.vue
+++ b/frontend/src/components/keys/KeyActionMenu.vue
@@ -1,23 +1,30 @@