feat(deploy): run as a Node container image instead of Cloudflare Pages
Use adapter-node, read Turnstile keys at runtime, add a Dockerfile, drop Pages-only files and cookie logic.
- commit
- af344e16dda96c8074677adaac47975c51ab9796
- author
- Kit · · 2026-10-12 12:31 UTC
- parent
- 86d866d
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
13 files · +70 -73
A .dockerignore +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | node_modules | |
| 2 | + | .svelte-kit | |
| 3 | + | build | |
| 4 | + | .git | |
| 5 | + | .worktrees | |
| 6 | + | .env | |
| 7 | + | .env.* | |
| 8 | + | !.env.example | |
| 9 | + | docs | |
| 10 | + | .claude | |
| 11 | + | .cursor | |
| 12 | + | .serena | |
| 13 | + | .wrangler | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | node_modules | |||
| 2 | + | .svelte-kit | |||
| 3 | + | build | |||
| 4 | + | .git | |||
| 5 | + | .worktrees | |||
| 6 | + | .env | |||
| 7 | + | .env.* | |||
| 8 | + | !.env.example | |||
| 9 | + | docs | |||
| 10 | + | .claude | |||
| 11 | + | .cursor | |||
| 12 | + | .serena | |||
| 13 | + | .wrangler | |||
M .gitignore +1 -0
| @@ -1,4 +1,5 @@ | |||
| 1 | 1 | node_modules | |
| 2 | + | .worktrees | |
| 2 | 3 | ||
| 3 | 4 | # Output | |
| 4 | 5 | .output | |
| @@ -1,4 +1,5 @@ | |||||
| 1 | node_modules | 1 | node_modules | ||
| 2 | + | .worktrees | |||
| 2 | 3 | ||||
| 3 | # Output | 4 | # Output | ||
| 4 | .output | 5 | .output | ||
A Dockerfile +32 -0
| @@ -0,0 +1,32 @@ | |||
| 1 | + | FROM node:22-alpine AS build | |
| 2 | + | WORKDIR /app | |
| 3 | + | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |
| 4 | + | RUN corepack enable | |
| 5 | + | ||
| 6 | + | COPY package.json pnpm-lock.yaml ./ | |
| 7 | + | RUN pnpm fetch | |
| 8 | + | COPY . . | |
| 9 | + | RUN pnpm install --frozen-lockfile --offline && pnpm run build | |
| 10 | + | ||
| 11 | + | FROM node:22-alpine AS deps | |
| 12 | + | WORKDIR /app | |
| 13 | + | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |
| 14 | + | RUN corepack enable | |
| 15 | + | COPY package.json pnpm-lock.yaml ./ | |
| 16 | + | RUN pnpm install --prod --frozen-lockfile --ignore-scripts | |
| 17 | + | ||
| 18 | + | FROM node:22-alpine | |
| 19 | + | WORKDIR /app | |
| 20 | + | ENV NODE_ENV=production \ | |
| 21 | + | HOST=0.0.0.0 \ | |
| 22 | + | PORT=3000 | |
| 23 | + | ||
| 24 | + | COPY --from=deps /app/node_modules ./node_modules | |
| 25 | + | COPY --from=build /app/build ./build | |
| 26 | + | COPY package.json ./ | |
| 27 | + | ||
| 28 | + | USER node | |
| 29 | + | EXPOSE 3000 | |
| 30 | + | HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ | |
| 31 | + | CMD node -e "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" | |
| 32 | + | CMD ["node", "build"] | |
| @@ -0,0 +1,32 @@ | |||||
| 1 | + | FROM node:22-alpine AS build | |||
| 2 | + | WORKDIR /app | |||
| 3 | + | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |||
| 4 | + | RUN corepack enable | |||
| 5 | + | ||||
| 6 | + | COPY package.json pnpm-lock.yaml ./ | |||
| 7 | + | RUN pnpm fetch | |||
| 8 | + | COPY . . | |||
| 9 | + | RUN pnpm install --frozen-lockfile --offline && pnpm run build | |||
| 10 | + | ||||
| 11 | + | FROM node:22-alpine AS deps | |||
| 12 | + | WORKDIR /app | |||
| 13 | + | ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |||
| 14 | + | RUN corepack enable | |||
| 15 | + | COPY package.json pnpm-lock.yaml ./ | |||
| 16 | + | RUN pnpm install --prod --frozen-lockfile --ignore-scripts | |||
| 17 | + | ||||
| 18 | + | FROM node:22-alpine | |||
| 19 | + | WORKDIR /app | |||
| 20 | + | ENV NODE_ENV=production \ | |||
| 21 | + | HOST=0.0.0.0 \ | |||
| 22 | + | PORT=3000 | |||
| 23 | + | ||||
| 24 | + | COPY --from=deps /app/node_modules ./node_modules | |||
| 25 | + | COPY --from=build /app/build ./build | |||
| 26 | + | COPY package.json ./ | |||
| 27 | + | ||||
| 28 | + | USER node | |||
| 29 | + | EXPOSE 3000 | |||
| 30 | + | HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ | |||
| 31 | + | CMD node -e "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" | |||
| 32 | + | CMD ["node", "build"] | |||
M README.md +13 -37
| @@ -76,48 +76,24 @@ Vous pouvez visualiser et gérer vos données via le dashboard Convex : | |||
| 76 | 76 | npx convex dashboard | |
| 77 | 77 | ``` | |
| 78 | 78 | ||
| 79 | - | ## Déploiement Cloudflare Pages | |
| 79 | + | ## Déploiement | |
| 80 | 80 | ||
| 81 | - | Le projet est déployé sur [Cloudflare Pages](https://pages.cloudflare.com/). Les Workers Cloudflare ont des **limitations strictes** à respecter. | |
| 81 | + | Le site tourne dans une image conteneur Node ([`adapter-node`](https://svelte.dev/docs/kit/adapter-node)), écoutant sur le port `3000`. | |
| 82 | 82 | ||
| 83 | - | ### Opérations interdites au niveau global/module | |
| 84 | - | ||
| 85 | - | Cloudflare Workers **n'autorisent PAS** les opérations suivantes au niveau global (en dehors des handlers de requête) : | |
| 86 | - | ||
| 87 | - | ```typescript | |
| 88 | - | // INTERDIT - provoque "Disallowed operation called within global scope" | |
| 89 | - | setInterval(() => { ... }, 1000); | |
| 90 | - | setTimeout(() => { ... }, 1000); | |
| 91 | - | fetch('https://api.example.com'); | |
| 92 | - | crypto.getRandomValues(new Uint8Array(16)); | |
| 93 | - | ||
| 94 | - | // AUTORISE - à l'intérieur d'un handler | |
| 95 | - | export const GET: RequestHandler = async () => { | |
| 96 | - | const random = crypto.getRandomValues(new Uint8Array(16)); | |
| 97 | - | const response = await fetch('https://api.example.com'); | |
| 98 | - | // ... | |
| 99 | - | }; | |
| 83 | + | ```bash | |
| 84 | + | docker build -t pdraim . | |
| 85 | + | docker run --rm -p 3000:3000 --env-file .env.local -e ORIGIN=http://localhost:3000 pdraim | |
| 100 | 86 | ``` | |
| 101 | 87 | ||
| 102 | - | ### Bonnes pratiques | |
| 103 | - | ||
| 104 | - | | A eviter | Alternative | | |
| 105 | - | | ----------------------------------------------------- | ------------------------------------------------------ | | |
| 106 | - | | `setInterval` pour cleanup | Lazy cleanup à chaque requête | | |
| 107 | - | | `fetch()` au chargement du module | `fetch()` dans les handlers | | |
| 108 | - | | Création de clients API au niveau module | Création lazy ou dans les handlers | | |
| 109 | - | | `process.exit()` | Retourner une Response avec status d'erreur | | |
| 110 | - | | Bibliothèques avec worker threads (ex: `pino-pretty`) | Versions compatibles edge (ex: `pino` sans transports) | | |
| 111 | - | ||
| 112 | - | ### Variables d'environnement | |
| 113 | - | ||
| 114 | - | - Utilisez `$env/dynamic/private` (runtime) au lieu de `$env/static/private` (build-time) | |
| 115 | - | - Les variables définies dans le dashboard Cloudflare ne sont disponibles qu'au runtime | |
| 116 | - | ||
| 117 | - | ### Références | |
| 88 | + | Toutes les variables sont lues au runtime (`$env/dynamic/*`) : la même image sert en local et en production. | |
| 118 | 89 | ||
| 119 | - | - [Cloudflare Workers Runtime APIs](https://developers.cloudflare.com/workers/runtime-apis/) | |
| 120 | - | - [SvelteKit Cloudflare Adapter](https://svelte.dev/docs/kit/adapter-cloudflare) | |
| 90 | + | | Variable | Rôle | | |
| 91 | + | | --------------------------------------------------- | --------------------------------------- | | |
| 92 | + | | `PUBLIC_CONVEX_URL`, `CONVEX_API_SECRET` | Accès à Convex | | |
| 93 | + | | `PUBLIC_TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile | | |
| 94 | + | | `ORIGIN` | URL publique (ex. `https://pdraim.org`) | | |
| 95 | + | | `ADDRESS_HEADER`, `XFF_DEPTH` | IP client derrière les reverse proxies | | |
| 96 | + | | `COOKIE_DOMAIN` (optionnel) | Cookie partagé entre sous-domaines | | |
| 121 | 97 | ||
| 122 | 98 | ## Documentation et Ressources | |
| 123 | 99 | ||
| @@ -76,48 +76,24 @@ Vous pouvez visualiser et gérer vos données via le dashboard Convex : | |||||
| 76 | npx convex dashboard | 76 | npx convex dashboard | ||
| 77 | ``` | 77 | ``` | ||
| 78 | 78 | ||||
| 79 | - | ## Déploiement Cloudflare Pages | 79 | + | ## Déploiement |
| 80 | 80 | ||||
| 81 | - | Le projet est déployé sur [Cloudflare Pages](https://pages.cloudflare.com/). Les Workers Cloudflare ont des **limitations strictes** à respecter. | 81 | + | Le site tourne dans une image conteneur Node ([`adapter-node`](https://svelte.dev/docs/kit/adapter-node)), écoutant sur le port `3000`. |
| 82 | 82 | ||||
| 83 | - | ### Opérations interdites au niveau global/module | 83 | + | ```bash |
| 84 | - | 84 | + | docker build -t pdraim . | |
| 85 | - | Cloudflare Workers **n'autorisent PAS** les opérations suivantes au niveau global (en dehors des handlers de requête) : | 85 | + | docker run --rm -p 3000:3000 --env-file .env.local -e ORIGIN=http://localhost:3000 pdraim |
| 86 | - | ||||
| 87 | - | ```typescript | |||
| 88 | - | // INTERDIT - provoque "Disallowed operation called within global scope" | |||
| 89 | - | setInterval(() => { ... }, 1000); | |||
| 90 | - | setTimeout(() => { ... }, 1000); | |||
| 91 | - | fetch('https://api.example.com'); | |||
| 92 | - | crypto.getRandomValues(new Uint8Array(16)); | |||
| 93 | - | ||||
| 94 | - | // AUTORISE - à l'intérieur d'un handler | |||
| 95 | - | export const GET: RequestHandler = async () => { | |||
| 96 | - | const random = crypto.getRandomValues(new Uint8Array(16)); | |||
| 97 | - | const response = await fetch('https://api.example.com'); | |||
| 98 | - | // ... | |||
| 99 | - | }; | |||
| 100 | ``` | 86 | ``` | ||
| 101 | 87 | ||||
| 102 | - | ### Bonnes pratiques | 88 | + | Toutes les variables sont lues au runtime (`$env/dynamic/*`) : la même image sert en local et en production. |
| 103 | - | ||||
| 104 | - | | A eviter | Alternative | | |||
| 105 | - | | ----------------------------------------------------- | ------------------------------------------------------ | | |||
| 106 | - | | `setInterval` pour cleanup | Lazy cleanup à chaque requête | | |||
| 107 | - | | `fetch()` au chargement du module | `fetch()` dans les handlers | | |||
| 108 | - | | Création de clients API au niveau module | Création lazy ou dans les handlers | | |||
| 109 | - | | `process.exit()` | Retourner une Response avec status d'erreur | | |||
| 110 | - | | Bibliothèques avec worker threads (ex: `pino-pretty`) | Versions compatibles edge (ex: `pino` sans transports) | | |||
| 111 | - | ||||
| 112 | - | ### Variables d'environnement | |||
| 113 | - | ||||
| 114 | - | - Utilisez `$env/dynamic/private` (runtime) au lieu de `$env/static/private` (build-time) | |||
| 115 | - | - Les variables définies dans le dashboard Cloudflare ne sont disponibles qu'au runtime | |||
| 116 | - | ||||
| 117 | - | ### Références | |||
| 118 | 89 | ||||
| 119 | - | - [Cloudflare Workers Runtime APIs](https://developers.cloudflare.com/workers/runtime-apis/) | 90 | + | | Variable | Rôle | |
| 120 | - | - [SvelteKit Cloudflare Adapter](https://svelte.dev/docs/kit/adapter-cloudflare) | 91 | + | | --------------------------------------------------- | --------------------------------------- | |
| 92 | + | | `PUBLIC_CONVEX_URL`, `CONVEX_API_SECRET` | Accès à Convex | | |||
| 93 | + | | `PUBLIC_TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY` | Cloudflare Turnstile | | |||
| 94 | + | | `ORIGIN` | URL publique (ex. `https://pdraim.org`) | | |||
| 95 | + | | `ADDRESS_HEADER`, `XFF_DEPTH` | IP client derrière les reverse proxies | | |||
| 96 | + | | `COOKIE_DOMAIN` (optionnel) | Cookie partagé entre sous-domaines | | |||
| 121 | 97 | ||||
| 122 | ## Documentation et Ressources | 98 | ## Documentation et Ressources | ||
| 123 | 99 | ||||
M src/hooks.server.ts +1 -1
| @@ -34,7 +34,7 @@ async function setAllUsersOffline() { | |||
| 34 | 34 | } | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | - | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | |
| 37 | + | // Initialize server lazily on the first API request | |
| 38 | 38 | let initPromise: Promise<void> | null = null; | |
| 39 | 39 | let lastFailedAttempt = 0; | |
| 40 | 40 | let failureCount = 0; | |
| @@ -34,7 +34,7 @@ async function setAllUsersOffline() { | |||||
| 34 | } | 34 | } | ||
| 35 | } | 35 | } | ||
| 36 | 36 | ||||
| 37 | - | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | 37 | + | // Initialize server lazily on the first API request |
| 38 | let initPromise: Promise<void> | null = null; | 38 | let initPromise: Promise<void> | null = null; | ||
| 39 | let lastFailedAttempt = 0; | 39 | let lastFailedAttempt = 0; | ||
| 40 | let failureCount = 0; | 40 | let failureCount = 0; | ||
M src/lib/api/rate-limiter.ts +1 -2
| @@ -36,7 +36,7 @@ const MAX_RATE_LIMIT_DURATION = Math.max( | |||
| 36 | 36 | RATE_LIMITS.protected.durationMs | |
| 37 | 37 | ); | |
| 38 | 38 | ||
| 39 | - | // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope) | |
| 39 | + | // Lazy cleanup: remove stale entries on each check | |
| 40 | 40 | let lastCleanup = 0; | |
| 41 | 41 | ||
| 42 | 42 | function cleanupStaleEntries() { | |
| @@ -69,7 +69,6 @@ function isRateLimited( | |||
| 69 | 69 | ip: string, | |
| 70 | 70 | endpointType: keyof typeof RATE_LIMITS | |
| 71 | 71 | ): { limited: boolean; retryAfter?: number } { | |
| 72 | - | // Lazy cleanup on each check (replaces setInterval for Cloudflare compatibility) | |
| 73 | 72 | cleanupStaleEntries(); | |
| 74 | 73 | ||
| 75 | 74 | // Skip rate limiting completely in development for localhost | |
| @@ -36,7 +36,7 @@ const MAX_RATE_LIMIT_DURATION = Math.max( | |||||
| 36 | RATE_LIMITS.protected.durationMs | 36 | RATE_LIMITS.protected.durationMs | ||
| 37 | ); | 37 | ); | ||
| 38 | 38 | ||||
| 39 | - | // Lazy cleanup: remove stale entries on each check (Cloudflare Workers don't allow setInterval at global scope) | 39 | + | // Lazy cleanup: remove stale entries on each check |
| 40 | let lastCleanup = 0; | 40 | let lastCleanup = 0; | ||
| 41 | 41 | ||||
| 42 | function cleanupStaleEntries() { | 42 | function cleanupStaleEntries() { | ||
| @@ -69,7 +69,6 @@ function isRateLimited( | |||||
| 69 | ip: string, | 69 | ip: string, | ||
| 70 | endpointType: keyof typeof RATE_LIMITS | 70 | endpointType: keyof typeof RATE_LIMITS | ||
| 71 | ): { limited: boolean; retryAfter?: number } { | 71 | ): { limited: boolean; retryAfter?: number } { | ||
| 72 | - | // Lazy cleanup on each check (replaces setInterval for Cloudflare compatibility) | |||
| 73 | cleanupStaleEntries(); | 72 | cleanupStaleEntries(); | ||
| 74 | 73 | ||||
| 75 | // Skip rate limiting completely in development for localhost | 74 | // Skip rate limiting completely in development for localhost | ||
M src/lib/api/session.cookie.ts +1 -19
| @@ -5,31 +5,13 @@ import { createLogger } from '$lib/utils/logger.server'; | |||
| 5 | 5 | ||
| 6 | 6 | const log = createLogger('session-cookie'); | |
| 7 | 7 | ||
| 8 | - | // Helper function to determine cookie domain | |
| 8 | + | // Host-only cookie by default; set COOKIE_DOMAIN for cross-subdomain cookies | |
| 9 | 9 | function getCookieDomain(): string | undefined { | |
| 10 | - | // Allow explicit override via environment variable | |
| 11 | 10 | const envDomain = env.COOKIE_DOMAIN; | |
| 12 | 11 | if (envDomain) { | |
| 13 | 12 | log.debug('Using COOKIE_DOMAIN from environment:', { domain: envDomain }); | |
| 14 | 13 | return envDomain; | |
| 15 | 14 | } | |
| 16 | - | ||
| 17 | - | if (!dev) { | |
| 18 | - | // Check for Cloudflare Pages domain | |
| 19 | - | if (env.CF_PAGES_URL) { | |
| 20 | - | const url = env.CF_PAGES_URL; | |
| 21 | - | log.debug('Cloudflare Pages URL:', { url }); | |
| 22 | - | // For preview deployments, use host-only cookie | |
| 23 | - | if (env.CF_PAGES_BRANCH !== 'main') { | |
| 24 | - | return undefined; | |
| 25 | - | } | |
| 26 | - | // For production on Cloudflare Pages, use host-only cookie (safest default) | |
| 27 | - | // Set COOKIE_DOMAIN explicitly if cross-subdomain cookies are needed | |
| 28 | - | return undefined; | |
| 29 | - | } | |
| 30 | - | // Production without explicit domain - use host-only cookie (safest default) | |
| 31 | - | return undefined; | |
| 32 | - | } | |
| 33 | 15 | return undefined; | |
| 34 | 16 | } | |
| 35 | 17 | ||
| @@ -5,31 +5,13 @@ import { createLogger } from '$lib/utils/logger.server'; | |||||
| 5 | 5 | ||||
| 6 | const log = createLogger('session-cookie'); | 6 | const log = createLogger('session-cookie'); | ||
| 7 | 7 | ||||
| 8 | - | // Helper function to determine cookie domain | 8 | + | // Host-only cookie by default; set COOKIE_DOMAIN for cross-subdomain cookies |
| 9 | function getCookieDomain(): string | undefined { | 9 | function getCookieDomain(): string | undefined { | ||
| 10 | - | // Allow explicit override via environment variable | |||
| 11 | const envDomain = env.COOKIE_DOMAIN; | 10 | const envDomain = env.COOKIE_DOMAIN; | ||
| 12 | if (envDomain) { | 11 | if (envDomain) { | ||
| 13 | log.debug('Using COOKIE_DOMAIN from environment:', { domain: envDomain }); | 12 | log.debug('Using COOKIE_DOMAIN from environment:', { domain: envDomain }); | ||
| 14 | return envDomain; | 13 | return envDomain; | ||
| 15 | } | 14 | } | ||
| 16 | - | ||||
| 17 | - | if (!dev) { | |||
| 18 | - | // Check for Cloudflare Pages domain | |||
| 19 | - | if (env.CF_PAGES_URL) { | |||
| 20 | - | const url = env.CF_PAGES_URL; | |||
| 21 | - | log.debug('Cloudflare Pages URL:', { url }); | |||
| 22 | - | // For preview deployments, use host-only cookie | |||
| 23 | - | if (env.CF_PAGES_BRANCH !== 'main') { | |||
| 24 | - | return undefined; | |||
| 25 | - | } | |||
| 26 | - | // For production on Cloudflare Pages, use host-only cookie (safest default) | |||
| 27 | - | // Set COOKIE_DOMAIN explicitly if cross-subdomain cookies are needed | |||
| 28 | - | return undefined; | |||
| 29 | - | } | |||
| 30 | - | // Production without explicit domain - use host-only cookie (safest default) | |||
| 31 | - | return undefined; | |||
| 32 | - | } | |||
| 33 | return undefined; | 15 | return undefined; | ||
| 34 | } | 16 | } | ||
| 35 | 17 | ||||
M src/lib/components/turnstile.svelte +2 -2
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | <script lang="ts"> | |
| 2 | 2 | import { onMount } from 'svelte'; | |
| 3 | 3 | import { fade } from 'svelte/transition'; | |
| 4 | - | import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public'; | |
| 4 | + | import { env } from '$env/dynamic/public'; | |
| 5 | 5 | import type { TurnstileOptions } from '$lib/types/turnstile'; | |
| 6 | 6 | ||
| 7 | 7 | let isVerified = $state(false); | |
| @@ -13,7 +13,7 @@ | |||
| 13 | 13 | let widgetId = ''; | |
| 14 | 14 | ||
| 15 | 15 | // Ensure site key is a string and trim any whitespace | |
| 16 | - | const siteKey = String(PUBLIC_TURNSTILE_SITE_KEY).trim(); | |
| 16 | + | const siteKey = String(env.PUBLIC_TURNSTILE_SITE_KEY ?? '').trim(); | |
| 17 | 17 | ||
| 18 | 18 | onMount(() => { | |
| 19 | 19 | if (!window.turnstile) { | |
| @@ -1,7 +1,7 @@ | |||||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> | ||
| 2 | import { onMount } from 'svelte'; | 2 | import { onMount } from 'svelte'; | ||
| 3 | import { fade } from 'svelte/transition'; | 3 | import { fade } from 'svelte/transition'; | ||
| 4 | - | import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public'; | 4 | + | import { env } from '$env/dynamic/public'; |
| 5 | import type { TurnstileOptions } from '$lib/types/turnstile'; | 5 | import type { TurnstileOptions } from '$lib/types/turnstile'; | ||
| 6 | 6 | ||||
| 7 | let isVerified = $state(false); | 7 | let isVerified = $state(false); | ||
| @@ -13,7 +13,7 @@ | |||||
| 13 | let widgetId = ''; | 13 | let widgetId = ''; | ||
| 14 | 14 | ||||
| 15 | // Ensure site key is a string and trim any whitespace | 15 | // Ensure site key is a string and trim any whitespace | ||
| 16 | - | const siteKey = String(PUBLIC_TURNSTILE_SITE_KEY).trim(); | 16 | + | const siteKey = String(env.PUBLIC_TURNSTILE_SITE_KEY ?? '').trim(); |
| 17 | 17 | ||||
| 18 | onMount(() => { | 18 | onMount(() => { | ||
| 19 | if (!window.turnstile) { | 19 | if (!window.turnstile) { | ||
M src/lib/utils/logger.server.ts +1 -2
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | import pino from 'pino'; | |
| 2 | 2 | ||
| 3 | - | // Pino without transports works in Cloudflare Workers (no worker threads) | |
| 4 | - | // transports like pino-pretty use worker threads which don't exist in CF Workers | |
| 3 | + | // JSON logs on stdout, collected by the container runtime | |
| 5 | 4 | // Use 'debug' level - filtering happens at log time based on env | |
| 6 | 5 | const logger = pino({ | |
| 7 | 6 | level: 'debug', | |
| @@ -1,7 +1,6 @@ | |||||
| 1 | import pino from 'pino'; | 1 | import pino from 'pino'; | ||
| 2 | 2 | ||||
| 3 | - | // Pino without transports works in Cloudflare Workers (no worker threads) | 3 | + | // JSON logs on stdout, collected by the container runtime |
| 4 | - | // transports like pino-pretty use worker threads which don't exist in CF Workers | |||
| 5 | // Use 'debug' level - filtering happens at log time based on env | 4 | // Use 'debug' level - filtering happens at log time based on env | ||
| 6 | const logger = pino({ | 5 | const logger = pino({ | ||
| 7 | level: 'debug', | 6 | level: 'debug', | ||
M src/lib/utils/turnstile.server.ts +2 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | import { TURNSTILE_SECRET_KEY } from '$env/static/private'; | |
| 1 | + | import { env } from '$env/dynamic/private'; | |
| 2 | 2 | import { dev } from '$app/environment'; | |
| 3 | 3 | import { createLogger } from './logger.server'; | |
| 4 | 4 | ||
| @@ -22,7 +22,7 @@ export async function validateTurnstileToken(token: string, remoteip?: string): | |||
| 22 | 22 | ||
| 23 | 23 | try { | |
| 24 | 24 | const formData = new FormData(); | |
| 25 | - | formData.append('secret', TURNSTILE_SECRET_KEY); | |
| 25 | + | formData.append('secret', env.TURNSTILE_SECRET_KEY ?? ''); | |
| 26 | 26 | formData.append('response', token); | |
| 27 | 27 | if (remoteip) { | |
| 28 | 28 | formData.append('remoteip', remoteip); | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | import { TURNSTILE_SECRET_KEY } from '$env/static/private'; | 1 | + | import { env } from '$env/dynamic/private'; |
| 2 | import { dev } from '$app/environment'; | 2 | import { dev } from '$app/environment'; | ||
| 3 | import { createLogger } from './logger.server'; | 3 | import { createLogger } from './logger.server'; | ||
| 4 | 4 | ||||
| @@ -22,7 +22,7 @@ export async function validateTurnstileToken(token: string, remoteip?: string): | |||||
| 22 | 22 | ||||
| 23 | try { | 23 | try { | ||
| 24 | const formData = new FormData(); | 24 | const formData = new FormData(); | ||
| 25 | - | formData.append('secret', TURNSTILE_SECRET_KEY); | 25 | + | formData.append('secret', env.TURNSTILE_SECRET_KEY ?? ''); |
| 26 | formData.append('response', token); | 26 | formData.append('response', token); | ||
| 27 | if (remoteip) { | 27 | if (remoteip) { | ||
| 28 | formData.append('remoteip', remoteip); | 28 | formData.append('remoteip', remoteip); | ||
M src/routes/api/register/+server.ts +1 -2
| @@ -10,7 +10,7 @@ const log = createLogger('register-server'); | |||
| 10 | 10 | // In-memory map to track failed captcha attempts per IP | |
| 11 | 11 | const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>(); | |
| 12 | 12 | ||
| 13 | - | // Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope | |
| 13 | + | // Lazy cleanup of stale captcha attempts | |
| 14 | 14 | const ONE_HOUR = 60 * 60 * 1000; | |
| 15 | 15 | let lastCleanup = 0; | |
| 16 | 16 | function cleanupOldCaptchaAttempts() { | |
| @@ -28,7 +28,6 @@ function cleanupOldCaptchaAttempts() { | |||
| 28 | 28 | export const POST: RequestHandler = async ({ request, getClientAddress }) => { | |
| 29 | 29 | log.debug('New registration attempt received'); | |
| 30 | 30 | ||
| 31 | - | // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility) | |
| 32 | 31 | cleanupOldCaptchaAttempts(); | |
| 33 | 32 | ||
| 34 | 33 | if (request.method !== 'POST') { | |
| @@ -10,7 +10,7 @@ const log = createLogger('register-server'); | |||||
| 10 | // In-memory map to track failed captcha attempts per IP | 10 | // In-memory map to track failed captcha attempts per IP | ||
| 11 | const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>(); | 11 | const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>(); | ||
| 12 | 12 | ||||
| 13 | - | // Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope | 13 | + | // Lazy cleanup of stale captcha attempts |
| 14 | const ONE_HOUR = 60 * 60 * 1000; | 14 | const ONE_HOUR = 60 * 60 * 1000; | ||
| 15 | let lastCleanup = 0; | 15 | let lastCleanup = 0; | ||
| 16 | function cleanupOldCaptchaAttempts() { | 16 | function cleanupOldCaptchaAttempts() { | ||
| @@ -28,7 +28,6 @@ function cleanupOldCaptchaAttempts() { | |||||
| 28 | export const POST: RequestHandler = async ({ request, getClientAddress }) => { | 28 | export const POST: RequestHandler = async ({ request, getClientAddress }) => { | ||
| 29 | log.debug('New registration attempt received'); | 29 | log.debug('New registration attempt received'); | ||
| 30 | 30 | ||||
| 31 | - | // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility) | |||
| 32 | cleanupOldCaptchaAttempts(); | 31 | cleanupOldCaptchaAttempts(); | ||
| 33 | 32 | ||||
| 34 | if (request.method !== 'POST') { | 33 | if (request.method !== 'POST') { | ||
M src/routes/api/session/login/+server.ts +1 -2
| @@ -17,7 +17,7 @@ const MAX_ATTEMPTS = 3; | |||
| 17 | 17 | const ONE_HOUR = 60 * 60 * 1000; | |
| 18 | 18 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| 19 | 19 | ||
| 20 | - | // Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope | |
| 20 | + | // Lazy cleanup of stale login attempts | |
| 21 | 21 | let lastCleanup = 0; | |
| 22 | 22 | function cleanupOldAttempts() { | |
| 23 | 23 | const now = Date.now(); | |
| @@ -34,7 +34,6 @@ function cleanupOldAttempts() { | |||
| 34 | 34 | export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { | |
| 35 | 35 | log.debug('New login attempt received'); | |
| 36 | 36 | ||
| 37 | - | // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility) | |
| 38 | 37 | cleanupOldAttempts(); | |
| 39 | 38 | ||
| 40 | 39 | if (request.method !== 'POST') { | |
| @@ -17,7 +17,7 @@ const MAX_ATTEMPTS = 3; | |||||
| 17 | const ONE_HOUR = 60 * 60 * 1000; | 17 | const ONE_HOUR = 60 * 60 * 1000; | ||
| 18 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | 18 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
| 19 | 19 | ||||
| 20 | - | // Lazy cleanup: Cloudflare Workers don't allow setInterval at global scope | 20 | + | // Lazy cleanup of stale login attempts |
| 21 | let lastCleanup = 0; | 21 | let lastCleanup = 0; | ||
| 22 | function cleanupOldAttempts() { | 22 | function cleanupOldAttempts() { | ||
| 23 | const now = Date.now(); | 23 | const now = Date.now(); | ||
| @@ -34,7 +34,6 @@ function cleanupOldAttempts() { | |||||
| 34 | export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { | 34 | export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { | ||
| 35 | log.debug('New login attempt received'); | 35 | log.debug('New login attempt received'); | ||
| 36 | 36 | ||||
| 37 | - | // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility) | |||
| 38 | cleanupOldAttempts(); | 37 | cleanupOldAttempts(); | ||
| 39 | 38 | ||||
| 40 | if (request.method !== 'POST') { | 39 | if (request.method !== 'POST') { | ||
M svelte.config.js +1 -4
| @@ -1,4 +1,4 @@ | |||
| 1 | - | import adapter from '@sveltejs/adapter-cloudflare'; | |
| 1 | + | import adapter from '@sveltejs/adapter-node'; | |
| 2 | 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; | |
| 3 | 3 | ||
| 4 | 4 | /** @type {import('@sveltejs/kit').Config} */ | |
| @@ -8,9 +8,6 @@ const config = { | |||
| 8 | 8 | preprocess: vitePreprocess(), | |
| 9 | 9 | ||
| 10 | 10 | kit: { | |
| 11 | - | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. | |
| 12 | - | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. | |
| 13 | - | // See https://svelte.dev/docs/kit/adapters for more information about adapters. | |
| 14 | 11 | adapter: adapter(), | |
| 15 | 12 | csp: { | |
| 16 | 13 | mode: 'auto', | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | import adapter from '@sveltejs/adapter-cloudflare'; | 1 | + | import adapter from '@sveltejs/adapter-node'; |
| 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; | 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; | ||
| 3 | 3 | ||||
| 4 | /** @type {import('@sveltejs/kit').Config} */ | 4 | /** @type {import('@sveltejs/kit').Config} */ | ||
| @@ -8,9 +8,6 @@ const config = { | |||||
| 8 | preprocess: vitePreprocess(), | 8 | preprocess: vitePreprocess(), | ||
| 9 | 9 | ||||
| 10 | kit: { | 10 | kit: { | ||
| 11 | - | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. | |||
| 12 | - | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. | |||
| 13 | - | // See https://svelte.dev/docs/kit/adapters for more information about adapters. | |||
| 14 | adapter: adapter(), | 11 | adapter: adapter(), | ||
| 15 | csp: { | 12 | csp: { | ||
| 16 | mode: 'auto', | 13 | mode: 'auto', | ||