@od-oneapp/security 2026.1.1301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit-DStYbhoa.mjs","names":[],"sources":["../env.ts","../rate-limit.ts"],"sourcesContent":["/**\n * @fileoverview env.ts\n */\n\nimport { logError, logWarn } from '@repo/shared/logger';\nimport { createEnv } from '@t3-oss/env-core';\nimport { z } from 'zod/v4';\n\n/**\n * Logger interface for structured logging\n *\n * Defines the contract for custom logger implementations used throughout\n * the security package. All internal logging uses this interface.\n *\n * @example\n * ```typescript\n * import type { Logger } from '@repo/security/server';\n *\n * const customLogger: Logger = {\n * warn: (msg, ctx) => myLogger.warn(msg, ctx),\n * error: (msg, ctx) => myLogger.error(msg, ctx),\n * };\n * ```\n */\nexport interface Logger {\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n}\n\n/**\n * Default logger implementation using structured JSON logging\n */\nlet loggerInstance: Logger = {\n warn: (message, context) => {\n if (process.env.NODE_ENV !== 'test') {\n logWarn(message, context);\n }\n },\n error: (message, context) => {\n if (process.env.NODE_ENV !== 'test') {\n logError(message, {\n ...context,\n timestamp: new Date().toISOString(),\n });\n }\n },\n};\n\n/**\n * Set a custom logger implementation for the security package\n *\n * This allows you to integrate your preferred logging solution (pino, winston, etc.)\n * with the security package's internal logging.\n *\n * @param customLogger - Custom logger implementation conforming to the Logger interface\n *\n * @example\n * ```typescript\n * import { setLogger } from '@repo/security/server';\n * import pino from 'pino';\n *\n * const logger = pino({ level: 'info' });\n * setLogger({\n * warn: (msg, ctx) => logger.warn(ctx, msg),\n * error: (msg, ctx) => logger.error(ctx, msg),\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Winston integration\n * import winston from 'winston';\n * import { setLogger } from '@repo/security/server';\n *\n * const logger = winston.createLogger({\n * level: 'info',\n * format: winston.format.json(),\n * transports: [new winston.transports.Console()],\n * });\n *\n * setLogger({\n * warn: (msg, ctx) => logger.warn(msg, ctx),\n * error: (msg, ctx) => logger.error(msg, ctx),\n * });\n * ```\n */\nexport function setLogger(customLogger: Logger): void {\n loggerInstance = customLogger;\n}\n\n/**\n * Get the current logger instance\n *\n * Returns the active logger (default JSON logger or custom logger set via setLogger).\n * The default logger outputs structured JSON logs to console and is silent in test environments.\n *\n * @returns The current logger instance\n *\n * @example\n * ```typescript\n * import { getLogger } from '@repo/security/server';\n *\n * const logger = getLogger();\n * logger.warn('Rate limiting disabled', { reason: 'no Redis config' });\n * logger.error('Security check failed', { error: 'timeout' });\n * ```\n */\nexport function getLogger(): Logger {\n return loggerInstance;\n}\n\n/**\n * Validated environment variables for the security package\n *\n * Type-safe environment configuration with automatic validation using @t3-oss/env-core.\n * In production, validation errors throw. In development, they log warnings and use fallback values.\n *\n * @example\n * ```typescript\n * import { env } from '@repo/security/server';\n *\n * // Access validated environment variables\n * const arcjetKey = env.ARCJET_KEY;\n * const redisToken = env.UPSTASH_REDIS_REST_TOKEN;\n * const isProduction = env.NODE_ENV === 'production';\n * ```\n *\n * @see {@link https://env.t3.gg/docs/core | @t3-oss/env-core Documentation}\n */\nexport const env: {\n ARCJET_KEY?: string;\n UPSTASH_REDIS_REST_TOKEN?: string;\n UPSTASH_REDIS_REST_URL?: string;\n NODE_ENV: 'development' | 'test' | 'production';\n} = createEnv({\n server: {\n // Use proper optional validation - empty strings become undefined due to emptyStringAsUndefined: true\n ARCJET_KEY: z.string().startsWith('ajkey_').optional(),\n UPSTASH_REDIS_REST_TOKEN: z.string().min(1).optional(),\n UPSTASH_REDIS_REST_URL: z.string().url().optional(),\n\n // Environment detection\n NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),\n },\n runtimeEnv: process.env,\n emptyStringAsUndefined: true,\n onValidationError: error => {\n const message = Array.isArray(error) ? error.map(e => e.message).join(', ') : String(error);\n const isProduction = process.env.NODE_ENV === 'production';\n\n if (isProduction) {\n // Fail fast in production for security misconfigurations\n loggerInstance.error('Security environment validation failed in production', {\n error: message,\n env: process.env.NODE_ENV,\n module: '@repo/security',\n });\n throw new Error(`Security configuration error: ${message}`);\n }\n\n // Development/test: log warning and continue with fallbacks\n loggerInstance.warn('Security environment validation failed', {\n error: message,\n env: process.env.NODE_ENV,\n module: '@repo/security',\n });\n\n // Throw to satisfy type system, but safeEnv() will handle fallback\n throw new Error(`Security environment validation failed: ${message}`);\n },\n});\n\n/**\n * Safe access to environment variables with fallback handling\n *\n * Provides resilient access to environment variables in non-Next.js contexts\n * (Node.js, workers, tests). Returns validated env or fallback values if validation fails.\n *\n * **Semantics:**\n * - If `env` is successfully validated by `@t3-oss/env-core`, returns the validated env object\n * - If validation fails (e.g., invalid format), returns fallback values matching the same structure\n * - The return type is always compatible with `SecurityEnv` (typeof env)\n * - Helper functions (`isProduction`, `hasArcjetConfig`, `hasUpstashConfig`) rely on this\n * consistent return structure for type safety\n *\n * @returns Environment variables object (validated or fallback values)\n * Always matches the structure of `SecurityEnv` for type safety\n *\n * @example\n * ```typescript\n * import { safeEnv } from '@repo/security/server';\n *\n * const env = safeEnv();\n * if (env.ARCJET_KEY) {\n * // Bot protection is configured\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use in rate limiter initialization\n * const env = safeEnv();\n * const isProduction = env.NODE_ENV === 'production';\n * if (!env.UPSTASH_REDIS_REST_TOKEN && isProduction) {\n * throw new Error('Redis required in production');\n * }\n * ```\n */\nexport function safeEnv(): SecurityEnv {\n return env;\n}\n\n/**\n * Check if the current environment is production\n *\n * Useful for conditional logic that should only run in production\n * (e.g., strict rate limiting, fail-closed security checks).\n *\n * @returns True if NODE_ENV is 'production'\n *\n * @example\n * ```typescript\n * import { isProduction } from '@repo/security/server';\n *\n * if (isProduction()) {\n * // Enforce strict security policies\n * await secure([], request); // Block all bots\n * } else {\n * // Development: allow some bots for testing\n * await secure(['GOOGLEBOT'], request);\n * }\n * ```\n */\nexport function isProduction(): boolean {\n const envVars = safeEnv();\n return envVars.NODE_ENV === 'production';\n}\n\n/**\n * Check if Arcjet is configured\n *\n * Returns true if ARCJET_KEY environment variable is set.\n * Use this to conditionally enable bot detection when Arcjet is available.\n *\n * @returns True if ARCJET_KEY environment variable is set\n *\n * @example\n * ```typescript\n * import { hasArcjetConfig, secure } from '@repo/security/server/next';\n *\n * export async function POST(request: Request) {\n * if (hasArcjetConfig()) {\n * await secure(['GOOGLEBOT', 'BINGBOT'], request);\n * }\n * // Continue with request processing\n * }\n * ```\n *\n * @see {@link https://docs.arcjet.com | Arcjet Documentation}\n */\nexport function hasArcjetConfig(): boolean {\n const envVars = safeEnv();\n return Boolean(envVars.ARCJET_KEY);\n}\n\n/**\n * Check if Upstash Redis is configured\n *\n * Returns true if both UPSTASH_REDIS_REST_TOKEN and UPSTASH_REDIS_REST_URL\n * environment variables are set. Use this to conditionally enable rate limiting.\n *\n * @returns True if both UPSTASH_REDIS_REST_TOKEN and UPSTASH_REDIS_REST_URL are set\n *\n * @example\n * ```typescript\n * import { hasUpstashConfig, createRateLimiter } from '@repo/security/server';\n * import { Ratelimit } from '@upstash/ratelimit';\n *\n * export async function POST(request: Request) {\n * if (hasUpstashConfig()) {\n * const limiter = createRateLimiter({\n * limiter: Ratelimit.slidingWindow(10, '1 m'),\n * });\n * const result = await limiter.limit('user-123');\n * if (!result.success) {\n * return new Response('Rate limited', { status: 429 });\n * }\n * }\n * // Continue with request processing\n * }\n * ```\n *\n * @see {@link https://upstash.com/docs/oss/sdks/ts/ratelimit/overview | Upstash Rate Limiting}\n */\nexport function hasUpstashConfig(): boolean {\n const envVars = safeEnv();\n return Boolean(envVars.UPSTASH_REDIS_REST_TOKEN && envVars.UPSTASH_REDIS_REST_URL);\n}\n\n/**\n * Type representing validated security environment variables\n *\n * Use this type for type-safe environment variable access throughout your application.\n * Inferred from the env object to ensure consistency.\n *\n * @example\n * ```typescript\n * import type { SecurityEnv } from '@repo/security/server';\n *\n * function checkSecurity(env: SecurityEnv) {\n * if (env.ARCJET_KEY) {\n * // Type-safe access to environment variables\n * }\n * }\n * ```\n */\nexport type SecurityEnv = typeof env;\n","/**\n * @fileoverview Rate limiting utilities using Upstash Redis\n * Rate limiting utilities using Upstash Redis\n * Uses shared Redis instance from @repo/db-upstash-redis\n *\n * @module @od-oneapp/security/rate-limit\n */\n\nimport 'server-only';\n\nimport { createHash } from 'node:crypto';\n\nimport { Ratelimit, type RatelimitConfig } from '@integrations/upstash/redis-client';\nimport { getServerClient } from '@repo/db-upstash-redis/server';\n\nimport { getLogger, safeEnv } from './env';\n\n/**\n * Sliding window rate limiting algorithm\n *\n * Provides smooth rate limiting by distributing requests across a rolling time window.\n * More accurate than fixed windows as it prevents burst traffic at window boundaries.\n *\n * @param tokens - Maximum number of requests allowed in the time window\n * @param window - Time window duration (e.g., '10 s', '1 m', '1 h')\n * @returns Rate limiter configuration\n *\n * @example\n * ```typescript\n * const limiter = createRateLimiter({\n * limiter: slidingWindow(100, '1 m'), // 100 requests per minute\n * });\n * ```\n */\nexport const { slidingWindow } = Ratelimit;\n\n/**\n * Fixed window rate limiting algorithm\n *\n * Resets the counter at fixed time intervals. Simpler but can allow bursts\n * at window boundaries (e.g., 100 requests at 00:59, 100 more at 01:00).\n *\n * @param tokens - Maximum number of requests allowed in the time window\n * @param window - Time window duration (e.g., '10 s', '1 m', '1 h')\n * @returns Rate limiter configuration\n *\n * @example\n * ```typescript\n * const limiter = createRateLimiter({\n * limiter: fixedWindow(1000, '1 h'), // 1000 requests per hour\n * });\n * ```\n */\nexport const { fixedWindow } = Ratelimit;\n\n/**\n * Token bucket rate limiting algorithm\n *\n * Allows burst traffic while maintaining average rate. Tokens refill at a constant rate,\n * and requests consume tokens. Good for APIs that need to handle occasional spikes.\n *\n * @param refillRate - Rate at which tokens are refilled\n * @param interval - Refill interval (e.g., '10 s', '1 m')\n * @returns Rate limiter configuration\n *\n * @example\n * ```typescript\n * const limiter = createRateLimiter({\n * limiter: tokenBucket(50, '10 s'), // 50 tokens, refills every 10s\n * });\n * ```\n */\nexport const { tokenBucket } = Ratelimit;\n\n/**\n * Type-safe rate limiter instance interface\n * Captures only the methods we actually use and want to support\n * Uses ReturnType to ensure compatibility with Ratelimit interface\n */\ntype RateLimiterInstance = {\n limit: Ratelimit['limit'];\n getRemaining: Ratelimit['getRemaining'];\n resetUsedTokens: Ratelimit['resetUsedTokens'];\n};\n\n/**\n * No-op rate limiter implementation for development when Redis is not configured\n * This is type-safe and doesn't bypass TypeScript's type checking\n */\nclass NoOpRateLimiter implements RateLimiterInstance {\n private readonly logger = getLogger();\n\n async getRemaining(\n _identifier: string,\n ): Promise<{ remaining: number; reset: number; limit: number }> {\n this.logger.warn('Rate limiting is disabled - no Redis configuration', {\n nodeEnv: safeEnv().NODE_ENV,\n });\n return {\n remaining: 999999,\n reset: Date.now() + 60000,\n limit: 999999,\n };\n }\n\n async limit(_identifier: string): Promise<Awaited<ReturnType<Ratelimit['limit']>>> {\n this.logger.warn('Rate limiting is disabled - no Redis configuration', {\n message: 'Set UPSTASH_REDIS_REST_TOKEN and UPSTASH_REDIS_REST_URL to enable rate limiting',\n nodeEnv: safeEnv().NODE_ENV,\n });\n\n // Return a value compatible with RatelimitResponse\n // Note: pending is a Promise<unknown> in RatelimitResponse\n return {\n success: true,\n limit: 999999,\n remaining: 999999,\n reset: Date.now() + 60000,\n pending: Promise.resolve(undefined),\n } as Awaited<ReturnType<Ratelimit['limit']>>;\n }\n\n async resetUsedTokens(_identifier: string): Promise<void> {\n // No-op in development\n }\n}\n\n/**\n * Hash identifiers containing unsafe characters (emails, URLs, etc.)\n *\n * Creates a stable SHA-256 hash for identifiers with special characters that\n * aren't allowed in rate limit keys. Use this for emails, user IDs with slashes,\n * or any identifier that fails `validateIdentifier()`.\n *\n * **Note:** This function truncates the SHA-256 hash from 64 to 32 hex characters,\n * reducing entropy from 256 bits to 128 bits. This is intentional and provides\n * sufficient collision resistance for rate limiting use cases while keeping\n * identifier lengths manageable. 128 bits of entropy is cryptographically secure\n * and exceeds the collision resistance needed for rate limit identifiers.\n *\n * @param identifier - The identifier to hash (can contain any characters)\n * @returns Hexadecimal hash (32 characters, safe for rate limiting)\n *\n * @example\n * ```typescript\n * // For email addresses\n * const userId = hashIdentifier('user@example.com');\n * await applyRateLimit(userId, 'api');\n * ```\n *\n * @example\n * ```typescript\n * // For URLs\n * const pathHash = hashIdentifier('/api/users/123/profile');\n * await applyRateLimit(pathHash, 'api');\n * ```\n */\nexport function hashIdentifier(identifier: string): string {\n return createHash('sha256').update(identifier).digest('hex').slice(0, 32);\n}\n\n/**\n * Wraps a promise with a timeout\n * @param promise - The promise to wrap\n * @param timeoutMs - Timeout in milliseconds\n * @param errorMessage - Error message to throw on timeout\n * @returns The promise result or throws on timeout\n */\nasync function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage: string,\n): Promise<T> {\n const timeout = new Promise<never>((_resolve, reject) => {\n setTimeout(() => reject(new Error(errorMessage)), timeoutMs);\n });\n\n return Promise.race([promise, timeout]);\n}\n\n// Default timeout for rate limit operations (5 seconds)\nconst DEFAULT_RATE_LIMIT_TIMEOUT_MS = 5000;\n\n// In-memory cache for rate limit info queries\nconst rateLimitInfoCache = new Map<\n string,\n { data: Omit<RateLimitResult, 'success'>; expires: number }\n>();\nconst CACHE_TTL_MS = 1000; // 1 second cache TTL\nconst MAX_CACHE_SIZE = 1000; // Maximum cache entries to prevent memory leaks\n\n// Pending requests for request coalescing (prevents cache stampede)\nconst pendingRequests = new Map<string, Promise<Omit<RateLimitResult, 'success'>>>();\n\n/**\n * Cleanup expired cache entries periodically\n * Runs every 5 seconds to prevent memory leaks\n */\nfunction cleanupCache(): void {\n const now = Date.now();\n const entries = Array.from(rateLimitInfoCache.entries());\n\n // Remove expired entries\n for (const [key, value] of entries) {\n if (value.expires <= now) {\n rateLimitInfoCache.delete(key);\n }\n }\n\n // Enforce size limit using LRU eviction\n // Note: Sorting is O(n log n), but with MAX_CACHE_SIZE = 1000, this is acceptable\n // for periodic cleanup (runs every 5 seconds). For higher-performance scenarios,\n // consider using a dedicated LRU cache library like 'lru-cache' that maintains\n // access order with O(1) operations.\n if (rateLimitInfoCache.size > MAX_CACHE_SIZE) {\n const sortedEntries = entries.sort((a, b) => a[1].expires - b[1].expires);\n const toDelete = sortedEntries.slice(0, rateLimitInfoCache.size - MAX_CACHE_SIZE);\n toDelete.forEach(([key]) => rateLimitInfoCache.delete(key));\n\n getLogger().warn('Rate limit cache size limit exceeded, evicted old entries', {\n evicted: toDelete.length,\n remaining: rateLimitInfoCache.size,\n });\n }\n}\n\n// Run cache cleanup every 5 seconds (Node 22+ optimized timer)\nconst cleanupInterval = setInterval(() => {\n try {\n cleanupCache();\n } catch (error) {\n getLogger().error('Cache cleanup failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}, 5000);\n\n// Ensure cleanup interval doesn't prevent process exit\ncleanupInterval.unref();\n\n/**\n * Create a rate limiter with shared Redis backend\n *\n * Returns a configured rate limiter using the shared Upstash Redis instance.\n * In development without Redis, returns a no-op limiter with `disabled: true`.\n * In production without Redis, throws an error.\n *\n * @param props - Rate limiter configuration (limiter algorithm, prefix, analytics)\n * @returns Configured Ratelimit instance\n * @throws {Error} In production if Redis is not configured\n *\n * @example\n * ```typescript\n * const limiter = createRateLimiter({\n * limiter: slidingWindow(100, '1 m'),\n * prefix: 'api:v1',\n * });\n * const result = await limiter.limit('user-123');\n * ```\n *\n * @example\n * ```typescript\n * // Custom burst handling\n * const burstLimiter = createRateLimiter({\n * limiter: tokenBucket(50, '10 s'),\n * prefix: 'uploads',\n * analytics: true,\n * });\n * ```\n */\nexport const createRateLimiter = (props: Omit<RatelimitConfig, 'redis'>): RateLimiterInstance => {\n const env = safeEnv();\n\n // Check if Redis is properly configured\n if (!env.UPSTASH_REDIS_REST_TOKEN || !env.UPSTASH_REDIS_REST_URL) {\n const isProduction = env.NODE_ENV === 'production';\n\n // In production, fail fast if Redis is not configured\n if (isProduction) {\n throw new Error(\n 'Rate limiting requires Redis configuration. ' +\n 'Set UPSTASH_REDIS_REST_TOKEN and UPSTASH_REDIS_REST_URL environment variables.',\n );\n }\n\n // In development/test, return type-safe no-op limiter\n return new NoOpRateLimiter();\n }\n\n try {\n const redisClient = getServerClient();\n return new Ratelimit({\n limiter: props.limiter,\n prefix: props.prefix ?? 'forge',\n redis: redisClient.redis, // Access the raw Redis client\n analytics: props.analytics ?? false,\n ephemeralCache: props.ephemeralCache ?? undefined,\n });\n } catch (error) {\n getLogger().error('Failed to create rate limiter', {\n error: error instanceof Error ? error.message : String(error),\n nodeEnv: env.NODE_ENV,\n });\n if (env.NODE_ENV === 'production') {\n throw new Error('Rate limiter initialization failed. Check Redis configuration.');\n }\n // In development, return type-safe no-op limiter\n return new NoOpRateLimiter() as unknown as Ratelimit;\n }\n};\n\n/**\n * Default rate limiter configurations for common use cases\n *\n * Readonly configurations optimized for different endpoint types.\n * All use sliding window algorithm for smooth rate limiting.\n *\n * @example\n * ```typescript\n * // Use pre-configured limiters\n * const result = await rateLimiters.api.limit('user-123');\n *\n * // Or create custom limiter from config\n * const customLimiter = createRateLimiter(rateLimitConfigs.api);\n * ```\n */\nexport const rateLimitConfigs = {\n /** API endpoints - 100 requests per minute */\n api: {\n limiter: Ratelimit.slidingWindow(100, '1 m'),\n prefix: 'ratelimit:api',\n },\n\n /** Authentication - 5 attempts per 15 minutes */\n auth: {\n limiter: Ratelimit.slidingWindow(5, '15 m'),\n prefix: 'ratelimit:auth',\n },\n\n /** File uploads - 10 uploads per hour */\n upload: {\n limiter: Ratelimit.slidingWindow(10, '1 h'),\n prefix: 'ratelimit:upload',\n },\n\n /** Webhook endpoints - 1000 requests per hour */\n webhook: {\n limiter: Ratelimit.slidingWindow(1000, '1 h'),\n prefix: 'ratelimit:webhook',\n },\n\n /** Search endpoints - 500 requests per minute */\n search: {\n limiter: Ratelimit.slidingWindow(500, '1 m'),\n prefix: 'ratelimit:search',\n },\n} as const satisfies Record<string, { limiter: unknown; prefix: string }>;\n\n/**\n * Pre-configured, ready-to-use rate limiters\n *\n * Instantiated limiters for common use cases. Use these directly\n * or create custom limiters with `createRateLimiter()`.\n *\n * @example\n * ```typescript\n * // Direct usage\n * const result = await rateLimiters.api.limit('192.168.1.1');\n * if (!result.success) {\n * return new Response('Too many requests', { status: 429 });\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Different limiters for different endpoints\n * await rateLimiters.auth.limit(userId); // Strict limits\n * await rateLimiters.search.limit(ip); // Higher limits\n * ```\n */\nexport const rateLimiters = {\n /*\n * Lazily instantiated rate limiters.\n *\n * Why lazy:\n * - `next build` runs with `NODE_ENV=production` and evaluates route modules to\n * collect page data. Eager instantiation would throw at build time when\n * Upstash env vars are intentionally unset in CI/local builds.\n * - At runtime, consumers still get fail-fast behavior when they actually\n * attempt to use a limiter in production without Redis configured.\n */\n get api() {\n return getCachedRateLimiter('api');\n },\n set api(value: RateLimiterInstance) {\n cachedRateLimiters.api = value;\n },\n get auth() {\n return getCachedRateLimiter('auth');\n },\n set auth(value: RateLimiterInstance) {\n cachedRateLimiters.auth = value;\n },\n get upload() {\n return getCachedRateLimiter('upload');\n },\n set upload(value: RateLimiterInstance) {\n cachedRateLimiters.upload = value;\n },\n get webhook() {\n return getCachedRateLimiter('webhook');\n },\n set webhook(value: RateLimiterInstance) {\n cachedRateLimiters.webhook = value;\n },\n get search() {\n return getCachedRateLimiter('search');\n },\n set search(value: RateLimiterInstance) {\n cachedRateLimiters.search = value;\n },\n} as const;\n\ntype RateLimiterKey = keyof typeof rateLimitConfigs;\n\nconst cachedRateLimiters: Partial<Record<RateLimiterKey, RateLimiterInstance>> = {};\n\nfunction getCachedRateLimiter(key: RateLimiterKey): RateLimiterInstance {\n const existing = cachedRateLimiters[key];\n if (existing) return existing;\n\n const created = createRateLimiter(rateLimitConfigs[key]);\n cachedRateLimiters[key] = created;\n return created;\n}\n\n/**\n * Rate limit result type\n */\nexport type RateLimitResult = {\n success: boolean;\n limit: number;\n remaining: number;\n reset: number;\n retryAfter?: number;\n /** Indicates that rate limiting is disabled (dev mode without Redis) */\n disabled?: boolean;\n};\n\n/**\n * Validates a rate limit identifier\n *\n * @param identifier - The identifier to validate\n * @returns Validated identifier (unchanged if valid)\n * @throws {Error} If identifier is invalid\n *\n * @remarks\n * Valid characters: alphanumeric, hyphens, underscores, colons, dots\n * For identifiers with special characters (emails, etc.), use hashIdentifier()\n *\n * @example\n * ```typescript\n * // Valid\n * validateIdentifier('user-123');\n * validateIdentifier('192.168.1.1');\n * validateIdentifier('api:v1:users');\n *\n * // Invalid - use hashIdentifier instead\n * hashIdentifier('user@example.com');\n * ```\n */\nfunction validateIdentifier(identifier: string): string {\n if (!identifier || typeof identifier !== 'string') {\n throw new Error('Rate limit identifier must be a non-empty string');\n }\n\n if (identifier.length > 255) {\n throw new Error('Rate limit identifier must be 255 characters or less');\n }\n\n // Check for invalid characters\n // Note: Dots (.) are allowed for compatibility with common identifier patterns,\n // but be aware that Redis Cluster uses dots for key namespace patterns.\n // Modern Redis clients (including Upstash) properly escape keys, so this is safe.\n // For Redis Cluster deployments, consider using hashIdentifier() for identifiers\n // that might conflict with namespace patterns.\n const hasInvalidChars = /[^a-zA-Z0-9\\-_:.]/.test(identifier);\n\n if (hasInvalidChars) {\n throw new Error(\n `Rate limit identifier contains invalid characters. ` +\n `Only alphanumeric, hyphens, underscores, colons, and dots are allowed. ` +\n `Got: \"${identifier}\". Consider using hashIdentifier() for email addresses or complex identifiers.`,\n );\n }\n\n return identifier;\n}\n\n/**\n * Apply rate limiting to a request\n * @param identifier - Unique identifier for the request (e.g., IP address, user ID).\n * Must be a non-empty string, max 255 characters.\n * Invalid characters will be sanitized.\n * @param type - Type of rate limiter to use. Defaults to 'api'.\n * Options: 'api', 'auth', 'upload', 'webhook', 'search'\n * @returns Rate limit result with success status and limit information\n * @throws {Error} If identifier is invalid or type doesn't exist\n *\n * @example\n * ```typescript\n * const result = await applyRateLimit('192.168.1.1', 'api');\n * if (!result.success) {\n * return new Response('Too many requests', { status: 429 });\n * }\n * ```\n *\n * @example\n * ```typescript\n * // For authenticated users\n * const result = await applyRateLimit(userId, 'auth');\n * ```\n */\nexport const applyRateLimit = async (\n identifier: string,\n type: keyof typeof rateLimiters = 'api',\n): Promise<RateLimitResult> => {\n // Validate identifier\n const validatedIdentifier = validateIdentifier(identifier);\n\n // Validate type\n if (!(type in rateLimiters)) {\n throw new Error(\n `Invalid rate limiter type: ${type}. Must be one of: ${Object.keys(rateLimiters).join(', ')}`,\n );\n }\n\n const limiter = rateLimiters[type];\n\n try {\n const result = await withTimeout(\n limiter.limit(validatedIdentifier),\n DEFAULT_RATE_LIMIT_TIMEOUT_MS,\n 'Rate limit check timed out',\n );\n\n return {\n success: result.success,\n limit: result.limit,\n remaining: result.remaining,\n reset: result.reset,\n retryAfter: result.success ? undefined : result.reset - Date.now(),\n };\n } catch (error) {\n // On timeout or error, fail closed in production, fail open in development\n const env = safeEnv();\n if (env.NODE_ENV === 'production') {\n throw error;\n }\n // Development: allow request on timeout (rate limiting effectively disabled)\n return {\n success: true,\n limit: 999999,\n remaining: 999999,\n reset: Date.now() + 60000,\n disabled: true,\n };\n }\n};\n\n/**\n * Check if a request is rate limited\n *\n * @param identifier - Unique identifier for the request (e.g., IP address, user ID).\n * Must be a non-empty string, max 255 characters.\n * Invalid characters will be sanitized.\n * @param type - Type of rate limiter to check. Defaults to 'api'.\n * Options: 'api', 'auth', 'upload', 'webhook', 'search'\n * @returns True if request should be blocked due to rate limiting\n * @throws {Error} If identifier is invalid or type doesn't exist\n *\n * @example\n * ```typescript\n * const isLimited = await isRateLimited('192.168.1.1', 'api');\n * if (isLimited) {\n * return new Response('Too many requests', { status: 429 });\n * }\n * ```\n */\nexport const isRateLimited = async (\n identifier: string,\n type: keyof typeof rateLimiters = 'api',\n): Promise<boolean> => {\n const result = await applyRateLimit(identifier, type);\n return !result.success;\n};\n\n/**\n * Get rate limit info without applying limits\n * Uses request coalescing to prevent cache stampede\n *\n * @param identifier - Unique identifier for the request (e.g., IP address, user ID).\n * Must be a non-empty string, max 255 characters.\n * Only alphanumeric, hyphens, underscores, colons, dots allowed.\n * @param type - Type of rate limiter to query. Defaults to 'api'.\n * Options: 'api', 'auth', 'upload', 'webhook', 'search'\n * @returns Rate limit information excluding success status\n * @throws {Error} If identifier is invalid or type doesn't exist\n *\n * @example\n * ```typescript\n * const info = await getRateLimitInfo('192.168.1.1', 'api');\n * console.log(`Limit: ${info.limit}, Remaining: ${info.remaining}`);\n * ```\n *\n * @example\n * ```typescript\n * // For identifiers with special characters, use hashIdentifier\n * const safeId = hashIdentifier('user@example.com');\n * const info = await getRateLimitInfo(safeId, 'api');\n * ```\n */\nexport const getRateLimitInfo = async (\n identifier: string,\n type: keyof typeof rateLimiters = 'api',\n): Promise<Omit<RateLimitResult, 'success'>> => {\n const validatedIdentifier = validateIdentifier(identifier);\n\n // Validate type\n if (!(type in rateLimiters)) {\n throw new Error(\n `Invalid rate limiter type: ${type}. Must be one of: ${Object.keys(rateLimiters).join(', ')}`,\n );\n }\n\n const limiter = rateLimiters[type];\n const cacheKey = `${type}:${validatedIdentifier}`;\n\n // Check cache first\n const cached = rateLimitInfoCache.get(cacheKey);\n if (cached && cached.expires > Date.now()) {\n return cached.data;\n }\n\n // Check for pending request (request coalescing)\n const pending = pendingRequests.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n // Create new request\n const requestPromise = (async () => {\n try {\n const result = await withTimeout(\n limiter.limit(validatedIdentifier),\n DEFAULT_RATE_LIMIT_TIMEOUT_MS,\n 'Rate limit info check timed out',\n );\n\n const data = {\n limit: result.limit,\n remaining: result.remaining,\n reset: result.reset,\n };\n\n // Cache the result\n rateLimitInfoCache.set(cacheKey, {\n data,\n expires: Date.now() + CACHE_TTL_MS,\n });\n\n return data;\n } catch (error) {\n // On timeout or error, fail closed in production, fail open in development\n const env = safeEnv();\n if (env.NODE_ENV === 'production') {\n throw error;\n }\n // Development: return default values on timeout (rate limiting effectively disabled)\n return {\n limit: 999999,\n remaining: 999999,\n reset: Date.now() + 60000,\n disabled: true,\n };\n } finally {\n // Clean up pending request only if it is still this promise (avoid race condition)\n pendingRequests.delete(cacheKey);\n }\n })();\n\n // Store pending request for coalescing (before async execution starts)\n pendingRequests.set(cacheKey, requestPromise);\n\n return requestPromise;\n};\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { _ as slidingWindow, a as isProduction, c as RateLimitResult, d as fixedWindow, f as getRateLimitInfo, g as rateLimiters, h as rateLimitConfigs, i as hasUpstashConfig, l as applyRateLimit, m as isRateLimited, n as getLogger, o as safeEnv, p as hashIdentifier, r as hasArcjetConfig, s as setLogger, t as env, u as createRateLimiter, v as tokenBucket } from "./env-DvTVXAjh.mjs";
2
+ import "./server.mjs";
3
+ import "server-only";
4
+ import { ArcjetBotCategory, ArcjetWellKnownBot } from "@integrations/arcjet/security-client";
5
+ import { NoseconeOptions, createMiddleware as noseconeMiddleware } from "@nosecone/next";
6
+ import { NextRequest } from "next/server";
7
+
8
+ //#region middleware.d.ts
9
+ declare const noseconeOptions: NoseconeOptions;
10
+ //#endregion
11
+ //#region src/server-next.d.ts
12
+ declare class SecurityDenialError extends Error {
13
+ readonly reason: 'bot' | 'rate_limit' | 'shield' | 'unknown';
14
+ readonly metadata?: {
15
+ ip?: string;
16
+ path?: string;
17
+ userAgent?: string;
18
+ decisionId?: string;
19
+ } | undefined;
20
+ constructor(message: string, reason: 'bot' | 'rate_limit' | 'shield' | 'unknown', metadata?: {
21
+ ip?: string;
22
+ path?: string;
23
+ userAgent?: string;
24
+ decisionId?: string;
25
+ } | undefined);
26
+ }
27
+ declare const secure: (allow: (ArcjetBotCategory | ArcjetWellKnownBot)[], sourceRequest?: NextRequest | Request) => Promise<void>;
28
+ //#endregion
29
+ export { type ArcjetBotCategory, type ArcjetWellKnownBot, type RateLimitResult, SecurityDenialError, applyRateLimit, createRateLimiter, env, fixedWindow, getLogger, getRateLimitInfo, hasArcjetConfig, hasUpstashConfig, hashIdentifier, isProduction, isRateLimited, noseconeMiddleware, noseconeOptions, rateLimitConfigs, rateLimiters, safeEnv, secure, setLogger, slidingWindow, tokenBucket };
30
+ //# sourceMappingURL=server-next.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-next.d.mts","names":[],"sources":["../middleware.ts","../src/server-next.ts"],"mappings":";;;;;;;;cAmEa,eAAA,EAAiB,eAAA;;;cCiCjB,mBAAA,SAA4B,KAAA;EAAA,SAUrB,MAAA;EAAA,SACA,QAAA;IAEd,EAAA;IAEA,IAAA;IAEA,SAAA;IAEA,UAAA;EAAA;cAVF,OAAA,UACgB,MAAA,+CACA,QAAA;IAEd,EAAA;IAEA,IAAA;IAEA,SAAA;IAEA,UAAA;EAAA;AAAA;AAAA,cAyEO,MAAA,GACX,KAAA,GAAQ,iBAAA,GAAoB,kBAAA,KAC5B,aAAA,GAAgB,WAAA,GAAc,OAAA,KAC7B,OAAA"}
@@ -0,0 +1,269 @@
1
+ import { _ as setLogger, a as hashIdentifier, c as rateLimiters, d as env, f as getLogger, g as safeEnv, h as isProduction, i as getRateLimitInfo, l as slidingWindow, m as hasUpstashConfig, n as createRateLimiter, o as isRateLimited, p as hasArcjetConfig, r as fixedWindow, s as rateLimitConfigs, t as applyRateLimit, u as tokenBucket } from "./rate-limit-DStYbhoa.mjs";
2
+ import "server-only";
3
+ import { arcjet, detectBot, request, shield } from "@integrations/arcjet/security-client";
4
+ import { createMiddleware as noseconeMiddleware, defaults } from "@nosecone/next";
5
+
6
+ //#region middleware.ts
7
+ /**
8
+ * @fileoverview middleware.ts
9
+ */
10
+ /**
11
+ * Default Nosecone security headers configuration
12
+ *
13
+ * Pre-configured security headers with CSP disabled by default. CSP should be
14
+ * configured based on your application's specific needs (inline scripts, third-party resources, etc.).
15
+ *
16
+ * Includes:
17
+ * - HSTS (HTTP Strict Transport Security)
18
+ * - X-Frame-Options (clickjacking protection)
19
+ * - X-Content-Type-Options (MIME sniffing protection)
20
+ * - Referrer-Policy
21
+ * - Permissions-Policy
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * import { noseconeMiddleware, noseconeOptions } from '@od-oneapp/security/server/next';
26
+ *
27
+ * // Use default configuration
28
+ * export const middleware = noseconeMiddleware(noseconeOptions);
29
+ * ```
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Custom CSP configuration
34
+ * import { noseconeMiddleware, noseconeOptions } from '@od-oneapp/security/server/next';
35
+ *
36
+ * export const middleware = noseconeMiddleware({
37
+ * ...noseconeOptions,
38
+ * contentSecurityPolicy: {
39
+ * directives: {
40
+ * defaultSrc: ["'self'"],
41
+ * scriptSrc: ["'self'", "'unsafe-inline'"],
42
+ * styleSrc: ["'self'", "'unsafe-inline'"],
43
+ * },
44
+ * },
45
+ * });
46
+ * ```
47
+ *
48
+ * @see {@link https://docs.arcjet.com/nosecone/quick-start | Nosecone Documentation}
49
+ */
50
+ const noseconeOptions = {
51
+ ...defaults,
52
+ contentSecurityPolicy: false
53
+ };
54
+
55
+ //#endregion
56
+ //#region src/server-next.ts
57
+ /**
58
+ * @fileoverview Server-side security exports for Next.js
59
+ *
60
+ * This file provides server-side security functionality specifically for Next.js applications.
61
+ * Includes Arcjet integration for bot detection, rate limiting, and Shield protection.
62
+ *
63
+ * Features:
64
+ * - Bot detection and filtering
65
+ * - Rate limiting
66
+ * - Shield protection against common attacks
67
+ * - Security denial error handling
68
+ *
69
+ * @module @od-oneapp/security/server/next
70
+ */
71
+ /**
72
+ * Custom error class for security denials.
73
+ *
74
+ * @remarks
75
+ * Thrown when a request is denied by Arcjet security checks (bot detection,
76
+ * rate limiting, or Shield protection). Includes detailed metadata for logging
77
+ * and debugging. The `reason` field indicates the type of denial, allowing
78
+ * for specific error handling logic.
79
+ *
80
+ * **Denial Reasons**:
81
+ * - `'bot'`: Request was identified as a bot and blocked
82
+ * - `'rate_limit'`: Request exceeded rate limits
83
+ * - `'shield'`: Request triggered Shield protection (malicious pattern detected)
84
+ * - `'unknown'`: Request was denied for an unspecified reason
85
+ *
86
+ * **Metadata Fields**:
87
+ * - `ip`: Client IP address (useful for blocking/whitelisting)
88
+ * - `path`: Request path (useful for route-specific logging)
89
+ * - `userAgent`: User agent string (useful for bot identification)
90
+ * - `decisionId`: Arcjet decision ID (useful for support/debugging)
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * import { secure, SecurityDenialError } from '@od-oneapp/security/server/next';
95
+ *
96
+ * try {
97
+ * await secure([], request); // Block all bots
98
+ * } catch (error) {
99
+ * if (error instanceof SecurityDenialError) {
100
+ * console.log(`Denied: ${error.reason}`, error.metadata);
101
+ * return new Response('Access denied', { status: 403 });
102
+ * }
103
+ * }
104
+ * ```
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * // Handle specific denial reasons
109
+ * catch (error) {
110
+ * if (error instanceof SecurityDenialError) {
111
+ * if (error.reason === 'rate_limit') {
112
+ * return new Response('Too many requests', { status: 429 });
113
+ * }
114
+ * return new Response('Forbidden', { status: 403 });
115
+ * }
116
+ * }
117
+ * ```
118
+ */
119
+ var SecurityDenialError = class extends Error {
120
+ /**
121
+ * Creates a new SecurityDenialError.
122
+ *
123
+ * @param message - Human-readable error message
124
+ * @param reason - Type of security denial
125
+ * @param metadata - Optional metadata about the denied request
126
+ */
127
+ constructor(message, reason, metadata) {
128
+ super(message);
129
+ this.reason = reason;
130
+ this.metadata = metadata;
131
+ this.name = "SecurityDenialError";
132
+ }
133
+ };
134
+ /**
135
+ * Secure a request using Arcjet bot detection and Shield protection.
136
+ *
137
+ * @remarks
138
+ * This function provides comprehensive security protection for Next.js routes:
139
+ * - **Bot Detection**: Blocks or allows bots based on the `allow` parameter
140
+ * - **Shield Protection**: Protects against common attacks (SQL injection, XSS, etc.)
141
+ * - **Rate Limiting**: Integrated rate limiting via Arcjet Shield
142
+ *
143
+ * The function uses Arcjet's decision system to evaluate requests. If a request
144
+ * is denied, a `SecurityDenialError` is thrown with detailed metadata for logging.
145
+ *
146
+ * **Error Handling**:
147
+ * - Network errors in development allow requests through with warnings (fail-open)
148
+ * - Non-network errors and all production errors fail closed for security
149
+ * - Detailed error logging helps debug configuration issues
150
+ *
151
+ * **Performance**: This function makes an external API call to Arcjet, so it
152
+ * adds latency. Consider caching decisions for high-traffic routes.
153
+ *
154
+ * @param allow - Array of bot categories or well-known bots to allow.
155
+ * Use empty array to block all bots. Common values:
156
+ * - `'GOOGLEBOT'`, `'BINGBOT'` for search engines
157
+ * - `'FACEBOOKBOT'`, `'TWITTERBOT'` for social media crawlers
158
+ * - `'AUTOMATED'` for all automated bots
159
+ * @param sourceRequest - Optional Next.js request object. If not provided,
160
+ * will use Arcjet's request() helper to get the current request.
161
+ * @returns Promise that resolves if request is allowed
162
+ * @throws {SecurityDenialError} If request is denied (bot, rate limit, or shield protection)
163
+ * @throws {Error} If security check fails in production or for non-network errors in development
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * // Allow only Google and Bing bots
168
+ * await secure(['GOOGLEBOT', 'BINGBOT'], request);
169
+ * ```
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * // Block all bots
174
+ * await secure([], request);
175
+ * ```
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * // In a Next.js API route handler
180
+ * import { secure } from '@od-oneapp/security/server/next';
181
+ * import { NextRequest } from 'next/server';
182
+ *
183
+ * export async function POST(request: NextRequest) {
184
+ * try {
185
+ * await secure([], request); // Block all bots
186
+ * // Process request...
187
+ * } catch (error) {
188
+ * if (error instanceof SecurityDenialError) {
189
+ * return Response.json(
190
+ * { error: 'Access denied' },
191
+ * { status: 403 }
192
+ * );
193
+ * }
194
+ * throw error;
195
+ * }
196
+ * }
197
+ * ```
198
+ */
199
+ const secure = async (allow, sourceRequest) => {
200
+ const env = safeEnv();
201
+ const arcjetKey = env.ARCJET_KEY;
202
+ if (!arcjetKey) return;
203
+ try {
204
+ const base = arcjet({
205
+ characteristics: ["ip.src"],
206
+ key: arcjetKey,
207
+ rules: [shield({ mode: "LIVE" })]
208
+ });
209
+ const req = sourceRequest ?? await request();
210
+ const decision = await base.withRule(detectBot({
211
+ allow,
212
+ mode: "LIVE"
213
+ })).protect(req);
214
+ if (decision.isDenied()) {
215
+ const getIp = (request) => {
216
+ if ("ip" in request && request.ip && typeof request.ip === "string") return request.ip;
217
+ const headers = "headers" in request ? request.headers : null;
218
+ if (headers && typeof headers.get === "function") {
219
+ const forwardedFor = headers.get("x-forwarded-for");
220
+ if (forwardedFor) return forwardedFor.split(",")[0]?.trim() ?? "unknown";
221
+ }
222
+ return "unknown";
223
+ };
224
+ const getHeader = (name) => {
225
+ const headers = "headers" in req ? req.headers : null;
226
+ if (headers && typeof headers.get === "function") return headers.get(name) ?? "unknown";
227
+ return "unknown";
228
+ };
229
+ const metadata = {
230
+ ip: getIp(req),
231
+ path: req.url ? new URL(req.url).pathname : "unknown",
232
+ userAgent: getHeader("user-agent"),
233
+ decisionId: decision.id
234
+ };
235
+ if (decision.reason.isBot()) throw new SecurityDenialError(`Bot access denied: ${metadata.ip} attempted to access ${metadata.path}`, "bot", metadata);
236
+ if (decision.reason.isRateLimit()) throw new SecurityDenialError(`Rate limit exceeded: ${metadata.ip} exceeded limits for ${metadata.path}`, "rate_limit", metadata);
237
+ if (decision.reason.isShield()) throw new SecurityDenialError(`Shield protection triggered: ${metadata.ip} blocked from ${metadata.path}`, "shield", metadata);
238
+ throw new SecurityDenialError(`Access denied: ${metadata.ip} denied access to ${metadata.path}`, "unknown", metadata);
239
+ }
240
+ } catch (error) {
241
+ if (error instanceof SecurityDenialError) {
242
+ getLogger().error("Arcjet protection denied request", {
243
+ error: error.message,
244
+ nodeEnv: env.NODE_ENV,
245
+ reason: error.reason,
246
+ decisionId: error.metadata?.decisionId
247
+ });
248
+ throw error;
249
+ }
250
+ const isNetworkError = error instanceof Error && ("code" in error && typeof error.code === "string" && (error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT" || error.code === "ENOTFOUND") || error.message.includes("ECONNREFUSED") || error.message.includes("ETIMEDOUT") || error.message.includes("ENOTFOUND") || error.message.includes("fetch failed") || error.message.includes("network"));
251
+ const errorContext = {
252
+ error: error instanceof Error ? error.message : String(error),
253
+ stack: error instanceof Error ? error.stack : void 0,
254
+ nodeEnv: env.NODE_ENV,
255
+ errorType: isNetworkError ? "network" : "unknown"
256
+ };
257
+ getLogger().error("Arcjet protection failed", errorContext);
258
+ if (env.NODE_ENV === "production") throw new Error("Security check failed. Request denied.");
259
+ if (!isNetworkError) {
260
+ getLogger().warn("Failing closed in development for non-network error");
261
+ throw new Error("Security check failed. See logs for details.");
262
+ }
263
+ getLogger().warn("Allowing request in development due to network error", { error: error instanceof Error ? error.message : String(error) });
264
+ }
265
+ };
266
+
267
+ //#endregion
268
+ export { SecurityDenialError, applyRateLimit, createRateLimiter, env, fixedWindow, getLogger, getRateLimitInfo, hasArcjetConfig, hasUpstashConfig, hashIdentifier, isProduction, isRateLimited, noseconeMiddleware, noseconeOptions, rateLimitConfigs, rateLimiters, safeEnv, secure, setLogger, slidingWindow, tokenBucket };
269
+ //# sourceMappingURL=server-next.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-next.mjs","names":[],"sources":["../middleware.ts","../src/server-next.ts"],"sourcesContent":["/**\n * @fileoverview middleware.ts\n */\n\nimport { defaults, type NoseconeOptions } from '@nosecone/next';\n\n/**\n * Create Next.js middleware with Nosecone security headers\n *\n * Re-exported from @nosecone/next for convenience. Use this to add security headers\n * (HSTS, X-Frame-Options, etc.) to your Next.js application.\n *\n * @example\n * ```typescript\n * import { noseconeMiddleware, noseconeOptions } from '@od-oneapp/security/server/next';\n *\n * export const middleware = noseconeMiddleware(noseconeOptions);\n *\n * export const config = {\n * matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n * };\n * ```\n *\n * @see {@link https://docs.arcjet.com/nosecone/quick-start | Nosecone Documentation}\n */\nexport { createMiddleware as noseconeMiddleware } from '@nosecone/next';\n\n/**\n * Default Nosecone security headers configuration\n *\n * Pre-configured security headers with CSP disabled by default. CSP should be\n * configured based on your application's specific needs (inline scripts, third-party resources, etc.).\n *\n * Includes:\n * - HSTS (HTTP Strict Transport Security)\n * - X-Frame-Options (clickjacking protection)\n * - X-Content-Type-Options (MIME sniffing protection)\n * - Referrer-Policy\n * - Permissions-Policy\n *\n * @example\n * ```typescript\n * import { noseconeMiddleware, noseconeOptions } from '@od-oneapp/security/server/next';\n *\n * // Use default configuration\n * export const middleware = noseconeMiddleware(noseconeOptions);\n * ```\n *\n * @example\n * ```typescript\n * // Custom CSP configuration\n * import { noseconeMiddleware, noseconeOptions } from '@od-oneapp/security/server/next';\n *\n * export const middleware = noseconeMiddleware({\n * ...noseconeOptions,\n * contentSecurityPolicy: {\n * directives: {\n * defaultSrc: [\"'self'\"],\n * scriptSrc: [\"'self'\", \"'unsafe-inline'\"],\n * styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n * },\n * },\n * });\n * ```\n *\n * @see {@link https://docs.arcjet.com/nosecone/quick-start | Nosecone Documentation}\n */\nexport const noseconeOptions: NoseconeOptions = {\n ...defaults,\n // Content Security Policy (CSP) is disabled by default because the values\n // depend on which OneApp features are enabled. See\n // https://docs.forge.com/features/security/headers for guidance on how\n // to configure it.\n contentSecurityPolicy: false,\n};\n","/**\n * @fileoverview Server-side security exports for Next.js\n *\n * This file provides server-side security functionality specifically for Next.js applications.\n * Includes Arcjet integration for bot detection, rate limiting, and Shield protection.\n *\n * Features:\n * - Bot detection and filtering\n * - Rate limiting\n * - Shield protection against common attacks\n * - Security denial error handling\n *\n * @module @od-oneapp/security/server/next\n */\n\nimport {\n arcjet,\n type ArcjetBotCategory,\n type ArcjetWellKnownBot,\n detectBot,\n request,\n shield,\n} from '@integrations/arcjet/security-client';\n\nimport { getLogger, safeEnv } from '../env';\n\nimport type { NextRequest } from 'next/server';\nimport 'server-only';\n\n// Re-export server functionality (explicit exports to avoid circular dependencies)\nexport { createRateLimiter, env, safeEnv } from './server';\n\n// Re-export rate limiting functionality\nexport {\n applyRateLimit,\n fixedWindow,\n getRateLimitInfo,\n hashIdentifier,\n isRateLimited,\n rateLimitConfigs,\n rateLimiters,\n slidingWindow,\n tokenBucket,\n type RateLimitResult,\n} from '../rate-limit';\n\n// Re-export middleware utilities for Next.js\nexport { noseconeMiddleware, noseconeOptions } from '../middleware';\n\n// Re-export environment helpers\nexport { getLogger, hasArcjetConfig, hasUpstashConfig, isProduction, setLogger } from '../env';\n\n/**\n * Custom error class for security denials.\n *\n * @remarks\n * Thrown when a request is denied by Arcjet security checks (bot detection,\n * rate limiting, or Shield protection). Includes detailed metadata for logging\n * and debugging. The `reason` field indicates the type of denial, allowing\n * for specific error handling logic.\n *\n * **Denial Reasons**:\n * - `'bot'`: Request was identified as a bot and blocked\n * - `'rate_limit'`: Request exceeded rate limits\n * - `'shield'`: Request triggered Shield protection (malicious pattern detected)\n * - `'unknown'`: Request was denied for an unspecified reason\n *\n * **Metadata Fields**:\n * - `ip`: Client IP address (useful for blocking/whitelisting)\n * - `path`: Request path (useful for route-specific logging)\n * - `userAgent`: User agent string (useful for bot identification)\n * - `decisionId`: Arcjet decision ID (useful for support/debugging)\n *\n * @example\n * ```typescript\n * import { secure, SecurityDenialError } from '@od-oneapp/security/server/next';\n *\n * try {\n * await secure([], request); // Block all bots\n * } catch (error) {\n * if (error instanceof SecurityDenialError) {\n * console.log(`Denied: ${error.reason}`, error.metadata);\n * return new Response('Access denied', { status: 403 });\n * }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Handle specific denial reasons\n * catch (error) {\n * if (error instanceof SecurityDenialError) {\n * if (error.reason === 'rate_limit') {\n * return new Response('Too many requests', { status: 429 });\n * }\n * return new Response('Forbidden', { status: 403 });\n * }\n * }\n * ```\n */\nexport class SecurityDenialError extends Error {\n /**\n * Creates a new SecurityDenialError.\n *\n * @param message - Human-readable error message\n * @param reason - Type of security denial\n * @param metadata - Optional metadata about the denied request\n */\n constructor(\n message: string,\n public readonly reason: 'bot' | 'rate_limit' | 'shield' | 'unknown',\n public readonly metadata?: {\n /** Client IP address. */\n ip?: string;\n /** Request path. */\n path?: string;\n /** User agent string. */\n userAgent?: string;\n /** Arcjet decision ID for debugging. */\n decisionId?: string;\n },\n ) {\n super(message);\n this.name = 'SecurityDenialError';\n }\n}\n\n/**\n * Secure a request using Arcjet bot detection and Shield protection.\n *\n * @remarks\n * This function provides comprehensive security protection for Next.js routes:\n * - **Bot Detection**: Blocks or allows bots based on the `allow` parameter\n * - **Shield Protection**: Protects against common attacks (SQL injection, XSS, etc.)\n * - **Rate Limiting**: Integrated rate limiting via Arcjet Shield\n *\n * The function uses Arcjet's decision system to evaluate requests. If a request\n * is denied, a `SecurityDenialError` is thrown with detailed metadata for logging.\n *\n * **Error Handling**:\n * - Network errors in development allow requests through with warnings (fail-open)\n * - Non-network errors and all production errors fail closed for security\n * - Detailed error logging helps debug configuration issues\n *\n * **Performance**: This function makes an external API call to Arcjet, so it\n * adds latency. Consider caching decisions for high-traffic routes.\n *\n * @param allow - Array of bot categories or well-known bots to allow.\n * Use empty array to block all bots. Common values:\n * - `'GOOGLEBOT'`, `'BINGBOT'` for search engines\n * - `'FACEBOOKBOT'`, `'TWITTERBOT'` for social media crawlers\n * - `'AUTOMATED'` for all automated bots\n * @param sourceRequest - Optional Next.js request object. If not provided,\n * will use Arcjet's request() helper to get the current request.\n * @returns Promise that resolves if request is allowed\n * @throws {SecurityDenialError} If request is denied (bot, rate limit, or shield protection)\n * @throws {Error} If security check fails in production or for non-network errors in development\n *\n * @example\n * ```typescript\n * // Allow only Google and Bing bots\n * await secure(['GOOGLEBOT', 'BINGBOT'], request);\n * ```\n *\n * @example\n * ```typescript\n * // Block all bots\n * await secure([], request);\n * ```\n *\n * @example\n * ```typescript\n * // In a Next.js API route handler\n * import { secure } from '@od-oneapp/security/server/next';\n * import { NextRequest } from 'next/server';\n *\n * export async function POST(request: NextRequest) {\n * try {\n * await secure([], request); // Block all bots\n * // Process request...\n * } catch (error) {\n * if (error instanceof SecurityDenialError) {\n * return Response.json(\n * { error: 'Access denied' },\n * { status: 403 }\n * );\n * }\n * throw error;\n * }\n * }\n * ```\n */\nexport const secure = async (\n allow: (ArcjetBotCategory | ArcjetWellKnownBot)[],\n sourceRequest?: NextRequest | Request,\n): Promise<void> => {\n const env = safeEnv();\n const arcjetKey = env.ARCJET_KEY;\n\n if (!arcjetKey) {\n return;\n }\n\n try {\n const base = arcjet({\n // Identify the user by their IP address\n characteristics: ['ip.src'],\n // Get your site key from https://app.arcjet.com\n key: arcjetKey,\n rules: [\n // Protect against common attacks with Arcjet Shield\n shield({\n // Will block requests. Use \"DRY_RUN\" to log only\n mode: 'LIVE',\n }),\n // Other rules are added in different routes\n ],\n });\n\n const req = sourceRequest ?? (await request());\n const aj = base.withRule(detectBot({ allow, mode: 'LIVE' }));\n const decision = await aj.protect(req);\n\n if (decision.isDenied()) {\n // Extract metadata for logging\n const getIp = (request: NextRequest | Request): string => {\n if ('ip' in request && request.ip && typeof request.ip === 'string') {\n return request.ip;\n }\n const headers = 'headers' in request ? request.headers : null;\n if (headers && typeof headers.get === 'function') {\n const forwardedFor = headers.get('x-forwarded-for');\n if (forwardedFor) {\n return forwardedFor.split(',')[0]?.trim() ?? 'unknown';\n }\n }\n return 'unknown';\n };\n\n const getHeader = (name: string): string => {\n const headers = 'headers' in req ? req.headers : null;\n if (headers && typeof headers.get === 'function') {\n return headers.get(name) ?? 'unknown';\n }\n return 'unknown';\n };\n\n const metadata = {\n ip: getIp(req as NextRequest | Request),\n path: req.url ? new URL(req.url).pathname : 'unknown',\n userAgent: getHeader('user-agent'),\n decisionId: decision.id,\n };\n\n if (decision.reason.isBot()) {\n throw new SecurityDenialError(\n `Bot access denied: ${metadata.ip} attempted to access ${metadata.path}`,\n 'bot',\n metadata,\n );\n }\n\n if (decision.reason.isRateLimit()) {\n throw new SecurityDenialError(\n `Rate limit exceeded: ${metadata.ip} exceeded limits for ${metadata.path}`,\n 'rate_limit',\n metadata,\n );\n }\n\n if (decision.reason.isShield()) {\n throw new SecurityDenialError(\n `Shield protection triggered: ${metadata.ip} blocked from ${metadata.path}`,\n 'shield',\n metadata,\n );\n }\n\n throw new SecurityDenialError(\n `Access denied: ${metadata.ip} denied access to ${metadata.path}`,\n 'unknown',\n metadata,\n );\n }\n } catch (error) {\n if (error instanceof SecurityDenialError) {\n getLogger().error('Arcjet protection denied request', {\n error: error.message,\n nodeEnv: env.NODE_ENV,\n reason: error.reason,\n decisionId: error.metadata?.decisionId,\n });\n throw error;\n }\n\n // Categorize error type for better handling\n // Prefer robust error.code check (Node.js standard) before falling back to message matching\n // This handles both Node.js network errors (with error.code) and fetch/Arcjet errors\n // (which may only have error.message). The dual approach ensures we catch network failures\n // reliably across different error sources.\n //\n // Note: In production, all errors result in fail-closed behavior for security.\n // In development, only confirmed network errors allow requests through (with warnings).\n // Non-network errors fail closed even in development to surface configuration issues early.\n const isNetworkError =\n error instanceof Error &&\n (('code' in error &&\n typeof (error as { code?: unknown }).code === 'string' &&\n ((error as { code: string }).code === 'ECONNREFUSED' ||\n (error as { code: string }).code === 'ETIMEDOUT' ||\n (error as { code: string }).code === 'ENOTFOUND')) ||\n // Fallback to message matching for fetch/Arcjet errors without error.code\n error.message.includes('ECONNREFUSED') ||\n error.message.includes('ETIMEDOUT') ||\n error.message.includes('ENOTFOUND') ||\n error.message.includes('fetch failed') ||\n error.message.includes('network'));\n\n const errorContext = {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n nodeEnv: env.NODE_ENV,\n errorType: isNetworkError ? 'network' : 'unknown',\n };\n\n getLogger().error('Arcjet protection failed', errorContext);\n\n // In production, always fail closed for security\n if (env.NODE_ENV === 'production') {\n throw new Error('Security check failed. Request denied.');\n }\n\n // In development, fail closed for non-network errors\n if (!isNetworkError) {\n getLogger().warn('Failing closed in development for non-network error');\n throw new Error('Security check failed. See logs for details.');\n }\n\n // Development + network error: allow with warning\n getLogger().warn('Allowing request in development due to network error', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n};\n\n// Re-export Arcjet types for Next.js\nexport type { ArcjetBotCategory, ArcjetWellKnownBot };\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { o as safeEnv, t as env, u as createRateLimiter } from "./env-DvTVXAjh.mjs";
2
+ export { createRateLimiter, env, safeEnv };
@@ -0,0 +1,3 @@
1
+ import { d as env, g as safeEnv, n as createRateLimiter } from "./rate-limit-DStYbhoa.mjs";
2
+
3
+ export { createRateLimiter, env, safeEnv };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@od-oneapp/security",
3
+ "version": "2026.1.1301",
4
+ "private": false,
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/OneDigital-Product/monorepo.git",
8
+ "directory": "platform/packages/security"
9
+ },
10
+ "sideEffects": false,
11
+ "type": "module",
12
+ "exports": {
13
+ "./client": {
14
+ "types": "./dist/client.d.mts",
15
+ "import": "./dist/client.mjs",
16
+ "default": "./dist/client.mjs"
17
+ },
18
+ "./server": {
19
+ "types": "./dist/server.d.mts",
20
+ "import": "./dist/server.mjs",
21
+ "default": "./dist/server.mjs"
22
+ },
23
+ "./client/next": {
24
+ "types": "./dist/client-next.d.mts",
25
+ "import": "./dist/client-next.mjs",
26
+ "default": "./dist/client-next.mjs"
27
+ },
28
+ "./server/next": {
29
+ "types": "./dist/server-next.d.mts",
30
+ "import": "./dist/server-next.mjs",
31
+ "default": "./dist/server-next.mjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "dependencies": {
39
+ "@nosecone/next": "1.1.0",
40
+ "@t3-oss/env-core": "^0.13.10",
41
+ "nosecone": "1.1.0",
42
+ "server-only": "^0.0.1",
43
+ "tsdown": "^0.20.3",
44
+ "zod": "4.3.6",
45
+ "@integrations/arcjet": "2026.1.1301",
46
+ "@integrations/upstash": "2026.1.1301",
47
+ "@repo/shared": "2026.1.1301"
48
+ },
49
+ "devDependencies": {
50
+ "@vitest/coverage-v8": "4.0.18",
51
+ "eslint": "9.39.2",
52
+ "knip": "^5.83.1",
53
+ "madge": "8.0.0",
54
+ "next": "16.1.6",
55
+ "typescript": "5.9.3",
56
+ "vitest": "4.0.18",
57
+ "@od-oneapp/qa": "2026.1.1301",
58
+ "@repo/config": "2026.1.1301",
59
+ "@repo/db-upstash-redis": "2026.1.1301"
60
+ },
61
+ "publishConfig": {
62
+ "access": "restricted",
63
+ "registry": "https://registry.npmjs.org/"
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown",
67
+ "build:publish": "tsdown && node ../../../scripts/prepare-publish.mjs",
68
+ "circular": "madge --circular --extensions ts,tsx,js,jsx .",
69
+ "coverage:collect": "vitest run --coverage --reporter=json",
70
+ "format": "prettier --write --cache --ignore-unknown --ignore-path ../../../.prettierignore .",
71
+ "format:check": "prettier --check --cache --ignore-unknown --ignore-path ../../../.prettierignore .",
72
+ "knip": "knip --reporter json --exclude unlisted,exports,files,binaries,types,duplicates",
73
+ "lint": "eslint . --fix",
74
+ "test": "vitest run",
75
+ "test:coverage": "vitest run --coverage",
76
+ "test:coverage:json": "vitest run --coverage --reporter=json",
77
+ "test:watch": "vitest",
78
+ "typecheck": "tsc --noEmit"
79
+ }
80
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview Client-side security exports for Next.js
3
+ *
4
+ * This file provides client-side security functionality specifically for Next.js applications.
5
+ * Re-exports client security utilities for Next.js client components.
6
+ *
7
+ * @module @repo/security/client/next
8
+ */
9
+
10
+ 'use client';
11
+
12
+ // Re-export client functionality (explicit exports to avoid circular dependencies)
13
+ export { getSecurityHeaders } from './client';
package/src/client.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview Client-side security exports (non-Next.js)
3
+ *
4
+ * This file provides client-side security functionality for non-Next.js environments.
5
+ * For Next.js applications, use '@repo/security/client/next' instead.
6
+ *
7
+ * Note: Security headers must be set server-side for actual protection.
8
+ * This module provides placeholder functions for client-side compatibility.
9
+ *
10
+ * @module @repo/security/client
11
+ */
12
+
13
+ /**
14
+ * Get security headers (client-side placeholder).
15
+ *
16
+ * @remarks
17
+ * This is a client-side placeholder function. Security headers must be set server-side
18
+ * for actual protection. Use '@repo/security/server' instead for real security headers.
19
+ *
20
+ * **Why client-side can't set security headers**:
21
+ * - Security headers (CSP, HSTS, etc.) must be set in HTTP responses
22
+ * - Client-side JavaScript cannot modify HTTP response headers
23
+ * - Headers set client-side are ignored by browsers for security reasons
24
+ *
25
+ * **Use server-side instead**:
26
+ * - Next.js: Use middleware or API routes with `@repo/security/server/next`
27
+ * - Express: Use middleware with `@repo/security/server`
28
+ * - Other frameworks: Set headers in response handlers
29
+ *
30
+ * @returns Empty object (security headers must be set server-side)
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { getSecurityHeaders } from '@repo/security/client';
35
+ *
36
+ * // Returns empty object - headers must be set server-side
37
+ * const headers = getSecurityHeaders();
38
+ * ```
39
+ *
40
+ * @see Use '@repo/security/server' for actual security headers
41
+ */
42
+ export function getSecurityHeaders() {
43
+ // This is a client-side placeholder
44
+ // Real security headers must be set server-side
45
+ // Use @repo/security/server instead for actual security headers
46
+ return {};
47
+ }