From 3ffd87d8def0334272e0eab2607f9528345b1d0c Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Tue, 23 Sep 2025 03:27:03 +0800 Subject: [PATCH] docs(sdk-access): add SDK usage guides in English and Chinese - Added `sdk-access.md` and `sdk-access_CN.md` documentation files. - Included detailed guidelines for authentication manager lifecycle, configuration, built-in and custom providers. - Documented integration steps with `cliproxy` and instructions for hot reloading. --- docs/sdk-access.md | 176 ++++++++++++++++++++++++++++++++++++++++++ docs/sdk-access_CN.md | 176 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 docs/sdk-access.md create mode 100644 docs/sdk-access_CN.md diff --git a/docs/sdk-access.md b/docs/sdk-access.md new file mode 100644 index 00000000..e4e69629 --- /dev/null +++ b/docs/sdk-access.md @@ -0,0 +1,176 @@ +# @sdk/access SDK Reference + +The `github.com/router-for-me/CLIProxyAPI/v6/sdk/access` package centralizes inbound request authentication for the proxy. It offers a lightweight manager that chains credential providers, so servers can reuse the same access control logic inside or outside the CLI runtime. + +## Importing + +```go +import ( + sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" +) +``` + +Add the module with `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access`. + +## Manager Lifecycle + +```go +manager := sdkaccess.NewManager() +providers, err := sdkaccess.BuildProviders(cfg) +if err != nil { + return err +} +manager.SetProviders(providers) +``` + +* `NewManager` constructs an empty manager. +* `SetProviders` replaces the provider slice using a defensive copy. +* `Providers` retrieves a snapshot that can be iterated safely from other goroutines. +* `BuildProviders` translates `config.Config` access declarations into runnable providers. When the config omits explicit providers but defines inline API keys, the helper auto-installs the built-in `config-api-key` provider. + +## Authenticating Requests + +```go +result, err := manager.Authenticate(ctx, req) +switch { +case err == nil: + // Authentication succeeded; result describes the provider and principal. +case errors.Is(err, sdkaccess.ErrNoCredentials): + // No recognizable credentials were supplied. +case errors.Is(err, sdkaccess.ErrInvalidCredential): + // Supplied credentials were present but rejected. +default: + // Transport-level failure was returned by a provider. +} +``` + +`Manager.Authenticate` walks the configured providers in order. It returns on the first success, skips providers that surface `ErrNotHandled`, and tracks whether any provider reported `ErrNoCredentials` or `ErrInvalidCredential` for downstream error reporting. + +If the manager itself is `nil` or no providers are registered, the call returns `nil, nil`, allowing callers to treat access control as disabled without branching on errors. + +Each `Result` includes the provider identifier, the resolved principal, and optional metadata (for example, which header carried the credential). + +## Configuration Layout + +The manager expects access providers under the `auth.providers` key inside `config.yaml`: + +```yaml +auth: + providers: + - name: inline-api + type: config-api-key + api-keys: + - sk-test-123 + - sk-prod-456 +``` + +Fields map directly to `config.AccessProvider`: `name` labels the provider, `type` selects the registered factory, `sdk` can name an external module, `api-keys` seeds inline credentials, and `config` passes provider-specific options. + +### Loading providers from external SDK modules + +To consume a provider shipped in another Go module, point the `sdk` field at the module path and import it for its registration side effect: + +```yaml +auth: + providers: + - name: partner-auth + type: partner-token + sdk: github.com/acme/xplatform/sdk/access/providers/partner + config: + region: us-west-2 + audience: cli-proxy +``` + +```go +import ( + _ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token + sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" +) +``` + +The blank identifier import ensures `init` runs so `sdkaccess.RegisterProvider` executes before `BuildProviders` is called. + +## Built-in Providers + +The SDK ships with one provider out of the box: + +- `config-api-key`: Validates API keys declared inline or under top-level `api-keys`. It accepts the key from `Authorization: Bearer`, `X-Goog-Api-Key`, `X-Api-Key`, or the `?key=` query string and reports `ErrInvalidCredential` when no match is found. + +Additional providers can be delivered by third-party packages. When a provider package is imported, it registers itself with `sdkaccess.RegisterProvider`. + +### Metadata and auditing + +`Result.Metadata` carries provider-specific context. The built-in `config-api-key` provider, for example, stores the credential source (`authorization`, `x-goog-api-key`, `x-api-key`, or `query-key`). Populate this map in custom providers to enrich logs and downstream auditing. + +## Writing Custom Providers + +```go +type customProvider struct{} + +func (p *customProvider) Identifier() string { return "my-provider" } + +func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) { + token := r.Header.Get("X-Custom") + if token == "" { + return nil, sdkaccess.ErrNoCredentials + } + if token != "expected" { + return nil, sdkaccess.ErrInvalidCredential + } + return &sdkaccess.Result{ + Provider: p.Identifier(), + Principal: "service-user", + Metadata: map[string]string{"source": "x-custom"}, + }, nil +} + +func init() { + sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) { + return &customProvider{}, nil + }) +} +``` + +A provider must implement `Identifier()` and `Authenticate()`. To expose it to configuration, call `RegisterProvider` inside `init`. Provider factories receive the specific `AccessProvider` block plus the full root configuration for contextual needs. + +## Error Semantics + +- `ErrNoCredentials`: no credentials were present or recognized by any provider. +- `ErrInvalidCredential`: at least one provider processed the credentials but rejected them. +- `ErrNotHandled`: instructs the manager to fall through to the next provider without affecting aggregate error reporting. + +Return custom errors to surface transport failures; they propagate immediately to the caller instead of being masked. + +## Integration with cliproxy Service + +`sdk/cliproxy` wires `@sdk/access` automatically when you build a CLI service via `cliproxy.NewBuilder`. Supplying a preconfigured manager allows you to extend or override the default providers: + +```go +coreCfg, _ := config.LoadConfig("config.yaml") +providers, _ := sdkaccess.BuildProviders(coreCfg) +manager := sdkaccess.NewManager() +manager.SetProviders(providers) + +svc, _ := cliproxy.NewBuilder(). + WithConfig(coreCfg). + WithAccessManager(manager). + Build() +``` + +The service reuses the manager for every inbound request, ensuring consistent authentication across embedded deployments and the canonical CLI binary. + +### Hot reloading providers + +When configuration changes, rebuild providers and swap them into the manager: + +```go +providers, err := sdkaccess.BuildProviders(newCfg) +if err != nil { + log.Errorf("reload auth providers failed: %v", err) + return +} +accessManager.SetProviders(providers) +``` + +This mirrors the behaviour in `cliproxy.Service.refreshAccessProviders` and `api.Server.applyAccessConfig`, enabling runtime updates without restarting the process. diff --git a/docs/sdk-access_CN.md b/docs/sdk-access_CN.md new file mode 100644 index 00000000..b3f26497 --- /dev/null +++ b/docs/sdk-access_CN.md @@ -0,0 +1,176 @@ +# @sdk/access 开发指引 + +`github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 包负责代理的入站访问认证。它提供一个轻量的管理器,用于按顺序链接多种凭证校验实现,让服务器在 CLI 运行时内外都能复用相同的访问控制逻辑。 + +## 引用方式 + +```go +import ( + sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" +) +``` + +通过 `go get github.com/router-for-me/CLIProxyAPI/v6/sdk/access` 添加依赖。 + +## 管理器生命周期 + +```go +manager := sdkaccess.NewManager() +providers, err := sdkaccess.BuildProviders(cfg) +if err != nil { + return err +} +manager.SetProviders(providers) +``` + +- `NewManager` 创建空管理器。 +- `SetProviders` 替换提供者切片并做防御性拷贝。 +- `Providers` 返回适合并发读取的快照。 +- `BuildProviders` 将 `config.Config` 中的访问配置转换成可运行的提供者。当配置没有显式声明但包含顶层 `api-keys` 时,会自动挂载内建的 `config-api-key` 提供者。 + +## 认证请求 + +```go +result, err := manager.Authenticate(ctx, req) +switch { +case err == nil: + // Authentication succeeded; result carries provider and principal. +case errors.Is(err, sdkaccess.ErrNoCredentials): + // No recognizable credentials were supplied. +case errors.Is(err, sdkaccess.ErrInvalidCredential): + // Credentials were present but rejected. +default: + // Provider surfaced a transport-level failure. +} +``` + +`Manager.Authenticate` 按配置顺序遍历提供者。遇到成功立即返回,`ErrNotHandled` 会继续尝试下一个;若发现 `ErrNoCredentials` 或 `ErrInvalidCredential`,会在遍历结束后汇总给调用方。 + +若管理器本身为 `nil` 或尚未注册提供者,调用会返回 `nil, nil`,让调用方无需针对错误做额外分支即可关闭访问控制。 + +`Result` 提供认证提供者标识、解析出的主体以及可选元数据(例如凭证来源)。 + +## 配置结构 + +在 `config.yaml` 的 `auth.providers` 下定义访问提供者: + +```yaml +auth: + providers: + - name: inline-api + type: config-api-key + api-keys: + - sk-test-123 + - sk-prod-456 +``` + +条目映射到 `config.AccessProvider`:`name` 指定实例名,`type` 选择注册的工厂,`sdk` 可引用第三方模块,`api-keys` 提供内联凭证,`config` 用于传递特定选项。 + +### 引入外部 SDK 提供者 + +若要消费其它 Go 模块输出的访问提供者,可在配置里填写 `sdk` 字段并在代码中引入该包,利用其 `init` 注册过程: + +```yaml +auth: + providers: + - name: partner-auth + type: partner-token + sdk: github.com/acme/xplatform/sdk/access/providers/partner + config: + region: us-west-2 + audience: cli-proxy +``` + +```go +import ( + _ "github.com/acme/xplatform/sdk/access/providers/partner" // registers partner-token + sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access" +) +``` + +通过空白标识符导入即可确保 `init` 调用,先于 `BuildProviders` 完成 `sdkaccess.RegisterProvider`。 + +## 内建提供者 + +当前 SDK 默认内置: + +- `config-api-key`:校验配置中的 API Key。它从 `Authorization: Bearer`、`X-Goog-Api-Key`、`X-Api-Key` 以及查询参数 `?key=` 提取凭证,不匹配时抛出 `ErrInvalidCredential`。 + +导入第三方包即可通过 `sdkaccess.RegisterProvider` 注册更多类型。 + +### 元数据与审计 + +`Result.Metadata` 用于携带提供者特定的上下文信息。内建的 `config-api-key` 会记录凭证来源(`authorization`、`x-goog-api-key`、`x-api-key` 或 `query-key`)。自定义提供者同样可以填充该 Map,以便丰富日志与审计场景。 + +## 编写自定义提供者 + +```go +type customProvider struct{} + +func (p *customProvider) Identifier() string { return "my-provider" } + +func (p *customProvider) Authenticate(ctx context.Context, r *http.Request) (*sdkaccess.Result, error) { + token := r.Header.Get("X-Custom") + if token == "" { + return nil, sdkaccess.ErrNoCredentials + } + if token != "expected" { + return nil, sdkaccess.ErrInvalidCredential + } + return &sdkaccess.Result{ + Provider: p.Identifier(), + Principal: "service-user", + Metadata: map[string]string{"source": "x-custom"}, + }, nil +} + +func init() { + sdkaccess.RegisterProvider("custom", func(cfg *config.AccessProvider, root *config.Config) (sdkaccess.Provider, error) { + return &customProvider{}, nil + }) +} +``` + +自定义提供者需要实现 `Identifier()` 与 `Authenticate()`。在 `init` 中调用 `RegisterProvider` 暴露给配置层,工厂函数既能读取当前条目,也能访问完整根配置。 + +## 错误语义 + +- `ErrNoCredentials`:任何提供者都未识别到凭证。 +- `ErrInvalidCredential`:至少一个提供者处理了凭证但判定无效。 +- `ErrNotHandled`:告诉管理器跳到下一个提供者,不影响最终错误统计。 + +自定义错误(例如网络异常)会马上冒泡返回。 + +## 与 cliproxy 集成 + +使用 `sdk/cliproxy` 构建服务时会自动接入 `@sdk/access`。如果需要扩展内置行为,可传入自定义管理器: + +```go +coreCfg, _ := config.LoadConfig("config.yaml") +providers, _ := sdkaccess.BuildProviders(coreCfg) +manager := sdkaccess.NewManager() +manager.SetProviders(providers) + +svc, _ := cliproxy.NewBuilder(). + WithConfig(coreCfg). + WithAccessManager(manager). + Build() +``` + +服务会复用该管理器处理每一个入站请求,实现与 CLI 二进制一致的访问控制体验。 + +### 动态热更新提供者 + +当配置发生变化时,可以重新构建提供者并替换当前列表: + +```go +providers, err := sdkaccess.BuildProviders(newCfg) +if err != nil { + log.Errorf("reload auth providers failed: %v", err) + return +} +accessManager.SetProviders(providers) +``` + +这一流程与 `cliproxy.Service.refreshAccessProviders` 和 `api.Server.applyAccessConfig` 保持一致,避免为更新访问策略而重启进程。