diff --git a/stasis-core/src/config.js b/stasis-core/src/config.js index 01f60ef6..1b0225a4 100644 --- a/stasis-core/src/config.js +++ b/stasis-core/src/config.js @@ -16,7 +16,14 @@ export const DEFAULT_LOCK = 'add' export const DEFAULT_BUNDLE = 'none' export const DEFAULT_SCOPE = 'full' -const envBool = (value) => Boolean(value && value !== '0') +// Strict boolean env parsing: ''/'0'/'false' -> false, '1'/'true' -> true. Anything else +// ('no', 'off', ...) throws -- a truthiness parse would read those as *enabling* the +// toggle, and a misconfigured env var should fail loudly, not silently flip a feature on. +const envBool = (name, value) => { + if (value === '' || value === '0' || value === 'false') return false + if (value === '1' || value === 'true') return true + throw new RangeError(`${name} must be ''/'0'/'false' or '1'/'true' (got '${value}')`) +} // EXODUS_STASIS_RESOURCES is a comma-separated extension list; split + trim + drop // empties so '' and ' png , svg ' normalize predictably (parseResourcesOption then @@ -121,10 +128,10 @@ export class Config { assert.equal(this.#env.resourcesBundleFile, resourcesBundleFile) } if (this.#env.debug !== undefined && debug !== undefined) { - assert.equal(envBool(this.#env.debug), debug) + assert.equal(envBool('EXODUS_STASIS_DEBUG', this.#env.debug), debug) } if (this.#env.childProcess !== undefined && childProcess !== undefined) { - assert.equal(envBool(this.#env.childProcess), childProcess) + assert.equal(envBool('EXODUS_STASIS_CHILD_PROCESS', this.#env.childProcess), childProcess) } if (this.#env.fs !== undefined && fs !== undefined) assert.equal(this.#env.fs, fs) // Compare parsed sets, not the raw strings: ['png','svg'] and 'svg,png' are the @@ -146,8 +153,8 @@ export class Config { this.#bundle = this.#env.bundle || bundle || DEFAULT_BUNDLE this.#bundleFile = this.#env.bundleFile || bundleFile || undefined this.#resourcesBundleFile = this.#env.resourcesBundleFile || resourcesBundleFile || undefined - this.#debug = this.#env.debug !== undefined ? envBool(this.#env.debug) : (debug ?? false) - this.#childProcess = this.#env.childProcess !== undefined ? envBool(this.#env.childProcess) : (childProcess ?? false) + this.#debug = this.#env.debug !== undefined ? envBool('EXODUS_STASIS_DEBUG', this.#env.debug) : (debug ?? false) + this.#childProcess = this.#env.childProcess !== undefined ? envBool('EXODUS_STASIS_CHILD_PROCESS', this.#env.childProcess) : (childProcess ?? false) // env wins over the option, like scope/lock/bundle; undefined means "fs untouched" (off). this.#fs = this.#env.fs || fs || undefined // env wins over the option (it's the process-wide signal); parseResourcesOption @@ -270,8 +277,8 @@ export class Config { if (this.#env.bundle !== undefined) assert.equal(this.#bundle, this.#env.bundle) if (this.#env.bundleFile !== undefined) assert.equal(this.#bundleFile, this.#env.bundleFile) if (this.#env.resourcesBundleFile !== undefined) assert.equal(this.#resourcesBundleFile, this.#env.resourcesBundleFile) - if (this.#env.debug !== undefined) assert.equal(this.#debug, envBool(this.#env.debug)) - if (this.#env.childProcess !== undefined) assert.equal(this.#childProcess, envBool(this.#env.childProcess)) + if (this.#env.debug !== undefined) assert.equal(this.#debug, envBool('EXODUS_STASIS_DEBUG', this.#env.debug)) + if (this.#env.childProcess !== undefined) assert.equal(this.#childProcess, envBool('EXODUS_STASIS_CHILD_PROCESS', this.#env.childProcess)) if (this.#env.fs !== undefined) assert.equal(this.#fs, this.#env.fs) if (this.#env.resources !== undefined) { assert.ok(extSetsEqual(this.#resources, parseResourcesOption('env', envList(this.#env.resources))), diff --git a/tests/config.test.js b/tests/config.test.js index 0567c6b7..0eb0cd58 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -614,6 +614,30 @@ test('Config debug env "1" is true', withEnv( (t) => { t.assert.equal(new Config().debug, true) } )) +test('Config debug env "false" is false (not truthy-parsed)', withEnv( + { EXODUS_STASIS_DEBUG: 'false' }, + (t) => { t.assert.equal(new Config().debug, false) } +)) + +test('Config debug env "true" is true', withEnv( + { EXODUS_STASIS_DEBUG: 'true' }, + (t) => { t.assert.equal(new Config().debug, true) } +)) + +test('Config debug env "" is unset and keeps the default', withEnv( + { EXODUS_STASIS_DEBUG: '' }, + (t) => { t.assert.equal(new Config().debug, false) } +)) + +// An unrecognized value ('no', 'off', ...) must throw, not silently enable: the old +// truthiness parse read EXODUS_STASIS_DEBUG=no as debug *on*. +test('Config debug env rejects unrecognized values', withEnv( + { EXODUS_STASIS_DEBUG: 'no' }, + (t) => { + t.assert.throws(() => new Config(), { name: 'RangeError', message: /EXODUS_STASIS_DEBUG must be/ }) + } +)) + test('Config debug=true option conflicts with env debug=0', withEnv( { EXODUS_STASIS_DEBUG: '0' }, (t) => { @@ -662,6 +686,29 @@ test('Config childProcess env "1" is true', withEnv( (t) => { t.assert.equal(new Config().childProcess, true) } )) +test('Config childProcess env "false" is false (not truthy-parsed)', withEnv( + { EXODUS_STASIS_CHILD_PROCESS: 'false' }, + (t) => { t.assert.equal(new Config().childProcess, false) } +)) + +test('Config childProcess env "true" is true', withEnv( + { EXODUS_STASIS_CHILD_PROCESS: 'true' }, + (t) => { t.assert.equal(new Config().childProcess, true) } +)) + +test('Config childProcess env "" is unset and keeps the default', withEnv( + { EXODUS_STASIS_CHILD_PROCESS: '' }, + (t) => { t.assert.equal(new Config().childProcess, false) } +)) + +// Mirrors the debug case: 'yes' would have silently enabled cross-process capture. +test('Config childProcess env rejects unrecognized values', withEnv( + { EXODUS_STASIS_CHILD_PROCESS: 'yes' }, + (t) => { + t.assert.throws(() => new Config(), { name: 'RangeError', message: /EXODUS_STASIS_CHILD_PROCESS must be/ }) + } +)) + test('Config childProcess=true option conflicts with env childProcess=0', withEnv( { EXODUS_STASIS_CHILD_PROCESS: '0' }, (t) => {