feat: F5.6c measured and accepted on test-filius; tickets up to 5 s ahead of F's clock

test-filius refused every ticket as from the future: its clock was 886 ms
behind the dashboard's machine, with NTP still 8 ms (one ticket in 125, as a
ticket carries whole seconds). F now takes a ticket signed up to 5 s ahead
(TICKET_AHEAD_SECONDS); itsdangerous refuses one second, and the oracle
keeps that on record.

tools/live measures and accepts on a live F, with fresh projects each run:
m3 (200 rooms of the largest example with pings: probe p95 37 ms), m4 (63 KB
document; dragging makes an update per pointer move, 59/s), m5 (an hour
through the proxy), acceptance (two browsers, 14 checks, passed three times)
and clock (F's clock against ours).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 19:22:22 +00:00
co-authored by Claude Opus 5.5
parent 087721cba3
commit 416d5fdea3
15 changed files with 630 additions and 7 deletions
+10 -3
View File
@@ -3,8 +3,13 @@
// JSON, zlib-packed when that is shorter (then a leading "."), base64url;
// the moment of signing as the shortest big-endian bytes, base64url; an
// HMAC-SHA1 over both under the key sha1(salt + "signer" + secret)
// ("django-concat"), base64url. Valid for TICKET_SECONDS after signing and
// not before it, as itsdangerous counts (whole seconds). What lies under a
// ("django-concat"), base64url. Valid for TICKET_SECONDS after signing, as
// itsdangerous counts (whole seconds), and — unlike itsdangerous, which
// refuses a ticket a second ahead — from TICKET_AHEAD_SECONDS before it:
// the dashboard's machine and F's each have their clock, and a ticket
// carries whole seconds. F's clock 8 ms behind refused one ticket in 125,
// 886 ms behind nearly nine in ten (measured on test-filius, 04.10.2026);
// a person then sees "invalid ticket" for nothing they did. What lies under a
// matching signature the dashboard made: its base64 is read as Node reads
// it (leniently, as Python does). Python's own tickets are the oracle
// (tools/ticket-oracle).
@@ -17,6 +22,8 @@ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
import { inflateSync } from 'node:zlib';
export const TICKET_SECONDS = 60;
/** How far ahead of F's clock a ticket may be signed: two machines' clocks, never quite one. */
export const TICKET_AHEAD_SECONDS = 5;
export const TICKET_SALT = 'mdg-filius-ticket';
export const LEVELS = ['read', 'write'] as const;
export type Level = (typeof LEVELS)[number];
@@ -61,7 +68,7 @@ export function readSigned(ticket: string, secret: string, now: number, salt = T
if (stamp < 0) throw new TicketError('no timestamp');
const age = now - Buffer.from(value.slice(stamp + 1), 'base64url').reduce((sum, byte) => sum * 256 + byte, 0);
if (age > TICKET_SECONDS) throw new TicketError(`ticket ${age} s old`);
if (age < 0) throw new TicketError('ticket from the future');
if (age < -TICKET_AHEAD_SECONDS) throw new TicketError('ticket from the future');
const payload = value.slice(0, stamp);
const packed = payload.startsWith('.');
try {