mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
17 Commits
improvemen
...
v0.5.107
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6586c5ce40 | ||
|
|
3ce947566d | ||
|
|
70c36cb7aa | ||
|
|
f1ec5fe824 | ||
|
|
e07e3c34cc | ||
|
|
0d2e6ff31d | ||
|
|
4fd0989264 | ||
|
|
67f8a687f6 | ||
|
|
af592349d3 | ||
|
|
0d86ea01f0 | ||
|
|
115f04e989 | ||
|
|
34d92fae89 | ||
|
|
67aa4bb332 | ||
|
|
15ace5e63f | ||
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -20,7 +20,6 @@ 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
|
||||
@@ -116,17 +115,12 @@ export const {ServiceName}Block: BlockConfig = {
|
||||
id: 'credential',
|
||||
title: 'Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}', // Must match OAuth provider service key
|
||||
requiredScopes: getScopesForService('{service}'), // Import from @/lib/oauth/utils
|
||||
serviceId: '{service}', // Must match OAuth provider
|
||||
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.)
|
||||
@@ -630,7 +624,6 @@ 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',
|
||||
@@ -661,7 +654,6 @@ export const ServiceBlock: BlockConfig = {
|
||||
title: 'Service Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'service',
|
||||
requiredScopes: getScopesForService('service'),
|
||||
placeholder: 'Select account',
|
||||
required: true,
|
||||
},
|
||||
@@ -800,8 +792,7 @@ 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` and `requiredScopes: getScopesForService(serviceId)`
|
||||
- [ ] Scope descriptions added to `SCOPE_DESCRIPTIONS` in `lib/oauth/utils.ts` for any new scopes
|
||||
- [ ] OAuth inputs have correct `serviceId`
|
||||
- [ ] Tools.access lists all tool IDs (snake_case)
|
||||
- [ ] Tools.config.tool returns correct tool ID (snake_case)
|
||||
- [ ] Outputs match tool outputs
|
||||
|
||||
@@ -114,7 +114,6 @@ 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}',
|
||||
@@ -145,7 +144,6 @@ export const {Service}Block: BlockConfig = {
|
||||
title: '{Service} Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: '{service}',
|
||||
requiredScopes: getScopesForService('{service}'),
|
||||
required: true,
|
||||
},
|
||||
// Conditional fields per operation
|
||||
@@ -411,7 +409,7 @@ If creating V2 versions (API-aligned outputs):
|
||||
### Block
|
||||
- [ ] Created `blocks/blocks/{service}.ts`
|
||||
- [ ] Defined operation dropdown with all operations
|
||||
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
|
||||
- [ ] Added credential field (oauth-input or short-input)
|
||||
- [ ] Added conditional fields per operation
|
||||
- [ ] Set up dependsOn for cascading selectors
|
||||
- [ ] Configured tools.access with all tool IDs
|
||||
@@ -421,12 +419,6 @@ 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`
|
||||
@@ -725,25 +717,6 @@ 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
|
||||
@@ -756,5 +729,3 @@ requiredScopes: getScopesForService('{service}'),
|
||||
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,9 +26,8 @@ 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 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
|
||||
apps/sim/lib/auth/auth.ts # OAuth scopes (if OAuth service)
|
||||
apps/sim/lib/oauth/oauth.ts # OAuth provider config (if OAuth service)
|
||||
```
|
||||
|
||||
## Step 2: Pull API Documentation
|
||||
@@ -200,14 +199,11 @@ For **each tool** in `tools.access`:
|
||||
|
||||
## Step 5: Validate OAuth Scopes (if OAuth service)
|
||||
|
||||
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`
|
||||
- [ ] `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
|
||||
- [ ] 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
|
||||
|
||||
@@ -248,8 +244,7 @@ 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
|
||||
- Hardcoded scope arrays instead of using `getScopesForService()` / `getCanonicalScopesForProvider()`
|
||||
- Missing scope description in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts`
|
||||
- Missing scope description in `oauth-required-modal.tsx`
|
||||
|
||||
**Suggestion** (minor improvements):
|
||||
- Better description text
|
||||
@@ -278,8 +273,7 @@ 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 use centralized utilities (getScopesForService, getCanonicalScopesForProvider) — no hardcoded arrays
|
||||
- [ ] Validated scope descriptions exist in `SCOPE_DESCRIPTIONS` within `lib/oauth/utils.ts` for all scopes
|
||||
- [ ] Validated OAuth scopes alignment across auth.ts, oauth.ts, block, and modal (if OAuth)
|
||||
- [ ] Validated pagination consistency across tools and block
|
||||
- [ ] Validated error handling (error checks, meaningful messages)
|
||||
- [ ] Validated registry entries (tools and block, alphabetical, correct imports)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.3.10-alpine
|
||||
FROM oven/bun:1.3.9-alpine
|
||||
|
||||
# Install necessary packages for development
|
||||
RUN apk add --no-cache \
|
||||
|
||||
2
.github/workflows/docs-embeddings.yml
vendored
2
.github/workflows/docs-embeddings.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
4
.github/workflows/i18n.yml
vendored
4
.github/workflows/i18n.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
@@ -122,7 +122,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/migrations.yml
vendored
2
.github/workflows/migrations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Cache Bun dependencies
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/publish-cli.yml
vendored
2
.github/workflows/publish-cli.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/publish-ts-sdk.yml
vendored
2
.github/workflows/publish-ts-sdk.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Setup Node.js for npm publishing
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/test-build.yml
vendored
2
.github/workflows/test-build.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
bun-version: 1.3.9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -6,33 +6,40 @@
|
||||
import { createMockRequest } from '@sim/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
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 {
|
||||
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 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,
|
||||
@@ -59,6 +66,7 @@ vi.mock('@sim/logger', () => ({
|
||||
|
||||
vi.mock('@/lib/oauth/utils', () => ({
|
||||
parseProvider: mockParseProvider,
|
||||
evaluateScopeCoverage: mockEvaluateScopeCoverage,
|
||||
}))
|
||||
|
||||
import { GET } from '@/app/api/auth/oauth/connections/route'
|
||||
@@ -75,6 +83,16 @@ 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 { parseProvider } from '@/lib/oauth'
|
||||
import { evaluateScopeCoverage, parseProvider } from '@/lib/oauth'
|
||||
|
||||
const logger = createLogger('OAuthConnectionsAPI')
|
||||
|
||||
@@ -49,7 +49,8 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
for (const acc of accounts) {
|
||||
const { baseProvider, featureType } = parseProvider(acc.providerId as OAuthProvider)
|
||||
const scopes = acc.scope ? acc.scope.split(/\s+/).filter(Boolean) : []
|
||||
const grantedScopes = acc.scope ? acc.scope.split(/\s+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes)
|
||||
|
||||
if (baseProvider) {
|
||||
// Try multiple methods to get a user-friendly display name
|
||||
@@ -95,6 +96,10 @@ 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) {
|
||||
@@ -103,8 +108,20 @@ export async function GET(request: NextRequest) {
|
||||
existingConnection.accounts.push(accountSummary)
|
||||
|
||||
existingConnection.scopes = Array.from(
|
||||
new Set([...(existingConnection.scopes || []), ...scopes])
|
||||
new Set([...(existingConnection.scopes || []), ...scopeEvaluation.grantedScopes])
|
||||
)
|
||||
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()
|
||||
@@ -121,7 +138,11 @@ export async function GET(request: NextRequest) {
|
||||
baseProvider,
|
||||
featureType,
|
||||
isConnected: true,
|
||||
scopes,
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
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, mockLogger } = vi.hoisted(() => {
|
||||
const { mockCheckSessionOrInternalAuth, mockEvaluateScopeCoverage, mockLogger } = vi.hoisted(() => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
@@ -19,6 +19,7 @@ const { mockCheckSessionOrInternalAuth, mockLogger } = vi.hoisted(() => {
|
||||
}
|
||||
return {
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
mockEvaluateScopeCoverage: vi.fn(),
|
||||
mockLogger: logger,
|
||||
}
|
||||
})
|
||||
@@ -27,6 +28,10 @@ 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'),
|
||||
}))
|
||||
@@ -82,6 +87,16 @@ 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,6 +7,7 @@ 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'
|
||||
|
||||
@@ -38,7 +39,8 @@ function toCredentialResponse(
|
||||
scope: string | null
|
||||
) {
|
||||
const storedScope = scope?.trim()
|
||||
const scopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
|
||||
const scopeEvaluation = evaluateScopeCoverage(providerId, grantedScopes)
|
||||
const [_, featureType = 'default'] = providerId.split('-')
|
||||
|
||||
return {
|
||||
@@ -47,7 +49,11 @@ function toCredentialResponse(
|
||||
provider: providerId,
|
||||
lastUsed: updatedAt.toISOString(),
|
||||
isDefault: featureType === 'default',
|
||||
scopes,
|
||||
scopes: scopeEvaluation.grantedScopes,
|
||||
canonicalScopes: scopeEvaluation.canonicalScopes,
|
||||
missingScopes: scopeEvaluation.missingScopes,
|
||||
extraScopes: scopeEvaluation.extraScopes,
|
||||
requiresReauthorization: scopeEvaluation.requiresReauthorization,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AirtableBasesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.airtable.com/v0/meta/bases', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Airtable bases', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Airtable bases', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const bases = (data.bases || []).map((base: { id: string; name: string }) => ({
|
||||
id: base.id,
|
||||
name: base.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ bases })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Airtable bases request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Airtable bases', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAirtableId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AirtableTablesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, baseId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!baseId) {
|
||||
logger.error('Missing baseId in request')
|
||||
return NextResponse.json({ error: 'Base ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseIdValidation = validateAirtableId(baseId, 'app', 'baseId')
|
||||
if (!baseIdValidation.isValid) {
|
||||
logger.error('Invalid baseId', { error: baseIdValidation.error })
|
||||
return NextResponse.json({ error: baseIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.airtable.com/v0/meta/bases/${baseIdValidation.sanitized}/tables`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Airtable tables', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
baseId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Airtable tables', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const tables = (data.tables || []).map((table: { id: string; name: string }) => ({
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ tables })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Airtable tables request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Airtable tables', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AsanaWorkspacesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://app.asana.com/api/1.0/workspaces', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Asana workspaces', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Asana workspaces', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const workspaces = (data.data || []).map((workspace: { gid: string; name: string }) => ({
|
||||
id: workspace.gid,
|
||||
name: workspace.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ workspaces })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Asana workspaces request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AttioListsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.attio.com/v2/lists', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Attio lists', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Attio lists', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const lists = (data.data || []).map((list: { api_slug: string; name: string }) => ({
|
||||
id: list.api_slug,
|
||||
name: list.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ lists })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Attio lists request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Attio lists', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('AttioObjectsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.attio.com/v2/objects', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Attio objects', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Attio objects', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const objects = (data.data || []).map((obj: { api_slug: string; singular_noun: string }) => ({
|
||||
id: obj.api_slug,
|
||||
name: obj.singular_noun,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ objects })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Attio objects request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Attio objects', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('CalcomEventTypesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.cal.com/v2/event-types', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'cal-api-version': '2024-06-14',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Cal.com event types', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Cal.com event types', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const eventTypes = (data.data || []).map(
|
||||
(eventType: { id: number; title: string; slug: string }) => ({
|
||||
id: String(eventType.id),
|
||||
title: eventType.title,
|
||||
slug: eventType.slug,
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ eventTypes })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Cal.com event types request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Cal.com event types', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('CalcomSchedulesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.cal.com/v2/schedules', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'cal-api-version': '2024-06-11',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Cal.com schedules', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Cal.com schedules', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const schedules = (data.data || []).map((schedule: { id: number; name: string }) => ({
|
||||
id: String(schedule.id),
|
||||
name: schedule.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ schedules })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Cal.com schedules request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Cal.com schedules', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
const logger = createLogger('ConfluenceSelectorSpacesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, domain } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = await getConfluenceCloudId(domain, accessToken)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudIdValidation.sanitized}/wiki/api/v2/spaces?limit=250`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null)
|
||||
logger.error('Confluence API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
})
|
||||
const errorMessage =
|
||||
errorData?.message || `Failed to list Confluence spaces (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const spaces = (data.results || []).map((space: { id: string; name: string; key: string }) => ({
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
key: space.key,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ spaces })
|
||||
} catch (error) {
|
||||
logger.error('Error listing Confluence spaces:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('GoogleBigQueryDatasetsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* POST /api/tools/google_bigquery/datasets
|
||||
*
|
||||
* Fetches the list of BigQuery datasets for a given project using the caller's OAuth credential.
|
||||
*
|
||||
* @param request - Incoming request containing `credential`, `workflowId`, and `projectId` in the JSON body
|
||||
* @returns JSON response with a `datasets` array, each entry containing `datasetReference` and optional `friendlyName`
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, projectId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
logger.error('Missing project ID in request')
|
||||
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets?maxResults=200`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch BigQuery datasets', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch BigQuery datasets', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const datasets = (data.datasets || []).map(
|
||||
(ds: {
|
||||
datasetReference: { datasetId: string; projectId: string }
|
||||
friendlyName?: string
|
||||
}) => ({
|
||||
datasetReference: ds.datasetReference,
|
||||
friendlyName: ds.friendlyName,
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ datasets })
|
||||
} catch (error) {
|
||||
logger.error('Error processing BigQuery datasets request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve BigQuery datasets', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('GoogleBigQueryTablesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, projectId, datasetId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
logger.error('Missing project ID in request')
|
||||
return NextResponse.json({ error: 'Project ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!datasetId) {
|
||||
logger.error('Missing dataset ID in request')
|
||||
return NextResponse.json({ error: 'Dataset ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(projectId)}/datasets/${encodeURIComponent(datasetId)}/tables?maxResults=200`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch BigQuery tables', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch BigQuery tables', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const tables = (data.tables || []).map(
|
||||
(t: { tableReference: { tableId: string }; friendlyName?: string }) => ({
|
||||
tableReference: t.tableReference,
|
||||
friendlyName: t.friendlyName,
|
||||
})
|
||||
)
|
||||
|
||||
return NextResponse.json({ tables })
|
||||
} catch (error) {
|
||||
logger.error('Error processing BigQuery tables request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve BigQuery tables', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('GoogleTasksTaskListsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://tasks.googleapis.com/tasks/v1/users/@me/lists', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Google Tasks task lists', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Google Tasks task lists', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const taskLists = (data.items || []).map((list: { id: string; title: string }) => ({
|
||||
id: list.id,
|
||||
title: list.title,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ taskLists })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Google Tasks task lists request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Google Tasks task lists', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
const logger = createLogger('JsmSelectorRequestTypesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, domain, serviceDeskId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = await getJiraCloudId(domain, accessToken)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskIdValidation.sanitized}/requesttype?limit=100`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const requestTypes = (data.values || []).map((rt: { id: string; name: string }) => ({
|
||||
id: rt.id,
|
||||
name: rt.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ requestTypes })
|
||||
} catch (error) {
|
||||
logger.error('Error listing JSM request types:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
const logger = createLogger('JsmSelectorServiceDesksAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, domain } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId = await getJiraCloudId(domain, accessToken)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudIdValidation.sanitized!)
|
||||
const url = `${baseUrl}/servicedesk?limit=100`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}` },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const serviceDesks = (data.values || []).map((sd: { id: string; projectName: string }) => ({
|
||||
id: sd.id,
|
||||
name: sd.projectName,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ serviceDesks })
|
||||
} catch (error) {
|
||||
logger.error('Error listing JSM service desks:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('MicrosoftPlannerPlansAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error(`[${requestId}] Missing credential in request`)
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to obtain valid access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://graph.microsoft.com/v1.0/me/planner/plans', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(`[${requestId}] Microsoft Graph API error:`, errorText)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch plans from Microsoft Graph' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const plans = data.value || []
|
||||
|
||||
const filteredPlans = plans.map((plan: { id: string; title: string }) => ({
|
||||
id: plan.id,
|
||||
title: plan.title,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ plans: filteredPlans })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching Microsoft Planner plans:`, error)
|
||||
return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,38 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import type { PlannerTask } from '@/tools/microsoft_planner/types'
|
||||
|
||||
const logger = createLogger('MicrosoftPlannerTasksAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, planId } = body
|
||||
const session = await getSession()
|
||||
|
||||
if (!credential) {
|
||||
logger.error(`[${requestId}] Missing credential in request`)
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthenticated request rejected`)
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const planId = searchParams.get('planId')
|
||||
|
||||
if (!credentialId) {
|
||||
logger.error(`[${requestId}] Missing credentialId parameter`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!planId) {
|
||||
logger.error(`[${requestId}] Missing planId in request`)
|
||||
logger.error(`[${requestId}] Missing planId parameter`)
|
||||
return NextResponse.json({ error: 'Plan ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -33,35 +42,52 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: planIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (resolved.workspaceId) {
|
||||
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
|
||||
const perm = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
resolved.workspaceId
|
||||
)
|
||||
if (perm === null) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`, { credentialId })
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const accountRow = credentials[0]
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to obtain valid access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/planner/plans/${planIdValidation.sanitized}/tasks`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { extractTitleFromItem } from '@/tools/notion/utils'
|
||||
|
||||
const logger = createLogger('NotionDatabasesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.notion.com/v1/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': '2022-06-28',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filter: { value: 'database', property: 'object' },
|
||||
page_size: 100,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Notion databases', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Notion databases', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const databases = (data.results || []).map((db: Record<string, unknown>) => ({
|
||||
id: db.id as string,
|
||||
name: extractTitleFromItem(db),
|
||||
}))
|
||||
|
||||
return NextResponse.json({ databases })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Notion databases request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Notion databases', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { extractTitleFromItem } from '@/tools/notion/utils'
|
||||
|
||||
const logger = createLogger('NotionPagesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.notion.com/v1/search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Notion-Version': '2022-06-28',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filter: { value: 'page', property: 'object' },
|
||||
page_size: 100,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Notion pages', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Notion pages', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const pages = (data.results || []).map((page: Record<string, unknown>) => ({
|
||||
id: page.id as string,
|
||||
name: extractTitleFromItem(page),
|
||||
}))
|
||||
|
||||
return NextResponse.json({ pages })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Notion pages request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Notion pages', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('PipedrivePipelinesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.pipedrive.com/v1/pipelines', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Pipedrive pipelines', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Pipedrive pipelines', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const pipelines = (data.data || []).map((pipeline: { id: number; name: string }) => ({
|
||||
id: String(pipeline.id),
|
||||
name: pipeline.name,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ pipelines })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Pipedrive pipelines request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Pipedrive pipelines', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateSharePointSiteId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SharePointListsAPI')
|
||||
|
||||
interface SharePointList {
|
||||
id: string
|
||||
displayName: string
|
||||
description?: string
|
||||
webUrl?: string
|
||||
list?: {
|
||||
hidden?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, siteId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error(`[${requestId}] Missing credential in request`)
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const siteIdValidation = validateSharePointSiteId(siteId)
|
||||
if (!siteIdValidation.isValid) {
|
||||
logger.error(`[${requestId}] Invalid siteId: ${siteIdValidation.error}`)
|
||||
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to obtain valid access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const url = `https://graph.microsoft.com/v1.0/sites/${siteIdValidation.sanitized}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=100`
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
|
||||
return NextResponse.json(
|
||||
{ error: errorData.error?.message || 'Failed to fetch lists from SharePoint' },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const lists = (data.value || [])
|
||||
.filter((list: SharePointList) => list.list?.hidden !== true)
|
||||
.map((list: SharePointList) => ({
|
||||
id: list.id,
|
||||
displayName: list.displayName,
|
||||
}))
|
||||
|
||||
logger.info(`[${requestId}] Successfully fetched ${lists.length} SharePoint lists`)
|
||||
return NextResponse.json({ lists }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching lists from SharePoint`, error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,79 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import type { SharepointSite } from '@/tools/sharepoint/types'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SharePointSitesAPI')
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
/**
|
||||
* Get SharePoint sites from Microsoft Graph API
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, query } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error(`[${requestId}] Missing credential in request`)
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const query = searchParams.get('query') || ''
|
||||
|
||||
if (!credentialId) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (resolved.workspaceId) {
|
||||
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
|
||||
const perm = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
resolved.workspaceId
|
||||
)
|
||||
if (perm === null) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
if (!credentials.length) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const accountRow = credentials[0]
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error(`[${requestId}] Failed to obtain valid access token`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to obtain valid access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
const searchQuery = query || '*'
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('TrelloBoardsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const apiKey = process.env.TRELLO_API_KEY
|
||||
if (!apiKey) {
|
||||
logger.error('Trello API key not configured')
|
||||
return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 })
|
||||
}
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.trello.com/1/members/me/boards?key=${apiKey}&token=${accessToken}&fields=id,name,closed`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Trello boards', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Trello boards', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const boards = (data || []).map((board: { id: string; name: string; closed: boolean }) => ({
|
||||
id: board.id,
|
||||
name: board.name,
|
||||
closed: board.closed,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ boards })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Trello boards request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Trello boards', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('ZoomMeetingsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Could not retrieve access token', authRequired: true },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'https://api.zoom.us/v2/users/me/meetings?page_size=300&type=scheduled',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to fetch Zoom meetings', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Zoom meetings', details: errorData },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const meetings = (data.meetings || []).map((meeting: { id: number; topic: string }) => ({
|
||||
id: String(meeting.id),
|
||||
name: meeting.topic,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ meetings })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Zoom meetings request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Zoom meetings', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import {
|
||||
getProviderIdFromServiceId,
|
||||
getScopeDescription,
|
||||
OAUTH_PROVIDERS,
|
||||
type OAuthProvider,
|
||||
parseProvider,
|
||||
@@ -34,6 +33,318 @@ 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,7 +15,6 @@ 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'
|
||||
@@ -26,6 +25,7 @@ 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) => {
|
||||
|
||||
@@ -1969,8 +1969,9 @@ export const ToolInput = memo(function ToolInput({
|
||||
}
|
||||
|
||||
if (useSubBlocks && displaySubBlocks.length > 0) {
|
||||
const allBlockSubBlocks = toolBlock?.subBlocks || []
|
||||
const coveredParamIds = new Set(
|
||||
displaySubBlocks.flatMap((sb) => {
|
||||
allBlockSubBlocks.flatMap((sb) => {
|
||||
const ids = [sb.id]
|
||||
if (sb.canonicalParamId) ids.push(sb.canonicalParamId)
|
||||
const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
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 { 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'
|
||||
|
||||
@@ -15,7 +12,8 @@ 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`, `oauthCredential`).
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`).
|
||||
* The one special case is `oauthCredential` which maps to `credentialId`.
|
||||
*
|
||||
* @param blockId - The block containing the selector sub-block
|
||||
* @param subBlock - The sub-block config (must have `selectorKey` set)
|
||||
@@ -31,58 +29,53 @@ 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(resolvedDependencyValues)) {
|
||||
for (const [depKey, value] of Object.entries(dependencyValues)) {
|
||||
if (value === null || value === undefined) continue
|
||||
const strValue = String(value)
|
||||
if (!strValue) continue
|
||||
if (isReference(strValue)) continue
|
||||
|
||||
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
|
||||
if (SELECTOR_CONTEXT_FIELDS.has(canonicalParamId as keyof SelectorContext)) {
|
||||
context[canonicalParamId as keyof SelectorContext] = strValue
|
||||
|
||||
if (canonicalParamId === 'oauthCredential') {
|
||||
context.credentialId = strValue
|
||||
} else if (canonicalParamId in CONTEXT_FIELD_SET) {
|
||||
;(context as Record<string, unknown>)[canonicalParamId] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
}, [resolvedDependencyValues, canonicalIndex, workflowId, subBlock.mimeType])
|
||||
}, [dependencyValues, canonicalIndex, workflowId, subBlock.mimeType])
|
||||
|
||||
return {
|
||||
selectorKey: (subBlock.selectorKey ?? null) as SelectorKey | null,
|
||||
selectorContext,
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
disabled: finalDisabled || !subBlock.selectorKey,
|
||||
dependencyValues: resolvedDependencyValues,
|
||||
dependencyValues,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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 oauthCredential =
|
||||
const credentialId =
|
||||
authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '')
|
||||
return { ...context, oauthCredential }
|
||||
return { ...context, credentialId }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -549,48 +549,21 @@ const SubBlockRow = memo(function SubBlockRow({
|
||||
return typeof option === 'string' ? option : option.label
|
||||
}, [subBlock, rawValue])
|
||||
|
||||
const resolveContextValue = useCallback(
|
||||
(key: string): string | undefined => {
|
||||
const resolved = resolveDependencyValue(
|
||||
key,
|
||||
rawValues,
|
||||
canonicalIndex || buildCanonicalIndex([]),
|
||||
canonicalModeOverrides
|
||||
)
|
||||
return typeof resolved === 'string' && resolved.length > 0 ? resolved : undefined
|
||||
},
|
||||
[rawValues, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
|
||||
const domainValue = resolveContextValue('domain')
|
||||
const teamIdValue = resolveContextValue('teamId')
|
||||
const projectIdValue = resolveContextValue('projectId')
|
||||
const planIdValue = resolveContextValue('planId')
|
||||
const baseIdValue = resolveContextValue('baseId')
|
||||
const datasetIdValue = resolveContextValue('datasetId')
|
||||
const serviceDeskIdValue = resolveContextValue('serviceDeskId')
|
||||
const siteIdValue = resolveContextValue('siteId')
|
||||
const collectionIdValue = resolveContextValue('collectionId')
|
||||
const spreadsheetIdValue = resolveContextValue('spreadsheetId')
|
||||
const fileIdValue = resolveContextValue('fileId')
|
||||
const domainValue = getStringValue('domain')
|
||||
const teamIdValue = getStringValue('teamId')
|
||||
const projectIdValue = getStringValue('projectId')
|
||||
const planIdValue = getStringValue('planId')
|
||||
|
||||
const { displayName: selectorDisplayName } = useSelectorDisplayName({
|
||||
subBlock,
|
||||
value: rawValue,
|
||||
workflowId,
|
||||
oauthCredential: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
credentialId: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined,
|
||||
domain: domainValue,
|
||||
teamId: teamIdValue,
|
||||
projectId: projectIdValue,
|
||||
planId: planIdValue,
|
||||
baseId: baseIdValue,
|
||||
datasetId: datasetIdValue,
|
||||
serviceDeskId: serviceDeskIdValue,
|
||||
siteId: siteIdValue,
|
||||
collectionId: collectionIdValue,
|
||||
spreadsheetId: spreadsheetIdValue,
|
||||
fileId: fileIdValue,
|
||||
})
|
||||
|
||||
const { knowledgeBase: kbForDisplayName } = useKnowledgeBase(
|
||||
|
||||
@@ -667,18 +667,15 @@ describe.concurrent('Blocks Module', () => {
|
||||
const errors: string[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
// Exclude trigger-mode subBlocks — they operate in a separate rendering context
|
||||
// and their IDs don't participate in canonical param resolution
|
||||
const nonTriggerSubBlocks = block.subBlocks.filter((sb) => sb.mode !== 'trigger')
|
||||
const allSubBlockIds = new Set(nonTriggerSubBlocks.map((sb) => sb.id))
|
||||
const allSubBlockIds = new Set(block.subBlocks.map((sb) => sb.id))
|
||||
const canonicalParamIds = new Set(
|
||||
nonTriggerSubBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId)
|
||||
block.subBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId)
|
||||
)
|
||||
|
||||
for (const canonicalId of canonicalParamIds) {
|
||||
if (allSubBlockIds.has(canonicalId!)) {
|
||||
// Check if the matching subBlock also has a canonicalParamId pointing to itself
|
||||
const matchingSubBlock = nonTriggerSubBlocks.find(
|
||||
const matchingSubBlock = block.subBlocks.find(
|
||||
(sb) => sb.id === canonicalId && !sb.canonicalParamId
|
||||
)
|
||||
if (matchingSubBlock) {
|
||||
@@ -860,10 +857,6 @@ describe.concurrent('Blocks Module', () => {
|
||||
if (typeof subBlock.condition === 'function') {
|
||||
continue
|
||||
}
|
||||
// Skip trigger-mode subBlocks — they operate in a separate rendering context
|
||||
if (subBlock.mode === 'trigger') {
|
||||
continue
|
||||
}
|
||||
const conditionKey = serializeCondition(subBlock.condition)
|
||||
if (!canonicalByCondition.has(subBlock.canonicalParamId)) {
|
||||
canonicalByCondition.set(subBlock.canonicalParamId, new Set())
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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'
|
||||
@@ -129,7 +128,7 @@ Return ONLY the JSON array.`,
|
||||
serviceId: 'vertex-ai',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('vertex-ai'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
placeholder: 'Select Google Cloud account',
|
||||
required: true,
|
||||
condition: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -39,7 +38,13 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'airtable',
|
||||
requiredScopes: getScopesForService('airtable'),
|
||||
requiredScopes: [
|
||||
'data.records:read',
|
||||
'data.records:write',
|
||||
'schema.bases:read',
|
||||
'user.email:read',
|
||||
'webhook:manage',
|
||||
],
|
||||
placeholder: 'Select Airtable account',
|
||||
required: true,
|
||||
},
|
||||
@@ -52,51 +57,21 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'baseSelector',
|
||||
title: 'Base',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'baseId',
|
||||
serviceId: 'airtable',
|
||||
selectorKey: 'airtable.bases',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Airtable base',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: 'listBases', not: true },
|
||||
required: { field: 'operation', value: 'listBases', not: true },
|
||||
},
|
||||
{
|
||||
id: 'baseId',
|
||||
title: 'Base ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'baseId',
|
||||
placeholder: 'Enter your base ID (e.g., appXXXXXXXXXXXXXX)',
|
||||
mode: 'advanced',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'listBases', not: true },
|
||||
required: { field: 'operation', value: 'listBases', not: true },
|
||||
},
|
||||
{
|
||||
id: 'tableSelector',
|
||||
title: 'Table',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'tableId',
|
||||
serviceId: 'airtable',
|
||||
selectorKey: 'airtable.tables',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Airtable table',
|
||||
dependsOn: ['credential', 'baseSelector'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['listBases', 'listTables'], not: true },
|
||||
required: { field: 'operation', value: ['listBases', 'listTables'], not: true },
|
||||
},
|
||||
{
|
||||
id: 'tableId',
|
||||
title: 'Table ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'tableId',
|
||||
placeholder: 'Enter table ID (e.g., tblXXXXXXXXXXXXXX)',
|
||||
mode: 'advanced',
|
||||
dependsOn: ['credential', 'baseId'],
|
||||
condition: { field: 'operation', value: ['listBases', 'listTables'], not: true },
|
||||
required: { field: 'operation', value: ['listBases', 'listTables'], not: true },
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -37,7 +36,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'asana',
|
||||
requiredScopes: getScopesForService('asana'),
|
||||
requiredScopes: ['default'],
|
||||
placeholder: 'Select Asana account',
|
||||
},
|
||||
{
|
||||
@@ -49,31 +48,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'workspaceSelector',
|
||||
title: 'Workspace',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'workspace',
|
||||
serviceId: 'asana',
|
||||
selectorKey: 'asana.workspaces',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Asana workspace',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_task', 'get_projects', 'search_tasks'],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'workspace',
|
||||
title: 'Workspace GID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'workspace',
|
||||
required: true,
|
||||
placeholder: 'Enter Asana workspace GID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_task', 'get_projects', 'search_tasks'],
|
||||
@@ -101,29 +81,11 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
value: ['update_task', 'add_comment'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'getTasksWorkspaceSelector',
|
||||
title: 'Workspace',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'getTasks_workspace',
|
||||
serviceId: 'asana',
|
||||
selectorKey: 'asana.workspaces',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Asana workspace',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_task'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'getTasks_workspace',
|
||||
title: 'Workspace GID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'getTasks_workspace',
|
||||
placeholder: 'Enter workspace GID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_task'],
|
||||
|
||||
@@ -86,47 +86,11 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
|
||||
},
|
||||
|
||||
// Record fields
|
||||
{
|
||||
id: 'objectTypeSelector',
|
||||
title: 'Object Type',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'objectType',
|
||||
serviceId: 'attio',
|
||||
selectorKey: 'attio.objects',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select object type',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_records',
|
||||
'get_record',
|
||||
'create_record',
|
||||
'update_record',
|
||||
'delete_record',
|
||||
'assert_record',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_records',
|
||||
'get_record',
|
||||
'create_record',
|
||||
'update_record',
|
||||
'delete_record',
|
||||
'assert_record',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'objectType',
|
||||
title: 'Object Type',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'objectType',
|
||||
placeholder: 'e.g. people, companies',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
@@ -560,49 +524,11 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
|
||||
},
|
||||
|
||||
// List fields
|
||||
{
|
||||
id: 'listSelector',
|
||||
title: 'List',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'listIdOrSlug',
|
||||
serviceId: 'attio',
|
||||
selectorKey: 'attio.lists',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Attio list',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_list',
|
||||
'update_list',
|
||||
'query_list_entries',
|
||||
'get_list_entry',
|
||||
'create_list_entry',
|
||||
'update_list_entry',
|
||||
'delete_list_entry',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_list',
|
||||
'update_list',
|
||||
'query_list_entries',
|
||||
'get_list_entry',
|
||||
'create_list_entry',
|
||||
'update_list_entry',
|
||||
'delete_list_entry',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'listIdOrSlug',
|
||||
title: 'List ID or Slug',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'listIdOrSlug',
|
||||
placeholder: 'Enter the list ID or slug',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
|
||||
@@ -65,30 +65,11 @@ export const CalComBlock: BlockConfig<ToolResponse> = {
|
||||
},
|
||||
|
||||
// === Create Booking fields ===
|
||||
{
|
||||
id: 'eventTypeSelector',
|
||||
title: 'Event Type',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'eventTypeId',
|
||||
serviceId: 'calcom',
|
||||
selectorKey: 'calcom.eventTypes',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select event type',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_create_booking', 'calcom_get_slots'],
|
||||
},
|
||||
required: { field: 'operation', value: 'calcom_create_booking' },
|
||||
},
|
||||
{
|
||||
id: 'eventTypeId',
|
||||
title: 'Event Type ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'eventTypeId',
|
||||
placeholder: 'Enter event type ID (number)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_create_booking', 'calcom_get_slots'],
|
||||
@@ -280,33 +261,11 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
|
||||
},
|
||||
|
||||
// === Event Type fields ===
|
||||
{
|
||||
id: 'eventTypeParamSelector',
|
||||
title: 'Event Type',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'eventTypeIdParam',
|
||||
serviceId: 'calcom',
|
||||
selectorKey: 'calcom.eventTypes',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select event type',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'eventTypeIdParam',
|
||||
title: 'Event Type ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'eventTypeIdParam',
|
||||
placeholder: 'Enter event type ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_event_type', 'calcom_update_event_type', 'calcom_delete_event_type'],
|
||||
@@ -405,27 +364,10 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'eventTypeScheduleSelector',
|
||||
title: 'Schedule',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'eventTypeScheduleId',
|
||||
serviceId: 'calcom',
|
||||
selectorKey: 'calcom.schedules',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select schedule',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_create_event_type', 'calcom_update_event_type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'eventTypeScheduleId',
|
||||
title: 'Schedule ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'eventTypeScheduleId',
|
||||
placeholder: 'Assign to a specific schedule',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -446,33 +388,11 @@ Return ONLY the IANA timezone string - no explanations or quotes.`,
|
||||
},
|
||||
|
||||
// === Schedule fields ===
|
||||
{
|
||||
id: 'scheduleSelector',
|
||||
title: 'Schedule',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'scheduleId',
|
||||
serviceId: 'calcom',
|
||||
selectorKey: 'calcom.schedules',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select schedule',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scheduleId',
|
||||
title: 'Schedule ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'scheduleId',
|
||||
placeholder: 'Enter schedule ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calcom_get_schedule', 'calcom_update_schedule', 'calcom_delete_schedule'],
|
||||
@@ -851,10 +771,7 @@ Return ONLY valid JSON - no explanations.`,
|
||||
cancellationReason: { type: 'string', description: 'Reason for cancellation' },
|
||||
reschedulingReason: { type: 'string', description: 'Reason for rescheduling' },
|
||||
bookingStatus: { type: 'string', description: 'Filter by booking status' },
|
||||
eventTypeIdParam: {
|
||||
type: 'number',
|
||||
description: 'Event type ID for get/update/delete',
|
||||
},
|
||||
eventTypeIdParam: { type: 'number', description: 'Event type ID for get/update/delete' },
|
||||
title: { type: 'string', description: 'Event type title' },
|
||||
slug: { type: 'string', description: 'URL-friendly slug' },
|
||||
eventLength: { type: 'number', description: 'Event duration in minutes' },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -56,7 +55,37 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'confluence',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
},
|
||||
@@ -435,7 +464,45 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'confluence',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
},
|
||||
@@ -578,44 +645,11 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'spaceSelector',
|
||||
title: 'Space',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'spaceId',
|
||||
serviceId: 'confluence',
|
||||
selectorKey: 'confluence.spaces',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Confluence space',
|
||||
dependsOn: ['credential', 'domain'],
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create',
|
||||
'get_space',
|
||||
'update_space',
|
||||
'delete_space',
|
||||
'list_pages_in_space',
|
||||
'search_in_space',
|
||||
'create_blogpost',
|
||||
'list_blogposts_in_space',
|
||||
'list_space_labels',
|
||||
'list_space_permissions',
|
||||
'list_space_properties',
|
||||
'create_space_property',
|
||||
'delete_space_property',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'spaceId',
|
||||
title: 'Space ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spaceId',
|
||||
placeholder: 'Enter Confluence space ID',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -1216,6 +1250,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
...rest
|
||||
} = params
|
||||
|
||||
// Use canonical param (serializer already handles basic/advanced mode)
|
||||
const effectivePageId = pageId ? String(pageId).trim() : ''
|
||||
|
||||
if (operation === 'add_label') {
|
||||
@@ -1476,7 +1511,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
domain: { type: 'string', description: 'Confluence domain' },
|
||||
oauthCredential: { type: 'string', description: 'Confluence access token' },
|
||||
pageId: { type: 'string', description: 'Page identifier' },
|
||||
pageId: { type: 'string', description: 'Page identifier (canonical param)' },
|
||||
spaceId: { type: 'string', description: 'Space identifier' },
|
||||
blogPostId: { type: 'string', description: 'Blog post identifier' },
|
||||
versionNumber: { type: 'number', description: 'Page version number' },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -42,7 +41,15 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'dropbox',
|
||||
requiredScopes: getScopesForService('dropbox'),
|
||||
requiredScopes: [
|
||||
'account_info.read',
|
||||
'files.metadata.read',
|
||||
'files.metadata.write',
|
||||
'files.content.read',
|
||||
'files.content.write',
|
||||
'sharing.read',
|
||||
'sharing.write',
|
||||
],
|
||||
placeholder: 'Select Dropbox account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -80,7 +79,11 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'gmail',
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/gmail.send',
|
||||
'https://www.googleapis.com/auth/gmail.modify',
|
||||
'https://www.googleapis.com/auth/gmail.labels',
|
||||
],
|
||||
placeholder: 'Select Gmail account',
|
||||
required: true,
|
||||
},
|
||||
@@ -219,7 +222,7 @@ Return ONLY the email body - no explanations, no extra text.`,
|
||||
canonicalParamId: 'folder',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select Gmail label/folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -300,7 +303,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'addLabelIds',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select destination label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -326,7 +329,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'removeLabelIds',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select label to remove',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -379,7 +382,7 @@ Return ONLY the search query - no explanations, no extra text.`,
|
||||
canonicalParamId: 'manageLabelId',
|
||||
serviceId: 'gmail',
|
||||
selectorKey: 'gmail.labels',
|
||||
requiredScopes: getScopesForService('gmail'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
|
||||
placeholder: 'Select label',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GoogleBigQueryIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -37,7 +36,7 @@ export const GoogleBigQueryBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-bigquery',
|
||||
requiredScopes: getScopesForService('google-bigquery'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/bigquery'],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
@@ -110,52 +109,20 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`,
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'datasetSelector',
|
||||
title: 'Dataset',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'datasetId',
|
||||
serviceId: 'google-bigquery',
|
||||
selectorKey: 'bigquery.datasets',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select BigQuery dataset',
|
||||
dependsOn: ['credential', 'projectId'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
|
||||
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
|
||||
},
|
||||
{
|
||||
id: 'datasetId',
|
||||
title: 'Dataset ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'datasetId',
|
||||
placeholder: 'Enter BigQuery dataset ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
|
||||
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'tableSelector',
|
||||
title: 'Table',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'tableId',
|
||||
serviceId: 'google-bigquery',
|
||||
selectorKey: 'bigquery.tables',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select BigQuery table',
|
||||
dependsOn: ['credential', 'projectId', 'datasetSelector'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['get_table', 'insert_rows'] },
|
||||
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
|
||||
},
|
||||
{
|
||||
id: 'tableId',
|
||||
title: 'Table ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'tableId',
|
||||
placeholder: 'Enter BigQuery table ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['get_table', 'insert_rows'] },
|
||||
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -44,7 +43,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-calendar',
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select Google Calendar account',
|
||||
},
|
||||
{
|
||||
@@ -65,7 +64,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
serviceId: 'google-calendar',
|
||||
selectorKey: 'google.calendar',
|
||||
selectorAllowSearch: false,
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select calendar',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -331,7 +330,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
serviceId: 'google-calendar',
|
||||
selectorKey: 'google.calendar',
|
||||
selectorAllowSearch: false,
|
||||
requiredScopes: getScopesForService('google-calendar'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select destination calendar',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'move' },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -38,7 +37,7 @@ export const GoogleContactsBlock: BlockConfig<GoogleContactsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-contacts',
|
||||
requiredScopes: getScopesForService('google-contacts'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/contacts'],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -37,7 +36,10 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-docs',
|
||||
requiredScopes: getScopesForService('google-docs'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -49,7 +48,10 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google Drive account',
|
||||
},
|
||||
{
|
||||
@@ -136,7 +138,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'uploadFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
mode: 'basic',
|
||||
@@ -206,7 +211,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'createFolderParentId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
mode: 'basic',
|
||||
@@ -231,7 +239,10 @@ Return ONLY the file content - no explanations, no markdown code blocks, no extr
|
||||
canonicalParamId: 'listFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select a folder to list files from',
|
||||
mode: 'basic',
|
||||
@@ -288,7 +299,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'downloadFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to download',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -347,7 +361,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'getFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to get info for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -372,7 +389,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'copyFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to copy',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -403,7 +423,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'copyDestFolderId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select destination folder (optional)',
|
||||
mode: 'basic',
|
||||
@@ -427,7 +450,10 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
canonicalParamId: 'updateFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to update',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -503,7 +529,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'trashFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to move to trash',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -528,7 +557,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'deleteFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to permanently delete',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -553,7 +585,10 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
canonicalParamId: 'shareFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to share',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -665,7 +700,10 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
canonicalParamId: 'unshareFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to remove sharing from',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
@@ -698,7 +736,10 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
canonicalParamId: 'listPermissionsFileId',
|
||||
serviceId: 'google-drive',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to list permissions for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GoogleFormsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -39,7 +38,13 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-forms',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GoogleGroupsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -47,7 +46,10 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-groups',
|
||||
requiredScopes: getScopesForService('google-groups'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/admin.directory.group',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
placeholder: 'Select Google Workspace account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -38,7 +37,10 @@ export const GoogleMeetBlock: BlockConfig<GoogleMeetResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-meet',
|
||||
requiredScopes: getScopesForService('google-meet'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly',
|
||||
],
|
||||
placeholder: 'Select Google Meet account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -41,7 +40,10 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
@@ -61,7 +63,10 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
@@ -334,7 +339,10 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
@@ -354,7 +362,10 @@ export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
selectorKey: 'google.drive',
|
||||
requiredScopes: getScopesForService('google-sheets'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -51,7 +50,10 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: getScopesForService('google-drive'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -39,7 +38,7 @@ export const GoogleTasksBlock: BlockConfig<GoogleTasksResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-tasks',
|
||||
requiredScopes: getScopesForService('google-tasks'),
|
||||
requiredScopes: ['https://www.googleapis.com/auth/tasks'],
|
||||
placeholder: 'Select Google Tasks account',
|
||||
},
|
||||
{
|
||||
@@ -52,27 +51,12 @@ export const GoogleTasksBlock: BlockConfig<GoogleTasksResponse> = {
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Task List - shown for all task operations (not list_task_lists)
|
||||
{
|
||||
id: 'taskListSelector',
|
||||
title: 'Task List',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'taskListId',
|
||||
serviceId: 'google-tasks',
|
||||
selectorKey: 'google.tasks.lists',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select task list',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: 'list_task_lists', not: true },
|
||||
},
|
||||
// Task List ID - shown for all task operations (not list_task_lists)
|
||||
{
|
||||
id: 'taskListId',
|
||||
title: 'Task List ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'taskListId',
|
||||
placeholder: 'Task list ID (leave empty for default list)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list_task_lists', not: true },
|
||||
},
|
||||
|
||||
@@ -226,9 +210,7 @@ Return ONLY the timestamp - no explanations, no extra text.`,
|
||||
params: (params) => {
|
||||
const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params
|
||||
|
||||
const processedParams: Record<string, unknown> = {
|
||||
...rest,
|
||||
}
|
||||
const processedParams: Record<string, unknown> = { ...rest }
|
||||
|
||||
if (maxResults && typeof maxResults === 'string') {
|
||||
processedParams.maxResults = Number.parseInt(maxResults, 10)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GoogleVaultIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -39,7 +38,10 @@ export const GoogleVaultBlock: BlockConfig = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-vault',
|
||||
requiredScopes: getScopesForService('google-vault'),
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
placeholder: 'Select Google Vault account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -43,7 +42,31 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'hubspot',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select HubSpot account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -65,7 +64,38 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'jira',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Jira account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -60,7 +59,42 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'jira',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Jira account',
|
||||
},
|
||||
{
|
||||
@@ -72,52 +106,11 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'serviceDeskSelector',
|
||||
title: 'Service Desk',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'serviceDeskId',
|
||||
serviceId: 'jira',
|
||||
selectorKey: 'jsm.serviceDesks',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select service desk',
|
||||
dependsOn: ['credential', 'domain'],
|
||||
mode: 'basic',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_request_types',
|
||||
'create_request',
|
||||
'get_customers',
|
||||
'add_customer',
|
||||
'get_organizations',
|
||||
'add_organization',
|
||||
'get_queues',
|
||||
'get_request_type_fields',
|
||||
],
|
||||
},
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_request_types',
|
||||
'create_request',
|
||||
'get_customers',
|
||||
'add_customer',
|
||||
'get_organizations',
|
||||
'add_organization',
|
||||
'get_queues',
|
||||
'get_requests',
|
||||
'get_request_type_fields',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'serviceDeskId',
|
||||
title: 'Service Desk ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'serviceDeskId',
|
||||
placeholder: 'Enter service desk ID',
|
||||
mode: 'advanced',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
@@ -146,28 +139,12 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'requestTypeSelector',
|
||||
title: 'Request Type',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'requestTypeId',
|
||||
serviceId: 'jira',
|
||||
selectorKey: 'jsm.requestTypes',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select request type',
|
||||
dependsOn: ['credential', 'domain', 'serviceDeskSelector'],
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] },
|
||||
},
|
||||
{
|
||||
id: 'requestTypeId',
|
||||
title: 'Request Type ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'requestTypeId',
|
||||
required: true,
|
||||
placeholder: 'Enter request type ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['create_request', 'get_request_type_fields'] },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -133,7 +132,7 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'linear',
|
||||
requiredScopes: getScopesForService('linear'),
|
||||
requiredScopes: ['read', 'write'],
|
||||
placeholder: 'Select Linear account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -36,7 +35,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
|
||||
serviceId: 'linkedin',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('linkedin'),
|
||||
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
|
||||
placeholder: 'Select LinkedIn account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -47,7 +46,13 @@ export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
|
||||
title: 'Microsoft Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'microsoft-dataverse',
|
||||
requiredScopes: getScopesForService('microsoft-dataverse'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -40,7 +39,14 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: getScopesForService('microsoft-excel'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
@@ -360,7 +366,14 @@ export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: getScopesForService('microsoft-excel'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -65,7 +64,15 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-planner',
|
||||
requiredScopes: getScopesForService('microsoft-planner'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Group.ReadWrite.All',
|
||||
'Group.Read.All',
|
||||
'Tasks.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
@@ -77,36 +84,12 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
placeholder: 'Enter credential ID',
|
||||
},
|
||||
|
||||
// Plan selector - basic mode
|
||||
{
|
||||
id: 'planSelector',
|
||||
title: 'Plan',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'planId',
|
||||
serviceId: 'microsoft-planner',
|
||||
selectorKey: 'microsoft.planner.plans',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select a plan',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'],
|
||||
},
|
||||
},
|
||||
|
||||
// Plan ID - advanced mode
|
||||
// Plan ID - for various operations
|
||||
{
|
||||
id: 'planId',
|
||||
title: 'Plan ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'planId',
|
||||
placeholder: 'Enter the plan ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'],
|
||||
@@ -127,7 +110,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
|
||||
serviceId: 'microsoft-planner',
|
||||
selectorKey: 'microsoft.planner',
|
||||
condition: { field: 'operation', value: ['read_task'] },
|
||||
dependsOn: ['credential', 'planSelector'],
|
||||
dependsOn: ['credential', 'planId'],
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'readTaskId',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -48,7 +47,28 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'microsoft-teams',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -53,49 +53,15 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'pageSelector',
|
||||
title: 'Page',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'pageId',
|
||||
serviceId: 'notion',
|
||||
selectorKey: 'notion.pages',
|
||||
placeholder: 'Select Notion page',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['notion_read', 'notion_write'],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
// Read/Write operation - Page ID
|
||||
{
|
||||
id: 'pageId',
|
||||
title: 'Page ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'pageId',
|
||||
placeholder: 'Enter Notion page ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['notion_read', 'notion_write'],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'databaseSelector',
|
||||
title: 'Database',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'databaseId',
|
||||
serviceId: 'notion',
|
||||
selectorKey: 'notion.databases',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Notion database',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'],
|
||||
value: 'notion_read',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
@@ -103,36 +69,31 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
id: 'databaseId',
|
||||
title: 'Database ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'databaseId',
|
||||
placeholder: 'Enter Notion database ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['notion_read_database', 'notion_query_database', 'notion_add_database_row'],
|
||||
value: 'notion_read_database',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'parentSelector',
|
||||
title: 'Parent Page',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'parentId',
|
||||
serviceId: 'notion',
|
||||
selectorKey: 'notion.pages',
|
||||
placeholder: 'Select parent page',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] },
|
||||
id: 'pageId',
|
||||
title: 'Page ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Notion page ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'notion_write',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
// Create operation fields
|
||||
{
|
||||
id: 'parentId',
|
||||
title: 'Parent Page ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'parentId',
|
||||
placeholder: 'ID of parent page',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['notion_create_page', 'notion_create_database'] },
|
||||
condition: { field: 'operation', value: 'notion_create_page' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@@ -187,6 +148,14 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
},
|
||||
},
|
||||
// Query Database Fields
|
||||
{
|
||||
id: 'databaseId',
|
||||
title: 'Database ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Notion database ID',
|
||||
condition: { field: 'operation', value: 'notion_query_database' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'filter',
|
||||
title: 'Filter',
|
||||
@@ -249,6 +218,14 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
condition: { field: 'operation', value: 'notion_search' },
|
||||
},
|
||||
// Create Database Fields
|
||||
{
|
||||
id: 'parentId',
|
||||
title: 'Parent Page ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'ID of parent page where database will be created',
|
||||
condition: { field: 'operation', value: 'notion_create_database' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Database Title',
|
||||
@@ -279,6 +256,14 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
},
|
||||
},
|
||||
// Add Database Row Fields
|
||||
{
|
||||
id: 'databaseId',
|
||||
title: 'Database ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Notion database ID',
|
||||
condition: { field: 'operation', value: 'notion_add_database_row' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
title: 'Row Properties',
|
||||
@@ -419,7 +404,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
|
||||
}
|
||||
|
||||
// V2 Block with API-aligned outputs
|
||||
|
||||
export const NotionV2Block: BlockConfig<any> = {
|
||||
type: 'notion_v2',
|
||||
name: 'Notion',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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'
|
||||
@@ -43,7 +42,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'onedrive',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
@@ -150,7 +156,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'uploadFolderId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
dependsOn: ['credential'],
|
||||
@@ -181,7 +194,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'createFolderParentId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a parent folder',
|
||||
dependsOn: ['credential'],
|
||||
@@ -207,7 +227,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'listFolderId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.folders',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a folder to list files from',
|
||||
dependsOn: ['credential'],
|
||||
@@ -247,7 +274,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'downloadFileId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.files',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'file', // Exclude folders, show only files
|
||||
placeholder: 'Select a file to download',
|
||||
mode: 'basic',
|
||||
@@ -281,7 +315,14 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
|
||||
canonicalParamId: 'deleteFileId',
|
||||
serviceId: 'onedrive',
|
||||
selectorKey: 'onedrive.files',
|
||||
requiredScopes: getScopesForService('onedrive'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'file', // Exclude folders, show only files
|
||||
placeholder: 'Select a file to delete',
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -43,7 +42,16 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'outlook',
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
requiredScopes: [
|
||||
'Mail.ReadWrite',
|
||||
'Mail.ReadBasic',
|
||||
'Mail.Read',
|
||||
'Mail.Send',
|
||||
'offline_access',
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
@@ -180,7 +188,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'folder',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
placeholder: 'Select Outlook folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -226,7 +234,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'destinationId',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
placeholder: 'Select destination folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
@@ -273,7 +281,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
||||
canonicalParamId: 'copyDestinationId',
|
||||
serviceId: 'outlook',
|
||||
selectorKey: 'outlook.folders',
|
||||
requiredScopes: getScopesForService('outlook'),
|
||||
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
|
||||
placeholder: 'Select destination folder',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -49,7 +48,15 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'pipedrive',
|
||||
requiredScopes: getScopesForService('pipedrive'),
|
||||
requiredScopes: [
|
||||
'base',
|
||||
'deals:full',
|
||||
'contacts:full',
|
||||
'leads:full',
|
||||
'activities:full',
|
||||
'mail:full',
|
||||
'projects:full',
|
||||
],
|
||||
placeholder: 'Select Pipedrive account',
|
||||
required: true,
|
||||
},
|
||||
@@ -89,35 +96,12 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
|
||||
placeholder: 'Filter by organization ID',
|
||||
condition: { field: 'operation', value: ['get_all_deals'] },
|
||||
},
|
||||
{
|
||||
id: 'pipelineSelector',
|
||||
title: 'Pipeline',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'pipeline_id',
|
||||
serviceId: 'pipedrive',
|
||||
selectorKey: 'pipedrive.pipelines',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select pipeline',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'],
|
||||
},
|
||||
required: { field: 'operation', value: 'get_pipeline_deals' },
|
||||
},
|
||||
{
|
||||
id: 'pipeline_id',
|
||||
title: 'Pipeline ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'pipeline_id',
|
||||
placeholder: 'Enter pipeline ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_all_deals', 'create_deal', 'get_pipeline_deals'],
|
||||
},
|
||||
required: { field: 'operation', value: 'get_pipeline_deals' },
|
||||
placeholder: 'Filter by pipeline ID ',
|
||||
condition: { field: 'operation', value: ['get_all_deals'] },
|
||||
},
|
||||
{
|
||||
id: 'updated_since',
|
||||
@@ -190,6 +174,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Associated organization ID ',
|
||||
condition: { field: 'operation', value: ['create_deal'] },
|
||||
},
|
||||
{
|
||||
id: 'pipeline_id',
|
||||
title: 'Pipeline ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pipeline ID ',
|
||||
condition: { field: 'operation', value: ['create_deal'] },
|
||||
},
|
||||
{
|
||||
id: 'stage_id',
|
||||
title: 'Stage ID',
|
||||
@@ -338,6 +329,14 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pipeline_id',
|
||||
title: 'Pipeline ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter pipeline ID',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: ['get_pipeline_deals'] },
|
||||
},
|
||||
{
|
||||
id: 'stage_id',
|
||||
title: 'Stage ID',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -50,7 +49,24 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
|
||||
serviceId: 'reddit',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('reddit'),
|
||||
requiredScopes: [
|
||||
'identity',
|
||||
'read',
|
||||
'submit',
|
||||
'vote',
|
||||
'save',
|
||||
'edit',
|
||||
'subscribe',
|
||||
'history',
|
||||
'privatemessages',
|
||||
'account',
|
||||
'mysubreddits',
|
||||
'flair',
|
||||
'report',
|
||||
'modposts',
|
||||
'modflair',
|
||||
'modmail',
|
||||
],
|
||||
placeholder: 'Select Reddit account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -66,7 +65,7 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'salesforce',
|
||||
requiredScopes: getScopesForService('salesforce'),
|
||||
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
placeholder: 'Select Salesforce account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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'
|
||||
@@ -42,7 +41,15 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'sharepoint',
|
||||
requiredScopes: getScopesForService('sharepoint'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Sites.Read.All',
|
||||
'Sites.ReadWrite.All',
|
||||
'Sites.Manage.All',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
},
|
||||
{
|
||||
@@ -61,7 +68,14 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
canonicalParamId: 'siteId',
|
||||
serviceId: 'sharepoint',
|
||||
selectorKey: 'sharepoint.sites',
|
||||
requiredScopes: getScopesForService('sharepoint'),
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
mimeType: 'application/vnd.microsoft.graph.folder',
|
||||
placeholder: 'Select a site',
|
||||
dependsOn: ['credential'],
|
||||
@@ -98,26 +112,12 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'listSelector',
|
||||
title: 'List',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'listId',
|
||||
serviceId: 'sharepoint',
|
||||
selectorKey: 'sharepoint.lists',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select a list',
|
||||
dependsOn: ['credential', 'siteSelector'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
|
||||
},
|
||||
{
|
||||
id: 'listId',
|
||||
title: 'List ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'listId',
|
||||
placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'listId',
|
||||
condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] },
|
||||
},
|
||||
|
||||
@@ -425,9 +425,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
includeColumns,
|
||||
includeItems,
|
||||
files, // canonical param from uploadFiles (basic) or files (advanced)
|
||||
driveId, // canonical param from driveId
|
||||
columnDefinitions,
|
||||
listId,
|
||||
...others
|
||||
} = rest as any
|
||||
|
||||
@@ -459,7 +457,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
try {
|
||||
logger.info('SharepointBlock list item param check', {
|
||||
siteId: effectiveSiteId || undefined,
|
||||
listId: listId,
|
||||
listId: (others as any)?.listId,
|
||||
listTitle: (others as any)?.listTitle,
|
||||
itemId: sanitizedItemId,
|
||||
hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object',
|
||||
@@ -479,8 +477,6 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
...others,
|
||||
...(listId ? { listId } : {}),
|
||||
...(driveId ? { driveId } : {}),
|
||||
itemId: sanitizedItemId,
|
||||
listItemFields: parsedItemFields,
|
||||
includeColumns: coerceBoolean(includeColumns),
|
||||
@@ -521,13 +517,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
includeItems: { type: 'boolean', description: 'Include items in response' },
|
||||
itemId: { type: 'string', description: 'List item ID (canonical param)' },
|
||||
listItemFields: { type: 'string', description: 'List item fields (canonical param)' },
|
||||
driveId: {
|
||||
type: 'string',
|
||||
description: 'Document library (drive) ID',
|
||||
},
|
||||
driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' },
|
||||
folderPath: { type: 'string', description: 'Folder path for file upload' },
|
||||
fileName: { type: 'string', description: 'File name override' },
|
||||
files: { type: 'array', description: 'Files to upload' },
|
||||
files: { type: 'array', description: 'Files to upload (canonical param)' },
|
||||
},
|
||||
outputs: {
|
||||
sites: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ShopifyIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -64,7 +63,14 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
|
||||
serviceId: 'shopify',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('shopify'),
|
||||
requiredScopes: [
|
||||
'write_products',
|
||||
'write_orders',
|
||||
'write_customers',
|
||||
'write_inventory',
|
||||
'read_locations',
|
||||
'write_merchant_managed_fulfillment_orders',
|
||||
],
|
||||
placeholder: 'Select Shopify account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -83,7 +82,22 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'slack',
|
||||
requiredScopes: getScopesForService('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',
|
||||
],
|
||||
placeholder: 'Select Slack workspace',
|
||||
dependsOn: ['authMethod'],
|
||||
condition: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -45,7 +44,7 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
serviceId: 'trello',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('trello'),
|
||||
requiredScopes: ['read', 'write'],
|
||||
placeholder: 'Select Trello account',
|
||||
required: true,
|
||||
},
|
||||
@@ -60,50 +59,26 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
},
|
||||
|
||||
{
|
||||
id: 'boardSelector',
|
||||
id: 'boardId',
|
||||
title: 'Board',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'boardId',
|
||||
serviceId: 'trello',
|
||||
selectorKey: 'trello.boards',
|
||||
selectorAllowSearch: false,
|
||||
placeholder: 'Select Trello board',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter board ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'trello_list_lists',
|
||||
'trello_list_cards',
|
||||
'trello_create_card',
|
||||
'trello_get_actions',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'],
|
||||
value: 'trello_list_lists',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'boardId',
|
||||
title: 'Board ID',
|
||||
title: 'Board',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'boardId',
|
||||
placeholder: 'Enter board ID',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Enter board ID or search for a board',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'trello_list_lists',
|
||||
'trello_list_cards',
|
||||
'trello_create_card',
|
||||
'trello_get_actions',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['trello_list_lists', 'trello_list_cards', 'trello_create_card'],
|
||||
value: 'trello_list_cards',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'listId',
|
||||
@@ -115,6 +90,17 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
|
||||
value: 'trello_list_cards',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'boardId',
|
||||
title: 'Board',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter board ID or search for a board',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'trello_create_card',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'listId',
|
||||
title: 'List',
|
||||
@@ -292,6 +278,16 @@ Return ONLY the date/timestamp string - no explanations, no quotes, no extra tex
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'boardId',
|
||||
title: 'Board ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter board ID to get board actions',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'trello_get_actions',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cardId',
|
||||
title: 'Card ID',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -37,7 +36,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'wealthbox',
|
||||
requiredScopes: getScopesForService('wealthbox'),
|
||||
requiredScopes: ['login', 'data'],
|
||||
placeholder: 'Select Wealthbox account',
|
||||
required: true,
|
||||
},
|
||||
@@ -63,7 +62,7 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
|
||||
type: 'file-selector',
|
||||
serviceId: 'wealthbox',
|
||||
selectorKey: 'wealthbox.contacts',
|
||||
requiredScopes: getScopesForService('wealthbox'),
|
||||
requiredScopes: ['login', 'data'],
|
||||
placeholder: 'Enter Contact ID',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'contactId',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -38,7 +37,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'webflow',
|
||||
requiredScopes: getScopesForService('webflow'),
|
||||
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
|
||||
placeholder: 'Select Webflow account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -69,7 +68,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
serviceId: 'wordpress',
|
||||
requiredScopes: getScopesForService('wordpress'),
|
||||
requiredScopes: ['global'],
|
||||
placeholder: 'Select WordPress account',
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { xIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
@@ -67,7 +66,23 @@ export const XBlock: BlockConfig = {
|
||||
serviceId: 'x',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('x'),
|
||||
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',
|
||||
],
|
||||
placeholder: 'Select X account',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -41,7 +40,19 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
serviceId: 'zoom',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
requiredScopes: getScopesForService('zoom'),
|
||||
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',
|
||||
],
|
||||
placeholder: 'Select Zoom account',
|
||||
required: true,
|
||||
},
|
||||
@@ -66,39 +77,12 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
value: ['zoom_create_meeting', 'zoom_list_meetings', 'zoom_list_recordings'],
|
||||
},
|
||||
},
|
||||
// Meeting selector for get/update/delete/invitation/recordings/participants operations
|
||||
{
|
||||
id: 'meetingSelector',
|
||||
title: 'Meeting',
|
||||
type: 'project-selector',
|
||||
canonicalParamId: 'meetingId',
|
||||
serviceId: 'zoom',
|
||||
selectorKey: 'zoom.meetings',
|
||||
selectorAllowSearch: true,
|
||||
placeholder: 'Select Zoom meeting',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'zoom_get_meeting',
|
||||
'zoom_update_meeting',
|
||||
'zoom_delete_meeting',
|
||||
'zoom_get_meeting_invitation',
|
||||
'zoom_get_meeting_recordings',
|
||||
'zoom_delete_recording',
|
||||
'zoom_list_past_participants',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Meeting ID for get/update/delete/invitation/recordings/participants operations
|
||||
{
|
||||
id: 'meetingId',
|
||||
title: 'Meeting ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'meetingId',
|
||||
placeholder: 'Enter meeting ID',
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -130,6 +114,7 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
|
||||
title: 'Topic',
|
||||
type: 'short-input',
|
||||
placeholder: 'Meeting topic (optional)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['zoom_update_meeting'],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,16 +7,38 @@ 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
|
||||
}
|
||||
|
||||
export function resolveSelectorForSubBlock(
|
||||
subBlock: SubBlockConfig,
|
||||
context: SelectorContext
|
||||
args: SelectorResolutionArgs
|
||||
): SelectorResolution | null {
|
||||
if (!subBlock.selectorKey) return null
|
||||
return {
|
||||
key: subBlock.selectorKey,
|
||||
context: {
|
||||
...context,
|
||||
mimeType: subBlock.mimeType ?? context.mimeType,
|
||||
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,
|
||||
mimeType: subBlock.mimeType,
|
||||
},
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
}
|
||||
|
||||
@@ -2,26 +2,6 @@ import type React from 'react'
|
||||
import type { QueryKey } from '@tanstack/react-query'
|
||||
|
||||
export type SelectorKey =
|
||||
| 'airtable.bases'
|
||||
| 'airtable.tables'
|
||||
| 'asana.workspaces'
|
||||
| 'attio.lists'
|
||||
| 'attio.objects'
|
||||
| 'bigquery.datasets'
|
||||
| 'bigquery.tables'
|
||||
| 'calcom.eventTypes'
|
||||
| 'calcom.schedules'
|
||||
| 'confluence.spaces'
|
||||
| 'google.tasks.lists'
|
||||
| 'jsm.requestTypes'
|
||||
| 'jsm.serviceDesks'
|
||||
| 'microsoft.planner.plans'
|
||||
| 'notion.databases'
|
||||
| 'notion.pages'
|
||||
| 'pipedrive.pipelines'
|
||||
| 'sharepoint.lists'
|
||||
| 'trello.boards'
|
||||
| 'zoom.meetings'
|
||||
| 'slack.channels'
|
||||
| 'slack.users'
|
||||
| 'gmail.labels'
|
||||
@@ -61,7 +41,7 @@ export interface SelectorOption {
|
||||
export interface SelectorContext {
|
||||
workspaceId?: string
|
||||
workflowId?: string
|
||||
oauthCredential?: string
|
||||
credentialId?: string
|
||||
serviceId?: string
|
||||
domain?: string
|
||||
teamId?: string
|
||||
@@ -74,9 +54,6 @@ export interface SelectorContext {
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
excludeWorkflowId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
}
|
||||
|
||||
export interface SelectorQueryArgs {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
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
|
||||
@@ -31,27 +29,14 @@ 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: resolvedDetailId,
|
||||
detailId: args.detailId,
|
||||
}
|
||||
const hasRealDetailId = Boolean(resolvedDetailId)
|
||||
const baseEnabled =
|
||||
hasRealDetailId && definition.fetchById !== undefined
|
||||
Boolean(args.detailId) && definition.fetchById !== undefined
|
||||
? definition.enabled
|
||||
? definition.enabled(queryArgs)
|
||||
: true
|
||||
@@ -59,7 +44,7 @@ export function useSelectorOptionDetail(
|
||||
const enabled = args.enabled ?? baseEnabled
|
||||
|
||||
const query = useQuery<SelectorOption | null>({
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'detail', resolvedDetailId ?? 'none'],
|
||||
queryKey: [...definition.getQueryKey(queryArgs), 'detail', args.detailId ?? 'none'],
|
||||
queryFn: () => definition.fetchById!(queryArgs),
|
||||
enabled,
|
||||
staleTime: definition.staleTime ?? 300_000,
|
||||
|
||||
92
apps/sim/hooks/use-oauth-scope-status.ts
Normal file
92
apps/sim/hooks/use-oauth-scope-status.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
'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,38 +12,24 @@ interface SelectorDisplayNameArgs {
|
||||
subBlock?: SubBlockConfig
|
||||
value: unknown
|
||||
workflowId?: string
|
||||
oauthCredential?: string
|
||||
credentialId?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
teamId?: string
|
||||
knowledgeBaseId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
siteId?: string
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
fileId?: string
|
||||
}
|
||||
|
||||
export function useSelectorDisplayName({
|
||||
subBlock,
|
||||
value,
|
||||
workflowId,
|
||||
oauthCredential,
|
||||
credentialId,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
teamId,
|
||||
knowledgeBaseId,
|
||||
baseId,
|
||||
datasetId,
|
||||
serviceDeskId,
|
||||
siteId,
|
||||
collectionId,
|
||||
spreadsheetId,
|
||||
fileId,
|
||||
}: SelectorDisplayNameArgs) {
|
||||
const detailId = typeof value === 'string' && value.length > 0 ? value : undefined
|
||||
|
||||
@@ -51,37 +37,23 @@ export function useSelectorDisplayName({
|
||||
if (!subBlock || !detailId) return null
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId,
|
||||
oauthCredential,
|
||||
credentialId,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
teamId,
|
||||
knowledgeBaseId,
|
||||
baseId,
|
||||
datasetId,
|
||||
serviceDeskId,
|
||||
siteId,
|
||||
collectionId,
|
||||
spreadsheetId,
|
||||
fileId,
|
||||
})
|
||||
}, [
|
||||
subBlock,
|
||||
detailId,
|
||||
workflowId,
|
||||
oauthCredential,
|
||||
credentialId,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
teamId,
|
||||
knowledgeBaseId,
|
||||
baseId,
|
||||
datasetId,
|
||||
serviceDeskId,
|
||||
siteId,
|
||||
collectionId,
|
||||
spreadsheetId,
|
||||
fileId,
|
||||
])
|
||||
|
||||
const key = resolution?.key
|
||||
|
||||
@@ -77,7 +77,6 @@ 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
|
||||
|
||||
@@ -763,7 +762,7 @@ export const auth = betterAuth({
|
||||
prompt: 'consent',
|
||||
tokenUrl: 'https://github.com/login/oauth/access_token',
|
||||
userInfoUrl: 'https://api.github.com/user',
|
||||
scopes: getCanonicalScopesForProvider('github-repo'),
|
||||
scopes: ['user:email', 'repo', 'read:user', 'workflow'],
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/github-repo`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
@@ -838,7 +837,13 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-email'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-email`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -874,7 +879,11 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-calendar'),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-calendar`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -910,7 +919,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-drive'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-drive`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -946,7 +960,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-docs'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-docs`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -982,7 +1001,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-sheets'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-sheets`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1019,7 +1043,11 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-contacts'),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/contacts',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-contacts`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1055,7 +1083,13 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-forms'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-forms`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1091,7 +1125,11 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-bigquery'),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/bigquery',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-bigquery`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1128,7 +1166,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-vault'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-vault`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1165,7 +1208,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-groups'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-groups`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1202,7 +1250,12 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-meet'),
|
||||
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',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-meet`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1238,7 +1291,11 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('google-tasks'),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/tasks',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-tasks`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1275,7 +1332,11 @@ export const auth = betterAuth({
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
|
||||
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('vertex-ai'),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/vertex-ai`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1313,7 +1374,28 @@ 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: getCanonicalScopesForProvider('microsoft-teams'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1353,7 +1435,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: getCanonicalScopesForProvider('microsoft-excel'),
|
||||
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1392,7 +1474,13 @@ 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: getCanonicalScopesForProvider('microsoft-dataverse'),
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1434,7 +1522,15 @@ 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: getCanonicalScopesForProvider('microsoft-planner'),
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Group.ReadWrite.All',
|
||||
'Group.Read.All',
|
||||
'Tasks.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1474,7 +1570,16 @@ 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: getCanonicalScopesForProvider('outlook'),
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Mail.ReadWrite',
|
||||
'Mail.ReadBasic',
|
||||
'Mail.Read',
|
||||
'Mail.Send',
|
||||
'offline_access',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1514,7 +1619,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: getCanonicalScopesForProvider('onedrive'),
|
||||
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1554,7 +1659,15 @@ 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: getCanonicalScopesForProvider('sharepoint'),
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Sites.Read.All',
|
||||
'Sites.ReadWrite.All',
|
||||
'Sites.Manage.All',
|
||||
'offline_access',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -1594,7 +1707,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: getCanonicalScopesForProvider('wealthbox'),
|
||||
scopes: ['login', 'data'],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wealthbox`,
|
||||
getUserInfo: async (_tokens) => {
|
||||
@@ -1627,7 +1740,15 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://oauth.pipedrive.com/oauth/token',
|
||||
userInfoUrl: 'https://api.pipedrive.com/v1/users/me',
|
||||
prompt: 'consent',
|
||||
scopes: getCanonicalScopesForProvider('pipedrive'),
|
||||
scopes: [
|
||||
'base',
|
||||
'deals:full',
|
||||
'contacts:full',
|
||||
'leads:full',
|
||||
'activities:full',
|
||||
'mail:full',
|
||||
'projects:full',
|
||||
],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/pipedrive`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -1676,7 +1797,31 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://api.hubapi.com/oauth/v1/token',
|
||||
userInfoUrl: 'https://api.hubapi.com/oauth/v1/access-tokens',
|
||||
prompt: 'consent',
|
||||
scopes: getCanonicalScopesForProvider('hubspot'),
|
||||
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',
|
||||
],
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/hubspot`,
|
||||
getUserInfo: async (tokens) => {
|
||||
try {
|
||||
@@ -1748,7 +1893,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: getCanonicalScopesForProvider('salesforce'),
|
||||
scopes: ['api', 'refresh_token', 'openid', 'offline_access'],
|
||||
pkce: true,
|
||||
prompt: 'consent',
|
||||
accessType: 'offline',
|
||||
@@ -1799,7 +1944,23 @@ export const auth = betterAuth({
|
||||
tokenUrl: 'https://api.x.com/2/oauth2/token',
|
||||
userInfoUrl: 'https://api.x.com/2/users/me',
|
||||
accessType: 'offline',
|
||||
scopes: getCanonicalScopesForProvider('x'),
|
||||
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',
|
||||
],
|
||||
pkce: true,
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
@@ -1858,7 +2019,45 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
scopes: getCanonicalScopesForProvider('confluence'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -1910,7 +2109,67 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
scopes: getCanonicalScopesForProvider('jira'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -1962,7 +2221,13 @@ 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: getCanonicalScopesForProvider('airtable'),
|
||||
scopes: [
|
||||
'data.records:read',
|
||||
'data.records:write',
|
||||
'schema.bases:read',
|
||||
'user.email:read',
|
||||
'webhook:manage',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
@@ -2062,7 +2327,24 @@ 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: getCanonicalScopesForProvider('reddit'),
|
||||
scopes: [
|
||||
'identity',
|
||||
'read',
|
||||
'submit',
|
||||
'vote',
|
||||
'save',
|
||||
'edit',
|
||||
'subscribe',
|
||||
'history',
|
||||
'privatemessages',
|
||||
'account',
|
||||
'mysubreddits',
|
||||
'flair',
|
||||
'report',
|
||||
'modposts',
|
||||
'modflair',
|
||||
'modmail',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: false,
|
||||
accessType: 'offline',
|
||||
@@ -2112,7 +2394,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: getCanonicalScopesForProvider('linear'),
|
||||
scopes: ['read', 'write'],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/linear`,
|
||||
pkce: true,
|
||||
@@ -2184,7 +2466,17 @@ 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: getCanonicalScopesForProvider('attio'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/attio`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -2237,7 +2529,15 @@ 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: getCanonicalScopesForProvider('dropbox'),
|
||||
scopes: [
|
||||
'account_info.read',
|
||||
'files.metadata.read',
|
||||
'files.metadata.write',
|
||||
'files.content.read',
|
||||
'files.content.write',
|
||||
'sharing.read',
|
||||
'sharing.write',
|
||||
],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/dropbox`,
|
||||
pkce: true,
|
||||
@@ -2293,7 +2593,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: getCanonicalScopesForProvider('asana'),
|
||||
scopes: ['default'],
|
||||
responseType: 'code',
|
||||
pkce: false,
|
||||
accessType: 'offline',
|
||||
@@ -2346,7 +2646,23 @@ 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: getCanonicalScopesForProvider('slack'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
@@ -2406,7 +2722,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: getCanonicalScopesForProvider('webflow'),
|
||||
scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write', 'forms:read'],
|
||||
responseType: 'code',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`,
|
||||
getUserInfo: async (tokens) => {
|
||||
@@ -2456,7 +2772,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: getCanonicalScopesForProvider('linkedin'),
|
||||
scopes: ['profile', 'openid', 'email', 'w_member_social'],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
@@ -2506,7 +2822,19 @@ 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: getCanonicalScopesForProvider('zoom'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
@@ -2558,7 +2886,25 @@ 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: getCanonicalScopesForProvider('spotify'),
|
||||
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',
|
||||
],
|
||||
responseType: 'code',
|
||||
authentication: 'basic',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/spotify`,
|
||||
@@ -2607,7 +2953,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: getCanonicalScopesForProvider('wordpress'),
|
||||
scopes: ['global'],
|
||||
responseType: 'code',
|
||||
prompt: 'consent',
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wordpress`,
|
||||
@@ -2654,7 +3000,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: getCanonicalScopesForProvider('calcom'),
|
||||
scopes: [],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
|
||||
@@ -553,51 +553,6 @@ export function validateMicrosoftGraphId(
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates SharePoint site IDs used in Microsoft Graph API.
|
||||
*
|
||||
* Site IDs are compound identifiers: `hostname,spsite-guid,spweb-guid`
|
||||
* (e.g. `contoso.sharepoint.com,2C712604-1370-44E7-A1F5-426573FDA80A,2D2244C3-251A-49EA-93A8-39E1C3A060FE`).
|
||||
* The API also accepts partial forms like a single GUID or just a hostname.
|
||||
*
|
||||
* Allowed characters: alphanumeric, periods, hyphens, and commas.
|
||||
*
|
||||
* @param value - The SharePoint site ID to validate
|
||||
* @param paramName - Name of the parameter for error messages
|
||||
* @returns ValidationResult
|
||||
*/
|
||||
export function validateSharePointSiteId(
|
||||
value: string | null | undefined,
|
||||
paramName = 'siteId'
|
||||
): ValidationResult {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} is required`,
|
||||
}
|
||||
}
|
||||
|
||||
if (value.length > 512) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} exceeds maximum length`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9.\-,]+$/.test(value)) {
|
||||
logger.warn('Invalid characters in SharePoint site ID', {
|
||||
paramName,
|
||||
value: value.substring(0, 100),
|
||||
})
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} contains invalid characters`,
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates Jira Cloud IDs (typically UUID format)
|
||||
*
|
||||
|
||||
@@ -63,8 +63,6 @@ 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',
|
||||
@@ -77,8 +75,6 @@ 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',
|
||||
],
|
||||
@@ -90,8 +86,6 @@ 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',
|
||||
],
|
||||
@@ -103,8 +97,6 @@ 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',
|
||||
],
|
||||
@@ -129,11 +121,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-calendar',
|
||||
icon: GoogleCalendarIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
},
|
||||
'google-contacts': {
|
||||
name: 'Google Contacts',
|
||||
@@ -141,11 +129,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-contacts',
|
||||
icon: GoogleContactsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/contacts',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/contacts'],
|
||||
},
|
||||
'google-bigquery': {
|
||||
name: 'Google BigQuery',
|
||||
@@ -153,11 +137,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-bigquery',
|
||||
icon: GoogleBigQueryIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/bigquery',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/bigquery'],
|
||||
},
|
||||
'google-tasks': {
|
||||
name: 'Google Tasks',
|
||||
@@ -165,11 +145,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-tasks',
|
||||
icon: GoogleTasksIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/tasks',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/tasks'],
|
||||
},
|
||||
'google-vault': {
|
||||
name: 'Google Vault',
|
||||
@@ -178,8 +154,6 @@ 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',
|
||||
],
|
||||
@@ -191,8 +165,6 @@ 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',
|
||||
],
|
||||
@@ -204,8 +176,6 @@ 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',
|
||||
],
|
||||
@@ -216,11 +186,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'vertex-ai',
|
||||
icon: VertexIcon,
|
||||
baseProviderIcon: VertexIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
},
|
||||
},
|
||||
defaultService: 'gmail',
|
||||
@@ -705,7 +671,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'webflow',
|
||||
icon: WebflowIcon,
|
||||
baseProviderIcon: WebflowIcon,
|
||||
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write', 'forms:read'],
|
||||
scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write'],
|
||||
},
|
||||
},
|
||||
defaultService: 'webflow',
|
||||
|
||||
@@ -122,6 +122,14 @@ 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
|
||||
@@ -130,6 +138,10 @@ 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,6 +361,209 @@ 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)
|
||||
@@ -599,111 +802,3 @@ 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,411 +4,9 @@ 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.
|
||||
@@ -478,53 +76,37 @@ export function getCanonicalScopesForProvider(providerId: string): string[] {
|
||||
return service?.scopes ? [...service.scopes] : []
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]
|
||||
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)
|
||||
}
|
||||
}
|
||||
return []
|
||||
return Array.from(seen)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
])
|
||||
export function evaluateScopeCoverage(
|
||||
providerId: string,
|
||||
grantedScopes: string[]
|
||||
): ScopeEvaluation {
|
||||
const canonicalScopes = getCanonicalScopesForProvider(providerId)
|
||||
const normalizedGranted = normalizeScopes(grantedScopes)
|
||||
|
||||
/**
|
||||
* 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 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,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user