// Defaults must run before bundled modules resolve paths or onboarding flags. // An esbuild define would replace reads, not populate the child process env. // Match scripts/releases/bundle_env.py and the channel request decoder. const ALLOWED_KEYS = new Set([ 'HERMES_HOME', 'HERMES_DATA_DIR_SUFFIX', 'HERMES_DESKTOP_USER_DATA_DIR', 'HERMES_SHARED_AUTH_DIR', 'HERMES_GUEST_ONBOARDING', 'HERMES_SKIP_INTRO' ]) /** Validate a bundle environment object: plain object of identifiers to * strings (defaults) or null (clears). Shared by the banner writer and the * smoke driver's stamp reader. * @param {unknown} values @returns {Record} */ export function validateBundleEnvironment(values) { if (!values || Array.isArray(values) || typeof values !== 'object') { throw new Error('Bundle environment must be a JSON object') } for (const [key, value] of Object.entries(values)) { if (!ALLOWED_KEYS.has(key) || (value !== null && (typeof value !== 'string' || value.includes('\0')))) { throw new Error('Bundle environment requires permitted desktop keys and string values without NUL, or null to clear') } } return /** @type {Record} */ (values) } /** Apply bundle environment defaults/clears to a base environment and return a * new object without mutating it. This is the pure twin of the side-effecting * loop the bundled banner emits at launch — keep the two in lockstep (pinned * by bundle-env.test.mjs). Clears (null) set the key to an empty string * unconditionally so a stale registry or ambient value cannot win; defaults * fill only an absent key, so an explicit runtime value always wins. * @param {Record} env * @param {Record} values */ export function applyBundleEnvironment(env, values) { const result = { ...env } for (const [key, value] of Object.entries(values)) { if (value === null) result[key] = '' else if (result[key] == null) result[key] = value } return result } /** @param {string} raw @returns {string} */ export function environmentDefaultsBanner(raw) { const values = validateBundleEnvironment(JSON.parse(raw)) // Keep clears present-but-empty so path resolution cannot restore registry // defaults. The emitted loop is the side-effecting twin of // applyBundleEnvironment above; it cannot call it (the banner runs before // the bundle's imports), so the two must stay in lockstep — see the test. return `\nfor (const [key, value] of ${JSON.stringify(Object.entries(values))}) { if (value === null) process.env[key] = ''; else process.env[key] ??= value; }\n` }