Take the e2e harness that reads the stylesheet off the page

The conformance job pinned flamenet-e2e at cbdadd9, whose landing-page check
fetched /landing.css by name. This branch renamed the stylesheet to
portal.css, so the check went 404 and the job failed on a change that had
not broken anything.

flamenet-e2e!43 makes that check read the href out of the markup, so a
rename cannot break it again. The pin moves to e2e main at d01cd0b to pick
it up.

Moving the pin also takes the spec that merged in e2e meanwhile, so
spec.leaf is regenerated with Scripts/build-doc-pages.sh. The other eight
generated pages came out identical. This is the atomic shape the conformance
job's comment asks for: bump the ref, regenerate the pages, make whatever
change the new harness expects.

Scripts/check-doc-pages.py reports all nine pages matching their sources,
and the public page suite passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-12 12:57:29 -04:00
co-authored by Claude Opus 5
parent b3a2f9b943
commit 232f2d5f1b
2 changed files with 158 additions and 2 deletions
+1 -1
View File
@@ -1 +1 @@
cbdadd9f82c2b64f2c0eb1b08b8ff2e88e48a5a7
d01cd0bbb267c591fd2ba7de7794f4bf461019cb
+157 -1
View File
@@ -3,7 +3,7 @@
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/docs/SPEC.md.
Edits made HERE are deleted the next time that script runs.
Change the markdown source and re-run it.
source-sha256: 7e3a8724ef27a6da -->
source-sha256: 9c3e570b61488cd7 -->
<article class="band doc doc-generated">
<div class="wrap wrap-narrow">
<h1 id="flamenet-messenger-end-to-end-encryption-spec">Flamenet
@@ -1405,6 +1405,162 @@ promise was made there is nothing to report against.</p>
<p>A client SHOULD send at most one notice per conversation per short
window. A screenshot often produces several platform events, and a
person who took one screenshot should not appear to have taken four.</p>
<h3 id="628-off-the-record">6.28 Off the record</h3>
<p>A conversation both ends agree not to write down. While it is on,
neither client persists the messages: they live in memory for as long as
the app is open and are gone when it closes. Nothing is deleted
afterwards, because nothing was ever written.</p>
<pre class="json"><code>{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;otr&quot;,&quot;ctl&quot;:1,&quot;otr&quot;:1}
{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;otr&quot;,&quot;ctl&quot;:1,&quot;otr&quot;:1,&quot;echo&quot;:1}
{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;otr&quot;,&quot;ctl&quot;:1,&quot;otr&quot;:0}</code></pre>
<p><code>ctl</code> is set (§6.10), so a build that does not know
<code>otr</code> ignores it rather than rendering it, and, critically,
never turns the mode on. That is why it is a <strong>proposal and an
echo</strong> rather than a single announcement. The proposing client
sets the mode locally only when the echo comes back. A client that
receives a proposal and supports the mode turns it on and replies with
the same envelope carrying <code>echo</code>, which MUST NOT itself be
echoed.</p>
<p>The handshake exists because the alternative is worse than the
feature. If one side believed the conversation was off the record and
the other was writing it to disk, the promise would be false in exactly
the case somebody relied on it. So the mode is never on for one end
alone: a proposal that is not echoed leaves both ends exactly as they
were, and the proposing client MUST say the other end did not agree
rather than showing the mode as on.</p>
<p>Turning it off (<code>"otr":0</code>) needs no echo. Either end may
stop at any time, and stopping is not something the other end can
refuse.</p>
<p><strong>What it does not do.</strong> It stops this app writing the
conversation to this device's storage. It does not stop a screenshot
(§6.27), a photograph of the screen, a modified build, or a platform
backup of anything that reached the screen. A client MUST describe it as
"neither phone writes this down" and MUST NOT describe it as messages
that cannot be kept.</p>
<p>Messages sent while it is on are ordinary envelopes. Nothing on the
wire marks them, and the relay learns nothing: the mode is a rule about
what the two clients do with plaintext they have already decrypted.</p>
<h3 id="629-polls-nobody-can-attribute">6.29 Polls nobody can
attribute</h3>
<p>A poll asked of a group where no individual vote is ever sent to
anybody, including the person who created it and including the
server.</p>
<p>Hiding a vote in the interface is not hiding it. In every messenger
that offers an "anonymous" poll today, the vote reaches somebody: a
server, or the poll's creator, or both. Group messages here are
encrypted to each member separately, so the recipient of any envelope
knows who sent it, and sending "I chose B" to anybody at all attributes
it.</p>
<p><strong>Secure aggregation.</strong> A vote is split into random
pieces that sum to it, one piece per member. Each member is handed one
piece of everybody's vote, publishes only the sum of the pieces they
hold, and the published sums add up to the counts.</p>
<pre class="json"><code>{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;poll&quot;,&quot;ctl&quot;:1,&quot;pid&quot;:&quot;&lt;uuid&gt;&quot;,&quot;q&quot;:&quot;&lt;question&gt;&quot;,&quot;opts&quot;:[&quot;&lt;a&gt;&quot;,&quot;&lt;b&gt;&quot;],&quot;mset&quot;:&quot;&lt;member set&gt;&quot;,&quot;gid&quot;:&quot;&lt;group&gt;&quot;}
{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;pshare&quot;,&quot;ctl&quot;:1,&quot;pid&quot;:&quot;&lt;uuid&gt;&quot;,&quot;v&quot;:[1732584193,-1732584192],&quot;gid&quot;:&quot;&lt;group&gt;&quot;}
{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;psum&quot;,&quot;ctl&quot;:1,&quot;pid&quot;:&quot;&lt;uuid&gt;&quot;,&quot;v&quot;:[-604832771,604832772],&quot;gid&quot;:&quot;&lt;group&gt;&quot;}</code></pre>
<p><code>ctl</code> is set on all three (§6.10), so a build that does
not know them ignores them rather than rendering a poll it cannot take
part in.</p>
<p><strong>The vector.</strong> <code>v</code> is one signed 32-bit
integer per option, in the order <code>opts</code> gives. A vote for
option <code>i</code> is the vector that is 1 at <code>i</code> and 0
elsewhere. Arithmetic wraps at 32 bits, and the wrapping is what does
the hiding: every piece but the last is a uniform random integer, the
last is whatever makes the set add up to the vote, and no single piece
says anything about the vote it came from. Implementations MUST use
wrapping addition over the full 32-bit range. A language that traps on
overflow MUST mask rather than throw.</p>
<p><strong><code>mset</code></strong> names the members the arithmetic
is agreed over, and every client MUST compute its pieces against that
list in that order. Two clients disagreeing about who is in the poll
produce sums that do not add up, and the result is a wrong tally rather
than an error, which is the failure this field exists to prevent.</p>
<p><strong>The guarantee, exactly.</strong> Every member other than the
voter would have to collude to recover one vote. That is what this
provides. It is not "nobody can ever know", and a client MUST state the
collusion bound rather than the shorter claim.</p>
<p>The bound is worth reading carefully at small sizes. Against a group
of two it is empty: the other member holds a piece of your vote and sees
the tally, and subtracting one from the other gives your vote with no
collusion at all, because there is nobody to collude with. The
arithmetic has no threshold to enforce this at, so a client MUST NOT
offer the word "anonymous" for a poll of two, and SHOULD set a floor
below which it does not offer one.</p>
<p><strong>Who voted is visible; what they voted is not.</strong>
Holding a piece of somebody's vote is how a member learns that person
has voted, and a client needs that to know when the poll can proceed.
The scheme hides the content of a vote, never the fact of one. A client
MUST NOT present participation as secret, and MAY show who has voted,
which is also the only honest way to say who a poll is still waiting
on.</p>
<p><strong>Everybody publishes, or there is no tally.</strong> A missing
sum does not give a smaller count. It leaves a uniform random number in
the total, so a partial result is not a partial answer but a meaningless
one. A client MUST wait for a sum from every member in <code>mset</code>
and MUST NOT show a result before then. It MUST say plainly that it is
waiting, and on whom.</p>
<p>There is deliberately no fallback to a poll that is merely hidden. A
fallback that kept the word "anonymous" while quietly sending votes to
somebody is the one outcome that must not ship, and offering it under
another name in the same interface invites exactly that confusion.</p>
<p><strong>Limits.</strong> Share traffic grows with the square of the
membership: every member sends a piece to every member. A client MUST
NOT start a poll in a group larger than it can serve and MUST say so
rather than starting one that will never complete. <code>opts</code>
carries at least 2 and at most 12 options.</p>
<h3 id="630-handing-a-call-to-another-of-your-devices">6.30 Handing a
call to another of your devices</h3>
<p>On a call on the phone, walk to the desk, take the call on the
tablet, and keep talking. The other person hears a short gap and nothing
else.</p>
<p>This account's own devices already have a private channel during a
call: the device that answers tells the others to stop ringing (§6.8).
Handoff is that channel carrying more.</p>
<pre class="json"><code>{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;call-handoff&quot;,&quot;call&quot;:{&quot;id&quot;:&quot;&lt;uuid&gt;&quot;,&quot;peer&quot;:4211,&quot;video&quot;:false}}
{&quot;fnc&quot;:1,&quot;kind&quot;:&quot;call-handoff-taken&quot;,&quot;call&quot;:{&quot;id&quot;:&quot;&lt;uuid&gt;&quot;}}</code></pre>
<p>Both go to this account's own devices and to nobody else, by the same
routing check the stop-ringing path already makes. Neither is ever sent
to the peer.</p>
<p><strong>The sequence.</strong></p>
<ol type="1">
<li>The device on the call offers it to this account's other devices,
naming the call and the peer. Only an idle device offers to take
it.</li>
<li>The taking device builds a fresh connection and sends the
<strong>peer</strong> a <code>call-offer</code> under the same
<code>call.id</code>.</li>
<li>The peer treats an offer for a call it is already on as a
renegotiation: it answers, stays on the call screen, and logs one call
rather than two.</li>
<li>The taking device sends <code>call-handoff-taken</code> to its own
devices. The original tears down its connection and sends the peer
nothing.</li>
</ol>
<p><strong>The peer is never hung up on.</strong> No
<code>call-end</code> is sent to the peer at any point during a handoff,
in either direction. An end would make the far end drop the call and
redial, which is the failure this whole sequence exists to avoid. The
original device MUST tear down locally and silently.</p>
<p><strong>A handoff that half happens leaves the original
working.</strong> If no device takes the call within a few seconds, the
offer expires on both sides and the call stays exactly where it is.
Expiry is a timeout at both ends rather than a withdrawal envelope, so a
lost withdrawal cannot leave a device believing an offer is still
live.</p>
<p><strong>Whether the peer can take a renegotiating offer is known
before the handoff starts.</strong> A client that accepts a
renegotiating <code>call-offer</code> sets <code>"rnv":1</code> on the
<code>call-offer</code> and <code>call-answer</code> it sends, so both
ends learn it while the call is being set up. A client MUST NOT offer a
handoff for a call whose peer did not set it. Discovering that the far
end cannot renegotiate by dropping the call is the wrong way to find
out.</p>
<p>A device that does not understand <code>call-handoff</code> ignores
it, which is correct: it does not offer to take the call, and the call
stays where it is.</p>
<p><strong>The relay learns nothing new.</strong> These ride the ratchet
like every other call signal, and the same envelopes to this account's
own devices already exist for stopping a ring.</p>
<h2 id="7-safety-numbers-verification">7. Safety numbers
(verification)</h2>
<p>For a local identity <code>IK_sig_local_pub</code> and remote