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