From 84fe0e42d85312d6d99af13b2a6ef7910fdc28e8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:50:35 -0700 Subject: [PATCH 1/3] fix(hubspot): fall back to objectType default in selector fetchOptions useSubBlockStore.getValue returns null for default-valued dropdowns until the user interacts with them. The properties, pipelines, stages, and ownerId selectors were treating that as "no selection" and short-circuiting, so the dropdowns appeared empty even though the trigger uses 'contact' as the visible default. Adds resolveSelectedObjectType to mirror the rendered default, so the selectors fire on first paint with a valid objectType. Co-Authored-By: Claude Opus 4.7 --- apps/sim/triggers/hubspot/poller.ts | 54 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/apps/sim/triggers/hubspot/poller.ts b/apps/sim/triggers/hubspot/poller.ts index 81cad5866a..fc21d27b94 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,10 @@ 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 = + (useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null) ?? + 'contact' + if (!credentialId) throw new Error('No HubSpot credential selected') if (isCredentialSetValue(credentialId)) return [] try { const data = await requestJson(hubspotPipelinesSelectorContract, { @@ -224,14 +231,15 @@ 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 = + (useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null) ?? + '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 +267,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, { From f32b53bf1cb19139c8c168452e74cebc7286c769 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:50:40 -0700 Subject: [PATCH 2/3] fix(hubspot): validate credentialId in selector routes Mirrors the Gmail/Webflow/Jira selector route security pattern by rejecting non-alphanumeric credentialId values before authorization or token refresh. Co-Authored-By: Claude Opus 4.7 --- apps/sim/app/api/tools/hubspot/lists/route.ts | 7 +++++++ apps/sim/app/api/tools/hubspot/owners/route.ts | 7 +++++++ apps/sim/app/api/tools/hubspot/pipelines/route.ts | 7 +++++++ apps/sim/app/api/tools/hubspot/properties/route.ts | 7 +++++++ 4 files changed, 28 insertions(+) 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, From 45cbd1805bd71380450744137f737ffdc0a7bd1f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:57:51 -0700 Subject: [PATCH 3/3] fix(hubspot): use resolveSelectedObjectType in pipelineId/stageId fetchOptions Both selectors used inline `?? 'contact'` fallbacks while properties and targetPropertyName already routed through the resolver. Switch to the shared helper so custom-object handling stays consistent across every cascading selector. Co-Authored-By: Claude Opus 4.7 --- apps/sim/triggers/hubspot/poller.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/sim/triggers/hubspot/poller.ts b/apps/sim/triggers/hubspot/poller.ts index fc21d27b94..9f7015eb98 100644 --- a/apps/sim/triggers/hubspot/poller.ts +++ b/apps/sim/triggers/hubspot/poller.ts @@ -200,9 +200,7 @@ export const hubspotPollingTrigger: TriggerConfig = { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string | null - const objectType = - (useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null) ?? - 'contact' + const objectType = resolveSelectedObjectType(blockId) ?? 'contact' if (!credentialId) throw new Error('No HubSpot credential selected') if (isCredentialSetValue(credentialId)) return [] try { @@ -231,9 +229,7 @@ export const hubspotPollingTrigger: TriggerConfig = { const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as | string | null - const objectType = - (useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null) ?? - 'contact' + const objectType = resolveSelectedObjectType(blockId) ?? 'contact' const pipelineId = useSubBlockStore.getState().getValue(blockId, 'pipelineId') as | string | null