import { StripeError, StripeSignatureVerificationError } from './Error.js';
import { CryptoProviderOnlySupportsAsyncError, } from './crypto/CryptoProvider.js';
import { maybeExtractFromCloudProviderEnvelope, parsePayload } from './utils.js';
export function createWebhooks(platformFunctions) {
    function buildEvent(jsonPayload) {
        if (jsonPayload && jsonPayload.object === 'v2.core.event') {
            throw new Error('You passed a thin event notification to a function that expects a webhook. Use the corresponding EventNotification method instead.');
        }
        return jsonPayload;
    }
    const Webhook = {
        DEFAULT_TOLERANCE: 300,
        signature: null,
        constructEvent(payload, header, secret, tolerance, cryptoProvider, receivedAt) {
            try {
                if (!this.signature) {
                    throw new Error('ERR: missing signature helper, unable to verify');
                }
                cryptoProvider = cryptoProvider || getCryptoProvider();
                this.signature.verifyHeader(payload, header, secret, tolerance || Webhook.DEFAULT_TOLERANCE, cryptoProvider, receivedAt);
            }
            catch (e) {
                if (e instanceof CryptoProviderOnlySupportsAsyncError) {
                    e.message +=
                        '\nUse `await constructEventAsync(...)` instead of `constructEvent(...)`';
                }
                throw e;
            }
            return buildEvent(parsePayload(payload));
        },
        async constructEventAsync(payload, header, secret, tolerance, cryptoProvider, receivedAt) {
            if (!this.signature) {
                throw new Error('ERR: missing signature helper, unable to verify');
            }
            cryptoProvider = cryptoProvider || getCryptoProvider();
            await this.signature.verifyHeaderAsync(payload, header, secret, tolerance || Webhook.DEFAULT_TOLERANCE, cryptoProvider, receivedAt);
            return buildEvent(parsePayload(payload));
        },
        constructEventWithoutVerification(payload) {
            return buildEvent(maybeExtractFromCloudProviderEnvelope(payload));
        },
        generateTestHeaderString: function (opts) {
            try {
                const preparedOpts = prepareOptions(opts);
                const signature = preparedOpts.signature ||
                    preparedOpts.cryptoProvider.computeHMACSignature(preparedOpts.payloadString, preparedOpts.secret);
                return preparedOpts.generateHeaderString(signature);
            }
            catch (e) {
                if (e instanceof CryptoProviderOnlySupportsAsyncError) {
                    e.message +=
                        '\nUse `await generateTestHeaderStringAsync(...)` instead of `generateTestHeaderString(...)`';
                }
                throw e;
            }
        },
        generateTestHeaderStringAsync: async function (opts) {
            const preparedOpts = prepareOptions(opts);
            const signature = preparedOpts.signature ||
                (await preparedOpts.cryptoProvider.computeHMACSignatureAsync(preparedOpts.payloadString, preparedOpts.secret));
            return preparedOpts.generateHeaderString(signature);
        },
    };
    const signature = {
        EXPECTED_SCHEME: 'v1',
        verifyHeader(encodedPayload, encodedHeader, secret, tolerance, cryptoProvider, receivedAt) {
            const { decodedHeader: header, decodedPayload: payload, details, suspectPayloadType, } = parseEventDetails(encodedPayload, encodedHeader, this.EXPECTED_SCHEME);
            const secretContainsWhitespace = /\s/.test(secret);
            cryptoProvider = cryptoProvider || getCryptoProvider();
            const expectedSignature = cryptoProvider.computeHMACSignature(makeHMACContent(payload, details), secret);
            /**
             * TODO(MAJOR): https://go/j/DEVSDK-3087
             * Passing in 0 by default skips timestamp tolerance verifications. Although it is mostly used in test,
             * we should change the default behavior to pass DEFAULT_TOLERANCE instead of 0 in the next major.
             */
            validateComputedSignature(payload, header, details, expectedSignature, tolerance || 0, suspectPayloadType, secretContainsWhitespace, receivedAt);
            return true;
        },
        async verifyHeaderAsync(encodedPayload, encodedHeader, secret, tolerance, cryptoProvider, receivedAt) {
            const { decodedHeader: header, decodedPayload: payload, details, suspectPayloadType, } = parseEventDetails(encodedPayload, encodedHeader, this.EXPECTED_SCHEME);
            const secretContainsWhitespace = /\s/.test(secret);
            cryptoProvider = cryptoProvider || getCryptoProvider();
            const expectedSignature = await cryptoProvider.computeHMACSignatureAsync(makeHMACContent(payload, details), secret);
            /**
             * TODO(MAJOR): https://go/j/DEVSDK-3087
             * Passing in 0 by default skips timestamp tolerance verifications. Although it is mostly used in test,
             * we should change the default behavior to pass DEFAULT_TOLERANCE instead of 0 in the next major.
             */
            return validateComputedSignature(payload, header, details, expectedSignature, tolerance || 0, suspectPayloadType, secretContainsWhitespace, receivedAt);
        },
    };
    function makeHMACContent(payload, details) {
        return `${details.timestamp}.${payload}`;
    }
    function parseEventDetails(encodedPayload, encodedHeader, expectedScheme) {
        // Express's type for `Request#headers` is `string | []string`
        // which is because the `set-cookie` header is an array,
        // but no other headers are an array (docs: https://nodejs.org/api/http.html#http_message_headers)
        // (Express's Request class is an extension of http.IncomingMessage, and doesn't appear to be relevantly modified: https://github.com/expressjs/express/blob/master/lib/request.js#L31)
        if (Array.isArray(encodedHeader)) {
            throw new Error('Unexpected: An array was passed as a header, which should not be possible for the stripe-signature header.');
        }
        if (!encodedPayload) {
            throw new StripeSignatureVerificationError(encodedHeader, encodedPayload, {
                message: 'No webhook payload was provided.',
            });
        }
        const suspectPayloadType = typeof encodedPayload != 'string' &&
            !(encodedPayload instanceof Uint8Array);
        const textDecoder = new TextDecoder('utf8');
        const decodedPayload = encodedPayload instanceof Uint8Array
            ? textDecoder.decode(encodedPayload)
            : encodedPayload;
        if (encodedHeader == null || encodedHeader == '') {
            throw new StripeSignatureVerificationError(encodedHeader, encodedPayload, {
                message: 'No stripe-signature header value was provided.',
            });
        }
        const decodedHeader = encodedHeader instanceof Uint8Array
            ? textDecoder.decode(encodedHeader)
            : encodedHeader;
        const details = parseHeader(decodedHeader, expectedScheme);
        if (!details || details.timestamp === -1) {
            throw new StripeSignatureVerificationError(decodedHeader, decodedPayload, {
                message: 'Unable to extract timestamp and signatures from header',
            });
        }
        if (!details.signatures.length) {
            throw new StripeSignatureVerificationError(decodedHeader, decodedPayload, {
                message: 'No signatures found with expected scheme',
            });
        }
        return {
            decodedPayload,
            decodedHeader,
            details,
            suspectPayloadType,
        };
    }
    /**
     * Validates that at least one signature in the parsed header matches the
     * expected signature, and that the event timestamp is within the allowed
     * {@link tolerance} window (in seconds). Set `tolerance` to `0` to skip
     * timestamp verification.
     *
     * TODO(MAJOR): https://go/j/DEVSDK-3087 - Change this default behavior to use DEFAULT_TOLERANCE instead of 0.
     * By default, validateComputedSignature doesn't perform timestamp verification.
     *
     * This method is mostly meant for tests or offline processing where the delivery time
     * of the event isn't important.
     * Integrations that process webhooks as they come in should use constructEvent method instead.
     *
     * @param payload The decoded webhook payload string.
     * @param header  The decoded `stripe-signature` header value.
     * @param details Parsed header containing timestamp and signatures.
     * @param expectedSignature HMAC signature computed from the payload and secret.
     * @param tolerance Maximum allowed age of the event in seconds. Use 0 to skip timestamp tolerance verification.
     * @param suspectPayloadType Whether the payload was not a string or Buffer.
     * @param secretContainsWhitespace Whether the signing secret contains whitespace.
     * @param receivedAt - Timestamp for age calculation
     * @returns `true` if the signature and timestamp are valid.
     *
     * @throws {StripeSignatureVerificationError} If verification fails.
     */
    function validateComputedSignature(payload, header, details, expectedSignature, tolerance, suspectPayloadType, secretContainsWhitespace, receivedAt) {
        const signatureFound = !!details.signatures.filter(platformFunctions.secureCompare.bind(platformFunctions, expectedSignature)).length;
        const docsLocation = '\nLearn more about webhook signing and explore webhook integration examples for various frameworks at ' +
            'https://docs.stripe.com/webhooks/signature';
        const whitespaceMessage = secretContainsWhitespace
            ? '\n\nNote: The provided signing secret contains whitespace. This often indicates an extra newline or space is in the value'
            : '';
        if (!signatureFound) {
            if (suspectPayloadType) {
                throw new StripeSignatureVerificationError(header, payload, {
                    message: 'Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the _raw_ request body.' +
                        'Payload was provided as a parsed JavaScript object instead. \n' +
                        'Signature verification is impossible without access to the original signed material. \n' +
                        docsLocation +
                        '\n' +
                        whitespaceMessage,
                });
            }
            throw new StripeSignatureVerificationError(header, payload, {
                message: 'No signatures found matching the expected signature for payload.' +
                    ' Are you passing the raw request body you received from Stripe? \n' +
                    ' If a webhook request is being forwarded by a third-party tool,' +
                    ' ensure that the exact request body, including JSON formatting and new line style, is preserved.\n' +
                    docsLocation +
                    '\n' +
                    whitespaceMessage,
            });
        }
        const timestampAge = Math.floor((typeof receivedAt === 'number' ? receivedAt : Date.now()) / 1000) - details.timestamp;
        if (tolerance > 0 && timestampAge > tolerance) {
            throw new StripeSignatureVerificationError(header, payload, {
                message: 'Timestamp outside the tolerance zone',
            });
        }
        return true;
    }
    function parseHeader(header, scheme) {
        if (typeof header !== 'string') {
            return null;
        }
        scheme = scheme || signature.EXPECTED_SCHEME;
        return header.split(',').reduce((accum, item) => {
            const kv = item.split('=');
            if (kv[0] === 't') {
                accum.timestamp = parseInt(kv[1], 10);
            }
            if (kv[0] === scheme) {
                accum.signatures.push(kv[1]);
            }
            return accum;
        }, {
            timestamp: -1,
            signatures: [],
        });
    }
    let webhooksCryptoProviderInstance = null;
    /**
     * Lazily instantiate a CryptoProvider instance. This is a stateless object
     * so a singleton can be used here.
     */
    function getCryptoProvider() {
        if (!webhooksCryptoProviderInstance) {
            webhooksCryptoProviderInstance = platformFunctions.createDefaultCryptoProvider();
        }
        return webhooksCryptoProviderInstance;
    }
    function prepareOptions(opts) {
        if (!opts) {
            throw new StripeError({
                message: 'Options are required',
            });
        }
        const timestamp = (opts.timestamp && Math.floor(opts.timestamp)) ||
            Math.floor(Date.now() / 1000);
        const scheme = opts.scheme || signature.EXPECTED_SCHEME;
        const cryptoProvider = opts.cryptoProvider || getCryptoProvider();
        const payloadString = `${timestamp}.${opts.payload}`;
        const generateHeaderString = (signature) => {
            return `t=${timestamp},${scheme}=${signature}`;
        };
        return {
            ...opts,
            timestamp,
            scheme,
            cryptoProvider,
            payloadString,
            generateHeaderString,
        };
    }
    Webhook.signature = signature;
    return Webhook;
}
//# sourceMappingURL=Webhooks.js.map