Driver v1.0.5 持久同意合同(B03)
此合同是配套 release/v1.0.5-driver-contracts 候选,初始基线 f0cdf2181068fc72f6e6ece192d814568b8529b4。Host 配对 release/v1.0.5,派发时 frontier 99dd5c229b1c3c5950908cb657216430c261c5cd,SDK/contract 1.25.0、test-kit 1.3.0。仅正式 reviewed merge 后供消费者启用;生产 migration、真实 grant、部署与 Host 自动恢复均未执行。
身份与信任
每个持久身份绑定服务器 issuer、真实 subject、官方 Host client、插件 target client、public/development mode。同意行不会因 logout/token revoke 消失,也不从历史 token 或客户端 receipt 回填 active。同意创建沿用现有官方 Host 安装确认架构:当前有效 oauth:delegate Host token、服务器白名单及 official standard 校验,Host 仅在用户实际明确安装/权限确认后调用 confirm。服务器不能凭布尔值独立证明物理点击;可信 Host 与安装确认实现是此通道的信任边界。JWT 用户后台负责查看/撤回,未强制改变安装为 JWT-only。
receipt 不是 Bearer,不含 code/token/verifier,也不能创建或恢复已撤回同意。每次请求仍需当前 Host token;expected_subject 必须等于该 token 的真实 subject。服务器 issuer 来自当前 Supabase 配置的 /auth/v1,客户端不能选择 issuer。
Prepare → Confirm
POST /oauth/delegated/consent/prepare,Bearer 当前官方 Host access token,JSON:
{
"expected_subject": "10000000-0000-0000-0000-000000000001",
"target_client_id": "synthetic-plugin-client",
"app_mode": "public",
"scopes": ["profile:read"],
"redirect_uri": "https://synthetic.invalid/callback"
}2
3
4
5
6
7
返回 {confirmation_id, expires_at, generation, snapshot}。snapshot 含 issuer/subject/Host 与目标身份、scope definition versions、scope fingerprint、redirect、revision、app definition version 和 authorization_config。TTL 300 秒;同 identity 的新 prepare 作废旧未用快照,不改变有效/已撤回状态;5 分钟最多 20 次。此步骤不授权或签码。
用户实际确认后,POST /oauth/delegated/authorize 在以上同一请求基础上增加:
{
"consent_action": "confirm",
"user_confirmed": true,
"confirmation_id": "20000000-0000-0000-0000-000000000001",
"code_challenge": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"code_challenge_method": "S256"
}2
3
4
5
6
7
authorize 的 consent_action 仅接受 confirm/restore;prepare/revoke 返回 400 invalid_request 且不调用任何 RPC。同事务重验全部服务器快照与当前代际;快照过期、已用、被新 prepare 作废或撤回后变旧均拒绝。成功一次性消费快照,创建新 active generation,撤旧代际短凭据并签普通 PKCE code。响应保留原 code/expires_in/scopes/scope_definition_versions/app_revision_id/app_mode 并加 consent_receipt:
{
"schema_version": 1,
"issuer": "https://synthetic.invalid/auth/v1",
"subject": "10000000-0000-0000-0000-000000000001",
"host_client_id": "synthetic-host-client",
"target_client_id": "synthetic-plugin-client",
"app_mode": "public",
"consent_id": "30000000-0000-0000-0000-000000000001",
"generation": "1",
"scopes": ["profile:read"],
"scope_definition_versions": {"profile:read": 1},
"scope_fingerprint": "opaque-server-sha256"
}2
3
4
5
6
7
8
9
10
11
12
13
generation 一律十进制字符串,不能经 JavaScript number 转换;prepare 可返回 "0",receipt/revoke/restore 必须正整数。SHA256 是服务器规范 JSON scope versions 的语义摘要,不是授权签名。HTTP 响应 Cache-Control: no-store。
Restore
同一 POST /oauth/delegated/authorize 使用 consent_action: "restore"、consent_id、generation 替代 confirmation/user_confirmed;仍带 expected_subject、target_client_id、明确 app_mode、当前 scopes/redirect/PKCE。不得混合两类字段。
只读取服务器现存 active 同意,逐请求复验官方 Host、目标 plugin、当前发布/审核 attestation、开发资格、当前 metadata、允许 scopes 与定义版本。requested scopes 可收窄,不能扩大;每个 requested scope 的 definition version 必须相同;authorization_config 需兼容。无关 revision 或 app definition version 更新不直接迫使重新同意,当前合法 redirect 依注册 metadata 校验。client/Host/issuer/subject/mode 不能混用。
缺 consent_action 的旧请求仍调用旧 v2。强制开关打开后(见下文「2026-10-16 B0 重新定版」),新的数据库 plugin code insert gate 返回 consent_required,无法隐式建同意;开关关闭(默认)时旧请求与安装前一致。以下限制同样只在开关打开后生效:未绑定新同意的存量 plugin code exchange/refresh 也要求真实重新确认;已有 access token 按原到期/显式撤回语义结束。standard OAuth 流程不受此门影响。这是有意的兼容边界,必须先完成 T03 的客户端版本配对再计划生产激活。
Revoke 与账户生命周期
POST /oauth/delegated/consent/revoke 使用当前官方 Host token,body 仅 {expected_subject, consent_id, generation},供用户卸载/删除(Keep/Delete 私人数据都撤同意)的离线补偿。内部 seed 包替换不是用户卸载。成功返回 {success:true, consent_id, generation, status:"revoked"}。
| 当前状态 | 请求代际 | 结果 |
|---|---|---|
| active | 相等 | 持久 revoked,generation + 1,清 code/token |
| active | 旧或未来 | stale_generation;不撤新同意 |
| revoked | 小于或等于当前 | 幂等成功;不再次推进 |
| revoked | 未来 | stale_generation |
Host 离线卸载应先停本地使用,保存精确 issuer/subject/Host/client/mode/id/gen 待撤回;临时失败保留重试,不能切回账号恢复待撤身份,也不能视超时为远端成功。重装需要新真实确认,迟到旧补偿不能撤新代际。
既有 DELETE /my/credentials/oauth-authorizations/:appId(JWT)同时持久撤回该 subject/app 下全部 Host/mode 同意,包含已无 token 的记录,保留旧影响计数响应。GET /my/credentials/oauth-consents(JWT)返回 {items:[receipt + status/confirmed_at/revoked_at]},使退出后仍可查看同意。旧 token-derived 授权列表的既有返回保持兼容。
POST /oauth/revoke 仍是 RFC7009 token/family-only:logout/switch 不撤长期同意。新明确确认才创建新代际,撤回与重复 confirm 永不从 receipt 自动复活。
稳定错误
| HTTP | error | Host 行为 |
|---|---|---|
| 403 | access_denied | 当前用户不满足所请求 Scope 的 public/development 资格(oauth_self_service_scope_not_allowed / oauth_development_eligibility_revoked),条件不变时不自动重试;2026-10-16 前 public 资格不满足误落 500 |
| 409 | consent_required / consent_revoked | 真实再次确认;不存在/越权 identity 统一 required |
| 409 | consent_incompatible(附 reason,见下表) | 按 reason 分流;未知或缺失 reason 时按 snapshot_changed 处理(重新 prepare 并真实确认) |
| 409 | consent_stale_generation | 丢弃旧任务/旧代际;不得覆盖新 receipt |
| 409 | consent_subject_mismatch | 旧账户迟到任务,丢弃结果 |
| 409 | consent_confirmation_invalid | 获取新快照并真实确认 |
| 429 | slow_down | 同意准备或开发模式签码限流,暂停并退避 |
| 503 | temporarily_unavailable | 保留仍有效 receipt,稍后重试 |
| 500 | server_error | 服务异常;不得视为已授权 |
错误响应体固定为 {error, error_description};仅 consent_incompatible 额外带稳定字段 reason(2026-10-16 新增,error 不变以兼容旧客户端)。 适用于 prepare、confirm / restore(/oauth/delegated/authorize 与 /authorize-release)以及 /oauth/token 换码 / 刷新遇到同意不兼容的情况。
| reason | 数据库原因 | 含义 | 建议 Host 行为 |
|---|---|---|---|
release_not_published | oauth_public_release_not_published | 插件当前公开修订没有可签的已发布版本(下架 / 未发布) | 平台侧问题,提示稍后重试,勿反复确认 |
target_not_approved | oauth_delegated_target_not_approved | 插件没有当前过审冻结修订 | 同上 |
scope_not_allowed | oauth_delegated_scope_not_allowed | 请求的 scope 不在插件当前公开修订内 | 客户端与平台登记不一致,上报,勿重试 |
redirect_uri_not_allowed | oauth_delegated_redirect_uri_not_allowed | 回调地址不在插件当前公开修订内 | 同上 |
scope_definition_changed | oauth_scope_definition_changed | scope 定义版本变化或已不可自助授予 | 重新 prepare 展示新权限并确认 |
review_attestation_required | oauth_delegated_review_attestation_required | 插件当前修订缺少合格的代签插件审核确认 | 平台侧问题,提示稍后重试 |
snapshot_changed | oauth_consent_incompatible | confirm 时快照与 prepare 不一致、restore 时已存同意与当前定义不兼容、或换码 / 刷新时同意范围不再覆盖 | 重新 prepare 并真实确认 |
新增 reason 取值属于合同变更:后端冻结测试 apps/backend/src/routes/__tests__/v105-consent-contract.spec.ts 同步更新后才会通过。 服务端会按 error + reason 统计首装失败并告警管理员(见 docs/features/oauth/driver-v105-consent-monitoring.md)。
身份/资格认证错误仍按既有官方 Host 门返回;新路由仅记录固定错误类型。RPC/表/列缺失、升级闸或锁/连接失败均 fail-closed,503 不等于同意缺失。
数据库与原子保证
2026-10-16 B0 重新定版(版本号 20261016200000)
- migration 改为
20261016200000_driver_v105_consent.sql(原20261005200000)。被替换的 exchange / refresh / user-revoke 以 main 当前定义为基准合并:exchange 保留20261008170000的发布绑定校验与 token 家族绑定写入。文件开头逐个比对这三个函数(及issue_release_bound_delegated_oauth_code)的 prosrc 指纹,漂移即driver_v105_consent_function_conflict拒绝安装。 - 强制开关
site_settings.driver_v105_consent_enforcement_enabled:只有值为 JSONtrue才强制。缺失、false或其他值都视为关闭。migration 不写这一行;安装前若该行已存在且不是false,以driver_v105_consent_enforcement_preenabled拒绝安装。- 关闭时:plugin 不带同意上下文的签码、换码、刷新与安装前完全一致(旧 Host、Preview 0.20.x 不受影响)。已带同意上下文的凭据始终校验同意仍有效,撤回立即生效。
- 打开时:plugin 不带同意的签码报
oauth_consent_context_required,换码/刷新报oauth_consent_required(HTTP 均映射为consent_required)。 - 打开前置条件:Driver 1.0.5 已铺开,并经用户单独批准;回退手段是把该行改回
false。
- 发布绑定 + 同意统一签码入口
issue_release_bound_driver_v105_consent_code:POST /oauth/delegated/authorize-release带consent_action(confirm / restore)时调用,同一事务完成同意确认/恢复、写同意代际、写oauth_authorization_code_release_bindings,只支持 public 模式。响应额外带authorizationBinding(kind=explicit_release)。发布绑定失效返回 409oauth_release_binding_*;统一入口缺失返回 503。
候选 migration 20261016200000_driver_v105_consent.sql(原 20261005200000)新增三个 RLS 表:consents、confirmation snapshots、非敏感 events。anon/authenticated 无表/RPC权限;service_role 仅通过受控 action RPC 写入,无法直接写 active。
code/token 绑定 consent id + generation;新 insert gate 校验真实主体/Host/target/mode/scopes 与 active generation。所有签发、换码、刷新、撤回沿 Scope → App advisory → Product/family 锁;撤回赢后旧 code/refresh/receipt 不可再 mint。UPDATE 无条件禁止换绑 consent 身份,controlled write 也没有例外;已绑定 token 的 scopes 只允许收窄(main 的 Scope 撤权 / 路由更新会剥离 scopes),不允许扩大或替换(2026-10-16 起)。consent 撤回的锁序与 main Scope 撤权一致(Scope 行 → Host token → App advisory)。legacy 未标记凭据不会生成同意。
迁移第一事务原子安装表、nullable 绑定列、NOT VALID FK/CHECK、函数、ACL 与 gate;NOT VALID 已约束所有新写入。第二事务单独验证历史约束,避免扫描时保留加列的 ACCESS EXCLUSIVE 锁。第二事务失败时第一事务已提交,必须按发布 SOP 核验状态后补验证,禁止盲目重跑整文件;普通索引仍需生产锁评估。此候选未生产 apply。
rollback 精确恢复最新 exchange/refresh/user-revoke 与原权限,不改历史 migration。存在任何持久同意历史时 rollback 拒绝,防止丢失撤回事实或重置代际。仅候选未使用时可回滚。生产应遵守正式 migration/apply/verify SOP,当前仅隔离测试。
验证证据
运行 python3 scripts/db/test-driver-v105-consent.py 使用既有合成底座、network-none/tmpfs 临时 PostgreSQL 15/17,自动销毁;不解析真实数据库 aliases/env。局部 Vitest 在 v105-consent*.spec.ts,类型 gate 为 pnpm --filter backend typecheck。详细命令、结果与实际 GLM review 身份保存在本任务 .artifacts/v105-b03-consent/。候选代码测试完成、head 审查和 I01 reviewed merge 状态见 PR;此文档不冒称已部署。
本轮候选核验
新增路由/服务 31 项 PASS;相关既有 delegate/revoke/my/lifecycle/scope/writeguard 195 项 PASS(共 226 项);backend typecheck PASS;PG15/17 真实隔离创建/未用回滚、ACL/RLS、代际/快照/账户/scope/client/mode/issuer/revision兼容、offline补偿、issue/exchange/refresh与revoke竞争及双方胜出、新Host family恢复、code与receipt scope区分、开发资格丢失、确认前config/redirect漂移、分事务约束验证 PASS。离线 alias 校验 PASS。D1–D20逐项核查留在本任务忽略证据目录 db-checklist.md,生产部分明确未执行。路由 manifest、单个 API sidebar 入口及生成 auth inventory 已按 I01 窄租约登记。API 权限 manifest 已按 cc14 生成闭包租约登记,仅新增 consent 页面的 user 条目并保留 B00 受保护管理页面权限。已整合精确 B00 reviewed frontier 464d44cee5b7f54b29dad2ed76829d37182b27d7;组合 docs/type/route 检查与最终 head 审查结果以 PR 证据为准,生产未部署。