morphit/apps/web/scripts/order-expiry-day-floor-smoke.ts

111 lines
4.1 KiB
TypeScript

#!/usr/bin/env tsx
/*
* order-expiry-day-floor — cp175 F-015 guard.
*
* Order `expires_at` is broadcast on the public Blurt chain. If it is computed
* as `new Date(Date.now() + expiresDays * 86_400_000)`, its ISO string carries
* the submit moment to millisecond precision (…T14:23:47.831Z). Because the
* interval is a round number of days, an observer can subtract it to recover
* the client's exact wall-clock at submit time — a secondary timing/clock-skew
* fingerprint independent of (and finer than) the block time.
*
* F-015 introduced `makeExpiryFlooredUtcDay()` (in $lib/orders/payload) which
* floors to UTC midnight, and routed the /post and /post/edit call sites
* through it. This sentinel:
* 1. asserts the helper actually floors to 00:00:00.000Z and never exceeds
* the requested interval (floor, not round-up — stays under the indexer's
* MAX_EXPIRES_AT_DAYS ceiling);
* 2. asserts NO order-form call site reintroduces the raw
* `Date.now() + ...expiresDays...* 86_400_000` pattern.
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, resolve, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { makeExpiryFlooredUtcDay } from '../src/lib/orders/payload';
const HERE = dirname(fileURLToPath(import.meta.url));
const WEB_SRC = resolve(HERE, '..', 'src');
let pass = 0;
let fail = 0;
function ok(name: string): void {
console.log(`${name}`);
pass++;
}
function bad(name: string, detail: string): void {
console.log(`${name}: ${detail}`);
fail++;
}
console.log('\n── order-expiry-day-floor (cp175 F-015 guard) ──\n');
// 1. Helper floors to UTC midnight.
for (const days of [1, 14, 90, 365]) {
const d = makeExpiryFlooredUtcDay(days);
const iso = d.toISOString();
if (!iso.endsWith('T00:00:00.000Z')) {
bad(`floor(${days}d)`, `expected UTC midnight, got ${iso}`);
continue;
}
// Must not exceed the requested interval (floor, not ceil): the floored
// value is <= now + days*86400000.
const upper = Date.now() + days * 86_400_000;
if (d.getTime() > upper) {
bad(`floor(${days}d)`, `floored time ${iso} exceeds now+${days}d — must floor, not round up`);
continue;
}
// And it should be within one day below the upper bound (sanity: it's the
// start of the target day, not wildly off).
if (upper - d.getTime() >= 2 * 86_400_000) {
bad(`floor(${days}d)`, `floored time ${iso} is more than a day below now+${days}d`);
continue;
}
ok(`floor(${days}d) → ${iso} (UTC midnight, ≤ requested)`);
}
// 2. No order-form call site uses the raw millisecond-precision pattern.
function walk(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
const st = statSync(p);
if (st.isDirectory()) {
if (entry === 'node_modules' || entry.startsWith('.')) continue;
out.push(...walk(p));
} else if (entry.endsWith('.svelte') || entry.endsWith('.ts')) {
out.push(p);
}
}
return out;
}
// Match `Date.now() + <something with expiresDays> * 86_400_000` (or 86400000),
// i.e. an unfloored day-interval expiry. The helper's own definition uses this
// internally, so we exclude the payload module that defines it.
const RAW_RE = /Date\.now\(\)\s*\+\s*expiresDays\s*\*\s*86[_]?400[_]?000/;
const offenders: string[] = [];
for (const f of walk(WEB_SRC)) {
const rel = relative(WEB_SRC, f);
// payload.ts legitimately contains the expression inside makeExpiryFlooredUtcDay.
if (rel.endsWith('lib/orders/payload.ts')) continue;
const src = readFileSync(f, 'utf8');
if (RAW_RE.test(src)) offenders.push(rel);
}
if (offenders.length === 0) {
ok('no order-form call site uses the raw unfloored Date.now()+expiresDays*86_400_000 pattern');
} else {
for (const o of offenders) {
bad(o, 'uses raw unfloored expiry — route it through makeExpiryFlooredUtcDay (cp175 F-015)');
}
}
console.log('\n' + '─'.repeat(56));
if (fail === 0) {
console.log(`✓ all ${pass} scenarios passed`);
process.exit(0);
} else {
console.log(`${fail} of ${pass + fail} scenarios FAILED`);
process.exit(1);
}