From bd29802e1e8d9240675f7ee001549643e7b80093 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:20:28 +0000 Subject: [PATCH] ui-ota publish: the dl token follows packaged-config.sh's .next rule (IGNEUM_DL_TOKEN_FILE, else dl-token.next while a rotation runs, else dl-token), so a bundle staged during the rotation lands in the new folder and --verify reads the new URL; tokenFile and readToken exported, token.test.mjs on the gate, known-failed first on the direct read (7 October 2026, the dl token rotates with 0.3.22) Co-Authored-By: Claude Fable 5.1 --- tools/ci/pre-push.sh | 2 +- tools/ui-ota/publish.mjs | 11 +++++++++-- tools/ui-ota/token.test.mjs | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 tools/ui-ota/token.test.mjs diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 7d361933..34e7149c 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -104,7 +104,7 @@ tree_checks() { run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs run "ship tool self-test" node tools/ship-app.mjs --self-test - run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test + run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test && node --test tools/ui-ota/token.test.mjs run "the interface and the app entries are one UI tree (version pair; self-test, then the staged manifest when one is given)" bash -c 'node tools/ui-ota/pair-check.mjs --self-test && { [ -z "${IGNEUM_MANIFEST:-}" ] || node tools/ui-ota/pair-check.mjs "$IGNEUM_MANIFEST"; }' run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.mjs --self-test run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs diff --git a/tools/ui-ota/publish.mjs b/tools/ui-ota/publish.mjs index b23a5b98..965c243f 100644 --- a/tools/ui-ota/publish.mjs +++ b/tools/ui-ota/publish.mjs @@ -36,6 +36,13 @@ const flag = (name) => process.argv.includes(name); const STAMP = new Date(Date.UTC(2026, 0, 1)); const threePart = (v) => /^\d+\.\d+\.\d+$/.test(v); +// the dl token (packaged-config.sh's igneum_secret_file rule, 7 October 2026): IGNEUM_DL_TOKEN_FILE when set, else +// dl-token.next while a rotation runs, else dl-token; a publish during the rotation lands in the NEW folder, and a cut that +// must target the old one says so with the variable +export function tokenFile(configDir, env) { env = env || process.env; if (env.IGNEUM_DL_TOKEN_FILE) return env.IGNEUM_DL_TOKEN_FILE; const next = path.join(configDir, 'dl-token.next'); return fs.existsSync(next) ? next : path.join(configDir, 'dl-token'); } +export function readToken(configDir, env) { return fs.readFileSync(tokenFile(configDir, env), 'utf8').trim(); } +const CONFIG_DIR = path.join(os.homedir(), '.config/igneum'); + export function sha256(file) { return createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } // Copies the served files into a clean folder with the VERSION stamped, packs them in sorted order (one tar, the @@ -111,7 +118,7 @@ function selfTest() { } function verifyLive() { - const token = fs.readFileSync(path.join(os.homedir(), '.config/igneum/dl-token'), 'utf8').trim(); + const token = readToken(CONFIG_DIR); const url = arg('--url', `https://dl.igneum.network/dl/${token}/igneum-app-latest.json`); const dlsite = process.env.IGNEUM_DLSITE || fs.readFileSync(path.join(os.homedir(), '.config/igneum/dlsite-dir'), 'utf8').trim(); const text = run('curl', ['-fsSL', '--max-time', '20', url]); @@ -143,7 +150,7 @@ function main() { if (!minEngine || !threePart(minEngine)) { console.error('--min-engine major.minor.patch is required (the lowest app version that may serve this bundle)'); process.exit(2); } const dry = flag('--dry-run'); const s = ensureSigner(); - const token = fs.readFileSync(path.join(os.homedir(), '.config/igneum/dl-token'), 'utf8').trim(); + const token = readToken(CONFIG_DIR); const dlsite = process.env.IGNEUM_DLSITE || fs.readFileSync(path.join(os.homedir(), '.config/igneum/dlsite-dir'), 'utf8').trim(); const dest = path.join(dlsite, 'dl', token); const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-ota-')); diff --git a/tools/ui-ota/token.test.mjs b/tools/ui-ota/token.test.mjs new file mode 100644 index 00000000..b2add24e --- /dev/null +++ b/tools/ui-ota/token.test.mjs @@ -0,0 +1,27 @@ +// node --test tools/ui-ota/token.test.mjs (the dl token rotation, 7 October 2026: tools/ui-ota/publish.mjs read +// ~/.config/igneum/dl-token directly, so after the rotation it would stage into the old folder and verify the old URL). +// The rule is packaged-config.sh's igneum_secret_file: IGNEUM_DL_TOKEN_FILE when set, else dl-token.next when present, +// else dl-token; the folder from IGNEUM_DLSITE, else dlsite-dir. Known-failed first on the direct read. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +const here = dirname(fileURLToPath(import.meta.url)); +const mod = await import(join(here, 'publish.mjs')); + +test('the token file follows the .next rule: the env var, else dl-token.next, else dl-token; the folder from IGNEUM_DLSITE else dlsite-dir', () => { + assert.equal(typeof mod.tokenFile, 'function', 'publish.mjs exports tokenFile'); + const dir = mkdtempSync(join(tmpdir(), 'igneum-token-')); + try { + writeFileSync(join(dir, 'dl-token'), 'oldtoken\n'); + assert.equal(mod.tokenFile(dir, {}), join(dir, 'dl-token'), 'plain file when no .next'); + writeFileSync(join(dir, 'dl-token.next'), 'newtoken\n'); + assert.equal(mod.tokenFile(dir, {}), join(dir, 'dl-token.next'), 'the .next file wins while a rotation runs'); + assert.equal(mod.tokenFile(dir, { IGNEUM_DL_TOKEN_FILE: join(dir, 'dl-token') }), join(dir, 'dl-token'), 'the variable pins the old folder knowingly'); + assert.equal(mod.readToken(dir, {}), 'newtoken'); + const src = readFileSync(join(here, 'publish.mjs'), 'utf8'); + assert.equal((src.match(/'\.config\/igneum\/dl-token'\)/g) || []).length, 0, 'no direct read of dl-token left'); + } finally { rmSync(dir, { recursive: true, force: true }); } +});