Contracts: Per-User OAuth Scope Selection
Spec: ../spec.md ยท Plan: ../plan.md
Service signatures (ui/src/lib/credentials/oauth-service.ts)โ
// NEW pure helper โ the bounding rule (see data-model.md)
function boundScopes(connectorScopes: string[], requested?: string[]): string[];
// EXTEND: optional per-request scope selection
startConnection(input: {
providerKey: string;
owner: CredentialOwnerRef;
state: string;
codeChallenge: string;
requestedScopes?: string[]; // NEW โ validated via boundScopes against connector.scopes
}): Promise<{ authorizationUrl: string; connectorId: string; requestedScopes: string[] }>;
// ^ NEW: the bounded set actually requested
// EXTEND: persist what was requested/granted
completeConnection(input: {
/* ...existing... */
requestedScopes?: string[]; // NEW โ carried from the connect state
}): Promise<...>; // persists requestedScopes (+ grantedScopes if token response has `scope`)
boundScopes throws ApiError("โฆ", 400, "VALIDATION_ERROR") on an out-of-bounds or empty selection (FR-004).
BFF routesโ
GET /api/credentials/oauth/[provider_key]/connectโ
- New optional input:
?scopes=a,b,c(comma/space separated) โ the user's chosen subset. - Behavior: parse โ
startConnection({ ..., requestedScopes }). The chosen set is stashed in the existing PKCE/state cookie (alongsidestate/codeChallenge) so the callback can persist it. - Errors: out-of-bounds/empty โ
400 VALIDATION_ERROR(no redirect issued). - Backward compatible: no
scopesโ connector default (today's behavior).
GET /api/credentials/oauth/[provider_key]/callbackโ
- Reads the stashed
requestedScopesfrom state, threads intocompleteConnection, and recordsgrantedScopesif the token response includesscope.
GET /api/credentials/connectionsโ
- Response per connection gains
requestedScopes?: string[](andgrantedScopes?if present) for display + advanced-editor pre-fill.
GET /api/credentials/oauth-connectorsโ
- Response per connector gains
scopes: string[](the allowed set) so the editor can render the toggle list. (Today this route strips scopes; this exposes the allowed set only โ still no secrets.)
UI contract (ProviderConnections.tsx)โ
- Per provider row: a collapsible "Advanced settings" panel.
- Renders one checkbox per
connector.scopes; initial checked =connection.requestedScopes ?? connector.scopes. - Connect/Relink popup URL includes
?scopes=<selected join ",">. - Shows "connected with: <scopes>" and a "relink to apply scope changes" hint (FR-009).
- Empty selection disables Connect (mirrors server-side rejection).