mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
7 Commits
waleedlati
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc5df60d8f | ||
|
|
adea9db89d | ||
|
|
94abc424be | ||
|
|
c1c6ed66d1 | ||
|
|
a71304200e | ||
|
|
a4d581c76f | ||
|
|
f1efc598d1 |
@@ -20,6 +20,7 @@ When the user asks you to create a block:
|
||||
import { {ServiceName}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const {ServiceName}Block: BlockConfig = {
|
||||
type: '{service}', // snake_case identifier
|
||||
@@ -115,12 +116,17 @@ export const {ServiceName}Block: BlockConfig = {
|
||||
id: 'credential',
|
||||
title: 'Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}', // Must match OAuth provider
|
||||
serviceId: '{service}', // Must match OAuth provider service key
|
||||
requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
**Scopes:** Always use `getScopesForService(serviceId)` from `@/lib/oauth/utils` for `requiredScopes`. Never hardcode scope arrays — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
|
||||
|
||||
**Scope descriptions:** When adding a new OAuth provider, also add human-readable descriptions for all scopes in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`.
|
||||
|
||||
### Selectors (with dynamic options)
|
||||
```typescript
|
||||
// Channel selector (Slack, Discord, etc.)
|
||||
@@ -624,6 +630,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
import { ServiceIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
type: 'service',
|
||||
@@ -654,6 +661,7 @@ export const ServiceBlock: BlockConfig = {
|
||||
title: 'Service Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'service',
|
||||
requiredScopes: getScopesForService('service'),
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
},
|
||||
@@ -792,7 +800,8 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
|
||||
- [ ] Conditions use correct syntax (field, value, not, and)
|
||||
- [ ] DependsOn set for fields that need other values
|
||||
- [ ] Required fields marked correctly (boolean or condition)
|
||||
- [ ] OAuth inputs have correct `serviceId`
|
||||
- [ ] OAuth inputs have correct `serviceId` and `requiredScopes: getScopesForService(serviceId)`
|
||||
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
|
||||
- [ ] Tools.access lists all tool IDs (snake_case)
|
||||
- [ ] Tools.config.tool returns correct tool ID (snake_case)
|
||||
- [ ] Outputs match tool outputs
|
||||
|
||||
@@ -114,6 +114,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
type: '{service}',
|
||||
@@ -144,6 +145,7 @@ export const {Service}Block: BlockConfig = {
|
||||
title: '{Service} Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}',
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
required: true,
|
||||
},
|
||||
// Conditional fields per operation
|
||||
@@ -409,7 +411,7 @@ If creating V2 versions (API-aligned outputs):
|
||||
### Block
|
||||
- [ ] Created `blocks/blocks/{service}.ts`
|
||||
- [ ] Defined operation dropdown with all operations
|
||||
- [ ] Added credential field (oauth-input or short-input)
|
||||
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
|
||||
- [ ] Added conditional fields per operation
|
||||
- [ ] Set up dependsOn for cascading selectors
|
||||
- [ ] Configured tools.access with all tool IDs
|
||||
@@ -419,6 +421,12 @@ If creating V2 versions (API-aligned outputs):
|
||||
- [ ] If triggers: set `triggers.enabled` and `triggers.available`
|
||||
- [ ] If triggers: spread trigger subBlocks with `getTrigger()`
|
||||
|
||||
### OAuth Scopes (if OAuth service)
|
||||
- [ ] Defined scopes in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS`
|
||||
- [ ] Added scope descriptions in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
- [ ] Used `getCanonicalScopesForProvider()` in `auth.ts` (never hardcode)
|
||||
- [ ] Used `getScopesForService()` in block `requiredScopes` (never hardcode)
|
||||
|
||||
### Icon
|
||||
- [ ] Asked user to provide SVG
|
||||
- [ ] Added icon to `components/icons.tsx`
|
||||
@@ -717,6 +725,25 @@ Use `wandConfig` for fields that are hard to fill out manually:
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Scopes (Centralized System)
|
||||
|
||||
Scopes are maintained in a single source of truth and reused everywhere:
|
||||
|
||||
1. **Define scopes** in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
|
||||
2. **Add descriptions** in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for the OAuth modal UI
|
||||
3. **Reference in auth.ts** using `getCanonicalScopesForProvider(providerId)` from `@/lib/oauth/utils`
|
||||
4. **Reference in blocks** using `getScopesForService(serviceId)` from `@/lib/oauth/utils`
|
||||
|
||||
**Never hardcode scope arrays** in `auth.ts` or block `requiredScopes`. Always import from the centralized source.
|
||||
|
||||
```typescript
|
||||
// In auth.ts (Better Auth config)
|
||||
scopes: getCanonicalScopesForProvider('{service}'),
|
||||
|
||||
// In block credential sub-block
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
```
|
||||
|
||||
### Common Gotchas
|
||||
|
||||
1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration
|
||||
@@ -729,3 +756,5 @@ Use `wandConfig` for fields that are hard to fill out manually:
|
||||
8. **Always handle legacy file params** - Keep hidden `fileContent` params for backwards compatibility
|
||||
9. **Optional fields use advanced mode** - Set `mode: 'advanced'` on rarely-used optional fields
|
||||
10. **Complex inputs need wandConfig** - Timestamps, JSON arrays, and other hard-to-type values should have `wandConfig` enabled
|
||||
11. **Never hardcode scopes** - Use `getScopesForService()` in blocks and `getCanonicalScopesForProvider()` in auth.ts
|
||||
12. **Always add scope descriptions** - New scopes must have entries in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
|
||||
@@ -26,8 +26,9 @@ apps/sim/blocks/blocks/{service}.ts # Block definition
|
||||
apps/sim/tools/registry.ts # Tool registry entries for this service
|
||||
apps/sim/blocks/registry.ts # Block registry entry for this service
|
||||
apps/sim/components/icons.tsx # Icon definition
|
||||
apps/sim/lib/auth/auth.ts # OAuth scopes (if OAuth service)
|
||||
apps/sim/lib/oauth/oauth.ts # OAuth provider config (if OAuth service)
|
||||
apps/sim/lib/auth/auth.ts # OAuth config — should use getCanonicalScopesForProvider()
|
||||
apps/sim/lib/oauth/oauth.ts # OAuth provider config — single source of truth for scopes
|
||||
apps/sim/lib/oauth/utils.ts # Scope utilities, SCOPE_DESCRIPTIONS for modal UI
|
||||
```
|
||||
|
||||
## Step 2: Pull API Documentation
|
||||
@@ -199,11 +200,14 @@ For **each tool** in `tools.access`:
|
||||
|
||||
## Step 5: Validate OAuth Scopes (if OAuth service)
|
||||
|
||||
- [ ] `auth.ts` scopes include ALL scopes needed by ALL tools in the integration
|
||||
- [ ] `oauth.ts` provider config scopes match `auth.ts` scopes
|
||||
- [ ] Block `requiredScopes` (if defined) matches `auth.ts` scopes
|
||||
Scopes are centralized — the single source of truth is `OAUTH_PROVIDERS` in `lib/oauth/oauth.ts`.
|
||||
|
||||
- [ ] Scopes defined in `lib/oauth/oauth.ts` under `OAUTH_PROVIDERS[provider].services[service].scopes`
|
||||
- [ ] `auth.ts` uses `getCanonicalScopesForProvider(providerId)` — NOT a hardcoded array
|
||||
- [ ] Block `requiredScopes` uses `getScopesForService(serviceId)` — NOT a hardcoded array
|
||||
- [ ] No hardcoded scope arrays in `auth.ts` or block files (should all use utility functions)
|
||||
- [ ] Each scope has a human-readable description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
- [ ] No excess scopes that aren't needed by any tool
|
||||
- [ ] Each scope has a human-readable description in `oauth-required-modal.tsx`'s `SCOPE_DESCRIPTIONS`
|
||||
|
||||
## Step 6: Validate Pagination Consistency
|
||||
|
||||
@@ -244,7 +248,8 @@ Group findings by severity:
|
||||
- Missing `.trim()` on ID fields in request URLs
|
||||
- Missing `?? null` on nullable response fields
|
||||
- Block condition array missing an operation that uses that field
|
||||
- Missing scope description in `oauth-required-modal.tsx`
|
||||
- Hardcoded scope arrays instead of using `getScopesForService()` / `getCanonicalScopesForProvider()`
|
||||
- Missing scope description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
|
||||
**Suggestion** (minor improvements):
|
||||
- Better description text
|
||||
@@ -273,7 +278,8 @@ After fixing, confirm:
|
||||
- [ ] Validated wandConfig on timestamps and complex inputs
|
||||
- [ ] Validated tools.config mapping, tool selector, and type coercions
|
||||
- [ ] Validated block outputs match what tools return, with typed JSON where possible
|
||||
- [ ] Validated OAuth scopes alignment across auth.ts, oauth.ts, block, and modal (if OAuth)
|
||||
- [ ] Validated OAuth scopes use centralized utilities (getScopesForService, getCanonicalScopesForProvider) — no hardcoded arrays
|
||||
- [ ] Validated scope descriptions exist in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for all scopes
|
||||
- [ ] Validated pagination consistency across tools and block
|
||||
- [ ] Validated error handling (error checks, meaningful messages)
|
||||
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
|
||||
|
||||
@@ -6,40 +6,33 @@
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockGetSession,
|
||||
mockDb,
|
||||
mockLogger,
|
||||
mockParseProvider,
|
||||
mockEvaluateScopeCoverage,
|
||||
mockJwtDecode,
|
||||
mockEq,
|
||||
} = vi.hoisted(() => {
|
||||
const db = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn(),
|
||||
const { mockGetSession, mockDb, mockLogger, mockParseProvider, mockJwtDecode, mockEq } = vi.hoisted(
|
||||
() => {
|
||||
const db = {
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
where: vi.fn().mockReturnThis(),
|
||||
limit: vi.fn(),
|
||||
}
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockGetSession: vi.fn(),
|
||||
mockDb: db,
|
||||
mockLogger: logger,
|
||||
mockParseProvider: vi.fn(),
|
||||
mockJwtDecode: vi.fn(),
|
||||
mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
|
||||
}
|
||||
}
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(),
|
||||
}
|
||||
return {
|
||||
mockGetSession: vi.fn(),
|
||||
mockDb: db,
|
||||
mockLogger: logger,
|
||||
mockParseProvider: vi.fn(),
|
||||
mockEvaluateScopeCoverage: vi.fn(),
|
||||
mockJwtDecode: vi.fn(),
|
||||
mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
getSession: mockGetSession,
|
||||
@@ -66,7 +59,6 @@ vi.mock('@sim/logger', () => ({
|
||||
|
||||
vi.mock('@/lib/oauth/utils', () => ({
|
||||
parseProvider: mockParseProvider,
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/auth/oauth/connections/route'
|
||||
@@ -83,16 +75,6 @@ describe('OAuth Connections API Route', () => {
|
||||
baseProvider: providerId.split('-')[0] || providerId,
|
||||
featureType: providerId.split('-')[1] || 'default',
|
||||
}))
|
||||
|
||||
mockEvaluateScopeCoverage.mockImplementation(
|
||||
(_providerId: string, _grantedScopes: string[]) => ({
|
||||
canonicalScopes: ['email', 'profile'],
|
||||
grantedScopes: ['email', 'profile'],
|
||||
missingScopes: [],
|
||||
extraScopes: [],
|
||||
requiresReauthorization: false,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should return connections successfully', async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import type { OAuthProvider } from '@/lib/oauth'
|
||||
import { evaluateScopeCoverage, parseProvider } from '@/lib/oauth'
|
||||
import { parseProvider } from '@/lib/oauth'
|
||||
|
||||
const logger = createLogger('OAuthConnectionsAPI')
|
||||
|
||||
@@ -49,8 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
for (const acc of accounts) {
|
||||
const { baseProvider, featureType } = parseProvider(acc.providerId as OAuthProvider)
|
||||
const grantedScopes = acc.scope ? acc.scope.split(/\s+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes)
|
||||
const scopes = acc.scope ? acc.scope.split(/\s+/).filter(Boolean) : []
|
||||
|
||||
if (baseProvider) {
|
||||
// Try multiple methods to get a user-friendly display name
|
||||
@@ -96,10 +95,6 @@ export async function GET(request: NextRequest) {
|
||||
const accountSummary = {
|
||||
id: acc.id,
|
||||
name: displayName,
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
}
|
||||
|
||||
if (existingConnection) {
|
||||
@@ -108,20 +103,8 @@ export async function GET(request: NextRequest) {
|
||||
existingConnection.accounts.push(accountSummary)
|
||||
|
||||
existingConnection.scopes = Array.from(
|
||||
new Set([...(existingConnection.scopes || []), ...scopeEvaluation.grantedScopes])
|
||||
new Set([...(existingConnection.scopes || []), ...scopes])
|
||||
)
|
||||
existingConnection.missingScopes = Array.from(
|
||||
new Set([...(existingConnection.missingScopes || []), ...scopeEvaluation.missingScopes])
|
||||
)
|
||||
existingConnection.extraScopes = Array.from(
|
||||
new Set([...(existingConnection.extraScopes || []), ...scopeEvaluation.extraScopes])
|
||||
)
|
||||
existingConnection.canonicalScopes =
|
||||
existingConnection.canonicalScopes && existingConnection.canonicalScopes.length > 0
|
||||
? existingConnection.canonicalScopes
|
||||
: scopeEvaluation.canonicalScopes
|
||||
existingConnection.requiresReauthorization =
|
||||
existingConnection.requiresReauthorization || scopeEvaluation.requiresReauthorization
|
||||
|
||||
const existingTimestamp = existingConnection.lastConnected
|
||||
? new Date(existingConnection.lastConnected).getTime()
|
||||
@@ -138,11 +121,7 @@ export async function GET(request: NextRequest) {
|
||||
baseProvider,
|
||||
featureType,
|
||||
isConnected: true,
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
scopes,
|
||||
lastConnected: acc.updatedAt.toISOString(),
|
||||
accounts: [accountSummary],
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockCheckSessionOrInternalAuth, mockEvaluateScopeCoverage, mockLogger } = vi.hoisted(() => {
|
||||
const { mockCheckSessionOrInternalAuth, mockLogger } = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -19,7 +19,6 @@ const { mockCheckSessionOrInternalAuth, mockEvaluateScopeCoverage, mockLogger }
|
||||
}
|
||||
return {
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
mockEvaluateScopeCoverage: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
@@ -28,10 +27,6 @@ vi.mock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/oauth', () => ({
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('mock-request-id'),
|
||||
}))
|
||||
@@ -87,16 +82,6 @@ describe('OAuth Credentials API Route', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockEvaluateScopeCoverage.mockImplementation(
|
||||
(_providerId: string, grantedScopes: string[]) => ({
|
||||
canonicalScopes: grantedScopes,
|
||||
grantedScopes,
|
||||
missingScopes: [],
|
||||
extraScopes: [],
|
||||
requiresReauthorization: false,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle unauthenticated user', async () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
|
||||
import { evaluateScopeCoverage } from '@/lib/oauth'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
@@ -39,8 +38,7 @@ function toCredentialResponse(
|
||||
scope: string | null
|
||||
) {
|
||||
const storedScope = scope?.trim()
|
||||
const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(providerId, grantedScopes)
|
||||
const scopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const [_, featureType = 'default'] = providerId.split('-')
|
||||
|
||||
return {
|
||||
@@ -49,11 +47,7 @@ function toCredentialResponse(
|
||||
provider: providerId,
|
||||
lastUsed: updatedAt.toISOString(),
|
||||
isDefault: featureType === 'default',
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
scopes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import {
|
||||
getProviderIdFromServiceId,
|
||||
getScopeDescription,
|
||||
OAUTH_PROVIDERS,
|
||||
type OAuthProvider,
|
||||
parseProvider,
|
||||
@@ -33,318 +34,6 @@ export interface OAuthRequiredModalProps {
|
||||
onConnect?: () => Promise<void> | void
|
||||
}
|
||||
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/gmail.send': 'Send emails',
|
||||
'https://www.googleapis.com/auth/gmail.labels': 'View and manage email labels',
|
||||
'https://www.googleapis.com/auth/gmail.modify': 'View and manage email messages',
|
||||
'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files',
|
||||
'https://www.googleapis.com/auth/drive': 'Access all Google Drive files',
|
||||
'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
|
||||
'https://www.googleapis.com/auth/contacts': 'View and manage Google Contacts',
|
||||
'https://www.googleapis.com/auth/tasks': 'Create, read, update, and delete Google Tasks',
|
||||
'https://www.googleapis.com/auth/userinfo.email': 'View email address',
|
||||
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
|
||||
'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
|
||||
'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery',
|
||||
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
|
||||
'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member':
|
||||
'Manage Google Workspace group memberships',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.readonly': 'View Google Workspace groups',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member.readonly':
|
||||
'View Google Workspace group memberships',
|
||||
'https://www.googleapis.com/auth/meetings.space.created':
|
||||
'Create and manage Google Meet meeting spaces',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly':
|
||||
'View Google Meet meeting space details',
|
||||
'https://www.googleapis.com/auth/cloud-platform':
|
||||
'Full access to Google Cloud resources for Vertex AI',
|
||||
'read:confluence-content.all': 'Read all Confluence content',
|
||||
'read:confluence-space.summary': 'Read Confluence space information',
|
||||
'read:space:confluence': 'View Confluence spaces',
|
||||
'read:space-details:confluence': 'View detailed Confluence space information',
|
||||
'write:confluence-content': 'Create and edit Confluence pages',
|
||||
'write:confluence-space': 'Manage Confluence spaces',
|
||||
'write:confluence-file': 'Upload files to Confluence',
|
||||
'read:content:confluence': 'Read Confluence content',
|
||||
'read:page:confluence': 'View Confluence pages',
|
||||
'write:page:confluence': 'Create and update Confluence pages',
|
||||
'read:comment:confluence': 'View comments on Confluence pages',
|
||||
'write:comment:confluence': 'Create and update comments',
|
||||
'delete:comment:confluence': 'Delete comments from Confluence pages',
|
||||
'read:attachment:confluence': 'View attachments on Confluence pages',
|
||||
'write:attachment:confluence': 'Upload and manage attachments',
|
||||
'delete:attachment:confluence': 'Delete attachments from Confluence pages',
|
||||
'delete:page:confluence': 'Delete Confluence pages',
|
||||
'read:label:confluence': 'View labels on Confluence content',
|
||||
'write:label:confluence': 'Add and remove labels',
|
||||
'search:confluence': 'Search Confluence content',
|
||||
'readonly:content.attachment:confluence': 'View attachments',
|
||||
'read:blogpost:confluence': 'View Confluence blog posts',
|
||||
'write:blogpost:confluence': 'Create and update Confluence blog posts',
|
||||
'read:content.property:confluence': 'View properties on Confluence content',
|
||||
'write:content.property:confluence': 'Create and manage content properties',
|
||||
'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)',
|
||||
'read:content.metadata:confluence': 'View content metadata (required for ancestors)',
|
||||
'read:user:confluence': 'View Confluence user profiles',
|
||||
'read:task:confluence': 'View Confluence inline tasks',
|
||||
'write:task:confluence': 'Update Confluence inline tasks',
|
||||
'delete:blogpost:confluence': 'Delete Confluence blog posts',
|
||||
'write:space:confluence': 'Create and update Confluence spaces',
|
||||
'delete:space:confluence': 'Delete Confluence spaces',
|
||||
'read:space.property:confluence': 'View Confluence space properties',
|
||||
'write:space.property:confluence': 'Create and manage space properties',
|
||||
'read:space.permission:confluence': 'View Confluence space permissions',
|
||||
'read:me': 'Read profile information',
|
||||
'database.read': 'Read database',
|
||||
'database.write': 'Write to database',
|
||||
'projects.read': 'Read projects',
|
||||
offline_access: 'Access account when not using the application',
|
||||
repo: 'Access repositories',
|
||||
workflow: 'Manage repository workflows',
|
||||
'read:user': 'Read public user information',
|
||||
'user:email': 'Access email address',
|
||||
'tweet.read': 'Read tweets and timeline',
|
||||
'tweet.write': 'Post and delete tweets',
|
||||
'tweet.moderate.write': 'Hide and unhide replies to tweets',
|
||||
'users.read': 'Read user profiles and account information',
|
||||
'follows.read': 'View followers and following lists',
|
||||
'follows.write': 'Follow and unfollow users',
|
||||
'bookmark.read': 'View bookmarked tweets',
|
||||
'bookmark.write': 'Add and remove bookmarks',
|
||||
'like.read': 'View liked tweets and liking users',
|
||||
'like.write': 'Like and unlike tweets',
|
||||
'block.read': 'View blocked users',
|
||||
'block.write': 'Block and unblock users',
|
||||
'mute.read': 'View muted users',
|
||||
'mute.write': 'Mute and unmute users',
|
||||
'offline.access': 'Access account when not using the application',
|
||||
'data.records:read': 'Read records',
|
||||
'data.records:write': 'Write to records',
|
||||
'schema.bases:read': 'View bases and tables',
|
||||
'webhook:manage': 'Manage webhooks',
|
||||
'page.read': 'Read Notion pages',
|
||||
'page.write': 'Write to Notion pages',
|
||||
'workspace.content': 'Read Notion content',
|
||||
'workspace.name': 'Read Notion workspace name',
|
||||
'workspace.read': 'Read Notion workspace',
|
||||
'workspace.write': 'Write to Notion workspace',
|
||||
'user.email:read': 'Read email address',
|
||||
'read:jira-user': 'Read Jira user',
|
||||
'read:jira-work': 'Read Jira work',
|
||||
'write:jira-work': 'Write to Jira work',
|
||||
'manage:jira-webhook': 'Register and manage Jira webhooks',
|
||||
'read:webhook:jira': 'View Jira webhooks',
|
||||
'write:webhook:jira': 'Create and update Jira webhooks',
|
||||
'delete:webhook:jira': 'Delete Jira webhooks',
|
||||
'read:issue-event:jira': 'Read Jira issue events',
|
||||
'write:issue:jira': 'Write to Jira issues',
|
||||
'read:project:jira': 'Read Jira projects',
|
||||
'read:issue-type:jira': 'Read Jira issue types',
|
||||
'read:issue-meta:jira': 'Read Jira issue meta',
|
||||
'read:issue-security-level:jira': 'Read Jira issue security level',
|
||||
'read:issue.vote:jira': 'Read Jira issue votes',
|
||||
'read:issue.changelog:jira': 'Read Jira issue changelog',
|
||||
'read:avatar:jira': 'Read Jira avatar',
|
||||
'read:issue:jira': 'Read Jira issues',
|
||||
'read:status:jira': 'Read Jira status',
|
||||
'read:user:jira': 'Read Jira user',
|
||||
'read:field-configuration:jira': 'Read Jira field configuration',
|
||||
'read:issue-details:jira': 'Read Jira issue details',
|
||||
'read:field:jira': 'Read Jira field configurations',
|
||||
'read:jql:jira': 'Use JQL to filter Jira issues',
|
||||
'read:comment.property:jira': 'Read Jira comment properties',
|
||||
'read:issue.property:jira': 'Read Jira issue properties',
|
||||
'delete:issue:jira': 'Delete Jira issues',
|
||||
'write:comment:jira': 'Add and update comments on Jira issues',
|
||||
'read:comment:jira': 'Read comments on Jira issues',
|
||||
'delete:comment:jira': 'Delete comments from Jira issues',
|
||||
'read:attachment:jira': 'Read attachments from Jira issues',
|
||||
'delete:attachment:jira': 'Delete attachments from Jira issues',
|
||||
'write:issue-worklog:jira': 'Add and update worklog entries on Jira issues',
|
||||
'read:issue-worklog:jira': 'Read worklog entries from Jira issues',
|
||||
'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues',
|
||||
'write:issue-link:jira': 'Create links between Jira issues',
|
||||
'delete:issue-link:jira': 'Delete links between Jira issues',
|
||||
'User.Read': 'Read Microsoft user',
|
||||
'Chat.Read': 'Read Microsoft chats',
|
||||
'Chat.ReadWrite': 'Write to Microsoft chats',
|
||||
'Chat.ReadBasic': 'Read Microsoft chats',
|
||||
'ChatMessage.Send': 'Send chat messages',
|
||||
'Channel.ReadBasic.All': 'Read Microsoft channels',
|
||||
'ChannelMessage.Send': 'Write to Microsoft channels',
|
||||
'ChannelMessage.Read.All': 'Read Microsoft channels',
|
||||
'ChannelMessage.ReadWrite': 'Read and write to Microsoft channels',
|
||||
'ChannelMember.Read.All': 'Read team channel members',
|
||||
'Group.Read.All': 'Read Microsoft groups',
|
||||
'Group.ReadWrite.All': 'Write to Microsoft groups',
|
||||
'Team.ReadBasic.All': 'Read Microsoft teams',
|
||||
'TeamMember.Read.All': 'Read team members',
|
||||
'Mail.ReadWrite': 'Write to Microsoft emails',
|
||||
'Mail.ReadBasic': 'Read Microsoft emails',
|
||||
'Mail.Read': 'Read Microsoft emails',
|
||||
'Mail.Send': 'Send emails',
|
||||
'Files.Read': 'Read OneDrive files',
|
||||
'Files.ReadWrite': 'Read and write OneDrive files',
|
||||
'Tasks.ReadWrite': 'Read and manage Planner tasks',
|
||||
'Sites.Read.All': 'Read Sharepoint sites',
|
||||
'Sites.ReadWrite.All': 'Read and write Sharepoint sites',
|
||||
'Sites.Manage.All': 'Manage Sharepoint sites',
|
||||
openid: 'Standard authentication',
|
||||
profile: 'Access profile information',
|
||||
email: 'Access email address',
|
||||
identify: 'Read Discord user',
|
||||
bot: 'Read Discord bot',
|
||||
'messages.read': 'Read Discord messages',
|
||||
guilds: 'Read Discord guilds',
|
||||
'guilds.members.read': 'Read Discord guild members',
|
||||
identity: 'Access Reddit identity',
|
||||
submit: 'Submit posts and comments',
|
||||
vote: 'Vote on posts and comments',
|
||||
save: 'Save and unsave posts and comments',
|
||||
edit: 'Edit posts and comments',
|
||||
subscribe: 'Subscribe and unsubscribe from subreddits',
|
||||
history: 'Access Reddit history',
|
||||
privatemessages: 'Access inbox and send private messages',
|
||||
account: 'Update account preferences and settings',
|
||||
mysubreddits: 'Access subscribed and moderated subreddits',
|
||||
flair: 'Manage user and post flair',
|
||||
report: 'Report posts and comments for rule violations',
|
||||
modposts: 'Approve, remove, and moderate posts in moderated subreddits',
|
||||
modflair: 'Manage flair in moderated subreddits',
|
||||
modmail: 'Access and respond to moderator mail',
|
||||
login: 'Access Wealthbox account',
|
||||
data: 'Access Wealthbox data',
|
||||
read: 'Read access to workspace',
|
||||
write: 'Write access to Linear workspace',
|
||||
'channels:read': 'View public channels',
|
||||
'channels:history': 'Read channel messages',
|
||||
'groups:read': 'View private channels',
|
||||
'groups:history': 'Read private messages',
|
||||
'chat:write': 'Send messages',
|
||||
'chat:write.public': 'Post to public channels',
|
||||
'im:write': 'Send direct messages',
|
||||
'im:history': 'Read direct message history',
|
||||
'im:read': 'View direct message channels',
|
||||
'users:read': 'View workspace users',
|
||||
'files:write': 'Upload files',
|
||||
'files:read': 'Download and read files',
|
||||
'canvases:write': 'Create canvas documents',
|
||||
'reactions:write': 'Add emoji reactions to messages',
|
||||
'sites:read': 'View Webflow sites',
|
||||
'sites:write': 'Manage webhooks and site settings',
|
||||
'cms:read': 'View CMS content',
|
||||
'cms:write': 'Manage CMS content',
|
||||
'crm.objects.contacts.read': 'Read HubSpot contacts',
|
||||
'crm.objects.contacts.write': 'Create and update HubSpot contacts',
|
||||
'crm.objects.companies.read': 'Read HubSpot companies',
|
||||
'crm.objects.companies.write': 'Create and update HubSpot companies',
|
||||
'crm.objects.deals.read': 'Read HubSpot deals',
|
||||
'crm.objects.deals.write': 'Create and update HubSpot deals',
|
||||
'crm.objects.owners.read': 'Read HubSpot object owners',
|
||||
'crm.objects.users.read': 'Read HubSpot users',
|
||||
'crm.objects.users.write': 'Create and update HubSpot users',
|
||||
'crm.objects.marketing_events.read': 'Read HubSpot marketing events',
|
||||
'crm.objects.marketing_events.write': 'Create and update HubSpot marketing events',
|
||||
'crm.objects.line_items.read': 'Read HubSpot line items',
|
||||
'crm.objects.line_items.write': 'Create and update HubSpot line items',
|
||||
'crm.objects.quotes.read': 'Read HubSpot quotes',
|
||||
'crm.objects.quotes.write': 'Create and update HubSpot quotes',
|
||||
'crm.objects.appointments.read': 'Read HubSpot appointments',
|
||||
'crm.objects.appointments.write': 'Create and update HubSpot appointments',
|
||||
'crm.objects.carts.read': 'Read HubSpot shopping carts',
|
||||
'crm.objects.carts.write': 'Create and update HubSpot shopping carts',
|
||||
'crm.import': 'Import data into HubSpot',
|
||||
'crm.lists.read': 'Read HubSpot lists',
|
||||
'crm.lists.write': 'Create and update HubSpot lists',
|
||||
tickets: 'Manage HubSpot tickets',
|
||||
api: 'Access Salesforce API',
|
||||
refresh_token: 'Maintain long-term access to Salesforce account',
|
||||
default: 'Access Asana workspace',
|
||||
base: 'Basic access to Pipedrive account',
|
||||
'deals:read': 'Read Pipedrive deals',
|
||||
'deals:full': 'Full access to manage Pipedrive deals',
|
||||
'contacts:read': 'Read Pipedrive contacts',
|
||||
'contacts:full': 'Full access to manage Pipedrive contacts',
|
||||
'leads:read': 'Read Pipedrive leads',
|
||||
'leads:full': 'Full access to manage Pipedrive leads',
|
||||
'activities:read': 'Read Pipedrive activities',
|
||||
'activities:full': 'Full access to manage Pipedrive activities',
|
||||
'mail:read': 'Read Pipedrive emails',
|
||||
'mail:full': 'Full access to manage Pipedrive emails',
|
||||
'projects:read': 'Read Pipedrive projects',
|
||||
'projects:full': 'Full access to manage Pipedrive projects',
|
||||
'webhooks:read': 'Read Pipedrive webhooks',
|
||||
'webhooks:full': 'Full access to manage Pipedrive webhooks',
|
||||
w_member_social: 'Access LinkedIn profile',
|
||||
// Box scopes
|
||||
root_readwrite: 'Read and write all files and folders in Box account',
|
||||
root_readonly: 'Read all files and folders in Box account',
|
||||
// Shopify scopes (write_* implicitly includes read access)
|
||||
write_products: 'Read and manage Shopify products',
|
||||
write_orders: 'Read and manage Shopify orders',
|
||||
write_customers: 'Read and manage Shopify customers',
|
||||
write_inventory: 'Read and manage Shopify inventory levels',
|
||||
read_locations: 'View store locations',
|
||||
write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders',
|
||||
// Zoom scopes
|
||||
'user:read:user': 'View Zoom profile information',
|
||||
'meeting:write:meeting': 'Create Zoom meetings',
|
||||
'meeting:read:meeting': 'View Zoom meeting details',
|
||||
'meeting:read:list_meetings': 'List Zoom meetings',
|
||||
'meeting:update:meeting': 'Update Zoom meetings',
|
||||
'meeting:delete:meeting': 'Delete Zoom meetings',
|
||||
'meeting:read:invitation': 'View Zoom meeting invitations',
|
||||
'meeting:read:list_past_participants': 'View past meeting participants',
|
||||
'cloud_recording:read:list_user_recordings': 'List Zoom cloud recordings',
|
||||
'cloud_recording:read:list_recording_files': 'View recording files',
|
||||
'cloud_recording:delete:recording_file': 'Delete cloud recordings',
|
||||
// Dropbox scopes
|
||||
'account_info.read': 'View Dropbox account information',
|
||||
'files.metadata.read': 'View file and folder names, sizes, and dates',
|
||||
'files.metadata.write': 'Modify file and folder metadata',
|
||||
'files.content.read': 'Download and read Dropbox files',
|
||||
'files.content.write': 'Upload, copy, move, and delete files in Dropbox',
|
||||
'sharing.read': 'View shared files and folders',
|
||||
'sharing.write': 'Share files and folders with others',
|
||||
// WordPress.com scopes
|
||||
global: 'Full access to manage WordPress.com sites, posts, pages, media, and settings',
|
||||
// Spotify scopes
|
||||
'user-read-private': 'View Spotify account details',
|
||||
'user-read-email': 'View email address on Spotify',
|
||||
'user-library-read': 'View saved tracks and albums',
|
||||
'user-library-modify': 'Save and remove tracks and albums from library',
|
||||
'playlist-read-private': 'View private playlists',
|
||||
'playlist-read-collaborative': 'View collaborative playlists',
|
||||
'playlist-modify-public': 'Create and manage public playlists',
|
||||
'playlist-modify-private': 'Create and manage private playlists',
|
||||
'user-read-playback-state': 'View current playback state',
|
||||
'user-modify-playback-state': 'Control playback on Spotify devices',
|
||||
'user-read-currently-playing': 'View currently playing track',
|
||||
'user-read-recently-played': 'View recently played tracks',
|
||||
'user-top-read': 'View top artists and tracks',
|
||||
'user-follow-read': 'View followed artists and users',
|
||||
'user-follow-modify': 'Follow and unfollow artists and users',
|
||||
'user-read-playback-position': 'View playback position in podcasts',
|
||||
'ugc-image-upload': 'Upload images to Spotify playlists',
|
||||
// Attio
|
||||
'record_permission:read-write': 'Read and write CRM records',
|
||||
'object_configuration:read-write': 'Read and manage object schemas',
|
||||
'list_configuration:read-write': 'Read and manage list configurations',
|
||||
'list_entry:read-write': 'Read and write list entries',
|
||||
'note:read-write': 'Read and write notes',
|
||||
'task:read-write': 'Read and write tasks',
|
||||
'comment:read-write': 'Read and write comments and threads',
|
||||
'user_management:read': 'View workspace members',
|
||||
'webhook:read-write': 'Manage webhooks',
|
||||
}
|
||||
|
||||
function getScopeDescription(scope: string): string {
|
||||
return SCOPE_DESCRIPTIONS[scope] || scope
|
||||
}
|
||||
|
||||
export function OAuthRequiredModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type OAuthProvider,
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { getMissingRequiredScopes } from '@/lib/oauth/utils'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
@@ -25,7 +26,6 @@ import { useOAuthCredentials } from '@/hooks/queries/oauth-credentials'
|
||||
import { useOrganizations } from '@/hooks/queries/organization'
|
||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
type OAuthService,
|
||||
parseProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { getMissingRequiredScopes } from '@/lib/oauth/utils'
|
||||
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal'
|
||||
import { useOAuthCredentials } from '@/hooks/queries/oauth-credentials'
|
||||
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
|
||||
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const getProviderIcon = (providerName: OAuthProvider) => {
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useDependsOnGate } from './use-depends-on-gate'
|
||||
|
||||
@@ -13,8 +15,7 @@ import { useDependsOnGate } from './use-depends-on-gate'
|
||||
*
|
||||
* Builds a `SelectorContext` by mapping each `dependsOn` entry through the
|
||||
* canonical index to its `canonicalParamId`, which maps directly to
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`).
|
||||
* The one special case is `oauthCredential` which maps to `credentialId`.
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `oauthCredential`).
|
||||
*
|
||||
* @param blockId - The block containing the selector sub-block
|
||||
* @param subBlock - The sub-block config (must have `selectorKey` set)
|
||||
@@ -30,57 +31,58 @@ export function useSelectorSetup(
|
||||
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
|
||||
const workflowId = (params?.workflowId as string) || activeWorkflowId || ''
|
||||
|
||||
const envVariables = useEnvironmentStore((s) => s.variables)
|
||||
|
||||
const { finalDisabled, dependencyValues, canonicalIndex } = useDependsOnGate(
|
||||
blockId,
|
||||
subBlock,
|
||||
opts
|
||||
)
|
||||
|
||||
const resolvedDependencyValues = useMemo(() => {
|
||||
const resolved: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(dependencyValues)) {
|
||||
if (value === null || value === undefined) {
|
||||
resolved[key] = value
|
||||
continue
|
||||
}
|
||||
const str = String(value)
|
||||
if (isEnvVarReference(str)) {
|
||||
const varName = extractEnvVarName(str)
|
||||
resolved[key] = envVariables[varName]?.value || undefined
|
||||
} else {
|
||||
resolved[key] = value
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}, [dependencyValues, envVariables])
|
||||
|
||||
const selectorContext = useMemo<SelectorContext>(() => {
|
||||
const context: SelectorContext = {
|
||||
workflowId,
|
||||
mimeType: subBlock.mimeType,
|
||||
}
|
||||
|
||||
for (const [depKey, value] of Object.entries(dependencyValues)) {
|
||||
for (const [depKey, value] of Object.entries(resolvedDependencyValues)) {
|
||||
if (value === null || value === undefined) continue
|
||||
const strValue = String(value)
|
||||
if (!strValue) continue
|
||||
if (isReference(strValue) || isEnvVarReference(strValue)) continue
|
||||
if (isReference(strValue)) continue
|
||||
|
||||
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
|
||||
|
||||
if (canonicalParamId === 'oauthCredential') {
|
||||
context.credentialId = strValue
|
||||
} else if (canonicalParamId in CONTEXT_FIELD_SET) {
|
||||
;(context as Record<string, unknown>)[canonicalParamId] = strValue
|
||||
if (SELECTOR_CONTEXT_FIELDS.has(canonicalParamId as keyof SelectorContext)) {
|
||||
context[canonicalParamId as keyof SelectorContext] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}, [dependencyValues, canonicalIndex, workflowId, subBlock.mimeType])
|
||||
}, [resolvedDependencyValues, canonicalIndex, workflowId, subBlock.mimeType])
|
||||
|
||||
return {
|
||||
selectorKey: (subBlock.selectorKey ?? null) as SelectorKey | null,
|
||||
selectorContext,
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
disabled: finalDisabled || !subBlock.selectorKey,
|
||||
dependencyValues,
|
||||
dependencyValues: resolvedDependencyValues,
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXT_FIELD_SET: Record<string, true> = {
|
||||
credentialId: true,
|
||||
domain: true,
|
||||
teamId: true,
|
||||
projectId: true,
|
||||
knowledgeBaseId: true,
|
||||
planId: true,
|
||||
siteId: true,
|
||||
collectionId: true,
|
||||
spreadsheetId: true,
|
||||
fileId: true,
|
||||
baseId: true,
|
||||
datasetId: true,
|
||||
serviceDeskId: true,
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'
|
||||
const SLACK_OVERRIDES: SelectorOverrides = {
|
||||
transformContext: (context, deps) => {
|
||||
const authMethod = deps.authMethod as string
|
||||
const credentialId =
|
||||
const oauthCredential =
|
||||
authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '')
|
||||
return { ...context, credentialId }
|
||||
return { ...context, oauthCredential }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ const SubBlockRow = memo(function SubBlockRow({
|
||||
subBlock,
|
||||
value: rawValue,
|
||||
workflowId,
|
||||
credentialId: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
oauthCredential: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined,
|
||||
domain: domainValue,
|
||||
teamId: teamIdValue,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getApiKeyCondition, getModelOptions } from '@/blocks/utils'
|
||||
@@ -128,7 +129,7 @@ Return ONLY the JSON array.`,
|
||||
serviceId: 'vertex-ai',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
requiredScopes: getScopesForService('vertex-ai'),
|
||||
placeholder: 'Select Google Cloud account',
|
||||
required: true,
|
||||
condition: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AirtableIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AirtableResponse } from '@/tools/airtable/types'
|
||||
@@ -38,13 +39,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'airtable',
|
||||
requiredScopes: [
|
||||
'data.records:read',
|
||||
'data.records:write',
|
||||
'schema.bases:read',
|
||||
'user.email:read',
|
||||
'webhook:manage',
|
||||
],
|
||||
requiredScopes: getScopesForService('airtable'),
|
||||
placeholder: 'Select Airtable account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AsanaIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AsanaResponse } from '@/tools/asana/types'
|
||||
@@ -36,7 +37,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'asana',
|
||||
requiredScopes: ['default'],
|
||||
requiredScopes: getScopesForService('asana'),
|
||||
placeholder: 'Select Asana account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ConfluenceIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -55,36 +56,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'confluence',
|
||||
requiredScopes: [
|
||||
'read:confluence-content.all',
|
||||
'read:confluence-space.summary',
|
||||
'read:space:confluence',
|
||||
'read:space-details:confluence',
|
||||
'write:confluence-content',
|
||||
'write:confluence-space',
|
||||
'write:confluence-file',
|
||||
'read:content:confluence',
|
||||
'read:page:confluence',
|
||||
'write:page:confluence',
|
||||
'read:comment:confluence',
|
||||
'write:comment:confluence',
|
||||
'delete:comment:confluence',
|
||||
'read:attachment:confluence',
|
||||
'write:attachment:confluence',
|
||||
'delete:attachment:confluence',
|
||||
'delete:page:confluence',
|
||||
'read:label:confluence',
|
||||
'write:label:confluence',
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
],
|
||||
requiredScopes: getScopesForService('confluence'),
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
},
|
||||
@@ -463,45 +435,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'confluence',
|
||||
requiredScopes: [
|
||||
'read:confluence-content.all',
|
||||
'read:confluence-space.summary',
|
||||
'read:space:confluence',
|
||||
'read:space-details:confluence',
|
||||
'write:confluence-content',
|
||||
'write:confluence-space',
|
||||
'write:confluence-file',
|
||||
'read:content:confluence',
|
||||
'read:page:confluence',
|
||||
'write:page:confluence',
|
||||
'read:comment:confluence',
|
||||
'write:comment:confluence',
|
||||
'delete:comment:confluence',
|
||||
'read:attachment:confluence',
|
||||
'write:attachment:confluence',
|
||||
'delete:attachment:confluence',
|
||||
'delete:page:confluence',
|
||||
'read:label:confluence',
|
||||
'write:label:confluence',
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
'read:user:confluence',
|
||||
'read:task:confluence',
|
||||
'write:task:confluence',
|
||||
'delete:blogpost:confluence',
|
||||
'write:space:confluence',
|
||||
'delete:space:confluence',
|
||||
'read:space.property:confluence',
|
||||
'write:space.property:confluence',
|
||||
'read:space.permission:confluence',
|
||||
],
|
||||
requiredScopes: getScopesForService('confluence'),
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DropboxIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -41,15 +42,7 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'dropbox',
|
||||
requiredScopes: [
|
||||
'account_info.read',
|
||||
'files.metadata.read',
|
||||
'files.metadata.write',
|
||||
'files.content.read',
|
||||
'files.content.write',
|
||||
'sharing.read',
|
||||
'sharing.write',
|
||||
],
|
||||
requiredScopes: getScopesForService('dropbox'),
|
||||
placeholder: 'Select Dropbox account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GmailIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { createVersionedToolSelector, normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -79,11 +80,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
placeholder: 'Select Gmail account',
|
||||
required: true,
|
||||
},
|
||||
@@ -222,7 +219,7 @@ Return ONLY the email body - no explanations, no extra text.`,
|
||||
canonicalParamId: 'folder',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
placeholder: 'Select Gmail label/folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -303,7 +300,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'addLabelIds',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
placeholder: 'Select destination label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -329,7 +326,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'removeLabelIds',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
placeholder: 'Select label to remove',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -382,7 +379,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'manageLabelId',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
placeholder: 'Select label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleBigQueryIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -36,7 +37,7 @@ export const GoogleBigQueryBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-bigquery',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/bigquery'],
|
||||
requiredScopes: getScopesForService('google-bigquery'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleCalendarIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { createVersionedToolSelector } from '@/blocks/utils'
|
||||
@@ -43,7 +44,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-calendar',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
placeholder: 'Select Google Calendar account',
|
||||
},
|
||||
{
|
||||
@@ -64,7 +65,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
serviceId: 'google-calendar',
|
||||
selectorKey: 'google.calendar',
|
||||
selectorAllowSearch: false,
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
placeholder: 'Select calendar',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -330,7 +331,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
serviceId: 'google-calendar',
|
||||
selectorKey: 'google.calendar',
|
||||
selectorAllowSearch: false,
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
placeholder: 'Select destination calendar',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'move' },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleContactsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleContactsResponse } from '@/tools/google_contacts/types'
|
||||
@@ -37,7 +38,7 @@ export const GoogleContactsBlock: BlockConfig<GoogleContactsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-contacts',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/contacts'],
|
||||
requiredScopes: getScopesForService('google-contacts'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleDocsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleDocsResponse } from '@/tools/google_docs/types'
|
||||
@@ -36,10 +37,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-docs',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-docs'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleDriveIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -48,10 +49,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select Google Drive account',
|
||||
},
|
||||
{
|
||||
@@ -138,10 +136,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'uploadFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
mode: 'basic',
|
||||
@@ -211,10 +206,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'createFolderParentId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
mode: 'basic',
|
||||
@@ -239,10 +231,7 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'listFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a folder to list files from',
|
||||
mode: 'basic',
|
||||
@@ -299,10 +288,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'downloadFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to download',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -361,10 +347,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'getFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to get info for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -389,10 +372,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'copyFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to copy',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -423,10 +403,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'copyDestFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select destination folder (optional)',
|
||||
mode: 'basic',
|
||||
@@ -450,10 +427,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'updateFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to update',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -529,10 +503,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'trashFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to move to trash',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -557,10 +528,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'deleteFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to permanently delete',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -585,10 +553,7 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'shareFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to share',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -700,10 +665,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
canonicalParamId: 'unshareFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to remove sharing from',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -736,10 +698,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
canonicalParamId: 'listPermissionsFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select a file to list permissions for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleFormsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -38,13 +39,7 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-forms',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-forms'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleGroupsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -46,10 +47,7 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-groups',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/admin.directory.group',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-groups'),
|
||||
placeholder: 'Select Google Workspace account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleMeetIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleMeetResponse } from '@/tools/google_meet/types'
|
||||
@@ -37,10 +38,7 @@ export const GoogleMeetBlock: BlockConfig<GoogleMeetResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-meet',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-meet'),
|
||||
placeholder: 'Select Google Meet account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleSheetsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { createVersionedToolSelector } from '@/blocks/utils'
|
||||
@@ -40,10 +41,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
@@ -63,10 +61,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
@@ -339,10 +334,7 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
@@ -362,10 +354,7 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleSlidesIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import { resolveHttpsUrlFromFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
@@ -50,10 +51,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleTasksIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleTasksResponse } from '@/tools/google_tasks/types'
|
||||
@@ -38,7 +39,7 @@ export const GoogleTasksBlock: BlockConfig<GoogleTasksResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-tasks',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/tasks'],
|
||||
requiredScopes: getScopesForService('google-tasks'),
|
||||
placeholder: 'Select Google Tasks account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GoogleVaultIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -38,10 +39,7 @@ export const GoogleVaultBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-vault',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
requiredScopes: getScopesForService('google-vault'),
|
||||
placeholder: 'Select Google Vault account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { HubspotIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { HubSpotResponse } from '@/tools/hubspot/types'
|
||||
@@ -42,31 +43,7 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'hubspot',
|
||||
requiredScopes: [
|
||||
'crm.objects.contacts.read',
|
||||
'crm.objects.contacts.write',
|
||||
'crm.objects.companies.read',
|
||||
'crm.objects.companies.write',
|
||||
'crm.objects.deals.read',
|
||||
'crm.objects.deals.write',
|
||||
'crm.objects.owners.read',
|
||||
'crm.objects.users.read',
|
||||
'crm.objects.users.write',
|
||||
'crm.objects.marketing_events.read',
|
||||
'crm.objects.marketing_events.write',
|
||||
'crm.objects.line_items.read',
|
||||
'crm.objects.line_items.write',
|
||||
'crm.objects.quotes.read',
|
||||
'crm.objects.quotes.write',
|
||||
'crm.objects.appointments.read',
|
||||
'crm.objects.appointments.write',
|
||||
'crm.objects.carts.read',
|
||||
'crm.objects.carts.write',
|
||||
'crm.import',
|
||||
'crm.lists.read',
|
||||
'crm.lists.write',
|
||||
'tickets',
|
||||
],
|
||||
requiredScopes: getScopesForService('hubspot'),
|
||||
placeholder: 'Select HubSpot account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { JiraIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -64,38 +65,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'jira',
|
||||
requiredScopes: [
|
||||
'read:jira-work',
|
||||
'read:jira-user',
|
||||
'write:jira-work',
|
||||
'read:issue-event:jira',
|
||||
'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',
|
||||
'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',
|
||||
],
|
||||
requiredScopes: getScopesForService('jira'),
|
||||
placeholder: 'Select Jira account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { JiraServiceManagementIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { JsmResponse } from '@/tools/jsm/types'
|
||||
@@ -59,42 +60,7 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'jira',
|
||||
requiredScopes: [
|
||||
'read:jira-user',
|
||||
'read:jira-work',
|
||||
'write:jira-work',
|
||||
'read:project:jira',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:issue:jira',
|
||||
'read:status:jira',
|
||||
'read:user:jira',
|
||||
'read:issue-details:jira',
|
||||
'write:comment:jira',
|
||||
'read:comment:jira',
|
||||
'read:servicedesk:jira-service-management',
|
||||
'read:requesttype:jira-service-management',
|
||||
'read:request:jira-service-management',
|
||||
'write:request:jira-service-management',
|
||||
'read:request.comment:jira-service-management',
|
||||
'write:request.comment:jira-service-management',
|
||||
'read:customer:jira-service-management',
|
||||
'write:customer:jira-service-management',
|
||||
'read:servicedesk.customer:jira-service-management',
|
||||
'write:servicedesk.customer:jira-service-management',
|
||||
'read:organization:jira-service-management',
|
||||
'write:organization:jira-service-management',
|
||||
'read:servicedesk.organization:jira-service-management',
|
||||
'write:servicedesk.organization:jira-service-management',
|
||||
'read:queue:jira-service-management',
|
||||
'read:request.sla:jira-service-management',
|
||||
'read:request.status:jira-service-management',
|
||||
'write:request.status:jira-service-management',
|
||||
'read:request.participant:jira-service-management',
|
||||
'write:request.participant:jira-service-management',
|
||||
'read:request.approval:jira-service-management',
|
||||
'write:request.approval:jira-service-management',
|
||||
],
|
||||
requiredScopes: getScopesForService('jira'),
|
||||
placeholder: 'Select Jira account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LinearIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -132,7 +133,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'linear',
|
||||
requiredScopes: ['read', 'write'],
|
||||
requiredScopes: getScopesForService('linear'),
|
||||
placeholder: 'Select Linear account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LinkedInIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { LinkedInResponse } from '@/tools/linkedin/types'
|
||||
@@ -35,7 +36,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
|
||||
serviceId: 'linkedin',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
|
||||
requiredScopes: getScopesForService('linkedin'),
|
||||
placeholder: 'Select LinkedIn account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MicrosoftDataverseIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -46,13 +47,7 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
|
||||
title: 'Microsoft Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'microsoft-dataverse',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('microsoft-dataverse'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MicrosoftExcelIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { createVersionedToolSelector } from '@/blocks/utils'
|
||||
@@ -39,14 +40,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('microsoft-excel'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
@@ -366,14 +360,7 @@ export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('microsoft-excel'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MicrosoftPlannerIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
|
||||
@@ -64,15 +65,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-planner',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Group.ReadWrite.All',
|
||||
'Group.Read.All',
|
||||
'Tasks.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('microsoft-planner'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MicrosoftTeamsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -47,28 +48,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-teams',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'User.Read',
|
||||
'Chat.Read',
|
||||
'Chat.ReadWrite',
|
||||
'Chat.ReadBasic',
|
||||
'ChatMessage.Send',
|
||||
'Channel.ReadBasic.All',
|
||||
'ChannelMessage.Send',
|
||||
'ChannelMessage.Read.All',
|
||||
'ChannelMessage.ReadWrite',
|
||||
'ChannelMember.Read.All',
|
||||
'Group.Read.All',
|
||||
'Group.ReadWrite.All',
|
||||
'Team.ReadBasic.All',
|
||||
'TeamMember.Read.All',
|
||||
'offline_access',
|
||||
'Files.Read',
|
||||
'Sites.Read.All',
|
||||
],
|
||||
requiredScopes: getScopesForService('microsoft-teams'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { MicrosoftOneDriveIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -42,14 +43,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'onedrive',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
@@ -156,14 +150,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'uploadFolderId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
dependsOn: ['credential'],
|
||||
@@ -194,14 +181,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'createFolderParentId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
dependsOn: ['credential'],
|
||||
@@ -227,14 +207,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'listFolderId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a folder to list files from',
|
||||
dependsOn: ['credential'],
|
||||
@@ -274,14 +247,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'downloadFileId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.files',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
mimeType: 'file', // Exclude folders, show only files
|
||||
placeholder: 'Select a file to download',
|
||||
mode: 'basic',
|
||||
@@ -315,14 +281,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'deleteFileId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.files',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
mimeType: 'file', // Exclude folders, show only files
|
||||
placeholder: 'Select a file to delete',
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OutlookIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -42,16 +43,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'outlook',
|
||||
requiredScopes: [
|
||||
'Mail.ReadWrite',
|
||||
'Mail.ReadBasic',
|
||||
'Mail.Read',
|
||||
'Mail.Send',
|
||||
'offline_access',
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
],
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
@@ -188,7 +180,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'folder',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
placeholder: 'Select Outlook folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -234,7 +226,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'destinationId',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
placeholder: 'Select destination folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -281,7 +273,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'copyDestinationId',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
placeholder: 'Select destination folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PipedriveIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { PipedriveResponse } from '@/tools/pipedrive/types'
|
||||
@@ -48,15 +49,7 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'pipedrive',
|
||||
requiredScopes: [
|
||||
'base',
|
||||
'deals:full',
|
||||
'contacts:full',
|
||||
'leads:full',
|
||||
'activities:full',
|
||||
'mail:full',
|
||||
'projects:full',
|
||||
],
|
||||
requiredScopes: getScopesForService('pipedrive'),
|
||||
placeholder: 'Select Pipedrive account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RedditIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { RedditResponse } from '@/tools/reddit/types'
|
||||
@@ -49,24 +50,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
serviceId: 'reddit',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: [
|
||||
'identity',
|
||||
'read',
|
||||
'submit',
|
||||
'vote',
|
||||
'save',
|
||||
'edit',
|
||||
'subscribe',
|
||||
'history',
|
||||
'privatemessages',
|
||||
'account',
|
||||
'mysubreddits',
|
||||
'flair',
|
||||
'report',
|
||||
'modposts',
|
||||
'modflair',
|
||||
'modmail',
|
||||
],
|
||||
requiredScopes: getScopesForService('reddit'),
|
||||
placeholder: 'Select Reddit account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SalesforceIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { SalesforceResponse } from '@/tools/salesforce/types'
|
||||
@@ -65,7 +66,7 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'salesforce',
|
||||
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
requiredScopes: getScopesForService('salesforce'),
|
||||
placeholder: 'Select Salesforce account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { MicrosoftSharepointIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -41,15 +42,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'sharepoint',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Sites.Read.All',
|
||||
'Sites.ReadWrite.All',
|
||||
'Sites.Manage.All',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('sharepoint'),
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
@@ -68,14 +61,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
canonicalParamId: 'siteId',
|
||||
serviceId: 'sharepoint',
|
||||
selectorKey: 'sharepoint.sites',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
requiredScopes: getScopesForService('sharepoint'),
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a site',
|
||||
dependsOn: ['credential'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ShopifyIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -63,14 +64,7 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
|
||||
serviceId: 'shopify',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: [
|
||||
'write_products',
|
||||
'write_orders',
|
||||
'write_customers',
|
||||
'write_inventory',
|
||||
'read_locations',
|
||||
'write_merchant_managed_fulfillment_orders',
|
||||
],
|
||||
requiredScopes: getScopesForService('shopify'),
|
||||
placeholder: 'Select Shopify account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SlackIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -82,22 +83,7 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'slack',
|
||||
requiredScopes: [
|
||||
'channels:read',
|
||||
'channels:history',
|
||||
'groups:read',
|
||||
'groups:history',
|
||||
'chat:write',
|
||||
'chat:write.public',
|
||||
'im:write',
|
||||
'im:history',
|
||||
'im:read',
|
||||
'users:read',
|
||||
'files:write',
|
||||
'files:read',
|
||||
'canvases:write',
|
||||
'reactions:write',
|
||||
],
|
||||
requiredScopes: getScopesForService('slack'),
|
||||
placeholder: 'Select Slack workspace',
|
||||
dependsOn: ['authMethod'],
|
||||
condition: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TrelloIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
@@ -44,7 +45,7 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
serviceId: 'trello',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: ['read', 'write'],
|
||||
requiredScopes: getScopesForService('trello'),
|
||||
placeholder: 'Select Trello account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WealthboxIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { WealthboxResponse } from '@/tools/wealthbox/types'
|
||||
@@ -36,7 +37,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'wealthbox',
|
||||
requiredScopes: ['login', 'data'],
|
||||
requiredScopes: getScopesForService('wealthbox'),
|
||||
placeholder: 'Select Wealthbox account',
|
||||
required: true,
|
||||
},
|
||||
@@ -62,7 +63,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
type: 'file-selector',
|
||||
serviceId: 'wealthbox',
|
||||
selectorKey: 'wealthbox.contacts',
|
||||
requiredScopes: ['login', 'data'],
|
||||
requiredScopes: getScopesForService('wealthbox'),
|
||||
placeholder: 'Enter Contact ID',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'contactId',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WebflowIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { WebflowResponse } from '@/tools/webflow/types'
|
||||
@@ -37,7 +38,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'webflow',
|
||||
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
|
||||
requiredScopes: getScopesForService('webflow'),
|
||||
placeholder: 'Select Webflow account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WordpressIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
@@ -68,7 +69,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'wordpress',
|
||||
requiredScopes: ['global'],
|
||||
requiredScopes: getScopesForService('wordpress'),
|
||||
placeholder: 'Select WordPress account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { xIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -66,23 +67,7 @@ export const XBlock: BlockConfig = {
|
||||
serviceId: 'x',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: [
|
||||
'tweet.read',
|
||||
'tweet.write',
|
||||
'tweet.moderate.write',
|
||||
'users.read',
|
||||
'follows.read',
|
||||
'follows.write',
|
||||
'bookmark.read',
|
||||
'bookmark.write',
|
||||
'like.read',
|
||||
'like.write',
|
||||
'block.read',
|
||||
'block.write',
|
||||
'mute.read',
|
||||
'mute.write',
|
||||
'offline.access',
|
||||
],
|
||||
requiredScopes: getScopesForService('x'),
|
||||
placeholder: 'Select X account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ZoomIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ZoomResponse } from '@/tools/zoom/types'
|
||||
@@ -40,19 +41,7 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
serviceId: 'zoom',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: [
|
||||
'user:read:user',
|
||||
'meeting:write:meeting',
|
||||
'meeting:read:meeting',
|
||||
'meeting:read:list_meetings',
|
||||
'meeting:update:meeting',
|
||||
'meeting:delete:meeting',
|
||||
'meeting:read:invitation',
|
||||
'meeting:read:list_past_participants',
|
||||
'cloud_recording:read:list_user_recordings',
|
||||
'cloud_recording:read:list_recording_files',
|
||||
'cloud_recording:delete:recording_file',
|
||||
],
|
||||
requiredScopes: getScopesForService('zoom'),
|
||||
placeholder: 'Select Zoom account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -39,10 +39,10 @@ type FolderResponse = { id: string; name: string }
|
||||
type PlannerTask = { id: string; title: string }
|
||||
|
||||
const ensureCredential = (context: SelectorContext, key: SelectorKey): string => {
|
||||
if (!context.credentialId) {
|
||||
if (!context.oauthCredential) {
|
||||
throw new Error(`Missing credential for selector ${key}`)
|
||||
}
|
||||
return context.credentialId
|
||||
return context.oauthCredential
|
||||
}
|
||||
|
||||
const ensureDomain = (context: SelectorContext, key: SelectorKey): string => {
|
||||
@@ -66,9 +66,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.bases',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.bases')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -104,10 +104,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.tables',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.baseId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.baseId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.tables')
|
||||
if (!context.baseId) {
|
||||
@@ -151,9 +151,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'asana.workspaces',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'asana.workspaces')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -182,9 +182,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.objects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.objects')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -216,9 +216,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.lists')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -250,10 +250,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.datasets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.projectId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.datasets')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
|
||||
@@ -298,12 +298,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.tables',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.datasetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.credentialId && context.projectId && context.datasetId),
|
||||
Boolean(context.oauthCredential && context.projectId && context.datasetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.tables')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
|
||||
@@ -347,9 +347,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.eventTypes',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.eventTypes')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -381,9 +381,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.schedules',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.schedules')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -415,10 +415,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.spaces',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.spaces')
|
||||
const domain = ensureDomain(context, 'confluence.spaces')
|
||||
@@ -460,10 +460,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.serviceDesks',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
|
||||
const domain = ensureDomain(context, 'jsm.serviceDesks')
|
||||
@@ -505,12 +505,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.requestTypes',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.serviceDeskId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.credentialId && context.domain && context.serviceDeskId),
|
||||
Boolean(context.oauthCredential && context.domain && context.serviceDeskId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.requestTypes')
|
||||
const domain = ensureDomain(context, 'jsm.requestTypes')
|
||||
@@ -556,9 +556,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.tasks.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
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 })
|
||||
@@ -587,9 +587,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner.plans',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -618,9 +618,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.databases',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.databases')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -652,9 +652,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.pages',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.pages')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -686,9 +686,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'pipedrive.pipelines',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -720,10 +720,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.lists')
|
||||
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
|
||||
@@ -761,9 +761,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'trello.boards',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'trello.boards')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -794,9 +794,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'zoom.meetings',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'zoom.meetings')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -828,12 +828,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.channels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
workflowId: context.workflowId,
|
||||
})
|
||||
const data = await fetchJson<{ channels: SlackChannel[] }>('/api/tools/slack/channels', {
|
||||
@@ -852,12 +852,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.users',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
workflowId: context.workflowId,
|
||||
})
|
||||
const data = await fetchJson<{ users: SlackUser[] }>('/api/tools/slack/users', {
|
||||
@@ -876,12 +876,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'gmail.labels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', {
|
||||
searchParams: { credentialId: context.credentialId },
|
||||
searchParams: { credentialId: context.oauthCredential },
|
||||
})
|
||||
return (data.labels || []).map((label) => ({
|
||||
id: label.id,
|
||||
@@ -895,12 +895,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'outlook.folders',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ folders: FolderResponse[] }>('/api/tools/outlook/folders', {
|
||||
searchParams: { credentialId: context.credentialId },
|
||||
searchParams: { credentialId: context.oauthCredential },
|
||||
})
|
||||
return (data.folders || []).map((folder) => ({
|
||||
id: folder.id,
|
||||
@@ -914,13 +914,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.calendar',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
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.credentialId } }
|
||||
{ searchParams: { credentialId: context.oauthCredential } }
|
||||
)
|
||||
return (data.calendars || []).map((calendar) => ({
|
||||
id: calendar.id,
|
||||
@@ -934,11 +934,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.teams',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({ credential: context.credentialId })
|
||||
const body = JSON.stringify({ credential: context.oauthCredential })
|
||||
const data = await fetchJson<{ teams: { id: string; displayName: string }[] }>(
|
||||
'/api/tools/microsoft-teams/teams',
|
||||
{ method: 'POST', body }
|
||||
@@ -955,11 +955,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.chats',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({ credential: context.credentialId })
|
||||
const body = JSON.stringify({ credential: context.oauthCredential })
|
||||
const data = await fetchJson<{ chats: { id: string; displayName: string }[] }>(
|
||||
'/api/tools/microsoft-teams/chats',
|
||||
{ method: 'POST', body }
|
||||
@@ -976,13 +976,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.channels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
teamId: context.teamId,
|
||||
})
|
||||
const data = await fetchJson<{ channels: { id: string; displayName: string }[] }>(
|
||||
@@ -1001,14 +1001,14 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'wealthbox.contacts',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ items: { id: string; name: string }[] }>(
|
||||
'/api/tools/wealthbox/items',
|
||||
{
|
||||
searchParams: { credentialId: context.credentialId, type: 'contact' },
|
||||
searchParams: { credentialId: context.oauthCredential, type: 'contact' },
|
||||
}
|
||||
)
|
||||
return (data.items || []).map((item) => ({
|
||||
@@ -1023,9 +1023,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.sites',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.sites')
|
||||
const body = JSON.stringify({
|
||||
@@ -1069,10 +1069,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.planId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.planId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.planId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner')
|
||||
const body = JSON.stringify({
|
||||
@@ -1112,11 +1112,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.projects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.projects')
|
||||
const domain = ensureDomain(context, 'jira.projects')
|
||||
@@ -1171,12 +1171,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.issues',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.issues')
|
||||
const domain = ensureDomain(context, 'jira.issues')
|
||||
@@ -1235,9 +1235,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.teams',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.teams')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -1260,10 +1260,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.projects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.projects')
|
||||
const body = JSON.stringify({
|
||||
@@ -1290,11 +1290,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.pages',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.pages')
|
||||
const domain = ensureDomain(context, 'confluence.pages')
|
||||
@@ -1343,9 +1343,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.files',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.files')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1366,9 +1366,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.folders',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.folders')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1389,12 +1389,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.drive',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.mimeType ?? 'any',
|
||||
context.fileId ?? 'root',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.drive')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1438,10 +1438,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.sheets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
@@ -1469,10 +1469,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel.sheets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
@@ -1500,10 +1500,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1528,10 +1528,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.word',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.word')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1596,9 +1596,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.sites',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.sites')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -1621,10 +1621,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.collections',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.collections')
|
||||
if (!context.siteId) {
|
||||
@@ -1654,11 +1654,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.items',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.collectionId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.collectionId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.items')
|
||||
if (!context.collectionId) {
|
||||
|
||||
@@ -7,46 +7,16 @@ export interface SelectorResolution {
|
||||
allowSearch: boolean
|
||||
}
|
||||
|
||||
export interface SelectorResolutionArgs {
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
teamId?: string
|
||||
knowledgeBaseId?: string
|
||||
siteId?: string
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
fileId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
}
|
||||
|
||||
export function resolveSelectorForSubBlock(
|
||||
subBlock: SubBlockConfig,
|
||||
args: SelectorResolutionArgs
|
||||
context: SelectorContext
|
||||
): SelectorResolution | null {
|
||||
if (!subBlock.selectorKey) return null
|
||||
return {
|
||||
key: subBlock.selectorKey,
|
||||
context: {
|
||||
workflowId: args.workflowId,
|
||||
credentialId: args.credentialId,
|
||||
domain: args.domain,
|
||||
projectId: args.projectId,
|
||||
planId: args.planId,
|
||||
teamId: args.teamId,
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
siteId: args.siteId,
|
||||
collectionId: args.collectionId,
|
||||
spreadsheetId: args.spreadsheetId,
|
||||
fileId: args.fileId,
|
||||
baseId: args.baseId,
|
||||
datasetId: args.datasetId,
|
||||
serviceDeskId: args.serviceDeskId,
|
||||
mimeType: subBlock.mimeType,
|
||||
...context,
|
||||
mimeType: subBlock.mimeType ?? context.mimeType,
|
||||
},
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface SelectorOption {
|
||||
export interface SelectorContext {
|
||||
workspaceId?: string
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
oauthCredential?: string
|
||||
serviceId?: string
|
||||
domain?: string
|
||||
teamId?: string
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import { getSelectorDefinition, mergeOption } from '@/hooks/selectors/registry'
|
||||
import type { SelectorKey, SelectorOption, SelectorQueryArgs } from '@/hooks/selectors/types'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
|
||||
interface SelectorHookArgs extends Omit<SelectorQueryArgs, 'key'> {
|
||||
search?: string
|
||||
@@ -30,14 +31,25 @@ export function useSelectorOptionDetail(
|
||||
key: SelectorKey,
|
||||
args: SelectorHookArgs & { detailId?: string }
|
||||
) {
|
||||
const envVariables = useEnvironmentStore((s) => s.variables)
|
||||
const definition = getSelectorDefinition(key)
|
||||
|
||||
const resolvedDetailId = useMemo(() => {
|
||||
if (!args.detailId) return undefined
|
||||
if (isReference(args.detailId)) return undefined
|
||||
if (isEnvVarReference(args.detailId)) {
|
||||
const varName = extractEnvVarName(args.detailId)
|
||||
return envVariables[varName]?.value || undefined
|
||||
}
|
||||
return args.detailId
|
||||
}, [args.detailId, envVariables])
|
||||
|
||||
const queryArgs: SelectorQueryArgs = {
|
||||
key,
|
||||
context: args.context,
|
||||
detailId: args.detailId,
|
||||
detailId: resolvedDetailId,
|
||||
}
|
||||
const hasRealDetailId =
|
||||
Boolean(args.detailId) && !isReference(args.detailId!) && !isEnvVarReference(args.detailId!)
|
||||
const hasRealDetailId = Boolean(resolvedDetailId)
|
||||
const baseEnabled =
|
||||
hasRealDetailId && definition.fetchById !== undefined
|
||||
? definition.enabled
|
||||
@@ -47,7 +59,7 @@ export function useSelectorOptionDetail(
|
||||
const enabled = args.enabled ?? baseEnabled
|
||||
|
||||
const query = useQuery<SelectorOption | null>({
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'detail', args.detailId ?? 'none'],
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'detail', resolvedDetailId ?? 'none'],
|
||||
queryFn: () => definition.fetchById!(queryArgs),
|
||||
enabled,
|
||||
staleTime: definition.staleTime ?? 300_000,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { Credential } from '@/lib/oauth'
|
||||
|
||||
export interface OAuthScopeStatus {
|
||||
requiresReauthorization: boolean
|
||||
missingScopes: string[]
|
||||
extraScopes: string[]
|
||||
canonicalScopes: string[]
|
||||
grantedScopes: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract scope status from a credential
|
||||
*/
|
||||
export function getCredentialScopeStatus(credential: Credential): OAuthScopeStatus {
|
||||
return {
|
||||
requiresReauthorization: credential.requiresReauthorization || false,
|
||||
missingScopes: credential.missingScopes || [],
|
||||
extraScopes: credential.extraScopes || [],
|
||||
canonicalScopes: credential.canonicalScopes || [],
|
||||
grantedScopes: credential.scopes || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a credential needs reauthorization
|
||||
*/
|
||||
export function credentialNeedsReauth(credential: Credential): boolean {
|
||||
return credential.requiresReauthorization || false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any credentials in a list need reauthorization
|
||||
*/
|
||||
export function anyCredentialNeedsReauth(credentials: Credential[]): boolean {
|
||||
return credentials.some(credentialNeedsReauth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all credentials that need reauthorization
|
||||
*/
|
||||
export function getCredentialsNeedingReauth(credentials: Credential[]): Credential[] {
|
||||
return credentials.filter(credentialNeedsReauth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes that control token behavior but are not returned in OAuth token responses.
|
||||
* These should be ignored when validating credential scopes.
|
||||
*/
|
||||
const IGNORED_SCOPES = new Set([
|
||||
'offline_access', // Microsoft - requests refresh token
|
||||
'refresh_token', // Salesforce - requests refresh token
|
||||
'offline.access', // Airtable - requests refresh token (note: dot not underscore)
|
||||
])
|
||||
|
||||
/**
|
||||
* Compute which of the provided requiredScopes are NOT granted by the credential.
|
||||
* Note: Ignores special OAuth scopes that control token behavior (like offline_access)
|
||||
* as they are not returned in the token response's scope list even when granted.
|
||||
*/
|
||||
export function getMissingRequiredScopes(
|
||||
credential: Credential | undefined,
|
||||
requiredScopes: string[] = []
|
||||
): string[] {
|
||||
if (!credential) {
|
||||
// Filter out ignored scopes from required scopes as they're not returned by OAuth providers
|
||||
return requiredScopes.filter((s) => !IGNORED_SCOPES.has(s))
|
||||
}
|
||||
|
||||
const granted = new Set((credential.scopes || []).map((s) => s))
|
||||
const missing: string[] = []
|
||||
|
||||
for (const s of requiredScopes) {
|
||||
// Skip ignored scopes as providers don't return them in the scope list even when granted
|
||||
if (IGNORED_SCOPES.has(s)) continue
|
||||
|
||||
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
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface SelectorDisplayNameArgs {
|
||||
subBlock?: SubBlockConfig
|
||||
value: unknown
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
oauthCredential?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
@@ -31,7 +31,7 @@ export function useSelectorDisplayName({
|
||||
subBlock,
|
||||
value,
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
@@ -51,7 +51,7 @@ export function useSelectorDisplayName({
|
||||
if (!subBlock || !detailId) return null
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
@@ -69,7 +69,7 @@ export function useSelectorDisplayName({
|
||||
subBlock,
|
||||
detailId,
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
|
||||
@@ -77,6 +77,7 @@ import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
||||
const logger = createLogger('Auth')
|
||||
|
||||
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
|
||||
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
|
||||
|
||||
const validStripeKey = env.STRIPE_SECRET_KEY
|
||||
|
||||
@@ -762,7 +763,7 @@ export const auth = betterAuth({
|
||||
prompt: 'consent',
|
||||
tokenUrl: 'https://github.com/login/oauth/access_token',
|
||||
userInfoUrl: 'https://api.github.com/user',
|
||||
scopes: ['user:email', 'repo', 'read:user', 'workflow'],
|
||||
scopes: getCanonicalScopesForProvider('github-repo'),
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/github-repo`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
@@ -837,13 +838,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-email'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-email`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -879,11 +874,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-calendar'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-calendar`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -919,12 +910,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-drive'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-drive`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -960,12 +946,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-docs'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-docs`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1001,12 +982,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-sheets'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-sheets`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1043,11 +1019,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/contacts',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-contacts'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-contacts`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1083,13 +1055,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-forms'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-forms`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1125,11 +1091,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/bigquery',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-bigquery'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-bigquery`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1166,12 +1128,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-vault'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-vault`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1208,12 +1165,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/admin.directory.group',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-groups'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-groups`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1250,12 +1202,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-meet'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-meet`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1291,11 +1238,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/tasks',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('google-tasks'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-tasks`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1332,11 +1275,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('vertex-ai'),
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/vertex-ai`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1374,28 +1313,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'User.Read',
|
||||
'Chat.Read',
|
||||
'Chat.ReadWrite',
|
||||
'Chat.ReadBasic',
|
||||
'ChatMessage.Send',
|
||||
'Channel.ReadBasic.All',
|
||||
'ChannelMessage.Send',
|
||||
'ChannelMessage.Read.All',
|
||||
'ChannelMessage.ReadWrite',
|
||||
'ChannelMember.Read.All',
|
||||
'Group.Read.All',
|
||||
'Group.ReadWrite.All',
|
||||
'Team.ReadBasic.All',
|
||||
'TeamMember.Read.All',
|
||||
'offline_access',
|
||||
'Files.Read',
|
||||
'Sites.Read.All',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('microsoft-teams'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1435,7 +1353,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
|
||||
scopes: getCanonicalScopesForProvider('microsoft-excel'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1474,13 +1392,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('microsoft-dataverse'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1522,15 +1434,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Group.ReadWrite.All',
|
||||
'Group.Read.All',
|
||||
'Tasks.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('microsoft-planner'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1570,16 +1474,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Mail.ReadWrite',
|
||||
'Mail.ReadBasic',
|
||||
'Mail.Read',
|
||||
'Mail.Send',
|
||||
'offline_access',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('outlook'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1619,7 +1514,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
|
||||
scopes: getCanonicalScopesForProvider('onedrive'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1659,15 +1554,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Sites.Read.All',
|
||||
'Sites.ReadWrite.All',
|
||||
'Sites.Manage.All',
|
||||
'offline_access',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('sharepoint'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1707,7 +1594,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://app.crmworkspace.com/oauth/authorize',
|
||||
tokenUrl: 'https://app.crmworkspace.com/oauth/token',
|
||||
userInfoUrl: 'https://dummy-not-used.wealthbox.com', // Dummy URL since no user info endpoint exists
|
||||
scopes: ['login', 'data'],
|
||||
scopes: getCanonicalScopesForProvider('wealthbox'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wealthbox`,
|
||||
getUserInfo: async (_tokens) => {
|
||||
@@ -1740,15 +1627,7 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://oauth.pipedrive.com/oauth/token',
|
||||
userInfoUrl: 'https://api.pipedrive.com/v1/users/me',
|
||||
prompt: 'consent',
|
||||
scopes: [
|
||||
'base',
|
||||
'deals:full',
|
||||
'contacts:full',
|
||||
'leads:full',
|
||||
'activities:full',
|
||||
'mail:full',
|
||||
'projects:full',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('pipedrive'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/pipedrive`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1797,31 +1676,7 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://api.hubapi.com/oauth/v1/token',
|
||||
userInfoUrl: 'https://api.hubapi.com/oauth/v1/access-tokens',
|
||||
prompt: 'consent',
|
||||
scopes: [
|
||||
'crm.objects.contacts.read',
|
||||
'crm.objects.contacts.write',
|
||||
'crm.objects.companies.read',
|
||||
'crm.objects.companies.write',
|
||||
'crm.objects.deals.read',
|
||||
'crm.objects.deals.write',
|
||||
'crm.objects.owners.read',
|
||||
'crm.objects.users.read',
|
||||
'crm.objects.users.write',
|
||||
'crm.objects.marketing_events.read',
|
||||
'crm.objects.marketing_events.write',
|
||||
'crm.objects.line_items.read',
|
||||
'crm.objects.line_items.write',
|
||||
'crm.objects.quotes.read',
|
||||
'crm.objects.quotes.write',
|
||||
'crm.objects.appointments.read',
|
||||
'crm.objects.appointments.write',
|
||||
'crm.objects.carts.read',
|
||||
'crm.objects.carts.write',
|
||||
'crm.import',
|
||||
'crm.lists.read',
|
||||
'crm.lists.write',
|
||||
'tickets',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('hubspot'),
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/hubspot`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
@@ -1893,7 +1748,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize',
|
||||
tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
|
||||
userInfoUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
|
||||
scopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
scopes: getCanonicalScopesForProvider('salesforce'),
|
||||
pkce: true,
|
||||
prompt: 'consent',
|
||||
accessType: 'offline',
|
||||
@@ -1944,23 +1799,7 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://api.x.com/2/oauth2/token',
|
||||
userInfoUrl: 'https://api.x.com/2/users/me',
|
||||
accessType: 'offline',
|
||||
scopes: [
|
||||
'tweet.read',
|
||||
'tweet.write',
|
||||
'tweet.moderate.write',
|
||||
'users.read',
|
||||
'follows.read',
|
||||
'follows.write',
|
||||
'bookmark.read',
|
||||
'bookmark.write',
|
||||
'like.read',
|
||||
'like.write',
|
||||
'block.read',
|
||||
'block.write',
|
||||
'mute.read',
|
||||
'mute.write',
|
||||
'offline.access',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('x'),
|
||||
pkce: true,
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
@@ -2019,45 +1858,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
scopes: [
|
||||
'read:confluence-content.all',
|
||||
'read:confluence-space.summary',
|
||||
'read:space:confluence',
|
||||
'read:space-details:confluence',
|
||||
'write:confluence-content',
|
||||
'write:confluence-space',
|
||||
'write:confluence-file',
|
||||
'read:page:confluence',
|
||||
'write:page:confluence',
|
||||
'read:comment:confluence',
|
||||
'read:content:confluence',
|
||||
'write:comment:confluence',
|
||||
'delete:comment:confluence',
|
||||
'read:attachment:confluence',
|
||||
'write:attachment:confluence',
|
||||
'delete:attachment:confluence',
|
||||
'delete:page:confluence',
|
||||
'read:label:confluence',
|
||||
'write:label:confluence',
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
'read:user:confluence',
|
||||
'read:task:confluence',
|
||||
'write:task:confluence',
|
||||
'delete:blogpost:confluence',
|
||||
'write:space:confluence',
|
||||
'delete:space:confluence',
|
||||
'read:space.property:confluence',
|
||||
'write:space.property:confluence',
|
||||
'read:space.permission:confluence',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('confluence'),
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -2109,67 +1910,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
scopes: [
|
||||
'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',
|
||||
// Jira Service Management scopes
|
||||
'read:servicedesk:jira-service-management',
|
||||
'read:requesttype:jira-service-management',
|
||||
'read:request:jira-service-management',
|
||||
'write:request:jira-service-management',
|
||||
'read:request.comment:jira-service-management',
|
||||
'write:request.comment:jira-service-management',
|
||||
'read:customer:jira-service-management',
|
||||
'write:customer:jira-service-management',
|
||||
'read:servicedesk.customer:jira-service-management',
|
||||
'write:servicedesk.customer:jira-service-management',
|
||||
'read:organization:jira-service-management',
|
||||
'write:organization:jira-service-management',
|
||||
'read:servicedesk.organization:jira-service-management',
|
||||
'write:servicedesk.organization:jira-service-management',
|
||||
'read:organization.user:jira-service-management',
|
||||
'write:organization.user:jira-service-management',
|
||||
'read:organization.property:jira-service-management',
|
||||
'write:organization.property:jira-service-management',
|
||||
'read:organization.profile:jira-service-management',
|
||||
'write:organization.profile:jira-service-management',
|
||||
'read:queue:jira-service-management',
|
||||
'read:request.sla:jira-service-management',
|
||||
'read:request.status:jira-service-management',
|
||||
'write:request.status:jira-service-management',
|
||||
'read:request.participant:jira-service-management',
|
||||
'write:request.participant:jira-service-management',
|
||||
'read:request.approval:jira-service-management',
|
||||
'write:request.approval:jira-service-management',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('jira'),
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -2221,13 +1962,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://airtable.com/oauth2/v1/authorize',
|
||||
tokenUrl: 'https://airtable.com/oauth2/v1/token',
|
||||
userInfoUrl: 'https://api.airtable.com/v0/meta/whoami',
|
||||
scopes: [
|
||||
'data.records:read',
|
||||
'data.records:write',
|
||||
'schema.bases:read',
|
||||
'user.email:read',
|
||||
'webhook:manage',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('airtable'),
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -2327,24 +2062,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://www.reddit.com/api/v1/authorize?duration=permanent',
|
||||
tokenUrl: 'https://www.reddit.com/api/v1/access_token',
|
||||
userInfoUrl: 'https://oauth.reddit.com/api/v1/me',
|
||||
scopes: [
|
||||
'identity',
|
||||
'read',
|
||||
'submit',
|
||||
'vote',
|
||||
'save',
|
||||
'edit',
|
||||
'subscribe',
|
||||
'history',
|
||||
'privatemessages',
|
||||
'account',
|
||||
'mysubreddits',
|
||||
'flair',
|
||||
'report',
|
||||
'modposts',
|
||||
'modflair',
|
||||
'modmail',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('reddit'),
|
||||
responseType: 'code',
|
||||
pkce: false,
|
||||
accessType: 'offline',
|
||||
@@ -2394,7 +2112,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.LINEAR_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://linear.app/oauth/authorize',
|
||||
tokenUrl: 'https://api.linear.app/oauth/token',
|
||||
scopes: ['read', 'write'],
|
||||
scopes: getCanonicalScopesForProvider('linear'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/linear`,
|
||||
pkce: true,
|
||||
@@ -2466,17 +2184,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.ATTIO_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://app.attio.com/authorize',
|
||||
tokenUrl: 'https://app.attio.com/oauth/token',
|
||||
scopes: [
|
||||
'record_permission:read-write',
|
||||
'object_configuration:read-write',
|
||||
'list_configuration:read-write',
|
||||
'list_entry:read-write',
|
||||
'note:read-write',
|
||||
'task:read-write',
|
||||
'comment:read-write',
|
||||
'user_management:read',
|
||||
'webhook:read-write',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('attio'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/attio`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -2529,15 +2237,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.DROPBOX_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://www.dropbox.com/oauth2/authorize',
|
||||
tokenUrl: 'https://api.dropboxapi.com/oauth2/token',
|
||||
scopes: [
|
||||
'account_info.read',
|
||||
'files.metadata.read',
|
||||
'files.metadata.write',
|
||||
'files.content.read',
|
||||
'files.content.write',
|
||||
'sharing.read',
|
||||
'sharing.write',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('dropbox'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/dropbox`,
|
||||
pkce: true,
|
||||
@@ -2593,7 +2293,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://app.asana.com/-/oauth_authorize',
|
||||
tokenUrl: 'https://app.asana.com/-/oauth_token',
|
||||
userInfoUrl: 'https://app.asana.com/api/1.0/users/me',
|
||||
scopes: ['default'],
|
||||
scopes: getCanonicalScopesForProvider('asana'),
|
||||
responseType: 'code',
|
||||
pkce: false,
|
||||
accessType: 'offline',
|
||||
@@ -2646,23 +2346,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
||||
tokenUrl: 'https://slack.com/api/oauth.v2.access',
|
||||
userInfoUrl: 'https://slack.com/api/users.identity',
|
||||
scopes: [
|
||||
// Bot token scopes only - app acts as a bot user
|
||||
'channels:read',
|
||||
'channels:history',
|
||||
'groups:read',
|
||||
'groups:history',
|
||||
'chat:write',
|
||||
'chat:write.public',
|
||||
'im:write',
|
||||
'im:history',
|
||||
'im:read',
|
||||
'users:read',
|
||||
'files:write',
|
||||
'files:read',
|
||||
'canvases:write',
|
||||
'reactions:write',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('slack'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
@@ -2722,7 +2406,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://webflow.com/oauth/authorize',
|
||||
tokenUrl: 'https://api.webflow.com/oauth/access_token',
|
||||
userInfoUrl: 'https://api.webflow.com/v2/token/introspect',
|
||||
scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write', 'forms:read'],
|
||||
scopes: getCanonicalScopesForProvider('webflow'),
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -2772,7 +2456,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization',
|
||||
tokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
userInfoUrl: 'https://api.linkedin.com/v2/userinfo',
|
||||
scopes: ['profile', 'openid', 'email', 'w_member_social'],
|
||||
scopes: getCanonicalScopesForProvider('linkedin'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
@@ -2822,19 +2506,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://zoom.us/oauth/authorize',
|
||||
tokenUrl: 'https://zoom.us/oauth/token',
|
||||
userInfoUrl: 'https://api.zoom.us/v2/users/me',
|
||||
scopes: [
|
||||
'user:read:user',
|
||||
'meeting:write:meeting',
|
||||
'meeting:read:meeting',
|
||||
'meeting:read:list_meetings',
|
||||
'meeting:update:meeting',
|
||||
'meeting:delete:meeting',
|
||||
'meeting:read:invitation',
|
||||
'meeting:read:list_past_participants',
|
||||
'cloud_recording:read:list_user_recordings',
|
||||
'cloud_recording:read:list_recording_files',
|
||||
'cloud_recording:delete:recording_file',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('zoom'),
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -2886,25 +2558,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://accounts.spotify.com/authorize',
|
||||
tokenUrl: 'https://accounts.spotify.com/api/token',
|
||||
userInfoUrl: 'https://api.spotify.com/v1/me',
|
||||
scopes: [
|
||||
'user-read-private',
|
||||
'user-read-email',
|
||||
'user-library-read',
|
||||
'user-library-modify',
|
||||
'playlist-read-private',
|
||||
'playlist-read-collaborative',
|
||||
'playlist-modify-public',
|
||||
'playlist-modify-private',
|
||||
'user-read-playback-state',
|
||||
'user-modify-playback-state',
|
||||
'user-read-currently-playing',
|
||||
'user-read-recently-played',
|
||||
'user-top-read',
|
||||
'user-follow-read',
|
||||
'user-follow-modify',
|
||||
'user-read-playback-position',
|
||||
'ugc-image-upload',
|
||||
],
|
||||
scopes: getCanonicalScopesForProvider('spotify'),
|
||||
responseType: 'code',
|
||||
authentication: 'basic',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/spotify`,
|
||||
@@ -2953,7 +2607,7 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://public-api.wordpress.com/oauth2/authorize',
|
||||
tokenUrl: 'https://public-api.wordpress.com/oauth2/token',
|
||||
userInfoUrl: 'https://public-api.wordpress.com/rest/v1.1/me',
|
||||
scopes: ['global'],
|
||||
scopes: getCanonicalScopesForProvider('wordpress'),
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wordpress`,
|
||||
@@ -3000,7 +2654,7 @@ export const auth = betterAuth({
|
||||
clientId: env.CALCOM_CLIENT_ID as string,
|
||||
authorizationUrl: 'https://app.cal.com/auth/oauth2/authorize',
|
||||
tokenUrl: 'https://app.cal.com/api/auth/oauth/token',
|
||||
scopes: [],
|
||||
scopes: getCanonicalScopesForProvider('calcom'),
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
|
||||
@@ -63,6 +63,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GmailIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
@@ -75,6 +77,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleDriveIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
@@ -86,6 +90,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleDocsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
@@ -97,6 +103,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleSheetsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
@@ -121,7 +129,11 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-calendar',
|
||||
icon: GoogleCalendarIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
},
|
||||
'google-contacts': {
|
||||
name: 'Google Contacts',
|
||||
@@ -129,7 +141,11 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-contacts',
|
||||
icon: GoogleContactsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: ['https://www.googleapis.com/auth/contacts'],
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/contacts',
|
||||
],
|
||||
},
|
||||
'google-bigquery': {
|
||||
name: 'Google BigQuery',
|
||||
@@ -137,7 +153,11 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-bigquery',
|
||||
icon: GoogleBigQueryIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: ['https://www.googleapis.com/auth/bigquery'],
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/bigquery',
|
||||
],
|
||||
},
|
||||
'google-tasks': {
|
||||
name: 'Google Tasks',
|
||||
@@ -145,7 +165,11 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-tasks',
|
||||
icon: GoogleTasksIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: ['https://www.googleapis.com/auth/tasks'],
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/tasks',
|
||||
],
|
||||
},
|
||||
'google-vault': {
|
||||
name: 'Google Vault',
|
||||
@@ -154,6 +178,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
@@ -165,6 +191,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleGroupsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/admin.directory.group',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
@@ -176,6 +204,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
icon: GoogleMeetIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly',
|
||||
],
|
||||
@@ -186,7 +216,11 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'vertex-ai',
|
||||
icon: VertexIcon,
|
||||
baseProviderIcon: VertexIcon,
|
||||
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultService: 'gmail',
|
||||
@@ -671,7 +705,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'webflow',
|
||||
icon: WebflowIcon,
|
||||
baseProviderIcon: WebflowIcon,
|
||||
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write'],
|
||||
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write', 'forms:read'],
|
||||
},
|
||||
},
|
||||
defaultService: 'webflow',
|
||||
|
||||
@@ -122,14 +122,6 @@ export interface OAuthServiceMetadata {
|
||||
baseProvider: string
|
||||
}
|
||||
|
||||
export interface ScopeEvaluation {
|
||||
canonicalScopes: string[]
|
||||
grantedScopes: string[]
|
||||
missingScopes: string[]
|
||||
extraScopes: string[]
|
||||
requiresReauthorization: boolean
|
||||
}
|
||||
|
||||
export interface Credential {
|
||||
id: string
|
||||
name: string
|
||||
@@ -138,10 +130,6 @@ export interface Credential {
|
||||
lastUsed?: string
|
||||
isDefault?: boolean
|
||||
scopes?: string[]
|
||||
canonicalScopes?: string[]
|
||||
missingScopes?: string[]
|
||||
extraScopes?: string[]
|
||||
requiresReauthorization?: boolean
|
||||
}
|
||||
|
||||
export interface ProviderConfig {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { OAuthProvider, OAuthServiceMetadata } from './types'
|
||||
import {
|
||||
evaluateScopeCoverage,
|
||||
getAllOAuthServices,
|
||||
getCanonicalScopesForProvider,
|
||||
getMissingRequiredScopes,
|
||||
getProviderIdFromServiceId,
|
||||
getScopesForService,
|
||||
getServiceByProviderAndId,
|
||||
getServiceConfigByProviderId,
|
||||
normalizeScopes,
|
||||
parseProvider,
|
||||
} from './utils'
|
||||
|
||||
@@ -361,209 +361,6 @@ describe('getCanonicalScopesForProvider', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeScopes', () => {
|
||||
it.concurrent('should remove duplicates from scope array', () => {
|
||||
const scopes = ['scope1', 'scope2', 'scope1', 'scope3', 'scope2']
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized.length).toBe(3)
|
||||
expect(normalized).toContain('scope1')
|
||||
expect(normalized).toContain('scope2')
|
||||
expect(normalized).toContain('scope3')
|
||||
})
|
||||
|
||||
it.concurrent('should trim whitespace from scopes', () => {
|
||||
const scopes = [' scope1 ', 'scope2', ' scope3 ']
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized).toEqual(['scope1', 'scope2', 'scope3'])
|
||||
})
|
||||
|
||||
it.concurrent('should remove empty strings', () => {
|
||||
const scopes = ['scope1', '', 'scope2', ' ', 'scope3']
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized.length).toBe(3)
|
||||
expect(normalized).toEqual(['scope1', 'scope2', 'scope3'])
|
||||
})
|
||||
|
||||
it.concurrent('should handle empty array', () => {
|
||||
const normalized = normalizeScopes([])
|
||||
|
||||
expect(Array.isArray(normalized)).toBe(true)
|
||||
expect(normalized.length).toBe(0)
|
||||
})
|
||||
|
||||
it.concurrent('should handle array with only empty strings', () => {
|
||||
const normalized = normalizeScopes(['', ' ', ' '])
|
||||
|
||||
expect(Array.isArray(normalized)).toBe(true)
|
||||
expect(normalized.length).toBe(0)
|
||||
})
|
||||
|
||||
it.concurrent('should preserve order of first occurrence', () => {
|
||||
const scopes = ['scope3', 'scope1', 'scope2', 'scope1', 'scope3']
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized).toEqual(['scope3', 'scope1', 'scope2'])
|
||||
})
|
||||
|
||||
it.concurrent('should handle scopes with special characters', () => {
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
]
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized.length).toBe(2)
|
||||
expect(normalized).toContain('https://www.googleapis.com/auth/gmail.send')
|
||||
expect(normalized).toContain('https://www.googleapis.com/auth/gmail.modify')
|
||||
})
|
||||
|
||||
it.concurrent('should handle single scope', () => {
|
||||
const normalized = normalizeScopes(['scope1'])
|
||||
|
||||
expect(normalized).toEqual(['scope1'])
|
||||
})
|
||||
|
||||
it.concurrent('should handle scopes with mixed whitespace', () => {
|
||||
const scopes = ['scope1', '\tscope2\t', '\nscope3\n', ' scope1 ']
|
||||
const normalized = normalizeScopes(scopes)
|
||||
|
||||
expect(normalized.length).toBe(3)
|
||||
expect(normalized).toContain('scope1')
|
||||
expect(normalized).toContain('scope2')
|
||||
expect(normalized).toContain('scope3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('evaluateScopeCoverage', () => {
|
||||
it.concurrent('should identify missing scopes', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
])
|
||||
|
||||
expect(evaluation.missingScopes.length).toBeGreaterThan(0)
|
||||
expect(evaluation.missingScopes).toContain('https://www.googleapis.com/auth/gmail.modify')
|
||||
expect(evaluation.requiresReauthorization).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should identify extra scopes', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
])
|
||||
|
||||
expect(evaluation.extraScopes.length).toBe(1)
|
||||
expect(evaluation.extraScopes).toContain('https://www.googleapis.com/auth/calendar')
|
||||
})
|
||||
|
||||
it.concurrent('should return no missing scopes when all are present', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
])
|
||||
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should normalize granted scopes before evaluation', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [
|
||||
' https://www.googleapis.com/auth/gmail.send ',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
])
|
||||
|
||||
expect(evaluation.grantedScopes.length).toBe(3)
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle empty granted scopes', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [])
|
||||
|
||||
expect(evaluation.grantedScopes.length).toBe(0)
|
||||
expect(evaluation.missingScopes.length).toBeGreaterThan(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should return correct structure', () => {
|
||||
const evaluation = evaluateScopeCoverage('google-email', [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
])
|
||||
|
||||
expect(evaluation).toHaveProperty('canonicalScopes')
|
||||
expect(evaluation).toHaveProperty('grantedScopes')
|
||||
expect(evaluation).toHaveProperty('missingScopes')
|
||||
expect(evaluation).toHaveProperty('extraScopes')
|
||||
expect(evaluation).toHaveProperty('requiresReauthorization')
|
||||
|
||||
expect(Array.isArray(evaluation.canonicalScopes)).toBe(true)
|
||||
expect(Array.isArray(evaluation.grantedScopes)).toBe(true)
|
||||
expect(Array.isArray(evaluation.missingScopes)).toBe(true)
|
||||
expect(Array.isArray(evaluation.extraScopes)).toBe(true)
|
||||
expect(typeof evaluation.requiresReauthorization).toBe('boolean')
|
||||
})
|
||||
|
||||
it.concurrent('should handle provider with no scopes', () => {
|
||||
const evaluation = evaluateScopeCoverage('notion', [])
|
||||
|
||||
expect(evaluation.canonicalScopes.length).toBe(0)
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle provider with no scopes but granted scopes present', () => {
|
||||
const evaluation = evaluateScopeCoverage('notion', ['some.scope', 'another.scope'])
|
||||
|
||||
expect(evaluation.canonicalScopes.length).toBe(0)
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.extraScopes.length).toBe(2)
|
||||
expect(evaluation.extraScopes).toContain('some.scope')
|
||||
expect(evaluation.extraScopes).toContain('another.scope')
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle invalid provider', () => {
|
||||
const evaluation = evaluateScopeCoverage('invalid-provider', ['scope1', 'scope2'])
|
||||
|
||||
expect(evaluation.canonicalScopes.length).toBe(0)
|
||||
expect(evaluation.grantedScopes.length).toBe(2)
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.extraScopes.length).toBe(2)
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should work with Microsoft services', () => {
|
||||
const evaluation = evaluateScopeCoverage('outlook', [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Mail.ReadWrite',
|
||||
'Mail.Send',
|
||||
])
|
||||
|
||||
expect(evaluation.canonicalScopes.length).toBeGreaterThan(0)
|
||||
expect(evaluation.missingScopes.length).toBeGreaterThan(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should handle exact match with no extra or missing scopes', () => {
|
||||
const canonicalScopes = getCanonicalScopesForProvider('linear')
|
||||
const evaluation = evaluateScopeCoverage('linear', [...canonicalScopes])
|
||||
|
||||
expect(evaluation.missingScopes.length).toBe(0)
|
||||
expect(evaluation.extraScopes.length).toBe(0)
|
||||
expect(evaluation.requiresReauthorization).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseProvider', () => {
|
||||
it.concurrent('should parse simple provider without hyphen', () => {
|
||||
const config = parseProvider('slack' as OAuthProvider)
|
||||
@@ -802,3 +599,111 @@ describe('parseProvider', () => {
|
||||
expect(config.featureType).toBe('sharepoint')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getScopesForService', () => {
|
||||
it.concurrent('should return scopes for a valid serviceId', () => {
|
||||
const scopes = getScopesForService('gmail')
|
||||
|
||||
expect(Array.isArray(scopes)).toBe(true)
|
||||
expect(scopes.length).toBeGreaterThan(0)
|
||||
expect(scopes).toContain('https://www.googleapis.com/auth/gmail.send')
|
||||
})
|
||||
|
||||
it.concurrent('should return empty array for unknown serviceId', () => {
|
||||
const scopes = getScopesForService('nonexistent-service')
|
||||
|
||||
expect(Array.isArray(scopes)).toBe(true)
|
||||
expect(scopes.length).toBe(0)
|
||||
})
|
||||
|
||||
it.concurrent('should return new array instance (not reference)', () => {
|
||||
const scopes1 = getScopesForService('gmail')
|
||||
const scopes2 = getScopesForService('gmail')
|
||||
|
||||
expect(scopes1).not.toBe(scopes2)
|
||||
expect(scopes1).toEqual(scopes2)
|
||||
})
|
||||
|
||||
it.concurrent('should work for Microsoft services', () => {
|
||||
const scopes = getScopesForService('outlook')
|
||||
|
||||
expect(scopes.length).toBeGreaterThan(0)
|
||||
expect(scopes).toContain('Mail.ReadWrite')
|
||||
})
|
||||
|
||||
it.concurrent('should return empty array for empty string', () => {
|
||||
const scopes = getScopesForService('')
|
||||
|
||||
expect(Array.isArray(scopes)).toBe(true)
|
||||
expect(scopes.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMissingRequiredScopes', () => {
|
||||
it.concurrent('should return empty array when all scopes are granted', () => {
|
||||
const credential = { scopes: ['read', 'write'] }
|
||||
const missing = getMissingRequiredScopes(credential, ['read', 'write'])
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it.concurrent('should return missing scopes', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential, ['read', 'write'])
|
||||
|
||||
expect(missing).toEqual(['write'])
|
||||
})
|
||||
|
||||
it.concurrent('should return all required scopes when credential is undefined', () => {
|
||||
const missing = getMissingRequiredScopes(undefined, ['read', 'write'])
|
||||
|
||||
expect(missing).toEqual(['read', 'write'])
|
||||
})
|
||||
|
||||
it.concurrent('should return all required scopes when credential has undefined scopes', () => {
|
||||
const missing = getMissingRequiredScopes({ scopes: undefined }, ['read', 'write'])
|
||||
|
||||
expect(missing).toEqual(['read', 'write'])
|
||||
})
|
||||
|
||||
it.concurrent('should ignore offline_access in required scopes', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential, ['read', 'offline_access'])
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it.concurrent('should ignore refresh_token in required scopes', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential, ['read', 'refresh_token'])
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it.concurrent('should ignore offline.access in required scopes', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential, ['read', 'offline.access'])
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it.concurrent('should filter ignored scopes even when credential is undefined', () => {
|
||||
const missing = getMissingRequiredScopes(undefined, ['read', 'offline_access', 'refresh_token'])
|
||||
|
||||
expect(missing).toEqual(['read'])
|
||||
})
|
||||
|
||||
it.concurrent('should return empty array when requiredScopes is empty', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential, [])
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it.concurrent('should return empty array when requiredScopes defaults to empty', () => {
|
||||
const credential = { scopes: ['read'] }
|
||||
const missing = getMissingRequiredScopes(credential)
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,9 +4,411 @@ import type {
|
||||
OAuthServiceConfig,
|
||||
OAuthServiceMetadata,
|
||||
ProviderConfig,
|
||||
ScopeEvaluation,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Centralized human-readable descriptions for OAuth scopes.
|
||||
* Used by the OAuth Required Modal and available for any UI that needs to display scope info.
|
||||
*/
|
||||
export const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
// Google scopes
|
||||
'https://www.googleapis.com/auth/gmail.send': 'Send emails',
|
||||
'https://www.googleapis.com/auth/gmail.labels': 'View and manage email labels',
|
||||
'https://www.googleapis.com/auth/gmail.modify': 'View and manage email messages',
|
||||
'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files',
|
||||
'https://www.googleapis.com/auth/drive': 'Access all Google Drive files',
|
||||
'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
|
||||
'https://www.googleapis.com/auth/contacts': 'View and manage Google Contacts',
|
||||
'https://www.googleapis.com/auth/tasks': 'Create, read, update, and delete Google Tasks',
|
||||
'https://www.googleapis.com/auth/userinfo.email': 'View email address',
|
||||
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
|
||||
'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
|
||||
'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery',
|
||||
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
|
||||
'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member':
|
||||
'Manage Google Workspace group memberships',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.readonly': 'View Google Workspace groups',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member.readonly':
|
||||
'View Google Workspace group memberships',
|
||||
'https://www.googleapis.com/auth/meetings.space.created':
|
||||
'Create and manage Google Meet meeting spaces',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly':
|
||||
'View Google Meet meeting space details',
|
||||
'https://www.googleapis.com/auth/cloud-platform':
|
||||
'Full access to Google Cloud resources for Vertex AI',
|
||||
|
||||
// Confluence scopes
|
||||
'read:confluence-content.all': 'Read all Confluence content',
|
||||
'read:confluence-space.summary': 'Read Confluence space information',
|
||||
'read:space:confluence': 'View Confluence spaces',
|
||||
'read:space-details:confluence': 'View detailed Confluence space information',
|
||||
'write:confluence-content': 'Create and edit Confluence pages',
|
||||
'write:confluence-space': 'Manage Confluence spaces',
|
||||
'write:confluence-file': 'Upload files to Confluence',
|
||||
'read:content:confluence': 'Read Confluence content',
|
||||
'read:page:confluence': 'View Confluence pages',
|
||||
'write:page:confluence': 'Create and update Confluence pages',
|
||||
'read:comment:confluence': 'View comments on Confluence pages',
|
||||
'write:comment:confluence': 'Create and update comments',
|
||||
'delete:comment:confluence': 'Delete comments from Confluence pages',
|
||||
'read:attachment:confluence': 'View attachments on Confluence pages',
|
||||
'write:attachment:confluence': 'Upload and manage attachments',
|
||||
'delete:attachment:confluence': 'Delete attachments from Confluence pages',
|
||||
'delete:page:confluence': 'Delete Confluence pages',
|
||||
'read:label:confluence': 'View labels on Confluence content',
|
||||
'write:label:confluence': 'Add and remove labels',
|
||||
'search:confluence': 'Search Confluence content',
|
||||
'readonly:content.attachment:confluence': 'View attachments',
|
||||
'read:blogpost:confluence': 'View Confluence blog posts',
|
||||
'write:blogpost:confluence': 'Create and update Confluence blog posts',
|
||||
'read:content.property:confluence': 'View properties on Confluence content',
|
||||
'write:content.property:confluence': 'Create and manage content properties',
|
||||
'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)',
|
||||
'read:content.metadata:confluence': 'View content metadata (required for ancestors)',
|
||||
'read:user:confluence': 'View Confluence user profiles',
|
||||
'read:task:confluence': 'View Confluence inline tasks',
|
||||
'write:task:confluence': 'Update Confluence inline tasks',
|
||||
'delete:blogpost:confluence': 'Delete Confluence blog posts',
|
||||
'write:space:confluence': 'Create and update Confluence spaces',
|
||||
'delete:space:confluence': 'Delete Confluence spaces',
|
||||
'read:space.property:confluence': 'View Confluence space properties',
|
||||
'write:space.property:confluence': 'Create and manage space properties',
|
||||
'read:space.permission:confluence': 'View Confluence space permissions',
|
||||
|
||||
// Common scopes
|
||||
'read:me': 'Read profile information',
|
||||
offline_access: 'Access account when not using the application',
|
||||
openid: 'Standard authentication',
|
||||
profile: 'Access profile information',
|
||||
email: 'Access email address',
|
||||
|
||||
// Notion scopes
|
||||
'database.read': 'Read database',
|
||||
'database.write': 'Write to database',
|
||||
'projects.read': 'Read projects',
|
||||
'page.read': 'Read Notion pages',
|
||||
'page.write': 'Write to Notion pages',
|
||||
'workspace.content': 'Read Notion content',
|
||||
'workspace.name': 'Read Notion workspace name',
|
||||
'workspace.read': 'Read Notion workspace',
|
||||
'workspace.write': 'Write to Notion workspace',
|
||||
'user.email:read': 'Read email address',
|
||||
|
||||
// GitHub scopes
|
||||
repo: 'Access repositories',
|
||||
workflow: 'Manage repository workflows',
|
||||
'read:user': 'Read public user information',
|
||||
'user:email': 'Access email address',
|
||||
|
||||
// X (Twitter) scopes
|
||||
'tweet.read': 'Read tweets and timeline',
|
||||
'tweet.write': 'Post and delete tweets',
|
||||
'tweet.moderate.write': 'Hide and unhide replies to tweets',
|
||||
'users.read': 'Read user profiles and account information',
|
||||
'follows.read': 'View followers and following lists',
|
||||
'follows.write': 'Follow and unfollow users',
|
||||
'bookmark.read': 'View bookmarked tweets',
|
||||
'bookmark.write': 'Add and remove bookmarks',
|
||||
'like.read': 'View liked tweets and liking users',
|
||||
'like.write': 'Like and unlike tweets',
|
||||
'block.read': 'View blocked users',
|
||||
'block.write': 'Block and unblock users',
|
||||
'mute.read': 'View muted users',
|
||||
'mute.write': 'Mute and unmute users',
|
||||
'offline.access': 'Access account when not using the application',
|
||||
|
||||
// Airtable scopes
|
||||
'data.records:read': 'Read records',
|
||||
'data.records:write': 'Write to records',
|
||||
'schema.bases:read': 'View bases and tables',
|
||||
'webhook:manage': 'Manage webhooks',
|
||||
|
||||
// Jira scopes
|
||||
'read:jira-user': 'Read Jira user',
|
||||
'read:jira-work': 'Read Jira work',
|
||||
'write:jira-work': 'Write to Jira work',
|
||||
'manage:jira-webhook': 'Register and manage Jira webhooks',
|
||||
'read:webhook:jira': 'View Jira webhooks',
|
||||
'write:webhook:jira': 'Create and update Jira webhooks',
|
||||
'delete:webhook:jira': 'Delete Jira webhooks',
|
||||
'read:issue-event:jira': 'Read Jira issue events',
|
||||
'write:issue:jira': 'Write to Jira issues',
|
||||
'read:project:jira': 'Read Jira projects',
|
||||
'read:issue-type:jira': 'Read Jira issue types',
|
||||
'read:issue-meta:jira': 'Read Jira issue meta',
|
||||
'read:issue-security-level:jira': 'Read Jira issue security level',
|
||||
'read:issue.vote:jira': 'Read Jira issue votes',
|
||||
'read:issue.changelog:jira': 'Read Jira issue changelog',
|
||||
'read:avatar:jira': 'Read Jira avatar',
|
||||
'read:issue:jira': 'Read Jira issues',
|
||||
'read:status:jira': 'Read Jira status',
|
||||
'read:user:jira': 'Read Jira user',
|
||||
'read:field-configuration:jira': 'Read Jira field configuration',
|
||||
'read:issue-details:jira': 'Read Jira issue details',
|
||||
'read:field:jira': 'Read Jira field configurations',
|
||||
'read:jql:jira': 'Use JQL to filter Jira issues',
|
||||
'read:comment.property:jira': 'Read Jira comment properties',
|
||||
'read:issue.property:jira': 'Read Jira issue properties',
|
||||
'delete:issue:jira': 'Delete Jira issues',
|
||||
'write:comment:jira': 'Add and update comments on Jira issues',
|
||||
'read:comment:jira': 'Read comments on Jira issues',
|
||||
'delete:comment:jira': 'Delete comments from Jira issues',
|
||||
'read:attachment:jira': 'Read attachments from Jira issues',
|
||||
'delete:attachment:jira': 'Delete attachments from Jira issues',
|
||||
'write:issue-worklog:jira': 'Add and update worklog entries on Jira issues',
|
||||
'read:issue-worklog:jira': 'Read worklog entries from Jira issues',
|
||||
'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues',
|
||||
'write:issue-link:jira': 'Create links between Jira issues',
|
||||
'delete:issue-link:jira': 'Delete links between Jira issues',
|
||||
|
||||
// Jira Service Management scopes
|
||||
'read:servicedesk:jira-service-management': 'View service desks and their settings',
|
||||
'read:requesttype:jira-service-management': 'View request types available in service desks',
|
||||
'read:request:jira-service-management': 'View customer requests in service desks',
|
||||
'write:request:jira-service-management': 'Create customer requests in service desks',
|
||||
'read:request.comment:jira-service-management': 'View comments on customer requests',
|
||||
'write:request.comment:jira-service-management': 'Add comments to customer requests',
|
||||
'read:customer:jira-service-management': 'View customer information',
|
||||
'write:customer:jira-service-management': 'Create and manage customers',
|
||||
'read:servicedesk.customer:jira-service-management': 'View customers linked to service desks',
|
||||
'write:servicedesk.customer:jira-service-management':
|
||||
'Add and remove customers from service desks',
|
||||
'read:organization:jira-service-management': 'View organizations',
|
||||
'write:organization:jira-service-management': 'Create and manage organizations',
|
||||
'read:servicedesk.organization:jira-service-management':
|
||||
'View organizations linked to service desks',
|
||||
'write:servicedesk.organization:jira-service-management':
|
||||
'Add and remove organizations from service desks',
|
||||
'read:organization.user:jira-service-management': 'View users in organizations',
|
||||
'write:organization.user:jira-service-management': 'Add and remove users from organizations',
|
||||
'read:organization.property:jira-service-management': 'View organization properties',
|
||||
'write:organization.property:jira-service-management':
|
||||
'Create and manage organization properties',
|
||||
'read:organization.profile:jira-service-management': 'View organization profiles',
|
||||
'write:organization.profile:jira-service-management': 'Update organization profiles',
|
||||
'read:queue:jira-service-management': 'View service desk queues and their issues',
|
||||
'read:request.sla:jira-service-management': 'View SLA information for customer requests',
|
||||
'read:request.status:jira-service-management': 'View status of customer requests',
|
||||
'write:request.status:jira-service-management': 'Transition customer request status',
|
||||
'read:request.participant:jira-service-management': 'View participants on customer requests',
|
||||
'write:request.participant:jira-service-management':
|
||||
'Add and remove participants from customer requests',
|
||||
'read:request.approval:jira-service-management': 'View approvals on customer requests',
|
||||
'write:request.approval:jira-service-management': 'Approve or decline customer requests',
|
||||
|
||||
// Microsoft scopes
|
||||
'User.Read': 'Read Microsoft user',
|
||||
'Chat.Read': 'Read Microsoft chats',
|
||||
'Chat.ReadWrite': 'Write to Microsoft chats',
|
||||
'Chat.ReadBasic': 'Read Microsoft chats',
|
||||
'ChatMessage.Send': 'Send chat messages',
|
||||
'Channel.ReadBasic.All': 'Read Microsoft channels',
|
||||
'ChannelMessage.Send': 'Write to Microsoft channels',
|
||||
'ChannelMessage.Read.All': 'Read Microsoft channels',
|
||||
'ChannelMessage.ReadWrite': 'Read and write to Microsoft channels',
|
||||
'ChannelMember.Read.All': 'Read team channel members',
|
||||
'Group.Read.All': 'Read Microsoft groups',
|
||||
'Group.ReadWrite.All': 'Write to Microsoft groups',
|
||||
'Team.ReadBasic.All': 'Read Microsoft teams',
|
||||
'TeamMember.Read.All': 'Read team members',
|
||||
'Mail.ReadWrite': 'Write to Microsoft emails',
|
||||
'Mail.ReadBasic': 'Read Microsoft emails',
|
||||
'Mail.Read': 'Read Microsoft emails',
|
||||
'Mail.Send': 'Send emails',
|
||||
'Files.Read': 'Read OneDrive files',
|
||||
'Files.ReadWrite': 'Read and write OneDrive files',
|
||||
'Tasks.ReadWrite': 'Read and manage Planner tasks',
|
||||
'Sites.Read.All': 'Read Sharepoint sites',
|
||||
'Sites.ReadWrite.All': 'Read and write Sharepoint sites',
|
||||
'Sites.Manage.All': 'Manage Sharepoint sites',
|
||||
'https://dynamics.microsoft.com/user_impersonation': 'Access Microsoft Dataverse on your behalf',
|
||||
|
||||
// Discord scopes
|
||||
identify: 'Read Discord user',
|
||||
bot: 'Read Discord bot',
|
||||
'messages.read': 'Read Discord messages',
|
||||
guilds: 'Read Discord guilds',
|
||||
'guilds.members.read': 'Read Discord guild members',
|
||||
|
||||
// Reddit scopes
|
||||
identity: 'Access Reddit identity',
|
||||
submit: 'Submit posts and comments',
|
||||
vote: 'Vote on posts and comments',
|
||||
save: 'Save and unsave posts and comments',
|
||||
edit: 'Edit posts and comments',
|
||||
subscribe: 'Subscribe and unsubscribe from subreddits',
|
||||
history: 'Access Reddit history',
|
||||
privatemessages: 'Access inbox and send private messages',
|
||||
account: 'Update account preferences and settings',
|
||||
mysubreddits: 'Access subscribed and moderated subreddits',
|
||||
flair: 'Manage user and post flair',
|
||||
report: 'Report posts and comments for rule violations',
|
||||
modposts: 'Approve, remove, and moderate posts in moderated subreddits',
|
||||
modflair: 'Manage flair in moderated subreddits',
|
||||
modmail: 'Access and respond to moderator mail',
|
||||
|
||||
// Wealthbox scopes
|
||||
login: 'Access Wealthbox account',
|
||||
data: 'Access Wealthbox data',
|
||||
|
||||
// Linear scopes
|
||||
read: 'Read access to workspace',
|
||||
write: 'Write access to Linear workspace',
|
||||
|
||||
// Slack scopes
|
||||
'channels:read': 'View public channels',
|
||||
'channels:history': 'Read channel messages',
|
||||
'groups:read': 'View private channels',
|
||||
'groups:history': 'Read private messages',
|
||||
'chat:write': 'Send messages',
|
||||
'chat:write.public': 'Post to public channels',
|
||||
'im:write': 'Send direct messages',
|
||||
'im:history': 'Read direct message history',
|
||||
'im:read': 'View direct message channels',
|
||||
'users:read': 'View workspace users',
|
||||
'files:write': 'Upload files',
|
||||
'files:read': 'Download and read files',
|
||||
'canvases:write': 'Create canvas documents',
|
||||
'reactions:write': 'Add emoji reactions to messages',
|
||||
|
||||
// Webflow scopes
|
||||
'sites:read': 'View Webflow sites',
|
||||
'sites:write': 'Manage webhooks and site settings',
|
||||
'cms:read': 'View CMS content',
|
||||
'cms:write': 'Manage CMS content',
|
||||
'forms:read': 'View form submissions',
|
||||
|
||||
// HubSpot scopes
|
||||
'crm.objects.contacts.read': 'Read HubSpot contacts',
|
||||
'crm.objects.contacts.write': 'Create and update HubSpot contacts',
|
||||
'crm.objects.companies.read': 'Read HubSpot companies',
|
||||
'crm.objects.companies.write': 'Create and update HubSpot companies',
|
||||
'crm.objects.deals.read': 'Read HubSpot deals',
|
||||
'crm.objects.deals.write': 'Create and update HubSpot deals',
|
||||
'crm.objects.owners.read': 'Read HubSpot object owners',
|
||||
'crm.objects.users.read': 'Read HubSpot users',
|
||||
'crm.objects.users.write': 'Create and update HubSpot users',
|
||||
'crm.objects.marketing_events.read': 'Read HubSpot marketing events',
|
||||
'crm.objects.marketing_events.write': 'Create and update HubSpot marketing events',
|
||||
'crm.objects.line_items.read': 'Read HubSpot line items',
|
||||
'crm.objects.line_items.write': 'Create and update HubSpot line items',
|
||||
'crm.objects.quotes.read': 'Read HubSpot quotes',
|
||||
'crm.objects.quotes.write': 'Create and update HubSpot quotes',
|
||||
'crm.objects.appointments.read': 'Read HubSpot appointments',
|
||||
'crm.objects.appointments.write': 'Create and update HubSpot appointments',
|
||||
'crm.objects.carts.read': 'Read HubSpot shopping carts',
|
||||
'crm.objects.carts.write': 'Create and update HubSpot shopping carts',
|
||||
'crm.import': 'Import data into HubSpot',
|
||||
'crm.lists.read': 'Read HubSpot lists',
|
||||
'crm.lists.write': 'Create and update HubSpot lists',
|
||||
tickets: 'Manage HubSpot tickets',
|
||||
|
||||
// Salesforce scopes
|
||||
api: 'Access Salesforce API',
|
||||
refresh_token: 'Maintain long-term access to Salesforce account',
|
||||
|
||||
// Asana scopes
|
||||
default: 'Access Asana workspace',
|
||||
|
||||
// Pipedrive scopes
|
||||
base: 'Basic access to Pipedrive account',
|
||||
'deals:read': 'Read Pipedrive deals',
|
||||
'deals:full': 'Full access to manage Pipedrive deals',
|
||||
'contacts:read': 'Read Pipedrive contacts',
|
||||
'contacts:full': 'Full access to manage Pipedrive contacts',
|
||||
'leads:read': 'Read Pipedrive leads',
|
||||
'leads:full': 'Full access to manage Pipedrive leads',
|
||||
'activities:read': 'Read Pipedrive activities',
|
||||
'activities:full': 'Full access to manage Pipedrive activities',
|
||||
'mail:read': 'Read Pipedrive emails',
|
||||
'mail:full': 'Full access to manage Pipedrive emails',
|
||||
'projects:read': 'Read Pipedrive projects',
|
||||
'projects:full': 'Full access to manage Pipedrive projects',
|
||||
'webhooks:read': 'Read Pipedrive webhooks',
|
||||
'webhooks:full': 'Full access to manage Pipedrive webhooks',
|
||||
|
||||
// LinkedIn scopes
|
||||
w_member_social: 'Access LinkedIn profile',
|
||||
|
||||
// Box scopes
|
||||
root_readwrite: 'Read and write all files and folders in Box account',
|
||||
root_readonly: 'Read all files and folders in Box account',
|
||||
|
||||
// Shopify scopes
|
||||
write_products: 'Read and manage Shopify products',
|
||||
write_orders: 'Read and manage Shopify orders',
|
||||
write_customers: 'Read and manage Shopify customers',
|
||||
write_inventory: 'Read and manage Shopify inventory levels',
|
||||
read_locations: 'View store locations',
|
||||
write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders',
|
||||
|
||||
// Zoom scopes
|
||||
'user:read:user': 'View Zoom profile information',
|
||||
'meeting:write:meeting': 'Create Zoom meetings',
|
||||
'meeting:read:meeting': 'View Zoom meeting details',
|
||||
'meeting:read:list_meetings': 'List Zoom meetings',
|
||||
'meeting:update:meeting': 'Update Zoom meetings',
|
||||
'meeting:delete:meeting': 'Delete Zoom meetings',
|
||||
'meeting:read:invitation': 'View Zoom meeting invitations',
|
||||
'meeting:read:list_past_participants': 'View past meeting participants',
|
||||
'cloud_recording:read:list_user_recordings': 'List Zoom cloud recordings',
|
||||
'cloud_recording:read:list_recording_files': 'View recording files',
|
||||
'cloud_recording:delete:recording_file': 'Delete cloud recordings',
|
||||
|
||||
// Dropbox scopes
|
||||
'account_info.read': 'View Dropbox account information',
|
||||
'files.metadata.read': 'View file and folder names, sizes, and dates',
|
||||
'files.metadata.write': 'Modify file and folder metadata',
|
||||
'files.content.read': 'Download and read Dropbox files',
|
||||
'files.content.write': 'Upload, copy, move, and delete files in Dropbox',
|
||||
'sharing.read': 'View shared files and folders',
|
||||
'sharing.write': 'Share files and folders with others',
|
||||
|
||||
// WordPress.com scopes
|
||||
global: 'Full access to manage WordPress.com sites, posts, pages, media, and settings',
|
||||
|
||||
// Spotify scopes
|
||||
'user-read-private': 'View Spotify account details',
|
||||
'user-read-email': 'View email address on Spotify',
|
||||
'user-library-read': 'View saved tracks and albums',
|
||||
'user-library-modify': 'Save and remove tracks and albums from library',
|
||||
'playlist-read-private': 'View private playlists',
|
||||
'playlist-read-collaborative': 'View collaborative playlists',
|
||||
'playlist-modify-public': 'Create and manage public playlists',
|
||||
'playlist-modify-private': 'Create and manage private playlists',
|
||||
'user-read-playback-state': 'View current playback state',
|
||||
'user-modify-playback-state': 'Control playback on Spotify devices',
|
||||
'user-read-currently-playing': 'View currently playing track',
|
||||
'user-read-recently-played': 'View recently played tracks',
|
||||
'user-top-read': 'View top artists and tracks',
|
||||
'user-follow-read': 'View followed artists and users',
|
||||
'user-follow-modify': 'Follow and unfollow artists and users',
|
||||
'user-read-playback-position': 'View playback position in podcasts',
|
||||
'ugc-image-upload': 'Upload images to Spotify playlists',
|
||||
|
||||
// Attio scopes
|
||||
'record_permission:read-write': 'Read and write CRM records',
|
||||
'object_configuration:read-write': 'Read and manage object schemas',
|
||||
'list_configuration:read-write': 'Read and manage list configurations',
|
||||
'list_entry:read-write': 'Read and write list entries',
|
||||
'note:read-write': 'Read and write notes',
|
||||
'task:read-write': 'Read and write tasks',
|
||||
'comment:read-write': 'Read and write comments and threads',
|
||||
'user_management:read': 'View workspace members',
|
||||
'webhook:read-write': 'Manage webhooks',
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable description for a scope.
|
||||
* Falls back to the raw scope string if no description is found.
|
||||
*/
|
||||
export function getScopeDescription(scope: string): string {
|
||||
return SCOPE_DESCRIPTIONS[scope] || scope
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flat list of all available OAuth services with metadata.
|
||||
* This is safe to use on the server as it doesn't include React components.
|
||||
@@ -76,37 +478,53 @@ export function getCanonicalScopesForProvider(providerId: string): string[] {
|
||||
return service?.scopes ? [...service.scopes] : []
|
||||
}
|
||||
|
||||
export function normalizeScopes(scopes: string[]): string[] {
|
||||
const seen = new Set<string>()
|
||||
for (const scope of scopes) {
|
||||
const trimmed = scope.trim()
|
||||
if (trimmed && !seen.has(trimmed)) {
|
||||
seen.add(trimmed)
|
||||
/**
|
||||
* Get canonical scopes for a service by its serviceId key in OAUTH_PROVIDERS.
|
||||
* Useful for block definitions to reference scopes from the single source of truth.
|
||||
*/
|
||||
export function getScopesForService(serviceId: string): string[] {
|
||||
for (const provider of Object.values(OAUTH_PROVIDERS)) {
|
||||
const service = provider.services[serviceId]
|
||||
if (service) {
|
||||
return [...service.scopes]
|
||||
}
|
||||
}
|
||||
return Array.from(seen)
|
||||
return []
|
||||
}
|
||||
|
||||
export function evaluateScopeCoverage(
|
||||
providerId: string,
|
||||
grantedScopes: string[]
|
||||
): ScopeEvaluation {
|
||||
const canonicalScopes = getCanonicalScopesForProvider(providerId)
|
||||
const normalizedGranted = normalizeScopes(grantedScopes)
|
||||
/**
|
||||
* Scopes that control token behavior but are not returned in OAuth token responses.
|
||||
* These should be ignored when validating credential scopes.
|
||||
*/
|
||||
const IGNORED_SCOPES = new Set([
|
||||
'offline_access', // Microsoft - requests refresh token
|
||||
'refresh_token', // Salesforce - requests refresh token
|
||||
'offline.access', // Airtable - requests refresh token (note: dot not underscore)
|
||||
])
|
||||
|
||||
const canonicalSet = new Set(canonicalScopes)
|
||||
const grantedSet = new Set(normalizedGranted)
|
||||
|
||||
const missingScopes = canonicalScopes.filter((scope) => !grantedSet.has(scope))
|
||||
const extraScopes = normalizedGranted.filter((scope) => !canonicalSet.has(scope))
|
||||
|
||||
return {
|
||||
canonicalScopes,
|
||||
grantedScopes: normalizedGranted,
|
||||
missingScopes,
|
||||
extraScopes,
|
||||
requiresReauthorization: missingScopes.length > 0,
|
||||
/**
|
||||
* Compute which of the provided requiredScopes are NOT granted by the credential.
|
||||
* Note: Ignores special OAuth scopes that control token behavior (like offline_access)
|
||||
* as they are not returned in the token response's scope list even when granted.
|
||||
*/
|
||||
export function getMissingRequiredScopes(
|
||||
credential: { scopes?: string[] } | undefined,
|
||||
requiredScopes: string[] = []
|
||||
): string[] {
|
||||
if (!credential) {
|
||||
return requiredScopes.filter((s) => !IGNORED_SCOPES.has(s))
|
||||
}
|
||||
|
||||
const granted = new Set(credential.scopes || [])
|
||||
const missing: string[] = []
|
||||
|
||||
for (const s of requiredScopes) {
|
||||
if (IGNORED_SCOPES.has(s)) continue
|
||||
|
||||
if (!granted.has(s)) missing.push(s)
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { buildSelectorContextFromBlock } from '@/lib/workflows/subblocks/context'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||
import { CREDENTIAL_SET, isUuid } from '@/executor/constants'
|
||||
@@ -6,7 +7,7 @@ import { fetchCredentialSetById } from '@/hooks/queries/credential-sets'
|
||||
import { fetchOAuthCredentialDetail } from '@/hooks/queries/oauth-credentials'
|
||||
import { getSelectorDefinition } from '@/hooks/selectors/registry'
|
||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||
import type { SelectorKey } from '@/hooks/selectors/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('ResolveValues')
|
||||
@@ -39,74 +40,8 @@ interface ResolutionContext {
|
||||
blockId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended context extracted from block subBlocks for selector resolution
|
||||
*/
|
||||
interface ExtendedSelectorContext {
|
||||
credentialId?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
teamId?: string
|
||||
knowledgeBaseId?: string
|
||||
siteId?: string
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
}
|
||||
|
||||
function getSemanticFallback(subBlockId: string, subBlockConfig?: SubBlockConfig): string {
|
||||
if (subBlockConfig?.title) {
|
||||
return subBlockConfig.title.toLowerCase()
|
||||
}
|
||||
|
||||
const patterns: Record<string, string> = {
|
||||
credential: 'credential',
|
||||
channel: 'channel',
|
||||
channelId: 'channel',
|
||||
user: 'user',
|
||||
userId: 'user',
|
||||
workflow: 'workflow',
|
||||
workflowId: 'workflow',
|
||||
file: 'file',
|
||||
fileId: 'file',
|
||||
folder: 'folder',
|
||||
folderId: 'folder',
|
||||
project: 'project',
|
||||
projectId: 'project',
|
||||
team: 'team',
|
||||
teamId: 'team',
|
||||
sheet: 'sheet',
|
||||
sheetId: 'sheet',
|
||||
document: 'document',
|
||||
documentId: 'document',
|
||||
knowledgeBase: 'knowledge base',
|
||||
knowledgeBaseId: 'knowledge base',
|
||||
server: 'server',
|
||||
serverId: 'server',
|
||||
tool: 'tool',
|
||||
toolId: 'tool',
|
||||
calendar: 'calendar',
|
||||
calendarId: 'calendar',
|
||||
label: 'label',
|
||||
labelId: 'label',
|
||||
site: 'site',
|
||||
siteId: 'site',
|
||||
collection: 'collection',
|
||||
collectionId: 'collection',
|
||||
item: 'item',
|
||||
itemId: 'item',
|
||||
contact: 'contact',
|
||||
contactId: 'contact',
|
||||
task: 'task',
|
||||
taskId: 'task',
|
||||
chat: 'chat',
|
||||
chatId: 'chat',
|
||||
}
|
||||
|
||||
return patterns[subBlockId] || 'value'
|
||||
function getSemanticFallback(subBlockConfig: SubBlockConfig): string {
|
||||
return (subBlockConfig.title ?? subBlockConfig.id).toLowerCase()
|
||||
}
|
||||
|
||||
async function resolveCredential(credentialId: string, workflowId: string): Promise<string | null> {
|
||||
@@ -150,26 +85,10 @@ async function resolveWorkflow(workflowId: string): Promise<string | null> {
|
||||
async function resolveSelectorValue(
|
||||
value: string,
|
||||
selectorKey: SelectorKey,
|
||||
extendedContext: ExtendedSelectorContext,
|
||||
workflowId: string
|
||||
selectorContext: SelectorContext
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const definition = getSelectorDefinition(selectorKey)
|
||||
const selectorContext = {
|
||||
workflowId,
|
||||
credentialId: extendedContext.credentialId,
|
||||
domain: extendedContext.domain,
|
||||
projectId: extendedContext.projectId,
|
||||
planId: extendedContext.planId,
|
||||
teamId: extendedContext.teamId,
|
||||
knowledgeBaseId: extendedContext.knowledgeBaseId,
|
||||
siteId: extendedContext.siteId,
|
||||
collectionId: extendedContext.collectionId,
|
||||
spreadsheetId: extendedContext.spreadsheetId,
|
||||
baseId: extendedContext.baseId,
|
||||
datasetId: extendedContext.datasetId,
|
||||
serviceDeskId: extendedContext.serviceDeskId,
|
||||
}
|
||||
|
||||
if (definition.fetchById) {
|
||||
const result = await definition.fetchById({
|
||||
@@ -219,37 +138,14 @@ export function formatValueForDisplay(value: unknown): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts extended context from a block's subBlocks for selector resolution.
|
||||
* This mirrors the context extraction done in the UI components.
|
||||
*/
|
||||
function extractExtendedContext(
|
||||
function extractSelectorContext(
|
||||
blockId: string,
|
||||
currentState: WorkflowState
|
||||
): ExtendedSelectorContext {
|
||||
currentState: WorkflowState,
|
||||
workflowId: string
|
||||
): SelectorContext {
|
||||
const block = currentState.blocks?.[blockId]
|
||||
if (!block?.subBlocks) return {}
|
||||
|
||||
const getStringValue = (id: string): string | undefined => {
|
||||
const subBlock = block.subBlocks[id] as { value?: unknown } | undefined
|
||||
const val = subBlock?.value
|
||||
return typeof val === 'string' ? val : undefined
|
||||
}
|
||||
|
||||
return {
|
||||
credentialId: getStringValue('credential'),
|
||||
domain: getStringValue('domain'),
|
||||
projectId: getStringValue('projectId'),
|
||||
planId: getStringValue('planId'),
|
||||
teamId: getStringValue('teamId'),
|
||||
knowledgeBaseId: getStringValue('knowledgeBaseId'),
|
||||
siteId: getStringValue('siteId'),
|
||||
collectionId: getStringValue('collectionId'),
|
||||
spreadsheetId: getStringValue('spreadsheetId') || getStringValue('fileId'),
|
||||
baseId: getStringValue('baseId') || getStringValue('baseSelector'),
|
||||
datasetId: getStringValue('datasetId') || getStringValue('datasetSelector'),
|
||||
serviceDeskId: getStringValue('serviceDeskId') || getStringValue('serviceDeskSelector'),
|
||||
}
|
||||
if (!block?.subBlocks) return { workflowId }
|
||||
return buildSelectorContextFromBlock(block.type, block.subBlocks, { workflowId })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,11 +171,14 @@ export async function resolveValueForDisplay(
|
||||
|
||||
const blockConfig = getBlock(context.blockType)
|
||||
const subBlockConfig = blockConfig?.subBlocks.find((sb) => sb.id === context.subBlockId)
|
||||
const semanticFallback = getSemanticFallback(context.subBlockId, subBlockConfig)
|
||||
if (!subBlockConfig) {
|
||||
return { original: value, displayLabel: formatValueForDisplay(value), resolved: false }
|
||||
}
|
||||
const semanticFallback = getSemanticFallback(subBlockConfig)
|
||||
|
||||
const extendedContext = context.blockId
|
||||
? extractExtendedContext(context.blockId, context.currentState)
|
||||
: {}
|
||||
const selectorCtx = context.blockId
|
||||
? extractSelectorContext(context.blockId, context.currentState, context.workflowId)
|
||||
: { workflowId: context.workflowId }
|
||||
|
||||
// Credential fields (oauth-input or credential subBlockId)
|
||||
const isCredentialField =
|
||||
@@ -311,29 +210,10 @@ export async function resolveValueForDisplay(
|
||||
// Selector types that require hydration (file-selector, sheet-selector, etc.)
|
||||
// These support external service IDs like Google Drive file IDs
|
||||
if (subBlockConfig && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlockConfig.type)) {
|
||||
const resolution = resolveSelectorForSubBlock(subBlockConfig, {
|
||||
workflowId: context.workflowId,
|
||||
credentialId: extendedContext.credentialId,
|
||||
domain: extendedContext.domain,
|
||||
projectId: extendedContext.projectId,
|
||||
planId: extendedContext.planId,
|
||||
teamId: extendedContext.teamId,
|
||||
knowledgeBaseId: extendedContext.knowledgeBaseId,
|
||||
siteId: extendedContext.siteId,
|
||||
collectionId: extendedContext.collectionId,
|
||||
spreadsheetId: extendedContext.spreadsheetId,
|
||||
baseId: extendedContext.baseId,
|
||||
datasetId: extendedContext.datasetId,
|
||||
serviceDeskId: extendedContext.serviceDeskId,
|
||||
})
|
||||
const resolution = resolveSelectorForSubBlock(subBlockConfig, selectorCtx)
|
||||
|
||||
if (resolution?.key) {
|
||||
const label = await resolveSelectorValue(
|
||||
value,
|
||||
resolution.key,
|
||||
extendedContext,
|
||||
context.workflowId
|
||||
)
|
||||
const label = await resolveSelectorValue(value, resolution.key, selectorCtx)
|
||||
if (label) {
|
||||
return { original: value, displayLabel: label, resolved: true }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
import { migrateSubblockIds } from './subblock-migrations'
|
||||
|
||||
vi.unmock('@/blocks/registry')
|
||||
|
||||
import { backfillCanonicalModes, migrateSubblockIds } from './subblock-migrations'
|
||||
|
||||
function makeBlock(overrides: Partial<BlockState> & { type: string }): BlockState {
|
||||
return {
|
||||
@@ -181,3 +184,185 @@ describe('migrateSubblockIds', () => {
|
||||
expect(migrated).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('backfillCanonicalModes', () => {
|
||||
it('should add missing canonicalModes entry for knowledge block with basic value', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: 'kb-uuid',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(true)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('basic')
|
||||
})
|
||||
|
||||
it('should resolve to advanced when only the advanced value is set', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
manualKnowledgeBaseId: {
|
||||
id: 'manualKnowledgeBaseId',
|
||||
type: 'short-input',
|
||||
value: 'kb-uuid-manual',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(true)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('advanced')
|
||||
})
|
||||
|
||||
it('should not overwrite existing canonicalModes entries', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: { canonicalModes: { knowledgeBaseId: 'advanced' } },
|
||||
subBlocks: {
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: 'kb-uuid',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(false)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('advanced')
|
||||
})
|
||||
|
||||
it('should skip blocks with no canonical pairs in their config', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'function',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
code: { id: 'code', type: 'code', value: '' },
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(false)
|
||||
})
|
||||
|
||||
it('should not mutate the input blocks', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: 'kb-uuid',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks } = backfillCanonicalModes(input)
|
||||
|
||||
expect(input.b1.data?.canonicalModes).toBeUndefined()
|
||||
expect((blocks.b1.data?.canonicalModes as Record<string, string>).knowledgeBaseId).toBe('basic')
|
||||
expect(blocks).not.toBe(input)
|
||||
})
|
||||
|
||||
it('should resolve correctly when existing field became the basic variant', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: 'kb-uuid',
|
||||
},
|
||||
manualKnowledgeBaseId: {
|
||||
id: 'manualKnowledgeBaseId',
|
||||
type: 'short-input',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(true)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('basic')
|
||||
})
|
||||
|
||||
it('should resolve correctly when existing field became the advanced variant', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: '',
|
||||
},
|
||||
manualKnowledgeBaseId: {
|
||||
id: 'manualKnowledgeBaseId',
|
||||
type: 'short-input',
|
||||
value: 'manually-entered-kb-id',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(true)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('advanced')
|
||||
})
|
||||
|
||||
it('should default to basic when neither value is set', () => {
|
||||
const input: Record<string, BlockState> = {
|
||||
b1: makeBlock({
|
||||
type: 'knowledge',
|
||||
data: {},
|
||||
subBlocks: {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const { blocks, migrated } = backfillCanonicalModes(input)
|
||||
|
||||
expect(migrated).toBe(true)
|
||||
const modes = blocks.b1.data?.canonicalModes as Record<string, string>
|
||||
expect(modes.knowledgeBaseId).toBe('basic')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
buildSubBlockValues,
|
||||
isCanonicalPair,
|
||||
resolveCanonicalMode,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('SubblockMigrations')
|
||||
@@ -88,3 +95,59 @@ export function migrateSubblockIds(blocks: Record<string, BlockState>): {
|
||||
|
||||
return { blocks: result, migrated: anyMigrated }
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfills missing `canonicalModes` entries in block data.
|
||||
*
|
||||
* When a canonical pair is added to a block definition, existing blocks
|
||||
* won't have the entry in `data.canonicalModes`. Without it the editor
|
||||
* toggle may not render correctly. This resolves the correct mode based
|
||||
* on which subblock value is populated and adds the missing entry.
|
||||
*/
|
||||
export function backfillCanonicalModes(blocks: Record<string, BlockState>): {
|
||||
blocks: Record<string, BlockState>
|
||||
migrated: boolean
|
||||
} {
|
||||
let anyMigrated = false
|
||||
const result: Record<string, BlockState> = {}
|
||||
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig?.subBlocks || !block.subBlocks) {
|
||||
result[blockId] = block
|
||||
continue
|
||||
}
|
||||
|
||||
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
|
||||
const pairs = Object.values(canonicalIndex.groupsById).filter(isCanonicalPair)
|
||||
if (pairs.length === 0) {
|
||||
result[blockId] = block
|
||||
continue
|
||||
}
|
||||
|
||||
const existing = (block.data?.canonicalModes ?? {}) as Record<string, 'basic' | 'advanced'>
|
||||
let patched: Record<string, 'basic' | 'advanced'> | null = null
|
||||
|
||||
const values = buildSubBlockValues(block.subBlocks)
|
||||
|
||||
for (const group of pairs) {
|
||||
if (existing[group.canonicalId] != null) continue
|
||||
|
||||
const resolved = resolveCanonicalMode(group, values)
|
||||
if (!patched) patched = { ...existing }
|
||||
patched[group.canonicalId] = resolved
|
||||
}
|
||||
|
||||
if (patched) {
|
||||
anyMigrated = true
|
||||
result[blockId] = {
|
||||
...block,
|
||||
data: { ...(block.data ?? {}), canonicalModes: patched },
|
||||
}
|
||||
} else {
|
||||
result[blockId] = block
|
||||
}
|
||||
}
|
||||
|
||||
return { blocks: result, migrated: anyMigrated }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ import { and, desc, eq, inArray, sql } from 'drizzle-orm'
|
||||
import type { Edge } from 'reactflow'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type { DbOrTx } from '@/lib/db/types'
|
||||
import { migrateSubblockIds } from '@/lib/workflows/migrations/subblock-migrations'
|
||||
import {
|
||||
backfillCanonicalModes,
|
||||
migrateSubblockIds,
|
||||
} from '@/lib/workflows/migrations/subblock-migrations'
|
||||
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validation'
|
||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types'
|
||||
@@ -176,6 +179,11 @@ const applyBlockMigrations = createMigrationPipeline([
|
||||
const { blocks, migrated } = migrateSubblockIds(ctx.blocks)
|
||||
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
|
||||
},
|
||||
|
||||
(ctx) => {
|
||||
const { blocks, migrated } = backfillCanonicalModes(ctx.blocks)
|
||||
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
|
||||
},
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -410,10 +418,14 @@ export async function loadWorkflowFromNormalizedTables(
|
||||
Promise.resolve().then(async () => {
|
||||
try {
|
||||
for (const [blockId, block] of Object.entries(finalBlocks)) {
|
||||
if (block.subBlocks !== blocksMap[blockId]?.subBlocks) {
|
||||
if (block !== blocksMap[blockId]) {
|
||||
await db
|
||||
.update(workflowBlocks)
|
||||
.set({ subBlocks: block.subBlocks, updatedAt: new Date() })
|
||||
.set({
|
||||
subBlocks: block.subBlocks,
|
||||
data: block.data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))
|
||||
)
|
||||
|
||||
125
apps/sim/lib/workflows/subblocks/context.test.ts
Normal file
125
apps/sim/lib/workflows/subblocks/context.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.unmock('@/blocks/registry')
|
||||
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import { buildSelectorContextFromBlock, SELECTOR_CONTEXT_FIELDS } from './context'
|
||||
import { buildCanonicalIndex, isCanonicalPair } from './visibility'
|
||||
|
||||
describe('buildSelectorContextFromBlock', () => {
|
||||
it('should extract knowledgeBaseId from knowledgeBaseSelector via canonical mapping', () => {
|
||||
const ctx = buildSelectorContextFromBlock('knowledge', {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: 'kb-uuid-123',
|
||||
},
|
||||
})
|
||||
|
||||
expect(ctx.knowledgeBaseId).toBe('kb-uuid-123')
|
||||
})
|
||||
|
||||
it('should extract knowledgeBaseId from manualKnowledgeBaseId via canonical mapping', () => {
|
||||
const ctx = buildSelectorContextFromBlock('knowledge', {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
manualKnowledgeBaseId: {
|
||||
id: 'manualKnowledgeBaseId',
|
||||
type: 'short-input',
|
||||
value: 'manual-kb-id',
|
||||
},
|
||||
})
|
||||
|
||||
expect(ctx.knowledgeBaseId).toBe('manual-kb-id')
|
||||
})
|
||||
|
||||
it('should skip null/empty values', () => {
|
||||
const ctx = buildSelectorContextFromBlock('knowledge', {
|
||||
knowledgeBaseSelector: {
|
||||
id: 'knowledgeBaseSelector',
|
||||
type: 'knowledge-base-selector',
|
||||
value: '',
|
||||
},
|
||||
})
|
||||
|
||||
expect(ctx.knowledgeBaseId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return empty context for unknown block types', () => {
|
||||
const ctx = buildSelectorContextFromBlock('nonexistent_block', {
|
||||
foo: { id: 'foo', type: 'short-input', value: 'bar' },
|
||||
})
|
||||
|
||||
expect(ctx).toEqual({})
|
||||
})
|
||||
|
||||
it('should pass through workflowId from opts', () => {
|
||||
const ctx = buildSelectorContextFromBlock(
|
||||
'knowledge',
|
||||
{ operation: { id: 'operation', type: 'dropdown', value: 'search' } },
|
||||
{ workflowId: 'wf-123' }
|
||||
)
|
||||
|
||||
expect(ctx.workflowId).toBe('wf-123')
|
||||
})
|
||||
|
||||
it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => {
|
||||
const ctx = buildSelectorContextFromBlock('knowledge', {
|
||||
operation: { id: 'operation', type: 'dropdown', value: 'search' },
|
||||
query: { id: 'query', type: 'short-input', value: 'some search query' },
|
||||
})
|
||||
|
||||
expect((ctx as Record<string, unknown>).query).toBeUndefined()
|
||||
expect((ctx as Record<string, unknown>).operation).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SELECTOR_CONTEXT_FIELDS validation', () => {
|
||||
it('every entry must be a canonicalParamId (if a canonical pair exists) or a direct subblock ID', () => {
|
||||
const allCanonicalParamIds = new Set<string>()
|
||||
const allSubBlockIds = new Set<string>()
|
||||
const idsInCanonicalPairs = new Set<string>()
|
||||
|
||||
for (const block of getAllBlocks()) {
|
||||
const index = buildCanonicalIndex(block.subBlocks)
|
||||
|
||||
for (const sb of block.subBlocks) {
|
||||
allSubBlockIds.add(sb.id)
|
||||
if (sb.canonicalParamId) {
|
||||
allCanonicalParamIds.add(sb.canonicalParamId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of Object.values(index.groupsById)) {
|
||||
if (!isCanonicalPair(group)) continue
|
||||
if (group.basicId) idsInCanonicalPairs.add(group.basicId)
|
||||
for (const advId of group.advancedIds) idsInCanonicalPairs.add(advId)
|
||||
}
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
|
||||
for (const field of SELECTOR_CONTEXT_FIELDS) {
|
||||
const f = field as string
|
||||
if (allCanonicalParamIds.has(f)) continue
|
||||
|
||||
if (idsInCanonicalPairs.has(f)) {
|
||||
errors.push(
|
||||
`"${f}" is a member subblock ID inside a canonical pair — use the canonicalParamId instead`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!allSubBlockIds.has(f)) {
|
||||
errors.push(`"${f}" is not a canonicalParamId or subblock ID in any block definition`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`SELECTOR_CONTEXT_FIELDS validation failed:\n${errors.join('\n')}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
60
apps/sim/lib/workflows/subblocks/context.ts
Normal file
60
apps/sim/lib/workflows/subblocks/context.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||
import type { SubBlockState } from '@/stores/workflows/workflow/types'
|
||||
import { buildCanonicalIndex } from './visibility'
|
||||
|
||||
/**
|
||||
* Canonical param IDs (or raw subblock IDs) that correspond to SelectorContext fields.
|
||||
* A subblock's resolved canonical key is set on the context only if it appears here.
|
||||
*/
|
||||
export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
|
||||
'oauthCredential',
|
||||
'domain',
|
||||
'teamId',
|
||||
'projectId',
|
||||
'knowledgeBaseId',
|
||||
'planId',
|
||||
'siteId',
|
||||
'collectionId',
|
||||
'spreadsheetId',
|
||||
'fileId',
|
||||
'baseId',
|
||||
'datasetId',
|
||||
'serviceDeskId',
|
||||
])
|
||||
|
||||
/**
|
||||
* Builds a SelectorContext from a block's subBlocks using the canonical index.
|
||||
*
|
||||
* Iterates all subblocks, resolves each through canonicalIdBySubBlockId to get
|
||||
* the canonical key, then checks it against SELECTOR_CONTEXT_FIELDS.
|
||||
* This avoids hardcoding subblock IDs and automatically handles basic/advanced
|
||||
* renames.
|
||||
*/
|
||||
export function buildSelectorContextFromBlock(
|
||||
blockType: string,
|
||||
subBlocks: Record<string, SubBlockState | { value?: unknown }>,
|
||||
opts?: { workflowId?: string }
|
||||
): SelectorContext {
|
||||
const context: SelectorContext = {}
|
||||
if (opts?.workflowId) context.workflowId = opts.workflowId
|
||||
|
||||
const blockConfig = getBlock(blockType)
|
||||
if (!blockConfig) return context
|
||||
|
||||
const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks)
|
||||
|
||||
for (const [subBlockId, subBlock] of Object.entries(subBlocks)) {
|
||||
const val = subBlock?.value
|
||||
if (val === null || val === undefined) continue
|
||||
const strValue = typeof val === 'string' ? val : String(val)
|
||||
if (!strValue) continue
|
||||
|
||||
const canonicalKey = canonicalIndex.canonicalIdBySubBlockId[subBlockId] ?? subBlockId
|
||||
if (SELECTOR_CONTEXT_FIELDS.has(canonicalKey as keyof SelectorContext)) {
|
||||
context[canonicalKey as keyof SelectorContext] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user