Improve error handling
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import type { Webflow } from "./util/types";
|
||||
import { FetchError, type DeepPartial } from "./util/misc";
|
||||
import { type DeepPartial } from "./util/misc";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const parsePayload = (res: Response): Promise<unknown> =>
|
||||
res.json().catch(() => res.text().catch(() => undefined));
|
||||
|
||||
type ClientOptions = {
|
||||
siteId: string;
|
||||
collectionsId: string;
|
||||
@@ -15,8 +18,19 @@ type Credentials = {
|
||||
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 {
|
||||
constructor(private creds: Credentials) {}
|
||||
constructor(private creds: Credentials) { }
|
||||
|
||||
async create(
|
||||
webflowProductId: string,
|
||||
@@ -33,7 +47,7 @@ class SkusClient {
|
||||
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 }[] };
|
||||
return payload.skus.map((sku) => sku.id);
|
||||
}
|
||||
@@ -54,7 +68,7 @@ class SkusClient {
|
||||
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),
|
||||
});
|
||||
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 } };
|
||||
return payload.product.id;
|
||||
}
|
||||
@@ -86,7 +100,7 @@ class ProductsClient {
|
||||
method: "GET",
|
||||
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[] };
|
||||
return payload.items;
|
||||
}
|
||||
@@ -97,7 +111,7 @@ class ProductsClient {
|
||||
headers: this.creds.authHeader,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -113,7 +127,7 @@ class ProductsClient {
|
||||
},
|
||||
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> {
|
||||
@@ -128,12 +142,12 @@ class ProductsClient {
|
||||
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 {
|
||||
constructor(private creds: Credentials) {}
|
||||
constructor(private creds: Credentials) { }
|
||||
|
||||
async getAll(opt: { status?: Webflow.Orders.Order["status"] } = {}): Promise<Webflow.Orders.Order[]> {
|
||||
// TODO: pagination
|
||||
@@ -143,7 +157,7 @@ class OrdersClient {
|
||||
method: "GET",
|
||||
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[] };
|
||||
return payload.orders;
|
||||
}
|
||||
@@ -165,7 +179,7 @@ class OrdersClient {
|
||||
},
|
||||
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(
|
||||
@@ -180,46 +194,37 @@ class OrdersClient {
|
||||
},
|
||||
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 {
|
||||
constructor(private webhookSecret: string) {}
|
||||
constructor(private webhookSecret: string) { }
|
||||
|
||||
verifyWebflowSignature(request: Request, body: unknown): boolean {
|
||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||
if (!timestamp) return false;
|
||||
|
||||
const providedSignature = request.headers.get("x-webflow-signature");
|
||||
if (!providedSignature) return false;
|
||||
|
||||
if (!body) return false;
|
||||
|
||||
const requestTimestamp = parseInt(timestamp, 10);
|
||||
if (Date.now() - requestTimestamp > 300000) return false;
|
||||
|
||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||
const hash = crypto
|
||||
.createHmac("sha256", this.webhookSecret)
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
|
||||
try {
|
||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||
if (!timestamp) throw new Error("No timestamp provided");
|
||||
|
||||
const providedSignature = request.headers.get("x-webflow-signature");
|
||||
if (!providedSignature) throw new Error("No signature provided");
|
||||
|
||||
if (!body) throw new Error("Body is empty");
|
||||
|
||||
const requestTimestamp = parseInt(timestamp, 10);
|
||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||
const hash = crypto
|
||||
.createHmac("sha256", this.webhookSecret)
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
|
||||
if (
|
||||
!crypto.timingSafeEqual(
|
||||
Buffer.from(hash, "hex"),
|
||||
Buffer.from(providedSignature, "hex"),
|
||||
)
|
||||
) {
|
||||
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 crypto.timingSafeEqual(
|
||||
Buffer.from(hash, "hex"),
|
||||
Buffer.from(providedSignature, "hex"),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user