mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Speak the Mercure 1.0 protocol, because the hub does now
Live updates were dead: every subscription came back 401 and the inbox sat on "Live updates unavailable" while the hub answered its own healthcheck happily. The hub had moved to the 1.0 protocol and now wants RFC 9068 access tokens — an `iss` it has been told to trust, an `aud` naming the hub the token is for, and `authorization_details` in place of the bare `mercure` claim. plMail was minting the older shape. NOT HAND-ROLLED CLAIMS. symfony/mercure already implements 1.0, so this is configuration: `protocol_version: '1.0'` plus the three claims the bundle requires (it refuses to build the container without `iss`, `sub` and `client_id`, which is how the requirement was found). They are merged into the token FACTORY as well as the publisher provider, so the subscriber cookie gets them too — one block covers both halves. The protocol switch also changes the subscription query parameter from `topic` to `match` and the cookie's name. Both belong to the bundle — the `mercure()` Twig helper writes the URL, Authorization writes the cookie — which is why no JavaScript and no controller is touched here. `resource_identifier` IS PINNED ON THE HUB, and that is not decoration. A 1.0 hub works its expected audience out from each request, and plMail reaches it by two roads: the browser subscribes at MERCURE_PUBLIC_URL, the application publishes to the internal MERCURE_URL. One token cannot name both, so a hub left to derive its own answer would refuse every publish. Issuer and audience are both read from MERCURE_PUBLIC_URL so the app and the hub cannot be configured into disagreeing. THE `__Secure-` COOKIE is the one thing an operator may have to know about. 1.0 names the cookie `__Secure-mercure_access_token`, and a browser drops a `__Secure-` cookie that did not arrive over TLS. Right for every real install, fatal for the test stack, which serves plain HTTP on 127.0.0.1 — so the name is a parameter defaulting to the secure one, and only the test stack overrides it, with the hub told the same name. Verified in a browser against the current hub image: the indicator reads "connected", and the subscription is GET /.well-known/mercure?match=mail%2Fuser %2F1 → 200. Full suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f68a2c05d0
commit
d35418c40a
6 files changed
+102
-2
No files matched your search
@@ -8,6 +8,24 @@ The published image tags: `latest` follows the most recent release below,
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Live updates stopped arriving, and the inbox said so.** The Mercure hub moved to the 1.0
|
||||
protocol, which wants RFC 9068 access tokens — an issuer it has been told to trust, an audience
|
||||
naming the hub, and the grants carried differently. plMail was still minting the older shape, so
|
||||
every subscription came back 401 and the indicator sat on "Live updates unavailable" while the
|
||||
hub itself was perfectly healthy. New mail still arrived; it just needed the page reloaded to
|
||||
show up, which is most of the point of the thing gone.
|
||||
|
||||
plMail now speaks the 1.0 protocol. The hub is deliberately not pinned to a version — it is meant
|
||||
to keep itself up to date — so the answer is to follow it rather than to hold it still.
|
||||
|
||||
**Nothing to configure.** The issuer and the audience are both derived from the hub URL the
|
||||
installation already has, so they cannot drift apart. The one exception is an install serving
|
||||
plain HTTP: the 1.0 cookie is `__Secure-` prefixed and a browser will not accept one of those
|
||||
without TLS, so such an install must set `MERCURE_COOKIE_NAME` to a name without the prefix. Any
|
||||
install on HTTPS — which is every install with working push — needs no change.
|
||||
|
||||
## v0.2.30 — 2026-09-21
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -101,6 +101,10 @@ x-test-app: &test-app
|
||||
# from this URL's scheme, so an https value here would mint a cookie the
|
||||
# browser refuses to send back over the test stack's plain HTTP.
|
||||
MERCURE_PUBLIC_URL: http://127.0.0.1:${TEST_HTTP_PORT:-8001}/.well-known/mercure
|
||||
# Plain HTTP here, so the `__Secure-` name Mercure 1.0 defaults to would be
|
||||
# dropped by the browser before it ever reached the hub. The hub is told the
|
||||
# same name below.
|
||||
MERCURE_COOKIE_NAME: mercure_access_token
|
||||
MERCURE_URL: http://mercure/.well-known/mercure
|
||||
# Fixed, like APP_SECRET above: the suite signs subscriber JWTs with it and
|
||||
# the hub verifies them, so both sides have to agree without depending on
|
||||
@@ -187,6 +191,13 @@ services:
|
||||
SERVER_NAME: ":80"
|
||||
MERCURE_PUBLISHER_JWT_KEY: e2e-mercure-secret-not-for-production
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: e2e-mercure-secret-not-for-production
|
||||
# RFC 9068: the issuer this hub trusts and the audience it demands, both
|
||||
# matching what config/packages/mercure.yaml mints. See compose.yaml,
|
||||
# which carries the reasoning.
|
||||
MERCURE_TRUSTED_ISSUERS: http://127.0.0.1:${TEST_HTTP_PORT:-8001}/.well-known/mercure
|
||||
MERCURE_EXTRA_DIRECTIVES: |
|
||||
resource_identifier http://127.0.0.1:${TEST_HTTP_PORT:-8001}/.well-known/mercure
|
||||
cookie_name mercure_access_token
|
||||
# NO healthcheck here, deliberately — the image's own is used, exactly as
|
||||
# in compose.yaml and for the same reason: the hub moved its readiness
|
||||
# probe out from under the URL we had hard-coded, and every job in CI then
|
||||
|
||||
@@ -276,6 +276,20 @@ services:
|
||||
# yourself still wins — the generator never overwrites it, so both sides
|
||||
# read the same value.
|
||||
MERCURE_JWT_SECRET: ${MERCURE_JWT_SECRET:-}
|
||||
# The two halves of RFC 9068, both read from the ONE url the app already
|
||||
# publishes under, so they cannot drift apart.
|
||||
#
|
||||
# TRUSTED_ISSUERS is the `iss` this hub will accept, and
|
||||
# config/packages/mercure.yaml mints exactly this value.
|
||||
#
|
||||
# resource_identifier pins the `aud` the hub demands. Without it the hub
|
||||
# works the audience out from each request, and plMail arrives at it by
|
||||
# two different roads: the browser subscribes at MERCURE_PUBLIC_URL while
|
||||
# the application publishes to the internal MERCURE_URL. One token cannot
|
||||
# name both, so publishing would be refused by a hub that had derived its
|
||||
# own answer from the container hostname.
|
||||
MERCURE_TRUSTED_ISSUERS: ${MERCURE_PUBLIC_URL:-https://localhost/.well-known/mercure}
|
||||
MERCURE_EXTRA_DIRECTIVES: resource_identifier ${MERCURE_PUBLIC_URL:-https://localhost/.well-known/mercure}
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
|
||||
@@ -3,9 +3,56 @@ mercure:
|
||||
default:
|
||||
url: '%env(default::MERCURE_URL)%'
|
||||
public_url: '%env(default::MERCURE_PUBLIC_URL)%'
|
||||
|
||||
# Mercure 1.0, because the hub is on a floating tag and has moved.
|
||||
#
|
||||
# The hub now wants RFC 9068 access tokens: an `iss` it has been
|
||||
# told to trust, an `aud` naming the hub the token is for, and
|
||||
# `authorization_details` in place of the bare `mercure` claim. The
|
||||
# tokens plMail used to mint carried none of that, so every
|
||||
# subscription came back 401 and the inbox sat on "Live updates
|
||||
# unavailable" while the hub itself was perfectly healthy.
|
||||
#
|
||||
# This is a protocol switch, not a patch: it also changes the
|
||||
# subscription query parameters and the cookie name. Both are the
|
||||
# bundle's business — the `mercure()` Twig helper writes the URL and
|
||||
# Authorization writes the cookie — which is why the JavaScript and
|
||||
# the controllers are untouched by it.
|
||||
protocol_version: '1.0'
|
||||
cookie_name: '%mercure.cookie_name%'
|
||||
|
||||
jwt:
|
||||
secret: '%env(MERCURE_JWT_SECRET)%'
|
||||
publish: '*'
|
||||
|
||||
# `iss`, and it has to equal MERCURE_TRUSTED_ISSUERS on the hub
|
||||
# or nothing this mints is accepted. Both sides read the same
|
||||
# variable in compose so they cannot be set to different things
|
||||
# by accident.
|
||||
#
|
||||
# The hub's public URL as the issuer identity is a little
|
||||
# literal — the issuer is really this application, not the hub —
|
||||
# but it is the one URL both halves already agree on, and an
|
||||
# installation should not have to configure a second one to keep
|
||||
# live updates working. `aud` is deliberately absent: the bundle
|
||||
# defaults it to public_url, which is what the hub is pinned to
|
||||
# expect (see resource_identifier in compose.yaml).
|
||||
# All three are mandatory under 1.0 — the bundle refuses to
|
||||
# build the container without them — and they are merged into
|
||||
# the token FACTORY as well as the publisher provider, so the
|
||||
# subscriber cookie MercureCookieSubscriber mints carries them
|
||||
# too. That is what makes this one block enough.
|
||||
#
|
||||
# `sub` and `client_id` both name the application rather than a
|
||||
# person on purpose. plMail issues these tokens to itself, for
|
||||
# its own hub, on nobody's behalf — the client-credentials
|
||||
# shape — and the thing that decides what a subscriber may
|
||||
# actually read is still the topic grant, which is minted per
|
||||
# user and has not changed.
|
||||
claims:
|
||||
iss: '%env(default::MERCURE_PUBLIC_URL)%'
|
||||
sub: 'plmail'
|
||||
client_id: 'plmail'
|
||||
# Empty, not '*'.
|
||||
#
|
||||
# Nothing server-side subscribes — the application only
|
||||
|
||||
@@ -20,6 +20,16 @@ parameters:
|
||||
# worth preventing is a real deployment turning into a demo, not the
|
||||
# reverse. See App\Service\Demo\DemoMode.
|
||||
app.demo_mode_default: false
|
||||
|
||||
# The subscriber cookie's name. The Mercure 1.0 default is `__Secure-`
|
||||
# prefixed, and a browser refuses a `__Secure-` cookie that did not arrive
|
||||
# over HTTPS — which is right for every real install and wrong for the test
|
||||
# stack, which serves plain HTTP on 127.0.0.1. Read through `default:` so an
|
||||
# install that has never heard of the variable gets the safe name, and only
|
||||
# a deployment that knows it is running without TLS has to say so.
|
||||
mercure.cookie_name_default: '__Secure-mercure_access_token'
|
||||
mercure.cookie_name: '%env(default:mercure.cookie_name_default:MERCURE_COOKIE_NAME)%'
|
||||
|
||||
app.demo_mode: '%env(default:app.demo_mode_default:bool:APP_DEMO_MODE)%'
|
||||
app.demo_ttl_default: 'PT2H'
|
||||
app.demo_ttl: '%env(default:app.demo_ttl_default:APP_DEMO_TTL)%'
|
||||
|
||||
@@ -98,7 +98,7 @@ async function hubPresent(baseURL: string): Promise<boolean> {
|
||||
try {
|
||||
// No credentials: a hub that is up answers 401 to this, which is itself
|
||||
// the proof it is both present and not anonymous.
|
||||
const response = await fetch(`${baseURL}${HUB_PATH}?topic=probe`, {
|
||||
const response = await fetch(`${baseURL}${HUB_PATH}?match=probe`, {
|
||||
signal: AbortSignal.timeout(4000),
|
||||
});
|
||||
|
||||
@@ -338,7 +338,7 @@ test.describe("mercure live updates", () => {
|
||||
* subscribe to mail/user/<any id> and watch that account's sync activity.
|
||||
*/
|
||||
test("refuses a subscription that presents no authorization", async ({ baseURL }) => {
|
||||
const response = await fetch(`${baseURL}${HUB_PATH}?topic=${encodeURIComponent("mail/user/1")}`, {
|
||||
const response = await fetch(`${baseURL}${HUB_PATH}?match=${encodeURIComponent("mail/user/1")}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user