cors-whitelist-ip
v1.0.3
Published
Framework-agnostic CORS whitelist middleware with IP and domain validation, supporting pluggable storage and caching
Maintainers
Readme
cors-whitelist-ip
A framework-agnostic CORS whitelist middleware with IP and domain validation, supporting pluggable storage and caching.
Features
- Framework-agnostic: Works with Express, Fastify, Koa, NestJS, or plain Node.js HTTP
- Pluggable storage: Implement your own storage adapter for any database
- Optional caching: Built-in in-memory cache or bring your own (Redis, etc.)
- IP validation: Whitelist specific IPv4/IPv6 addresses
- Domain patterns: Support for wildcard domains (e.g.,
*.example.com) - API key association: Link domains to organizations and API keys
- Zero production dependencies: Only peer dependencies for framework integrations
Installation
npm install cors-whitelist-ipQuick Start
Express
import express from 'express';
import { CorsWhitelist, InMemoryStorageAdapter } from 'cors-whitelist-ip';
import { createExpressMiddleware } from 'cors-whitelist-ip/express';
const app = express();
const storage = new InMemoryStorageAdapter({
globalWhitelist: ['https://trusted-app.com', '*'], // '*' allows all origins
whitelistEntries: [
{ id: 1, type: 'DOMAIN', value: '*.example.com', isActive: true },
{ id: 2, type: 'IP', value: '192.168.1.100', isActive: true },
],
});
const corsWhitelist = new CorsWhitelist({ storage, debug: true });
app.use(createExpressMiddleware(corsWhitelist));
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello!' });
});
app.listen(3000);Fastify
import Fastify from 'fastify';
import { fastifyCorsWhitelist } from 'cors-whitelist-ip/fastify';
import { InMemoryStorageAdapter } from 'cors-whitelist-ip/storage';
const fastify = Fastify();
const storage = new InMemoryStorageAdapter({
globalWhitelist: ['https://trusted-app.com'],
});
fastify.register(fastifyCorsWhitelist, {
storage,
hook: 'onRequest',
debug: true,
});
fastify.get('/api/data', async () => {
return { message: 'Hello!' };
});
fastify.listen({ port: 3000 });Koa
import Koa from 'koa';
import { CorsWhitelist, InMemoryStorageAdapter } from 'cors-whitelist-ip';
import { createKoaMiddleware } from 'cors-whitelist-ip/koa';
const app = new Koa();
const storage = new InMemoryStorageAdapter({
globalWhitelist: ['https://trusted-app.com'],
});
const corsWhitelist = new CorsWhitelist({ storage });
app.use(createKoaMiddleware(corsWhitelist));
app.use((ctx) => {
ctx.body = { message: 'Hello!' };
});
app.listen(3000);NestJS
// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CorsWhitelistModule, getCorsWhitelistMiddleware } from 'cors-whitelist-ip/nestjs';
import { InMemoryStorageAdapter } from 'cors-whitelist-ip/storage';
@Module({
imports: [
CorsWhitelistModule.forRoot({
storage: new InMemoryStorageAdapter({
globalWhitelist: ['https://trusted-app.com'],
}),
debug: true,
}),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(getCorsWhitelistMiddleware()).forRoutes('*');
}
}With async configuration (e.g., using ConfigService):
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
CorsWhitelistModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
storage: new InMemoryStorageAdapter({
globalWhitelist: configService.get<string[]>('CORS_WHITELIST') || ['*'],
}),
}),
inject: [ConfigService],
}),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(getCorsWhitelistMiddleware()).forRoutes('*');
}
}Configuration
CorsWhitelistOptions
interface CorsWhitelistOptions {
// Required: Storage adapter for fetching whitelist data
storage: StorageAdapter;
// Optional: Cache adapter for caching whitelist lookups
cache?: CacheAdapter;
// Cache TTL in seconds (default: 3600)
cacheTtl?: number;
// Custom logger (default: console)
logger?: Logger;
// CORS headers configuration
corsHeaders?: {
allowMethods?: string; // default: 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
allowHeaders?: string; // default: 'Content-Type, Authorization, X-Requested-With'
allowCredentials?: boolean; // default: true
maxAge?: number; // default: 86400 (24 hours)
exposeHeaders?: string;
};
// Allow requests with no origin header (default: true)
allowNoOrigin?: boolean;
// Custom function to extract client IP
getClientIp?: (req: GenericRequest) => string;
// Enable debug logging (default: false)
debug?: boolean;
}Custom Storage Adapter
Implement the StorageAdapter interface to use your own database:
import { StorageAdapter, WhitelistEntry } from 'cors-whitelist-ip';
class PostgresStorageAdapter implements StorageAdapter {
constructor(private db: Pool) {}
async getGlobalWhitelist(): Promise<string[]> {
const result = await this.db.query(
'SELECT domains FROM global_cors_whitelist WHERE is_active = true'
);
return result.rows[0]?.domains || [];
}
async getWhitelistEntries(): Promise<WhitelistEntry[]> {
const result = await this.db.query(
'SELECT * FROM organisation_whitelist WHERE is_active = true'
);
return result.rows;
}
async getOrganisationWhitelist(orgId: string | number): Promise<WhitelistEntry[]> {
const result = await this.db.query(
'SELECT * FROM organisation_whitelist WHERE organisation_id = $1 AND is_active = true',
[orgId]
);
return result.rows;
}
async findOrganisationByDomain(domain: string): Promise<string | number | null> {
const result = await this.db.query(
`SELECT organisation_id FROM organisation_whitelist
WHERE type = 'DOMAIN' AND is_active = true
AND (value = $1 OR ($1 LIKE '%' || SUBSTRING(value FROM 3)))`,
[domain]
);
return result.rows[0]?.organisation_id || null;
}
async getApiKey(orgId: string | number): Promise<string | null> {
const result = await this.db.query(
'SELECT api_key FROM organisation_api_key WHERE organisation_id = $1',
[orgId]
);
return result.rows[0]?.api_key || null;
}
async isIpWhitelisted(ip: string): Promise<boolean> {
const result = await this.db.query(
`SELECT 1 FROM organisation_whitelist
WHERE type = 'IP' AND value = $1 AND is_active = true LIMIT 1`,
[ip]
);
return result.rows.length > 0;
}
async isDomainWhitelisted(domain: string): Promise<boolean> {
const result = await this.db.query(
`SELECT 1 FROM organisation_whitelist
WHERE type = 'DOMAIN' AND is_active = true
AND (value = $1 OR $1 LIKE '%' || SUBSTRING(value FROM 3)) LIMIT 1`,
[domain]
);
return result.rows.length > 0;
}
}Custom Cache Adapter
Implement the CacheAdapter interface for Redis or other caching solutions:
import { CacheAdapter } from 'cors-whitelist-ip';
import Redis from 'ioredis';
class RedisCacheAdapter implements CacheAdapter {
constructor(private redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
async deletePattern(pattern: string): Promise<void> {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) await this.redis.del(...keys);
}
async clear(): Promise<void> {
await this.redis.flushdb();
}
async has(key: string): Promise<boolean> {
return (await this.redis.exists(key)) === 1;
}
}API
CorsWhitelist
const cors = new CorsWhitelist(options);
// Initialize (optional, for adapters that need setup)
await cors.initialize();
// Handle a request (used internally by framework adapters)
const shouldContinue = await cors.handleRequest(req, res);
// Validate an origin directly
const result = await cors.validateOrigin('https://example.com');
// Returns: { allowed: boolean, reason: string, organisationId?, apiKey? }
// Invalidate cache
await cors.invalidateCache(); // Clear all CORS cache
await cors.invalidateCache('example'); // Clear cache matching pattern
// Clean up
await cors.dispose();Validation Flow
- Global Whitelist: Check if origin is in the global whitelist (supports
*wildcard) - Domain Lookup: Find if domain is associated with an organization
- API Key Check: Verify organization has a valid API key
- IP Whitelist: Check if origin IP is whitelisted
- Domain Pattern: Match against wildcard patterns (e.g.,
*.example.com)
License
MIT
