atproto user agency toolkit for individuals and groups
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 173 lines 6.2 kB view raw
1import { readFileSync, existsSync } from "node:fs"; 2import { randomBytes } from "node:crypto"; 3import { resolve } from "node:path"; 4import type { PolicySet } from "./policy/types.js"; 5 6export interface Config { 7 /** Social account DID (optional — omit for replication-only node). */ 8 DID?: string; 9 /** Social account handle (optional). */ 10 HANDLE?: string; 11 PDS_HOSTNAME?: string; 12 AUTH_TOKEN: string; 13 /** Social account signing key hex (optional). */ 14 SIGNING_KEY?: string; 15 /** Social account signing key public multibase (optional). */ 16 SIGNING_KEY_PUBLIC?: string; 17 JWT_SECRET?: string; 18 PASSWORD_HASH?: string; 19 EMAIL?: string; 20 DATA_DIR: string; 21 PORT: number; 22 IPFS_ENABLED: boolean; 23 IPFS_NETWORKING: boolean; 24 REPLICATE_DIDS: string[]; 25 POLICY_FILE?: string; 26 FIREHOSE_URL: string; 27 FIREHOSE_ENABLED: boolean; 28 /** Whether rate limiting is enabled (default true). */ 29 RATE_LIMIT_ENABLED: boolean; 30 /** Per-pool rate limit overrides (requests per minute). */ 31 RATE_LIMIT_READ_PER_MIN: number; 32 RATE_LIMIT_SYNC_PER_MIN: number; 33 RATE_LIMIT_SESSION_PER_MIN: number; 34 RATE_LIMIT_WRITE_PER_MIN: number; 35 RATE_LIMIT_CHALLENGE_PER_MIN: number; 36 RATE_LIMIT_MAX_CONNECTIONS: number; 37 RATE_LIMIT_FIREHOSE_PER_IP: number; 38 /** Whether OAuth login is enabled for remote PDS publishing (default true). */ 39 OAUTH_ENABLED: boolean; 40 /** Public URL of this p2pds instance, used for push notifications between nodes. */ 41 PUBLIC_URL: string; 42} 43 44/** Required when OAuth is disabled (legacy mode). With OAuth, identity comes from login. */ 45const LEGACY_REQUIRED_KEYS = [ 46 "PDS_HOSTNAME", 47 "AUTH_TOKEN", 48 "JWT_SECRET", 49 "PASSWORD_HASH", 50] as const; 51 52/** 53 * Load a .env file into process.env (simple key=value parser). 54 * Skips comments and empty lines. 55 */ 56function loadDotEnv(path: string): void { 57 let content: string; 58 try { 59 content = readFileSync(path, "utf-8"); 60 } catch { 61 return; // .env file is optional 62 } 63 64 for (const line of content.split("\n")) { 65 const trimmed = line.trim(); 66 if (!trimmed || trimmed.startsWith("#")) continue; 67 const eqIdx = trimmed.indexOf("="); 68 if (eqIdx === -1) continue; 69 const key = trimmed.slice(0, eqIdx).trim(); 70 let value = trimmed.slice(eqIdx + 1).trim(); 71 // Strip surrounding quotes 72 if ( 73 (value.startsWith('"') && value.endsWith('"')) || 74 (value.startsWith("'") && value.endsWith("'")) 75 ) { 76 value = value.slice(1, -1); 77 } 78 if (!process.env[key]) { 79 process.env[key] = value; 80 } 81 } 82} 83 84/** 85 * Load and validate configuration from environment variables. 86 * Optionally loads a .env file first. 87 * 88 * Social account fields (DID, HANDLE, SIGNING_KEY, SIGNING_KEY_PUBLIC) are optional. 89 * When omitted, the node runs as replication-only. 90 */ 91export function loadConfig(envPath?: string): Config { 92 // Load .env file if it exists 93 const dotenvPath = envPath ?? process.env.DOTENV_PATH ?? resolve(process.cwd(), ".env"); 94 loadDotEnv(dotenvPath); 95 96 // Validate required variables (legacy auth fields only required without OAuth) 97 const oauthEnabled = process.env.OAUTH_ENABLED !== "false"; 98 if (!oauthEnabled) { 99 const missing: string[] = []; 100 for (const key of LEGACY_REQUIRED_KEYS) { 101 if (!process.env[key]) { 102 missing.push(key); 103 } 104 } 105 if (missing.length > 0) { 106 throw new Error( 107 `Missing required environment variables: ${missing.join(", ")}`, 108 ); 109 } 110 } 111 112 const pdsHostname = process.env.PDS_HOSTNAME || undefined; 113 114 return { 115 DID: process.env.DID || undefined, 116 HANDLE: process.env.HANDLE || undefined, 117 PDS_HOSTNAME: pdsHostname, 118 AUTH_TOKEN: process.env.AUTH_TOKEN || randomBytes(32).toString("hex"), 119 SIGNING_KEY: process.env.SIGNING_KEY || undefined, 120 SIGNING_KEY_PUBLIC: process.env.SIGNING_KEY_PUBLIC || undefined, 121 JWT_SECRET: process.env.JWT_SECRET || undefined, 122 PASSWORD_HASH: process.env.PASSWORD_HASH || undefined, 123 EMAIL: process.env.EMAIL, 124 DATA_DIR: process.env.DATA_DIR ?? "./data", 125 PORT: parseInt(process.env.PORT ?? "3000", 10), 126 IPFS_ENABLED: process.env.IPFS_ENABLED !== "false", 127 IPFS_NETWORKING: process.env.IPFS_NETWORKING !== "false", 128 REPLICATE_DIDS: (process.env.REPLICATE_DIDS ?? "").split(",").map(s => s.trim()).filter(Boolean), 129 POLICY_FILE: process.env.POLICY_FILE || undefined, 130 FIREHOSE_URL: process.env.FIREHOSE_URL ?? "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos", 131 FIREHOSE_ENABLED: process.env.FIREHOSE_ENABLED !== "false", 132 RATE_LIMIT_ENABLED: process.env.RATE_LIMIT_ENABLED !== "false", 133 RATE_LIMIT_READ_PER_MIN: parseInt(process.env.RATE_LIMIT_READ_PER_MIN ?? "300", 10), 134 RATE_LIMIT_SYNC_PER_MIN: parseInt(process.env.RATE_LIMIT_SYNC_PER_MIN ?? "30", 10), 135 RATE_LIMIT_SESSION_PER_MIN: parseInt(process.env.RATE_LIMIT_SESSION_PER_MIN ?? "10", 10), 136 RATE_LIMIT_WRITE_PER_MIN: parseInt(process.env.RATE_LIMIT_WRITE_PER_MIN ?? "200", 10), 137 RATE_LIMIT_CHALLENGE_PER_MIN: parseInt(process.env.RATE_LIMIT_CHALLENGE_PER_MIN ?? "20", 10), 138 RATE_LIMIT_MAX_CONNECTIONS: parseInt(process.env.RATE_LIMIT_MAX_CONNECTIONS ?? "100", 10), 139 RATE_LIMIT_FIREHOSE_PER_IP: parseInt(process.env.RATE_LIMIT_FIREHOSE_PER_IP ?? "3", 10), 140 OAUTH_ENABLED: process.env.OAUTH_ENABLED !== "false", 141 PUBLIC_URL: process.env.PUBLIC_URL || `http://localhost:${parseInt(process.env.PORT ?? "3000", 10)}`, 142 }; 143} 144 145/** 146 * Load policies from a JSON file. Returns null if no file is configured or found. 147 * The file should contain a PolicySet JSON object. 148 */ 149export function loadPolicies(config: Config): PolicySet | null { 150 const policyPath = config.POLICY_FILE; 151 if (!policyPath) return null; 152 153 const resolved = resolve(policyPath); 154 if (!existsSync(resolved)) { 155 console.warn(`Policy file not found: ${resolved}`); 156 return null; 157 } 158 159 try { 160 const content = readFileSync(resolved, "utf-8"); 161 const parsed = JSON.parse(content) as PolicySet; 162 if (parsed.version !== 1) { 163 throw new Error(`Unsupported policy version: ${parsed.version}`); 164 } 165 if (!Array.isArray(parsed.policies)) { 166 throw new Error("Policy file must contain a 'policies' array"); 167 } 168 return parsed; 169 } catch (err) { 170 const message = err instanceof Error ? err.message : String(err); 171 throw new Error(`Failed to load policy file ${resolved}: ${message}`); 172 } 173}