diff --git a/docs/plans/2026-08-19-frontend-simplification.md b/docs/plans/2026-08-19-frontend-simplification.md index abdbbf3d..d9206a2f 100644 --- a/docs/plans/2026-08-19-frontend-simplification.md +++ b/docs/plans/2026-08-19-frontend-simplification.md @@ -109,6 +109,41 @@ it is partitioned. #2214's control cookie encodes exactly that: an unpartitioned cookie written by the same script in the same frame has to be dropped, or the case declares itself vacuous. +**Measured across four engines, and the mechanism is not uniform.** The above was reasoned from the +code and verified on Chromium. Driving the real thing on two genuinely different registrable domains +with real certificates, across Chromium, Firefox, WebKit and Safari 27, shows the recommended +arrangement working everywhere except one case, and working for two different reasons. + +On Chromium, WebKit and Safari the server's own pair is refused, because `AUTH_SAME_SITE` defaults +to +emitting no `SameSite` attribute at all and a cross-site cookie without one is rejected. That +refusal +is what clears the field for the widget's partitioned pair, which is what survives. The absence of +the attribute is doing work here, which is worth stating because it reads like an unset default. + +On Firefox the server's pair is accepted, and since the `JWT` it sets is `HttpOnly`, the browser +then +forbids the widget's script from replacing a cookie of that name. So Firefox never holds a +partitioned +copy and the session rides on an ordinary unpartitioned third-party cookie even with the flag on. It +works, and it stops working the moment the reader blocks those. Firefox's own default, Total Cookie +Protection, partitions rather than blocks and is therefore fine; its opt-in "block all third-party +cookies" discards partitioned cookies too, so no configuration survives it and nothing in remark42 +can change that. + +Safari is the sharp end: it blocks third-party cookies with nothing configured, so +`AUTH_SAME_SITE=none` +on its own has already stopped working there for every reader. The old recipe is not deprecated, it +is broken, which is the strongest argument for treating R1 as live work rather than a documented +limitation. + +**That changes what the upstream `Partitioned` work is worth.** Building `go-pkgz/auth` with a +`PartitionedCookies` option and running the same matrix gives the same persistence with the server's +`JWT` still `HttpOnly`: on Safari, `document.cookie` inside the frame returns the token under the +header flag and does not under the partitioned build, while both keep the reader signed in. So the +two routes are not equivalent, and the header path's XSS cost is avoidable rather than inherent. +`go-pkgz/auth` #318 carries that change with the measurements behind it. + The documentation gap this section named is closed by #2218. The separate-domain manual now recommends `AUTH_SEND_JWT_HEADER=true` and carries the XSS trade-off in the same breath, and the parameter page no longer promises `SameSite=Strict` cookies and a `__Host-` prefix that @@ -595,9 +630,13 @@ subscription flows. R1 costs Path B less than an earlier version of this line cl server-rendered fragment has no client-side writer and therefore needs the upstream `Partitioned` work, which contradicts Path B's own design, since the task list below retains the existing auth through `htmx:configRequest`. `request` in `fetcher.ts` already turns `X-JWT` into the partitioned -pair, so Path B can keep that writer or attach the same handling to a response hook. The upstream -work is mandatory only for a server-set, `HttpOnly`, default-on solution, which is a different -choice from the one Path B is making. +pair, so Path B can keep that writer or attach the same handling to a response hook. + +The upstream work is still the better answer for either path, and for a reason measurement supplied +rather than argument: with `PartitionedCookies` the server sets the pair itself and the `JWT` stays +`HttpOnly`, so the session survives third-party blocking without the token ever becoming readable +from script. The header route buys the same persistence by giving that readability away. So Path B +does not depend on the upstream change, and both paths are better with it. On one contributor the realistic figure is long enough that the plan's own warning applies to the schedule and not only to the design. Deletes the entire npm toolchain. **Against it**: 2,400 lines of the most stateful code get rewritten; a second HTML-fragment API surface becomes permanent