From 03ccbbc99b4c8e134dcac949acf09b3dfc9e0c61 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 18 Jul 2026 02:13:04 -0400 Subject: [PATCH] Improve printful secret hash --- apps/api/src/scripts/register-webhooks.ts | 3 ++- apps/api/src/scripts/validate-products.ts | 8 ++------ apps/api/src/server.ts | 6 +++--- apps/api/src/services/commerce.ts | 3 ++- packages/commerce/src/printful.ts | 17 +++++++++++++++++ packages/commerce/src/webflow.ts | 2 +- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/api/src/scripts/register-webhooks.ts b/apps/api/src/scripts/register-webhooks.ts index ced5611..22fd2c6 100644 --- a/apps/api/src/scripts/register-webhooks.ts +++ b/apps/api/src/scripts/register-webhooks.ts @@ -21,8 +21,9 @@ function printError(err: unknown): never { try { console.log("Registering printful webhook..."); const printful = new PrintfulClient({ - token: env.PRINTFUL_AUTH, storeId: env.PRINTFUL_STORE_ID, + token: env.PRINTFUL_AUTH, + webhookSecret: env.PRINTFUL_WEBHOOK_SECRET }); await printful.Webhooks.create(PRINTFUL_WEBHOOK_URL, [ diff --git a/apps/api/src/scripts/validate-products.ts b/apps/api/src/scripts/validate-products.ts index 374641a..8e352e3 100644 --- a/apps/api/src/scripts/validate-products.ts +++ b/apps/api/src/scripts/validate-products.ts @@ -27,8 +27,9 @@ const validateEquality = (wValue: any, pValue: any): Validation => { }; const printful = new PrintfulClient({ - token: env.PRINTFUL_AUTH, storeId: env.PRINTFUL_STORE_ID, + token: env.PRINTFUL_AUTH, + webhookSecret: env.PRINTFUL_WEBHOOK_SECRET }); const webflow = new WebflowClient({ @@ -70,11 +71,6 @@ for (const wProduct of wProducts) { ), ]; - // if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) { - // sku.fieldData.name = pVariant.name; - // await webflow.Products.Skus.update(wProduct.product.id, sku.id, sku); - // } - const errors = validations.filter((v) => !v.isValid); if (errors.length) { ++invalidCount; diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index b22de36..86c623e 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -324,8 +324,8 @@ export const app = new Elysia() // WEBHOOKS .post("/webhooks/printful", async ({ body, query }) => { // https://webflow.com/integrations/printful - const verified = crypto.timingSafeEqual(Buffer.from(query.secret, "hex"), Buffer.from(env.PRINTFUL_WEBHOOK_SECRET, "hex")); - if (!verified) throw status(400, "Invalid secret"); + if (!s.Commerce.Printful.Util.verifySecret(query.secret)) + throw status(400, "Invalid secret"); const payload = body as Printful.Webhook.EventPayload; @@ -379,7 +379,7 @@ export const app = new Elysia() } }, { query: t.Object({ secret: t.String() }) }) .post("/webhooks/webflow", async ({ request, body }) => { - if (!s.Commerce.Webflow.Util.verifyWebflowSignature(request, body)) + if (!s.Commerce.Webflow.Util.verifySecret(request, body)) throw status(400, "Invalid signature"); const payload = body as Webflow.Webhook.EventPayload; diff --git a/apps/api/src/services/commerce.ts b/apps/api/src/services/commerce.ts index 3d155f9..12d879e 100644 --- a/apps/api/src/services/commerce.ts +++ b/apps/api/src/services/commerce.ts @@ -20,8 +20,9 @@ export const WOrderStatusSchema = t.Union([ export class CommerceService { readonly Printful = new PrintfulClient({ - token: env.PRINTFUL_AUTH, storeId: env.PRINTFUL_STORE_ID, + token: env.PRINTFUL_AUTH, + webhookSecret: env.PRINTFUL_WEBHOOK_SECRET }); readonly Webflow = new WebflowClient({ siteId: env.WEBFLOW_SITE_ID, diff --git a/packages/commerce/src/printful.ts b/packages/commerce/src/printful.ts index b80ed53..15a8eca 100644 --- a/packages/commerce/src/printful.ts +++ b/packages/commerce/src/printful.ts @@ -1,6 +1,7 @@ import type { Printful } from "./util/types"; import { type DeepPartial, type PagingOptions } from "./util/misc"; import { RateLimitError, parseRetryAfterMs } from "@blade-and-brawn/domain"; +import { timingSafeEqual } from "crypto"; const parsePayload = (res: Response): Promise => res.json().catch(() => res.text().catch(() => undefined)); @@ -8,6 +9,7 @@ const parsePayload = (res: Response): Promise => type ClientOptions = { token: string; storeId: string; + webhookSecret: string; }; type Credentials = { @@ -186,10 +188,24 @@ class WebhooksClient { } } +class UtilClient { + constructor(private printfulSecret: string) { } + + verifySecret(secret: string): boolean { + try { + return timingSafeEqual(Buffer.from(secret, "hex"), Buffer.from(this.printfulSecret, "hex")); + } + catch { + return false; + } + } +} + export class PrintfulClient { readonly Products: ProductsClient; readonly Orders: OrdersClient; readonly Webhooks: WebhooksClient; + readonly Util: UtilClient; constructor(options: ClientOptions) { const creds: Credentials = { @@ -202,6 +218,7 @@ export class PrintfulClient { this.Products = new ProductsClient(creds); this.Orders = new OrdersClient(creds); this.Webhooks = new WebhooksClient(creds); + this.Util = new UtilClient(options.webhookSecret); } static Util = { diff --git a/packages/commerce/src/webflow.ts b/packages/commerce/src/webflow.ts index ad07782..5bc6712 100644 --- a/packages/commerce/src/webflow.ts +++ b/packages/commerce/src/webflow.ts @@ -340,7 +340,7 @@ class CollectionsClient { class UtilClient { constructor(private webhookSecret: string) { } - verifyWebflowSignature(request: Request, body: unknown): boolean { + verifySecret(request: Request, body: unknown): boolean { const timestamp = request.headers.get("x-webflow-timestamp"); if (!timestamp) return false;