/** * Load vendored `bare-discord-js` onto `ctx.bare.discordJS`. * * discord.js is CJS and remaps Node builtins through `bare-node-runtime` on Bare. * Do not ESM-import the package from the booter (its ESM entry uses `node:module`). */ import { readFileSync } from '#host-fs' import { tryRequireFromBooter } from '../ctx/bare-os-ctx-bare.js' import { bareOsHostBooterWarn } from '../host/bare-os-host-booter-log.js' import { takePackedDiscordJs } from '../ctx/bare-os-ctx-discord-registry.js' /** @type {Record | undefined} */ let discordJsCache let discordJsTried = false /** @type {string} */ let discordJsLastError = '' /** Last load failure (empty when unused or successful). */ export function getBareDiscordJsLoadError() { return discordJsLastError } function noteDiscordLoadError(err) { const msg = err && typeof err === 'object' && 'message' in err ? String(/** @type {{ message?: unknown }} */ (err).message || err) : String(err || 'unknown error') discordJsLastError = msg.slice(0, 800) return discordJsLastError } function bareRuntime() { if (typeof globalThis.Bare !== 'undefined') return true const v = globalThis.process?.versions return Boolean(v && typeof v.bare === 'string') } /** * Discord rejects tokens with trailing newlines, BOM, or copy-paste invisibles. * @param {unknown} raw * @returns {string} */ export function normalizeDiscordToken(raw) { if (typeof raw !== 'string') return '' let t = raw.trim() if (t.charCodeAt(0) === 0xfeff) t = t.slice(1) t = t.replace(/[\u200B-\u200D\uFEFF]/g, '') return t.trim() } /** * @param {string} line * @returns {{ key: string, value: string } | null} */ export function parseDotEnvLine(line) { let s = String(line) .replace(/^\uFEFF/, '') .trim() if (!s || s.startsWith('#')) return null if (s.startsWith('export ')) s = s.slice(7).trim() const eq = s.indexOf('=') if (eq === -1) return null const key = s.slice(0, eq).trim() if (!key) return null let val = s.slice(eq + 1).trim() if ( (val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")) ) { val = val.slice(1, -1) } return { key, value: val } } /** * @param {string} text * @returns {Record} */ export function parseDotEnvText(text) { /** @type {Record} */ const out = {} if (typeof text !== 'string' || !text) return out let body = text if (body.charCodeAt(0) === 0xfeff) body = body.slice(1) for (const line of body.split(/\r?\n/)) { const parsed = parseDotEnvLine(line) if (!parsed) continue out[parsed.key] = parsed.value } return out } /** * Copy host Discord token / env-file settings into the guest session. * When `DISCORD_ENV_FILE` (or `BARE_OS_DISCORD_ENV_FILE`) is a **host** path, * read it here so the guest does not need host filesystem access. * * @param {Record | NodeJS.ProcessEnv | null | undefined} hostEnv * @param {Record} shellEnv */ export function applyDiscordHostEnvToShellEnv(hostEnv, shellEnv) { if (!hostEnv || !shellEnv) return for (const k of [ 'BARE_OS_DISCORD', 'DISCORD_TOKEN', 'DISCORD_GUILD_ID', 'DISCORD_ID_WHITELIST', 'DISCORD_USER_INSTALL', 'DISCORD_ENV_FILE', 'BARE_OS_DISCORD_ENV_FILE' ]) { const v = hostEnv[k] if (v != null && String(v).trim()) shellEnv[k] = String(v) } const envFile = String( hostEnv.DISCORD_ENV_FILE || hostEnv.BARE_OS_DISCORD_ENV_FILE || '' ).trim() if (!envFile) return try { const parsed = parseDotEnvText(readFileSync(envFile, 'utf8')) const token = normalizeDiscordToken(parsed.DISCORD_TOKEN || '') if (token && !String(shellEnv.DISCORD_TOKEN || '').trim()) { shellEnv.DISCORD_TOKEN = token } const guild = String(parsed.DISCORD_GUILD_ID || '').trim() if (guild && !String(shellEnv.DISCORD_GUILD_ID || '').trim()) { shellEnv.DISCORD_GUILD_ID = guild } const whitelist = String(parsed.DISCORD_ID_WHITELIST || '').trim() if (whitelist && !String(shellEnv.DISCORD_ID_WHITELIST || '').trim()) { shellEnv.DISCORD_ID_WHITELIST = whitelist } const userInstall = String( parsed.DISCORD_USER_INSTALL || parsed.BARE_OS_DISCORD_USER_INSTALL || '' ).trim() if (userInstall && !String(shellEnv.DISCORD_USER_INSTALL || '').trim()) { shellEnv.DISCORD_USER_INSTALL = userInstall } } catch { // Guest may still read the same path from the VFS (`--env`). } } function unwrapDiscordModule(mod) { if (!mod || typeof mod !== 'object') return undefined const direct = /** @type {Record} */ (mod) if (typeof direct.Client === 'function') return direct const def = direct.default if (def && typeof def === 'object' && typeof def.Client === 'function') { return /** @type {Record} */ (def) } return undefined } function applyBareProcessEmitWarning() { const proc = globalThis.process if (!proc || typeof proc.emitWarning === 'function') return proc.emitWarning = function emitWarning(warning, type, code) { const msg = warning instanceof Error ? warning.message : String(warning) const name = typeof type === 'string' ? type : 'Warning' const id = typeof code === 'string' ? code : '' const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg try { if (typeof proc.emit === 'function') proc.emit('warning', warning) } catch { /* ignore */ } try { console.error(line) } catch { /* ignore */ } } } function applyBareTlsCompat() { const proc = globalThis.process if (!proc || !proc.env) return if (!proc.env.NODE_TLS_REJECT_UNAUTHORIZED) { proc.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' } try { const req = globalThis.require if (typeof req !== 'function') return const bareHttps = req('bare-https') if (!bareHttps || typeof bareHttps.Agent !== 'function') return const insecureAgent = new bareHttps.Agent({ rejectUnauthorized: false }) bareHttps.globalAgent = insecureAgent if (bareHttps.Agent) bareHttps.Agent.global = insecureAgent } catch { /* optional */ } } /** * @param {Record | null} [_ctx] * @returns {Promise | undefined>} */ export async function loadBareDiscordJs(_ctx) { void _ctx if (discordJsTried) return discordJsCache discordJsTried = true discordJsLastError = '' if (bareRuntime()) applyBareTlsCompat() applyBareProcessEmitWarning() /** @type {unknown} */ let lastErr = null try { const packed = unwrapDiscordModule(takePackedDiscordJs()) if (packed) { discordJsCache = packed return discordJsCache } } catch (err) { lastErr = err } try { const mod = await tryRequireFromBooter('bare-discord-js') const discord = unwrapDiscordModule(mod) if (discord) { discordJsCache = discord return discordJsCache } if (!lastErr) { lastErr = new Error( String(import.meta.url || '').startsWith('bare:') ? 'standalone pack did not register discord.js (bare-os-ctx-discord-packed.js)' : 'require("bare-discord-js") returned no Client' ) } } catch (err) { lastErr = err } const detail = noteDiscordLoadError( lastErr || 'vendored bare-discord-js unresolved' ) bareOsHostBooterWarn( 'ctx_bare_discord_js_load_failed', 'ctx.bare.discordJS: failed to load vendored bare-discord-js', detail ) discordJsCache = undefined return undefined }