Compare commits

...

3 Commits

Author SHA1 Message Date
Vikhyath Mondreti
7e4997b278 feat(oauth): pass through option for token in advanced mode 2025-12-13 14:28:09 -08:00
Vikhyath Mondreti
73940ab390 fix(deployed-chat): voice mode (#2358)
* fix(deployed-chat): voice mode

* remove redundant check

* consolidate query

* invalidate session on password change + race condition fix
2025-12-13 12:31:03 -08:00
Vikhyath Mondreti
f111dac020 improvement(autolayout): reduce horizontal spacing (#2357) 2025-12-13 11:06:42 -08:00
45 changed files with 685 additions and 205 deletions

View File

@@ -1,6 +1,6 @@
import { createLogger } from '@/lib/logs/console/logger'
const DEFAULT_STARS = '18.6k'
const DEFAULT_STARS = '19.4k'
const logger = createLogger('GitHubStars')

View File

@@ -132,7 +132,7 @@ export async function POST(
if ((password || email) && !input) {
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
setChatAuthCookie(response, deployment.id, deployment.authType)
setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)
return response
}
@@ -315,7 +315,7 @@ export async function GET(
if (
deployment.authType !== 'public' &&
authCookie &&
validateAuthToken(authCookie.value, deployment.id)
validateAuthToken(authCookie.value, deployment.id, deployment.password)
) {
return addCorsHeaders(
createSuccessResponse({

View File

@@ -1,3 +1,4 @@
import { createHash } from 'crypto'
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
@@ -9,6 +10,10 @@ import { hasAdminPermission } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('ChatAuthUtils')
function hashPassword(encryptedPassword: string): string {
return createHash('sha256').update(encryptedPassword).digest('hex').substring(0, 8)
}
/**
* Check if user has permission to create a chat for a specific workflow
* Either the user owns the workflow directly OR has admin permission for the workflow's workspace
@@ -77,14 +82,20 @@ export async function checkChatAccess(
return { hasAccess: false }
}
const encryptAuthToken = (chatId: string, type: string): string => {
return Buffer.from(`${chatId}:${type}:${Date.now()}`).toString('base64')
function encryptAuthToken(chatId: string, type: string, encryptedPassword?: string | null): string {
const pwHash = encryptedPassword ? hashPassword(encryptedPassword) : ''
return Buffer.from(`${chatId}:${type}:${Date.now()}:${pwHash}`).toString('base64')
}
export const validateAuthToken = (token: string, chatId: string): boolean => {
export function validateAuthToken(
token: string,
chatId: string,
encryptedPassword?: string | null
): boolean {
try {
const decoded = Buffer.from(token, 'base64').toString()
const [storedId, _type, timestamp] = decoded.split(':')
const parts = decoded.split(':')
const [storedId, _type, timestamp, storedPwHash] = parts
if (storedId !== chatId) {
return false
@@ -92,20 +103,32 @@ export const validateAuthToken = (token: string, chatId: string): boolean => {
const createdAt = Number.parseInt(timestamp)
const now = Date.now()
const expireTime = 24 * 60 * 60 * 1000 // 24 hours
const expireTime = 24 * 60 * 60 * 1000
if (now - createdAt > expireTime) {
return false
}
if (encryptedPassword) {
const currentPwHash = hashPassword(encryptedPassword)
if (storedPwHash !== currentPwHash) {
return false
}
}
return true
} catch (_e) {
return false
}
}
export const setChatAuthCookie = (response: NextResponse, chatId: string, type: string): void => {
const token = encryptAuthToken(chatId, type)
export function setChatAuthCookie(
response: NextResponse,
chatId: string,
type: string,
encryptedPassword?: string | null
): void {
const token = encryptAuthToken(chatId, type, encryptedPassword)
response.cookies.set({
name: `chat_auth_${chatId}`,
value: token,
@@ -113,7 +136,7 @@ export const setChatAuthCookie = (response: NextResponse, chatId: string, type:
secure: !isDev,
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24, // 24 hours
maxAge: 60 * 60 * 24,
})
}
@@ -145,7 +168,7 @@ export async function validateChatAuth(
const cookieName = `chat_auth_${deployment.id}`
const authCookie = request.cookies.get(cookieName)
if (authCookie && validateAuthToken(authCookie.value, deployment.id)) {
if (authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password)) {
return { authorized: true }
}

View File

@@ -1,26 +1,81 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { env } from '@/lib/core/config/env'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAuthToken } from '@/app/api/chat/utils'
const logger = createLogger('ProxyTTSStreamAPI')
export async function POST(request: NextRequest) {
/**
* Validates chat-based authentication for deployed chat voice mode
* Checks if the user has a valid chat auth cookie for the given chatId
*/
async function validateChatAuth(request: NextRequest, chatId: string): Promise<boolean> {
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.error('Authentication failed for TTS stream proxy:', authResult.error)
return new Response('Unauthorized', { status: 401 })
const chatResult = await db
.select({
id: chat.id,
isActive: chat.isActive,
authType: chat.authType,
password: chat.password,
})
.from(chat)
.where(eq(chat.id, chatId))
.limit(1)
if (chatResult.length === 0 || !chatResult[0].isActive) {
logger.warn('Chat not found or inactive for TTS auth:', chatId)
return false
}
const body = await request.json()
const { text, voiceId, modelId = 'eleven_turbo_v2_5' } = body
const chatData = chatResult[0]
if (chatData.authType === 'public') {
return true
}
const cookieName = `chat_auth_${chatId}`
const authCookie = request.cookies.get(cookieName)
if (authCookie && validateAuthToken(authCookie.value, chatId, chatData.password)) {
return true
}
return false
} catch (error) {
logger.error('Error validating chat auth for TTS:', error)
return false
}
}
export async function POST(request: NextRequest) {
try {
let body: any
try {
body = await request.json()
} catch {
return new Response('Invalid request body', { status: 400 })
}
const { text, voiceId, modelId = 'eleven_turbo_v2_5', chatId } = body
if (!chatId) {
return new Response('chatId is required', { status: 400 })
}
if (!text || !voiceId) {
return new Response('Missing required parameters', { status: 400 })
}
const isChatAuthed = await validateChatAuth(request, chatId)
if (!isChatAuthed) {
logger.warn('Chat authentication failed for TTS, chatId:', chatId)
return new Response('Unauthorized', { status: 401 })
}
const voiceIdValidation = validateAlphanumericId(voiceId, 'voiceId', 255)
if (!voiceIdValidation.isValid) {
logger.error(`Invalid voice ID: ${voiceIdValidation.error}`)

View File

@@ -23,13 +23,13 @@ export async function GET() {
if (!response.ok) {
console.warn('GitHub API request failed:', response.status)
return NextResponse.json({ stars: formatStarCount(14500) })
return NextResponse.json({ stars: formatStarCount(19400) })
}
const data = await response.json()
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 14500)) })
return NextResponse.json({ stars: formatStarCount(Number(data?.stargazers_count ?? 19400)) })
} catch (error) {
console.warn('Error fetching GitHub stars:', error)
return NextResponse.json({ stars: formatStarCount(14500) })
return NextResponse.json({ stars: formatStarCount(19400) })
}
}

View File

@@ -39,6 +39,7 @@ interface ChatConfig {
interface AudioStreamingOptions {
voiceId: string
chatId?: string
onError: (error: Error) => void
}
@@ -62,16 +63,19 @@ function fileToBase64(file: File): Promise<string> {
* Creates an audio stream handler for text-to-speech conversion
* @param streamTextToAudio - Function to stream text to audio
* @param voiceId - The voice ID to use for TTS
* @param chatId - Optional chat ID for deployed chat authentication
* @returns Audio stream handler function or undefined
*/
function createAudioStreamHandler(
streamTextToAudio: (text: string, options: AudioStreamingOptions) => Promise<void>,
voiceId: string
voiceId: string,
chatId?: string
) {
return async (text: string) => {
try {
await streamTextToAudio(text, {
voiceId,
chatId,
onError: (error: Error) => {
logger.error('Audio streaming error:', error)
},
@@ -113,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
const [error, setError] = useState<string | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
const [starCount, setStarCount] = useState('3.4k')
const [starCount, setStarCount] = useState('19.4k')
const [conversationId, setConversationId] = useState('')
const [showScrollButton, setShowScrollButton] = useState(false)
@@ -391,7 +395,11 @@ export default function ChatClient({ identifier }: { identifier: string }) {
// Use the streaming hook with audio support
const shouldPlayAudio = isVoiceInput || isVoiceFirstMode
const audioHandler = shouldPlayAudio
? createAudioStreamHandler(streamTextToAudio, DEFAULT_VOICE_SETTINGS.voiceId)
? createAudioStreamHandler(
streamTextToAudio,
DEFAULT_VOICE_SETTINGS.voiceId,
chatConfig?.id
)
: undefined
logger.info('Starting to handle streamed response:', { shouldPlayAudio })

View File

@@ -68,7 +68,6 @@ export function VoiceInterface({
messages = [],
className,
}: VoiceInterfaceProps) {
// Simple state machine
const [state, setState] = useState<'idle' | 'listening' | 'agent_speaking'>('idle')
const [isInitialized, setIsInitialized] = useState(false)
const [isMuted, setIsMuted] = useState(false)
@@ -76,12 +75,10 @@ export function VoiceInterface({
const [permissionStatus, setPermissionStatus] = useState<'prompt' | 'granted' | 'denied'>(
'prompt'
)
// Current turn transcript (subtitle)
const [currentTranscript, setCurrentTranscript] = useState('')
// State tracking
const currentStateRef = useRef<'idle' | 'listening' | 'agent_speaking'>('idle')
const isCallEndedRef = useRef(false)
useEffect(() => {
currentStateRef.current = state
@@ -98,12 +95,10 @@ export function VoiceInterface({
const isSupported =
typeof window !== 'undefined' && !!(window.SpeechRecognition || window.webkitSpeechRecognition)
// Update muted ref
useEffect(() => {
isMutedRef.current = isMuted
}, [isMuted])
// Timeout to handle cases where agent doesn't provide audio response
const setResponseTimeout = useCallback(() => {
if (responseTimeoutRef.current) {
clearTimeout(responseTimeoutRef.current)
@@ -113,7 +108,7 @@ export function VoiceInterface({
if (currentStateRef.current === 'listening') {
setState('idle')
}
}, 5000) // 5 second timeout (increased from 3)
}, 5000)
}, [])
const clearResponseTimeout = useCallback(() => {
@@ -123,14 +118,12 @@ export function VoiceInterface({
}
}, [])
// Sync with external state
useEffect(() => {
if (isPlayingAudio && state !== 'agent_speaking') {
clearResponseTimeout() // Clear timeout since agent is responding
clearResponseTimeout()
setState('agent_speaking')
setCurrentTranscript('')
// Mute microphone immediately
setIsMuted(true)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
@@ -138,7 +131,6 @@ export function VoiceInterface({
})
}
// Stop speech recognition completely
if (recognitionRef.current) {
try {
recognitionRef.current.abort()
@@ -150,7 +142,6 @@ export function VoiceInterface({
setState('idle')
setCurrentTranscript('')
// Re-enable microphone
setIsMuted(false)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
@@ -160,7 +151,6 @@ export function VoiceInterface({
}
}, [isPlayingAudio, state, clearResponseTimeout])
// Audio setup
const setupAudio = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
@@ -175,7 +165,6 @@ export function VoiceInterface({
setPermissionStatus('granted')
mediaStreamRef.current = stream
// Setup audio context for visualization
if (!audioContextRef.current) {
const AudioContext = window.AudioContext || window.webkitAudioContext
audioContextRef.current = new AudioContext()
@@ -194,7 +183,6 @@ export function VoiceInterface({
source.connect(analyser)
analyserRef.current = analyser
// Start visualization
const updateVisualization = () => {
if (!analyserRef.current) return
@@ -223,7 +211,6 @@ export function VoiceInterface({
}
}, [])
// Speech recognition setup
const setupSpeechRecognition = useCallback(() => {
if (!isSupported) return
@@ -259,14 +246,11 @@ export function VoiceInterface({
}
}
// Update live transcript
setCurrentTranscript(interimTranscript || finalTranscript)
// Send final transcript (but keep listening state until agent responds)
if (finalTranscript.trim()) {
setCurrentTranscript('') // Clear transcript
setCurrentTranscript('')
// Stop recognition to avoid interference while waiting for response
if (recognitionRef.current) {
try {
recognitionRef.current.stop()
@@ -275,7 +259,6 @@ export function VoiceInterface({
}
}
// Start timeout in case agent doesn't provide audio response
setResponseTimeout()
onVoiceTranscript?.(finalTranscript)
@@ -283,13 +266,14 @@ export function VoiceInterface({
}
recognition.onend = () => {
if (isCallEndedRef.current) return
const currentState = currentStateRef.current
// Only restart recognition if we're in listening state and not muted
if (currentState === 'listening' && !isMutedRef.current) {
// Add a delay to avoid immediate restart after sending transcript
setTimeout(() => {
// Double-check state hasn't changed during delay
if (isCallEndedRef.current) return
if (
recognitionRef.current &&
currentStateRef.current === 'listening' &&
@@ -301,14 +285,12 @@ export function VoiceInterface({
logger.debug('Error restarting speech recognition:', error)
}
}
}, 1000) // Longer delay to give agent time to respond
}, 1000)
}
}
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
// Filter out "aborted" errors - these are expected when we intentionally stop recognition
if (event.error === 'aborted') {
// Ignore
return
}
@@ -320,7 +302,6 @@ export function VoiceInterface({
recognitionRef.current = recognition
}, [isSupported, onVoiceTranscript, setResponseTimeout])
// Start/stop listening
const startListening = useCallback(() => {
if (!isInitialized || isMuted || state !== 'idle') {
return
@@ -351,17 +332,12 @@ export function VoiceInterface({
}
}, [])
// Handle interrupt
const handleInterrupt = useCallback(() => {
if (state === 'agent_speaking') {
// Clear any subtitle timeouts and text
// (No longer needed after removing subtitle system)
onInterrupt?.()
setState('listening')
setCurrentTranscript('')
// Unmute microphone for user input
setIsMuted(false)
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
@@ -369,7 +345,6 @@ export function VoiceInterface({
})
}
// Start listening immediately
if (recognitionRef.current) {
try {
recognitionRef.current.start()
@@ -380,14 +355,13 @@ export function VoiceInterface({
}
}, [state, onInterrupt])
// Handle call end with proper cleanup
const handleCallEnd = useCallback(() => {
// Stop everything immediately
isCallEndedRef.current = true
setState('idle')
setCurrentTranscript('')
setIsMuted(false)
// Stop speech recognition
if (recognitionRef.current) {
try {
recognitionRef.current.abort()
@@ -396,17 +370,11 @@ export function VoiceInterface({
}
}
// Clear timeouts
clearResponseTimeout()
// Stop audio playback and streaming immediately
onInterrupt?.()
// Call the original onCallEnd
onCallEnd?.()
}, [onCallEnd, onInterrupt, clearResponseTimeout])
// Keyboard handler
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.code === 'Space') {
@@ -419,7 +387,6 @@ export function VoiceInterface({
return () => document.removeEventListener('keydown', handleKeyDown)
}, [handleInterrupt])
// Mute toggle
const toggleMute = useCallback(() => {
if (state === 'agent_speaking') {
handleInterrupt()
@@ -442,7 +409,6 @@ export function VoiceInterface({
}
}, [isMuted, state, handleInterrupt, stopListening, startListening])
// Initialize
useEffect(() => {
if (isSupported) {
setupSpeechRecognition()
@@ -450,47 +416,40 @@ export function VoiceInterface({
}
}, [isSupported, setupSpeechRecognition, setupAudio])
// Auto-start listening when ready
useEffect(() => {
if (isInitialized && !isMuted && state === 'idle') {
startListening()
}
}, [isInitialized, isMuted, state, startListening])
// Cleanup when call ends or component unmounts
useEffect(() => {
return () => {
// Stop speech recognition
isCallEndedRef.current = true
if (recognitionRef.current) {
try {
recognitionRef.current.abort()
} catch (error) {
} catch (_e) {
// Ignore
}
recognitionRef.current = null
}
// Stop media stream
if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => {
track.stop()
})
mediaStreamRef.current.getTracks().forEach((track) => track.stop())
mediaStreamRef.current = null
}
// Stop audio context
if (audioContextRef.current) {
audioContextRef.current.close()
audioContextRef.current = null
}
// Cancel animation frame
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
animationFrameRef.current = null
}
// Clear timeouts
if (responseTimeoutRef.current) {
clearTimeout(responseTimeoutRef.current)
responseTimeoutRef.current = null
@@ -498,7 +457,6 @@ export function VoiceInterface({
}
}, [])
// Get status text
const getStatusText = () => {
switch (state) {
case 'listening':
@@ -510,7 +468,6 @@ export function VoiceInterface({
}
}
// Get button content
const getButtonContent = () => {
if (state === 'agent_speaking') {
return (
@@ -524,9 +481,7 @@ export function VoiceInterface({
return (
<div className={cn('fixed inset-0 z-[100] flex flex-col bg-white text-gray-900', className)}>
{/* Main content */}
<div className='flex flex-1 flex-col items-center justify-center px-8'>
{/* Voice visualization */}
<div className='relative mb-16'>
<ParticlesVisualization
audioLevels={audioLevels}
@@ -538,7 +493,6 @@ export function VoiceInterface({
/>
</div>
{/* Live transcript - subtitle style */}
<div className='mb-16 flex h-24 items-center justify-center'>
{currentTranscript && (
<div className='max-w-2xl px-8'>
@@ -549,17 +503,14 @@ export function VoiceInterface({
)}
</div>
{/* Status */}
<p className='mb-8 text-center text-gray-600 text-lg'>
{getStatusText()}
{isMuted && <span className='ml-2 text-gray-400 text-sm'>(Muted)</span>}
</p>
</div>
{/* Controls */}
<div className='px-8 pb-12'>
<div className='flex items-center justify-center space-x-12'>
{/* End call */}
<Button
onClick={handleCallEnd}
variant='outline'
@@ -569,7 +520,6 @@ export function VoiceInterface({
<Phone className='h-6 w-6 rotate-[135deg]' />
</Button>
{/* Mic/Stop button */}
<Button
onClick={toggleMute}
variant='outline'

View File

@@ -14,6 +14,7 @@ declare global {
interface AudioStreamingOptions {
voiceId: string
modelId?: string
chatId?: string
onAudioStart?: () => void
onAudioEnd?: () => void
onError?: (error: Error) => void
@@ -76,7 +77,14 @@ export function useAudioStreaming(sharedAudioContextRef?: RefObject<AudioContext
}
const { text, options } = item
const { voiceId, modelId = 'eleven_turbo_v2_5', onAudioStart, onAudioEnd, onError } = options
const {
voiceId,
modelId = 'eleven_turbo_v2_5',
chatId,
onAudioStart,
onAudioEnd,
onError,
} = options
try {
const audioContext = getAudioContext()
@@ -93,6 +101,7 @@ export function useAudioStreaming(sharedAudioContextRef?: RefObject<AudioContext
text,
voiceId,
modelId,
chatId,
}),
signal: abortControllerRef.current?.signal,
})

View File

@@ -41,6 +41,16 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
],
placeholder: 'Select Airtable account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'baseId',
@@ -124,7 +134,8 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
}
},
params: (params) => {
const { credential, records, fields, ...rest } = params
const { credential, accessToken, records, fields, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
let parsedRecords: any | undefined
let parsedFields: any | undefined
@@ -142,7 +153,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
// Construct parameters based on operation
const baseParams = {
credential,
...authParam,
...rest,
}

View File

@@ -32,11 +32,20 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
id: 'credential',
title: 'Asana Account',
type: 'oauth-input',
required: true,
serviceId: 'asana',
requiredScopes: ['default'],
placeholder: 'Select Asana account',
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'workspace',
@@ -202,7 +211,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
},
params: (params) => {
const { credential, operation } = params
const { credential, accessToken, operation } = params
const projectsArray = params.projects
? params.projects
@@ -211,14 +220,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
.filter((p: string) => p.length > 0)
: undefined
const baseParams = {
accessToken: credential?.accessToken,
}
const authParam = credential ? { credential } : { accessToken }
switch (operation) {
case 'get_task':
return {
...baseParams,
...authParam,
taskGid: params.taskGid,
workspace: params.getTasks_workspace,
project: params.getTasks_project,
@@ -226,7 +233,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'create_task':
return {
...baseParams,
...authParam,
workspace: params.workspace,
name: params.name,
notes: params.notes,
@@ -235,7 +242,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'update_task':
return {
...baseParams,
...authParam,
taskGid: params.taskGid,
name: params.name,
notes: params.notes,
@@ -245,12 +252,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'get_projects':
return {
...baseParams,
...authParam,
workspace: params.workspace,
}
case 'search_tasks':
return {
...baseParams,
...authParam,
workspace: params.workspace,
text: params.searchText,
assignee: params.assignee,
@@ -259,12 +266,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'add_comment':
return {
...baseParams,
...authParam,
taskGid: params.taskGid,
text: params.commentText,
}
default:
return baseParams
return authParam
}
},
},

View File

@@ -76,6 +76,16 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
],
placeholder: 'Select Confluence account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'pageId',
@@ -255,6 +265,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
params: (params) => {
const {
credential,
accessToken,
pageId,
manualPageId,
operation,
@@ -264,6 +275,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
...rest
} = params
const authParam = credential ? { credential } : { accessToken }
const effectivePageId = (pageId || manualPageId || '').trim()
const requiresPageId = [
@@ -289,7 +301,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
if (operation === 'upload_attachment') {
return {
credential,
...authParam,
pageId: effectivePageId,
operation,
file: attachmentFile,
@@ -300,7 +312,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
}
return {
credential,
...authParam,
pageId: effectivePageId || undefined,
operation,
...rest,

View File

@@ -49,6 +49,16 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
],
placeholder: 'Select Dropbox account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Upload operation inputs
{

View File

@@ -38,7 +38,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
],
value: () => 'send_gmail',
},
// Gmail Credentials
// Gmail Credentials (basic mode)
{
id: 'credential',
title: 'Gmail Account',
@@ -51,6 +51,17 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
],
placeholder: 'Select Gmail account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Send Email Fields
{
@@ -377,6 +388,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
params: (params) => {
const {
credential,
accessToken,
folder,
manualFolder,
destinationLabel,
@@ -437,7 +449,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
return {
...rest,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -28,6 +28,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
],
value: () => 'create',
},
// Google Calendar Credentials (basic mode)
{
id: 'credential',
title: 'Google Calendar Account',
@@ -36,6 +37,17 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select Google Calendar account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Calendar selector (basic mode)
{
@@ -49,7 +61,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
dependsOn: ['credential'],
mode: 'basic',
},
// Manual calendar ID input (advanced mode)
// Manual calendar ID input (advanced mode) - no dependsOn needed for text input
{
id: 'manualCalendarId',
title: 'Calendar ID',
@@ -213,6 +225,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
params: (params) => {
const {
credential,
accessToken,
operation,
attendees,
replaceExisting,
@@ -253,7 +266,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
}
return {
credential,
...(credential ? { credential } : { accessToken }),
...processedParams,
}
},

View File

@@ -27,7 +27,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
],
value: () => 'read',
},
// Google Docs Credentials
// Google Docs Credentials (basic mode)
{
id: 'credential',
title: 'Google Account',
@@ -39,6 +39,17 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Document selector (basic mode)
{
@@ -61,7 +72,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'short-input',
canonicalParamId: 'documentId',
placeholder: 'Enter document ID',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: ['read', 'write'] },
},
@@ -95,7 +105,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create' },
},
@@ -133,8 +142,15 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
}
},
params: (params) => {
const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } =
params
const {
credential,
accessToken,
documentId,
manualDocumentId,
folderSelector,
folderId,
...rest
} = params
const effectiveDocumentId = (documentId || manualDocumentId || '').trim()
const effectiveFolderId = (folderSelector || folderId || '').trim()
@@ -143,7 +159,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
...rest,
documentId: effectiveDocumentId || undefined,
folderId: effectiveFolderId || undefined,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -28,7 +28,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
],
value: () => 'create_folder',
},
// Google Drive Credentials
// Google Drive Credentials (basic mode)
{
id: 'credential',
title: 'Google Drive Account',
@@ -40,6 +40,17 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google Drive account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create/Upload File Fields
{
@@ -324,6 +335,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
params: (params) => {
const {
credential,
accessToken,
folderSelector,
manualFolderId,
fileSelector,
@@ -339,7 +351,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
const effectiveFileId = (fileSelector || manualFileId || '').trim()
return {
credential,
...(credential ? { credential } : { accessToken }),
folderId: effectiveFolderId || undefined,
fileId: effectiveFileId || undefined,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,

View File

@@ -13,6 +13,7 @@ export const GoogleFormsBlock: BlockConfig = {
bgColor: '#E0E0E0',
icon: GoogleFormsIcon,
subBlocks: [
// Google Forms Credentials (basic mode)
{
id: 'credential',
title: 'Google Account',
@@ -25,6 +26,17 @@ export const GoogleFormsBlock: BlockConfig = {
'https://www.googleapis.com/auth/forms.responses.readonly',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'formId',
@@ -32,7 +44,6 @@ export const GoogleFormsBlock: BlockConfig = {
type: 'short-input',
required: true,
placeholder: 'Enter the Google Form ID',
dependsOn: ['credential'],
},
{
id: 'responseId',
@@ -53,7 +64,7 @@ export const GoogleFormsBlock: BlockConfig = {
config: {
tool: () => 'google_forms_get_responses',
params: (params) => {
const { credential, formId, responseId, pageSize, ...rest } = params
const { credential, accessToken, formId, responseId, pageSize, ...rest } = params
const effectiveFormId = String(formId || '').trim()
if (!effectiveFormId) {
@@ -65,7 +76,7 @@ export const GoogleFormsBlock: BlockConfig = {
formId: effectiveFormId,
responseId: responseId ? String(responseId).trim() : undefined,
pageSize: pageSize ? Number(pageSize) : undefined,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -33,6 +33,7 @@ export const GoogleGroupsBlock: BlockConfig = {
],
value: () => 'list_groups',
},
// Google Groups Credentials (basic mode)
{
id: 'credential',
title: 'Google Groups Account',
@@ -44,6 +45,17 @@ export const GoogleGroupsBlock: BlockConfig = {
'https://www.googleapis.com/auth/admin.directory.group.member',
],
placeholder: 'Select Google Workspace account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -221,12 +233,13 @@ export const GoogleGroupsBlock: BlockConfig = {
}
},
params: (params) => {
const { credential, operation, ...rest } = params
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
switch (operation) {
case 'list_groups':
return {
credential,
...authParam,
customer: rest.customer,
domain: rest.domain,
query: rest.query,
@@ -235,19 +248,19 @@ export const GoogleGroupsBlock: BlockConfig = {
case 'get_group':
case 'delete_group':
return {
credential,
...authParam,
groupKey: rest.groupKey,
}
case 'create_group':
return {
credential,
...authParam,
email: rest.email,
name: rest.name,
description: rest.description,
}
case 'update_group':
return {
credential,
...authParam,
groupKey: rest.groupKey,
name: rest.newName,
email: rest.newEmail,
@@ -255,7 +268,7 @@ export const GoogleGroupsBlock: BlockConfig = {
}
case 'list_members':
return {
credential,
...authParam,
groupKey: rest.groupKey,
maxResults: rest.maxResults ? Number(rest.maxResults) : undefined,
roles: rest.roles,
@@ -263,32 +276,32 @@ export const GoogleGroupsBlock: BlockConfig = {
case 'get_member':
case 'remove_member':
return {
credential,
...authParam,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
case 'add_member':
return {
credential,
...authParam,
groupKey: rest.groupKey,
email: rest.memberEmail,
role: rest.role,
}
case 'update_member':
return {
credential,
...authParam,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
role: rest.role,
}
case 'has_member':
return {
credential,
...authParam,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
default:
return { credential, ...rest }
return { ...(credential ? { credential } : { accessToken }), ...rest }
}
},
},

View File

@@ -28,7 +28,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
],
value: () => 'read',
},
// Google Sheets Credentials
// Google Sheets Credentials (basic mode)
{
id: 'credential',
title: 'Google Account',
@@ -40,6 +40,17 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Spreadsheet Selector
{
@@ -64,7 +75,6 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
type: 'short-input',
canonicalParamId: 'spreadsheetId',
placeholder: 'ID of the spreadsheet (from URL)',
dependsOn: ['credential'],
mode: 'advanced',
},
// Range
@@ -168,7 +178,8 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
}
},
params: (params) => {
const { credential, values, spreadsheetId, manualSpreadsheetId, ...rest } = params
const { credential, accessToken, values, spreadsheetId, manualSpreadsheetId, ...rest } =
params
const parsedValues = values ? JSON.parse(values as string) : undefined
@@ -182,7 +193,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
...rest,
spreadsheetId: effectiveSpreadsheetId,
values: parsedValues,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -31,7 +31,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
],
value: () => 'read',
},
// Google Slides Credentials
// Google Slides Credentials (basic mode)
{
id: 'credential',
title: 'Google Account',
@@ -43,6 +43,17 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Presentation selector (basic mode) - for operations that need an existing presentation
{
@@ -68,7 +79,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'short-input',
canonicalParamId: 'presentationId',
placeholder: 'Enter presentation ID',
dependsOn: ['credential'],
mode: 'advanced',
condition: {
field: 'operation',
@@ -123,7 +133,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create' },
},
@@ -316,6 +325,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
params: (params) => {
const {
credential,
accessToken,
presentationId,
manualPresentationId,
folderSelector,
@@ -334,7 +344,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
const result: Record<string, any> = {
...rest,
presentationId: effectivePresentationId || undefined,
credential,
...(credential ? { credential } : { accessToken }),
}
// Handle operation-specific params

View File

@@ -30,6 +30,7 @@ export const GoogleVaultBlock: BlockConfig = {
value: () => 'list_matters_export',
},
// Google Vault Credentials (basic mode)
{
id: 'credential',
title: 'Google Vault Account',
@@ -41,6 +42,17 @@ export const GoogleVaultBlock: BlockConfig = {
'https://www.googleapis.com/auth/devstorage.read_only',
],
placeholder: 'Select Google Vault account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create Hold inputs
{
@@ -218,10 +230,10 @@ export const GoogleVaultBlock: BlockConfig = {
}
},
params: (params) => {
const { credential, ...rest } = params
const { credential, accessToken, ...rest } = params
return {
...rest,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -67,6 +67,16 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
],
placeholder: 'Select HubSpot account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'contactId',
@@ -824,6 +834,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
params: (params) => {
const {
credential,
accessToken,
operation,
propertiesToSet,
properties,
@@ -834,8 +845,9 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
...rest
} = params
const authParam = credential ? { credential } : { accessToken }
const cleanParams: Record<string, any> = {
credential,
...authParam,
}
const createUpdateOps = [

View File

@@ -92,6 +92,16 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'delete:issue-link:jira',
],
placeholder: 'Select Jira account',
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Project selector (basic mode)
{
@@ -443,14 +453,23 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
}
},
params: (params) => {
const { credential, projectId, manualProjectId, issueKey, manualIssueKey, ...rest } = params
const {
credential,
accessToken,
projectId,
manualProjectId,
issueKey,
manualIssueKey,
...rest
} = params
// Use the selected IDs or the manually entered ones
const effectiveProjectId = (projectId || manualProjectId || '').trim()
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
const authParam = credential ? { credential } : { accessToken }
const baseParams = {
credential,
...authParam,
domain: params.domain,
}

View File

@@ -133,6 +133,16 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
requiredScopes: ['read', 'write'],
placeholder: 'Select Linear account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Team selector (for most operations)
{
@@ -1261,9 +1271,14 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim()
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
// Auth param handling
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
// Base params that most operations need
const baseParams: Record<string, any> = {
credential: params.credential,
...authParam,
}
// Operation-specific param mapping

View File

@@ -27,7 +27,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
value: () => 'share_post',
},
// LinkedIn OAuth Authentication
// LinkedIn OAuth Authentication (basic mode)
{
id: 'credential',
title: 'LinkedIn Account',
@@ -35,6 +35,17 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
serviceId: 'linkedin',
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
placeholder: 'Select LinkedIn account',
mode: 'basic',
required: true,
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
@@ -80,18 +91,18 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
},
params: (inputs) => {
const operation = inputs.operation || 'share_post'
const { credential, ...rest } = inputs
const { credential, accessToken, ...rest } = inputs
const authParam = credential ? { credential } : { accessToken }
if (operation === 'get_profile') {
return {
accessToken: credential,
}
return authParam
}
return {
text: rest.text,
visibility: rest.visibility || 'PUBLIC',
accessToken: credential,
...authParam,
}
},
},

View File

@@ -27,6 +27,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
],
value: () => 'read',
},
// Microsoft Excel Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -42,6 +43,17 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'spreadsheetId',
@@ -61,7 +73,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
type: 'short-input',
canonicalParamId: 'spreadsheetId',
placeholder: 'Enter spreadsheet ID',
dependsOn: ['credential'],
mode: 'advanced',
},
{
@@ -160,6 +171,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
params: (params) => {
const {
credential,
accessToken,
values,
spreadsheetId,
manualSpreadsheetId,
@@ -193,7 +205,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
...rest,
spreadsheetId: effectiveSpreadsheetId,
values: parsedValues,
credential,
...(credential ? { credential } : { accessToken }),
}
if (params.operation === 'table_add') {

View File

@@ -57,6 +57,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
{ label: 'Update Task Details', id: 'update_task_details' },
],
},
// Microsoft Planner Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -72,6 +73,17 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Plan ID - for various operations
@@ -342,7 +354,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
const baseParams: MicrosoftPlannerBlockParams = {
...rest,
credential,
...(credential ? { credential } : { accessToken }),
}
// Handle different task ID fields

View File

@@ -39,6 +39,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
],
value: () => 'read_chat',
},
// Microsoft Teams Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -68,6 +69,17 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'teamId',
@@ -315,6 +327,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
params: (params) => {
const {
credential,
accessToken,
operation,
teamId,
manualTeamId,
@@ -336,7 +349,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
const baseParams: Record<string, any> = {
...rest,
credential,
...(credential ? { credential } : { accessToken }),
}
if ((operation === 'read_chat' || operation === 'read_channel') && includeAttachments) {

View File

@@ -38,6 +38,16 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
placeholder: 'Select Notion account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Read/Write operation - Page ID
{
@@ -222,7 +232,8 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
}
},
params: (params) => {
const { credential, operation, properties, filter, sorts, ...rest } = params
const { credential, accessToken, operation, properties, filter, sorts, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
// Parse properties from JSON string for create operations
let parsedProperties
@@ -265,7 +276,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
return {
...rest,
credential,
...authParam,
...(parsedProperties ? { properties: parsedProperties } : {}),
...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}),
...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}),

View File

@@ -33,7 +33,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
{ label: 'Delete File', id: 'delete' },
],
},
// One Drive Credentials
// OneDrive Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -48,6 +48,17 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create File Fields
{
@@ -164,7 +175,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: ['create_file', 'upload'] },
},
@@ -202,7 +212,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create_folder' },
},
@@ -234,7 +243,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'list' },
},
@@ -352,7 +360,16 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
}
},
params: (params) => {
const { credential, folderId, fileId, mimeType, values, downloadFileName, ...rest } = params
const {
credential,
accessToken,
folderId,
fileId,
mimeType,
values,
downloadFileName,
...rest
} = params
let normalizedValues: ReturnType<typeof normalizeExcelValuesForToolParams>
if (values !== undefined) {
@@ -360,7 +377,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
}
return {
credential,
...(credential ? { credential } : { accessToken }),
...rest,
values: normalizedValues,
folderId: folderId || undefined,

View File

@@ -34,6 +34,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
],
value: () => 'send_outlook',
},
// Microsoft Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -51,6 +52,17 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'to',
@@ -326,6 +338,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
params: (params) => {
const {
credential,
accessToken,
folder,
manualFolder,
destinationFolder,
@@ -378,7 +391,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
return {
...rest,
credential,
...(credential ? { credential } : { accessToken }),
}
},
},

View File

@@ -57,6 +57,16 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
],
placeholder: 'Select Pipedrive account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'status',
@@ -660,10 +670,11 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
}
},
params: (params) => {
const { credential, operation, ...rest } = params
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const cleanParams: Record<string, any> = {
credential,
...authParam,
}
Object.entries(rest).forEach(([key, value]) => {

View File

@@ -37,7 +37,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
value: () => 'get_posts',
},
// Reddit OAuth Authentication
// Reddit OAuth Authentication (basic mode)
{
id: 'credential',
title: 'Reddit Account',
@@ -63,6 +63,17 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
],
placeholder: 'Select Reddit account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Common fields - appear for all actions
@@ -555,7 +566,9 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
},
params: (inputs) => {
const operation = inputs.operation || 'get_posts'
const { credential, ...rest } = inputs
const { credential, accessToken, ...rest } = inputs
const authParam = credential ? { credential } : { accessToken }
if (operation === 'get_comments') {
return {
@@ -563,7 +576,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
subreddit: rest.subreddit,
sort: rest.commentSort,
limit: rest.commentLimit ? Number.parseInt(rest.commentLimit) : undefined,
credential: credential,
...authParam,
}
}
@@ -572,7 +585,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
subreddit: rest.subreddit,
time: rest.controversialTime,
limit: rest.controversialLimit ? Number.parseInt(rest.controversialLimit) : undefined,
credential: credential,
...authParam,
}
}
@@ -583,7 +596,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
sort: rest.searchSort,
time: rest.searchTime,
limit: rest.searchLimit ? Number.parseInt(rest.searchLimit) : undefined,
credential: credential,
...authParam,
}
}
@@ -595,7 +608,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
url: rest.postType === 'link' ? rest.url : undefined,
nsfw: rest.nsfw === 'true',
spoiler: rest.spoiler === 'true',
credential: credential,
...authParam,
}
}
@@ -603,7 +616,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
id: rest.voteId,
dir: Number.parseInt(rest.voteDirection),
credential: credential,
...authParam,
}
}
@@ -611,14 +624,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
id: rest.saveId,
category: rest.saveCategory,
credential: credential,
...authParam,
}
}
if (operation === 'unsave') {
return {
id: rest.saveId,
credential: credential,
...authParam,
}
}
@@ -626,7 +639,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
parent_id: rest.replyParentId,
text: rest.replyText,
credential: credential,
...authParam,
}
}
@@ -634,14 +647,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
thing_id: rest.editThingId,
text: rest.editText,
credential: credential,
...authParam,
}
}
if (operation === 'delete') {
return {
id: rest.deleteId,
credential: credential,
...authParam,
}
}
@@ -649,7 +662,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
subreddit: rest.subscribeSubreddit,
action: rest.subscribeAction,
credential: credential,
...authParam,
}
}
@@ -658,7 +671,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
sort: rest.sort,
limit: rest.limit ? Number.parseInt(rest.limit) : undefined,
time: rest.sort === 'top' ? rest.time : undefined,
credential: credential,
...authParam,
}
},
},

View File

@@ -66,6 +66,16 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
placeholder: 'Select Salesforce account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Common fields for GET operations
{
@@ -590,8 +600,9 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
}
},
params: (params) => {
const { credential, operation, ...rest } = params
const cleanParams: Record<string, any> = { credential }
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const cleanParams: Record<string, any> = { ...authParam }
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
cleanParams[key] = value

View File

@@ -33,6 +33,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
{ label: 'Upload File', id: 'upload_file' },
],
},
// SharePoint Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -48,6 +49,17 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -155,7 +167,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
type: 'short-input',
canonicalParamId: 'siteId',
placeholder: 'Enter site ID (leave empty for root site)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create_page' },
},
@@ -255,7 +266,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
}
},
params: (params) => {
const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params
const { credential, accessToken, siteSelector, manualSiteId, mimeType, ...rest } = params
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
@@ -315,7 +326,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
// Handle file upload files parameter
const fileParam = uploadFiles || files
const baseParams = {
credential,
...(credential ? { credential } : { accessToken }),
siteId: effectiveSiteId || undefined,
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
mimeType: mimeType,

View File

@@ -71,6 +71,16 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
],
placeholder: 'Select Shopify account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'shopDomain',
@@ -526,8 +536,11 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
return params.operation || 'shopify_list_products'
},
params: (params) => {
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, unknown> = {
credential: params.credential,
...authParam,
shopDomain: params.shopDomain?.trim(),
}

View File

@@ -159,6 +159,16 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
type: 'oauth-input',
serviceId: 'spotify',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// === SEARCH ===

View File

@@ -45,6 +45,16 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
requiredScopes: ['read', 'write'],
placeholder: 'Select Trello account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -330,9 +340,10 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
}
},
params: (params) => {
const { operation, limit, closed, dueComplete, ...rest } = params
const { credential, accessToken, operation, limit, closed, dueComplete, ...rest } = params
const result: Record<string, any> = { ...rest }
const authParam = credential ? { credential } : { accessToken }
const result: Record<string, any> = { ...rest, ...authParam }
if (limit && operation === 'trello_get_actions') {
result.limit = Number.parseInt(limit, 10)

View File

@@ -37,6 +37,16 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
requiredScopes: ['login', 'data'],
placeholder: 'Select Wealthbox account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'noteId',
@@ -167,16 +177,25 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
}
},
params: (params) => {
const { credential, operation, contactId, manualContactId, taskId, manualTaskId, ...rest } =
params
const {
credential,
accessToken,
operation,
contactId,
manualContactId,
taskId,
manualTaskId,
...rest
} = params
const authParam = credential ? { credential } : { accessToken }
// Handle both selector and manual inputs
const effectiveContactId = (contactId || manualContactId || '').trim()
const effectiveTaskId = (taskId || manualTaskId || '').trim()
const baseParams = {
...rest,
credential,
...authParam,
}
if (operation === 'read_note' || operation === 'write_note') {

View File

@@ -38,6 +38,16 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
placeholder: 'Select Webflow account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'collectionId',
@@ -108,7 +118,8 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
}
},
params: (params) => {
const { credential, fieldData, ...rest } = params
const { credential, accessToken, fieldData, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
let parsedFieldData: any | undefined
try {
@@ -120,7 +131,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
}
const baseParams = {
credential,
...authParam,
...rest,
}

View File

@@ -59,7 +59,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
value: () => 'wordpress_create_post',
},
// Credential selector for OAuth
// Credential selector for OAuth (basic mode)
{
id: 'credential',
title: 'WordPress Account',
@@ -68,6 +68,16 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
requiredScopes: ['global'],
placeholder: 'Select WordPress account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Site ID for WordPress.com (required for OAuth)
@@ -665,8 +675,11 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
tool: (params) => params.operation || 'wordpress_create_post',
params: (params) => {
// OAuth authentication for WordPress.com
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, any> = {
credential: params.credential,
...authParam,
siteId: params.siteId,
}

View File

@@ -27,6 +27,7 @@ export const XBlock: BlockConfig<XResponse> = {
],
value: () => 'x_write',
},
// X Credentials (basic mode)
{
id: 'credential',
title: 'X Account',
@@ -34,6 +35,17 @@ export const XBlock: BlockConfig<XResponse> = {
serviceId: 'x',
requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
placeholder: 'Select X account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'text',
@@ -143,11 +155,9 @@ export const XBlock: BlockConfig<XResponse> = {
}
},
params: (params) => {
const { credential, ...rest } = params
const { credential, accessToken, ...rest } = params
const parsedParams: Record<string, any> = {
credential: credential,
}
const parsedParams: Record<string, any> = credential ? { credential } : { accessToken }
Object.keys(rest).forEach((key) => {
const value = rest[key]

View File

@@ -53,6 +53,16 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
],
placeholder: 'Select Zoom account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// User ID for create/list operations
{
@@ -366,8 +376,11 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
return params.operation || 'zoom_create_meeting'
},
params: (params) => {
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, any> = {
credential: params.credential,
...authParam,
}
switch (params.operation) {

View File

@@ -11,7 +11,7 @@ export { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/b
/**
* Horizontal spacing between layers (columns)
*/
export const DEFAULT_HORIZONTAL_SPACING = 350
export const DEFAULT_HORIZONTAL_SPACING = 250
/**
* Vertical spacing between blocks in the same layer

View File

@@ -32,7 +32,11 @@ function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: bool
const fieldMode = subBlockConfig.mode
if (fieldMode === 'advanced' && !isAdvancedMode) {
return false // Skip advanced-only fields when in basic mode
return false
}
if (fieldMode === 'basic' && isAdvancedMode) {
return false
}
return true