Improve error handling
This commit is contained in:
+22
-24
@@ -12,11 +12,12 @@ import { cors } from "@elysiajs/cors";
|
|||||||
import { Elysia, NotFoundError, status } from "elysia";
|
import { Elysia, NotFoundError, status } from "elysia";
|
||||||
import {
|
import {
|
||||||
PrintfulClient,
|
PrintfulClient,
|
||||||
|
PrintfulError,
|
||||||
ProductSyncer,
|
ProductSyncer,
|
||||||
WebflowClient,
|
WebflowClient,
|
||||||
|
WebflowError,
|
||||||
Printful,
|
Printful,
|
||||||
Webflow,
|
Webflow,
|
||||||
FetchError,
|
|
||||||
} from "@blade-and-brawn/commerce";
|
} from "@blade-and-brawn/commerce";
|
||||||
import zipcodesUs from "zipcodes-us";
|
import zipcodesUs from "zipcodes-us";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
@@ -66,14 +67,18 @@ export const app = new Elysia()
|
|||||||
)
|
)
|
||||||
|
|
||||||
.error({
|
.error({
|
||||||
FetchError,
|
PrintfulError,
|
||||||
|
WebflowError,
|
||||||
})
|
})
|
||||||
|
|
||||||
.onError(async ({ code, error }) => {
|
.onError(({ code, error }) => {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case "FetchError":
|
case "PrintfulError":
|
||||||
console.error(error.message, await error.parse());
|
case "WebflowError":
|
||||||
return error;
|
console.error(error.message, error.status, error.payload);
|
||||||
|
return status(502, { error: error.message });
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,36 +135,35 @@ export const app = new Elysia()
|
|||||||
.post("/", async ({ }) => {
|
.post("/", async ({ }) => {
|
||||||
const syncState = await productSyncer.getState();
|
const syncState = await productSyncer.getState();
|
||||||
if (syncState.isSyncing)
|
if (syncState.isSyncing)
|
||||||
return status(409, { error: "Sync already in progress" });
|
throw status(409, "Sync already in progress");
|
||||||
|
|
||||||
await productSyncer.sync();
|
await productSyncer.sync();
|
||||||
return { ok: true };
|
|
||||||
})
|
})
|
||||||
// Per-product sync
|
// Per-product sync
|
||||||
.post("/:printfulProductId", async ({ params }) => {
|
.post("/:printfulProductId", async ({ params }) => {
|
||||||
const syncState = await productSyncer.getState();
|
const syncState = await productSyncer.getState();
|
||||||
if (syncState.isSyncing)
|
if (syncState.isSyncing)
|
||||||
return status(409, { error: "Sync already in progress" });
|
throw status(409, "Sync already in progress");
|
||||||
|
|
||||||
await productSyncer.sync(params.printfulProductId);
|
await productSyncer.sync(params.printfulProductId);
|
||||||
return { ok: true };
|
}, { params: z.object({ printfulProductId: z.coerce.number() }) }),
|
||||||
}, { params: z.object({ printfulProductId: z.number() }) }),
|
|
||||||
)
|
)
|
||||||
// TODO: add schema validation
|
// TODO: add schema validation
|
||||||
.get("/:printfulProductId", async ({ params }) => {
|
.get("/:printfulProductId", async ({ params }) => {
|
||||||
const printfulProductId = +params.printfulProductId;
|
|
||||||
const printfulProduct =
|
const printfulProduct =
|
||||||
await printful.Products.get(printfulProductId);
|
await printful.Products.get(params.printfulProductId);
|
||||||
if (!printfulProduct) return new NotFoundError();
|
if (!printfulProduct) throw new NotFoundError("Missing printful product");
|
||||||
|
|
||||||
const webflowProductId =
|
const webflowProductId =
|
||||||
printfulProduct.sync_product.external_id.split("-")[0];
|
printfulProduct.sync_product.external_id.split("-")[0];
|
||||||
if (!webflowProductId) return new NotFoundError();
|
if (!webflowProductId) throw new NotFoundError("Missing webflow product ID");
|
||||||
|
|
||||||
const webflowProduct =
|
const webflowProduct =
|
||||||
await webflow.Products.get(webflowProductId);
|
await webflow.Products.get(webflowProductId);
|
||||||
|
|
||||||
return { printfulProduct, webflowProduct };
|
return { printfulProduct, webflowProduct };
|
||||||
|
}, {
|
||||||
|
params: z.object({ printfulProductId: z.coerce.number() })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,14 +198,10 @@ export const app = new Elysia()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
})
|
})
|
||||||
.post("/webhook/webflow", async ({ request, body, set }) => {
|
.post("/webhook/webflow", async ({ request, body }) => {
|
||||||
if (!webflow.Util.verifyWebflowSignature(request, body)) {
|
if (!webflow.Util.verifyWebflowSignature(request, body))
|
||||||
set.status = 400;
|
throw status(400, "Invalid signature");
|
||||||
return "Invalid signature";
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = body as Webflow.Webhook.EventPayload;
|
const payload = body as Webflow.Webhook.EventPayload;
|
||||||
|
|
||||||
@@ -237,8 +237,6 @@ export const app = new Elysia()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(3000);
|
app.listen(3000);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Printful } from "./util/types";
|
import type { Printful } from "./util/types";
|
||||||
import { FetchError, type DeepPartial } from "./util/misc";
|
import { type DeepPartial } from "./util/misc";
|
||||||
|
|
||||||
|
const parsePayload = (res: Response): Promise<unknown> =>
|
||||||
|
res.json().catch(() => res.text().catch(() => undefined));
|
||||||
|
|
||||||
type ClientOptions = {
|
type ClientOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -11,6 +14,17 @@ type Credentials = {
|
|||||||
authHeaders: Record<string, string>;
|
authHeaders: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class PrintfulError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public status: number,
|
||||||
|
public payload: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "PrintfulError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class VariantsClient {
|
class VariantsClient {
|
||||||
constructor(private creds: Credentials) { }
|
constructor(private creds: Credentials) { }
|
||||||
|
|
||||||
@@ -20,7 +34,7 @@ class VariantsClient {
|
|||||||
headers: this.creds.authHeaders,
|
headers: this.creds.authHeaders,
|
||||||
});
|
});
|
||||||
if (res.status === 404 || res.status === 400) return;
|
if (res.status === 404 || res.status === 400) return;
|
||||||
if (!res.ok) throw new FetchError("Failed to get Printful variant", res);
|
if (!res.ok) throw new PrintfulError("Failed to get Printful variant", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.SyncVariant>;
|
const payload = (await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.SyncVariant>;
|
||||||
return payload.result;
|
return payload.result;
|
||||||
}
|
}
|
||||||
@@ -40,7 +54,7 @@ class ProductsClient {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: this.creds.authHeaders,
|
headers: this.creds.authHeaders,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to get all Printful products", res);
|
if (!res.ok) throw new PrintfulError("Failed to get all Printful products", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as Printful.Products.MetaDataMulti<Printful.Products.SyncProduct>;
|
const payload = (await res.json()) as Printful.Products.MetaDataMulti<Printful.Products.SyncProduct>;
|
||||||
allSyncProducts.push(...payload.result);
|
allSyncProducts.push(...payload.result);
|
||||||
offset = allSyncProducts.length;
|
offset = allSyncProducts.length;
|
||||||
@@ -55,7 +69,7 @@ class ProductsClient {
|
|||||||
headers: this.creds.authHeaders,
|
headers: this.creds.authHeaders,
|
||||||
});
|
});
|
||||||
if (res.status === 404 || res.status === 400) return;
|
if (res.status === 404 || res.status === 400) return;
|
||||||
if (!res.ok) throw new FetchError("Failed to get Printful product", res);
|
if (!res.ok) throw new PrintfulError("Failed to get Printful product", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.Product>;
|
const payload = (await res.json()) as Printful.Products.MetaDataSingle<Printful.Products.Product>;
|
||||||
return payload.result;
|
return payload.result;
|
||||||
}
|
}
|
||||||
@@ -69,7 +83,7 @@ class ProductsClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(printfulProduct),
|
body: JSON.stringify(printfulProduct),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to update Printful product", res);
|
if (!res.ok) throw new PrintfulError("Failed to update Printful product", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +99,7 @@ class OrdersClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(printfulOrder),
|
body: JSON.stringify(printfulOrder),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to create printful order", res);
|
if (!res.ok) throw new PrintfulError("Failed to create Printful order", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(offset = 0): Promise<Printful.Orders.Order[]> {
|
async getAll(offset = 0): Promise<Printful.Orders.Order[]> {
|
||||||
@@ -95,7 +109,7 @@ class OrdersClient {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: this.creds.authHeaders,
|
headers: this.creds.authHeaders,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to get all Printful orders", res);
|
if (!res.ok) throw new PrintfulError("Failed to get all Printful orders", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as Printful.Products.MetaDataMulti<Printful.Orders.Order>;
|
const payload = (await res.json()) as Printful.Products.MetaDataMulti<Printful.Orders.Order>;
|
||||||
allPrintfulOrders.push(...payload.result);
|
allPrintfulOrders.push(...payload.result);
|
||||||
offset = allPrintfulOrders.length;
|
offset = allPrintfulOrders.length;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Printful, Webflow } from "./util/types";
|
import type { Printful, Webflow } from "./util/types";
|
||||||
import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
|
import { formatSlug, type DeepPartial } from "./util/misc";
|
||||||
import { WebflowClient } from "./webflow";
|
import { WebflowClient } from "./webflow";
|
||||||
import { PrintfulClient } from "./printful";
|
import { PrintfulClient } from "./printful";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
@@ -31,12 +31,12 @@ export class ProductSyncer {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
async sync(printfulProductId?: number) {
|
async sync(printfulProductId?: number) {
|
||||||
|
try {
|
||||||
await this.setState({ isSyncing: true, syncingIds: [] });
|
await this.setState({ isSyncing: true, syncingIds: [] });
|
||||||
|
|
||||||
// Populate products
|
// Populate products
|
||||||
const mainPrintfulProducts: MainProduct[] = [];
|
const mainPrintfulProducts: MainProduct[] = [];
|
||||||
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
|
const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
|
||||||
try {
|
|
||||||
let printfulProducts = await this.printful.Products.getAll();
|
let printfulProducts = await this.printful.Products.getAll();
|
||||||
if (printfulProductId) {
|
if (printfulProductId) {
|
||||||
const printfulProduct = printfulProducts.find(
|
const printfulProduct = printfulProducts.find(
|
||||||
@@ -82,17 +82,12 @@ export class ProductSyncer {
|
|||||||
});
|
});
|
||||||
mainPrintfulProduct.colors.add(productColor);
|
mainPrintfulProduct.colors.add(productColor);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
await this.setState({ isSyncing: false, syncingIds: [] });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Validate main printful products
|
// TODO: Validate main printful products
|
||||||
|
|
||||||
// Sync the main printful products
|
// Sync the main printful products
|
||||||
console.log("Syncing main products...");
|
console.log("Syncing main products...");
|
||||||
for (const mainPrintfulProduct of mainPrintfulProducts) {
|
for (const mainPrintfulProduct of mainPrintfulProducts) {
|
||||||
try {
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
isSyncing: true,
|
isSyncing: true,
|
||||||
syncingIds: mainPrintfulProduct.variants.map(
|
syncingIds: mainPrintfulProduct.variants.map(
|
||||||
@@ -161,9 +156,8 @@ export class ProductSyncer {
|
|||||||
|
|
||||||
const webflowProductId =
|
const webflowProductId =
|
||||||
mainPrintfulProduct.externalId.split("-")[0];
|
mainPrintfulProduct.externalId.split("-")[0];
|
||||||
if (!webflowProductId) {
|
if (!webflowProductId)
|
||||||
throw new Error("Malformed printful product ID");
|
throw new Error("Malformed printful product ID");
|
||||||
}
|
|
||||||
await this.webflow.Products.update(webflowProductId, {
|
await this.webflow.Products.update(webflowProductId, {
|
||||||
product: {
|
product: {
|
||||||
fieldData: {
|
fieldData: {
|
||||||
@@ -254,10 +248,8 @@ export class ProductSyncer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
existingWebflowProduct = await this.webflow.Products.get(webflowProductId);
|
existingWebflowProduct = await this.webflow.Products.get(webflowProductId);
|
||||||
if (!existingWebflowProduct) {
|
if (!existingWebflowProduct)
|
||||||
console.error("Missing webflow product");
|
throw new Error("Webflow product missing after create");
|
||||||
throw new Error("Failed to create Webflow product");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const specialVariant of mainPrintfulProduct.variants) {
|
for (const specialVariant of mainPrintfulProduct.variants) {
|
||||||
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
|
const newPrintfulVariants: DeepPartial<Printful.Products.SyncVariant>[] =
|
||||||
@@ -289,15 +281,17 @@ export class ProductSyncer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof FetchError) {
|
|
||||||
console.error(err.message, err.payload);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.setState({ isSyncing: false, syncingIds: [] });
|
|
||||||
console.log("Done");
|
console.log("Done");
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
try {
|
||||||
|
await this.setState({ isSyncing: false, syncingIds: [] });
|
||||||
|
} catch {
|
||||||
|
console.error("Failed to reset sync state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async setState(state: SyncState) {
|
async setState(state: SyncState) {
|
||||||
await redis.set("commerce:sync:state", JSON.stringify(state));
|
await redis.set("commerce:sync:state", JSON.stringify(state));
|
||||||
@@ -323,14 +317,4 @@ export class ProductSyncer {
|
|||||||
return productName;
|
return productName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getProductImageUrls(slug: string): Promise<string[]> {
|
|
||||||
const res = await fetch(`${R2_WORKER_URL}/product-images/${slug}`);
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error("Failed to get product image keys:", await res.json());
|
|
||||||
throw new Error("Failed to get product image keys");
|
|
||||||
}
|
|
||||||
const productImageKeys: string[] = (await res.json()) as string[];
|
|
||||||
return productImageKeys.map((key) => `${WEBSITE_MEDIA_URL}/${key}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,25 +13,3 @@ export const formatSlug = (name: string): string => {
|
|||||||
.replace(/--+/g, "-") // collapse multiple dashes
|
.replace(/--+/g, "-") // collapse multiple dashes
|
||||||
.replace(/^([^a-z0-9_])/, "_$1"); // if it starts with an invalid char, prefix underscore
|
.replace(/^([^a-z0-9_])/, "_$1"); // if it starts with an invalid char, prefix underscore
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FetchError extends Error {
|
|
||||||
public payload: Object | undefined;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public response: Response,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "FetchError";
|
|
||||||
}
|
|
||||||
|
|
||||||
async parse() {
|
|
||||||
try {
|
|
||||||
this.payload = (await this.response.clone().json()) as Object;
|
|
||||||
} catch {
|
|
||||||
this.payload = await this.response.text().catch(() => undefined);
|
|
||||||
} finally {
|
|
||||||
return this.payload;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { Webflow } from "./util/types";
|
import type { Webflow } from "./util/types";
|
||||||
import { FetchError, type DeepPartial } from "./util/misc";
|
import { type DeepPartial } from "./util/misc";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
const parsePayload = (res: Response): Promise<unknown> =>
|
||||||
|
res.json().catch(() => res.text().catch(() => undefined));
|
||||||
|
|
||||||
type ClientOptions = {
|
type ClientOptions = {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
collectionsId: string;
|
collectionsId: string;
|
||||||
@@ -15,8 +18,19 @@ type Credentials = {
|
|||||||
authHeader: Record<string, string>;
|
authHeader: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class WebflowError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public status: number,
|
||||||
|
public payload: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "WebflowError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SkusClient {
|
class SkusClient {
|
||||||
constructor(private creds: Credentials) {}
|
constructor(private creds: Credentials) { }
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
webflowProductId: string,
|
webflowProductId: string,
|
||||||
@@ -33,7 +47,7 @@ class SkusClient {
|
|||||||
body: JSON.stringify({ skus }),
|
body: JSON.stringify({ skus }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new FetchError("Failed to create Webflow product SKU", res);
|
if (!res.ok) throw new WebflowError("Failed to create Webflow product SKU", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as { skus: { id: string }[] };
|
const payload = (await res.json()) as { skus: { id: string }[] };
|
||||||
return payload.skus.map((sku) => sku.id);
|
return payload.skus.map((sku) => sku.id);
|
||||||
}
|
}
|
||||||
@@ -54,7 +68,7 @@ class SkusClient {
|
|||||||
body: JSON.stringify({ sku: webflowSku }),
|
body: JSON.stringify({ sku: webflowSku }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new FetchError("Failed to update Webflow product SKU", res);
|
if (!res.ok) throw new WebflowError("Failed to update Webflow product SKU", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +90,7 @@ class ProductsClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(webflowProductAndSku),
|
body: JSON.stringify(webflowProductAndSku),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to create Webflow product", res);
|
if (!res.ok) throw new WebflowError("Failed to create Webflow product", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as { product: { id: string } };
|
const payload = (await res.json()) as { product: { id: string } };
|
||||||
return payload.product.id;
|
return payload.product.id;
|
||||||
}
|
}
|
||||||
@@ -86,7 +100,7 @@ class ProductsClient {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: this.creds.authHeader,
|
headers: this.creds.authHeader,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to get all Webflow products", res);
|
if (!res.ok) throw new WebflowError("Failed to get all Webflow products", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as { items: Webflow.Products.ProductAndSkus[] };
|
const payload = (await res.json()) as { items: Webflow.Products.ProductAndSkus[] };
|
||||||
return payload.items;
|
return payload.items;
|
||||||
}
|
}
|
||||||
@@ -97,7 +111,7 @@ class ProductsClient {
|
|||||||
headers: this.creds.authHeader,
|
headers: this.creds.authHeader,
|
||||||
});
|
});
|
||||||
if (res.status === 404 || res.status === 400) return;
|
if (res.status === 404 || res.status === 400) return;
|
||||||
if (!res.ok) throw new FetchError("Failed to get Webflow product", res);
|
if (!res.ok) throw new WebflowError("Failed to get Webflow product", res.status, await parsePayload(res));
|
||||||
return (await res.json()) as Webflow.Products.ProductAndSkus;
|
return (await res.json()) as Webflow.Products.ProductAndSkus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +127,7 @@ class ProductsClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(webflowProduct),
|
body: JSON.stringify(webflowProduct),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to update Webflow product", res);
|
if (!res.ok) throw new WebflowError("Failed to update Webflow product", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(webflowProductId: string): Promise<void> {
|
async remove(webflowProductId: string): Promise<void> {
|
||||||
@@ -128,12 +142,12 @@ class ProductsClient {
|
|||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new FetchError("Failed to remove Webflow product", res);
|
if (!res.ok) throw new WebflowError("Failed to remove Webflow product", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OrdersClient {
|
class OrdersClient {
|
||||||
constructor(private creds: Credentials) {}
|
constructor(private creds: Credentials) { }
|
||||||
|
|
||||||
async getAll(opt: { status?: Webflow.Orders.Order["status"] } = {}): Promise<Webflow.Orders.Order[]> {
|
async getAll(opt: { status?: Webflow.Orders.Order["status"] } = {}): Promise<Webflow.Orders.Order[]> {
|
||||||
// TODO: pagination
|
// TODO: pagination
|
||||||
@@ -143,7 +157,7 @@ class OrdersClient {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: this.creds.authHeader,
|
headers: this.creds.authHeader,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to get all Webflow orders", res);
|
if (!res.ok) throw new WebflowError("Failed to get all Webflow orders", res.status, await parsePayload(res));
|
||||||
const payload = (await res.json()) as { orders: Webflow.Orders.Order[] };
|
const payload = (await res.json()) as { orders: Webflow.Orders.Order[] };
|
||||||
return payload.orders;
|
return payload.orders;
|
||||||
}
|
}
|
||||||
@@ -165,7 +179,7 @@ class OrdersClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(webflowOrderUpdate),
|
body: JSON.stringify(webflowOrderUpdate),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to update Webflow order", res);
|
if (!res.ok) throw new WebflowError("Failed to update Webflow order", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(
|
async fulfill(
|
||||||
@@ -180,46 +194,37 @@ class OrdersClient {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(opt),
|
body: JSON.stringify(opt),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new FetchError("Failed to fulfill Webflow order", res);
|
if (!res.ok) throw new WebflowError("Failed to fulfill Webflow order", res.status, await parsePayload(res));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UtilClient {
|
class UtilClient {
|
||||||
constructor(private webhookSecret: string) {}
|
constructor(private webhookSecret: string) { }
|
||||||
|
|
||||||
verifyWebflowSignature(request: Request, body: unknown): boolean {
|
verifyWebflowSignature(request: Request, body: unknown): boolean {
|
||||||
try {
|
|
||||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||||
if (!timestamp) throw new Error("No timestamp provided");
|
if (!timestamp) return false;
|
||||||
|
|
||||||
const providedSignature = request.headers.get("x-webflow-signature");
|
const providedSignature = request.headers.get("x-webflow-signature");
|
||||||
if (!providedSignature) throw new Error("No signature provided");
|
if (!providedSignature) return false;
|
||||||
|
|
||||||
if (!body) throw new Error("Body is empty");
|
if (!body) return false;
|
||||||
|
|
||||||
const requestTimestamp = parseInt(timestamp, 10);
|
const requestTimestamp = parseInt(timestamp, 10);
|
||||||
|
if (Date.now() - requestTimestamp > 300000) return false;
|
||||||
|
|
||||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
.createHmac("sha256", this.webhookSecret)
|
.createHmac("sha256", this.webhookSecret)
|
||||||
.update(data)
|
.update(data)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
if (
|
try {
|
||||||
!crypto.timingSafeEqual(
|
return crypto.timingSafeEqual(
|
||||||
Buffer.from(hash, "hex"),
|
Buffer.from(hash, "hex"),
|
||||||
Buffer.from(providedSignature, "hex"),
|
Buffer.from(providedSignature, "hex"),
|
||||||
)
|
);
|
||||||
) {
|
} catch {
|
||||||
throw new Error("Invalid signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() - requestTimestamp > 300000) {
|
|
||||||
throw new Error("Request is older than 5 minutes");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error verifying signature: ${err}`);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user