diff --git a/.env.example b/.env.example index 17a2c88..d7e13ca 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,6 @@ BASE_URL=https://guide.sudloh.com # Public AdSense publisher client ID. Leave unset to disable ads locally. NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597 -# Local-only demo mode serves sanitized fixtures and bypasses external services. -# Keep this false in every deployed environment. -BUZZ_DEMO_MODE=false - # PostgreSQL DATABASE_URL=postgresql://buzz_sheet:replace-me@postgres.example.internal:5432/buzz_sheet?sslmode=require DATABASE_POOL_SIZE=10 @@ -27,6 +23,7 @@ REDIS_URL=rediss://default:replace-me@redis.example.internal:6379 REDIS_CACHE_PREFIX=buzz:next-cache REDIS_EVENT_PREFIX=buzz:events REDIS_PRESENCE_PREFIX=buzz:presence +REDIS_SECURITY_PREFIX=buzz:security REDIS_CACHE_MAX_ENTRY_BYTES=5242880 OUTBOX_BATCH_SIZE=20 OUTBOX_POLL_MS=1000 @@ -38,7 +35,7 @@ DISCORD_BOT_TOKEN=replace-with-discord-bot-token DISCORD_CHANNEL_ID=replace-with-private-channel-id DISCORD_LOG_CHANNEL_ID=1547193755772125184 -# S3-compatible private media storage +# S3-compatible media storage; direct guide uploads use public CDN objects. S3_ENDPOINT=https://s3.example.internal S3_PUBLIC_URL=https://buzz-cdn.astrxl.dev S3_REGION=auto @@ -55,3 +52,6 @@ NEXT_DEPLOYMENT_ID=local-development # Readiness dependency timeout. HEALTHCHECK_TIMEOUT_MS=2500 + +# Set only when Traefik overwrites this header and direct pod ingress is denied. +TRUSTED_CLIENT_IP_HEADER= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b4195b3..5985512 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,40 +2,56 @@ name: CI on: push: - branches: - - main + branches: [main] + schedule: + - cron: '17 3 * * 1' + +concurrency: + group: buzz-sheet-${{ gitea.ref }} + cancel-in-progress: false env: BASE_URL: https://guide.sudloh.com NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID: ca-pub-9687404323559597 REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet DEPLOY_NAMESPACE: buzz-sheet - KUBE_API_SERVER: https://100.112.189.33:6443/ jobs: verify: - name: Verify + name: Verify and audit runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 steps: - - name: Check out repository - uses: actions/checkout@v4 + - name: Check out repository and history + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + persist-credentials: false - name: Set up Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - name: Set up kubectl - uses: azure/setup-kubectl@v4 + uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4 + with: + version: v1.34.1 - name: Verify application and manifests run: | bun install --frozen-lockfile + bun run security:audit bun run test bun run typecheck bun run lint kubectl kustomize k8s/ >/dev/null + - name: Scan Git history for secrets + run: | + docker run --rm --cap-drop=ALL --security-opt=no-new-privileges \ + --volume "$PWD:/repo:ro" --workdir /repo \ + ghcr.io/gitleaks/gitleaks@sha256:691af3c7c5a48b16f187ce3446d5f194838f91238f27270ed36eef6359a574d9 \ + git --redact=100 --no-banner --log-opts=--all . build-and-deploy: - name: Build immutable images and deploy + name: Build, scan and deploy immutable images needs: verify if: >- gitea.event_name == 'push' && @@ -45,130 +61,81 @@ jobs: timeout-minutes: 45 steps: - name: Check out repository - uses: actions/checkout@v4 - - - name: Build and push application image + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Build and scan application and migration images + env: + REVISION: ${{ gitea.sha }} run: | - docker build \ - --target app \ + set -Eeuo pipefail + docker build --target app \ --build-arg BASE_URL="$BASE_URL" \ --build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID" \ - --build-arg NEXT_DEPLOYMENT_ID=${{ gitea.sha }} \ - --build-arg VCS_REF=${{ gitea.sha }} \ - --tag ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} \ - . - docker push ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} - - - name: Build and push migration image - run: | - docker build \ - --target migration \ - --build-arg VCS_REF=${{ gitea.sha }} \ - --tag ${{ env.REGISTRY_IMAGE }}:migrate-${{ gitea.sha }} \ - . - docker push ${{ env.REGISTRY_IMAGE }}:migrate-${{ gitea.sha }} - + --build-arg NEXT_DEPLOYMENT_ID="$REVISION" \ + --build-arg VCS_REF="$REVISION" \ + --tag "$REGISTRY_IMAGE:$REVISION" . + docker build --target migration --build-arg VCS_REF="$REVISION" \ + --tag "$REGISTRY_IMAGE:migrate-$REVISION" . + mkdir -p "$RUNNER_TEMP/buzz-trivy-cache" + for tag in "$REVISION" "migrate-$REVISION"; do + docker save --output "$RUNNER_TEMP/buzz-image.tar" "$REGISTRY_IMAGE:$tag" + docker run --rm --user "$(id -u):$(id -g)" --cap-drop=ALL --security-opt=no-new-privileges \ + --volume "$RUNNER_TEMP:/scan:ro" \ + --volume "$RUNNER_TEMP/buzz-trivy-cache:/cache" \ + aquasec/trivy@sha256:bcc376de8d77cfe086a917230e818dc9f8528e3c852f7b1aff648949b6258d1c \ + image --input /scan/buzz-image.tar --cache-dir /cache --scanners vuln --severity HIGH,CRITICAL --exit-code 1 + rm "$RUNNER_TEMP/buzz-image.tar" + done + docker push "$REGISTRY_IMAGE:$REVISION" | tee "$RUNNER_TEMP/buzz-app-push.log" + docker push "$REGISTRY_IMAGE:migrate-$REVISION" | tee "$RUNNER_TEMP/buzz-migration-push.log" - name: Set up kubectl - uses: azure/setup-kubectl@v4 - - - name: Configure kubectl - env: - KUBE_CONFIG_B64: ${{ secrets.KUBE_CONFIG_B64 }} - run: | - if [ -z "$KUBE_CONFIG_B64" ]; then - echo "Missing KUBE_CONFIG_B64 secret for the CI deploy identity" >&2 - exit 1 - fi - - kube_dir="$RUNNER_TEMP/buzz-sheet-kube" - mkdir -p "$kube_dir" - chmod 700 "$kube_dir" - export KUBECONFIG="$kube_dir/config" - printf '%s' "$KUBE_CONFIG_B64" | base64 --decode >"$KUBECONFIG" - chmod 600 "$KUBECONFIG" - cluster_name="$(kubectl config view --kubeconfig "$KUBECONFIG" --minify -o jsonpath='{.clusters[0].name}')" - kubectl config set-cluster "$cluster_name" \ - --kubeconfig "$KUBECONFIG" \ - --server "$KUBE_API_SERVER" \ - --insecure-skip-tls-verify=true >/dev/null - - env_file="${GITEA_ENV_FILE:-${GITHUB_ENV:-}}" - if [ -n "$env_file" ]; then - printf '%s\n' "KUBECONFIG=$KUBECONFIG" >> "$env_file" - fi - - - name: Migrate, then roll out the immutable revision + uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4 + with: + version: v1.34.1 + - name: Migrate, then roll out verified digests env: REVISION: ${{ gitea.sha }} KUBE_CONFIG_B64: ${{ secrets.KUBE_CONFIG_B64 }} shell: bash run: | set -Eeuo pipefail - - if [ -z "$KUBE_CONFIG_B64" ]; then - echo "Missing KUBE_CONFIG_B64 secret for the CI deploy identity" >&2 - exit 1 - fi - kube_dir="$RUNNER_TEMP/buzz-sheet-kube" mkdir -p "$kube_dir" chmod 700 "$kube_dir" export KUBECONFIG="$kube_dir/config" - if ! printf '%s' "$KUBE_CONFIG_B64" | base64 --decode >"$KUBECONFIG"; then - echo "KUBE_CONFIG_B64 is not valid base64" >&2 - exit 1 - fi + trap 'rm -f "$KUBECONFIG"' EXIT + printf '%s' "$KUBE_CONFIG_B64" | base64 --decode > "$KUBECONFIG" chmod 600 "$KUBECONFIG" - if [ ! -s "$KUBECONFIG" ]; then - echo "KUBE_CONFIG_B64 decoded to an empty kubeconfig" >&2 + test -s "$KUBECONFIG" + insecure="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.insecure-skip-tls-verify}')" + if [ "$insecure" = true ]; then + echo 'The deployment kubeconfig must validate the Kubernetes certificate.' >&2 exit 1 fi - cluster_name="$(kubectl config view --kubeconfig "$KUBECONFIG" --minify -o jsonpath='{.clusters[0].name}')" - if [ -z "$cluster_name" ]; then - echo "Decoded kubeconfig has no active cluster" >&2 + # Preserve the CA and certificate-valid server supplied in the kubeconfig. + kubectl --namespace "$DEPLOY_NAMESPACE" get deployment buzz-sheet >/dev/null + app_digest="$(sed -nE 's/.*digest: (sha256:[a-f0-9]{64}).*/\1/p' "$RUNNER_TEMP/buzz-app-push.log" | tail -n 1)" + migration_digest="$(sed -nE 's/.*digest: (sha256:[a-f0-9]{64}).*/\1/p' "$RUNNER_TEMP/buzz-migration-push.log" | tail -n 1)" + app_image="$REGISTRY_IMAGE@$app_digest" + migration_image="$REGISTRY_IMAGE@$migration_digest" + [[ "$app_image" =~ @sha256:[a-f0-9]{64}$ ]] + [[ "$migration_image" =~ @sha256:[a-f0-9]{64}$ ]] + migration_manifest="$RUNNER_TEMP/buzz-sheet-migration.yaml" + kubectl kustomize k8s/migration > "$migration_manifest" + sed -i "s#image: $REGISTRY_IMAGE:.*#image: $migration_image#" "$migration_manifest" + grep -Fq "image: $migration_image" "$migration_manifest" + kubectl --namespace "$DEPLOY_NAMESPACE" delete job buzz-sheet-migrate --ignore-not-found + migration_resource="$(kubectl --namespace "$DEPLOY_NAMESPACE" create -f "$migration_manifest" -o name)" + if ! kubectl --namespace "$DEPLOY_NAMESPACE" wait --for=condition=complete --timeout=10m "$migration_resource"; then + kubectl --namespace "$DEPLOY_NAMESPACE" logs "$migration_resource" --all-containers=true || true exit 1 fi - kubectl config set-cluster "$cluster_name" \ - --kubeconfig "$KUBECONFIG" \ - --server "$KUBE_API_SERVER" \ - --insecure-skip-tls-verify=true >/dev/null - - revision_short="${REVISION:0:12}" - migration_dir="$RUNNER_TEMP/buzz-sheet-migration-$revision_short" - migration_manifest="$RUNNER_TEMP/buzz-sheet-migration-$revision_short.yaml" - cp -R k8s/migration "$migration_dir" - sed -i "s#newName: .*#newName: $REGISTRY_IMAGE#" "$migration_dir/kustomization.yaml" - sed -i "s/newTag: .*/newTag: migrate-$REVISION/" "$migration_dir/kustomization.yaml" - kubectl kustomize "$migration_dir" >"$migration_manifest" - - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" delete job buzz-sheet-migrate --ignore-not-found - migration_resource="$(kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" create --validate=false -f "$migration_manifest" -o name)" - if ! kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" wait \ - --for=condition=complete \ - --timeout=10m \ - "$migration_resource"; then - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" logs "$migration_resource" --all-containers=true || true - exit 1 - fi - - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" patch configmap buzz-sheet-config \ - --type=merge \ + kubectl --namespace "$DEPLOY_NAMESPACE" patch configmap buzz-sheet-config --type=merge \ --patch "{\"data\":{\"NEXT_DEPLOYMENT_ID\":\"$REVISION\",\"BASE_URL\":\"$BASE_URL\"}}" - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" set image \ - deployment/buzz-sheet \ - app="$REGISTRY_IMAGE:$REVISION" - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" set image \ - deployment/buzz-sheet-worker \ - worker="$REGISTRY_IMAGE:$REVISION" - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" set image \ - deployment/buzz-sheet-discord-worker \ - discord-worker="$REGISTRY_IMAGE:$REVISION" - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" rollout status \ - deployment/buzz-sheet \ - --timeout=10m - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" rollout status \ - deployment/buzz-sheet-worker \ - --timeout=10m - kubectl --kubeconfig "$KUBECONFIG" --namespace "$DEPLOY_NAMESPACE" rollout status \ - deployment/buzz-sheet-discord-worker \ - --timeout=10m + kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet app="$app_image" + kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet-worker worker="$app_image" + kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet-discord-worker discord-worker="$app_image" + for deployment in buzz-sheet buzz-sheet-worker buzz-sheet-discord-worker; do + kubectl --namespace "$DEPLOY_NAMESPACE" rollout status "deployment/$deployment" --timeout=10m + done diff --git a/Dockerfile b/Dockerfile index 4f44637..4632503 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1.7 -ARG BUN_VERSION=1.3.14 +ARG BUN_IMAGE=oven/bun:1.3.14-alpine@sha256:5acc90a93e91ff07bf72aa90a7c9f0fa189765aec90b47bdbf2152d2196383c0 -FROM oven/bun:${BUN_VERSION} AS dependencies +FROM ${BUN_IMAGE} AS dependencies WORKDIR /app COPY package.json bun.lock ./ RUN bun install --frozen-lockfile @@ -17,9 +17,10 @@ RUN bun build scripts/outbox-worker.ts \ bun build backend/discord.ts \ --target=bun \ --minify \ - --outfile=dist/discord.js + --outfile=dist/discord.js && \ + bun build scripts/migrate.ts --target=bun --minify --outfile=dist/migrate.js -FROM node:22-bookworm-slim AS next-builder +FROM node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 AS next-builder WORKDIR /app ARG NEXT_DEPLOYMENT_ID=container-build ARG BASE_URL @@ -34,20 +35,20 @@ COPY . . # still runs on Bun; Node is used only for the deterministic compiler process. RUN node node_modules/next/dist/bin/next build -FROM dependencies AS migration +FROM ${BUN_IMAGE} AS migration +RUN apk upgrade --no-cache WORKDIR /app ARG VCS_REF=unknown LABEL org.opencontainers.image.title="Buzz Sheet migrations" \ org.opencontainers.image.revision=${VCS_REF} ENV NODE_ENV=production -COPY --chown=bun:bun db ./db +COPY --from=worker-builder --chown=bun:bun /app/dist/migrate.js ./migrate.js COPY --chown=bun:bun drizzle ./drizzle -COPY --chown=bun:bun scripts/migrate.ts ./scripts/migrate.ts -COPY --chown=bun:bun tsconfig.json ./tsconfig.json USER 1000:1000 -ENTRYPOINT ["bun", "scripts/migrate.ts"] +ENTRYPOINT ["bun", "migrate.js"] -FROM oven/bun:${BUN_VERSION} AS app +FROM ${BUN_IMAGE} AS app +RUN apk upgrade --no-cache WORKDIR /app ARG VCS_REF=unknown LABEL org.opencontainers.image.title="Buzz Sheet" \ diff --git a/README.md b/README.md index 6dbb708..fcd0043 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ not affiliated with or endorsed by HoYoverse. - Structured editors for overview, weapons, artifacts, constellations, teams, and custom sections with autosave and conflict recovery. - Better Auth email/password login with administrator-managed accounts. -- Private S3-compatible media uploads with reference-aware same-origin delivery. +- S3-compatible media uploads with reference-aware same-origin delivery for + staged private objects. - PostgreSQL transactions and a retryable outbox. - Shared Redis cache invalidation and privacy-safe Server-Sent Events across replicas. @@ -35,7 +36,7 @@ formula fixtures. Their guide text and media are not imported or published. | `/admin` | Character and page overview | | `/admin/[character]/[page]` | Visual page editor | | `/admin/create` | Create a structured guide from the synced character catalog | -| `/media/[id]` | Authorized same-origin media response | +| `/media/[id]` | Same-origin media response with publication checks | | `/api/health` | Liveness response | | `/api/health?ready=1` | PostgreSQL and Redis readiness response | @@ -49,7 +50,7 @@ Requirements: - Bun 1.3.14 - PostgreSQL - Redis -- Private S3-compatible object storage +- S3-compatible object storage Install dependencies and create your local environment file: @@ -71,18 +72,6 @@ bun run db:migrate bun run dev ``` -### Fixture-only demo - -The UI can be inspected without PostgreSQL, Redis, S3, or authentication -credentials by running: - -```bash -BUZZ_DEMO_MODE=true bun run dev -``` - -Demo mode uses sanitized local fixtures and bypasses production authorization -and external services. Never enable it in a deployed environment. - ## Commands | Command | Purpose | @@ -92,6 +81,7 @@ and external services. Never enable it in a deployed environment. | `bun run test:coverage` | Produce V8 coverage output | | `bun run typecheck` | Run TypeScript without emitting files | | `bun run lint` | Run ESLint | +| `bun run security:audit` | Check dependency advisories and time-limited exceptions | | `bun run build` | Compile the production application | | `bun run db:generate` | Generate a Drizzle migration from schema changes | | `bun run db:migrate` | Apply committed Drizzle migrations | @@ -122,9 +112,11 @@ Public guides use a shared Redis-backed Next.js cache. Admin routes remain dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so clients reconnect. Traefik buffering is disabled by the response headers. -Media objects remain private in S3. `/media/[id]` serves an object publicly only -while a published guide references it; otherwise administrator authorization is -required. +Presigned staged uploads remain private in S3. `/media/[id]` serves a staged +object publicly only while a published guide references it; otherwise +administrator authorization is required. The direct upload path writes public +CDN objects for guide images, so draft uploads on that path must not contain +sensitive material. ## Verification @@ -132,6 +124,7 @@ Run the complete host-safe verification set with: ```bash bun install --frozen-lockfile +bun run security:audit bun run test bun run typecheck bun run lint @@ -190,6 +183,7 @@ Kustomize resources live in `k8s/` and define: - Web requests of 500m CPU/1 GiB and limits of 1 CPU/2 GiB. - HPA from 2 to 6 web replicas at 70% CPU or 75% memory and a PDB with one available. - Non-root, read-only containers with dropped capabilities and seccomp. +- Ingress NetworkPolicy allowing the web pods only from Traefik in `kube-system`. - ARM64 scheduling constraints matching the production hosts and images. - A versioned migration Job and namespace-scoped CI deployer permissions. @@ -205,19 +199,28 @@ Before the first rollout, a cluster administrator must provision a DATABASE_URL BETTER_AUTH_SECRET REDIS_URL -CATALOG_SYNC_CONCURRENCY S3_ENDPOINT S3_PUBLIC_URL S3_BUCKET S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY NEXT_SERVER_ACTIONS_ENCRYPTION_KEY +TURNSTILE_SITE_KEY +TURNSTILE_SECRET_KEY +DISCORD_BOT_TOKEN +DISCORD_CHANNEL_ID ``` `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` must be generated once and remain stable across replicas and rolling deployments. Do not place secret values in the ConfigMap or commit them to this repository. +Traefik must overwrite `X-Real-Ip` for every request, and the web pods must be +reachable only through Traefik before enabling the configured client IP based +rate limits. The NetworkPolicy assumes Traefik runs in `kube-system` with the +`app.kubernetes.io/name=traefik` pod label; verify those labels in the target +cluster before applying it. + Every page also subscribes to `/api/active`. When a serving pod reports a newer deployment ID than the browser's current build, a persistent Thai notification offers a full reload into the new release. @@ -236,9 +239,11 @@ kubectl kustomize k8s/ >/dev/null ### Gitea Actions release -`.gitea/workflows/ci.yml` verifies every push and pull request. After the +`.gitea/workflows/ci.yml` verifies pushes to `main` and scheduled runs, including a +dependency audit and a Git history secret scan. A time-limited advisory +exception is recorded in `security/audit-exceptions.json`. After the repository variable `DEPLOY_ENABLED` is explicitly set to `true`, a successful -push to `main` builds multi-architecture images at: +push to `main` builds and scans ARM64 images at: ```text registry.neko-piranha.ts.net/astral/buzz-sheet: @@ -250,7 +255,10 @@ Configure these Gitea secrets later: | Secret | Purpose | | --- | --- | | `KUBE_CONFIG_B64` | Base64 kubeconfig for the `ci-deployer` identity | -| `REDIS_INTEGRATION_URL` | Optional isolated Redis endpoint for CI integration coverage | + +The kubeconfig must contain a certificate-valid API server address and its CA. +The workflow rejects `insecure-skip-tls-verify` and does not rewrite the +cluster server address. The deploy job creates a revision-named migration Job, waits for completion, updates `NEXT_DEPLOYMENT_ID`, and only then rolls out the immutable application diff --git a/app/[character]/[page]/page.tsx b/app/[character]/[page]/page.tsx index 3fee79f..0794904 100644 --- a/app/[character]/[page]/page.tsx +++ b/app/[character]/[page]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation"; import { GoogleAdsense } from "@/components/public/google-adsense"; import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide"; -import { getCachedPublicGuide } from "@/lib/cache/public"; +import { getCachedPublicGuide, getCachedPublicGuideSummary } from "@/lib/cache/public"; type Params = Promise<{ character: string; page: string }>; @@ -11,7 +11,7 @@ export const instant = false; export async function generateMetadata({ params }: { params: Params }): Promise { const { character } = await params; - const guide = await getCachedPublicGuide(character); + const guide = await getCachedPublicGuideSummary(character); return guide ? { title: `${guide.name}`, @@ -24,7 +24,7 @@ export async function generateMetadata({ params }: { params: Params }): Promise< export default async function GuideSectionPage({ params }: { params: Params }) { const { character, page } = await params; - const guide = await getCachedPublicGuide(character); + const guide = await getCachedPublicGuide(character, page); if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound(); return ( <> diff --git a/app/[character]/page.tsx b/app/[character]/page.tsx index f70b63f..ce98877 100644 --- a/app/[character]/page.tsx +++ b/app/[character]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation"; import { GoogleAdsense } from "@/components/public/google-adsense"; import { StructuredGuideShell } from "@/components/public/structured-guide"; -import { getCachedPublicGuide } from "@/lib/cache/public"; +import { getCachedPublicGuide, getCachedPublicGuideSummary } from "@/lib/cache/public"; type Params = Promise<{ character: string }>; @@ -14,7 +14,7 @@ export async function generateMetadata({ }: { params: Params; }): Promise { - const guide = await getCachedPublicGuide((await params).character); + const guide = await getCachedPublicGuideSummary((await params).character); return guide ? { title: `${guide.name}`, diff --git a/app/admin/register/actions.ts b/app/admin/register/actions.ts index 7a0abad..50b6a02 100644 --- a/app/admin/register/actions.ts +++ b/app/admin/register/actions.ts @@ -8,6 +8,7 @@ import { z } from "zod"; import { getDb } from "@/db"; import { users } from "@/db/schema"; import { getAuth, requireAdmin } from "@/lib/auth/server"; +import { securityLog } from "@/lib/security/http"; export interface CreateAccountState { status: "idle" | "error" | "success"; @@ -16,9 +17,9 @@ export interface CreateAccountState { const accountSchema = z .object({ - name: z.string().trim().min(1), + name: z.string().trim().min(1).max(100), email: z.email().transform((value) => value.toLowerCase()), - password: z.string().min(8).max(128), + password: z.string().min(12).max(128), passwordConfirmation: z.string(), }) .refine((data) => data.password === data.passwordConfirmation, { @@ -29,7 +30,7 @@ export async function createAccount( _previousState: CreateAccountState, formData: FormData, ): Promise { - await requireAdmin(); + const admin = await requireAdmin(); const parsed = accountSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) { return { status: "error", message: "โปรดตรวจสอบชื่อ อีเมล และรหัสผ่านให้ถูกต้อง" }; @@ -49,6 +50,8 @@ export async function createAccount( name: parsed.data.name, email: parsed.data.email, password: parsed.data.password, + role: "admin", + data: { emailVerified: true }, }, headers: await headers(), }); @@ -57,6 +60,7 @@ export async function createAccount( } revalidatePath("/admin/register"); + securityLog("account-created", { actorId: admin.user.id }); return { status: "success", message: `สร้างบัญชี ${parsed.data.email} แล้ว` }; } @@ -64,6 +68,9 @@ export async function removeAccount( userId: string, ): Promise<{ status: "error" | "success"; message?: string }> { const session = await requireAdmin(); + if (!z.string().min(1).max(128).safeParse(userId).success) { + return { status: "error", message: "บัญชีไม่ถูกต้อง" }; + } if (userId === session.user.id) { return { status: "error", message: "ไม่สามารถลบบัญชีที่กำลังใช้งานอยู่" }; } @@ -78,5 +85,6 @@ export async function removeAccount( } revalidatePath("/admin/register"); + securityLog("account-removed", { actorId: session.user.id, targetId: userId }); return { status: "success" }; } diff --git a/app/api/active/route.ts b/app/api/active/route.ts index c42b410..56943d3 100644 --- a/app/api/active/route.ts +++ b/app/api/active/route.ts @@ -1,3 +1,5 @@ +import { errorResponse } from "@/lib/security/http"; +import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit"; import { connection } from "next/server"; import { normalizeDeploymentId } from "@/lib/deployment/version"; @@ -5,12 +7,17 @@ import { createEventStream, eventStreamHeaders } from "@/lib/events/sse"; export async function GET(request: Request) { await connection(); - const deploymentId = - normalizeDeploymentId(process.env.NEXT_DEPLOYMENT_ID) ?? "development"; - const channel = createEventStream({ signal: request.signal }); - channel.sendNamed("deployment", deploymentId); + try { + await limitRequest("stream-open", trustedClientAddress(request.headers), 60); + const deploymentId = + normalizeDeploymentId(process.env.NEXT_DEPLOYMENT_ID) ?? "development"; + const channel = createEventStream({ signal: request.signal }); + channel.sendNamed("deployment", deploymentId); - return new Response(channel.stream, { - headers: eventStreamHeaders(), - }); + return new Response(channel.stream, { + headers: eventStreamHeaders(), + }); + } catch (cause) { + return errorResponse(cause); + } } diff --git a/app/api/admin/media/[id]/complete/route.ts b/app/api/admin/media/[id]/complete/route.ts index 980c8c7..740735e 100644 --- a/app/api/admin/media/[id]/complete/route.ts +++ b/app/api/admin/media/[id]/complete/route.ts @@ -1,29 +1,37 @@ +import { errorResponse, requireSameOrigin, withUploadSlot } from "@/lib/security/http"; +import { limitRequest } from "@/lib/security/rate-limit"; import * as z from "zod"; import { requireAdmin } from "@/lib/auth/server"; import { completePendingMedia } from "@/lib/media/repository"; export async function POST( - _request: Request, + request: Request, context: { params: Promise<{ id: string }> }, ) { - await requireAdmin(); - const { id } = await context.params; - if (!z.string().uuid().safeParse(id).success) { - return Response.json({ error: "invalid-id" }, { status: 400 }); + try { + requireSameOrigin(request); + const admin = await requireAdmin(); + await limitRequest("media", admin.user.id, 20); + const { id } = await context.params; + if (!z.string().uuid().safeParse(id).success) { + return Response.json({ error: "invalid-id" }, { status: 400 }); + } + const result = await withUploadSlot(() => completePendingMedia(id)); + if (result.status === "ready") { + return Response.json({ + media: { + id: result.media.id, + fileName: result.media.fileName, + mimeType: result.media.mimeType, + byteSize: result.media.byteSize, + url: `/media/${result.media.id}`, + }, + }); + } + const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422; + return Response.json({ error: result.status }, { status }); + } catch (cause) { + return errorResponse(cause); } - const result = await completePendingMedia(id); - if (result.status === "ready") { - return Response.json({ - media: { - id: result.media.id, - fileName: result.media.fileName, - mimeType: result.media.mimeType, - byteSize: result.media.byteSize, - url: `/media/${result.media.id}`, - }, - }); - } - const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422; - return Response.json({ error: result.status }, { status }); } diff --git a/app/api/admin/media/[id]/route.ts b/app/api/admin/media/[id]/route.ts index b6cc504..0889da2 100644 --- a/app/api/admin/media/[id]/route.ts +++ b/app/api/admin/media/[id]/route.ts @@ -1,19 +1,27 @@ +import { errorResponse, requireSameOrigin } from "@/lib/security/http"; +import { limitRequest } from "@/lib/security/rate-limit"; import * as z from "zod"; import { requireAdmin } from "@/lib/auth/server"; import { discardUnreferencedMedia } from "@/lib/media/repository"; export async function DELETE( - _request: Request, + request: Request, context: { params: Promise<{ id: string }> }, ) { - await requireAdmin(); - const { id } = await context.params; - if (!z.string().uuid().safeParse(id).success) { - return Response.json({ error: "invalid-id" }, { status: 400 }); + try { + requireSameOrigin(request); + const admin = await requireAdmin(); + await limitRequest("media", admin.user.id, 20); + const { id } = await context.params; + if (!z.string().uuid().safeParse(id).success) { + return Response.json({ error: "invalid-id" }, { status: 400 }); + } + const deleted = await discardUnreferencedMedia(id); + return deleted + ? new Response(null, { status: 204 }) + : Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 }); + } catch (cause) { + return errorResponse(cause); } - const deleted = await discardUnreferencedMedia(id); - return deleted - ? new Response(null, { status: 204 }) - : Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 }); } diff --git a/app/api/admin/media/presign/route.ts b/app/api/admin/media/presign/route.ts index c2f353a..4180864 100644 --- a/app/api/admin/media/presign/route.ts +++ b/app/api/admin/media/presign/route.ts @@ -1,32 +1,35 @@ +import { errorResponse, requireSameOrigin, readJson } from "@/lib/security/http"; +import { limitRequest } from "@/lib/security/rate-limit"; import { requireAdmin } from "@/lib/auth/server"; import { createPendingMedia } from "@/lib/media/repository"; import { mediaUploadSchema } from "@/lib/media/validation"; export async function POST(request: Request) { - const admin = await requireAdmin(); - let input: unknown; try { - input = await request.json(); - } catch { - return Response.json({ error: "invalid-json" }, { status: 400 }); - } - const parsed = mediaUploadSchema.safeParse(input); - if (!parsed.success) { - return Response.json( - { error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) }, - { status: 400 }, - ); - } - try { - const upload = await createPendingMedia(parsed.data, admin.user.id); - return Response.json({ - mediaId: upload.id, - uploadUrl: upload.uploadUrl, - method: "PUT", - headers: { "Content-Type": parsed.data.mimeType }, - expiresIn: upload.expiresIn, - }); - } catch { - return Response.json({ error: "storage-unavailable" }, { status: 503 }); + requireSameOrigin(request); + const admin = await requireAdmin(); + await limitRequest("media", admin.user.id, 20); + const input = await readJson(request); + const parsed = mediaUploadSchema.safeParse(input); + if (!parsed.success) { + return Response.json( + { error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) }, + { status: 400 }, + ); + } + try { + const upload = await createPendingMedia(parsed.data, admin.user.id); + return Response.json({ + mediaId: upload.id, + uploadUrl: upload.uploadUrl, + method: "PUT", + headers: { "Content-Type": parsed.data.mimeType }, + expiresIn: upload.expiresIn, + }); + } catch { + return Response.json({ error: "storage-unavailable" }, { status: 503 }); + } + } catch (cause) { + return errorResponse(cause); } } diff --git a/app/api/admin/media/route.ts b/app/api/admin/media/route.ts index d2df3b8..0de9c2d 100644 --- a/app/api/admin/media/route.ts +++ b/app/api/admin/media/route.ts @@ -1,21 +1,36 @@ -import { NextResponse } from "next/server"; - import { requireAdmin } from "@/lib/auth/server"; import { uploadPublicMedia } from "@/lib/media/repository"; +import { MAX_MEDIA_BYTES, mediaUploadSchema } from "@/lib/media/validation"; +import { boundedBody, errorResponse, HttpError, requireSameOrigin, securityLog, withUploadSlot } from "@/lib/security/http"; +import { limitRequest } from "@/lib/security/rate-limit"; export async function POST(request: Request) { - const admin = await requireAdmin(); - const form = await request.formData(); - const file = form.get("file"); - if (!(file instanceof File)) { - return NextResponse.json({ error: "กรุณาเลือกไฟล์รูปภาพ" }, { status: 400 }); - } try { - return NextResponse.json(await uploadPublicMedia(file, admin.user.id), { status: 201 }); + requireSameOrigin(request); + const admin = await requireAdmin(); + await limitRequest("media", admin.user.id, 20); + if (!request.headers.get("content-type")?.toLowerCase().startsWith("multipart/form-data;")) { + throw new HttpError(415, "expected-multipart"); + } + return await withUploadSlot(async () => { + let form: FormData; + try { + form = await boundedBody(request, MAX_MEDIA_BYTES + 64 * 1024).formData(); + } catch (cause) { + if (cause instanceof HttpError) throw cause; + throw new HttpError(400, "invalid-form"); + } + const file = form.get("file"); + if (!(file instanceof File)) throw new HttpError(400, "missing-image"); + if (file.size > MAX_MEDIA_BYTES) throw new HttpError(413, "image-too-large"); + if (!mediaUploadSchema.safeParse({ fileName: file.name, mimeType: file.type, byteSize: file.size }).success) { + throw new HttpError(415, "invalid-image"); + } + const result = await uploadPublicMedia(file, admin.user.id); + securityLog("media-uploaded", { actorId: admin.user.id, targetId: result.id }); + return Response.json(result, { status: 201, headers: { "Cache-Control": "no-store" } }); + }); } catch (cause) { - return NextResponse.json( - { error: cause instanceof Error ? cause.message : "อัปโหลดไม่สำเร็จ" }, - { status: 400 }, - ); + return errorResponse(cause); } } diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts index 741a91c..cd52ec5 100644 --- a/app/api/auth/[...all]/route.ts +++ b/app/api/auth/[...all]/route.ts @@ -1,11 +1,32 @@ import { toNextJsHandler } from "better-auth/next-js"; import { getAuth } from "@/lib/auth/server"; +import { errorResponse, HttpError, readJson, requireSameOrigin } from "@/lib/security/http"; const handlers = toNextJsHandler((request) => getAuth().handler(request)); export const GET = handlers.GET; -export const POST = handlers.POST; -export const PATCH = handlers.PATCH; -export const PUT = handlers.PUT; -export const DELETE = handlers.DELETE; + +async function mutate(request: Request) { + try { + requireSameOrigin(request); + const input = await readJson(request.clone()); + const path = new URL(request.url).pathname; + if (["/admin/create-user", "/admin/set-user-password"].some((endpoint) => path.endsWith(endpoint))) { + const password = input && typeof input === "object" && "password" in input ? input.password : undefined; + const newPassword = input && typeof input === "object" && "newPassword" in input ? input.newPassword : undefined; + const value = password ?? newPassword; + if (typeof value !== "string" || value.length < 12 || value.length > 128) { + throw new HttpError(400, "invalid-password-length"); + } + } + return await getAuth().handler(request); + } catch (cause) { + return errorResponse(cause); + } +} + +export const POST = mutate; +export const PATCH = mutate; +export const PUT = mutate; +export const DELETE = mutate; diff --git a/app/api/events/admin/route.ts b/app/api/events/admin/route.ts index e9e4709..97b977c 100644 --- a/app/api/events/admin/route.ts +++ b/app/api/events/admin/route.ts @@ -1,3 +1,5 @@ +import { errorResponse } from "@/lib/security/http"; +import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit"; import { connection } from "next/server"; import { @@ -6,7 +8,6 @@ import { } from "@/lib/auth/server"; import { createRedisEventResponse, - eventsUnavailableResponse, } from "@/lib/events/redis-stream"; export async function GET(request: Request) { @@ -20,8 +21,9 @@ export async function GET(request: Request) { throw cause; } try { + await limitRequest("stream-open", trustedClientAddress(request.headers), 60); return await createRedisEventResponse("admin", request.signal); - } catch { - return eventsUnavailableResponse(); + } catch (cause) { + return errorResponse(cause); } } diff --git a/app/api/events/directory/route.ts b/app/api/events/directory/route.ts index 9a7e5ca..8f9f6b3 100644 --- a/app/api/events/directory/route.ts +++ b/app/api/events/directory/route.ts @@ -1,15 +1,17 @@ +import { errorResponse } from "@/lib/security/http"; +import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit"; import { connection } from "next/server"; import { createRedisEventResponse, - eventsUnavailableResponse, } from "@/lib/events/redis-stream"; export async function GET(request: Request) { await connection(); try { + await limitRequest("stream-open", trustedClientAddress(request.headers), 60); return await createRedisEventResponse("directory", request.signal); - } catch { - return eventsUnavailableResponse(); + } catch (cause) { + return errorResponse(cause); } } diff --git a/app/api/events/page/[id]/route.ts b/app/api/events/page/[id]/route.ts index 7210679..59d1565 100644 --- a/app/api/events/page/[id]/route.ts +++ b/app/api/events/page/[id]/route.ts @@ -1,8 +1,13 @@ +import { errorResponse } from "@/lib/security/http"; +import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit"; import { connection } from "next/server"; +import { and, eq, isNull } from "drizzle-orm"; +import { getDb } from "@/db"; +import { guides } from "@/db/schema"; +import { getAdminSession } from "@/lib/auth/server"; import { createRedisEventResponse, - eventsUnavailableResponse, } from "@/lib/events/redis-stream"; const UUID_PATTERN = @@ -18,8 +23,14 @@ export async function GET( return Response.json({ error: "invalid-page-id" }, { status: 400 }); } try { + await limitRequest("stream-open", trustedClientAddress(request.headers), 60); + const [published] = await getDb().select({ id: guides.id }).from(guides) + .where(and(eq(guides.id, id), eq(guides.isPublic, true), isNull(guides.trashedAt))).limit(1); + if (!published && !(await getAdminSession())) { + return new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } }); + } return await createRedisEventResponse(`page:${id}`, request.signal); - } catch { - return eventsUnavailableResponse(); + } catch (cause) { + return errorResponse(cause); } } diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 0ef0aee..991c046 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -18,13 +18,6 @@ export async function GET(request: Request) { ); } - if (process.env.BUZZ_DEMO_MODE === "true") { - return Response.json( - { status: "ready", deploymentId, checks: { demo: "ok" } }, - { headers: NO_STORE_HEADERS }, - ); - } - const timeout = Number(process.env.HEALTHCHECK_TIMEOUT_MS || 2_500); const result = await checkReadiness( { diff --git a/app/api/presence/admin/route.ts b/app/api/presence/admin/route.ts index 786a121..88abde1 100644 --- a/app/api/presence/admin/route.ts +++ b/app/api/presence/admin/route.ts @@ -1,7 +1,9 @@ +import { errorResponse, readJson, requireSameOrigin } from "@/lib/security/http"; +import { limitRequest } from "@/lib/security/rate-limit"; import { connection } from "next/server"; import * as z from "zod"; -import { AdminAuthorizationError, requireAdmin } from "@/lib/auth/server"; +import { requireAdmin } from "@/lib/auth/server"; import { updateAdminPresence } from "@/lib/presence"; const inputSchema = z.strictObject({ @@ -14,8 +16,10 @@ const inputSchema = z.strictObject({ export async function POST(request: Request) { await connection(); try { + requireSameOrigin(request); const admin = await requireAdmin(); - const parsed = inputSchema.safeParse(await request.json().catch(() => null)); + await limitRequest("admin-presence", admin.session.id, 120); + const parsed = inputSchema.safeParse(await readJson(request, 2048)); if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 }); return Response.json(await updateAdminPresence({ ...parsed.data, @@ -23,12 +27,6 @@ export async function POST(request: Request) { user: admin.user, }), { headers: { "Cache-Control": "no-store" } }); } catch (cause) { - if (cause instanceof AdminAuthorizationError) { - return Response.json({ error: "unauthorized" }, { status: 401 }); - } - return Response.json({ error: "presence-unavailable" }, { - status: 503, - headers: { "Cache-Control": "no-store" }, - }); + return errorResponse(cause); } } diff --git a/app/api/presence/visitors/route.ts b/app/api/presence/visitors/route.ts index ddfe4c3..c0b2969 100644 --- a/app/api/presence/visitors/route.ts +++ b/app/api/presence/visitors/route.ts @@ -1,3 +1,5 @@ +import { errorResponse, readJson, requireSameOrigin } from "@/lib/security/http"; +import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit"; import { connection } from "next/server"; import * as z from "zod"; @@ -11,16 +13,15 @@ const inputSchema = z.strictObject({ export async function POST(request: Request) { await connection(); - const parsed = inputSchema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 }); try { + requireSameOrigin(request); + await limitRequest("presence", trustedClientAddress(request.headers), 600); + const parsed = inputSchema.safeParse(await readJson(request, 1024)); + if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 }); return Response.json(await updateVisitorPresence(parsed.data), { headers: { "Cache-Control": "no-store" }, }); - } catch { - return Response.json({ error: "presence-unavailable" }, { - status: 503, - headers: { "Cache-Control": "no-store" }, - }); + } catch (cause) { + return errorResponse(cause); } } diff --git a/bun.lock b/bun.lock index eec7ce9..c32e3af 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ "@tiptap/pm": "3.31.3", "@tiptap/react": "3.31.3", "@tiptap/starter-kit": "3.31.3", - "better-auth": "^1.7.2", + "better-auth": "1.7.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -38,9 +38,9 @@ "react-resizable-panels": "^4.12.3", "react-spreadsheet": "0.10.1", "recharts": "3.8.0", + "sanitize-html": "2.17.7", "scheduler": "^0.27.0", "server-only": "^0.0.1", - "shadcn": "^4.19.0", "sharp": "^0.35.4", "sonner": "^2.0.8", "tailwind-merge": "^3.6.0", @@ -53,10 +53,12 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/sanitize-html": "^2.16.1", "@vitest/coverage-v8": "^4.1.11", - "drizzle-kit": "^0.31.10", + "drizzle-kit": "0.31.11", "eslint": "^9", "eslint-config-next": "16.3.3", + "shadcn": "4.19.0", "tailwindcss": "^4", "typescript": "^5", "vitest": "^4.1.11", @@ -67,6 +69,9 @@ "sharp", "unrs-resolver", ], + "overrides": { + "qs": "6.16.0", + }, "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -134,23 +139,23 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@better-auth/core": ["@better-auth/core@1.7.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-j0nM4ygsWbF/fcYRoKtDn8gn8uLXkmC+075HqSqsJEAV828cJR9bvYBCUQ1zmxNyRBk6Iz/qXsA0Zm2oksiOTg=="], + "@better-auth/core": ["@better-auth/core@1.7.5", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.5.4" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-kVlSu4H8OKQfjg4b/Zj5MOaospt83N0JbX38wsDzE58Yw95jzovFkU3pxzB1eUFYc4mkuhUMZD8iT1gpUjNMcQ=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-A5wE10PIv3aS5LGePecEHntQylKy6OOF17B4dqlE0DwJeqU/IOBSd7/LZhMop9cNJ3WFjKMpazVSf91yYM/NFg=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-9SM7v1735SoaedRDcDbHc5ULgXEd2vUlEJkvRHpMF2Q9qf59TRh1b5A9hryyecyi56bm/0CNUDU3nY0uVWj5/Q=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-LYdSRLOvZiF+6S0UThu+wE/Qxsq9P2jQs7ZKkY6BIBJqUjYyxVDmi8HFcantBvWWW1/BeQCSsD7YVDG4gICMIQ=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-1wE5gvnjW+c1i4GrLtL9HLn3s0Xrq4YneCDan1NO1dpz0mEguLFNlzfnUGykTFrDwvFh2X5rkIbA8ALCj6WzXQ=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2" } }, "sha512-0q1SXMzm5esH9L0xVuM6IxCk59E4G+3HySX4My9gvEwqtmUobykn+iuc/si3Y4xwUO7JODqQ5o+/pPcLDDMIrA=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2" } }, "sha512-YDmnfR9zOXbn5SNYYwfHBPuc19hg1d1C1vUXb+Hm8Q91pTsstFNX5OTlZbo7f28q06FpogAEfGGCN/2QV39cig=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-4879SmUWHUs0OYlvHoCFbycZ7i1bqytkcgAUdt9RLQMvZ5H3LRMTgax2YVlGZEXgwNjY/X7xAoXOecWLhlQWeA=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-Yq0LfF0VlA9Kfjcjp/v43MSsChv7vKd1ct0Mt15px4zlqaCPIGfPbjw9YNM6jTo0fGpqLR0n15PllSWmLLRp9g=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-mXTr/83WrNWLrvzIjtgDgdu9iXhOcSG1+qBQOAKlbGSFiOB+z4IMRneQ2wmMOiB8mKY9qGkClVUjKRFXqtHnFQ=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-QfW6HS9vK0FMcbI/GsQLdplICxOz0EPzYWGONZT4ovL3cSItd4YH/09USbPPVl+VUQCdznAQIk+n+NKvHdmSag=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.7.2", "", { "peerDependencies": { "@better-auth/core": "^1.7.2", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-LcWu+O0zrxYDQj8E36vfkJwGPW4k9ZDA/rCo0zST6ihzL+juR7pBowoZIM9E6tK0Vit52mf6412bGT4XM4eTjQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.7.5", "", { "peerDependencies": { "@better-auth/core": "^1.7.5", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2" } }, "sha512-e/REPqMy9Em+gC6G0xWBikiMLRuy532Er7jqdoNkPBa65FbywgWcm1cZbgdW5CnHsrM3OLZsmKn14yeGoDISeg=="], "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], - "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + "@better-fetch/fetch": ["@better-fetch/fetch@1.3.2", "", {}, "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow=="], "@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=="], @@ -690,6 +695,8 @@ "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], + "@types/sanitize-html": ["@types/sanitize-html@2.16.1", "", { "dependencies": { "htmlparser2": "^10.1" } }, "sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], @@ -842,7 +849,7 @@ "bessel": ["bessel@1.0.2", "", {}, "sha512-Al3nHGQGqDYqqinXhQzmwmcRToe/3WyBv4N8aZc5Pef8xw2neZlR9VPi84Sa23JtgWcucu18HxVZrnI0fn2etw=="], - "better-auth": ["better-auth@1.7.2", "", { "dependencies": { "@better-auth/core": "1.7.2", "@better-auth/drizzle-adapter": "1.7.2", "@better-auth/kysely-adapter": "1.7.2", "@better-auth/memory-adapter": "1.7.2", "@better-auth/mongo-adapter": "1.7.2", "@better-auth/prisma-adapter": "1.7.2", "@better-auth/telemetry": "1.7.2", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-gKapKBEvYIGcMxi74RjQ7EbFLiqyQt58vdoJmL1qAlWSkY1Bc2Vqshl524/3u1NxauiOU03M/Ebh762Brmac9A=="], + "better-auth": ["better-auth@1.7.5", "", { "dependencies": { "@better-auth/core": "1.7.5", "@better-auth/drizzle-adapter": "1.7.5", "@better-auth/kysely-adapter": "1.7.5", "@better-auth/memory-adapter": "1.7.5", "@better-auth/mongo-adapter": "1.7.5", "@better-auth/prisma-adapter": "1.7.5", "@better-auth/telemetry": "1.7.5", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.2", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.5.4" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-aKE0Zt2EPTpFvmq4/oATNyG/mAfc6JUqWkW9pzGlrVnzUb0lso7GJ9BPxD6JPhLqYV1a9zOAb0uAz1Q5fm+eHA=="], "better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], @@ -960,6 +967,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], + "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1000,11 +1009,19 @@ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], + "drizzle-kit": ["drizzle-kit@0.31.11", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg=="], "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], @@ -1028,6 +1045,8 @@ "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "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=="], @@ -1216,6 +1235,8 @@ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], @@ -1292,6 +1313,8 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-plain-object": ["is-plain-object@5.1.0", "", {}, "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], @@ -1370,6 +1393,8 @@ "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + "launder": ["launder@1.7.1", "", { "dependencies": { "dayjs": "^1.11.7" } }, "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1518,6 +1543,8 @@ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], @@ -1544,7 +1571,7 @@ "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], - "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], @@ -1588,7 +1615,7 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1666,6 +1693,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-html": ["sanitize-html@2.17.7", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^12.0.0", "is-plain-object": "^5.0.0", "launder": "^1.7.1", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-PGtEkc9cbnedU3s9TmzDbpsZ8w086g/0Q8k8/oIO1NLNU3i5k9yn835CrjJSajp1KMmkisbO1qPXxNKO3welAg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -1818,7 +1847,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1900,8 +1929,12 @@ "@base-ui/utils/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "@better-auth/core/zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@discordjs/rest/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -1910,8 +1943,6 @@ "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - "@dotenvx/dotenvx/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], - "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -1942,6 +1973,8 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "@ts-morph/common/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], @@ -1956,8 +1989,12 @@ "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "better-auth/zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], + "better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "conf/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -1966,6 +2003,10 @@ "conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="], + "discord.js/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2010,12 +2051,12 @@ "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "sanitize-html/htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="], + + "sanitize-html/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "shadcn/postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], - "shadcn/postcss-selector-parser": ["postcss-selector-parser@7.1.5", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw=="], - - "shadcn/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], - "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -2114,6 +2155,14 @@ "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "sanitize-html/htmlparser2/domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], + + "sanitize-html/htmlparser2/domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="], + + "sanitize-html/htmlparser2/domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="], + + "sanitize-html/htmlparser2/entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], @@ -2196,6 +2245,8 @@ "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "sanitize-html/htmlparser2/domutils/dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], + "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/components/admin/register-card.tsx b/components/admin/register-card.tsx index b1fb23f..77ff625 100644 --- a/components/admin/register-card.tsx +++ b/components/admin/register-card.tsx @@ -113,12 +113,12 @@ export function RegisterCard({ name="password" type="password" autoComplete="new-password" - minLength={8} + minLength={12} maxLength={128} disabled={pending} required /> - อย่างน้อย 8 ตัวอักษร + อย่างน้อย 12 ตัวอักษร @@ -129,7 +129,7 @@ export function RegisterCard({ name="passwordConfirmation" type="password" autoComplete="new-password" - minLength={8} + minLength={12} maxLength={128} disabled={pending} required diff --git a/components/public/google-adsense.tsx b/components/public/google-adsense.tsx index f86357a..b1e8822 100644 --- a/components/public/google-adsense.tsx +++ b/components/public/google-adsense.tsx @@ -23,7 +23,7 @@ export function GoogleAdsense() {