diff --git a/.env.example b/.env.example
deleted file mode 100644
index d770447..0000000
--- a/.env.example
+++ /dev/null
@@ -1,15 +0,0 @@
-# Auth.js Configuration
-AUTH_SECRET=your-secret-here-min-32-chars-long
-
-# Google OAuth Configuration
-GOOGLE_CLIENT_ID=your-google-client-id
-GOOGLE_CLIENT_SECRET=your-google-client-secret
-
-# Facebook OAuth Configuration
-FACEBOOK_CLIENT_ID=your-facebook-app-id
-FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
-
-# Email Encryption Configuration (Optional)
-# Generate a strong 32+ character key for encrypting email addresses in the database
-# If not provided, emails will be stored in plaintext (not recommended for production)
-EMAIL_ENCRYPTION_KEY=your-email-encryption-key-min-32-chars-long
\ No newline at end of file
diff --git a/functions/_shared/types.ts b/functions/_shared/types.ts
index 798cb5f..e865b40 100644
--- a/functions/_shared/types.ts
+++ b/functions/_shared/types.ts
@@ -1,5 +1,7 @@
///
+import type { AuthenticatedUser } from "./utils"
+
export interface Env {
DB: D1Database
CACHE: KVNamespace
@@ -22,3 +24,10 @@ export interface EventContext {
next(input?: Request | string, init?: RequestInit): Promise
data: Record
}
+export type AuthResult = {
+ success: true
+ user: AuthenticatedUser
+ } | {
+ success: false
+ response: Response
+ }
\ No newline at end of file
diff --git a/functions/_shared/validation.ts b/functions/_shared/validation.ts
new file mode 100644
index 0000000..5a3038a
--- /dev/null
+++ b/functions/_shared/validation.ts
@@ -0,0 +1,69 @@
+// Centralized validation schemas
+import { z } from 'zod'
+
+// Base schemas
+export const IdParamSchema = z.string().regex(/^\d+$/, 'Invalid ID format').transform(Number)
+export const SlugSchema = z.string().min(1, 'Slug is required').max(100, 'Slug too long')
+export const PaginationSchema = z.object({
+ limit: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(1).max(100)),
+ offset: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(0))
+})
+
+// Petition schemas
+export const CreatePetitionSchema = z.object({
+ title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
+ description: z.string().min(10, 'Description must be at least 10 characters').max(5000, 'Description too long'),
+ type: z.enum(['local', 'national'], { errorMap: () => ({ message: 'Type must be "local" or "national"' }) }),
+ image_url: z.string().url().optional(),
+ target_count: z.number().min(1, 'Target count must be at least 1').max(1000000, 'Target count too high').optional(),
+ location: z.string().max(100, 'Location too long').optional(),
+ due_date: z.string().datetime().optional(),
+ category_ids: z.array(z.number()).max(5, 'Maximum 5 categories allowed').optional()
+})
+
+export const UpdatePetitionSchema = CreatePetitionSchema.partial()
+
+// Signature schemas
+export const CreateSignatureSchema = z.object({
+ petition_id: z.number().positive('Invalid petition ID'),
+ comment: z.string().max(1000, 'Comment too long').optional(),
+ anonymous: z.boolean().optional()
+})
+
+// Report schemas
+export const CreateReportSchema = z.object({
+ reported_item_type: z.enum(['petition', 'signature'], {
+ errorMap: () => ({ message: 'Reported item type must be "petition" or "signature"' })
+ }),
+ reported_item_id: z.number().positive('Invalid reported item ID'),
+ report_reason: z.enum([
+ 'spam', 'inappropriate_content', 'harassment', 'misinformation',
+ 'hate_speech', 'violence', 'copyright_violation', 'other'
+ ], { errorMap: () => ({ message: 'Invalid report reason' }) }),
+ report_description: z.string().max(1000, 'Description too long').optional()
+})
+
+export const UpdateReportSchema = z.object({
+ status: z.enum(['pending', 'reviewed', 'resolved', 'dismissed']).optional(),
+ admin_notes: z.string().max(1000, 'Admin notes too long').optional()
+})
+
+// Category schemas
+export const CreateCategorySchema = z.object({
+ name: z.string().min(1, 'Category name is required').max(50, 'Category name too long'),
+ description: z.string().max(200, 'Description too long').optional()
+})
+
+// Query parameter schemas
+export const PetitionQuerySchema = z.object({
+ type: z.enum(['local', 'national']).optional(),
+ limit: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(1).max(100)).optional(),
+ offset: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(0)).optional(),
+ userId: z.string().optional()
+})
+
+export const ReportQuerySchema = z.object({
+ status: z.enum(['pending', 'reviewed', 'resolved', 'dismissed']).optional(),
+ limit: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(1).max(100)).optional(),
+ offset: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(0)).optional()
+})
\ No newline at end of file
diff --git a/functions/helpers/context.ts b/functions/helpers/context.ts
new file mode 100644
index 0000000..ff16815
--- /dev/null
+++ b/functions/helpers/context.ts
@@ -0,0 +1,18 @@
+import type { Env, EventContext } from '../_shared/types'
+
+export function createContext(
+ request: Request,
+ env: Env,
+ params: Record = {}
+): EventContext {
+ return {
+ request,
+ env,
+ params,
+ waitUntil: () => {},
+ next: async () => new Response('Not implemented', { status: 501 }),
+ data: {},
+ }
+}
+
+
diff --git a/functions/helpers/wrappers.ts b/functions/helpers/wrappers.ts
new file mode 100644
index 0000000..14c41b5
--- /dev/null
+++ b/functions/helpers/wrappers.ts
@@ -0,0 +1,57 @@
+import type { Env, EventContext } from '../_shared/types'
+import { createContext } from './context'
+import { requireAuth, requirePetitionOwnership } from '../middleware/auth-middleware'
+
+export function wrapHandler(handler: (context: EventContext) => Promise) {
+ return async (request: Request, env: Env) => {
+ const context = createContext(request, env, request.params || {})
+ return await handler(context)
+ }
+}
+
+export function wrapHandlerWithAuth(handler: (context: EventContext) => Promise) {
+ return async (request: Request, env: Env) => {
+ const context = createContext(request, env, request.params || {})
+ const authResult = await requireAuth(context)
+ if (!authResult.success) {
+ return authResult.response
+ }
+ context.data.user = authResult.user
+ return await handler(context)
+ }
+}
+
+export function wrapHandlerWithConditionalAuth(handler: (context: EventContext) => Promise) {
+ return async (request: Request, env: Env) => {
+ const url = new URL(request.url)
+ const userId = url.searchParams.get('userId')
+
+ const context = createContext(request, env, request.params || {})
+
+ if (userId) {
+ const authResult = await requireAuth(context)
+ if (!authResult.success) {
+ return authResult.response
+ }
+ context.data.user = authResult.user
+ return await handler(context)
+ }
+
+ return await handler(context)
+ }
+}
+
+export function wrapHandlerWithOwnership(handler: (context: EventContext) => Promise) {
+ return async (request: Request, env: Env) => {
+ const petitionId = parseInt(request.params?.id || '0')
+ const context = createContext(request, env, request.params || {})
+ const authResult = await requirePetitionOwnership(context, petitionId)
+ if (!authResult.success) {
+ return authResult.response
+ }
+ context.data.user = authResult.user
+ return await handler(context)
+ }
+}
+
+
diff --git a/functions/index.ts b/functions/index.ts
index 62566c2..142cbb5 100644
--- a/functions/index.ts
+++ b/functions/index.ts
@@ -1,5 +1,21 @@
-// Main Cloudflare Worker entry point - routes to individual functions
-import type { Env, EventContext } from './_shared/types'
+// Main Cloudflare Worker entry point with itty-router
+import { AutoRouter } from 'itty-router'
+import { withParams } from 'itty-router-extras'
+import type { Env } from './_shared/types'
+import { createContext } from './helpers/context'
+import {
+ wrapHandler,
+ wrapHandlerWithAuth,
+ wrapHandlerWithConditionalAuth,
+ wrapHandlerWithOwnership,
+} from './helpers/wrappers'
+
+// Extend Request type to include params
+declare global {
+ interface Request {
+ params?: Record
+ }
+}
// Import individual function handlers
import { onRequest as uptimeHandler } from './api/uptime'
@@ -17,49 +33,75 @@ import { onRequest as categoriesHandler } from './api/categories'
import { onRequest as reportsHandler } from './api/reports'
import { onRequest as adminReportsHandler } from './api/admin/reports'
import { onRequest as authHandler } from './auth/[...auth]'
+
import {
getCorsHeaders,
handleCORS,
- requireAuthentication,
- requirePetitionOwnership,
} from './_shared/utils'
-
-function createContext(
- request: Request,
- env: Env,
- params: Record = {}
-): EventContext {
- return {
- request,
- env,
- params,
- waitUntil: () => {}, // Simplified for now
- next: async () => new Response('Not implemented', { status: 501 }),
- data: {},
- }
-}
-
-function parsePathParams(path: string, pattern: string): Record {
- const pathParts = path.split('/').filter(p => p)
- const patternParts = pattern.split('/').filter(p => p)
- const params: Record = {}
-
- for (let i = 0; i < patternParts.length; i++) {
- const part = patternParts[i]
- if (part.startsWith('[') && part.endsWith(']')) {
- const paramName = part.slice(1, -1)
- params[paramName] = pathParts[i] || ''
- }
- }
-
- return params
-}
+// auth middleware used inside helpers/wrappers
+
+// moved to ./helpers/context
+
+// Create router with params middleware
+const router = AutoRouter()
+
+// Apply withParams middleware to all routes
+router.all('*', withParams)
+
+// wrappers moved to ./helpers/wrappers
+
+// Auth routes - handle all /auth/* paths
+router.all('/auth/*', async (request, env: Env) => {
+ const response = await authHandler(createContext(request, env))
+ // Add CORS headers to auth responses
+ const corsHeaders = getCorsHeaders(request, env)
+ Object.entries(corsHeaders).forEach(([key, value]) => {
+ response.headers.set(key, value)
+ })
+ return response
+})
+
+// Public routes (no authentication required)
+router
+ .get('/api/uptime', wrapHandler(uptimeHandler))
+ .get('/api/petitions', wrapHandlerWithConditionalAuth(petitionsHandler))
+ .get('/api/petition/:slug', wrapHandler(petitionBySlugHandler))
+ .get('/api/petitions/:id', wrapHandler(petitionByIdHandler))
+ .get('/api/petitions/:id/signatures', wrapHandler(petitionSignaturesHandler))
+ .get('/api/categories', wrapHandler(categoriesHandler))
+
+// Protected routes (authentication required)
+router
+ .post('/api/petitions', wrapHandlerWithAuth(petitionsHandler))
+ .post('/api/signatures', wrapHandlerWithAuth(signaturesHandler))
+ .post('/api/reports', wrapHandlerWithAuth(reportsHandler))
+ .get('/api/users/:id', wrapHandlerWithAuth(userByIdHandler))
+ .get('/api/users/:id/signatures', wrapHandlerWithAuth(userSignaturesHandler))
+ .get('/api/users/:id/petitions', wrapHandlerWithAuth(userPetitionsHandler))
+
+// Petition ownership routes (require petition ownership)
+router
+ .put('/api/petitions/:id', wrapHandlerWithOwnership(petitionByIdHandler))
+ .delete('/api/petitions/:id', wrapHandlerWithOwnership(petitionByIdHandler))
+ .post('/api/petition/:id/publish', wrapHandlerWithOwnership(petitionPublishHandler))
+ .post('/api/petition/:id/unpublish', wrapHandlerWithOwnership(petitionUnpublishHandler))
+
+// Admin routes
+router
+ .get('/api/admin/reports', wrapHandlerWithAuth(adminReportsHandler))
+ .put('/api/admin/reports/:id', wrapHandlerWithAuth(adminReportsHandler))
+
+// 404 handler
+router.all('*', (request, env: Env) => {
+ const corsHeaders = getCorsHeaders(request, env)
+ return new Response('Not Found', {
+ status: 404,
+ headers: corsHeaders,
+ })
+})
export default {
async fetch(request: Request, env: Env): Promise {
- const url = new URL(request.url)
- const path = url.pathname
-
// Handle preflight OPTIONS requests
const corsResponse = handleCORS(request, env)
if (corsResponse) {
@@ -67,176 +109,7 @@ export default {
}
try {
- // Route to appropriate function handler
-
- // Auth routes - handle all /auth/* paths
- if (path.startsWith('/auth/')) {
- const response = await authHandler(createContext(request, env))
- // Add CORS headers to auth responses
- const corsHeaders = getCorsHeaders(request, env)
- Object.entries(corsHeaders).forEach(([key, value]) => {
- response.headers.set(key, value)
- })
- return response
- }
-
- if (path === '/api/uptime') {
- return await uptimeHandler(createContext(request, env))
- }
-
- if (path.match(/^\/api\/users\/[^/]+\/signatures$/)) {
- const params = parsePathParams(path, '/api/users/[id]/signatures')
- // Require authentication for user signatures
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await userSignaturesHandler(context)
- }
-
- if (path.match(/^\/api\/users\/[^/]+\/petitions$/)) {
- const params = parsePathParams(path, '/api/users/[id]/petitions')
- // Require authentication for user petitions
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await userPetitionsHandler(context)
- }
-
- if (path.match(/^\/api\/users\/[^/]+$/)) {
- const params = parsePathParams(path, '/api/users/[id]')
- // Require authentication for user profiles
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await userByIdHandler(context)
- }
-
- if (path === '/api/petitions') {
- const url = new URL(request.url)
- const userId = url.searchParams.get('userId')
-
- // Require authentication for POST (creating petitions) or GET with userId parameter
- if (request.method === 'POST' || userId) {
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- // Add user to context for the handler
- const context = createContext(request, env)
- context.data.user = authResult.user
- return await petitionsHandler(context)
- }
- return await petitionsHandler(createContext(request, env))
- }
-
- if (path.match(/^\/api\/petition\/[^/]+$/)) {
- const params = parsePathParams(path, '/api/petition/[slug]')
- return await petitionBySlugHandler(createContext(request, env, params))
- }
-
- if (path.match(/^\/api\/petition\/\d+\/publish$/)) {
- const params = parsePathParams(path, '/api/petition/[id]/publish')
- // Require petition ownership for publishing
- const petitionId = parseInt(params.id)
- const authResult = await requirePetitionOwnership(request, env, petitionId)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await petitionPublishHandler(context)
- }
-
- if (path.match(/^\/api\/petition\/\d+\/unpublish$/)) {
- const params = parsePathParams(path, '/api/petition/[id]/unpublish')
- // Require petition ownership for unpublishing
- const petitionId = parseInt(params.id)
- const authResult = await requirePetitionOwnership(request, env, petitionId)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await petitionUnpublishHandler(context)
- }
-
- if (path.match(/^\/api\/petitions\/\d+\/signatures$/)) {
- const params = parsePathParams(path, '/api/petitions/[id]/signatures')
- return await petitionSignaturesHandler(createContext(request, env, params))
- }
-
- if (path.match(/^\/api\/petitions\/\d+$/)) {
- const params = parsePathParams(path, '/api/petitions/[id]')
- return await petitionByIdHandler(createContext(request, env, params))
- }
-
- if (path === '/api/signatures') {
- // Require authentication for POST (creating signatures)
- if (request.method === 'POST') {
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env)
- context.data.user = authResult.user
- return await signaturesHandler(context)
- }
- return await signaturesHandler(createContext(request, env))
- }
-
- if (path === '/api/categories') {
- return await categoriesHandler(createContext(request, env))
- }
-
- if (path === '/api/reports') {
- // Require authentication for reporting
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env)
- context.data.user = authResult.user
- return await reportsHandler(context)
- }
-
- if (path === '/api/admin/reports') {
- // Require authentication for admin reports
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env)
- context.data.user = authResult.user
- return await adminReportsHandler(context)
- }
-
- if (path.match(/^\/api\/admin\/reports\/\d+$/)) {
- const params = parsePathParams(path, '/api/admin/reports/[id]')
- // Require authentication for admin report updates
- const authResult = await requireAuthentication(request, env)
- if (authResult instanceof Response) {
- return authResult
- }
- const context = createContext(request, env, params)
- context.data.user = authResult.user
- return await adminReportsHandler(context)
- }
-
- // 404 for unmatched routes
- const corsHeaders = getCorsHeaders(request, env)
- return new Response('Not Found', {
- status: 404,
- headers: corsHeaders,
- })
+ return await router.fetch(request, env)
} catch (error: unknown) {
console.error('Router Error:', error)
const message = error instanceof Error ? error.message : 'Unknown error'
@@ -250,4 +123,4 @@ export default {
})
}
},
-}
+}
\ No newline at end of file
diff --git a/functions/middleware/auth-middleware.ts b/functions/middleware/auth-middleware.ts
new file mode 100644
index 0000000..69a31b6
--- /dev/null
+++ b/functions/middleware/auth-middleware.ts
@@ -0,0 +1,65 @@
+// functions/_shared/auth-middleware.ts
+import { getAuthenticatedUser, createErrorResponseWithCors, getDbService } from '../_shared/utils'
+import type { EventContext, Env } from '../_shared/types'
+import type { AuthResult } from '../_shared/types'
+
+
+
+export async function requireAuth(
+ context: EventContext
+): Promise {
+ const user = await getAuthenticatedUser(context.request, context.env)
+
+ if (!user) {
+ return {
+ success: false,
+ response: createErrorResponseWithCors(
+ 'Authentication required',
+ context.request,
+ context.env,
+ 401
+ )
+ }
+ }
+
+ return { success: true, user }
+}
+
+export async function requirePetitionOwnership(
+ context: EventContext,
+ petitionId: number
+): Promise {
+ const authResult = await requireAuth(context)
+ if (!authResult.success) {
+ return authResult
+ }
+
+ const db = getDbService(context)
+ const petition = await db.getPetitionById(petitionId)
+
+ if (!petition) {
+ return {
+ success: false,
+ response: createErrorResponseWithCors(
+ 'Petition not found',
+ context.request,
+ context.env,
+ 404
+ )
+ }
+ }
+
+ if (petition.created_by !== authResult.user.id) {
+ return {
+ success: false,
+ response: createErrorResponseWithCors(
+ 'You can only modify petitions you created',
+ context.request,
+ context.env,
+ 403
+ )
+ }
+ }
+
+ return authResult
+}
\ No newline at end of file
diff --git a/functions/middleware/with-middleware.ts b/functions/middleware/with-middleware.ts
new file mode 100644
index 0000000..7b12e77
--- /dev/null
+++ b/functions/middleware/with-middleware.ts
@@ -0,0 +1,44 @@
+import { requireAuth, requirePetitionOwnership } from './auth-middleware'
+import type { EventContext, Env } from '../_shared/types'
+import type { AuthenticatedUser } from '../_shared/utils'
+
+// Higher-order function to enforce authentication
+export function withAuth>(
+ handler: (context: T & { data: { user: AuthenticatedUser } }) => Promise
+) {
+ return async (context: T): Promise => {
+ const authResult = await requireAuth(context)
+ if (!authResult.success) return authResult.response
+
+ return handler({
+ ...context,
+ data: { ...context.data, user: authResult.user }
+ })
+ }
+}
+
+// Higher-order function to enforce petition ownership
+export function withPetitionOwnership>(
+ petitionIdExtractor: (context: T) => number,
+ handler: (context: T & { data: { user: AuthenticatedUser } }) => Promise
+) {
+ return async (context: T): Promise => {
+ const petitionId = petitionIdExtractor(context)
+ const authResult = await requirePetitionOwnership(context, petitionId)
+ if (!authResult.success) return authResult.response
+
+ return handler({
+ ...context,
+ data: { ...context.data, user: authResult.user }
+ })
+ }
+}
+
+// Higher-order function for public endpoints (no auth required)
+export function withPublic>(
+ handler: (context: T) => Promise
+) {
+ return async (context: T): Promise => {
+ return handler(context)
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index f0e3b90..73e179c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,8 @@
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
+ "itty-router": "^5.0.22",
+ "itty-router-extras": "^0.4.6",
"lucide-react": "^0.544.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -34,6 +36,7 @@
"@cloudflare/workers-types": "^4.20250924.0",
"@eslint/js": "^9.36.0",
"@playwright/test": "^1.55.1",
+ "@types/itty-router-extras": "^0.4.3",
"@types/node": "^24.5.2",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
@@ -2428,6 +2431,23 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/itty-router-extras": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@types/itty-router-extras/-/itty-router-extras-0.4.3.tgz",
+ "integrity": "sha512-mBLJnci8IQRqWCURrBW45XVtXtwuTig0DWKBZQlLB36yOZoxkNDf8pSsHW/gOFqyqtO21zcSfgTjrA6/bKJwEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "itty-router": "^2.3.10"
+ }
+ },
+ "node_modules/@types/itty-router-extras/node_modules/itty-router": {
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-2.6.6.tgz",
+ "integrity": "sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -6208,6 +6228,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/itty-router": {
+ "version": "5.0.22",
+ "resolved": "https://registry.npmjs.org/itty-router/-/itty-router-5.0.22.tgz",
+ "integrity": "sha512-9hmdGErWdYDOurGYxSbqLhy4EFReIwk71hMZTJ5b+zfa2zjMNV1ftFno2b8VjAQvX615gNB8Qxbl9JMRqHnIVA==",
+ "license": "MIT"
+ },
+ "node_modules/itty-router-extras": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/itty-router-extras/-/itty-router-extras-0.4.6.tgz",
+ "integrity": "sha512-6r7HQBkFMPSJfcKksrKC7avEQnPCSSEvoz6PAAZMNhz8hthYu1pzedXDrvTFDWXJosfuaittzoNciWHO/TxMaw==",
+ "license": "MIT"
+ },
"node_modules/jiti": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
diff --git a/package.json b/package.json
index ed88281..9169630 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,8 @@
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
+ "itty-router": "^5.0.22",
+ "itty-router-extras": "^0.4.6",
"lucide-react": "^0.544.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -51,6 +53,7 @@
"@cloudflare/workers-types": "^4.20250924.0",
"@eslint/js": "^9.36.0",
"@playwright/test": "^1.55.1",
+ "@types/itty-router-extras": "^0.4.3",
"@types/node": "^24.5.2",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",