/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/currenciestrader 可以访问 /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 方法,例如 GET、POSTPATH:/api/pinnacle 后面的路径,例如 /events、/v4/bets/placeQUERY_JSON:query 参数按 key 排序后的 JSON 字符串BODY_SHA256:body JSON 按 key 排序后做 SHA256;空 body 使用空字符串的 SHA256TIMESTAMP:同 X-Api-TimestampNONCE:同 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:用户没有该接口权限。