# Pinnacle Proxy API Auth `/api/pinnacle` 不需要登录态,但每个调用方需要配置独立用户和密钥,并通过动态签名访问接口。 ## 服务端配置 在 `pinnacle/.env` 中配置: ```env PINNACLE_PROXY_USERS=alice:aliceSecret:/events,/v3/sports;bob:bobSecret:* ``` 格式: ```txt 用户名:密钥:权限列表;用户名:密钥:权限列表 ``` 说明: - `用户名`:前端请求时放在 `X-Api-User`。 - `密钥`:只保存在服务端和可信调用方,不在请求中明文传递。 - `权限列表`:逗号分隔,写 `/api/pinnacle` 后面的实际路径。 - `*` 表示允许访问所有 `/api/pinnacle` 接口。 示例: ```env 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` 都需要带: ```txt 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 行组成: ```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`:用户没有该接口权限。