webhooks.ts 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
  1. import { createCipheriv, createHash, createHmac, hkdfSync, randomBytes } from 'node:crypto'
  2. /**
  3. * Standing in for a messaging vendor.
  4. *
  5. * A connection's keys are sealed before they reach the database, and the
  6. * vault derives its key from `BETTER_AUTH_SECRET` when no dedicated one is
  7. * set, which is how the suite's server runs. Sealing here, the same way,
  8. * lets a spec plant a connection without a vendor to test the keys against;
  9. * `src/features/integrations/Lib/vault.ts` is the original.
  10. */
  11. /** The secret the app server signs sessions with; the config's fallback when unset. */
  12. const AUTH_SECRET = process.env.BETTER_AUTH_SECRET ?? 'k3Qb8vZ1hN7pXtR2yJm5Ls9CwD4gFa6UeH0iOoT+PbY='
  13. export function sealCredentials(value: Record<string, unknown>): string {
  14. const key = Buffer.from(hkdfSync('sha256', AUTH_SECRET, 'torqvoice', 'integrations-vault', 32))
  15. const iv = randomBytes(12)
  16. const cipher = createCipheriv('aes-256-gcm', key, iv)
  17. const encrypted = Buffer.concat([
  18. cipher.update(Buffer.from(JSON.stringify(value), 'utf8')),
  19. cipher.final(),
  20. ])
  21. return [
  22. 'v1',
  23. iv.toString('base64url'),
  24. cipher.getAuthTag().toString('base64url'),
  25. encrypted.toString('base64url'),
  26. ].join('.')
  27. }
  28. /** How the app files a connection's inbound URL secret, so the route can find the workshop. */
  29. export function webhookSecretHash(secret: string): string {
  30. return createHash('sha256').update(secret).digest('hex')
  31. }
  32. /**
  33. * What Twilio puts in `X-Twilio-Signature`: HMAC-SHA1 over the URL as
  34. * registered in its console followed by every form field, sorted by name,
  35. * keyed with the account's auth token.
  36. */
  37. export function twilioSignature(
  38. authToken: string,
  39. url: string,
  40. params: Record<string, string>
  41. ): string {
  42. let data = url
  43. for (const key of Object.keys(params).sort()) data += key + params[key]
  44. return createHmac('sha1', authToken).update(data, 'utf8').digest('base64')
  45. }