Repository navigation
feat(integrations): model OAuth apps and connections as first-class tables #979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
d7257fa
feat(integrations): add user_oauth_apps and user_integrations tables
cursoragent 4d31d73
feat(integrations): read capabilities from D1 and add oauth app surface
cursoragent 14bdda4
refactor(search): build integration entities from D1 instead of value…
cursoragent 0b8af9e
refactor(runtime): resolve integration client ids inline
cursoragent 281f728
feat(account): group connections by oauth app and stop reading values…
cursoragent f455f97
fix(values): hide platform-reserved value prefixes from agents
cursoragent 33d9aa2
docs(integrations): document the oauth app model and cover deletion i…
cursoragent 3e2883e
refactor(integrations): delete the values-era config helpers
cursoragent d31ec81
fix(integrations): preserve non-canonical integration value names in …
cursoragent fdf5b7f
Merge remote-tracking branch 'origin/main' into cursor/integrations-t…
cursoragent 11fe326
fix(integrations): store canonical use_pkce and canonicalize app slugs
cursoragent 9a3759c
fix(integrations): persist the setup client id and share one app-reso…
cursoragent 98b865f
fix(integrations): resolve setup prefill by provider family, not slug…
cursoragent 80b6c78
fix(integrations): write canonical column values in the backfill
cursoragent 8148d67
fix(integrations): agree field by field on provider-family prefill
cursoragent 7f6e517
refactor(account): drop the unguarded value actions from secrets.json
cursoragent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| # OAuth integrations (apps and connections) | ||
|
|
||
| Saved third-party OAuth config is a first-class per-user primitive: an **OAuth | ||
| app** (shared client credentials and provider endpoints) plus one or more | ||
| **connections** (connected accounts that share that app). Credential _values_ | ||
| never enter these tables — they live in the secret store and are referenced by | ||
| name. | ||
|
|
||
| Data access lives under `packages/worker/src/integrations/`. MCP capabilities | ||
| live under `packages/worker/src/mcp/capabilities/integrations/`. The hosted | ||
| connect UI is `/connect/oauth`. | ||
|
|
||
| ## App / connection split | ||
|
|
||
| An **OAuth app** (`user_oauth_apps`) holds the provider client identity and | ||
| endpoint config that many connections can share: | ||
|
|
||
| - `client_id` (inline non-secret identifier) | ||
| - `client_secret_secret_name` (secret-store name, or null for public PKCE apps) | ||
| - `token_url`, optional `authorize_url` / `api_base_url` | ||
| - `flow` (`pkce` | `confidential`), optional `use_pkce`, `token_exchange_style`, | ||
| `scope_separator`, and `extra_authorize_params_json` | ||
|
|
||
| A **connection** (`user_integrations`) is one connected account on that app: | ||
|
|
||
| - `name` (canonical integration key agents pass to `createAuthenticatedFetch`) | ||
| - `app_slug` (composite FK to the owning app) | ||
| - `scopes_json`, `required_hosts_json` | ||
| - `access_token_secret_name` and optional `refresh_token_secret_name` | ||
| - optional `account_label` / `description`, plus connect/refresh timestamps | ||
|
|
||
| The split exists so rotating client credentials is one write on the shared app | ||
| instead of one write per connection. Multiple Google accounts share one app; `x` | ||
| and `x-kodykoala` share one app when their app-level fields match. Two | ||
| connections that share a client id but reference different client-secret names | ||
| remain separate apps (for example `github` vs `github-kent`). | ||
|
|
||
| Composite primary keys `(user_id, slug)` / `(user_id, name)` and the composite | ||
| FK `(user_id, app_slug) → user_oauth_apps(user_id, slug)` keep per-user | ||
| isolation structural. The connection FK uses `ON DELETE RESTRICT`, so | ||
| connections must be removed before their app. | ||
|
|
||
| ## Where credentials live | ||
|
|
||
| | Field | Storage | Notes | | ||
| | ------------- | --------------------------- | ----------------------------------------- | | ||
| | Client id | `user_oauth_apps.client_id` | Non-secret OAuth client identifier | | ||
| | Client secret | Secret store | Referenced by `client_secret_secret_name` | | ||
| | Access token | Secret store | Referenced by `access_token_secret_name` | | ||
| | Refresh token | Secret store | Referenced by `refresh_token_secret_name` | | ||
|
|
||
| Account export includes both tables. Rows contain secret _names_ and the inline | ||
| `client_id`, never encrypted secret payloads. Exporting `client_id` is | ||
| deliberate: it is not a credential value, and the same identifier was previously | ||
| portable as an ordinary user value. | ||
|
|
||
| Leftover `<provider>-client-id` values in the values store (when present) are | ||
| ordinary user data left after migration; they are not the live source of truth | ||
| for integrations. | ||
|
|
||
| ## Two independent host gates | ||
|
|
||
| Outbound OAuth calls enforce two allowlists that are not collapsed: | ||
|
|
||
| 1. **Connection `requiredHosts`** (plus the host of `apiBaseUrl`) — enforced by | ||
| `createAuthenticatedFetch` / `assertIntegrationHostAllowed` before an OAuth | ||
| bearer token is attached. A materialized token is not a `{{secret:…}}` | ||
| placeholder, so the fetch gateway cannot apply secret host policy to it. | ||
| 2. **Each secret's `allowedHosts`** — enforced when secret placeholders resolve | ||
| through the fetch gateway (client secret on token exchange, token mounts, and | ||
| similar paths). | ||
|
|
||
| Both must allow a destination for the full connect → refresh → API call path to | ||
| succeed. See the | ||
| [OAuth integration host allowlist](./index.md#oauth-integration-host-allowlist) | ||
| note on the architecture index. | ||
|
|
||
| ## `/connect/oauth` and authenticated fetch | ||
|
|
||
| `/connect/oauth` runs authorize → callback → token exchange in the browser | ||
| session, writes access/refresh tokens through the account secrets flow, and | ||
| upserts the app + connection via the integrations service. Reconnect with | ||
| `?provider=<integration-name>` reuses saved authorize metadata (scopes, | ||
| `scopeSeparator`, `extraAuthorizeParams`) and the current app client | ||
| credentials. | ||
|
|
||
| `createAuthenticatedFetch(providerName)` (execute runtime helper) loads the | ||
| named connection joined to its app, refreshes the access token when needed, and | ||
| returns a fetch wrapper that: | ||
|
|
||
| 1. asserts the request host against the connection allowlist | ||
| 2. attaches the bearer token only after that check passes | ||
|
|
||
| Capability and search detail surfaces keep a **flat connection-shaped** config | ||
| (`clientId`, secret names, endpoints, `authorization`, `requiredHosts`) so | ||
| callers do not need to join app and connection themselves. | ||
|
|
||
| ## Capability surface | ||
|
|
||
| Domain: `integrations` | ||
| (`packages/worker/src/mcp/capabilities/integrations/domain.ts`). | ||
|
|
||
| | Capability | Role | | ||
| | ------------------------------------------------------ | ------------------------------------------------------------- | | ||
| | `integration_save` / `_get` / `_list` / `_delete` | Connection CRUD with flat `clientId` output | | ||
| | `integration_oauth_app_list` | Apps with connection counts and sibling connection names | | ||
| | `integration_oauth_app_rotate_credentials` | Rotate shared app `clientId` / client-secret name | | ||
| | `integration_registry_search` / `integration_discover` | Untrusted integrations.sh research | | ||
| | `openapi_spec_summarize` / `openapi_client_scaffold` | Spec research helpers (bindings live in the `openapi` domain) | | ||
|
|
||
| OpenAPI provider bindings (`_openapi:*` values-store config) are a separate | ||
| primitive; see [OpenAPI provider bindings](./openapi-bindings.md). | ||
|
|
||
| ## Account deletion order | ||
|
|
||
| Deletion targets in `account-data-targets.ts` list `user_integrations` before | ||
| `user_oauth_apps` so the `ON DELETE RESTRICT` FK cannot block cleanup when | ||
| cascades are disabled. | ||
|
|
||
| ## Related docs | ||
|
|
||
| - [Data storage](./data-storage.md) — D1 inventory and JSON shadow schemas | ||
| - [OAuth guide](../../guides/oauth.md) — agent-facing `/connect/oauth` workflow | ||
| - [Primitives map](./primitives.yaml) — `integrations` primitive and | ||
| `integration-host-allowlist` invariant |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.