PINNACLE_PROXY_AUTH.md 4.9 KB

Pinnacle Proxy API Auth

/api/pinnacle 不需要登录态,但每个调用方需要配置独立用户和密钥,并通过动态签名访问接口。

服务端配置

pinnacle/.env 中配置:

PINNACLE_PROXY_USERS=alice:aliceSecret:/events,/v3/sports;bob:bobSecret:*

格式:

用户名:密钥:权限列表;用户名:密钥:权限列表

说明:

  • 用户名:前端请求时放在 X-Api-User
  • 密钥:只保存在服务端和可信调用方,不在请求中明文传递。
  • 权限列表:逗号分隔,写 /api/pinnacle 后面的实际路径。
  • * 表示允许访问所有 /api/pinnacle 接口。

示例:

PINNACLE_PROXY_USERS=viewer:viewerSecret:/events,/v2/currencies;trader:traderSecret:/events,/v4/bets/place,/v4/bets/parlay

对应权限:

  • viewer 可以访问 /api/pinnacle/events/api/pinnacle/v2/currencies
  • trader 可以访问 /api/pinnacle/events/api/pinnacle/v4/bets/place/api/pinnacle/v4/bets/parlay

请求头

前端每次请求 /api/pinnacle 都需要带:

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

说明:

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