客户端接入文档 v0.1

目标:首次登录 → 保存存档 → 退出并重新登录 → 读回同一存档;能处理会话过期和版本冲突。

本文覆盖 14 个核心操作。接口存在、契约通过、本地测试通过、development 联调通过是四个不同状态。当前尚未交付已验收的公网测试环境;不要把后台可登录当成玩家 API 已就绪。

1. 从后台取得公开参数

管理员进入「游戏配置」,选择游戏,再选择同一 environment 下的 iOS/Android App,打开应用配置。交给客户端的是 gameSlug、gameId、gameAppId、platform、appIdentifier,以及发布负责人提供的已验收 apiBaseUrl。客户端不需要后台 Access 凭据。

参数来源与用途不能替代它的字段
apiBaseUrl玩家 API 的 HTTPS origin,不带 /v1;SDK 自己添加路径管理后台地址、文档域名、content 域名
gameSlug后台游戏 slug;bootstrap 和平台登录使用显示名称
gameId游戏 UUID;bootstrap.gameId、session.gameId 和后台游戏 ID 应一致gameSlug、gameAppId
gameAppId后台所选应用 UUID,即 catalog.game_apps.id;设备挑战中的 clientAppIdgameId、Bundle ID、Apple App ID
Bundle ID/包名iOS 的反向域名字符串/Android applicationId;登录字段分别为 bundleId/packageName内部 UUID
Apple App IDApp Store Connect 的数字应用 ID;后台 apple_app_idApple Team ID、Bundle ID、gameAppId
Apple Team IDApple 开发者团队标识;由后端核验配置App Store 数字应用 ID
installationId每个安装持久保存的随机 UUID,与设备私钥一起使用玩家 ID、每请求随机 ID、广告 ID
deviceKeyId首次登录响应的设备注册 UUID,后续刷新使用gameAppId、installationId
gameAccountId登录响应的该游戏玩家 ID;同平台账号再次登录应恢复它gameId、平台原始玩家标识

iOS 与 Android App 分开登记;development 和 production 不混用会话、密钥或本地缓存。公开参数不是 Secret,也不是登录凭据。只有这些参数仍不足以完成平台登录:游戏必须配置 Game Center 或 Play Games,并接好平台证明、P-256 签名和安全存储适配器。

不要在客户端、文档、日志或 Git 中放 JWT 私钥、数据库连接串、OAuth client secret、Apple 私钥或后台凭据。

2. 中文快速开始

先由发布负责人交付真实 development 参数和就绪记录。文档已发布于 docs.apemotion.app;建议的玩家 API 域名 api-dev.apemotion.app 仍是候选地址,不是已上线承诺。OpenAPI 的 /v1 是相对于玩家 API origin,不是相对于文档站。

安装 Unity SDK 后从 Package Manager 导入「First Game」示例。初始化 FirstGameExample 时传入公开参数、ISecureStorage、IDeviceProofProvider 和 IPlatformIdentityAdapter。设备私钥应保存在 Keychain/Keystore,签名适配器输出 P-256 ES256 的 64 字节 r||s 签名的无填充 base64url,而不是 ASN.1 DER。

  1. GET /v1/public/games/{gameSlug}/bootstrap;检查维护状态、最低版本、environment、gameId。维护中停止登录/写入;低于 minimumClientVersion 先升级。使用 configVersion 获取公开游戏配置。
  2. 恢复持久 session。同一安装必须仍能使用原设备私钥。GET /v1/me 验证会话;过期时 SDK 单次串行刷新;刷新被拒绝则清理 session 并重新平台登录,不能无限重试。
  3. 首次登录先取得 Game Center 签名材料或 Play Games serverAuthCode,再申请 game_login 设备挑战、签名,发送平台登录。成功后持久保存整个 session,核对 gameId。
  4. GET /v1/games/{gameId}/saves/main。404 NOT_FOUND 只表示该槽不存在,此时新建本地进度并用 baseRevision=0 保存;不能把所有 404 都当成空存档。
  5. PUT 同一路径,携带 Idempotency-Key、当前 baseRevision、document 和 sha256。成功后保存响应 revision,不要继续用旧版本。
  6. POST /v1/auth/logout,成功后清理本地 token。同一平台账号重新登录,再 GET 同一槽,核对 gameAccountId、document 和 revision。

资料响应使用 display_fields,存档使用 inline_document、content_sha256、updated_at。请求使用 displayFields、baseRevision。不要自行把所有 JSON 字段统一转换成 camelCase。

3. 平台登录与设备签名

Game Center 需要 publicKeyUrl、signature、salt、timestamp、teamPlayerId、bundleId,可选 gamePlayerId。timestamp 是平台原始整数时间戳,不是 ISO 字符串。这里不是 Sign in with Apple。Android 需要 serverAuthCode、packageName,可选 recallSessionId;serverAuthCode 必须对应后端登记的 server/Web OAuth client。

登录请求还需要 gameSlug、installationId、devicePublicJwk、authRequestId 和三个设备证明字段。完整字段定义和约束见接口检索,字段均由共享 Zod 契约生成。

设备签名步骤:

  1. 按请求 schema 得到语义请求(补齐 JWK 的 alg=ES256、ext=true;去掉未知字段)。移除 deviceChallengeId、deviceChallengeNonce、deviceProof 三项;其余字段不得变动。
  2. 对语义对象递归按当前协议排序键并紧凑序列化,SHA-256 得到小写十六进制 semanticRequestHash。认证字段为 ASCII,数字 timestamp 用整数。以仓库 canonicalJson 和 SDK 实现为准;不得把整段原始 HTTP JSON 当作签名输入。
  3. POST /v1/auth/device-challenges,传 operation=game_login、installationId、platform、devicePublicJwk、clientAppId=gameAppId。刷新时改 game_refresh,并加 session.deviceKeyId。
  4. 签名以下 UTF-8 文本,行间是 LF,末尾没有换行;追加三个证明字段后发送登录/刷新。
ape-device-proof-v1
{operation}
{challengeId}
{nonce}
{semanticRequestHash}

挑战响应为 HTTP 201,nonce 在登录请求中叫 deviceChallengeNonce。挑战一次性使用,有效期以 expiresAt 为准。网络超时可能已消费挑战,每次重发都要申请新挑战,但不能变更同一次尝试的语义字段。

4. 幂等、过期和重试

场景保留的内容下一步
普通写入超时/503原 Idempotency-Key、请求体、baseRevision、originAccountLifecycleId有限指数退避并加抖动,原样重发;跨重启需安全持久化待提交操作
登录超时authRequestId、幂等键、平台证明及其余语义字段只更新设备挑战和签名后重发;不能把已兑换 auth code 用在新尝试中
平台证明需重签不再复用旧兑换尝试EXTERNAL_IDENTITY_PROOF_REISSUE_REQUIRED 后向平台取新证明、新 authRequestId 和新键
refresh 超时旧 refreshToken、refreshRequestId、installationId、deviceKeyId、JWK只换挑战和签名;未知结果时持久保留该尝试,不能生成新 ID 再用旧 token
401 AUTHENTICATION_REQUIRED待提交写入保持原键和请求体single-flight 刷新一次并重发;刷新 401 或重发仍 401 时要求重新登录
409 IDEMPOTENCY_IN_PROGRESS原键和请求体有限退避后重试,不新建写入
409 REVISION_CONFLICT保留未同步本地 document重新读取远端,由游戏合并或让用户选择,然后用最新 revision、新键提交
409 IDEMPOTENCY_CONFLICT保留诊断 requestId停止:同一键对应了不同请求;检查客户端逻辑
429 RATE_LIMITED当前账号、安装身份保持不变指数退避+抖动,不能轮换安装标识;当前 API 不保证 Retry-After
403/422保留诊断 requestId修正 scope、平台配置或校验和;不盲目重试

Idempotency-Key 为 8–128 个可打印 ASCII 字符,推荐 UUID。设备挑战和 refresh 不要求此 header;refresh 的去重标识在请求体。GET 不要求幂等键。

accessToken 和 refreshToken 只能保存在安全存储;不要放 PlayerPrefs。会话过期依据 expiresAt,并参考 Date 响应头矫正设备时钟。刷新成功后原子替换 session,旧 refreshToken 不可用于另一 refreshRequestId,否则会撤销整个会话家族。

5. 存档和版本冲突

首版使用 JSON inline 存档。document 必须是对象,服务端以 JSON.stringify(document) 的 UTF-8 字节计算 SHA-256;上限是 128 KiB,hash 必须 64 位小写十六进制。这里不是认证语义对象的排序哈希。

跨语言序列化不是天然一致的。首版示例将游戏状态编码为 document.payload 字符串,避免浮点、指数、数值键排序、日期或自定义 Newtonsoft converter 导致 .NET 与 JavaScript 输出不同。复杂对象直接存入 document 前必须增加跨语言校验测试;不要修改服务端已有哈希语义来绕过不一致。

更新用读到的 revision 作为 baseRevision。并发两个写入只允许一个成功,另一个返回 REVISION_CONFLICT;客户端不得吞掉冲突、递增猜测 revision 或自动覆盖。PATCH profile 的 displayFields 合并行为由服务端实现决定,客户端也必须使用最新 revision。

超时不等于失败,已提交的存档可通过同一键重放获得结果。切换账号时冻结旧账号待同步操作,绝不把旧账号文档写入新账号。不要将 object_key 当作可直接下载的 URL;大文件上传协议不在本版接入范围。

6. 错误结构

失败返回 application/problem+json,包括 type、title、status、code、requestId,可选 detail、details。依据 code 和 HTTP status 分支,不匹配英文 detail。记录 requestId、操作名、HTTP status 和 code;不记录 token、签名、平台证明或存档正文。所有错误码和状态映射由现有 ApiProblem 生成在接口页面。

7. 可用范围

领域v0.1 文档状态运行验收状态
配置、设备挑战、登录/刷新/退出、玩家资料、inline 存档核心契约、示例与 CI 覆盖本地验证与公网验收分开记录
运动、排行榜、邮件仅接口目录,待逐项补齐不承诺可直接接入
购买、账号合并复杂证明与跨服务依赖未纳入首版不承诺可直接接入
导出、删除部分契约已存在,完整客户端教程待补齐不承诺可直接接入
大文件存档、资源更新已有 SDK 能力,非首版登录/存档路径需专项验收

8. 首个游戏验收清单

  1. 文档构建、OpenAPI 一致性、请求/响应 fixture、真实路由响应测试通过;后台能打开客户端文档,下载 OpenAPI 和示例。
  2. 发布负责人提供已确认参数和真实 development API。先验证 DNS/TLS、health、bootstrap;连续与并发玩家请求不得出现跨请求 I/O 错误。
  3. 在真机使用真实平台测试账号登录,记录脱敏 requestId、gameAccountId、gameId;保存测试存档并记录 revision。
  4. 退出、重新平台登录,gameAccountId 不变,读取相同存档;重新启动应用也能恢复会话。
  5. 等待/测试控制使 access token 过期,确认只刷新一次;撤销会话后应提示重新登录而不是死循环。
  6. 两个客户端读同一 revision 后分别写入,验证一方 409,未同步本地数据仍保留;用户确认合并后才提交。
  7. 模拟响应丢失,重试同一写入不增加额外 revision;刷新响应丢失后仍沿用同一个 refreshRequestId。

仅本地 mock、静态 SDK 检查或 Worker dry-run 不能勾选真机/公网验收。真实环境的剩余输入和发布门禁由 development-launch-inputs.md 管理;本文不授权放宽门禁。