54c143a1b4
refs https://jsdoc.app/tags-param.html#optional-parameters-and-default-values - using an equals sign in the type definition is part of the Google Closure syntax but we use the JSDoc syntax in all other places, and tsc detects the different syntax - this commit standardizes the syntax ahead of enforcing a certain style down the line
169 lines
4.5 KiB
JavaScript
169 lines
4.5 KiB
JavaScript
/**
|
|
* @typedef {import('stripe').Stripe.WebhookEndpointCreateParams.EnabledEvent} WebhookEvent
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('stripe').Stripe.WebhookEndpoint} Webhook
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./StripeAPI')} StripeAPI
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} StripeWebhookModel
|
|
* @prop {string} webhook_id
|
|
* @prop {string} secret
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} StripeWebhook
|
|
* @prop {(data: StripeWebhookModel) => Promise<StripeWebhookModel>} save
|
|
* @prop {() => Promise<StripeWebhookModel>} get
|
|
*/
|
|
|
|
module.exports = class WebhookManager {
|
|
/**
|
|
* @param {object} deps
|
|
* @param {StripeWebhook} deps.StripeWebhook
|
|
* @param {StripeAPI} deps.api
|
|
*/
|
|
constructor({
|
|
StripeWebhook,
|
|
api
|
|
}) {
|
|
/** @private */
|
|
this.StripeWebhook = StripeWebhook;
|
|
/** @private */
|
|
this.api = api;
|
|
/** @private */
|
|
this.config = null;
|
|
/** @private */
|
|
this.webhookSecret = null;
|
|
/**
|
|
* @private
|
|
* @type {'network'|'local'}
|
|
*/
|
|
this.mode = 'network';
|
|
}
|
|
|
|
/** @type {WebhookEvent[]} */
|
|
static events = [
|
|
'checkout.session.completed',
|
|
'customer.subscription.deleted',
|
|
'customer.subscription.updated',
|
|
'customer.subscription.created',
|
|
'invoice.payment_succeeded'
|
|
];
|
|
|
|
/**
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async stop() {
|
|
if (this.mode !== 'network') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const existingWebhook = await this.StripeWebhook.get();
|
|
if (existingWebhook.webhook_id) {
|
|
await this.api.deleteWebhookEndpoint(existingWebhook.webhook_id);
|
|
}
|
|
await this.StripeWebhook.save({
|
|
webhook_id: null,
|
|
secret: null
|
|
});
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async start() {
|
|
if (this.mode !== 'network') {
|
|
return;
|
|
}
|
|
const existingWebhook = await this.StripeWebhook.get();
|
|
|
|
const webhook = await this.setupWebhook(existingWebhook.webhook_id, existingWebhook.secret);
|
|
|
|
await this.StripeWebhook.save({
|
|
webhook_id: webhook.id,
|
|
secret: webhook.secret
|
|
});
|
|
|
|
this.webhookSecret = webhook.secret;
|
|
}
|
|
|
|
/**
|
|
* @param {object} config
|
|
* @param {string} [config.webhookSecret] An optional webhook secret for use with stripe-cli, passing this will ensure a webhook is not created in Stripe
|
|
* @param {string} config.webhookHandlerUrl The URL which the Webhook should hit
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async configure(config) {
|
|
this.config = config;
|
|
if (config.webhookSecret) {
|
|
this.webhookSecret = config.webhookSecret;
|
|
this.mode = 'local';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} [id]
|
|
* @param {string} [secret]
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.forceCreate]
|
|
* @param {boolean} [opts.skipDelete]
|
|
*
|
|
* @returns {Promise<Webhook>}
|
|
*/
|
|
async setupWebhook(id, secret, opts = {}) {
|
|
if (!id || !secret || opts.forceCreate) {
|
|
if (id && !opts.skipDelete) {
|
|
try {
|
|
await this.api.deleteWebhookEndpoint(id);
|
|
} catch (err) {
|
|
// Continue
|
|
}
|
|
}
|
|
const webhook = await this.api.createWebhookEndpoint(
|
|
this.config.webhookHandlerUrl,
|
|
WebhookManager.events
|
|
);
|
|
return {
|
|
id: webhook.id,
|
|
secret: webhook.secret
|
|
};
|
|
} else {
|
|
try {
|
|
await this.api.updateWebhookEndpoint(
|
|
id,
|
|
this.config.webhookHandlerUrl,
|
|
WebhookManager.events
|
|
);
|
|
|
|
return {
|
|
id,
|
|
secret
|
|
};
|
|
} catch (err) {
|
|
if (err.code === 'resource_missing') {
|
|
return this.setupWebhook(id, secret, {skipDelete: true, forceCreate: true});
|
|
}
|
|
return this.setupWebhook(id, secret, {skipDelete: false, forceCreate: true});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} body
|
|
* @param {string} signature
|
|
* @returns {import('stripe').Stripe.Event}
|
|
*/
|
|
parseWebhook(body, signature) {
|
|
return this.api.parseWebhook(body, signature, this.webhookSecret);
|
|
}
|
|
};
|