diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 47719be..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,5 +0,0 @@
-fly.toml
-Dockerfile
-.dockerignore
-node_modules
-.git
diff --git a/.gitignore b/.gitignore
index 835cf44..a14702c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,34 @@
-node_modules/
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
.env
-.svelte-kit/
-build/
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/.zed/settings.json b/.zed/settings.json
index e69de29..79860b3 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -0,0 +1,5 @@
+// Folder-specific settings
+//
+// For a full list of overridable settings, and general information on folder-specific settings,
+// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
+{}
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0ed6f4c..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,39 +0,0 @@
-# syntax = docker/dockerfile:1
-
-FROM oven/bun:1.2.12-slim as base
-
-LABEL fly_launch_runtime="Bun"
-
-# NodeJS app lives here
-WORKDIR /app
-
-# Set production environment
-ENV NODE_ENV=production
-
-
-# Throw-away build stage to reduce size of final image
-FROM base as build
-
-# Install packages needed to build node modules
-RUN apt-get update -qq && \
- apt-get install -y python-is-python3 pkg-config build-essential
-
-# Install node modules
-COPY --link package.json .
-# RUN bun install --production
-RUN bun install
-
-# Copy application code
-COPY --link . .
-
-# Final stage for app image
-FROM base
-
-# Copy built application
-COPY --from=build /app /app
-
-RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
-
-# Start the server by default, this can be overwritten at runtime
-CMD [ "bun", "--bun", "run", "build" ]
-CMD [ "bun", "./build/index.js" ]
diff --git a/Makefile b/Makefile
deleted file mode 100644
index acbfd3b..0000000
--- a/Makefile
+++ /dev/null
@@ -1,5 +0,0 @@
-deploy-secrets:
- fly secrets import < .env
-
-deploy:
- bun vite build && fly deploy
diff --git a/apps/api/README.md b/apps/api/README.md
new file mode 100644
index 0000000..64fd6e2
--- /dev/null
+++ b/apps/api/README.md
@@ -0,0 +1,15 @@
+# backend
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
diff --git a/apps/api/bun.lock b/apps/api/bun.lock
new file mode 100644
index 0000000..b738da8
--- /dev/null
+++ b/apps/api/bun.lock
@@ -0,0 +1,81 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "backend",
+ "dependencies": {
+ "@elysiajs/cors": "^1.4.1",
+ "elysia": "^1.4.27",
+ "ml-levenberg-marquardt": "^5.0.0",
+ "zipcodes-us": "^1.1.2",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
+
+ "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
+
+ "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
+
+ "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
+
+ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+
+ "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
+
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "elysia": ["elysia@1.4.27", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="],
+
+ "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
+
+ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
+
+ "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
+
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+ "is-any-array": ["is-any-array@2.0.1", "", {}, "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ=="],
+
+ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
+
+ "ml-array-max": ["ml-array-max@1.2.4", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ=="],
+
+ "ml-array-min": ["ml-array-min@1.2.3", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q=="],
+
+ "ml-array-rescale": ["ml-array-rescale@1.3.7", "", { "dependencies": { "is-any-array": "^2.0.0", "ml-array-max": "^1.2.4", "ml-array-min": "^1.2.3" } }, "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ=="],
+
+ "ml-levenberg-marquardt": ["ml-levenberg-marquardt@5.0.0", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-matrix": "^6.12.1" } }, "sha512-vCFoO2DyYKGZFp+KxofN2cAvSc/3gufJHj/oaDHA2nV+NZH509fD7m35zQR2ZXA8fepxpVrwgCAQ0aAIsV2WtA=="],
+
+ "ml-matrix": ["ml-matrix@6.12.1", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-array-rescale": "^1.3.7" } }, "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
+
+ "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
+
+ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "zipcodes-us": ["zipcodes-us@1.1.2", "", {}, "sha512-UmUTS0O0/IHb0hbxv5r6IfATvD2B7FKcJTLsSv/dENBRiNACFUrGmhPuVss3kAhuhEpPH6NwKHJ7/rGMNXq5yA=="],
+ }
+}
diff --git a/apps/api/package.json b/apps/api/package.json
new file mode 100644
index 0000000..a46ffd1
--- /dev/null
+++ b/apps/api/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@blade-and-brawn/api",
+ "module": "index.ts",
+ "type": "module",
+ "private": true,
+ "dependencies": {
+ "@elysiajs/cors": "^1.4.1",
+ "elysia": "^1.4.27",
+ "zipcodes-us": "^1.1.2",
+ "@blade-and-brawn/calculator": "workspace:*",
+ "@blade-and-brawn/commerce": "workspace:*",
+ "ml-levenberg-marquardt": "^5.0.0"
+ }
+}
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
new file mode 100644
index 0000000..e5e0a71
--- /dev/null
+++ b/apps/api/src/index.ts
@@ -0,0 +1,195 @@
+import {
+ DEFAULT_STANDARDS_DATA,
+ LevelCalculator,
+ Standards,
+ type ActivityPerformance,
+ type Player,
+} from "@blade-and-brawn/calculator";
+import { cors } from "@elysiajs/cors";
+import { Elysia, NotFoundError } from "elysia";
+import { PrintfulService, SyncService, WebflowService, Printful, Webflow, FetchError } from "@blade-and-brawn/commerce";
+import zipcodesUs from "zipcodes-us";
+
+const levelCalculator = new LevelCalculator(new Standards(DEFAULT_STANDARDS_DATA));
+
+export const app = new Elysia()
+ .use(
+ cors({
+ origin: [
+ // bladeandbrawn.com (apex + any subdomain)
+ /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
+ // Webflow preview domains
+ /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
+ // Fly app host (this one already works)
+ /^https?:\/\/blade-and-brawn\.fly\.dev$/i,
+ // Local dev portal
+ "http://localhost:5173",
+ ],
+ }),
+ )
+
+ .error({
+ FetchError,
+ })
+
+ .onError(async ({ code, error }) => {
+ switch (code) {
+ case "FetchError":
+ console.error(error.message, await error.parse());
+ return error;
+ }
+ })
+
+ .onAfterHandle(({ request, set }) => {
+ console.log(
+ JSON.stringify({
+ lvl: "info",
+ msg: "req",
+ method: request.method,
+ path: new URL(request.url).pathname,
+ status: set.status ?? 200,
+ }),
+ );
+ })
+
+ .get("/", () => "Hello world")
+
+ // CALCULATOR
+
+ .post("/calculate", async ({ body, query }) => {
+ interface CalcRequest {
+ player: Player;
+ activityPerformances: ActivityPerformance[];
+ }
+ const { player, activityPerformances } = body as CalcRequest;
+ const output = {
+ levels: levelCalculator.calculate(player, activityPerformances),
+ };
+
+ // console.log({ log: query?.log });
+ // if (query?.log === "true") {
+ // void fetch("https://kv-logger.xominus.workers.dev", {
+ // method: "POST",
+ // headers: { "content-type": "application/json" },
+ // body: JSON.stringify({ output, player, activityPerformances }),
+ // }).catch(() => {});
+ // }
+
+ return output;
+ })
+
+ // COMMERCE
+ // TODO: protect with api key/token
+ .group("/products", (app) =>
+ app
+ .group("/sync", (app) =>
+ app
+ .get("/", async ({ }) => SyncService.state)
+ .post("/:printfulProductId?", async ({ params, set }) => {
+ if (SyncService.state.isSyncing) {
+ set.status = 409;
+ return { error: "Sync already in progress" };
+ }
+ const printfulProductId = params.printfulProductId
+ ? +params.printfulProductId
+ : undefined;
+ await SyncService.sync(printfulProductId);
+ return { ok: true };
+ }),
+ )
+ .get("/:printfulProductId", async ({ params }) => {
+ const printfulProductId = +params.printfulProductId;
+ const printfulProduct =
+ await PrintfulService.Products.get(printfulProductId);
+ if (!printfulProduct) {
+ return new NotFoundError();
+ }
+
+ const webflowProductId =
+ printfulProduct.sync_product.external_id.split("-")[0];
+ if (!webflowProductId) {
+ return new NotFoundError();
+ }
+
+ const webflowProduct =
+ await WebflowService.Products.get(webflowProductId);
+
+ return { printfulProduct, webflowProduct };
+ }),
+ )
+
+ // WEBHOOKS
+ .post("/webhook/printful", async ({ body }) => {
+ const payload = body as Printful.Webhook.EventPayload;
+
+ switch (payload.type) {
+ case Printful.Webhook.Event.ProductUpdated: {
+ const printfulProduct = payload.data.sync_product;
+ await SyncService.sync(printfulProduct.id);
+ break;
+ }
+ case Printful.Webhook.Event.ProductDeleted: {
+ await WebflowService.Products.remove(
+ payload.data.sync_product.external_id,
+ );
+ break;
+ }
+ case Printful.Webhook.Event.PackageShipped: {
+ const webflowOrderId = payload.data.order.external_id;
+ const shipInfo = payload.data.shipment;
+
+ await WebflowService.Orders.update(webflowOrderId, {
+ shippingTrackingURL: shipInfo.tracking_url,
+ shippingTracking: shipInfo.tracking_number,
+ shippingProvider: shipInfo.carrier,
+ });
+ await WebflowService.Orders.fulfill(webflowOrderId, {
+ sendOrderFulfilledEmail: true,
+ });
+ break;
+ }
+ }
+
+ return { ok: true };
+ })
+ .post("/webhook/webflow", async ({ request, body, set }) => {
+ if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
+ set.status = 400;
+ return "Invalid signature";
+ }
+
+ const payload = body as Webflow.Webhook.EventPayload;
+
+ switch (payload.triggerType) {
+ case Webflow.Webhook.Event.OrderCreated: {
+ const webflowOrder = payload.payload;
+
+ await PrintfulService.Orders.create({
+ external_id: webflowOrder.orderId,
+ // TODO: derive from webflow
+ shipping: "STANDARD",
+ recipient: {
+ name: webflowOrder.shippingAddress.addressee,
+ address1: webflowOrder.shippingAddress.line1,
+ address2: webflowOrder.shippingAddress.line2,
+ city: webflowOrder.shippingAddress.city,
+ state_code: zipcodesUs.find(
+ webflowOrder.shippingAddress.postalCode.split("-")[0] ?? "",
+ ).stateCode,
+ country_code: webflowOrder.shippingAddress.country,
+ zip: webflowOrder.shippingAddress.postalCode,
+ },
+ items: webflowOrder.purchasedItems.map((webflowOrderSku) => ({
+ external_variant_id: webflowOrderSku.variantId,
+ quantity: webflowOrderSku.count,
+ })),
+ });
+
+ break;
+ }
+ }
+
+ return { ok: true };
+ });
+
+app.listen(3000);
diff --git a/apps/api/src/scripts/analyze.ts b/apps/api/src/scripts/analyze.ts
new file mode 100644
index 0000000..63ec011
--- /dev/null
+++ b/apps/api/src/scripts/analyze.ts
@@ -0,0 +1,87 @@
+import {
+ type Levels,
+ DEFAULT_STANDARDS_DATA,
+} from "@blade-and-brawn/calculator";
+import { Activity, clamp, Gender, range } from "@blade-and-brawn/calculator";
+import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
+
+for (const activity of Object.values(Activity)) {
+ const DATASET_MAX_LEVEL = 5;
+ const NEW_LEVEL_COUNT = 2;
+ const RATIO_CLAMP = 1.4;
+
+ function expDecayModel([A, B, C]: number[]) {
+ return (i: number) => A! * Math.exp(-B! * i) + C!;
+ }
+
+ for (const gender of Object.values(Gender)) {
+ const standardsByGender = DEFAULT_STANDARDS_DATA[activity].standards.filter(
+ (s) => s["metrics"].gender === gender,
+ );
+ const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
+ for (const age of ages) {
+ const standards = standardsByGender.filter(
+ (s) => s.metrics.age === age,
+ );
+
+ const data = {
+ x: [] as number[],
+ y: [] as number[],
+ };
+
+ const isIncreasing =
+ standards[0]!.levels["2"]! > standards[0]!.levels["1"]!;
+
+ for (const i of range(DATASET_MAX_LEVEL).slice(0, -1)) {
+ const level = i + 1;
+ const progressionRatios: number[] = [];
+ for (const standard of standards) {
+ const curr = standard.levels[level]!;
+ const next = standard.levels[level]!;
+ progressionRatios.push(
+ isIncreasing ? next / curr : curr / next,
+ );
+ }
+
+ const sum = progressionRatios.reduce((p, c) => p + c, 0);
+ const progressionRatioAvg = sum / progressionRatios.length;
+
+ data.x.push(i);
+ data.y.push(progressionRatioAvg);
+ }
+
+ const fittedParams = LM(data, expDecayModel, {
+ initialValues: [0.4, 0.5, 1.1],
+ minValues: [0.0, 0.0, 1.02],
+ maxValues: [1.0, 2.0, 1.25],
+ maxIterations: 200,
+ });
+ const getDecayRatio = (i: number) =>
+ clamp(
+ expDecayModel(fittedParams.parameterValues)(i),
+ 1,
+ RATIO_CLAMP,
+ );
+
+ // UPDATE THE DATA
+ for (const standard of standards) {
+ const newLevels: Levels = {};
+
+ let prev = standard.levels["1"]!;
+ for (const i of range(NEW_LEVEL_COUNT)) {
+ const level = i + 1;
+ const ratio = getDecayRatio(level - NEW_LEVEL_COUNT);
+ prev = isIncreasing ? prev / ratio : prev * ratio;
+ newLevels[level] = Math.round(prev);
+ }
+
+ const oldLevels = standard.levels as Levels;
+ for (const i of range(DATASET_MAX_LEVEL)) {
+ const level = i + 1;
+ newLevels[level + NEW_LEVEL_COUNT] = oldLevels[level]!;
+ }
+ standard.levels = newLevels;
+ }
+ }
+ }
+}
diff --git a/apps/api/src/scripts/validate-products.ts b/apps/api/src/scripts/validate-products.ts
new file mode 100644
index 0000000..52693d7
--- /dev/null
+++ b/apps/api/src/scripts/validate-products.ts
@@ -0,0 +1,75 @@
+import { PrintfulService, SyncService, WebflowService } from "@blade-and-brawn/commerce";
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+type Validation = {
+ isValid: boolean;
+ message: string;
+};
+
+const validateEquality = (wValue: any, pValue: any): Validation => {
+ const validation = {
+ isValid: true,
+ message: "Valid",
+ };
+
+ if (wValue !== pValue) {
+ validation.isValid = false;
+ validation.message = `${wValue} !== ${pValue}`;
+ }
+
+ return validation;
+};
+
+const wProducts = await WebflowService.Products.getAll();
+
+let invalidCount = 0;
+for (const wProduct of wProducts) {
+ for (const sku of wProduct.skus ?? []) {
+ console.log("Webflow: " + sku.id, sku.fieldData.name);
+
+ const pVariant = await PrintfulService.Products.Variants.get(
+ `@${sku.id}`,
+ );
+ if (pVariant) {
+ console.log("Printful: " + pVariant.id, pVariant.name);
+ const validations = [
+ // validateEquality(sku.fieldData.name, pVariant.name),
+ validateEquality(
+ sku.fieldData["sku-values"]?.["color"],
+ SyncService.findColorInProductName(pVariant.name),
+ ),
+ validateEquality(
+ sku.fieldData["sku-values"]?.["size"],
+ pVariant.size,
+ ),
+ validateEquality(
+ sku.fieldData.price.value,
+ Math.floor(+pVariant.retail_price * 100),
+ ),
+ ];
+
+ // if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) {
+ // sku.fieldData.name = pVariant.name;
+ // await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku);
+ // }
+
+ const errors = validations.filter((v) => !v.isValid);
+ if (errors.length) {
+ ++invalidCount;
+ console.log("INVALID: ");
+ console.log(errors.map((e) => e.message).join(", "));
+ } else {
+ console.log("VALID");
+ }
+ } else {
+ ++invalidCount;
+ console.log("INVALID");
+ console.log("Not found");
+ }
+ console.log();
+ }
+ await sleep(3000);
+}
+
+console.log("INVALID COUNT: " + invalidCount);
diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json
new file mode 100644
index 0000000..96a27d9
--- /dev/null
+++ b/apps/api/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig.base.json",
+}
diff --git a/apps/portal/.gitignore b/apps/portal/.gitignore
new file mode 100644
index 0000000..f38e8b8
--- /dev/null
+++ b/apps/portal/.gitignore
@@ -0,0 +1,3 @@
+.env
+.svelte-kit/
+build/
diff --git a/.prettierrc b/apps/portal/.prettierrc
similarity index 100%
rename from .prettierrc
rename to apps/portal/.prettierrc
diff --git a/apps/portal/bun.lock b/apps/portal/bun.lock
new file mode 100644
index 0000000..02327cd
--- /dev/null
+++ b/apps/portal/bun.lock
@@ -0,0 +1,339 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "@tailwindcss/vite": "^4.2.1",
+ "daisyui": "^5.5.19",
+ "memoirist": "^0.4.0",
+ "ml-levenberg-marquardt": "^5.0.0",
+ "tailwindcss": "^4.2.1",
+ },
+ "devDependencies": {
+ "@sveltejs/kit": "^2.53.4",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@types/bun": "^1.3.9",
+ "svelte": "^5.53.6",
+ "svelte-adapter-bun": "^0.5.2",
+ "svelte-check": "^4.4.4",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1",
+ },
+ },
+ },
+ "packages": {
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
+
+ "@sveltejs/kit": ["@sveltejs/kit@2.53.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA=="],
+
+ "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
+
+ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
+
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+
+ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
+
+ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+
+ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+
+ "daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
+
+ "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "globalyzer": ["globalyzer@0.1.0", "", {}, "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q=="],
+
+ "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "is-any-array": ["is-any-array@2.0.1", "", {}, "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ=="],
+
+ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
+
+ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
+
+ "ml-array-max": ["ml-array-max@1.2.4", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ=="],
+
+ "ml-array-min": ["ml-array-min@1.2.3", "", { "dependencies": { "is-any-array": "^2.0.0" } }, "sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q=="],
+
+ "ml-array-rescale": ["ml-array-rescale@1.3.7", "", { "dependencies": { "is-any-array": "^2.0.0", "ml-array-max": "^1.2.4", "ml-array-min": "^1.2.3" } }, "sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ=="],
+
+ "ml-levenberg-marquardt": ["ml-levenberg-marquardt@5.0.0", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-matrix": "^6.12.1" } }, "sha512-vCFoO2DyYKGZFp+KxofN2cAvSc/3gufJHj/oaDHA2nV+NZH509fD7m35zQR2ZXA8fepxpVrwgCAQ0aAIsV2WtA=="],
+
+ "ml-matrix": ["ml-matrix@6.12.1", "", { "dependencies": { "is-any-array": "^2.0.1", "ml-array-rescale": "^1.3.7" } }, "sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw=="],
+
+ "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
+
+ "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
+ "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
+
+ "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
+
+ "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="],
+
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "svelte": ["svelte@5.53.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-lP5DGF3oDDI9fhHcSpaBiJEkFLuS16h92DhM1L5K1lFm0WjOmUh1i2sNkBBk8rkxJRpob0dBE75jRfUzGZUOGA=="],
+
+ "svelte-adapter-bun": ["svelte-adapter-bun@0.5.2", "", { "dependencies": { "tiny-glob": "^0.2.9" } }, "sha512-xEtFgaal6UgrCwwkSIcapO9kopoFNUYCYqyKCikdqxX9bz2TDYnrWQZ7qBnkunMxi1HOIERUCvTcebYGiarZLA=="],
+
+ "svelte-check": ["svelte-check@4.4.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ=="],
+
+ "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
+
+ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
+
+ "tiny-glob": ["tiny-glob@0.2.9", "", { "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
+
+ "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ }
+}
diff --git a/apps/portal/package.json b/apps/portal/package.json
new file mode 100644
index 0000000..bb340b5
--- /dev/null
+++ b/apps/portal/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@blade-and-brawn/portal",
+ "type": "module",
+ "devDependencies": {
+ "@sveltejs/kit": "^2.53.4",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@types/bun": "^1.3.9",
+ "svelte": "^5.53.6",
+ "svelte-adapter-bun": "^0.5.2",
+ "svelte-check": "^4.4.4",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1"
+ },
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "prepare": "svelte-kit sync || echo ''",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.2.1",
+ "daisyui": "^5.5.19",
+ "memoirist": "^0.4.0",
+ "tailwindcss": "^4.2.1",
+ "@blade-and-brawn/calculator": "workspace:*",
+ "@blade-and-brawn/commerce": "workspace:*"
+ }
+}
diff --git a/src/app.d.ts b/apps/portal/src/app.d.ts
similarity index 100%
rename from src/app.d.ts
rename to apps/portal/src/app.d.ts
diff --git a/src/app.html b/apps/portal/src/app.html
similarity index 100%
rename from src/app.html
rename to apps/portal/src/app.html
diff --git a/src/lib/assets/favicon.svg b/apps/portal/src/lib/assets/favicon.svg
similarity index 100%
rename from src/lib/assets/favicon.svg
rename to apps/portal/src/lib/assets/favicon.svg
diff --git a/apps/portal/src/lib/components/PlayersTable.svelte b/apps/portal/src/lib/components/PlayersTable.svelte
new file mode 100644
index 0000000..3557404
--- /dev/null
+++ b/apps/portal/src/lib/components/PlayersTable.svelte
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+ {#each calculations as calculation, index}
+
+
+
+
+
+ -
+
+
OVERALL
+
+ {calculation?.levels?.player || "N/A"}
+
+
+
+
+ {#each Object.values(Attribute) as attribute}
+ -
+
+
+ {attribute}
+
+
+ {calculation?.levels?.attributes?.[
+ attribute
+ ] || "N/A"}
+
+
+
+ {/each}
+
+
+
+
+
+
+
+
+ {/each}
+
diff --git a/src/lib/components/StandardsTable.svelte b/apps/portal/src/lib/components/StandardsTable.svelte
similarity index 97%
rename from src/lib/components/StandardsTable.svelte
rename to apps/portal/src/lib/components/StandardsTable.svelte
index 8280e87..3f92c6c 100644
--- a/src/lib/components/StandardsTable.svelte
+++ b/apps/portal/src/lib/components/StandardsTable.svelte
@@ -1,17 +1,14 @@
+
+
+
+
+
+
+
+
+ {@render children?.()}
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/+page.server.ts b/apps/portal/src/routes/+page.server.ts
similarity index 100%
rename from src/routes/+page.server.ts
rename to apps/portal/src/routes/+page.server.ts
diff --git a/src/app.css b/apps/portal/src/routes/app.css
similarity index 100%
rename from src/app.css
rename to apps/portal/src/routes/app.css
diff --git a/src/routes/calculator/+page.svelte b/apps/portal/src/routes/calculator/+page.svelte
similarity index 97%
rename from src/routes/calculator/+page.svelte
rename to apps/portal/src/routes/calculator/+page.svelte
index 3bcad31..a11470b 100644
--- a/src/routes/calculator/+page.svelte
+++ b/apps/portal/src/routes/calculator/+page.svelte
@@ -1,20 +1,19 @@
+
+{#await data.products.printful}
+ Loading...
+{:then printfulProducts}
+
+
+
+
+
+ | Name |
+ Status |
+ Sync |
+
+
+
+ {#each printfulProducts as printfulProduct}
+
+ |
+
+ {#await isProductSynced(printfulProduct) then isSynced}
+ {#if synchronizer.syncingIds.includes(printfulProduct.id)}
+
+ Syncing...
+
+ {:else if isSynced}
+
+ Synced
+
+ {:else}
+
+ Desynced
+
+ {/if}
+ {/await}
+ |
+
+
+ |
+
+ {/each}
+
+
+
+{:catch error}
+ Something went wrong: {error.message}
+{/await}
diff --git a/static/robots.txt b/apps/portal/static/robots.txt
similarity index 100%
rename from static/robots.txt
rename to apps/portal/static/robots.txt
diff --git a/svelte.config.js b/apps/portal/svelte.config.js
similarity index 100%
rename from svelte.config.js
rename to apps/portal/svelte.config.js
diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json
new file mode 100644
index 0000000..fcffc73
--- /dev/null
+++ b/apps/portal/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "rewriteRelativeImportExtensions": true,
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler",
+ },
+}
diff --git a/vite.config.ts b/apps/portal/vite.config.ts
similarity index 100%
rename from vite.config.ts
rename to apps/portal/vite.config.ts
diff --git a/bun.lock b/bun.lock
index 8bc24ac..ac6130b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -2,36 +2,71 @@
"lockfileVersion": 1,
"workspaces": {
"": {
+ "name": "blade-and-brawn",
+ "devDependencies": {
+ "@types/bun": "^1.3.9",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ "apps/api": {
+ "name": "@blade-and-brawn/api",
"dependencies": {
- "@elysiajs/cors": "^1.4.0",
- "@elysiajs/eden": "^1.4.5",
- "@tailwindcss/vite": "^4.1.18",
- "daisyui": "^5.5.13",
- "elysia": "^1.4.18",
- "memoirist": "^0.4.0",
+ "@blade-and-brawn/calculator": "workspace:*",
+ "@blade-and-brawn/commerce": "workspace:*",
+ "@elysiajs/cors": "^1.4.1",
+ "elysia": "^1.4.27",
"ml-levenberg-marquardt": "^5.0.0",
- "tailwindcss": "^4.1.18",
"zipcodes-us": "^1.1.2",
},
- "devDependencies": {
- "@sveltejs/kit": "^2.49.2",
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
- "@types/bun": "^1.3.4",
- "svelte": "^5.45.10",
- "svelte-adapter-bun": "^0.5.2",
- "svelte-check": "^4.3.4",
- "typescript": "^5.9.3",
- "vite": "^7.2.7",
+ },
+ "apps/portal": {
+ "name": "@blade-and-brawn/portal",
+ "dependencies": {
+ "@blade-and-brawn/calculator": "workspace:*",
+ "@blade-and-brawn/commerce": "workspace:*",
+ "@tailwindcss/vite": "^4.2.1",
+ "daisyui": "^5.5.19",
+ "memoirist": "^0.4.0",
+ "tailwindcss": "^4.2.1",
},
+ "devDependencies": {
+ "@sveltejs/kit": "^2.53.4",
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
+ "@types/bun": "^1.3.9",
+ "svelte": "^5.53.6",
+ "svelte-adapter-bun": "^0.5.2",
+ "svelte-check": "^4.4.4",
+ "typescript": "^5.9.3",
+ "vite": "^7.3.1",
+ },
+ },
+ "packages/calculator": {
+ "name": "@blade-and-brawn/calculator",
+ "version": "0.0.0",
+ "dependencies": {
+ "ml-levenberg-marquardt": "^5.0.0",
+ },
+ },
+ "packages/commerce": {
+ "name": "@blade-and-brawn/commerce",
+ "version": "0.0.0",
},
},
"packages": {
- "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
+ "@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"],
+
+ "@blade-and-brawn/calculator": ["@blade-and-brawn/calculator@workspace:packages/calculator"],
+
+ "@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"],
+
+ "@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"],
+
+ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
- "@elysiajs/eden": ["@elysiajs/eden@1.4.8", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-a7oct2kFa49tH+GawZtSUCZR2rQgucNYgGLz8alXUqb4IrU3PASA0T4zXJw4MhdV1Xb6vyiR7p7kkqJcVjgbkA=="],
-
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -96,61 +131,67 @@
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
- "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="],
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
- "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="],
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
- "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="],
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
- "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="],
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
- "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="],
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
- "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="],
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
- "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="],
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
- "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="],
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
- "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="],
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
- "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="],
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
- "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="],
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
- "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="],
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
- "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="],
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
- "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="],
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
- "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="],
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
- "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="],
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
- "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="],
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
- "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="],
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
- "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="],
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
- "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="],
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
- "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="],
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
- "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="],
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
- "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
- "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
- "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="],
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
"@sveltejs/kit": ["@sveltejs/kit@2.53.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
- "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="],
+ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
@@ -182,7 +223,7 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
- "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
+ "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@@ -192,11 +233,11 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
- "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="],
+ "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
- "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
@@ -208,7 +249,7 @@
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
- "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"daisyui": ["daisyui@5.5.19", "", {}, "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA=="],
@@ -236,9 +277,7 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
- "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
-
- "file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
+ "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -318,7 +357,7 @@
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
- "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="],
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
@@ -344,7 +383,7 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
- "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
+ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
@@ -352,16 +391,18 @@
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
- "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
- "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
+ "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zipcodes-us": ["zipcodes-us@1.1.2", "", {}, "sha512-UmUTS0O0/IHb0hbxv5r6IfATvD2B7FKcJTLsSv/dENBRiNACFUrGmhPuVss3kAhuhEpPH6NwKHJ7/rGMNXq5yA=="],
+ "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
+
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -373,7 +414,5 @@
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
- "elysia/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
}
}
diff --git a/fly.toml b/fly.toml
deleted file mode 100644
index 1421632..0000000
--- a/fly.toml
+++ /dev/null
@@ -1,29 +0,0 @@
-# fly.toml app configuration file generated for blade-and-brawn on 2025-05-04T14:56:24-04:00
-#
-# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
-#
-
-app = 'blade-and-brawn'
-primary_region = 'ord'
-
-[build]
-
-[env]
- PORT = '3000'
-
-[http_service]
- internal_port = 3000
- force_https = true
- auto_stop_machines = 'stop'
- auto_start_machines = true
- min_machines_running = 0
- processes = ['app']
-
-[[vm]]
- memory = '1gb'
- cpu_kind = 'shared'
- cpus = 1
-
-[[mounts]]
- source = "data"
- destination = "/data"
diff --git a/misc/website/audio/knight.ogg b/misc/website/audio/knight.ogg
new file mode 100644
index 0000000..9f72882
Binary files /dev/null and b/misc/website/audio/knight.ogg differ
diff --git a/misc/website/audio/rogue.ogg b/misc/website/audio/rogue.ogg
new file mode 100644
index 0000000..f37b663
Binary files /dev/null and b/misc/website/audio/rogue.ogg differ
diff --git a/misc/website/audio/warrior.ogg b/misc/website/audio/warrior.ogg
new file mode 100644
index 0000000..602afd7
Binary files /dev/null and b/misc/website/audio/warrior.ogg differ
diff --git a/misc/website/index.html b/misc/website/index.html
new file mode 100644
index 0000000..a3933e8
--- /dev/null
+++ b/misc/website/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+
diff --git a/misc/website/main.js b/misc/website/main.js
new file mode 100644
index 0000000..7971312
--- /dev/null
+++ b/misc/website/main.js
@@ -0,0 +1,96 @@
+const $ = (id) => document.getElementById(id);
+
+let currentAudio = null;
+function playAudio(newAudio) {
+ // --------------
+ // HELPERS
+ // --------------
+ const TIME_STEP = 500;
+ const VOL_STEP = .05;
+
+ function fadeOut(audio) {
+ const intervalId = setInterval(() => {
+ audio.volume = Math.max(0, audio.volume - VOL_STEP);
+ if (audio.volume <= 0) {
+ clearInterval(intervalId);
+ }
+ }, TIME_STEP)
+ }
+
+ function fadeIn(audio) {
+ audio.volume = 0;
+ audio.play();
+ const intervalId = setInterval(() => {
+ audio.volume = Math.min(1, audio.volume + VOL_STEP);
+ if (audio.volume >= 1) {
+ clearInterval(intervalId);
+ }
+ }, TIME_STEP)
+ }
+ // --------------
+
+ // --------------
+ // MAIN LOGIC
+ // --------------
+
+ const prevAudio = currentAudio;
+
+ if (prevAudio === newAudio)
+ return;
+
+ if (prevAudio) {
+ fadeOut(prevAudio);
+ }
+
+ fadeIn(newAudio);
+
+ currentAudio = newAudio;
+ // --------------
+}
+
+document.addEventListener("DOMContentLoaded", async function() {
+ const $ = (id) => document.getElementById(id);
+
+ const classNames = ["rogue", "knight", "warrior"];
+ const audios = {};
+
+ for (const className of classNames) {
+ const audio = document.createElement("audio");
+ audio.src = `https://bladeandbrawn.com/${className}.ogg`;
+ audio.loop = true;
+ audio.volume = 0;
+ audio.id = `audio-${className}`;
+ $("audioContainer").appendChild(audio);
+ audios[className] = audio;
+ }
+
+ await Promise.all(classNames.map(async (className) => {
+ const audio = audios[className];
+ try {
+ await audio.play();
+ audio.pause();
+ audio.currentTime = 0;
+ } catch (e) {
+ console.warn(`Autoplay failed for ${className}:`, e);
+ }
+ }));
+
+ // Start all in perfect sync
+ classNames.forEach(className => {
+ const audio = audios[className];
+ audio.currentTime = 0;
+ audio.play();
+ });
+
+ // Hook up buttons
+ classNames.forEach(className => {
+ $(`btn-${className}`).onclick = () => {
+ $("currentClass").textContent = className;
+ playAudio(audios[className]);
+ };
+ });
+
+ // Set initial state
+ $("currentClass").textContent = "rogue";
+ playAudio(audios["rogue"]);
+});
diff --git a/package.json b/package.json
index 4be739f..af154c2 100644
--- a/package.json
+++ b/package.json
@@ -1,32 +1,14 @@
{
- "type": "module",
+ "name": "blade-and-brawn",
+ "private": true,
+ "workspaces": [
+ "apps/*",
+ "packages/*"
+ ],
+ "peerDependencies": {
+ "typescript": "^5"
+ },
"devDependencies": {
- "@sveltejs/kit": "^2.53.4",
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
- "@types/bun": "^1.3.9",
- "svelte": "^5.53.6",
- "svelte-adapter-bun": "^0.5.2",
- "svelte-check": "^4.4.4",
- "typescript": "^5.9.3",
- "vite": "^7.3.1"
- },
- "scripts": {
- "dev": "vite dev",
- "build": "vite build",
- "preview": "vite preview",
- "prepare": "svelte-kit sync || echo ''",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
- },
- "dependencies": {
- "@elysiajs/cors": "^1.4.1",
- "@elysiajs/eden": "^1.4.8",
- "@tailwindcss/vite": "^4.2.1",
- "daisyui": "^5.5.19",
- "elysia": "^1.4.27",
- "memoirist": "^0.4.0",
- "ml-levenberg-marquardt": "^5.0.0",
- "tailwindcss": "^4.2.1",
- "zipcodes-us": "^1.1.2"
+ "@types/bun": "^1.3.9"
}
}
diff --git a/packages/calculator/package.json b/packages/calculator/package.json
new file mode 100644
index 0000000..b4776a1
--- /dev/null
+++ b/packages/calculator/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@blade-and-brawn/calculator",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "dependencies": {
+ "ml-levenberg-marquardt": "^5.0.0"
+ },
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/src/lib/services/calculator/config.json b/packages/calculator/src/config.json
similarity index 100%
rename from src/lib/services/calculator/config.json
rename to packages/calculator/src/config.json
diff --git a/src/lib/data/avg-weights.json b/packages/calculator/src/data/avg-weights.json
similarity index 100%
rename from src/lib/data/avg-weights.json
rename to packages/calculator/src/data/avg-weights.json
diff --git a/src/lib/data/standards.json b/packages/calculator/src/data/default-standards.json
similarity index 100%
rename from src/lib/data/standards.json
rename to packages/calculator/src/data/default-standards.json
diff --git a/packages/calculator/src/index.ts b/packages/calculator/src/index.ts
new file mode 100644
index 0000000..7015486
--- /dev/null
+++ b/packages/calculator/src/index.ts
@@ -0,0 +1,902 @@
+import {
+ Activity,
+ Attribute,
+ Gender,
+ getAvgWeight,
+ kgToLb,
+ lbToKg,
+ range,
+ type ActivityPerformance,
+ type Player,
+ type StandardUnit,
+} from "../src/util";
+import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
+import defaultConfig from "./config.json" assert { type: "json" };
+import defaultStandardsJson from "./data/default-standards.json" assert { type: "json" };
+
+// SOURCES
+// Squat, Bench, Dead Lift:
+// http://lonkilgore.com/resources/Lon_Kilgore_Strength_Standard_Tables-Copyright-2023.pdf
+// 1 mile run:
+// https://runninglevel.com/running-times/1-mile-times
+// Broad Jump:
+// https://nrpt.co.uk/training/tests/power/broad.htm
+// 3 Cone drill:
+// https://nflsavant.com/combine.php?utm_source=chatgpt.com
+
+export * from "./util"
+
+export type LevelCalculatorOutput = {
+ player: number;
+ attributes: Record;
+};
+
+export type Levels = Record;
+
+export interface Metrics {
+ age: number;
+ weight: number;
+ gender: Gender;
+}
+
+type NumberMetric = Extract;
+
+export interface Standard {
+ metrics: Metrics;
+ levels: Levels;
+}
+
+type Generator = {
+ metric: NumberMetric;
+};
+
+export type StandardsData = Record<
+ Activity,
+ {
+ metadata: {
+ attribute: Attribute;
+ generators: Generator[];
+ unit: StandardUnit;
+ name: string;
+ };
+ standards: Standard[];
+ }
+>;
+
+const metricPriority = (m: NumberMetric) => {
+ if (m === "age") return 0;
+ if (m === "weight") return 1;
+ return 2;
+};
+
+export const DEFAULT_STANDARDS_DATA = defaultStandardsJson as StandardsData;
+
+export class LevelCalculator {
+ standards: Standards;
+
+ public constructor(standards: Standards) {
+ this.standards = standards;
+ }
+
+ // -------------------------------------------------------------------------------------------------
+ // Level calculations
+ // -------------------------------------------------------------------------------------------------
+
+ public calculate(
+ player: Player,
+ activityPerformances: ActivityPerformance[],
+ ): LevelCalculatorOutput {
+ const levels: LevelCalculatorOutput = {
+ player: 0,
+ attributes: {
+ [Attribute.Strength]: 0,
+ [Attribute.Power]: 0,
+ [Attribute.Endurance]: 0,
+ [Attribute.Agility]: 0,
+ } as const,
+ };
+
+ for (const value of Object.values(player)) {
+ if (!value) return levels;
+ }
+
+ levels.attributes = this.calculateAllAttributeLevels(
+ player,
+ activityPerformances,
+ );
+ for (const level of Object.values(levels.attributes)) {
+ if (!level) return levels;
+ }
+
+ const attrLevelsSum = Object.values(levels.attributes).reduce(
+ (sum, lvl) => sum + lvl,
+ 0,
+ );
+ levels.player = Math.round(
+ attrLevelsSum / Object.keys(Attribute).length,
+ );
+
+ return levels;
+ }
+
+ private calculateAllAttributeLevels(
+ player: Player,
+ activityPerformances: ActivityPerformance[],
+ ): Record {
+ const attrLevels: Record = {
+ [Attribute.Strength]: 0,
+ [Attribute.Power]: 0,
+ [Attribute.Endurance]: 0,
+ [Attribute.Agility]: 0,
+ } as const;
+
+ for (const value of Object.values(player)) {
+ if (!value) return attrLevels;
+ }
+
+ (Object.values(Attribute) as Attribute[]).forEach((attribute) => {
+ const attrActivityPerformances = activityPerformances.filter(
+ (p) =>
+ this.standards.byActivity(p.activity).getMetadata()
+ .attribute === attribute,
+ );
+ attrLevels[attribute] = this.calculateAttributeLevel(
+ attribute,
+ player,
+ attrActivityPerformances,
+ );
+ });
+
+ return attrLevels;
+ }
+
+ private calculateAttributeLevel(
+ attribute: Attribute,
+ player: Player,
+ activityPerformances: ActivityPerformance[],
+ ): number {
+ // must be activities of the given attribute
+ if (
+ activityPerformances.some(
+ (p) =>
+ this.standards.byActivity(p.activity).getMetadata()
+ .attribute !== attribute,
+ )
+ ) {
+ throw new Error("Wrong activity performance attribute");
+ }
+
+ // must perform all of an attributes activities
+ for (const activity of this.standards.getAttributeActivities(
+ attribute,
+ )) {
+ const filteredActivites = activityPerformances.map(
+ (p) => p.activity,
+ );
+ if (!filteredActivites.includes(activity)) return 0;
+ }
+
+ // must have all activties using a valid performance value (> 0)
+ for (const activityPerformance of activityPerformances) {
+ if (activityPerformance.performance <= 0) return 0;
+ }
+
+ const activityLevels = activityPerformances.map((p) => {
+ const interpolatedStandard = this.standards
+ .byActivity(p.activity)
+ .byMetrics(player.metrics)
+ .getOneInterpolated();
+ return this.findLevel(interpolatedStandard, p.performance);
+ });
+
+ const activityLevelsAvg = Math.round(
+ activityLevels.reduce((sum, curr) => sum + curr, 0) /
+ activityLevels.length,
+ );
+
+ return activityLevelsAvg;
+ }
+
+ // -------------------------------------------------------------------------------------------------
+ // Level utilities (helpers)
+ // -------------------------------------------------------------------------------------------------
+
+ private findLevel(standard: Standard, performance: number): number {
+ let resultLevel = 1;
+ let resultLevelDiff = Infinity;
+
+ for (const level in standard.levels) {
+ if (!standard.levels[level]) continue;
+
+ const diff = Math.abs(performance - standard.levels[level]);
+ if (diff < resultLevelDiff) {
+ resultLevel = +level;
+ resultLevelDiff = diff;
+ }
+ }
+ return resultLevel;
+ }
+}
+
+export type StandardsConfig = {
+ global: {
+ maxLevel: number;
+ };
+ activity: Record<
+ Activity,
+ {
+ weightModifier: number;
+ weightSkew: number;
+ ageModifier: number;
+ enableGeneration: boolean;
+ difficultyModifier: number;
+ peakAge: number;
+ stretch: {
+ upper: number;
+ lower: number;
+ };
+ }
+ >;
+};
+
+export class Standards {
+ readonly cfg: StandardsConfig;
+ private standardsData: StandardsData;
+
+ constructor(
+ standardsData: StandardsData,
+ cfg?: Partial,
+ ) {
+ this.cfg = Object.assign(defaultConfig, cfg);
+
+ // prepare data
+ this.standardsData = structuredClone(standardsData);
+
+ // STRETCH
+ for (const activity of Object.values(Activity)) {
+ const BASE_MAX_LEVEL = 5;
+
+ function expDecayModel([A, B, C]: number[]) {
+ return (i: number) => A * Math.exp(-B * i) + C;
+ }
+
+ if (!this.cfg.activity[activity].enableGeneration) {
+ continue;
+ }
+
+ for (const gender of Object.values(Gender)) {
+ const standardsByGender = this.standardsData[
+ activity
+ ].standards.filter((s) => s["metrics"].gender === gender);
+ const ages = [
+ ...new Set(standardsByGender.map((s) => s.metrics.age)),
+ ];
+ for (const age of ages) {
+ const standards = standardsByGender.filter(
+ (s) => s.metrics.age === age,
+ );
+
+ const data = {
+ x: [] as number[],
+ y: [] as number[],
+ };
+
+ const isIncreasing =
+ standards[0].levels["2"] > standards[0].levels["1"];
+
+ for (const i of range(BASE_MAX_LEVEL).slice(0, -1)) {
+ const level = i + 1;
+ const progressionRatios: number[] = [];
+ for (const standard of standards) {
+ const curr = standard.levels[level];
+ const next = standard.levels[level + 1];
+ progressionRatios.push(
+ isIncreasing ? next / curr : curr / next,
+ );
+ }
+
+ const sum = progressionRatios.reduce(
+ (p, c) => p + c,
+ 0,
+ );
+ const progressionRatioAvg =
+ sum / progressionRatios.length;
+
+ data.x.push(i);
+ data.y.push(progressionRatioAvg);
+ }
+
+ const fittedParams = LM(data, expDecayModel, {
+ initialValues: [0.4, 0.5, 1.1],
+ minValues: [0.0, 0.0, 1.02],
+ maxValues: [1.0, 2.0, 1.2],
+ maxIterations: 200,
+ });
+ const getDecayRatio = (i: number) =>
+ expDecayModel(fittedParams.parameterValues)(i);
+
+ // UPDATE THE DATA
+ for (const standard of standards) {
+ const newLevels: Levels = {};
+ const newLowerLevelCount = Math.max(
+ Math.round(
+ this.cfg.activity[activity].stretch.lower,
+ ),
+ 0,
+ );
+ const newUpperLevelCount = Math.max(
+ Math.round(
+ this.cfg.activity[activity].stretch.upper,
+ ),
+ 0,
+ );
+
+ const oldLevels = standard.levels as Levels;
+ let prev = oldLevels["1"];
+ for (const i of range(newLowerLevelCount).reverse()) {
+ const level = i + 1;
+ const ratio = getDecayRatio(-i - 1);
+ prev = isIncreasing ? prev / ratio : prev * ratio;
+ newLevels[level] = Math.round(prev);
+ }
+
+ for (const i of range(BASE_MAX_LEVEL)) {
+ const level = i + 1;
+ newLevels[level + newLowerLevelCount] =
+ oldLevels[level];
+ }
+
+ prev = oldLevels[BASE_MAX_LEVEL];
+ for (const i of range(newUpperLevelCount)) {
+ const level =
+ BASE_MAX_LEVEL + newLowerLevelCount + (i + 1);
+ const ratio = getDecayRatio(level - 1);
+ prev = isIncreasing ? prev * ratio : prev / ratio;
+ newLevels[level] = Math.round(prev);
+ }
+
+ standard.levels = newLevels;
+ }
+ }
+ }
+ }
+
+ // EXPAND, COMPRESS, SKEW
+ for (const activity of Object.values(Activity)) {
+ for (const standard of this.standardsData[activity].standards) {
+ // expand
+ let i = 1;
+ while (
+ Object.keys(standard.levels).length <
+ this.cfg.global.maxLevel
+ ) {
+ standard.levels = this.expandLevels(standard.levels, i++);
+ }
+
+ // compress
+ if (
+ Object.keys(standard.levels).length >
+ this.cfg.global.maxLevel
+ ) {
+ standard.levels = this.compressLevels(
+ standard.levels,
+ this.cfg.global.maxLevel,
+ );
+ }
+
+ // apply skew config
+ for (const level in standard.levels) {
+ standard.levels[level] =
+ standard.levels[level] *
+ (1 + this.cfg.activity[activity].difficultyModifier);
+ }
+ }
+
+ // generate data
+ const allGenerators = this.cfg.activity[activity].enableGeneration
+ ? this.standardsData[activity].metadata.generators.sort(
+ (a, b) =>
+ metricPriority(a.metric) - metricPriority(b.metric),
+ )
+ : [];
+
+ for (const generator of allGenerators) {
+ if (generator.metric === "age") {
+ for (const gender of Object.values(Gender)) {
+ const peakAge = this.cfg.activity[activity].peakAge;
+ const ageStep = 10;
+
+ const minAge = 0;
+ const maxAge = 100;
+
+ const referenceStandard = this.byActivity(activity)
+ .byMetrics({
+ weight: getAvgWeight(gender, peakAge),
+ gender,
+ age: peakAge,
+ })
+ .getOneInterpolated();
+
+ let currAge = minAge;
+ while (currAge <= maxAge) {
+ const newStandard: Standard = {
+ metrics: {
+ weight: getAvgWeight(gender, currAge),
+ age: currAge,
+ gender,
+ },
+ levels: {},
+ };
+
+ for (const lvl in referenceStandard.levels) {
+ const base = referenceStandard.levels[lvl];
+ if (base == null) continue;
+
+ const ageModifier =
+ this.cfg.activity[activity].ageModifier;
+
+ const leftSpan = peakAge - minAge;
+ const rightSpan = maxAge - peakAge;
+
+ // avoid divide-by-zero
+ if (leftSpan <= 0 || rightSpan <= 0) {
+ newStandard.levels[lvl] = base;
+ continue;
+ }
+
+ const cy =
+ (ageModifier * 0.5) / (leftSpan * leftSpan);
+ const co =
+ (ageModifier * 0.5) /
+ (rightSpan * rightSpan);
+
+ const d =
+ currAge <= peakAge
+ ? peakAge - currAge
+ : currAge - peakAge;
+ const c = currAge <= peakAge ? cy : co;
+
+ let f = 1 - c * d * d;
+
+ const minFactor = 0.2;
+ const maxFactor = 10.0;
+ f = Math.min(maxFactor, Math.max(minFactor, f));
+
+ newStandard.levels[lvl] = base * f;
+ }
+
+ // prefer real data over generated data
+ const overlappingStandard = this.byActivity(
+ activity,
+ )
+ .byMetrics(newStandard.metrics)
+ .getOne();
+ if (!overlappingStandard)
+ this.standardsData[activity].standards.push(
+ newStandard,
+ );
+
+ currAge += ageStep;
+ }
+ }
+
+ // now that we generated metric data, remove the old data
+ this.standardsData[activity].standards =
+ this.standardsData[activity].standards.filter(
+ (s) => s.metrics.age,
+ );
+ } else if (generator.metric === "weight") {
+ for (const gender of Object.values(Gender)) {
+ for (const age of this.agesFor(activity, gender)) {
+ const weightStep = 12;
+ const referenceWeight = getAvgWeight(gender, age);
+
+ const minWeight = Math.max(
+ referenceWeight - 3 * weightStep,
+ 1,
+ );
+ const maxWeight = referenceWeight + 3 * weightStep;
+
+ const referenceStandard = this.byActivity(activity)
+ .byMetrics({
+ weight: referenceWeight,
+ gender,
+ age,
+ })
+ .getOneInterpolated();
+
+ let currWeight = minWeight;
+ while (currWeight <= maxWeight) {
+ const newStandard: Standard = {
+ metrics: {
+ weight: currWeight,
+ age,
+ gender,
+ },
+ levels: {},
+ };
+
+ // apply allometric scaling to generate levels
+ for (const lvl in referenceStandard.levels) {
+ if (!referenceStandard.levels[lvl])
+ continue;
+
+ const scalingExponent =
+ this.cfg.activity[activity]
+ .weightModifier;
+ const coefficient =
+ currWeight / referenceWeight;
+
+ newStandard.levels[lvl] =
+ referenceStandard.levels[lvl] *
+ coefficient ** scalingExponent;
+ }
+
+ // prefer real data over generated data
+ const overlappingStandard = this.byActivity(
+ activity,
+ )
+ .byMetrics(newStandard.metrics)
+ .getOne();
+ if (!overlappingStandard)
+ this.standardsData[
+ activity
+ ].standards.push(newStandard);
+
+ currWeight += weightStep;
+ }
+ }
+ }
+
+ // now that we generated metric data, remove the old data
+ this.standardsData[activity].standards =
+ this.standardsData[activity].standards.filter(
+ (s) => s.metrics.weight,
+ );
+ }
+ }
+
+ this.standardsData[activity].standards = this.standardsData[
+ activity
+ ].standards.sort((a, b) => {
+ const genderOrder: Record = {
+ Male: 0,
+ Female: 1,
+ };
+ const gA = genderOrder[b.metrics.gender] ?? 99;
+ const gB = genderOrder[b.metrics.gender] ?? 99;
+ if (gA !== gB) return gA - gB;
+ if (a.metrics.age !== b.metrics.age)
+ return a.metrics.age - b.metrics.age;
+ return a.metrics.weight - b.metrics.weight;
+ });
+ }
+ }
+
+ public byActivity(activity: Activity) {
+ const self = this;
+
+ const state: Partial = {};
+
+ // EXEC METHODS
+ const execMethods = {
+ exact: {
+ // these will get EXACT matches
+ getAll(): Standard[] {
+ const src = self.standardsData[activity].standards;
+
+ const out: Standard[] = [];
+ for (let i = 0; i < src.length; i++) {
+ const s = src[i];
+ if (!s) continue;
+
+ if (
+ state.gender != null &&
+ s.metrics.gender !== state.gender
+ )
+ continue;
+ if (state.age != null && s.metrics.age !== state.age)
+ continue;
+ if (
+ state.weight != null &&
+ s.metrics.weight !== state.weight
+ )
+ continue;
+ out.push(s);
+ }
+ return out;
+ },
+ getOne: function (): Standard | undefined {
+ return execMethods.exact.getAll()[0];
+ },
+ getNearest(
+ metric: NumberMetric,
+ target: number,
+ ): { lower: Standard; upper: Standard } {
+ const standards = execMethods.exact.getAll();
+ if (standards.length === 0) throw new Error("no standards");
+
+ let lower = standards[0]; // best <= target
+ let upper = standards[0]; // best >= target
+ let hasLower = false,
+ hasUpper = false;
+
+ for (const standard of standards) {
+ const standardMetricValue = standard.metrics[metric];
+
+ if (standardMetricValue <= target) {
+ if (
+ !hasLower ||
+ standardMetricValue > lower.metrics[metric]
+ ) {
+ lower = standard;
+ hasLower = true;
+ }
+ }
+ if (standardMetricValue >= target) {
+ if (
+ !hasUpper ||
+ standardMetricValue < upper.metrics[metric]
+ ) {
+ upper = standard;
+ hasUpper = true;
+ }
+ }
+ }
+
+ // clamp to ends if one side missing
+ if (!hasLower) lower = standards[0];
+ if (!hasUpper) upper = standards[standards.length - 1];
+ return { lower, upper };
+ },
+ },
+ };
+
+ const interpolateByAgeAndWeight = (metrics: Metrics): Standard => {
+ const interpolateByWeight = (targetAge: number): Levels => {
+ const {
+ lower: lowerAgeWeightStandard,
+ upper: upperAgeWeightStandard,
+ } = this.byActivity(activity)
+ .byGender(metrics.gender)
+ .byAge(targetAge)
+ .getNearest("weight", metrics.weight);
+
+ const weightLower = lowerAgeWeightStandard.metrics.weight;
+ const weightUpper = upperAgeWeightStandard.metrics.weight;
+
+ let weightRatio =
+ weightUpper === weightLower
+ ? 1
+ : (metrics.weight - weightLower) /
+ (weightUpper - weightLower);
+ weightRatio = Math.max(0, Math.min(1, weightRatio));
+
+ // metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio;
+
+ return this.interpolateLevels(
+ lowerAgeWeightStandard.levels,
+ upperAgeWeightStandard.levels,
+ weightRatio,
+ );
+ };
+
+ // Find nearest age standards
+ const { lower: lowerAgeStandard, upper: upperAgeStandard } =
+ this.byActivity(activity)
+ .byGender(metrics.gender)
+ .getNearest("age", metrics.age);
+
+ const ageLower = lowerAgeStandard.metrics.age;
+ const ageUpper = upperAgeStandard.metrics.age;
+
+ // Interpolate by age and weight
+ let ageRatio =
+ ageUpper === ageLower
+ ? 1
+ : (metrics.age - ageLower) / (ageUpper - ageLower);
+ ageRatio = Math.max(0, Math.min(1, ageRatio));
+
+ const interpolatedLevels = this.interpolateLevels(
+ interpolateByWeight(ageLower),
+ interpolateByWeight(ageUpper),
+ ageRatio,
+ );
+
+ return {
+ metrics: metrics,
+ levels: interpolatedLevels,
+ };
+ };
+
+ const getAllInterpolated = (opt?: {
+ normalizeForLb?: boolean;
+ }): Standard[] => {
+ const metrics: Metrics = state as Metrics;
+
+ let weights = this.weightsFor(
+ activity,
+ metrics.gender,
+ metrics.age,
+ );
+ if (opt?.normalizeForLb) {
+ weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
+ }
+
+ const interpolatedStandards: Standard[] = weights.map((weight) =>
+ interpolateByAgeAndWeight({ ...metrics, weight }),
+ );
+
+ return interpolatedStandards;
+ };
+
+ const getOneInterpolated = (): Standard => {
+ const metrics: Metrics = state as Metrics;
+ return interpolateByAgeAndWeight(metrics);
+ };
+
+ const getMetadata = () => this.standardsData[activity].metadata;
+
+ // QUERY METHODS
+
+ const byGender = (gender: Gender) => {
+ state.gender = gender;
+ return { byAge, ...execMethods.exact };
+ };
+
+ const byAge = (age: number) => {
+ state.age = age;
+ return { byWeight, ...execMethods.exact, getAllInterpolated };
+ };
+
+ const byWeight = (weight: number) => {
+ state.weight = weight;
+ return { ...execMethods.exact, getOneInterpolated };
+ };
+
+ const byMetrics = (metrics: Metrics) => {
+ Object.assign(state, metrics);
+ return { ...execMethods.exact, getOneInterpolated };
+ };
+
+ const initialMethods = {
+ byGender,
+ byMetrics,
+ getMetadata,
+ ...execMethods.exact,
+ };
+ return initialMethods;
+ }
+
+ public getAttributeActivities(attribute: Attribute): Activity[] {
+ const activities: Activity[] = [];
+ for (const activity of Object.keys(
+ this.standardsData,
+ ) as Activity[]) {
+ const standardData = this.standardsData[
+ activity
+ ] as StandardsData[Activity];
+ if (standardData.metadata.attribute === attribute) {
+ activities.push(activity as Activity);
+ }
+ }
+ return activities;
+ }
+
+ // TODO: turn these into queries?
+ public weightsFor(activity: Activity, gender: Gender, age: number) {
+ const { lower, upper } = this.byActivity(activity)
+ .byGender(gender)
+ .getNearest("age", age);
+
+ const lowerDiff = Math.abs(lower.metrics.age - age);
+ const upperDiff = Math.abs(upper.metrics.age - age);
+ const closestAge =
+ lowerDiff < upperDiff ? lower.metrics.age : upper.metrics.age;
+
+ const weights = this.byActivity(activity)
+ .byGender(gender)
+ .byAge(closestAge)
+ .getAll()
+ .map((s) => s.metrics.weight);
+
+ return weights;
+ }
+
+ public agesFor(activity: Activity, gender: Gender) {
+ const standardsByGender = this.byActivity(activity)
+ .byGender(gender)
+ .getAll();
+ const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
+ return ages;
+ }
+
+ private expandLevels(levels: Levels, i: number): Levels {
+ if (i == 0) {
+ return levels;
+ }
+ const levelKeys = Object.keys(levels)
+ .map(Number)
+ .sort((a, b) => a - b);
+
+ const newLevels: Levels = {};
+ let j = 1;
+
+ for (let k = 0; k < levelKeys.length; ++k) {
+ const currLevel = levelKeys[k];
+ const nextLevel = levelKeys[k + 1];
+
+ newLevels[j++] = levels[currLevel];
+
+ if (nextLevel) {
+ newLevels[j++] = (levels[currLevel] + levels[nextLevel]) / 2;
+ }
+ }
+
+ return this.expandLevels(newLevels, i - 1);
+ }
+
+ private compressLevels(levels: Levels, targetLvl: number): Levels {
+ const keys = Object.keys(levels)
+ .map(Number)
+ .sort((a, b) => a - b);
+ const lvlCnt = keys.length;
+
+ if (targetLvl <= 0) {
+ throw new Error("Target levels amount must be a positive integer");
+ }
+ if (lvlCnt === 0) {
+ return {};
+ }
+ if (targetLvl === 1) {
+ return { 1: levels[keys[0]] };
+ }
+ if (lvlCnt < targetLvl) {
+ throw new Error(
+ "Target levels amount must be less than or equal to the current levels amount",
+ );
+ }
+
+ // Linear resample
+ const compressedLevels: Levels = {};
+
+ for (let targetIndex = 0; targetIndex < targetLvl; ++targetIndex) {
+ const sourcePos = (targetIndex * (lvlCnt - 1)) / (targetLvl - 1);
+
+ const sourceIndexLower = Math.floor(sourcePos);
+ const sourceIndexUpper = Math.min(sourceIndexLower + 1, lvlCnt - 1);
+
+ const interpolationWeight = sourcePos - sourceIndexLower;
+
+ const lowerValue = levels[keys[sourceIndexLower]];
+ const upperValue = levels[keys[sourceIndexUpper]];
+
+ compressedLevels[targetIndex + 1] =
+ lowerValue + (upperValue - lowerValue) * interpolationWeight;
+ }
+
+ return compressedLevels;
+ }
+
+ private interpolateLevels(
+ lower: Levels,
+ upper: Levels,
+ ratio: number,
+ ): Levels {
+ if (Object.keys(lower).length !== Object.keys(upper).length)
+ throw new Error(
+ "Cannot interpolate between varying number of levels",
+ );
+
+ const lerp = (lvl: string) =>
+ (lower[lvl] as number) +
+ ((upper[lvl] as number) - (lower[lvl] as number)) * ratio;
+
+ const interpolatedLevels: Levels = {};
+ for (const lvl in lower) {
+ interpolatedLevels[lvl] = lerp(lvl);
+ }
+
+ return interpolatedLevels;
+ }
+}
diff --git a/packages/calculator/src/util.ts b/packages/calculator/src/util.ts
new file mode 100644
index 0000000..844650e
--- /dev/null
+++ b/packages/calculator/src/util.ts
@@ -0,0 +1,100 @@
+import { type Metrics } from ".";
+import avgWeightDataRaw from "./data/avg-weights.json";
+
+// -------------------------------------------------------------------------------------------------
+// DATA MODELS
+// -------------------------------------------------------------------------------------------------
+
+export type Player = {
+ name?: string;
+ metrics: Metrics;
+};
+
+export enum Attribute {
+ Strength = "Strength",
+ Power = "Power",
+ Endurance = "Endurance",
+ Agility = "Agility",
+}
+
+export enum Activity {
+ BackSquat = "BackSquat",
+ Deadlift = "Deadlift",
+ BenchPress = "BenchPress",
+ Run = "Run",
+ BroadJump = "BroadJump",
+ ConeDrill = "ConeDrill",
+}
+
+export interface ActivityPerformance {
+ activity: Activity;
+ performance: number;
+}
+
+export enum Gender {
+ Male = "Male",
+ Female = "Female",
+}
+
+// -------------------------------------------------------------------------------------------------
+// Conversion utilities
+// -------------------------------------------------------------------------------------------------
+
+export type StandardUnit = "ms" | "cm" | "kg";
+
+export const lbToKg = (lb: number | string): number => +lb * 0.453592;
+export const kgToLb = (kg: number | string): number => +kg * 2.20462;
+
+export const minToMs = (min: number) => min * 60000;
+export const secToMs = (sec: number) => sec * 1000;
+export const msToMin = (ms: number) => ms / 60000;
+
+export const ftToCm = (ft: number) => ft * 30.48;
+export const inToCm = (inches: number) => inches * 2.54;
+export const cmToIn = (cm: number) => cm / 2.54;
+
+export const msToTime = (ms: number, includeMs = false): string => {
+ const minutes = Math.floor(ms / 60000);
+ const seconds = Math.floor((ms % 60000) / 1000);
+ const milliseconds = Math.floor(ms % 1000);
+
+ let formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`;
+ if (includeMs) {
+ formattedTime += `.${milliseconds.toString().padStart(3, "0")}`;
+ }
+ return formattedTime;
+};
+
+export const range = (length: number) =>
+ Array.from({ length: length }, (_, i) => i);
+
+export const clamp = (x: number, lo: number, hi: number) =>
+ Math.max(lo, Math.min(hi, x));
+
+export const getAvgWeight = (gender: Gender, age: number) => {
+ type AvgWeightData = {
+ metadata: {
+ unit: StandardUnit;
+ };
+ weights: Metrics[];
+ };
+ let avgWeightData = avgWeightDataRaw as AvgWeightData;
+
+ const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
+ const firstAvgWeight = avgWeights[0];
+ if (!firstAvgWeight) throw new Error("No average weights");
+
+ const closest = {
+ diff: Math.abs(firstAvgWeight.age - age),
+ avg: firstAvgWeight,
+ };
+ for (const avg of avgWeights) {
+ const diff = Math.abs(avg.age - age);
+ if (diff < closest.diff) {
+ closest.diff = diff;
+ closest.avg = avg;
+ }
+ }
+
+ return closest.avg.weight;
+};
diff --git a/packages/calculator/tsconfig.json b/packages/calculator/tsconfig.json
new file mode 100644
index 0000000..31e07a2
--- /dev/null
+++ b/packages/calculator/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "noUncheckedIndexedAccess": false,
+ },
+}
diff --git a/packages/commerce/package.json b/packages/commerce/package.json
new file mode 100644
index 0000000..2537252
--- /dev/null
+++ b/packages/commerce/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@blade-and-brawn/commerce",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts"
+ }
+}
diff --git a/packages/commerce/src/index.ts b/packages/commerce/src/index.ts
new file mode 100644
index 0000000..4ddd407
--- /dev/null
+++ b/packages/commerce/src/index.ts
@@ -0,0 +1,6 @@
+export * from "./printful"
+export * from "./sync"
+export * from "./webflow"
+export * from "./webflow"
+export * from "./util/misc"
+export * from "./util/types"
diff --git a/packages/commerce/src/printful.ts b/packages/commerce/src/printful.ts
new file mode 100644
index 0000000..c01d0df
--- /dev/null
+++ b/packages/commerce/src/printful.ts
@@ -0,0 +1,207 @@
+import type { Printful } from "./util/types";
+import { FetchError, type DeepPartial } from "./util/misc";
+
+export class PrintfulService {
+ static Products = class {
+ static Variants = class {
+ static async get(
+ variantId: number | string,
+ ): Promise {
+ const res = await fetch(
+ `${env().API_URL}/store/variants/${variantId}`,
+ {
+ method: "GET",
+ headers: { ...env().AUTH_HEADERS },
+ },
+ );
+
+ if (res.status === 404 || res.status === 400) {
+ return;
+ }
+
+ if (!res.ok) {
+ throw new FetchError("Failed to get Printful variant", res);
+ }
+
+ const payload =
+ (await res.json()) as Printful.Products.MetaDataSingle;
+ return payload.result;
+ }
+ };
+
+ static async getAll(
+ offset: number = 0,
+ ): Promise {
+ const allSyncProducts: Printful.Products.SyncProduct[] = [];
+
+ while (true) {
+ try {
+ const res = await fetch(
+ `${env().API_URL}/store/products?offset=${offset}`,
+ {
+ method: "GET",
+ headers: { ...env().AUTH_HEADERS },
+ },
+ );
+
+ if (!res.ok) {
+ throw new FetchError(
+ "Failed to get all Printful products",
+ res,
+ );
+ }
+
+ const payload =
+ (await res.json()) as Printful.Products.MetaDataMulti;
+
+ allSyncProducts.push(...payload.result);
+ offset = allSyncProducts.length;
+
+ if (allSyncProducts.length >= payload.paging.total) break;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ return allSyncProducts;
+ }
+
+ static async get(
+ productId: number | string,
+ ): Promise {
+ const res = await fetch(
+ `${env().API_URL}/store/products/${productId}`,
+ {
+ method: "GET",
+ headers: { ...env().AUTH_HEADERS },
+ },
+ );
+
+ if (res.status === 404 || res.status === 400) {
+ return;
+ }
+
+ if (!res.ok) {
+ throw new FetchError("Failed to get Printful product", res);
+ }
+
+ const payload =
+ (await res.json()) as Printful.Products.MetaDataSingle;
+ return payload.result;
+ }
+
+ static async update(
+ printfulProductId: number,
+ printfulProduct: DeepPartial,
+ ) {
+ const res = await fetch(
+ `${env().API_URL}/store/products/${printfulProductId}`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ ...env().AUTH_HEADERS,
+ },
+ body: JSON.stringify(printfulProduct),
+ },
+ );
+
+ if (!res.ok) {
+ throw new FetchError("Failed to update Printful product", res);
+ }
+ }
+ };
+
+ static Orders = class {
+ static async create(printfulOrder: Printful.Orders.Order) {
+ const res = await fetch(`${env().API_URL}/orders`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...env().AUTH_HEADERS,
+ },
+ body: JSON.stringify(printfulOrder),
+ });
+
+ if (!res.ok) {
+ throw new FetchError("Failed to create printful order", res);
+ }
+ }
+
+ static async getAll(
+ offset: number = 0,
+ ): Promise {
+ const allPrintfulOrders: Printful.Orders.Order[] = [];
+
+ while (true) {
+ try {
+ const res = await fetch(
+ `${env().API_URL}/orders?offset=${offset}`,
+ {
+ method: "GET",
+ headers: { ...env().AUTH_HEADERS },
+ },
+ );
+
+ if (!res.ok) {
+ throw new FetchError(
+ "Failed to get all Printful orders",
+ res,
+ );
+ }
+
+ const payload =
+ (await res.json()) as Printful.Products.MetaDataMulti;
+
+ allPrintfulOrders.push(...payload.result);
+ offset = allPrintfulOrders.length;
+
+ if (allPrintfulOrders.length >= payload.paging.total) break;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ return allPrintfulOrders;
+ }
+ };
+
+ static Util = class {
+ static getVariantMainImage(
+ syncVariant: Printful.Products.SyncVariant,
+ ): string {
+ const previewFile = syncVariant.files.find(
+ (f) => f.type === "preview",
+ );
+ return previewFile?.preview_url ?? syncVariant.product.image;
+ }
+ };
+}
+
+export const env = () => {
+ if (typeof Bun === "undefined") {
+ throw new Error(
+ "Must be in a server context. Make sure to run using --bun.",
+ );
+ }
+
+ const vars = {
+ AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
+ STORE_ID: Bun.env.PRINTFUL_STORE_ID,
+ } as Record;
+
+ for (const varKey in vars) {
+ if (!vars[varKey]) {
+ console.log(`Missing ${varKey} environment variable`);
+ }
+ }
+
+ return {
+ ...vars,
+ API_URL: "https://api.printful.com",
+ AUTH_HEADERS: {
+ Authorization: `Bearer ${vars.AUTH_TOKEN}`,
+ "X-PF-Store-Id": `${vars.STORE_ID}`,
+ },
+ };
+};
diff --git a/packages/commerce/src/sync.ts b/packages/commerce/src/sync.ts
new file mode 100644
index 0000000..6c41afa
--- /dev/null
+++ b/packages/commerce/src/sync.ts
@@ -0,0 +1,359 @@
+import type { Printful, Webflow } from "./util/types";
+import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
+import { WebflowService } from "./webflow";
+import { PrintfulService } from "./printful";
+
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
+export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
+
+type MainProduct = {
+ name: string;
+ externalId: string;
+ colors: Set;
+ variants: {
+ color: string;
+ product: Printful.Products.Product;
+ }[];
+};
+
+export class SyncService {
+ static state = {
+ isSyncing: false,
+ syncingIds: [] as number[],
+ };
+
+ static async sync(printfulProductId: number | undefined) {
+ this.state.isSyncing = true;
+
+ const mainPrintfulProducts: MainProduct[] = [];
+ const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
+ try {
+ let printfulProducts = await PrintfulService.Products.getAll();
+ if (printfulProductId) {
+ const printfulProduct = printfulProducts.find(
+ (p) => p.id === printfulProductId,
+ );
+ if (printfulProduct) {
+ const mainProductName = this.getMainProductName(
+ printfulProduct.name,
+ );
+ printfulProducts = printfulProducts.filter((p) =>
+ p.name.includes(mainProductName),
+ );
+ }
+ }
+ webflowProducts.push(...(await WebflowService.Products.getAll()));
+
+ // Generate main printful products
+ console.log("Generating main products...");
+ for (const printfulProduct of printfulProducts) {
+ const mainProductName = this.getMainProductName(
+ printfulProduct.name,
+ );
+
+ let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
+ mp.name.includes(mainProductName),
+ );
+ if (!mainPrintfulProduct) {
+ mainPrintfulProduct = {
+ name: mainProductName,
+ variants: [],
+ externalId: printfulProduct.external_id,
+ colors: new Set(),
+ };
+ mainPrintfulProducts.push(mainPrintfulProduct);
+ }
+
+ const productColor = this.findColorInProductName(
+ printfulProduct.name,
+ );
+ mainPrintfulProduct.variants.push({
+ color: productColor,
+ product: (await PrintfulService.Products.get(
+ printfulProduct.id,
+ ))!,
+ });
+ mainPrintfulProduct.colors.add(productColor);
+ }
+ } catch (error) {
+ throw error;
+ } finally {
+ this.state.isSyncing = false;
+ this.state.syncingIds = [];
+ }
+
+ // TODO: Validate main printful products
+
+ // Sync the main printful products
+ console.log("Syncing main products...");
+ for (const mainPrintfulProduct of mainPrintfulProducts) {
+ try {
+ this.state.isSyncing = true;
+ this.state.syncingIds = mainPrintfulProduct.variants.map(
+ (v) => v.product.sync_product.id,
+ );
+
+ const foundColors = mainPrintfulProduct.colors;
+ const foundSizes: Set = new Set();
+
+ const webflowSkus: DeepPartial[] =
+ [];
+
+ for (const specialVariant of mainPrintfulProduct.variants) {
+ for (const printfulVariant of specialVariant.product
+ .sync_variants) {
+ // check if this variant size is in all other special variants
+ let sizeIsInAllVariants = true;
+ for (const specialVariant of mainPrintfulProduct.variants) {
+ const sizes =
+ specialVariant.product.sync_variants.map(
+ (v) => v.size,
+ );
+ if (!sizes.includes(printfulVariant.size))
+ sizeIsInAllVariants = false;
+ }
+
+ if (sizeIsInAllVariants) {
+ foundSizes.add(printfulVariant.size);
+
+ webflowSkus.push({
+ id: printfulVariant.external_id,
+ fieldData: {
+ name: printfulVariant.name,
+ slug: formatSlug(printfulVariant.name),
+ "sku-values": {
+ color: specialVariant.color,
+ size: printfulVariant.size,
+ },
+ price: {
+ value: Math.floor(
+ +printfulVariant.retail_price * 100,
+ ),
+ unit: printfulVariant.currency,
+ currency: printfulVariant.currency,
+ },
+ "main-image":
+ PrintfulService.Util.getVariantMainImage(
+ printfulVariant,
+ ),
+ },
+ });
+ }
+ }
+ }
+
+ // SYNC PRODUCT DATA
+ let existingWebflowProduct = webflowProducts.find(
+ (webflowProduct) =>
+ webflowProduct.product.id ===
+ mainPrintfulProduct.externalId.split("-")[0],
+ );
+ if (existingWebflowProduct) {
+ // SYNC PRODUCT UPDATE
+
+ // do not update images
+ for (const webflowSku of webflowSkus) {
+ delete webflowSku.fieldData?.["main-image"];
+ }
+
+ const webflowProductId =
+ mainPrintfulProduct.externalId.split("-")[0];
+ if (!webflowProductId) {
+ throw new Error("Malformed printful product ID");
+ }
+ WebflowService.Products.update(webflowProductId, {
+ product: {
+ fieldData: {
+ name: mainPrintfulProduct.name,
+ slug: formatSlug(mainPrintfulProduct.name),
+ shippable: true,
+ "tax-category": "standard-taxable",
+ "sku-properties": [
+ {
+ id: "color",
+ name: "Color",
+ enum: Array.from(foundColors).map(
+ (color) => ({
+ id: color,
+ slug: formatSlug(color),
+ name: color,
+ }),
+ ),
+ },
+ {
+ id: "size",
+ name: "Size",
+ enum: Array.from(foundSizes).map(
+ (size) => ({
+ id: size,
+ slug: formatSlug(size),
+ name: size,
+ }),
+ ),
+ },
+ ],
+ },
+ },
+ sku: webflowSkus[0],
+ });
+
+ for (const webflowSku of webflowSkus.slice(1)) {
+ const existingWebflowSku =
+ existingWebflowProduct.skus.find(
+ (sku) => sku.id === webflowSku.id,
+ );
+ if (webflowSku.id && existingWebflowSku) {
+ await WebflowService.Products.Skus.update(
+ webflowProductId,
+ webflowSku.id,
+ webflowSku,
+ );
+ } else {
+ await WebflowService.Products.Skus.create(
+ webflowProductId,
+ [webflowSku],
+ );
+ }
+ }
+ } else {
+ // SYNC PRODUCT CREATE
+ const webflowProductId =
+ await WebflowService.Products.create({
+ product: {
+ fieldData: {
+ name: mainPrintfulProduct.name,
+ slug: formatSlug(mainPrintfulProduct.name),
+ shippable: true,
+ "tax-category": "standard-taxable",
+ "sku-properties": [
+ {
+ id: "color",
+ name: "Color",
+ enum: Array.from(foundColors).map(
+ (color) => ({
+ id: color,
+ slug: formatSlug(color),
+ name: color,
+ }),
+ ),
+ },
+ {
+ id: "size",
+ name: "Size",
+ enum: Array.from(foundSizes).map(
+ (size) => ({
+ id: size,
+ slug: formatSlug(size),
+ name: size,
+ }),
+ ),
+ },
+ ],
+ },
+ },
+ sku: webflowSkus[0],
+ });
+
+ // create webflow product SKUs
+ await WebflowService.Products.Skus.create(
+ webflowProductId,
+ webflowSkus.slice(1),
+ );
+
+ existingWebflowProduct =
+ await WebflowService.Products.get(webflowProductId);
+ if (!existingWebflowProduct) {
+ console.error("Missing webflow product");
+ throw new Error("Failed to create Webflow product");
+ }
+
+ for (const specialVariant of mainPrintfulProduct.variants) {
+ const newPrintfulVariants: DeepPartial[] =
+ [];
+ for (const printfulVariant of specialVariant.product
+ .sync_variants) {
+ const associatedWebflowSku =
+ existingWebflowProduct.skus.find(
+ (sku) =>
+ sku.fieldData["sku-values"]?.[
+ "color"
+ ] === printfulVariant.color &&
+ sku.fieldData["sku-values"]?.[
+ "size"
+ ] === printfulVariant.size,
+ );
+ if (associatedWebflowSku) {
+ newPrintfulVariants.push({
+ id: printfulVariant.id, // printful variant id
+ external_id: String(
+ associatedWebflowSku.id,
+ ), // webflow variant id
+ });
+ }
+ }
+
+ await sleep(10000);
+ await PrintfulService.Products.update(
+ specialVariant.product.sync_product.id,
+ {
+ sync_product: {
+ id: specialVariant.product.sync_product.id,
+ external_id: `${webflowProductId}-${specialVariant.color}`,
+ },
+ sync_variants: newPrintfulVariants,
+ },
+ );
+ }
+ }
+ } catch (err) {
+ // SYNC IMAGES
+ // for (const sku of existingWebflowProduct.skus) {
+ // const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
+ // const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`;
+ // const skuImageUrls = await this.getProductImageUrls(skuSlug);
+ //
+ // sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"];
+ // sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url }));
+ //
+ // await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
+ // }
+ //
+ if (err instanceof FetchError) {
+ console.error(err.message, err.payload);
+ }
+ }
+ }
+ this.state.isSyncing = false;
+ this.state.syncingIds = [];
+ console.log("Done");
+ }
+
+ private static getProductImageUrls = async (slug: 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}`);
+ };
+
+ public static findColorInProductName = (productName: string): string => {
+ const m = productName.match(/\[([^\]]+)\]/);
+ return m?.[1] ? m[1] : "N/A";
+ };
+
+ public static getMainProductName = (productName: string) => {
+ const colorInName = this.findColorInProductName(productName);
+ if (colorInName) {
+ return productName.replace(`[${colorInName}]`, "").trimEnd();
+ } else {
+ return productName;
+ }
+ };
+}
diff --git a/src/lib/services/commerce/util/misc.ts b/packages/commerce/src/util/misc.ts
similarity index 92%
rename from src/lib/services/commerce/util/misc.ts
rename to packages/commerce/src/util/misc.ts
index 40c34ac..0cc8b76 100644
--- a/src/lib/services/commerce/util/misc.ts
+++ b/packages/commerce/src/util/misc.ts
@@ -27,7 +27,7 @@ export class FetchError extends Error {
async parse() {
try {
- this.payload = await this.response.clone().json();
+ this.payload = (await this.response.clone().json()) as Object;
} catch {
this.payload = await this.response.text().catch(() => undefined);
} finally {
diff --git a/src/lib/services/commerce/util/types.ts b/packages/commerce/src/util/types.ts
similarity index 85%
rename from src/lib/services/commerce/util/types.ts
rename to packages/commerce/src/util/types.ts
index 5b46327..89ac387 100644
--- a/src/lib/services/commerce/util/types.ts
+++ b/packages/commerce/src/util/types.ts
@@ -1,5 +1,3 @@
-import type { DeepPartial } from "./misc";
-
export namespace Printful {
export namespace Products {
export interface MetaDataSingle {
@@ -125,56 +123,53 @@ export namespace Printful {
PackageShipped = "package_shipped",
}
- export interface ProductUpdated
- extends MetaData<
- Event.ProductUpdated,
- {
- sync_product: {
- id: number;
- external_id: string;
- name: string;
- variants: number;
- synced: number;
- thumbnail_url: string;
- is_ignored: boolean;
- };
- }
- > {}
+ export interface ProductUpdated extends MetaData<
+ Event.ProductUpdated,
+ {
+ sync_product: {
+ id: number;
+ external_id: string;
+ name: string;
+ variants: number;
+ synced: number;
+ thumbnail_url: string;
+ is_ignored: boolean;
+ };
+ }
+ > {}
- export interface ProductDeleted
- extends MetaData<
- Event.ProductDeleted,
- {
- sync_product: {
- id: number;
- external_id: string;
- name: string;
- };
- }
- > {}
+ export interface ProductDeleted extends MetaData<
+ Event.ProductDeleted,
+ {
+ sync_product: {
+ id: number;
+ external_id: string;
+ name: string;
+ };
+ }
+ > {}
- export interface PackageShipped
- extends MetaData<
- Event.PackageShipped,
- {
- shipment: {
- id: number;
- carrier: string;
- service: string;
- tracking_number: string;
- tracking_url: string;
- created: number;
- ship_date: string;
- shipped_at: number;
- reshipment: boolean;
- };
- order: {
- id: number;
- external_id: string;
- status: string;
- };
- }
- > {}
+ export interface PackageShipped extends MetaData<
+ Event.PackageShipped,
+ {
+ shipment: {
+ id: number;
+ carrier: string;
+ service: string;
+ tracking_number: string;
+ tracking_url: string;
+ created: number;
+ ship_date: string;
+ shipped_at: number;
+ reshipment: boolean;
+ };
+ order: {
+ id: number;
+ external_id: string;
+ status: string;
+ };
+ }
+ > {}
export type EventPayload =
| ProductUpdated
@@ -422,10 +417,14 @@ export namespace Webflow {
OrderUpdated = "ecomm_order_changed",
}
- export interface OrderCreated
- extends MetaData {}
- export interface OrderUpdated
- extends MetaData {}
+ export interface OrderCreated extends MetaData<
+ Event.OrderCreated,
+ Orders.Order
+ > {}
+ export interface OrderUpdated extends MetaData<
+ Event.OrderUpdated,
+ Orders.Order
+ > {}
export type EventPayload = OrderCreated | OrderUpdated;
}
diff --git a/packages/commerce/src/webflow.ts b/packages/commerce/src/webflow.ts
new file mode 100644
index 0000000..9696155
--- /dev/null
+++ b/packages/commerce/src/webflow.ts
@@ -0,0 +1,330 @@
+import type { Webflow } from "./util/types";
+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[],
+ ): Promise {
+ 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,
+ );
+ }
+
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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 {
+ 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 {
+ 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,
+ ): Promise {
+ 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 {
+ 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 {
+ // 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 {
+ 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 {
+ 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 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 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;
+
+ 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}` },
+ };
+};
diff --git a/packages/commerce/tsconfig.json b/packages/commerce/tsconfig.json
new file mode 100644
index 0000000..96a27d9
--- /dev/null
+++ b/packages/commerce/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig.base.json",
+}
diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json
new file mode 100644
index 0000000..96a27d9
--- /dev/null
+++ b/packages/domain/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../tsconfig.base.json",
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
deleted file mode 100644
index 407a28e..0000000
--- a/src/lib/api.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { treaty } from "@elysiajs/eden";
-import type { App } from "./server/types";
-
-const baseURL = import.meta.env.PROD
- ? "https://blade-and-brawn.fly.dev"
- : "http://localhost:5173";
-
-export const api = treaty(baseURL).api;
diff --git a/src/lib/components/PlayersTable.svelte b/src/lib/components/PlayersTable.svelte
deleted file mode 100644
index a4559f9..0000000
--- a/src/lib/components/PlayersTable.svelte
+++ /dev/null
@@ -1,297 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {#each calculations as calculation, index}
-
-
-
-
-
- -
-
-
OVERALL
-
- {calculation?.levels?.player || "N/A"}
-
-
-
-
- {#each Object.values(Attribute) as attribute}
- -
-
-
- {attribute}
-
-
- {calculation?.levels?.attributes?.[
- attribute
- ] || "N/A"}
-
-
-
- {/each}
-
-
-
-
-
-
-
-
- {/each}
-
diff --git a/src/lib/data/analyze.ts b/src/lib/data/analyze.ts
deleted file mode 100644
index ae7a1af..0000000
--- a/src/lib/data/analyze.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import {
- type ActivityStandards,
- type Levels,
-} from "$lib/services/calculator/main";
-import { Activity, clamp, Gender, range } from "$lib/services/calculator/util";
-import allStandards from "./standards.json";
-import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
-
-const activityStandards = allStandards as ActivityStandards;
-
-for (const activity of Object.values(Activity)) {
- const DATASET_MAX_LEVEL = 5;
- const NEW_LEVEL_COUNT = 2;
- const RATIO_CLAMP = 1.4;
-
- function expDecayModel([A, B, C]: number[]) {
- return (i: number) => A * Math.exp(-B * i) + C;
- }
-
- for (const gender of Object.values(Gender)) {
- const standardsByGender = activityStandards[activity].standards.filter(
- (s) => s["metrics"].gender === gender,
- );
- const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
- for (const age of ages) {
- const standards = standardsByGender.filter(
- (s) => s.metrics.age === age,
- );
-
- const data = {
- x: [] as number[],
- y: [] as number[],
- };
-
- const isIncreasing =
- standards[0].levels["2"] > standards[0].levels["1"];
-
- for (const i of range(DATASET_MAX_LEVEL).slice(0, -1)) {
- const level = i + 1;
- const progressionRatios: number[] = [];
- for (const standard of standards) {
- const curr = standard.levels[level];
- const next = standard.levels[level];
- progressionRatios.push(
- isIncreasing ? next / curr : curr / next,
- );
- }
-
- const sum = progressionRatios.reduce((p, c) => p + c, 0);
- const progressionRatioAvg = sum / progressionRatios.length;
-
- data.x.push(i);
- data.y.push(progressionRatioAvg);
- }
-
- const fittedParams = LM(data, expDecayModel, {
- initialValues: [0.4, 0.5, 1.1],
- minValues: [0.0, 0.0, 1.02],
- maxValues: [1.0, 2.0, 1.25],
- maxIterations: 200,
- });
- const getDecayRatio = (i: number) =>
- clamp(
- expDecayModel(fittedParams.parameterValues)(i),
- 1,
- RATIO_CLAMP,
- );
-
- // UPDATE THE DATA
- for (const standard of standards) {
- const newLevels: Levels = {};
-
- let prev = standard.levels["1"];
- for (const i of range(NEW_LEVEL_COUNT)) {
- const level = i + 1;
- const ratio = getDecayRatio(level - NEW_LEVEL_COUNT);
- prev = isIncreasing ? prev / ratio : prev * ratio;
- newLevels[level] = Math.round(prev);
- }
-
- const oldLevels = standard.levels as Levels;
- for (const i of range(DATASET_MAX_LEVEL)) {
- const level = i + 1;
- newLevels[level + NEW_LEVEL_COUNT] = oldLevels[level];
- }
- standard.levels = newLevels;
- console.log(newLevels);
- }
- }
- }
-}
-// Bun.write("./newStandards.json", JSON.stringify(allStandards, null, 2));
diff --git a/src/lib/server/app.ts b/src/lib/server/app.ts
deleted file mode 100644
index 0a5bf8f..0000000
--- a/src/lib/server/app.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import rawStandards from "$lib/data/standards.json" assert { type: "json" };
-import {
- LevelCalculator,
- Standards,
- type ActivityStandards,
-} from "$lib/services/calculator/main";
-import type {
- ActivityPerformance,
- Player,
-} from "$lib/services/calculator/util";
-import { cors } from "@elysiajs/cors";
-import { Elysia, NotFoundError } from "elysia";
-import PrintfulService from "../services/commerce/printful";
-import SyncService from "../services/commerce/sync";
-import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
-import { Printful, Webflow } from "../services/commerce/util/types";
-import WebflowService from "../services/commerce/webflow";
-import zipcodesUs from "zipcodes-us";
-
-const MAIN_STANDARDS = new Standards(rawStandards as ActivityStandards);
-const levelCalculator = new LevelCalculator(MAIN_STANDARDS);
-
-export const app = new Elysia({ prefix: "/api" })
- .use(
- cors({
- origin: [
- // bladeandbrawn.com (apex + any subdomain)
- /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i,
- // Webflow preview domains
- /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i,
- // Fly app host (this one already works)
- /^https?:\/\/blade-and-brawn\.fly\.dev$/i,
- // Local dev
- "http://localhost:5173",
- ],
- }),
- )
-
- .error({
- FetchError,
- })
-
- .onError(async ({ code, error }) => {
- switch (code) {
- case "FetchError":
- console.error(error.message, await error.parse());
- return error;
- }
- })
-
- .onAfterHandle(({ request, set }) => {
- console.log(
- JSON.stringify({
- lvl: "info",
- msg: "req",
- method: request.method,
- path: new URL(request.url).pathname,
- status: set.status ?? 200,
- }),
- );
- })
-
- .get("/", () => "Hello world")
-
- // CALCULATOR
-
- .post("/calculate", async ({ body, query }) => {
- interface CalcRequest {
- player: Player;
- activityPerformances: ActivityPerformance[];
- }
- const { player, activityPerformances } = body as CalcRequest;
- const output = {
- levels: levelCalculator.calculate(player, activityPerformances),
- };
-
- console.log({ log: query?.log });
- if (query?.log === "true") {
- void fetch("https://kv-logger.xominus.workers.dev", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ output, player, activityPerformances }),
- }).catch(() => {});
- }
-
- return output;
- })
-
- // COMMERCE
- // TODO: protect with api key/token
- .group("/products", (app) =>
- app
- .group("/sync", (app) =>
- app
- .get("/", async ({}) => SyncService.state)
- .post("/:printfulProductId?", async ({ params, set }) => {
- if (SyncService.state.isSyncing) {
- set.status = 409;
- return { error: "Sync already in progress" };
- }
- const printfulProductId = params.printfulProductId
- ? +params.printfulProductId
- : undefined;
- await SyncService.sync(printfulProductId);
- return { ok: true };
- }),
- )
- .get("/:printfulProductId", async ({ params }) => {
- const printfulProductId = +params.printfulProductId;
- const printfulProduct =
- await PrintfulService.Products.get(printfulProductId);
- if (!printfulProduct) {
- return new NotFoundError();
- }
-
- const webflowProductId =
- printfulProduct.sync_product.external_id.split("-")[0];
- const webflowProduct =
- await WebflowService.Products.get(webflowProductId);
-
- return { printfulProduct, webflowProduct };
- }),
- )
-
- // WEBHOOKS
- .post("/webhook/printful", async ({ body }) => {
- const payload = body as Printful.Webhook.EventPayload;
-
- switch (payload.type) {
- case Printful.Webhook.Event.ProductUpdated: {
- const printfulProduct = payload.data.sync_product;
- await SyncService.sync(printfulProduct.id);
- break;
- }
- case Printful.Webhook.Event.ProductDeleted: {
- await WebflowService.Products.remove(
- payload.data.sync_product.external_id,
- );
- break;
- }
- case Printful.Webhook.Event.PackageShipped: {
- const webflowOrderId = payload.data.order.external_id;
- const shipInfo = payload.data.shipment;
-
- await WebflowService.Orders.update(webflowOrderId, {
- shippingTrackingURL: shipInfo.tracking_url,
- shippingTracking: shipInfo.tracking_number,
- shippingProvider: shipInfo.carrier,
- });
- await WebflowService.Orders.fulfill(webflowOrderId, {
- sendOrderFulfilledEmail: true,
- });
- break;
- }
- }
-
- return { ok: true };
- })
- .post("/webhook/webflow", async ({ request, body, set }) => {
- if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
- set.status = 400;
- return "Invalid signature";
- }
-
- const payload = body as Webflow.Webhook.EventPayload;
-
- switch (payload.triggerType) {
- case Webflow.Webhook.Event.OrderCreated: {
- const webflowOrder = payload.payload;
-
- await PrintfulService.Orders.create({
- external_id: webflowOrder.orderId,
- // TODO: derive from webflow
- shipping: "STANDARD",
- recipient: {
- name: webflowOrder.shippingAddress.addressee,
- address1: webflowOrder.shippingAddress.line1,
- address2: webflowOrder.shippingAddress.line2,
- city: webflowOrder.shippingAddress.city,
- state_code: zipcodesUs.find(
- webflowOrder.shippingAddress.postalCode.split(
- "-",
- )[0],
- ).stateCode,
- country_code: webflowOrder.shippingAddress.country,
- zip: webflowOrder.shippingAddress.postalCode,
- },
- items: webflowOrder.purchasedItems.map(
- (webflowOrderSku) => ({
- external_variant_id: webflowOrderSku.variantId,
- quantity: webflowOrderSku.count,
- }),
- ),
- });
-
- console.log("Complete");
-
- break;
- }
- }
-
- return { ok: true };
- });
diff --git a/src/lib/server/types.ts b/src/lib/server/types.ts
deleted file mode 100644
index b02f418..0000000
--- a/src/lib/server/types.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-import type { app } from "./app";
-export type App = typeof app;
diff --git a/src/lib/services/calculator/main.ts b/src/lib/services/calculator/main.ts
deleted file mode 100644
index cd68f27..0000000
--- a/src/lib/services/calculator/main.ts
+++ /dev/null
@@ -1,896 +0,0 @@
-import {
- Activity,
- Attribute,
- clamp,
- Gender,
- getAvgWeight,
- kgToLb,
- lbToKg,
- range,
- type ActivityPerformance,
- type Player,
- type StandardUnit,
-} from "./util";
-import { levenbergMarquardt as LM } from "ml-levenberg-marquardt";
-import defaultConfig from "$lib/services/calculator/config.json" assert { type: "json" };
-
-// SOURCES
-// Squat, Bench, Dead Lift:
-// http://lonkilgore.com/resources/Lon_Kilgore_Strength_Standard_Tables-Copyright-2023.pdf
-// 1 mile run:
-// https://runninglevel.com/running-times/1-mile-times
-// Broad Jump:
-// https://nrpt.co.uk/training/tests/power/broad.htm
-// 3 Cone drill:
-// https://nflsavant.com/combine.php?utm_source=chatgpt.com
-
-export type LevelCalculatorOutput = {
- player: number;
- attributes: Record;
-};
-
-export type Levels = Record;
-
-export interface Metrics {
- age: number;
- weight: number;
- gender: Gender;
-}
-
-type NumberMetric = Extract;
-
-export interface Standard {
- metrics: Metrics;
- levels: Levels;
-}
-
-type Generator = {
- metric: NumberMetric;
-};
-
-export type ActivityStandards = Record<
- Activity,
- {
- metadata: {
- attribute: Attribute;
- generators: Generator[];
- unit: StandardUnit;
- name: string;
- };
- standards: Standard[];
- }
->;
-
-const metricPriority = (m: NumberMetric) => {
- if (m === "age") return 0;
- if (m === "weight") return 1;
- return 2;
-};
-
-export class LevelCalculator {
- standards: Standards;
-
- public constructor(standards: Standards) {
- this.standards = standards;
- }
-
- // -------------------------------------------------------------------------------------------------
- // Level calculations
- // -------------------------------------------------------------------------------------------------
-
- public calculate(
- player: Player,
- activityPerformances: ActivityPerformance[],
- ): LevelCalculatorOutput {
- const levels: LevelCalculatorOutput = {
- player: 0,
- attributes: {
- [Attribute.Strength]: 0,
- [Attribute.Power]: 0,
- [Attribute.Endurance]: 0,
- [Attribute.Agility]: 0,
- } as const,
- };
-
- for (const value of Object.values(player)) {
- if (!value) return levels;
- }
-
- levels.attributes = this.calculateAllAttributeLevels(
- player,
- activityPerformances,
- );
- for (const level of Object.values(levels.attributes)) {
- if (!level) return levels;
- }
-
- const attrLevelsSum = Object.values(levels.attributes).reduce(
- (sum, lvl) => sum + lvl,
- 0,
- );
- levels.player = Math.round(
- attrLevelsSum / Object.keys(Attribute).length,
- );
-
- return levels;
- }
-
- private calculateAllAttributeLevels(
- player: Player,
- activityPerformances: ActivityPerformance[],
- ): Record {
- const attrLevels: Record = {
- [Attribute.Strength]: 0,
- [Attribute.Power]: 0,
- [Attribute.Endurance]: 0,
- [Attribute.Agility]: 0,
- } as const;
-
- for (const value of Object.values(player)) {
- if (!value) return attrLevels;
- }
-
- (Object.values(Attribute) as Attribute[]).forEach((attribute) => {
- const attrActivityPerformances = activityPerformances.filter(
- (p) =>
- this.standards.byActivity(p.activity).getMetadata()
- .attribute === attribute,
- );
- attrLevels[attribute] = this.calculateAttributeLevel(
- attribute,
- player,
- attrActivityPerformances,
- );
- });
-
- return attrLevels;
- }
-
- private calculateAttributeLevel(
- attribute: Attribute,
- player: Player,
- activityPerformances: ActivityPerformance[],
- ): number {
- // must be activities of the given attribute
- if (
- activityPerformances.some(
- (p) =>
- this.standards.byActivity(p.activity).getMetadata()
- .attribute !== attribute,
- )
- ) {
- throw new Error("Wrong activity performance attribute");
- }
-
- // must perform all of an attributes activities
- for (const activity of this.standards.getAttributeActivities(
- attribute,
- )) {
- const filteredActivites = activityPerformances.map(
- (p) => p.activity,
- );
- if (!filteredActivites.includes(activity)) return 0;
- }
-
- // must have all activties using a valid performance value (> 0)
- for (const activityPerformance of activityPerformances) {
- if (activityPerformance.performance <= 0) return 0;
- }
-
- const activityLevels = activityPerformances.map((p) => {
- const interpolatedStandard = this.standards
- .byActivity(p.activity)
- .byMetrics(player.metrics)
- .getOneInterpolated();
- return this.findLevel(interpolatedStandard, p.performance);
- });
-
- const activityLevelsAvg = Math.round(
- activityLevels.reduce((sum, curr) => sum + curr, 0) /
- activityLevels.length,
- );
-
- return activityLevelsAvg;
- }
-
- // -------------------------------------------------------------------------------------------------
- // Level utilities (helpers)
- // -------------------------------------------------------------------------------------------------
-
- private findLevel(standard: Standard, performance: number): number {
- let resultLevel = 1;
- let resultLevelDiff = Infinity;
-
- for (const level in standard.levels) {
- if (!standard.levels[level]) continue;
-
- const diff = Math.abs(performance - standard.levels[level]);
- if (diff < resultLevelDiff) {
- resultLevel = +level;
- resultLevelDiff = diff;
- }
- }
- return resultLevel;
- }
-}
-
-export type StandardsConfig = {
- global: {
- maxLevel: number;
- };
- activity: Record<
- Activity,
- {
- weightModifier: number;
- weightSkew: number;
- ageModifier: number;
- enableGeneration: boolean;
- difficultyModifier: number;
- peakAge: number;
- stretch: {
- upper: number;
- lower: number;
- };
- }
- >;
-};
-
-export class Standards {
- readonly cfg: StandardsConfig;
- private activityStandards: ActivityStandards;
-
- constructor(
- activityStandards: ActivityStandards,
- cfg?: Partial,
- ) {
- this.cfg = Object.assign(defaultConfig, cfg);
-
- // prepare data
- this.activityStandards = structuredClone(activityStandards);
-
- // STRETCH
- for (const activity of Object.values(Activity)) {
- const BASE_MAX_LEVEL = 5;
-
- function expDecayModel([A, B, C]: number[]) {
- return (i: number) => A * Math.exp(-B * i) + C;
- }
-
- if (!this.cfg.activity[activity].enableGeneration) {
- continue;
- }
-
- for (const gender of Object.values(Gender)) {
- const standardsByGender = this.activityStandards[
- activity
- ].standards.filter((s) => s["metrics"].gender === gender);
- const ages = [
- ...new Set(standardsByGender.map((s) => s.metrics.age)),
- ];
- for (const age of ages) {
- const standards = standardsByGender.filter(
- (s) => s.metrics.age === age,
- );
-
- const data = {
- x: [] as number[],
- y: [] as number[],
- };
-
- const isIncreasing =
- standards[0].levels["2"] > standards[0].levels["1"];
-
- for (const i of range(BASE_MAX_LEVEL).slice(0, -1)) {
- const level = i + 1;
- const progressionRatios: number[] = [];
- for (const standard of standards) {
- const curr = standard.levels[level];
- const next = standard.levels[level + 1];
- progressionRatios.push(
- isIncreasing ? next / curr : curr / next,
- );
- }
-
- const sum = progressionRatios.reduce(
- (p, c) => p + c,
- 0,
- );
- const progressionRatioAvg =
- sum / progressionRatios.length;
-
- data.x.push(i);
- data.y.push(progressionRatioAvg);
- }
-
- const fittedParams = LM(data, expDecayModel, {
- initialValues: [0.4, 0.5, 1.1],
- minValues: [0.0, 0.0, 1.02],
- maxValues: [1.0, 2.0, 1.2],
- maxIterations: 200,
- });
- const getDecayRatio = (i: number) =>
- expDecayModel(fittedParams.parameterValues)(i);
-
- // UPDATE THE DATA
- for (const standard of standards) {
- const newLevels: Levels = {};
- const newLowerLevelCount = Math.max(
- Math.round(
- this.cfg.activity[activity].stretch.lower,
- ),
- 0,
- );
- const newUpperLevelCount = Math.max(
- Math.round(
- this.cfg.activity[activity].stretch.upper,
- ),
- 0,
- );
-
- const oldLevels = standard.levels as Levels;
- let prev = oldLevels["1"];
- for (const i of range(newLowerLevelCount).reverse()) {
- const level = i + 1;
- const ratio = getDecayRatio(-i - 1);
- prev = isIncreasing ? prev / ratio : prev * ratio;
- newLevels[level] = Math.round(prev);
- }
-
- for (const i of range(BASE_MAX_LEVEL)) {
- const level = i + 1;
- newLevels[level + newLowerLevelCount] =
- oldLevels[level];
- }
-
- prev = oldLevels[BASE_MAX_LEVEL];
- for (const i of range(newUpperLevelCount)) {
- const level =
- BASE_MAX_LEVEL + newLowerLevelCount + (i + 1);
- const ratio = getDecayRatio(level - 1);
- prev = isIncreasing ? prev * ratio : prev / ratio;
- newLevels[level] = Math.round(prev);
- }
-
- standard.levels = newLevels;
- }
- }
- }
- }
-
- // EXPAND, COMPRESS, SKEW
- for (const activity of Object.values(Activity)) {
- for (const standard of this.activityStandards[activity].standards) {
- // expand
- let i = 1;
- while (
- Object.keys(standard.levels).length <
- this.cfg.global.maxLevel
- ) {
- standard.levels = this.expandLevels(standard.levels, i++);
- }
-
- // compress
- if (
- Object.keys(standard.levels).length >
- this.cfg.global.maxLevel
- ) {
- standard.levels = this.compressLevels(
- standard.levels,
- this.cfg.global.maxLevel,
- );
- }
-
- // apply skew config
- for (const level in standard.levels) {
- standard.levels[level] =
- standard.levels[level] *
- (1 + this.cfg.activity[activity].difficultyModifier);
- }
- }
-
- // generate data
- const allGenerators = this.cfg.activity[activity].enableGeneration
- ? this.activityStandards[activity].metadata.generators.sort(
- (a, b) =>
- metricPriority(a.metric) - metricPriority(b.metric),
- )
- : [];
-
- for (const generator of allGenerators) {
- if (generator.metric === "age") {
- for (const gender of Object.values(Gender)) {
- const peakAge = this.cfg.activity[activity].peakAge;
- const ageStep = 10;
-
- const minAge = 0;
- const maxAge = 100;
-
- const referenceStandard = this.byActivity(activity)
- .byMetrics({
- weight: getAvgWeight(gender, peakAge),
- gender,
- age: peakAge,
- })
- .getOneInterpolated();
-
- let currAge = minAge;
- while (currAge <= maxAge) {
- const newStandard: Standard = {
- metrics: {
- weight: getAvgWeight(gender, currAge),
- age: currAge,
- gender,
- },
- levels: {},
- };
-
- for (const lvl in referenceStandard.levels) {
- const base = referenceStandard.levels[lvl];
- if (base == null) continue;
-
- const ageModifier =
- this.cfg.activity[activity].ageModifier;
-
- const leftSpan = peakAge - minAge;
- const rightSpan = maxAge - peakAge;
-
- // avoid divide-by-zero
- if (leftSpan <= 0 || rightSpan <= 0) {
- newStandard.levels[lvl] = base;
- continue;
- }
-
- const cy =
- (ageModifier * 0.5) / (leftSpan * leftSpan);
- const co =
- (ageModifier * 0.5) /
- (rightSpan * rightSpan);
-
- const d =
- currAge <= peakAge
- ? peakAge - currAge
- : currAge - peakAge;
- const c = currAge <= peakAge ? cy : co;
-
- let f = 1 - c * d * d;
-
- const minFactor = 0.2;
- const maxFactor = 10.0;
- f = Math.min(maxFactor, Math.max(minFactor, f));
-
- newStandard.levels[lvl] = base * f;
- }
-
- // prefer real data over generated data
- const overlappingStandard = this.byActivity(
- activity,
- )
- .byMetrics(newStandard.metrics)
- .getOne();
- if (!overlappingStandard)
- this.activityStandards[activity].standards.push(
- newStandard,
- );
-
- currAge += ageStep;
- }
- }
-
- // now that we generated metric data, remove the old data
- this.activityStandards[activity].standards =
- this.activityStandards[activity].standards.filter(
- (s) => s.metrics.age,
- );
- } else if (generator.metric === "weight") {
- for (const gender of Object.values(Gender)) {
- for (const age of this.agesFor(activity, gender)) {
- const weightStep = 12;
- const referenceWeight = getAvgWeight(gender, age);
-
- const minWeight = Math.max(
- referenceWeight - 3 * weightStep,
- 1,
- );
- const maxWeight = referenceWeight + 3 * weightStep;
-
- const referenceStandard = this.byActivity(activity)
- .byMetrics({
- weight: referenceWeight,
- gender,
- age,
- })
- .getOneInterpolated();
-
- let currWeight = minWeight;
- while (currWeight <= maxWeight) {
- const newStandard: Standard = {
- metrics: {
- weight: currWeight,
- age,
- gender,
- },
- levels: {},
- };
-
- // apply allometric scaling to generate levels
- for (const lvl in referenceStandard.levels) {
- if (!referenceStandard.levels[lvl])
- continue;
-
- const scalingExponent =
- this.cfg.activity[activity]
- .weightModifier;
- const coefficient =
- currWeight / referenceWeight;
-
- newStandard.levels[lvl] =
- referenceStandard.levels[lvl] *
- coefficient ** scalingExponent;
- }
-
- // prefer real data over generated data
- const overlappingStandard = this.byActivity(
- activity,
- )
- .byMetrics(newStandard.metrics)
- .getOne();
- if (!overlappingStandard)
- this.activityStandards[
- activity
- ].standards.push(newStandard);
-
- currWeight += weightStep;
- }
- }
- }
-
- // now that we generated metric data, remove the old data
- this.activityStandards[activity].standards =
- this.activityStandards[activity].standards.filter(
- (s) => s.metrics.weight,
- );
- }
- }
-
- this.activityStandards[activity].standards = this.activityStandards[
- activity
- ].standards.sort((a, b) => {
- const genderOrder: Record = {
- Male: 0,
- Female: 1,
- };
- const gA = genderOrder[b.metrics.gender] ?? 99;
- const gB = genderOrder[b.metrics.gender] ?? 99;
- if (gA !== gB) return gA - gB;
- if (a.metrics.age !== b.metrics.age)
- return a.metrics.age - b.metrics.age;
- return a.metrics.weight - b.metrics.weight;
- });
- }
- }
-
- public byActivity(activity: Activity) {
- const self = this;
-
- const state: Partial = {};
-
- // EXEC METHODS
- const execMethods = {
- exact: {
- // these will get EXACT matches
- getAll(): Standard[] {
- const src = self.activityStandards[activity].standards;
-
- const out: Standard[] = [];
- for (let i = 0; i < src.length; i++) {
- const s = src[i];
- if (
- state.gender != null &&
- s.metrics.gender !== state.gender
- )
- continue;
- if (state.age != null && s.metrics.age !== state.age)
- continue;
- if (
- state.weight != null &&
- s.metrics.weight !== state.weight
- )
- continue;
- out.push(s);
- }
- return out;
- },
- getOne: function (): Standard | undefined {
- return execMethods.exact.getAll()[0];
- },
- getNearest(
- metric: NumberMetric,
- target: number,
- ): { lower: Standard; upper: Standard } {
- const standards = execMethods.exact.getAll();
- if (standards.length === 0) throw new Error("no standards");
-
- let lower = standards[0]; // best <= target
- let upper = standards[0]; // best >= target
- let hasLower = false,
- hasUpper = false;
-
- for (const standard of standards) {
- const standardMetricValue = standard.metrics[metric];
-
- if (standardMetricValue <= target) {
- if (
- !hasLower ||
- standardMetricValue > lower.metrics[metric]
- ) {
- lower = standard;
- hasLower = true;
- }
- }
- if (standardMetricValue >= target) {
- if (
- !hasUpper ||
- standardMetricValue < upper.metrics[metric]
- ) {
- upper = standard;
- hasUpper = true;
- }
- }
- }
-
- // clamp to ends if one side missing
- if (!hasLower) lower = standards[0];
- if (!hasUpper) upper = standards[standards.length - 1];
- return { lower, upper };
- },
- },
- };
-
- const interpolateByAgeAndWeight = (metrics: Metrics): Standard => {
- const interpolateByWeight = (targetAge: number): Levels => {
- const {
- lower: lowerAgeWeightStandard,
- upper: upperAgeWeightStandard,
- } = this.byActivity(activity)
- .byGender(metrics.gender)
- .byAge(targetAge)
- .getNearest("weight", metrics.weight);
-
- const weightLower = lowerAgeWeightStandard.metrics.weight;
- const weightUpper = upperAgeWeightStandard.metrics.weight;
-
- let weightRatio =
- weightUpper === weightLower
- ? 1
- : (metrics.weight - weightLower) /
- (weightUpper - weightLower);
- weightRatio = Math.max(0, Math.min(1, weightRatio));
-
- // metrics.weight = lowerAgeWeightStandard.metrics.weight + (upperAgeWeightStandard.metrics.weight - lowerAgeWeightStandard.metrics.weight) * weightRatio;
-
- return this.interpolateLevels(
- lowerAgeWeightStandard.levels,
- upperAgeWeightStandard.levels,
- weightRatio,
- );
- };
-
- // Find nearest age standards
- const { lower: lowerAgeStandard, upper: upperAgeStandard } =
- this.byActivity(activity)
- .byGender(metrics.gender)
- .getNearest("age", metrics.age);
-
- const ageLower = lowerAgeStandard.metrics.age;
- const ageUpper = upperAgeStandard.metrics.age;
-
- // Interpolate by age and weight
- let ageRatio =
- ageUpper === ageLower
- ? 1
- : (metrics.age - ageLower) / (ageUpper - ageLower);
- ageRatio = Math.max(0, Math.min(1, ageRatio));
-
- const interpolatedLevels = this.interpolateLevels(
- interpolateByWeight(ageLower),
- interpolateByWeight(ageUpper),
- ageRatio,
- );
-
- return {
- metrics: metrics,
- levels: interpolatedLevels,
- };
- };
-
- const getAllInterpolated = (opt?: {
- normalizeForLb?: boolean;
- }): Standard[] => {
- const metrics: Metrics = state as Metrics;
-
- let weights = this.weightsFor(
- activity,
- metrics.gender,
- metrics.age,
- );
- if (opt?.normalizeForLb) {
- weights = weights.map((w) => lbToKg(Math.round(kgToLb(w))));
- }
-
- const interpolatedStandards: Standard[] = weights.map((weight) =>
- interpolateByAgeAndWeight({ ...metrics, weight }),
- );
-
- return interpolatedStandards;
- };
-
- const getOneInterpolated = (): Standard => {
- const metrics: Metrics = state as Metrics;
- return interpolateByAgeAndWeight(metrics);
- };
-
- const getMetadata = () => this.activityStandards[activity].metadata;
-
- // QUERY METHODS
-
- const byGender = (gender: Gender) => {
- state.gender = gender;
- return { byAge, ...execMethods.exact };
- };
-
- const byAge = (age: number) => {
- state.age = age;
- return { byWeight, ...execMethods.exact, getAllInterpolated };
- };
-
- const byWeight = (weight: number) => {
- state.weight = weight;
- return { ...execMethods.exact, getOneInterpolated };
- };
-
- const byMetrics = (metrics: Metrics) => {
- Object.assign(state, metrics);
- return { ...execMethods.exact, getOneInterpolated };
- };
-
- const initialMethods = {
- byGender,
- byMetrics,
- getMetadata,
- ...execMethods.exact,
- };
- return initialMethods;
- }
-
- public getAttributeActivities(attribute: Attribute): Activity[] {
- const activities: Activity[] = [];
- for (const activity of Object.keys(
- this.activityStandards,
- ) as Activity[]) {
- const activityStandard = this.activityStandards[
- activity
- ] as ActivityStandards[Activity];
- if (activityStandard.metadata.attribute === attribute) {
- activities.push(activity as Activity);
- }
- }
- return activities;
- }
-
- // TODO: turn these into queries?
- public weightsFor(activity: Activity, gender: Gender, age: number) {
- const { lower, upper } = this.byActivity(activity)
- .byGender(gender)
- .getNearest("age", age);
-
- const lowerDiff = Math.abs(lower.metrics.age - age);
- const upperDiff = Math.abs(upper.metrics.age - age);
- const closestAge =
- lowerDiff < upperDiff ? lower.metrics.age : upper.metrics.age;
-
- const weights = this.byActivity(activity)
- .byGender(gender)
- .byAge(closestAge)
- .getAll()
- .map((s) => s.metrics.weight);
-
- return weights;
- }
-
- public agesFor(activity: Activity, gender: Gender) {
- const standardsByGender = this.byActivity(activity)
- .byGender(gender)
- .getAll();
- const ages = [...new Set(standardsByGender.map((s) => s.metrics.age))];
- return ages;
- }
-
- private expandLevels(levels: Levels, i: number): Levels {
- if (i == 0) {
- return levels;
- }
- const levelKeys = Object.keys(levels)
- .map(Number)
- .sort((a, b) => a - b);
-
- const newLevels: Levels = {};
- let j = 1;
-
- for (let k = 0; k < levelKeys.length; ++k) {
- const currLevel = levelKeys[k];
- const nextLevel = levelKeys[k + 1];
-
- newLevels[j++] = levels[currLevel];
-
- if (nextLevel) {
- newLevels[j++] = (levels[currLevel] + levels[nextLevel]) / 2;
- }
- }
-
- return this.expandLevels(newLevels, i - 1);
- }
-
- private compressLevels(levels: Levels, targetLvl: number): Levels {
- const keys = Object.keys(levels)
- .map(Number)
- .sort((a, b) => a - b);
- const lvlCnt = keys.length;
-
- if (targetLvl <= 0) {
- throw new Error("Target levels amount must be a positive integer");
- }
- if (lvlCnt === 0) {
- return {};
- }
- if (targetLvl === 1) {
- return { 1: levels[keys[0]] };
- }
- if (lvlCnt < targetLvl) {
- throw new Error(
- "Target levels amount must be less than or equal to the current levels amount",
- );
- }
-
- // Linear resample
- const compressedLevels: Levels = {};
-
- for (let targetIndex = 0; targetIndex < targetLvl; ++targetIndex) {
- const sourcePos = (targetIndex * (lvlCnt - 1)) / (targetLvl - 1);
-
- const sourceIndexLower = Math.floor(sourcePos);
- const sourceIndexUpper = Math.min(sourceIndexLower + 1, lvlCnt - 1);
-
- const interpolationWeight = sourcePos - sourceIndexLower;
-
- const lowerValue = levels[keys[sourceIndexLower]];
- const upperValue = levels[keys[sourceIndexUpper]];
-
- compressedLevels[targetIndex + 1] =
- lowerValue + (upperValue - lowerValue) * interpolationWeight;
- }
-
- return compressedLevels;
- }
-
- private interpolateLevels(
- lower: Levels,
- upper: Levels,
- ratio: number,
- ): Levels {
- if (Object.keys(lower).length !== Object.keys(upper).length)
- throw new Error(
- "Cannot interpolate between varying number of levels",
- );
-
- const lerp = (lvl: string) =>
- (lower[lvl] as number) +
- ((upper[lvl] as number) - (lower[lvl] as number)) * ratio;
-
- const interpolatedLevels: Levels = {};
- for (const lvl in lower) {
- interpolatedLevels[lvl] = lerp(lvl);
- }
-
- return interpolatedLevels;
- }
-}
diff --git a/src/lib/services/calculator/util.ts b/src/lib/services/calculator/util.ts
deleted file mode 100644
index 1828b7c..0000000
--- a/src/lib/services/calculator/util.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { type Metrics } from "$lib/services/calculator/main";
-import avgWeightDataRaw from "$lib/data/avg-weights.json";
-
-// -------------------------------------------------------------------------------------------------
-// DATA MODELS
-// -------------------------------------------------------------------------------------------------
-
-export type Player = {
- name?: string;
- metrics: Metrics;
-};
-
-export enum Attribute {
- Strength = "Strength",
- Power = "Power",
- Endurance = "Endurance",
- Agility = "Agility",
-}
-
-export enum Activity {
- BackSquat = "BackSquat",
- Deadlift = "Deadlift",
- BenchPress = "BenchPress",
- Run = "Run",
- BroadJump = "BroadJump",
- ConeDrill = "ConeDrill",
-}
-
-export interface ActivityPerformance {
- activity: Activity;
- performance: number;
-}
-
-export enum Gender {
- Male = "Male",
- Female = "Female",
-}
-
-// -------------------------------------------------------------------------------------------------
-// Conversion utilities
-// -------------------------------------------------------------------------------------------------
-
-export type StandardUnit = "ms" | "cm" | "kg";
-
-export const lbToKg = (lb: number | string): number => +lb * 0.453592;
-export const kgToLb = (kg: number | string): number => +kg * 2.20462;
-
-export const minToMs = (min: number) => min * 60000;
-export const secToMs = (sec: number) => sec * 1000;
-export const msToMin = (ms: number) => ms / 60000;
-
-export const ftToCm = (ft: number) => ft * 30.48;
-export const inToCm = (inches: number) => inches * 2.54;
-export const cmToIn = (cm: number) => cm / 2.54;
-
-export const msToTime = (ms: number, includeMs = false): string => {
- const minutes = Math.floor(ms / 60000);
- const seconds = Math.floor((ms % 60000) / 1000);
- const milliseconds = Math.floor(ms % 1000);
-
- let formattedTime = `${minutes}:${seconds.toString().padStart(2, "0")}`;
- if (includeMs) {
- formattedTime += `.${milliseconds.toString().padStart(3, "0")}`;
- }
- return formattedTime;
-};
-
-export const range = (length: number) =>
- Array.from({ length: length }, (_, i) => i);
-
-export const clamp = (x: number, lo: number, hi: number) =>
- Math.max(lo, Math.min(hi, x));
-
-export const getAvgWeight = (gender: Gender, age: number) => {
- type AvgWeightData = {
- metadata: {
- unit: StandardUnit;
- };
- weights: Metrics[];
- };
- let avgWeightData = avgWeightDataRaw as AvgWeightData;
-
- const avgWeights = avgWeightData.weights.filter((a) => a.gender === gender);
- if (!avgWeights.length) throw new Error("No average weights");
-
- const closest = {
- diff: Math.abs(avgWeights[0].age - age),
- avg: avgWeights[0],
- };
- for (const avg of avgWeights) {
- const diff = Math.abs(avg.age - age);
- if (diff < closest.diff) {
- closest.diff = diff;
- closest.avg = avg;
- }
- }
-
- return closest.avg.weight;
-};
diff --git a/src/lib/services/commerce/printful.ts b/src/lib/services/commerce/printful.ts
deleted file mode 100644
index db804c5..0000000
--- a/src/lib/services/commerce/printful.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import type { Printful } from "./util/types";
-import { FetchError, type DeepPartial } from "./util/misc";
-
-export default class PrintfulService {
- static Products = class {
- static Variants = class {
- static async get(
- variantId: number | string,
- ): Promise {
- const res = await fetch(
- `${env().API_URL}/store/variants/${variantId}`,
- {
- method: "GET",
- headers: { ...env().AUTH_HEADERS },
- },
- );
-
- if (res.status === 404 || res.status === 400) {
- return;
- }
-
- if (!res.ok) {
- throw new FetchError("Failed to get Printful variant", res);
- }
-
- const payload: Printful.Products.MetaDataSingle =
- await res.json();
- return payload.result;
- }
- };
-
- static async getAll(
- offset: number = 0,
- ): Promise {
- const allSyncProducts: Printful.Products.SyncProduct[] = [];
-
- while (true) {
- try {
- const res = await fetch(
- `${env().API_URL}/store/products?offset=${offset}`,
- {
- method: "GET",
- headers: { ...env().AUTH_HEADERS },
- },
- );
-
- if (!res.ok) {
- throw new FetchError(
- "Failed to get all Printful products",
- res,
- );
- }
-
- const payload: Printful.Products.MetaDataMulti =
- await res.json();
-
- allSyncProducts.push(...payload.result);
- offset = allSyncProducts.length;
-
- if (allSyncProducts.length >= payload.paging.total) break;
- } catch (error) {
- throw error;
- }
- }
-
- return allSyncProducts;
- }
-
- static async get(
- productId: number | string,
- ): Promise {
- const res = await fetch(
- `${env().API_URL}/store/products/${productId}`,
- {
- method: "GET",
- headers: { ...env().AUTH_HEADERS },
- },
- );
-
- if (res.status === 404 || res.status === 400) {
- return;
- }
-
- if (!res.ok) {
- throw new FetchError("Failed to get Printful product", res);
- }
-
- const payload: Printful.Products.MetaDataSingle =
- await res.json();
- return payload.result;
- }
-
- static async update(
- printfulProductId: number,
- printfulProduct: DeepPartial,
- ) {
- const res = await fetch(
- `${env().API_URL}/store/products/${printfulProductId}`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- ...env().AUTH_HEADERS,
- },
- body: JSON.stringify(printfulProduct),
- },
- );
-
- if (!res.ok) {
- throw new FetchError("Failed to update Printful product", res);
- }
- }
- };
-
- static Orders = class {
- static async create(printfulOrder: Printful.Orders.Order) {
- const res = await fetch(`${env().API_URL}/orders`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...env().AUTH_HEADERS,
- },
- body: JSON.stringify(printfulOrder),
- });
-
- if (!res.ok) {
- throw new FetchError("Failed to create printful order", res);
- }
- }
-
- static async getAll(
- offset: number = 0,
- ): Promise {
- const allPrintfulOrders: Printful.Orders.Order[] = [];
-
- while (true) {
- try {
- const res = await fetch(
- `${env().API_URL}/orders?offset=${offset}`,
- {
- method: "GET",
- headers: { ...env().AUTH_HEADERS },
- },
- );
-
- if (!res.ok) {
- throw new FetchError(
- "Failed to get all Printful orders",
- res,
- );
- }
-
- const payload: Printful.Products.MetaDataMulti =
- await res.json();
-
- allPrintfulOrders.push(...payload.result);
- offset = allPrintfulOrders.length;
-
- if (allPrintfulOrders.length >= payload.paging.total) break;
- } catch (error) {
- throw error;
- }
- }
-
- return allPrintfulOrders;
- }
- };
-
- static Util = class {
- static getVariantMainImage(
- syncVariant: Printful.Products.SyncVariant,
- ): string {
- const previewFile = syncVariant.files.find(
- (f) => f.type === "preview",
- );
- return previewFile?.preview_url ?? syncVariant.product.image;
- }
- };
-}
-
-export const env = () => {
- if (typeof Bun === "undefined") {
- throw new Error(
- "Must be in a server context. Make sure to run using --bun.",
- );
- }
-
- const vars = {
- API_URL: "https://api.printful.com",
- AUTH_TOKEN: Bun.env.PRINTFUL_AUTH,
- STORE_ID: Bun.env.PRINTFUL_STORE_ID,
- };
-
- return {
- ...vars,
- AUTH_HEADERS: {
- Authorization: `Bearer ${vars.AUTH_TOKEN}`,
- "X-PF-Store-Id": `${vars.STORE_ID}`,
- },
- };
-};
diff --git a/src/lib/services/commerce/sync.ts b/src/lib/services/commerce/sync.ts
deleted file mode 100644
index c4a8fe4..0000000
--- a/src/lib/services/commerce/sync.ts
+++ /dev/null
@@ -1,356 +0,0 @@
-import type { Printful, Webflow } from "./util/types";
-import { FetchError, formatSlug, type DeepPartial } from "./util/misc";
-import WebflowService from "./webflow";
-import PrintfulService from "./printful";
-
-const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
-
-export const R2_WORKER_URL = "https://r2-worker.xominus.workers.dev";
-export const WEBSITE_MEDIA_URL = "https://website-media.bladeandbrawn.com";
-
-type MainProduct = {
- name: string;
- externalId: string;
- colors: Set;
- variants: {
- color: string;
- product: Printful.Products.Product;
- }[];
-};
-
-export default class SyncService {
- static state = {
- isSyncing: false,
- syncingIds: [] as number[],
- };
-
- static async sync(printfulProductId: number | undefined) {
- this.state.isSyncing = true;
-
- const mainPrintfulProducts: MainProduct[] = [];
- const webflowProducts: Webflow.Products.ProductAndSkus[] = [];
- try {
- let printfulProducts = await PrintfulService.Products.getAll();
- if (printfulProductId) {
- const printfulProduct = printfulProducts.find(
- (p) => p.id === printfulProductId,
- );
- if (printfulProduct) {
- const mainProductName = this.getMainProductName(
- printfulProduct.name,
- );
- printfulProducts = printfulProducts.filter((p) =>
- p.name.includes(mainProductName),
- );
- }
- }
- webflowProducts.push(...(await WebflowService.Products.getAll()));
-
- // Generate main printful products
- console.log("Generating main products...");
- for (const printfulProduct of printfulProducts) {
- const mainProductName = this.getMainProductName(
- printfulProduct.name,
- );
-
- let mainPrintfulProduct = mainPrintfulProducts.find((mp) =>
- mp.name.includes(mainProductName),
- );
- if (!mainPrintfulProduct) {
- mainPrintfulProduct = {
- name: mainProductName,
- variants: [],
- externalId: printfulProduct.external_id,
- colors: new Set(),
- };
- mainPrintfulProducts.push(mainPrintfulProduct);
- }
-
- const productColor = this.findColorInProductName(
- printfulProduct.name,
- );
- mainPrintfulProduct.variants.push({
- color: productColor,
- product: (await PrintfulService.Products.get(
- printfulProduct.id,
- ))!,
- });
- mainPrintfulProduct.colors.add(productColor);
- }
- } catch (error) {
- throw error;
- } finally {
- this.state.isSyncing = false;
- this.state.syncingIds = [];
- }
-
- // TODO: Validate main printful products
-
- // Sync the main printful products
- console.log("Syncing main products...");
- for (const mainPrintfulProduct of mainPrintfulProducts) {
- try {
- this.state.isSyncing = true;
- this.state.syncingIds = mainPrintfulProduct.variants.map(
- (v) => v.product.sync_product.id,
- );
-
- const foundColors = mainPrintfulProduct.colors;
- const foundSizes: Set = new Set();
-
- const webflowSkus: DeepPartial[] =
- [];
-
- for (const specialVariant of mainPrintfulProduct.variants) {
- for (const printfulVariant of specialVariant.product
- .sync_variants) {
- // check if this variant size is in all other special variants
- let sizeIsInAllVariants = true;
- for (const specialVariant of mainPrintfulProduct.variants) {
- const sizes =
- specialVariant.product.sync_variants.map(
- (v) => v.size,
- );
- if (!sizes.includes(printfulVariant.size))
- sizeIsInAllVariants = false;
- }
-
- if (sizeIsInAllVariants) {
- foundSizes.add(printfulVariant.size);
-
- webflowSkus.push({
- id: printfulVariant.external_id,
- fieldData: {
- name: printfulVariant.name,
- slug: formatSlug(printfulVariant.name),
- "sku-values": {
- color: specialVariant.color,
- size: printfulVariant.size,
- },
- price: {
- value: Math.floor(
- +printfulVariant.retail_price * 100,
- ),
- unit: printfulVariant.currency,
- currency: printfulVariant.currency,
- },
- "main-image":
- PrintfulService.Util.getVariantMainImage(
- printfulVariant,
- ),
- },
- });
- }
- }
- }
-
- // SYNC PRODUCT DATA
- let existingWebflowProduct = webflowProducts.find(
- (webflowProduct) =>
- webflowProduct.product.id ===
- mainPrintfulProduct.externalId.split("-")[0],
- );
- if (existingWebflowProduct) {
- // SYNC PRODUCT UPDATE
-
- // do not update images
- for (const webflowSku of webflowSkus) {
- delete webflowSku.fieldData?.["main-image"];
- }
-
- const webflowProductId =
- mainPrintfulProduct.externalId.split("-")[0];
- WebflowService.Products.update(webflowProductId, {
- product: {
- fieldData: {
- name: mainPrintfulProduct.name,
- slug: formatSlug(mainPrintfulProduct.name),
- shippable: true,
- "tax-category": "standard-taxable",
- "sku-properties": [
- {
- id: "color",
- name: "Color",
- enum: Array.from(foundColors).map(
- (color) => ({
- id: color,
- slug: formatSlug(color),
- name: color,
- }),
- ),
- },
- {
- id: "size",
- name: "Size",
- enum: Array.from(foundSizes).map(
- (size) => ({
- id: size,
- slug: formatSlug(size),
- name: size,
- }),
- ),
- },
- ],
- },
- },
- sku: webflowSkus[0],
- });
-
- for (const webflowSku of webflowSkus.slice(1)) {
- const existingWebflowSku =
- existingWebflowProduct.skus.find(
- (sku) => sku.id === webflowSku.id,
- );
- if (webflowSku.id && existingWebflowSku) {
- await WebflowService.Products.Skus.update(
- webflowProductId,
- webflowSku.id,
- webflowSku,
- );
- } else {
- await WebflowService.Products.Skus.create(
- webflowProductId,
- [webflowSku],
- );
- }
- }
- } else {
- // SYNC PRODUCT CREATE
- const webflowProductId =
- await WebflowService.Products.create({
- product: {
- fieldData: {
- name: mainPrintfulProduct.name,
- slug: formatSlug(mainPrintfulProduct.name),
- shippable: true,
- "tax-category": "standard-taxable",
- "sku-properties": [
- {
- id: "color",
- name: "Color",
- enum: Array.from(foundColors).map(
- (color) => ({
- id: color,
- slug: formatSlug(color),
- name: color,
- }),
- ),
- },
- {
- id: "size",
- name: "Size",
- enum: Array.from(foundSizes).map(
- (size) => ({
- id: size,
- slug: formatSlug(size),
- name: size,
- }),
- ),
- },
- ],
- },
- },
- sku: webflowSkus[0],
- });
-
- // create webflow product SKUs
- await WebflowService.Products.Skus.create(
- webflowProductId,
- webflowSkus.slice(1),
- );
-
- existingWebflowProduct =
- await WebflowService.Products.get(webflowProductId);
- if (!existingWebflowProduct) {
- console.error("Missing webflow product");
- throw new Error("Failed to create Webflow product");
- }
-
- for (const specialVariant of mainPrintfulProduct.variants) {
- const newPrintfulVariants: DeepPartial[] =
- [];
- for (const printfulVariant of specialVariant.product
- .sync_variants) {
- const associatedWebflowSku =
- existingWebflowProduct.skus.find(
- (sku) =>
- sku.fieldData["sku-values"]?.[
- "color"
- ] === printfulVariant.color &&
- sku.fieldData["sku-values"]?.[
- "size"
- ] === printfulVariant.size,
- );
- if (associatedWebflowSku) {
- newPrintfulVariants.push({
- id: printfulVariant.id, // printful variant id
- external_id: String(
- associatedWebflowSku.id,
- ), // webflow variant id
- });
- }
- }
-
- await sleep(10000);
- await PrintfulService.Products.update(
- specialVariant.product.sync_product.id,
- {
- sync_product: {
- id: specialVariant.product.sync_product.id,
- external_id: `${webflowProductId}-${specialVariant.color}`,
- },
- sync_variants: newPrintfulVariants,
- },
- );
- }
- }
- } catch (err) {
- // SYNC IMAGES
- // for (const sku of existingWebflowProduct.skus) {
- // const skuColor = sku.fieldData["sku-values"]?.["color"]?.toLowerCase() ?? "None";
- // const skuSlug = `${existingWebflowProduct.product.fieldData.slug}-${skuColor}`;
- // const skuImageUrls = await this.getProductImageUrls(skuSlug);
- //
- // sku.fieldData["main-image"] = skuImageUrls[0] ?? sku.fieldData["main-image"];
- // sku.fieldData["more-images"] = skuImageUrls.slice(1).map(url => ({ url }));
- //
- // await WebflowService.Products.Skus.update(existingWebflowProduct.product.id, sku.id, sku);
- // }
- //
- if (err instanceof FetchError) {
- console.error(err.message, err.payload);
- }
- }
- }
- this.state.isSyncing = false;
- this.state.syncingIds = [];
- console.log("Done");
- }
-
- private static getProductImageUrls = async (slug: 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();
- return productImageKeys.map((key) => `${WEBSITE_MEDIA_URL}/${key}`);
- };
-
- public static findColorInProductName = (productName: string): string => {
- const m = productName.match(/\[([^\]]+)\]/);
- return m ? m[1] : "N/A";
- };
-
- public static getMainProductName = (productName: string) => {
- const colorInName = this.findColorInProductName(productName);
- if (colorInName) {
- return productName.replace(`[${colorInName}]`, "").trimEnd();
- } else {
- return productName;
- }
- };
-}
diff --git a/src/lib/services/commerce/webflow.ts b/src/lib/services/commerce/webflow.ts
deleted file mode 100644
index ef29e80..0000000
--- a/src/lib/services/commerce/webflow.ts
+++ /dev/null
@@ -1,309 +0,0 @@
-import type { Webflow } from "./util/types";
-import { FetchError, type DeepPartial } from "./util/misc";
-import crypto from "node:crypto";
-
-export default class WebflowService {
- static Products = class {
- static Skus = class {
- static async create(
- webflowProductId: string,
- skus: DeepPartial[],
- ) {
- 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,
- );
- }
- }
-
- static async update(
- webflowProductId: string,
- webflowSkuId: string,
- webflowSku: DeepPartial,
- ) {
- 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,
- ) {
- 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();
- return createdWebflowProduct.product.id;
- }
-
- static async getAll(): Promise {
- 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();
- return payload.items as Webflow.Products.ProductAndSkus[];
- }
-
- static async get(
- webflowProductId: string,
- ): Promise {
- 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();
- return payload as Webflow.Products.ProductAndSkus;
- }
-
- static async update(
- webflowProductId: string,
- webflowProduct: DeepPartial,
- ) {
- 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) {
- 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 {
- // 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();
- return payload.orders as Webflow.Orders.Order[];
- }
-
- static async update(
- webflowOrderId: string,
- webflowOrderUpdate: {
- comment?: string;
- shippingProvider?: string;
- shippingTracking?: string;
- shippingTrackingURL: string;
- },
- ) {
- 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 } = {},
- ) {
- 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) {
- 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 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 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,
- };
-
- 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}` },
- };
-};
diff --git a/src/lib/test/validate-products.ts b/src/lib/test/validate-products.ts
deleted file mode 100644
index bff3ca9..0000000
--- a/src/lib/test/validate-products.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import PrintfulService from "$lib/services/commerce/printful";
-import SyncService from "$lib/services/commerce/sync";
-import WebflowService from "$lib/services/commerce/webflow";
-
-const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
-
-type Validation = {
- isValid: boolean;
- message: string;
-};
-
-const validateEquality = (wValue: any, pValue: any): Validation => {
- const validation = {
- isValid: true,
- message: "Valid",
- };
-
- if (wValue !== pValue) {
- validation.isValid = false;
- validation.message = `${wValue} !== ${pValue}`;
- }
-
- return validation;
-};
-
-const wProducts = await WebflowService.Products.getAll();
-
-let invalidCount = 0;
-for (const wProduct of wProducts) {
- for (const sku of wProduct.skus ?? []) {
- console.log("Webflow: " + sku.id, sku.fieldData.name);
-
- const pVariant = await PrintfulService.Products.Variants.get(
- `@${sku.id}`,
- );
- if (pVariant) {
- console.log("Printful: " + pVariant.id, pVariant.name);
- const validations = [
- // validateEquality(sku.fieldData.name, pVariant.name),
- validateEquality(
- sku.fieldData["sku-values"]?.["color"],
- SyncService.findColorInProductName(pVariant.name),
- ),
- validateEquality(
- sku.fieldData["sku-values"]?.["size"],
- pVariant.size,
- ),
- validateEquality(
- sku.fieldData.price.value,
- Math.floor(+pVariant.retail_price * 100),
- ),
- ];
-
- // if (!validateEquality(sku.fieldData.name, pVariant.name).isValid) {
- // sku.fieldData.name = pVariant.name;
- // await WebflowService.Products.Skus.update(wProduct.product.id, sku.id, sku);
- // }
-
- const errors = validations.filter((v) => !v.isValid);
- if (errors.length) {
- ++invalidCount;
- console.log("INVALID: ");
- console.log(errors.map((e) => e.message).join(", "));
- } else {
- console.log("VALID");
- }
- } else {
- ++invalidCount;
- console.log("INVALID");
- console.log("Not found");
- }
- console.log();
- }
- await sleep(3000);
-}
-
-console.log("INVALID COUNT: " + invalidCount);
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
deleted file mode 100644
index e9699f3..0000000
--- a/src/routes/+layout.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
- {@render children?.()}
-
-
-
-
-
-
-
-
-
diff --git a/src/routes/[...slugs]/+server.ts b/src/routes/[...slugs]/+server.ts
deleted file mode 100644
index 08bc4e4..0000000
--- a/src/routes/[...slugs]/+server.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { app } from "$lib/server/app";
-
-type RequestHandler = (v: { request: Request }) => Response | Promise;
-export const fallback: RequestHandler = ({ request }) => app.handle(request);
diff --git a/src/routes/commerce/+page.server.ts b/src/routes/commerce/+page.server.ts
deleted file mode 100644
index b1b4975..0000000
--- a/src/routes/commerce/+page.server.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import PrintfulService from "$lib/services/commerce/printful";
-import WebflowService from "$lib/services/commerce/webflow";
-import type { PageServerLoad } from "./$types";
-
-export const load = async ({}: Parameters[0]) => {
- return {
- products: {
- printful: PrintfulService.Products.getAll(),
- webflow: WebflowService.Products.getAll(),
- },
- };
-};
diff --git a/src/routes/commerce/+page.svelte b/src/routes/commerce/+page.svelte
deleted file mode 100644
index 0fe8e2f..0000000
--- a/src/routes/commerce/+page.svelte
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-{#await data.products.printful}
- Loading...
-{:then printfulProducts}
-
-
-
-
-
- | Name |
- Status |
- Sync |
-
-
-
- {#each printfulProducts as printfulProduct}
-
- |
-
- {#await isProductSynced(printfulProduct) then isSynced}
- {#if synchronizer.syncingIds.includes(printfulProduct.id)}
-
- Syncing...
-
- {:else if isSynced}
-
- Synced
-
- {:else}
-
- Desynced
-
- {/if}
- {/await}
- |
-
-
- |
-
- {/each}
-
-
-
-{:catch error}
- Something went wrong: {error.message}
-{/await}
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..ba56241
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ // Environment setup & latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "forceConsistentCasingInFileNames": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false,
+ },
+}
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index ecd756a..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "extends": "./.svelte-kit/tsconfig.json",
- "compilerOptions": {
- "allowJs": true,
- "checkJs": true,
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "resolveJsonModule": true,
- "verbatimModuleSyntax": true,
- "skipLibCheck": true,
- "sourceMap": true,
- "strict": true,
- "moduleResolution": "bundler"
- }
- // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
- // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
- //
- // To make changes to top-level options such as include and exclude, we recommend extending
- // the generated config; see https://svelte.dev/docs/kit/configuration#typescript
-}
diff --git a/workers/kv/Makefile b/workers/kv/Makefile
new file mode 100644
index 0000000..ce95a37
--- /dev/null
+++ b/workers/kv/Makefile
@@ -0,0 +1,2 @@
+deploy:
+ npx wrangler deploy
diff --git a/workers/kv/src/index.ts b/workers/kv/src/index.ts
new file mode 100644
index 0000000..3665554
--- /dev/null
+++ b/workers/kv/src/index.ts
@@ -0,0 +1,17 @@
+export default {
+ async fetch(request: Request, env: Env) {
+ if (request.method !== "POST") {
+ return new Response("Method Not Allowed", { status: 405 });
+ }
+
+ const body = await request.json().catch(() => null);
+ if (!body) return new Response("Bad Request", { status: 400 });
+
+ const key = `calc:${Date.now()}:${crypto.randomUUID()}`;
+ await env.CALCULATOR_LOGGING.put(key, JSON.stringify(body));
+
+ return new Response(JSON.stringify({ ok: true, key }), {
+ headers: { "content-type": "application/json" },
+ });
+ },
+} satisfies ExportedHandler;
diff --git a/workers/kv/test.ts b/workers/kv/test.ts
new file mode 100644
index 0000000..f170055
--- /dev/null
+++ b/workers/kv/test.ts
@@ -0,0 +1,5 @@
+fetch("https://kv-logger.xominus.workers.dev", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ "test": 1 })
+}).catch((e) => { console.error(e) });
diff --git a/workers/kv/wrangler.toml b/workers/kv/wrangler.toml
new file mode 100644
index 0000000..d99ccd0
--- /dev/null
+++ b/workers/kv/wrangler.toml
@@ -0,0 +1,7 @@
+name = "kv-logger"
+main = "src/index.ts"
+compatibility_date = "2025-10-17"
+
+[[kv_namespaces]]
+binding = "CALCULATOR_LOGGING"
+id = "4b585549cf074f729d9ec4ac39e2c879"
diff --git a/workers/r2/.editorconfig b/workers/r2/.editorconfig
new file mode 100644
index 0000000..a727df3
--- /dev/null
+++ b/workers/r2/.editorconfig
@@ -0,0 +1,12 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml]
+indent_style = space
diff --git a/workers/r2/.gitignore b/workers/r2/.gitignore
new file mode 100644
index 0000000..4138168
--- /dev/null
+++ b/workers/r2/.gitignore
@@ -0,0 +1,167 @@
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+\*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+\*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+\*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+\*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.cache
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+.cache/
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+.cache
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.\*
+
+# wrangler project
+
+.dev.vars*
+!.dev.vars.example
+.env*
+!.env.example
+.wrangler/
diff --git a/workers/r2/.prettierrc b/workers/r2/.prettierrc
new file mode 100644
index 0000000..5c7b5d3
--- /dev/null
+++ b/workers/r2/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "printWidth": 140,
+ "singleQuote": true,
+ "semi": true,
+ "useTabs": true
+}
diff --git a/workers/r2/.vscode/settings.json b/workers/r2/.vscode/settings.json
new file mode 100644
index 0000000..0126e59
--- /dev/null
+++ b/workers/r2/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "wrangler.json": "jsonc"
+ }
+}
\ No newline at end of file
diff --git a/workers/r2/bun.lock b/workers/r2/bun.lock
new file mode 100644
index 0000000..f84b9bc
--- /dev/null
+++ b/workers/r2/bun.lock
@@ -0,0 +1,447 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "r2-worker",
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "^0.8.19",
+ "typescript": "^5.5.2",
+ "vitest": "~3.2.0",
+ "wrangler": "^4.38.0",
+ },
+ },
+ },
+ "packages": {
+ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
+
+ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.4", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250912.0" }, "optionalPeers": ["workerd"] }, "sha512-KIjbu/Dt50zseJIoOOK5y4eYpSojD9+xxkePYVK1Rg9k/p/st4YyMtz1Clju/zrenJHrOH+AAcjNArOPMwH4Bw=="],
+
+ "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.8.71", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^5.3.2", "miniflare": "4.20250906.0", "semver": "^7.7.1", "wrangler": "4.35.0", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-keu2HCLQfRNwbmLBCDXJgCFpANTaYnQpE01fBOo4CNwiWHUT7SZGN7w64RKiSWRHyYppStXBuE5Ng7F42+flpg=="],
+
+ "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250917.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0kL/kFnKUSycoo7b3PgM0nRyZ+1MGQAKaXtE6a2+SAeUkZ2FLnuFWmASi0s4rlWGsf/rlTw4AwXROePir9dUcQ=="],
+
+ "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250917.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3/N1QmEJsC8Byxt1SGgVp5o0r+eKjuUEMbIL2yzLk/jrMdErPXy/DGf/tXZoACU68a/gMEbbT1itkYrm85iQHg=="],
+
+ "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250917.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E7sEow7CErbWY3olMmlbj6iss9r7Xb2uMyc+MKzYC9/J6yFlJd/dNHvjey9QIdxzbkC9qGe90a+KxQrjs+fspA=="],
+
+ "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250917.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-roOnRjxut2FUxo6HA9spbfs32naXAsnSQqsgku3iq6BYKv1QqGiFoY5bReK72N5uxmhxo7+RiTo8ZEkxA/vMIQ=="],
+
+ "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250917.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gslh6Ou9+kshHjR1BJX47OsbPw3/cZCvGDompvaW/URCgr7aMzljbgmBb7p0uhwGy1qCXcIt31St6pd3IEcLng=="],
+
+ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
+
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
+
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
+
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
+
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
+
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
+
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
+
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
+
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
+
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
+
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
+
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
+
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
+
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
+
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
+
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
+
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
+
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
+
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
+
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
+
+ "@poppinss/colors": ["@poppinss/colors@4.1.5", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw=="],
+
+ "@poppinss/dumper": ["@poppinss/dumper@0.6.4", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ=="],
+
+ "@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.0", "", { "os": "android", "cpu": "arm" }, "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.0", "", { "os": "android", "cpu": "arm64" }, "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.0", "", { "os": "linux", "cpu": "none" }, "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.0", "", { "os": "none", "cpu": "arm64" }, "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ=="],
+
+ "@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="],
+
+ "@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="],
+
+ "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
+
+ "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
+
+ "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
+
+ "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
+
+ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
+
+ "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
+
+ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "birpc": ["birpc@0.2.14", "", {}, "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA=="],
+
+ "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
+
+ "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+
+ "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
+
+ "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
+
+ "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
+
+ "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
+
+ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
+
+ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
+
+ "detect-libc": ["detect-libc@2.1.0", "", {}, "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="],
+
+ "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
+
+ "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
+
+ "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
+
+ "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
+
+ "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
+
+ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
+
+ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+
+ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
+
+ "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
+
+ "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
+
+ "miniflare": ["miniflare@4.20250906.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^7.10.0", "workerd": "1.20250906.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-T/RWn1sa0ien80s6NjU+Un/tj12gR6wqScZoiLeMJDD4/fK0UXfnbWXJDubnUED8Xjm7RPQ5ESYdE+mhPmMtuQ=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
+
+ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "rollup": ["rollup@4.52.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.0", "@rollup/rollup-android-arm64": "4.52.0", "@rollup/rollup-darwin-arm64": "4.52.0", "@rollup/rollup-darwin-x64": "4.52.0", "@rollup/rollup-freebsd-arm64": "4.52.0", "@rollup/rollup-freebsd-x64": "4.52.0", "@rollup/rollup-linux-arm-gnueabihf": "4.52.0", "@rollup/rollup-linux-arm-musleabihf": "4.52.0", "@rollup/rollup-linux-arm64-gnu": "4.52.0", "@rollup/rollup-linux-arm64-musl": "4.52.0", "@rollup/rollup-linux-loong64-gnu": "4.52.0", "@rollup/rollup-linux-ppc64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-gnu": "4.52.0", "@rollup/rollup-linux-riscv64-musl": "4.52.0", "@rollup/rollup-linux-s390x-gnu": "4.52.0", "@rollup/rollup-linux-x64-gnu": "4.52.0", "@rollup/rollup-linux-x64-musl": "4.52.0", "@rollup/rollup-openharmony-arm64": "4.52.0", "@rollup/rollup-win32-arm64-msvc": "4.52.0", "@rollup/rollup-win32-ia32-msvc": "4.52.0", "@rollup/rollup-win32-x64-gnu": "4.52.0", "@rollup/rollup-win32-x64-msvc": "4.52.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g=="],
+
+ "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
+ "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
+
+ "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
+
+ "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
+
+ "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
+
+ "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
+
+ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
+
+ "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
+
+ "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="],
+
+ "unenv": ["unenv@2.0.0-rc.21", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A=="],
+
+ "vite": ["vite@7.1.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ=="],
+
+ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
+
+ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "workerd": ["workerd@1.20250917.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250917.0", "@cloudflare/workerd-darwin-arm64": "1.20250917.0", "@cloudflare/workerd-linux-64": "1.20250917.0", "@cloudflare/workerd-linux-arm64": "1.20250917.0", "@cloudflare/workerd-windows-64": "1.20250917.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-0D+wWaccyYQb2Zx2DZDC77YDn9kOpkpGMCgyKgIHilghut5hBQ/adUIEseS4iuIZxBPeFSn6zFtICP0SxZ3z0g=="],
+
+ "wrangler": ["wrangler@4.38.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.4", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250917.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20250917.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250917.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-ITL4VZ4KWs8LMDEttDTrAKLktwtv1NxHBd5QIqHOczvcjnAQr+GQoE6XYQws+w8jlOjDV7KyvbFqAdyRh5om3g=="],
+
+ "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
+
+ "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
+
+ "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
+
+ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.35.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.3", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250906.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20250906.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250906.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-HbyXtbrh4Fi3mU8ussY85tVdQ74qpVS1vctUgaPc+bPrXBTqfDLkZ6VRtHAVF/eBhz4SFmhJtCQpN1caY2Ak8A=="],
+
+ "miniflare/workerd": ["workerd@1.20250906.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250906.0", "@cloudflare/workerd-darwin-arm64": "1.20250906.0", "@cloudflare/workerd-linux-64": "1.20250906.0", "@cloudflare/workerd-linux-arm64": "1.20250906.0", "@cloudflare/workerd-windows-64": "1.20250906.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw=="],
+
+ "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
+
+ "vite/esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
+
+ "wrangler/miniflare": ["miniflare@4.20250917.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20250917.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-A7kYEc/Y6ohiiTji4W/qGJj3aJNc/9IMj/6wLy2phD/iMjcoY8t35654gR5mHbMx0AgUolDdr3HOsHB0cYBf+Q=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.3", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250828.1" }, "optionalPeers": ["workerd"] }, "sha512-tsQQagBKjvpd9baa6nWVIv399ejiqcrUBBW6SZx6Z22+ymm+Odv5+cFimyuCsD/fC1fQTwfRmwXBNpzvHSeGCw=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20250906.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250906.0", "@cloudflare/workerd-darwin-arm64": "1.20250906.0", "@cloudflare/workerd-linux-64": "1.20250906.0", "@cloudflare/workerd-linux-arm64": "1.20250906.0", "@cloudflare/workerd-windows-64": "1.20250906.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ryVyEaqXPPsr/AxccRmYZZmDAkfQVjhfRqrNTlEeN8aftBk6Ca1u7/VqmfOayjCXrA+O547TauebU+J3IpvFXw=="],
+
+ "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250906.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA=="],
+
+ "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250906.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA=="],
+
+ "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250906.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q=="],
+
+ "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250906.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA=="],
+
+ "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="],
+
+ "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
+
+ "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
+
+ "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
+
+ "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
+
+ "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
+
+ "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
+
+ "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
+
+ "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
+
+ "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
+
+ "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
+
+ "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
+
+ "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
+
+ "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
+
+ "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
+
+ "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
+
+ "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
+
+ "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
+
+ "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
+
+ "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
+
+ "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
+
+ "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
+
+ "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
+
+ "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
+
+ "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
+
+ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
+
+ "wrangler/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
+
+ "wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250906.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-E+X/YYH9BmX0ew2j/mAWFif2z05NMNuhCTlNYEGLkqMe99K15UewBqajL9pMcMUKxylnlrEoK3VNxl33DkbnPA=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250906.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-X5apsZ1SFW4FYTM19ISHf8005FJMPfrcf4U5rO0tdj+TeJgQgXuZ57IG0WeW7SpLVeBo8hM6WC8CovZh41AfnA=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250906.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rlKzWgsLnlQ5Nt9W69YBJKcmTmZbOGu0edUsenXPmc6wzULUxoQpi7ZE9k3TfTonJx4WoQsQlzCUamRYFsX+0Q=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250906.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DdedhiQ+SeLzpg7BpcLrIPEZ33QKioJQ1wvL4X7nuLzEB9rWzS37NNNahQzc1+44rhG4fyiHbXBPOeox4B9XVA=="],
+
+ "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250906.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q8Qjfs8jGVILnZL6vUpQ90q/8MTCYaGR3d1LGxZMBqte8Vr7xF3KFHPEy7tFs0j0mMjnqCYzlofmPNY+9ZaDRg=="],
+ }
+}
diff --git a/workers/r2/package.json b/workers/r2/package.json
new file mode 100644
index 0000000..0d32a57
--- /dev/null
+++ b/workers/r2/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "r2-worker",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "deploy": "wrangler deploy",
+ "dev": "wrangler dev",
+ "start": "wrangler dev",
+ "test": "vitest",
+ "cf-typegen": "wrangler types"
+ },
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "^0.8.19",
+ "typescript": "^5.5.2",
+ "vitest": "~3.2.0",
+ "wrangler": "^4.38.0"
+ }
+}
\ No newline at end of file
diff --git a/workers/r2/src/index.ts b/workers/r2/src/index.ts
new file mode 100644
index 0000000..ebb934c
--- /dev/null
+++ b/workers/r2/src/index.ts
@@ -0,0 +1,29 @@
+export default {
+ async fetch(request, env, ctx): Promise {
+ const url = new URL(request.url);
+ const prefix = url.pathname.slice(1);
+
+ switch (request.method) {
+ case 'GET':
+ const listResponse = await env.media.list({ prefix, limit: 1000 });
+ const keys = listResponse.objects.map(o => o.key);
+
+ const headers = new Headers();
+ headers.set('Content-Type', 'application/json');
+
+ return new Response(JSON.stringify(keys), {
+ headers,
+ status: 200,
+ });
+
+ default:
+ return new Response(`${request.method} is not allowed.`, {
+ status: 405,
+ headers: {
+ Allow: 'GET',
+ },
+ });
+ }
+
+ },
+} satisfies ExportedHandler;
diff --git a/workers/r2/test/env.d.ts b/workers/r2/test/env.d.ts
new file mode 100644
index 0000000..d145fc7
--- /dev/null
+++ b/workers/r2/test/env.d.ts
@@ -0,0 +1,3 @@
+declare module 'cloudflare:test' {
+ interface ProvidedEnv extends Env {}
+}
diff --git a/workers/r2/test/index.spec.ts b/workers/r2/test/index.spec.ts
new file mode 100644
index 0000000..5197296
--- /dev/null
+++ b/workers/r2/test/index.spec.ts
@@ -0,0 +1,24 @@
+import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
+import { describe, it, expect } from 'vitest';
+import worker from '../src/index';
+
+// For now, you'll need to do something like this to get a correctly-typed
+// `Request` to pass to `worker.fetch()`.
+const IncomingRequest = Request;
+
+describe('Hello World worker', () => {
+ it('responds with Hello World! (unit style)', async () => {
+ const request = new IncomingRequest('http://example.com');
+ // Create an empty context to pass to `worker.fetch()`.
+ const ctx = createExecutionContext();
+ const response = await worker.fetch(request, env, ctx);
+ // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
+ await waitOnExecutionContext(ctx);
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
+ });
+
+ it('responds with Hello World! (integration style)', async () => {
+ const response = await SELF.fetch('https://example.com');
+ expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
+ });
+});
diff --git a/workers/r2/test/tsconfig.json b/workers/r2/test/tsconfig.json
new file mode 100644
index 0000000..978ecd8
--- /dev/null
+++ b/workers/r2/test/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "types": ["@cloudflare/vitest-pool-workers"]
+ },
+ "include": ["./**/*.ts", "../worker-configuration.d.ts"],
+ "exclude": []
+}
diff --git a/workers/r2/tsconfig.json b/workers/r2/tsconfig.json
new file mode 100644
index 0000000..ca1f83e
--- /dev/null
+++ b/workers/r2/tsconfig.json
@@ -0,0 +1,45 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "target": "es2021",
+ /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ "lib": ["es2021"],
+ /* Specify what JSX code is generated. */
+ "jsx": "react-jsx",
+
+ /* Specify what module code is generated. */
+ "module": "es2022",
+ /* Specify how TypeScript looks up a file from a given module specifier. */
+ "moduleResolution": "Bundler",
+ /* Enable importing .json files */
+ "resolveJsonModule": true,
+
+ /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
+ "allowJs": true,
+ /* Enable error reporting in type-checked JavaScript files. */
+ "checkJs": false,
+
+ /* Disable emitting files from a compilation. */
+ "noEmit": true,
+
+ /* Ensure that each file can be safely transpiled without relying on other imports. */
+ "isolatedModules": true,
+ /* Allow 'import x from y' when a module doesn't have a default export. */
+ "allowSyntheticDefaultImports": true,
+ /* Ensure that casing is correct in imports. */
+ "forceConsistentCasingInFileNames": true,
+
+ /* Enable all strict type-checking options. */
+ "strict": true,
+
+ /* Skip type checking all .d.ts files. */
+ "skipLibCheck": true,
+ "types": [
+ "./worker-configuration.d.ts"
+ ]
+ },
+ "exclude": ["test"],
+ "include": ["worker-configuration.d.ts", "src/**/*.ts"]
+}
diff --git a/workers/r2/vitest.config.mts b/workers/r2/vitest.config.mts
new file mode 100644
index 0000000..977f64c
--- /dev/null
+++ b/workers/r2/vitest.config.mts
@@ -0,0 +1,11 @@
+import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
+
+export default defineWorkersConfig({
+ test: {
+ poolOptions: {
+ workers: {
+ wrangler: { configPath: './wrangler.jsonc' },
+ },
+ },
+ },
+});
diff --git a/workers/r2/worker-configuration.d.ts b/workers/r2/worker-configuration.d.ts
new file mode 100644
index 0000000..678aeff
--- /dev/null
+++ b/workers/r2/worker-configuration.d.ts
@@ -0,0 +1,7691 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: b5973daa282d3bd160a194158b415574)
+// Runtime types generated with workerd@1.20250917.0 2025-09-21
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./src/index");
+ }
+ interface Env {
+ media: R2Bucket;
+ }
+}
+interface Env extends Cloudflare.Env {}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never;
+/**
+ * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string);
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */
+ readonly message: string;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */
+ readonly name: string;
+ /**
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number;
+ static readonly INDEX_SIZE_ERR: number;
+ static readonly DOMSTRING_SIZE_ERR: number;
+ static readonly HIERARCHY_REQUEST_ERR: number;
+ static readonly WRONG_DOCUMENT_ERR: number;
+ static readonly INVALID_CHARACTER_ERR: number;
+ static readonly NO_DATA_ALLOWED_ERR: number;
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ static readonly NOT_FOUND_ERR: number;
+ static readonly NOT_SUPPORTED_ERR: number;
+ static readonly INUSE_ATTRIBUTE_ERR: number;
+ static readonly INVALID_STATE_ERR: number;
+ static readonly SYNTAX_ERR: number;
+ static readonly INVALID_MODIFICATION_ERR: number;
+ static readonly NAMESPACE_ERR: number;
+ static readonly INVALID_ACCESS_ERR: number;
+ static readonly VALIDATION_ERR: number;
+ static readonly TYPE_MISMATCH_ERR: number;
+ static readonly SECURITY_ERR: number;
+ static readonly NETWORK_ERR: number;
+ static readonly ABORT_ERR: number;
+ static readonly URL_MISMATCH_ERR: number;
+ static readonly QUOTA_EXCEEDED_ERR: number;
+ static readonly TIMEOUT_ERR: number;
+ static readonly INVALID_NODE_TYPE_ERR: number;
+ static readonly DATA_CLONE_ERR: number;
+ get stack(): any;
+ set stack(value: any);
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent;
+ scheduled: ScheduledEvent;
+ queue: QueueEvent;
+ unhandledrejection: PromiseRejectionEvent;
+ rejectionhandled: PromiseRejectionEvent;
+};
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget;
+}
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */
+interface Console {
+ "assert"(condition?: boolean, ...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */
+ clear(): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */
+ count(label?: string): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */
+ countReset(label?: string): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */
+ debug(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */
+ dir(item?: any, options?: any): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */
+ dirxml(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */
+ error(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */
+ group(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */
+ groupCollapsed(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */
+ groupEnd(): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */
+ info(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */
+ log(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */
+ table(tabularData?: any, properties?: string[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */
+ time(label?: string): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */
+ timeEnd(label?: string): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */
+ timeLog(label?: string, ...data: any[]): void;
+ timeStamp(label?: string): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */
+ trace(...data: any[]): void;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */
+ warn(...data: any[]): void;
+}
+declare const console: Console;
+type BufferSource = ArrayBufferView | ArrayBuffer;
+type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string);
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string);
+ }
+ type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";
+ interface GlobalDescriptor {
+ value: ValueType;
+ mutable?: boolean;
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+ value: any;
+ valueOf(): any;
+ }
+ type ImportValue = ExportValue | number;
+ type ModuleImports = Record;
+ type Imports = Record;
+ type ExportValue = Function | Global | Memory | Table;
+ type Exports = Record;
+ class Instance {
+ constructor(module: Module, imports?: Imports);
+ readonly exports: Exports;
+ }
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ shared?: boolean;
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+ readonly buffer: ArrayBuffer;
+ grow(delta: number): number;
+ }
+ type ImportExportKind = "function" | "global" | "memory" | "table";
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind;
+ name: string;
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind;
+ module: string;
+ name: string;
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[];
+ static exports(module: Module): ModuleExportDescriptor[];
+ static imports(module: Module): ModuleImportDescriptor[];
+ }
+ type TableKind = "anyfunc" | "externref";
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any);
+ readonly length: number;
+ get(index: number): any;
+ grow(delta: number, value?: any): number;
+ set(index: number, value?: any): void;
+ }
+ function instantiate(module: Module, imports?: Imports): Promise;
+ function validate(bytes: BufferSource): boolean;
+}
+/**
+ * This ServiceWorker API interface represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException;
+ WorkerGlobalScope: typeof WorkerGlobalScope;
+ btoa(data: string): string;
+ atob(data: string): string;
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+ setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearTimeout(timeoutId: number | null): void;
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+ setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearInterval(timeoutId: number | null): void;
+ queueMicrotask(task: Function): void;
+ structuredClone(value: T, options?: StructuredSerializeOptions): T;
+ reportError(error: any): void;
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+ self: ServiceWorkerGlobalScope;
+ crypto: Crypto;
+ caches: CacheStorage;
+ scheduler: Scheduler;
+ performance: Performance;
+ Cloudflare: Cloudflare;
+ readonly origin: string;
+ Event: typeof Event;
+ ExtendableEvent: typeof ExtendableEvent;
+ CustomEvent: typeof CustomEvent;
+ PromiseRejectionEvent: typeof PromiseRejectionEvent;
+ FetchEvent: typeof FetchEvent;
+ TailEvent: typeof TailEvent;
+ TraceEvent: typeof TailEvent;
+ ScheduledEvent: typeof ScheduledEvent;
+ MessageEvent: typeof MessageEvent;
+ CloseEvent: typeof CloseEvent;
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;
+ ReadableStream: typeof ReadableStream;
+ WritableStream: typeof WritableStream;
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;
+ TransformStream: typeof TransformStream;
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
+ CountQueuingStrategy: typeof CountQueuingStrategy;
+ ErrorEvent: typeof ErrorEvent;
+ MessageChannel: typeof MessageChannel;
+ MessagePort: typeof MessagePort;
+ EventSource: typeof EventSource;
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
+ ReadableByteStreamController: typeof ReadableByteStreamController;
+ WritableStreamDefaultController: typeof WritableStreamDefaultController;
+ TransformStreamDefaultController: typeof TransformStreamDefaultController;
+ CompressionStream: typeof CompressionStream;
+ DecompressionStream: typeof DecompressionStream;
+ TextEncoderStream: typeof TextEncoderStream;
+ TextDecoderStream: typeof TextDecoderStream;
+ Headers: typeof Headers;
+ Body: typeof Body;
+ Request: typeof Request;
+ Response: typeof Response;
+ WebSocket: typeof WebSocket;
+ WebSocketPair: typeof WebSocketPair;
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;
+ AbortController: typeof AbortController;
+ AbortSignal: typeof AbortSignal;
+ TextDecoder: typeof TextDecoder;
+ TextEncoder: typeof TextEncoder;
+ navigator: Navigator;
+ Navigator: typeof Navigator;
+ URL: typeof URL;
+ URLSearchParams: typeof URLSearchParams;
+ URLPattern: typeof URLPattern;
+ Blob: typeof Blob;
+ File: typeof File;
+ FormData: typeof FormData;
+ Crypto: typeof Crypto;
+ SubtleCrypto: typeof SubtleCrypto;
+ CryptoKey: typeof CryptoKey;
+ CacheStorage: typeof CacheStorage;
+ Cache: typeof Cache;
+ FixedLengthStream: typeof FixedLengthStream;
+ IdentityTransformStream: typeof IdentityTransformStream;
+ HTMLRewriter: typeof HTMLRewriter;
+}
+declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void;
+declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void;
+/**
+ * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(value: T, options?: StructuredSerializeOptions): T;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+declare const self: ServiceWorkerGlobalScope;
+/**
+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+* The Workers runtime implements the full surface of this API, but with some differences in
+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+* compared to those implemented in most browsers.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+*/
+declare const crypto: Crypto;
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare const caches: CacheStorage;
+declare const scheduler: Scheduler;
+/**
+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+* as well as timing of subrequests and other operations.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+*/
+declare const performance: Performance;
+declare const Cloudflare: Cloudflare;
+declare const origin: string;
+declare const navigator: Navigator;
+interface TestController {
+}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void;
+ passThroughOnException(): void;
+ readonly props: Props;
+}
+type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise;
+type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise;
+type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise;
+interface ExportedHandler {
+ fetch?: ExportedHandlerFetchHandler;
+ tail?: ExportedHandlerTailHandler;
+ trace?: ExportedHandlerTraceHandler;
+ tailStream?: ExportedHandlerTailStreamHandler;
+ scheduled?: ExportedHandlerScheduledHandler;
+ test?: ExportedHandlerTestHandler;
+ email?: EmailExportedHandler;
+ queue?: ExportedHandlerQueueHandler;
+}
+interface StructuredSerializeOptions {
+ transfer?: any[];
+}
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) */
+declare abstract class PromiseRejectionEvent extends Event {
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) */
+ readonly promise: Promise;
+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) */
+ readonly reason: any;
+}
+declare abstract class Navigator {
+ sendBeacon(url: string, body?: (ReadableStream | string | (ArrayBuffer | ArrayBufferView) | Blob | FormData | URLSearchParams | URLSearchParams)): boolean;
+ readonly userAgent: string;
+ readonly hardwareConcurrency: number;
+ readonly language: string;
+ readonly languages: string[];
+}
+/**
+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+* as well as timing of subrequests and other operations.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+*/
+interface Performance {
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */
+ readonly timeOrigin: number;
+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */
+ now(): number;
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean;
+ readonly retryCount: number;
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record;
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise;
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise;
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise;
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise;
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise;
+}
+type DurableObjectStub = Fetcher & {
+ readonly id: DurableObjectId;
+ readonly name?: string;
+};
+interface DurableObjectId {
+ toString(): string;
+ equals(other: DurableObjectId): boolean;
+ readonly name?: string;
+}
+declare abstract class DurableObjectNamespace {
+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;
+ idFromName(name: string): DurableObjectId;
+ idFromString(id: string): DurableObjectId;
+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace;
+}
+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high";
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction;
+}
+type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me";
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint;
+}
+interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {
+}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void;
+ readonly props: Props;
+ readonly id: DurableObjectId;
+ readonly storage: DurableObjectStorage;
+ container?: Container;
+ blockConcurrencyWhile(callback: () => Promise): Promise;
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
+ getWebSockets(tag?: string): WebSocket[];
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;
+ getHibernatableWebSocketEventTimeout(): number | null;
+ getTags(ws: WebSocket): string[];
+ abort(reason?: string): void;
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise