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:
giancarlo
2024-04-17 15:47:50 +08:00
parent d62bcad657
commit bf43c48dff
9 changed files with 107 additions and 30 deletions

View File

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

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

View File

@@ -1,10 +1,8 @@
interface SubscriptionWebhookResponse {
export interface SubscriptionWebhook {
meta: Meta;
data: Data;
}
export default SubscriptionWebhookResponse;
interface Data {
type: string;
id: string;

View File

@@ -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 [];
},
};
}

View File

@@ -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': {