A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove pds gatekeeper deployment assets

- Delete the gatekeeper and edge proxy app sources
- Remove the Railway deployment guide
- Prune the related pnpm lockfile entries

-1590
-27
apps/pds-edge-proxy/Caddyfile
··· 1 - :{$PORT:8080} { 2 - encode zstd gzip 3 - 4 - @gatekeeper { 5 - path /xrpc/com.atproto.server.describeServer 6 - path /xrpc/com.atproto.server.createAccount 7 - path /gate/* 8 - } 9 - 10 - handle @gatekeeper { 11 - reverse_proxy {$GATEKEEPER_BASE_URL} { 12 - header_up Host {http.request.host} 13 - header_up X-Forwarded-Host {http.request.host} 14 - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} 15 - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} 16 - header_up X-Real-IP {http.request.header.CF-Connecting-IP} 17 - } 18 - } 19 - 20 - reverse_proxy {$PDS_BASE_URL} { 21 - header_up Host {http.request.host} 22 - header_up X-Forwarded-Host {http.request.host} 23 - header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} 24 - header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} 25 - header_up X-Real-IP {http.request.header.CF-Connecting-IP} 26 - } 27 - }
-7
apps/pds-edge-proxy/Dockerfile
··· 1 - FROM caddy:2.10-alpine 2 - 3 - COPY apps/pds-edge-proxy/Caddyfile /etc/caddy/Caddyfile 4 - 5 - ENV PORT=8080 6 - 7 - EXPOSE 8080
-49
apps/pds-gatekeeper/Dockerfile
··· 1 - FROM node:22-bookworm-slim AS builder 2 - 3 - ENV PNPM_HOME=/pnpm 4 - ENV PATH=$PNPM_HOME:$PATH 5 - 6 - RUN corepack enable 7 - 8 - WORKDIR /app 9 - 10 - COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ 11 - COPY apps/pds-gatekeeper/package.json apps/pds-gatekeeper/package.json 12 - 13 - RUN pnpm install --frozen-lockfile --ignore-scripts --filter @opnshelf/pds-gatekeeper... 14 - 15 - COPY apps/pds-gatekeeper apps/pds-gatekeeper 16 - 17 - RUN pnpm --filter @opnshelf/pds-gatekeeper build 18 - 19 - # Verify the expected entry point exists so the build fails fast if the 20 - # output path ever drifts again. 21 - RUN test -f /app/apps/pds-gatekeeper/dist/server.js 22 - 23 - # --------------------------------------------------------------------------- 24 - 25 - FROM node:22-bookworm-slim AS runner 26 - 27 - ENV PNPM_HOME=/pnpm 28 - ENV PATH=$PNPM_HOME:$PATH 29 - 30 - RUN corepack enable 31 - 32 - WORKDIR /app 33 - 34 - COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ 35 - COPY apps/pds-gatekeeper/package.json apps/pds-gatekeeper/package.json 36 - 37 - RUN pnpm install --frozen-lockfile --ignore-scripts --filter @opnshelf/pds-gatekeeper... --prod 38 - 39 - # Copy the compiled output from the builder stage. 40 - COPY --from=builder /app/apps/pds-gatekeeper/dist /app/apps/pds-gatekeeper/dist 41 - 42 - WORKDIR /app/apps/pds-gatekeeper 43 - 44 - ENV NODE_ENV=production 45 - ENV PORT=8080 46 - 47 - EXPOSE 8080 48 - 49 - CMD ["node", "dist/server.js"]
-24
apps/pds-gatekeeper/package.json
··· 1 - { 2 - "name": "@opnshelf/pds-gatekeeper", 3 - "private": true, 4 - "type": "module", 5 - "scripts": { 6 - "dev": "tsx watch src/server.ts", 7 - "build": "tsc -p tsconfig.json", 8 - "start": "node dist/server.js", 9 - "test": "node --import tsx --test test/app.test.ts", 10 - "check": "node ./scripts/check.mjs" 11 - }, 12 - "dependencies": { 13 - "@fastify/formbody": "^8.0.2", 14 - "fastify": "^5.6.1", 15 - "sql.js": "^1.13.0" 16 - }, 17 - "devDependencies": { 18 - "@types/node": "^22.10.2", 19 - "@types/sql.js": "^1.4.10", 20 - "tsx": "^4.20.6", 21 - "typescript": "^5.7.2" 22 - }, 23 - "packageManager": "pnpm@10.30.2" 24 - }
-16
apps/pds-gatekeeper/scripts/check.mjs
··· 1 - import { spawnSync } from "node:child_process"; 2 - 3 - const unsupportedTurboArgs = new Set(["--write", "--unsafe"]); 4 - const forwardedArgs = process.argv 5 - .slice(2) 6 - .filter((arg) => !unsupportedTurboArgs.has(arg)); 7 - 8 - const result = spawnSync("tsc", ["--noEmit", "-p", "tsconfig.json", ...forwardedArgs], { 9 - stdio: "inherit", 10 - }); 11 - 12 - if (result.error) { 13 - throw result.error; 14 - } 15 - 16 - process.exit(result.status ?? 1);
-342
apps/pds-gatekeeper/src/app.ts
··· 1 - import crypto from "node:crypto"; 2 - import Fastify, { type FastifyInstance, type FastifyReply } from "fastify"; 3 - import fastifyFormbody from "@fastify/formbody"; 4 - import type { GatekeeperConfig } from "./config.js"; 5 - import { renderSignupPage } from "./html.js"; 6 - import { GateCodeStore } from "./store.js"; 7 - import { validateTurnstileToken } from "./turnstile.js"; 8 - 9 - export interface CreateAppOptions { 10 - config: GatekeeperConfig; 11 - store?: GateCodeStore; 12 - fetchImpl?: typeof fetch; 13 - } 14 - 15 - const HOP_BY_HOP_HEADERS = new Set([ 16 - "connection", 17 - "content-encoding", 18 - "content-length", 19 - "host", 20 - "keep-alive", 21 - "proxy-authenticate", 22 - "proxy-authorization", 23 - "te", 24 - "trailer", 25 - "transfer-encoding", 26 - "upgrade", 27 - ]); 28 - 29 - function jsonError( 30 - reply: FastifyReply, 31 - statusCode: number, 32 - error: string, 33 - message: string, 34 - ) { 35 - reply.code(statusCode).type("application/json"); 36 - return reply.send({ error, message }); 37 - } 38 - 39 - function normalizeRedirectUrl( 40 - config: Pick< 41 - GatekeeperConfig, 42 - "defaultCaptchaRedirect" | "allowedCaptchaRedirects" 43 - >, 44 - redirectUrl?: string, 45 - ) { 46 - if (!redirectUrl) { 47 - return config.defaultCaptchaRedirect; 48 - } 49 - 50 - try { 51 - const normalized = new URL(redirectUrl).toString().replace(/\/$/, ""); 52 - return config.allowedCaptchaRedirects.includes(normalized) 53 - ? normalized 54 - : config.defaultCaptchaRedirect; 55 - } catch { 56 - return config.defaultCaptchaRedirect; 57 - } 58 - } 59 - 60 - function remoteIpFromHeaders(headers: Record<string, unknown>) { 61 - const candidates = [ 62 - headers["cf-connecting-ip"], 63 - headers["x-real-ip"], 64 - headers["x-forwarded-for"], 65 - ]; 66 - 67 - for (const value of candidates) { 68 - if (typeof value === "string" && value.trim() !== "") { 69 - return value.split(",")[0]?.trim(); 70 - } 71 - } 72 - 73 - return undefined; 74 - } 75 - 76 - function buildErrorRedirect( 77 - handle: string, 78 - state: string, 79 - message: string, 80 - redirectUrl?: string, 81 - ) { 82 - const params = new URLSearchParams({ 83 - handle, 84 - state, 85 - error: message, 86 - }); 87 - 88 - if (redirectUrl) { 89 - params.set("redirect_url", redirectUrl); 90 - } 91 - 92 - return `/gate/signup?${params.toString()}`; 93 - } 94 - 95 - function copyRequestHeaders(headers: Record<string, unknown>) { 96 - const nextHeaders = new Headers(); 97 - 98 - for (const [key, value] of Object.entries(headers)) { 99 - if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) { 100 - continue; 101 - } 102 - 103 - if (typeof value === "string") { 104 - nextHeaders.set(key, value); 105 - continue; 106 - } 107 - 108 - if (Array.isArray(value)) { 109 - nextHeaders.set(key, value.join(", ")); 110 - } 111 - } 112 - 113 - return nextHeaders; 114 - } 115 - 116 - async function proxyResponse( 117 - reply: FastifyReply, 118 - response: Response, 119 - bodyOverride?: string, 120 - ) { 121 - reply.code(response.status); 122 - for (const [key, value] of response.headers.entries()) { 123 - if (HOP_BY_HOP_HEADERS.has(key.toLowerCase())) { 124 - continue; 125 - } 126 - reply.header(key, value); 127 - } 128 - 129 - const body = bodyOverride ?? (await response.text()); 130 - return reply.send(body); 131 - } 132 - 133 - export async function createApp({ 134 - config, 135 - fetchImpl = fetch, 136 - store, 137 - }: CreateAppOptions): Promise<FastifyInstance> { 138 - const resolvedStore = store ?? (await GateCodeStore.open(config)); 139 - const app = Fastify({ 140 - logger: true, 141 - disableRequestLogging: false, 142 - }); 143 - 144 - app.addHook("onClose", async () => { 145 - resolvedStore.close(); 146 - }); 147 - 148 - app.register(fastifyFormbody); 149 - 150 - app.get("/health", async () => ({ ok: true })); 151 - 152 - app.get("/", async () => ({ 153 - service: "pds-gatekeeper", 154 - pdsBaseUrl: config.pdsBaseUrl, 155 - signupProtectionEnabled: config.enableSignupProtection, 156 - })); 157 - 158 - app.get("/gate/signup", async (request, reply) => { 159 - const query = request.query as { 160 - error?: string; 161 - handle?: string; 162 - redirect_url?: string; 163 - state?: string; 164 - }; 165 - 166 - if (!query.handle || !query.state) { 167 - return jsonError( 168 - reply, 169 - 400, 170 - "InvalidRequest", 171 - "handle and state are required", 172 - ); 173 - } 174 - 175 - reply.type("text/html; charset=utf-8"); 176 - return reply.send( 177 - renderSignupPage(config, { 178 - handle: query.handle, 179 - state: query.state, 180 - errorMessage: query.error, 181 - redirectUrl: query.redirect_url, 182 - }), 183 - ); 184 - }); 185 - 186 - app.post("/gate/signup", async (request, reply) => { 187 - const query = request.query as { 188 - handle?: string; 189 - redirect_url?: string; 190 - state?: string; 191 - }; 192 - const body = request.body as { 193 - "cf-turnstile-response"?: string; 194 - redirect_url?: string; 195 - state?: string; 196 - }; 197 - 198 - const handle = query.handle; 199 - const state = query.state; 200 - const redirectUrl = body.redirect_url ?? query.redirect_url; 201 - 202 - if (!handle || !state) { 203 - return jsonError( 204 - reply, 205 - 400, 206 - "InvalidRequest", 207 - "handle and state are required", 208 - ); 209 - } 210 - 211 - const token = body["cf-turnstile-response"]; 212 - if (!token) { 213 - return reply.redirect( 214 - buildErrorRedirect( 215 - handle, 216 - state, 217 - "Verification failed. Please try again.", 218 - redirectUrl, 219 - ), 220 - ); 221 - } 222 - 223 - const remoteIp = remoteIpFromHeaders(request.headers); 224 - const validation = await validateTurnstileToken( 225 - config, 226 - token, 227 - remoteIp, 228 - fetchImpl, 229 - ); 230 - 231 - if (!validation.success) { 232 - request.log.warn( 233 - { errorCodes: validation.errorCodes, handle }, 234 - "turnstile validation failed", 235 - ); 236 - return reply.redirect( 237 - buildErrorRedirect( 238 - handle, 239 - state, 240 - "Verification failed. Please try again.", 241 - redirectUrl, 242 - ), 243 - ); 244 - } 245 - 246 - const code = resolvedStore.issueCode(handle); 247 - const successRedirectBase = normalizeRedirectUrl(config, redirectUrl); 248 - const successRedirect = new URL(successRedirectBase); 249 - successRedirect.searchParams.set("code", code); 250 - successRedirect.searchParams.set("state", state); 251 - 252 - return reply.redirect(successRedirect.toString()); 253 - }); 254 - 255 - app.get("/xrpc/com.atproto.server.describeServer", async (request, reply) => { 256 - const headers = copyRequestHeaders(request.headers); 257 - headers.set("accept", "application/json"); 258 - const response = await fetchImpl( 259 - `${config.pdsBaseUrl}/xrpc/com.atproto.server.describeServer`, 260 - { 261 - headers, 262 - method: "GET", 263 - }, 264 - ); 265 - 266 - const contentType = response.headers.get("content-type") ?? ""; 267 - if (!contentType.includes("application/json")) { 268 - return proxyResponse(reply, response); 269 - } 270 - 271 - const payload = (await response.json()) as Record<string, unknown>; 272 - payload.phoneVerificationRequired = config.enableSignupProtection; 273 - reply.type("application/json"); 274 - reply.code(response.status); 275 - return reply.send(payload); 276 - }); 277 - 278 - app.post("/xrpc/com.atproto.server.createAccount", async (request, reply) => { 279 - const payload = request.body as Record<string, unknown> | undefined; 280 - const handle = typeof payload?.handle === "string" ? payload.handle : undefined; 281 - const verificationCode = 282 - typeof payload?.verificationCode === "string" 283 - ? payload.verificationCode 284 - : undefined; 285 - 286 - if (!handle) { 287 - return jsonError( 288 - reply, 289 - 400, 290 - "InvalidRequest", 291 - "The 'handle' field is required.", 292 - ); 293 - } 294 - 295 - if (config.enableSignupProtection) { 296 - if (!verificationCode) { 297 - return jsonError( 298 - reply, 299 - 400, 300 - "InvalidRequest", 301 - "Verification is required on this server.", 302 - ); 303 - } 304 - 305 - const result = resolvedStore.consumeCode( 306 - handle, 307 - verificationCode, 308 - config.signupCodeTtlSeconds, 309 - ); 310 - 311 - if (!result.ok) { 312 - return jsonError( 313 - reply, 314 - result.reason === "expired" ? 400 : 400, 315 - result.reason === "expired" ? "ExpiredToken" : "InvalidToken", 316 - result.reason === "expired" 317 - ? "Token has expired" 318 - : "Token could not be verified", 319 - ); 320 - } 321 - } 322 - 323 - const headers = copyRequestHeaders(request.headers); 324 - headers.set("content-type", "application/json"); 325 - if (!headers.has("x-request-id")) { 326 - headers.set("x-request-id", crypto.randomUUID()); 327 - } 328 - 329 - const response = await fetchImpl( 330 - `${config.pdsBaseUrl}/xrpc/com.atproto.server.createAccount`, 331 - { 332 - method: "POST", 333 - headers, 334 - body: JSON.stringify(payload ?? {}), 335 - }, 336 - ); 337 - 338 - return proxyResponse(reply, response); 339 - }); 340 - 341 - return app; 342 - }
-125
apps/pds-gatekeeper/src/config.ts
··· 1 - import { mkdirSync } from "node:fs"; 2 - import { dirname } from "node:path"; 3 - 4 - export interface GatekeeperConfig { 5 - host: string; 6 - port: number; 7 - pdsBaseUrl: string; 8 - pdsHostname: string; 9 - enableSignupProtection: boolean; 10 - signupCodeTtlSeconds: number; 11 - turnstileSiteKey: string; 12 - turnstileSecretKey: string; 13 - turnstileExpectedHostname: string; 14 - turnstileExpectedAction: string; 15 - defaultCaptchaRedirect: string; 16 - allowedCaptchaRedirects: string[]; 17 - dbPath: string; 18 - } 19 - 20 - function readEnv(name: string): string { 21 - const value = process.env[name]; 22 - if (!value || value.trim() === "") { 23 - throw new Error(`${name} is required`); 24 - } 25 - return value.trim(); 26 - } 27 - 28 - function readOptionalEnv(name: string, fallback: string): string { 29 - const value = process.env[name]; 30 - return value && value.trim() !== "" ? value.trim() : fallback; 31 - } 32 - 33 - function readBooleanEnv(name: string, fallback: boolean): boolean { 34 - const value = process.env[name]; 35 - if (value === undefined) { 36 - return fallback; 37 - } 38 - 39 - return ["1", "true", "yes", "on"].includes(value.toLowerCase()); 40 - } 41 - 42 - function readIntegerEnv(name: string, fallback: number): number { 43 - const raw = process.env[name]; 44 - if (raw === undefined) { 45 - return fallback; 46 - } 47 - 48 - const parsed = Number.parseInt(raw, 10); 49 - if (!Number.isFinite(parsed) || parsed <= 0) { 50 - throw new Error(`${name} must be a positive integer`); 51 - } 52 - return parsed; 53 - } 54 - 55 - function normalizeBaseUrl(url: string): string { 56 - return url.endsWith("/") ? url.slice(0, -1) : url; 57 - } 58 - 59 - function parseRedirects(csv: string, defaultRedirect: string): string[] { 60 - const values = csv 61 - .split(",") 62 - .map((value) => value.trim()) 63 - .filter(Boolean); 64 - 65 - if (values.length === 0) { 66 - return [defaultRedirect]; 67 - } 68 - 69 - if (!values.includes(defaultRedirect)) { 70 - values.unshift(defaultRedirect); 71 - } 72 - 73 - return values; 74 - } 75 - 76 - export function loadConfig(): GatekeeperConfig { 77 - const pdsBaseUrl = normalizeBaseUrl(readEnv("PDS_BASE_URL")); 78 - const pdsHostname = readEnv("PDS_HOSTNAME"); 79 - const defaultCaptchaRedirect = readOptionalEnv( 80 - "GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT", 81 - "https://bsky.app", 82 - ); 83 - const dbPath = readOptionalEnv( 84 - "GATEKEEPER_DB_PATH", 85 - "/data/gatekeeper.sqlite", 86 - ); 87 - 88 - mkdirSync(dirname(dbPath), { recursive: true }); 89 - 90 - const config: GatekeeperConfig = { 91 - host: readOptionalEnv("HOST", "0.0.0.0"), 92 - port: readIntegerEnv("PORT", 8080), 93 - pdsBaseUrl, 94 - pdsHostname, 95 - enableSignupProtection: readBooleanEnv( 96 - "GATEKEEPER_ENABLE_SIGNUP_PROTECTION", 97 - true, 98 - ), 99 - signupCodeTtlSeconds: readIntegerEnv( 100 - "GATEKEEPER_SIGNUP_CODE_TTL_SECONDS", 101 - 300, 102 - ), 103 - turnstileSiteKey: readEnv("TURNSTILE_SITE_KEY"), 104 - turnstileSecretKey: readEnv("TURNSTILE_SECRET_KEY"), 105 - turnstileExpectedHostname: readOptionalEnv( 106 - "TURNSTILE_EXPECTED_HOSTNAME", 107 - pdsHostname, 108 - ), 109 - turnstileExpectedAction: readOptionalEnv( 110 - "TURNSTILE_EXPECTED_ACTION", 111 - "signup", 112 - ), 113 - defaultCaptchaRedirect, 114 - allowedCaptchaRedirects: parseRedirects( 115 - readOptionalEnv( 116 - "GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS", 117 - defaultCaptchaRedirect, 118 - ), 119 - defaultCaptchaRedirect, 120 - ), 121 - dbPath, 122 - }; 123 - 124 - return config; 125 - }
-185
apps/pds-gatekeeper/src/html.ts
··· 1 - import type { GatekeeperConfig } from "./config.js"; 2 - 3 - function escapeHtml(value: string): string { 4 - return value 5 - .replaceAll("&", "&amp;") 6 - .replaceAll("<", "&lt;") 7 - .replaceAll(">", "&gt;") 8 - .replaceAll('"', "&quot;") 9 - .replaceAll("'", "&#39;"); 10 - } 11 - 12 - export function renderSignupPage( 13 - config: Pick<GatekeeperConfig, "pdsHostname" | "turnstileExpectedAction" | "turnstileSiteKey">, 14 - options: { 15 - handle: string; 16 - state: string; 17 - errorMessage?: string; 18 - redirectUrl?: string; 19 - }, 20 - ): string { 21 - const errorBlock = options.errorMessage 22 - ? `<p class="message message-error">${escapeHtml(options.errorMessage)}</p>` 23 - : ""; 24 - const redirectInput = options.redirectUrl 25 - ? `<input type="hidden" name="redirect_url" value="${escapeHtml(options.redirectUrl)}" />` 26 - : ""; 27 - 28 - return `<!doctype html> 29 - <html lang="en"> 30 - <head> 31 - <meta charset="utf-8" /> 32 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 33 - <title>${escapeHtml(config.pdsHostname)} signup verification</title> 34 - <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> 35 - <style> 36 - :root { 37 - color-scheme: light dark; 38 - --bg: #f4f1ea; 39 - --panel: rgba(255, 255, 255, 0.9); 40 - --ink: #1f1a14; 41 - --muted: #675d52; 42 - --accent: #0f766e; 43 - --accent-strong: #115e59; 44 - --error-bg: rgba(185, 28, 28, 0.08); 45 - --error-ink: #991b1b; 46 - font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif; 47 - } 48 - @media (prefers-color-scheme: dark) { 49 - :root { 50 - --bg: #161311; 51 - --panel: rgba(31, 24, 20, 0.92); 52 - --ink: #f6eee5; 53 - --muted: #d1c4b2; 54 - --accent: #5eead4; 55 - --accent-strong: #99f6e4; 56 - --error-bg: rgba(248, 113, 113, 0.16); 57 - --error-ink: #fecaca; 58 - } 59 - } 60 - * { box-sizing: border-box; } 61 - body { 62 - margin: 0; 63 - min-height: 100vh; 64 - display: grid; 65 - place-items: center; 66 - padding: 24px; 67 - background: 68 - radial-gradient(circle at top, rgba(15, 118, 110, 0.15), transparent 36%), 69 - linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent), 70 - var(--bg); 71 - color: var(--ink); 72 - } 73 - .shell { 74 - width: min(100%, 560px); 75 - background: var(--panel); 76 - border: 1px solid rgba(120, 98, 76, 0.16); 77 - border-radius: 28px; 78 - padding: 32px; 79 - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.12); 80 - backdrop-filter: blur(12px); 81 - } 82 - .eyebrow { 83 - margin: 0 0 8px; 84 - letter-spacing: 0.16em; 85 - text-transform: uppercase; 86 - font-size: 0.78rem; 87 - color: var(--muted); 88 - } 89 - h1 { 90 - margin: 0; 91 - font-size: clamp(2rem, 5vw, 3rem); 92 - line-height: 0.96; 93 - } 94 - .lede { 95 - margin: 16px 0 24px; 96 - font-size: 1rem; 97 - line-height: 1.6; 98 - color: var(--muted); 99 - } 100 - .meta { 101 - display: grid; 102 - gap: 6px; 103 - margin: 0 0 24px; 104 - padding: 16px 18px; 105 - border-radius: 18px; 106 - background: rgba(15, 118, 110, 0.08); 107 - } 108 - .meta strong { 109 - font-size: 0.86rem; 110 - letter-spacing: 0.08em; 111 - text-transform: uppercase; 112 - color: var(--accent-strong); 113 - } 114 - .message { 115 - margin: 18px 0 0; 116 - padding: 12px 14px; 117 - border-radius: 14px; 118 - line-height: 1.5; 119 - } 120 - .message-error { 121 - background: var(--error-bg); 122 - color: var(--error-ink); 123 - } 124 - .submit-note { 125 - margin: 18px 0 0; 126 - font-size: 0.95rem; 127 - color: var(--muted); 128 - } 129 - .cf-turnstile { 130 - min-height: 70px; 131 - } 132 - </style> 133 - </head> 134 - <body> 135 - <main class="shell"> 136 - <p class="eyebrow">Protected signup</p> 137 - <h1>Verify you’re human.</h1> 138 - <p class="lede"> 139 - Complete the Turnstile check for <strong>${escapeHtml(options.handle)}</strong>. 140 - Once approved, you’ll be sent back to finish account creation on ${escapeHtml(config.pdsHostname)}. 141 - </p> 142 - <section class="meta"> 143 - <strong>Why this exists</strong> 144 - <span>This PDS uses a short-lived verification code to slow down automated signups without changing the ATProto account creation flow.</span> 145 - </section> 146 - <form id="gate-form" method="POST" action=""> 147 - <input type="hidden" name="state" value="${escapeHtml(options.state)}" /> 148 - ${redirectInput} 149 - <div 150 - class="cf-turnstile" 151 - data-sitekey="${escapeHtml(config.turnstileSiteKey)}" 152 - data-action="${escapeHtml(config.turnstileExpectedAction)}" 153 - data-theme="auto" 154 - data-callback="onTurnstileSuccess" 155 - data-error-callback="onTurnstileError" 156 - data-expired-callback="onTurnstileExpired" 157 - ></div> 158 - </form> 159 - <p class="submit-note">The form submits automatically after a successful check.</p> 160 - ${errorBlock} 161 - </main> 162 - <script> 163 - function updateError(message) { 164 - const url = new URL(window.location.href); 165 - url.searchParams.set("error", message); 166 - window.location.assign(url.toString()); 167 - } 168 - 169 - function onTurnstileSuccess() { 170 - window.setTimeout(function () { 171 - document.getElementById("gate-form").submit(); 172 - }, 200); 173 - } 174 - 175 - function onTurnstileError() { 176 - updateError("Verification failed. Please try again."); 177 - } 178 - 179 - function onTurnstileExpired() { 180 - updateError("Verification expired. Please try again."); 181 - } 182 - </script> 183 - </body> 184 - </html>`; 185 - }
-15
apps/pds-gatekeeper/src/server.ts
··· 1 - import { loadConfig } from "./config.js"; 2 - import { createApp } from "./app.js"; 3 - 4 - const config = loadConfig(); 5 - const app = await createApp({ config }); 6 - 7 - try { 8 - await app.listen({ 9 - host: config.host, 10 - port: config.port, 11 - }); 12 - } catch (error) { 13 - app.log.error(error); 14 - process.exit(1); 15 - }
-104
apps/pds-gatekeeper/src/store.ts
··· 1 - import crypto from "node:crypto"; 2 - import { existsSync, readFileSync, writeFileSync } from "node:fs"; 3 - import initSqlJs, { type Database, type SqlJsStatic } from "sql.js"; 4 - import type { GatekeeperConfig } from "./config.js"; 5 - 6 - export type ConsumeCodeResult = 7 - | { ok: true } 8 - | { ok: false; reason: "invalid" | "expired" }; 9 - 10 - export class GateCodeStore { 11 - private constructor( 12 - private readonly sql: SqlJsStatic, 13 - private readonly db: Database, 14 - private readonly dbPath: string, 15 - ) {} 16 - 17 - static async open( 18 - config: Pick<GatekeeperConfig, "dbPath" | "signupCodeTtlSeconds">, 19 - ): Promise<GateCodeStore> { 20 - const SQL = await initSqlJs(); 21 - const db = existsSync(config.dbPath) 22 - ? new SQL.Database(readFileSync(config.dbPath)) 23 - : new SQL.Database(); 24 - db.run(` 25 - CREATE TABLE IF NOT EXISTS gate_codes ( 26 - code TEXT PRIMARY KEY, 27 - handle TEXT NOT NULL, 28 - created_at_ms INTEGER NOT NULL 29 - ); 30 - CREATE INDEX IF NOT EXISTS idx_gate_codes_handle ON gate_codes(handle); 31 - CREATE INDEX IF NOT EXISTS idx_gate_codes_created_at ON gate_codes(created_at_ms); 32 - `); 33 - const store = new GateCodeStore(SQL, db, config.dbPath); 34 - store.persist(); 35 - return store; 36 - } 37 - 38 - close() { 39 - this.persist(); 40 - this.db.close(); 41 - } 42 - 43 - issueCode(handle: string, now = Date.now()): string { 44 - this.cleanupExpired(now); 45 - const code = crypto.randomBytes(32).toString("base64url"); 46 - this.db.run( 47 - "INSERT INTO gate_codes (code, handle, created_at_ms) VALUES ($code, $handle, $createdAtMs)", 48 - { 49 - $code: code, 50 - $handle: handle, 51 - $createdAtMs: now, 52 - }, 53 - ); 54 - this.persist(); 55 - return code; 56 - } 57 - 58 - consumeCode( 59 - handle: string, 60 - code: string, 61 - ttlSeconds: number, 62 - now = Date.now(), 63 - ): ConsumeCodeResult { 64 - const statement = this.db.prepare( 65 - "SELECT created_at_ms FROM gate_codes WHERE code = $code AND handle = $handle LIMIT 1", 66 - ); 67 - statement.bind({ 68 - $code: code, 69 - $handle: handle, 70 - }); 71 - const row = statement.step() 72 - ? (statement.getAsObject() as { created_at_ms: number }) 73 - : undefined; 74 - statement.free(); 75 - 76 - if (!row) { 77 - return { ok: false, reason: "invalid" }; 78 - } 79 - 80 - this.db.run("DELETE FROM gate_codes WHERE code = $code", { 81 - $code: code, 82 - }); 83 - this.persist(); 84 - 85 - if (now - row.created_at_ms > ttlSeconds * 1000) { 86 - return { ok: false, reason: "expired" }; 87 - } 88 - 89 - return { ok: true }; 90 - } 91 - 92 - private cleanupExpired(now: number) { 93 - const oldestAllowed = now - 24 * 60 * 60 * 1000; 94 - this.db.run("DELETE FROM gate_codes WHERE created_at_ms < $oldestAllowed", { 95 - $oldestAllowed: oldestAllowed, 96 - }); 97 - this.persist(); 98 - } 99 - 100 - private persist() { 101 - const bytes = this.db.export(); 102 - writeFileSync(this.dbPath, Buffer.from(bytes)); 103 - } 104 - }
-66
apps/pds-gatekeeper/src/turnstile.ts
··· 1 - import type { GatekeeperConfig } from "./config.js"; 2 - 3 - export interface TurnstileValidationResult { 4 - success: boolean; 5 - errorCodes: string[]; 6 - } 7 - 8 - export async function validateTurnstileToken( 9 - config: Pick< 10 - GatekeeperConfig, 11 - | "turnstileExpectedAction" 12 - | "turnstileExpectedHostname" 13 - | "turnstileSecretKey" 14 - >, 15 - token: string, 16 - remoteIp?: string, 17 - fetchImpl: typeof fetch = fetch, 18 - ): Promise<TurnstileValidationResult> { 19 - const params = new URLSearchParams({ 20 - secret: config.turnstileSecretKey, 21 - response: token, 22 - }); 23 - 24 - if (remoteIp) { 25 - params.set("remoteip", remoteIp); 26 - } 27 - 28 - const response = await fetchImpl( 29 - "https://challenges.cloudflare.com/turnstile/v0/siteverify", 30 - { 31 - method: "POST", 32 - headers: { 33 - "content-type": "application/x-www-form-urlencoded", 34 - }, 35 - body: params.toString(), 36 - }, 37 - ); 38 - 39 - if (!response.ok) { 40 - return { success: false, errorCodes: ["siteverify-http-error"] }; 41 - } 42 - 43 - const payload = (await response.json()) as { 44 - success?: boolean; 45 - action?: string; 46 - hostname?: string; 47 - ["error-codes"]?: string[]; 48 - }; 49 - 50 - if (payload.success !== true) { 51 - return { 52 - success: false, 53 - errorCodes: payload["error-codes"] ?? ["turnstile-validation-failed"], 54 - }; 55 - } 56 - 57 - if (payload.action !== config.turnstileExpectedAction) { 58 - return { success: false, errorCodes: ["action-mismatch"] }; 59 - } 60 - 61 - if (payload.hostname !== config.turnstileExpectedHostname) { 62 - return { success: false, errorCodes: ["hostname-mismatch"] }; 63 - } 64 - 65 - return { success: true, errorCodes: [] }; 66 - }
-197
apps/pds-gatekeeper/test/app.test.ts
··· 1 - import test from "node:test"; 2 - import assert from "node:assert/strict"; 3 - import { mkdtempSync, rmSync } from "node:fs"; 4 - import { join } from "node:path"; 5 - import { tmpdir } from "node:os"; 6 - import { createApp } from "../src/app.js"; 7 - import type { GatekeeperConfig } from "../src/config.js"; 8 - import { GateCodeStore } from "../src/store.js"; 9 - 10 - function createConfig(dbDir: string): GatekeeperConfig { 11 - return { 12 - host: "127.0.0.1", 13 - port: 8080, 14 - pdsBaseUrl: "http://pds-origin.railway.internal:3000", 15 - pdsHostname: "example.com", 16 - enableSignupProtection: true, 17 - signupCodeTtlSeconds: 300, 18 - turnstileSiteKey: "site-key", 19 - turnstileSecretKey: "secret-key", 20 - turnstileExpectedHostname: "example.com", 21 - turnstileExpectedAction: "signup", 22 - defaultCaptchaRedirect: "https://bsky.app", 23 - allowedCaptchaRedirects: ["https://bsky.app", "https://app.example.com"], 24 - dbPath: join(dbDir, "gatekeeper.sqlite"), 25 - }; 26 - } 27 - 28 - test("describeServer advertises signup protection", async () => { 29 - const workDir = mkdtempSync(join(tmpdir(), "gatekeeper-test-")); 30 - const config = createConfig(workDir); 31 - const app = await createApp({ 32 - config, 33 - fetchImpl: async () => 34 - new Response( 35 - JSON.stringify({ 36 - availableUserDomains: ["example.com"], 37 - }), 38 - { 39 - status: 200, 40 - headers: { 41 - "content-type": "application/json", 42 - }, 43 - }, 44 - ), 45 - }); 46 - 47 - try { 48 - const response = await app.inject({ 49 - method: "GET", 50 - url: "/xrpc/com.atproto.server.describeServer", 51 - }); 52 - assert.equal(response.statusCode, 200); 53 - assert.deepEqual(response.json(), { 54 - availableUserDomains: ["example.com"], 55 - phoneVerificationRequired: true, 56 - }); 57 - } finally { 58 - await app.close(); 59 - rmSync(workDir, { force: true, recursive: true }); 60 - } 61 - }); 62 - 63 - test("createAccount rejects missing verification codes", async () => { 64 - const workDir = mkdtempSync(join(tmpdir(), "gatekeeper-test-")); 65 - const config = createConfig(workDir); 66 - const app = await createApp({ 67 - config, 68 - fetchImpl: async () => 69 - new Response("{}", { 70 - status: 200, 71 - headers: { 72 - "content-type": "application/json", 73 - }, 74 - }), 75 - }); 76 - 77 - try { 78 - const response = await app.inject({ 79 - method: "POST", 80 - url: "/xrpc/com.atproto.server.createAccount", 81 - payload: { 82 - handle: "alice.example.com", 83 - }, 84 - }); 85 - assert.equal(response.statusCode, 400); 86 - assert.deepEqual(response.json(), { 87 - error: "InvalidRequest", 88 - message: "Verification is required on this server.", 89 - }); 90 - } finally { 91 - await app.close(); 92 - rmSync(workDir, { force: true, recursive: true }); 93 - } 94 - }); 95 - 96 - test("createAccount accepts a valid single-use code", async () => { 97 - const workDir = mkdtempSync(join(tmpdir(), "gatekeeper-test-")); 98 - const config = createConfig(workDir); 99 - let issuedCode = ""; 100 - 101 - const app = await createApp({ 102 - config, 103 - fetchImpl: async (input, init) => { 104 - const url = input instanceof Request ? input.url : String(input); 105 - if (url.includes("siteverify")) { 106 - return new Response( 107 - JSON.stringify({ 108 - success: true, 109 - action: "signup", 110 - hostname: "example.com", 111 - }), 112 - { 113 - status: 200, 114 - headers: { 115 - "content-type": "application/json", 116 - }, 117 - }, 118 - ); 119 - } 120 - 121 - const payload = JSON.parse(String(init?.body ?? "{}")) as { 122 - verificationCode?: string; 123 - }; 124 - assert.equal(payload.verificationCode, issuedCode); 125 - return new Response(JSON.stringify({ did: "did:plc:alice" }), { 126 - status: 200, 127 - headers: { 128 - "content-type": "application/json", 129 - }, 130 - }); 131 - }, 132 - }); 133 - 134 - try { 135 - const gateResponse = await app.inject({ 136 - method: "POST", 137 - url: "/gate/signup?handle=alice.example.com&state=opaque&redirect_url=https%3A%2F%2Fapp.example.com", 138 - headers: { 139 - "content-type": "application/x-www-form-urlencoded", 140 - "cf-connecting-ip": "203.0.113.10", 141 - }, 142 - payload: "cf-turnstile-response=token-value&redirect_url=https%3A%2F%2Fapp.example.com", 143 - }); 144 - 145 - assert.equal(gateResponse.statusCode, 302); 146 - const location = gateResponse.headers.location; 147 - assert.ok(location); 148 - const redirect = new URL(location); 149 - issuedCode = redirect.searchParams.get("code") ?? ""; 150 - assert.ok(issuedCode); 151 - 152 - const createResponse = await app.inject({ 153 - method: "POST", 154 - url: "/xrpc/com.atproto.server.createAccount", 155 - payload: { 156 - handle: "alice.example.com", 157 - verificationCode: issuedCode, 158 - }, 159 - }); 160 - 161 - assert.equal(createResponse.statusCode, 200); 162 - assert.deepEqual(createResponse.json(), { did: "did:plc:alice" }); 163 - 164 - const replayResponse = await app.inject({ 165 - method: "POST", 166 - url: "/xrpc/com.atproto.server.createAccount", 167 - payload: { 168 - handle: "alice.example.com", 169 - verificationCode: issuedCode, 170 - }, 171 - }); 172 - 173 - assert.equal(replayResponse.statusCode, 400); 174 - assert.deepEqual(replayResponse.json(), { 175 - error: "InvalidToken", 176 - message: "Token could not be verified", 177 - }); 178 - } finally { 179 - await app.close(); 180 - rmSync(workDir, { force: true, recursive: true }); 181 - } 182 - }); 183 - 184 - test("issued codes expire after the configured ttl", async () => { 185 - const workDir = mkdtempSync(join(tmpdir(), "gatekeeper-test-")); 186 - const config = createConfig(workDir); 187 - const store = await GateCodeStore.open(config); 188 - 189 - try { 190 - const code = store.issueCode("alice.example.com", 0); 191 - const result = store.consumeCode("alice.example.com", code, 300, 301_000); 192 - assert.deepEqual(result, { ok: false, reason: "expired" }); 193 - } finally { 194 - store.close(); 195 - rmSync(workDir, { force: true, recursive: true }); 196 - } 197 - });
-15
apps/pds-gatekeeper/tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "ES2022", 4 - "module": "NodeNext", 5 - "moduleResolution": "NodeNext", 6 - "outDir": "dist", 7 - "rootDir": "src", 8 - "strict": true, 9 - "esModuleInterop": true, 10 - "forceConsistentCasingInFileNames": true, 11 - "skipLibCheck": true, 12 - "types": ["node"] 13 - }, 14 - "include": ["src/**/*.ts"] 15 - }
-117
docs/pds-turnstile-railway.md
··· 1 - # Turnstile-Protected Railway PDS 2 - 3 - This repo now includes two deployment helpers for running a Bluesky PDS behind a Turnstile-backed signup gate while keeping Railway as the host: 4 - 5 - - `apps/pds-gatekeeper`: a small ATProto-compatible signup gatekeeper 6 - - `apps/pds-edge-proxy`: a Caddy reverse proxy that exposes the PDS publicly and routes protected signup paths to the gatekeeper 7 - 8 - ## Service layout 9 - 10 - Create three Railway services from this repo or from your existing PDS repo setup: 11 - 12 - 1. `pds-origin` 13 - - Your existing Bluesky PDS template service 14 - - Internal only after rollout 15 - - Private network target: `http://pds-origin.railway.internal:3000` 16 - 2. `pds-gatekeeper` 17 - - Dockerfile path: `apps/pds-gatekeeper/Dockerfile` 18 - - Public networking disabled 19 - - Attach a Railway volume mounted at `/data` 20 - 3. `pds-edge-proxy` 21 - - Dockerfile path: `apps/pds-edge-proxy/Dockerfile` 22 - - Public networking enabled 23 - - Attach both your apex domain and wildcard domain 24 - 25 - ## Gatekeeper environment 26 - 27 - Set these on `pds-gatekeeper`: 28 - 29 - ```env 30 - HOST=0.0.0.0 31 - PORT=8080 32 - PDS_BASE_URL=http://pds-origin.railway.internal:3000 33 - PDS_HOSTNAME=example.com 34 - TURNSTILE_SITE_KEY=your-turnstile-site-key 35 - TURNSTILE_SECRET_KEY=your-turnstile-secret-key 36 - TURNSTILE_EXPECTED_HOSTNAME=example.com 37 - TURNSTILE_EXPECTED_ACTION=signup 38 - GATEKEEPER_DB_PATH=/data/gatekeeper.sqlite 39 - GATEKEEPER_SIGNUP_CODE_TTL_SECONDS=300 40 - GATEKEEPER_ENABLE_SIGNUP_PROTECTION=true 41 - GATEKEEPER_DEFAULT_CAPTCHA_REDIRECT=https://bsky.app 42 - GATEKEEPER_CAPTCHA_SUCCESS_REDIRECTS=https://bsky.app,https://your-app.example.com 43 - ``` 44 - 45 - ## Edge proxy environment 46 - 47 - Set these on `pds-edge-proxy`: 48 - 49 - ```env 50 - PORT=8080 51 - PDS_BASE_URL=http://pds-origin.railway.internal:3000 52 - GATEKEEPER_BASE_URL=http://pds-gatekeeper.railway.internal:8080 53 - ``` 54 - 55 - ## Public routing behavior 56 - 57 - The proxy routes these paths to the gatekeeper: 58 - 59 - - `/xrpc/com.atproto.server.describeServer` 60 - - `/xrpc/com.atproto.server.createAccount` 61 - - `/gate/*` 62 - 63 - Everything else is forwarded directly to the origin PDS, including normal XRPC traffic and websocket federation. 64 - 65 - ## Cloudflare and Railway DNS 66 - 67 - Use your apex domain for the PDS host when possible, for example: 68 - 69 - - PDS hostname: `example.com` 70 - - User handles: `alice.example.com` 71 - 72 - On Railway: 73 - 74 - 1. Attach `example.com` to `pds-edge-proxy` 75 - 2. Attach `*.example.com` to `pds-edge-proxy` 76 - 3. Keep `_acme-challenge` verification records DNS-only 77 - 78 - On Cloudflare: 79 - 80 - 1. Proxy the apex and wildcard CNAMEs through the orange cloud 81 - 2. Enable Universal SSL 82 - 3. Set SSL/TLS mode to `Full` 83 - 84 - ## Suggested Cloudflare rate limits 85 - 86 - Use rate limiting rules as defense in depth, not as the primary signup gate. 87 - 88 - Recommended starting points: 89 - 90 - - `/gate/*` 91 - - Count by IP 92 - - 10 requests per minute 93 - - Action: Managed Challenge or block after repeated abuse 94 - - `/xrpc/com.atproto.server.createAccount` 95 - - Count by IP 96 - - 5 requests per 10 minutes 97 - - Action: block 98 - 99 - Do not put a Cloudflare challenge directly in front of the XRPC signup endpoint as the main control. The client needs the ATProto-compatible `verificationCode` flow. 100 - 101 - ## Rollout checklist 102 - 103 - 1. Deploy `pds-gatekeeper` 104 - 2. Deploy `pds-edge-proxy` 105 - 3. Move the public custom domains from `pds-origin` to `pds-edge-proxy` 106 - 4. Confirm `https://example.com/xrpc/_health` still works 107 - 5. Confirm `GET /xrpc/com.atproto.server.describeServer` returns `phoneVerificationRequired: true` 108 - 6. Confirm direct `createAccount` calls without `verificationCode` fail 109 - 7. Confirm `/gate/signup` returns a code and a fresh signup succeeds 110 - 8. Confirm reusing the same code fails 111 - 9. Confirm websocket federation still works through the proxy 112 - 113 - ## Notes 114 - 115 - - This phase intentionally protects signup only. 116 - - Login and 2FA overrides are out of scope here. 117 - - The gatekeeper stores one-time signup codes in SQLite on the attached Railway volume.
-301
pnpm-lock.yaml
··· 178 178 specifier: ~5.9.2 179 179 version: 5.9.3 180 180 181 - apps/pds-gatekeeper: 182 - dependencies: 183 - '@fastify/formbody': 184 - specifier: ^8.0.2 185 - version: 8.0.2 186 - fastify: 187 - specifier: ^5.6.1 188 - version: 5.8.2 189 - sql.js: 190 - specifier: ^1.13.0 191 - version: 1.14.1 192 - devDependencies: 193 - '@types/node': 194 - specifier: ^22.10.2 195 - version: 22.19.7 196 - '@types/sql.js': 197 - specifier: ^1.4.10 198 - version: 1.4.10 199 - tsx: 200 - specifier: ^4.20.6 201 - version: 4.21.0 202 - typescript: 203 - specifier: ^5.7.2 204 - version: 5.9.3 205 - 206 181 apps/web: 207 182 dependencies: 208 183 '@material/material-color-utilities': ··· 1653 1628 resolution: {integrity: sha512-o2qDlTqJ606h4xR36H2zWTywmZ3v3842K6TU8Ik2n1mfW0S580VHlt3eItVYdLYz+klaPp7CXqanja8eASZjRw==} 1654 1629 hasBin: true 1655 1630 1656 - '@fastify/ajv-compiler@4.0.5': 1657 - resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} 1658 - 1659 - '@fastify/error@4.2.0': 1660 - resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} 1661 - 1662 - '@fastify/fast-json-stringify-compiler@5.0.3': 1663 - resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} 1664 - 1665 - '@fastify/formbody@8.0.2': 1666 - resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} 1667 - 1668 - '@fastify/forwarded@3.0.1': 1669 - resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} 1670 - 1671 - '@fastify/merge-json-schemas@0.2.1': 1672 - resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} 1673 - 1674 - '@fastify/proxy-addr@5.1.0': 1675 - resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} 1676 - 1677 1631 '@floating-ui/core@1.7.4': 1678 1632 resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} 1679 1633 ··· 2376 2330 2377 2331 '@paralleldrive/cuid2@2.3.1': 2378 2332 resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} 2379 - 2380 - '@pinojs/redact@0.4.0': 2381 - resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} 2382 2333 2383 2334 '@pkgjs/parseargs@0.11.0': 2384 2335 resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} ··· 3968 3919 '@types/deep-eql@4.0.2': 3969 3920 resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 3970 3921 3971 - '@types/emscripten@1.41.5': 3972 - resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} 3973 - 3974 3922 '@types/eslint-scope@3.7.7': 3975 3923 resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} 3976 3924 ··· 4044 3992 4045 3993 '@types/serve-static@2.2.0': 4046 3994 resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} 4047 - 4048 - '@types/sql.js@1.4.10': 4049 - resolution: {integrity: sha512-E7XnsrWm01Uvp0/0+iRI9ZwO/BvKyiiHUpcVKJenVVH2pUdZndsgQ5BWXNxKaEO+bkKbvU29Ky9o21juMip1ww==} 4050 3995 4051 3996 '@types/stack-utils@2.0.3': 4052 3997 resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} ··· 4277 4222 resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 4278 4223 engines: {node: '>=6.5'} 4279 4224 4280 - abstract-logging@2.0.1: 4281 - resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} 4282 - 4283 4225 accepts@1.3.8: 4284 4226 resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 4285 4227 engines: {node: '>= 0.6'} ··· 4437 4379 atomic-sleep@1.0.0: 4438 4380 resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} 4439 4381 engines: {node: '>=8.0.0'} 4440 - 4441 - avvio@9.2.0: 4442 - resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} 4443 4382 4444 4383 await-lock@2.2.2: 4445 4384 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} ··· 4970 4909 resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 4971 4910 engines: {node: '>= 0.6'} 4972 4911 4973 - cookie@1.1.1: 4974 - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 4975 - engines: {node: '>=18'} 4976 - 4977 4912 cookiejar@2.1.4: 4978 4913 resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} 4979 4914 ··· 5177 5112 resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 5178 5113 engines: {node: '>= 0.8'} 5179 5114 5180 - dequal@2.0.3: 5181 - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 5182 - engines: {node: '>=6'} 5183 - 5184 5115 destr@2.0.5: 5185 5116 resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} 5186 5117 ··· 5673 5604 resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} 5674 5605 engines: {node: '>=8.0.0'} 5675 5606 5676 - fast-decode-uri-component@1.0.1: 5677 - resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} 5678 - 5679 5607 fast-deep-equal@3.1.3: 5680 5608 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 5681 5609 5682 5610 fast-json-stable-stringify@2.1.0: 5683 5611 resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 5684 5612 5685 - fast-json-stringify@6.3.0: 5686 - resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} 5687 - 5688 - fast-querystring@1.1.2: 5689 - resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} 5690 - 5691 5613 fast-redact@3.5.0: 5692 5614 resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} 5693 5615 engines: {node: '>=6'} ··· 5697 5619 5698 5620 fast-uri@3.1.0: 5699 5621 resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} 5700 - 5701 - fastify-plugin@5.1.0: 5702 - resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} 5703 - 5704 - fastify@5.8.2: 5705 - resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} 5706 - 5707 - fastq@1.20.1: 5708 - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} 5709 5622 5710 5623 fb-watchman@2.0.2: 5711 5624 resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} ··· 5758 5671 finalhandler@2.1.1: 5759 5672 resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} 5760 5673 engines: {node: '>= 18.0.0'} 5761 - 5762 - find-my-way@9.5.0: 5763 - resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} 5764 - engines: {node: '>=20'} 5765 5674 5766 5675 find-up@4.1.0: 5767 5676 resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} ··· 6438 6347 json-parse-even-better-errors@2.3.1: 6439 6348 resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 6440 6349 6441 - json-schema-ref-resolver@3.0.0: 6442 - resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} 6443 - 6444 6350 json-schema-traverse@0.4.1: 6445 6351 resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 6446 6352 ··· 6476 6382 libphonenumber-js@1.12.35: 6477 6383 resolution: {integrity: sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==} 6478 6384 6479 - light-my-request@6.6.0: 6480 - resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} 6481 - 6482 6385 lighthouse-logger@1.4.2: 6483 6386 resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} 6484 6387 ··· 7304 7207 pino-abstract-transport@1.2.0: 7305 7208 resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} 7306 7209 7307 - pino-abstract-transport@3.0.0: 7308 - resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} 7309 - 7310 7210 pino-std-serializers@6.2.2: 7311 7211 resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} 7312 - 7313 - pino-std-serializers@7.1.0: 7314 - resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} 7315 - 7316 - pino@10.3.1: 7317 - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} 7318 - hasBin: true 7319 7212 7320 7213 pino@8.21.0: 7321 7214 resolution: {integrity: sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==} ··· 7482 7375 7483 7376 process-warning@3.0.0: 7484 7377 resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} 7485 - 7486 - process-warning@4.0.1: 7487 - resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} 7488 - 7489 - process-warning@5.0.0: 7490 - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} 7491 7378 7492 7379 process@0.11.10: 7493 7380 resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} ··· 7880 7767 resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} 7881 7768 engines: {node: '>=8'} 7882 7769 7883 - ret@0.5.0: 7884 - resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} 7885 - engines: {node: '>=10'} 7886 - 7887 7770 retry@0.12.0: 7888 7771 resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} 7889 7772 engines: {node: '>= 4'} 7890 7773 7891 - reusify@1.1.0: 7892 - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 7893 - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 7894 - 7895 - rfdc@1.4.1: 7896 - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} 7897 - 7898 7774 rimraf@3.0.2: 7899 7775 resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 7900 7776 deprecated: Rimraf versions prior to v4 are no longer supported ··· 7933 7809 safe-buffer@5.2.1: 7934 7810 resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 7935 7811 7936 - safe-regex2@5.1.0: 7937 - resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} 7938 - hasBin: true 7939 - 7940 7812 safe-stable-stringify@2.5.0: 7941 7813 resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 7942 7814 engines: {node: '>=10'} ··· 7965 7837 schema-utils@4.3.3: 7966 7838 resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} 7967 7839 engines: {node: '>= 10.13.0'} 7968 - 7969 - secure-json-parse@4.1.0: 7970 - resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} 7971 7840 7972 7841 semver@6.3.1: 7973 7842 resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} ··· 8042 7911 server-only@0.0.1: 8043 7912 resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} 8044 7913 8045 - set-cookie-parser@2.7.2: 8046 - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} 8047 - 8048 7914 setimmediate@1.0.5: 8049 7915 resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} 8050 7916 ··· 8129 7995 sonic-boom@3.8.1: 8130 7996 resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} 8131 7997 8132 - sonic-boom@4.2.1: 8133 - resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} 8134 - 8135 7998 sonner@2.0.7: 8136 7999 resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} 8137 8000 peerDependencies: ··· 8170 8033 8171 8034 sprintf-js@1.0.3: 8172 8035 resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} 8173 - 8174 - sql.js@1.14.1: 8175 - resolution: {integrity: sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==} 8176 8036 8177 8037 sqlstring@2.3.3: 8178 8038 resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} ··· 8407 8267 thread-stream@2.7.0: 8408 8268 resolution: {integrity: sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==} 8409 8269 8410 - thread-stream@4.0.0: 8411 - resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} 8412 - engines: {node: '>=20'} 8413 - 8414 8270 throat@5.0.0: 8415 8271 resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} 8416 8272 ··· 8466 8322 to-regex-range@5.0.1: 8467 8323 resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 8468 8324 engines: {node: '>=8.0'} 8469 - 8470 - toad-cache@3.7.0: 8471 - resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} 8472 - engines: {node: '>=12'} 8473 8325 8474 8326 toidentifier@1.0.1: 8475 8327 resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} ··· 10851 10703 chalk: 4.1.2 10852 10704 js-yaml: 4.1.1 10853 10705 10854 - '@fastify/ajv-compiler@4.0.5': 10855 - dependencies: 10856 - ajv: 8.17.1 10857 - ajv-formats: 3.0.1(ajv@8.17.1) 10858 - fast-uri: 3.1.0 10859 - 10860 - '@fastify/error@4.2.0': {} 10861 - 10862 - '@fastify/fast-json-stringify-compiler@5.0.3': 10863 - dependencies: 10864 - fast-json-stringify: 6.3.0 10865 - 10866 - '@fastify/formbody@8.0.2': 10867 - dependencies: 10868 - fast-querystring: 1.1.2 10869 - fastify-plugin: 5.1.0 10870 - 10871 - '@fastify/forwarded@3.0.1': {} 10872 - 10873 - '@fastify/merge-json-schemas@0.2.1': 10874 - dependencies: 10875 - dequal: 2.0.3 10876 - 10877 - '@fastify/proxy-addr@5.1.0': 10878 - dependencies: 10879 - '@fastify/forwarded': 3.0.1 10880 - ipaddr.js: 2.3.0 10881 - 10882 10706 '@floating-ui/core@1.7.4': 10883 10707 dependencies: 10884 10708 '@floating-ui/utils': 0.2.10 ··· 11829 11653 '@paralleldrive/cuid2@2.3.1': 11830 11654 dependencies: 11831 11655 '@noble/hashes': 1.8.0 11832 - 11833 - '@pinojs/redact@0.4.0': {} 11834 11656 11835 11657 '@pkgjs/parseargs@0.11.0': 11836 11658 optional: true ··· 13728 13550 13729 13551 '@types/deep-eql@4.0.2': {} 13730 13552 13731 - '@types/emscripten@1.41.5': {} 13732 - 13733 13553 '@types/eslint-scope@3.7.7': 13734 13554 dependencies: 13735 13555 '@types/eslint': 9.6.1 ··· 13820 13640 '@types/serve-static@2.2.0': 13821 13641 dependencies: 13822 13642 '@types/http-errors': 2.0.5 13823 - '@types/node': 22.19.7 13824 - 13825 - '@types/sql.js@1.4.10': 13826 - dependencies: 13827 - '@types/emscripten': 1.41.5 13828 13643 '@types/node': 22.19.7 13829 13644 13830 13645 '@types/stack-utils@2.0.3': {} ··· 14065 13880 dependencies: 14066 13881 event-target-shim: 5.0.1 14067 13882 14068 - abstract-logging@2.0.1: {} 14069 - 14070 13883 accepts@1.3.8: 14071 13884 dependencies: 14072 13885 mime-types: 2.1.35 ··· 14191 14004 14192 14005 atomic-sleep@1.0.0: {} 14193 14006 14194 - avvio@9.2.0: 14195 - dependencies: 14196 - '@fastify/error': 4.2.0 14197 - fastq: 1.20.1 14198 - 14199 14007 await-lock@2.2.2: {} 14200 14008 14201 14009 await-to-js@3.0.0: {} ··· 14858 14666 cookie-signature@1.2.2: {} 14859 14667 14860 14668 cookie@0.7.2: {} 14861 - 14862 - cookie@1.1.1: {} 14863 14669 14864 14670 cookiejar@2.1.4: {} 14865 14671 ··· 15009 14815 15010 14816 depd@2.0.0: {} 15011 14817 15012 - dequal@2.0.3: {} 15013 - 15014 14818 destr@2.0.5: {} 15015 14819 15016 14820 destroy@1.2.0: {} ··· 15589 15393 dependencies: 15590 15394 pure-rand: 6.1.0 15591 15395 15592 - fast-decode-uri-component@1.0.1: {} 15593 - 15594 15396 fast-deep-equal@3.1.3: {} 15595 15397 15596 15398 fast-json-stable-stringify@2.1.0: {} 15597 15399 15598 - fast-json-stringify@6.3.0: 15599 - dependencies: 15600 - '@fastify/merge-json-schemas': 0.2.1 15601 - ajv: 8.17.1 15602 - ajv-formats: 3.0.1(ajv@8.17.1) 15603 - fast-uri: 3.1.0 15604 - json-schema-ref-resolver: 3.0.0 15605 - rfdc: 1.4.1 15606 - 15607 - fast-querystring@1.1.2: 15608 - dependencies: 15609 - fast-decode-uri-component: 1.0.1 15610 - 15611 15400 fast-redact@3.5.0: {} 15612 15401 15613 15402 fast-safe-stringify@2.1.1: {} 15614 15403 15615 15404 fast-uri@3.1.0: {} 15616 15405 15617 - fastify-plugin@5.1.0: {} 15618 - 15619 - fastify@5.8.2: 15620 - dependencies: 15621 - '@fastify/ajv-compiler': 4.0.5 15622 - '@fastify/error': 4.2.0 15623 - '@fastify/fast-json-stringify-compiler': 5.0.3 15624 - '@fastify/proxy-addr': 5.1.0 15625 - abstract-logging: 2.0.1 15626 - avvio: 9.2.0 15627 - fast-json-stringify: 6.3.0 15628 - find-my-way: 9.5.0 15629 - light-my-request: 6.6.0 15630 - pino: 10.3.1 15631 - process-warning: 5.0.0 15632 - rfdc: 1.4.1 15633 - secure-json-parse: 4.1.0 15634 - semver: 7.7.4 15635 - toad-cache: 3.7.0 15636 - 15637 - fastq@1.20.1: 15638 - dependencies: 15639 - reusify: 1.1.0 15640 - 15641 15406 fb-watchman@2.0.2: 15642 15407 dependencies: 15643 15408 bser: 2.1.1 ··· 15720 15485 statuses: 2.0.2 15721 15486 transitivePeerDependencies: 15722 15487 - supports-color 15723 - 15724 - find-my-way@9.5.0: 15725 - dependencies: 15726 - fast-deep-equal: 3.1.3 15727 - fast-querystring: 1.1.2 15728 - safe-regex2: 5.1.0 15729 15488 15730 15489 find-up@4.1.0: 15731 15490 dependencies: ··· 16651 16410 jsesc@3.1.0: {} 16652 16411 16653 16412 json-parse-even-better-errors@2.3.1: {} 16654 - 16655 - json-schema-ref-resolver@3.0.0: 16656 - dependencies: 16657 - dequal: 2.0.3 16658 16413 16659 16414 json-schema-traverse@0.4.1: {} 16660 16415 ··· 16683 16438 16684 16439 libphonenumber-js@1.12.35: {} 16685 16440 16686 - light-my-request@6.6.0: 16687 - dependencies: 16688 - cookie: 1.1.1 16689 - process-warning: 4.0.1 16690 - set-cookie-parser: 2.7.2 16691 - 16692 16441 lighthouse-logger@1.4.2: 16693 16442 dependencies: 16694 16443 debug: 2.6.9 ··· 17530 17279 readable-stream: 4.7.0 17531 17280 split2: 4.2.0 17532 17281 17533 - pino-abstract-transport@3.0.0: 17534 - dependencies: 17535 - split2: 4.2.0 17536 - 17537 17282 pino-std-serializers@6.2.2: {} 17538 17283 17539 - pino-std-serializers@7.1.0: {} 17540 - 17541 - pino@10.3.1: 17542 - dependencies: 17543 - '@pinojs/redact': 0.4.0 17544 - atomic-sleep: 1.0.0 17545 - on-exit-leak-free: 2.1.2 17546 - pino-abstract-transport: 3.0.0 17547 - pino-std-serializers: 7.1.0 17548 - process-warning: 5.0.0 17549 - quick-format-unescaped: 4.0.4 17550 - real-require: 0.2.0 17551 - safe-stable-stringify: 2.5.0 17552 - sonic-boom: 4.2.1 17553 - thread-stream: 4.0.0 17554 - 17555 17284 pino@8.21.0: 17556 17285 dependencies: 17557 17286 atomic-sleep: 1.0.0 ··· 17708 17437 proc-log@4.2.0: {} 17709 17438 17710 17439 process-warning@3.0.0: {} 17711 - 17712 - process-warning@4.0.1: {} 17713 - 17714 - process-warning@5.0.0: {} 17715 17440 17716 17441 process@0.11.10: {} 17717 17442 ··· 18254 17979 onetime: 5.1.2 18255 17980 signal-exit: 3.0.7 18256 17981 18257 - ret@0.5.0: {} 18258 - 18259 17982 retry@0.12.0: {} 18260 - 18261 - reusify@1.1.0: {} 18262 - 18263 - rfdc@1.4.1: {} 18264 17983 18265 17984 rimraf@3.0.2: 18266 17985 dependencies: ··· 18344 18063 18345 18064 safe-buffer@5.2.1: {} 18346 18065 18347 - safe-regex2@5.1.0: 18348 - dependencies: 18349 - ret: 0.5.0 18350 - 18351 18066 safe-stable-stringify@2.5.0: {} 18352 18067 18353 18068 safer-buffer@2.1.2: {} ··· 18374 18089 ajv: 8.17.1 18375 18090 ajv-formats: 2.1.1(ajv@8.17.1) 18376 18091 ajv-keywords: 5.1.0(ajv@8.17.1) 18377 - 18378 - secure-json-parse@4.1.0: {} 18379 18092 18380 18093 semver@6.3.1: {} 18381 18094 ··· 18461 18174 18462 18175 server-only@0.0.1: {} 18463 18176 18464 - set-cookie-parser@2.7.2: {} 18465 - 18466 18177 setimmediate@1.0.5: {} 18467 18178 18468 18179 setprototypeof@1.2.0: {} ··· 18551 18262 dependencies: 18552 18263 atomic-sleep: 1.0.0 18553 18264 18554 - sonic-boom@4.2.1: 18555 - dependencies: 18556 - atomic-sleep: 1.0.0 18557 - 18558 18265 sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): 18559 18266 dependencies: 18560 18267 react: 19.2.4 ··· 18583 18290 split2@4.2.0: {} 18584 18291 18585 18292 sprintf-js@1.0.3: {} 18586 - 18587 - sql.js@1.14.1: {} 18588 18293 18589 18294 sqlstring@2.3.3: {} 18590 18295 ··· 18816 18521 dependencies: 18817 18522 real-require: 0.2.0 18818 18523 18819 - thread-stream@4.0.0: 18820 - dependencies: 18821 - real-require: 0.2.0 18822 - 18823 18524 throat@5.0.0: {} 18824 18525 18825 18526 tiny-invariant@1.3.3: {} ··· 18858 18559 to-regex-range@5.0.1: 18859 18560 dependencies: 18860 18561 is-number: 7.0.0 18861 - 18862 - toad-cache@3.7.0: {} 18863 18562 18864 18563 toidentifier@1.0.1: {} 18865 18564