DUNGEONS-30: add access gateway with Telegram and Discord approvals
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
69
src/config.js
Normal file
69
src/config.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const allowedFields = ['username', 'display_name', 'language_code', 'guild_id', 'created_at'];
|
||||
|
||||
export function loadConfig(env = process.env) {
|
||||
const integer = (key, fallback, min, max) => {
|
||||
const value = env[key] ?? String(fallback);
|
||||
if (!/^\d+$/.test(value) || Number(value) < min || Number(value) > max) {
|
||||
throw new Error(`${key} must be an integer from ${min} to ${max}`);
|
||||
}
|
||||
return Number(value);
|
||||
};
|
||||
const origin = (key, fallback) => {
|
||||
let url;
|
||||
try { url = new URL(env[key] || fallback); } catch { throw new Error(`${key} must be a valid HTTP(S) origin`); }
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
||||
throw new Error(`${key} must be an HTTP(S) origin without credentials, path, query or fragment`);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
const ids = (key) => {
|
||||
const values = [...new Set((env[key] || '').split(',').map(v => v.trim()).filter(Boolean))];
|
||||
if (values.some(value => !/^[1-9]\d{0,24}$/.test(value))) throw new Error(`${key} must contain comma-separated numeric user IDs`);
|
||||
return values;
|
||||
};
|
||||
const publicUrl = origin('PUBLIC_URL', 'http://localhost:8080');
|
||||
const upstreamUrl = origin('UPSTREAM_URL', 'http://127.0.0.1:3000');
|
||||
if (publicUrl.origin === upstreamUrl.origin) throw new Error('UPSTREAM_URL cannot point to PUBLIC_URL');
|
||||
if (env.NODE_ENV === 'production' && publicUrl.protocol !== 'https:') throw new Error('PUBLIC_URL must use HTTPS in production');
|
||||
const secret = env.SESSION_SECRET || '';
|
||||
if (secret.length < 32 || /replace-with|change.?me/i.test(secret)) throw new Error('Set SESSION_SECRET to a random secret of at least 32 characters');
|
||||
if (env.TRUST_PROXY && !['true', 'false'].includes(env.TRUST_PROXY)) throw new Error('TRUST_PROXY must be true or false');
|
||||
const telegram = { token: env.TELEGRAM_BOT_TOKEN || '', username: env.TELEGRAM_BOT_USERNAME || '', admins: ids('TELEGRAM_ADMIN_IDS') };
|
||||
if (telegram.token && (!/^\d+:[A-Za-z0-9_-]+$/.test(telegram.token) || !/^[A-Za-z0-9_]{5,32}$/.test(telegram.username) || !telegram.admins.length)) {
|
||||
throw new Error('Telegram requires TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME (without @), and TELEGRAM_ADMIN_IDS');
|
||||
}
|
||||
const discord = {
|
||||
token: env.DISCORD_BOT_TOKEN || '', applicationId: env.DISCORD_APPLICATION_ID || '',
|
||||
publicKey: env.DISCORD_PUBLIC_KEY || '', admins: ids('DISCORD_ADMIN_IDS'),
|
||||
accessUrl: env.DISCORD_ACCESS_URL || '', guildId: env.DISCORD_GUILD_ID || '',
|
||||
};
|
||||
if (discord.token) {
|
||||
if (!/^\d{1,25}$/.test(discord.applicationId) || !/^[a-f\d]{64}$/i.test(discord.publicKey) || !discord.admins.length) {
|
||||
throw new Error('Discord requires DISCORD_APPLICATION_ID, DISCORD_PUBLIC_KEY and DISCORD_ADMIN_IDS');
|
||||
}
|
||||
let link;
|
||||
try { link = new URL(discord.accessUrl); } catch { throw new Error('Set DISCORD_ACCESS_URL to your Discord channel or invite'); }
|
||||
if (link.protocol !== 'https:' || !['discord.com', 'discord.gg'].includes(link.hostname) || link.username || link.password) {
|
||||
throw new Error('DISCORD_ACCESS_URL must be an HTTPS discord.com or discord.gg link');
|
||||
}
|
||||
if (discord.guildId && !/^\d{1,25}$/.test(discord.guildId)) throw new Error('DISCORD_GUILD_ID must be numeric');
|
||||
}
|
||||
if (env.NODE_ENV === 'production' && !telegram.token && !discord.token) throw new Error('Enable at least one bot in production');
|
||||
const fields = (env.ADMIN_REQUEST_FIELDS ?? allowedFields.join(',')).split(',').map(v => v.trim()).filter(Boolean);
|
||||
if (fields.some(field => !allowedFields.includes(field))) throw new Error(`ADMIN_REQUEST_FIELDS may contain: ${allowedFields.join(', ')}`);
|
||||
return {
|
||||
publicUrl, upstreamUrl, secret, telegram, discord, adminFields: fields,
|
||||
host: env.HOST || '0.0.0.0', port: integer('PORT', 8080, 1, 65535),
|
||||
dataFile: resolve(env.DATA_FILE || './data/access.json'), secureCookie: publicUrl.protocol === 'https:',
|
||||
trustProxy: env.TRUST_PROXY === 'true', codeLength: integer('CODE_LENGTH', 10, 8, 12),
|
||||
codeTtlMs: integer('CODE_TTL_DAYS', 15, 1, 90) * 86400_000,
|
||||
sessionTtlMs: integer('SESSION_TTL_HOURS', 360, 1, 8760) * 3_600_000,
|
||||
requestTtlMs: integer('REQUEST_TTL_HOURS', 24, 1, 168) * 3_600_000,
|
||||
requestCooldownMs: integer('REQUEST_COOLDOWN_SECONDS', 300, 1, 86400) * 1000,
|
||||
loginAttempts: integer('LOGIN_ATTEMPTS', 5, 1, 100),
|
||||
globalLoginAttempts: integer('GLOBAL_LOGIN_ATTEMPTS', 100, 1, 10000),
|
||||
loginWindowMs: integer('LOGIN_WINDOW_SECONDS', 900, 1, 86400) * 1000,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user