# Pinnacle Proxy Frontend Auth

前端访问 `/api/pinnacle` 时，需要使用动态签名。请求中不会直接传密钥。

## 访问地址

公网访问：

```txt
https://cb.long.bid
```

内网访问：

```txt
http://172.27.74.243
```

接口路径统一以 `/api/pinnacle` 开头，例如：

```txt
https://cb.long.bid/api/pinnacle/events
http://172.27.74.243/api/pinnacle/events
```

## 支持的接口

本地数据接口：

```txt
GET  /api/pinnacle/events
POST /api/pinnacle/events
```

Pinnacle Lines API：

```txt
GET  /api/pinnacle/v2/line
POST /api/pinnacle/v3/line/parlay
POST /api/pinnacle/v1/line/teaser
GET  /api/pinnacle/v2/line/special
GET  /api/pinnacle/v3/sports
GET  /api/pinnacle/v3/leagues
GET  /api/pinnacle/v1/periods
GET  /api/pinnacle/v2/inrunning
GET  /api/pinnacle/v1/cancellationreasons
GET  /api/pinnacle/v2/currencies
GET  /api/pinnacle/v1/client/balance
```

Pinnacle Bets API：

```txt
POST /api/pinnacle/v4/bets/place
POST /api/pinnacle/v4/bets/parlay
POST /api/pinnacle/v4/bets/teaser
POST /api/pinnacle/v4/bets/special
GET  /api/pinnacle/v3/bets
GET  /api/pinnacle/v3/bets/settled
GET  /api/pinnacle/v1/regrades/wager-history
GET  /api/pinnacle/v1/bets/betting-status
```

签名中的 `PATH` 不包含 `/api/pinnacle` 前缀。例如请求 `/api/pinnacle/events`，签名时 `PATH` 使用 `/events`。

## /events 参数

`/events` 是本地数据接口，不请求上游 Pinnacle，直接读取服务端内存中的 `GLOBAL_DATA.gamesMap`。

GET 支持 query 参数：

```txt
GET /api/pinnacle/events
GET /api/pinnacle/events?id=123
GET /api/pinnacle/events?ids=123,456
GET /api/pinnacle/events?eventId=123
GET /api/pinnacle/events?eventIds=123,456
```

POST 支持 JSON body：

```json
{ "id": 123 }
```

```json
{ "ids": [123, 456] }
```

```json
{ "eventId": 123 }
```

```json
{ "eventIds": "123,456" }
```

参数优先级：

```txt
ids -> id -> eventIds -> eventId
```

返回说明：

- 始终返回数组。
- 传入单个 ID 时，返回匹配到的 0 或 1 条数据。
- 传入多个 ID 时，返回匹配到的多条数据。
- 不传 ID 时，返回当前 `GLOBAL_DATA.gamesMap` 中的全部赛事。
- 找不到 ID 时不会报错，只是不返回对应项。

## 请求头

每次请求都需要带：

```txt
X-Api-User: alice
X-Api-Timestamp: 1710000000000
X-Api-Nonce: random-string
X-Api-Signature: hmac-sha256-hex
```

说明：

- `X-Api-User`：服务端 `.env` 中配置的用户名。
- `X-Api-Timestamp`：当前毫秒时间戳，允许和服务端相差 5 分钟。
- `X-Api-Nonce`：每次请求都要不同，防止重放。
- `X-Api-Signature`：用用户密钥对签名内容做 HMAC-SHA256。

## 签名内容

签名字符串由 6 行组成：

```txt
METHOD
PATH
QUERY_JSON
BODY_SHA256
TIMESTAMP
NONCE
```

示例：

```txt
GET
/events
{"id":"123"}
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
1710000000000
abc123
```

其中：

- `METHOD`：大写 HTTP 方法，例如 `GET`、`POST`
- `PATH`：`/api/pinnacle` 后面的路径，例如 `/events`、`/v4/bets/place`
- `QUERY_JSON`：query 参数按 key 排序后的 JSON 字符串
- `BODY_SHA256`：body JSON 按 key 排序后做 SHA256；空 body 使用空字符串的 SHA256
- `TIMESTAMP`：同 `X-Api-Timestamp`
- `NONCE`：同 `X-Api-Nonce`

## 签名工具

浏览器端可使用 Web Crypto API：

```js
const stableStringify = (value) => {
  if (Array.isArray(value)) {
    return `[${value.map(stableStringify).join(',')}]`;
  }

  if (value && typeof value === 'object') {
    return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
  }

  return JSON.stringify(value);
};

const toHex = (buffer) => {
  return [...new Uint8Array(buffer)]
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
};

const sha256 = async (text) => {
  const data = new TextEncoder().encode(text);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return toHex(hash);
};

const hmacSha256 = async (secret, text) => {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(text));
  return toHex(signature);
};

const createPinnacleHeaders = async ({ method, path, query = {}, body, user, secret }) => {
  const timestamp = String(Date.now());
  const nonce = crypto.randomUUID();
  const bodyText = body === undefined || body === null ? '' : stableStringify(body);
  const bodyHash = await sha256(bodyText);

  const payload = [
    method.toUpperCase(),
    path,
    stableStringify(query),
    bodyHash,
    timestamp,
    nonce,
  ].join('\n');

  const signature = await hmacSha256(secret, payload);

  return {
    'X-Api-User': user,
    'X-Api-Timestamp': timestamp,
    'X-Api-Nonce': nonce,
    'X-Api-Signature': signature,
  };
};
```

## GET 示例

```js
const user = 'alice';
const secret = 'aliceSecret';
const path = '/events';
const query = { ids: '123,456' };
const search = new URLSearchParams(query).toString();

const headers = await createPinnacleHeaders({
  method: 'GET',
  path,
  query,
  user,
  secret,
});

const res = await fetch(`/api/pinnacle${path}?${search}`, { headers });
const data = await res.json();
```

## POST 示例

```js
const user = 'trader';
const secret = 'traderSecret';
const path = '/v4/bets/place';
const body = {
  uniqueRequestId: crypto.randomUUID(),
  stake: 100,
  winRiskStake: 'RISK',
  oddsFormat: 'DECIMAL',
  lineId: 123456,
};

const headers = await createPinnacleHeaders({
  method: 'POST',
  path,
  body,
  user,
  secret,
});

const res = await fetch(`/api/pinnacle${path}`, {
  method: 'POST',
  headers: {
    ...headers,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});

const data = await res.json();
```

## 常见错误

- `401 Missing API signature headers`：缺少签名请求头。
- `401 Invalid API timestamp`：时间戳过期或不是毫秒时间戳。
- `401 API nonce has already been used`：nonce 重复。
- `401 Invalid API signature`：签名不匹配。
- `403 Forbidden`：用户没有该接口权限。
