webhook.mjs 2.63 KB
#!/usr/bin/env node
/**
 * Webhook 的真实链路脚本。目前覆盖 WH-03(非 https、回环、内网、元数据地址一律拒绝)与登记 / 取消。
 * WH-01、WH-02 需要一个公网可达的 HTTPS 接收端(平台拒绝向回环与内网地址投递,本机起的接收服务收不到),
 * 在 stage 环境验收;签名算法由 packages/voko-access/test/webhooks.test.js 覆盖。
 */
import { runScenario } from './_lib.mjs'

await runScenario('wh', async ({ db, check, call, createOwner, createGuild, createAgent }) => {
  const alice = await createOwner('alice')
  const guild = await createGuild('main', alice.user_id)
  const agent = await createAgent('hooked', alice, guild)
  const register = (webhook_url) => call('PATCH', '/v1/agents/me', { token: agent.key, body: { webhook_url } })

  for (const url of ['http://example.com/hook', 'https://127.0.0.1/hook', 'https://localhost/hook', 'https://10.0.0.5/hook', 'https://192.168.1.10/hook', 'https://169.254.169.254/latest/meta-data', 'https://[::1]/hook', 'https://user:pass@example.com/hook', 'https://no-such-host.invalid/hook']) {
    const refused = await register(url)
    check(`WH-03 ${url} → 400`, refused.status === 400 && refused.json.error.code === 'invalid_request' && typeof refused.json.error.hint === 'string', refused)
  }
  const stored = await db('vk_agents').where({ id: agent.id }).first('webhook_url', 'webhook_secret_enc')
  check('被拒绝的地址没有落库', stored.webhook_url === null && stored.webhook_secret_enc === null, stored)

  // 登记一个公网 https 地址后立刻取消,期间不产生任何事件,所以不会真的向它发请求。
  // 用 IP 字面量:开发机若开着 fake-ip 代理,域名会被解析到 198.18.0.0/15 保留段而被(正确地)拒绝。
  const ok = await register('https://1.1.1.1/voko-hook')
  const row = await db('vk_agents').where({ id: agent.id }).first('webhook_url', 'webhook_secret_enc')
  check('公网 https 地址登记成功,secret 只返回一次且加密存储', ok.status === 200 && /^[0-9a-f]{64}$/.test(ok.json.data.webhook_secret) && row.webhook_url === 'https://1.1.1.1/voko-hook' && !row.webhook_secret_enc.includes(ok.json.data.webhook_secret), ok)
  const me = await call('GET', '/v1/agents/me', { token: agent.key })
  check('之后的接口不再返回 secret', !JSON.stringify(me.json).includes(ok.json.data.webhook_secret), me)
  const cleared = await register(null)
  const after = await db('vk_agents').where({ id: agent.id }).first('webhook_url', 'webhook_secret_enc')
  check('取消登记', cleared.status === 200 && after.webhook_url === null && after.webhook_secret_enc === null, cleared)
})