Say what is true, without dating it

Comments and docs kept dating themselves: when a constraint was set,
when a pair was verified, what a limit was before some day, which day a
subtree was split out. A reader wants the current state, and the date
goes stale faster than the sentence around it.

Kept, because they are data rather than narration: ISO timestamps in
test fixtures, the certificate expiry in project.yml, which an operator
acts on, and docs/BUGS-2026-09-01.md, whose name is the record.

⚠️ The first pass collapsed indentation. A whitespace rule meant for
prose matched YAML list items and markdown continuation lines, and
project.yml stopped parsing: "did not find expected key: - path:
Sources". The tests still passed, because they ran against the project
generated before the break, which is exactly how this would reach
somebody else. Reverted and redone without touching leading whitespace,
then xcodegen was run to prove the file parses.

764 XCTest and 75 Swift Testing pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-09 09:27:20 -04:00
co-authored by Claude Opus 5
parent 013eb5266e
commit 15b9d5b44a
17 changed files with 39 additions and 39 deletions
+2 -2
View File
@@ -32,7 +32,7 @@ unit-tests:
# that script copies it into the generated workspace. Running xcodegen
# directly here meant CI resolved every dependency fresh against the
# network while local builds were correctly pinned, which is why this only
# ever broke on CI. It broke on 2026-08-31 when upstream MOVED the
# ever broke on CI. It broke when upstream MOVED the
# `151.0.0` tag of stasel/WebRTC, a prebuilt XCFramework, from
# 19aa8c1f to 3e43fa9b. The pin is what noticed.
# The protocol vectors are copied into `Tests/Vectors`, so check the
@@ -40,7 +40,7 @@ unit-tests:
# checkout because that is exactly how Android lost twenty conformance tests
# its engine looks for `../../../flamenet-e2e/...`, which exists on a
# developer's machine and never in a build directory, so its suite failed
# here from the day it was written until 2026-09-06 without anyone seeing
# here from the day it was written without anyone seeing
# it. A copy in the test bundle cannot be absent. What a copy CAN do is go
# stale, so this is the other half of that bargain.
#
+3 -3
View File
@@ -1,4 +1,4 @@
# Export compliance, v0.1 (decided 2026-08-19)
# Export compliance, v0.1
Answer on the App Store Connect encryption questions:
@@ -15,7 +15,7 @@ Answer on the App Store Connect encryption questions:
implemented over Apple CryptoKit primitives.
**The post-quantum algorithms were added after this document was first
written and were missing from it until 2026-09-01.** Both are NIST
written and were missing from it.** Both are NIST
standards implemented via CryptoKit's `MLKEM768` and `MLDSA65`, nothing
proprietary, so the answer above does not change, but a declaration that
omits an algorithm the binary actually contains is inaccurate, and this one
@@ -24,7 +24,7 @@ Answer on the App Store Connect encryption questions:
- Classification: **mass-market, self-classified 5D992.c** (ECCN), License
Exception ENC favorable treatment not required for mass-market.
**Plist key note (learned 2026-08-19):** do NOT set
**Plist key note :** do NOT set
`ITSAppUsesNonExemptEncryption` alone. ASC rejects the upload with "Invalid
Export Compliance Code" because a non-exempt declaration in the plist must be
accompanied by an `ITSEncryptionExportComplianceCode` that matches compliance
+9 -9
View File
@@ -3,7 +3,7 @@
Verified by driving the apps and the relays, not by reading code. Where
something is unproven it says so.
**Re-checked 2026-08-28**, 45 commits after `baseline-2026-08-27`. Three rows
**Re-checked** 45 commits after the baseline. Three rows
moved. What was re-run this time is marked; anything not marked is carried
forward from the baseline and has not been re-driven.
@@ -44,14 +44,14 @@ Repeatable via `Scripts/android-signup.sh` and `RelayTypesUITests`.
## 3, cross-relay messaging
**The relays do it.** `test/federation.mjs` (in [flamenet-e2e](https://gitlab.com/paulhitt/flamenet-e2e) since the 2026-08-28 split) against the live fleet:
**The relays do it.** `test/federation.mjs` (in [flamenet-e2e](https://gitlab.com/paulhitt/flamenet-e2e) since the split) against the live fleet:
**11 passed, 0 failed**: relay1 fetches bob's devices from relay2, sends
across the boundary, bob's relay accepts exactly one envelope, he polls and it
is there, it carries no sender attribution across the boundary, a relay off
the allowlist is refused, and an unknown address is reported rather than
silently dropped.
Re-run 2026-08-28 against the live fleet: 11 passed, 0 failed. Unchanged.
Re-run against the live fleet: 11 passed, 0 failed. Unchanged.
iOS now has the client path; Android still does not. `E2EClient` gained
`remoteDevices`, `remoteBundles` and `sendRemote`, and `E2EInbox` gained
@@ -71,17 +71,17 @@ for.
## 8, deployed
**Deployed and current, verified by checksum.** Re-checked 2026-08-28 after
**Deployed and current, verified by checksum.** Re-checked after
two deploys: `Config.swift`, `RoutesAccount.swift` and `Retention.swift` on the
box are md5-identical to `flamenet-relay` HEAD. Production has the signup rate
limit (verified live: five signups pass, the sixth is 429), the reset-token
sweep, and retention logging at a level that is actually visible.
Since the 2026-08-28 split, deploys rsync from a `flamenet-relay` checkout,
Since the split, deploys rsync from a `flamenet-relay` checkout,
`flamenetmessenger/server/` no longer exists.
Production also runs `FN_LOG_LEVEL=error`, which suppresses the hourly
retention line entirely. Retention *is* working, verified 2026-08-28 by
retention line entirely. Retention *is* working, verified by
querying the database directly and finding zero rows past any window, oldest
envelope hours old, but there is no way to see that from the logs, which is
the same unverifiable claim the retention code says it exists to avoid.
@@ -107,11 +107,11 @@ Continue by design.
Not exercised: a payment taken end to end. Doing so means creating a
real PaymentIntent on the live connected account, so it was left alone. Closed
on the owner's call 2026-08-27. Recorded here rather than dropped, because "we
on the owner's call. Recorded here rather than dropped, because "we
verified the donation page" and "we watched a payment complete" are different
claims and only one of them is true.
## Found after the baseline, 2026-08-28
## Found after the baseline
**Android silently lost messages.** A send to somebody with no registered
device, anyone signed out, looped over an empty device list, made no request,
@@ -140,4 +140,4 @@ by five tests.
- Call audio has never been heard by anyone.
- No third-party audit, no reproducible builds, no post-quantum below iOS 26.
- `FN_DELETE_ON_ACK` is **off** and must stay off until it has an end-to-end
test. It deleted a message on production on 2026-08-27.
test. It deleted a message on production.
+1 -1
View File
@@ -1,6 +1,6 @@
# Delivery without Google or Apple in the middle
> **Constraint set by the owner, 2026-08-26:** no third party on the delivery
> **Constraint set by the owner:** no third party on the delivery
> path. That rules out FCM and rules out APNs. It does not rule out fast
> delivery, but it does rule out one thing completely, and that is worth
> stating before anything else.
+2 -2
View File
@@ -27,7 +27,7 @@ pipeline:
| [flamenet-relay](https://gitlab.com/paulhitt/flamenet-relay) | The server |
| [flamenet-messenger-android](https://gitlab.com/paulhitt/flamenet-messenger-android) | The Android app |
Until 2026-08-28 those three lived here as subtrees, and the public
those three lived here as subtrees, and the public
repos were rsync extracts of them, refreshed by hand. The extracts drifted for
days at a time. They are standalone now, split out with their history, work on
each product happens in its own repository, and a patch to a subtree here has
@@ -126,7 +126,7 @@ a first TestFlight build.
All four repositories are public under the MIT licence, this one included.
That was not always true: the app was private while the engine and server were
mirrored out of it by an allowlist, and until 2026-08-28 those mirrors were the
mirrored out of it by an allowlist, and those mirrors were the
only public copies. They are standalone repositories now, so what you are
reading is the source rather than an extract of it.
+4 -4
View File
@@ -1,6 +1,6 @@
# Flamenet Messenger, scoping the first TestFlight
> **Status 2026-08-21.** Step 1 is done, all seven stub surfaces were cut in
> **Status.** Step 1 is done, all seven stub surfaces were cut in
> MR !4 (`67cebad`), 356 deletions, three tabs, verified in the simulator. Most
> of the distribution prerequisites below are done too. **What now gates a
> TestFlight build is deploying the relay**, because the app has been pinned to
@@ -10,7 +10,7 @@
> not serve and answered 404 on one of the app's three tabs. It is an
> exact-username search now.
Written 2026-08-18 against `main` @ `de23972`. Build verified: `xcodegen` +
Written against `main` @ `de23972`. Build verified: `xcodegen` +
simulator build **succeeds**: 63 Swift files, 12,574 lines.
The app has never shipped, and the landing page still carries a `TODO` waiting
@@ -88,7 +88,7 @@ Roughly a day's work, almost all deletion. Nothing in the messaging or E2E path
These block a build reaching anyone, no matter what the app contains.
> **Status 2026-08-21.** Five of the seven are resolved. What is left is one
> **Status.** Five of the seven are resolved. What is left is one
> thing only I can do and two that need you.
| # | Prerequisite | State |
@@ -103,7 +103,7 @@ These block a build reaching anyone, no matter what the app contains.
A prerequisite that did not exist when this was written: the app had no
backend. It defaulted to `https://flamenet.io`, whose plugin was removed on
2026-08-20. It is pinned to `messenger.flamenet.io` now, which means the relay
It is pinned to `messenger.flamenet.io` now, which means the relay
has to be deployed before a tester can do anything at all. DNS for that name
does not resolve yet. `server/DEPLOY.md` has the sequence.
+1 -1
View File
@@ -12,7 +12,7 @@ hours.
is covered by the XCTest suite (95 checks), alongside the shared 253-check relay conformance suite and
cross-platform interop tests that run the real engine on both sides.
An internal adversarial audit on 2026-08-29 found and fixed a family of
An internal adversarial audit found and fixed a family of
sender-attribution flaws: the ratchet authenticates a *device*, and the user id
inside a sealed envelope is a claim its author chooses, the two were not bound.
Devices now bind to their user on first proven use, control traffic (delivery
+2 -2
View File
@@ -11,7 +11,7 @@ app is visible the moment somebody opens the screen. These are not:
- **Envelope kinds.** A kind one app sends and the other cannot parse is
swallowed as unknown control and shows nothing at either end. The sender is
not told. Bridge sharing had exactly this shape for a few hours on
2026-09-06, between the Android half landing and the iOS half.
Between the Android half landing and the iOS half.
- **Unwired capability.** A method on the view model that no screen calls.
`shareBridge` shipped that way the same day: you could receive a bridge and
had no way to send one.
@@ -29,7 +29,7 @@ see that a button is behind a screen nobody can reach, that a toggle is stored
and ignored, or that a route returns the wrong thing. Those need the app.
It also does not compare *features*, the settings each app offers, the screens
each has. That part is manual, and the 2026-09-06 run found eight preferences
each has. That part is manual, and that run found eight preferences
on iOS with no Android counterpart, which no automated check here would have
raised.
+1 -1
View File
@@ -193,7 +193,7 @@ enum E2EAttachment {
let fwd: Flag?
/// A flag written as `1` by this engine and the specification, and as
/// `true` by builds of the Android app before 2026-08-30.
/// `true` by older builds of the Android app.
struct Flag: Codable, Equatable {
let on: Bool
init(on: Bool) { self.on = on }
+1 -1
View File
@@ -10,7 +10,7 @@ final class MessengerAPI {
self.client = client
}
// The plaintext transport is gone (2026-08-29).
// The plaintext transport is gone.
/// `POST flamenet/v1/messenger/status`, backs `/away`, `/back`,
/// `/invisible` and `/status`.
+3 -3
View File
@@ -74,7 +74,7 @@ contact permanently unmessageable on Android once.
**The exposure is closed.** The burned passphrase opened exactly two
accounts, `appreview1demo` and `appreview2demo`, and both were deleted
through the app's own `DELETE /messenger/account` route on 2026-08-28, so the
through the app's own `DELETE /messenger/account` route, so the
device rows, prekeys, envelopes, group memberships and buddy edges went with
them. Signing in with the old value now returns 401.
@@ -93,7 +93,7 @@ Still cruft, though not exposed: `fndemo1`, `fndemo2`, `fnreview1demo`,
`fnreview2demo` are three superseded generations of review account. Worth
tidying, not urgent.
**How this pair was verified, 2026-08-28.** Registration and sign-in
**How this pair was verified.** Registration and sign-in
round-tripped against the live account service with an independently
implemented KDF (byte-compared against the app's); the app's own client stack
signed in via a unit-test probe with the passphrase supplied as bytes; and both
@@ -189,7 +189,7 @@ Recurring: the annual **BIS self-classification report**, due each February.
## 5. Before you hit submit
- [x] Both demo accounts verified signing in through the app UI (2026-08-26).
- [x] Both demo accounts verified signing in through the app UI.
- [ ] Walk the five steps above once on a real iPhone. Sign-in is proven; the
full send-and-read round trip on one physical device is not.
- [ ] `sudo xcode-select -s /Applications/Xcode.app`, you are pointed at
+1 -1
View File
@@ -74,7 +74,7 @@ final class AutomaticKeyLogTests: XCTestCase {
override func setUp() {
super.setUp()
LogRelay.reset()
// The pre-2026-08-30 global pin is still read as a fallback, so one
// The older global pin is still read as a fallback, so one
// test's pin would otherwise be checked against the next test's head
// and reported as a fork.
UserDefaults.standard.removeObject(forKey: "fnmsg.keyLogSize")
+1 -1
View File
@@ -23,7 +23,7 @@ final class PresenceTests: XCTestCase {
}
/// A relay may serve a state this build predates, `busy` was exactly that
/// for every client before 2026-08-29.
/// for every older client.
func testUnknownServerValueReadsAsOffline() {
XCTAssertEqual(Presence(serverValue: "busy"), .busy)
XCTAssertEqual(Presence(serverValue: "telepathic"), .offline)
+1 -1
View File
@@ -9,7 +9,7 @@ that is how Android lost twenty tests.** The Android engine looks for
`../../../flamenet-e2e/test/vectors/...`, which exists on a developer's
machine and has never existed in a CI build directory, so its
conformance suite failed there from the day it was written until
2026-09-06, invisibly, because a build cache replayed local passes. A
invisibly, because a build cache replayed local passes. A
copy inside the test bundle cannot be absent, cannot depend on what else
is checked out beside it, and needs no network.
+2 -2
View File
@@ -6,7 +6,7 @@ an fnid is derived from a passphrase, and this document is the plan for making
"my relay died and I kept my account and my contacts" a feature somebody can
use, not an architecture diagram.
Written 2026-08-28, from the code and the live fleet, not from memory.
Written, from the code and the live fleet, not from memory.
## What is already built AND proven
@@ -31,7 +31,7 @@ More than the roadmap credits. All of this is running today:
- **Proven against the live fleet**: `test/multihoming.mjs`. Bob registers
the same account key and same device identity at two relays, publishes the
record, and Alice, naming only one relay, reaches him at both.
**12 passed, 0 failed**, run 2026-08-28.
**12 passed, 0 failed**, run.
## The insight that makes migration small
+3 -3
View File
@@ -17,7 +17,7 @@ Ordered by what is being built now, then by what is agreed and waiting.
## Agreed, waiting
### Group sends need the outbox too, done on both, 2026-08-28
### Group sends need the outbox too, done on both
Same rules both platforms: exactly the unreached members are queued, entries
carry the group thread they restate, and the bubble flips only when the LAST
@@ -68,7 +68,7 @@ of every decentralised group protocol.
### Relay migration, the flagship, see docs/MIGRATION.md
Surveyed 2026-08-28 and far closer than this entry believed: signed,
Surveyed and far closer than this entry believed: signed,
serial-numbered home-relay records with rollback protection are implemented,
the sending relay already verifies and fans out to every home, and
`multihoming.mjs` proves the whole server half against the live fleet (12/0).
@@ -244,7 +244,7 @@ cannot be rotated.
## Account lifecycle and anti-abuse
Measured on the live relay, 2026-08-27, not estimated:
Measured on the live relay, not estimated:
| | |
|---|---|
+2 -2
View File
@@ -13,7 +13,7 @@ options:
settings:
base:
# 1.0 for the first App Store submission (2026-09-07). App Store Connect
# 1.0 for the first App Store submission. App Store Connect
# refuses a version string that disagrees with the uploaded binary, so this
# is what the listing has to say too.
#
@@ -157,7 +157,7 @@ targets:
Flamenet can save a story you posted yourself to your photos, so you
keep it after it disappears for everyone else. It only ever adds;
it cannot see the photos you already have.
# Export compliance (decided 2026-08-19): non-exempt, mass-market
# Export compliance: non-exempt, mass-market
# self-classified 5D992.c, see EXPORT-COMPLIANCE.md. Answered
# interactively in App Store Connect for now: embedding
# ITSAppUsesNonExemptEncryption without a matching