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
|