feat: remove paypal webhook listener (#54395)

This commit is contained in:
Ahmad Abdolsaheb 2024-04-29 08:49:21 +03:00 committed by GitHub
parent 5cbe0b709e
commit 20b6b83e99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 31 additions and 687 deletions

View File

@ -57,7 +57,6 @@ jobs:
echo 'STRIPE_PUBLIC_KEY=${{ secrets.STRIPE_PUBLIC_KEY }}' >> .env
echo 'STRIPE_SECRET_KEY=${{ secrets.STRIPE_SECRET_KEY }}' >> .env
echo 'PAYPAL_CLIENT_ID=${{ secrets.PAYPAL_CLIENT_ID }}' >> .env
echo 'PAYPAL_SECRET=${{ secrets.PAYPAL_SECRET }}' >> .env
- name: Install and Build
run: |

View File

@ -30,13 +30,7 @@ const {
SENTRY_DSN,
STRIPE_PUBLIC_KEY,
STRIPE_SECRET_KEY,
PAYPAL_CLIENT_ID,
PAYPAL_SECRET,
PAYPAL_VERIFY_WEBHOOK_URL,
PAYPAL_API_TOKEN_URL,
PAYPAL_WEBHOOK_ID
STRIPE_SECRET_KEY
} = process.env;
module.exports = {
@ -98,13 +92,5 @@ module.exports = {
stripe: {
public: STRIPE_PUBLIC_KEY,
secret: STRIPE_SECRET_KEY
},
paypal: {
client: PAYPAL_CLIENT_ID,
secret: PAYPAL_SECRET,
verifyWebhookURL: PAYPAL_VERIFY_WEBHOOK_URL,
tokenUrl: PAYPAL_API_TOKEN_URL,
webhookId: PAYPAL_WEBHOOK_ID
}
};

View File

@ -32,7 +32,6 @@
"@sentry/node": "7.37.1",
"@sentry/tracing": "7.37.1",
"accepts": "1.3.8",
"axios": "0.23.0",
"body-parser": "1.20.0",
"compression": "1.7.4",
"connect-mongo": "3.2.0",
@ -88,7 +87,6 @@
"@babel/preset-env": "7.18.0",
"@babel/register": "7.17.7",
"loopback-component-explorer": "6.4.0",
"nodemon": "2.0.16",
"smee-client": "1.2.3"
"nodemon": "2.0.16"
}
}

View File

@ -3,30 +3,8 @@ require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
const createDebugger = require('debug');
const nodemon = require('nodemon');
const SmeeClient = require('smee-client');
const log = createDebugger('fcc:start:development');
if (process.env.WEBHOOK_PROXY_URL) {
const paypalPayloadHandler = new SmeeClient({
source: process.env.WEBHOOK_PROXY_URL,
target: process.env.API_LOCATION + '/hooks/update-paypal',
logger: { info: log, error: log }
});
const paypalevents = paypalPayloadHandler.start();
process.on('exit', () => {
log('Stopping webhook proxy client.');
paypalevents.close(() => {
log('Webhook proxy client is stopped.');
});
});
} else {
log('Webhook client is not configured.');
log('This can be ignored when not working with webhooks locally.');
}
nodemon({
ext: 'js json',
// --silent squashes an ELIFECYCLE error when the server exits

View File

@ -4,10 +4,6 @@ import Stripe from 'stripe';
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
import keys from '../../../config/secrets';
import {
getAsyncPaypalToken,
verifyWebHook,
updateUser,
verifyWebHookType,
createStripeCardDonation,
handleStripeCardUpdateSession
} from '../utils/donation';
@ -195,37 +191,15 @@ export default function donateBoot(app, done) {
}
}
function updatePaypal(req, res) {
const { headers, body } = req;
return Promise.resolve(req)
.then(verifyWebHookType)
.then(getAsyncPaypalToken)
.then(token => verifyWebHook(headers, body, token, keys.paypal.webhookId))
.then(hookBody => updateUser(hookBody, app))
.catch(err => {
// Todo: This probably need to be thrown and caught in error handler
log(err.message);
})
.finally(() => res.status(200).json({ message: 'received paypal hook' }));
}
const stripeKey = keys.stripe.public;
const secKey = keys.stripe.secret;
const paypalKey = keys.paypal.client;
const paypalSec = keys.paypal.secret;
const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
const stripPublicInvalid =
!stripeKey || stripeKey === 'pk_from_stripe_dashboard';
const paypalSecretInvalid =
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
const paypalPublicInvalid =
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
if (stripeInvalid || paypalInvalid) {
if (stripeInvalid) {
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
throw new Error('Donation API keys are required to boot the server!');
}
@ -236,7 +210,6 @@ export default function donateBoot(app, done) {
api.post('/charge-stripe-card', handleStripeCardDonation);
api.put('/update-stripe-card', handleStripeCardUpdate);
api.post('/add-donation', addDonation);
hooks.post('/update-paypal', updatePaypal);
donateRouter.use('/donate', api);
donateRouter.use('/hooks', hooks);
app.use(donateRouter);

View File

@ -14,9 +14,7 @@ export default function getCsurf() {
const { path } = req;
if (
// eslint-disable-next-line max-len
/^\/hooks\/update-paypal$|^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(
path
)
/^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(path)
) {
next();
} else {

View File

@ -22,7 +22,6 @@ const signinRE = /^\/signin/;
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$/;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
@ -40,7 +39,6 @@ const _pathsAllowedREs = [
statusRE,
unsubscribedRE,
unsubscribeRE,
updateHooksRE,
donateRE,
submitCoderoadChallengeRE,
mobileLoginRE

View File

@ -48,7 +48,6 @@ describe('request-authorization', () => {
const statusRE = /^\/status\/ping$/;
const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$/;
const allowedPathsList = [
authRE,
@ -61,8 +60,7 @@ describe('request-authorization', () => {
signinRE,
statusRE,
unsubscribedRE,
unsubscribeRE,
updateHooksRE
unsubscribeRE
];
it('returns a boolean', () => {
@ -79,10 +77,8 @@ describe('request-authorization', () => {
'/ue/WmjInLerysPrcon6fMb/',
allowedPathsList
);
const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
expect(resultA).toBe(true);
expect(resultB).toBe(true);
expect(resultC).toBe(true);
});
it('returns false for a non-white-listed path', () => {

View File

@ -1,205 +0,0 @@
/* eslint-disable camelcase */
export const mockCancellationHook = {
headers: {
host: 'a47fb0f4.ngrok.io',
accept: '*/*',
'paypal-transmission-id': '2e24bc40-61d1-11ea-8ac4-7d4e2605c70c',
'paypal-transmission-time': '2020-03-09T06:42:43Z',
'paypal-transmission-sig': 'ODCa4gXmfnxkNga1t9p2HTIWFjlTj68P7MhueQd',
'paypal-auth-version': 'v2',
'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs',
'paypal-auth-algo': 'SHA256withRSA',
'content-type': 'application/json',
'user-agent': 'PayPal/AUHD-214.0-54280748',
'correlation-id': 'c3823d4c07ce5',
cal_poolstack: 'amqunphttpdeliveryd:UNPHTTPDELIVERY',
client_pid: '23853',
'content-length': '1706',
'x-forwarded-proto': 'https',
'x-forwarded-for': '173.0.82.126'
},
body: {
id: 'WH-1VF24938EU372274X-83540367M0110254R',
event_version: '1.0',
create_time: '2020-03-06T15:34:50.000Z',
resource_type: 'subscription',
resource_version: '2.0',
event_type: 'BILLING.SUBSCRIPTION.CANCELLED',
summary: 'Subscription cancelled',
resource: {
shipping_amount: { currency_code: 'USD', value: '0.0' },
start_time: '2020-03-05T08:00:00Z',
update_time: '2020-03-09T06:42:09Z',
quantity: '1',
subscriber: {
name: [Object],
email_address: 'sb-zdry81054163@personal.example.com',
payer_id: '82PVXVLDAU3E8',
shipping_address: [Object]
},
billing_info: {
outstanding_balance: [Object],
cycle_executions: [Array],
last_payment: [Object],
next_billing_time: '2020-04-05T10:00:00Z',
failed_payments_count: 0
},
create_time: '2020-03-06T07:34:50Z',
links: [[Object]],
id: 'I-BA1ATBNF8T3P',
plan_id: 'P-6VP46874PR423771HLZDKFBA',
status: 'CANCELLED',
status_update_time: '2020-03-09T06:42:09Z'
},
links: [
{
href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R',
rel: 'self',
method: 'GET'
},
{
href: 'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R/resend',
rel: 'resend',
method: 'POST'
}
]
}
};
export const mockActivationHook = {
headers: {
host: 'a47fb0f4.ngrok.io',
accept: '*/*',
'paypal-transmission-id': '22103660-5f7d-11ea-8ac4-7d4e2605c70c',
'paypal-transmission-time': '2020-03-06T07:36:03Z',
'paypal-transmission-sig':
'a;sldfn;lqwjhepjtn12l3n5123mnpu1i-sc-_+++dsflqenwpk1n234uthmsqwr123',
'paypal-auth-version': 'v2',
'paypal-cert-url':
'https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270',
'paypal-auth-algo': 'SHASHASHA',
'content-type': 'application/json',
'user-agent': 'PayPal/AUHD-214.0-54280748',
'correlation-id': 'e0b25772e11af',
client_pid: '14973',
'content-length': '2201',
'x-forwarded-proto': 'https',
'x-forwarded-for': '173.0.82.126'
},
body: {
id: 'WH-77687562XN25889J8-8Y6T55435R66168T6',
create_time: '2018-19-12T22:20:32.000Z',
resource_type: 'subscription',
event_type: 'BILLING.SUBSCRIPTION.ACTIVATED',
summary: 'A billing agreement was activated.',
resource: {
quantity: '20',
subscriber: {
name: {
given_name: 'John',
surname: 'Doe'
},
email_address: 'donor@freecodecamp.com',
shipping_address: {
name: {
full_name: 'John Doe'
},
address: {
address_line_1: '2211 N First Street',
address_line_2: 'Building 17',
admin_area_2: 'San Jose',
admin_area_1: 'CA',
postal_code: '95131',
country_code: 'US'
}
}
},
create_time: '2018-12-10T21:20:49Z',
shipping_amount: {
currency_code: 'USD',
value: '10.00'
},
start_time: '2018-11-01T00:00:00Z',
update_time: '2018-12-10T21:20:49Z',
billing_info: {
outstanding_balance: {
currency_code: 'USD',
value: '10.00'
},
cycle_executions: [
{
tenure_type: 'TRIAL',
sequence: 1,
cycles_completed: 1,
cycles_remaining: 0,
current_pricing_scheme_version: 1
},
{
tenure_type: 'REGULAR',
sequence: 2,
cycles_completed: 1,
cycles_remaining: 0,
current_pricing_scheme_version: 2
}
],
last_payment: {
amount: {
currency_code: 'USD',
value: '500.00'
},
time: '2018-12-01T01:20:49Z'
},
next_billing_time: '2019-01-01T00:20:49Z',
final_payment_time: '2020-01-01T00:20:49Z',
failed_payments_count: 2
},
links: [
{
href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
rel: 'self',
method: 'GET'
},
{
href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
rel: 'edit',
method: 'PATCH'
},
{
href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/suspend',
rel: 'suspend',
method: 'POST'
},
{
href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/cancel',
rel: 'cancel',
method: 'POST'
},
{
href: 'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/capture',
rel: 'capture',
method: 'POST'
}
],
id: 'I-BW452GLLEP1G',
plan_id: 'P-5ML4271244454362WXNWU5NQ',
auto_renewal: true,
status: 'ACTIVE',
status_update_time: '2018-12-10T21:20:49Z'
},
links: [
{
href: 'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6',
rel: 'self',
method: 'GET',
encType: 'application/json'
},
{
href: 'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6/resend',
rel: 'resend',
method: 'POST',
encType: 'application/json'
}
],
event_version: '1.0',
resource_version: '2.0'
}
};

View File

@ -1,91 +1,15 @@
/* eslint-disable camelcase */
import axios from 'axios';
import debug from 'debug';
import isEmail from 'validator/lib/isEmail';
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
import keys from '../../../config/secrets';
const log = debug('fcc:boot:donate');
const paypalVerifyWebhookURL =
keys.paypal.verifyWebhookURL ||
`https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`;
const paypalTokenURL =
keys.paypal.tokenUrl || `https://api.sandbox.paypal.com/v1/oauth2/token`;
export async function getAsyncPaypalToken() {
const res = await axios.post(paypalTokenURL, null, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: keys.paypal.client,
password: keys.paypal.secret
},
params: {
grant_type: 'client_credentials'
}
});
return res.data.access_token;
}
export function capitalizeKeys(object) {
Object.keys(object).forEach(function (key) {
object[key.toUpperCase()] = object[key];
});
}
export async function verifyWebHook(headers, body, token, webhookId) {
var webhookEventBody = typeof body === 'string' ? JSON.parse(body) : body;
capitalizeKeys(headers);
const payload = {
auth_algo: headers['PAYPAL-AUTH-ALGO'],
cert_url: headers['PAYPAL-CERT-URL'],
transmission_id: headers['PAYPAL-TRANSMISSION-ID'],
transmission_sig: headers['PAYPAL-TRANSMISSION-SIG'],
transmission_time: headers['PAYPAL-TRANSMISSION-TIME'],
webhook_id: webhookId,
webhook_event: webhookEventBody
};
const response = await axios.post(paypalVerifyWebhookURL, payload, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
});
if (response.data.verification_status === 'SUCCESS') {
return body;
} else {
throw {
// if verification fails, throw token verification error
message: `Failed token verification.`,
type: 'FailedPaypalTokenVerificationError'
};
}
}
export function verifyWebHookType(req) {
// check if webhook type for creation
const {
body: { event_type }
} = req;
if (
event_type === 'BILLING.SUBSCRIPTION.ACTIVATED' ||
event_type === 'BILLING.SUBSCRIPTION.CANCELLED'
)
return req;
else
throw {
message: 'Webhook type is not supported',
type: 'UnsupportedWebhookType'
};
}
export const createAsyncUserDonation = (user, donation) => {
log(`Creating donation:${donation.subscriptionId}`);
// log user donation
@ -97,108 +21,6 @@ export const createAsyncUserDonation = (user, donation) => {
});
};
export function createDonationObj(body) {
// creates donation object
const {
resource: {
id,
status_update_time,
subscriber: { email_address } = {
email_address: null
}
}
} = body;
let donation = {
email: email_address,
amount: 500,
duration: 'month',
provider: 'paypal',
subscriptionId: id,
customerId: email_address,
startDate: new Date(status_update_time).toISOString()
};
return donation;
}
export function createDonation(body, app) {
const { User } = app.models;
const {
resource: {
subscriber: { email_address } = {
email_address: null
}
}
} = body;
let donation = createDonationObj(body);
let email = email_address;
if (!email || !isEmail(email)) {
throw {
message: 'Paypal webhook email is not valid',
type: 'InvalidPaypalWebhookEmail'
};
}
return User.findOne({ where: { email } }, (err, user) => {
if (err) throw new Error(err);
if (!user) {
log(`Creating new user:${email}`);
return User.create({ email })
.then(user => {
createAsyncUserDonation(user, donation);
})
.catch(err => {
throw {
message:
err.message || 'findOne Donation records with email failed',
type: err.name || 'FailedFindingOneDonationEmail'
};
});
}
return createAsyncUserDonation(user, donation);
});
}
export async function cancelDonation(body, app) {
const {
resource: { id, status_update_time = new Date(Date.now()).toISOString() }
} = body;
const { Donation } = app.models;
Donation.findOne({ where: { subscriptionId: id } }, (err, donation) => {
if (err)
throw {
message:
err.message || 'findOne Donation records with subscriptionId failed',
type: err.name || 'FailedFindingOneSubscriptionId'
};
if (!donation)
throw {
message: 'Donation record with provided subscription id is not found',
type: 'SubscriptionIdNotFound'
};
log(`Updating donation record: ${donation.subscriptionId}`);
donation.updateAttributes({
endDate: new Date(status_update_time).toISOString()
});
});
}
export async function updateUser(body, app) {
const { event_type } = body;
if (event_type === 'BILLING.SUBSCRIPTION.ACTIVATED') {
// update user status based on new billing subscription events
createDonation(body, app);
} else if (event_type === 'BILLING.SUBSCRIPTION.CANCELLED') {
cancelDonation(body, app);
} else
throw {
message: 'Webhook type is not supported',
type: 'UnsupportedWebhookType'
};
}
export async function createStripeCardDonation(req, res, stripe) {
const {
body: { paymentMethodId, amount, duration },

View File

@ -1,26 +1,8 @@
/* eslint-disable camelcase */
import axios from 'axios';
import stripe from 'stripe';
import { ObjectId } from 'mongodb';
import keys from '../../../config/secrets';
import {
mockApp,
createDonationMockFn,
createUserMockFn,
updateDonationAttr,
updateUserAttr
} from '../boot_tests/fixtures';
import { mockActivationHook, mockCancellationHook } from './__mocks__/donation';
import {
getAsyncPaypalToken,
verifyWebHook,
updateUser,
capitalizeKeys,
createDonationObj,
handleStripeCardUpdateSession
} from './donation';
import { handleStripeCardUpdateSession } from './donation';
jest.mock('axios');
jest.mock('stripe', () => ({
checkout: {
sessions: {
@ -29,143 +11,7 @@ jest.mock('stripe', () => ({
}
}));
const verificationUrl = `https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature`;
const tokenUrl = `https://api.sandbox.paypal.com/v1/oauth2/token`;
const { body: activationHookBody, headers: activationHookHeaders } =
mockActivationHook;
describe('donation', () => {
describe('getAsyncPaypalToken', () => {
it('call paypal api for token ', async () => {
const res = {
data: {
access_token: 'token'
}
};
axios.post.mockImplementationOnce(() => Promise.resolve(res));
await expect(getAsyncPaypalToken()).resolves.toEqual(
res.data.access_token
);
expect(axios.post).toHaveBeenCalledTimes(1);
expect(axios.post).toHaveBeenCalledWith(tokenUrl, null, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
auth: {
username: keys.paypal.client,
password: keys.paypal.secret
},
params: {
grant_type: 'client_credentials'
}
});
});
});
describe('verifyWebHook', () => {
// normalize headers
capitalizeKeys(activationHookHeaders);
const mockWebhookId = 'qwdfq;3w12341dfa4';
const mockAccessToken = '241231223$!@$#1243';
const mockPayLoad = {
auth_algo: activationHookHeaders['PAYPAL-AUTH-ALGO'],
cert_url: activationHookHeaders['PAYPAL-CERT-URL'],
transmission_id: activationHookHeaders['PAYPAL-TRANSMISSION-ID'],
transmission_sig: activationHookHeaders['PAYPAL-TRANSMISSION-SIG'],
transmission_time: activationHookHeaders['PAYPAL-TRANSMISSION-TIME'],
webhook_id: mockWebhookId,
webhook_event: activationHookBody
};
const failedVerificationErr = {
message: `Failed token verification.`,
type: 'FailedPaypalTokenVerificationError'
};
const axiosOptions = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${mockAccessToken}`
}
};
const successVerificationResponse = {
data: {
verification_status: 'SUCCESS'
}
};
const failedVerificationResponse = {
data: {
verification_status: 'FAILED'
}
};
it('calls paypal for Webhook verification', async () => {
axios.post.mockImplementationOnce(() =>
Promise.resolve(successVerificationResponse)
);
await expect(
verifyWebHook(
activationHookHeaders,
activationHookBody,
mockAccessToken,
mockWebhookId
)
).resolves.toEqual(activationHookBody);
expect(axios.post).toHaveBeenCalledWith(
verificationUrl,
mockPayLoad,
axiosOptions
);
});
it('throws error if verification not successful', async () => {
axios.post.mockImplementationOnce(() =>
Promise.resolve(failedVerificationResponse)
);
await expect(
verifyWebHook(
activationHookHeaders,
activationHookBody,
mockAccessToken,
mockWebhookId
)
).rejects.toEqual(failedVerificationErr);
});
});
describe('updateUser', () => {
it('created a donation when a machting user found', () => {
updateUser(activationHookBody, mockApp);
expect(createDonationMockFn).toHaveBeenCalledTimes(1);
expect(createDonationMockFn).toHaveBeenCalledWith(
createDonationObj(activationHookBody)
);
});
it('create a user and donation when no machting user found', () => {
let newActivationHookBody = activationHookBody;
newActivationHookBody.resource.subscriber.email_address =
'new@freecodecamp.org';
updateUser(newActivationHookBody, mockApp);
expect(createUserMockFn).toHaveBeenCalledTimes(1);
});
it('modify user and donation records on cancellation', () => {
const { body: cancellationHookBody } = mockCancellationHook;
const {
resource: { status_update_time = new Date(Date.now()).toISOString() }
} = cancellationHookBody;
updateUser(cancellationHookBody, mockApp);
expect(updateDonationAttr).toHaveBeenCalledWith({
endDate: new Date(status_update_time).toISOString()
});
expect(updateUserAttr).not.toHaveBeenCalled();
});
});
describe('handleStripeCardUpdateSession', () => {
const mockUserId = ObjectId('507f1f77bcf86cd799439011');
const mockDonation = {

View File

@ -310,9 +310,6 @@ importers:
accepts:
specifier: 1.3.8
version: 1.3.8
axios:
specifier: 0.23.0
version: 0.23.0(debug@2.2.0)
body-parser:
specifier: 1.20.0
version: 1.20.0
@ -476,9 +473,6 @@ importers:
nodemon:
specifier: 2.0.16
version: 2.0.16
smee-client:
specifier: 1.2.3
version: 1.2.3
client:
dependencies:
@ -2196,7 +2190,7 @@ packages:
'@babel/traverse': 7.23.0
'@babel/types': 7.23.0
convert-source-map: 1.9.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -2430,7 +2424,7 @@ packages:
'@babel/core': 7.18.0
'@babel/helper-compilation-targets': 7.22.15
'@babel/helper-plugin-utils': 7.22.5
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
lodash.debounce: 4.0.8
resolve: 1.22.8
semver: 6.3.1
@ -5675,7 +5669,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.3
'@babel/types': 7.23.3
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -5692,7 +5686,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.9
'@babel/types': 7.23.9
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -9424,7 +9418,7 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
transitivePeerDependencies:
- supports-color
@ -9984,14 +9978,6 @@ packages:
- debug
dev: false
/axios@0.23.0(debug@2.2.0):
resolution: {integrity: sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==}
dependencies:
follow-redirects: 1.15.3(debug@2.2.0)
transitivePeerDependencies:
- debug
dev: false
/axios@1.6.7(debug@4.3.4):
resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==}
dependencies:
@ -11886,6 +11872,7 @@ packages:
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
requiresBuild: true
dependencies:
anymatch: 3.1.3
braces: 3.0.2
@ -13015,6 +13002,17 @@ packages:
ms: 2.1.2
dev: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
/debug@4.3.4(supports-color@8.1.1):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -14703,11 +14701,6 @@ packages:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
/eventsource@1.1.2:
resolution: {integrity: sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==}
engines: {node: '>=0.12.0'}
dev: true
/evp_bytestokey@1.0.3:
resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
dependencies:
@ -17100,7 +17093,7 @@ packages:
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
transitivePeerDependencies:
- supports-color
@ -19277,7 +19270,7 @@ packages:
dependencies:
async: 3.2.4
bluebird: 3.7.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
msgpack5: 4.5.1
strong-globalize: 5.1.0
uuid: 7.0.3
@ -24347,19 +24340,6 @@ packages:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
/smee-client@1.2.3:
resolution: {integrity: sha512-uDrU8u9/Ln7aRXyzGHgVaNUS8onHZZeSwQjCdkMoSL7U85xI+l+Y2NgjibkMJAyXkW7IAbb8rw9RMHIjS6lAwA==}
hasBin: true
dependencies:
commander: 2.20.3
eventsource: 1.1.2
morgan: 1.10.0
superagent: 7.1.5
validator: 13.11.0
transitivePeerDependencies:
- supports-color
dev: true
/smtp-connection@2.12.0:
resolution: {integrity: sha512-UP5jK4s5SGcUcqPN4U9ingqKt9mXYSKa52YhqxPuMecAnUOsVJpOmtgGaOm1urUBJZlzDt1M9WhZZkgbhxQlvg==}
dependencies:
@ -24968,7 +24948,7 @@ packages:
dependencies:
'@types/express': 4.17.18
accepts: 1.3.8
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
ejs: 3.1.9
fast-safe-stringify: 2.1.1
http-status: 1.7.0
@ -24983,7 +24963,7 @@ packages:
engines: {node: '>=6'}
dependencies:
accept-language: 3.0.18
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globalize: 1.7.0
lodash: 4.17.21
md5: 2.3.0
@ -24998,7 +24978,7 @@ packages:
engines: {node: '>=8.9'}
dependencies:
accept-language: 3.0.18
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globalize: 1.7.0
lodash: 4.17.21
md5: 2.3.0
@ -25014,7 +24994,7 @@ packages:
engines: {node: '>=10'}
dependencies:
accept-language: 3.0.18
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
globalize: 1.7.0
lodash: 4.17.21
md5: 2.3.0
@ -25031,7 +25011,7 @@ packages:
dependencies:
async: 3.2.4
body-parser: 1.20.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4
depd: 2.0.0
escape-string-regexp: 2.0.0
eventemitter2: 5.0.1
@ -25119,25 +25099,6 @@ packages:
/sudo-prompt@8.2.5:
resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==}
/superagent@7.1.5:
resolution: {integrity: sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==}
engines: {node: '>=6.4.0 <13 || >=14'}
dependencies:
component-emitter: 1.3.0
cookiejar: 2.1.4
debug: 4.3.4(supports-color@8.1.1)
fast-safe-stringify: 2.1.1
form-data: 4.0.0
formidable: 2.1.2
methods: 1.1.2
mime: 2.6.0
qs: 6.11.2
readable-stream: 3.6.2
semver: 7.6.0
transitivePeerDependencies:
- supports-color
dev: true
/superagent@8.1.2:
resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==}
engines: {node: '>=6.4.0 <13 || >=14'}
@ -26408,6 +26369,7 @@ packages:
/validator@13.11.0:
resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==}
engines: {node: '>= 0.10'}
dev: false
/validator@13.7.0:
resolution: {integrity: sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==}

View File

@ -27,13 +27,6 @@ STRIPE_SECRET_KEY=sk_from_stripe_dashboard
# PayPal
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
PAYPAL_SECRET=secret_from_paypal_dashboard
PAYPAL_VERIFY_WEBHOOK_URL=https://api.sandbox.paypal.com/v1/notifications/verify-webhook-signature
PAYPAL_API_TOKEN_URL=https://api.sandbox.paypal.com/v1/oauth2/token
PAYPAL_WEBHOOK_ID=webhook_id_from_paypal_dashboard
# Webhook proxy url from smee.io for PayPal
WEBHOOK_PROXY_URL=
# Patreon
PATREON_CLIENT_ID=id_from_patreon_dashboard