Add HMAC function, and update the CMS and Mailer services
A new module has been added to create HMACs, primarily used in the billing service for data verification. Keystatic CMS usage has been conditioned to Node.js runtime only, and a fallback to the mock CMS client has been implemented for Edge Runtime. Mailer services now accommodate environment-specific providers.
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
import { getOrder, getVariant } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
import {
|
||||
BillingConfig,
|
||||
BillingWebhookHandlerService,
|
||||
getLineItemTypeById,
|
||||
} from '@kit/billing';
|
||||
|
||||
|
||||
import { BillingConfig, BillingWebhookHandlerService, getLineItemTypeById } from '@kit/billing';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
|
||||
|
||||
import { getLemonSqueezyEnv } from '../schema/lemon-squeezy-server-env.schema';
|
||||
import { OrderWebhook } from '../types/order-webhook';
|
||||
import SubscriptionWebhook from '../types/subscription-webhook';
|
||||
import { SubscriptionWebhook } from '../types/subscription-webhook';
|
||||
import { initializeLemonSqueezyClient } from './lemon-squeezy-sdk';
|
||||
import { createHmac } from "./verify-hmac";
|
||||
|
||||
|
||||
type UpsertSubscriptionParams =
|
||||
Database['public']['Functions']['upsert_subscription']['Args'];
|
||||
@@ -68,8 +69,10 @@ export class LemonSqueezyWebhookHandlerService
|
||||
throw new Error('Signature header not found');
|
||||
}
|
||||
|
||||
const isValid = await isSigningSecretValid(rawBody, signature);
|
||||
|
||||
// if the signature is invalid, throw an error
|
||||
if (!isSigningSecretValid(Buffer.from(rawBody), signature)) {
|
||||
if (!isValid) {
|
||||
logger.error(
|
||||
{
|
||||
eventName,
|
||||
@@ -411,12 +414,19 @@ function getISOString(date: number | null) {
|
||||
return date ? new Date(date).toISOString() : undefined;
|
||||
}
|
||||
|
||||
function isSigningSecretValid(rawBody: Buffer, signatureHeader: string) {
|
||||
async function isSigningSecretValid(rawBody: string, signatureHeader: string) {
|
||||
const { webhooksSecret } = getLemonSqueezyEnv();
|
||||
const hmac = createHmac('sha256', webhooksSecret);
|
||||
|
||||
const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8');
|
||||
const { hex: digest } = await createHmac({
|
||||
key: webhooksSecret,
|
||||
data: rawBody
|
||||
});
|
||||
|
||||
const signature = Buffer.from(signatureHeader, 'utf8');
|
||||
|
||||
return timingSafeEqual(digest, signature);
|
||||
}
|
||||
|
||||
function timingSafeEqual(digest: string, signature: Buffer) {
|
||||
return digest.toString() === signature.toString();
|
||||
}
|
||||
|
||||
33
packages/billing/lemon-squeezy/src/services/verify-hmac.ts
Normal file
33
packages/billing/lemon-squeezy/src/services/verify-hmac.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
function bufferToHex(buffer: ArrayBuffer) {
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
export async function createHmac({ key, data }: { key: string; data: string }) {
|
||||
const encoder = new TextEncoder();
|
||||
const crypto = new Crypto();
|
||||
const encodedKey = encoder.encode(key);
|
||||
const encodedData = encoder.encode(data);
|
||||
|
||||
const hmacKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encodedKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
true,
|
||||
['sign', 'verify'],
|
||||
);
|
||||
|
||||
const signature = await window.crypto.subtle.sign(
|
||||
'HMAC',
|
||||
hmacKey,
|
||||
encodedData,
|
||||
);
|
||||
|
||||
const hex = bufferToHex(signature);
|
||||
|
||||
return { hex };
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
interface SubscriptionWebhookResponse {
|
||||
export interface SubscriptionWebhook {
|
||||
meta: Meta;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export default SubscriptionWebhookResponse;
|
||||
|
||||
interface Data {
|
||||
type: string;
|
||||
id: string;
|
||||
|
||||
@@ -34,7 +34,21 @@ async function getWordpressClient() {
|
||||
}
|
||||
|
||||
async function getKeystaticClient() {
|
||||
const { KeystaticClient } = await import('../../keystatic/src/client');
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { KeystaticClient } = await import('../../keystatic/src/client');
|
||||
|
||||
return new KeystaticClient();
|
||||
return new KeystaticClient();
|
||||
}
|
||||
|
||||
console.error(`[CMS] Keystatic client is only available in Node.js runtime. Please choose a different CMS client. Returning a mock client instead of throwing an error.`);
|
||||
|
||||
return mockCMSClient();
|
||||
}
|
||||
|
||||
function mockCMSClient() {
|
||||
return {
|
||||
async getContentItems() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,11 +9,15 @@ const MAILER_PROVIDER = z
|
||||
* @description Get the mailer based on the environment variable.
|
||||
*/
|
||||
export async function getMailer() {
|
||||
switch (MAILER_PROVIDER) {
|
||||
switch (process.env.MAILER_PROVIDER as typeof MAILER_PROVIDER) {
|
||||
case 'nodemailer': {
|
||||
const { Nodemailer } = await import('./impl/nodemailer');
|
||||
if (process.env.NEXT_RUNTIME !== 'edge') {
|
||||
const { Nodemailer } = await import('./impl/nodemailer');
|
||||
|
||||
return new Nodemailer();
|
||||
return new Nodemailer();
|
||||
} else {
|
||||
throw new Error('Nodemailer is not available on the edge runtime side');
|
||||
}
|
||||
}
|
||||
|
||||
case 'cloudflare': {
|
||||
|
||||
Reference in New Issue
Block a user