Refresh creds on typing in impersonated email

This commit is contained in:
Theodore Li
2026-03-28 17:43:24 -07:00
parent 7ec025973e
commit e8717bb5c0
18 changed files with 291 additions and 97 deletions

View File

@@ -308,17 +308,34 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
}
/**
* Refreshes an OAuth token if needed based on credential information
* Refreshes an OAuth token if needed based on credential information.
* Also handles service account credentials by generating a JWT-based token.
* @param credentialId The ID of the credential to check and potentially refresh
* @param userId The user ID who owns the credential (for security verification)
* @param requestId Request ID for log correlation
* @param scopes Optional scopes for service account token generation
* @returns The valid access token or null if refresh fails
*/
export async function refreshAccessTokenIfNeeded(
credentialId: string,
userId: string,
requestId: string
requestId: string,
scopes?: string[],
impersonateEmail?: string
): Promise<string | null> {
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return null
}
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
const effectiveScopes = scopes?.length
? scopes
: ['https://www.googleapis.com/auth/cloud-platform']
logger.info(`[${requestId}] Using service account token for credential`)
return getServiceAccountToken(resolved.credentialId, effectiveScopes, impersonateEmail)
}
// Get the credential directly using the getCredential helper
const credential = await getCredential(requestId, credentialId, userId)

View File

@@ -6,7 +6,11 @@ import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import {
getServiceAccountToken,
refreshTokenIfNeeded,
resolveOAuthAccountId,
} from '@/app/api/auth/oauth/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
@@ -365,6 +369,14 @@ async function resolveVertexCredential(requestId: string, credentialId: string):
throw new Error(`Vertex AI credential not found: ${credentialId}`)
}
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
const accessToken = await getServiceAccountToken(resolved.credentialId, [
'https://www.googleapis.com/auth/cloud-platform',
])
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
return accessToken
}
const credential = await db.query.account.findFirst({
where: eq(account.id, resolved.accountId),
})

View File

@@ -26,6 +26,7 @@ export async function GET(request: NextRequest) {
const credentialId = searchParams.get('credentialId')
const fileId = searchParams.get('fileId')
const workflowId = searchParams.get('workflowId') || undefined
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId || !fileId) {
logger.warn(`[${requestId}] Missing required parameters`)
@@ -46,7 +47,9 @@ export async function GET(request: NextRequest) {
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/drive'],
impersonateEmail
)
if (!accessToken) {

View File

@@ -85,6 +85,7 @@ export async function GET(request: NextRequest) {
const query = searchParams.get('query') || ''
const folderId = searchParams.get('folderId') || searchParams.get('parentId') || ''
const workflowId = searchParams.get('workflowId') || undefined
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId) {
logger.warn(`[${requestId}] Missing credential ID`)
@@ -100,7 +101,9 @@ export async function GET(request: NextRequest) {
const accessToken = await refreshAccessTokenIfNeeded(
credentialId!,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/drive'],
impersonateEmail
)
if (!accessToken) {

View File

@@ -6,7 +6,11 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import {
getServiceAccountToken,
refreshAccessTokenIfNeeded,
resolveOAuthAccountId,
} from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -26,6 +30,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const labelId = searchParams.get('labelId')
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId || !labelId) {
logger.warn(`[${requestId}] Missing required parameters`)
@@ -58,29 +63,40 @@ export async function GET(request: NextRequest) {
}
}
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
let accessToken: string | null = null
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
accessToken = await getServiceAccountToken(
resolved.credentialId,
['https://www.googleapis.com/auth/gmail.labels'],
impersonateEmail
)
} else {
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const accountRow = credentials[0]
logger.info(
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)
accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId,
['https://www.googleapis.com/auth/gmail.labels']
)
}
const accountRow = credentials[0]
logger.info(
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

View File

@@ -6,7 +6,11 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import {
getServiceAccountToken,
refreshAccessTokenIfNeeded,
resolveOAuthAccountId,
} from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GmailLabelsAPI')
@@ -33,6 +37,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query')
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
@@ -62,29 +67,40 @@ export async function GET(request: NextRequest) {
}
}
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
let accessToken: string | null = null
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
accessToken = await getServiceAccountToken(
resolved.credentialId,
['https://www.googleapis.com/auth/gmail.labels'],
impersonateEmail
)
} else {
const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const accountRow = credentials[0]
logger.info(
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)
accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId,
['https://www.googleapis.com/auth/gmail.labels']
)
}
const accountRow = credentials[0]
logger.info(
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

View File

@@ -20,7 +20,7 @@ export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, projectId } = body
const { credential, workflowId, projectId, impersonateEmail } = body
if (!credential) {
logger.error('Missing credential in request')
@@ -43,7 +43,9 @@ export async function POST(request: Request) {
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/bigquery'],
impersonateEmail
)
if (!accessToken) {
logger.error('Failed to get access token', {

View File

@@ -12,7 +12,7 @@ export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId, projectId, datasetId } = body
const { credential, workflowId, projectId, datasetId, impersonateEmail } = body
if (!credential) {
logger.error('Missing credential in request')
@@ -40,7 +40,9 @@ export async function POST(request: Request) {
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/bigquery'],
impersonateEmail
)
if (!accessToken) {
logger.error('Failed to get access token', {

View File

@@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const workflowId = searchParams.get('workflowId') || undefined
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
@@ -41,7 +42,9 @@ export async function GET(request: NextRequest) {
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/calendar'],
impersonateEmail
)
if (!accessToken) {

View File

@@ -40,6 +40,7 @@ export async function GET(request: NextRequest) {
const credentialId = searchParams.get('credentialId')
const spreadsheetId = searchParams.get('spreadsheetId')
const workflowId = searchParams.get('workflowId') || undefined
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
@@ -59,7 +60,9 @@ export async function GET(request: NextRequest) {
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/drive'],
impersonateEmail
)
if (!accessToken) {

View File

@@ -12,7 +12,7 @@ export async function POST(request: Request) {
const requestId = generateRequestId()
try {
const body = await request.json()
const { credential, workflowId } = body
const { credential, workflowId, impersonateEmail } = body
if (!credential) {
logger.error('Missing credential in request')
@@ -30,7 +30,9 @@ export async function POST(request: Request) {
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
requestId,
['https://www.googleapis.com/auth/tasks'],
impersonateEmail
)
if (!accessToken) {
logger.error('Failed to get access token', {

View File

@@ -22,6 +22,7 @@ import {
Tooltip,
} from '@/components/emcn'
import { Input as UiInput } from '@/components/ui'
import { cn } from '@/lib/core/utils/cn'
import { useSession } from '@/lib/auth/auth-client'
import {
clearPendingCredentialCreateRequest,
@@ -96,6 +97,7 @@ export function IntegrationsManager() {
const [saDescription, setSaDescription] = useState('')
const [saError, setSaError] = useState<string | null>(null)
const [saIsSubmitting, setSaIsSubmitting] = useState(false)
const [saDragActive, setSaDragActive] = useState(false)
const { data: session } = useSession()
const currentUserId = session?.user?.id || ''
@@ -689,29 +691,63 @@ export function IntegrationsManager() {
}
}
const readSaJsonFile = useCallback(
(file: File) => {
if (!file.name.endsWith('.json')) {
setSaError('Only .json files are supported')
return
}
const reader = new FileReader()
reader.onload = (e) => {
const text = e.target?.result
if (typeof text === 'string') {
setSaJsonInput(text)
setSaError(null)
try {
const parsed = JSON.parse(text)
if (parsed.client_email && !saDisplayName.trim()) {
setSaDisplayName(parsed.client_email)
}
} catch {
// validation will catch this on submit
}
}
}
reader.readAsText(file)
},
[saDisplayName]
)
const handleSaFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
const text = e.target?.result
if (typeof text === 'string') {
setSaJsonInput(text)
setSaError(null)
try {
const parsed = JSON.parse(text)
if (parsed.client_email && !saDisplayName.trim()) {
setSaDisplayName(parsed.client_email)
}
} catch {
// validation will catch this on submit
}
}
}
reader.readAsText(file)
readSaJsonFile(file)
event.target.value = ''
}
const handleSaDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault()
event.stopPropagation()
setSaDragActive(true)
}, [])
const handleSaDragLeave = useCallback((event: React.DragEvent) => {
event.preventDefault()
event.stopPropagation()
setSaDragActive(false)
}, [])
const handleSaDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault()
event.stopPropagation()
setSaDragActive(false)
const file = event.dataTransfer.files[0]
if (file) readSaJsonFile(file)
},
[readSaJsonFile]
)
const filteredServices = useMemo(() => {
if (!serviceSearch.trim()) return oauthServiceOptions
const q = serviceSearch.toLowerCase()
@@ -963,26 +999,48 @@ export function IntegrationsManager() {
<Label>
JSON Key<span className='ml-1'>*</span>
</Label>
<Textarea
value={saJsonInput}
onChange={(event) => {
setSaJsonInput(event.target.value)
setSaError(null)
if (!saDisplayName.trim()) {
try {
const parsed = JSON.parse(event.target.value)
if (parsed.client_email) setSaDisplayName(parsed.client_email)
} catch {
// not valid yet
<div
onDragOver={handleSaDragOver}
onDragLeave={handleSaDragLeave}
onDrop={handleSaDrop}
className={cn(
'relative mt-1.5 rounded-md border-2 border-dashed transition-colors',
saDragActive
? 'border-[var(--accent)] bg-[var(--accent)]/5'
: 'border-transparent'
)}
>
{saDragActive && (
<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-md bg-[var(--accent)]/5'>
<p className='font-medium text-[13px] text-[var(--accent)]'>
Drop JSON key file here
</p>
</div>
)}
<Textarea
value={saJsonInput}
onChange={(event) => {
setSaJsonInput(event.target.value)
setSaError(null)
if (!saDisplayName.trim()) {
try {
const parsed = JSON.parse(event.target.value)
if (parsed.client_email) setSaDisplayName(parsed.client_email)
} catch {
// not valid yet
}
}
}
}}
placeholder='Paste your service account JSON key here...'
autoComplete='off'
data-lpignore='true'
className='mt-1.5 min-h-[120px] resize-none font-mono text-[12px]'
autoFocus
/>
}}
placeholder='Paste your service account JSON key here or drag & drop a .json file...'
autoComplete='off'
data-lpignore='true'
className={cn(
'min-h-[120px] resize-none border-0 font-mono text-[12px]',
saDragActive && 'opacity-30'
)}
autoFocus
/>
</div>
<div className='mt-1.5'>
<label className='inline-flex cursor-pointer items-center gap-1.5 text-[12px] text-[var(--text-muted)] hover:text-[var(--text-secondary)]'>
<input

View File

@@ -377,7 +377,7 @@ export function CredentialSelector({
className={overlayContent ? 'pl-7' : ''}
/>
{supportsServiceAccount && !isPreview && (
{((supportsServiceAccount && subBlock.mode === 'advanced') || isServiceAccount) && !isPreview && (
<div className='mt-2.5 flex flex-col gap-2.5'>
<div className='flex items-center gap-1.5 pl-0.5'>
<Label>

View File

@@ -2,7 +2,11 @@ import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import {
getServiceAccountToken,
refreshTokenIfNeeded,
resolveOAuthAccountId,
} from '@/app/api/auth/oauth/utils'
const logger = createLogger('VertexCredential')
@@ -23,6 +27,14 @@ export async function resolveVertexCredential(
throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`)
}
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
const accessToken = await getServiceAccountToken(resolved.credentialId, [
'https://www.googleapis.com/auth/cloud-platform',
])
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
return accessToken
}
const credential = await db.query.account.findFirst({
where: eq(account.id, resolved.accountId),
})

View File

@@ -252,6 +252,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
'bigquery.datasets',
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
fetchList: async ({ context }: SelectorQueryArgs) => {
@@ -261,6 +262,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
'/api/tools/google_bigquery/datasets',
@@ -278,6 +280,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
credential: credentialId,
workflowId: context.workflowId,
projectId: context.projectId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
'/api/tools/google_bigquery/datasets',
@@ -301,6 +304,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
context.oauthCredential ?? 'none',
context.projectId ?? 'none',
context.datasetId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) =>
Boolean(context.oauthCredential && context.projectId && context.datasetId),
@@ -313,6 +317,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ tables: BigQueryTable[] }>(
'/api/tools/google_bigquery/tables',
@@ -331,6 +336,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
workflowId: context.workflowId,
projectId: context.projectId,
datasetId: context.datasetId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ tables: BigQueryTable[] }>(
'/api/tools/google_bigquery/tables',
@@ -557,11 +563,16 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
'selectors',
'google.tasks.lists',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const credentialId = ensureCredential(context, 'google.tasks.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
'/api/tools/google_tasks/task-lists',
{ method: 'POST', body }
@@ -571,7 +582,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
if (!detailId) return null
const credentialId = ensureCredential(context, 'google.tasks.lists')
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
const body = JSON.stringify({
credential: credentialId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
})
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
'/api/tools/google_tasks/task-lists',
{ method: 'POST', body }
@@ -877,11 +892,15 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
'selectors',
'gmail.labels',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', {
searchParams: { credentialId: context.oauthCredential },
searchParams: {
credentialId: context.oauthCredential,
impersonateEmail: context.impersonateUserEmail,
},
})
return (data.labels || []).map((label) => ({
id: label.id,
@@ -915,12 +934,18 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
'selectors',
'google.calendar',
context.oauthCredential ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context }: SelectorQueryArgs) => {
const data = await fetchJson<{ calendars: { id: string; summary: string }[] }>(
'/api/tools/google_calendar/calendars',
{ searchParams: { credentialId: context.oauthCredential } }
{
searchParams: {
credentialId: context.oauthCredential,
impersonateEmail: context.impersonateUserEmail,
},
}
)
return (data.calendars || []).map((calendar) => ({
id: calendar.id,
@@ -1393,6 +1418,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
context.mimeType ?? 'any',
context.fileId ?? 'root',
search ?? '',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential),
fetchList: async ({ context, search }: SelectorQueryArgs) => {
@@ -1406,6 +1432,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
parentId: context.fileId,
query: search,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
}
)
@@ -1424,6 +1451,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
credentialId,
fileId: detailId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
}
)
@@ -1440,6 +1468,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
'google.sheets',
context.oauthCredential ?? 'none',
context.spreadsheetId ?? 'none',
context.impersonateUserEmail ?? 'none',
],
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
fetchList: async ({ context }: SelectorQueryArgs) => {
@@ -1454,6 +1483,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
credentialId,
spreadsheetId: context.spreadsheetId,
workflowId: context.workflowId,
impersonateEmail: context.impersonateUserEmail,
},
}
)

View File

@@ -77,6 +77,7 @@ export interface SelectorContext {
baseId?: string
datasetId?: string
serviceDeskId?: string
impersonateUserEmail?: string
}
export interface SelectorQueryArgs {

View File

@@ -86,6 +86,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
],
serviceAccountProviderId: 'google-service-account',
},
'google-docs': {
name: 'Google Docs',
@@ -99,6 +100,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
],
serviceAccountProviderId: 'google-service-account',
},
'google-sheets': {
name: 'Google Sheets',
@@ -112,6 +114,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/drive.file',
'https://www.googleapis.com/auth/drive',
],
serviceAccountProviderId: 'google-service-account',
},
'google-forms': {
name: 'Google Forms',
@@ -126,6 +129,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/forms.body',
'https://www.googleapis.com/auth/forms.responses.readonly',
],
serviceAccountProviderId: 'google-service-account',
},
'google-calendar': {
name: 'Google Calendar',
@@ -138,6 +142,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/calendar',
],
serviceAccountProviderId: 'google-service-account',
},
'google-contacts': {
name: 'Google Contacts',
@@ -150,6 +155,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/contacts',
],
serviceAccountProviderId: 'google-service-account',
},
'google-ads': {
name: 'Google Ads',
@@ -162,6 +168,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/adwords',
],
serviceAccountProviderId: 'google-service-account',
},
'google-bigquery': {
name: 'Google BigQuery',
@@ -174,6 +181,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/bigquery',
],
serviceAccountProviderId: 'google-service-account',
},
'google-tasks': {
name: 'Google Tasks',
@@ -186,6 +194,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/tasks',
],
serviceAccountProviderId: 'google-service-account',
},
'google-vault': {
name: 'Google Vault',
@@ -199,6 +208,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/ediscovery',
'https://www.googleapis.com/auth/devstorage.read_only',
],
serviceAccountProviderId: 'google-service-account',
},
'google-groups': {
name: 'Google Groups',
@@ -212,6 +222,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/admin.directory.group',
'https://www.googleapis.com/auth/admin.directory.group.member',
],
serviceAccountProviderId: 'google-service-account',
},
'google-meet': {
name: 'Google Meet',
@@ -225,6 +236,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/meetings.space.created',
'https://www.googleapis.com/auth/meetings.space.readonly',
],
serviceAccountProviderId: 'google-service-account',
},
'google-service-account': {
name: 'Google Service Account',
@@ -246,6 +258,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cloud-platform',
],
serviceAccountProviderId: 'google-service-account',
},
},
defaultService: 'gmail',

View File

@@ -21,6 +21,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
'baseId',
'datasetId',
'serviceDeskId',
'impersonateUserEmail',
])
/**