webhooks.js 5.4 KB

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