fix(dev-fresh): keyless launch no longer crashes on stock macOS bash 3.2 (#144)

Expanding the empty KEY_SETUP_EXTERNAL_KEYS array under set -u is a fatal
'unbound variable' on bash 3.2, which macOS ships as /bin/bash — so a keyless
./scripts/dev-fresh.sh died at the provenance-marker line before doing
anything. The ${arr[*]:-} guard keeps 3.2 alive and leaves populated launches
byte-identical (same idiom the script already uses for DEV_UNSET).

Verified with /bin/bash 3.2.57: the extracted block runs clean under set -u
with no keys (empty CSV) and with keys (ordered comma-joined names); the full
launcher boots keyless to a live server, and exported keys still classify as
externally managed in Provider Settings. A behavioral guard now runs the real
block from the script both ways in the dev-fresh test surface, plus a textual
assertion pinning the 3.2-safe idiom.
This commit is contained in:
Bilawal Sidhu authored and GitHub committed 2026-08-31 19:00:33 -05:00
1 parent 9db0813daf
commit d8f1742783
3 files changed
+33 -1

No files matched your search

+4
View File
@@ -12,6 +12,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
the two largest sources and dropped them entirely — leaving roughly a third of the two largest sources and dropped them entirely — leaving roughly a third of
global detections while reporting each dropped source twice, once as global detections while reporting each dropped source twice, once as
successful with its real count and once as failed. successful with its real count and once as failed.
- `./scripts/dev-fresh.sh` no longer crashes on stock macOS bash 3.2 when no
provider keys are exported: expanding the empty external-keys provenance
array under `set -u` was fatal there. Launches with exported keys are
unchanged.
### Security ### Security
+1 -1
View File
@@ -38,7 +38,7 @@ KEY_SETUP_EXTERNAL_KEYS=()
[[ -n "${OPENSKY_CLIENT_ID:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_ID) [[ -n "${OPENSKY_CLIENT_ID:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_ID)
[[ -n "${OPENSKY_CLIENT_SECRET:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_SECRET) [[ -n "${OPENSKY_CLIENT_SECRET:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_SECRET)
[[ -n "${LL2_API_TOKEN:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(LL2_API_TOKEN) [[ -n "${LL2_API_TOKEN:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(LL2_API_TOKEN)
KEY_SETUP_EXTERNAL_KEYS_CSV="$(IFS=,; printf '%s' "${KEY_SETUP_EXTERNAL_KEYS[*]}")" KEY_SETUP_EXTERNAL_KEYS_CSV="$(IFS=,; printf '%s' "${KEY_SETUP_EXTERNAL_KEYS[*]:-}")"
if command -v npm >/dev/null 2>&1; then if command -v npm >/dev/null 2>&1; then
DEV_COMMAND=(npm run dev --) DEV_COMMAND=(npm run dev --)
+28
View File
@@ -79,3 +79,31 @@ test('dev-fresh passes names-only boot provenance before resolving file fallback
assert.match(source, /put_env GEV_LAUNCHER "dev-fresh"/); assert.match(source, /put_env GEV_LAUNCHER "dev-fresh"/);
assert.match(source, /put_env GEV_KEY_SETUP_EXTERNAL_KEYS "\$\{KEY_SETUP_EXTERNAL_KEYS_CSV\}"/); assert.match(source, /put_env GEV_KEY_SETUP_EXTERNAL_KEYS "\$\{KEY_SETUP_EXTERNAL_KEYS_CSV\}"/);
}); });
// Stock macOS ships bash 3.2, where expanding an EMPTY array under `set -u`
// is a fatal "unbound variable" — the `:-` guard on the provenance CSV is what
// keeps the keyless `dev-fresh.sh` launch alive there. Newer bash never fails
// this way, so the idiom itself is asserted textually and the block's behavior
// is exercised for both the keyless and the populated case.
const bashTest = process.platform === 'win32' ? test.skip : test;
bashTest('the external-keys provenance block survives set -u keyless and joins names when keys are exported', async () => {
const script = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8');
const start = script.indexOf('KEY_SETUP_EXTERNAL_KEYS=()');
const csvAt = script.indexOf('KEY_SETUP_EXTERNAL_KEYS_CSV=');
const end = script.indexOf('\n', csvAt);
assert.ok(start > 0 && csvAt > start && end > csvAt, 'provenance block not found in dev-fresh.sh');
const block = script.slice(start, end);
assert.match(block, /\$\{KEY_SETUP_EXTERNAL_KEYS\[\*\]:-\}/);
const { execFile } = await import('node:child_process');
const { promisify } = await import('node:util');
const run = promisify(execFile);
const probe = `set -u\n${block}\nprintf '%s' "$KEY_SETUP_EXTERNAL_KEYS_CSV"`;
const keyless = await run('bash', ['-c', probe], { env: { PATH: process.env.PATH } });
assert.equal(keyless.stdout, '');
const keyed = await run('bash', ['-c', probe], {
env: { PATH: process.env.PATH, FIRMS_MAP_KEY: 'x', TOMTOM_API_KEY: 'y' },
});
assert.equal(keyed.stdout, 'FIRMS_MAP_KEY,TOMTOM_API_KEY');
});