Skip to content

Commit 19b5099

Browse files
waleedlatif1claude
andauthored
fix(hubspot): selector fetchOptions default + credentialId validation (#4723)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b6d08fb commit 19b5099

5 files changed

Lines changed: 55 additions & 23 deletions

File tree

apps/sim/app/api/tools/hubspot/lists/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotListsSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2728
if (!parsed.success) return parsed.response
2829
const { credentialId, objectTypeId, query } = parsed.data.query
2930

31+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
32+
if (!credentialIdValidation.isValid) {
33+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
34+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
35+
}
36+
3037
const authz = await authorizeCredentialUse(request, {
3138
credentialId,
3239
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/owners/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotOwnersSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -27,6 +28,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2728
if (!parsed.success) return parsed.response
2829
const { credentialId, query } = parsed.data.query
2930

31+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
32+
if (!credentialIdValidation.isValid) {
33+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
34+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
35+
}
36+
3037
const authz = await authorizeCredentialUse(request, {
3138
credentialId,
3239
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/pipelines/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotPipelinesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -33,6 +34,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3334
if (!parsed.success) return parsed.response
3435
const { credentialId, objectType } = parsed.data.query
3536

37+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
38+
if (!credentialIdValidation.isValid) {
39+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
40+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
41+
}
42+
3643
const authz = await authorizeCredentialUse(request, {
3744
credentialId,
3845
requireWorkflowIdForInternal: false,

apps/sim/app/api/tools/hubspot/properties/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { hubspotPropertiesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
44
import { parseRequest } from '@/lib/api/server'
55
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -36,6 +37,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
3637
if (!parsed.success) return parsed.response
3738
const { credentialId, objectType, query } = parsed.data.query
3839

40+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
41+
if (!credentialIdValidation.isValid) {
42+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
43+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
44+
}
45+
3946
const authz = await authorizeCredentialUse(request, {
4047
credentialId,
4148
requireWorkflowIdForInternal: false,

apps/sim/triggers/hubspot/poller.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ import type { TriggerConfig } from '@/triggers/types'
1414

1515
const logger = createLogger('HubSpotPollingTrigger')
1616

17+
/**
18+
* Resolves the effective object type from the subblock store. `getValue` returns `null`
19+
* for fields the user hasn't interacted with yet, so we fall back to the dropdown's
20+
* default ('contact') — otherwise the cascading property selectors render empty on
21+
* first render even when the dropdown visibly shows "contact".
22+
*/
23+
function resolveSelectedObjectType(blockId: string): string | null {
24+
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as string | null
25+
const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as
26+
| string
27+
| null
28+
const selected = objectType ?? 'contact'
29+
if (selected === 'custom') {
30+
const trimmed = customId?.trim()
31+
return trimmed ? trimmed : null
32+
}
33+
return selected
34+
}
35+
1736
async function fetchHubSpotProperties(blockId: string, objectType: string) {
1837
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
1938
| string
@@ -128,13 +147,7 @@ export const hubspotPollingTrigger: TriggerConfig = {
128147
placeholder: 'Select a property',
129148
options: [],
130149
fetchOptions: async (blockId: string) => {
131-
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as
132-
| string
133-
| null
134-
const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as
135-
| string
136-
| null
137-
const resolved = objectType === 'custom' ? customId : objectType
150+
const resolved = resolveSelectedObjectType(blockId)
138151
if (!resolved) throw new Error('Select an object type first')
139152
try {
140153
return await fetchHubSpotProperties(blockId, resolved)
@@ -162,13 +175,7 @@ export const hubspotPollingTrigger: TriggerConfig = {
162175
placeholder: 'Select properties (optional)',
163176
options: [],
164177
fetchOptions: async (blockId: string) => {
165-
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as
166-
| string
167-
| null
168-
const customId = useSubBlockStore.getState().getValue(blockId, 'customObjectTypeId') as
169-
| string
170-
| null
171-
const resolved = objectType === 'custom' ? customId : objectType
178+
const resolved = resolveSelectedObjectType(blockId)
172179
if (!resolved) return []
173180
try {
174181
return await fetchHubSpotProperties(blockId, resolved)
@@ -193,10 +200,8 @@ export const hubspotPollingTrigger: TriggerConfig = {
193200
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
194201
| string
195202
| null
196-
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as
197-
| string
198-
| null
199-
if (!credentialId || !objectType) return []
203+
const objectType = resolveSelectedObjectType(blockId) ?? 'contact'
204+
if (!credentialId) throw new Error('No HubSpot credential selected')
200205
if (isCredentialSetValue(credentialId)) return []
201206
try {
202207
const data = await requestJson(hubspotPipelinesSelectorContract, {
@@ -224,14 +229,13 @@ export const hubspotPollingTrigger: TriggerConfig = {
224229
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
225230
| string
226231
| null
227-
const objectType = useSubBlockStore.getState().getValue(blockId, 'objectType') as
228-
| string
229-
| null
232+
const objectType = resolveSelectedObjectType(blockId) ?? 'contact'
230233
const pipelineId = useSubBlockStore.getState().getValue(blockId, 'pipelineId') as
231234
| string
232235
| null
233-
if (!credentialId || !objectType || !pipelineId) return []
236+
if (!credentialId) throw new Error('No HubSpot credential selected')
234237
if (isCredentialSetValue(credentialId)) return []
238+
if (!pipelineId) return []
235239
try {
236240
const data = await requestJson(hubspotPipelinesSelectorContract, {
237241
query: { credentialId, objectType },
@@ -259,7 +263,7 @@ export const hubspotPollingTrigger: TriggerConfig = {
259263
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
260264
| string
261265
| null
262-
if (!credentialId) return []
266+
if (!credentialId) throw new Error('No HubSpot credential selected')
263267
if (isCredentialSetValue(credentialId)) return []
264268
try {
265269
const data = await requestJson(hubspotOwnersSelectorContract, {

0 commit comments

Comments
 (0)