Files
bare-operating-system/packages/bare-os-booter/lib/services/bare-discord-js-loader.js
T
2026-08-18 18:11:34 -04:00

251 lines
7.4 KiB
JavaScript

/**
* 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<string, unknown> | 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<string, string>}
*/
export function parseDotEnvText(text) {
/** @type {Record<string, string>} */
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<string, string | undefined> | NodeJS.ProcessEnv | null | undefined} hostEnv
* @param {Record<string, string>} 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<string, unknown>} */ (mod)
if (typeof direct.Client === 'function') return direct
const def = direct.default
if (def && typeof def === 'object' && typeof def.Client === 'function') {
return /** @type {Record<string, unknown>} */ (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<string, unknown> | null} [_ctx]
* @returns {Promise<Record<string, unknown> | 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
}