From 81c5b9f2e005f76bfb030fbb3a42e7cad6ecb924 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sun, 22 Feb 2026 20:58:17 +0800 Subject: [PATCH] chore(infra): migrate desktop web to Cloudflare (#4869) * chore(infra): migrate desktop web from Vercel to Cloudflare Workers Replaces separate Vercel deployments (follow SPA + follow-external-ssr) with unified Cloudflare Workers + Assets deployment. Implements meta tag injection, OG image generation, and environment variable management in Hono-based Worker. Adds GitHub Actions CI/CD for automatic deployment on push to dev/main branches. - Replace Fastify with Hono for Cloudflare Workers compatibility - Create Worker entry point with SSR routes and SPA fallback - Add AsyncLocalStorage-based request context shim - Implement WASM-based OG image rendering with R2 font storage - Split SPA and SSR routing: /share/* and auth routes use SSR, others fallback to SPA - Add Cloudflare wrangler configuration with dev/prod environments - Create GitHub Actions workflow for automated deployments - Add build scripts for font data and WASM patching Co-Authored-By: Claude Opus 4.6 * fix(ssr): exclude worker files from typecheck and fix tsdown config Worker-specific files (*.worker.ts) use Cloudflare Workers types and generated modules that aren't available during the main tsc typecheck. Exclude them from tsconfig since they're only used via tsdown aliases. Also fix broken path.resolve reference in tsdown.worker.config.ts. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/deploy-cloudflare.yml | 70 +++ .gitignore | 2 + apps/ssr/package.json | 8 +- apps/ssr/scripts/check-fonts.ts | 20 + apps/ssr/scripts/generate-font-data.ts | 46 ++ apps/ssr/scripts/patch-worker-build.ts | 24 + apps/ssr/scripts/upload-fonts-to-r2.ts | 36 ++ apps/ssr/src/lib/load-env.worker.ts | 2 + apps/ssr/src/lib/og/fonts.worker.ts | 66 ++ apps/ssr/src/lib/og/render-to-image.worker.ts | 33 + apps/ssr/src/lib/og/resvg-wasm-shim.ts | 22 + apps/ssr/src/lib/worker-request-context.ts | 43 ++ apps/ssr/tsconfig.json | 2 +- apps/ssr/tsdown.worker.config.ts | 28 + apps/ssr/worker-app.ts | 71 +++ apps/ssr/worker-entry.ts | 334 +++++++++++ apps/ssr/wrangler.jsonc | 74 +++ pnpm-lock.yaml | 562 +++++++++++++++++- 18 files changed, 1411 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/deploy-cloudflare.yml create mode 100644 apps/ssr/scripts/check-fonts.ts create mode 100644 apps/ssr/scripts/generate-font-data.ts create mode 100644 apps/ssr/scripts/patch-worker-build.ts create mode 100644 apps/ssr/scripts/upload-fonts-to-r2.ts create mode 100644 apps/ssr/src/lib/load-env.worker.ts create mode 100644 apps/ssr/src/lib/og/fonts.worker.ts create mode 100644 apps/ssr/src/lib/og/render-to-image.worker.ts create mode 100644 apps/ssr/src/lib/og/resvg-wasm-shim.ts create mode 100644 apps/ssr/src/lib/worker-request-context.ts create mode 100644 apps/ssr/tsdown.worker.config.ts create mode 100644 apps/ssr/worker-app.ts create mode 100644 apps/ssr/worker-entry.ts create mode 100644 apps/ssr/wrangler.jsonc diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml new file mode 100644 index 000000000..aca9c7e24 --- /dev/null +++ b/.github/workflows/deploy-cloudflare.yml @@ -0,0 +1,70 @@ +on: + push: + branches: [main, dev] + +name: ☁️ Deploy to Cloudflare Workers +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + deploy: + name: Build & Deploy + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [lts/*] + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + lfs: true + + - name: Checkout LFS objects + run: git lfs checkout + + - name: Cache turbo build setup + uses: actions/cache@v5 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Build desktop web (SPA) + run: pnpm exec turbo run Folo#build:web + + - name: Build SSR Worker + working-directory: apps/ssr + run: pnpm run build:worker + + - name: Copy WASM file + run: cp node_modules/@resvg/resvg-wasm/index_bg.wasm apps/ssr/dist/worker/resvg.wasm + + - name: Deploy to Cloudflare (dev) + if: github.ref == 'refs/heads/dev' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/ssr + command: deploy --env dev + + - name: Deploy to Cloudflare (prod) + if: github.ref == 'refs/heads/main' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/ssr + command: deploy diff --git a/.gitignore b/.gitignore index 6bec08d32..da495911b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ apps/desktop/build/appxmanifest.xml .claude/settings.local.json .serena + +.wrangler diff --git a/apps/ssr/package.json b/apps/ssr/package.json index 6875cc608..0dbf62f58 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -4,6 +4,9 @@ "private": true, "scripts": { "build": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsdown && tsx scripts/cleanup-vercel-build.ts", + "build:worker": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsx scripts/generate-font-data.ts && tsdown --config tsdown.worker.config.ts && tsx scripts/patch-worker-build.ts && cp -r dist/dist-external ../desktop/out/web/dist-external", + "deploy:dev": "wrangler deploy --env dev", + "deploy:prod": "wrangler deploy", "dev": "cross-env NODE_ENV=development tsx watch --include \"src/**/*.ts\" --exclude \"./*.ts\" --exclude \"./*.mjs\" index.ts", "meta": "tsx helper/meta-map.ts --watch", "start": "tsx index.ts", @@ -54,6 +57,7 @@ "@follow/shared": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", + "@resvg/resvg-wasm": "2.6.2", "@types/html-minifier-terser": "7.0.2", "chokidar": "4.0.3", "code-inspector-plugin": "1.4.2", @@ -62,6 +66,7 @@ "es-toolkit": "1.44.0", "fast-glob": "3.3.3", "foxact": "0.2.52", + "hono": "4.12.1", "html-minifier-terser": "7.2.0", "lightningcss": "1.31.1", "masonic": "4.1.0", @@ -72,6 +77,7 @@ "tsx": "4.21.0", "typescript": "catalog:", "vite": "7.3.1", - "vite-plugin-route-builder": "0.4.1" + "vite-plugin-route-builder": "0.4.1", + "wrangler": "4.67.0" } } diff --git a/apps/ssr/scripts/check-fonts.ts b/apps/ssr/scripts/check-fonts.ts new file mode 100644 index 000000000..9c01c40fe --- /dev/null +++ b/apps/ssr/scripts/check-fonts.ts @@ -0,0 +1,20 @@ +import fs from "node:fs" +import { createRequire } from "node:module" + +import path, { resolve } from "pathe" + +const require = createRequire(import.meta.url) + +const snPath = require.resolve("@fontsource/sn-pro") +const filesDir = resolve(snPath, "../files") +const files = fs.readdirSync(filesDir).filter((f) => !f.endsWith(".woff2") && !f.includes("italic")) +files.forEach((f) => { + const stat = fs.statSync(path.join(filesDir, f)) + console.info(f, `${(stat.size / 1024).toFixed(1)}KB`) +}) +const kosePath = require.resolve("kose-font") +const koseSize = fs.statSync(kosePath).size +console.info(`kose-font: ${(koseSize / 1024).toFixed(1)}KB`) +let total = files.reduce((sum, f) => sum + fs.statSync(path.join(filesDir, f)).size, 0) +total += koseSize +console.info(`Total: ${(total / 1024 / 1024).toFixed(1)}MB`) diff --git a/apps/ssr/scripts/generate-font-data.ts b/apps/ssr/scripts/generate-font-data.ts new file mode 100644 index 000000000..fd6937899 --- /dev/null +++ b/apps/ssr/scripts/generate-font-data.ts @@ -0,0 +1,46 @@ +import fs from "node:fs" +import { createRequire } from "node:module" +import { fileURLToPath } from "node:url" + +import path, { dirname, resolve } from "pathe" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const require = createRequire(import.meta.url) + +const weights = [ + { name: "Thin", weight: 100 }, + { name: "ExtraLight", weight: 200 }, + { name: "Light", weight: 300 }, + { name: "Regular", weight: 400 }, + { name: "Italic", weight: 400 }, + { name: "Medium", weight: 500 }, + { name: "SemiBold", weight: 600 }, + { name: "Bold", weight: 700 }, + { name: "ExtraBold", weight: 800 }, + { name: "Black", weight: 900 }, +] as const + +const snFontDepsPath = require.resolve("@fontsource/sn-pro") +const snFontsDirPath = resolve(snFontDepsPath, "../files") +const snFontsDir = fs + .readdirSync(snFontsDirPath) + .filter((name) => !name.endsWith(".woff2") && !name.includes("italic")) + +const fontsData: Record = {} + +for (const file of snFontsDir) { + const weight = weights.find((w) => file.includes(w.weight.toString())) + if (!weight) continue + const data = fs.readFileSync(path.join(snFontsDirPath, file)) + fontsData[`sn-pro-${weight.weight}`] = data.toString("base64") +} + +// kose-font is too large (~24MB) to bundle, loaded from R2 at runtime instead + +const outDir = path.join(__dirname, "../.generated") +fs.mkdirSync(outDir, { recursive: true }) +fs.writeFileSync( + path.join(outDir, "fonts-data.ts"), + `export default ${JSON.stringify(fontsData)} as Record`, +) +console.info("Generated fonts-data.ts with", Object.keys(fontsData).length, "fonts") diff --git a/apps/ssr/scripts/patch-worker-build.ts b/apps/ssr/scripts/patch-worker-build.ts new file mode 100644 index 000000000..a9624088b --- /dev/null +++ b/apps/ssr/scripts/patch-worker-build.ts @@ -0,0 +1,24 @@ +import fs from "node:fs" + +import { dirname, join } from "pathe" + +const distDir = join(dirname(import.meta.url.replace("file://", "")), "../dist/worker") + +const files = fs.readdirSync(distDir).filter((f) => f.endsWith(".mjs")) + +for (const file of files) { + const filePath = join(distDir, file) + const code = fs.readFileSync(filePath, "utf-8") + + // Fix createRequire(import.meta.url) - import.meta.url is undefined in Cloudflare Workers + // Provide a fallback URL so createRequire can initialize properly + const patched = code.replaceAll( + "createRequire(import.meta.url)", + 'createRequire(import.meta.url || "file:///worker.mjs")', + ) + + if (patched !== code) { + fs.writeFileSync(filePath, patched) + console.info(`Patched createRequire in ${file}`) + } +} diff --git a/apps/ssr/scripts/upload-fonts-to-r2.ts b/apps/ssr/scripts/upload-fonts-to-r2.ts new file mode 100644 index 000000000..1bb145357 --- /dev/null +++ b/apps/ssr/scripts/upload-fonts-to-r2.ts @@ -0,0 +1,36 @@ +import { execSync } from "node:child_process" +import fs from "node:fs" +import { createRequire } from "node:module" + +import path, { resolve } from "pathe" + +const require = createRequire(import.meta.url) + +const BUCKET = "follow" +const PREFIX = "ssr-fonts" +const ACCOUNT_ID = "1f1d1678a2413a54c944b3081bab5c84" + +const snFontDepsPath = require.resolve("@fontsource/sn-pro") +const snFontsDirPath = resolve(snFontDepsPath, "../files") +const snFontsDir = fs + .readdirSync(snFontsDirPath) + .filter((name) => !name.endsWith(".woff2") && !name.includes("italic")) + +for (const file of snFontsDir) { + const filePath = path.join(snFontsDirPath, file) + const key = `${PREFIX}/${file}` + console.info(`Uploading ${file}...`) + execSync( + `CLOUDFLARE_ACCOUNT_ID=${ACCOUNT_ID} npx wrangler r2 object put ${BUCKET}/${key} --file "${filePath}" --remote`, + { stdio: "inherit" }, + ) +} + +const koseFontPath = require.resolve("kose-font") +console.info("Uploading kose-font.ttf...") +execSync( + `CLOUDFLARE_ACCOUNT_ID=${ACCOUNT_ID} npx wrangler r2 object put ${BUCKET}/${PREFIX}/kose-font.ttf --file "${koseFontPath}" --remote`, + { stdio: "inherit" }, +) + +console.info("All fonts uploaded successfully!") diff --git a/apps/ssr/src/lib/load-env.worker.ts b/apps/ssr/src/lib/load-env.worker.ts new file mode 100644 index 000000000..daa7f8835 --- /dev/null +++ b/apps/ssr/src/lib/load-env.worker.ts @@ -0,0 +1,2 @@ +// No-op for Cloudflare Workers - environment variables are provided via wrangler config +export {} diff --git a/apps/ssr/src/lib/og/fonts.worker.ts b/apps/ssr/src/lib/og/fonts.worker.ts new file mode 100644 index 000000000..5b9fb6267 --- /dev/null +++ b/apps/ssr/src/lib/og/fonts.worker.ts @@ -0,0 +1,66 @@ +import fontsBase64Data from "../../../.generated/fonts-data" + +let cachedFonts: any[] | null = null +let koseFont: any | null = null + +// Global reference to R2 bucket, set by worker-entry.ts +let _fontsBucket: R2Bucket | null = null +export function setFontsBucket(bucket: R2Bucket) { + _fontsBucket = bucket +} + +function decodeSNProFonts(): any[] { + const fontsData: any[] = [] + for (const [key, base64] of Object.entries(fontsBase64Data)) { + if (key === "kose-400") continue + const weightStr = key.split("-").pop()! + const weight = Number.parseInt(weightStr) + const buf = Uint8Array.from(atob(base64), (c) => c.codePointAt(0)!) + fontsData.push({ + name: "SN Pro", + data: buf.buffer, + weight, + style: "normal" as const, + }) + } + return fontsData +} + +async function loadKoseFont(): Promise { + if (koseFont) return koseFont + if (!_fontsBucket) { + console.warn("R2 fonts bucket not configured, skipping kose-font") + return null + } + try { + const obj = await _fontsBucket.get("ssr-fonts/kose-font.ttf") + if (!obj) { + console.warn("Kose font not found in R2") + return null + } + const data = await obj.arrayBuffer() + koseFont = { + name: "Kose", + data, + weight: 400, + style: "normal" as const, + } + return koseFont + } catch (e) { + console.error("Failed to load kose font from R2:", e) + return null + } +} + +export async function getFonts(): Promise { + if (cachedFonts) return cachedFonts + + const snProFonts = decodeSNProFonts() + const kose = await loadKoseFont() + + cachedFonts = kose ? [...snProFonts, kose] : snProFonts + return cachedFonts +} + +// Default export for compatibility with original fonts module API +export default [] as any[] diff --git a/apps/ssr/src/lib/og/render-to-image.worker.ts b/apps/ssr/src/lib/og/render-to-image.worker.ts new file mode 100644 index 000000000..2048f7bfa --- /dev/null +++ b/apps/ssr/src/lib/og/render-to-image.worker.ts @@ -0,0 +1,33 @@ +import type { ReactElement } from "react" +import type { SatoriOptions } from "satori" +import satori from "satori" + +import { getFonts } from "./fonts.worker" +import { ensureInitialized, Resvg } from "./resvg-wasm-shim" + +export async function renderToImage( + node: ReactElement, + options: { + width?: number + height: number + debug?: boolean + fonts?: SatoriOptions["fonts"] + }, +) { + await ensureInitialized() + + const fonts = options.fonts || (await getFonts()) + + const svg = await satori(node, { + ...options, + fonts, + }) + + const w = new Resvg(svg) + const image = w.render().asPng() + + return { + image, + contentType: "image/png", + } +} diff --git a/apps/ssr/src/lib/og/resvg-wasm-shim.ts b/apps/ssr/src/lib/og/resvg-wasm-shim.ts new file mode 100644 index 000000000..65017bb0b --- /dev/null +++ b/apps/ssr/src/lib/og/resvg-wasm-shim.ts @@ -0,0 +1,22 @@ +import { initWasm } from "@resvg/resvg-wasm" + +let initialized = false + +let _wasmModule: WebAssembly.Module | null = null + +export function setWasmModule(mod: WebAssembly.Module) { + _wasmModule = mod +} + +async function ensureInitialized() { + if (!initialized) { + if (_wasmModule) { + await initWasm(_wasmModule) + } + initialized = true + } +} + +export { ensureInitialized } + +export { Resvg } from "@resvg/resvg-wasm" diff --git a/apps/ssr/src/lib/worker-request-context.ts b/apps/ssr/src/lib/worker-request-context.ts new file mode 100644 index 000000000..8c7bd16fd --- /dev/null +++ b/apps/ssr/src/lib/worker-request-context.ts @@ -0,0 +1,43 @@ +import { AsyncLocalStorage } from "node:async_hooks" + +// Shim for @fastify/request-context that works in Workers +// Uses AsyncLocalStorage to provide per-request context + +const storage = new AsyncLocalStorage>() + +export const requestContext = { + get(key: string) { + const store = storage.getStore() + return store?.get(key) + }, + set(key: string, value: any) { + const store = storage.getStore() + store?.set(key, value) + }, +} + +export function runWithRequestContext(fn: () => T | Promise): T | Promise { + const store = new Map() + return storage.run(store, fn) +} + +// Provide a req-like object with requestContext for compatibility +export function createRequestProxy( + url: string, + headers: Record, + params: Record = {}, +) { + return { + originalUrl: url, + headers, + params, + requestContext: { + get(key: string) { + return requestContext.get(key) + }, + set(key: string, value: any) { + requestContext.set(key, value) + }, + }, + } +} diff --git a/apps/ssr/tsconfig.json b/apps/ssr/tsconfig.json index ee71e60f4..5ee15d7bc 100644 --- a/apps/ssr/tsconfig.json +++ b/apps/ssr/tsconfig.json @@ -40,5 +40,5 @@ "./tailwind.config.ts", "./helper/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "./src/**/*.worker.ts"] } diff --git a/apps/ssr/tsdown.worker.config.ts b/apps/ssr/tsdown.worker.config.ts new file mode 100644 index 000000000..c6f6b1dc7 --- /dev/null +++ b/apps/ssr/tsdown.worker.config.ts @@ -0,0 +1,28 @@ +import { dirname, resolve } from "pathe" +import { defineConfig } from "tsdown" + +const __dirname = dirname(import.meta.url.replace("file://", "")) + +export default defineConfig({ + entry: ["./worker-entry.ts"], + outDir: "dist/worker", + + clean: true, + format: ["esm"], + external: ["node:*", /\.wasm$/], + noExternal: ["**"], + treeshake: true, + splitting: false, + + alias: { + "./src/lib/og/render-to-image": "./src/lib/og/render-to-image.worker", + "./src/lib/og/fonts": "./src/lib/og/fonts.worker", + "../../lib/og/render-to-image": "../../lib/og/render-to-image.worker", + "./src/lib/load-env": "./src/lib/load-env.worker", + "@fastify/request-context": resolve(__dirname, "src/lib/worker-request-context.ts"), + }, + + define: { + __DEV__: JSON.stringify(false), + }, +}) diff --git a/apps/ssr/worker-app.ts b/apps/ssr/worker-app.ts new file mode 100644 index 000000000..93295f5c1 --- /dev/null +++ b/apps/ssr/worker-app.ts @@ -0,0 +1,71 @@ +import { fastifyRequestContext } from "@fastify/request-context" +import { env } from "@follow/shared/env.ssr" +import type { FastifyRequest } from "fastify" +import Fastify from "fastify" +import { nanoid } from "nanoid" +import { FetchError } from "ofetch" + +import { MetaError } from "./src/meta-handler" +import { globalRoute } from "./src/router/global" +import { ogRoute } from "./src/router/og" + +declare module "@fastify/request-context" { + interface RequestContextData { + req: FastifyRequest + upstreamEnv: "prod" | "dev" + upstreamOrigin: string + } +} + +export const createApp = () => { + const app = Fastify({}) + + // Test: minimal route to verify Fastify works in Workers + app.get("/healthz", async () => ({ ok: true })) + + app.register(fastifyRequestContext) + + app.after(() => { + app.setErrorHandler(function (err, req, reply) { + this.log.error(err) + + const traceId = nanoid(8) + + if (err instanceof FetchError) { + reply + .status((err as FetchError).response?.status || 500) + .send({ ok: false, traceId, message: err.message }) + } else if (err instanceof MetaError) { + reply.status(err.status).send({ ok: false, traceId, message: err.metaMessage }) + } else { + const message = (err as any).message || "Internal Server Error" + const status = Number.parseInt((err as any).code as string) || 500 + reply.status(status).send({ ok: false, message, traceId }) + } + }) + + app.addHook("onRequest", (req, reply, done) => { + req.requestContext.set("req", req) + + const { host } = req.headers + + const forwardedHost = req.headers["x-forwarded-host"] + const finalHost = forwardedHost || host + + const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" + req.requestContext.set("upstreamEnv", upstreamEnv) + if (upstreamEnv === "prod") { + req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) + } else { + req.requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) + } + reply.header("x-handled-host", finalHost) + done() + }) + + ogRoute(app) + globalRoute(app) + }) + + return app +} diff --git a/apps/ssr/worker-entry.ts b/apps/ssr/worker-entry.ts new file mode 100644 index 000000000..f29f62b09 --- /dev/null +++ b/apps/ssr/worker-entry.ts @@ -0,0 +1,334 @@ +import "./global" + +// Global route dependencies +import { env } from "@follow/shared/env.ssr" +import { Hono } from "hono" +import { minify } from "html-minifier-terser" +import { parseHTML } from "linkedom" +import { FetchError } from "ofetch" +import xss from "xss" + +// @ts-expect-error - WASM import handled by Wrangler +import resvgWasm from "./resvg.wasm" +// OG image rendering +import { createFollowClient } from "./src/lib/api-client" +import { NotFoundError } from "./src/lib/not-found" +import { setFontsBucket } from "./src/lib/og/fonts.worker" +import { setWasmModule } from "./src/lib/og/resvg-wasm-shim" +import { buildSeoMetaTags } from "./src/lib/seo" +import { + createRequestProxy, + requestContext, + runWithRequestContext, +} from "./src/lib/worker-request-context" +import { injectMetaHandler, MetaError } from "./src/meta-handler" +import { renderFeedOG } from "./src/router/og/feed" +import { renderListOG } from "./src/router/og/list" +import { renderUserOG } from "./src/router/og/user" + +Object.assign(globalThis, { + __DEV__: false, +}) + +// Initialize WASM module +setWasmModule(resvgWasm) + +interface Env { + FONTS_BUCKET: R2Bucket + ASSETS: Fetcher + VITE_API_URL: string + VITE_WEB_URL: string + VITE_EXTERNAL_DEV_API_URL: string + VITE_EXTERNAL_PROD_API_URL: string + VITE_WEB_DEV_URL: string + VITE_SENTRY_DSN: string +} + +const app = new Hono<{ Bindings: Env }>() + +let envInitialized = false + +// Redirects (migrated from vercel.json) +app.get("/feed/:id", (c) => { + return c.redirect(`/share/feeds/${c.req.param("id")}`, 301) +}) +app.get("/list/:id", (c) => { + return c.redirect(`/share/lists/${c.req.param("id")}`, 301) +}) +app.get("/profile/:path{.*}", (c) => { + return c.redirect(`/share/users/${c.req.param("path")}`, 301) +}) + +// Middleware: set up env vars and request context +app.use("*", async (c, next) => { + if (!envInitialized) { + const bindings = c.env + for (const [key, value] of Object.entries(bindings)) { + if (typeof value === "string") { + process.env[key] = value + } + } + if (bindings.FONTS_BUCKET) { + setFontsBucket(bindings.FONTS_BUCKET) + } + envInitialized = true + } + + return runWithRequestContext(async () => { + // Determine upstream env from host + const host = c.req.header("host") || "" + const forwardedHost = c.req.header("x-forwarded-host") + const finalHost = forwardedHost || host + const upstreamEnv = + finalHost === "dev.folo.is" || finalHost?.includes("folo-ssr-dev") ? "dev" : "prod" + + // Create a req-like proxy for compatibility with existing modules + const headers: Record = {} + c.req.raw.headers.forEach((value, key) => { + headers[key] = value + }) + + const reqProxy = createRequestProxy( + c.req.path + (c.req.raw.url.includes("?") ? `?${c.req.raw.url.split("?")[1]}` : ""), + headers, + ) + + // Set request context values + requestContext.set("req", reqProxy) + requestContext.set("upstreamEnv", upstreamEnv) + if (upstreamEnv === "prod") { + requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) + } else { + requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) + } + + await next() + + c.header("x-handled-host", finalHost) + }) +}) + +// OG image route +app.get("/og/:type/:id", async (c) => { + const type = c.req.param("type") + const id = c.req.param("id") + + const apiClient = createFollowClient() + let imageRes: { image: Buffer; contentType: string } | null = null + + try { + switch (type) { + case "feed": { + imageRes = await renderFeedOG(apiClient, id) + break + } + case "user": { + imageRes = await renderUserOG(apiClient, id) + break + } + case "list": { + imageRes = await renderListOG(apiClient, id) + break + } + default: { + return c.text("Not found", 404) + } + } + } catch (e: any) { + if (typeof e === "number") { + return c.text(e === 404 ? "Not found" : "Internal server error", e) + } + console.error("OG render error:", e) + return c.text(e?.message || "Internal server error", 500) + } + + if (!imageRes) { + return c.text("Not found", 404) + } + + return new Response(imageRes.image, { + headers: { + "Content-Type": imageRes.contentType, + "Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + "Cloudflare-CDN-Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + "CDN-Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + }, + }) +}) + +// SSR routes - use SSR template with meta injection +// These routes correspond to the vercel.json rewrites to follow-external-ssr +const ssrHandler = async (c: any) => { + // @ts-ignore - dynamic import of generated template + const template = await import("./.generated/index.template").then((m) => m.default) + const { document } = parseHTML(template) + + // Inject meta tags + try { + await injectMetaToTemplate(document, c) + } catch (e) { + console.error("inject meta error", e) + + if (e instanceof NotFoundError) { + c.status(404) + document.documentElement.dataset.notFound = "true" + } else if (e instanceof FetchError && e.response?.status) { + c.status(e.response.status as any) + } else if (e instanceof MetaError) { + return c.json({ ok: false, message: e.metaMessage }, e.status as any) + } + } + + injectEnvToDocument(document) + + const html = await minify(document.toString(), { + removeComments: true, + html5: true, + minifyJS: true, + minifyCSS: true, + removeTagWhitespace: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + collapseInlineTagWhitespace: true, + }) + + return c.html(html) +} + +app.get("/share/*", ssrHandler) +app.get("/login", ssrHandler) +app.get("/register", ssrHandler) +app.get("/forget-password", ssrHandler) +app.get("/reset-password", ssrHandler) + +// SPA catch-all - fetch index.html from Assets and inject env vars +app.get("*", async (c) => { + const assetResponse = await c.env.ASSETS.fetch(new Request("http://fakehost/index.html")) + const spaHtml = await assetResponse.text() + const { document } = parseHTML(spaHtml) + + injectEnvToDocument(document) + + const html = document.toString() + return c.html(html) +}) + +// Error handling +app.onError((err, c) => { + console.error(err) + + if (err instanceof FetchError) { + return c.json({ ok: false, message: err.message }, (err.response?.status as any) || 500) + } + if (err instanceof MetaError) { + return c.json({ ok: false, message: err.metaMessage }, err.status as any) + } + + return c.json({ ok: false, message: err.message || "Internal Server Error" }, 500) +}) + +export default app + +// Helper: inject env vars into HTML document +function injectEnvToDocument(document: any) { + const upstreamEnv = requestContext.get("upstreamEnv") as string + const upstreamOrigin = requestContext.get("upstreamOrigin") as string + + if (upstreamEnv) { + document.head.prepend(document.createComment(`upstreamEnv: ${upstreamEnv}`)) + + const injectScript = (apiUrl: string) => { + const scriptContent = `function injectEnv(env2) { + for (const key in env2) { + if (env2[key] === void 0) continue; + globalThis["__followEnv"] ??= {}; + globalThis["__followEnv"][key] = env2[key]; + } + } +injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_WEB_URL":"${upstreamOrigin}"})` + const $script = document.createElement("script") + $script.innerHTML = scriptContent + document.head.prepend($script) + } + if (upstreamEnv === "dev" && env.VITE_EXTERNAL_DEV_API_URL) { + injectScript(env.VITE_EXTERNAL_DEV_API_URL) + } + if (upstreamEnv === "prod" && env.VITE_EXTERNAL_PROD_API_URL) { + injectScript(env.VITE_EXTERNAL_PROD_API_URL) + } + } +} + +// Helper: inject meta tags into HTML document +async function injectMetaToTemplate(document: Document, c: any) { + // Create a req/res proxy compatible with injectMetaHandler + const reqProxy = requestContext.get("req") + const resProxy = { + status(code: number) { + c.status(code) + }, + raw: { statusMessage: "" }, + } + + const injectMetadata = await injectMetaHandler(reqProxy as any, resProxy as any) + + if (!injectMetadata) return document + + for (const meta of injectMetadata) { + switch (meta.type) { + case "openGraph": { + const $metaArray = buildSeoMetaTags(document, { openGraph: meta }) + for (const $meta of $metaArray) { + document.head.append($meta) + } + break + } + case "meta": { + const $oldMeta = document.querySelector(`meta[name="${meta.property}"]`) + if ($oldMeta) { + $oldMeta.setAttribute("content", xss(meta.content)) + } else { + const $meta = document.createElement("meta") + $meta.setAttribute("name", meta.property) + $meta.setAttribute("content", xss(meta.content)) + document.head.append($meta) + } + break + } + case "title": { + if (meta.title) { + const $title = document.querySelector("title") + if ($title) { + $title.textContent = `${xss(meta.title)} | Folo` + } else { + const $head = document.querySelector("head") + if ($head) { + const $title = document.createElement("title") + $title.textContent = `${xss(meta.title)} | Folo` + $head.append($title) + } + } + } + break + } + case "description": { + const $meta = document.createElement("meta") + $meta.setAttribute("name", "description") + $meta.setAttribute("content", xss(meta.description)) + document.head.append($meta) + break + } + case "hydrate": { + const script = document.createElement("script") + script.innerHTML = ` + window.__HYDRATE__ = window.__HYDRATE__ || {} + window.__HYDRATE__[${JSON.stringify(meta.key)}] = JSON.parse(${JSON.stringify(JSON.stringify(meta.data))}) + ` + document.head.append(script) + break + } + } + } + + return document +} diff --git a/apps/ssr/wrangler.jsonc b/apps/ssr/wrangler.jsonc new file mode 100644 index 000000000..11092ff3e --- /dev/null +++ b/apps/ssr/wrangler.jsonc @@ -0,0 +1,74 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "folo-ssr", + "main": "dist/worker/worker-entry.mjs", + "compatibility_date": "2026-02-01", + "compatibility_flags": ["nodejs_compat"], + "account_id": "1f1d1678a2413a54c944b3081bab5c84", + "placement": { + "mode": "smart", + }, + "limits": { + "cpu_ms": 30000, + }, + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": true, + }, + ], + "assets": { + "directory": "../desktop/out/web", + "not_found_handling": "none", + "html_handling": "none", + "binding": "ASSETS", + }, + "workers_dev": true, + "routes": [ + { + "pattern": "app.folo.is/*", + "zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63", + }, + ], + "r2_buckets": [ + { + "binding": "FONTS_BUCKET", + "bucket_name": "follow", + }, + ], + "vars": { + "VITE_API_URL": "https://api.folo.is", + "VITE_WEB_URL": "https://app.folo.is", + "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", + "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", + "VITE_WEB_DEV_URL": "https://dev.folo.is", + "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", + }, + "env": { + "dev": { + "name": "folo-ssr-dev", + "workers_dev": true, + "routes": [ + { + "pattern": "dev.folo.is/*", + "zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63", + }, + ], + "r2_buckets": [ + { + "binding": "FONTS_BUCKET", + "bucket_name": "follow", + }, + ], + "vars": { + "VITE_API_URL": "https://api.dev.folo.is", + "VITE_WEB_URL": "https://dev.folo.is", + "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", + "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", + "VITE_WEB_DEV_URL": "https://dev.folo.is", + "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", + }, + }, + }, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4d8098a3..6196896bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,7 +335,7 @@ importers: version: 4.3.0 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow-app/readability': specifier: workspace:* version: link:../../../../packages/readability @@ -447,7 +447,7 @@ importers: version: 3.0.2(electron@38.3.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -769,7 +769,7 @@ importers: version: link:../../../../packages/internal/utils '@folo-services/ai-tools': specifier: 'catalog:' - version: 0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3) + version: 0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) '@types/node': specifier: 25.2.3 version: 25.2.3 @@ -802,7 +802,7 @@ importers: version: 4.1.1(@types/react@19.1.17)(react@19.1.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/components': specifier: workspace:* version: link:../../packages/internal/components @@ -1183,7 +1183,7 @@ importers: version: 6.2.1 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/tracker': specifier: workspace:* version: link:../../packages/internal/tracker @@ -1302,6 +1302,9 @@ importers: '@follow/utils': specifier: workspace:* version: link:../../packages/internal/utils + '@resvg/resvg-wasm': + specifier: 2.6.2 + version: 2.6.2 '@types/html-minifier-terser': specifier: 7.0.2 version: 7.0.2 @@ -1326,6 +1329,9 @@ importers: foxact: specifier: 0.2.52 version: 0.2.52(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + hono: + specifier: 4.12.1 + version: 4.12.1 html-minifier-terser: specifier: 7.2.0 version: 7.2.0 @@ -1359,6 +1365,9 @@ importers: vite-plugin-route-builder: specifier: 0.4.1 version: 0.4.1 + wrangler: + specifier: 4.67.0 + version: 4.67.0(bufferutil@4.1.0) packages/configs: dependencies: @@ -1678,7 +1687,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1690,7 +1699,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1762,7 +1771,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1793,10 +1802,10 @@ importers: version: 2.0.0(@types/node@25.2.3) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@folo-services/drizzle': specifier: 0.1.44 - version: 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + version: 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) '@t3-oss/env-core': specifier: 0.13.10 version: 0.13.10(typescript@5.9.3)(zod@3.25.76) @@ -1823,7 +1832,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1872,7 +1881,7 @@ importers: devDependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -2820,6 +2829,49 @@ packages: '@clack/prompts@0.8.2': resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.14.0': + resolution: {integrity: sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260218.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260219.0': + resolution: {integrity: sha512-k+xM+swQBQnkrvwobjRPxyeYwjLSludJusR0PqeHe+h6X9QIRGgw3s1AO38lXQsqzMSgG5709oOXSF19NKVVaQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + resolution: {integrity: sha512-EyfQdsG1KcIVAf4qndT00LZly7sLFm1VxMWHBvOFB/EVYF2sE5HZ0dPbe+yrax5p3eS0oLZthR8ynhz4UulMUQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260219.0': + resolution: {integrity: sha512-N0UHXILYYa6htFO/uC92uAqusvynbSbOcHcrVXMKqP9Jy7eqXGMovyKIrNgzYnKIszNB+0lfUYdGI3Wci07LuA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + resolution: {integrity: sha512-835pjQ9uuAtwPBOAkPf+oxH3mNE5mqWuE3H7hJsul7WZsRD2FDcariyoT2AW6xyOePILrn4uMnmG1KGc9m/8Pg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260219.0': + resolution: {integrity: sha512-i7qcuOsuAxqqn1n5Ar3Rh1dHUL9vNmpF9FcdMTT84jIrdm5UNrPZz5grJthPmpB9LTcreT9iiP6qKbzGjnCwPA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@code-inspector/core@1.4.2': resolution: {integrity: sha512-7OPkFtkfYaXhuTlwub2jT++rW7VggMMEeqsPIZGvHdXykwKAtzB8nnrj3N3uBT/mRoFfP627ShrVyRzCqyfr2w==} @@ -4601,111 +4653,248 @@ packages: '@ide/backoff@1.0.0': resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@innei/react-resizable-layout@0.7.3-fork.1': resolution: {integrity: sha512-xBgY0+prk58+Hq235dmdi9AI60xYxqGEYxP44t2+3IqZoa/MUNC3FbGz1ICFGhaQurY/hUPQGOawZNVmcw8tNw==} peerDependencies: @@ -5228,6 +5417,15 @@ packages: resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} engines: {node: '>=16.14'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posthog/core@1.22.0': resolution: {integrity: sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==} @@ -6135,6 +6333,10 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} + '@resvg/resvg-wasm@2.6.2': + resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} + engines: {node: '>= 10'} + '@rolldown/binding-android-arm64@1.0.0-rc.3': resolution: {integrity: sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6495,12 +6697,19 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@splinetool/react-spline@4.1.0': resolution: {integrity: sha512-Y379gm17gw+1nxT/YXTCJnVIWuu7tsUH1tp/YxsYb0pZnc9Gljk7Om4Kpq7WPq0bZ4zidVCxf6xn6jgDcbHifQ==} peerDependencies: @@ -7816,6 +8025,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bluebird-lst@1.0.9: resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} @@ -9247,6 +9459,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -10663,8 +10878,8 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.11.10: - resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} + hono@4.12.1: + resolution: {integrity: sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==} engines: {node: '>=16.9.0'} hookable@6.0.1: @@ -12110,6 +12325,11 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + miniflare@4.20260219.0: + resolution: {integrity: sha512-EIb5wXbWUnnC60XU2aiFOPNd4fgTXzECkwRSOXZ1vdcY9WZaEE9rVf+h+Apw+WkOHRkp3Dr9/ZhQ5y1R+9iZ4Q==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -13236,6 +13456,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -14383,6 +14604,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -14803,6 +15028,10 @@ packages: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} engines: {node: '>=14.0.0'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -15374,6 +15603,13 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -15932,10 +16168,25 @@ packages: workbox-window@7.4.0: resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} + workerd@1.20260219.0: + resolution: {integrity: sha512-l4U4iT5H8jNV6+EK23ExnUV2z6JvqQtQPrT8XCm4G8RpwC9EPpYTOO9s/ImMPJKe1WSbQUQoJ4k8Nd83fz8skQ==} + engines: {node: '>=16'} + hasBin: true + workspace-root@3.3.1: resolution: {integrity: sha512-t/KqdJ8+Fa6ovo2o0tV2+WKfd+5gg2SP59MicW8dnwiN6A2huw73yQrnFyrFokJLPJ+ItwJHUjjhNvabUvl/gg==} hasBin: true + wrangler@4.67.0: + resolution: {integrity: sha512-58OoVth7bqm0nqsRgcI67gHbpp0IfR1JIBqDY0XR1FzRu9Qkjn6v2iJAdFf82QcVBFhaMBYQi88WqYGswq5wlQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260219.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -15985,6 +16236,18 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -16105,6 +16368,12 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zeego@3.0.6: resolution: {integrity: sha512-vg0GCMPYg6or/J91bwRnUpIYwz7QnhkyeKOdd3FjvICg+Gzq2D5QhD8k5RUSv1B+048LpNmNYdLm8qJVIbBONw==} peerDependencies: @@ -17254,6 +17523,29 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260219.0 + + '@cloudflare/workerd-darwin-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260219.0': + optional: true + '@code-inspector/core@1.4.2': dependencies: '@vue/compiler-dom': 3.5.28 @@ -19562,12 +19854,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@follow-app/client-sdk@0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@follow-app/client-sdk@0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@folo-services/constants': 0.1.53 - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) '@folo-services/exceptions': 0.1.28 - '@folo-services/shared': 0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3) + '@folo-services/shared': 0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3) better-auth: 1.3.28(better-sqlite3@12.6.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zod: 4.1.12 transitivePeerDependencies: @@ -19611,10 +19903,10 @@ snapshots: - svelte - vue - '@folo-services/ai-tools@0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)': + '@folo-services/ai-tools@0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3)': dependencies: '@ai-sdk/openai': 2.0.63(zod@4.1.12) - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) ai: 5.0.88(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) zod: 4.1.12 @@ -19655,10 +19947,10 @@ snapshots: dependencies: zod: 4.1.12 - '@folo-services/drizzle@0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)': + '@folo-services/drizzle@0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)': dependencies: '@folo-services/exceptions': 0.1.28 - '@hono/zod-openapi': 1.1.4(hono@4.11.10)(zod@4.1.12) + '@hono/zod-openapi': 1.1.4(hono@4.12.1)(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3))(zod@4.1.12) nanoid: 5.1.6 @@ -19698,11 +19990,11 @@ snapshots: '@folo-services/exceptions@0.1.28': {} - '@folo-services/shared@0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)': + '@folo-services/shared@0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)': dependencies: '@folo-services/constants': 0.1.53 - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) - '@hono/zod-openapi': 1.1.4(hono@4.11.10)(zod@4.1.12) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) + '@hono/zod-openapi': 1.1.4(hono@4.12.1)(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3))(zod@4.1.12) zod: 4.1.12 @@ -19781,17 +20073,17 @@ snapshots: '@hexagon/base64@1.1.28': {} - '@hono/zod-openapi@1.1.4(hono@4.11.10)(zod@4.1.12)': + '@hono/zod-openapi@1.1.4(hono@4.12.1)(zod@4.1.12)': dependencies: '@asteasolutions/zod-to-openapi': 8.4.1(zod@4.1.12) - '@hono/zod-validator': 0.7.6(hono@4.11.10)(zod@4.1.12) - hono: 4.11.10 + '@hono/zod-validator': 0.7.6(hono@4.12.1)(zod@4.1.12) + hono: 4.12.1 openapi3-ts: 4.5.0 zod: 4.1.12 - '@hono/zod-validator@0.7.6(hono@4.11.10)(zod@4.1.12)': + '@hono/zod-validator@0.7.6(hono@4.12.1)(zod@4.1.12)': dependencies: - hono: 4.11.10 + hono: 4.12.1 zod: 4.1.12 '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.1.0))': @@ -19875,81 +20167,177 @@ snapshots: '@ide/backoff@1.0.0': {} + '@img/colour@1.0.0': {} + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.8.1 optional: true + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.5': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.5': + optional: true + '@innei/react-resizable-layout@0.7.3-fork.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 @@ -20809,6 +21197,18 @@ snapshots: dependencies: '@pnpm/constants': 7.1.1 + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@posthog/core@1.22.0': dependencies: cross-spawn: 7.0.6 @@ -21776,6 +22176,8 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@resvg/resvg-wasm@2.6.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.3': optional: true @@ -22083,6 +22485,8 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@sindresorhus/is@7.2.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -22091,6 +22495,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@speed-highlight/core@1.2.14': {} + '@splinetool/react-spline@4.1.0(@splinetool/runtime@1.12.58)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@splinetool/runtime': 1.12.58 @@ -23711,6 +24117,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + bluebird-lst@1.0.9: dependencies: bluebird: 3.7.2 @@ -25285,6 +25693,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -27326,7 +27736,7 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.10: {} + hono@4.12.1: {} hookable@6.0.1: {} @@ -29102,6 +29512,18 @@ snapshots: mimic-response@3.1.0: {} + miniflare@4.20260219.0(bufferutil@4.1.0): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260219.0 + ws: 8.18.0(bufferutil@4.1.0) + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.1 @@ -31454,6 +31876,37 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -31903,6 +32356,8 @@ snapshots: superstruct@2.0.2: {} + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -32479,6 +32934,12 @@ snapshots: undici@6.23.0: {} + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -33177,12 +33638,36 @@ snapshots: '@types/trusted-types': 2.0.7 workbox-core: 7.4.0 + workerd@1.20260219.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260219.0 + '@cloudflare/workerd-darwin-arm64': 1.20260219.0 + '@cloudflare/workerd-linux-64': 1.20260219.0 + '@cloudflare/workerd-linux-arm64': 1.20260219.0 + '@cloudflare/workerd-windows-64': 1.20260219.0 + workspace-root@3.3.1: dependencies: '@node-kit/lerna-workspace-root': 3.3.1 '@node-kit/pnpm-workspace-root': 3.3.1 '@node-kit/yarn-workspace-root': 3.3.1 + wrangler@4.67.0(bufferutil@4.1.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260219.0(bufferutil@4.1.0) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260219.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -33230,6 +33715,10 @@ snapshots: optionalDependencies: bufferutil: 4.1.0 + ws@8.18.0(bufferutil@4.1.0): + optionalDependencies: + bufferutil: 4.1.0 + ws@8.19.0(bufferutil@4.1.0): optionalDependencies: bufferutil: 4.1.0 @@ -33336,6 +33825,19 @@ snapshots: yoga-layout@3.2.1: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + zeego@3.0.6(@react-native-menu/menu@2.0.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react-native-ios-context-menu@3.2.1(react-native-ios-utilities@5.2.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native-ios-utilities@5.2.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)