Fix tabbing

This commit is contained in:
Dominic Ferrando
2026-06-14 13:14:01 -04:00
parent 42f47c8601
commit 05dc26adb5
31 changed files with 9137 additions and 8508 deletions
+301 -301
View File
@@ -3,328 +3,328 @@ import { FetchError, type DeepPartial } from "./util/misc";
import crypto from "node:crypto";
export class WebflowService {
static Products = class {
static Skus = class {
static async create(
webflowProductId: string,
skus: DeepPartial<Webflow.Products.Skus.Sku>[],
): Promise<string[]> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
skus: skus,
}),
},
);
static Products = class {
static Skus = class {
static async create(
webflowProductId: string,
skus: DeepPartial<Webflow.Products.Skus.Sku>[],
): Promise<string[]> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
skus: skus,
}),
},
);
if (!res.ok) {
throw new FetchError(
"Failed to create Webflow product SKU",
res,
);
}
if (!res.ok) {
throw new FetchError(
"Failed to create Webflow product SKU",
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);
}
static async update(
webflowProductId: string,
webflowSkuId: string,
webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
sku: webflowSku,
}),
},
);
if (!res.ok) {
throw new FetchError(
"Failed to update Webflow product SKU",
res,
);
}
}
};
return payload.skus.map((sku) => sku.id);
}
static async update(
webflowProductId: string,
webflowSkuId: string,
webflowSku: DeepPartial<Webflow.Products.Skus.Sku>,
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}/skus/${webflowSkuId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({
sku: webflowSku,
}),
},
);
if (!res.ok) {
throw new FetchError(
"Failed to update Webflow product SKU",
res,
);
static async create(
webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>,
): Promise<string> {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProductAndSku),
});
if (!res.ok) {
throw new FetchError("Failed to create Webflow product", res);
}
const createdWebflowProduct = (await res.json()) as {
product: { id: string };
};
return createdWebflowProduct.product.id;
}
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "GET",
headers: {
...env().AUTH_HEADER,
},
});
if (!res.ok) {
throw new FetchError("Failed to get all Webflow products", res);
}
const payload = (await res.json()) as {
items: Webflow.Products.ProductAndSkus[];
};
return payload.items;
}
static async get(
webflowProductId: string,
): Promise<Webflow.Products.ProductAndSkus | undefined> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "GET",
headers: {
...env().AUTH_HEADER,
},
},
);
if (res.status === 404 || res.status === 400) {
return;
}
if (!res.ok) {
throw new FetchError("Failed to get Webflow product", res);
}
const payload =
(await res.json()) as Webflow.Products.ProductAndSkus;
return payload;
}
static async update(
webflowProductId: string,
webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProduct),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow product", res);
}
}
static async remove(webflowProductId: string): Promise<void> {
const res = await fetch(
`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({}),
},
);
if (!res.ok) {
throw new FetchError("Failed to remove Webflow product", res);
}
}
}
};
static async create(
webflowProductAndSku: DeepPartial<Webflow.Products.ProductAndSku>,
): Promise<string> {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProductAndSku),
});
static Orders = class {
static async getAll(
opt: { status?: Webflow.Orders.Order["status"] } = {},
): Promise<Webflow.Orders.Order[]> {
// TODO: pagination
const params = new URLSearchParams();
if (opt.status !== undefined) {
params.set("status", opt.status);
}
if (!res.ok) {
throw new FetchError("Failed to create Webflow product", res);
}
const res = await fetch(
`${env().API_SITES_URL}/orders?${params.toString()}`,
{
method: "GET",
headers: { ...env().AUTH_HEADER },
},
);
const createdWebflowProduct = (await res.json()) as {
product: { id: string };
};
return createdWebflowProduct.product.id;
}
if (!res.ok) {
throw new FetchError("Failed to get all Webflow orders", res);
}
static async getAll(): Promise<Webflow.Products.ProductAndSkus[]> {
const res = await fetch(`${env().API_SITES_URL}/products`, {
method: "GET",
headers: {
...env().AUTH_HEADER,
},
});
if (!res.ok) {
throw new FetchError("Failed to get all Webflow products", res);
}
const payload = (await res.json()) as {
items: Webflow.Products.ProductAndSkus[];
};
return payload.items;
}
static async get(
webflowProductId: string,
): Promise<Webflow.Products.ProductAndSkus | undefined> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "GET",
headers: {
...env().AUTH_HEADER,
},
},
);
if (res.status === 404 || res.status === 400) {
return;
}
if (!res.ok) {
throw new FetchError("Failed to get Webflow product", res);
}
const payload =
(await res.json()) as Webflow.Products.ProductAndSkus;
return payload;
}
static async update(
webflowProductId: string,
webflowProduct: DeepPartial<Webflow.Products.ProductAndSku>,
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/products/${webflowProductId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowProduct),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow product", res);
}
}
static async remove(webflowProductId: string): Promise<void> {
const res = await fetch(
`${env().API_COLLECTIONS_URL}/items/${webflowProductId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify({}),
},
);
if (!res.ok) {
throw new FetchError("Failed to remove Webflow product", res);
}
}
};
static Orders = class {
static async getAll(
opt: { status?: Webflow.Orders.Order["status"] } = {},
): Promise<Webflow.Orders.Order[]> {
// TODO: pagination
const params = new URLSearchParams();
if (opt.status !== undefined) {
params.set("status", opt.status);
}
const res = await fetch(
`${env().API_SITES_URL}/orders?${params.toString()}`,
{
method: "GET",
headers: { ...env().AUTH_HEADER },
},
);
if (!res.ok) {
throw new FetchError("Failed to get all Webflow orders", res);
}
const payload = (await res.json()) as {
orders: Webflow.Orders.Order[];
};
return payload.orders;
}
static async update(
webflowOrderId: string,
webflowOrderUpdate: {
comment?: string;
shippingProvider?: string;
shippingTracking?: string;
shippingTrackingURL: string;
},
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowOrderUpdate),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow order", res);
}
}
static async fulfill(
webflowOrderId: string,
opt: { sendOrderFulfilledEmail?: boolean } = {},
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(opt),
},
);
if (!res.ok) {
throw new FetchError("Failed to fulfill Webflow order", res);
}
}
};
static Util = class {
static verifyWebflowSignature(
request: Request,
body: unknown,
): Boolean {
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");
const payload = (await res.json()) as {
orders: Webflow.Orders.Order[];
};
return payload.orders;
}
const secret = env().WEBHOOK_SECRET;
if (!secret) {
throw new Error("No secret provided");
static async update(
webflowOrderId: string,
webflowOrderUpdate: {
comment?: string;
shippingProvider?: string;
shippingTracking?: string;
shippingTrackingURL: string;
},
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(webflowOrderUpdate),
},
);
if (!res.ok) {
throw new FetchError("Failed to update Webflow order", res);
}
}
if (!body) {
throw new Error("Body is empty");
static async fulfill(
webflowOrderId: string,
opt: { sendOrderFulfilledEmail?: boolean } = {},
): Promise<void> {
const res = await fetch(
`${env().API_SITES_URL}/orders/${webflowOrderId}/fulfill`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
...env().AUTH_HEADER,
},
body: JSON.stringify(opt),
},
);
if (!res.ok) {
throw new FetchError("Failed to fulfill Webflow order", res);
}
}
};
const requestTimestamp = parseInt(timestamp, 10);
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
const hash = crypto
.createHmac("sha256", secret)
.update(data)
.digest("hex");
static Util = class {
static verifyWebflowSignature(
request: Request,
body: unknown,
): Boolean {
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 (
!crypto.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(providedSignature, "hex"),
)
) {
throw new Error("Invalid signature");
const secret = env().WEBHOOK_SECRET;
if (!secret) {
throw new Error("No secret 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", secret)
.update(data)
.digest("hex");
if (
!crypto.timingSafeEqual(
Buffer.from(hash, "hex"),
Buffer.from(providedSignature, "hex"),
)
) {
throw new Error("Invalid signature");
}
const currentTime = Date.now();
if (currentTime - requestTimestamp > 300000) {
throw new Error("Request is older than 5 minutes");
}
return true;
} catch (err) {
console.error(`Error verifying signature: ${err}`);
return false;
}
}
const currentTime = Date.now();
if (currentTime - requestTimestamp > 300000) {
throw new Error("Request is older than 5 minutes");
}
return true;
} catch (err) {
console.error(`Error verifying signature: ${err}`);
return false;
}
}
};
};
}
const env = () => {
if (typeof Bun === "undefined") {
throw new Error(
"Must be in a server context. Make sure to run using --bun.",
);
}
const vars = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET,
} as Record<string, string>;
for (const varKey in vars) {
if (!vars[varKey]) {
console.log(`Missing ${varKey} environment variable`);
if (typeof Bun === "undefined") {
throw new Error(
"Must be in a server context. Make sure to run using --bun.",
);
}
}
return {
...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`,
API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`,
AUTH_HEADER: { Authorization: `bearer ${vars.AUTH_TOKEN}` },
};
const vars = {
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET,
} as Record<string, string>;
for (const varKey in vars) {
if (!vars[varKey]) {
console.log(`Missing ${varKey} environment variable`);
}
}
return {
...vars,
API_SITES_URL: `https://api.webflow.com/v2/sites/${vars.SITE_ID}`,
API_COLLECTIONS_URL: `https://api.webflow.com/v2/collections/${vars.COLLECTIONS_ID}`,
AUTH_HEADER: { Authorization: `bearer ${vars.AUTH_TOKEN}` },
};
};