Improve printful secret hash

This commit is contained in:
Dominic Ferrando
2026-07-18 02:13:04 -04:00
parent 529e011dd2
commit 03ccbbc99b
6 changed files with 27 additions and 12 deletions
+2 -1
View File
@@ -21,8 +21,9 @@ function printError(err: unknown): never {
try { try {
console.log("Registering printful webhook..."); console.log("Registering printful webhook...");
const printful = new PrintfulClient({ const printful = new PrintfulClient({
token: env.PRINTFUL_AUTH,
storeId: env.PRINTFUL_STORE_ID, storeId: env.PRINTFUL_STORE_ID,
token: env.PRINTFUL_AUTH,
webhookSecret: env.PRINTFUL_WEBHOOK_SECRET
}); });
await printful.Webhooks.create(PRINTFUL_WEBHOOK_URL, [ await printful.Webhooks.create(PRINTFUL_WEBHOOK_URL, [
+2 -6
View File
@@ -27,8 +27,9 @@ const validateEquality = (wValue: any, pValue: any): Validation => {
}; };
const printful = new PrintfulClient({ const printful = new PrintfulClient({
token: env.PRINTFUL_AUTH,
storeId: env.PRINTFUL_STORE_ID, storeId: env.PRINTFUL_STORE_ID,
token: env.PRINTFUL_AUTH,
webhookSecret: env.PRINTFUL_WEBHOOK_SECRET
}); });
const webflow = new WebflowClient({ 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); const errors = validations.filter((v) => !v.isValid);
if (errors.length) { if (errors.length) {
++invalidCount; ++invalidCount;
+3 -3
View File
@@ -324,8 +324,8 @@ export const app = new Elysia()
// WEBHOOKS // WEBHOOKS
.post("/webhooks/printful", async ({ body, query }) => { .post("/webhooks/printful", async ({ body, query }) => {
// https://webflow.com/integrations/printful // https://webflow.com/integrations/printful
const verified = crypto.timingSafeEqual(Buffer.from(query.secret, "hex"), Buffer.from(env.PRINTFUL_WEBHOOK_SECRET, "hex")); if (!s.Commerce.Printful.Util.verifySecret(query.secret))
if (!verified) throw status(400, "Invalid secret"); throw status(400, "Invalid secret");
const payload = body as Printful.Webhook.EventPayload; const payload = body as Printful.Webhook.EventPayload;
@@ -379,7 +379,7 @@ export const app = new Elysia()
} }
}, { query: t.Object({ secret: t.String() }) }) }, { query: t.Object({ secret: t.String() }) })
.post("/webhooks/webflow", async ({ request, body }) => { .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"); throw status(400, "Invalid signature");
const payload = body as Webflow.Webhook.EventPayload; const payload = body as Webflow.Webhook.EventPayload;
+2 -1
View File
@@ -20,8 +20,9 @@ export const WOrderStatusSchema = t.Union([
export class CommerceService { export class CommerceService {
readonly Printful = new PrintfulClient({ readonly Printful = new PrintfulClient({
token: env.PRINTFUL_AUTH,
storeId: env.PRINTFUL_STORE_ID, storeId: env.PRINTFUL_STORE_ID,
token: env.PRINTFUL_AUTH,
webhookSecret: env.PRINTFUL_WEBHOOK_SECRET
}); });
readonly Webflow = new WebflowClient({ readonly Webflow = new WebflowClient({
siteId: env.WEBFLOW_SITE_ID, siteId: env.WEBFLOW_SITE_ID,
+17
View File
@@ -1,6 +1,7 @@
import type { Printful } from "./util/types"; import type { Printful } from "./util/types";
import { type DeepPartial, type PagingOptions } from "./util/misc"; import { type DeepPartial, type PagingOptions } from "./util/misc";
import { RateLimitError, parseRetryAfterMs } from "@blade-and-brawn/domain"; import { RateLimitError, parseRetryAfterMs } from "@blade-and-brawn/domain";
import { timingSafeEqual } from "crypto";
const parsePayload = (res: Response): Promise<unknown> => const parsePayload = (res: Response): Promise<unknown> =>
res.json().catch(() => res.text().catch(() => undefined)); res.json().catch(() => res.text().catch(() => undefined));
@@ -8,6 +9,7 @@ const parsePayload = (res: Response): Promise<unknown> =>
type ClientOptions = { type ClientOptions = {
token: string; token: string;
storeId: string; storeId: string;
webhookSecret: string;
}; };
type Credentials = { 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 { export class PrintfulClient {
readonly Products: ProductsClient; readonly Products: ProductsClient;
readonly Orders: OrdersClient; readonly Orders: OrdersClient;
readonly Webhooks: WebhooksClient; readonly Webhooks: WebhooksClient;
readonly Util: UtilClient;
constructor(options: ClientOptions) { constructor(options: ClientOptions) {
const creds: Credentials = { const creds: Credentials = {
@@ -202,6 +218,7 @@ export class PrintfulClient {
this.Products = new ProductsClient(creds); this.Products = new ProductsClient(creds);
this.Orders = new OrdersClient(creds); this.Orders = new OrdersClient(creds);
this.Webhooks = new WebhooksClient(creds); this.Webhooks = new WebhooksClient(creds);
this.Util = new UtilClient(options.webhookSecret);
} }
static Util = { static Util = {
+1 -1
View File
@@ -340,7 +340,7 @@ class CollectionsClient {
class UtilClient { class UtilClient {
constructor(private webhookSecret: string) { } constructor(private webhookSecret: string) { }
verifyWebflowSignature(request: Request, body: unknown): boolean { verifySecret(request: Request, body: unknown): boolean {
const timestamp = request.headers.get("x-webflow-timestamp"); const timestamp = request.headers.get("x-webflow-timestamp");
if (!timestamp) return false; if (!timestamp) return false;