diff --git a/apps/sim/blocks/blocks/hubspot.ts b/apps/sim/blocks/blocks/hubspot.ts index d99899c5f70..5a7a1df5f94 100644 --- a/apps/sim/blocks/blocks/hubspot.ts +++ b/apps/sim/blocks/blocks/hubspot.ts @@ -2,6 +2,7 @@ import { HubspotIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { toolCallArg } from '@/blocks/utils' import { getTrigger } from '@/triggers' export const HubSpotBlock: BlockConfig = { @@ -1631,7 +1632,7 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no throw new Error(`Unknown operation: ${params.operation}`) } }, - params: (params) => { + params: (params, context) => { const { oauthCredential, operation, @@ -1698,8 +1699,8 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no 'search_line_items', 'search_quotes', ] - if (searchProperties && searchOps.includes(operation as string)) { - cleanParams.properties = searchProperties + if (searchOps.includes(operation as string)) { + cleanParams.properties = searchProperties || toolCallArg(context, properties) } if (filterGroups && searchOps.includes(operation as string)) { diff --git a/apps/sim/blocks/blocks/servicenow.test.ts b/apps/sim/blocks/blocks/servicenow.test.ts index e882578f21b..b580ac7737b 100644 --- a/apps/sim/blocks/blocks/servicenow.test.ts +++ b/apps/sim/blocks/blocks/servicenow.test.ts @@ -159,3 +159,75 @@ describe('ServiceNow state written after the split is never re-migrated', () => expect(second.blocks['block-1'].subBlocks.readFields?.value).toBe('number,state') }) }) + +/** + * A tool call (agent, Pi, Human v2) passes the model's args under the tool param + * names. Operations whose value has a dedicated, differently-named subblock must + * fall back to the model's arg instead of overwriting it with the blank subblock. + */ +describe('ServiceNow tool-call args reach the tool', () => { + function mapToolCall(operation: string, args: Record): Record { + const transform = ServiceNowBlock.tools.config?.params + if (!transform) throw new Error('ServiceNow block has no params transform') + const params = { ...CREDENTIALS, ...args } + return { ...params, ...transform(params, { operation }) } + } + + it('keeps the model close code and notes on Resolve Incident', () => { + const mapped = mapToolCall('servicenow_resolve_incident', { + sysId: 'abc', + closeCode: 'Solution provided', + closeNotes: 'Restarted the service', + }) + + expect(mapped.closeCode).toBe('Solution provided') + expect(mapped.closeNotes).toBe('Restarted the service') + }) + + it('keeps the model state filter, projection, and display mode on List Incidents', () => { + const mapped = mapToolCall('servicenow_list_incidents', { + state: '2', + fields: 'number,state', + displayValue: 'false', + }) + + expect(mapped.state).toBe('2') + expect(mapped.fields).toBe('number,state') + expect(mapped.displayValue).toBe('false') + }) + + it('does not send a stored Create/Update JSON body as the List Incidents projection', () => { + const mapped = mapToolCall('servicenow_list_incidents', { + fields: '{"short_description":"Printer down"}', + }) + + expect(mapped.fields).toBeUndefined() + }) + + it('keeps the model comment on Update Approval and the search text on Search Knowledge', () => { + expect( + mapToolCall('servicenow_update_approval', { + approvalSysId: 'abc', + decision: 'approved', + comments: 'Looks good', + }).comments + ).toBe('Looks good') + expect(mapToolCall('servicenow_search_knowledge', { query: 'vpn' }).query).toBe('vpn') + }) + + it('keeps the model limit on List Attachments', () => { + const mapped = mapToolCall('servicenow_list_attachments', { + tableName: 'incident', + recordSysId: 'abc', + limit: 5, + }) + + expect(mapped.limit).toBe(5) + }) + + it('prefers a configured subblock value over the model arg', () => { + const mapped = mapToolCall('servicenow_list_incidents', { incidentState: '6', state: '2' }) + + expect(mapped.state).toBe('6') + }) +}) diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts index 5724ad3ae06..d1eb197c8e4 100644 --- a/apps/sim/blocks/blocks/servicenow.ts +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -1,7 +1,7 @@ import { ServiceNowIcon } from '@/components/icons' -import type { BlockConfig, BlockMeta } from '@/blocks/types' +import type { BlockConfig, BlockMeta, BlockToolParamsContext } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' -import { normalizeFileInput, withSavedCredential } from '@/blocks/utils' +import { isToolCall, normalizeFileInput, toolCallArg, withSavedCredential } from '@/blocks/utils' import { APPROVAL_DECISION_OPTIONS, APPROVAL_STATE, @@ -1622,9 +1622,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st ], config: { tool: (params) => params.operation, - params: (params) => { + params: (params, context?: BlockToolParamsContext) => { const { - operation, + operation: storedOperation, fields, readFields, returnFields, @@ -1644,11 +1644,23 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st knowledgeQuery, ...rest } = params + const operation = context?.operation ?? storedOperation + /** + * A dedicated subblock's value, or on a tool call where it is blank, the + * model's arg under the tool param name. Without a model arg the subblock + * value stands exactly, so an explicit "Any" (`''`) is not replaced. + */ + const dedicatedOrToolArg = (dedicated: unknown, toolArg: unknown) => + isToolCall(context) && + toolArg !== undefined && + (dedicated === undefined || dedicated === null || dedicated === '') + ? toolArg + : dedicated const isCreateOrUpdate = operation === 'servicenow_create_record' || operation === 'servicenow_update_record' if (SEMANTIC_DISPLAY_VALUE_OPS.has(operation)) { - rest.displayValue = semanticDisplayValue + rest.displayValue = dedicatedOrToolArg(semanticDisplayValue, rest.displayValue) } /** @@ -1659,7 +1671,9 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st * query — so each gets its own subblock and is republished here for the * operations that own it. Assign explicitly rather than conditionally: * `finalInputs` merges this result over the raw inputs, so a key left - * off is not dropped, it keeps whatever the raw input held. + * off is not dropped, it keeps whatever the raw input held. On a tool + * call the model supplies these under the tool param names, so a blank + * dedicated subblock falls back to the model's value instead of wiping it. */ const stateSource = operation === 'servicenow_update_change_state' @@ -1669,12 +1683,24 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st : CHANGE_STATE_OPS.has(operation) ? changeState : incidentState - rest.state = SEMANTIC_STATE_OPS.has(operation) ? stateSource : undefined - rest.comments = - operation === 'servicenow_update_approval' ? approvalComments : incidentComments - rest.closeCode = CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode - rest.closeNotes = CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes - if (operation === 'servicenow_search_knowledge') rest.query = knowledgeQuery + rest.state = SEMANTIC_STATE_OPS.has(operation) + ? dedicatedOrToolArg(stateSource, rest.state) + : undefined + rest.comments = dedicatedOrToolArg( + operation === 'servicenow_update_approval' ? approvalComments : incidentComments, + rest.comments + ) + rest.closeCode = dedicatedOrToolArg( + CHANGE_CLOSE_OPS.has(operation) ? changeCloseCode : resolutionCode, + rest.closeCode + ) + rest.closeNotes = dedicatedOrToolArg( + CHANGE_CLOSE_OPS.has(operation) ? changeCloseNotes : resolutionNotes, + rest.closeNotes + ) + if (operation === 'servicenow_search_knowledge') { + rest.query = dedicatedOrToolArg(knowledgeQuery, rest.query) + } /** * `attachmentLimit` exists as a separate subblock precisely so a limit @@ -1684,8 +1710,8 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st * Every paginated operation reads exactly one of the two. */ if (operation === 'servicenow_list_attachments') { - rest.limit = - attachmentLimit != null && attachmentLimit !== '' ? Number(attachmentLimit) : undefined + const limit = dedicatedOrToolArg(attachmentLimit, rest.limit) + rest.limit = limit != null && limit !== '' ? Number(limit) : undefined } else if (rest.limit != null) { rest.limit = rest.limit === '' ? undefined : Number(rest.limit) } @@ -1741,7 +1767,12 @@ Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and st } const projection = operation === 'servicenow_read_record' ? readFields : returnFields - return { ...rest, fields: projection || undefined } + const toolFields = toolCallArg(context, fields) + const toolProjection = + typeof toolFields === 'string' && !toolFields.trim().startsWith('{') + ? toolFields + : undefined + return { ...rest, fields: projection || toolProjection || undefined } }, }, }, diff --git a/apps/sim/tools/hubspot/add_list_memberships.ts b/apps/sim/tools/hubspot/add_list_memberships.ts index 779571b8ba1..834453a3b7f 100644 --- a/apps/sim/tools/hubspot/add_list_memberships.ts +++ b/apps/sim/tools/hubspot/add_list_memberships.ts @@ -4,6 +4,7 @@ import type { HubSpotAddListMembershipsResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotAddListMemberships') @@ -44,7 +45,8 @@ export const hubspotAddListMembershipsTool: ToolConfig< }, request: { - url: (params) => `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}/memberships/add`, + url: (params) => + `https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}/memberships/add`, method: 'PUT', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/create_association.ts b/apps/sim/tools/hubspot/create_association.ts index 8454543ba09..5bedc29699c 100644 --- a/apps/sim/tools/hubspot/create_association.ts +++ b/apps/sim/tools/hubspot/create_association.ts @@ -4,6 +4,7 @@ import type { HubSpotCreateAssociationResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotCreateAssociation') @@ -71,8 +72,8 @@ export const hubspotCreateAssociationTool: ToolConfig< request: { url: (params) => { - const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}` - const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}` + const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}` + const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}` return params.associationTypeId != null ? `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}` diff --git a/apps/sim/tools/hubspot/delete_association.ts b/apps/sim/tools/hubspot/delete_association.ts index e51ca6db930..c238e5dbb18 100644 --- a/apps/sim/tools/hubspot/delete_association.ts +++ b/apps/sim/tools/hubspot/delete_association.ts @@ -4,6 +4,7 @@ import type { HubSpotDeleteAssociationResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteAssociation') @@ -56,8 +57,8 @@ export const hubspotDeleteAssociationTool: ToolConfig< request: { url: (params) => { - const from = `${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}` - const to = `${encodeURIComponent(params.toObjectType.trim())}/${encodeURIComponent(params.toObjectId.trim())}` + const from = `${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}` + const to = `${safeUrlPathSegment(params.toObjectType, 'toObjectType')}/${safeUrlPathSegment(params.toObjectId, 'toObjectId')}` return `https://api.hubapi.com/crm/v4/objects/${from}/associations/${to}` }, method: 'DELETE', diff --git a/apps/sim/tools/hubspot/delete_company.ts b/apps/sim/tools/hubspot/delete_company.ts index 26b7d78bcf7..7cfecaf2cf7 100644 --- a/apps/sim/tools/hubspot/delete_company.ts +++ b/apps/sim/tools/hubspot/delete_company.ts @@ -4,6 +4,7 @@ import type { HubSpotDeleteCompanyResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteCompany') @@ -37,7 +38,8 @@ export const hubspotDeleteCompanyTool: ToolConfig< }, request: { - url: (params) => `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/objects/companies/${safeUrlPathSegment(params.companyId, 'companyId')}`, method: 'DELETE', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/delete_contact.ts b/apps/sim/tools/hubspot/delete_contact.ts index b6476f1c4e5..1805b2f3410 100644 --- a/apps/sim/tools/hubspot/delete_contact.ts +++ b/apps/sim/tools/hubspot/delete_contact.ts @@ -4,6 +4,7 @@ import type { HubSpotDeleteContactResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteContact') @@ -37,7 +38,8 @@ export const hubspotDeleteContactTool: ToolConfig< }, request: { - url: (params) => `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/objects/contacts/${safeUrlPathSegment(params.contactId, 'contactId')}`, method: 'DELETE', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/delete_deal.ts b/apps/sim/tools/hubspot/delete_deal.ts index 98850da0b85..f796bbff03d 100644 --- a/apps/sim/tools/hubspot/delete_deal.ts +++ b/apps/sim/tools/hubspot/delete_deal.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotDeleteDealParams, HubSpotDeleteDealResponse } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteDeal') @@ -32,7 +33,8 @@ export const hubspotDeleteDealTool: ToolConfig `https://api.hubapi.com/crm/v3/objects/deals/${params.dealId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/objects/deals/${safeUrlPathSegment(params.dealId, 'dealId')}`, method: 'DELETE', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/delete_line_item.ts b/apps/sim/tools/hubspot/delete_line_item.ts index e1b71a5b019..45a98b4f3f8 100644 --- a/apps/sim/tools/hubspot/delete_line_item.ts +++ b/apps/sim/tools/hubspot/delete_line_item.ts @@ -4,6 +4,7 @@ import type { HubSpotDeleteLineItemResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteLineItem') @@ -37,7 +38,8 @@ export const hubspotDeleteLineItemTool: ToolConfig< }, request: { - url: (params) => `https://api.hubapi.com/crm/v3/objects/line_items/${params.lineItemId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/objects/line_items/${safeUrlPathSegment(params.lineItemId, 'lineItemId')}`, method: 'DELETE', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/delete_ticket.ts b/apps/sim/tools/hubspot/delete_ticket.ts index 6308a6107ed..0201b648ef7 100644 --- a/apps/sim/tools/hubspot/delete_ticket.ts +++ b/apps/sim/tools/hubspot/delete_ticket.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotDeleteTicketParams, HubSpotDeleteTicketResponse } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotDeleteTicket') @@ -34,7 +35,8 @@ export const hubspotDeleteTicketTool: ToolConfig< }, request: { - url: (params) => `https://api.hubapi.com/crm/v3/objects/tickets/${params.ticketId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/objects/tickets/${safeUrlPathSegment(params.ticketId, 'ticketId')}`, method: 'DELETE', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/get_appointment.ts b/apps/sim/tools/hubspot/get_appointment.ts index c0c3a59217e..ce227f29430 100644 --- a/apps/sim/tools/hubspot/get_appointment.ts +++ b/apps/sim/tools/hubspot/get_appointment.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { APPOINTMENT_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetAppointment') @@ -59,7 +60,7 @@ export const hubspotGetAppointmentTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${params.appointmentId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${safeUrlPathSegment(params.appointmentId, 'appointmentId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) if (params.properties) queryParams.append('properties', params.properties) diff --git a/apps/sim/tools/hubspot/get_association_labels.ts b/apps/sim/tools/hubspot/get_association_labels.ts index 4e9beb4de83..449cbdb12cd 100644 --- a/apps/sim/tools/hubspot/get_association_labels.ts +++ b/apps/sim/tools/hubspot/get_association_labels.ts @@ -4,6 +4,7 @@ import type { HubSpotGetAssociationLabelsResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetAssociationLabels') @@ -45,8 +46,8 @@ export const hubspotGetAssociationLabelsTool: ToolConfig< request: { url: (params) => { - const from = encodeURIComponent(params.objectType.trim()) - const to = encodeURIComponent(params.toObjectType.trim()) + const from = safeUrlPathSegment(params.objectType, 'objectType') + const to = safeUrlPathSegment(params.toObjectType, 'toObjectType') return `https://api.hubapi.com/crm/v4/associations/${from}/${to}/labels` }, method: 'GET', diff --git a/apps/sim/tools/hubspot/get_cart.ts b/apps/sim/tools/hubspot/get_cart.ts index 147cfd1c7b7..f4e545282df 100644 --- a/apps/sim/tools/hubspot/get_cart.ts +++ b/apps/sim/tools/hubspot/get_cart.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetCartParams, HubSpotGetCartResponse } from '@/tools/hubspot/types' import { GENERIC_CRM_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetCart') @@ -45,7 +46,7 @@ export const hubspotGetCartTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/carts/${params.cartId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/carts/${safeUrlPathSegment(params.cartId, 'cartId')}` const queryParams = new URLSearchParams() if (params.properties) queryParams.append('properties', params.properties) if (params.associations) queryParams.append('associations', params.associations) diff --git a/apps/sim/tools/hubspot/get_company.ts b/apps/sim/tools/hubspot/get_company.ts index d1cc6cb7915..b3b75cde675 100644 --- a/apps/sim/tools/hubspot/get_company.ts +++ b/apps/sim/tools/hubspot/get_company.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetCompanyParams, HubSpotGetCompanyResponse } from '@/tools/hubspot/types' import { COMPANY_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetCompany') @@ -55,7 +56,7 @@ export const hubspotGetCompanyTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${safeUrlPathSegment(params.companyId, 'companyId')}` const queryParams = new URLSearchParams() if (params.idProperty) { diff --git a/apps/sim/tools/hubspot/get_contact.ts b/apps/sim/tools/hubspot/get_contact.ts index 811c4b1ac04..b08e1d23e82 100644 --- a/apps/sim/tools/hubspot/get_contact.ts +++ b/apps/sim/tools/hubspot/get_contact.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetContactParams, HubSpotGetContactResponse } from '@/tools/hubspot/types' import { CONTACT_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetContact') @@ -55,7 +56,7 @@ export const hubspotGetContactTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${safeUrlPathSegment(params.contactId, 'contactId')}` const queryParams = new URLSearchParams() if (params.idProperty) { diff --git a/apps/sim/tools/hubspot/get_deal.ts b/apps/sim/tools/hubspot/get_deal.ts index b42dec365aa..6164ea2f69c 100644 --- a/apps/sim/tools/hubspot/get_deal.ts +++ b/apps/sim/tools/hubspot/get_deal.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetDealParams, HubSpotGetDealResponse } from '@/tools/hubspot/types' import { DEAL_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetDeal') @@ -53,7 +54,7 @@ export const hubspotGetDealTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/deals/${params.dealId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/deals/${safeUrlPathSegment(params.dealId, 'dealId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) if (params.properties) queryParams.append('properties', params.properties) diff --git a/apps/sim/tools/hubspot/get_email.ts b/apps/sim/tools/hubspot/get_email.ts index 0097867bd1c..77f7fe19adc 100644 --- a/apps/sim/tools/hubspot/get_email.ts +++ b/apps/sim/tools/hubspot/get_email.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetEmailParams, HubSpotGetEmailResponse } from '@/tools/hubspot/types' import { EMAIL_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetEmail') @@ -48,7 +49,7 @@ export const hubspotGetEmailTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/emails/${params.emailId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/emails/${safeUrlPathSegment(params.emailId, 'emailId')}` const queryParams = new URLSearchParams() if (params.properties) { diff --git a/apps/sim/tools/hubspot/get_line_item.ts b/apps/sim/tools/hubspot/get_line_item.ts index 7bc6925a5ad..7a3c6f160af 100644 --- a/apps/sim/tools/hubspot/get_line_item.ts +++ b/apps/sim/tools/hubspot/get_line_item.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetLineItemParams, HubSpotGetLineItemResponse } from '@/tools/hubspot/types' import { LINE_ITEM_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetLineItem') @@ -56,7 +57,7 @@ export const hubspotGetLineItemTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/line_items/${params.lineItemId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/line_items/${safeUrlPathSegment(params.lineItemId, 'lineItemId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) if (params.properties) queryParams.append('properties', params.properties) diff --git a/apps/sim/tools/hubspot/get_list.ts b/apps/sim/tools/hubspot/get_list.ts index 2edfd253635..b3375201234 100644 --- a/apps/sim/tools/hubspot/get_list.ts +++ b/apps/sim/tools/hubspot/get_list.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetListParams, HubSpotGetListResponse } from '@/tools/hubspot/types' import { LIST_OUTPUT_PROPERTIES } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetList') @@ -32,7 +33,8 @@ export const hubspotGetListTool: ToolConfig `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}`, + url: (params) => + `https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}`, method: 'GET', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/get_list_memberships.ts b/apps/sim/tools/hubspot/get_list_memberships.ts index c3a8992170e..8d38147a0b7 100644 --- a/apps/sim/tools/hubspot/get_list_memberships.ts +++ b/apps/sim/tools/hubspot/get_list_memberships.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { METADATA_OUTPUT, PAGING_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetListMemberships') @@ -51,7 +52,7 @@ export const hubspotGetListMembershipsTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}/memberships` + const baseUrl = `https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}/memberships` const queryParams = new URLSearchParams() if (params.limit) queryParams.append('limit', params.limit) if (params.after) queryParams.append('after', params.after) diff --git a/apps/sim/tools/hubspot/get_marketing_event.ts b/apps/sim/tools/hubspot/get_marketing_event.ts index 1df13f27ebb..08c89083106 100644 --- a/apps/sim/tools/hubspot/get_marketing_event.ts +++ b/apps/sim/tools/hubspot/get_marketing_event.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { MARKETING_EVENT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetMarketingEvent') @@ -39,7 +40,7 @@ export const hubspotGetMarketingEventTool: ToolConfig< request: { url: (params) => - `https://api.hubapi.com/marketing/v3/marketing-events/${params.eventId.trim()}`, + `https://api.hubapi.com/marketing/v3/marketing-events/${safeUrlPathSegment(params.eventId, 'eventId')}`, method: 'GET', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/get_note.ts b/apps/sim/tools/hubspot/get_note.ts index 4e41f0ae00e..be774f1f7e9 100644 --- a/apps/sim/tools/hubspot/get_note.ts +++ b/apps/sim/tools/hubspot/get_note.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetNoteParams, HubSpotGetNoteResponse } from '@/tools/hubspot/types' import { NOTE_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetNote') @@ -47,7 +48,7 @@ export const hubspotGetNoteTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/notes/${params.noteId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/notes/${safeUrlPathSegment(params.noteId, 'noteId')}` const queryParams = new URLSearchParams() if (params.properties) { diff --git a/apps/sim/tools/hubspot/get_properties.ts b/apps/sim/tools/hubspot/get_properties.ts index ef601eb3f12..09e771ae21a 100644 --- a/apps/sim/tools/hubspot/get_properties.ts +++ b/apps/sim/tools/hubspot/get_properties.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { PROPERTIES_ARRAY_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetProperties') @@ -54,10 +55,10 @@ export const hubspotGetPropertiesTool: ToolConfig< request: { url: (params) => { - const objectType = params.objectType.trim() + const objectType = safeUrlPathSegment(params.objectType, 'objectType') const baseUrl = params.propertyName - ? `https://api.hubapi.com/crm/v3/properties/${encodeURIComponent(objectType)}/${encodeURIComponent(params.propertyName.trim())}` - : `https://api.hubapi.com/crm/v3/properties/${encodeURIComponent(objectType)}` + ? `https://api.hubapi.com/crm/v3/properties/${objectType}/${safeUrlPathSegment(params.propertyName, 'propertyName')}` + : `https://api.hubapi.com/crm/v3/properties/${objectType}` const queryParams = new URLSearchParams() if (params.archived !== undefined) { diff --git a/apps/sim/tools/hubspot/get_quote.ts b/apps/sim/tools/hubspot/get_quote.ts index e02c846e947..cbbb49f2872 100644 --- a/apps/sim/tools/hubspot/get_quote.ts +++ b/apps/sim/tools/hubspot/get_quote.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetQuoteParams, HubSpotGetQuoteResponse } from '@/tools/hubspot/types' import { QUOTE_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetQuote') @@ -53,7 +54,7 @@ export const hubspotGetQuoteTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/quotes/${params.quoteId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/quotes/${safeUrlPathSegment(params.quoteId, 'quoteId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) if (params.properties) queryParams.append('properties', params.properties) diff --git a/apps/sim/tools/hubspot/get_ticket.ts b/apps/sim/tools/hubspot/get_ticket.ts index 4d53c722c11..00cc03c3fcd 100644 --- a/apps/sim/tools/hubspot/get_ticket.ts +++ b/apps/sim/tools/hubspot/get_ticket.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotGetTicketParams, HubSpotGetTicketResponse } from '@/tools/hubspot/types' import { TICKET_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotGetTicket') @@ -53,7 +54,7 @@ export const hubspotGetTicketTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/tickets/${params.ticketId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/tickets/${safeUrlPathSegment(params.ticketId, 'ticketId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) if (params.properties) queryParams.append('properties', params.properties) diff --git a/apps/sim/tools/hubspot/list_associations.ts b/apps/sim/tools/hubspot/list_associations.ts index d463555cbe1..3921ce0b3aa 100644 --- a/apps/sim/tools/hubspot/list_associations.ts +++ b/apps/sim/tools/hubspot/list_associations.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { ASSOCIATIONS_ARRAY_OUTPUT, METADATA_OUTPUT, PAGING_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotListAssociations') @@ -64,7 +65,7 @@ export const hubspotListAssociationsTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v4/objects/${encodeURIComponent(params.objectType.trim())}/${encodeURIComponent(params.objectId.trim())}/associations/${encodeURIComponent(params.toObjectType.trim())}` + const baseUrl = `https://api.hubapi.com/crm/v4/objects/${safeUrlPathSegment(params.objectType, 'objectType')}/${safeUrlPathSegment(params.objectId, 'objectId')}/associations/${safeUrlPathSegment(params.toObjectType, 'toObjectType')}` const queryParams = new URLSearchParams() if (params.limit) { diff --git a/apps/sim/tools/hubspot/remove_list_memberships.ts b/apps/sim/tools/hubspot/remove_list_memberships.ts index a5706f00687..900b62159df 100644 --- a/apps/sim/tools/hubspot/remove_list_memberships.ts +++ b/apps/sim/tools/hubspot/remove_list_memberships.ts @@ -4,6 +4,7 @@ import type { HubSpotRemoveListMembershipsResponse, } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotRemoveListMemberships') @@ -45,7 +46,7 @@ export const hubspotRemoveListMembershipsTool: ToolConfig< request: { url: (params) => - `https://api.hubapi.com/crm/v3/lists/${params.listId.trim()}/memberships/remove`, + `https://api.hubapi.com/crm/v3/lists/${safeUrlPathSegment(params.listId, 'listId')}/memberships/remove`, method: 'PUT', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/hubspot/update_appointment.ts b/apps/sim/tools/hubspot/update_appointment.ts index 965b5d86b12..18aa8e65479 100644 --- a/apps/sim/tools/hubspot/update_appointment.ts +++ b/apps/sim/tools/hubspot/update_appointment.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { APPOINTMENT_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateAppointment') @@ -52,7 +53,7 @@ export const hubspotUpdateAppointmentTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${params.appointmentId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/appointments/${safeUrlPathSegment(params.appointmentId, 'appointmentId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) const queryString = queryParams.toString() diff --git a/apps/sim/tools/hubspot/update_company.ts b/apps/sim/tools/hubspot/update_company.ts index 8bb8c0e99aa..79ef29af9ef 100644 --- a/apps/sim/tools/hubspot/update_company.ts +++ b/apps/sim/tools/hubspot/update_company.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { COMPANY_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateCompany') @@ -53,7 +54,7 @@ export const hubspotUpdateCompanyTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${safeUrlPathSegment(params.companyId, 'companyId')}` if (params.idProperty) { return `${baseUrl}?${new URLSearchParams({ idProperty: params.idProperty })}` } diff --git a/apps/sim/tools/hubspot/update_contact.ts b/apps/sim/tools/hubspot/update_contact.ts index 364550812e0..c7d58820554 100644 --- a/apps/sim/tools/hubspot/update_contact.ts +++ b/apps/sim/tools/hubspot/update_contact.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { CONTACT_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateContact') @@ -53,7 +54,7 @@ export const hubspotUpdateContactTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${safeUrlPathSegment(params.contactId, 'contactId')}` if (params.idProperty) { return `${baseUrl}?${new URLSearchParams({ idProperty: params.idProperty })}` } diff --git a/apps/sim/tools/hubspot/update_deal.ts b/apps/sim/tools/hubspot/update_deal.ts index 7b842d2a4be..a57b5e03511 100644 --- a/apps/sim/tools/hubspot/update_deal.ts +++ b/apps/sim/tools/hubspot/update_deal.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotUpdateDealParams, HubSpotUpdateDealResponse } from '@/tools/hubspot/types' import { DEAL_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateDeal') @@ -47,7 +48,7 @@ export const hubspotUpdateDealTool: ToolConfig { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/deals/${params.dealId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/deals/${safeUrlPathSegment(params.dealId, 'dealId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) const queryString = queryParams.toString() diff --git a/apps/sim/tools/hubspot/update_line_item.ts b/apps/sim/tools/hubspot/update_line_item.ts index 14a0418f1be..005cde6eccd 100644 --- a/apps/sim/tools/hubspot/update_line_item.ts +++ b/apps/sim/tools/hubspot/update_line_item.ts @@ -5,6 +5,7 @@ import type { } from '@/tools/hubspot/types' import { LINE_ITEM_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateLineItem') @@ -52,7 +53,7 @@ export const hubspotUpdateLineItemTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/line_items/${params.lineItemId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/line_items/${safeUrlPathSegment(params.lineItemId, 'lineItemId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) const queryString = queryParams.toString() diff --git a/apps/sim/tools/hubspot/update_ticket.ts b/apps/sim/tools/hubspot/update_ticket.ts index b8fea578e4e..0548a7d3c32 100644 --- a/apps/sim/tools/hubspot/update_ticket.ts +++ b/apps/sim/tools/hubspot/update_ticket.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { HubSpotUpdateTicketParams, HubSpotUpdateTicketResponse } from '@/tools/hubspot/types' import { TICKET_OBJECT_OUTPUT } from '@/tools/hubspot/types' import type { ToolConfig } from '@/tools/types' +import { safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('HubSpotUpdateTicket') @@ -49,7 +50,7 @@ export const hubspotUpdateTicketTool: ToolConfig< request: { url: (params) => { - const baseUrl = `https://api.hubapi.com/crm/v3/objects/tickets/${params.ticketId.trim()}` + const baseUrl = `https://api.hubapi.com/crm/v3/objects/tickets/${safeUrlPathSegment(params.ticketId, 'ticketId')}` const queryParams = new URLSearchParams() if (params.idProperty) queryParams.append('idProperty', params.idProperty) const queryString = queryParams.toString() diff --git a/apps/sim/tools/salesforce/delete_account.ts b/apps/sim/tools/salesforce/delete_account.ts index f43a15eb60b..0484c265eee 100644 --- a/apps/sim/tools/salesforce/delete_account.ts +++ b/apps/sim/tools/salesforce/delete_account.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteAccountTool: ToolConfig< @@ -47,7 +47,7 @@ export const salesforceDeleteAccountTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const accountId = requireId(params.accountId, 'Account ID') + const accountId = idPathSegment(params.accountId, 'Account ID') return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, diff --git a/apps/sim/tools/salesforce/delete_case.ts b/apps/sim/tools/salesforce/delete_case.ts index 2a77f70d609..0092fd13546 100644 --- a/apps/sim/tools/salesforce/delete_case.ts +++ b/apps/sim/tools/salesforce/delete_case.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteCaseResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteCaseTool: ToolConfig< @@ -46,7 +46,7 @@ export const salesforceDeleteCaseTool: ToolConfig< request: { url: (params) => { - const caseId = requireId(params.caseId, 'Case ID') + const caseId = idPathSegment(params.caseId, 'Case ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_contact.ts b/apps/sim/tools/salesforce/delete_contact.ts index 22e9574666e..ccd3b19e1c0 100644 --- a/apps/sim/tools/salesforce/delete_contact.ts +++ b/apps/sim/tools/salesforce/delete_contact.ts @@ -4,7 +4,7 @@ import type { SalesforceDeleteContactResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -35,7 +35,7 @@ export const salesforceDeleteContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const contactId = requireId(params.contactId, 'Contact ID') + const contactId = idPathSegment(params.contactId, 'Contact ID') return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_custom_field.ts b/apps/sim/tools/salesforce/delete_custom_field.ts index 5889fdcb340..39828d86738 100644 --- a/apps/sim/tools/salesforce/delete_custom_field.ts +++ b/apps/sim/tools/salesforce/delete_custom_field.ts @@ -4,7 +4,7 @@ import type { SalesforceDeleteCustomFieldResponse, } from '@/tools/salesforce/types' import { CUSTOM_FIELD_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceDeleteCustomField') @@ -45,7 +45,7 @@ export const salesforceDeleteCustomFieldTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const fieldId = requireId(params.fieldId, 'Field ID') + const fieldId = idPathSegment(params.fieldId, 'Field ID') return `${instanceUrl}/services/data/v59.0/tooling/sobjects/CustomField/${fieldId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_lead.ts b/apps/sim/tools/salesforce/delete_lead.ts index d3d357b3790..fc6363020ef 100644 --- a/apps/sim/tools/salesforce/delete_lead.ts +++ b/apps/sim/tools/salesforce/delete_lead.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteLeadResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteLeadTool: ToolConfig< @@ -34,7 +34,7 @@ export const salesforceDeleteLeadTool: ToolConfig< request: { url: (params) => { - const leadId = requireId(params.leadId, 'Lead ID') + const leadId = idPathSegment(params.leadId, 'Lead ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_opportunity.ts b/apps/sim/tools/salesforce/delete_opportunity.ts index 4d52d7349a1..ede7263acc8 100644 --- a/apps/sim/tools/salesforce/delete_opportunity.ts +++ b/apps/sim/tools/salesforce/delete_opportunity.ts @@ -4,7 +4,7 @@ import type { SalesforceDeleteOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceDeleteOpportunity') @@ -37,7 +37,7 @@ export const salesforceDeleteOpportunityTool: ToolConfig< request: { url: (params) => { - const opportunityId = requireId(params.opportunityId, 'Opportunity ID') + const opportunityId = idPathSegment(params.opportunityId, 'Opportunity ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/delete_task.ts b/apps/sim/tools/salesforce/delete_task.ts index 372a3dba11b..9875d59ceb4 100644 --- a/apps/sim/tools/salesforce/delete_task.ts +++ b/apps/sim/tools/salesforce/delete_task.ts @@ -3,7 +3,7 @@ import type { SalesforceDeleteTaskResponse, } from '@/tools/salesforce/types' import { SOBJECT_DELETE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceDeleteTaskTool: ToolConfig< @@ -46,7 +46,7 @@ export const salesforceDeleteTaskTool: ToolConfig< request: { url: (params) => { - const taskId = requireId(params.taskId, 'Task ID') + const taskId = idPathSegment(params.taskId, 'Task ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` }, method: 'DELETE', diff --git a/apps/sim/tools/salesforce/describe_object.ts b/apps/sim/tools/salesforce/describe_object.ts index 20fd5efd57f..66d2fd16ba0 100644 --- a/apps/sim/tools/salesforce/describe_object.ts +++ b/apps/sim/tools/salesforce/describe_object.ts @@ -4,7 +4,7 @@ import type { SalesforceDescribeObjectResponse, } from '@/tools/salesforce/types' import { DESCRIBE_OBJECT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceQuery') @@ -47,7 +47,7 @@ export const salesforceDescribeObjectTool: ToolConfig< ) } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const objectName = params.objectName.trim() + const objectName = idPathSegment(params.objectName, 'Object Name') return `${instanceUrl}/services/data/v59.0/sobjects/${objectName}/describe` }, method: 'GET', diff --git a/apps/sim/tools/salesforce/get_cases.ts b/apps/sim/tools/salesforce/get_cases.ts index cd45a33c064..640296511a0 100644 --- a/apps/sim/tools/salesforce/get_cases.ts +++ b/apps/sim/tools/salesforce/get_cases.ts @@ -1,6 +1,6 @@ import type { SalesforceGetCasesParams, SalesforceGetCasesResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetCasesTool: ToolConfig< @@ -52,7 +52,7 @@ export const salesforceGetCasesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.caseId) { - const caseId = requireId(params.caseId, 'Case ID') + const caseId = idPathSegment(params.caseId, 'Case ID') const fields = params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId' return `${instanceUrl}/services/data/v59.0/sobjects/Case/${caseId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_contacts.ts b/apps/sim/tools/salesforce/get_contacts.ts index acfbdc50170..c4cb726c9ab 100644 --- a/apps/sim/tools/salesforce/get_contacts.ts +++ b/apps/sim/tools/salesforce/get_contacts.ts @@ -4,7 +4,7 @@ import type { SalesforceGetContactsResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -60,7 +60,7 @@ export const salesforceGetContactsTool: ToolConfig< // Single contact by ID if (params.contactId) { - const contactId = requireId(params.contactId, 'Contact ID') + const contactId = idPathSegment(params.contactId, 'Contact ID') const fields = params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department' return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_dashboard.ts b/apps/sim/tools/salesforce/get_dashboard.ts index 41549a684a7..6ac42036360 100644 --- a/apps/sim/tools/salesforce/get_dashboard.ts +++ b/apps/sim/tools/salesforce/get_dashboard.ts @@ -4,7 +4,7 @@ import type { SalesforceGetDashboardResponse, } from '@/tools/salesforce/types' import { DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceDashboards') @@ -45,7 +45,7 @@ export const salesforceGetDashboardTool: ToolConfig< throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.') } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}` + return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${idPathSegment(params.dashboardId, 'Dashboard ID')}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/salesforce/get_leads.ts b/apps/sim/tools/salesforce/get_leads.ts index e49afe9a0c5..59c31437b6a 100644 --- a/apps/sim/tools/salesforce/get_leads.ts +++ b/apps/sim/tools/salesforce/get_leads.ts @@ -1,6 +1,6 @@ import type { SalesforceGetLeadsParams, SalesforceGetLeadsResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetLeadsTool: ToolConfig< @@ -52,7 +52,7 @@ export const salesforceGetLeadsTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.leadId) { - const leadId = requireId(params.leadId, 'Lead ID') + const leadId = idPathSegment(params.leadId, 'Lead ID') const fields = params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource' return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${leadId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/get_opportunities.ts b/apps/sim/tools/salesforce/get_opportunities.ts index b4a86a2dda5..250b9363ecd 100644 --- a/apps/sim/tools/salesforce/get_opportunities.ts +++ b/apps/sim/tools/salesforce/get_opportunities.ts @@ -4,7 +4,7 @@ import type { SalesforceGetOpportunitiesResponse, } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceGetOpportunities') @@ -58,7 +58,7 @@ export const salesforceGetOpportunitiesTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.opportunityId) { - const opportunityId = requireId(params.opportunityId, 'Opportunity ID') + const opportunityId = idPathSegment(params.opportunityId, 'Opportunity ID') const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability' return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${opportunityId}?fields=${encodeURIComponent(fields)}` } diff --git a/apps/sim/tools/salesforce/get_report.ts b/apps/sim/tools/salesforce/get_report.ts index 2d68d7a8d1f..1a3194ced26 100644 --- a/apps/sim/tools/salesforce/get_report.ts +++ b/apps/sim/tools/salesforce/get_report.ts @@ -4,7 +4,7 @@ import type { SalesforceGetReportResponse, } from '@/tools/salesforce/types' import { GET_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceReports') @@ -45,7 +45,7 @@ export const salesforceGetReportTool: ToolConfig< throw new Error('Report ID is required. Please provide a valid Salesforce Report ID.') } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}/describe` + return `${instanceUrl}/services/data/v59.0/analytics/reports/${idPathSegment(params.reportId, 'Report ID')}/describe` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/salesforce/get_tasks.ts b/apps/sim/tools/salesforce/get_tasks.ts index be1e7c0f686..af53bf4b57c 100644 --- a/apps/sim/tools/salesforce/get_tasks.ts +++ b/apps/sim/tools/salesforce/get_tasks.ts @@ -1,6 +1,6 @@ import type { SalesforceGetTasksParams, SalesforceGetTasksResponse } from '@/tools/salesforce/types' import { QUERY_PAGING_OUTPUT, RESPONSE_METADATA_OUTPUT } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceGetTasksTool: ToolConfig< @@ -65,7 +65,7 @@ export const salesforceGetTasksTool: ToolConfig< url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) if (params.taskId) { - const taskId = requireId(params.taskId, 'Task ID') + const taskId = idPathSegment(params.taskId, 'Task ID') const fields = params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId' return `${instanceUrl}/services/data/v59.0/sobjects/Task/${taskId}?fields=${encodeURIComponent(fields)}` diff --git a/apps/sim/tools/salesforce/query_more.ts b/apps/sim/tools/salesforce/query_more.ts index a6efc0bed5c..b216ce05382 100644 --- a/apps/sim/tools/salesforce/query_more.ts +++ b/apps/sim/tools/salesforce/query_more.ts @@ -4,7 +4,11 @@ import type { SalesforceQueryMoreResponse, } from '@/tools/salesforce/types' import { QUERY_MORE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { + extractErrorMessage, + getInstanceUrl, + resolveNextRecordsPath, +} from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceQuery') @@ -48,11 +52,7 @@ export const salesforceQueryMoreTool: ToolConfig< ) } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - // nextRecordsUrl is typically a relative path like /services/data/v59.0/query/01g... - const nextUrl = params.nextRecordsUrl.startsWith('/') - ? params.nextRecordsUrl - : `/${params.nextRecordsUrl}` - return `${instanceUrl}${nextUrl}` + return `${instanceUrl}${resolveNextRecordsPath(params.nextRecordsUrl, instanceUrl)}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/salesforce/refresh_dashboard.ts b/apps/sim/tools/salesforce/refresh_dashboard.ts index 0179b707dfe..e86ef8d1143 100644 --- a/apps/sim/tools/salesforce/refresh_dashboard.ts +++ b/apps/sim/tools/salesforce/refresh_dashboard.ts @@ -4,7 +4,7 @@ import type { SalesforceRefreshDashboardResponse, } from '@/tools/salesforce/types' import { REFRESH_DASHBOARD_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceDashboards') @@ -45,7 +45,7 @@ export const salesforceRefreshDashboardTool: ToolConfig< throw new Error('Dashboard ID is required. Please provide a valid Salesforce Dashboard ID.') } const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${params.dashboardId}` + return `${instanceUrl}/services/data/v59.0/analytics/dashboards/${idPathSegment(params.dashboardId, 'Dashboard ID')}` }, method: 'PUT', headers: (params) => ({ diff --git a/apps/sim/tools/salesforce/run_report.ts b/apps/sim/tools/salesforce/run_report.ts index cf777033cb5..184495bccbc 100644 --- a/apps/sim/tools/salesforce/run_report.ts +++ b/apps/sim/tools/salesforce/run_report.ts @@ -5,7 +5,7 @@ import type { SalesforceRunReportResponse, } from '@/tools/salesforce/types' import { RUN_REPORT_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceReports') @@ -61,7 +61,7 @@ export const salesforceRunReportTool: ToolConfig< // Default to including detail rows (Salesforce's own API default is false); // report runs in a workflow almost always want the underlying rows. const includeDetails = params.includeDetails !== 'false' - return `${instanceUrl}/services/data/v59.0/analytics/reports/${params.reportId}?includeDetails=${includeDetails}` + return `${instanceUrl}/services/data/v59.0/analytics/reports/${idPathSegment(params.reportId, 'Report ID')}?includeDetails=${includeDetails}` }, // Use GET for simple report runs, POST only when filters are provided method: (params) => (params.filters ? 'POST' : 'GET'), diff --git a/apps/sim/tools/salesforce/update_account.ts b/apps/sim/tools/salesforce/update_account.ts index 8fc579c8b96..4181619dbfb 100644 --- a/apps/sim/tools/salesforce/update_account.ts +++ b/apps/sim/tools/salesforce/update_account.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateAccountResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateAccountTool: ToolConfig< @@ -125,7 +125,7 @@ export const salesforceUpdateAccountTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const accountId = requireId(params.accountId, 'Account ID') + const accountId = idPathSegment(params.accountId, 'Account ID') return `${instanceUrl}/services/data/v59.0/sobjects/Account/${accountId}` }, diff --git a/apps/sim/tools/salesforce/update_case.ts b/apps/sim/tools/salesforce/update_case.ts index 604285f0923..299fbca1689 100644 --- a/apps/sim/tools/salesforce/update_case.ts +++ b/apps/sim/tools/salesforce/update_case.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateCaseResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateCaseTool: ToolConfig< @@ -88,7 +88,7 @@ export const salesforceUpdateCaseTool: ToolConfig< request: { url: (params) => { - const caseId = requireId(params.caseId, 'Case ID') + const caseId = idPathSegment(params.caseId, 'Case ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${caseId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_contact.ts b/apps/sim/tools/salesforce/update_contact.ts index ff745bb80df..b67b78c2a71 100644 --- a/apps/sim/tools/salesforce/update_contact.ts +++ b/apps/sim/tools/salesforce/update_contact.ts @@ -4,7 +4,7 @@ import type { SalesforceUpdateContactResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceContacts') @@ -108,7 +108,7 @@ export const salesforceUpdateContactTool: ToolConfig< request: { url: (params) => { const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl) - const contactId = requireId(params.contactId, 'Contact ID') + const contactId = idPathSegment(params.contactId, 'Contact ID') return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${contactId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_lead.ts b/apps/sim/tools/salesforce/update_lead.ts index c0c44f8e2ef..70e48e5f163 100644 --- a/apps/sim/tools/salesforce/update_lead.ts +++ b/apps/sim/tools/salesforce/update_lead.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateLeadResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateLeadTool: ToolConfig< @@ -83,7 +83,7 @@ export const salesforceUpdateLeadTool: ToolConfig< request: { url: (params) => { - const leadId = requireId(params.leadId, 'Lead ID') + const leadId = idPathSegment(params.leadId, 'Lead ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${leadId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_opportunity.ts b/apps/sim/tools/salesforce/update_opportunity.ts index bdfa007f801..f742e29a6e4 100644 --- a/apps/sim/tools/salesforce/update_opportunity.ts +++ b/apps/sim/tools/salesforce/update_opportunity.ts @@ -4,7 +4,7 @@ import type { SalesforceUpdateOpportunityResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SalesforceUpdateOpportunity') @@ -79,7 +79,7 @@ export const salesforceUpdateOpportunityTool: ToolConfig< request: { url: (params) => { - const opportunityId = requireId(params.opportunityId, 'Opportunity ID') + const opportunityId = idPathSegment(params.opportunityId, 'Opportunity ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${opportunityId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/update_task.ts b/apps/sim/tools/salesforce/update_task.ts index e42f9722b8f..a81f63e50f6 100644 --- a/apps/sim/tools/salesforce/update_task.ts +++ b/apps/sim/tools/salesforce/update_task.ts @@ -3,7 +3,7 @@ import type { SalesforceUpdateTaskResponse, } from '@/tools/salesforce/types' import { SOBJECT_UPDATE_OUTPUT_PROPERTIES } from '@/tools/salesforce/types' -import { extractErrorMessage, getInstanceUrl, requireId } from '@/tools/salesforce/utils' +import { extractErrorMessage, getInstanceUrl, idPathSegment } from '@/tools/salesforce/utils' import type { ToolConfig } from '@/tools/types' export const salesforceUpdateTaskTool: ToolConfig< @@ -102,7 +102,7 @@ export const salesforceUpdateTaskTool: ToolConfig< request: { url: (params) => { - const taskId = requireId(params.taskId, 'Task ID') + const taskId = idPathSegment(params.taskId, 'Task ID') return `${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${taskId}` }, method: 'PATCH', diff --git a/apps/sim/tools/salesforce/utils.test.ts b/apps/sim/tools/salesforce/utils.test.ts index a8589f61d5a..fd94d4d31b6 100644 --- a/apps/sim/tools/salesforce/utils.test.ts +++ b/apps/sim/tools/salesforce/utils.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' +import { salesforceDeleteLeadTool } from '@/tools/salesforce/delete_lead' +import { salesforceQueryMoreTool } from '@/tools/salesforce/query_more' +import { salesforceRunReportTool } from '@/tools/salesforce/run_report' import { getInstanceUrl } from '@/tools/salesforce/utils' +import type { ToolConfig } from '@/tools/types' /** Builds an unsigned JWT carrying the given payload, which is all the decoder reads. */ function idTokenWith(payload: Record): string { @@ -89,3 +93,44 @@ describe('getInstanceUrl', () => { ) }) }) + +/** Resolves a tool's request URL for the given params. */ +function requestUrl

(tool: ToolConfig

, params: P): string { + const { url } = tool.request + return typeof url === 'function' ? url(params) : url +} + +describe('request paths', () => { + const ORG = 'https://acme.my.salesforce.com' + const auth = { accessToken: 'token', instanceUrl: ORG } + + it('requests an absolute nextRecordsUrl on the connected instance as a path', () => { + expect( + requestUrl(salesforceQueryMoreTool, { + ...auth, + nextRecordsUrl: `${ORG}/services/data/v59.0/query/01gxx000000B5OgAAK-2000`, + }) + ).toBe(`${ORG}/services/data/v59.0/query/01gxx000000B5OgAAK-2000`) + }) + + it.each([ + ['another host', 'https://other.my.salesforce.com/services/data/v59.0/query/01g-2000'], + ['a path outside the data API', '/secur/logout.jsp'], + ['a dot segment', '/services/data/v59.0/query/../../../secur/logout.jsp'], + ])('rejects a nextRecordsUrl on %s', (_label, nextRecordsUrl) => { + expect(() => requestUrl(salesforceQueryMoreTool, { ...auth, nextRecordsUrl })).toThrow() + }) + + it('trims a report id carried in from another block', () => { + expect( + requestUrl(salesforceRunReportTool, { ...auth, reportId: ' 00Oxx0000001abcEAA\n' }) + ).toBe(`${ORG}/services/data/v59.0/analytics/reports/00Oxx0000001abcEAA?includeDetails=true`) + }) + + it.each(['..', '00Qxx/../../sobjects/Account/001xx'])( + 'rejects a record id that would leave its sObject path: %s', + (leadId) => { + expect(() => requestUrl(salesforceDeleteLeadTool, { ...auth, leadId })).toThrow() + } + ) +}) diff --git a/apps/sim/tools/salesforce/utils.ts b/apps/sim/tools/salesforce/utils.ts index cf80f473656..9a76bb92556 100644 --- a/apps/sim/tools/salesforce/utils.ts +++ b/apps/sim/tools/salesforce/utils.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { validateSalesforceInstanceUrl } from '@/lib/core/security/input-validation' import { isSalesforceLoginOrigin } from '@/lib/oauth/salesforce' +import { assertNoDotPathSegments, safeUrlPathSegment } from '@/tools/url-path' const logger = createLogger('SalesforceUtils') @@ -70,6 +71,46 @@ export function requireId(value: string | undefined, label: string): string { return trimmed } +/** + * Trims a record, report, dashboard, or object identifier and returns it as one + * encoded URL path segment, rejecting empty, dot, and separator values. + * @param value - The raw ID value from params + * @param label - Human-readable field name used in the error message + * @returns The encoded path segment + */ +export function idPathSegment(value: string | undefined, label: string): string { + return safeUrlPathSegment(requireId(value, label), label) +} + +/** + * Resolves a query `nextRecordsUrl` to a path on the connected instance. Salesforce + * returns it relative (`/services/data/vXX.X/query/01g...-2000`); an absolute URL on + * the same instance is accepted too, so a value copied from a browser or log works. + * @param value - The nextRecordsUrl from a previous query response + * @param instanceUrl - The connected org's https origin + * @returns The path (with query string) to request on `instanceUrl` + * @throws Error if the URL points at another host or outside the REST data API + * @see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_query.htm + */ +export function resolveNextRecordsPath(value: string, instanceUrl: string): string { + let path = value.trim() + if (/^https?:\/\//i.test(path)) { + const parsed = new URL(path) + if (parsed.origin !== new URL(instanceUrl).origin) { + throw new Error('Next Records URL must belong to the connected Salesforce instance.') + } + path = `${parsed.pathname}${parsed.search}` + } + if (!path.startsWith('/')) path = `/${path}` + if (!path.startsWith('/services/data/')) { + throw new Error( + 'Next Records URL must be the nextRecordsUrl from a query response (e.g., /services/data/v59.0/query/01g...).' + ) + } + assertNoDotPathSegments(path) + return path +} + /** * Ensures a custom field/object API name carries the required `__c` suffix. * Salesforce metadata components created via the Tooling API must end in `__c`;