PINNACLE_PROXY_FRONTEND.md 6.1 KB

Pinnacle Proxy Frontend Auth

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

访问地址

公网访问:

https://cb.long.bid

内网访问:

http://172.27.74.243

接口路径统一以 /api/pinnacle 开头,例如:

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

支持的接口

本地数据接口:

GET  /api/pinnacle/events
POST /api/pinnacle/events

Pinnacle Lines API:

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:

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 参数:

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:

{ "id": 123 }
{ "ids": [123, 456] }
{ "eventId": 123 }
{ "eventIds": "123,456" }

参数优先级:

ids -> id -> eventIds -> eventId

返回说明:

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

请求头

每次请求都需要带:

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 行组成:

METHOD
PATH
QUERY_JSON
BODY_SHA256
TIMESTAMP
NONCE

示例:

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

其中:

  • METHOD:大写 HTTP 方法,例如 GETPOST
  • 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:

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 示例

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 示例

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:用户没有该接口权限。