Files
hermes-agent/apps/desktop/scripts/bundle-env.mjs

51 lines
2.6 KiB
JavaScript

// 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<string, string|null>} */
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<string, string|null>} */ (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<string, string|undefined>} env
* @param {Record<string, string|null>} 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`
}