Spaces:
Paused
Paused
File size: 3,287 Bytes
2276d59 62527b4 2276d59 d38cac2 65fc551 d38cac2 2276d59 65fc551 2276d59 d38cac2 62527b4 d38cac2 62527b4 d38cac2 1f34571 2276d59 d38cac2 2276d59 d38cac2 2276d59 d38cac2 2276d59 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | import { Request, Response, NextFunction } from 'express';
import { logger } from '../infra/logger.util.js';
import { timingSafeEqual } from 'node:crypto';
function isLocalRequest(req: Request): boolean {
// proxied/spoofed requests are not local
if (req.headers['x-forwarded-for']) return false;
const ip = req.socket.remoteAddress?.replace(/^::ffff:/u, '');
return ip === '127.0.0.1' || ip === '::1';
}
export type AuthMode = 'open' | 'apikey' | 'deny';
export function resolveAuthMode(
env: NodeJS.ProcessEnv = process.env
): AuthMode {
const explicit = (env.AUTH_MODE ?? '').toLowerCase().trim();
if (explicit === 'open') return 'open';
if (explicit === 'apikey') return 'apikey';
// a configured key implies apikey mode
if (env.API_KEY) return 'apikey';
// prod fails closed; dev stays open
if (env.NODE_ENV === 'production') return 'deny';
return 'open';
}
// fail fast on unsafe prod config
export function assertProdConfig(env: NodeJS.ProcessEnv = process.env): void {
if (env.NODE_ENV !== 'production') return;
if (!env.PROXY_SIGNING_SECRET) {
throw new Error(
'PROXY_SIGNING_SECRET is required when NODE_ENV=production'
);
}
const mode = resolveAuthMode(env);
if (mode === 'deny') {
logger.warn(
'[auth] DENY MODE — no API_KEY and no AUTH_MODE set. ' +
'Non-localhost requests are blocked. ' +
'Set API_KEY to require a key, or AUTH_MODE=open for an open public instance.'
);
} else if (mode === 'open') {
logger.warn(
'[auth] OPEN MODE — all requests allowed without authentication.'
);
}
}
function extractKey(req: Request): string | undefined {
const auth = req.headers.authorization;
if (auth?.startsWith('Bearer ')) return auth.slice(7);
const headerKey = req.headers['x-api-key'];
if (typeof headerKey === 'string') return headerKey;
const queryKey = req.query.key ?? req.query.apiKey;
return typeof queryKey === 'string' ? queryKey : undefined;
}
function keysMatch(provided: string, expected: string): boolean {
const providedBuf = Buffer.from(provided);
const expectedBuf = Buffer.from(expected);
if (providedBuf.length !== expectedBuf.length) return false;
return timingSafeEqual(providedBuf, expectedBuf);
}
export function requireApiKey(
req: Request,
res: Response,
next: NextFunction
): void {
const mode = resolveAuthMode();
if (mode === 'open') {
next();
return;
}
// local self-host stays frictionless
if (isLocalRequest(req)) {
next();
return;
}
if (mode === 'deny') {
res.status(403).json({ error: 'Public access is not configured' });
return;
}
const expected = process.env.API_KEY;
const provided = extractKey(req);
if (expected && provided && keysMatch(provided, expected)) {
next();
return;
}
res.status(401).json({ error: 'Unauthorized' });
}
// metrics gate: localhost or valid key
export function requireLocalOrApiKey(
req: Request,
res: Response,
next: NextFunction
): void {
if (isLocalRequest(req)) {
next();
return;
}
const expected = process.env.API_KEY;
const provided = extractKey(req);
if (expected && provided && keysMatch(provided, expected)) {
next();
return;
}
res.status(403).json({ error: 'Forbidden' });
}
|