diff --git a/docs/assets/openapi.json b/docs/assets/openapi.json index 602a7f7b5..6c0e82093 100644 --- a/docs/assets/openapi.json +++ b/docs/assets/openapi.json @@ -1628,7 +1628,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -1718,6 +1718,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -1772,6 +1783,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken", "connect" @@ -2017,7 +2029,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -2107,6 +2119,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -2143,6 +2166,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], @@ -2374,7 +2398,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -2464,6 +2488,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -2500,6 +2535,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], @@ -2727,7 +2763,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -2817,6 +2853,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -2853,6 +2900,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], @@ -3096,7 +3144,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -3186,6 +3234,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -3264,6 +3323,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken", "link" @@ -3533,7 +3593,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -3623,6 +3683,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -3659,6 +3730,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], @@ -3954,7 +4026,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -4044,6 +4116,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -4080,6 +4163,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], @@ -4559,7 +4643,7 @@ }, "scope": { "type": "string", - "description": "For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed" + "description": "For client_credentials and authorization, the scopes asked, separated by spaces, none when left out" }, "parameters": { "type": "object", @@ -4649,6 +4733,17 @@ ], "description": "The id of the caller who connected it, approving it or giving its credential" }, + "granted_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null" + }, "credential_ends_with": { "anyOf": [ { @@ -4685,6 +4780,7 @@ "added_at", "connected_at", "connected_by", + "granted_scope", "credential_ends_with", "broken" ], diff --git a/docs/reference/http.md b/docs/reference/http.md index 60f84cb4d..939aa71b8 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -1101,15 +1101,15 @@ A connection is a tool server that a person adds to the org through their agent, | `start` | `POST /connections/{id}/consent` | any, with the link's token and the adder's session | `token` | | `finish` | `POST /connections/{id}/consent/code` | any, with the link's token and the adder's session | `token`, `code`, `state`, and `iss` where the authorization server sent one; or `token`, `state` and `handle` for a consent begun through Auto's consent service | -Every operation answers the connection: `id`, a UUIDv7; `name`; `type`, `mcp` or `openapi`; `provider`, the name of the provider it was added from, or `null` for a server given by hand; `placeholders`, the values given for the provider's placeholders, or `null`; `entry`; `credential`, how it is credentialled, never its value, and for a connection connected by consent the client it was made with and the scope the person allowed; `status`, `pending`, `connected`, `broken` or `removed`; `added_by`; `added_at`; `connected_at`; `connected_by`, the id of the caller who connected it, by approving it or giving its credential, or `null`; `credential_ends_with`, the last four characters of a secret or client secret of twelve characters or more, which the secret store keeps beside the value, or `null`; and `broken`, why it is broken, or `null`. `add_connection` answers `201` with the connection and `connect`, `{ url, expires_at }`: `url` is Studio's page `connect` under the Studio the person can reach, `/studio/connect?connection=&token=` where the runtime serves Studio, as it does in local mode, since that page knows the runtime that serves it and asks it for the org, and `https://studio.on.auto/connect?server=&org=&connection=&token=` otherwise; the runtime's origin is `PUBLIC_URL`, or the server's own address in local mode, and the link expires ten minutes on. `list_connections` answers them newest first in `connections`, the removed ones only with `include_removed`. `read_link` answers the connection with `link`, `{ expires_at }`, when its link expires, so the page can say how long the person has, `consent`, `{ started_at, expires_at }`, while a consent is started and not finished, and, for a connection whose credential is `authorization`, `client_source`, the source of the client a start would take now, by the rule the start takes it by, `auto` where it would begin through Auto's consent service, left out where the runtime has no client the provider's way takes; `approve`, `give_credential` and `finish`, which use the link up, answer the connection alone. +Every operation answers the connection: `id`, a UUIDv7; `name`; `type`, `mcp` or `openapi`; `provider`, the name of the provider it was added from, or `null` for a server given by hand; `placeholders`, the values given for the provider's placeholders, or `null`; `entry`; `credential`, how it is credentialled, never its value, and for a connection connected by consent the client it was made with; `status`, `pending`, `connected`, `broken` or `removed`; `added_by`; `added_at`; `connected_at`; `connected_by`, the id of the caller who connected it, by approving it or giving its credential, or `null`; `granted_scope`, for a connection connected by consent, the scope the server granted, which may be fewer scopes than its credential asked, as when the person unticks one on the provider's page, or `null`; `credential_ends_with`, the last four characters of a secret or client secret of twelve characters or more, which the secret store keeps beside the value, or `null`; and `broken`, why it is broken, or `null`. `add_connection` answers `201` with the connection and `connect`, `{ url, expires_at }`: `url` is Studio's page `connect` under the Studio the person can reach, `/studio/connect?connection=&token=` where the runtime serves Studio, as it does in local mode, since that page knows the runtime that serves it and asks it for the org, and `https://studio.on.auto/connect?server=&org=&connection=&token=` otherwise; the runtime's origin is `PUBLIC_URL`, or the server's own address in local mode, and the link expires ten minutes on. `list_connections` answers them newest first in `connections`, the removed ones only with `include_removed`. `read_link` answers the connection with `link`, `{ expires_at }`, when its link expires, so the page can say how long the person has, `consent`, `{ started_at, expires_at }`, while a consent is started and not finished, and, for a connection whose credential is `authorization`, `client_source`, the source of the client a start would take now, by the rule the start takes it by, `auto` where it would begin through Auto's consent service, left out where the runtime has no client the provider's way takes; `approve`, `give_credential` and `finish`, which use the link up, answer the connection alone. - With `provider`, the provider gives the server and how it is credentialled. `name` is the provider's when left out; `placeholders` gives the value of each placeholder the provider takes; `credential` may give its `kind` alone, one of the provider's ways, and when it is left out the addition takes the first way `list_providers` marks offered, or the provider's first where it offers none, which the addition then refuses in its words; and `entry` may give `playbooks`, `allowed` and `record_content` alone. `type`, the server's fields of `entry` and every field of `credential` but `kind` are refused, each at its field, since the provider gives them; `credential.client_id` is refused since the person gives the client id on the page. The connection's `entry` and `credential` are the provider's with each value in its place, and every rule below applies to them as to a server given by hand. Without `provider`, `name`, `type`, `entry` and `credential` are each required and `placeholders` is refused. - `entry` holds the server without its credential: `url` for `mcp`; `document` and `base_url` for `openapi`; and `headers`, `query_params`, `allowed`, `playbooks`, `record_content` and `request_id`, as an entry of the configuration file has them. `url` and `document` are each an `https` URL, or an `http` URL on a loopback address. -- `credential` is one object, `kind` and the fields that kind takes, each optional in the schema and refused as `invalid_input` at the field where its kind lacks it or does not take it: `none` takes no other field; `secret` takes `name` and `in`, `header`, `query` or `security`, so `name` is the header, the query parameter or the security scheme of the document the value is sent as, and only an `openapi` connection takes `query` or `security`; `reference` takes `variable`, a variable of the runtime such as `CRM_API_KEY` that the operator lists in `connections.variables`, whose value is sent where its `name` and `in` say, as a secret's; `client_credentials` takes `issuer`, an `https` URL or an `http` URL on a loopback address, an optional `client_id` and an optional `scope`; and `authorization`, a provider's way alone, by which the person signs in to the provider and allows Auto, takes `issuer`, and as the provider gives them `scope`, `parameters`, `authorization_endpoint`, `token_endpoint`, `revocation_endpoint` and `clients`, the client sources the provider's authorization server takes, `registered`, `metadata_document` or `dynamic`, and once connected `client_id` and `client_source`, the client the connection was made with, and `scope` as the server granted it. A server given by hand is refused `authorization` at `/credential/kind`, since consent is offered through a provider. The same object is what every operation answers and what the org's record keeps. +- `credential` is one object, `kind` and the fields that kind takes, each optional in the schema and refused as `invalid_input` at the field where its kind lacks it or does not take it: `none` takes no other field; `secret` takes `name` and `in`, `header`, `query` or `security`, so `name` is the header, the query parameter or the security scheme of the document the value is sent as, and only an `openapi` connection takes `query` or `security`; `reference` takes `variable`, a variable of the runtime such as `CRM_API_KEY` that the operator lists in `connections.variables`, whose value is sent where its `name` and `in` say, as a secret's; `client_credentials` takes `issuer`, an `https` URL or an `http` URL on a loopback address, an optional `client_id` and an optional `scope`; and `authorization`, a provider's way alone, by which the person signs in to the provider and allows Auto, takes `issuer`, and as the provider gives them `scope`, `parameters`, `authorization_endpoint`, `token_endpoint`, `revocation_endpoint` and `clients`, the client sources the provider's authorization server takes, `registered`, `metadata_document` or `dynamic`, and once connected `client_id` and `client_source`, the client the connection was made with, while `scope` stays the scope the way asked. A server given by hand is refused `authorization` at `/credential/kind`, since consent is offered through a provider. The same object is what every operation answers and what the org's record keeps. - A `secret` or a `reference` sent in a header may name its `scheme`, such as `Bearer`, which the runtime writes, with a space, before the value it sends, so the person gives the token alone, the variable of a reference holds the token alone, and the server receives `Authorization: Bearer `. A `scheme` holds the characters of a header name and no `$`, and a credential that places no value in a header takes none. `give_credential` refuses a value that already starts with the scheme and a space, in any case, at `/value`. - The `client_id` of client credentials is the org's, so the agent may leave it out and the person gives it on Studio's page with the client secret or the private key: a connection whose credential holds none answers its record without it, `give_credential` takes `client_id`, and the record holds it from then on. `give_credential` refuses a `client_id` at `/client_id` where the credential holds one, refuses one missing where it holds none, and refuses one that looks like a credential, since the record keeps the client id in the open and the secret or the key goes in its own field, where it is sealed. `add_connection` refuses a `client_id` that looks like a credential at `/credential/client_id`. - `get_connection` and the five routes of the link are served over HTTP alone, and no MCP endpoint serves them. `get_connection` reads one connection of the org by its id; an agent sees whether the person approved a connection in the `status` `list_connections` answers for each. The three routes of the link are Studio's page's alone. Each takes the token, which the runtime keeps only as its SHA-256, and matches it to the open link of the connection: one given less than ten minutes ago that nothing has used up. -- `start` and `finish` connect a connection whose credential is `authorization`. `start` answers `201` with `authorize`, `{ url, expires_at }`, and `client_source`, the source of the client it started with: `url` is the page of the provider's authorization server where the person signs in and allows Auto, which carries the client, the return address, the page `connect-return` of the Studio the connection's link opens, the Studio host's `/connect-return`, `STUDIO_ORIGIN` followed by it, `https://studio.on.auto/connect-return` unless it is set, or, where the runtime serves Studio, `/studio/connect-return` on the runtime's own address, the scope, a state and a challenge, and `expires_at` is ten minutes on, the time the consent has of its own; a second start replaces the first. The server sends the person back to the return address with a code, the state and its issuer, and Studio sends them to `finish` with `token`, `code`, `state` and `iss`, which redeems the code, keeps the tokens sealed in the runtime's secret store and connects the connection. `finish` is bound to the start and not to the link's ten minutes, and refuses a state the consent was not started with at `/state`, an issuer that is not the server's at `/iss`, and a code the server refuses at `/code`, in Auto's own words; a refused code leaves the start open. The scope it records is the one the server granted, its scopes separated by single spaces, or the one asked where the server says none; a grant of no scope where one was asked, or of a scope that is not one, is refused, an empty grant where none was asked records none, and a finish refused once the tokens exist revokes them first. A connection connected by consent then serves its tools, and the runtime renews its tokens; [Let people connect by consent](../self-host/connections.md#let-people-connect-by-consent) says how. A start of `client_source` `auto` began through Auto's consent service, with Auto's client at the vendor: its `url` is the page Auto's consent service made, whose return is the Studio host, which sends the person back to the Studio the link opened with a handle and the state, and `finish` takes `token`, `state` and `handle`, takes the tokens from the service and connects the connection. A finish of the other shape than its start's is refused as `invalid_input` at the field it gave, `/code` or `/handle`, saying which the consent answers, and a handle the service refuses at `/handle`; [Consent through Auto](../self-host/connections.md#consent-through-auto) says when a runtime starts one. +- `start` and `finish` connect a connection whose credential is `authorization`. `start` answers `201` with `authorize`, `{ url, expires_at }`, and `client_source`, the source of the client it started with: `url` is the page of the provider's authorization server where the person signs in and allows Auto, which carries the client, the return address, the page `connect-return` of the Studio the connection's link opens, the Studio host's `/connect-return`, `STUDIO_ORIGIN` followed by it, `https://studio.on.auto/connect-return` unless it is set, or, where the runtime serves Studio, `/studio/connect-return` on the runtime's own address, the scope, a state and a challenge, and `expires_at` is ten minutes on, the time the consent has of its own; a second start replaces the first. The server sends the person back to the return address with a code, the state and its issuer, and Studio sends them to `finish` with `token`, `code`, `state` and `iss`, which redeems the code, keeps the tokens sealed in the runtime's secret store and connects the connection. `finish` is bound to the start and not to the link's ten minutes, and refuses a state the consent was not started with at `/state`, an issuer that is not the server's at `/iss`, and a code the server refuses at `/code`, in Auto's own words; a refused code leaves the start open. The scope it records as `granted_scope` is the one the server granted, its scopes separated by single spaces, or the one asked where the server says none; a grant of no scope where one was asked, or of a scope that is not one, is refused, an empty grant where none was asked records none, and a finish refused once the tokens exist revokes them first. A connection connected by consent then serves its tools, and the runtime renews its tokens; [Let people connect by consent](../self-host/connections.md#let-people-connect-by-consent) says how. A start of `client_source` `auto` began through Auto's consent service, with Auto's client at the vendor: its `url` is the page Auto's consent service made, whose return is the Studio host, which sends the person back to the Studio the link opened with a handle and the state, and `finish` takes `token`, `state` and `handle`, takes the tokens from the service and connects the connection. A finish of the other shape than its start's is refused as `invalid_input` at the field it gave, `/code` or `/handle`, saying which the consent answers, and a handle the service refuses at `/handle`; [Consent through Auto](../self-host/connections.md#consent-through-auto) says when a runtime starts one. - `approve` connects a connection whose credential is `none` or `reference` and uses its link up. `give_credential` keeps the value sealed in the runtime's secret store, connects the connection and uses its link up: giving the credential is its approval. - A connection is added by a person: on a runtime that signs people in, `add_connection` refuses a key, and a runtime of API keys alone, which signs nobody in, refuses every connection, since no person could approve it. - On a runtime that signs people in, `approve`, `give_credential`, `start` and `finish` need the session of the person who added the connection. In local mode the token alone binds. diff --git a/docs/self-host/connections.md b/docs/self-host/connections.md index 60a6d3454..3234194b1 100644 --- a/docs/self-host/connections.md +++ b/docs/self-host/connections.md @@ -28,12 +28,12 @@ A provider is a service Auto knows by its name: its server, an MCP server or an | `display-video-360` | Read partners, advertisers, campaigns, insertion orders, line items, ad groups and creatives, and change campaigns, insertion orders and line items | The Display & Video 360 API, `displayvideo.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `display-video-360-reports` | Create, run, list and remove the report queries of Display & Video 360, and read their reports | The Bid Manager API, `doubleclickbidmanager.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `github` | Read repositories, files and commits, and read, open and update issues and pull requests | GitHub's MCP server, `api.githubcopilot.com` | Signs in to GitHub, or gives a fine-grained personal access token | -| `gmail` | Search and read threads and their messages, list and create drafts, and list labels and label or unlabel messages and threads; it sends no mail | Google's Gmail MCP server, `gmailmcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | +| `gmail` | Search and read threads and their messages, and list drafts and labels; it drafts, labels and sends no mail, since it asks the scope that reads alone | Google's Gmail MCP server, `gmailmcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-ads` | Query accounts with the query language of Google Ads, and create or change campaign budgets, campaigns, ad groups and ad group criteria | The Google Ads API, `googleads.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-analytics` | Run reports, pivot, batch and realtime reports of Google Analytics 4, read a property's metadata and make audience exports | The Google Analytics Data API, `analyticsdata.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | -| `google-analytics-admin` | Read accounts, properties, data streams, custom dimensions and metrics and key events, create custom dimensions and key events, and run data access reports | The Google Analytics Admin API, `analyticsadmin.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | +| `google-analytics-admin` | Read accounts, properties, data streams, custom dimensions and metrics and key events, and run data access reports; it creates and changes nothing, since it asks the scope that reads alone | The Google Analytics Admin API, `analyticsadmin.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-docs` | Read documents and update their content | Google's Docs MCP server, `docsmcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | -| `google-drive` | Search and list files, read their metadata, sharing and content, download them, and create and copy files | Google's Drive MCP server, `drivemcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | +| `google-drive` | Search and list the files the person opens with Auto or Auto creates, not the rest of their Drive, read their metadata, sharing and content, download them, and create and copy files | Google's Drive MCP server, `drivemcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-search-console` | Query clicks, impressions and positions, inspect how Google indexes a URL, list sitemaps and add or remove properties, of domain and URL-prefix properties | The Google Search Console API, `searchconsole.googleapis.com`, from a document Auto ships | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-sheets` | Read spreadsheets and cell values, update values and formulas, insert rows and columns, and change spreadsheets | Google's Sheets MCP server, `sheetsmcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | | `google-slides` | Read presentations and update them | Google's Slides MCP server, `slidesmcp.googleapis.com`, in Google's developer preview | By consent: signing in to Google, as [The Google providers](#the-google-providers) sets out | @@ -282,13 +282,13 @@ Auto takes the first source it has: Until Auto has a source a provider's way takes, `list_providers` shows the way as one you set up first, `search` leaves the provider out of the providers it lists to an agent, and `add_connection` refuses it. `allowed_hosts` applies to the issuer and to every endpoint the way names when a connection is added, to every endpoint the authorization server's metadata names when a consent starts, before the person is sent anywhere, and to every request a consent, a renewal of its tokens or their revocation sends. Leaving `authorization` out of `credential_kinds` lists no provider whose only way it is. -The consent starts on Studio's page and has ten minutes of its own. The person signs in to the provider and allows Auto, the provider sends them back to the return address of the Studio the connection's link opens, the page `connect-return` of the Studio Auto serves at `/studio/` where it serves one, as it does in local mode, and otherwise the Studio host's `/connect-return`, `STUDIO_ORIGIN` followed by it, `https://studio.on.auto/connect-return` unless it is set, and Studio finishes the consent, which keeps the tokens sealed in the secret store; no record, answer or history holds a token, a code or the state of a consent. The connection records the client it was made with and the scope the person allowed, which may be narrower than the way asks; where the metadata Auto discovers for the authorization server lists `offline_access` and the way's scope lacks it, Auto asks for it too, so the server gives a refresh token, and a consent that grants no scope where one was asked, or a scope that is not one, is refused and its tokens revoked. +The consent starts on Studio's page and has ten minutes of its own. The person signs in to the provider and allows Auto, the provider sends them back to the return address of the Studio the connection's link opens, the page `connect-return` of the Studio Auto serves at `/studio/` where it serves one, as it does in local mode, and otherwise the Studio host's `/connect-return`, `STUDIO_ORIGIN` followed by it, `https://studio.on.auto/connect-return` unless it is set, and Studio finishes the consent, which keeps the tokens sealed in the secret store; no record, answer or history holds a token, a code or the state of a consent. The connection records the client it was made with and, as `granted_scope` beside the scope its credential asked, the scope the person allowed, which is narrower than the way asks where the person unticks a scope on the provider's page, so Studio and an agent see a connection that holds less than its provider asked; where the metadata Auto discovers for the authorization server lists `offline_access` and the way's scope lacks it, Auto asks for it too, so the server gives a refresh token, and a consent that grants no scope where one was asked, or a scope that is not one, is refused and its tokens revoked. A connection connected by consent serves its tools as any connected connection does, sending the access token the provider gave. Auto renews the tokens itself, with the refresh token and the client the connection was made with, a minute before the access token ends or halfway through a shorter life, and keeps the new ones in the same sealed row; every server that shares the ledger serves that row, and one renews it at a time while the others wait for it and take what it renewed. A token the provider gave without an end, or without a refresh token, is used until the provider refuses it. The connection breaks, and the person who added it removes it and adds it again, when the provider refuses to renew the tokens because the consent was revoked or has ended, when it refuses an access token that no refresh token can renew, and when the client the connection was made with is no longer under `registered_clients` with its id; any other failure to renew fails that call and breaks nothing. The server that failed holds the row for 45 seconds from when it claimed the row to renew it, the 30 seconds a request to the provider may take and 15 more, so no server renews it in that time: its later calls in that time use the access token while it lasts and otherwise fail at once in the same words, without waiting, and the first call after that tries again. Removing the connection revokes its tokens at the provider first, with the client it was made with; where the provider refuses, does not answer or cannot be asked, since `allowed_hosts` refuses its host or the client is no longer under `registered_clients`, Auto logs at WARN why the tokens end on their own, in words that hold no token, and removes the connection all the same. ### The providers Auto ships that take consent -Each way names as its issuer the authorization server its server names in its own metadata, or, for Zoom's HTTP API, which names none, the issuer of Zoom's own metadata; asks the scope its tools need, or, at Zoom, none, so that Zoom grants the scopes of its app; and takes the client sources its authorization server takes. A client you register at the vendor takes the return address of your runtime as its redirect address, as `registered` above says, and goes under `registered_clients` with the issuer below. Every host of a way passes `allowed_hosts`, those of its endpoints among them. Google's thirteen share one issuer, with a client for each tier of Google's verification you offer, and Microsoft's two, Amazon's two and Adobe's two each take a setup of their vendor's, as [The Google providers](#the-google-providers), [The Microsoft providers](#the-microsoft-providers), [The Amazon providers](#the-amazon-providers) and [The Adobe providers](#the-adobe-providers) set out after the four below. +Each way names as its issuer the authorization server its server names in its own metadata, or, for Zoom's HTTP API, which names none, the issuer of Zoom's own metadata; asks the scope its tools need, or, at Zoom, none, so that Zoom grants the scopes of its app; and takes the client sources its authorization server takes. A client you register at the vendor takes the return address of your runtime as its redirect address, as `registered` above says, and goes under `registered_clients` with the issuer below. Every host of a way passes `allowed_hosts`, those of its endpoints among them. Google's thirteen share one issuer, with one client for the APIs you offer and another for the Workspace servers, and Microsoft's two, Amazon's two and Adobe's two each take a setup of their vendor's, as [The Google providers](#the-google-providers), [The Microsoft providers](#the-microsoft-providers), [The Amazon providers](#the-amazon-providers) and [The Adobe providers](#the-adobe-providers) set out after the four below. | Provider | Issuer | Scope asked | Client sources | What you do at the vendor | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -307,39 +307,40 @@ A Zoom for Government account is served by its own marketplace at `zoomgov.com` ### The Google providers -Thirteen of the providers this version ships are Google's, each a provider of its own with a consent of its own, so a person who connects Drive and Gmail allows Auto twice, once for each. Each takes consent at Google's authorization server, the issuer `https://accounts.google.com`, through a client you register there, `registered`, the one source Google takes, since it registers no client itself and names no client by a metadata document. Each asks Google for `access_type: offline`, without which Google gives no refresh token, and `prompt: consent`, with which it gives one again to a person who allowed Auto before, and asks the least scope its tools need, the scope that reads before the one that writes, each written in full as `https://www.googleapis.com/auth/` and the name below. The table marks each scope Google classifies as restricted, and each provider that asks one names it in its description. Google verifies a project at the tier of the most demanding scope its consent screen asks, so the table gives each provider's tier: sensitive for the eight APIs, and restricted for the five Workspace servers, whose scopes Google's own pages fix, `drive.readonly` or `gmail.modify` among them. - -| Provider | The scopes it asks | Google's tier | The APIs you turn on | -| --------------------------- | -------------------------------------------------------------------------------------- | ------------- | ------------------------------------------- | -| `campaign-manager-360` | `dfareporting`, `dfatrafficking` | sensitive | Campaign Manager 360 API | -| `display-video-360` | `display-video` | sensitive | Display & Video 360 API | -| `display-video-360-reports` | `doubleclickbidmanager` | sensitive | DoubleClick Bid Manager API | -| `gmail` | `gmail.modify` (restricted) | restricted | Gmail API and Gmail MCP API | -| `google-ads` | `adwords` | sensitive | Google Ads API | -| `google-analytics` | `analytics.readonly` | sensitive | Google Analytics Data API | -| `google-analytics-admin` | `analytics.edit` | sensitive | Google Analytics Admin API | -| `google-docs` | `drive.readonly` (restricted), `drive.file`, `documents.readonly`, `documents` | restricted | Google Docs API and Google Docs MCP API | -| `google-drive` | `drive.readonly` (restricted), `drive.file` | restricted | Google Drive API and Google Drive MCP API | -| `google-search-console` | `webmasters` | sensitive | Google Search Console API | -| `google-sheets` | `drive.readonly` (restricted), `drive.file`, `spreadsheets.readonly`, `spreadsheets` | restricted | Google Sheets API and Google Sheets MCP API | -| `google-slides` | `drive.readonly` (restricted), `drive.file`, `presentations.readonly`, `presentations` | restricted | Google Slides API and Google Slides MCP API | -| `search-ads-360` | `doubleclicksearch` | sensitive | Search Ads 360 Reporting API | - -What you do at Google, once for each tier you offer, both or one, so that the providers of one tier are verified apart from those of the other: - -1. Make one Google Cloud project for the tier, and turn on in it the APIs of each provider of that tier you offer, in the table above. -2. Set up its consent screen in the Google Auth Platform: the branding a person sees when they allow Auto, the audience, External for people of any Google account or Internal for the people of your Google Workspace organization alone, and, under data access, the scopes of each provider of the tier you offer, and none of the other tier's. -3. Make one client in the project, of the type web application, with the return address the `registered` source above names among its authorized redirect addresses, and, in the project of the restricted tier, with Use this client for an AI-powered agent selected, since the Workspace servers refuse the requests of a client without it. List it under `registered_clients` with the issuer `https://accounts.google.com` and, as `scopes`, the scopes of its consent screen, each written in full, so that each provider takes the client of its tier: +Thirteen of the providers this version ships are Google's, each a provider of its own with a consent of its own, so a person who connects Drive and Gmail allows Auto twice, once for each. Each takes consent at Google's authorization server, the issuer `https://accounts.google.com`, through a client you register there, `registered`, the one source Google takes, since it registers no client itself and names no client by a metadata document. Each asks Google for `access_type: offline`, without which Google gives no refresh token, and `prompt: consent`, with which it gives one again to a person who allowed Auto before, and asks the scope its tools need, the scope that reads before the one that writes, each written in full as `https://www.googleapis.com/auth/` and the name below. The table marks each scope Google classifies as restricted, and each provider that asks one names it in its description. Google verifies a project at the tier of the most demanding scope its consent screen asks, so the table gives each provider's tier: sensitive for the eight APIs and for `google-docs`, `google-sheets` and `google-slides`, non-sensitive for `google-drive`, and restricted for `gmail`. + +| Provider | The scopes it asks | Google's tier | The APIs you turn on | +| --------------------------- | ------------------------------------------------------- | ------------- | ------------------------------------------- | +| `campaign-manager-360` | `dfareporting`, `dfatrafficking` | sensitive | Campaign Manager 360 API | +| `display-video-360` | `display-video` | sensitive | Display & Video 360 API | +| `display-video-360-reports` | `doubleclickbidmanager` | sensitive | DoubleClick Bid Manager API | +| `gmail` | `gmail.readonly` (restricted) | restricted | Gmail API and Gmail MCP API | +| `google-ads` | `adwords` | sensitive | Google Ads API | +| `google-analytics` | `analytics.readonly` | sensitive | Google Analytics Data API | +| `google-analytics-admin` | `analytics.readonly` | sensitive | Google Analytics Admin API | +| `google-docs` | `documents.readonly`, `documents` | sensitive | Google Docs API and Google Docs MCP API | +| `google-drive` | `drive.file` | non-sensitive | Google Drive API and Google Drive MCP API | +| `google-search-console` | `webmasters` | sensitive | Google Search Console API | +| `google-sheets` | `spreadsheets.readonly`, `spreadsheets` | sensitive | Google Sheets API and Google Sheets MCP API | +| `google-slides` | `drive.file`, `presentations.readonly`, `presentations` | sensitive | Google Slides API and Google Slides MCP API | +| `search-ads-360` | `doubleclicksearch` | sensitive | Search Ads 360 Reporting API | + +What the scopes leave out: `gmail` reads mail and lists drafts and labels, while the tools of Google's Gmail MCP server that draft, label or change mail take a scope it does not ask, and the server has no tool that sends; `google-analytics-admin` reads alone, so it creates no custom dimension or key event; and `google-drive` reaches the files the person opens with Auto or Auto creates, not the rest of their Drive, which `drive.file` limits every tool of Google's Drive MCP server to. The tools of the Docs, Sheets and Slides servers each take the scope of their own API, so `google-docs`, `google-sheets` and `google-slides` reach every document, spreadsheet and presentation the person can without a scope of Drive; `google-slides` asks `drive.file` too, which its server protects beside them. + +What you do at Google, once for the APIs and once for the Workspace servers, both or one, so that each is verified apart from the other: + +1. Make one Google Cloud project for the APIs, or for the Workspace servers, and turn on in it the APIs of each provider of that set you offer, in the table above. +2. Set up its consent screen in the Google Auth Platform: the branding a person sees when they allow Auto, the audience, External for people of any Google account or Internal for the people of your Google Workspace organization alone, and, under data access, the scopes of each provider of the set you offer, and none of the other set's. +3. Make one client in the project, of the type web application, with the return address the `registered` source above names among its authorized redirect addresses, and, in the project of the Workspace servers, with Use this client for an AI-powered agent selected, since the Workspace servers refuse the requests of a client without it. List it under `registered_clients` with the issuer `https://accounts.google.com` and, as `scopes`, the scopes of its consent screen, each written in full, so that each provider takes the client of its set: ```yaml connections: registered_clients: - issuers: [https://accounts.google.com] client_id: 1234567890-example.apps.googleusercontent.com - client_secret: ${GOOGLE_SENSITIVE_CLIENT_SECRET} + client_secret: ${GOOGLE_APIS_CLIENT_SECRET} scopes: - https://www.googleapis.com/auth/adwords - - https://www.googleapis.com/auth/analytics.edit - https://www.googleapis.com/auth/analytics.readonly - https://www.googleapis.com/auth/dfareporting - https://www.googleapis.com/auth/dfatrafficking @@ -349,26 +350,25 @@ What you do at Google, once for each tier you offer, both or one, so that the pr - https://www.googleapis.com/auth/webmasters - issuers: [https://accounts.google.com] client_id: 2345678901-example.apps.googleusercontent.com - client_secret: ${GOOGLE_RESTRICTED_CLIENT_SECRET} + client_secret: ${GOOGLE_WORKSPACE_CLIENT_SECRET} scopes: - https://www.googleapis.com/auth/documents - https://www.googleapis.com/auth/documents.readonly - https://www.googleapis.com/auth/drive.file - - https://www.googleapis.com/auth/drive.readonly - - https://www.googleapis.com/auth/gmail.modify + - https://www.googleapis.com/auth/gmail.readonly - https://www.googleapis.com/auth/presentations - https://www.googleapis.com/auth/presentations.readonly - https://www.googleapis.com/auth/spreadsheets - https://www.googleapis.com/auth/spreadsheets.readonly ``` - With one tier alone, its one client may leave `scopes` out. + With one set alone, its one client may leave `scopes` out. -4. Have each project verified for the providers of its tier. For both tiers Google verifies the brand of the consent screen and reviews a justification of each scope it asks and a video of how Auto uses it; for the restricted tier alone it also asks for a security assessment by an assessor it approves, since Auto reaches that data from a server, and asks for it again every year. Each project is verified on its own, so a finding on the one never touches the other. An Internal app, for the people of your Workspace organization alone, needs none of this. +4. Have each project verified for the providers it offers. Google verifies the brand of the consent screen and reviews a justification of each sensitive or restricted scope it asks and a video of how Auto uses it; where a project asks a restricted scope, as the project of the Workspace servers does for `gmail`, it also asks for a security assessment by an assessor it approves, since Auto reaches that data from a server, and asks for it again every year. Each project is verified on its own, so a finding on the one never touches the other. An Internal app, for the people of your Workspace organization alone, needs none of this. Until Google verifies a project, its consent screen says the app is not verified to the person who allows Auto. While the publishing status of an External app is Testing, the app takes the test users you list alone, and Google ends each refresh token it gives seven days after it gave it, so Auto cannot renew the tokens of a connection after that: the connection breaks, and the person who added it removes it and adds it again. -The five Workspace providers, `google-drive`, `google-sheets`, `google-docs`, `google-slides` and `gmail`, are Google's MCP servers in its developer preview: you join the Google Workspace Developer Preview Program, which each of them asks, and the project of the restricted tier turns on the MCP API of each server, as `drivemcp.googleapis.com`, beside the API the server calls, as `drive.googleapis.com`. The Google Ads API gives the project of the sensitive tier Test access when it is turned on, which reaches test accounts alone; the project applies for Explorer access or more on the Google Ads API overview of the console before a person connects a production account. Auto sends Google Ads no developer token, which Google no longer asks for since 9 September 2026. +The five Workspace providers, `google-drive`, `google-sheets`, `google-docs`, `google-slides` and `gmail`, are Google's MCP servers in its developer preview: you join the Google Workspace Developer Preview Program, which each of them asks, and the project of the Workspace servers turns on the MCP API of each server, as `drivemcp.googleapis.com`, beside the API the server calls, as `drive.googleapis.com`. The Google Ads API gives the project of the APIs Test access when it is turned on, which reaches test accounts alone; the project applies for Explorer access or more on the Google Ads API overview of the console before a person connects a production account. Auto sends Google Ads no developer token, which Google no longer asks for since 9 September 2026. ### The Microsoft providers diff --git a/packages/connections/README.md b/packages/connections/README.md index b4e081e34..085085079 100644 --- a/packages/connections/README.md +++ b/packages/connections/README.md @@ -18,6 +18,7 @@ The connections of an org: tool servers a person adds to the org through their a | `added_at` | When it was added | | `connected_at` | When it was connected, or `null` | | `connected_by` | The id of the caller who connected it, approving it or giving its credential, from the context of `connection_connected`, or `null` | +| `granted_scope` | For a connection connected by consent, the scope its authorization server granted, its scopes separated by single spaces, which may be fewer than the credential's `scope` asked, as when the person unticks one on the provider's page; `null` for another kind, before the connection is connected, and where none was asked or granted | | `credential_ends_with` | The ending the secret store keeps of its secret, as the ending of a credential below says, or `null` | | `broken` | The sentence of why it is broken while it is, else `null` | @@ -39,7 +40,7 @@ The connections of an org live in one stream, `connections` relative to the org, | `connection_broken` | `connection` and `reason`, the sentence of why | | `connection_removed` | `connection` | -There is no read model: each call folds the stream (`connectionsFold`, the fold of `registryDecider`). The fold keeps, by id, the record, the open link, the open start of a consent, `consent`, with its `started_at` from the context of its fact, which `connection_connected` drops and a later start replaces, the id of its secret, and the algorithm of a private key; it keeps the secret's id and the start's `verifier_secret_id` after a removal, so the removal can forget both. It sets the `client_id`, the `client_source` and the `scope` of `connection_connected` on the record's credential, so a connection of consent says the client it was made with and the scope the person allowed, so a connection connected holds one shape of credential, its client id given with the credential or on the page, and a pending one whose credential leaves its client id out is answered without it. It answers the connections newest first. `connection_connected` closes the link, and so does `connection_removed`. A pure decider holds the rules, and the handlers pass it who and when in the command: +There is no read model: each call folds the stream (`connectionsFold`, the fold of `registryDecider`). The fold keeps, by id, the record, the open link, the open start of a consent, `consent`, with its `started_at` from the context of its fact, which `connection_connected` drops and a later start replaces, the id of its secret, and the algorithm of a private key; it keeps the secret's id and the start's `verifier_secret_id` after a removal, so the removal can forget both. It sets the `client_id` and the `client_source` of `connection_connected` on the record's credential, so a connection of consent says the client it was made with, and its `scope` on the record as `granted_scope`, beside the scope the credential keeps as its way asked it, so a connection the person allowed less than its way asked shows it; a connection connected holds one shape of credential, its client id given with the credential or on the page, and a pending one whose credential leaves its client id out is answered without it. It answers the connections newest first. `connection_connected` closes the link, and so does `connection_removed`. A pure decider holds the rules, and the handlers pass it who and when in the command: - `add` refuses a name a connection of the org holds that is not removed, `conflict` of the kind `taken`, and a connection past the 50 an org may hold that are not removed (`mostConnectionsOfAnOrg`), `conflict` of the kind `connections_full`, `The org holds 50 connections, the most it may; remove one before adding another`, which bounds the links the tool access keeps for the org, and records the connection and its link. - `approve` and `give` look the connection up, `not_found`, then the link: open while the connection is pending, its digest the token's and `expires_at` later than now, else `not_found`, `There is no open link of this token: it is unknown, used up or expired`. Where people sign in, the caller must be `added_by`, else `forbidden`; `makeConnectionOperations` hands the two whether a link binds a person, which it does where `access` is `people`. `approve` connects a connection of the credential `none` or `reference`, recording `connection_approved` and `connection_connected`, and refuses one that takes a value with `conflict`; `give` records the two with the secret's id, and refuses a connection that takes none with `conflict`. Both refuse a connection whose credential is `authorization` with `conflict`, `The connection drive is connected by signing in to its provider and allowing Auto, which its page starts`, and `give_credential` refuses it so before it keeps anything. diff --git a/packages/connections/src/page/consent-clients.test.ts b/packages/connections/src/page/consent-clients.test.ts index 3a171edec..87002885c 100644 --- a/packages/connections/src/page/consent-clients.test.ts +++ b/packages/connections/src/page/consent-clients.test.ts @@ -62,7 +62,7 @@ describe('a connection of an HTTP API whose way gives its endpoints', () => { const connected = await finishing(h, link, await consentedOf(h, link)); - expect(connected).toMatchObject({ output: { status: 'connected', type: 'openapi' } }); + expect(connected).toMatchObject({ output: { status: 'connected', type: 'openapi', granted_scope: null } }); expect(connected).not.toHaveProperty('output.credential.scope'); }); }); diff --git a/packages/connections/src/page/consent-finish.test.ts b/packages/connections/src/page/consent-finish.test.ts index 87c2c44c6..fc2cbcf0a 100644 --- a/packages/connections/src/page/consent-finish.test.ts +++ b/packages/connections/src/page/consent-finish.test.ts @@ -50,6 +50,7 @@ describe('the finish of a consent', () => { client_source: 'registered', scope: 'files.read', }, + granted_scope: 'files.read', credential_ends_with: null, }, }); @@ -73,13 +74,15 @@ describe('the finish of a consent', () => { }); describe('the scope of a finish', () => { - it('records the scope the server granted where it is narrower than asked', async () => { + it('records the scope the server granted beside the scope asked, which the credential keeps, where it is narrower', async () => { const fake = await served({ grantedScope: 'files.read' }); const h = consentHarnessAt(fake, { way: { scope: 'files.read files.write' } }); const link = await filesOf(h); - expect(await finishing(h, link, await consentedOf(h, link))).toMatchObject({ - output: { credential: { scope: 'files.read' } }, + await finishing(h, link, await consentedOf(h, link)); + + expect(await h.call(operationNamed(h.operations, 'get_connection'), ada, { id: link.id })).toMatchObject({ + output: { credential: { scope: 'files.read files.write' }, granted_scope: 'files.read' }, }); }); @@ -103,7 +106,7 @@ describe('a scope the server grants', () => { const link = await filesOf(h); expect(await finishing(h, link, await consentedOf(h, link))).toMatchObject({ - output: { credential: { scope: 'files.read files.list' } }, + output: { granted_scope: 'files.read files.list' }, }); expect(await h.call(operationNamed(h.operations, 'list_connections'), ada)).toMatchObject({ status: 'succeeded' }); }); @@ -144,7 +147,7 @@ describe('a scope the server grants that is not one, or that answers a way askin const finished = await finishing(h, link, await consentedOf(h, link)); - expect(finished).toMatchObject({ status: 'succeeded', output: { status: 'connected' } }); + expect(finished).toMatchObject({ status: 'succeeded', output: { status: 'connected', granted_scope: null } }); expect(finished).not.toHaveProperty('output.credential.scope'); expect(revocationsAt(fake)).toBe(0); }); diff --git a/packages/connections/src/providers/shipped-consents.test.ts b/packages/connections/src/providers/shipped-consents.test.ts index 4b517a7f9..1586814e2 100644 --- a/packages/connections/src/providers/shipped-consents.test.ts +++ b/packages/connections/src/providers/shipped-consents.test.ts @@ -66,7 +66,8 @@ const connectedOf = Schema.decodeUnknownSync( Schema.Struct({ output: Schema.Struct({ status: Schema.String, - credential: Schema.Struct({ scope: Schema.optionalKey(Schema.String), client_source: Schema.String }), + granted_scope: Schema.NullOr(Schema.String), + credential: Schema.Struct({ client_source: Schema.String }), }), }), ); @@ -92,7 +93,7 @@ async function consentOf({ name, placeholders, issuer, vendor }: WayOfConsent, r { fake, vendor: theVendor }, ); const facts = await h.facts(); - const { status, credential } = connectedOf(finished).output; + const { status, granted_scope: grantedScope, credential } = connectedOf(finished).output; return { provider: name, authorization_endpoint: `${asked.origin}${asked.pathname}`.replace(fake.origin, ''), @@ -103,7 +104,7 @@ async function consentOf({ name, placeholders, issuer, vendor }: WayOfConsent, r client_sources: facts.flatMap((fact) => Option.toArray(Option.map(startedSourceOf(fact), ({ data }) => data.client_source)), ), - connected: { status, scope: credential.scope, client_source: credential.client_source }, + connected: { status, granted_scope: grantedScope, client_source: credential.client_source }, }; } @@ -117,7 +118,7 @@ function consentExpectedOf({ name, scope, vendor }: WayOfConsent, source: string code_challenge_method: 'S256', parameters, client_sources: [source], - connected: { status: 'connected', scope: metadata.grantedScope ?? asked, client_source: source }, + connected: { status: 'connected', granted_scope: metadata.grantedScope ?? asked, client_source: source }, }; } diff --git a/packages/connections/src/registry/connection.ts b/packages/connections/src/registry/connection.ts index 0854178f5..05b52ebb5 100644 --- a/packages/connections/src/registry/connection.ts +++ b/packages/connections/src/registry/connection.ts @@ -26,6 +26,10 @@ export const ConnectionSchema = Schema.Struct({ connected_by: Schema.NullOr(Schema.String).annotate({ description: 'The id of the caller who connected it, approving it or giving its credential', }), + granted_scope: Schema.NullOr(Schema.String).annotate({ + description: + 'For a connection connected by consent, the scopes its authorization server granted, separated by single spaces, which may be fewer than the scope its credential asked; else null', + }), credential_ends_with: Schema.NullOr(Schema.String).annotate({ description: 'The last four characters of the secret or client secret the person gave, where it holds twelve or more, kept beside the sealed value', diff --git a/packages/connections/src/registry/consent-decisions.test.ts b/packages/connections/src/registry/consent-decisions.test.ts index fd0795de5..2fa1ed2c2 100644 --- a/packages/connections/src/registry/consent-decisions.test.ts +++ b/packages/connections/src/registry/consent-decisions.test.ts @@ -155,3 +155,43 @@ describe('the fold of a consent', () => { }); }); }); + +const askingToReadAndWrite: ConnectionEvent = { + ...driveAdded, + data: { + ...driveAdded.data, + credential: { + kind: 'authorization', + issuer: 'https://accounts.drive.example.com', + scope: 'files.read files.write', + }, + }, +}; + +const grantedTheRead: ConnectionEvent = { + type: 'connection_connected', + data: { + connection: 'drive-id', + secret_id: 'secret-2', + client_id: 'auto-runtime', + client_source: 'registered', + scope: 'files.read', + }, +}; + +function scopesOf(...events: readonly ConnectionEvent[]) { + const connection = fold(...events)?.connection; + return { asked: connection?.credential.scope, granted: connection?.granted_scope }; +} + +describe('the scopes of a connection of consent, as the stream folds them', () => { + it('keep the scope its way asked in the credential and the scope the server granted beside it, so a narrower grant shows', () => { + expect([ + scopesOf(askingToReadAndWrite, linkIssued, started), + scopesOf(askingToReadAndWrite, linkIssued, started, grantedTheRead), + ]).toEqual([ + { asked: 'files.read files.write', granted: null }, + { asked: 'files.read files.write', granted: 'files.read' }, + ]); + }); +}); diff --git a/packages/connections/src/registry/credential.ts b/packages/connections/src/registry/credential.ts index 33d60de34..8851239b8 100644 --- a/packages/connections/src/registry/credential.ts +++ b/packages/connections/src/registry/credential.ts @@ -62,7 +62,7 @@ export const CredentialFieldsSchema = Schema.Struct({ scope: Schema.optionalKey( wayFields.scope.annotate({ description: - 'For client_credentials and authorization, the scopes, separated by spaces, none when left out; for authorization once connected, the scopes allowed', + 'For client_credentials and authorization, the scopes asked, separated by spaces, none when left out', }), ), parameters: Schema.optionalKey( diff --git a/packages/connections/src/registry/registry.test.ts b/packages/connections/src/registry/registry.test.ts index 347b7b492..cb156ecc8 100644 --- a/packages/connections/src/registry/registry.test.ts +++ b/packages/connections/src/registry/registry.test.ts @@ -63,6 +63,7 @@ describe('an added connection, as the stream folds it', () => { added_at: '2026-10-15T14:00:00.000Z', connected_at: null, connected_by: null, + granted_scope: null, broken: null, }, link: { token_sha256: 'a1b2', expires_at: '2026-10-15T14:10:00.000Z' }, diff --git a/packages/connections/src/registry/registry.ts b/packages/connections/src/registry/registry.ts index 5e3890f50..36d2e5290 100644 --- a/packages/connections/src/registry/registry.ts +++ b/packages/connections/src/registry/registry.ts @@ -53,6 +53,7 @@ function addedConnection({ data, context }: ConnectionAdded): KeptConnection { added_at: context.at, connected_at: null, connected_by: null, + granted_scope: null, broken: null, }, link: undefined, @@ -75,12 +76,11 @@ function withLink(kept: KeptConnection, { data }: LinkIssued): KeptConnection { } function credentialOf({ connection }: KeptConnection, { data }: ConnectionConnected): Credential { - const { client_id: clientId, client_source: clientSource, scope } = data; + const { client_id: clientId, client_source: clientSource } = data; return { ...connection.credential, ...(clientId === undefined ? {} : { client_id: clientId }), ...(clientSource === undefined ? {} : { client_source: clientSource }), - ...(scope === undefined ? {} : { scope }), }; } @@ -93,6 +93,7 @@ function connected(kept: KeptConnection, event: ConnectionConnected): KeptConnec status: 'connected', connected_at: context.at, connected_by: context.by, + granted_scope: data.scope ?? null, broken: null, }, link: undefined, diff --git a/packages/connections/src/serving/given-credentials.ts b/packages/connections/src/serving/given-credentials.ts index 44cb97b98..09a728dd8 100644 --- a/packages/connections/src/serving/given-credentials.ts +++ b/packages/connections/src/serving/given-credentials.ts @@ -154,7 +154,7 @@ export function consentAuthOf(serving: Serving): Effect.Effect { ['campaign-manager-360', []], ['display-video-360', []], ['display-video-360-reports', []], - ['gmail', ['gmail.modify']], + ['gmail', ['gmail.readonly']], ['google-ads', []], ['google-analytics', []], ['google-analytics-admin', []], - ['google-docs', ['drive.readonly']], - ['google-drive', ['drive.readonly']], + ['google-docs', []], + ['google-drive', []], ['google-search-console', []], - ['google-sheets', ['drive.readonly']], - ['google-slides', ['drive.readonly']], + ['google-sheets', []], + ['google-slides', []], ['search-ads-360', []], ]); });