回调

任务完成和账单结算时,把通知推送到你的服务器。

地址签名密钥状态添加时间操作

校验签名

每个通知都带 Chenvoya-Signature 请求头。先用这个地址的签名密钥校验,再处理通知。

  1. 把签名密钥按 base64url 解码成 32 字节。
  2. 从 Chenvoya-Signature 取出 t 和 v1;t 与当前时间相差超过 5 分钟就拒绝。
  3. 对 chenvoya.v1. 加 t、一个点和原始请求正文计算 HMAC-SHA256,与 v1 做常量时间比较。
  4. 用正文里的 event_id 去重并保留至少 7 天 5 分钟,拒绝更早或 5 分钟后才创建的事件,处理成功返回 2xx。

验证地址时,把收到的 nonce 按 base64url 解码,对 chenvoya.verify. 加 nonce 计算 HMAC-SHA256,返回小写十六进制。

回调只是通知,任务状态和下载结果以查询任务为准。

Node.js 示例
// Node.js 22.3+
const { createHmac, timingSafeEqual } = process.getBuiltinModule("node:crypto");
function challenge(nonce, signingSecret) {
  if (Buffer.from(signingSecret, "base64url").length !== 32 || Buffer.from(nonce, "base64url").length !== 32) throw new Error();
  return createHmac("sha256", Buffer.from(signingSecret, "base64url"))
    .update("chenvoya.verify.").update(Buffer.from(nonce, "base64url")).digest("hex");
}
function receive(rawBody, signature, signingSecret, recordOnce) {
  if (Buffer.from(signingSecret, "base64url").length !== 32) return 400;
  const match = /^t=([0-9]+),v1=([0-9a-f]{64})$/.exec(signature);
  if (!match) return 400;
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isSafeInteger(Number(match[1])) || Math.abs(now - Number(match[1])) > 300) return 400;
  const expected = createHmac("sha256", Buffer.from(signingSecret, "base64url"))
    .update("chenvoya.v1." + match[1] + ".").update(rawBody).digest();
  if (!timingSafeEqual(expected, Buffer.from(match[2], "hex"))) return 400;
  const event = JSON.parse(rawBody);
  if (!Number.isSafeInteger(event.created_at) || now - event.created_at > 605100 || event.created_at - now > 300) return 400;
  recordOnce(event.event_id, event, 605100);
  return 204;
}