# Vimine（浏览器）

> @vimine/sdk 中 Vimine 类的参考——登录、会话、API 调用、存储。

URL: https://developers.vimine.network/zh-hans/sdk/browser/

```js
import { Vimine } from '@vimine/sdk';
const vimine = new Vimine(options);
```

`Vimine` 负责**使用 Vimine 登录**（OAuth 2.1 + PKCE）、会员的会话以及对会员 API 的调用。每个客户端一个实例；创建一次，反复使用。

## 选项

| 选项 | 类型 | 默认值 | |
|---|---|---|---|
| `clientId` | `string` | — | 您的 `vmc_…`（必填） |
| `redirectUri` | `string` | — | 调用 `handleRedirect()` 的页面，已为该客户端登记（必填） |
| `scopes` | `Scope[]` | `['profile:read', 'membership:read']` | `signIn()` 申请的权限 |
| `storage` | `'session' \| 'local' \| 'memory' \| TokenStorage` | `'session'` | 会话和进行中的登录保存在哪里 |
| `storageKey` | `string` | `vimine:<clientId>:` | 存储中的键前缀 |
| `locale` | `Locale` | `'it'` | 登录和授权同意页面的语言 |
| `apiUrl` | `string` | `https://api.vimine.network` | API 的来源 |
| `appUrl` | `string` | `https://app.vimine.network` | 登录页面的来源 |
| `fetch` | `typeof fetch` | `globalThis.fetch` | 用于测试和 polyfill |
| `navigate` | `(url) => void` | `top.location.assign` | 如何离开当前页面 |

缺少 `clientId` 或 `redirectUri` 时，构造函数抛出 `VimineError`（`invalid_options`）。

## 登录

### `signIn(options?)`

把**顶层窗口**送到登录和授权同意页面。请在用户操作（一次点击）中调用。在跨域 iframe 内部，导航会被拒绝（`framed`）：登录页面从不显示在框架中。

| 选项 | |
|---|---|
| `scopes` | 本次登录的权限 |
| `prompt` | `'login'`（始终要求输入登录信息）或 `'consent'`（始终要求同意） |
| `returnTo` | 您自己的状态，保存在本地，由 `handleRedirect()` 返回——不会被发送 |

### `createSignInUrl(options?)`

构建授权 URL 并记住进行中的登录（PKCE verifier 和 `state`），但不进行跳转。适合您自己渲染链接的场景。

### `hasRedirectParams(url?)`

当 URL（默认：当前页面）带有返回参数（`code`+`state` 或 `error`）时为 `true`。

### `handleRedirect({ url?, cleanUrl? })`

在回调页面上：检查 `state`，用授权码交换令牌并保存会话。返回 `{ session, returnTo }`；URL 中没有登录参数时返回 `null`。启用 `cleanUrl`（默认 `true`）时，会从地址栏中移除 `code`、`state` 和 `error`。

错误（`VimineError.code`）：会员未授权时为 `access_denied` 及其他 OAuth 错误；本浏览器没有发起过登录时（存储为空或已过期）为 `no_flow`；返回不匹配时为 `state_mismatch`；授权码已过期或已被使用时为 `invalid_grant`。

## 会话

### `getSession()` · `isSignedIn()`

已保存的会话，或 `null`。

```ts
interface Session {
    accessToken: string;
    refreshToken: string | null;
    expiresAt: number;            // 毫秒，绝对时间
    scope: Scope[];
    sub: string | null;           // 会员在您的协会中的标识（vms_…）
    mode?: 'live' | 'test';       // 测试客户端的会话为 'test'
}
```

### `getAccessToken()`

一个有效的访问令牌；若在 30 秒内到期则先刷新。没有会话时抛出 `not_signed_in`。

### `refresh()`

用刷新令牌轮换会话（即使被并发调用，也只发出一次请求）。如果刷新令牌已无法使用，会话会被清除，错误会被重新抛出。

### `signOut({ everywhere?, postLogoutRedirectUri?, state? })`

撤销刷新令牌（RFC 7009，尽力而为）并清除会话。使用 `everywhere: true` 时，随后会把浏览器送到 Vimine 应用的 `/oauth/logout`，结束会员的会话，再返回 `postLogoutRedirectUri`（默认为 `redirectUri`；必须位于已登记的来源上）。

### `onChange(listener)`

每次登录、刷新和退出登录时调用 `listener(session | null)`；使用 `storage: 'local'` 时，其他标签页中发生的这些事件也会触发。返回取消订阅的函数。

## 会员 API

### `request(path, init?)`

一次经过认证的调用：`Authorization: Bearer`，输入输出均为 JSON，错误以 `VimineError` 抛出，响应为 `401` 时在刷新后重试一次。`init.idempotencyKey` 会成为 `Idempotency-Key` 请求头。

```js
const events = await vimine.request('/v1/me/membership/events?limit=5');
```

### `me`

| | Scope | |
|---|---|---|
| `me.get()` | `profile:read` | 个人资料（`Me`），带有 `codicefiscale_check`：当事人的身份识别方式（`null` = 尚未识别） |
| `me.update(patch)` | `profile:write` | 电话、性别，以及身份识别信息：出生日期、出生地和出生国（`birth_country`）、税号——可选，可以之后再补充。不一致的税号返回 `422`，带有一条 `codicefiscale_mismatch` 记录；当它为 `confirmable` 时，当事人可以用 `codicefiscale_confirmed: true` 确认（[指南](https://developers.vimine.network/zh-hans/guides/members-without-cf/)） |

### `membership`

| | Scope | |
|---|---|---|
| `membership.get()` | `membership:read` | 会员卡：`status`、`verdict`、`expires_at`、`statuto`、`card` |
| `membership.request(body, { idempotencyKey? })` | `membership:request` | 入会申请；未提供幂等键时会自动生成。个人资料尚未识别身份时返回 `422 identification_required`：`err.errors` 列出缺少的内容，用 `me.update()` 补全 |
| `membership.events({ limit?, cursor? })` | `membership:read` | 状态历史，分页 |
| `membership.statuto()` | `membership:read` | 现行章程及会员的接受情况 |
| `membership.acceptStatuto({ version, sha256? })` | `statuto:accept` | 记录接受 |
| `membership.card()` | `membership:read` | 会员卡令牌（二维码）及其时间窗口；会员资格未生效时返回 `409 not_active` |
| `membership.renew({ version, sha256? })` | `POST /v1/me/membership/renew` | 自助续期：免费且无需审批时立即完成，否则向秘书处提交申请 |
| `membership.setNetworkOptIn(bool)` | `PATCH /v1/me/membership/network-opt-in` | 加入网络的同意（`network:manage`） |
| `membership.googleWalletUrl()` | `GET /v1/me/membership/wallet/google` | “添加到 Google Wallet”链接；会员资格未生效时返回 `409 not_active` |
| `membership.cardSvg()` | `GET /v1/me/membership/card.svg` | SVG 格式的会员卡（带二维码）；会员资格未生效时返回 `409 not_active` |
| `membership.emailCard()` | `POST /v1/me/membership/card/email` | 通过电子邮件把会员卡发给会员 |

### `events`

协会已发布的活动，包括仅限会员的活动（`events:read`）。公开活动无需登录：`vimine.public.events`。

| | |
|---|---|
| `events.list({ filter?, from?, to?, limit? })` | `filter`：`upcoming`（默认）、`past`、`cancelled`、`all` |
| `events.get(slug)` | 文案、图片、各报名类型及剩余名额 |
| `events.eligibility(slug)` | 会员能否报名？按类型给出，并附原因（`membership_required`、`sold_out` 等） |

### `registrations`

会员的报名（`registrations:read` / `registrations:write`）。参见[活动与报名](https://developers.vimine.network/zh-hans/guides/events/)指南。

| | |
|---|---|
| `registrations.create(slug, { registration_type, note?, quantity? }, opts?)` | 免费类型 → `confirmed`，带二维码 `token`；需付参与费 → `reserved`，带 `payment_instructions`；`422 not_eligible` 附原因；自动生成 `Idempotency-Key` |
| `registrations.list({ filter? })` · `registrations.get(id)` | 会员的报名（`filter: 'upcoming'`） |
| `registrations.cancel(id)` | 在活动的取消期限内取消（超过期限返回 `409 window_closed`） |
| `registrations.tokenPayload(id)` | 已确认报名的二维码内容，或 `null` |

### `connections`

| | |
|---|---|
| `connections.list()` | 会员在本协会中授权过的应用 |
| `connections.revoke(clientId)` | 撤回对某个应用的同意（其会话随即结束） |

### `public`

无需凭据的公开端点：参见 [`publicApi`](https://developers.vimine.network/zh-hans/sdk/public/)。

## 存储

| `storage` | 位置 | 适用场景 |
|---|---|---|
| `'session'`（默认） | `sessionStorage`：每个标签页独立，关闭即清除 | 对公开客户端而言最安全的默认值 |
| `'local'` | `localStorage`：多个标签页共享，重启后仍保留；退出登录会同步到其他标签页 | 会员期望保持登录状态的应用 |
| `'memory'` | 仅在内存中 | SSR、测试、自助终端 |
| `TokenStorage` | 您自己的对象 `{ get, set, remove }` | Node、加密存储、原生应用 |

在无法使用浏览器存储的情况下（隐私模式、`about:blank`），SDK 会退回到内存存储。`MemoryStorage` 已导出。

## 工具函数

- `createPkce()` → `{ verifier, challenge, method: 'S256' }`、`createVerifier()`、`createChallenge(verifier)`、`createState()`、`base64url(bytes)`——基于 WebCrypto 的 PKCE 和 `state`，供您手动构建流程时使用。
- `DEFAULT_API_URL`、`DEFAULT_APP_URL`。
- `publicApi(options?)`——参见[公开 API](https://developers.vimine.network/zh-hans/sdk/public/)。
- `VimineError`——参见[错误](https://developers.vimine.network/zh-hans/sdk/errors/)。
