97 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			97 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
 | ||
|  | var _Webhooks_instances, _Webhooks_validateSecret, _Webhooks_getRequiredHeader; | ||
|  | import { __classPrivateFieldGet } from "../internal/tslib.mjs"; | ||
|  | import { InvalidWebhookSignatureError } from "../error.mjs"; | ||
|  | import { APIResource } from "../core/resource.mjs"; | ||
|  | import { buildHeaders } from "../internal/headers.mjs"; | ||
|  | export class Webhooks extends APIResource { | ||
|  |     constructor() { | ||
|  |         super(...arguments); | ||
|  |         _Webhooks_instances.add(this); | ||
|  |     } | ||
|  |     /** | ||
|  |      * Validates that the given payload was sent by OpenAI and parses the payload. | ||
|  |      */ | ||
|  |     async unwrap(payload, headers, secret = this._client.webhookSecret, tolerance = 300) { | ||
|  |         await this.verifySignature(payload, headers, secret, tolerance); | ||
|  |         return JSON.parse(payload); | ||
|  |     } | ||
|  |     /** | ||
|  |      * Validates whether or not the webhook payload was sent by OpenAI. | ||
|  |      * | ||
|  |      * An error will be raised if the webhook payload was not sent by OpenAI. | ||
|  |      * | ||
|  |      * @param payload - The webhook payload | ||
|  |      * @param headers - The webhook headers | ||
|  |      * @param secret - The webhook secret (optional, will use client secret if not provided) | ||
|  |      * @param tolerance - Maximum age of the webhook in seconds (default: 300 = 5 minutes) | ||
|  |      */ | ||
|  |     async verifySignature(payload, headers, secret = this._client.webhookSecret, tolerance = 300) { | ||
|  |         if (typeof crypto === 'undefined' || | ||
|  |             typeof crypto.subtle.importKey !== 'function' || | ||
|  |             typeof crypto.subtle.verify !== 'function') { | ||
|  |             throw new Error('Webhook signature verification is only supported when the `crypto` global is defined'); | ||
|  |         } | ||
|  |         __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_validateSecret).call(this, secret); | ||
|  |         const headersObj = buildHeaders([headers]).values; | ||
|  |         const signatureHeader = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-signature'); | ||
|  |         const timestamp = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-timestamp'); | ||
|  |         const webhookId = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, 'webhook-id'); | ||
|  |         // Validate timestamp to prevent replay attacks
 | ||
|  |         const timestampSeconds = parseInt(timestamp, 10); | ||
|  |         if (isNaN(timestampSeconds)) { | ||
|  |             throw new InvalidWebhookSignatureError('Invalid webhook timestamp format'); | ||
|  |         } | ||
|  |         const nowSeconds = Math.floor(Date.now() / 1000); | ||
|  |         if (nowSeconds - timestampSeconds > tolerance) { | ||
|  |             throw new InvalidWebhookSignatureError('Webhook timestamp is too old'); | ||
|  |         } | ||
|  |         if (timestampSeconds > nowSeconds + tolerance) { | ||
|  |             throw new InvalidWebhookSignatureError('Webhook timestamp is too new'); | ||
|  |         } | ||
|  |         // Extract signatures from v1,<base64> format
 | ||
|  |         // The signature header can have multiple values, separated by spaces.
 | ||
|  |         // Each value is in the format v1,<base64>. We should accept if any match.
 | ||
|  |         const signatures = signatureHeader | ||
|  |             .split(' ') | ||
|  |             .map((part) => (part.startsWith('v1,') ? part.substring(3) : part)); | ||
|  |         // Decode the secret if it starts with whsec_
 | ||
|  |         const decodedSecret = secret.startsWith('whsec_') ? | ||
|  |             Buffer.from(secret.replace('whsec_', ''), 'base64') | ||
|  |             : Buffer.from(secret, 'utf-8'); | ||
|  |         // Create the signed payload: {webhook_id}.{timestamp}.{payload}
 | ||
|  |         const signedPayload = webhookId ? `${webhookId}.${timestamp}.${payload}` : `${timestamp}.${payload}`; | ||
|  |         // Import the secret as a cryptographic key for HMAC
 | ||
|  |         const key = await crypto.subtle.importKey('raw', decodedSecret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']); | ||
|  |         // Check if any signature matches using timing-safe WebCrypto verify
 | ||
|  |         for (const signature of signatures) { | ||
|  |             try { | ||
|  |                 const signatureBytes = Buffer.from(signature, 'base64'); | ||
|  |                 const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, new TextEncoder().encode(signedPayload)); | ||
|  |                 if (isValid) { | ||
|  |                     return; // Valid signature found
 | ||
|  |                 } | ||
|  |             } | ||
|  |             catch { | ||
|  |                 // Invalid base64 or signature format, continue to next signature
 | ||
|  |                 continue; | ||
|  |             } | ||
|  |         } | ||
|  |         throw new InvalidWebhookSignatureError('The given webhook signature does not match the expected signature'); | ||
|  |     } | ||
|  | } | ||
|  | _Webhooks_instances = new WeakSet(), _Webhooks_validateSecret = function _Webhooks_validateSecret(secret) { | ||
|  |     if (typeof secret !== 'string' || secret.length === 0) { | ||
|  |         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`); | ||
|  |     } | ||
|  | }, _Webhooks_getRequiredHeader = function _Webhooks_getRequiredHeader(headers, name) { | ||
|  |     if (!headers) { | ||
|  |         throw new Error(`Headers are required`); | ||
|  |     } | ||
|  |     const value = headers.get(name); | ||
|  |     if (value === null || value === undefined) { | ||
|  |         throw new Error(`Missing required header: ${name}`); | ||
|  |     } | ||
|  |     return value; | ||
|  | }; | ||
|  | //# sourceMappingURL=webhooks.mjs.map
 |