Repository navigation
v4.1.2
"Reliable Bridge" (LOG #97–#98): the Reality Bridge of 4.1.1 after an outside review relayed by the maintainer and
checked against the code (three of its four P0s real, the fourth shown not to apply), and the first release of the
plan towards a professional engine for a small studio: nothing added to the content, the commands or the scripts;
what 4.1.1 promised made true under concurrency, across devices, through a rotation and under load, then measured
by mutation. Nothing to change in a game; a 4.1.1 Bridge keeps its config.json and its journal.
Fixes
- Bridge: one proposal at a time per player. In 4.1.1
proposeawaited the Biscuit check and the signature
between reading a player's last sequence and writing the journal, so concurrent proposals could all take the same
sequence, and two with onededupeKeywere both accepted; the player's client then skipped the others as already
seen, and a signal was lost. The deduplication, the quotas, the sequence, the signature and the journal line now
run under a lock per player (bridge/src/lock.ts), where the player and the token are checked again: a proposal
that overlaps a revocation is refused. A pairing code confirmed twice at once is confirmed once (409 for the other).
Found by an outside review of 4.1.1 (the first of its four P0s), reproduced intests/bridge.test.ts. - A save is bound to its player. In 4.1.1 the client acknowledged a loaded save's cursor with whatever link the
device held, so a save imported from another device (or saved before an unlink and a new pairing) could
acknowledge, for the new player, signals that save never applied, or loop on a refused acknowledgement. The first
signal now writes the Bridge's pseudonymous player id into the save (GameState.reality.playerId, the field 4.1.1
declared and never filled; a session entry carries it too, so a replay is exact); a signal for another player is
mismatch: nothing applied, nothing acknowledged, the link idles with that status and the pause menu offers to
use this device's link with that save (its link state starts over, cursor 0). The outside review's second P0.
docs/en/REALITY.md, two interface texts (realityMismatch,realityRelink). - A rotation no longer strands a player. In 4.1.1 the journal kept the signature only, nothing re-signed, and a
link that was open kept its keyring, so a signal signed by a key past its window was refused for good and the
cursor stuck (the outside review's fourth P0). The journal now keeps the payload as accepted and the Bridge signs
what waits again under its current key at delivery (JournalEntry.payload,kid); a client asks for the keys once
when a signal names one it does not know (RealityClientOptions.refreshKeys, wired in the player). Key windows
and expiries carry five minutes of clock tolerance (CLOCK_SKEW_MS, the same in the Rust cross-check; the
conformance corpus has a case on each side, 22 → 25).GET /v1/signalsanswers{ signals, sequences }and the
poll transport follows the sequences instead of counting. Event streams: at most 4 per player (429 beyond), closed
by the Bridge when the link is revoked or expires or when the reader stops reading (64 KB unsent); a signal
accepted while a stream opens is held and sent after the backlog (the review's third P0 could not happen: the
backlog and the subscription were one synchronous tick; the stream now registers first, and a test proposes while
it opens).docs/en/REALITY-OPS.md"Keys and rotation" rewritten. - The journal survives a crash and is read, never matched. A last line cut short by a crash (no newline, not
JSON) stopped the Bridge from starting; it is dropped and said in the log (journal.repaired), while any other
line that does not parse is still refused as corruption, with its number. Forgetting a player rewrote the file
by matching its id as text, which took another player's line whosededupeKeywas that id; each line is now read
as an event. The rewrite fsyncs the directory after the rename where the system allows. Two commands:
web-scumm-bridge doctor(what the journal holds, torn or corrupt) andweb-scumm-bridge compact [--retention-days=90](pairings past their time, earlier versions of a player's line and signals acknowledged and
older than the retention dropped; a player's last signal and everything unacknowledged kept). The review's P1s on
the store. - The Bridge's surface, bounded. Codes waiting for a confirmation live in memory only (1000 at most, swept),
where 4.1.1 appended and fsync'd a journal line for every anonymous request. Per address: 60 requests a minute on
the routes anyone may call, and 60 failed authentications a minute before every request of that address is
refused for a while (Retry-After);serve --trust-proxyreadsX-Forwarded-For. A proposal's token is checked
before any player is named (a 401 or 403 before any 404; the manifest, public, still answers 422 first). A capability lives 180 days at most from its
pairing, renewals included.POST /v1/unlink(Bearer capability): the pause menu's "Unlink" revokes the link on
the Bridge, not only on the device, and a reconnection planned before it no longer brings the old link back. The
operator's token is compared in constant time.initwrites the Biscuit root's private half toroot.key, read by
grantonly (servenever loads it; a 4.1.1config.jsonstill works); the demonstration webhooks' tokens live 30
days (--demo-days), andinit --no-demo-webhooksleaves them out for a Bridge on the Internet. The review's
remaining P1s; its key-pinning ask is answered indocs/dev/THREAT-MODEL.mdinstead (the signing key lives on the
Bridge: a pin would not survive its compromise, and TLS already names the host). - A required signal's fallback is proved, not only named.
validatechecked that afallbackmatched a rule;
the content lint now checks that the closed witness plays it, and saysfallback-unplayed(a warning) when the
game finishes without the world outside by another route: the declared rule is not the way through, and may be
dead. The review's P1 on the fallback proof, as a lint rather than a validation error: the closed proof still
holds, the declaration is what is wrong. - What a signal rests on is falsified like what a save rests on. Mutation testing gains a
realityset
(npm run test:mutation:core -- --set=reality: the engine's receive, the protocol, the client, the Bridge, its
store, its lock and its policy, judged by the Reality and Bridge tests) and a gate by identity: every surviving
mutant is named indocs/dev/mutants.json(file, operator, from, to, why), and a survivor not named there fails
the run in every mode, where 4.1.1 compared a count. Measured on the reality set: 382 of 484 killed, 14
equivalents named, 88 survivors listed as missing tests indocs/dev/MUTANTS.md(the nightly runs the set
without gating until they are killed or named). The corpus gains seven cases (the exact edges of the clock
tolerance, a header that is a list or null, a payload that is JSON but not an object), and one found a divergence:
a list header wasalgorithmin JavaScript andheaderin Rust; it isheaderin both. Coverage includes
bridge/srcwith floors. Properties at
random (fast-check) on concurrent proposals: sequences 1..n and one acceptance perdedupeKeywhatever the
interleaving; a write that fails (disk full) leaves nothing behind and the next proposal takes the same sequence.
The packageweb-scumm-bridgeis now one JavaScript module (esbuild) plus its Datalog policies: Node 22,
Biscuit's WebAssembly and zod, notsx, no TypeScript at run time;fresh-installinstalls it from its tarball
outside the repository, runsinit --no-demo-webhooksanddoctor, starts it and reads its keys; the release
carries the Bridge's own SBOM and attests every file it uploads. The Studio's write routes accept aHostthat
names this machine (localhost,127.0.0.1,[::1], or the LAN addressdev:lanserves) and nothing else, so a
page that rebinds a name to the Studio's port finds the door closed.npm run sealno longer prints the password.
Manual passes (D12: reported, not blocking)
11 of 16 done.
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
|---|---|---|---|---|
| 1 | two proposals for one player can share a sequence, or a dedupeKey be accepted twice |
one at a time per player under a lock; 100 concurrent proposals, 50 with one key, a proposal overlapping a revocation, a code confirmed twice, random interleavings (tests/bridge.test.ts) |
||
| 2 | a save's cursor can be acknowledged for a player that never applied those signals | the save is bound to its player on the first signal; a save under another link is mismatch, nothing applied or acknowledged; relinked on request (tests/reality-engine.test.ts) |
||
| 3 | a rotation can strand a player | what waits is signed again under the current key; a client asks for the keys once; five minutes of clock tolerance, the same in Rust (tests/bridge.test.ts rotation, the conformance corpus, 32 cases) |
||
| 4 | a stream can outlive a revoked or expired link, or a reader that stopped | closed by the Bridge on revocation, expiry and 64 KB unsent; four per player (tests/bridge.test.ts) |
||
| 5 | anyone can make the journal grow, or a bad token learn which players exist | codes in memory, 1000 at most; 60 anonymous requests and 60 failed authentications per address and minute; the token before any player (tests/bridge.test.ts) |
||
| 6 | a torn journal line keeps the Bridge from starting, or a deletion takes another player's line | the last fragment dropped and reported, any other bad line refused with its number; deletion by reading each event (tests/bridge.test.ts) |
||
| 7 | a required signal's fallback is a rule nobody plays | fallback-unplayed from the closed witness (tests/reality-proof.test.ts) |
||
| 8 | the Bridge's package needs TypeScript at run time, or was never installed outside the repository | one JavaScript module; fresh-install installs it from its tarball, runs init, doctor and serve, reads its keys (CI) |
||
| 9 | the tests of what a signal rests on are not measured | mutation set reality: 382 of 484 killed, 14 equivalents named, 88 missing tests listed (docs/dev/MUTANTS.md), the nightly runs it |
||
| 10 | a page can rebind its name to the Studio's port and write | the Studio answers its own names only (tests/studio-security.test.ts) |
||
| Pass | Status | Who, when | Device, OS, browser, versions | What failed |
A Bridge deployed behind HTTPS with --trust-proxy, a game on a phone linked to it, a rotation while it is open |
not done | |||
| A real webhook or email connector sending a signal | not done | |||
The seven field passes (docs/en/FIELD.md) |
not done | |||
| The 88 surviving mutants of the reality set killed or named | not done | |||
Signed tag (git tag -s) |
not done |