Since #1243 the engine stamps the member's session JWT onto every read-accelerator request, and crates/engine/src/content/read.rs treats a loopback accelerator as able to keep it:
matches!(host, "localhost" | "127.0.0.1" | "[::1]")
No browser can deliver that header to such a gateway. Authorization makes the cross-origin block read non-simple, and a stock Kubo gateway answers the preflight with
Access-Control-Allow-Headers: Content-Type, Range, User-Agent, X-Requested-With
which omits it. Measured in the page against the local stack:
| request |
result |
Accept: application/vnd.ipld.raw |
200, 1052 bytes |
the same plus Authorization: Bearer … |
Failed to fetch — preflight rejected |
What it breaks
VITE_PUBLIC_GATEWAYS is unset in apps/web/.env, so the accelerator is the only content source and there is nothing to rotate to. Every block read fails, so:
- the root never adopts, and the resolve tick reports
RefreshVerdict::Unreachable — the chrome shows refresh failed: no endpoint served a record this pass could adopt
Drain::load_scope_root (crates/engine/src/sync/drain.rs:838-852) cannot assemble the head envelope and returns Halt::UploadAttempt, so a queued write never publishes
The write stays pending with no dead letter, and the status indicator still reads synced. A cold start to an empty vault still works, which is why the smoke tier never saw it.
Reproduction
Against the local stack with VITE_READ_ACCELERATOR_URL=http://127.0.0.1:8080: log in, create a folder, force one refresh pass, wait. The child stays pending: "metadata" forever. Allowing Authorization in that Kubo's Gateway.HTTPHeaders makes the same build pass 11 of 11 web e2e specs.
Why no test caught it
crates/engine/tests/write_plane.rs builds engines on ApiBaseUrl::offline() and plants records, so "first-run mint then a queued write drains" is never composed. The gateway tests drive a scripted HTTP fake and assert only that the header is present. CORS exists only in a browser, so the web e2e write path is the first suite in the repo that can catch this class.
Fix
TLS is the whole rule: a plain-http accelerator falls to GatewaySource::public and still serves reads, unauthenticated. That also stops the session token reaching a gateway the host does not own.
Related, not covered here
Whatever accelerator staging points at must list Authorization in its CORS allow-list, or the deployed web app fails the same way.
Since #1243 the engine stamps the member's session JWT onto every read-accelerator request, and
crates/engine/src/content/read.rstreats a loopback accelerator as able to keep it:No browser can deliver that header to such a gateway.
Authorizationmakes the cross-origin block read non-simple, and a stock Kubo gateway answers the preflight withwhich omits it. Measured in the page against the local stack:
Accept: application/vnd.ipld.rawAuthorization: Bearer …Failed to fetch— preflight rejectedWhat it breaks
VITE_PUBLIC_GATEWAYSis unset inapps/web/.env, so the accelerator is the only content source and there is nothing to rotate to. Every block read fails, so:RefreshVerdict::Unreachable— the chrome showsrefresh failed: no endpoint served a record this pass could adoptDrain::load_scope_root(crates/engine/src/sync/drain.rs:838-852) cannot assemble the head envelope and returnsHalt::UploadAttempt, so a queued write never publishesThe write stays
pendingwith no dead letter, and the status indicator still readssynced. A cold start to an empty vault still works, which is why the smoke tier never saw it.Reproduction
Against the local stack with
VITE_READ_ACCELERATOR_URL=http://127.0.0.1:8080: log in, create a folder, force one refresh pass, wait. The child stayspending: "metadata"forever. AllowingAuthorizationin that Kubo'sGateway.HTTPHeadersmakes the same build pass 11 of 11 web e2e specs.Why no test caught it
crates/engine/tests/write_plane.rsbuilds engines onApiBaseUrl::offline()and plants records, so "first-run mint then a queued write drains" is never composed. The gateway tests drive a scripted HTTP fake and assert only that the header is present. CORS exists only in a browser, so the web e2e write path is the first suite in the repo that can catch this class.Fix
TLS is the whole rule: a plain-http accelerator falls to
GatewaySource::publicand still serves reads, unauthenticated. That also stops the session token reaching a gateway the host does not own.Related, not covered here
Whatever accelerator staging points at must list
Authorizationin its CORS allow-list, or the deployed web app fails the same way.