@gagandeep023/api-gateway 0.3.1 → 0.4.1

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/backend/middleware/gateway.ts","../../src/backend/services/RateLimiterService.ts","../../src/backend/services/AnalyticsService.ts","../../src/config/defaults.ts","../../src/backend/middleware/apiKeyAuth.ts","../../src/backend/middleware/ipFilter.ts","../../src/backend/middleware/rateLimiter.ts","../../src/backend/middleware/requestLogger.ts","../../src/backend/routes/gateway.ts"],"sourcesContent":["import { Router } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\nimport { AnalyticsService } from '../services/AnalyticsService';\nimport type { GatewayMiddlewareConfig } from '../../types';\nimport { DEFAULT_RATE_LIMIT_CONFIG, DEFAULT_IP_RULES, DEFAULT_API_KEYS } from '../../config/defaults';\nimport { createApiKeyAuth } from './apiKeyAuth';\nimport { createIpFilter } from './ipFilter';\nimport { createRateLimiter } from './rateLimiter';\nimport { createRequestLogger } from './requestLogger';\n\nexport interface GatewayInstances {\n rateLimiterService: RateLimiterService;\n analyticsService: AnalyticsService;\n middleware: Router;\n config: Required<GatewayMiddlewareConfig>;\n}\n\nexport function createGatewayMiddleware(userConfig?: GatewayMiddlewareConfig): GatewayInstances {\n const config: Required<GatewayMiddlewareConfig> = {\n rateLimits: userConfig?.rateLimits ?? DEFAULT_RATE_LIMIT_CONFIG,\n ipRules: userConfig?.ipRules ?? DEFAULT_IP_RULES,\n apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS,\n };\n\n const rateLimiterService = new RateLimiterService(config.rateLimits);\n const analyticsService = new AnalyticsService();\n\n const router = Router();\n router.use(createRequestLogger(analyticsService));\n router.use(createApiKeyAuth(() => config.apiKeys));\n router.use(createIpFilter(() => config.ipRules));\n router.use(createRateLimiter(rateLimiterService));\n\n return {\n rateLimiterService,\n analyticsService,\n middleware: router,\n config,\n };\n}\n","import type { BucketState, SlidingWindowState, FixedWindowState, TierConfig, RateLimitConfig } from '../../types';\n\nclass TokenBucket {\n private buckets = new Map<string, BucketState>();\n\n tryConsume(ip: string, maxTokens: number, refillRate: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let bucket = this.buckets.get(ip);\n\n if (!bucket) {\n bucket = { tokens: maxTokens, lastRefill: now };\n this.buckets.set(ip, bucket);\n }\n\n const elapsed = (now - bucket.lastRefill) / 1000;\n bucket.tokens = Math.min(maxTokens, bucket.tokens + elapsed * refillRate);\n bucket.lastRefill = now;\n\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1;\n const resetMs = bucket.tokens <= 0 ? Math.ceil((1 / refillRate) * 1000) : 0;\n return { allowed: true, remaining: Math.floor(bucket.tokens), resetMs };\n }\n\n const resetMs = Math.ceil(((1 - bucket.tokens) / refillRate) * 1000);\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nclass SlidingWindowLog {\n private windows = new Map<string, SlidingWindowState>();\n\n tryConsume(ip: string, maxRequests: number, windowMs: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let state = this.windows.get(ip);\n\n if (!state) {\n state = { timestamps: [] };\n this.windows.set(ip, state);\n }\n\n state.timestamps = state.timestamps.filter(t => now - t < windowMs);\n\n if (state.timestamps.length < maxRequests) {\n state.timestamps.push(now);\n return {\n allowed: true,\n remaining: maxRequests - state.timestamps.length,\n resetMs: state.timestamps.length > 0 ? windowMs - (now - state.timestamps[0]) : windowMs,\n };\n }\n\n const oldestInWindow = state.timestamps[0];\n const resetMs = windowMs - (now - oldestInWindow);\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nclass FixedWindowCounter {\n private windows = new Map<string, FixedWindowState>();\n\n tryConsume(ip: string, maxRequests: number, windowMs: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let state = this.windows.get(ip);\n\n if (!state || now - state.windowStart >= windowMs) {\n state = { count: 0, windowStart: now };\n this.windows.set(ip, state);\n }\n\n const resetMs = windowMs - (now - state.windowStart);\n\n if (state.count < maxRequests) {\n state.count++;\n return { allowed: true, remaining: maxRequests - state.count, resetMs };\n }\n\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nexport class RateLimiterService {\n private tokenBucket = new TokenBucket();\n private slidingWindow = new SlidingWindowLog();\n private fixedWindow = new FixedWindowCounter();\n private globalWindow = new FixedWindowCounter();\n private config: RateLimitConfig;\n private _rateLimitHits = 0;\n\n constructor(config: RateLimitConfig) {\n this.config = config;\n }\n\n get rateLimitHits(): number {\n return this._rateLimitHits;\n }\n\n checkLimit(ip: string, tier: string): { allowed: boolean; remaining: number; resetMs: number; limit: number } {\n const globalResult = this.globalWindow.tryConsume(\n '__global__',\n this.config.globalLimit.maxRequests,\n this.config.globalLimit.windowMs\n );\n\n if (!globalResult.allowed) {\n this._rateLimitHits++;\n return { allowed: false, remaining: 0, resetMs: globalResult.resetMs, limit: this.config.globalLimit.maxRequests };\n }\n\n const tierConfig = this.config.tiers[tier] || this.config.tiers[this.config.defaultTier];\n\n if (!tierConfig || tierConfig.algorithm === 'none') {\n return { allowed: true, remaining: -1, resetMs: 0, limit: -1 };\n }\n\n let result: { allowed: boolean; remaining: number; resetMs: number };\n\n switch (tierConfig.algorithm) {\n case 'tokenBucket':\n result = this.tokenBucket.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.refillRate || 1\n );\n break;\n case 'slidingWindow':\n result = this.slidingWindow.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.windowMs!\n );\n break;\n case 'fixedWindow':\n result = this.fixedWindow.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.windowMs!\n );\n break;\n default:\n return { allowed: true, remaining: -1, resetMs: 0, limit: -1 };\n }\n\n if (!result.allowed) {\n this._rateLimitHits++;\n }\n\n return { ...result, limit: tierConfig.maxRequests! };\n }\n\n getConfig(): RateLimitConfig {\n return this.config;\n }\n}\n","import type { RequestLog, GatewayAnalytics } from '../../types';\n\nconst MAX_LOG_SIZE = 10000;\nconst ACTIVE_WINDOW_MS = 300000; // 5 minutes\n\nexport class AnalyticsService {\n private logs: RequestLog[] = [];\n private head = 0;\n private count = 0;\n\n addLog(log: RequestLog): void {\n if (this.count < MAX_LOG_SIZE) {\n this.logs.push(log);\n this.count++;\n } else {\n this.logs[this.head] = log;\n this.head = (this.head + 1) % MAX_LOG_SIZE;\n }\n }\n\n getRecentLogs(limit = 20, offset = 0): RequestLog[] {\n const ordered = this.getOrderedLogs();\n return ordered.slice(offset, offset + limit);\n }\n\n getAnalytics(rateLimitHits: number): GatewayAnalytics {\n const now = Date.now();\n const oneMinuteAgo = now - 60000;\n const activeWindowStart = now - ACTIVE_WINDOW_MS;\n const ordered = this.getOrderedLogs();\n\n const recentLogs = ordered.filter(l => l.timestamp > oneMinuteAgo);\n const requestsPerMinute = recentLogs.length;\n\n // Top endpoints\n const endpointCounts = new Map<string, number>();\n for (const log of ordered) {\n const current = endpointCounts.get(log.path) || 0;\n endpointCounts.set(log.path, current + 1);\n }\n const topEndpoints = Array.from(endpointCounts.entries())\n .map(([path, count]) => ({ path, count }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 5);\n\n // Error rate\n const errorCount = ordered.filter(l => l.statusCode >= 400).length;\n const errorRate = this.count > 0 ? (errorCount / this.count) * 100 : 0;\n\n // Average response time\n const totalResponseTime = ordered.reduce((sum, l) => sum + l.responseTime, 0);\n const avgResponseTime = this.count > 0 ? totalResponseTime / this.count : 0;\n\n // Active clients: unique IPs in last 5 minutes\n const activeLogs = ordered.filter(l => l.timestamp > activeWindowStart);\n const uniqueIps = new Set(activeLogs.map(l => l.ip));\n\n // Active key uses: unique (IP + apiKey) pairs in last 5 minutes\n const keyUsePairs = new Set<string>();\n for (const log of activeLogs) {\n if (log.apiKey) {\n keyUsePairs.add(`${log.ip}::${log.apiKey}`);\n }\n }\n\n return {\n totalRequests: this.count,\n requestsPerMinute,\n topEndpoints,\n errorRate: Math.round(errorRate * 100) / 100,\n avgResponseTime: Math.round(avgResponseTime * 100) / 100,\n activeClients: uniqueIps.size,\n activeKeyUses: keyUsePairs.size,\n rateLimitHits,\n };\n }\n\n private getOrderedLogs(): RequestLog[] {\n if (this.count < MAX_LOG_SIZE) {\n return [...this.logs].reverse();\n }\n const tail = this.logs.slice(0, this.head);\n const headPart = this.logs.slice(this.head);\n return [...headPart, ...tail].reverse();\n }\n}\n","import type { RateLimitConfig, IpRules, ApiKeysConfig } from '../types';\n\nexport const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {\n tiers: {\n free: { algorithm: 'tokenBucket', maxRequests: 100, windowMs: 60000, refillRate: 10 },\n pro: { algorithm: 'slidingWindow', maxRequests: 1000, windowMs: 60000 },\n unlimited: { algorithm: 'none' },\n },\n defaultTier: 'free',\n globalLimit: { maxRequests: 10000, windowMs: 60000 },\n};\n\nexport const DEFAULT_IP_RULES: IpRules = {\n allowlist: [],\n blocklist: [],\n mode: 'blocklist',\n};\n\nexport const DEFAULT_API_KEYS: ApiKeysConfig = {\n keys: [],\n};\n","import { Request, Response, NextFunction } from 'express';\nimport type { ApiKeysConfig } from '../../types';\n\nexport function createApiKeyAuth(getKeys: () => ApiKeysConfig) {\n return function apiKeyAuth(req: Request, res: Response, next: NextFunction): void {\n const apiKey = req.header('X-API-Key') || req.query.apiKey as string;\n\n if (!apiKey) {\n (req as any).clientId = req.ip || 'unknown';\n (req as any).tier = 'free';\n next();\n return;\n }\n\n const config = getKeys();\n const keyEntry = config.keys.find(k => k.key === apiKey && k.active);\n\n if (!keyEntry) {\n res.status(401).json({ error: 'Invalid or revoked API key' });\n return;\n }\n\n (req as any).clientId = keyEntry.id;\n (req as any).tier = keyEntry.tier;\n (req as any).apiKeyValue = keyEntry.key;\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport type { IpRules } from '../../types';\n\nexport function createIpFilter(getRules: () => IpRules) {\n return function ipFilter(req: Request, res: Response, next: NextFunction): void {\n const rules = getRules();\n const clientIp = req.ip || req.socket.remoteAddress || 'unknown';\n\n if (rules.mode === 'allowlist' && rules.allowlist.length > 0) {\n if (!rules.allowlist.includes(clientIp)) {\n res.status(403).json({ error: 'IP not in allowlist' });\n return;\n }\n }\n\n if (rules.mode === 'blocklist' && rules.blocklist.length > 0) {\n if (rules.blocklist.includes(clientIp)) {\n res.status(403).json({ error: 'IP is blocked' });\n return;\n }\n }\n\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\n\nexport function createRateLimiter(service: RateLimiterService) {\n return function rateLimiter(req: Request, res: Response, next: NextFunction): void {\n const ip = req.ip || req.socket.remoteAddress || 'unknown';\n const tier = (req as any).tier || 'free';\n\n const result = service.checkLimit(ip, tier);\n\n if (result.limit > 0) {\n res.setHeader('X-RateLimit-Limit', result.limit);\n res.setHeader('X-RateLimit-Remaining', Math.max(0, result.remaining));\n res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetMs / 1000));\n }\n\n if (!result.allowed) {\n res.status(429).json({\n error: 'Rate limit exceeded',\n retryAfter: Math.ceil(result.resetMs / 1000),\n });\n return;\n }\n\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport { AnalyticsService } from '../services/AnalyticsService';\n\nexport function createRequestLogger(analytics: AnalyticsService) {\n return function requestLogger(req: Request, res: Response, next: NextFunction): void {\n const start = Date.now();\n\n res.on('finish', () => {\n const responseTime = Date.now() - start;\n const apiKeyValue = (req as any).apiKeyValue || undefined;\n analytics.addLog({\n timestamp: Date.now(),\n method: req.method,\n path: req.originalUrl,\n statusCode: res.statusCode,\n responseTime,\n clientId: (req as any).clientId || req.ip || 'unknown',\n ip: req.ip || req.socket.remoteAddress || 'unknown',\n apiKey: apiKeyValue,\n authenticated: !!apiKeyValue,\n });\n });\n\n next();\n };\n}\n","import { Router, Request, Response } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\nimport { AnalyticsService } from '../services/AnalyticsService';\nimport type { GatewayMiddlewareConfig } from '../../types';\nimport crypto from 'crypto';\n\nexport interface GatewayRoutesOptions {\n rateLimiterService: RateLimiterService;\n analyticsService: AnalyticsService;\n config: Required<GatewayMiddlewareConfig>;\n}\n\nexport function createGatewayRoutes(options: GatewayRoutesOptions): Router {\n const { rateLimiterService, analyticsService, config } = options;\n const router = Router();\n\n // GET /analytics - Returns current analytics snapshot\n router.get('/analytics', (_req: Request, res: Response) => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.json(analytics);\n });\n\n // GET /analytics/live - SSE stream pushing analytics every 5 seconds\n router.get('/analytics/live', (_req: Request, res: Response) => {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n res.flushHeaders();\n\n const send = (): void => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.write(`data: ${JSON.stringify(analytics)}\\n\\n`);\n };\n\n send();\n const interval = setInterval(send, 5000);\n\n _req.on('close', () => {\n clearInterval(interval);\n });\n });\n\n // GET /config - Returns current gateway config\n router.get('/config', (_req: Request, res: Response) => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.json({\n rateLimits: config.rateLimits,\n ipRules: config.ipRules,\n activeKeys: config.apiKeys.keys.filter(k => k.active).length,\n activeKeyUses: analytics.activeKeyUses,\n });\n });\n\n // POST /keys - Create a new API key\n router.post('/keys', (req: Request, res: Response) => {\n const { name, tier } = req.body;\n\n if (!name) {\n res.status(400).json({ error: 'Name is required' });\n return;\n }\n\n const newKey = {\n id: `key_${String(config.apiKeys.keys.length + 1).padStart(3, '0')}`,\n key: `gw_live_${crypto.randomBytes(16).toString('hex')}`,\n name,\n tier: tier || 'free',\n createdAt: new Date().toISOString(),\n active: true,\n };\n\n config.apiKeys.keys.push(newKey);\n res.status(201).json(newKey);\n });\n\n // DELETE /keys/:keyId - Revoke an API key\n router.delete('/keys/:keyId', (req: Request, res: Response) => {\n const { keyId } = req.params;\n const key = config.apiKeys.keys.find(k => k.id === keyId);\n\n if (!key) {\n res.status(404).json({ error: 'API key not found' });\n return;\n }\n\n key.active = false;\n res.json({ message: 'API key revoked', id: keyId });\n });\n\n // GET /logs - Returns recent request logs (paginated)\n router.get('/logs', (req: Request, res: Response) => {\n const limit = parseInt(req.query.limit as string) || 20;\n const offset = parseInt(req.query.offset as string) || 0;\n const logs = analyticsService.getRecentLogs(limit, offset);\n res.json({ logs, limit, offset });\n });\n\n return router;\n}\n"],"mappings":"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","names":["resetMs","Router"]}
1
+ {"version":3,"sources":["../../src/backend/middleware/gateway.ts","../../src/backend/services/RateLimiterService.ts","../../src/backend/services/AnalyticsService.ts","../../src/backend/services/DeviceRegistryService.ts","../../src/backend/utils/totp.ts","../../src/config/defaults.ts","../../src/backend/middleware/apiKeyAuth.ts","../../src/backend/middleware/ipFilter.ts","../../src/backend/middleware/rateLimiter.ts","../../src/backend/middleware/requestLogger.ts","../../src/backend/routes/gateway.ts","../../src/backend/routes/deviceAuth.ts"],"sourcesContent":["import { Router } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\nimport { AnalyticsService } from '../services/AnalyticsService';\nimport { DeviceRegistryService } from '../services/DeviceRegistryService';\nimport type { GatewayMiddlewareConfig } from '../../types';\nimport { DEFAULT_RATE_LIMIT_CONFIG, DEFAULT_IP_RULES, DEFAULT_API_KEYS } from '../../config/defaults';\nimport { createApiKeyAuth } from './apiKeyAuth';\nimport { createIpFilter } from './ipFilter';\nimport { createRateLimiter } from './rateLimiter';\nimport { createRequestLogger } from './requestLogger';\n\nexport interface GatewayInstances {\n rateLimiterService: RateLimiterService;\n analyticsService: AnalyticsService;\n deviceRegistry?: DeviceRegistryService;\n middleware: Router;\n config: Required<GatewayMiddlewareConfig>;\n}\n\nexport function createGatewayMiddleware(userConfig?: GatewayMiddlewareConfig): GatewayInstances {\n const config: Required<GatewayMiddlewareConfig> = {\n rateLimits: userConfig?.rateLimits ?? DEFAULT_RATE_LIMIT_CONFIG,\n ipRules: userConfig?.ipRules ?? DEFAULT_IP_RULES,\n apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS,\n deviceRegistryPath: userConfig?.deviceRegistryPath ?? '',\n };\n\n const rateLimiterService = new RateLimiterService(config.rateLimits);\n const analyticsService = new AnalyticsService();\n\n let deviceRegistry: DeviceRegistryService | undefined;\n if (config.deviceRegistryPath) {\n deviceRegistry = new DeviceRegistryService(config.deviceRegistryPath);\n }\n\n const router = Router();\n router.use(createRequestLogger(analyticsService));\n router.use(createApiKeyAuth(() => config.apiKeys, deviceRegistry));\n router.use(createIpFilter(() => config.ipRules));\n router.use(createRateLimiter(rateLimiterService));\n\n return {\n rateLimiterService,\n analyticsService,\n deviceRegistry,\n middleware: router,\n config,\n };\n}\n","import type { BucketState, SlidingWindowState, FixedWindowState, TierConfig, RateLimitConfig } from '../../types';\n\nclass TokenBucket {\n private buckets = new Map<string, BucketState>();\n\n tryConsume(ip: string, maxTokens: number, refillRate: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let bucket = this.buckets.get(ip);\n\n if (!bucket) {\n bucket = { tokens: maxTokens, lastRefill: now };\n this.buckets.set(ip, bucket);\n }\n\n const elapsed = (now - bucket.lastRefill) / 1000;\n bucket.tokens = Math.min(maxTokens, bucket.tokens + elapsed * refillRate);\n bucket.lastRefill = now;\n\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1;\n const resetMs = bucket.tokens <= 0 ? Math.ceil((1 / refillRate) * 1000) : 0;\n return { allowed: true, remaining: Math.floor(bucket.tokens), resetMs };\n }\n\n const resetMs = Math.ceil(((1 - bucket.tokens) / refillRate) * 1000);\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nclass SlidingWindowLog {\n private windows = new Map<string, SlidingWindowState>();\n\n tryConsume(ip: string, maxRequests: number, windowMs: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let state = this.windows.get(ip);\n\n if (!state) {\n state = { timestamps: [] };\n this.windows.set(ip, state);\n }\n\n state.timestamps = state.timestamps.filter(t => now - t < windowMs);\n\n if (state.timestamps.length < maxRequests) {\n state.timestamps.push(now);\n return {\n allowed: true,\n remaining: maxRequests - state.timestamps.length,\n resetMs: state.timestamps.length > 0 ? windowMs - (now - state.timestamps[0]) : windowMs,\n };\n }\n\n const oldestInWindow = state.timestamps[0];\n const resetMs = windowMs - (now - oldestInWindow);\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nclass FixedWindowCounter {\n private windows = new Map<string, FixedWindowState>();\n\n tryConsume(ip: string, maxRequests: number, windowMs: number): { allowed: boolean; remaining: number; resetMs: number } {\n const now = Date.now();\n let state = this.windows.get(ip);\n\n if (!state || now - state.windowStart >= windowMs) {\n state = { count: 0, windowStart: now };\n this.windows.set(ip, state);\n }\n\n const resetMs = windowMs - (now - state.windowStart);\n\n if (state.count < maxRequests) {\n state.count++;\n return { allowed: true, remaining: maxRequests - state.count, resetMs };\n }\n\n return { allowed: false, remaining: 0, resetMs };\n }\n}\n\nexport class RateLimiterService {\n private tokenBucket = new TokenBucket();\n private slidingWindow = new SlidingWindowLog();\n private fixedWindow = new FixedWindowCounter();\n private globalWindow = new FixedWindowCounter();\n private config: RateLimitConfig;\n private _rateLimitHits = 0;\n\n constructor(config: RateLimitConfig) {\n this.config = config;\n }\n\n get rateLimitHits(): number {\n return this._rateLimitHits;\n }\n\n checkLimit(ip: string, tier: string): { allowed: boolean; remaining: number; resetMs: number; limit: number } {\n const globalResult = this.globalWindow.tryConsume(\n '__global__',\n this.config.globalLimit.maxRequests,\n this.config.globalLimit.windowMs\n );\n\n if (!globalResult.allowed) {\n this._rateLimitHits++;\n return { allowed: false, remaining: 0, resetMs: globalResult.resetMs, limit: this.config.globalLimit.maxRequests };\n }\n\n const tierConfig = this.config.tiers[tier] || this.config.tiers[this.config.defaultTier];\n\n if (!tierConfig || tierConfig.algorithm === 'none') {\n return { allowed: true, remaining: -1, resetMs: 0, limit: -1 };\n }\n\n let result: { allowed: boolean; remaining: number; resetMs: number };\n\n switch (tierConfig.algorithm) {\n case 'tokenBucket':\n result = this.tokenBucket.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.refillRate || 1\n );\n break;\n case 'slidingWindow':\n result = this.slidingWindow.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.windowMs!\n );\n break;\n case 'fixedWindow':\n result = this.fixedWindow.tryConsume(\n ip,\n tierConfig.maxRequests!,\n tierConfig.windowMs!\n );\n break;\n default:\n return { allowed: true, remaining: -1, resetMs: 0, limit: -1 };\n }\n\n if (!result.allowed) {\n this._rateLimitHits++;\n }\n\n return { ...result, limit: tierConfig.maxRequests! };\n }\n\n getConfig(): RateLimitConfig {\n return this.config;\n }\n}\n","import type { RequestLog, GatewayAnalytics } from '../../types';\n\nconst MAX_LOG_SIZE = 10000;\nconst ACTIVE_WINDOW_MS = 300000; // 5 minutes\n\nexport class AnalyticsService {\n private logs: RequestLog[] = [];\n private head = 0;\n private count = 0;\n\n addLog(log: RequestLog): void {\n if (this.count < MAX_LOG_SIZE) {\n this.logs.push(log);\n this.count++;\n } else {\n this.logs[this.head] = log;\n this.head = (this.head + 1) % MAX_LOG_SIZE;\n }\n }\n\n getRecentLogs(limit = 20, offset = 0): RequestLog[] {\n const ordered = this.getOrderedLogs();\n return ordered.slice(offset, offset + limit);\n }\n\n getAnalytics(rateLimitHits: number): GatewayAnalytics {\n const now = Date.now();\n const oneMinuteAgo = now - 60000;\n const activeWindowStart = now - ACTIVE_WINDOW_MS;\n const ordered = this.getOrderedLogs();\n\n const recentLogs = ordered.filter(l => l.timestamp > oneMinuteAgo);\n const requestsPerMinute = recentLogs.length;\n\n // Top endpoints\n const endpointCounts = new Map<string, number>();\n for (const log of ordered) {\n const current = endpointCounts.get(log.path) || 0;\n endpointCounts.set(log.path, current + 1);\n }\n const topEndpoints = Array.from(endpointCounts.entries())\n .map(([path, count]) => ({ path, count }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 5);\n\n // Error rate\n const errorCount = ordered.filter(l => l.statusCode >= 400).length;\n const errorRate = this.count > 0 ? (errorCount / this.count) * 100 : 0;\n\n // Average response time\n const totalResponseTime = ordered.reduce((sum, l) => sum + l.responseTime, 0);\n const avgResponseTime = this.count > 0 ? totalResponseTime / this.count : 0;\n\n // Active clients: unique IPs in last 5 minutes\n const activeLogs = ordered.filter(l => l.timestamp > activeWindowStart);\n const uniqueIps = new Set(activeLogs.map(l => l.ip));\n\n // Active key uses: unique (IP + apiKey) pairs in last 5 minutes\n const keyUsePairs = new Set<string>();\n for (const log of activeLogs) {\n if (log.apiKey) {\n keyUsePairs.add(`${log.ip}::${log.apiKey}`);\n }\n }\n\n return {\n totalRequests: this.count,\n requestsPerMinute,\n topEndpoints,\n errorRate: Math.round(errorRate * 100) / 100,\n avgResponseTime: Math.round(avgResponseTime * 100) / 100,\n activeClients: uniqueIps.size,\n activeKeyUses: keyUsePairs.size,\n rateLimitHits,\n };\n }\n\n private getOrderedLogs(): RequestLog[] {\n if (this.count < MAX_LOG_SIZE) {\n return [...this.logs].reverse();\n }\n const tail = this.logs.slice(0, this.head);\n const headPart = this.logs.slice(this.head);\n return [...headPart, ...tail].reverse();\n }\n}\n","import fs from 'fs';\nimport path from 'path';\nimport { generateSecret } from '../utils/totp';\nimport type { DeviceEntry, DevicesFile } from '../../types';\n\nconst ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;\nconst CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour\nconst DEBOUNCE_WRITE_MS = 2000;\nconst MAX_REGISTRATIONS_PER_IP_PER_MIN = 10;\nconst MAX_REGISTRATIONS_PER_IP_TOTAL = 30;\nconst RATE_WINDOW_MS = 60 * 1000; // 1 minute\n\nexport class DeviceRegistryService {\n private devices: Map<string, DeviceEntry> = new Map();\n private filePath: string;\n private writeTimeout: ReturnType<typeof setTimeout> | null = null;\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n // In-memory rate tracking: ip -> timestamps of recent registration attempts\n private registrationAttempts: Map<string, number[]> = new Map();\n\n constructor(filePath: string) {\n this.filePath = filePath;\n this.loadFromDisk();\n this.cleanupExpired();\n this.cleanupInterval = setInterval(() => this.cleanupExpired(), CLEANUP_INTERVAL_MS);\n }\n\n private loadFromDisk(): void {\n try {\n if (fs.existsSync(this.filePath)) {\n const raw = fs.readFileSync(this.filePath, 'utf-8');\n const data: DevicesFile = JSON.parse(raw);\n for (const device of data.devices) {\n this.devices.set(device.browserId, device);\n }\n console.log(`[DeviceRegistry] Loaded ${this.devices.size} devices from ${this.filePath}`);\n } else {\n // Create the directory and empty file\n const dir = path.dirname(this.filePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n this.saveToDiskSync();\n console.log(`[DeviceRegistry] Created new devices file at ${this.filePath}`);\n }\n } catch (err) {\n console.error('[DeviceRegistry] Failed to load devices file:', err);\n }\n }\n\n private saveToDiskSync(): void {\n const data: DevicesFile = {\n devices: Array.from(this.devices.values()),\n };\n fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');\n }\n\n private debouncedSave(): void {\n if (this.writeTimeout) clearTimeout(this.writeTimeout);\n this.writeTimeout = setTimeout(() => {\n this.saveToDiskSync();\n }, DEBOUNCE_WRITE_MS);\n }\n\n private cleanupExpired(): void {\n const now = Date.now();\n let removed = 0;\n for (const [id, device] of this.devices) {\n if (new Date(device.expiresAt).getTime() <= now) {\n this.devices.delete(id);\n removed++;\n }\n }\n if (removed > 0) {\n console.log(`[DeviceRegistry] Cleaned up ${removed} expired devices`);\n this.debouncedSave();\n }\n }\n\n private getActiveCountByIp(ip: string): number {\n const now = Date.now();\n let count = 0;\n for (const device of this.devices.values()) {\n if (device.ip === ip && device.active && new Date(device.expiresAt).getTime() > now) {\n count++;\n }\n }\n return count;\n }\n\n private checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const attempts = this.registrationAttempts.get(ip) || [];\n // Remove attempts older than the rate window\n const recent = attempts.filter(t => now - t < RATE_WINDOW_MS);\n this.registrationAttempts.set(ip, recent);\n return recent.length < MAX_REGISTRATIONS_PER_IP_PER_MIN;\n }\n\n private recordAttempt(ip: string): void {\n const attempts = this.registrationAttempts.get(ip) || [];\n attempts.push(Date.now());\n this.registrationAttempts.set(ip, attempts);\n }\n\n registerDevice(\n browserId: string,\n ip: string,\n userAgent: string\n ): { success: true; device: DeviceEntry } | { success: false; error: string; status: number } {\n // Check rate limit: 10 per IP per minute\n if (!this.checkRateLimit(ip)) {\n return {\n success: false,\n error: 'Registration rate limit exceeded. Max 10 per minute per IP.',\n status: 429,\n };\n }\n\n this.recordAttempt(ip);\n\n // Check total cap: 30 active devices per IP\n if (this.getActiveCountByIp(ip) >= MAX_REGISTRATIONS_PER_IP_TOTAL) {\n return {\n success: false,\n error: 'Maximum device registrations reached for this IP. Max 30 per IP.',\n status: 403,\n };\n }\n\n // Check if this browserId is already registered and still valid\n const existing = this.devices.get(browserId);\n if (existing && existing.active && new Date(existing.expiresAt).getTime() > Date.now()) {\n // Re-registration: return existing secret, refresh expiry\n existing.expiresAt = new Date(Date.now() + ONE_WEEK_MS).toISOString();\n existing.lastSeen = new Date().toISOString();\n existing.lastIp = ip;\n this.debouncedSave();\n return { success: true, device: existing };\n }\n\n const now = new Date();\n const device: DeviceEntry = {\n browserId,\n sharedSecret: generateSecret(),\n ip,\n userAgent,\n registeredAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + ONE_WEEK_MS).toISOString(),\n lastSeen: now.toISOString(),\n lastIp: ip,\n active: true,\n };\n\n this.devices.set(browserId, device);\n this.debouncedSave();\n return { success: true, device };\n }\n\n getDevice(browserId: string): DeviceEntry | null {\n const device = this.devices.get(browserId) || null;\n if (!device) return null;\n\n // Check expiry\n if (new Date(device.expiresAt).getTime() <= Date.now()) {\n this.devices.delete(browserId);\n this.debouncedSave();\n return null;\n }\n\n if (!device.active) return null;\n return device;\n }\n\n updateLastSeen(browserId: string, ip: string): void {\n const device = this.devices.get(browserId);\n if (!device) return;\n device.lastSeen = new Date().toISOString();\n if (device.lastIp !== ip) {\n console.log(`[DeviceRegistry] IP change for ${browserId}: ${device.lastIp} -> ${ip}`);\n device.lastIp = ip;\n }\n this.debouncedSave();\n }\n\n revokeDevice(browserId: string): boolean {\n const device = this.devices.get(browserId);\n if (!device) return false;\n device.active = false;\n this.debouncedSave();\n return true;\n }\n\n getStats(): { total: number; active: number; expired: number } {\n const now = Date.now();\n let active = 0;\n let expired = 0;\n for (const device of this.devices.values()) {\n if (!device.active || new Date(device.expiresAt).getTime() <= now) {\n expired++;\n } else {\n active++;\n }\n }\n return { total: this.devices.size, active, expired };\n }\n\n destroy(): void {\n if (this.cleanupInterval) clearInterval(this.cleanupInterval);\n if (this.writeTimeout) {\n clearTimeout(this.writeTimeout);\n this.saveToDiskSync(); // Final save\n }\n }\n}\n","import crypto from 'crypto';\n\nconst TIME_WINDOW_MS = 3600 * 1000; // 1 hour\nconst CODE_LENGTH = 16; // truncated HMAC hex chars\n\nexport function generateTOTP(browserId: string, secret: string, timeOffset: number = 0): string {\n const timeWindow = Math.floor(Date.now() / TIME_WINDOW_MS) + timeOffset;\n const message = `${browserId}:${timeWindow}`;\n const hmac = crypto.createHmac('sha256', secret).update(message).digest('hex');\n return hmac.substring(0, CODE_LENGTH);\n}\n\nexport function validateTOTP(browserId: string, secret: string, providedCode: string): boolean {\n const currentCode = generateTOTP(browserId, secret, 0);\n const previousCode = generateTOTP(browserId, secret, -1);\n return timingSafeEqual(providedCode, currentCode) || timingSafeEqual(providedCode, previousCode);\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n const bufA = Buffer.from(a);\n const bufB = Buffer.from(b);\n return crypto.timingSafeEqual(bufA, bufB);\n}\n\nexport function formatKey(browserId: string, code: string): string {\n return `totp_${browserId}_${code}`;\n}\n\nexport function parseKey(key: string): { browserId: string; code: string } | null {\n if (!key.startsWith('totp_')) return null;\n const parts = key.slice(5).split('_');\n // UUID has 4 dashes, so browserId has 5 parts when split by _\n // Format: totp_<uuid>_<code> where uuid = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n // But UUID uses dashes, not underscores, so it's a single segment\n if (parts.length < 2) return null;\n const code = parts[parts.length - 1];\n const browserId = parts.slice(0, -1).join('_');\n if (!browserId || !code) return null;\n return { browserId, code };\n}\n\nexport function generateSecret(): string {\n return crypto.randomBytes(32).toString('hex');\n}\n","import type { RateLimitConfig, IpRules, ApiKeysConfig } from '../types';\n\nexport const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {\n tiers: {\n free: { algorithm: 'tokenBucket', maxRequests: 100, windowMs: 60000, refillRate: 10 },\n pro: { algorithm: 'slidingWindow', maxRequests: 1000, windowMs: 60000 },\n unlimited: { algorithm: 'none' },\n },\n defaultTier: 'free',\n globalLimit: { maxRequests: 10000, windowMs: 60000 },\n};\n\nexport const DEFAULT_IP_RULES: IpRules = {\n allowlist: [],\n blocklist: [],\n mode: 'blocklist',\n};\n\nexport const DEFAULT_API_KEYS: ApiKeysConfig = {\n keys: [],\n};\n","import { Request, Response, NextFunction } from 'express';\nimport type { ApiKeysConfig } from '../../types';\nimport { parseKey, validateTOTP } from '../utils/totp';\nimport { DeviceRegistryService } from '../services/DeviceRegistryService';\n\nexport function createApiKeyAuth(\n getKeys: () => ApiKeysConfig,\n deviceRegistry?: DeviceRegistryService\n) {\n return function apiKeyAuth(req: Request, res: Response, next: NextFunction): void {\n const apiKey = req.header('X-API-Key') || req.query.apiKey as string;\n\n if (!apiKey) {\n (req as any).clientId = req.ip || 'unknown';\n (req as any).tier = 'free';\n next();\n return;\n }\n\n // TOTP key: totp_<browserId>_<code>\n if (apiKey.startsWith('totp_') && deviceRegistry) {\n const parsed = parseKey(apiKey);\n if (!parsed) {\n res.status(401).json({ error: 'Malformed TOTP key' });\n return;\n }\n\n const device = deviceRegistry.getDevice(parsed.browserId);\n if (!device) {\n res.status(401).json({ error: 'Device not registered or expired' });\n return;\n }\n\n if (!validateTOTP(parsed.browserId, device.sharedSecret, parsed.code)) {\n res.status(401).json({ error: 'Invalid or expired TOTP code' });\n return;\n }\n\n // Valid TOTP - update tracking\n const ip = req.ip || req.socket.remoteAddress || 'unknown';\n deviceRegistry.updateLastSeen(parsed.browserId, ip);\n\n (req as any).clientId = parsed.browserId;\n (req as any).tier = 'free';\n (req as any).apiKeyValue = apiKey;\n next();\n return;\n }\n\n // Static key: gw_live_*\n const config = getKeys();\n const keyEntry = config.keys.find(k => k.key === apiKey && k.active);\n\n if (!keyEntry) {\n res.status(401).json({ error: 'Invalid or revoked API key' });\n return;\n }\n\n (req as any).clientId = keyEntry.id;\n (req as any).tier = keyEntry.tier;\n (req as any).apiKeyValue = keyEntry.key;\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport type { IpRules } from '../../types';\n\nexport function createIpFilter(getRules: () => IpRules) {\n return function ipFilter(req: Request, res: Response, next: NextFunction): void {\n const rules = getRules();\n const clientIp = req.ip || req.socket.remoteAddress || 'unknown';\n\n if (rules.mode === 'allowlist' && rules.allowlist.length > 0) {\n if (!rules.allowlist.includes(clientIp)) {\n res.status(403).json({ error: 'IP not in allowlist' });\n return;\n }\n }\n\n if (rules.mode === 'blocklist' && rules.blocklist.length > 0) {\n if (rules.blocklist.includes(clientIp)) {\n res.status(403).json({ error: 'IP is blocked' });\n return;\n }\n }\n\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\n\nexport function createRateLimiter(service: RateLimiterService) {\n return function rateLimiter(req: Request, res: Response, next: NextFunction): void {\n const ip = req.ip || req.socket.remoteAddress || 'unknown';\n const tier = (req as any).tier || 'free';\n\n const result = service.checkLimit(ip, tier);\n\n if (result.limit > 0) {\n res.setHeader('X-RateLimit-Limit', result.limit);\n res.setHeader('X-RateLimit-Remaining', Math.max(0, result.remaining));\n res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetMs / 1000));\n }\n\n if (!result.allowed) {\n res.status(429).json({\n error: 'Rate limit exceeded',\n retryAfter: Math.ceil(result.resetMs / 1000),\n });\n return;\n }\n\n next();\n };\n}\n","import { Request, Response, NextFunction } from 'express';\nimport { AnalyticsService } from '../services/AnalyticsService';\n\nexport function createRequestLogger(analytics: AnalyticsService) {\n return function requestLogger(req: Request, res: Response, next: NextFunction): void {\n const start = Date.now();\n\n res.on('finish', () => {\n const responseTime = Date.now() - start;\n const apiKeyValue = (req as any).apiKeyValue || undefined;\n analytics.addLog({\n timestamp: Date.now(),\n method: req.method,\n path: req.originalUrl,\n statusCode: res.statusCode,\n responseTime,\n clientId: (req as any).clientId || req.ip || 'unknown',\n ip: req.ip || req.socket.remoteAddress || 'unknown',\n apiKey: apiKeyValue,\n authenticated: !!apiKeyValue,\n });\n });\n\n next();\n };\n}\n","import { Router, Request, Response } from 'express';\nimport { RateLimiterService } from '../services/RateLimiterService';\nimport { AnalyticsService } from '../services/AnalyticsService';\nimport type { GatewayMiddlewareConfig } from '../../types';\nimport crypto from 'crypto';\n\nexport interface GatewayRoutesOptions {\n rateLimiterService: RateLimiterService;\n analyticsService: AnalyticsService;\n config: Required<GatewayMiddlewareConfig>;\n}\n\nexport function createGatewayRoutes(options: GatewayRoutesOptions): Router {\n const { rateLimiterService, analyticsService, config } = options;\n const router = Router();\n\n // GET /analytics - Returns current analytics snapshot\n router.get('/analytics', (_req: Request, res: Response) => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.json(analytics);\n });\n\n // GET /analytics/live - SSE stream pushing analytics every 5 seconds\n router.get('/analytics/live', (_req: Request, res: Response) => {\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n res.flushHeaders();\n\n const send = (): void => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.write(`data: ${JSON.stringify(analytics)}\\n\\n`);\n };\n\n send();\n const interval = setInterval(send, 5000);\n\n _req.on('close', () => {\n clearInterval(interval);\n });\n });\n\n // GET /config - Returns current gateway config\n router.get('/config', (_req: Request, res: Response) => {\n const analytics = analyticsService.getAnalytics(rateLimiterService.rateLimitHits);\n res.json({\n rateLimits: config.rateLimits,\n ipRules: config.ipRules,\n activeKeys: config.apiKeys.keys.filter(k => k.active).length,\n activeKeyUses: analytics.activeKeyUses,\n });\n });\n\n // POST /keys - Create a new API key\n router.post('/keys', (req: Request, res: Response) => {\n const { name, tier } = req.body;\n\n if (!name) {\n res.status(400).json({ error: 'Name is required' });\n return;\n }\n\n const newKey = {\n id: `key_${String(config.apiKeys.keys.length + 1).padStart(3, '0')}`,\n key: `gw_live_${crypto.randomBytes(16).toString('hex')}`,\n name,\n tier: tier || 'free',\n createdAt: new Date().toISOString(),\n active: true,\n };\n\n config.apiKeys.keys.push(newKey);\n res.status(201).json(newKey);\n });\n\n // DELETE /keys/:keyId - Revoke an API key\n router.delete('/keys/:keyId', (req: Request, res: Response) => {\n const { keyId } = req.params;\n const key = config.apiKeys.keys.find(k => k.id === keyId);\n\n if (!key) {\n res.status(404).json({ error: 'API key not found' });\n return;\n }\n\n key.active = false;\n res.json({ message: 'API key revoked', id: keyId });\n });\n\n // GET /logs - Returns recent request logs (paginated)\n router.get('/logs', (req: Request, res: Response) => {\n const limit = parseInt(req.query.limit as string) || 20;\n const offset = parseInt(req.query.offset as string) || 0;\n const logs = analyticsService.getRecentLogs(limit, offset);\n res.json({ logs, limit, offset });\n });\n\n return router;\n}\n","import { Router, Request, Response } from 'express';\nimport { DeviceRegistryService } from '../services/DeviceRegistryService';\n\nexport interface DeviceAuthRoutesOptions {\n deviceRegistry: DeviceRegistryService;\n}\n\nexport function createDeviceAuthRoutes(options: DeviceAuthRoutesOptions): Router {\n const { deviceRegistry } = options;\n const router = Router();\n\n // POST /register - Register a browser device\n router.post('/register', (req: Request, res: Response) => {\n const { browserId } = req.body;\n\n if (!browserId || typeof browserId !== 'string') {\n res.status(400).json({ error: 'browserId is required' });\n return;\n }\n\n // Basic UUID format validation\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (!uuidRegex.test(browserId)) {\n res.status(400).json({ error: 'browserId must be a valid UUID' });\n return;\n }\n\n const ip = req.ip || req.socket.remoteAddress || 'unknown';\n const userAgent = req.headers['user-agent'] || 'unknown';\n\n const result = deviceRegistry.registerDevice(browserId, ip, userAgent);\n\n if (!result.success) {\n res.status(result.status).json({ error: result.error });\n return;\n }\n\n res.status(201).json({\n browserId: result.device.browserId,\n sharedSecret: result.device.sharedSecret,\n expiresAt: result.device.expiresAt,\n });\n });\n\n // GET /status/:browserId - Check device registration status\n router.get('/status/:browserId', (req: Request, res: Response) => {\n const browserId = req.params.browserId as string;\n const device = deviceRegistry.getDevice(browserId);\n\n if (!device) {\n res.status(404).json({ registered: false });\n return;\n }\n\n res.json({\n registered: true,\n browserId: device.browserId,\n expiresAt: device.expiresAt,\n registeredAt: device.registeredAt,\n });\n });\n\n // DELETE /:browserId - Revoke a device (admin)\n router.delete('/:browserId', (req: Request, res: Response) => {\n const browserId = req.params.browserId as string;\n const revoked = deviceRegistry.revokeDevice(browserId);\n\n if (!revoked) {\n res.status(404).json({ error: 'Device not found' });\n return;\n }\n\n res.json({ message: 'Device revoked', browserId });\n });\n\n // GET /stats - Device registry stats\n router.get('/stats', (_req: Request, res: Response) => {\n res.json(deviceRegistry.getStats());\n });\n\n return router;\n}\n"],"mappings":"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","names":["resetMs","path","Router","crypto","Router"]}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
- export { AnalyticsService, GatewayInstances, GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger } from './backend/index.mjs';
1
+ export { AnalyticsService, DeviceAuthRoutesOptions, DeviceRegistryService, GatewayInstances, GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createDeviceAuthRoutes, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger, formatKey, generateSecret, generateTOTP, parseKey, validateTOTP } from './backend/index.mjs';
2
2
  export { GatewayDashboard, GatewayDashboardProps } from './frontend/index.mjs';
3
3
  import { ApiKeysConfig, IpRules, RateLimitConfig } from './types/index.mjs';
4
- export { ApiKey, BucketState, FixedWindowState, GatewayAnalytics, GatewayConfig, GatewayMiddlewareConfig, RequestLog, SlidingWindowState, TierConfig } from './types/index.mjs';
4
+ export { ApiKey, BucketState, DeviceEntry, DevicesFile, FixedWindowState, GatewayAnalytics, GatewayConfig, GatewayMiddlewareConfig, RequestLog, SlidingWindowState, TierConfig } from './types/index.mjs';
5
5
  import 'express';
6
6
  import 'react/jsx-runtime';
7
7
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { AnalyticsService, GatewayInstances, GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger } from './backend/index.js';
1
+ export { AnalyticsService, DeviceAuthRoutesOptions, DeviceRegistryService, GatewayInstances, GatewayRoutesOptions, RateLimiterService, createApiKeyAuth, createDeviceAuthRoutes, createGatewayMiddleware, createGatewayRoutes, createIpFilter, createRateLimiter, createRequestLogger, formatKey, generateSecret, generateTOTP, parseKey, validateTOTP } from './backend/index.js';
2
2
  export { GatewayDashboard, GatewayDashboardProps } from './frontend/index.js';
3
3
  import { ApiKeysConfig, IpRules, RateLimitConfig } from './types/index.js';
4
- export { ApiKey, BucketState, FixedWindowState, GatewayAnalytics, GatewayConfig, GatewayMiddlewareConfig, RequestLog, SlidingWindowState, TierConfig } from './types/index.js';
4
+ export { ApiKey, BucketState, DeviceEntry, DevicesFile, FixedWindowState, GatewayAnalytics, GatewayConfig, GatewayMiddlewareConfig, RequestLog, SlidingWindowState, TierConfig } from './types/index.js';
5
5
  import 'express';
6
6
  import 'react/jsx-runtime';
7
7
 
package/dist/index.js CHANGED
@@ -34,14 +34,21 @@ __export(src_exports, {
34
34
  DEFAULT_API_KEYS: () => DEFAULT_API_KEYS,
35
35
  DEFAULT_IP_RULES: () => DEFAULT_IP_RULES,
36
36
  DEFAULT_RATE_LIMIT_CONFIG: () => DEFAULT_RATE_LIMIT_CONFIG,
37
+ DeviceRegistryService: () => DeviceRegistryService,
37
38
  GatewayDashboard: () => GatewayDashboard,
38
39
  RateLimiterService: () => RateLimiterService,
39
40
  createApiKeyAuth: () => createApiKeyAuth,
41
+ createDeviceAuthRoutes: () => createDeviceAuthRoutes,
40
42
  createGatewayMiddleware: () => createGatewayMiddleware,
41
43
  createGatewayRoutes: () => createGatewayRoutes,
42
44
  createIpFilter: () => createIpFilter,
43
45
  createRateLimiter: () => createRateLimiter,
44
- createRequestLogger: () => createRequestLogger
46
+ createRequestLogger: () => createRequestLogger,
47
+ formatKey: () => formatKey,
48
+ generateSecret: () => generateSecret,
49
+ generateTOTP: () => generateTOTP,
50
+ parseKey: () => parseKey,
51
+ validateTOTP: () => validateTOTP
45
52
  });
46
53
  module.exports = __toCommonJS(src_exports);
47
54
 
@@ -205,7 +212,7 @@ var AnalyticsService = class {
205
212
  const current = endpointCounts.get(log.path) || 0;
206
213
  endpointCounts.set(log.path, current + 1);
207
214
  }
208
- const topEndpoints = Array.from(endpointCounts.entries()).map(([path, count]) => ({ path, count })).sort((a, b) => b.count - a.count).slice(0, 5);
215
+ const topEndpoints = Array.from(endpointCounts.entries()).map(([path2, count]) => ({ path: path2, count })).sort((a, b) => b.count - a.count).slice(0, 5);
209
216
  const errorCount = ordered.filter((l) => l.statusCode >= 400).length;
210
217
  const errorRate = this.count > 0 ? errorCount / this.count * 100 : 0;
211
218
  const totalResponseTime = ordered.reduce((sum, l) => sum + l.responseTime, 0);
@@ -239,6 +246,226 @@ var AnalyticsService = class {
239
246
  }
240
247
  };
241
248
 
249
+ // src/backend/services/DeviceRegistryService.ts
250
+ var import_fs = __toESM(require("fs"));
251
+ var import_path = __toESM(require("path"));
252
+
253
+ // src/backend/utils/totp.ts
254
+ var import_crypto = __toESM(require("crypto"));
255
+ var TIME_WINDOW_MS = 3600 * 1e3;
256
+ var CODE_LENGTH = 16;
257
+ function generateTOTP(browserId, secret, timeOffset = 0) {
258
+ const timeWindow = Math.floor(Date.now() / TIME_WINDOW_MS) + timeOffset;
259
+ const message = `${browserId}:${timeWindow}`;
260
+ const hmac = import_crypto.default.createHmac("sha256", secret).update(message).digest("hex");
261
+ return hmac.substring(0, CODE_LENGTH);
262
+ }
263
+ function validateTOTP(browserId, secret, providedCode) {
264
+ const currentCode = generateTOTP(browserId, secret, 0);
265
+ const previousCode = generateTOTP(browserId, secret, -1);
266
+ return timingSafeEqual(providedCode, currentCode) || timingSafeEqual(providedCode, previousCode);
267
+ }
268
+ function timingSafeEqual(a, b) {
269
+ if (a.length !== b.length) return false;
270
+ const bufA = Buffer.from(a);
271
+ const bufB = Buffer.from(b);
272
+ return import_crypto.default.timingSafeEqual(bufA, bufB);
273
+ }
274
+ function formatKey(browserId, code) {
275
+ return `totp_${browserId}_${code}`;
276
+ }
277
+ function parseKey(key) {
278
+ if (!key.startsWith("totp_")) return null;
279
+ const parts = key.slice(5).split("_");
280
+ if (parts.length < 2) return null;
281
+ const code = parts[parts.length - 1];
282
+ const browserId = parts.slice(0, -1).join("_");
283
+ if (!browserId || !code) return null;
284
+ return { browserId, code };
285
+ }
286
+ function generateSecret() {
287
+ return import_crypto.default.randomBytes(32).toString("hex");
288
+ }
289
+
290
+ // src/backend/services/DeviceRegistryService.ts
291
+ var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
292
+ var CLEANUP_INTERVAL_MS = 60 * 60 * 1e3;
293
+ var DEBOUNCE_WRITE_MS = 2e3;
294
+ var MAX_REGISTRATIONS_PER_IP_PER_MIN = 10;
295
+ var MAX_REGISTRATIONS_PER_IP_TOTAL = 30;
296
+ var RATE_WINDOW_MS = 60 * 1e3;
297
+ var DeviceRegistryService = class {
298
+ devices = /* @__PURE__ */ new Map();
299
+ filePath;
300
+ writeTimeout = null;
301
+ cleanupInterval = null;
302
+ // In-memory rate tracking: ip -> timestamps of recent registration attempts
303
+ registrationAttempts = /* @__PURE__ */ new Map();
304
+ constructor(filePath) {
305
+ this.filePath = filePath;
306
+ this.loadFromDisk();
307
+ this.cleanupExpired();
308
+ this.cleanupInterval = setInterval(() => this.cleanupExpired(), CLEANUP_INTERVAL_MS);
309
+ }
310
+ loadFromDisk() {
311
+ try {
312
+ if (import_fs.default.existsSync(this.filePath)) {
313
+ const raw = import_fs.default.readFileSync(this.filePath, "utf-8");
314
+ const data = JSON.parse(raw);
315
+ for (const device of data.devices) {
316
+ this.devices.set(device.browserId, device);
317
+ }
318
+ console.log(`[DeviceRegistry] Loaded ${this.devices.size} devices from ${this.filePath}`);
319
+ } else {
320
+ const dir = import_path.default.dirname(this.filePath);
321
+ if (!import_fs.default.existsSync(dir)) {
322
+ import_fs.default.mkdirSync(dir, { recursive: true });
323
+ }
324
+ this.saveToDiskSync();
325
+ console.log(`[DeviceRegistry] Created new devices file at ${this.filePath}`);
326
+ }
327
+ } catch (err) {
328
+ console.error("[DeviceRegistry] Failed to load devices file:", err);
329
+ }
330
+ }
331
+ saveToDiskSync() {
332
+ const data = {
333
+ devices: Array.from(this.devices.values())
334
+ };
335
+ import_fs.default.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
336
+ }
337
+ debouncedSave() {
338
+ if (this.writeTimeout) clearTimeout(this.writeTimeout);
339
+ this.writeTimeout = setTimeout(() => {
340
+ this.saveToDiskSync();
341
+ }, DEBOUNCE_WRITE_MS);
342
+ }
343
+ cleanupExpired() {
344
+ const now = Date.now();
345
+ let removed = 0;
346
+ for (const [id, device] of this.devices) {
347
+ if (new Date(device.expiresAt).getTime() <= now) {
348
+ this.devices.delete(id);
349
+ removed++;
350
+ }
351
+ }
352
+ if (removed > 0) {
353
+ console.log(`[DeviceRegistry] Cleaned up ${removed} expired devices`);
354
+ this.debouncedSave();
355
+ }
356
+ }
357
+ getActiveCountByIp(ip) {
358
+ const now = Date.now();
359
+ let count = 0;
360
+ for (const device of this.devices.values()) {
361
+ if (device.ip === ip && device.active && new Date(device.expiresAt).getTime() > now) {
362
+ count++;
363
+ }
364
+ }
365
+ return count;
366
+ }
367
+ checkRateLimit(ip) {
368
+ const now = Date.now();
369
+ const attempts = this.registrationAttempts.get(ip) || [];
370
+ const recent = attempts.filter((t) => now - t < RATE_WINDOW_MS);
371
+ this.registrationAttempts.set(ip, recent);
372
+ return recent.length < MAX_REGISTRATIONS_PER_IP_PER_MIN;
373
+ }
374
+ recordAttempt(ip) {
375
+ const attempts = this.registrationAttempts.get(ip) || [];
376
+ attempts.push(Date.now());
377
+ this.registrationAttempts.set(ip, attempts);
378
+ }
379
+ registerDevice(browserId, ip, userAgent) {
380
+ if (!this.checkRateLimit(ip)) {
381
+ return {
382
+ success: false,
383
+ error: "Registration rate limit exceeded. Max 10 per minute per IP.",
384
+ status: 429
385
+ };
386
+ }
387
+ this.recordAttempt(ip);
388
+ if (this.getActiveCountByIp(ip) >= MAX_REGISTRATIONS_PER_IP_TOTAL) {
389
+ return {
390
+ success: false,
391
+ error: "Maximum device registrations reached for this IP. Max 30 per IP.",
392
+ status: 403
393
+ };
394
+ }
395
+ const existing = this.devices.get(browserId);
396
+ if (existing && existing.active && new Date(existing.expiresAt).getTime() > Date.now()) {
397
+ existing.expiresAt = new Date(Date.now() + ONE_WEEK_MS).toISOString();
398
+ existing.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
399
+ existing.lastIp = ip;
400
+ this.debouncedSave();
401
+ return { success: true, device: existing };
402
+ }
403
+ const now = /* @__PURE__ */ new Date();
404
+ const device = {
405
+ browserId,
406
+ sharedSecret: generateSecret(),
407
+ ip,
408
+ userAgent,
409
+ registeredAt: now.toISOString(),
410
+ expiresAt: new Date(now.getTime() + ONE_WEEK_MS).toISOString(),
411
+ lastSeen: now.toISOString(),
412
+ lastIp: ip,
413
+ active: true
414
+ };
415
+ this.devices.set(browserId, device);
416
+ this.debouncedSave();
417
+ return { success: true, device };
418
+ }
419
+ getDevice(browserId) {
420
+ const device = this.devices.get(browserId) || null;
421
+ if (!device) return null;
422
+ if (new Date(device.expiresAt).getTime() <= Date.now()) {
423
+ this.devices.delete(browserId);
424
+ this.debouncedSave();
425
+ return null;
426
+ }
427
+ if (!device.active) return null;
428
+ return device;
429
+ }
430
+ updateLastSeen(browserId, ip) {
431
+ const device = this.devices.get(browserId);
432
+ if (!device) return;
433
+ device.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
434
+ if (device.lastIp !== ip) {
435
+ console.log(`[DeviceRegistry] IP change for ${browserId}: ${device.lastIp} -> ${ip}`);
436
+ device.lastIp = ip;
437
+ }
438
+ this.debouncedSave();
439
+ }
440
+ revokeDevice(browserId) {
441
+ const device = this.devices.get(browserId);
442
+ if (!device) return false;
443
+ device.active = false;
444
+ this.debouncedSave();
445
+ return true;
446
+ }
447
+ getStats() {
448
+ const now = Date.now();
449
+ let active = 0;
450
+ let expired = 0;
451
+ for (const device of this.devices.values()) {
452
+ if (!device.active || new Date(device.expiresAt).getTime() <= now) {
453
+ expired++;
454
+ } else {
455
+ active++;
456
+ }
457
+ }
458
+ return { total: this.devices.size, active, expired };
459
+ }
460
+ destroy() {
461
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
462
+ if (this.writeTimeout) {
463
+ clearTimeout(this.writeTimeout);
464
+ this.saveToDiskSync();
465
+ }
466
+ }
467
+ };
468
+
242
469
  // src/config/defaults.ts
243
470
  var DEFAULT_RATE_LIMIT_CONFIG = {
244
471
  tiers: {
@@ -259,7 +486,7 @@ var DEFAULT_API_KEYS = {
259
486
  };
260
487
 
261
488
  // src/backend/middleware/apiKeyAuth.ts
262
- function createApiKeyAuth(getKeys) {
489
+ function createApiKeyAuth(getKeys, deviceRegistry) {
263
490
  return function apiKeyAuth(req, res, next) {
264
491
  const apiKey = req.header("X-API-Key") || req.query.apiKey;
265
492
  if (!apiKey) {
@@ -268,6 +495,29 @@ function createApiKeyAuth(getKeys) {
268
495
  next();
269
496
  return;
270
497
  }
498
+ if (apiKey.startsWith("totp_") && deviceRegistry) {
499
+ const parsed = parseKey(apiKey);
500
+ if (!parsed) {
501
+ res.status(401).json({ error: "Malformed TOTP key" });
502
+ return;
503
+ }
504
+ const device = deviceRegistry.getDevice(parsed.browserId);
505
+ if (!device) {
506
+ res.status(401).json({ error: "Device not registered or expired" });
507
+ return;
508
+ }
509
+ if (!validateTOTP(parsed.browserId, device.sharedSecret, parsed.code)) {
510
+ res.status(401).json({ error: "Invalid or expired TOTP code" });
511
+ return;
512
+ }
513
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
514
+ deviceRegistry.updateLastSeen(parsed.browserId, ip);
515
+ req.clientId = parsed.browserId;
516
+ req.tier = "free";
517
+ req.apiKeyValue = apiKey;
518
+ next();
519
+ return;
520
+ }
271
521
  const config = getKeys();
272
522
  const keyEntry = config.keys.find((k) => k.key === apiKey && k.active);
273
523
  if (!keyEntry) {
@@ -352,18 +602,24 @@ function createGatewayMiddleware(userConfig) {
352
602
  const config = {
353
603
  rateLimits: userConfig?.rateLimits ?? DEFAULT_RATE_LIMIT_CONFIG,
354
604
  ipRules: userConfig?.ipRules ?? DEFAULT_IP_RULES,
355
- apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS
605
+ apiKeys: userConfig?.apiKeys ?? DEFAULT_API_KEYS,
606
+ deviceRegistryPath: userConfig?.deviceRegistryPath ?? ""
356
607
  };
357
608
  const rateLimiterService = new RateLimiterService(config.rateLimits);
358
609
  const analyticsService = new AnalyticsService();
610
+ let deviceRegistry;
611
+ if (config.deviceRegistryPath) {
612
+ deviceRegistry = new DeviceRegistryService(config.deviceRegistryPath);
613
+ }
359
614
  const router = (0, import_express.Router)();
360
615
  router.use(createRequestLogger(analyticsService));
361
- router.use(createApiKeyAuth(() => config.apiKeys));
616
+ router.use(createApiKeyAuth(() => config.apiKeys, deviceRegistry));
362
617
  router.use(createIpFilter(() => config.ipRules));
363
618
  router.use(createRateLimiter(rateLimiterService));
364
619
  return {
365
620
  rateLimiterService,
366
621
  analyticsService,
622
+ deviceRegistry,
367
623
  middleware: router,
368
624
  config
369
625
  };
@@ -371,7 +627,7 @@ function createGatewayMiddleware(userConfig) {
371
627
 
372
628
  // src/backend/routes/gateway.ts
373
629
  var import_express2 = require("express");
374
- var import_crypto = __toESM(require("crypto"));
630
+ var import_crypto2 = __toESM(require("crypto"));
375
631
  function createGatewayRoutes(options) {
376
632
  const { rateLimiterService, analyticsService, config } = options;
377
633
  const router = (0, import_express2.Router)();
@@ -414,7 +670,7 @@ function createGatewayRoutes(options) {
414
670
  }
415
671
  const newKey = {
416
672
  id: `key_${String(config.apiKeys.keys.length + 1).padStart(3, "0")}`,
417
- key: `gw_live_${import_crypto.default.randomBytes(16).toString("hex")}`,
673
+ key: `gw_live_${import_crypto2.default.randomBytes(16).toString("hex")}`,
418
674
  name,
419
675
  tier: tier || "free",
420
676
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -442,18 +698,76 @@ function createGatewayRoutes(options) {
442
698
  return router;
443
699
  }
444
700
 
701
+ // src/backend/routes/deviceAuth.ts
702
+ var import_express3 = require("express");
703
+ function createDeviceAuthRoutes(options) {
704
+ const { deviceRegistry } = options;
705
+ const router = (0, import_express3.Router)();
706
+ router.post("/register", (req, res) => {
707
+ const { browserId } = req.body;
708
+ if (!browserId || typeof browserId !== "string") {
709
+ res.status(400).json({ error: "browserId is required" });
710
+ return;
711
+ }
712
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
713
+ if (!uuidRegex.test(browserId)) {
714
+ res.status(400).json({ error: "browserId must be a valid UUID" });
715
+ return;
716
+ }
717
+ const ip = req.ip || req.socket.remoteAddress || "unknown";
718
+ const userAgent = req.headers["user-agent"] || "unknown";
719
+ const result = deviceRegistry.registerDevice(browserId, ip, userAgent);
720
+ if (!result.success) {
721
+ res.status(result.status).json({ error: result.error });
722
+ return;
723
+ }
724
+ res.status(201).json({
725
+ browserId: result.device.browserId,
726
+ sharedSecret: result.device.sharedSecret,
727
+ expiresAt: result.device.expiresAt
728
+ });
729
+ });
730
+ router.get("/status/:browserId", (req, res) => {
731
+ const browserId = req.params.browserId;
732
+ const device = deviceRegistry.getDevice(browserId);
733
+ if (!device) {
734
+ res.status(404).json({ registered: false });
735
+ return;
736
+ }
737
+ res.json({
738
+ registered: true,
739
+ browserId: device.browserId,
740
+ expiresAt: device.expiresAt,
741
+ registeredAt: device.registeredAt
742
+ });
743
+ });
744
+ router.delete("/:browserId", (req, res) => {
745
+ const browserId = req.params.browserId;
746
+ const revoked = deviceRegistry.revokeDevice(browserId);
747
+ if (!revoked) {
748
+ res.status(404).json({ error: "Device not found" });
749
+ return;
750
+ }
751
+ res.json({ message: "Device revoked", browserId });
752
+ });
753
+ router.get("/stats", (_req, res) => {
754
+ res.json(deviceRegistry.getStats());
755
+ });
756
+ return router;
757
+ }
758
+
445
759
  // src/frontend/GatewayDashboard.tsx
446
760
  var import_react = require("react");
447
761
  var import_recharts = require("recharts");
448
762
  var import_jsx_runtime = require("react/jsx-runtime");
449
- function useGatewayApi(apiBaseUrl, path, apiKey) {
763
+ function useGatewayApi(apiBaseUrl, path2, apiKey) {
450
764
  const [data, setData] = (0, import_react.useState)(null);
451
765
  (0, import_react.useEffect)(() => {
452
766
  const headers = {};
453
767
  if (apiKey) headers["X-API-Key"] = apiKey;
454
- fetch(`${apiBaseUrl}${path}`, { headers }).then((r) => r.json()).then(setData).catch(() => {
768
+ fetch(`${apiBaseUrl}${path2}`, { headers }).then((r) => r.json()).then(setData).catch(() => {
455
769
  });
456
- }, [apiBaseUrl, path, apiKey]);
770
+ }, [apiBaseUrl, path2, apiKey]);
457
771
  return { data };
458
772
  }
459
773
  function GatewayDashboard({ apiBaseUrl, apiKey }) {
@@ -844,13 +1158,20 @@ function GatewayDashboard({ apiBaseUrl, apiKey }) {
844
1158
  DEFAULT_API_KEYS,
845
1159
  DEFAULT_IP_RULES,
846
1160
  DEFAULT_RATE_LIMIT_CONFIG,
1161
+ DeviceRegistryService,
847
1162
  GatewayDashboard,
848
1163
  RateLimiterService,
849
1164
  createApiKeyAuth,
1165
+ createDeviceAuthRoutes,
850
1166
  createGatewayMiddleware,
851
1167
  createGatewayRoutes,
852
1168
  createIpFilter,
853
1169
  createRateLimiter,
854
- createRequestLogger
1170
+ createRequestLogger,
1171
+ formatKey,
1172
+ generateSecret,
1173
+ generateTOTP,
1174
+ parseKey,
1175
+ validateTOTP
855
1176
  });
856
1177
  //# sourceMappingURL=index.js.map