// Config for the atproto MEMBRANE — a file (${HOME}/.config/atproto-bridge/config.json)
// overlaid by PDS_* env, with defaults. The membrane holds NO identity/key/account, so this
// carries only edge + fronting settings. Env wins over file wins over default.
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import process from '#node:process'
const DEFAULT_DIR = path.join(os.homedir(), '.config', 'atproto-bridge')
function expand (v) {
if (typeof v !== 'string') return v
return v.replace(/\$\{HOME\}/g, os.homedir()).replace(/^~(?=\/|$)/, os.homedir())
}
export function configPath () {
return process.env.PDS_CONFIG || path.join(DEFAULT_DIR, 'config.json')
}
export function loadConfig () {
let file = {}
const p = configPath()
try { file = JSON.parse(fs.readFileSync(p, 'utf8')) } catch { /* absent/invalid → defaults */ }
const env = process.env
const pick = (fileKey, envKey, def) => {
if (env[envKey] != null && env[envKey] !== '') return env[envKey]
if (file[fileKey] != null) return file[fileKey]
return def
}
const storage = expand(pick('storage', 'PDS_STORAGE', path.join(DEFAULT_DIR, 'data')))
// describeServer advertises this (the membrane's public identity as an atproto service). Pulled out of the
// return literal so crawlHostname can default to its host part (did:web:<host> → <host>).
const serverDid = pick('serverDid', 'PDS_SERVER_DID', 'did:web:pds.7rnx.net')
const serverHost = /^did:web:/.test(serverDid) ? serverDid.slice('did:web:'.length) : null
return {
serverDid,
userDomain: pick('userDomain', 'PDS_USER_DOMAIN', '.7rnx.net'),
port: parseInt(pick('port', 'PDS_PORT', '3399'), 10),
bind: pick('bind', 'PDS_BIND', '127.0.0.1'),
storage, // holds the front-store (aggregate firehose + replicated peer cores) — NOT accounts
uds: expand(pick('uds', 'PDS_SOCK', path.join(storage, 'svc.sock'))),
// Fronted DIDs — the self-custody hyper-proto-peers this membrane presents to bsky (Model B).
// A JSON array in the config file, or a comma-separated PDS_FRONTED_DIDS env.
frontedDids: (() => {
const v = pick('frontedDids', 'PDS_FRONTED_DIDS', null)
if (!v) return []
return Array.isArray(v) ? v : String(v).split(',').map((s) => s.trim()).filter(Boolean)
})(),
// Feed registries (community-ux.md §111) — durable P2P allow-sets of feeds the membrane REACTS to: front
// each feed's home DID + requestCrawl once. JSON array in the file, or comma-separated PDS_FEED_REGISTRIES.
feedRegistries: (() => {
const v = pick('feedRegistries', 'PDS_FEED_REGISTRIES', null)
if (!v) return []
return Array.isArray(v) ? v : String(v).split(',').map((s) => s.trim()).filter(Boolean)
})(),
// bsky relay base URL to POST com.atproto.sync.requestCrawl to when a NEW feed DID is fronted (the
// federation step). Null = OFF: the reactor still fronts the DID but fires NO live crawl — the mechanism
// is inert until deployment sets this. e.g. https://bsky.network
crawlRelay: pick('crawlRelay', 'PDS_CRAWL_RELAY', null),
// The hostname in the requestCrawl payload = OUR public membrane host (the PDS bsky enumerates via
// listRepos). Defaults to the host part of serverDid (did:web:<host>).
crawlHostname: pick('crawlHostname', 'PDS_CRAWL_HOSTNAME', serverHost),
configPath: p
}
}