---
title: 原生声明与 Errand
description: 为原生 direct-issue 选择声明策略，并在缺少 required 同意或资料时处理浏览器 Errand。
editUrl: true
head: []
template: doc
sidebar:
  order: 2
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

Steam ticket 与 AccessKey direct-issue 不经过应用自己的登录页面。这让成功路径非常短，但也意味着客户端无法展示同意界面，或要求用户当场补齐个人资料。

本页同时处理这个限制的两面：

1. 为非交互式客户端选择合适的声明策略。
2. 当 required 真实声明无法签发时，处理 **Errand** 浏览器交接。

共享的策略与授权模型见[身份声明与共享](/zh-cn/concepts/identity-claims/)。

## 选择原生声明策略

| 策略 | 原生 direct-issue 行为 |
|---|---|
| **Off** | 从不请求。 |
| **Optional** | 仅在用户此前已经授权时共享。永不阻塞，因此纯原生用户可能永远不会被提示。 |
| **Required** | 保证使用真实数据且一定存在。缺少同意或数据时返回带 Errand 的 `403`。 |
| **Synthetic** | 保证存在；已授权时使用真实数据，否则使用稳定占位值。永不阻塞，也不会创建 Errand。 |

对大多数原生集成，除非你明确需要经过验证的真实数据，否则应优先选择 **`SYNTHETIC`** 而不是 **`REQUIRED`**。

- 只需要稳定的姓名或邮箱形态值，可以接受占位：使用 `SYNTHETIC`。
- 需要真实、已验证的邮箱用于送达或外部对账：使用 `REQUIRED` 并实现 Errand 流程。
- 用户已经授权时希望拿到真实数据，但缺少时仍可继续：使用 `OPTIONAL`。

如果所有请求的声明都是 `OFF` 或 `SYNTHETIC`，声明策略就永远不会迫使 direct-issue 进入浏览器交接。

Synthetic 姓名是生成的占位值；Synthetic 邮箱使用稳定的 `…@proxy.sudomimus.email` 地址。代理投递仅为尽力而为，不保证送达；OIDC 会把 synthetic 邮箱标记为 `email_verified: false`。

## Errand

**Errand** 是短暂的账户补救流程，不负责签发令牌。当 direct-issue 无法满足 required 声明时，`403` 响应会向客户端提供一个浏览器 URL。用户在那里完成同意或补资料，随后客户端重试原来的 direct-issue。

只有两种声明把关原因会携带 Errand：

| `403` reason | 含义 | 浏览器内的工作 |
|---|---|---|
| `ClaimConsentRequired` | required 声明尚未获得授权。 | 授予同意；必要时先补齐缺失数据。 |
| `RequiredClaimDataMissing` | 已有同意，但账户缺少真实值。 | 注册邮箱或补全缺失姓名。 |

规则拒绝、账户禁用等其他 `403` 都是终态，不会包含 Errand。

### 交接响应

```json
{
  "reason": "ClaimConsentRequired",
  "claims": {
    "email":     { "requirement": "REQUIRED", "state": "UNKNOWN" },
    "firstName": { "requirement": "OPTIONAL", "state": "UNKNOWN" },
    "lastName":  { "requirement": "OFF",      "state": "UNKNOWN" }
  },
  "errand": {
    "errandKey": "ernd_...",
    "url": "https://via.sudomimus.com/errand?key=ernd_...",
    "expiresAt": "2026-06-10T12:30:00Z"
  }
}
```

- 在用户的**系统浏览器**中打开 `errand.url`。
- 把 `errandKey` 当作 bearer secret；轮询状态时也使用它。
- Errand 只能使用一次，并在 **30 分钟**后过期。

### 客户端循环

```text
原生客户端 ── direct-issue ──▶ 403 { reason, claims, errand }
   │
   ├── 在系统浏览器中打开 errand.url
   ├── 轮询 GET /errand/{errandKey}/status
   └── 收到 COMPLETED 后重试一次 direct-issue ──▶ 200 { tokens, claims }
```

轮询不是强制的。客户端也可以让用户在浏览器完成后手动确认，再执行重试。

```bash
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" }
# → { "status": "COMPLETED" }
# → { "status": "EXPIRED" }
```

建议大约每两秒轮询一次，并设置合理的总超时。`EXPIRED` 会刻意覆盖未知、格式错误、已消费和真正过期的 key；收到后重新执行 direct-issue 获取新的交接。状态端点本身永远不会签发令牌。

只要现有 Errand 至少还剩 15 分钟，且待办事项没有变化，重试通常会返回同一个 Errand，避免激进的重试循环把用户进度拆到多个 URL。

## 安全行为

- **仅需同意：**不要求额外登录，因为凭据持有者已经证明自己控制着一项可签发令牌的凭据。
- **需要写入身份数据：**浏览器会要求登录，且登录账户必须与 Steam ticket 或 AccessKey 解析出的账户一致。
- Optional 与 Synthetic 声明永远不会创建 Errand。
- Connect `/refresh` 与 OIDC `/token` 不会内嵌 Errand。原生会话在 refresh 时被阻塞，需要重新执行 direct-issue。

## 相关阅读

- [原生应用集成](/zh-cn/native/overview/) —— 浏览器轮询、Steam ticket 与 AccessKey 流程。
- [身份声明与共享](/zh-cn/concepts/identity-claims/) —— 共享的策略、授权与纳入模型。
- [令牌与验证](/zh-cn/concepts/tokens-and-verification/) —— 声明把关满足后最终签发的令牌。