diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 74fff323ed..5e8b12aa45 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -693,7 +693,6 @@ func TestMetrics(t *testing.T) { mercure { transport local anonymous - publisher_jwt !ChangeMe! } php { @@ -707,7 +706,6 @@ func TestMetrics(t *testing.T) { mercure { transport local anonymous - publisher_jwt !ChangeMe! } php { diff --git a/caddy/frankenphp/Caddyfile b/caddy/frankenphp/Caddyfile index 0d85fc8019..b0b551c0c6 100644 --- a/caddy/frankenphp/Caddyfile +++ b/caddy/frankenphp/Caddyfile @@ -30,10 +30,20 @@ # Uncomment the following lines to enable Mercure and Vulcain modules #mercure { - # # Publisher JWT key - # publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG} - # # Subscriber JWT key - # subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG} + # # Trusted issuer: binds the iss claim access tokens must carry to its keys. + # # The localhost default suits local development only: in production, set + # # MERCURE_TRUSTED_ISSUERS to the stable identifier of your token issuer + # # (your app's URL for self-issued tokens). + # issuer {$MERCURE_TRUSTED_ISSUERS:https://localhost} { + # # Publisher JWT key + # publisher { + # jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG} + # } + # # Subscriber JWT key + # subscriber { + # jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG} + # } + # } # # Allow anonymous subscribers (double-check that it's what you want) # anonymous # # Enable the subscription API (double-check that it's what you want) diff --git a/caddy/go.mod b/caddy/go.mod index 55476af4c7..19f12ba7d4 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -11,8 +11,8 @@ require ( github.com/caddyserver/certmagic v0.25.4 github.com/dunglas/caddy-cbrotli v1.0.1 github.com/dunglas/frankenphp v1.12.7 - github.com/dunglas/mercure v0.24.2 - github.com/dunglas/mercure/caddy v0.24.2 + github.com/dunglas/mercure v1.0.0-alpha.3 + github.com/dunglas/mercure/caddy v1.0.0-alpha.3 github.com/dunglas/vulcain/caddy v1.4.2 github.com/prometheus/client_golang v1.24.1 github.com/spf13/cobra v1.10.2 @@ -60,6 +60,7 @@ require ( github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/dlclark/regexp2/v2 v2.1.1 // indirect + github.com/dunglas/go-urlpattern v0.0.0-20260716093037-fb05c4998526 // indirect github.com/dunglas/httpsfv v1.1.0 // indirect github.com/dunglas/skipfilter v1.0.0 // indirect github.com/dunglas/vulcain v1.4.2 // indirect @@ -117,6 +118,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nlnwa/whatwg-url v0.6.2 // indirect github.com/oasdiff/yaml v0.1.0 // indirect github.com/oasdiff/yaml3 v0.0.13 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect @@ -214,7 +216,7 @@ require ( google.golang.org/api v0.283.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.81.1 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/caddy/go.sum b/caddy/go.sum index c4bf12cf14..4dde30b47d 100644 --- a/caddy/go.sum +++ b/caddy/go.sum @@ -93,6 +93,7 @@ github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.24.6 h1:qcrftZUVBIwfs+m+nhoCBAPT+ZPZZjti8SbHbDQQkZ4= github.com/bits-and-blooms/bitset v1.24.6/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/caddyserver/caddy/v2 v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0= @@ -152,12 +153,18 @@ github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtth github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmVmdBE3Y= github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY= +github.com/dunglas/go-urlpattern v0.0.0-20260716093037-fb05c4998526 h1:biCci7wlx/ChMqOkoUw6FPrhEZYRpulBZ+fO4Geo5Xs= +github.com/dunglas/go-urlpattern v0.0.0-20260716093037-fb05c4998526/go.mod h1:9qyjDljBPOWyWCGz7vo3Ek7cdnoG/DVk0Ucle7gWVS8= github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dunglas/mercure v0.24.2 h1:0uKT5XR/tkaGWhwUlXqjWek/0w+dY1n8HEY9Q8YB0hw= github.com/dunglas/mercure v0.24.2/go.mod h1:BAaiHFKr5+rS5SUP1tJBRXlCjPWZmzy1n0BDlVJnWzY= +github.com/dunglas/mercure v1.0.0-alpha.3 h1:e+u4JpBTMNv0sd3igwSo5RzqSVWuo3E6HOMonhfTP94= +github.com/dunglas/mercure v1.0.0-alpha.3/go.mod h1:7i/xv2fXgCRgoJe/Yj+FYY7YV4VdMGkCpFNJEfFfqCo= github.com/dunglas/mercure/caddy v0.24.2 h1:QjdJXvyJQaOtrt/LSk4QoCdFmRbyTVMl72bqmAGFPxw= github.com/dunglas/mercure/caddy v0.24.2/go.mod h1:9e8vEukfn1eq0iHCsBwWWSsHBlNt660L2wUsisnOOWM= +github.com/dunglas/mercure/caddy v1.0.0-alpha.3 h1:l5cfLXIjKsZqNsMYYnYUwk3QYFMxDWb06P9HOn/HL64= +github.com/dunglas/mercure/caddy v1.0.0-alpha.3/go.mod h1:jm1xTnsY4vY20Rc4dDYZI2MNY+IaVQmi5opHWOiuUgs= github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4= github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w= github.com/dunglas/vulcain v1.4.2 h1:Y5iOA/wq8TJuxBPg9tIbTjgoaCrK0hqOETOb3WN1a8U= @@ -313,6 +320,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q= +github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk= github.com/oasdiff/yaml v0.1.0 h1:0bqZjfKc/8S9urj4JuwepX41WX9EoA6ifhU3SV06cXg= github.com/oasdiff/yaml v0.1.0/go.mod h1:kOlRmMdL2X3vucLCEQO5u61SU22RysnfXvcttrZA1O0= github.com/oasdiff/yaml3 v0.0.13 h1:06svmvOHOVBqF81+sY2EUScvUI/iS/vl2VIeUUxZQwg= @@ -551,6 +560,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= @@ -573,6 +583,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -583,6 +594,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= @@ -600,6 +612,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= @@ -611,6 +624,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= @@ -622,6 +636,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= @@ -648,6 +663,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 h1:rgSNvqscFZ1JgV/4wH5GOsZFSFkR2Eua9As3KIr2LlM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2/go.mod h1:iMEtFwDlAhjDU9L5mY6U1XLwlIId/G3h+QcBHDIvrJ8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/caddy/hotreload.go b/caddy/hotreload.go index 4b9c0ebe69..ddba50fb70 100644 --- a/caddy/hotreload.go +++ b/caddy/hotreload.go @@ -49,7 +49,9 @@ func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error { } app.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch)) - f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?topic=" + url.QueryEscape(f.HotReload.Topic) + // "match" is the exact topic matcher of the Mercure protocol, the "topic" + // parameter it replaces is only honored in compatibility mode. + f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?match=" + url.QueryEscape(f.HotReload.Topic) return nil } diff --git a/caddy/hotreload_test.go b/caddy/hotreload_test.go index 9eed620453..4324dc7d63 100644 --- a/caddy/hotreload_test.go +++ b/caddy/hotreload_test.go @@ -20,7 +20,7 @@ import ( func TestHotReload(t *testing.T) { const topic = "https://frankenphp.dev/hot-reload/test" - u := "/.well-known/mercure?topic=" + url.QueryEscape(topic) + u := "/.well-known/mercure?match=" + url.QueryEscape(topic) tmpDir := t.TempDir() indexFile := filepath.Join(tmpDir, "index.php") @@ -40,7 +40,6 @@ func TestHotReload(t *testing.T) { http://localhost:`+testPort+` { mercure { transport local - subscriber_jwt TestKey anonymous } diff --git a/caddy/mercure.go b/caddy/mercure.go index 502f32895c..9bc780be02 100644 --- a/caddy/mercure.go +++ b/caddy/mercure.go @@ -48,16 +48,32 @@ func createMercureRoute() (caddyhttp.Route, error) { return caddyhttp.Route{}, errors.New(`the "MERCURE_SUBSCRIBER_JWT_KEY" environment variable must be set to use the Mercure.rocks hub`) } + // The protocol requires access tokens to name their issuer, so the keys are + // bound to one trusted issuer instead of being set globally. + issuer := os.Getenv("MERCURE_TRUSTED_ISSUERS") + if issuer == "" { + issuer = "https://localhost" + } + mercureRoute := caddyhttp.Route{ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject( mercureCaddy.Mercure{ - PublisherJWT: mercureCaddy.JWTConfig{ - Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"), - Key: mercurePublisherJwtKey, - }, - SubscriberJWT: mercureCaddy.JWTConfig{ - Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"), - Key: mercureSubscriberJwtKey, + Issuers: []mercureCaddy.IssuerConfig{ + { + Identifier: issuer, + Publisher: mercureCaddy.VerifierConfig{ + JWT: mercureCaddy.JWTConfig{ + Alg: os.Getenv("MERCURE_PUBLISHER_JWT_ALG"), + Key: mercurePublisherJwtKey, + }, + }, + Subscriber: mercureCaddy.VerifierConfig{ + JWT: mercureCaddy.JWTConfig{ + Alg: os.Getenv("MERCURE_SUBSCRIBER_JWT_ALG"), + Key: mercureSubscriberJwtKey, + }, + }, + }, }, }, "handler", diff --git a/docs/mercure.md b/docs/mercure.md index ebf6541e50..abccd70be6 100644 --- a/docs/mercure.md +++ b/docs/mercure.md @@ -22,11 +22,16 @@ Here is a minimal example of a `Caddyfile` enabling both FrankenPHP and the Merc localhost mercure { - # The secret key used to sign the JWT tokens for publishers - publisher_jwt !ChangeThisMercureHubJWTSecretKey! - # When publisher_jwt is set, you must set subscriber_jwt too! - subscriber_jwt !ChangeThisMercureHubJWTSecretKey! - # Allows anonymous subscribers (without JWT) + # The issuer of the access tokens, and the keys verifying them + issuer https://localhost { + publisher { + jwt !ChangeThisMercureHubJWTSecretKey! + } + subscriber { + jwt !ChangeThisMercureHubJWTSecretKey! + } + } + # Allows anonymous subscribers (without access token) anonymous } @@ -34,6 +39,9 @@ root public/ php_server ``` +Access tokens must carry the identifier of the `issuer` they were signed by in their `iss` claim. +`mercure_publish()` doesn't need any token: it publishes directly, in-process. + > [!TIP] > > The [sample `Caddyfile`](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile) @@ -52,7 +60,7 @@ To subscribe to updates, use the native [`EventSource`](https://developer.mozill