From 37787ff0dd4a294c7bb8aaba58fe81237355dd19 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 14 Aug 2026 12:30:15 -0400 Subject: [PATCH 01/25] Add migration for accounts and assessments tables --- .../api/src/database/migrations/2026-06-14.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 apps/api/src/database/migrations/2026-06-14.ts diff --git a/apps/api/src/database/migrations/2026-06-14.ts b/apps/api/src/database/migrations/2026-06-14.ts new file mode 100644 index 0000000..6a9d63f --- /dev/null +++ b/apps/api/src/database/migrations/2026-06-14.ts @@ -0,0 +1,41 @@ +import { Kysely, sql } from 'kysely' +import { addDefaultColumns } from '../db'; + +export async function up(db: Kysely): Promise { + // TABLE: ACCOUNTS + await db.schema.createTable("accounts") + .addColumn("id", "uuid", (cb) => cb.primaryKey().defaultTo(sql`gen_random_uuid()`)) + .addColumn("created_at", "timestamptz", (cb) => cb + .notNull() + .defaultTo(sql`now()`) + ) + .addColumn("discord_id", "text", (cb) => cb.unique()) + .addColumn("email", "text", (cb) => cb.unique()) + .addColumn("name", "text", (cb) => cb.notNull().defaultTo("")) + .addColumn("gender", "text", (cb) => cb.notNull()) + .execute(); + + // TABLE: ASSESSMENTS + await db.schema.createTable("assessments") + .$call(addDefaultColumns) + .addColumn("account_id", "uuid") + .addColumn("name", "text", (cb) => cb.notNull().defaultTo("")) + .addColumn("gender", "text", (cb) => cb.notNull()) + .addColumn("age", "numeric", (cb) => cb.notNull()) + .addColumn("weight", "numeric", (cb) => cb.notNull()) + .addForeignKeyConstraint( + "fk_assessments_account_id", + ["account_id"], + "accounts", + ["id"], + (cb) => cb.onDelete("cascade") + ) + .execute(); +} + +export async function down(db: Kysely): Promise { + // TABLE: ASSESSMENTS + await db.schema.dropTable("assessments").ifExists().execute() + // TABLE: ACCOUNTS + await db.schema.dropTable("accounts").ifExists().execute() +} From ae8841540618b52e7b680eb1b13f0a511410f42c Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 14 Aug 2026 13:04:31 -0400 Subject: [PATCH 02/25] Add migrate.sh script to easily run db migration/seed in production --- apps/api/README.md | 20 ++++++++++++++++++ migrate.sh | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100755 migrate.sh diff --git a/apps/api/README.md b/apps/api/README.md index b0f3ca5..fe4bf14 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -57,3 +57,23 @@ this directory: Deployed to Fly.io (`fly.toml`). After a domain changes, re-run `src/scripts/register-webhooks.ts` with production env vars to point Printful's and Webflow's webhooks at the new domain — see issue #3 for the full checklist. + +### Running migrations/seed against production + +The deployed image only contains the compiled binary (see `Dockerfile`) — no +source, no `bun_modules`, no migration files — so these can't be run from +`fly ssh console` on the API app itself. Instead, use `migrate.sh` at the repo +root, which tunnels to the Postgres app (`blade-and-brawn-db`, legacy/unmanaged +Fly Postgres) via `fly proxy`, fetches the production `DATABASE_URL` for you, +and runs the scripts against it: + +```bash +./migrate.sh migrate # db:migrate:latest (default if no argument given) +./migrate.sh seed # db:seed +./migrate.sh both # both, in order +``` + +Both underlying scripts prompt for a `y/N` confirmation before touching the +database, and `db:seed` is idempotent (skips seeding if the default rows +already exist). If the tunneled connection fails on TLS, legacy Postgres +sometimes needs `?sslmode=disable` appended — edit `migrate.sh` if so. diff --git a/migrate.sh b/migrate.sh new file mode 100755 index 0000000..2c8b089 --- /dev/null +++ b/migrate.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +PG_APP="blade-and-brawn-db" +API_APP="blade-and-brawn-api" +LOCAL_PORT=5433 + +proxy_pid="" +cleanup() { + if [[ -n "$proxy_pid" ]]; then + kill "$proxy_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "Fetching production DATABASE_URL from $API_APP..." >&2 +prod_url=$(fly ssh console -a "$API_APP" -C "printenv DATABASE_URL" 2>/dev/null | grep -m1 '^postgres') +if [[ -z "$prod_url" ]]; then + echo "Failed to fetch DATABASE_URL from $API_APP" >&2 + exit 1 +fi +tunneled_url=$(echo "$prod_url" | sed -E "s#@[^/]+/#@localhost:${LOCAL_PORT}/#") + +echo "Starting tunnel to $PG_APP on localhost:${LOCAL_PORT}..." >&2 +fly proxy "${LOCAL_PORT}:5432" -a "$PG_APP" & +proxy_pid=$! + +echo "Waiting for tunnel..." >&2 +for _ in $(seq 1 30); do + nc -z localhost "$LOCAL_PORT" 2>/dev/null && break + sleep 0.5 +done + +target="${1:-migrate}" + +case "$target" in + migrate) + DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest + ;; + seed) + DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed + ;; + both) + DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:migrate:latest + DATABASE_URL="$tunneled_url" bun run --filter @blade-and-brawn/api db:seed + ;; + *) + echo "Usage: $0 [migrate|seed|both]" >&2 + exit 1 + ;; +esac From b395a0f98c7443578cec543923d31c46484517f0 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 13:53:31 -0400 Subject: [PATCH 03/25] Bump dep versions --- apps/api/package.json | 6 +++--- apps/portal/package.json | 8 ++++---- bun.lock | 32 +++++++++++++++++--------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index d685343..eea57f6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,11 +27,11 @@ "@elysia/jwt": "^1.4.2", "@elysia/server-timing": "^1.4.1", "@elysiajs/cors": "^1.4.2", - "@types/pg": "^8.20.0", + "@types/pg": "^8.21.0", "elysia": "^1.4.29", - "kysely": "^0.29.4", + "kysely": "^0.29.5", "ml-levenberg-marquardt": "^5.1.0", - "pg": "^8.22.0", + "pg": "^8.23.0", "zipcodes-us": "^1.1.3" }, "devDependencies": { diff --git a/apps/portal/package.json b/apps/portal/package.json index a1ff7a3..ef87d10 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -7,9 +7,9 @@ "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/bun": "^1.3.14", "elysia": "^1.4.29", - "svelte": "^5.56.8", + "svelte": "^5.56.9", "svelte-adapter-bun": "^1.0.1", - "svelte-check": "^4.7.4", + "svelte-check": "^4.7.6", "vite": "^7.3.6" }, "scripts": { @@ -26,8 +26,8 @@ "@blade-and-brawn/domain": "workspace:*", "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", - "daisyui": "^5.7.9", - "jose": "^6.2.5", + "daisyui": "^5.7.17", + "jose": "^6.2.9", "tailwindcss": "^4.3.3" } } diff --git a/bun.lock b/bun.lock index 7f93c5c..8c4c40e 100644 --- a/bun.lock +++ b/bun.lock @@ -22,11 +22,11 @@ "@elysia/jwt": "^1.4.2", "@elysia/server-timing": "^1.4.1", "@elysiajs/cors": "^1.4.2", - "@types/pg": "^8.20.0", + "@types/pg": "^8.21.0", "elysia": "^1.4.29", - "kysely": "^0.29.4", + "kysely": "^0.29.5", "ml-levenberg-marquardt": "^5.1.0", - "pg": "^8.22.0", + "pg": "^8.23.0", "zipcodes-us": "^1.1.3", }, "devDependencies": { @@ -42,8 +42,8 @@ "@blade-and-brawn/domain": "workspace:*", "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", - "daisyui": "^5.7.9", - "jose": "^6.2.5", + "daisyui": "^5.7.17", + "jose": "^6.2.9", "tailwindcss": "^4.3.3", }, "devDependencies": { @@ -52,9 +52,9 @@ "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/bun": "^1.3.14", "elysia": "^1.4.29", - "svelte": "^5.56.8", + "svelte": "^5.56.9", "svelte-adapter-bun": "^1.0.1", - "svelte-check": "^4.7.4", + "svelte-check": "^4.7.6", "vite": "^7.3.6", }, }, @@ -272,7 +272,7 @@ "@sveltejs/kit": ["@sveltejs/kit@2.70.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "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 || ^6.0.0", "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-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w=="], - "@sveltejs/load-config": ["@sveltejs/load-config@0.2.1", "", {}, "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA=="], + "@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="], "@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=="], @@ -322,7 +322,7 @@ "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], - "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + "@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], @@ -400,7 +400,7 @@ "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], - "daisyui": ["daisyui@5.7.9", "", {}, "sha512-oPL7yddYPQrMsDmYtxNqPGBYc+gqm14GTwu7cDxAaUq0bPg5ZcaeR/DVMgSwgo26InVn8AECbUBUCK0rMs96Kg=="], + "daisyui": ["daisyui@5.7.17", "", {}, "sha512-w5U5FdL9hH4/zh8eLXytWQt3c0DgHS1WDw+1yuQykT8nSIE9Vc5wkJVeDymD1JlRDLANDawvN4iIGVaxgV6Cjw=="], "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], @@ -470,7 +470,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.5", "", {}, "sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ=="], + "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -482,7 +482,7 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "kysely": ["kysely@0.29.4", "", {}, "sha512-y5mVgQNkMbs1eK9Xyc0pmNdabN2wHhRYY/5r4W5HrUT1rYCEPeVNSj1RUJeSDKT3U0p+mXCvLgkrFuIafYI6BA=="], + "kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="], "kysely-codegen": ["kysely-codegen@0.20.0", "", { "dependencies": { "chalk": "4.1.2", "cosmiconfig": "^9.0.0", "diff": "^8.0.3", "dotenv": "^17.2.4", "dotenv-expand": "^12.0.3", "micromatch": "^4.0.8", "minimist": "^1.2.8", "pluralize": "^8.0.0", "zod": "^4.3.6" }, "peerDependencies": { "@libsql/kysely-libsql": ">=0.3.0 <0.5.0", "@tediousjs/connection-string": "^1.0.0", "better-sqlite3": ">=7.6.2 <13.0.0", "kysely": ">=0.27.0 <1.0.0", "kysely-bun-sqlite": ">=0.3.2 <1.0.0", "kysely-bun-worker": ">=1.2.0 <2.0.0", "mysql2": ">=2.3.3 <4.0.0", "pg": ">=8.8.0 <9.0.0", "tarn": ">=3.0.0 <4.0.0", "tedious": ">=18.0.0 <20.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-sqlite", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-LSi2KBG7uDmNCZ+XurLSA9LH7XFyyoQ6xb5DLJSInPTSYLVApjOP2KwO8mSaREWTtoX+C2AG2GTlYR0DLjTbcA=="], @@ -552,7 +552,7 @@ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - "pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], + "pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="], "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], @@ -630,11 +630,11 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "svelte": ["svelte@5.56.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@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.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg=="], + "svelte": ["svelte@5.56.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@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.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA=="], "svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="], - "svelte-check": ["svelte-check@4.7.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.1", "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 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ=="], + "svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "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 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], @@ -692,6 +692,8 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "pg/pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], } } From 2d20f5e893613d8f78589f8c5bbd714a692e37d6 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 14:01:19 -0400 Subject: [PATCH 04/25] Switch to uuidv7 for accounts id --- apps/api/src/database/migrations/2026-06-14.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/database/migrations/2026-06-14.ts b/apps/api/src/database/migrations/2026-06-14.ts index 6a9d63f..ecd2304 100644 --- a/apps/api/src/database/migrations/2026-06-14.ts +++ b/apps/api/src/database/migrations/2026-06-14.ts @@ -4,7 +4,7 @@ import { addDefaultColumns } from '../db'; export async function up(db: Kysely): Promise { // TABLE: ACCOUNTS await db.schema.createTable("accounts") - .addColumn("id", "uuid", (cb) => cb.primaryKey().defaultTo(sql`gen_random_uuid()`)) + .addColumn("id", "uuid", (cb) => cb.primaryKey().defaultTo(sql`uuidv7()`)) .addColumn("created_at", "timestamptz", (cb) => cb .notNull() .defaultTo(sql`now()`) From e96c40c9c7728ccc93d2547b6ff169a60e49bfa1 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 14:53:36 -0400 Subject: [PATCH 05/25] Implement basic service and api for accounts --- .../api/src/database/migrations/2026-06-14.ts | 16 ++-- apps/api/src/server.ts | 27 ++++++- apps/api/src/services/accounts.ts | 73 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/services/accounts.ts diff --git a/apps/api/src/database/migrations/2026-06-14.ts b/apps/api/src/database/migrations/2026-06-14.ts index ecd2304..c93f2c6 100644 --- a/apps/api/src/database/migrations/2026-06-14.ts +++ b/apps/api/src/database/migrations/2026-06-14.ts @@ -11,18 +11,24 @@ export async function up(db: Kysely): Promise { ) .addColumn("discord_id", "text", (cb) => cb.unique()) .addColumn("email", "text", (cb) => cb.unique()) - .addColumn("name", "text", (cb) => cb.notNull().defaultTo("")) - .addColumn("gender", "text", (cb) => cb.notNull()) + .addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous")) + .addColumn("gender", "text") .execute(); // TABLE: ASSESSMENTS await db.schema.createTable("assessments") .$call(addDefaultColumns) .addColumn("account_id", "uuid") - .addColumn("name", "text", (cb) => cb.notNull().defaultTo("")) + .addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous")) .addColumn("gender", "text", (cb) => cb.notNull()) - .addColumn("age", "numeric", (cb) => cb.notNull()) - .addColumn("weight", "numeric", (cb) => cb.notNull()) + .addColumn("age", "integer", (cb) => cb.notNull()) + .addColumn("weight", "float8", (cb) => cb.notNull()) + .addColumn("perf_back_squat", "float8") // kg + .addColumn("perf_deadlift", "float8") // kg + .addColumn("perf_bench_press", "float8") // kg + .addColumn("perf_run", "float8") // ms + .addColumn("perf_broad_jump", "float8") // cm + .addColumn("perf_cone_drill", "float8") // ms .addForeignKeyConstraint( "fk_assessments_account_id", ["account_id"], diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 5813c59..d7c318a 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -20,10 +20,11 @@ import { CalculatorService, CalculatorUnavailableError } from "./services/calcul import { StandardsParamsSchema } from "@blade-and-brawn/calculator"; import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; +import { AccountsService } from "./services/accounts"; // CONSTANTS // ----------------------- -const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec +const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec const JWT_EXP = "1d"; // SERVICES @@ -32,8 +33,9 @@ const s = (() => { const Standards = new StandardsService(); const Calculator = new CalculatorService(DEFAULT_NAME); const Commerce = new CommerceService(); + const Accounts = new AccountsService(Calculator); const Events = new EventsService(); - return { Standards, Calculator, Commerce, Events }; + return { Standards, Calculator, Commerce, Accounts, Events }; })(); // QUEUES @@ -337,6 +339,27 @@ export const app = new Elysia() .get("/groups", async ({ }) => queues.map((q) => q.group)) ) + // ACCOUNTS + .group("/accounts", (app) => app + // Non-authenticated + .get("/:id/stats", async ({ params: { id } }) => { + const stats = await s.Accounts.stats(id); + if (!stats) throw new NotFoundError("Account stats not found"); + return stats; + }, { + params: t.Object({ id: t.String() }) + }) + // Authenticated + .guard({ auth: true }) + .get("/:id", async ({ params: { id } }) => { + const account = await s.Accounts.get(id); + if (!account) throw new NotFoundError("Account not found"); + return account; + }, { + params: t.Object({ id: t.String() }) + }) + ) + // WEBHOOKS .post("/webhooks/printful", async ({ body, query }) => { // https://webflow.com/integrations/printful diff --git a/apps/api/src/services/accounts.ts b/apps/api/src/services/accounts.ts new file mode 100644 index 0000000..8cb2c6e --- /dev/null +++ b/apps/api/src/services/accounts.ts @@ -0,0 +1,73 @@ +import { Activity, Gender, type Player } from "@blade-and-brawn/domain"; +import { db } from "../database/db"; +import type { CalculatorService } from "./calculator"; +import { Value } from "@sinclair/typebox/value"; +import { t } from "elysia"; + +export class AccountsService { + constructor(private Calculator: CalculatorService) { } + + async get(id: string) { + return await (db).selectFrom("accounts") + .selectAll() + .where("id", "=", id) + .executeTakeFirst(); + } + + async stats(id: string) { + const latestAssessment = await db.selectFrom("accounts") + .innerJoin("assessments", "assessments.account_id", "accounts.id") + .select([ + "assessments.age", "assessments.gender", "assessments.weight", "assessments.name", + "assessments.perf_back_squat", "assessments.perf_bench_press", "assessments.perf_broad_jump", + "assessments.perf_cone_drill", "assessments.perf_deadlift", "assessments.perf_run" + ]) + .where("accounts.id", "=", id) + .orderBy("assessments.created_at", "desc") + .limit(1) + .executeTakeFirst(); + if (!latestAssessment) return; + Value.Assert(t.Enum(Gender), latestAssessment.gender); + + return this.Calculator.calculate( + { + name: latestAssessment.name, + metrics: { + age: latestAssessment.age, + weight: latestAssessment.weight, + gender: latestAssessment.gender + } + }, + [ + // Strength + { + activity: Activity.BenchPress, + performance: latestAssessment.perf_bench_press ?? 0 + }, + { + activity: Activity.Deadlift, + performance: latestAssessment.perf_deadlift ?? 0 + }, + { + activity: Activity.BackSquat, + performance: latestAssessment.perf_back_squat ?? 0 + }, + // Power + { + activity: Activity.BroadJump, + performance: latestAssessment.perf_broad_jump ?? 0 + }, + // Endurance + { + activity: Activity.Run, + performance: latestAssessment.perf_run ?? 0 + }, + // Agility + { + activity: Activity.ConeDrill, + performance: latestAssessment.perf_cone_drill ?? 0 + }, + ] + ); + } +} From 88107ae5ace7d2a5124803fd3a14349e1874d8b1 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 15:03:06 -0400 Subject: [PATCH 06/25] Add default account to seed script --- apps/api/src/database/seed.ts | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/api/src/database/seed.ts b/apps/api/src/database/seed.ts index cf0e6d6..95d9561 100644 --- a/apps/api/src/database/seed.ts +++ b/apps/api/src/database/seed.ts @@ -1,6 +1,7 @@ import { StandardsConfigSchema, } from "@blade-and-brawn/calculator"; +import { Gender } from "@blade-and-brawn/domain"; import { Value } from "@sinclair/typebox/value"; import { db } from "./db"; import { DEFAULT_NAME, env, log } from "../util"; @@ -68,6 +69,44 @@ async function seedCalculator(standardsConfigId: string): Promise { log.info({ id: result.id }, "seeded default calculator"); } +async function seedAccounts(): Promise { + const existing = await db.selectFrom("accounts") + .select(["id"]) + .where("name", "=", DEFAULT_NAME) + .executeTakeFirst(); + if (existing) { + log.info({ id: existing.id }, "default account already seeded, skipping"); + return; + } + + const account = await db.insertInto("accounts") + .values({ + name: DEFAULT_NAME, + discord_id: "1521640671721685157", + email: "outgrow_document040@simplelogin.com" + }) + .returning("id") + .executeTakeFirstOrThrow(); + log.info({ id: account.id }, "seeded default account"); + + await db.insertInto("assessments") + .values({ + account_id: account.id, + name: DEFAULT_NAME, + gender: Gender.Male, + age: 25, + weight: 80, + perf_back_squat: 72.57472, + perf_deadlift: 127.00576, + perf_bench_press: 102.0582, + perf_run: 446000, + perf_broad_jump: 185.42000000000002, + perf_cone_drill: 9870, + }) + .execute(); + log.info({ accountId: account.id }, "seeded placeholder assessment"); +} + (async () => { const answer = prompt(`Seed the database (${env.DATABASE_URL}) with default standards data? (y/N)`); if (answer?.trim().toLowerCase() !== "y") { @@ -80,6 +119,7 @@ async function seedCalculator(standardsConfigId: string): Promise { const datasetId = await seedStandardsDataset(); const standardsConfigId = await seedStandardsConfig(datasetId); await seedCalculator(standardsConfigId); + await seedAccounts(); log.info("seed finished"); } catch (err) { From 1b886e47445350a716548fb2fa277cec742e822c Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 16:25:36 -0400 Subject: [PATCH 07/25] Initialize apps/bot and other cleanup --- apps/bot/.gitignore | 0 apps/bot/CLAUDE.md | 106 ++++++++++++++++++++ apps/bot/README.md | 15 +++ apps/bot/package.json | 14 +++ apps/bot/src/index.ts | 30 ++++++ apps/bot/src/services/cmd/registry/stats.ts | 10 ++ apps/bot/src/services/cmd/service.ts | 33 ++++++ apps/bot/tsconfig.json | 3 + apps/portal/package.json | 1 - bun.lock | 61 ++++++++++- package.json | 1 + packages/calculator/package.json | 2 +- packages/commerce/package.json | 2 +- packages/domain/package.json | 2 +- tsconfig.base.json | 12 ++- 15 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 apps/bot/.gitignore create mode 100644 apps/bot/CLAUDE.md create mode 100644 apps/bot/README.md create mode 100644 apps/bot/package.json create mode 100644 apps/bot/src/index.ts create mode 100644 apps/bot/src/services/cmd/registry/stats.ts create mode 100644 apps/bot/src/services/cmd/service.ts create mode 100644 apps/bot/tsconfig.json diff --git a/apps/bot/.gitignore b/apps/bot/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/apps/bot/CLAUDE.md b/apps/bot/CLAUDE.md new file mode 100644 index 0000000..764c1dd --- /dev/null +++ b/apps/bot/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/apps/bot/README.md b/apps/bot/README.md new file mode 100644 index 0000000..c028a6e --- /dev/null +++ b/apps/bot/README.md @@ -0,0 +1,15 @@ +# bot + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/apps/bot/package.json b/apps/bot/package.json new file mode 100644 index 0000000..d37ff3d --- /dev/null +++ b/apps/bot/package.json @@ -0,0 +1,14 @@ +{ + "name": "@blade-and-brawn/bot", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": {}, + "peerDependencies": {}, + "dependencies": { + "discord.js": "^14.27.0" + }, + "scripts": { + "dev": "bun run src/index.ts" + } +} diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts new file mode 100644 index 0000000..2f5c055 --- /dev/null +++ b/apps/bot/src/index.ts @@ -0,0 +1,30 @@ +import { Client, Events, GatewayIntentBits } from 'discord.js'; +import { CommandService } from './services/cmd/service'; + +const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); + +// SERVICES +// ----------------------- +const s = (() => { + const Commands = new CommandService(client); + return { Commands }; +})(); + +client.once(Events.ClientReady, async (readyClient) => { + // Initialize + await s.Commands.init(); + + console.log(`Ready! Logged in as ${readyClient.user.tag}`); +}); + +client.on("guildMemberAdd", async (guildMember) => { +}); + +client.on("messageCreate", async (message) => { + if (message.author.bot) return; + + const command = s.Commands.parse(message); + if (command) command.execute(message); +}); + +client.login(Bun.env.BOT_TOKEN); diff --git a/apps/bot/src/services/cmd/registry/stats.ts b/apps/bot/src/services/cmd/registry/stats.ts new file mode 100644 index 0000000..22afdb8 --- /dev/null +++ b/apps/bot/src/services/cmd/registry/stats.ts @@ -0,0 +1,10 @@ +import type { Message } from "discord.js"; +import type { Command } from "../service"; + +export default { + name: "stats", + description: "View your fitness statistics!", + execute: async (message: Message) => { + console.log("Stats executed"); + } +} as Command; diff --git a/apps/bot/src/services/cmd/service.ts b/apps/bot/src/services/cmd/service.ts new file mode 100644 index 0000000..4b30d16 --- /dev/null +++ b/apps/bot/src/services/cmd/service.ts @@ -0,0 +1,33 @@ +import type { Client, Message } from "discord.js"; +import { readdir } from "fs/promises"; + +const CMD_PREFIX = "."; + +export interface Command { + name: string, + description: string, + execute: (message: Message) => void | Promise +} + +export class CommandService { + private client: Client + private registry: Record = {} + + constructor(client: Client) { + this.client = client; + } + + async init() { + const commands = await Promise.all( + (await readdir(`${import.meta.dir}/registry`)).map(async file => (await import(`./registry/${file}`)).default as Command) + ); + for (const command of commands) + this.registry[command.name] = command; + }; + + parse(message: Message): Command | undefined { + if (!message.content.startsWith(CMD_PREFIX)) return; + const name = message.content.slice(CMD_PREFIX.length).split(/\s+/)[0]; + return this.registry[name ?? ""]; + } +} diff --git a/apps/bot/tsconfig.json b/apps/bot/tsconfig.json new file mode 100644 index 0000000..02ec15a --- /dev/null +++ b/apps/bot/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base.json" +} diff --git a/apps/portal/package.json b/apps/portal/package.json index ef87d10..1cb954c 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -5,7 +5,6 @@ "@blade-and-brawn/api": "workspace:*", "@sveltejs/kit": "^2.70.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@types/bun": "^1.3.14", "elysia": "^1.4.29", "svelte": "^5.56.9", "svelte-adapter-bun": "^1.0.1", diff --git a/bun.lock b/bun.lock index 8c4c40e..bf3c452 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,15 @@ "pino-pretty": "^13.1.3", }, }, + "apps/bot": { + "name": "@blade-and-brawn/bot", + "dependencies": { + "discord.js": "^14.27.0", + }, + "devDependencies": { + "@types/bun": "^1.3.14", + }, + }, "apps/portal": { "name": "@blade-and-brawn/portal", "dependencies": { @@ -60,7 +69,7 @@ }, "packages/calculator": { "name": "@blade-and-brawn/calculator", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@blade-and-brawn/domain": "workspace:*", "ml-levenberg-marquardt": "^5.0.1", @@ -68,14 +77,14 @@ }, "packages/commerce": { "name": "@blade-and-brawn/commerce", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@blade-and-brawn/domain": "workspace:*", }, }, "packages/domain": { "name": "@blade-and-brawn/domain", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@blade-and-brawn/domain": "workspace:*", }, @@ -88,6 +97,8 @@ "@blade-and-brawn/api": ["@blade-and-brawn/api@workspace:apps/api"], + "@blade-and-brawn/bot": ["@blade-and-brawn/bot@workspace:apps/bot"], + "@blade-and-brawn/calculator": ["@blade-and-brawn/calculator@workspace:packages/calculator"], "@blade-and-brawn/commerce": ["@blade-and-brawn/commerce@workspace:packages/commerce"], @@ -98,6 +109,18 @@ "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + "@elysia/eden": ["@elysia/eden@1.4.10", "", { "peerDependencies": { "elysia": ">=1.4.19" } }, "sha512-vcZXQcW6wZj6rhTxaiTkuCbVuS/yAJQ9jqCM6b83a5hu99F5Aj3HOyWXL17iod2Rz+pJpYsQGX0XaeoCsMpw2g=="], "@elysia/jwt": ["@elysia/jwt@1.4.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.27" } }, "sha512-KhLe6XxI+NHoYZiHqPhWg8SU0v+pegsxXsZSa/CXic/BrjlFbO26uTERtOQNlnByp/IGUB4qpLF6kQhvfYvGyg=="], @@ -264,6 +287,12 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + "@sinclair/typebox": ["@sinclair/typebox@0.34.52", "", {}, "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -326,6 +355,8 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -366,6 +397,8 @@ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -414,6 +447,10 @@ "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "discord-api-types": ["discord-api-types@0.38.53", "", {}, "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A=="], + + "discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="], @@ -440,6 +477,8 @@ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -514,6 +553,12 @@ "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="], + "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=="], @@ -650,12 +695,16 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.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-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], @@ -664,6 +713,8 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], @@ -672,6 +723,10 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@elysia/jwt/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], diff --git a/package.json b/package.json index 5f80980..273eded 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "blade-and-brawn", + "version": "1.1.0", "private": true, "workspaces": [ "apps/*", diff --git a/packages/calculator/package.json b/packages/calculator/package.json index 9050137..06dcfe1 100644 --- a/packages/calculator/package.json +++ b/packages/calculator/package.json @@ -1,7 +1,7 @@ { "name": "@blade-and-brawn/calculator", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "dependencies": { "ml-levenberg-marquardt": "^5.0.1", diff --git a/packages/commerce/package.json b/packages/commerce/package.json index 309f8c3..735ee09 100644 --- a/packages/commerce/package.json +++ b/packages/commerce/package.json @@ -1,7 +1,7 @@ { "name": "@blade-and-brawn/commerce", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "exports": { ".": "./src/index.ts" diff --git a/packages/domain/package.json b/packages/domain/package.json index b142b53..e199e25 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -1,7 +1,7 @@ { "name": "@blade-and-brawn/domain", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "dependencies": { "@blade-and-brawn/domain": "workspace:*" diff --git a/tsconfig.base.json b/tsconfig.base.json index 49a5104..cfcd348 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,19 +1,24 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ES2015", "ESNext", "DOM"], + "lib": [ + "ES2015", + "ESNext", + "DOM" + ], "target": "ESNext", "module": "Preserve", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, - + "types": [ + "bun" + ], // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, - // Best practices "strict": true, "skipLibCheck": true, @@ -21,7 +26,6 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "forceConsistentCasingInFileNames": true, - // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, From a4e70605384c4575f6c45e3609468c369f151a2d Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 16:37:35 -0400 Subject: [PATCH 08/25] Setup elysia eden in bot --- apps/bot/package.json | 5 ++++- apps/bot/src/index.ts | 4 +++- apps/bot/src/util.ts | 38 ++++++++++++++++++++++++++++++++++++++ bun.lock | 4 ++-- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 apps/bot/src/util.ts diff --git a/apps/bot/package.json b/apps/bot/package.json index d37ff3d..affa188 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -3,9 +3,12 @@ "module": "index.ts", "type": "module", "private": true, - "devDependencies": {}, + "devDependencies": { + "@blade-and-brawn/api": "workspace:*" + }, "peerDependencies": {}, "dependencies": { + "@elysia/eden": "^1.4.10", "discord.js": "^14.27.0" }, "scripts": { diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index 2f5c055..d258c63 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -1,7 +1,9 @@ import { Client, Events, GatewayIntentBits } from 'discord.js'; import { CommandService } from './services/cmd/service'; -const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] +}); // SERVICES // ----------------------- diff --git a/apps/bot/src/util.ts b/apps/bot/src/util.ts new file mode 100644 index 0000000..08ed4c9 --- /dev/null +++ b/apps/bot/src/util.ts @@ -0,0 +1,38 @@ +import cluster from "node:cluster"; +import { pino } from "pino"; +import { treaty } from '@elysia/eden'; +import { type API } from "@blade-and-brawn/api"; + +export const log = pino({ + level: Bun.env.LOG_LEVEL ?? "info", + transport: Bun.env.NODE_ENV != "production" + ? { target: "pino-pretty" } + : undefined, +}); + +export const env = { + PUBLIC_API_URL: requireEnv("PUBLIC_API_URL"), + BOT_TOKEN: requireEnv("BOT_TOKEN"), +}; + +export const api = treaty(env.PUBLIC_API_URL ?? ""); + +function requireEnv(key: string): string { + const val = Bun.env[key]; + if (!val) { + if (cluster.worker?.id === 1) + log.error({ name: key }, "Missing required environment variable"); + throw new Error(`Missing required environment variable: ${key}`) + }; + return val; +} + +function optionEnv(key: string, fallback: string = ""): string { + const val = Bun.env[key]; + if (!val) { + if (cluster.worker?.id === 1) + log.warn({ name: key }, `Missing optional environmental variable, falling back to "${fallback}"`) + return fallback + }; + return val; +} diff --git a/bun.lock b/bun.lock index bf3c452..03ccbb6 100644 --- a/bun.lock +++ b/bun.lock @@ -37,10 +37,11 @@ "apps/bot": { "name": "@blade-and-brawn/bot", "dependencies": { + "@elysia/eden": "^1.4.10", "discord.js": "^14.27.0", }, "devDependencies": { - "@types/bun": "^1.3.14", + "@blade-and-brawn/api": "workspace:*", }, }, "apps/portal": { @@ -59,7 +60,6 @@ "@blade-and-brawn/api": "workspace:*", "@sveltejs/kit": "^2.70.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@types/bun": "^1.3.14", "elysia": "^1.4.29", "svelte": "^5.56.9", "svelte-adapter-bun": "^1.0.1", From 4d7581620fddb11216f66b3318739345be2a8d20 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 16:59:50 -0400 Subject: [PATCH 09/25] Get stats command to console log the current users stats --- apps/api/src/database/seed.ts | 12 +++++++----- apps/api/src/services/accounts.ts | 10 ++++++++-- apps/bot/src/services/cmd/registry/stats.ts | 5 ++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/api/src/database/seed.ts b/apps/api/src/database/seed.ts index 95d9561..36a0652 100644 --- a/apps/api/src/database/seed.ts +++ b/apps/api/src/database/seed.ts @@ -70,9 +70,11 @@ async function seedCalculator(standardsConfigId: string): Promise { } async function seedAccounts(): Promise { + const ACCOUNT_NAME = "Xominus"; + const existing = await db.selectFrom("accounts") .select(["id"]) - .where("name", "=", DEFAULT_NAME) + .where("name", "=", ACCOUNT_NAME) .executeTakeFirst(); if (existing) { log.info({ id: existing.id }, "default account already seeded, skipping"); @@ -81,9 +83,9 @@ async function seedAccounts(): Promise { const account = await db.insertInto("accounts") .values({ - name: DEFAULT_NAME, - discord_id: "1521640671721685157", - email: "outgrow_document040@simplelogin.com" + name: ACCOUNT_NAME, + discord_id: "1325552430942916731", + email: "xominus@bladeandbrawn.com" }) .returning("id") .executeTakeFirstOrThrow(); @@ -92,7 +94,7 @@ async function seedAccounts(): Promise { await db.insertInto("assessments") .values({ account_id: account.id, - name: DEFAULT_NAME, + name: ACCOUNT_NAME, gender: Gender.Male, age: 25, weight: 80, diff --git a/apps/api/src/services/accounts.ts b/apps/api/src/services/accounts.ts index 8cb2c6e..b07747c 100644 --- a/apps/api/src/services/accounts.ts +++ b/apps/api/src/services/accounts.ts @@ -7,10 +7,16 @@ import { t } from "elysia"; export class AccountsService { constructor(private Calculator: CalculatorService) { } + private static idComparison(id: string): ["accounts.id" | "accounts.discord_id", "=", string] { + return id.startsWith("@") + ? ["accounts.discord_id", "=", id.slice(1)] + : ["accounts.id", "=", id]; + } + async get(id: string) { return await (db).selectFrom("accounts") .selectAll() - .where("id", "=", id) + .where(...AccountsService.idComparison(id)) .executeTakeFirst(); } @@ -22,7 +28,7 @@ export class AccountsService { "assessments.perf_back_squat", "assessments.perf_bench_press", "assessments.perf_broad_jump", "assessments.perf_cone_drill", "assessments.perf_deadlift", "assessments.perf_run" ]) - .where("accounts.id", "=", id) + .where(...AccountsService.idComparison(id)) .orderBy("assessments.created_at", "desc") .limit(1) .executeTakeFirst(); diff --git a/apps/bot/src/services/cmd/registry/stats.ts b/apps/bot/src/services/cmd/registry/stats.ts index 22afdb8..645c8f2 100644 --- a/apps/bot/src/services/cmd/registry/stats.ts +++ b/apps/bot/src/services/cmd/registry/stats.ts @@ -1,10 +1,13 @@ import type { Message } from "discord.js"; import type { Command } from "../service"; +import { api } from "../../../util"; export default { name: "stats", description: "View your fitness statistics!", execute: async (message: Message) => { - console.log("Stats executed"); + const res = await api.accounts({ id: `@${message.author.id}` }).stats.get(); + const stats = res.data; + console.log(stats); } } as Command; From d4a450ab3ebda10b3672968895766e58e83b3f8e Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 15 Aug 2026 17:04:29 -0400 Subject: [PATCH 10/25] Improve embed response for stats command --- apps/bot/src/services/cmd/registry/stats.ts | 34 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/bot/src/services/cmd/registry/stats.ts b/apps/bot/src/services/cmd/registry/stats.ts index 645c8f2..fb9df17 100644 --- a/apps/bot/src/services/cmd/registry/stats.ts +++ b/apps/bot/src/services/cmd/registry/stats.ts @@ -1,13 +1,41 @@ -import type { Message } from "discord.js"; +import { EmbedBuilder, type Message } from "discord.js"; import type { Command } from "../service"; import { api } from "../../../util"; +const ATTRIBUTE_EMOJI: Record = { + Strength: "💪", + Power: "⚡", + Endurance: "🏃", + Agility: "🤸", +}; + export default { name: "stats", description: "View your fitness statistics!", execute: async (message: Message) => { const res = await api.accounts({ id: `@${message.author.id}` }).stats.get(); - const stats = res.data; - console.log(stats); + if (!res.data) { + await message.reply("No stats found yet — submit an assessment first!"); + return; + } + + const { player, attributes } = res.data; + + const embed = new EmbedBuilder() + .setColor(0xEEE8AA) + .setTitle(`${message.author.username}'s Stats`) + .setThumbnail(message.author.displayAvatarURL()) + .setDescription(`**Overall Level: ${player}**`) + .addFields( + Object.entries(attributes).map(([attribute, level]) => ({ + name: `${ATTRIBUTE_EMOJI[attribute] ?? ""} ${attribute}`, + value: `Level ${level}`, + inline: true, + })) + ) + .setFooter({ text: "Blade & Brawn" }) + .setTimestamp(); + + await message.reply({ embeds: [embed] }); } } as Command; From 8db1db2ec696597981c367002cb9e2fd3ea6da97 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sun, 16 Aug 2026 00:00:50 -0400 Subject: [PATCH 11/25] Setup account creation through discord OAuth2 --- apps/api/src/server.ts | 90 ++++++++++++++++++++++++++++++- apps/api/src/services/accounts.ts | 12 ++++- apps/api/src/util.ts | 3 ++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index d7c318a..ee94dda 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -3,7 +3,7 @@ import { PlayerSchema, } from "@blade-and-brawn/domain" import { cors } from "@elysiajs/cors"; -import { Elysia, NotFoundError, status, t } from "elysia"; +import { Elysia, NotFoundError, redirect, status, t } from "elysia"; import { PrintfulError, WebflowError, @@ -21,6 +21,7 @@ import { StandardsParamsSchema } from "@blade-and-brawn/calculator"; import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; import { AccountsService } from "./services/accounts"; +import { Value } from "@sinclair/typebox/value"; // CONSTANTS // ----------------------- @@ -76,7 +77,7 @@ export const app = new Elysia() ], }), ) - .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) + .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()), discordOAuthState: t.Optional(t.String()) }) }) .use(authPlugin) .error({ @@ -140,6 +141,91 @@ export const app = new Elysia() }); }, { body: t.Object({ password: t.String() }) }) + .group("/auth/discord", (app) => app + .get("/login", async ({ cookie: { discordOAuthState } }) => { + const state = crypto.randomUUID(); + + discordOAuthState.set({ + value: state, + path: "/", + maxAge: 60 * 10, // 10 min + sameSite: "lax", + httpOnly: true, + secure: env.NODE_ENV === "production" + }); + + const url = new URL("https://discord.com/api/oauth2/authorize"); + url.searchParams.set("client_id", env.BOT_CLIENT_ID); + url.searchParams.set("redirect_uri", env.BOT_REDIRECT_URL); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", "identify email"); + url.searchParams.set("state", state); + + return redirect(url.toString(), 302); + }) + .get("/callback", async ({ query, cookie: { discordOAuthState, auth }, jwt }) => { + if (query.error) throw status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error }); + + if (!query.state) throw status(400, "No Discord OAuth2 state query parameter provided"); + if (query.state !== discordOAuthState.value) throw status(400, "Invalid Discord OAuth state"); + + const tokenRes = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: env.BOT_CLIENT_ID, + client_secret: env.BOT_CLIENT_SECRET, + grant_type: "authorization_code", + code: query.code ?? "", + redirect_uri: env.BOT_REDIRECT_URL + }).toString() + }); + if (!tokenRes.ok) throw status(502, { error: await tokenRes.json() }); + + const tokenResPayload = await tokenRes.json(); + Value.Assert(t.Object({ access_token: t.String() }), tokenResPayload); + + const identityRes = await fetch("https://discord.com/api/users/@me", { + headers: { "Authorization": `Bearer ${tokenResPayload.access_token}` } + }); + if (!identityRes.ok) throw status(502, { error: await identityRes.json() }); + + const identityResPayload = await identityRes.json(); + Value.Assert(t.Object({ id: t.String(), email: t.Optional(t.String()), username: t.String() }), identityResPayload); + + // Create the account + const existingAccount = await s.Accounts.get(`@${identityResPayload.id}`); + if (!existingAccount) { + await s.Accounts.create( + identityResPayload.id, + identityResPayload.email, + identityResPayload.username + ); + } + + // auth.set({ + // value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }), + // path: "/", + // maxAge: 60 * 60 * 24 * 7, + // sameSite: "lax", + // httpOnly: true, + // secure: env.NODE_ENV === "production", + // domain: env.NODE_ENV === "production" ? + // ".bladeandbrawn.com" : + // undefined + // }); + }, { + query: t.Object({ + code: t.Optional(t.String()), + state: t.Optional(t.String()), + error: t.Optional(t.String()), + error_description: t.Optional(t.String()), + }) + }) + ) + // CALCULATOR .group("/calculator", (app) => app // Non-authenticated diff --git a/apps/api/src/services/accounts.ts b/apps/api/src/services/accounts.ts index b07747c..c04c4e9 100644 --- a/apps/api/src/services/accounts.ts +++ b/apps/api/src/services/accounts.ts @@ -13,9 +13,19 @@ export class AccountsService { : ["accounts.id", "=", id]; } + async create(discordId: string, email?: string, name?: string) { + await db.insertInto("accounts") + .values({ + discord_id: discordId, + name: name ?? "", + email: email ?? null + }) + .execute(); + } + async get(id: string) { return await (db).selectFrom("accounts") - .selectAll() + .select(["discord_id", "name", "email", "gender"]) .where(...AccountsService.idComparison(id)) .executeTakeFirst(); } diff --git a/apps/api/src/util.ts b/apps/api/src/util.ts index 4e50f7e..f3d97fc 100644 --- a/apps/api/src/util.ts +++ b/apps/api/src/util.ts @@ -26,6 +26,9 @@ export const env = { MAX_WORKER_COUNT: requireEnv("MAX_WORKER_COUNT"), NODE_ENV: optionEnv("NODE_ENV", "development"), LOG_LEVEL: optionEnv("LOG_LEVEL", "info"), + BOT_CLIENT_ID: requireEnv("BOT_CLIENT_ID"), + BOT_CLIENT_SECRET: requireEnv("BOT_CLIENT_SECRET"), + BOT_REDIRECT_URL: requireEnv("BOT_REDIRECT_URL"), }; export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT); From 17af2f3d1f4e9876260aca0ebf0d90d2e6da7b2d Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sun, 16 Aug 2026 01:51:12 -0400 Subject: [PATCH 12/25] Implement full discord OAuth api flow --- apps/api/src/scripts/register-webhooks.ts | 2 +- apps/api/src/server.ts | 93 +++++++++++++++-------- apps/api/src/services/accounts.ts | 7 +- apps/api/src/util.ts | 1 + apps/portal/src/routes/login/+page.svelte | 8 +- 5 files changed, 70 insertions(+), 41 deletions(-) diff --git a/apps/api/src/scripts/register-webhooks.ts b/apps/api/src/scripts/register-webhooks.ts index 675bb75..93edbad 100644 --- a/apps/api/src/scripts/register-webhooks.ts +++ b/apps/api/src/scripts/register-webhooks.ts @@ -2,7 +2,7 @@ import { Printful, PrintfulClient, PrintfulError, Webflow, WebflowClient, Webflo import { env } from "../util"; const DOMAIN = env.NODE_ENV === "development" ? - "dev.api.bladeandbrawn.com" : + "api-dev.bladeandbrawn.com" : "api.bladeandbrawn.com"; const PRINTFUL_WEBHOOK_URL = env.NODE_ENV === "development" ? diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index ee94dda..f517461 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -27,6 +27,7 @@ import { Value } from "@sinclair/typebox/value"; // ----------------------- const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec const JWT_EXP = "1d"; +const JWT_EXP_SECONDS = 60 * 60 * 24; // keep in sync with JWT_EXP; used for cookie maxAge // SERVICES // ----------------------- @@ -52,12 +53,25 @@ const authPlugin = new Elysia({ name: "auth" }) .use(jwt({ name: "jwt", secret: env.AUTH_SECRET })) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) .macro({ - auth: { + authAdmin: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); - if (!token || !token.sessionId) throw status(401, "Unauthorized"); + if (!token || token.role !== "admin" || !token.sessionId) throw status(401, "Unauthorized"); return { sessionId: token.sessionId.toString() }; } + }, + authUser: { + async resolve({ jwt, cookie: { auth } }) { + const token = auth.value && await jwt.verify(auth.value); + if (!token || !token.sessionId || (token.role !== "user" && token.role !== "admin")) throw status(401, "Unauthorized"); + if (token.role === "user" && !token.accountId) throw status(401, "Unauthorized"); + + return { + sessionId: token.sessionId.toString(), + isAdmin: token.role === "admin", + accountId: token.role === "user" ? token.accountId!.toString() : null, + }; + } } }); @@ -72,12 +86,18 @@ export const app = new Elysia() /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i, // testing /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i, + /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn-test\.webflow\.io$/i, // development "http://localhost:5173", ], }), ) - .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()), discordOAuthState: t.Optional(t.String()) }) }) + .guard({ + cookie: t.Cookie({ + auth: t.Optional(t.String()), + authDiscord: t.Optional(t.String()) + }) + }) .use(authPlugin) .error({ @@ -124,14 +144,14 @@ export const app = new Elysia() .get("/health", () => ({ status: "ok" })) // AUTHENTICATION - .post("/auth/login", async ({ jwt, body, cookie: { auth } }) => { + .post("/auth/admin/login", async ({ jwt, body, cookie: { auth } }) => { const match = crypto.timingSafeEqual(sha256Sum(body.password), Buffer.from(env.ADMIN_PASSWORD, "hex")); if (!match) throw status(401, "Invalid credentials"); auth.set({ - value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }), + value: await jwt.sign({ role: "admin", sessionId: randomUUIDv7(), exp: JWT_EXP }), path: "/", - maxAge: 60 * 60 * 24 * 7, + maxAge: JWT_EXP_SECONDS, sameSite: "lax", httpOnly: true, secure: env.NODE_ENV === "production", @@ -142,10 +162,10 @@ export const app = new Elysia() }, { body: t.Object({ password: t.String() }) }) .group("/auth/discord", (app) => app - .get("/login", async ({ cookie: { discordOAuthState } }) => { + .get("/login", async ({ cookie: { authDiscord } }) => { const state = crypto.randomUUID(); - discordOAuthState.set({ + authDiscord.set({ value: state, path: "/", maxAge: 60 * 10, // 10 min @@ -163,11 +183,11 @@ export const app = new Elysia() return redirect(url.toString(), 302); }) - .get("/callback", async ({ query, cookie: { discordOAuthState, auth }, jwt }) => { + .get("/callback", async ({ query, cookie: { authDiscord, auth }, jwt }) => { if (query.error) throw status(400, { error: query.error_description ? `${query.error}: ${query.error_description}` : query.error }); if (!query.state) throw status(400, "No Discord OAuth2 state query parameter provided"); - if (query.state !== discordOAuthState.value) throw status(400, "Invalid Discord OAuth state"); + if (query.state !== authDiscord.value) throw status(400, "Invalid Discord OAuth state"); const tokenRes = await fetch("https://discord.com/api/oauth2/token", { method: "POST", @@ -182,7 +202,10 @@ export const app = new Elysia() redirect_uri: env.BOT_REDIRECT_URL }).toString() }); - if (!tokenRes.ok) throw status(502, { error: await tokenRes.json() }); + if (!tokenRes.ok) { + const errorBody = await tokenRes.json().catch(() => null); + throw status(502, { error: errorBody ?? "Discord token exchange failed" }); + } const tokenResPayload = await tokenRes.json(); Value.Assert(t.Object({ access_token: t.String() }), tokenResPayload); @@ -190,32 +213,38 @@ export const app = new Elysia() const identityRes = await fetch("https://discord.com/api/users/@me", { headers: { "Authorization": `Bearer ${tokenResPayload.access_token}` } }); - if (!identityRes.ok) throw status(502, { error: await identityRes.json() }); + if (!identityRes.ok) { + const errorBody = await identityRes.json().catch(() => null); + throw status(502, { error: errorBody ?? "Failed to fetch Discord identity" }); + } const identityResPayload = await identityRes.json(); Value.Assert(t.Object({ id: t.String(), email: t.Optional(t.String()), username: t.String() }), identityResPayload); // Create the account - const existingAccount = await s.Accounts.get(`@${identityResPayload.id}`); - if (!existingAccount) { - await s.Accounts.create( + let accountId = (await s.Accounts.get(`@${identityResPayload.id}`))?.id; + if (!accountId) { + accountId = (await s.Accounts.create( identityResPayload.id, identityResPayload.email, identityResPayload.username - ); + )).id; } - // auth.set({ - // value: await jwt.sign({ sessionId: randomUUIDv7(), exp: JWT_EXP }), - // path: "/", - // maxAge: 60 * 60 * 24 * 7, - // sameSite: "lax", - // httpOnly: true, - // secure: env.NODE_ENV === "production", - // domain: env.NODE_ENV === "production" ? - // ".bladeandbrawn.com" : - // undefined - // }); + // Authenticate + auth.set({ + value: await jwt.sign({ role: "user", sessionId: randomUUIDv7(), accountId, exp: JWT_EXP }), + path: "/", + maxAge: JWT_EXP_SECONDS, + sameSite: env.NODE_ENV === "production" ? "lax" : "none", + httpOnly: true, + secure: true, + domain: env.NODE_ENV === "production" ? + ".bladeandbrawn.com" : + undefined + }); + + return redirect(env.BOT_LOGIN_REDIRECT_URL, 302); }, { query: t.Object({ code: t.Optional(t.String()), @@ -238,7 +267,7 @@ export const app = new Elysia() }), }) // Authenticated - .guard({ auth: true }) + .guard({ authAdmin: true }) .get("/standards/config", async () => { return await s.Calculator.Standards.Config.get(); }) @@ -248,7 +277,7 @@ export const app = new Elysia() body: t.Object({ standardsConfigId: t.String() }) }) ) - .group("/standards", { auth: true }, (app) => app + .group("/standards", { authAdmin: true }, (app) => app .post("/configs", async ({ body: { name, datasetId, params } }) => { return await s.Standards.Configs.create(name, datasetId, params); }, { @@ -288,7 +317,7 @@ export const app = new Elysia() ) // COMMERCE - .group("/commerce", { auth: true }, (app) => app + .group("/commerce", { authAdmin: true }, (app) => app .group("/products", (app) => app .get("/", async ({ query }) => { const [pProducts, wProducts] = await Promise.all([ @@ -392,7 +421,7 @@ export const app = new Elysia() ) // EVENTS - .group("/events", { auth: true }, (app) => app + .group("/events", { authAdmin: true }, (app) => app .get("/", async ({ query }) => { const events = await s.Events.list({ filter: { status: query.status, type: query.type, group: query.group }, @@ -436,7 +465,7 @@ export const app = new Elysia() params: t.Object({ id: t.String() }) }) // Authenticated - .guard({ auth: true }) + .guard({ authAdmin: true }) .get("/:id", async ({ params: { id } }) => { const account = await s.Accounts.get(id); if (!account) throw new NotFoundError("Account not found"); diff --git a/apps/api/src/services/accounts.ts b/apps/api/src/services/accounts.ts index c04c4e9..fb6c30b 100644 --- a/apps/api/src/services/accounts.ts +++ b/apps/api/src/services/accounts.ts @@ -14,18 +14,19 @@ export class AccountsService { } async create(discordId: string, email?: string, name?: string) { - await db.insertInto("accounts") + return await db.insertInto("accounts") .values({ discord_id: discordId, name: name ?? "", email: email ?? null }) - .execute(); + .returning("id") + .executeTakeFirstOrThrow(); } async get(id: string) { return await (db).selectFrom("accounts") - .select(["discord_id", "name", "email", "gender"]) + .select(["id", "discord_id", "name", "email", "gender"]) .where(...AccountsService.idComparison(id)) .executeTakeFirst(); } diff --git a/apps/api/src/util.ts b/apps/api/src/util.ts index f3d97fc..bd100e8 100644 --- a/apps/api/src/util.ts +++ b/apps/api/src/util.ts @@ -29,6 +29,7 @@ export const env = { BOT_CLIENT_ID: requireEnv("BOT_CLIENT_ID"), BOT_CLIENT_SECRET: requireEnv("BOT_CLIENT_SECRET"), BOT_REDIRECT_URL: requireEnv("BOT_REDIRECT_URL"), + BOT_LOGIN_REDIRECT_URL: requireEnv("BOT_LOGIN_REDIRECT_URL"), }; export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT); diff --git a/apps/portal/src/routes/login/+page.svelte b/apps/portal/src/routes/login/+page.svelte index 8a0af1c..9892edf 100644 --- a/apps/portal/src/routes/login/+page.svelte +++ b/apps/portal/src/routes/login/+page.svelte @@ -14,7 +14,7 @@ loading = true; try { - const { status } = await api.auth.login.post({ password }); + const { status } = await api.auth.admin.login.post({ password }); if (status === 401) { error = "Invalid password"; @@ -22,11 +22,9 @@ } await goto("/"); - } - catch { + } catch { error = "Unable to reach the server. Please try again."; - } - finally { + } finally { loading = false; } } From 7173d6e4770491d1b27d8999d3827cffd43d1059 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sun, 16 Aug 2026 02:10:01 -0400 Subject: [PATCH 13/25] Improve accounts api and auth --- apps/api/src/server.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index f517461..38300c8 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -63,14 +63,8 @@ const authPlugin = new Elysia({ name: "auth" }) authUser: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); - if (!token || !token.sessionId || (token.role !== "user" && token.role !== "admin")) throw status(401, "Unauthorized"); - if (token.role === "user" && !token.accountId) throw status(401, "Unauthorized"); - - return { - sessionId: token.sessionId.toString(), - isAdmin: token.role === "admin", - accountId: token.role === "user" ? token.accountId!.toString() : null, - }; + if (!token || token.role !== "user" || !token.accountId || !token.sessionId) throw status(401, "Unauthorized"); + return { sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; } } }); @@ -456,7 +450,6 @@ export const app = new Elysia() // ACCOUNTS .group("/accounts", (app) => app - // Non-authenticated .get("/:id/stats", async ({ params: { id } }) => { const stats = await s.Accounts.stats(id); if (!stats) throw new NotFoundError("Account stats not found"); @@ -464,7 +457,12 @@ export const app = new Elysia() }, { params: t.Object({ id: t.String() }) }) - // Authenticated + .guard({ authUser: true }) + .get("/me/stats", async ({ accountId }) => { + const stats = await s.Accounts.stats(accountId); + if (!stats) throw new NotFoundError("Account stats not found"); + return stats; + }) .guard({ authAdmin: true }) .get("/:id", async ({ params: { id } }) => { const account = await s.Accounts.get(id); From a406bb07a89484cae50afe44ec074294dc2726e5 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sun, 16 Aug 2026 02:36:14 -0400 Subject: [PATCH 14/25] Small naming change --- apps/api/src/scripts/register-webhooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/scripts/register-webhooks.ts b/apps/api/src/scripts/register-webhooks.ts index 93edbad..dda5593 100644 --- a/apps/api/src/scripts/register-webhooks.ts +++ b/apps/api/src/scripts/register-webhooks.ts @@ -2,7 +2,7 @@ import { Printful, PrintfulClient, PrintfulError, Webflow, WebflowClient, Webflo import { env } from "../util"; const DOMAIN = env.NODE_ENV === "development" ? - "api-dev.bladeandbrawn.com" : + "dev-api.bladeandbrawn.com" : "api.bladeandbrawn.com"; const PRINTFUL_WEBHOOK_URL = env.NODE_ENV === "development" ? From c5f6c255635630a163fdaeb1805a8a586340090b Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sun, 16 Aug 2026 03:13:56 -0400 Subject: [PATCH 15/25] Auth guard fixes --- apps/api/src/server.ts | 50 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 38300c8..241c2d2 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -80,7 +80,6 @@ export const app = new Elysia() /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i, // testing /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i, - /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn-test\.webflow\.io$/i, // development "http://localhost:5173", ], @@ -261,15 +260,16 @@ export const app = new Elysia() }), }) // Authenticated - .guard({ authAdmin: true }) - .get("/standards/config", async () => { - return await s.Calculator.Standards.Config.get(); - }) - .post("/standards/config/switch", async ({ body: { standardsConfigId } }) => { - await s.Calculator.Standards.Config.switch(standardsConfigId); - }, { - body: t.Object({ standardsConfigId: t.String() }) - }) + .guard({ authAdmin: true }, (app) => app + .get("/standards/config", async () => { + return await s.Calculator.Standards.Config.get(); + }) + .post("/standards/config/switch", async ({ body: { standardsConfigId } }) => { + await s.Calculator.Standards.Config.switch(standardsConfigId); + }, { + body: t.Object({ standardsConfigId: t.String() }) + }) + ) ) .group("/standards", { authAdmin: true }, (app) => app .post("/configs", async ({ body: { name, datasetId, params } }) => { @@ -457,20 +457,22 @@ export const app = new Elysia() }, { params: t.Object({ id: t.String() }) }) - .guard({ authUser: true }) - .get("/me/stats", async ({ accountId }) => { - const stats = await s.Accounts.stats(accountId); - if (!stats) throw new NotFoundError("Account stats not found"); - return stats; - }) - .guard({ authAdmin: true }) - .get("/:id", async ({ params: { id } }) => { - const account = await s.Accounts.get(id); - if (!account) throw new NotFoundError("Account not found"); - return account; - }, { - params: t.Object({ id: t.String() }) - }) + .guard({ authUser: true }, (app) => app + .get("/me/stats", async ({ accountId }) => { + const stats = await s.Accounts.stats(accountId); + if (!stats) throw new NotFoundError("Account stats not found"); + return stats; + }) + ) + .guard({ authAdmin: true }, (app) => app + .get("/:id", async ({ params: { id } }) => { + const account = await s.Accounts.get(id); + if (!account) throw new NotFoundError("Account not found"); + return account; + }, { + params: t.Object({ id: t.String() }) + }) + ) ) // WEBHOOKS From 54326100908e85273feab6c736c4e22fd52b6fac Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 21 Aug 2026 09:34:33 -0400 Subject: [PATCH 16/25] Update deps --- bun.lock | 222 ++++++++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 106 insertions(+), 118 deletions(-) diff --git a/bun.lock b/bun.lock index 03ccbb6..f5af0c3 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "pino": "^10.3.1", }, "devDependencies": { - "@types/bun": "^1.3.14", + "@types/bun": "^1.4.0", "typescript": "^7.0.2", }, }, @@ -22,7 +22,7 @@ "@elysia/jwt": "^1.4.2", "@elysia/server-timing": "^1.4.1", "@elysiajs/cors": "^1.4.2", - "@types/pg": "^8.21.0", + "@types/pg": "^8.23.1", "elysia": "^1.4.29", "kysely": "^0.29.5", "ml-levenberg-marquardt": "^5.1.0", @@ -52,16 +52,16 @@ "@blade-and-brawn/domain": "workspace:*", "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", - "daisyui": "^5.7.17", + "daisyui": "^5.7.20", "jose": "^6.2.9", "tailwindcss": "^4.3.3", }, "devDependencies": { "@blade-and-brawn/api": "workspace:*", - "@sveltejs/kit": "^2.70.2", + "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^6.2.4", "elysia": "^1.4.29", - "svelte": "^5.56.9", + "svelte": "^5.56.10", "svelte-adapter-bun": "^1.0.1", "svelte-check": "^4.7.6", "vite": "^7.3.6", @@ -107,7 +107,7 @@ "@blade-and-brawn/portal": ["@blade-and-brawn/portal@workspace:apps/portal"], - "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], @@ -129,63 +129,57 @@ "@elysiajs/cors": ["@elysiajs/cors@1.4.2", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-FTCcbH35brTLigF1W7BYySRZomgI/dBEMK9BgK9RP9Nez7zmpGh4koL/Yr1BFv8nYz7CfhRvcM8d/c+XnwMaVQ=="], - "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], - "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], - "@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=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], "@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=="], @@ -197,95 +191,95 @@ "@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=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], - "@oxc-project/types": ["@oxc-project/types@0.138.0", "", {}, "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA=="], + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.4", "", { "os": "none", "cpu": "arm64" }, "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.4", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="], - "@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-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="], - "@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-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="], - "@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-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="], - "@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-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="], - "@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-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="], - "@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-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="], - "@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-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="], "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], @@ -297,9 +291,9 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="], + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="], - "@sveltejs/kit": ["@sveltejs/kit@2.70.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "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 || ^6.0.0", "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-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w=="], + "@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "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 || ^6.0.0", "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-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="], "@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="], @@ -341,17 +335,15 @@ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], - "@types/pg": ["@types/pg@8.21.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q=="], + "@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], @@ -399,7 +391,7 @@ "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], - "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -413,7 +405,7 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -433,7 +425,7 @@ "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], - "daisyui": ["daisyui@5.7.17", "", {}, "sha512-w5U5FdL9hH4/zh8eLXytWQt3c0DgHS1WDw+1yuQykT8nSIE9Vc5wkJVeDymD1JlRDLANDawvN4iIGVaxgV6Cjw=="], + "daisyui": ["daisyui@5.7.20", "", {}, "sha512-qoL9qXXo/K/MzcteD1SvZOSeBaL8F9qBJvwX3KEpiVHQLzIEtGkNl/ZznSI7J0d+qnQJa2dAAFzTFDAx9df1rw=="], "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], @@ -443,7 +435,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="], + "devalue": ["devalue@5.9.1", "", {}, "sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw=="], "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], @@ -459,21 +451,21 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "enhanced-resolve": ["enhanced-resolve@5.24.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "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=="], + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "esrap": ["esrap@2.2.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-On0QbLyaiAkVC4eXtgnXK9Kh2opit+3rcUSOc45DqJ2s/X2eXAHsGOKRSJ6IDagQEW5vPyivANfXUiqgXC67Rw=="], + "esrap": ["esrap@2.3.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-KciPkeZZX8srrh6xELzLR2cBaWOzgBQ4CwggO6Dh/KMlrfOcIcfyXCVy/Vvc3/OlS5gvFOd5tcxUYuaGM8o45A=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], - "fast-copy": ["fast-copy@4.0.3", "", {}, "sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw=="], + "fast-copy": ["fast-copy@4.0.4", "", {}, "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -483,7 +475,7 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "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=="], + "file-type": ["file-type@22.0.2", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -515,7 +507,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], @@ -575,7 +567,7 @@ "ml-levenberg-marquardt": ["ml-levenberg-marquardt@5.1.0", "", { "dependencies": { "is-any-array": "^3.0.0", "ml-matrix": "^6.14.0" } }, "sha512-yBYlUQV8+zCmz3CT7Qzt4o2o+UvOx9oxaG/HTgrMjcZRjRxVIAHm8mZM3L0GS2hLMUJpzSLilZdLb4Z5WkgdJw=="], - "ml-matrix": ["ml-matrix@6.14.0", "", { "dependencies": { "is-any-array": "^3.0.0", "ml-array-rescale": "^2.0.0" } }, "sha512-5W31+w+6jIm05l85N3Ik04fl+5LfN1lyJLBrEM/r/j7YmvwRclcUyQGXGFWXnN7ASzVjsAnAa3FUXrduAt168A=="], + "ml-matrix": ["ml-matrix@6.15.0", "", { "dependencies": { "is-any-array": "^3.0.0", "ml-array-rescale": "^2.0.0" } }, "sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -583,9 +575,9 @@ "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=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], @@ -607,7 +599,7 @@ "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], - "pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="], + "pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], @@ -615,7 +607,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], @@ -627,7 +619,7 @@ "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], - "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=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], @@ -637,7 +629,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], @@ -649,9 +641,9 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "rolldown": ["rolldown@1.1.4", "", { "dependencies": { "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.4", "@rolldown/binding-darwin-arm64": "1.1.4", "@rolldown/binding-darwin-x64": "1.1.4", "@rolldown/binding-freebsd-x64": "1.1.4", "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", "@rolldown/binding-linux-arm64-gnu": "1.1.4", "@rolldown/binding-linux-arm64-musl": "1.1.4", "@rolldown/binding-linux-ppc64-gnu": "1.1.4", "@rolldown/binding-linux-s390x-gnu": "1.1.4", "@rolldown/binding-linux-x64-gnu": "1.1.4", "@rolldown/binding-linux-x64-musl": "1.1.4", "@rolldown/binding-openharmony-arm64": "1.1.4", "@rolldown/binding-wasm32-wasi": "1.1.4", "@rolldown/binding-win32-arm64-msvc": "1.1.4", "@rolldown/binding-win32-x64-msvc": "1.1.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA=="], + "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], - "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=="], + "rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="], "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], @@ -659,7 +651,7 @@ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], "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=="], @@ -671,11 +663,11 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "svelte": ["svelte@5.56.9", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@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.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA=="], + "svelte": ["svelte@5.56.10", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@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.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g=="], "svelte-adapter-bun": ["svelte-adapter-bun@1.0.1", "", { "dependencies": { "rolldown": "^1.0.0-beta.38" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0", "typescript": "^5" } }, "sha512-tNOvfm8BGgG+rmEA7hkmqtq07v7zoo4skLQc+hIoQ79J+1fkEMpJEA2RzCIe3aPc8JdrsMJkv3mpiZPMsgahjA=="], @@ -687,7 +679,7 @@ "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -705,11 +697,11 @@ "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.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-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], - "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=="], + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -727,8 +719,6 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - "@elysia/jwt/jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -747,8 +737,6 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "pg/pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], - "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], } } diff --git a/package.json b/package.json index 273eded..b058f39 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:portal": "bun run --filter @blade-and-brawn/portal build" }, "devDependencies": { - "@types/bun": "^1.3.14", + "@types/bun": "^1.4.0", "typescript": "^7.0.2" }, "dependencies": { From 4dbcccdef73a76359b55aa023f58c918a79659f5 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 21 Aug 2026 10:10:22 -0400 Subject: [PATCH 17/25] Seed admin acccount and generalize login/auth routes --- apps/api/package.json | 2 +- .../api/src/database/migrations/2026-06-14.ts | 2 + apps/api/src/database/seed.ts | 38 +++++-------------- apps/api/src/server.ts | 35 +++++++++-------- apps/api/src/services/accounts.ts | 13 ++++++- apps/api/src/util.ts | 3 +- apps/portal/package.json | 6 +-- apps/portal/src/routes/login/+page.svelte | 6 ++- 8 files changed, 54 insertions(+), 51 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index eea57f6..592fd06 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -27,7 +27,7 @@ "@elysia/jwt": "^1.4.2", "@elysia/server-timing": "^1.4.1", "@elysiajs/cors": "^1.4.2", - "@types/pg": "^8.21.0", + "@types/pg": "^8.23.1", "elysia": "^1.4.29", "kysely": "^0.29.5", "ml-levenberg-marquardt": "^5.1.0", diff --git a/apps/api/src/database/migrations/2026-06-14.ts b/apps/api/src/database/migrations/2026-06-14.ts index c93f2c6..4e011e4 100644 --- a/apps/api/src/database/migrations/2026-06-14.ts +++ b/apps/api/src/database/migrations/2026-06-14.ts @@ -11,6 +11,8 @@ export async function up(db: Kysely): Promise { ) .addColumn("discord_id", "text", (cb) => cb.unique()) .addColumn("email", "text", (cb) => cb.unique()) + .addColumn("password_hash", "text") + .addColumn("role", "text", (cb) => cb.notNull().defaultTo("user")) .addColumn("name", "text", (cb) => cb.notNull().defaultTo("Anonymous")) .addColumn("gender", "text") .execute(); diff --git a/apps/api/src/database/seed.ts b/apps/api/src/database/seed.ts index 36a0652..2ff759c 100644 --- a/apps/api/src/database/seed.ts +++ b/apps/api/src/database/seed.ts @@ -1,11 +1,11 @@ import { StandardsConfigSchema, } from "@blade-and-brawn/calculator"; -import { Gender } from "@blade-and-brawn/domain"; import { Value } from "@sinclair/typebox/value"; import { db } from "./db"; import { DEFAULT_NAME, env, log } from "../util"; import standardsConfig from "./seed-data/standards-config.json" with {type: "json"}; +import type { AccountRole } from "../services/accounts"; Value.Assert(StandardsConfigSchema, standardsConfig); @@ -69,44 +69,26 @@ async function seedCalculator(standardsConfigId: string): Promise { log.info({ id: result.id }, "seeded default calculator"); } -async function seedAccounts(): Promise { - const ACCOUNT_NAME = "Xominus"; - +async function seedAdminAccount(): Promise { const existing = await db.selectFrom("accounts") .select(["id"]) - .where("name", "=", ACCOUNT_NAME) + .where("email", "=", env.PUBLIC_ADMIN_EMAIL) .executeTakeFirst(); if (existing) { - log.info({ id: existing.id }, "default account already seeded, skipping"); + log.info({ id: existing.id }, "admin account already seeded, skipping"); return; } const account = await db.insertInto("accounts") .values({ - name: ACCOUNT_NAME, - discord_id: "1325552430942916731", - email: "xominus@bladeandbrawn.com" + name: "Admin", + email: env.PUBLIC_ADMIN_EMAIL, + role: "admin" satisfies AccountRole, + password_hash: env.ADMIN_PASSWORD_HASH }) .returning("id") .executeTakeFirstOrThrow(); - log.info({ id: account.id }, "seeded default account"); - - await db.insertInto("assessments") - .values({ - account_id: account.id, - name: ACCOUNT_NAME, - gender: Gender.Male, - age: 25, - weight: 80, - perf_back_squat: 72.57472, - perf_deadlift: 127.00576, - perf_bench_press: 102.0582, - perf_run: 446000, - perf_broad_jump: 185.42000000000002, - perf_cone_drill: 9870, - }) - .execute(); - log.info({ accountId: account.id }, "seeded placeholder assessment"); + log.info({ id: account.id }, "seeded admin account"); } (async () => { @@ -121,7 +103,7 @@ async function seedAccounts(): Promise { const datasetId = await seedStandardsDataset(); const standardsConfigId = await seedStandardsConfig(datasetId); await seedCalculator(standardsConfigId); - await seedAccounts(); + await seedAdminAccount(); log.info("seed finished"); } catch (err) { diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 241c2d2..11b9e30 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -20,7 +20,7 @@ import { CalculatorService, CalculatorUnavailableError } from "./services/calcul import { StandardsParamsSchema } from "@blade-and-brawn/calculator"; import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; -import { AccountsService } from "./services/accounts"; +import { AccountsService, type AccountRole } from "./services/accounts"; import { Value } from "@sinclair/typebox/value"; // CONSTANTS @@ -56,15 +56,15 @@ const authPlugin = new Elysia({ name: "auth" }) authAdmin: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); - if (!token || token.role !== "admin" || !token.sessionId) throw status(401, "Unauthorized"); - return { sessionId: token.sessionId.toString() }; + if (!token || token.role !== "admin" || !token.accountId || !token.sessionId) throw status(401, "Unauthorized"); + return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; } }, - authUser: { + auth: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); - if (!token || token.role !== "user" || !token.accountId || !token.sessionId) throw status(401, "Unauthorized"); - return { sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; + if (!token || !token.role || !token.accountId || !token.sessionId) throw status(401, "Unauthorized"); + return { role: token.role.toString(), sessionId: token.sessionId.toString(), accountId: token.accountId.toString() }; } } }); @@ -137,22 +137,27 @@ export const app = new Elysia() .get("/health", () => ({ status: "ok" })) // AUTHENTICATION - .post("/auth/admin/login", async ({ jwt, body, cookie: { auth } }) => { - const match = crypto.timingSafeEqual(sha256Sum(body.password), Buffer.from(env.ADMIN_PASSWORD, "hex")); - if (!match) throw status(401, "Invalid credentials"); + .post("/auth/login", async ({ jwt, body: { password, email }, cookie: { auth } }) => { + const account = await s.Accounts.getByEmail(email); + const password_match = crypto.timingSafeEqual( + sha256Sum(password), + account?.password_hash ? Buffer.from(account?.password_hash, "hex") : sha256Sum("Dummy") + ); + + if (!account?.password_hash || !password_match) throw status(401, "Invalid credentials"); auth.set({ - value: await jwt.sign({ role: "admin", sessionId: randomUUIDv7(), exp: JWT_EXP }), + value: await jwt.sign({ role: account.role, sessionId: randomUUIDv7(), accountId: account.id, exp: JWT_EXP }), path: "/", maxAge: JWT_EXP_SECONDS, - sameSite: "lax", + sameSite: env.NODE_ENV === "production" ? "lax" : "none", httpOnly: true, - secure: env.NODE_ENV === "production", + secure: true, domain: env.NODE_ENV === "production" ? ".bladeandbrawn.com" : undefined }); - }, { body: t.Object({ password: t.String() }) }) + }, { body: t.Object({ email: t.String(), password: t.String() }) }) .group("/auth/discord", (app) => app .get("/login", async ({ cookie: { authDiscord } }) => { @@ -226,7 +231,7 @@ export const app = new Elysia() // Authenticate auth.set({ - value: await jwt.sign({ role: "user", sessionId: randomUUIDv7(), accountId, exp: JWT_EXP }), + value: await jwt.sign({ role: "user" satisfies AccountRole, sessionId: randomUUIDv7(), accountId, exp: JWT_EXP }), path: "/", maxAge: JWT_EXP_SECONDS, sameSite: env.NODE_ENV === "production" ? "lax" : "none", @@ -457,7 +462,7 @@ export const app = new Elysia() }, { params: t.Object({ id: t.String() }) }) - .guard({ authUser: true }, (app) => app + .guard({ auth: true }, (app) => app .get("/me/stats", async ({ accountId }) => { const stats = await s.Accounts.stats(accountId); if (!stats) throw new NotFoundError("Account stats not found"); diff --git a/apps/api/src/services/accounts.ts b/apps/api/src/services/accounts.ts index fb6c30b..dab83fd 100644 --- a/apps/api/src/services/accounts.ts +++ b/apps/api/src/services/accounts.ts @@ -1,9 +1,11 @@ -import { Activity, Gender, type Player } from "@blade-and-brawn/domain"; +import { Activity, Gender } from "@blade-and-brawn/domain"; import { db } from "../database/db"; import type { CalculatorService } from "./calculator"; import { Value } from "@sinclair/typebox/value"; import { t } from "elysia"; +export type AccountRole = "user" | "admin"; + export class AccountsService { constructor(private Calculator: CalculatorService) { } @@ -26,11 +28,18 @@ export class AccountsService { async get(id: string) { return await (db).selectFrom("accounts") - .select(["id", "discord_id", "name", "email", "gender"]) + .select(["id", "discord_id", "name", "email", "gender", "role"]) .where(...AccountsService.idComparison(id)) .executeTakeFirst(); } + async getByEmail(email: string) { + return await (db).selectFrom("accounts") + .select(["id", "discord_id", "name", "email", "gender", "password_hash", "role"]) + .where("email", "=", email) + .executeTakeFirst(); + } + async stats(id: string) { const latestAssessment = await db.selectFrom("accounts") .innerJoin("assessments", "assessments.account_id", "accounts.id") diff --git a/apps/api/src/util.ts b/apps/api/src/util.ts index bd100e8..7f7538d 100644 --- a/apps/api/src/util.ts +++ b/apps/api/src/util.ts @@ -20,7 +20,8 @@ export const env = { WEBFLOW_AUTH: requireEnv("WEBFLOW_AUTH"), WEBFLOW_WEBHOOK_SECRET: requireEnv("WEBFLOW_WEBHOOK_SECRET"), AUTH_SECRET: requireEnv("AUTH_SECRET"), - ADMIN_PASSWORD: requireEnv("ADMIN_PASSWORD"), + PUBLIC_ADMIN_EMAIL: requireEnv("PUBLIC_ADMIN_EMAIL"), + ADMIN_PASSWORD_HASH: requireEnv("ADMIN_PASSWORD_HASH"), DATABASE_URL: requireEnv("DATABASE_URL"), DATABASE_POOL_MAX: requireEnv("DATABASE_POOL_MAX"), MAX_WORKER_COUNT: requireEnv("MAX_WORKER_COUNT"), diff --git a/apps/portal/package.json b/apps/portal/package.json index 1cb954c..76daa92 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -3,10 +3,10 @@ "type": "module", "devDependencies": { "@blade-and-brawn/api": "workspace:*", - "@sveltejs/kit": "^2.70.2", + "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^6.2.4", "elysia": "^1.4.29", - "svelte": "^5.56.9", + "svelte": "^5.56.10", "svelte-adapter-bun": "^1.0.1", "svelte-check": "^4.7.6", "vite": "^7.3.6" @@ -25,7 +25,7 @@ "@blade-and-brawn/domain": "workspace:*", "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", - "daisyui": "^5.7.17", + "daisyui": "^5.7.20", "jose": "^6.2.9", "tailwindcss": "^4.3.3" } diff --git a/apps/portal/src/routes/login/+page.svelte b/apps/portal/src/routes/login/+page.svelte index 9892edf..c626fe9 100644 --- a/apps/portal/src/routes/login/+page.svelte +++ b/apps/portal/src/routes/login/+page.svelte @@ -2,6 +2,7 @@ import "../app.css"; import { goto } from "$app/navigation"; import { api } from "$lib/api"; + import { env } from "$env/dynamic/public"; let error = $state(""); let loading = $state(false); @@ -14,7 +15,10 @@ loading = true; try { - const { status } = await api.auth.admin.login.post({ password }); + const { status } = await api.auth.login.post({ + email: env.PUBLIC_ADMIN_EMAIL ?? "", + password, + }); if (status === 401) { error = "Invalid password"; From 7a4a8ee50b59ff5419bc2f2fd75ce4eab15e8e75 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 21 Aug 2026 10:27:55 -0400 Subject: [PATCH 18/25] Replace auth verification with bun password api --- apps/api/src/server.ts | 10 +++------- apps/api/src/util.ts | 1 + 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 11b9e30..aa6cd8a 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -10,7 +10,7 @@ import { Printful, Webflow, } from "@blade-and-brawn/commerce"; -import { DEFAULT_NAME, env, log, sha256Sum } from "./util"; +import { DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log, sha256Sum } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; import { CommerceService, WOrderStatusSchema } from "./services/commerce"; @@ -139,12 +139,8 @@ export const app = new Elysia() // AUTHENTICATION .post("/auth/login", async ({ jwt, body: { password, email }, cookie: { auth } }) => { const account = await s.Accounts.getByEmail(email); - const password_match = crypto.timingSafeEqual( - sha256Sum(password), - account?.password_hash ? Buffer.from(account?.password_hash, "hex") : sha256Sum("Dummy") - ); - - if (!account?.password_hash || !password_match) throw status(401, "Invalid credentials"); + const password_match = await Bun.password.verify(password, account?.password_hash ?? DUMMY_PASSWORD_HASH); + if (!account || !password_match) throw status(401, "Invalid credentials"); auth.set({ value: await jwt.sign({ role: account.role, sessionId: randomUUIDv7(), accountId: account.id, exp: JWT_EXP }), diff --git a/apps/api/src/util.ts b/apps/api/src/util.ts index 7f7538d..db66c0e 100644 --- a/apps/api/src/util.ts +++ b/apps/api/src/util.ts @@ -35,6 +35,7 @@ export const env = { export const WORKER_COUNT = Math.min(os.availableParallelism(), +env.MAX_WORKER_COUNT); export const DEFAULT_NAME = "Default"; +export const DUMMY_PASSWORD_HASH = await Bun.password.hash("Dummy"); function requireEnv(key: string): string { const val = Bun.env[key]; From dbcfb6900b81a16148868f56b1611c45c77e6851 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 22 Aug 2026 11:42:59 -0400 Subject: [PATCH 19/25] Implement assessment service and basic endpoints --- apps/api/src/server.ts | 36 ++++++++++++++++++++++-- apps/api/src/services/assessments.ts | 42 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/services/assessments.ts diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index aa6cd8a..cf291f5 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -10,7 +10,7 @@ import { Printful, Webflow, } from "@blade-and-brawn/commerce"; -import { DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log, sha256Sum } from "./util"; +import { DEFAULT_NAME, DUMMY_PASSWORD_HASH, env, log } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; import { CommerceService, WOrderStatusSchema } from "./services/commerce"; @@ -22,6 +22,7 @@ import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; import { AccountsService, type AccountRole } from "./services/accounts"; import { Value } from "@sinclair/typebox/value"; +import { AssessmentsService } from "./services/assessments"; // CONSTANTS // ----------------------- @@ -37,7 +38,8 @@ const s = (() => { const Commerce = new CommerceService(); const Accounts = new AccountsService(Calculator); const Events = new EventsService(); - return { Standards, Calculator, Commerce, Accounts, Events }; + const Assessments = new AssessmentsService(); + return { Standards, Calculator, Commerce, Accounts, Events, Assessments }; })(); // QUEUES @@ -476,6 +478,36 @@ export const app = new Elysia() ) ) + // ASSESSMENTS + .group("/assessments", (app) => app + .guard({ auth: true }, (app) => app + .post("/me", async ({ body: { player, activityPerformances }, accountId }) => { + await s.Assessments.create(player, activityPerformances, accountId); + }, { + body: t.Object({ + player: PlayerSchema, + activityPerformances: t.Array(ActivityPerformanceSchema) + }) + }) + .get("/me", async ({ accountId }) => { + const assessments = await s.Assessments.list({ filter: { accountId } }); + if (!assessments) throw new NotFoundError("Assessments not found"); + return assessments; + }) + ) + .guard({ authAdmin: true }, (app) => app + .post("/", async ({ body: { player, activityPerformances, id } }) => { + await s.Assessments.create(player, activityPerformances, id); + }, { + body: t.Object({ + player: PlayerSchema, + activityPerformances: t.Array(ActivityPerformanceSchema), + id: t.Optional(t.String()), + }) + }) + ) + ) + // WEBHOOKS .post("/webhooks/printful", async ({ body, query }) => { // https://webflow.com/integrations/printful diff --git a/apps/api/src/services/assessments.ts b/apps/api/src/services/assessments.ts new file mode 100644 index 0000000..db6b218 --- /dev/null +++ b/apps/api/src/services/assessments.ts @@ -0,0 +1,42 @@ +import { Activity, type ActivityPerformance, type Player } from "@blade-and-brawn/domain"; +import { db } from "../database/db"; + +export class AssessmentsService { + async create(player: Player, activityPerformances: ActivityPerformance[], accountId?: string) { + const performanceFor = (activity: Activity) => + activityPerformances.find((p) => p.activity === activity)?.performance ?? null; + + return await db.insertInto("assessments") + .values({ + account_id: accountId ?? null, + name: player.name ?? "Anonymous", + age: player.metrics.age, + weight: player.metrics.weight, + gender: player.metrics.gender, + perf_back_squat: performanceFor(Activity.BackSquat), + perf_deadlift: performanceFor(Activity.Deadlift), + perf_bench_press: performanceFor(Activity.BenchPress), + perf_broad_jump: performanceFor(Activity.BroadJump), + perf_run: performanceFor(Activity.Run), + perf_cone_drill: performanceFor(Activity.ConeDrill), + }) + .returning("id") + .executeTakeFirstOrThrow(); + } + + async list(opt: { + filter?: { accountId?: string }, + limit?: number, + offset?: number, + } = {}) { + return await db.selectFrom("assessments") + .selectAll() + .$if(opt.filter?.accountId !== undefined, (qb) => qb + .where("account_id", "=", opt.filter!.accountId!) + ) + .orderBy("created_at", "desc") + .$if(opt.limit !== undefined, (qb) => qb.limit(opt.limit!)) + .$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!)) + .execute(); + } +} From 01e9ac8403db66740ebbe534f3f171a0674530f9 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 22 Aug 2026 11:43:25 -0400 Subject: [PATCH 20/25] Update deps --- apps/portal/package.json | 2 +- bun.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 76daa92..f58b12e 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -26,7 +26,7 @@ "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", "daisyui": "^5.7.20", - "jose": "^6.2.9", + "jose": "^6.2.10", "tailwindcss": "^4.3.3" } } diff --git a/bun.lock b/bun.lock index f5af0c3..4cf86f6 100644 --- a/bun.lock +++ b/bun.lock @@ -53,7 +53,7 @@ "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.3", "daisyui": "^5.7.20", - "jose": "^6.2.9", + "jose": "^6.2.10", "tailwindcss": "^4.3.3", }, "devDependencies": { @@ -461,7 +461,7 @@ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "esrap": ["esrap@2.3.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-KciPkeZZX8srrh6xELzLR2cBaWOzgBQ4CwggO6Dh/KMlrfOcIcfyXCVy/Vvc3/OlS5gvFOd5tcxUYuaGM8o45A=="], + "esrap": ["esrap@2.3.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], @@ -501,7 +501,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], From 951f9286aa39d99d1bb19e9c2bb3ee41799c44a5 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 22 Aug 2026 12:00:36 -0400 Subject: [PATCH 21/25] Implement assessment delete service method and endpoint --- apps/api/src/server.ts | 12 ++++++++---- apps/api/src/services/assessments.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index cf291f5..a846fda 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -482,7 +482,7 @@ export const app = new Elysia() .group("/assessments", (app) => app .guard({ auth: true }, (app) => app .post("/me", async ({ body: { player, activityPerformances }, accountId }) => { - await s.Assessments.create(player, activityPerformances, accountId); + return await s.Assessments.create(player, activityPerformances, accountId); }, { body: t.Object({ player: PlayerSchema, @@ -490,9 +490,13 @@ export const app = new Elysia() }) }) .get("/me", async ({ accountId }) => { - const assessments = await s.Assessments.list({ filter: { accountId } }); - if (!assessments) throw new NotFoundError("Assessments not found"); - return assessments; + return await s.Assessments.list({ filter: { accountId } }); + }) + .delete("/me/:id", async ({ params: { id }, accountId }) => { + const deleted = await s.Assessments.delete(id, accountId); + if (!deleted) throw new NotFoundError("Assessment not found"); + }, { + params: t.Object({ id: t.String() }) }) ) .guard({ authAdmin: true }, (app) => app diff --git a/apps/api/src/services/assessments.ts b/apps/api/src/services/assessments.ts index db6b218..70277ba 100644 --- a/apps/api/src/services/assessments.ts +++ b/apps/api/src/services/assessments.ts @@ -39,4 +39,12 @@ export class AssessmentsService { .$if(opt.offset !== undefined, (qb) => qb.offset(opt.offset!)) .execute(); } + + async delete(id: string, accountId: string): Promise { + const result = await db.deleteFrom("assessments") + .where("id", "=", id) + .where("account_id", "=", accountId) + .executeTakeFirst(); + return result.numDeletedRows > 0n; + } } From e9908aaa9837804c5963fd584d8efc2788e04f33 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 22 Aug 2026 12:01:53 -0400 Subject: [PATCH 22/25] Persist players tab to assessments table --- .../src/lib/components/PlayersTable.svelte | 200 ++++++++++++++---- 1 file changed, 155 insertions(+), 45 deletions(-) diff --git a/apps/portal/src/lib/components/PlayersTable.svelte b/apps/portal/src/lib/components/PlayersTable.svelte index c3415e8..4109dd0 100644 --- a/apps/portal/src/lib/components/PlayersTable.svelte +++ b/apps/portal/src/lib/components/PlayersTable.svelte @@ -1,4 +1,5 @@ -
+
+ +
-
+{#if loadError} + +{/if} +
- {#each calculations as calculation, index} + {#each calculations as calculation, index (calculation.key)}
@@ -192,7 +279,7 @@
- {#each Object.values(Attribute) as attribute} + {#each Object.values(Attribute) as attribute (attribute)}
  • @@ -223,7 +310,7 @@ class="select select-bordered select-sm w-full" bind:value={calculation.player.metrics.gender} > - {#each Object.values(Gender) as gender} + {#each Object.values(Gender) as gender (gender)} {/each} @@ -247,7 +334,7 @@ class="select select-bordered select-sm w-full" bind:value={calculation.player.metrics.weight} > - {#each range(400) as weight} + {#each range(400) as weight (weight)} @@ -266,7 +353,7 @@ ))} >
    - {#each calculation.activityPerformances as activityPerformance} + {#each calculation.activityPerformances as activityPerformance (activityPerformance.activity)}
    + + {#if calculation.saveError} + + {/if} + {#if calculation.deleteError} + + {/if} +
    +
    + +
    -
    {/each}
  • From ce363d3b51850d64658f35078d7593a3d2590560 Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Sat, 22 Aug 2026 12:08:25 -0400 Subject: [PATCH 23/25] Improve unsaved state in players tab --- .../src/lib/components/PlayersTable.svelte | 91 +++++++++++++++---- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/apps/portal/src/lib/components/PlayersTable.svelte b/apps/portal/src/lib/components/PlayersTable.svelte index 4109dd0..911e641 100644 --- a/apps/portal/src/lib/components/PlayersTable.svelte +++ b/apps/portal/src/lib/components/PlayersTable.svelte @@ -1,5 +1,6 @@