761 Commits
Author SHA1 Message Date
extant2000git 0886c4a415 Merge pull request 'Prod compose: record the hardening running on the box since 2026-10-15' (#262) from chore/record-box-hardening into main
ci / checks (push) Successful in 13s
ci / swift (push) Successful in 9m7s
ci / image (push) Successful in 51s
ci / deploy (push) Successful in 17s
2026-10-16 10:48:19 +00:00
Paul HittandClaude Opus 5.5 af56f810a8 Prod compose: record the hardening running on the box since 2026-10-15
ci / checks (pull_request) Successful in 12s
ci / swift (pull_request) Successful in 8m50s
ci / image (pull_request) Successful in 50s
ci / deploy (pull_request) Skipped
The box's compose got these during the security audit and the repo copy
did not, so a rebuild from the repo would have undone them:
- Redis requires a password (FN_REDIS_PASSWORD, written to a config file
  inside the container; healthcheck via REDISCLI_AUTH); FN_REDIS_URL
  carries it.
- The internal network is internal: true, so the database and Redis have
  no route off the box. The relay reaches S3 and APNs through web.
- relay, redis and obfs4bridge drop every capability; db keeps the five its
  entrypoint needs. All four set no-new-privileges.
- obfs4bridge is on web only.

.env.example documents FN_REDIS_PASSWORD. The deploy itself uses the
box's compose, so this changes nothing that is running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 06:38:04 -04:00
extant2000git 91af917aa4 Put FN_FUNDING_MANIFESTS in the compose the repository ships (#261)
ci / checks (push) Successful in 11s
ci / swift (push) Successful in 8m45s
ci / image (push) Successful in 51s
ci / deploy (push) Successful in 17s
The box copy is no longer the only one. All gates green.
2026-10-15 23:08:20 +00:00
Paul HittandClaude Opus 5 1dc4d1b713 Put FN_FUNDING_MANIFESTS in the compose the repository ships
ci / swift (pull_request) Successful in 9m5s
ci / checks (pull_request) Successful in 14s
ci / image (pull_request) Successful in 51s
ci / deploy (pull_request) Skipped
The route landed in #259 and the variable went into the box's compose by hand,
because a deploy alone did not pick it up: this file lists every variable
explicitly under environment: and interpolates from .env, so a variable present
only in .env reaches the container not at all. The relay restarted from the new
image and still answered 404 until the compose named it.

That left the only copy on the box, one deploy away from being dropped, and the
FLOSS/fund listing depends on the path it enables. Now the repository carries it
and .env.example documents it, so a fresh clone and a rebuilt compose both keep
working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 18:58:07 -04:00
extant2000git 36d873ff6d Prove this repository belongs to the funding manifest (#260)
ci / checks (push) Successful in 12s
ci / swift (push) Successful in 8m31s
ci / image (push) Successful in 1m14s
ci / deploy (push) Successful in 19s
One file. All four jobs green including the mac2 staging gate.
2026-10-15 22:51:44 +00:00
Paul HittandClaude Opus 5 f61389474c Prove this repository belongs to the funding manifest
ci / swift (pull_request) Successful in 8m45s
ci / checks (pull_request) Successful in 13s
ci / image (pull_request) Successful in 48s
ci / deploy (pull_request) Skipped
The FLOSS/fund directory listed the flamenet-messenger project's repository
link as unverified: sharing codeberg.org with the manifest proves nothing,
because anyone can serve a manifest from codeberg.org. The spec's own example
is a file committed inside the repository it vouches for.

This file names the manifest, which lives in flamenet-e2e and lists this
repository as the flamenet-messenger project. One line, mirrored to Codeberg
with the rest of the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 18:41:23 -04:00
extant2000git 9fbe8045d4 Vouch for a funding.json manifest, when this relay is configured to (#259)
ci / checks (push) Successful in 13s
ci / swift (push) Successful in 8m54s
ci / image (push) Successful in 57s
ci / deploy (push) Successful in 19s
checks, swift and the mac2 staging gate all green. Route 404s unless FN_FUNDING_MANIFESTS is set, verified both ways against a local relay.
2026-10-15 20:28:02 +00:00
Paul HittandClaude Opus 5 a07e0a86ce Regenerate configuration.leaf, which is the page people actually read
ci / checks (pull_request) Successful in 24s
ci / swift (pull_request) Successful in 8m43s
ci / image (pull_request) Successful in 1m52s
ci / deploy (pull_request) Skipped
Scripts/check-doc-pages.py failed the PR: configuration.leaf is generated from
docs/CONFIGURATION.md and hashed against it, so documenting a variable without
rebuilding the page leaves the served page saying something older.

Build with FLAMENET_E2E_DIR pointed at the engine AT THE PIN in .e2e-ref. With
a sibling checkout on the engine's main instead, spec.leaf and engine.leaf
regenerate from unpinned content and the check fails on two more pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 16:16:42 -04:00
Paul HittandClaude Opus 5 3ab3e8af03 Document FN_FUNDING_MANIFESTS, which CONFIGURATION.md promises to list
ci / checks (pull_request) Failing after 10s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
ci / swift (pull_request) Failing after 8m6s
check-configuration-doc.py failed the PR: that file says it lists every
variable the relay reads, and the new one was missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 16:14:43 -04:00
Paul HittandClaude Opus 5 0d06092a28 Ignore agent worktrees, which git add -A turned into gitlinks
ci / checks (pull_request) Failing after 11s
ci / swift (pull_request) Successful in 8m56s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
Seven .claude/worktrees/agent-* directories reached the index on 2026-10-15.
They are other sessions' checkouts of this repository, so they commit as
gitlinks pointing at commits no clone can fetch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 16:04:31 -04:00
Paul HittandClaude Opus 5 a3675e49c6 Vouch for a funding.json manifest, when this relay is configured to
funding.json is how FLOSS/fund takes applications: a manifest in the repository
rather than a proposal, submitted to their directory by URL. Its provenance rule
is that a manifest served from a code host while claiming a hostname needs that
hostname to confirm it, which is what .well-known/funding-manifest-urls is for.

FN_FUNDING_MANIFESTS holds the URLs, whitespace or comma separated, and anything
not beginning with https:// is dropped at config time. Unset means the route
404s, for the same reason the contact addresses render as nothing when
unconfigured: a relay somebody else runs must never tell a funder that
donations for it reach us. The manifest itself carries a legal name, a contact
address and bank details, so it stays in the repository and out of this binary.

Verified against a local relay: 404 with the variable unset, 200 with it set,
and an http:// entry passed alongside an https:// one was filtered out rather
than served.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 16:04:23 -04:00
extant2000git 41580da291 Merge pull request 'Security audit 2026-10-15: relay fixes' (#258) from fix/security-audit-2026-10-15 into main
ci / checks (push) Successful in 11s
ci / swift (push) Successful in 9m4s
ci / image (push) Successful in 59s
ci / deploy (push) Successful in 20s
2026-10-15 13:35:03 +00:00
Paul HittandClaude Opus 5.5 842f11e952 Security audit 2026-10-15: relay fixes
ci / checks (pull_request) Successful in 12s
ci / swift (pull_request) Successful in 9m10s
ci / image (pull_request) Successful in 1m49s
ci / deploy (pull_request) Skipped
- Block list: a blocked account that is unlisted and not on the caller's
  buddy list comes back as "User #<id>" with an empty slug. Rows and shape
  are unchanged, so unblock still works in both apps.
- Vouching: a suspended account can no longer be issued a vouch (403
  account_suspended).
- Federation: outbound deposit, get and fetch run the outbound address
  guard, with allowlisted peers exempt as in PeerDirectory.
- Email: control characters in an address are refused as bad_email, and
  the mailer refuses such a recipient and strips them from headers.
- Key log: inclusion, consistency, device and subject reads are metered at
  3600 per address per hour each, like the head.

Not changed: email_taken stays (both apps map it), strict account tokens
stay off until the account service stamps iss, aud and jti, and the
credential and current-password hardening needs an app update first.

swift test: 1015 of 1015 pass, 6 new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:21:34 -04:00
extant2000git ec9c542cf5 Merge pull request 'Messenger pages: sign-up open in 12 countries (adds Canada, Australia, New Zealand)' (#257) from extant2000git/copy/countries-12 into main
ci / deploy (push) Successful in 21s
ci / checks (push) Successful in 16s
ci / swift (push) Successful in 9m9s
ci / image (push) Successful in 57s
2026-10-15 12:56:49 +00:00
Paul HittandClaude Opus 5.5 d159e126cd Messenger pages: sign-up is open in 12 countries, adding Canada, Australia and New Zealand
ci / checks (pull_request) Successful in 12s
ci / swift (pull_request) Successful in 9m20s
ci / image (pull_request) Successful in 1m48s
ci / deploy (pull_request) Skipped
The edge now allows CA, AU and NZ on messenger, relay and accounts.flamenet.io.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:44:51 -04:00
extant2000git 9d2bbbf0e9 Merge pull request 'Relay 0.32: merge the release branch into main, with the badge column carried onto the new write path' (#256) from merge/relay-0-32-unify into main
ci / checks (push) Successful in 12s
ci / swift (push) Successful in 9m29s
ci / image (push) Successful in 1m2s
ci / deploy (push) Successful in 26s
checks, swift and image all green on dca5333e. Two defects only the Linux build could show: TodayFile.Key stored a Curve25519.Signing.PublicKey in a Sendable struct, which swift-crypto does not allow and CryptoKit does, and BadgeCountTests still called Routes.placeDeposits, which wave 2 split into resolveDeposits plus Delivery.write.
2026-10-14 17:01:59 +00:00
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
extant2000git a13e396023 Merge pull request 'ci: the staging gate waits for Postgres over TCP, from another container, twice' (#255) from ci/gate-waits-for-postgres-over-tcp into main
ci / checks (push) Successful in 13s
ci / swift (push) Successful in 11m43s
ci / deploy (push) Successful in 21s
ci / image (push) Successful in 1m3s
checks, swift and image all green on c4d50f45. Worth recording: the image job also passed on main at 6d1013dc without this fix, so the Postgres readiness problem is a race rather than a hard failure. It failed on ebdfc87e and passed on 6d1013dc with the same gate, which is the definition of one.
2026-10-14 16:33:57 +00:00
Paul HittandClaude Opus 5 c4d50f45f7 ci: the staging gate waits for Postgres over TCP, from another container, twice
ci / checks (pull_request) Successful in 12s
ci / swift (pull_request) Successful in 8m29s
ci / image (pull_request) Successful in 52s
ci / deploy (pull_request) Skipped
The image job is red on main and the deploy has not run. The gate's readiness
check was wrong in both directions, and the two failures looked nothing alike.

On mac1 `docker exec <db> pg_isready` never succeeded inside 90 s, and the
gate said only "never became ready". On mac2, with the diagnostics from #253
in place, it succeeded 2.4 seconds after `docker run`, and the candidate's own
log says what happened next:

  Opening new connection for pool failed ... Connection refused (errno: 111)
  Fatal error: Error raised at top level: PSQLError(code: connectionError ...)

The postgres image runs initdb against a bootstrap server on the unix socket
and then restarts, so a local-socket `pg_isready` can answer "accepting
connections" while nothing is listening on TCP. The relay only speaks TCP, and
it exits on a refused first connection rather than retrying, so that one early
answer ended the run: the 90 seconds that followed were spent waiting for
/health from a container that had already died.

Readiness is now checked the way the relay will use it, over TCP from a
separate container on the gate's network, and twice in a row so the restart in
the middle of the init cycle is not mistaken for the end of it.

Worth a separate decision, not changed here: the relay exits on one refused
connection to Postgres. In production that is a crashloop until the database
answers, which is survivable and noisy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 12:24:06 -04:00
extant2000git 6d1013dc89 Merge pull request 'Relay review wave 2: in-process wake, signin and tokens, scale, streaming attachments' (#254) from release/relay-review-wave2-2026-10-14 into main
ci / checks (push) Successful in 10s
ci / swift (push) Successful in 9m17s
ci / image (push) Successful in 1m1s
ci / deploy (push) Successful in 21s
swift, checks and image all green on 404468d9. The image job passed on mac2 with the staging gate, which is what #253 fixed; on mac1 it had failed twice at the same step.
2026-10-14 16:21:52 +00:00
Paul Hitt 404468d983 Merge remote-tracking branch 'gitea/main' into HEAD
ci / checks (pull_request) Successful in 16s
ci / swift (pull_request) Successful in 13m2s
ci / image (pull_request) Successful in 2m46s
ci / deploy (pull_request) Skipped
2026-10-14 11:13:26 -04:00
extant2000git ebdfc87eb9 Merge pull request 'ci: run the image job on mac2, and make the staging gate say what failed' (#253) from ci/image-on-mac2 into main
ci / checks (push) Successful in 13s
ci / swift (push) Successful in 15m29s
ci / image (push) Failing after 2m5s
ci / deploy (push) Skipped
Three jobs green on 15737e05, the image job among them: pinned to mac2, it built the candidate and passed the staging gate that had failed twice on mac1.
2026-10-14 15:13:17 +00:00
Paul HittandClaude Opus 5 15737e05b4 ci: run the image job on mac2, and make the staging gate say what failed
ci / checks (pull_request) Successful in 14s
ci / swift (pull_request) Successful in 11m51s
ci / image (pull_request) Successful in 51s
ci / deploy (pull_request) Skipped
The image job failed on PR #252 and passed on main with no code difference
between them that touches CI. The runner is the difference: run 434 built on
mac2 and passed, run 441 built on mac1 and failed at "staging postgres never
became ready", on the same two commits. mac1 has the smaller OrbStack
allocation, and the gate starts three sibling containers on the host daemon.
The job now names mac2 rather than taking whichever runner is free.

The gate also prints the container's state and log when Postgres does not come
up. "Never became ready" covered both a Postgres still starting and a
container that never ran, which need different fixes, and mac1 said nothing
further, so 90 seconds of waiting read as a relay bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 10:59:31 -04:00
Paul HittandClaude Opus 5.5 be5acc5b64 ci: re-run; the staging gate's postgres never became ready on the runner (run 440)
ci / swift (pull_request) Successful in 8m42s
ci / image (pull_request) Failing after 17s
ci / deploy (pull_request) Skipped
ci / checks (pull_request) Successful in 16s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:52:34 -04:00
Paul HittandClaude Opus 5.5 a83e7a57a8 Document the sign-in and account-token settings; rebuild doc pages
ci / checks (pull_request) Successful in 13s
ci / image (pull_request) Failing after 2m10s
ci / deploy (pull_request) Skipped
ci / swift (pull_request) Successful in 8m23s
CI's configuration-doc check found FN_ACCOUNT_TOKEN_ISSUER,
FN_ACCOUNT_TOKEN_STRICT and FN_SIGNIN_MAX_PER_IP read but not
documented. configuration.leaf and relay.leaf regenerated with
Scripts/build-doc-pages.sh; 9 generated pages match their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:41:06 -04:00
Paul HittandClaude Opus 5.5 38f2af7276 Attachments refuse only listed deleted accounts
ci / checks (pull_request) Failing after 9s
ci / swift (pull_request) Successful in 8m23s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
The streaming upload path repeats the account check before reading the
body, and its copy still refused any subject without a row on an
issuing relay. Same rule as Routes.noteAliveOrRefuse now: refuse only
ids in deleted_accounts. Conformance 266/0/2 (was failing 6 in
sealed attachments); full suite 971/971 twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:12:46 -04:00
Paul Hitt 2ea4cbe153 Merge feat/attachment-streaming 2026-10-14 09:07:41 -04:00
Paul HittandClaude Opus 5.5 6ea4f001e0 Stop test fixtures starving the cooperative pool
WakeTests "a send that commits between the cursor query and the wait"
failed in full runs (14 s, 36 s, 42 s against a 5 s bound) and passed
alone. The wake was never lost: with timestamps, the send committed and
signalled at +13.41 s and the poll returned at +14.12 s. The 13 s went
before the send, in a process-wide stall.

A `sample` taken during a stall showed all 8 cooperative threads busy:
7 in Bcrypt.hash at the default cost 12, called synchronously from
fixtures (RosterTagTests, MobileRoutesTests, MobileExtrasTests,
PasswordResetTests account helpers), and 1 in blockingShutdown's
semaphore. One cost-12 hash is 0.31 s in a debug build; cost 4 is
1.3 ms. Every fixture now hashes at cost 4, as WaveZeroBSupport already
did, and a source scan fails if one goes back to the default. The full
run drops from about 60 s to 45 s.

WakeTests now times the poll from the moment the send committed, so a
stall before the send cannot read as a lost wake. With the product's
pending flag disabled it still fails, at 31.8 s.

DeliveryOrderPostgresTests counted every ungranted advisory lock on the
server, including other suites' key-log appends in their own databases,
so the second send could start before the first had inserted ("free"
got id 1). The wait now counts only this database's lock on holdKey.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:07:15 -04:00
Paul HittandClaude Opus 5.5 ea1b2f228e Stream attachment uploads and downloads through storage
Uploads are authenticated before the body is read: token, proof headers,
device and account are checked first, then Content-Length (required, 411
without it) against attachmentMaxBytes and the rate limits. The body then
streams to storage while it is counted and hashed. The proof signature,
which covers the body's SHA-256, is checked after the last byte, and a
blob whose proof fails, or whose body ends short of Content-Length, is
deleted.

S3 PUTs are signed UNSIGNED-PAYLOAD and sent with a known length through
AsyncHTTPClient. The digest is unknown until the last byte, so
x-amz-checksum-sha256 cannot go in a header, and B2 refuses aws-chunked.
GETs stream the bucket's body to the client, Range still asked of the
bucket. The disk store writes to a .partial file and renames it.

Vapor runs a streamed body's closure a second time when writing the
response fails; the bucket body is handed out once, because a second read
traps the process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:49:39 -04:00
Paul HittandClaude Opus 5.5 eef3b65003 Count Redis wake-up failures in /metrics
Adds flamenet_wake_errors_total and counts both catch blocks in
RoutesWake (send and wait). Only the replica (Redis) path can fail;
the in-process waiter cannot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:40:53 -04:00
Paul Hitt f95cd947e5 Merge feat/fed-reports-metrics
# Conflicts:
#	Sources/FlamenetRelay/Config.swift
#	Sources/FlamenetRelay/Migrations.swift
2026-10-14 08:40:35 -04:00
Paul Hitt 7064c5c3f4 Merge feat/scans-and-scale
# Conflicts:
#	Sources/FlamenetRelay/Migrations.swift
#	Sources/FlamenetRelay/Routes.swift
2026-10-14 08:40:27 -04:00
Paul Hitt f1c19902b7 Merge feat/signin-and-tokens 2026-10-14 08:40:12 -04:00
Paul Hitt d5b319b233 Merge feat/inprocess-wake 2026-10-14 08:40:12 -04:00
Paul HittandClaude Opus 5.5 b82c6dfc8e Keep held-poll tests off the shared capacity gauge
RelayCapacity.shared is process-global. WakeTests holds 21 polls at once,
which moved the exact peakHeld that CapacityTests asserts. Both now take
one lock (ProcessEnvironmentLock.withExclusiveHeldPolls). The wake-race
test allows 5 s against a 30 s hold, so a loaded runner does not fail it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:39:42 -04:00
Paul HittandClaude Opus 5.5 ec6755b15a Relay review 2026-10-14: bounded presence, cached log head, set-based retention
- Presence by token: three queries per request (device tags, account
  tags, users) with digest IN (...), down from two or three per token.
  New index roster_tags_digest; device_roster_tags_digest already existed.
- Key log head: KeyLogHeadCache serves (size, root, signature) after one
  MAX(seq) read, and loads only leaves past the cached seq. It refreshes
  on the first read after an append commits, never inside the append's
  transaction, and stays correct across replicas. GET /e2e/log/head is
  metered at 3600 per address per hour.
- Retention: expired devices picked by one SQL query (ids only), orphaned
  push tokens deleted with NOT EXISTS, used prekeys deleted with a bare
  `used` predicate matching the new partial index prekeys_used_created.
- noteAliveOrRefuse reads only suspended_at per request. Last-seen is
  written at most once per heartbeat and once per 60 s per account,
  tracked in memory per process.
- AddScanIndexes builds both indexes CONCURRENTLY on Postgres: FluentKit
  runs migrations outside a transaction. An INVALID leftover is rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:31:29 -04:00
Paul HittandClaude Opus 5.5 966c747881 Docs: Postgres for self-hosting, key pinning and rotation, report digest, metrics
SQLite is documented as local development only, with no new features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:29:28 -04:00
Paul HittandClaude Opus 5.5 9473d304b4 Pin federation peer keys, digest reports hourly, serve /metrics on its own port
- FN_FEDERATION_PEERS takes host, host=key or host=key1|key2. A pinned peer
  whose descriptor publishes another key is refused (keyNotPinned, 401).
  Boot warns for each bare host and for each malformed pin.
- Content reports: FN_REPORT_RATE (default 10/hr) per reporter through the
  existing limiter, 429 over it. One mail per report replaced by
  ReportDigest: at most one mail an hour listing reports since the last,
  none when there are none. Watermark in a new report_digests table, seeded
  at the newest existing report; content_reports is unchanged.
- RelayMetrics counters and a NIO listener on FN_METRICS_PORT (off by
  default, 127.0.0.1 by default) answering only GET /metrics. Boot refuses
  FN_METRICS_PORT equal to FN_PORT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:29:28 -04:00
Paul HittandClaude Opus 5.5 5a0331e37a Source page and site layout link to git.extant2000.com
ci / checks (push) Successful in 12s
ci / swift (push) Successful in 10m20s
ci / image (push) Successful in 1m4s
ci / deploy (push) Successful in 22s
ci / swift (pull_request) Successful in 14m10s
ci / image (pull_request) Successful in 1m48s
ci / deploy (pull_request) Skipped
ci / checks (pull_request) Successful in 33s
The four repositories moved from GitLab to Gitea. The hand-written
source page (en, es) and the layout's sameAs list now point at
https://git.extant2000.com/extant2000/<repo>; all four answer 200
and clone without credentials. Generated doc pages still carry
GitLab links until their source documents change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:25:49 -04:00
Paul HittandClaude Opus 5.5 44b3ddc01b Throttle sign-in per address, move bcrypt off the event loop, check account tokens
Relay review 2026-10-14, three owner-approved changes, each safe to deploy
before anything else moves.

Sign-in: a second failure bucket per address (FN_SIGNIN_MAX_PER_IP, 30 per
15 minutes, 0 = off) sits beside the address+username pair, so one address
can no longer try a password against every username. A success clears only
the pair. Every bcrypt hash and verify in AccountRoutes and PasswordReset now
runs on application.threadPool through offLoop.

App credentials: the relay mints every one as 32 bytes from the CSPRNG
(randomHexToken). New rows are stored as sha256:<hex>; a bcrypt row is
verified once and rewritten as SHA-256 on success, only when the secret has
the 64-hex shape the relay mints.

Account-service tokens: /messenger/session reads iss, aud (string or list)
and jti. FN_ACCOUNT_TOKEN_STRICT (off by default) logs and counts tokens that
lack them, carry the wrong ones, or replay a jti, and accepts them; on, it
refuses them. jti is spent through the replay cache with a TTL to the token's
exp, after the name checks so a refused handle does not burn the token. Boot
fails with strict on and FN_ACCOUNT_TOKEN_ISSUER unset. SECURITY.md has the
rollout order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:24:12 -04:00
Paul HittandClaude Opus 5.5 42be0d9116 Document in-process wake-ups and the Redis pool defaults
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:21:36 -04:00
Paul HittandClaude Opus 5.5 2e5f0e3d39 Wake held polls in process and read poll sizes before payloads
One relay keeps a waiter per held poll in memory (WakeHub). The poll
registers once, before its first cursor query, and Delivery.write signals
it after the commit, so a send landing between the query and the wait is
kept and the poll returns at once. A held poll holds no Redis connection.
With FN_RELAY_REPLICAS above 1 wake-ups still go through BRPOP on a Redis
list, behind the same Waiter interface; registration there deletes tokens
left by earlier sends instead of popping them later. A failed wait sleeps
one second before the next cursor query. The Redis pool default is 32 per
loop on one relay and stays 512 with replicas.

The cursor query reads id and payload length first, cuts at the 2 MiB
budget, then loads payloads for the kept rows only. Response shape is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:21:36 -04:00
Paul HittandClaude Opus 5.5 774b59bd10 Refuse only deleted accounts, not every subject without a row
ci / image (pull_request) Successful in 1m58s
ci / deploy (pull_request) Skipped
ci / checks (pull_request) Successful in 13s
ci / swift (pull_request) Successful in 14m43s
CI's e2e conformance suite (test/relay.mjs at 310c315) mints tokens
for bare subjects on an issuing relay, and the account check refused
them all as deleted. Account delete now lists the id in a new
deleted_accounts table (id and time only), and a missing row is
refused only when the id is listed there. Production had 0 devices
without a user row. Conformance: 266 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:20:33 -04:00
Paul Hitt cf1ce36602 Merge ops/pitr-monthly-drill
ci / checks (pull_request) Successful in 15s
ci / swift (pull_request) Failing after 8m0s
ci / image (pull_request) Skipped
ci / deploy (pull_request) Skipped
2026-10-14 07:58:01 -04:00
Paul Hitt daa00051c1 Merge fix/delivery-ordering 2026-10-14 07:58:01 -04:00
Paul Hitt 4df5244911 Merge fix/keys-and-account-delete 2026-10-14 07:58:00 -04:00
Paul HittandClaude Opus 5.5 1802676a9c Retry Postgres deadlock and serialization failures in retryingTransaction
retryingTransaction now retries PSQLError with SQLSTATE 40P01
(deadlock_detected) or 40001 (serialization_failure), with the same
5 attempts and backoff as SQLite BUSY. No other code is retried.
Postgres has already rolled the attempt back, so a rerun is clean.

Only the outermost call retries a Postgres error. A nested call shares
an already aborted transaction, so it rethrows and the outer call
starts over. This covers KeyLog.append taking the advisory lock inside
a caller's transaction.

Test (FN_TEST_POSTGRES): two connections lock rows 1 and 2 in opposite
order inside retryingTransaction. One gets 40P01, runs again, and both
commit once: 3 attempts, rows end at [2, 2]. Fails with 40P01 before
this change; passed 5 of 5 runs after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:57:45 -04:00
Paul HittandClaude Opus 5.5 1748ee999a Document the delivery write path, the device lock and the push worker
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:52:57 -04:00