# 服务端接入

当前只支持 Global。Web、静态 SPA、Native 都先调用客户自己的后端，只有后端持有 v-auth 的调用密钥。App 和 test / production 的账号、会话、数据库、密钥相互隔离。

## 从 Studio 开始

1. 注册 Studio 管理账号，创建 App，等待两个环境开通。失败时查看错误代码，在原环境重试。
2. 打开 test 的 Get started，获取带 App ID 和环境的示例。先创建最小 scopes 的 API key，明文只显示一次。
3. 把 base URL、App ID、环境和密钥保存到客户后端。不要使用 `VITE_`、`NEXT_PUBLIC_` 等公开变量。
4. 执行会话校验示例，使用不存在的 session_token。200 且 `data.active=false` 证明连接和权限正常，不代表注册或登录成功。
5. 使用已有的已验证账号测试密码登录；新终端用户注册必须先通过邮件 proof，不提供绕过验证的注册接口。邮件真实收发暂缓，不伪造注册成功。

## SDK

`@v-auth/sdk` 提供独立 ESM 和 TypeScript 声明，无运行时依赖。仓库根目录执行 `pnpm sdk:pack` 后，消费项目可通过 `npm install /path/to/v-auth-sdk-0.1.0.tgz` 安装归档。归档随附当版 OpenAPI，生成校验清单并在隔离 Node / Workers 消费验证，见[SDK 候选包与发布](sdk-release.md)；当前按约定只交付安装包，暂不发布 npm。也可直接使用 HTTP API；可下载的 OpenAPI 仅列已经实现的十二个操作。

```ts
import { VAuthClient, VAuthError } from '@v-auth/sdk';

const auth = new VAuthClient({
  baseUrl: 'https://auth-api.virtneu.com',
  applicationId: process.env.VAUTH_APPLICATION_ID!,
  environment: 'test',
  serviceCredential: process.env.VAUTH_SERVICE_CREDENTIAL!,
});

// 把 key 与同一次登录尝试一起保存；丢包重试必须复用 key 和完整请求。
const idempotencyKey = crypto.randomUUID();
const { data } = await auth.createSession({
  external_user_id: 'stable-business-user-id',
  password: suppliedPassword,
}, { idempotencyKey });
// 不记录 data.session_token，不把整个响应转发给浏览器。
```

方法：createChallenge、verifyChallenge、registerUser、createSession、validateSession、refreshSession、revokeSession、resetPassword、setUserDisabled、revokeUserSessions、deleteUser、listAuthenticationEvents。返回 `{data, request_id}`。需要幂等的操作强制显式传 `idempotencyKey`；SDK 不自动重试、不切换域名。默认超时15秒，支持调用方 AbortSignal。

SDK 使用不可覆盖的 App / 环境上下文，拒绝非 HTTPS 远程地址、带凭证或查询串的 URL、服务端跳转；HTTP 仅允许 loopback。错误不携带请求体、密钥或供应商原始消息。类型检查之外，仍由服务端验证所有请求与 scopes。

## 登录和会话

`external_user_id` 是客户维护的稳定业务标识，不能通过邮箱自动合并。邮箱只是该身份已验证的绑定。新用户：challenge(register) → verify → registerUser；注册不会隐式创建会话。随后使用密码或新的 login proof 创建会话。

客户后端负责把 session_token 保存在服务端 session，或受保护的 HttpOnly / Secure / SameSite Cookie。Native 可通过客户后端签发其自有会话。v-auth Token 不作为服务调用凭证，也不赋予 Registry 下载、License 或业务角色。

- validateSession：200 active=false 表示无效；503 表示无法校验，不能当作退出或有效。校验不续期。
- refreshSession：显式续期，不换 Token，不延长绝对期限；过期或撤销后不能复活。
- revokeSession：退出对应会话，重复撤销仍成功。
- 禁用、密码重置、全部撤销会提升安全版本，旧会话 / proof 失效；恢复账号不恢复旧会话。

默认会话7天，绝对期限30天。客户可以提前刷新自己的 Cookie 到期时间，但必须以 v-auth 的真实 expires_at 为上限。

## 错误与重试

`VAuthError` 提供 code、status、retryable、requestId、retryAfterSeconds、idempotencyKey。NETWORK_ERROR / REQUEST_TIMEOUT 的结果可能已经提交；保持原 key 重试。REQUEST_ABORTED 不自动重试。401 必须区分调用密钥失败和用户认证失败。403 检查 scope、App 和环境。429 遵守 Retry-After。409 幂等冲突不得更换 key 掩盖载荷不一致。

出错日志只记录 request_id、操作名和错误代码，不记录请求体。SDK 无自动重试；上层须设置总时限和次数。创建密钥只支持单次交付，丢失明文时撤销并重新创建。

所有 HTTP 尝试（包括413）有服务器生成的 X-Request-Id，与认证 JSON 的 request_id 一致；SDK 错误可读取 requestId。每次重试编号不同，稳定的 Idempotency-Key 保持原值。运营检索及日志边界见[请求诊断](./request-diagnostics.md)。

## 本地示例

仓库提供 `examples/backend.ts`，使用 Node 24 的 TypeScript 支持：

```sh
VAUTH_BASE_URL=https://auth-api.virtneu.com \
VAUTH_APPLICATION_ID=<app-id> VAUTH_ENVIRONMENT=test \
VAUTH_SERVICE_CREDENTIAL=<backend-secret> \
pnpm exec node --experimental-transform-types examples/backend.ts
```

示例仅监听127.0.0.1，提供登录、当前身份、续期和退出。生产接入须接入自己的用户映射、限速、HTTPS、CSRF和业务权限；先在 test 验证，再单独配置 production。示例不是托管登录页。

密码登录防护按 App / 环境 / 外部用户身份共享失败预算，不因调用密钥轮换而重置。默认10分钟最多10次失败或尚未完成的尝试，成功只释放本次占用。429 RATE_LIMITED 后遵守 Retry-After，以新操作键发起新的登录尝试；已有会话与邮箱 proof 登录不受该密码预算影响。详见[密码防护](password-protection.md)。

认证排障可用独立 auth-events:read 密钥调用 listAuthenticationEvents({request_id})。返回白名单请求结果及签名分页，不含用户身份或秘密。查询不需要幂等键，旧密钥权限保持不变；见[认证请求事件](authentication-events.md)。
