diff --git a/apps/sim/app/api/tools/hubspot/lists/route.ts b/apps/sim/app/api/tools/hubspot/lists/route.ts index 0ee11b7c04..ab7cf55230 100644 --- a/apps/sim/app/api/tools/hubspot/lists/route.ts +++ b/apps/sim/app/api/tools/hubspot/lists/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotListsSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const { credentialId, objectTypeId, query } = parsed.data.query + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) + if (!credentialIdValidation.isValid) { + logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`) + return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + } + const authz = await authorizeCredentialUse(request, { credentialId, requireWorkflowIdForInternal: false, diff --git a/apps/sim/app/api/tools/hubspot/owners/route.ts b/apps/sim/app/api/tools/hubspot/owners/route.ts index da58d59b2b..be34256def 100644 --- a/apps/sim/app/api/tools/hubspot/owners/route.ts +++ b/apps/sim/app/api/tools/hubspot/owners/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotOwnersSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const { credentialId, query } = parsed.data.query + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) + if (!credentialIdValidation.isValid) { + logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`) + return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + } + const authz = await authorizeCredentialUse(request, { credentialId, requireWorkflowIdForInternal: false, diff --git a/apps/sim/app/api/tools/hubspot/pipelines/route.ts b/apps/sim/app/api/tools/hubspot/pipelines/route.ts index 7543120e57..fd9643bed3 100644 --- a/apps/sim/app/api/tools/hubspot/pipelines/route.ts +++ b/apps/sim/app/api/tools/hubspot/pipelines/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotPipelinesSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -33,6 +34,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const { credentialId, objectType } = parsed.data.query + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) + if (!credentialIdValidation.isValid) { + logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`) + return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + } + const authz = await authorizeCredentialUse(request, { credentialId, requireWorkflowIdForInternal: false, diff --git a/apps/sim/app/api/tools/hubspot/properties/route.ts b/apps/sim/app/api/tools/hubspot/properties/route.ts index 1fafcaab6f..e52185455f 100644 --- a/apps/sim/app/api/tools/hubspot/properties/route.ts +++ b/apps/sim/app/api/tools/hubspot/properties/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { hubspotPropertiesSelectorContract } from '@/lib/api/contracts/selectors/hubspot' import { parseRequest } from '@/lib/api/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -36,6 +37,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const { credentialId, objectType, query } = parsed.data.query + const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255) + if (!credentialIdValidation.isValid) { + logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`) + return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 }) + } + const authz = await authorizeCredentialUse(request, { credentialId, requireWorkflowIdForInternal: false, diff --git a/apps/sim/triggers/hubspot/poller.ts b/apps/sim/triggers/hubspot/poller.ts index 81cad5866a..9f7015eb98 100644 --- a/apps/sim/triggers/hubspot/poller.ts +++ b/apps/sim/triggers/hubspot/poller.ts @@ -14,6 +14,25 @@ import type { TriggerConfig } from '@/triggers/types' const logger = createLogger('HubSpotPollingTrigger') +/** + * Resolves the effective object type from the subblock store. `getValue` returns `null` + * for fields the user hasn't interacted with yet, so we fall back to the dropdown's + * default ('contact') — otherwise the cascading property selectors render empty on + * first render even when the dropdown visibly shows "contact". + */ +function resolveSelectedObjectType(blockId: string): string | null { + const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null + const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as + | string + | null + const selected = objectType ?? 'contact' + if (selected === 'custom') { + const trimmed = customId?.trim() + return trimmed ? trimmed : null + } + return selected +} + async function fetchHubSpotProperties(blockId: string, objectType: string) { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string @@ -128,13 +147,7 @@ export const hubspotPollingTrigger: TriggerConfig = { placeholder: 'Select a property', options: [], fetchOptions: async (blockId: string) => { - const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as - | string - | null - const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as - | string - | null - const resolved = objectType === 'custom' ? customId : objectType + const resolved = resolveSelectedObjectType(blockId) if (!resolved) throw new Error('Select an object type first') try { return await fetchHubSpotProperties(blockId, resolved) @@ -162,13 +175,7 @@ export const hubspotPollingTrigger: TriggerConfig = { placeholder: 'Select properties (optional)', options: [], fetchOptions: async (blockId: string) => { - const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as - | string - | null - const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as - | string - | null - const resolved = objectType === 'custom' ? customId : objectType + const resolved = resolveSelectedObjectType(blockId) if (!resolved) return [] try { return await fetchHubSpotProperties(blockId, resolved) @@ -193,10 +200,8 @@ export const hubspotPollingTrigger: TriggerConfig = { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string | null - const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as - | string - | null - if (!credentialId || !objectType) return [] + const objectType = resolveSelectedObjectType(blockId) ?? 'contact' + if (!credentialId) throw new Error('No HubSpot credential selected') if (isCredentialSetValue(credentialId)) return [] try { const data = await requestJson(hubspotPipelinesSelectorContract, { @@ -224,14 +229,13 @@ export const hubspotPollingTrigger: TriggerConfig = { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string | null - const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as - | string - | null + const objectType = resolveSelectedObjectType(blockId) ?? 'contact' const pipelineId = useSubBlockStore.getState().getValue(blockId, 'pipelineId') as | string | null - if (!credentialId || !objectType || !pipelineId) return [] + if (!credentialId) throw new Error('No HubSpot credential selected') if (isCredentialSetValue(credentialId)) return [] + if (!pipelineId) return [] try { const data = await requestJson(hubspotPipelinesSelectorContract, { query: { credentialId, objectType }, @@ -259,7 +263,7 @@ export const hubspotPollingTrigger: TriggerConfig = { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string | null - if (!credentialId) return [] + if (!credentialId) throw new Error('No HubSpot credential selected') if (isCredentialSetValue(credentialId)) return [] try { const data = await requestJson(hubspotOwnersSelectorContract, {