Relay 0.32: merge the release branch into main, with the badge column carried onto the new write path #256

Merged
extant2000git merged 14 commits from merge/relay-0-32-unify into main 2026-10-14 17:01:59 +00:00
14 Commits
Author SHA1 Message Date
Paul HittandClaude Opus 5 dca5333e9b BadgeCountTests follows the deposit path wave 2 left behind
ci / checks (pull_request) Successful in 22s
ci / swift (pull_request) Successful in 9m50s
ci / image (pull_request) Successful in 1m56s
ci / deploy (pull_request) Skipped
`Routes.placeDeposits` is gone: wave 2 split it into `resolveDeposits`, which
turns a peer's envelopes into `Delivery.Item`s, and `Delivery.write`, which
inserts them and then wakes and pushes. The test still called the old one and
the Linux test build stopped on "type 'Routes' has no member 'placeDeposits'".

It now does what the route does, in the same two steps, and still asserts the
same thing: a deposit is written with `announced` false, so the badge does not
count it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 12:47:33 -04:00
Paul HittandClaude Opus 5 ba68984eab TodayFile.Key holds the raw bytes, because CryptoKit's PublicKey is not Sendable on Linux
ci / checks (pull_request) Successful in 15s
ci / swift (pull_request) Failing after 4m10s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
The relay's `swift` job failed on this and `swift build` on macOS did not:

  TodayFile.swift💯 error: stored property 'key' of 'Sendable'-conforming
  struct 'Key' has non-sendable type 'Curve25519.Signing.PublicKey'

`Curve25519.Signing.PublicKey` is Sendable in Apple's CryptoKit and is not in
swift-crypto, which is what the Linux build uses. The struct now holds the 32
raw bytes, which are Sendable everywhere, validates them in `init` exactly as
before, and rebuilds the verifying key at the one place a signature is
checked. A key that passed `init` rebuilds, so the `?? false` is unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 12:41:10 -04:00
Paul HittandClaude Opus 5 b005993b95 Merge release/0-32 into main: /today.json, the /hi page, and the badge that counts messages
ci / checks (pull_request) Successful in 21s
ci / swift (pull_request) Failing after 3m54s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
14 conflict hunks across 8 files. Wave 2 landed on main while this branch sat,
and the two sides rewrote the same send path in different directions.

Routes.swift and RoutesFederation.swift: wave 2 extracted the write, the wake
and the push into `Delivery.write`, and this branch still had the inline
version plus one thing wave 2 had no idea about, the `announced` column. The
extraction is kept; `Delivery.Item` gains `announced`, defaulting false as
`Envelope.init` does, and `Delivery.insert` writes it. Without that the model,
the migration and `badgeCount` would all have merged cleanly and every badge
would have read zero, because nothing on the surviving path ever set the
column. The submit route passes `!silent`; a federated deposit passes false,
since nothing announced it.

docs/CONFIGURATION.md: the union. Main lists six outbound connections with the
setting that enables each; this branch added the Today fetch and dropped the
other five. Seven are listed now.

PostgresMigrationTests: both sides' tests, with the `LockedRows` actor left
last in the file.

.e2e-ref moves to d0274ca0, the current flamenet-e2e main. The old pin was
310c315, which was a branch head and not on main at all.

The three generated doc pages were not resolved by hand. `check-doc-pages.py`
reported spec, audit, engine and self-hosting all stale against their sources,
so `Scripts/build-doc-pages.sh` ran against the new pin and all nine pages now
match. That also fixes the spec page's section numbers: flamenet-e2e moved
anonymous reads and contact links to 8.13 and 8.14 to make room for the push
token and the Today file.

Eight more references renumbered by hand, in Wire, RoutesAnonymous,
RoutesDiscovery, ContactLinkPageTests and AnonymousReadsTests. The remaining
8.11 and 8.12 references are about push and the Today file and are correct.

`swift build` is clean on Swift 6.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 12:34:32 -04:00
Paul HittandClaude Opus 5.5 1ff23af40d Pin flamenet-e2e at the 0.32 spec and regenerate its pages
.e2e-ref moves to d07ed14 (docs/spec-0-32). spec.leaf and engine.leaf
are rebuilt from that commit with Scripts/build-doc-pages.sh (pandoc
3.11); the other seven pages come out unchanged, and
check-doc-pages.py reports all nine current.

The pinned commit has to be on the remote before this pipeline runs, so
the spec branch is pushed first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:02:14 -04:00
Paul HittandClaude Opus 5.5 e332ce83a1 /hi: lower the host by ASCII rules only, and refuse spaces
DESIGN 41. JavaScript's toLowerCase folds a Kelvin sign (U+212A) into
"k", so the page took relay.e<Kelvin>ample.com as relay.ekample.com, a
host both apps refuse. The host is now lowered A to Z only; any other
character stays as it is and fails the ASCII host check. Whitespace
anywhere in the address, around the @ included, is refused outright, and
the 400-character fragment cap stays.

Two JavaScriptCore tests: the Kelvin case (plain and percent-encoded),
which failed before this change, and spaces around the @, which the old
patterns already refused and now pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:10:43 -04:00
Paul HittandClaude Opus 5.5 3230d4c748 Cite the Today file as §8.12
§8.11 is push-token registration, which shipped relay and iOS code
already cite, so the spec gives the Today file §8.12 (DESIGN 34). Every
0.32 citation of the Today file moves to §8.12: TodayFile.swift,
Config.swift, Entrypoint.swift, TodayFileTests, docs/CONFIGURATION.md and
docs/API.md, with configuration.leaf and api.leaf rebuilt from the
markdown. The push-token citations stay §8.11.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:22:55 -04:00
Paul HittandClaude Opus 5.5 1fa259b079 Merge branch 'fix/badge-counts-announced-only' into release/0-32
The iPhone badge counts announced envelopes only, with the /metadata row
that says what is now kept on a waiting envelope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:02:25 -04:00
Paul HittandClaude Opus 5.5 5590587a98 Merge branch 'feature/0-32-today-and-hello' into release/0-32
The Today file at /today.json, the /hi page a hello link opens, and the
drafted /metadata line for the Today file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:02:25 -04:00
Paul HittandClaude Opus 5.5 d0e5cf858f Configuration: a closed relay fetches the Today file if told to
"A closed relay opens no outbound connections at all" stopped being true
once FN_TODAY_SOURCE existed. It now says the exception, and that the
fetch is all it opens one for. Wording approved by the owner; the
generated page is rebuilt from the markdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:02:10 -04:00
Paul HittandClaude Opus 5.5 b88baf7c94 /metadata: the silent flag is kept on the waiting envelope
The row said the flag is never written down. With the badge counting
announced envelopes only, it is kept, inverted, as one bit on the
envelope until the envelope goes. The English row is the owner's
approved wording. The Spanish row is the draft, marked in the page as
waiting for a native speaker's check before any deploy, so the old
Spanish line, false once this ships, does not go out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:01:46 -04:00
Paul HittandClaude Opus 5.5 e7345230bc The iPhone badge counts announced envelopes only
The badge was the number of undelivered envelopes for the device, and
delivery keys, receipts and answers wait in the same mailbox as messages,
so one message read as 2. Each envelope now carries one bit, announced,
set from the send's silent flag (urgent wins, as it does for the push),
and the badge counts envelopes that are undelivered and announced. A
deposit from another relay sends no push, so it is not announced. The bit
lives in the row and goes with it.

AddEnvelopeAnnounced adds the column NOT NULL DEFAULT false in one ADD,
which SQLite accepts because of the default; on Postgres a constant
default is a catalogue change. Rows already waiting take false: nothing
recorded whether they were announced, false can only leave such a message
out of the count until its device collects it, and true would count the
silent envelopes already queued, the over-count being fixed. A new
Envelope starts unannounced, so a row written by any other path can only
leave the badge short.

The /metadata row about the silent flag says it is never written down,
which this makes false. Replacement wording is drafted for the owner and
nothing public is changed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:46:18 -04:00
Paul HittandClaude Opus 5.5 4a20f39ef5 Draft: one /metadata line for the Today file, for the owner to approve
DRAFT. Not for deploy until the owner approves the wording, and a native
speaker the Spanish. One line at the end of "The short version" on
/metadata and /es/metadata saying the server offers one public Today file,
the same for everybody, and that asking for it tells the server nothing
it does not already see. It renders only on a relay that offers the
file, because the page describes the running configuration, and each
copy carries an HTML comment marking it as a draft.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:07:00 -04:00
Paul HittandClaude Opus 5.5 534f8513a3 The /hi page a hello link opens
A hello link is https://<relay>/hi#<address> (§6.34). The address is in
the fragment, which no browser sends, so /hi and /es/hi are one static
page for every link, and Public/hi.js reads the address on the device:
fnid: and 52 characters of the apps' base32 alphabet, then @ and a host
of letters, digits, hyphens and dots, the rule both apps use. A good
address is shown as text with "Open in Flamenet Messenger"
(flamenetmessenger://hi#<address>) and a copy button; anything else gets
a plain explanation. Both carry the install links and the iPhone
App Store or TestFlight switch from /get.

The script makes no request and builds no URL but the app link. A test
reads it for every way a page can send something, and on macOS runs it
in JavaScriptCore against stand-ins for the page's elements. Both pages
ask not to be indexed (header and meta tag), stay out of the sitemap,
load nothing from another host, and are in the staging gate's page list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:06:44 -04:00
Paul HittandClaude Opus 5.5 414bed231c Serve the Today file at /today.json
GET /today.json (§8.11) serves one public, signed file, the same bytes
for everybody. It comes from FN_TODAY_FILE, read again when the file
changes, or from FN_TODAY_SOURCE, fetched every 15 minutes give or take
a minute, with a 10 second deadline over the whole answer and a 512 KB
cap. Nothing is served until it verifies against a key in FN_TODAY_KEYS
(kid = first eight hex characters of the key's SHA-256), and a file that
fails the check never replaces the last one that passed. FN_TODAY_CACHE
keeps that copy across a restart.

404 {"error":"not_offered"} with no source set, 503 while nothing has
verified. A 200 is the bytes unchanged as application/json, public for
900 seconds, with the quoted SHA-256 of those bytes as the ETag, so every
client gets the same one; If-None-Match gets a 304. The route reads no
credential, cookie or query, writes no log line per request, and adds no
table.

Documented in docs/CONFIGURATION.md, docs/API.md and .env.example. The
flamenet.io builder's dev-signed sample is a test fixture; the dev key's
private half is not in the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:56:29 -04:00