Keep the landing page grouped, and regenerate the spec pages
The new features went onto the landing page as three more cards, which took it to nine in a flat list and tripped relay#44: the rule is to group by what somebody is trying to do rather than add another card to the pile. The cards there are the three products sharing one lock, and these are not a fourth product, so they read as prose in their own band and the card count goes back to six. The doc pages were stale against flamenet-e2e, which moved when the private-reaction flag and the call-reaction kind were specified. The pin is bumped to that commit and engine.leaf and spec.leaf are regenerated from it, which is the procedure .e2e-ref.md asks for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bd6428444f
commit
d6f0f345e0
@@ -1 +1 @@
|
||||
57732df84e8d94366374d7576a6d156cf703e432
|
||||
cbdadd9f82c2b64f2c0eb1b08b8ff2e88e48a5a7
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED by Scripts/build-doc-pages.sh from flamenet-e2e/README.md.
|
||||
Edits made HERE are deleted the next time that script runs.
|
||||
Change the markdown source and re-run it.
|
||||
source-sha256: 1d0581b4b6b91fcb -->
|
||||
source-sha256: 9b98fb4d38c257ca -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="flamenet-e2e">flamenet-e2e</h1>
|
||||
@@ -11,7 +11,9 @@
|
||||
Dependency-free JavaScript, WebCrypto only, runs in a browser or in Node
|
||||
20+.</p>
|
||||
<p>This is the reference implementation of <a
|
||||
href="docs/SPEC.md"><code>docs/SPEC.md</code></a>.</p>
|
||||
href="docs/SPEC.md"><code>docs/SPEC.md</code></a>, the engine behind
|
||||
Flamenet Messenger: <a
|
||||
href="/">https://flamenetmessenger.com</a>.</p>
|
||||
<h2 id="check-it-yourself">Check it yourself</h2>
|
||||
<pre><code>git clone https://gitlab.com/paulhitt/flamenet-e2e && cd flamenet-e2e
|
||||
npm test</code></pre>
|
||||
|
||||
@@ -93,34 +93,20 @@
|
||||
<p class="band-lede">
|
||||
Cada una es pequeña, y cada una funciona como funciona el resto de la app.
|
||||
</p>
|
||||
|
||||
<div class="cards cards-3">
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v10H9l-5 4z"/></svg></span>
|
||||
<h3>Una reacción que solo ve esa persona</h3>
|
||||
<p>
|
||||
En un grupo, reacciona a quien lo escribió en vez de a toda la sala.
|
||||
A nadie más se le envía.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4l4 1 1 4-2 2a12 12 0 0 0 5 5l2-2 4 1 1 4a2 2 0 0 1-2 2A17 17 0 0 1 3 6a2 2 0 0 1 2-2"/></svg></span>
|
||||
<h3>Reacciona durante una llamada</h3>
|
||||
<p>
|
||||
Di que sí sin interrumpir. Se ve un momento y no se guarda nada.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.5"/><path d="M3 3l18 18"/></svg></span>
|
||||
<h3>Fuera de registro</h3>
|
||||
<p>
|
||||
Los dos teléfonos dejan de escribir la conversación. Cierra la app y
|
||||
ya no está. Lo acuerdan los dos teléfonos, así que no puede estar
|
||||
activo para uno y no para el otro.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>Una reacción que solo ve esa persona.</strong> En un grupo,
|
||||
reacciona a quien lo escribió en vez de a toda la sala. A nadie más se le
|
||||
envía.
|
||||
</p>
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>Reacciona durante una llamada.</strong> Di que sí sin interrumpir.
|
||||
Se ve un momento y no se guarda nada.
|
||||
</p>
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>Fuera de registro.</strong> Los dos teléfonos dejan de escribir la
|
||||
conversación. Cierra la app y ya no está. Lo acuerdan los dos teléfonos,
|
||||
así que no puede estar activo para uno y no para el otro.
|
||||
</p>
|
||||
<p class="fineprint">
|
||||
En Android desde la versión 0.17. En iPhone, en la próxima compilación de
|
||||
TestFlight.
|
||||
|
||||
@@ -105,35 +105,19 @@
|
||||
<p class="band-lede">
|
||||
Each one is small, and each one works the way the rest of the app does.
|
||||
</p>
|
||||
|
||||
<div class="cards cards-3">
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v10H9l-5 4z"/></svg></span>
|
||||
<h3>A reaction only they see</h3>
|
||||
<p>
|
||||
In a group, react to the person who wrote it rather than to the room.
|
||||
Nobody else is sent it.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4l4 1 1 4-2 2a12 12 0 0 0 5 5l2-2 4 1 1 4a2 2 0 0 1-2 2A17 17 0 0 1 3 6a2 2 0 0 1 2-2"/></svg></span>
|
||||
<h3>React during a call</h3>
|
||||
<p>
|
||||
Say yes without interrupting. It shows for a moment and nothing is
|
||||
saved.
|
||||
</p>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.5"/><path d="M3 3l18 18"/></svg></span>
|
||||
<h3>Off the record</h3>
|
||||
<p>
|
||||
Both phones stop writing the conversation down. Close the app and it
|
||||
is gone. It is agreed between the two phones, so it cannot be on for
|
||||
one of you and not the other.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>A reaction only they see.</strong> In a group, react to the person
|
||||
who wrote it rather than to the room. Nobody else is sent it.
|
||||
</p>
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>React during a call.</strong> Say yes without interrupting. It
|
||||
shows for a moment and nothing is saved.
|
||||
</p>
|
||||
<p class="band-lede band-lede-spaced">
|
||||
<strong>Off the record.</strong> Both phones stop writing the conversation
|
||||
down. Close the app and it is gone. It is agreed between the two phones, so
|
||||
it cannot be on for one of you and not the other.
|
||||
</p>
|
||||
<p class="fineprint">
|
||||
On Android from version 0.17. On iPhone in the next TestFlight build.
|
||||
</p>
|
||||
|
||||
+118
-12
@@ -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: 8d1cd3d583818c4f -->
|
||||
source-sha256: 7e3a8724ef27a6da -->
|
||||
<article class="band doc doc-generated">
|
||||
<div class="wrap wrap-narrow">
|
||||
<h1 id="flamenet-messenger-end-to-end-encryption-spec">Flamenet
|
||||
@@ -391,6 +391,66 @@ metadata already leaks (§11).</p>
|
||||
<p>An unknown <code>kind</code> MUST render as a placeholder
|
||||
("unsupported attachment"), not be silently dropped, the sender meant to
|
||||
say something.</p>
|
||||
<p><strong>Additive top-level keys.</strong> These ride beside
|
||||
<code>kind</code> and <code>caption</code>, never inside
|
||||
<code>att</code>. All are optional; a receiver that does not know one
|
||||
ignores it. Each was on the wire before it was written down here, which
|
||||
is the gap <code>test/vectors/content-v1.json</code> now closes.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Type</th>
|
||||
<th>Meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>men</code></td>
|
||||
<td>array of user ids</td>
|
||||
<td>Who a group message is addressed to (§6.18).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fn</code></td>
|
||||
<td>string</td>
|
||||
<td>A document's own filename, for <code>kind: "file"</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cls</code></td>
|
||||
<td>integer</td>
|
||||
<td>The label a document was filed under; <code>0</code> means
|
||||
none.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cmp</code></td>
|
||||
<td>array of integers</td>
|
||||
<td>Compartments within that label.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>st</code></td>
|
||||
<td><code>1</code> or <code>true</code></td>
|
||||
<td>This envelope is about a story rather than a message (§6.20,
|
||||
§6.22).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gname</code></td>
|
||||
<td>string</td>
|
||||
<td>The group's name, carried by the sender (§6.7).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><code>fn</code>, <code>cls</code> and <code>cmp</code> are top-level
|
||||
rather than part of <code>att</code> deliberately: they describe what
|
||||
the document IS to the person receiving it, and a receiver that
|
||||
understands attachments but not labels must still be able to show the
|
||||
file.</p>
|
||||
<p><code>st</code> is written by at least one client as the integer
|
||||
<code>1</code> rather than the boolean <code>true</code>. Receivers MUST
|
||||
accept both. This is not a style note: several JSON libraries decline to
|
||||
coerce a number to a boolean, so a receiver reading only the boolean
|
||||
form answers <code>false</code> and silently files a story reaction
|
||||
against a message that does not exist. The same rule applies to
|
||||
<code>fwd</code> (§6.16).</p>
|
||||
<h3 id="67-groups-pairwise-fan-out">6.7 Groups (pairwise fan-out)</h3>
|
||||
<p>Groups add <strong>no new cryptography</strong>. A group message is
|
||||
the same content envelope, encrypted independently to every member
|
||||
@@ -476,11 +536,20 @@ member).</p>
|
||||
<hr />
|
||||
<h3 id="68-voice-calls">6.8 Voice calls</h3>
|
||||
<p>Calls use <strong>WebRTC</strong> for media (DTLS-SRTP encrypts the
|
||||
audio) and the <strong>ratchet as the signalling channel</strong>, three
|
||||
audio) and the <strong>ratchet as the signalling channel</strong>, four
|
||||
content-envelope kinds, all with <code>att</code> absent:</p>
|
||||
<pre class="json"><code>{ "fnc": 1, "kind": "call-offer", "call": { "id": "<uuid>", "sdp": "<offer sdp>" } }
|
||||
{ "fnc": 1, "kind": "call-answer", "call": { "id": "<uuid>", "sdp": "<answer sdp>" } }
|
||||
{ "fnc": 1, "kind": "call-end", "call": { "id": "<uuid>", "reason": "hangup" | "declined" | "busy" | "timeout" } }</code></pre>
|
||||
{ "fnc": 1, "kind": "call-end", "call": { "id": "<uuid>", "reason": "hangup" | "declined" | "busy" | "timeout" | "answered-elsewhere" | "declined-elsewhere" } }
|
||||
{ "fnc": 1, "kind": "call-reaction", "call": { "id": "<uuid>", "emoji": "<one emoji>" } }</code></pre>
|
||||
<p><strong>Reactions during a call.</strong> <code>call-reaction</code>
|
||||
carries one emoji for the call named by <code>call.id</code>, so
|
||||
somebody can agree without interrupting whoever is talking. A client
|
||||
accepts it only while that call is connecting or up, shows it briefly,
|
||||
and records it nowhere: it never becomes a message in a thread and never
|
||||
reaches the call log. <code>emoji</code> is absent on every other kind,
|
||||
so an older build sees the envelopes it always saw, and one that does
|
||||
not know the kind ignores it.</p>
|
||||
<p><strong>Why this is end-to-end.</strong> The SDP carries the DTLS
|
||||
certificate fingerprint that the media channel's keys are derived
|
||||
against. Because the SDP travels <em>inside the Double Ratchet</em>, the
|
||||
@@ -500,10 +569,20 @@ rings only while the recipient's app is polling. The caller SHOULD give
|
||||
up with <code>call-end</code> / <code>timeout</code> after ~45 s. A
|
||||
<code>call-offer</code> for a call already ended, or arriving while
|
||||
another call is active, is answered with <code>call-end</code> /
|
||||
<code>busy</code>. Multi-device: every device of the callee receives the
|
||||
offer; the first <code>call-answer</code> wins and other devices stop
|
||||
ringing when they see the winner's answer come back through their own
|
||||
poll (or the <code>call-end</code>).</p>
|
||||
<code>busy</code>.</p>
|
||||
<p><strong>Multi-device.</strong> Every device of the callee receives
|
||||
the offer, and the first <code>call-answer</code> to reach the caller
|
||||
wins. The answer goes to the caller only, so the device that answers or
|
||||
declines also sends <code>call-end</code> to its own account's other
|
||||
devices, with reason <code>answered-elsewhere</code> or
|
||||
<code>declined-elsewhere</code>. A device still ringing for that
|
||||
<code>call.id</code> stops and does not file the call as missed; a
|
||||
device already past ringing ignores it. These two reasons go only to the
|
||||
account's own devices, never to the other party. A device whose call has
|
||||
connected ignores a <code>call-end</code> with reason
|
||||
<code>declined</code>, <code>busy</code> or <code>timeout</code>: those
|
||||
come from one of the callee's other devices, which never picked up. The
|
||||
callee's own ring timeout ends the ring locally and sends nothing.</p>
|
||||
<p><strong>TURN.</strong> <code>GET /e2e/turn</code> (§8.10) returns
|
||||
time-limited credentials for the coturn relay. Clients put both STUN and
|
||||
TURN entries in their ICE configuration; media flows peer-to-peer when
|
||||
@@ -561,6 +640,18 @@ people turn a messenger off over. One reaction per sender per message, a
|
||||
second one replaces the first.</p>
|
||||
<p><code>gid</code> applies as elsewhere: a reaction on a group message
|
||||
carries the group id and routes to the group thread.</p>
|
||||
<p><strong>A reaction meant for one person.</strong> In a group, a
|
||||
reaction MAY be addressed to the author of the message alone:</p>
|
||||
<pre class="json"><code>{ "fnc": 1, "kind": "react", "caption": "<emoji>", "priv": 1, "gid": "<group>", "re": { "mid": "<target>", "author": 12, "preview": "" } }</code></pre>
|
||||
<p>It is sent to that author and to nobody else: no other member of the
|
||||
group receives an envelope at all. That is the whole guarantee.
|
||||
<code>priv</code> exists so the author's client can label what it
|
||||
received, and is NOT a request to other clients to hide something,
|
||||
because a request is not privacy. A client MUST keep these apart from
|
||||
the public tally rather than folding them into the same count, MUST NOT
|
||||
set <code>priv</code> on a reaction it sends to a whole thread, and MUST
|
||||
treat an envelope without the key as public, which is what an older
|
||||
build sends.</p>
|
||||
<p><strong>Status.</strong> Implemented in all three engines. iOS and
|
||||
JavaScript are checked against each other in both directions by
|
||||
<code>test/interop.mjs</code>, including a preview containing quotes and
|
||||
@@ -905,13 +996,28 @@ Kotlin engine and Android app, including the combining rule and
|
||||
per-message pruning on both.</p>
|
||||
<h3 id="618-mentions">6.18 Mentions</h3>
|
||||
<p>A group message MAY name the members it is addressed to, in
|
||||
<code>ment</code>:</p>
|
||||
<pre class="json"><code>{ "fnc": 1, "kind": "text", "gid": "<group>", "ment": [12, 34], "caption": "@Ada @Adam thoughts?" }</code></pre>
|
||||
<code>men</code>:</p>
|
||||
<pre class="json"><code>{ "fnc": 1, "kind": "text", "gid": "<group>", "men": [12, 34], "caption": "@Ada @Adam thoughts?" }</code></pre>
|
||||
<blockquote>
|
||||
<p><strong>This key was written <code>ment</code> here until 2026-09-11,
|
||||
and no client has ever sent that.</strong> Both shipped clients emit and
|
||||
read <code>men</code>, and have since mentions were added. The clients
|
||||
agreed with each other and only this document disagreed, so nothing
|
||||
broke and nothing could: the only thing that would have caught it is a
|
||||
conformance test written from the spec, and there was none. A third
|
||||
client built honestly from this page would have shipped mentions that
|
||||
silently never notified anybody.</p>
|
||||
<p>The key is now pinned by <code>test/vectors/content-v1.json</code>,
|
||||
generated by running a client's encoder rather than by reading it. The
|
||||
spec was corrected to the clients rather than the other way round: they
|
||||
are deployed, they agree, and changing the wire to match a document
|
||||
would break every installed copy to fix a typo.</p>
|
||||
</blockquote>
|
||||
<p>A mention decides whether the receiver is notified, and nothing else.
|
||||
It confers no permission, changes no state, and is not evidence of
|
||||
anything. This matters because <code>ment</code> is a claim by the
|
||||
sender like every other field in the envelope: the worst a dishonest one
|
||||
can achieve is a notification the reader is able to turn off.</p>
|
||||
anything. This matters because <code>men</code> is a claim by the sender
|
||||
like every other field in the envelope: the worst a dishonest one can
|
||||
achieve is a notification the reader is able to turn off.</p>
|
||||
<p>Which is the rule that makes mentions safe to have. A mention that
|
||||
always broke through a mute would hand every member of a group a way to
|
||||
interrupt somebody whenever they liked, which is how mentions become the
|
||||
|
||||
Reference in New Issue
Block a user