0
第十五章:凭证与授权
本章目标:帮助你理解 dsh 的凭证接缝(Credential Seam)、环境变量 → .env → Settings 的优先级链、OAuth/授权流的人类交互、以及 API Key 的安全存储与轮换。阅读本章后,你应该能回答"凭证是如何被管理和使用的"以及"如何添加新的凭证类型"。
15.1 凭证架构
15.1.1 凭证接缝
预览
源码
graph TB
subgraph "credentials/ 能力"
A["credentials/ (Service Definition)"]
B["credentials-env/ (Provider)"]
C["credentials-settings/ (Provider)"]
D["消费者: llm, tools, web"]
end
A --> B
A --> C
D --> A
15.1.2 凭证类型
// 来自 packages/credentials/credentials/src/index.ts
type CredentialType =
| 'api-key' // API 密钥
| 'oauth-token' // OAuth 令牌
| 'certificate' // 证书
| 'password' // 密码
| 'ssh-key' // SSH 密钥
15.2 凭证服务
15.2.1 CredentialsService
// 来自 packages/credentials/credentials/src/index.ts
class CredentialsService {
/** 获取凭证 */
async get(name: string): Promise<Credential | null> {
// 按优先级查找
for (const provider of this.providers) {
const credential = await provider.get(name)
if (credential) return credential
}
return null
}
/** 存储凭证 */
async set(name: string, credential: Credential): Promise<void> {
// 存储到默认提供商
await this.defaultProvider.set(name, credential)
}
/** 删除凭证 */
async delete(name: string): Promise<void> {
await this.defaultProvider.delete(name)
}
/** 列出所有凭证 */
async list(): Promise<CredentialEntry[]> {
const entries: CredentialEntry[] = []
for (const provider of this.providers) {
const providerEntries = await provider.list()
entries.push(...providerEntries)
}
return entries
}
}
15.2.2 Credential
interface Credential {
/** 凭证名称 */
name: string
/** 凭证类型 */
type: CredentialType
/** 凭证值 */
value: string
/** 元数据 */
metadata?: Record<string, unknown>
/** 过期时间 */
expiresAt?: Date
}
15.3 凭证提供者
15.3.1 环境变量提供者
// 来自 packages/credentials/credentials-env/src/index.ts
class EnvCredentialProvider implements CredentialProvider {
/** 获取凭证 */
async get(name: string): Promise<Credential | null> {
// 转换名称格式:my-api-key → MY_API_KEY
const envName = name.replace(/-/g, '_').toUpperCase()
const value = process.env[envName]
if (!value) return null
return {
name,
type: 'api-key',
value
}
}
/** 列出所有凭证 */
async list(): Promise<CredentialEntry[]> {
return Object.entries(process.env)
.filter(([key]) => key.startsWith('DSH_'))
.map(([key, value]) => ({
name: key.replace(/^DSH_/, '').toLowerCase().replace(/_/g, '-'),
type: 'api-key',
provider: 'env'
}))
}
}
15.3.2 .env 文件提供者
// 来自 packages/credentials/credentials-env/src/index.ts
class DotEnvCredentialProvider implements CredentialProvider {
constructor(private envPath: string) {}
/** 获取凭证 */
async get(name: string): Promise<Credential | null> {
// 读取 .env 文件
const env = await this.loadEnvFile()
const value = env[name]
if (!value) return null
return {
name,
type: 'api-key',
value
}
}
/** 加载 .env 文件 */
private async loadEnvFile(): Promise<Record<string, string>> {
const content = await readFile(this.envPath, 'utf-8')
const env: Record<string, string> = {}
for (const line of content.split('\n')) {
const trimmed = line.trim()
if (!trimmed || trimmed.startsWith('#')) continue
const [key, ...valueParts] = trimmed.split('=')
env[key.trim()] = valueParts.join('=').trim()
}
return env
}
}
15.3.3 Settings 提供者
// 来自 packages/credentials/credentials-settings/src/index.ts
class SettingsCredentialProvider implements CredentialProvider {
constructor(private ctx: Context) {}
/** 获取凭证 */
async get(name: string): Promise<Credential | null> {
// 从用户设置中获取
const settings = await this.ctx.settings.get('credentials')
const value = settings?.[name]
if (!value) return null
return {
name,
type: 'api-key',
value
}
}
/** 存储凭证 */
async set(name: string, credential: Credential): Promise<void> {
const settings = await this.ctx.settings.get('credentials') ?? {}
settings[name] = credential.value
await this.ctx.settings.set('credentials', settings)
}
}
15.4 凭证解析优先级
15.4.1 优先级链
预览
源码
graph TB
A[凭证请求] --> B{环境变量?}
B -->|"找到"| C[返回凭证]
B -->|"未找到"| D{.env 文件?}
D -->|"找到"| C
D -->|"未找到"| E{Settings?}
E -->|"找到"| C
E -->|"未找到"| F{用户输入?}
F -->|"提供"| G[存储到 Settings]
G --> C
F -->|"拒绝"| H[返回 null]
15.4.2 实现
// 概念性描述
class CredentialResolver {
/** 解析凭证 */
async resolve(name: string, options?: ResolveOptions): Promise<Credential | null> {
// 1. 尝试环境变量
let credential = await this.envProvider.get(name)
if (credential) return credential
// 2. 尝试 .env 文件
credential = await this.dotEnvProvider.get(name)
if (credential) return credential
// 3. 尝试 Settings
credential = await this.settingsProvider.get(name)
if (credential) return credential
// 4. 如果需要,询问用户
if (options?.askUser) {
const value = await this.askUserForCredential(name)
if (value) {
credential = { name, type: 'api-key', value }
await this.settingsProvider.set(name, credential)
return credential
}
}
return null
}
}
15.5 OAuth 授权流
15.5.1 概念
对于需要 OAuth 的服务,dsh 支持人类交互式的授权流。
预览
源码
sequenceDiagram
participant Agent
participant Auth as AuthorizationService
participant User
participant Provider as OAuth Provider
Agent->>Auth: requestAuthorization(provider)
Auth->>Provider: 生成授权 URL
Provider-->>Auth: authorizationUrl
Auth->>User: 显示授权 URL
User->>Provider: 授权
Provider-->>Auth: authorizationCode
Auth->>Provider: 交换令牌
Provider-->>Auth: accessToken
Auth-->>Agent: credential
15.5.2 AuthorizationService
// 来自 packages/credentials/authorization/src/index.ts
class AuthorizationService {
/** 注册授权流 */
registerFlow(flow: AuthorizationFlow): Disposable {
this.flows.set(flow.provider, flow)
return () => {
this.flows.delete(flow.provider)
}
}
/** 请求授权 */
async requestAuthorization(provider: string): Promise<Credential> {
const flow = this.flows.get(provider)
if (!flow) throw new Error(`No authorization flow for provider: ${provider}`)
// 1. 生成授权 URL
const authUrl = await flow.generateAuthorizationUrl()
// 2. 显示给用户
await this.showAuthorizationUrl(authUrl)
// 3. 等待回调
const code = await this.waitForCallback(flow.callbackUrl)
// 4. 交换令牌
const token = await flow.exchangeCode(code)
return token
}
}
15.6 API Key 安全
15.6.1 安全存储
// 概念性描述
class SecureCredentialStorage {
/** 加密存储 */
async store(credential: Credential): Promise<void> {
// 1. 加密凭证值
const encrypted = await this.encrypt(credential.value)
// 2. 存储到安全位置
await this.secureStore.set(credential.name, {
...credential,
value: encrypted
})
}
/** 解密读取 */
async retrieve(name: string): Promise<Credential | null> {
const stored = await this.secureStore.get(name)
if (!stored) return null
// 解密
const decrypted = await this.decrypt(stored.value)
return {
...stored,
value: decrypted
}
}
}
15.6.2 密钥轮换
// 概念性描述
class CredentialRotation {
/** 检查是否需要轮换 */
async needsRotation(credential: Credential): Promise<boolean> {
if (!credential.expiresAt) return false
// 检查是否即将过期(7 天内)
const daysUntilExpiry = (credential.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
return daysUntilExpiry < 7
}
/** 轮换密钥 */
async rotate(name: string): Promise<Credential> {
// 1. 获取当前凭证
const current = await this.credentials.get(name)
if (!current) throw new Error(`Credential not found: ${name}`)
// 2. 生成新凭证
const newCredential = await this.generateNewCredential(current)
// 3. 存储新凭证
await this.credentials.set(name, newCredential)
// 4. 删除旧凭证(可选)
// await this.credentials.delete(`${name}-old`)
return newCredential
}
}
15.7 配置
15.7.1 凭证配置
# cordis.patch.yml
- id: credentials
config:
# 提供者优先级
providers:
- 'env'
- 'dot-env'
- 'settings'
# .env 文件路径
dotEnvPath: '${DSH_HOME}/.env'
# 安全存储
secureStorage:
enabled: true
type: 'keychain' # 'keychain' | 'file' | 'memory'
15.8 小结
| 概念 | 一句话解释 |
|---|---|
| Credential Seam | 凭证管理的能力接缝 |
| Env Provider | 环境变量凭证提供者 |
| DotEnv Provider | .env 文件凭证提供者 |
| Settings Provider | 用户设置凭证提供者 |
| Authorization Flow | OAuth 授权流 |
| 密钥轮换 | 自动轮换即将过期的凭证 |
下一步:第十六章:Webhook 与外部事件——深入理解验证外部事件的签名机制和可信规则。
DeepSeek-Harness
/
15-凭证与授权
0 字
0 行
iliuqi
2026-09-04T07:48:53.495134837Z
2026-09-04T07:58:27.702620227Z