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>
3 files changed,
+19
−11
broker/broker_test.go+11 −7docs/integrations.md+7 −2integrations/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": [ |