diff --git a/Caddyfile b/Caddyfile index aeb7a79..7d16573 100644 --- a/Caddyfile +++ b/Caddyfile @@ -43,8 +43,26 @@ handle / { } mercure { - publisher_jwt key - subscriber_jwt key - anonymous true - ui true + anonymous + + # Mercure 1.0 modern mode: access tokens are RFC 9068 JWTs bound to an issuer + # and to this hub's resource identifier. An identifier ending in + # /.well-known/mercure doubles as the base URL relative topics resolve + # against, which is what our rel="self" link values are. + resource_identifier https://localhost/.well-known/mercure + issuer https://localhost { + publisher { + jwt key HS256 + } + subscriber { + jwt key HS256 + } + } + + # INSECURE: dev only. Serves the hub's debugger UI at + # /.well-known/mercure/debug/. This is the 1.0 name of the former "ui" + # directive. Not "playground": that one also forces cors_origins and + # publish_origins to "*" and drops the cookie name prefix, none of which this + # same-origin test server needs. + debugger } diff --git a/Dockerfile b/Dockerfile index 93a57db..e1f2fa7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ -FROM caddy:2.8-builder AS builder +FROM caddy:2.11.4-builder AS builder -RUN xcaddy build --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy +RUN xcaddy build \ + --with github.com/dunglas/mercure/caddy@v1.0.0-alpha.3 \ + --with github.com/dunglas/vulcain/caddy@v1.4.3 -FROM caddy:2.8 AS app_server +FROM caddy:2.11.4 AS app_server COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/packages/mercure/README.md b/packages/mercure/README.md index a1e67e7..99b3c31 100644 --- a/packages/mercure/README.md +++ b/packages/mercure/README.md @@ -54,6 +54,10 @@ Available options: This can be used in conjunction with [@api-platform/ld](/linked-data) as the `fetchFn`. +### Resuming after a disconnection + +The id of the last update received is kept and sent back when a subscription is rebuilt, as both the `last_event_id` query parameter and the `Last-Event-Id` request header. The query parameter is what makes this work with a native `EventSource`, which cannot set headers. + ### Examples See [our Tanstack query example](https://github.com/api-platform/esa/blob/main/tests-server/mercure.html) or the source code of our [home page](https://github.com/api-platform/esa/blob/main/api/public/index.js). diff --git a/packages/mercure/mercure.ts b/packages/mercure/mercure.ts index 1a5e941..8d3428a 100644 --- a/packages/mercure/mercure.ts +++ b/packages/mercure/mercure.ts @@ -26,11 +26,20 @@ function listen(mercureUrl: string, options: Options = {}) { const url = new URL(mercureUrl) topics.forEach((_, topic) => { - url.searchParams.append('topic', topic) + url.searchParams.append('match', topic) }) const headers: {[key: string]: string} = options.headers || {} if (lastEventId) { + // Every call here opens a fresh connection, so the cursor has to travel + // with the request. A native EventSource cannot set headers, hence the + // query parameter: the hub takes the union of the query and body + // components, and last_event_id is single-valued. The header is sent too, + // for EventSource implementations that support it and for the automatic + // reconnections they perform on their own. + url.searchParams.append('last_event_id', lastEventId) + // The request header keeps its name in 1.0; only the hub's response header + // was renamed to Mercure-Last-Event-ID. headers['Last-Event-Id'] = lastEventId } diff --git a/tests-server/github.html b/tests-server/github.html index fdad921..637b48d 100644 --- a/tests-server/github.html +++ b/tests-server/github.html @@ -74,7 +74,7 @@ // You can pass formData as a fetch body directly: fetch(form.action, { method: form.method, body: body.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl0sInB1Ymxpc2giOlsiKiJdfX0.NXhzhXJ8VTxiRRW3pAB4EgP7s_guZeibwzAGw3wZ_KY' + 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ly53ZWxsLWtub3duL21lcmN1cmUiLCJzdWIiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJjbGllbnRfaWQiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJpYXQiOjE3ODg0MjU1MDUsImV4cCI6NDEwMjQ0NDgwMCwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbeyJ0eXBlIjoiaHR0cHM6Ly9tZXJjdXJlLnJvY2tzL2F1dGhvcml6YXRpb24tZGV0YWlsIiwiYWN0aW9ucyI6WyJwdWJsaXNoIl0sInRvcGljcyI6W3sibWF0Y2giOiIqIn1dfV19.825udJ6bE3p1HpZM_1QZ83DVpOTtuvGE_cQvJby7d0k' } }); } diff --git a/tests-server/mercure.html b/tests-server/mercure.html index d162ad8..5325d7e 100644 --- a/tests-server/mercure.html +++ b/tests-server/mercure.html @@ -53,7 +53,7 @@ // You can pass formData as a fetch body directly: fetch(form.action, { method: form.method, body: body.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl0sInB1Ymxpc2giOlsiKiJdfX0.NXhzhXJ8VTxiRRW3pAB4EgP7s_guZeibwzAGw3wZ_KY' + 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0Ly53ZWxsLWtub3duL21lcmN1cmUiLCJzdWIiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJjbGllbnRfaWQiOiJlc2EtdGVzdC1zZXJ2ZXIiLCJpYXQiOjE3ODg0MjU1MDUsImV4cCI6NDEwMjQ0NDgwMCwiYXV0aG9yaXphdGlvbl9kZXRhaWxzIjpbeyJ0eXBlIjoiaHR0cHM6Ly9tZXJjdXJlLnJvY2tzL2F1dGhvcml6YXRpb24tZGV0YWlsIiwiYWN0aW9ucyI6WyJwdWJsaXNoIl0sInRvcGljcyI6W3sibWF0Y2giOiIqIn1dfV19.825udJ6bE3p1HpZM_1QZ83DVpOTtuvGE_cQvJby7d0k' } }); } diff --git a/tests/mercure.spec.ts b/tests/mercure.spec.ts index 84c6829..c4e8d90 100644 --- a/tests/mercure.spec.ts +++ b/tests/mercure.spec.ts @@ -10,15 +10,15 @@ test('mercure', async ({ page }) => { num++ } - if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F1')) { + if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F1')) { requestedMercure = true } - if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F1&topic=%2Fauthors%2F2')) { + if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F1&match=%2Fauthors%2F2')) { subscribedToBoth = true } - if (request.url().startsWith('https://localhost/.well-known/mercure?topic=%2Fauthors%2F2')) { + if (request.url().startsWith('https://localhost/.well-known/mercure?match=%2Fauthors%2F2')) { unsubscribedAuthor1 = true } })