137 lines
6.7 KiB
TypeScript
137 lines
6.7 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* Smoke: the warrant canary is byte-portable and human-readable. Anchor
|
|
* 2026-07-08 (Ken).
|
|
*
|
|
* Two reported problems, both guarded here:
|
|
*
|
|
* 1. MOJIBAKE — the published canary showed "Morphit operator canary —
|
|
* morphit.io" and "•" bullets. Those are UTF-8 em-dash / bullet bytes
|
|
* being decoded as Latin-1 by whatever is reading the file. A warrant
|
|
* canary is a plain-text legal declaration that people fetch with curl,
|
|
* open in random viewers, and feed to gpg — it must not depend on the
|
|
* reader guessing a charset. The template is therefore PURE ASCII, which
|
|
* makes the failure mode structurally impossible rather than merely fixed.
|
|
*
|
|
* 2. ZULU TIMESTAMPS — "2026-07-08T23:45:18Z" reads as machine output to a
|
|
* human trying to judge whether the canary is fresh, which is the single
|
|
* thing this file exists to communicate. The generator now emits Ken's
|
|
* sitewide format: "8 July, 2026 @ 23:45:18 UTC".
|
|
*
|
|
* `verify.ts` must accept BOTH the new format and the legacy ISO form, or the
|
|
* canary currently signed + deployed on morphit.io would stop verifying.
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
/**
|
|
* `scripts/canary/verify.ts` lives in the ROOT workspace (CommonJS) while this
|
|
* smoke runs in apps/web (ESM), so a named import across that boundary fails.
|
|
* Exercise the verifier the way operators actually use it instead — through
|
|
* its CLI, over real fixture files. That also covers the argv/exit-code
|
|
* contract, which a direct function import would silently skip.
|
|
*/
|
|
function runVerify(body: string): { code: number; out: string } {
|
|
const dir = mkdtempSync(join(tmpdir(), 'morphit-canary-'));
|
|
const f = join(dir, 'canary.txt');
|
|
writeFileSync(f, body, 'utf8');
|
|
const r = spawnSync('npx', ['tsx', join(REPO, 'scripts', 'canary', 'verify.ts'), f], {
|
|
encoding: 'utf8',
|
|
timeout: 60_000
|
|
});
|
|
return { code: r.status ?? -1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
}
|
|
|
|
/** A minimal, fully-substituted canary with the given Generated: stamp. */
|
|
function canaryWith(stamp: string): string {
|
|
return template
|
|
.replace(/\{\{OPERATOR_NAME\}\}/g, 'morphit.io')
|
|
.replace(/\{\{INSTANCE_ORIGIN\}\}/g, 'https://morphit.io')
|
|
.replace(/\{\{GENERATED_AT_ISO\}\}/g, stamp)
|
|
.replace(/\{\{VALID_THROUGH_ISO\}\}/g, stamp)
|
|
.replace(/\{\{OPERATOR_ACCOUNT\}\}/g, 'morphit')
|
|
.replace(/\{\{BLURT_HEAD_HEIGHT\}\}/g, '12345678')
|
|
.replace(/\{\{BLURT_HEAD_HASH\}\}/g, 'abc123')
|
|
.replace(/\{\{BLURT_HEAD_TIMESTAMP\}\}/g, stamp)
|
|
.replace(/\{\{BTC_HEAD_HEIGHT\}\}/g, '900001')
|
|
.replace(/\{\{BTC_HEAD_HASH\}\}/g, '0000deadbeef')
|
|
.replace(/\{\{NEWS_HEADLINE\}\}/g, 'A headline')
|
|
.replace(/\{\{NEWS_RSS\}\}/g, 'https://example.org/rss')
|
|
.replace(/\{\{NEWS_FETCHED_AT\}\}/g, stamp);
|
|
}
|
|
|
|
function nowStamp(offsetDays = 0): string {
|
|
const d = new Date(Date.now() + offsetDays * 86_400_000);
|
|
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
|
const p = (n: number) => String(n).padStart(2, '0');
|
|
return `${d.getUTCDate()} ${months[d.getUTCMonth()]}, ${d.getUTCFullYear()} @ ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())} UTC`;
|
|
}
|
|
|
|
function nowIso(): string {
|
|
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
}
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const WEB = join(__dirname, '..');
|
|
const REPO = join(WEB, '..', '..');
|
|
|
|
const template = readFileSync(join(WEB, 'static', 'canary.txt.template'), 'utf8');
|
|
const templateBytes = readFileSync(join(WEB, 'static', 'canary.txt.template'));
|
|
const generate = readFileSync(join(REPO, 'scripts', 'canary', 'generate.sh'), 'utf8');
|
|
const verify = readFileSync(join(REPO, 'scripts', 'canary', 'verify.ts'), 'utf8');
|
|
|
|
let pass = 0;
|
|
let fail = 0;
|
|
function check(name: string, ok: boolean): void {
|
|
if (ok) {
|
|
pass++;
|
|
console.log(` \u2713 ${name}`);
|
|
} else {
|
|
fail++;
|
|
console.error(` \u2717 ${name}`);
|
|
}
|
|
}
|
|
|
|
// ─── 1. Pure ASCII: mojibake is structurally impossible ──────────────
|
|
const highBytes = [...templateBytes].filter((b) => b > 127);
|
|
check('canary template contains ZERO bytes above 0x7F (pure ASCII)', highBytes.length === 0);
|
|
check('no em-dash / bullet left in the template', !template.includes('\u2014') && !template.includes('\u2022'));
|
|
check('the mojibake strings themselves never appear', !/\u00e2\u20ac/.test(template));
|
|
|
|
// ─── 2. Human timestamps, ASCII-safe, English months ─────────────────
|
|
check('generator emits the sitewide stamp "%-d %B, %Y @ %H:%M:%S UTC"', /%-d %B, %Y @ %H:%M:%S UTC/.test(generate));
|
|
check('generator no longer emits a bare Zulu ISO stamp', !/\+%Y-%m-%dT%H:%M:%SZ/.test(generate));
|
|
check('generator pins LC_ALL=C so month names are English + ASCII', /LC_ALL=C date/.test(generate));
|
|
check('the Blurt head timestamp is reformatted too', /BLURT_HEAD_TIMESTAMP="\$\(canary_stamp/.test(generate));
|
|
check('a malformed chain timestamp cannot abort generation (fallback)', /\|\| printf '%s' "\$BLURT_HEAD_TIMESTAMP"/.test(generate));
|
|
|
|
// ─── 3. verify.ts parses BOTH formats ────────────────────────────────
|
|
check('Generated: capture takes the whole line (not the first token)', /\^Generated:\[ \\t\]\*\(\.\+\)\$/.test(verify));
|
|
|
|
// Behavioural: run the REAL verifier CLI over real fixtures.
|
|
const fresh = runVerify(canaryWith(nowStamp()));
|
|
check('verifier accepts the new human stamp', fresh.code === 0 && /canary-verify: OK/.test(fresh.out));
|
|
|
|
const legacy = runVerify(canaryWith(nowIso()));
|
|
check('verifier still accepts the LEGACY Zulu ISO stamp (deployed canary)', legacy.code === 0 && /canary-verify: OK/.test(legacy.out));
|
|
|
|
const stale = runVerify(canaryWith(nowStamp(-30)));
|
|
check('a stale canary still FAILS (freshness guard not weakened)', stale.code !== 0 && /stale/.test(stale.out));
|
|
|
|
const bogus = runVerify(canaryWith('30 Junius, 2026 @ 16:45:18 UTC'));
|
|
check('a bogus month is rejected, not silently treated as fresh', bogus.code !== 0 && /not parseable/.test(bogus.out));
|
|
|
|
// ─── 4. verify.ts stays importable (run-as-main guard) ───────────────
|
|
check('verify.ts has a run-as-main guard (importing it must not run the CLI)', /invokedDirectly/.test(verify) && /if \(invokedDirectly\)/.test(verify));
|
|
|
|
console.log('');
|
|
if (fail === 0) {
|
|
console.log(`\u2713 all ${pass} canary-ascii-and-dates scenarios passed`);
|
|
} else {
|
|
console.error(`\u2717 ${fail} of ${pass + fail} canary-ascii-and-dates checks FAILED`);
|
|
process.exit(1);
|
|
}
|