mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(permissions): extend hook to detect missing scopes to return those scopes for upgrade, update credential selector subblock (#1869)
This commit is contained in:
@@ -18,7 +18,11 @@ const credentialsQuerySchema = z
|
||||
.object({
|
||||
provider: z.string().nullish(),
|
||||
workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(),
|
||||
credentialId: z.string().uuid('Credential ID must be a valid UUID').nullish(),
|
||||
credentialId: z
|
||||
.string()
|
||||
.min(1, 'Credential ID must not be empty')
|
||||
.max(255, 'Credential ID is too long')
|
||||
.nullish(),
|
||||
})
|
||||
.refine((data) => data.provider || data.credentialId, {
|
||||
message: 'Provider or credentialId is required',
|
||||
@@ -206,7 +210,7 @@ export async function GET(request: NextRequest) {
|
||||
displayName = `${acc.accountId} (${baseProvider})`
|
||||
}
|
||||
|
||||
const grantedScopes = acc.scope ? acc.scope.split(/\s+/).filter(Boolean) : []
|
||||
const grantedScopes = acc.scope ? acc.scope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -29,13 +29,13 @@ export interface OAuthRequiredModalProps {
|
||||
toolName: string
|
||||
requiredScopes?: string[]
|
||||
serviceId?: string
|
||||
newScopes?: string[]
|
||||
}
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf',
|
||||
'https://www.googleapis.com/auth/gmail.labels': 'View and manage your email labels',
|
||||
'https://www.googleapis.com/auth/gmail.modify': 'View and manage your email messages',
|
||||
'https://www.googleapis.com/auth/gmail.readonly': 'View and read your email messages',
|
||||
'https://www.googleapis.com/auth/drive.readonly': 'View and read your Google Drive files',
|
||||
'https://www.googleapis.com/auth/drive.file': 'View and manage your Google Drive files',
|
||||
'https://www.googleapis.com/auth/calendar': 'View and manage your calendar',
|
||||
@@ -202,6 +202,7 @@ export function OAuthRequiredModal({
|
||||
toolName,
|
||||
requiredScopes = [],
|
||||
serviceId,
|
||||
newScopes = [],
|
||||
}: OAuthRequiredModalProps) {
|
||||
const effectiveServiceId = serviceId || getServiceIdFromScopes(provider, requiredScopes)
|
||||
const { baseProvider } = parseProvider(provider)
|
||||
@@ -223,6 +224,11 @@ export function OAuthRequiredModal({
|
||||
const displayScopes = requiredScopes.filter(
|
||||
(scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile')
|
||||
)
|
||||
const newScopesSet = new Set(
|
||||
(newScopes || []).filter(
|
||||
(scope) => !scope.includes('userinfo.email') && !scope.includes('userinfo.profile')
|
||||
)
|
||||
)
|
||||
|
||||
const handleConnectDirectly = async () => {
|
||||
try {
|
||||
@@ -278,7 +284,14 @@ export function OAuthRequiredModal({
|
||||
<div className='mt-1 rounded-full bg-muted p-0.5'>
|
||||
<Check className='h-3 w-3' />
|
||||
</div>
|
||||
<span className='text-muted-foreground'>{getScopeDescription(scope)}</span>
|
||||
<div className='text-muted-foreground'>
|
||||
<span>{getScopeDescription(scope)}</span>
|
||||
{newScopesSet.has(scope) && (
|
||||
<span className='ml-2 rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300'>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -289,7 +302,12 @@ export function OAuthRequiredModal({
|
||||
<Button variant='outline' onClick={onClose} className='sm:order-1'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='button' onClick={handleConnectDirectly} className='sm:order-3'>
|
||||
<Button
|
||||
variant='primary'
|
||||
type='button'
|
||||
onClick={handleConnectDirectly}
|
||||
className='sm:order-3'
|
||||
>
|
||||
Connect Now
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
type Credential,
|
||||
getCanonicalScopesForProvider,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceIdFromScopes,
|
||||
OAUTH_PROVIDERS,
|
||||
@@ -25,6 +26,7 @@ import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('CredentialSelector')
|
||||
@@ -210,6 +212,14 @@ export function CredentialSelector({
|
||||
? 'Saved by collaborator'
|
||||
: undefined
|
||||
|
||||
// Determine if additional permissions are required for the selected credential
|
||||
const hasSelection = !!selectedCredential
|
||||
const missingRequiredScopes = hasSelection
|
||||
? getMissingRequiredScopes(selectedCredential, requiredScopes || [])
|
||||
: []
|
||||
const needsUpdate =
|
||||
hasSelection && missingRequiredScopes.length > 0 && !disabled && !isPreview && !isLoading
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = (credentialId: string) => {
|
||||
const previousId = selectedId || (effectiveValue as string) || ''
|
||||
@@ -331,13 +341,21 @@ export function CredentialSelector({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{needsUpdate && (
|
||||
<div className='mt-2 flex items-center justify-between rounded-[6px] border border-amber-300/40 bg-amber-50/60 px-2 py-1 font-medium text-[12px] transition-colors dark:bg-amber-950/10'>
|
||||
<span>Additional permissions required</span>
|
||||
{!isForeign && <Button onClick={() => setShowOAuthModal(true)}>Update access</Button>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showOAuthModal && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
provider={provider}
|
||||
toolName={getProviderName(provider)}
|
||||
requiredScopes={requiredScopes}
|
||||
requiredScopes={getCanonicalScopesForProvider(effectiveProviderId)}
|
||||
newScopes={missingRequiredScopes}
|
||||
serviceId={effectiveServiceId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, Plus, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -12,17 +12,19 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
type Credential,
|
||||
getCanonicalScopesForProvider,
|
||||
getProviderIdFromServiceId,
|
||||
OAUTH_PROVIDERS,
|
||||
type OAuthProvider,
|
||||
type OAuthService,
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('ToolCredentialSelector')
|
||||
|
||||
// Helper functions for provider icons and names
|
||||
const getProviderIcon = (providerName: OAuthProvider) => {
|
||||
const { baseProvider } = parseProvider(providerName)
|
||||
const baseProviderConfig = OAUTH_PROVIDERS[baseProvider]
|
||||
@@ -158,6 +160,13 @@ export function ToolCredentialSelector({
|
||||
const selectedCredential = credentials.find((cred) => cred.id === selectedId)
|
||||
const isForeign = !!(selectedId && !selectedCredential)
|
||||
|
||||
// Determine if additional permissions are required for the selected credential
|
||||
const hasSelection = !!selectedCredential
|
||||
const missingRequiredScopes = hasSelection
|
||||
? getMissingRequiredScopes(selectedCredential, requiredScopes || [])
|
||||
: []
|
||||
const needsUpdate = hasSelection && missingRequiredScopes.length > 0 && !disabled && !isLoading
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
@@ -240,12 +249,23 @@ export function ToolCredentialSelector({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{needsUpdate && (
|
||||
<div className='mt-2 flex items-center justify-between rounded-[6px] border border-amber-300/40 bg-amber-50/60 px-2 py-1 font-medium text-[12px] transition-colors dark:bg-amber-950/10'>
|
||||
<span>Additional permissions required</span>
|
||||
{/* We don't have reliable foreign detection context here; always show CTA */}
|
||||
<Button onClick={() => setShowOAuthModal(true)}>Update access</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={handleOAuthClose}
|
||||
provider={provider}
|
||||
toolName={label}
|
||||
requiredScopes={requiredScopes}
|
||||
requiredScopes={getCanonicalScopesForProvider(
|
||||
serviceId ? getProviderIdFromServiceId(serviceId) : (provider as string)
|
||||
)}
|
||||
newScopes={missingRequiredScopes}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -48,7 +48,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
placeholder: 'Select Gmail account',
|
||||
@@ -162,10 +161,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'folder',
|
||||
provider: 'google-email',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select Gmail label/folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -241,10 +237,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'addLabelIds',
|
||||
provider: 'google-email',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select destination label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -271,10 +264,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'removeLabelIds',
|
||||
provider: 'google-email',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select label to remove',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -327,10 +317,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'labelIds',
|
||||
provider: 'google-email',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
|
||||
@@ -43,3 +43,29 @@ export function anyCredentialNeedsReauth(credentials: Credential[]): boolean {
|
||||
export function getCredentialsNeedingReauth(credentials: Credential[]): Credential[] {
|
||||
return credentials.filter(credentialNeedsReauth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute which of the provided requiredScopes are NOT granted by the credential.
|
||||
*/
|
||||
export function getMissingRequiredScopes(
|
||||
credential: Credential | undefined,
|
||||
requiredScopes: string[] = []
|
||||
): string[] {
|
||||
if (!credential) return [...requiredScopes]
|
||||
const granted = new Set((credential.scopes || []).map((s) => s))
|
||||
const missing: string[] = []
|
||||
for (const s of requiredScopes) {
|
||||
if (!granted.has(s)) missing.push(s)
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a credential needs an upgrade specifically for the provided required scopes.
|
||||
*/
|
||||
export function needsUpgradeForRequiredScopes(
|
||||
credential: Credential | undefined,
|
||||
requiredScopes: string[] = []
|
||||
): boolean {
|
||||
return getMissingRequiredScopes(credential, requiredScopes).length > 0
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export type OAuthService =
|
||||
| 'wealthbox'
|
||||
| 'onedrive'
|
||||
| 'webflow'
|
||||
|
||||
export interface OAuthProviderConfig {
|
||||
id: OAuthProvider
|
||||
name: string
|
||||
@@ -95,6 +96,7 @@ export interface OAuthServiceConfig {
|
||||
icon: (props: { className?: string }) => ReactNode
|
||||
baseProviderIcon: (props: { className?: string }) => ReactNode
|
||||
scopes: string[]
|
||||
scopeHints?: string[]
|
||||
}
|
||||
|
||||
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
@@ -113,9 +115,9 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
// 'https://www.googleapis.com/auth/gmail.readonly',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
scopeHints: ['gmail', 'mail'],
|
||||
},
|
||||
'google-drive': {
|
||||
id: 'google-drive',
|
||||
@@ -128,6 +130,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
scopeHints: ['drive'],
|
||||
},
|
||||
'google-docs': {
|
||||
id: 'google-docs',
|
||||
@@ -140,6 +143,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
scopeHints: ['docs'],
|
||||
},
|
||||
'google-sheets': {
|
||||
id: 'google-sheets',
|
||||
@@ -152,6 +156,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
scopeHints: ['sheets'],
|
||||
},
|
||||
'google-forms': {
|
||||
id: 'google-forms',
|
||||
@@ -165,6 +170,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
scopeHints: ['forms'],
|
||||
},
|
||||
'google-calendar': {
|
||||
id: 'google-calendar',
|
||||
@@ -174,6 +180,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: (props) => GoogleCalendarIcon(props),
|
||||
baseProviderIcon: (props) => GoogleIcon(props),
|
||||
scopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
scopeHints: ['calendar'],
|
||||
},
|
||||
'google-vault': {
|
||||
id: 'google-vault',
|
||||
@@ -186,6 +193,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
scopeHints: ['ediscovery', 'devstorage'],
|
||||
},
|
||||
},
|
||||
defaultService: 'gmail',
|
||||
@@ -244,6 +252,9 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'Group.ReadWrite.All',
|
||||
'Team.ReadBasic.All',
|
||||
'offline_access',
|
||||
'Files.Read',
|
||||
'Sites.Read.All',
|
||||
'TeamMember.Read.All',
|
||||
],
|
||||
},
|
||||
outlook: {
|
||||
@@ -402,10 +413,41 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'read:jira-user',
|
||||
'read:jira-work',
|
||||
'write:jira-work',
|
||||
'write:issue:jira',
|
||||
'read:project:jira',
|
||||
'read:issue-type:jira',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:issue-meta:jira',
|
||||
'read:issue-security-level:jira',
|
||||
'read:issue.vote:jira',
|
||||
'read:issue.changelog:jira',
|
||||
'read:avatar:jira',
|
||||
'read:issue:jira',
|
||||
'read:status:jira',
|
||||
'read:user:jira',
|
||||
'read:field-configuration:jira',
|
||||
'read:issue-details:jira',
|
||||
'read:issue-event:jira',
|
||||
'delete:issue:jira',
|
||||
'write:comment:jira',
|
||||
'read:comment:jira',
|
||||
'delete:comment:jira',
|
||||
'read:attachment:jira',
|
||||
'delete:attachment:jira',
|
||||
'write:issue-worklog:jira',
|
||||
'read:issue-worklog:jira',
|
||||
'delete:issue-worklog:jira',
|
||||
'write:issue-link:jira',
|
||||
'delete:issue-link:jira',
|
||||
'manage:jira-webhook',
|
||||
'read:webhook:jira',
|
||||
'write:webhook:jira',
|
||||
'delete:webhook:jira',
|
||||
'read:issue.property:jira',
|
||||
'read:comment.property:jira',
|
||||
'read:jql:jira',
|
||||
'read:field:jira',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -493,12 +535,16 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
baseProviderIcon: (props) => SlackIcon(props),
|
||||
scopes: [
|
||||
'channels:read',
|
||||
'channels:history',
|
||||
'groups:read',
|
||||
'groups:history',
|
||||
'chat:write',
|
||||
'chat:write.public',
|
||||
'users:read',
|
||||
'files:write',
|
||||
'files:read',
|
||||
'links:read',
|
||||
'links:write',
|
||||
'canvases:write',
|
||||
'reactions:write',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -516,7 +562,24 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'reddit',
|
||||
icon: (props) => RedditIcon(props),
|
||||
baseProviderIcon: (props) => RedditIcon(props),
|
||||
scopes: ['identity', 'read'],
|
||||
scopes: [
|
||||
'identity',
|
||||
'read',
|
||||
'submit',
|
||||
'vote',
|
||||
'save',
|
||||
'edit',
|
||||
'subscribe',
|
||||
'history',
|
||||
'privatemessages',
|
||||
'account',
|
||||
'mysubreddits',
|
||||
'flair',
|
||||
'report',
|
||||
'modposts',
|
||||
'modflair',
|
||||
'modmail',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultService: 'reddit',
|
||||
@@ -557,7 +620,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
}
|
||||
|
||||
// Helper function to get a service by provider and service ID
|
||||
export function getServiceByProviderAndId(
|
||||
provider: OAuthProvider,
|
||||
serviceId?: string
|
||||
@@ -576,77 +638,29 @@ export function getServiceByProviderAndId(
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to determine service ID from scopes
|
||||
export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]): string {
|
||||
const providerConfig = OAUTH_PROVIDERS[provider]
|
||||
const { baseProvider, featureType } = parseProvider(provider)
|
||||
const providerConfig = OAUTH_PROVIDERS[baseProvider] || OAUTH_PROVIDERS[provider]
|
||||
if (!providerConfig) {
|
||||
return provider
|
||||
}
|
||||
|
||||
if (provider === 'google') {
|
||||
if (scopes.some((scope) => scope.includes('gmail') || scope.includes('mail'))) {
|
||||
return 'gmail'
|
||||
if (featureType !== 'default' && providerConfig.services[featureType]) {
|
||||
return featureType
|
||||
}
|
||||
|
||||
const normalizedScopes = (scopes || []).map((s) => s.toLowerCase())
|
||||
for (const service of Object.values(providerConfig.services)) {
|
||||
const hints = (service.scopeHints || []).map((h) => h.toLowerCase())
|
||||
if (hints.length === 0) continue
|
||||
if (normalizedScopes.some((scope) => hints.some((hint) => scope.includes(hint)))) {
|
||||
return service.id
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('drive'))) {
|
||||
return 'google-drive'
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('docs'))) {
|
||||
return 'google-docs'
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('sheets'))) {
|
||||
return 'google-sheets'
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('calendar'))) {
|
||||
return 'google-calendar'
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('forms'))) {
|
||||
return 'google-forms'
|
||||
}
|
||||
if (scopes.some((scope) => scope.includes('ediscovery'))) {
|
||||
return 'google-vault'
|
||||
}
|
||||
} else if (provider === 'microsoft-teams') {
|
||||
return 'microsoft-teams'
|
||||
} else if (provider === 'outlook') {
|
||||
return 'outlook'
|
||||
} else if (provider === 'sharepoint') {
|
||||
return 'sharepoint'
|
||||
} else if (provider === 'microsoft-planner') {
|
||||
return 'microsoft-planner'
|
||||
} else if (provider === 'onedrive') {
|
||||
return 'onedrive'
|
||||
} else if (provider === 'github') {
|
||||
return 'github'
|
||||
} else if (provider === 'supabase') {
|
||||
return 'supabase'
|
||||
} else if (provider === 'x') {
|
||||
return 'x'
|
||||
} else if (provider === 'confluence') {
|
||||
return 'confluence'
|
||||
} else if (provider === 'jira') {
|
||||
return 'jira'
|
||||
} else if (provider === 'airtable') {
|
||||
return 'airtable'
|
||||
} else if (provider === 'notion') {
|
||||
return 'notion'
|
||||
} else if (provider === 'discord') {
|
||||
return 'discord'
|
||||
} else if (provider === 'linear') {
|
||||
return 'linear'
|
||||
} else if (provider === 'slack') {
|
||||
return 'slack'
|
||||
} else if (provider === 'reddit') {
|
||||
return 'reddit'
|
||||
} else if (provider === 'wealthbox') {
|
||||
return 'wealthbox'
|
||||
} else if (provider === 'webflow') {
|
||||
return 'webflow'
|
||||
}
|
||||
|
||||
return providerConfig.defaultService
|
||||
}
|
||||
|
||||
// Helper function to get provider ID from service ID
|
||||
export function getProviderIdFromServiceId(serviceId: string): string {
|
||||
for (const provider of Object.values(OAUTH_PROVIDERS)) {
|
||||
for (const [id, service] of Object.entries(provider.services)) {
|
||||
@@ -660,7 +674,6 @@ export function getProviderIdFromServiceId(serviceId: string): string {
|
||||
return serviceId
|
||||
}
|
||||
|
||||
// Helper to locate a service configuration by its providerId
|
||||
export function getServiceConfigByProviderId(providerId: string): OAuthServiceConfig | null {
|
||||
for (const provider of Object.values(OAUTH_PROVIDERS)) {
|
||||
for (const service of Object.values(provider.services)) {
|
||||
@@ -673,13 +686,11 @@ export function getServiceConfigByProviderId(providerId: string): OAuthServiceCo
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the canonical scopes for a given providerId (service instance)
|
||||
export function getCanonicalScopesForProvider(providerId: string): string[] {
|
||||
const service = getServiceConfigByProviderId(providerId)
|
||||
return service?.scopes ? [...service.scopes] : []
|
||||
}
|
||||
|
||||
// Normalize scopes by trimming, filtering empties, and deduplicating
|
||||
export function normalizeScopes(scopes: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
for (const scope of scopes) {
|
||||
@@ -699,7 +710,6 @@ export interface ScopeEvaluation {
|
||||
requiresReauthorization: boolean
|
||||
}
|
||||
|
||||
// Compare granted scopes with canonical ones for a providerId
|
||||
export function evaluateScopeCoverage(
|
||||
providerId: string,
|
||||
grantedScopes: string[]
|
||||
@@ -722,7 +732,6 @@ export function evaluateScopeCoverage(
|
||||
}
|
||||
}
|
||||
|
||||
// Interface for credential objects
|
||||
export interface Credential {
|
||||
id: string
|
||||
name: string
|
||||
@@ -737,7 +746,6 @@ export interface Credential {
|
||||
requiresReauthorization?: boolean
|
||||
}
|
||||
|
||||
// Interface for provider configuration
|
||||
export interface ProviderConfig {
|
||||
baseProvider: string
|
||||
featureType: string
|
||||
|
||||
@@ -19,10 +19,7 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-email',
|
||||
additionalScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
'https://www.googleapis.com/auth/gmail.readonly',
|
||||
],
|
||||
additionalScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
},
|
||||
|
||||
params: {
|
||||
|
||||
Reference in New Issue
Block a user