# Vimine（瀏覽器）

> @vimine/sdk 中 Vimine 類別的參考——登入、工作階段、API 呼叫、儲存。

URL: https://developers.vimine.network/zh-hant/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-hant/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` | 會員卡權杖（QR Code）及其有效時段；會員資格未生效時傳回 `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 格式的會員卡（帶 QR Code）；會員資格未生效時傳回 `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-hant/guides/events/)指南。

| | |
|---|---|
| `registrations.create(slug, { registration_type, note?, quantity? }, opts?)` | 免費類型 → `confirmed`，帶 QR Code `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)` | 已確認報名的 QR Code 內容，或 `null` |

### `connections`

| | |
|---|---|
| `connections.list()` | 會員在本協會中授權過的應用 |
| `connections.revoke(clientId)` | 撤回對某個應用的同意（其工作階段隨即結束） |

### `public`

無需憑證的公開端點：參見 [`publicApi`](https://developers.vimine.network/zh-hant/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-hant/sdk/public/)。
- `VimineError`——參見[錯誤](https://developers.vimine.network/zh-hant/sdk/errors/)。
