eleven / messenger Public

integrations: Stripe OAuth mints read_write for both caps (#760)

Stripe refuses the read_only OAuth scope without a per-platform support
grant ('Please use the read_write scope, or contact support…' — hit live on
the sandbox). Until that's requested, one scope list serves both caps and
the connection cap alone enforces read-only for Stripe; documented honestly
in docs/integrations.md.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell fd6be7d898ca5b1cee2f1ca0223fe3c6a3f3567f parent 46530a8
3 files changed, +19 −11
  • broker/broker_test.go +11 −7
  • docs/integrations.md +7 −2
  • integrations/registry/manifests/stripe.json +1 −2
diff --git a/broker/broker_test.go b/broker/broker_test.go
index 925145b..73cc6f3 100644
--- a/broker/broker_test.go
+++ b/broker/broker_test.go
@@ -217,10 +217,12 @@ func TestMultiServiceExchangeAndConfig(t *testing.T) {
// /api/refresh rotates an expiring pair: grant_type=refresh_token with the
// service's client pair, and the WHOLE new pair (access + rotated refresh +
// expiry) comes back for the sidecar to reseal. Unauthenticated → 401.
-// A manifest that declares OAuth scope lists (stripe: read_only vs
-// read_write) gets them on /config — public data the client uses to put the
-// cap-matched scope set on the authorize URL. Scope-less services (dnsimple)
-// stay scope-less.
+// A manifest that declares OAuth scope lists gets them on /config — public
+// data the client uses to put the cap-matched scope set on the authorize URL.
+// Stripe declares ONE scope list (read_write serves both caps: Stripe refuses
+// the read_only OAuth scope without a per-platform support grant, so the
+// connection cap is the read-only enforcement there); HubSpot declares both.
+// Scope-less services (dnsimple) stay scope-less.
func TestConfigCarriesManifestScopes(t *testing.T) {
ts := newTestBroker(t, "https://unused.example")
resp, err := http.Get(ts.URL + "/config?service=stripe")
@@ -231,9 +233,11 @@ func TestConfigCarriesManifestScopes(t *testing.T) {
var c map[string]any
json.NewDecoder(resp.Body).Decode(&c)
scopes, _ := c["scopes"].([]any)
- scopesWrite, _ := c["scopes_write"].([]any)
- if len(scopes) != 1 || scopes[0] != "read_only" || len(scopesWrite) != 1 || scopesWrite[0] != "read_write" {
- t.Fatalf("stripe scopes: %v / %v", c["scopes"], c["scopes_write"])
+ if len(scopes) != 1 || scopes[0] != "read_write" {
+ t.Fatalf("stripe scopes: %v", c["scopes"])
+ }
+ if _, has := c["scopes_write"]; has {
+ t.Fatalf("stripe declares one scope list; scopes_write should be absent: %v", c)
}
respD, err := http.Get(ts.URL + "/config?service=dnsimple")
diff --git a/docs/integrations.md b/docs/integrations.md
index e1b6500..83a64c5 100644
--- a/docs/integrations.md
+++ b/docs/integrations.md
@@ -199,8 +199,13 @@ can do, growable by PR without touching this codebase. The pipeline:
Out of scope, called out honestly: standing charter grants (above),
registering the OAuth apps themselves (a human act per provider — the Stripe
and HubSpot manifests are OAuth-ready, scopes mapped to the connection cap
-[Stripe `read_only`/`read_write`; HubSpot CRM read scopes, contacts.write
-only on a read & write connection]; the broker offers each Connect button
+[HubSpot: CRM read scopes always, contacts.write only on a read & write
+connection. Stripe: `read_write` for BOTH caps — Stripe refuses the
+`read_only` OAuth scope unless support grants the platform read-only
+connections, so until that's requested the connection cap alone enforces
+read-only there, and the provider token is broader than the cap; ask Stripe
+support for read-only connections before go-live if that residue matters];
+the broker offers each Connect button
the moment `LCHAT_OAUTH_<ID>_CLIENT_ID`/`_CLIENT_SECRET` land in its env,
and until then the catalog offers paste-a-token alone. NB Stripe's OAuth
`client_secret` is the platform's own secret key — treat its env value with
diff --git a/integrations/registry/manifests/stripe.json b/integrations/registry/manifests/stripe.json
index f8d0807..042cfb9 100644
--- a/integrations/registry/manifests/stripe.json
+++ b/integrations/registry/manifests/stripe.json
@@ -11,8 +11,7 @@
"oauth": {
"authorize_url": "https://connect.stripe.com/oauth/authorize",
"token_url": "https://connect.stripe.com/oauth/token",
- "scopes": ["read_only"],
- "scopes_write": ["read_write"]
+ "scopes": ["read_write"]
}
},
"ops": [