# VimineServer

> @vimine/sdk/server 中供机密客户端使用的 VimineServer 类的参考。

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

```js
import { VimineServer } from '@vimine/sdk/server';
const vimineServer = new VimineServer({ clientId: 'vmc_…', clientSecret: process.env.VIMINE_SECRET });
```

用于协会**自己的**系统：一个通过 `client_credentials` 认证的机密客户端，读取会员卡状态并操作其沙盒。需要 Node ≥ 18。**secret 绝不能出现在浏览器中。**

## 选项

| 选项 | 类型 | 默认值 | |
|---|---|---|---|
| `clientId` | `string` | — | 必填 |
| `clientSecret` | `string` | — | 必填；`vms_…` 或 `vms_test_…` |
| `scopes` | `Scope[]` | 所有已登记的权限 | 要申请的子集 |
| `apiUrl` | `string` | `https://api.vimine.network` | |
| `fetch` | `typeof fetch` | `globalThis.fetch` | |

## 方法

### `token()`

客户端访问令牌（`client_credentials`，HTTP Basic），缓存至到期（1 小时）前 30 秒；即使被并发调用，也只发出一次请求。

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

一次带 `Authorization: Bearer` 的认证调用；输入输出均为 JSON；错误以 [`VimineError`](https://developers.vimine.network/zh-hans/sdk/errors/) 抛出。收到 `401`（凭据已轮换或已撤销）时，用新令牌重试一次。

### `memberships.status(id)`

协会某个会员资格的状态，不含个人数据（`memberships:read`）：

```js
const s = await vimineServer.memberships.status('membership-uuid');
// { id, status: 'active', verdict: 'valid', valid_until: '2027-03-31T22:59:59.000Z', membership_type: { cod: 'ORD' } }
```

### `signup(input, opts?)`

为某人创建 Vimine 账户（`identity:signup`）：当事人会收到邮件，用于确认邮箱地址并设置密码。始终返回 `{ status: 'accepted' }`——不会告诉您该地址是否已有账户（邮箱的所有者会通过邮件得知）。`opts.idempotencyKey`（未提供时自动生成）。

```js
await vimineServer.signup({ email: 'mario.rossi@example.org', first_name: 'Mario', last_name: 'Rossi', phone: '+39 …' });
```

### `enroll(input, opts?)`

为某人提交入会申请（`enroll:write`）：新地址会获得一个账户和一份*待处理*的申请；已有账户的所有者则会收到一个确认链接（在其使用链接之前不写入任何内容）。`accept_statuto`（现行版本）为必填；当协会发布了面向会员的隐私声明时（`association(slug).privacy_notice`），`privacy_notice` 也为必填：当事人在您的网站上阅读后，您传入所展示的版本。参见[服务器集成](https://developers.vimine.network/zh-hans/guides/server/#从您的系统为会员办理入会)。

识别当事人，要么通过 `codicefiscale`，**要么**通过 `dob` + `birthplace` + `birth_country`——两者不能都没有（`422`，每缺少一项出生信息就有一条 `required`）。税号与出生信息不一致时，`err.errors` 中会有一条 `codicefiscale_mismatch` 记录，带有 `mismatches`、`hints` 和 `confirmable`：当 `confirmable` 为 `true` 且当事人确认时，请带上 `codicefiscale_confirmed: true` 重复调用。如果协会登记会员的地址（`aps.member_address`），请传入 `address`（`MemberAddress`）。参见[没有意大利税号的会员](https://developers.vimine.network/zh-hans/guides/members-without-cf/)。

```js
// 有税号
await vimineServer.enroll({ ...person, codicefiscale: 'RSSMRA80A01H501U', accept_statuto: { version: 3 }, privacy_notice: { version: 2 } }, { idempotencyKey: 'order-42' });
// 没有税号：出生日期、出生地和出生国
await vimineServer.enroll({ ...person, dob: '1985-03-10', birthplace: 'Lyon', birth_country: 'FR', accept_statuto: { version: 3 }, privacy_notice: { version: 2 } });
```

### `checkins.create(token)`

用从二维码中读取的令牌记录一次入场登记（`checkins:write`）。会员卡返回 `{ kind: 'card', verdict, status, membership_id, valid_until, valid_from, identity_check, checked_in_at }`——`identity_check: 'pending'` 是需要显示给入口值守人员的提示，它不属于 `verdict`，也不限制任何操作（[指南](https://developers.vimine.network/zh-hans/guides/members-without-cf/#入场时身份核对)）；活动报名的二维码返回 `{ kind: 'registration', verdict, registration_id, event, first, membership_verdict, … }`（参见[活动与报名](https://developers.vimine.network/zh-hans/guides/events/#入场时)）。

### `events`

`events.list(params?)` · `events.get(slug)`——协会已发布的活动，包括仅限会员的活动（任何客户端令牌均可）。

### `registrations`

| | |
|---|---|
| `registrations.listForEvent(slug, { status?, limit?, cursor? })` | 某个活动的报名：id、状态、类型、会员资格、入场登记——不含个人数据（`registrations:read`，适用于支持 API 列表的套餐） |
| `registrations.guest(slug, { email, first_name, last_name, registration_type, note? }, opts?)` | 为没有账户的**嘉宾**报名公开活动；始终返回 `{ status: 'accepted' }`；自动生成 `Idempotency-Key`（参见[嘉宾](https://developers.vimine.network/zh-hans/guides/events/#嘉宾公开活动)） |

### `memberships.list(params?)`

协会的会员资格列表（`memberships:read`，适用于支持列表的套餐）：`{ items: [{ id, status, verdict, membership_type, issued_at, valid_until, updated_at }], next_cursor }`；`params`：`status`、`updated_since`、`limit`、`cursor`。

### `sandbox.members()` · `sandbox.reset()`

仅限测试客户端：[沙盒](https://developers.vimine.network/zh-hans/guides/sandbox/)中的虚构会员，以及重置（新的共用密码，只返回一次）。生产客户端会得到 `403`。

### `public`

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

## 中间件示例

```js title="express.js"
import express from 'express';
import { VimineServer, VimineError } from '@vimine/sdk/server';

const vimineServer = new VimineServer({ clientId: process.env.VIMINE_CLIENT_ID, clientSecret: process.env.VIMINE_SECRET });
const app = express();

app.get('/cards/:id/status', async (req, res) => {
    try {
        const { status, verdict, valid_until } = await vimineServer.memberships.status(req.params.id);
        res.json({ status, verdict, valid_until });
    } catch (err) {
        if (err instanceof VimineError && err.code === 'not_found') return res.status(404).end();
        throw err;
    }
});
```
