Ghost/ghost/stripe/lib/WebhookManager.js
Daniel Lockyer 54c143a1b4
Fixed optional syntax style for jsdoc
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
2022-10-16 14:48:05 +07:00

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);
}
};