webhooks.mjs 5.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
  2. var _Webhooks_instances, _Webhooks_validateSecret, _Webhooks_getRequiredHeader;
  3. import { __classPrivateFieldGet } from "../internal/tslib.mjs";
  4. import { InvalidWebhookSignatureError } from "../error.mjs";
  5. import { APIResource } from "../core/resource.mjs";
  6. import { buildHeaders } from "../internal/headers.mjs";
  7. export class Webhooks extends APIResource {
  8. constructor() {
  9. super(...arguments);
  10. _Webhooks_instances.add(this);
  11. }
  12. /**
  13. * Validates that the given payload was sent by OpenAI and parses the payload.
  14. */
  15. async unwrap(payload, headers, secret = this._client.webhookSecret, tolerance = 300) {
  16. await this.verifySignature(payload, headers, secret, tolerance);
  17. return JSON.parse(payload);
  18. }
  19. /**
  20. * Validates whether or not the webhook payload was sent by OpenAI.
  21. *
  22. * An error will be raised if the webhook payload was not sent by OpenAI.
  23. *
  24. * @param payload - The webhook payload
  25. * @param headers - The webhook headers
  26. * @param secret - The webhook secret (optional, will use client secret if not provided)
  27. * @param tolerance - Maximum age of the webhook in seconds (default: 300 = 5 minutes)
  28. */
  29. async verifySignature(payload, headers, secret = this._client.webhookSecret, tolerance = 300) {
  30. if (typeof crypto === 'undefined' ||
  31. typeof crypto.subtle.importKey !== 'function' ||
  32. typeof crypto.subtle.verify !== 'function') {
  33. throw new Error('Webhook signature verification is only supported when the `crypto` global is defined');
  34. }
  35. __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_validateSecret).call(this, secret);
  36. const headersObj = buildHeaders([headers]).values;
  37. const signatureHeader = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-signature');
  38. const timestamp = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-timestamp');
  39. const webhookId = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-id');
  40. // Validate timestamp to prevent replay attacks
  41. const timestampSeconds = parseInt(timestamp, 10);
  42. if (isNaN(timestampSeconds)) {
  43. throw new InvalidWebhookSignatureError('Invalid webhook timestamp format');
  44. }
  45. const nowSeconds = Math.floor(Date.now() / 1000);
  46. if (nowSeconds - timestampSeconds > tolerance) {
  47. throw new InvalidWebhookSignatureError('Webhook timestamp is too old');
  48. }
  49. if (timestampSeconds > nowSeconds + tolerance) {
  50. throw new InvalidWebhookSignatureError('Webhook timestamp is too new');
  51. }
  52. // Extract signatures from v1,<base64> format
  53. // The signature header can have multiple values, separated by spaces.
  54. // Each value is in the format v1,<base64>. We should accept if any match.
  55. const signatures = signatureHeader
  56. .split(' ')
  57. .map((part) => (part.startsWith('v1,') ? part.substring(3) : part));
  58. // Decode the secret if it starts with whsec_
  59. const decodedSecret = secret.startsWith('whsec_') ?
  60. Buffer.from(secret.replace('whsec_', ''), 'base64')
  61. : Buffer.from(secret, 'utf-8');
  62. // Create the signed payload: {webhook_id}.{timestamp}.{payload}
  63. const signedPayload = webhookId ? `${webhookId}.${timestamp}.${payload}` : `${timestamp}.${payload}`;
  64. // Import the secret as a cryptographic key for HMAC
  65. const key = await crypto.subtle.importKey('raw', decodedSecret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
  66. // Check if any signature matches using timing-safe WebCrypto verify
  67. for (const signature of signatures) {
  68. try {
  69. const signatureBytes = Buffer.from(signature, 'base64');
  70. const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, new TextEncoder().encode(signedPayload));
  71. if (isValid) {
  72. return; // Valid signature found
  73. }
  74. }
  75. catch {
  76. // Invalid base64 or signature format, continue to next signature
  77. continue;
  78. }
  79. }
  80. throw new InvalidWebhookSignatureError('The given webhook signature does not match the expected signature');
  81. }
  82. }
  83. _Webhooks_instances = new WeakSet(), _Webhooks_validateSecret = function _Webhooks_validateSecret(secret) {
  84. if (typeof secret !== 'string' || secret.length === 0) {
  85. throw new Error(`The webhook secret must either be set using the env var, OPENAI_WEBHOOK_SECRET, on the client class, OpenAI({ webhookSecret: '123' }), or passed to this function`);
  86. }
  87. }, _Webhooks_getRequiredHeader = function _Webhooks_getRequiredHeader(headers, name) {
  88. if (!headers) {
  89. throw new Error(`Headers are required`);
  90. }
  91. const value = headers.get(name);
  92. if (value === null || value === undefined) {
  93. throw new Error(`Missing required header: ${name}`);
  94. }
  95. return value;
  96. };
  97. //# sourceMappingURL=webhooks.mjs.map