npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@goobits/security

v1.2.0

Published

Comprehensive security utilities for Goobits projects - CSRF, reCAPTCHA, validation, rate limiting, admin auth, and audit logging

Readme

@goobits/security

Version 1.2.0 - Stable

Comprehensive security utilities for Goobits projects including CSRF protection, reCAPTCHA verification, request validation, rate limiting, admin authentication, and audit logging. Built for SvelteKit applications with integrated logging via @goobits/logger.

🔒 Features

  • 🛡️ CSRF Protection: Double-submit cookie pattern with timing-safe comparison and token expiration
  • 🤖 reCAPTCHA Verification: Support for both v2 and v3 with score thresholds and flexible options API
  • Request Validation: Zod-based middleware for body, query, and params
  • ⏱️ Rate Limiting: In-memory and Redis-based rate limiting with factory pattern ⭐ NEW in v1.2.0
  • 🔐 Admin Authentication: JWT-based admin auth with API key fallback ⭐ NEW in v1.2.0
  • 📝 Audit Logging: Security-sensitive operation tracking for GDPR compliance ⭐ NEW in v1.2.0
  • Token Expiration: Automatic CSRF token expiration and cleanup (1-hour default)
  • 📊 Integrated Logging: Built-in logging via @goobits/logger
  • 🔧 SvelteKit Native: Designed for SvelteKit's request/response model
  • 🧪 Test-Friendly: Debug flags for development and testing
  • 📦 Minimal Dependencies: Only depends on @goobits/logger, jsonwebtoken, and peer deps

📦 Installation

# In workspace monorepo
pnpm add @goobits/security

# Or in individual package
npm install @goobits/security zod

🛡️ CSRF Protection

Implements the double-submit cookie pattern for CSRF protection with timing-safe token comparison.

Quick Start

// src/routes/api/contact/+server.js
import { validateCsrfToken, createCsrfProtectedResponse } from '@goobits/security'
import { json } from '@sveltejs/kit'

export async function POST({ request }) {
  // Verify CSRF token
  if (!validateCsrfToken(request)) {
    return json({ error: 'Invalid CSRF token' }, { status: 403 })
  }

  // Process the request...
  const result = await processContactForm(data)

  // Return response with new CSRF token
  return createCsrfProtectedResponse(
    json({ success: true, data: result })
  )
}

API Reference

generateCsrfToken()

Generate a new random CSRF token.

import { generateCsrfToken } from '@goobits/security'

const token = generateCsrfToken()
// Returns: 64-character hex string

setCsrfCookie(response, token)

Set CSRF token as HTTP-only cookie in response.

import { setCsrfCookie } from '@goobits/security'

export async function GET() {
  const response = json({ message: 'Hello' })
  const token = generateCsrfToken()
  setCsrfCookie(response, token)
  return response
}

getCsrfToken(request)

Extract CSRF token from request cookies.

import { getCsrfToken } from '@goobits/security'

const token = getCsrfToken(request)
// Returns: token string or null

validateCsrfToken(request)

Validate CSRF token using double-submit cookie pattern with timing-safe comparison.

import { validateCsrfToken } from '@goobits/security'

if (!validateCsrfToken(request)) {
  return json({ error: 'Invalid CSRF token' }, { status: 403 })
}

createCsrfProtectedResponse(response)

Convenience function to generate token and set cookie in one call.

import { createCsrfProtectedResponse } from '@goobits/security'

return createCsrfProtectedResponse(
  json({ success: true })
)

generateCsrfTokenWithExpiry(options) ⭐ NEW in v1.1.0

Generate a CSRF token with expiration tracking.

import { generateCsrfTokenWithExpiry } from '@goobits/security'

// Default: 1 hour expiry with tracking
const token = generateCsrfTokenWithExpiry()

// Custom expiry time
const token = generateCsrfTokenWithExpiry({ expiryMs: 30 * 60 * 1000 }) // 30 minutes

// Without expiry tracking
const token = generateCsrfTokenWithExpiry({ trackExpiry: false })

validateCsrfToken(request, options) - Enhanced in v1.1.0

Now supports expiry checking with options parameter:

import { validateCsrfToken } from '@goobits/security'

// Basic validation (backward compatible)
if (!validateCsrfToken(request)) {
  return json({ error: 'Invalid CSRF token' }, { status: 403 })
}

// With expiry checking
if (!validateCsrfToken(request, { checkExpiry: true })) {
  return json({ error: 'Invalid or expired CSRF token' }, { status: 403 })
}

Token Store Management ⭐ NEW in v1.1.0

Utilities for managing the in-memory token store:

import {
  getCsrfTokenStoreSize,
  cleanupCsrfTokens,
  clearCsrfTokenStore
} from '@goobits/security'

// Get number of tracked tokens
console.log(`Active tokens: ${ getCsrfTokenStoreSize() }`)

// Manually cleanup expired tokens
const cleaned = cleanupCsrfTokens()
console.log(`Cleaned up ${ cleaned } expired tokens`)

// Clear all tokens (useful for testing)
clearCsrfTokenStore()

Configuration

// Disable CSRF validation for testing (DO NOT USE IN PRODUCTION)
// Set environment variable: DISABLE_CSRF=true

Cookie Settings:

  • Name: csrf-token
  • HttpOnly: true
  • Secure: true (production), false (development)
  • SameSite: lax
  • Path: /
  • MaxAge: 24 hours

Header: X-CSRF-Token

Client-Side Integration

// Fetch the CSRF token from cookie and send in header
async function submitForm(data) {
  const csrfToken = getCookie('csrf-token')

  const response = await fetch('/api/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
  })

  return response.json()
}

function getCookie(name) {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
  return match ? match[2] : null
}

🤖 reCAPTCHA Verification

Server-side reCAPTCHA token verification supporting both v2 (checkbox) and v3 (score-based).

Setup

# Set environment variable
RECAPTCHA_SECRET_KEY=your_secret_key_here

Basic Usage

// src/routes/api/contact/+server.js
import { verifyRecaptchaToken } from '@goobits/security'
import { json } from '@sveltejs/kit'

export async function POST({ request }) {
  const data = await request.json()
  const { recaptchaToken } = data

  // Verify token (v2 or v3)
  const isValid = await verifyRecaptchaToken(recaptchaToken)

  if (!isValid) {
    return json({ error: 'reCAPTCHA verification failed' }, { status: 400 })
  }

  // Process the request...
  return json({ success: true })
}

reCAPTCHA v3 with Score and Action

import { verifyRecaptchaToken } from '@goobits/security'

// Positional arguments (backward compatible)
const isValid = await verifyRecaptchaToken(
  token,
  'contact_form', // Expected action
  0.7             // Minimum score (0.0 - 1.0)
)

// Options object pattern ⭐ NEW in v1.1.0
const isValid = await verifyRecaptchaToken(token, {
  action: 'contact_form',
  minScore: 0.7,
  allowInDevelopment: false
})

if (!isValid) {
  return json({ error: 'reCAPTCHA failed' }, { status: 400 })
}

Detailed Verification

For debugging and monitoring, use the detailed verification function:

import { verifyRecaptchaTokenWithDetails } from '@goobits/security'

const result = await verifyRecaptchaTokenWithDetails(token, 'submit', 0.5)

if (!result.success) {
  console.error('Verification failed:', result.error, result.details)
  return json({ error: 'Verification failed' }, { status: 400 })
}

// Access detailed information
console.log('Score:', result.score)
console.log('Action:', result.action)
console.log('Timestamp:', result.challenge_ts)

API Reference

verifyRecaptchaToken(token, actionOrOptions, minScore) - Enhanced in v1.1.0

Now supports both positional arguments (backward compatible) and options object pattern.

Positional Arguments (backward compatible):

  • token (string, required): The reCAPTCHA response token
  • action (string, optional): Expected action name (v3 only)
  • minScore (number, optional, default: 0.5): Minimum score threshold (v3 only, 0.0 to 1.0)

Options Object ⭐ NEW:

  • token (string, required): The reCAPTCHA response token
  • options (object, optional):
    • action (string): Expected action name (v3 only)
    • minScore (number, default: 0.5): Minimum score threshold (0.0 to 1.0)
    • secretKey (string): Override RECAPTCHA_SECRET_KEY env var
    • allowInDevelopment (boolean, default: true): Bypass verification in dev when secret key is missing

Returns: Promise<boolean>

Examples:

// Positional args (backward compatible)
await verifyRecaptchaToken(token, 'submit_form', 0.7)

// Options object
await verifyRecaptchaToken(token, {
  action: 'submit_form',
  minScore: 0.7,
  allowInDevelopment: false
})

verifyRecaptchaTokenWithDetails(token, action, minScore)

Same parameters as above, but returns detailed result object:

{
  success: boolean
  score?: number          // v3 only
  action?: string         // v3 only
  challenge_ts?: string   // ISO timestamp
  hostname?: string       // Request hostname
  error?: string          // Error message if failed
  details?: object        // Additional error details
  devBypass?: boolean     // True if bypassed in development
}

Development Mode

In development (NODE_ENV !== 'production'), if RECAPTCHA_SECRET_KEY is not set:

  • verifyRecaptchaToken() returns true (bypasses verification)
  • verifyRecaptchaTokenWithDetails() returns { success: false, devBypass: true }

In production, missing secret key always fails verification.

✅ Request Validation

Zod-based middleware for validating request body, query parameters, and URL parameters.

Basic Usage

// src/routes/api/users/+server.js
import { withValidation } from '@goobits/security'
import { z } from 'zod'
import { json } from '@sveltejs/kit'

// Define schemas
const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional()
})

// Wrap handler with validation
export const POST = withValidation({
  body: createUserSchema
})(async (event) => {
  const { body } = event.locals.validated

  // body is now typed and validated
  const user = await createUser(body)

  return json({ success: true, user })
})

Validating Query Parameters

import { withValidation } from '@goobits/security'
import { z } from 'zod'

const searchSchema = z.object({
  q: z.string().min(1),
  page: z.string().regex(/^\d+$/).transform(Number).optional(),
  limit: z.string().regex(/^\d+$/).transform(Number).optional()
})

export const GET = withValidation({
  query: searchSchema
})(async (event) => {
  const { query } = event.locals.validated

  // query.q is string, query.page and query.limit are numbers
  const results = await search(query.q, query.page, query.limit)

  return json({ results })
})

Validating URL Parameters

// src/routes/api/users/[id]/+server.js
import { withValidation } from '@goobits/security'
import { z } from 'zod'

const paramsSchema = z.object({
  id: z.string().uuid()
})

export const GET = withValidation({
  params: paramsSchema
})(async (event) => {
  const { params } = event.locals.validated

  const user = await getUserById(params.id)

  return json({ user })
})

Combined Validation

const bodySchema = z.object({
  name: z.string().min(2),
  status: z.enum(['active', 'inactive'])
})

const querySchema = z.object({
  notify: z.string().transform(val => val === 'true')
})

const paramsSchema = z.object({
  id: z.string().uuid()
})

export const PUT = withValidation({
  body: bodySchema,
  query: querySchema,
  params: paramsSchema
})(async (event) => {
  const { body, query, params } = event.locals.validated

  const user = await updateUser(params.id, body, query.notify)

  return json({ user })
})

Alternative Pattern: getInputValidator

For more control, use the validator directly:

import { getInputValidator } from '@goobits/security'
import { z } from 'zod'

const schema = z.object({
  email: z.string().email()
})

export async function POST({ request }) {
  const validator = getInputValidator(schema)
  const body = await request.json()

  const result = validator(body)

  if (!result.success) {
    return json({
      error: 'Validation failed',
      details: result.error
    }, { status: 400 })
  }

  // Use result.data (validated and typed)
  return json({ success: true, email: result.data.email })
}

Error Responses

Validation failures return:

{
  "success": false,
  "error": "Invalid request data",
  "details": [
    {
      "code": "too_small",
      "minimum": 2,
      "path": ["name"],
      "message": "String must contain at least 2 character(s)"
    }
  ]
}

⏱️ Rate Limiting

NEW in v1.2.0 - Protect your API endpoints from abuse with in-memory or Redis-based rate limiting.

Quick Start

// src/routes/api/contact/+server.js
import { withRateLimit } from '@goobits/security'
import { json } from '@sveltejs/kit'

export const POST = withRateLimit({
  action: 'contact-form',
  windowMs: 60000,      // 1 minute
  maxRequests: 5,       // 5 requests per minute
  message: 'Too many requests. Please try again later.'
})(async ({ request, locals }) => {
  // Your handler code here
  return json({ success: true })
})

Factory Pattern (In-Memory vs Redis)

The rate limiter automatically selects the appropriate backend:

import { getRateLimiter } from '@goobits/security'

// Get the configured rate limiter (in-memory or Redis)
const rateLimiter = await getRateLimiter()

// Use it directly
const result = await rateLimiter.rateLimitRequest('user-123', 'login')
if (!result.allowed) {
  return json({ error: 'Rate limit exceeded' }, { status: 429 })
}

Configuration:

  • Development: Uses in-memory rate limiting by default
  • Production: Uses Redis if REDIS_HOST or REDIS_URL is set
  • Force Redis in dev: Set FORCE_REDIS_RATELIMIT=true

Middleware Patterns

Form Submission Rate Limiting

import { rateLimitFormSubmission } from '@goobits/security'

export const POST = async ({ request, locals }) => {
  const data = await request.json()

  // Rate limit by IP + email
  const rateLimitResult = await rateLimitFormSubmission(
    locals.clientIP,
    data.email,
    'contact',
    { maxAttempts: 3, windowMinutes: 10 }
  )

  if (!rateLimitResult.allowed) {
    return json({
      error: 'Too many attempts. Please try again later.',
      retryAfter: rateLimitResult.retryAfter
    }, { status: 429 })
  }

  // Process form...
  return json({ success: true })
}

Custom Rate Limit Handler

import { createRateLimitHandler } from '@goobits/security'

const apiRateLimit = createRateLimitHandler({
  action: 'api-call',
  windowMs: 60000,
  maxRequests: 100,
  keyGenerator: (event) => event.locals.userId || event.locals.clientIP
})

export const GET = async (event) => {
  const rateLimitResponse = await apiRateLimit(event)
  if (rateLimitResponse) {
    return rateLimitResponse // 429 response
  }

  // Handle request...
  return json({ data: [] })
}

API Reference

withRateLimit(options)

Middleware wrapper for rate limiting.

Options:

  • action (string): Action identifier
  • windowMs (number): Time window in milliseconds
  • maxRequests (number): Max requests per window
  • message (string): Error message
  • keyGenerator (function): Custom key function (optional)
  • skipSuccessfulRequests (boolean): Don't count successful requests (default: false)

rateLimitRequest(identifier, action)

Check rate limit for an identifier.

Returns: { allowed: boolean, remaining: number, retryAfter?: number }

cleanupRateLimits()

Manually cleanup expired rate limit entries (automatic in background).

🔐 Admin Authentication

NEW in v1.2.0 - JWT-based admin authentication with audit logging integration.

Setup

# Required environment variables
JWT_SECRET=your_jwt_secret_here

# Optional: Admin API key for service-to-service auth
ADMIN_API_KEY=your_api_key_here

Basic Usage

// src/routes/api/admin/users/+server.js
import { requireAdmin } from '@goobits/security'
import { json } from '@sveltejs/kit'

export const GET = requireAdmin(async ({ locals }) => {
  // locals.adminUser contains authenticated admin info
  const { adminUser } = locals

  console.log('Admin user:', adminUser.id, adminUser.role)

  // Admin-only logic here
  const users = await getAllUsers()

  return json({ users })
})

Authentication Methods

1. JWT Bearer Token

// Client request with JWT
fetch('/api/admin/users', {
  headers: {
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  }
})

2. API Key

// Client request with API key (header)
fetch('/api/admin/users', {
  headers: {
    'Authorization': 'ApiKey your_api_key_here'
  }
})

// Or using custom header
fetch('/api/admin/users', {
  headers: {
    'X-Admin-API-Key': 'your_api_key_here'
  }
})

API Reference

requireAdmin(handler)

Middleware that requires admin authentication. Returns 401 if not authenticated.

Features:

  • Verifies JWT tokens with admin role
  • Supports API key authentication
  • Automatic audit logging
  • Sets locals.adminUser for handlers

getAdminUser(request)

Extract and verify admin credentials from request.

Returns: Admin user object or null

{
  id: string,
  email?: string,
  role: 'admin',
  authMethod: 'jwt' | 'apikey'
}

createAdminToken(payload)

Create a JWT token with admin role (for testing/development).

import { createAdminToken } from '@goobits/security'

const token = createAdminToken({
  id: 'user-123',
  email: '[email protected]'
})
// Expires in 24 hours

generateAdminApiKey()

Generate a secure random API key.

import { generateAdminApiKey } from '@goobits/security'

const apiKey = await generateAdminApiKey()
// Returns: 64-character hex string

JWT Token Requirements

Your JWT must include one of:

  • role: 'admin'
  • isAdmin: true
  • admin: true
// Example JWT payload
{
  "id": "user-123",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1234567890,
  "exp": 1234654290
}

📝 Audit Logging

NEW in v1.2.0 - Track security-sensitive operations for compliance and monitoring.

Basic Usage

// src/routes/api/admin/delete-user/+server.js
import { withAuditLogging, auditLog } from '@goobits/security'
import { json } from '@sveltejs/kit'

export const DELETE = withAuditLogging({
  action: 'delete_user',
  includeRequestBody: true
})(async ({ request, locals, params }) => {
  const userId = params.id

  // Delete the user
  await deleteUser(userId)

  // Log specific audit event
  await auditLog({
    eventType: 'user.deleted',
    severity: 'high',
    actor: locals.adminUser?.email || 'system',
    action: 'delete_user',
    outcome: 'success',
    metadata: {
      userId,
      deletedBy: locals.adminUser?.id
    },
    ipAddress: locals.clientIP,
    userAgent: request.headers.get('user-agent')
  })

  return json({ success: true })
})

Middleware Usage

import { withAuditLogging } from '@goobits/security'

// Automatically log request start/end with timing
export const POST = withAuditLogging({
  action: 'payment_processed',
  includeRequestBody: false,  // Don't log sensitive payment data
  includeResponse: false
})(async (event) => {
  // Handler code...
  return json({ success: true })
})

Direct Logging

import { auditLog } from '@goobits/security'

await auditLog({
  eventType: 'login.failed',
  severity: 'medium',
  actor: email,
  action: 'user_login',
  outcome: 'failure',
  metadata: {
    reason: 'invalid_password',
    attempts: 3
  },
  ipAddress: request.headers.get('x-forwarded-for'),
  userAgent: request.headers.get('user-agent')
})

Audit Event Structure

{
  eventType: string       // Event classification (e.g., 'user.deleted')
  severity: string        // 'info' | 'low' | 'medium' | 'high' | 'critical'
  actor: string          // Who performed the action
  action: string         // What action was performed
  outcome: string        // 'success' | 'failure' | 'blocked'
  metadata?: object      // Additional context
  ipAddress?: string     // Client IP
  userAgent?: string     // User agent
  timestamp: string      // ISO 8601 timestamp (auto-added)
}

Integration with Admin Auth

Admin authentication automatically logs:

  • Successful admin logins
  • Failed authentication attempts
  • Admin access to protected endpoints
// Combines admin auth + audit logging
import { requireAdmin } from '@goobits/security'

export const DELETE = requireAdmin(async ({ locals }) => {
  // Admin auth automatically logs:
  // - admin.authentication_success on successful auth
  // - admin.authentication_failed on failed auth

  // Your admin logic here...
  return json({ success: true })
})

GDPR Compliance

Audit logs via @goobits/logger include:

  • Timestamp for all events
  • Actor identification
  • Action performed
  • Outcome tracking
  • IP address logging (for security)

Configure retention policies in your logging infrastructure.

🔐 Security Best Practices

1. CSRF Protection

DO:

  • ✅ Use CSRF tokens for all state-changing operations (POST, PUT, DELETE)
  • ✅ Validate tokens on the server side
  • ✅ Rotate tokens after sensitive operations
  • ✅ Use secure, HTTP-only cookies

DON'T:

  • ❌ Disable CSRF in production
  • ❌ Store CSRF tokens in localStorage (XSS vulnerability)
  • ❌ Skip validation for "internal" APIs
  • ❌ Use predictable token generation

2. reCAPTCHA

DO:

  • ✅ Set appropriate score thresholds for your use case (0.5 is a good default)
  • ✅ Validate action names to prevent token reuse
  • ✅ Keep your secret key secure (environment variables)
  • ✅ Monitor scores and adjust thresholds over time

DON'T:

  • ❌ Trust client-side validation alone
  • ❌ Use the same reCAPTCHA for different actions
  • ❌ Set score threshold too high (may block legitimate users)
  • ❌ Commit secret keys to version control

3. Request Validation

DO:

  • ✅ Validate all user input (body, query, params)
  • ✅ Use strict schemas with appropriate constraints
  • ✅ Sanitize output when rendering user data
  • ✅ Log validation failures for monitoring

DON'T:

  • ❌ Trust any client-side validation
  • ❌ Skip validation for "internal" endpoints
  • ❌ Expose detailed error messages in production
  • ❌ Use overly permissive schemas

4. General

DO:

  • ✅ Use HTTPS in production
  • ✅ Set appropriate CORS policies
  • ✅ Implement rate limiting
  • ✅ Keep dependencies updated
  • ✅ Use environment-based configuration

DON'T:

  • ❌ Disable security features in production
  • ❌ Log sensitive data (passwords, tokens)
  • ❌ Trust X-Forwarded-For without validation
  • ❌ Use default or weak secrets

🏗️ SvelteKit Integration

Complete Example: Contact Form

// src/routes/api/contact/+server.js
import {
  validateCsrfToken,
  verifyRecaptchaToken,
  withValidation,
  createCsrfProtectedResponse
} from '@goobits/security'
import { z } from 'zod'
import { json } from '@sveltejs/kit'

const contactSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  message: z.string().min(10).max(5000),
  recaptchaToken: z.string()
})

export const POST = withValidation({
  body: contactSchema
})(async (event) => {
  const { request, locals } = event
  const { body } = locals.validated

  // 1. Validate CSRF token
  if (!validateCsrfToken(request)) {
    return json({ error: 'Invalid CSRF token' }, { status: 403 })
  }

  // 2. Verify reCAPTCHA
  const isHuman = await verifyRecaptchaToken(
    body.recaptchaToken,
    'contact_form',
    0.5
  )

  if (!isHuman) {
    return json({ error: 'reCAPTCHA verification failed' }, { status: 400 })
  }

  // 3. Process the contact form
  await sendEmail({
    to: '[email protected]',
    from: body.email,
    subject: `Contact from ${body.name}`,
    text: body.message
  })

  // 4. Return success with new CSRF token
  return createCsrfProtectedResponse(
    json({ success: true, message: 'Message sent successfully' })
  )
})

Hooks Integration

// src/hooks.server.js
import { generateCsrfToken, setCsrfCookie } from '@goobits/security'

export async function handle({ event, resolve }) {
  // Generate CSRF token for GET requests if not present
  if (event.request.method === 'GET') {
    const response = await resolve(event)

    // Add CSRF token to response if it's an HTML page
    if (response.headers.get('content-type')?.includes('text/html')) {
      const token = generateCsrfToken()
      setCsrfCookie(response, token)
    }

    return response
  }

  return resolve(event)
}

📊 Logging

All security operations are logged via @goobits/logger:

import { LoggerConfig, LogLevel } from '@goobits/logger'

// Configure log level for security operations
LoggerConfig.setLogLevel(LogLevel.INFO)

// Security logs will show with [CSRF], [reCAPTCHA], or [Validation] prefixes

Logged Events:

  • CSRF token generation and validation
  • reCAPTCHA verification attempts and failures
  • Request validation errors
  • Security configuration warnings

🧪 Testing

Disable CSRF for Tests

# In test environment
DISABLE_CSRF=true

Or programmatically:

import { DISABLE_CSRF } from '@goobits/security'

if (DISABLE_CSRF) {
  console.log('CSRF validation is disabled')
}

Mock reCAPTCHA

// In development, set invalid secret to bypass
// (verifyRecaptchaToken returns true when secret is missing in dev mode)
delete process.env.RECAPTCHA_SECRET_KEY

📄 License

MIT

🤝 Contributing

Part of the Goobits monorepo. See main repository for contribution guidelines.

🔗 Related Packages

  • @goobits/logger - Unified logging (used internally)
  • @goobits/store - E-commerce components using security utilities
  • @goobits/ui - UI components

📚 Further Reading