Skip to content

Commit 6511ea4

Browse files
authored
fix(helpscout): require explicit tags to clear, validate custom field entries and positive IDs, and correct trigger and credential copy (#9271)
* fix(helpscout): require explicit tags to clear, validate custom field entries and positive IDs, and correct trigger and credential copy * fix(helpscout): treat only a literal empty tag list as clearing every tag * fix(helpscout): keep bracketed tags on the comma-separated path and clear only on a literal []
1 parent f2ead22 commit 6511ea4

14 files changed

Lines changed: 94 additions & 84 deletions

File tree

‎apps/docs/content/docs/integrations/helpscout.mdx‎

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ Move a Help Scout conversation to another inbox.
432432

433433
### Help Scout Set Conversation Tags
434434

435-
Replace the full tag list on a Help Scout conversation. Tags left out are removed, an empty list removes every tag, and unknown tags are created.
435+
Replace the full tag list on a Help Scout conversation. Tags left out are removed, [] removes every tag, and unknown tags are created.
436436

437437
<ReferenceDetails>
438438

@@ -441,7 +441,7 @@ Replace the full tag list on a Help Scout conversation. Tags left out are remove
441441
| Parameter | Type | Required | Description |
442442
| --------- | ---- | -------- | ----------- |
443443
| `conversationId` | string | Yes | Conversation ID |
444-
| `tags` | string | No | Comma-separated tags the conversation should have, including any existing tags to keep. Leave empty to remove every tag. |
444+
| `tags` | string | Yes | Comma-separated tags the conversation should have, including any existing tags to keep. Pass \[\] to remove every tag. |
445445

446446
#### Output
447447

@@ -1345,7 +1345,7 @@ A trigger starts a workflow when a supported event occurs in this service.
13451345

13461346
### Help Scout Agent Replied
13471347

1348-
Trigger workflow when a user replies to a Help Scout conversation (auto replies are not included)
1348+
Trigger workflow when an agent replies to a Help Scout conversation (auto replies are not included)
13491349

13501350
<ReferenceDetails label="Configuration and outputs">
13511351

@@ -1373,9 +1373,9 @@ Trigger workflow when a user replies to a Help Scout conversation (auto replies
13731373
| ↳ `subject` | string | Subject |
13741374
| ↳ `preview` | string | Preview text from the most recent thread |
13751375
| ↳ `mailboxId` | number | Inbox ID |
1376-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1377-
| ↳ `createdBy` | json | User or customer who created the conversation |
1378-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1376+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1377+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1378+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
13791379
| ↳ `createdAt` | string | When the conversation was created |
13801380
| ↳ `closedAt` | string | When the conversation was closed |
13811381
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1423,9 +1423,9 @@ Trigger workflow when a Help Scout conversation is assigned
14231423
| ↳ `subject` | string | Subject |
14241424
| ↳ `preview` | string | Preview text from the most recent thread |
14251425
| ↳ `mailboxId` | number | Inbox ID |
1426-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1427-
| ↳ `createdBy` | json | User or customer who created the conversation |
1428-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1426+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1427+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1428+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
14291429
| ↳ `createdAt` | string | When the conversation was created |
14301430
| ↳ `closedAt` | string | When the conversation was closed |
14311431
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1473,9 +1473,9 @@ Trigger workflow when a conversation is created in Help Scout
14731473
| ↳ `subject` | string | Subject |
14741474
| ↳ `preview` | string | Preview text from the most recent thread |
14751475
| ↳ `mailboxId` | number | Inbox ID |
1476-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1477-
| ↳ `createdBy` | json | User or customer who created the conversation |
1478-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1476+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1477+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1478+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
14791479
| ↳ `createdAt` | string | When the conversation was created |
14801480
| ↳ `closedAt` | string | When the conversation was closed |
14811481
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1547,9 +1547,9 @@ Trigger workflow when Help Scout conversations are merged
15471547
| ↳ `subject` | string | Subject |
15481548
| ↳ `preview` | string | Preview text from the most recent thread |
15491549
| ↳ `mailboxId` | number | Inbox ID |
1550-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1551-
| ↳ `createdBy` | json | User or customer who created the conversation |
1552-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1550+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1551+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1552+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
15531553
| ↳ `createdAt` | string | When the conversation was created |
15541554
| ↳ `closedAt` | string | When the conversation was closed |
15551555
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1597,9 +1597,9 @@ Trigger workflow when a Help Scout conversation is moved to another inbox
15971597
| ↳ `subject` | string | Subject |
15981598
| ↳ `preview` | string | Preview text from the most recent thread |
15991599
| ↳ `mailboxId` | number | Inbox ID |
1600-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1601-
| ↳ `createdBy` | json | User or customer who created the conversation |
1602-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1600+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1601+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1602+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
16031603
| ↳ `createdAt` | string | When the conversation was created |
16041604
| ↳ `closedAt` | string | When the conversation was closed |
16051605
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1647,9 +1647,9 @@ Trigger workflow when the status of a Help Scout conversation changes
16471647
| ↳ `subject` | string | Subject |
16481648
| ↳ `preview` | string | Preview text from the most recent thread |
16491649
| ↳ `mailboxId` | number | Inbox ID |
1650-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1651-
| ↳ `createdBy` | json | User or customer who created the conversation |
1652-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1650+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1651+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1652+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
16531653
| ↳ `createdAt` | string | When the conversation was created |
16541654
| ↳ `closedAt` | string | When the conversation was closed |
16551655
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1697,9 +1697,9 @@ Trigger workflow when the tags on a Help Scout conversation change
16971697
| ↳ `subject` | string | Subject |
16981698
| ↳ `preview` | string | Preview text from the most recent thread |
16991699
| ↳ `mailboxId` | number | Inbox ID |
1700-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1701-
| ↳ `createdBy` | json | User or customer who created the conversation |
1702-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1700+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1701+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1702+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
17031703
| ↳ `createdAt` | string | When the conversation was created |
17041704
| ↳ `closedAt` | string | When the conversation was closed |
17051705
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1791,9 +1791,9 @@ Trigger workflow when a customer replies to a Help Scout conversation
17911791
| ↳ `subject` | string | Subject |
17921792
| ↳ `preview` | string | Preview text from the most recent thread |
17931793
| ↳ `mailboxId` | number | Inbox ID |
1794-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1795-
| ↳ `createdBy` | json | User or customer who created the conversation |
1796-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1794+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1795+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1796+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
17971797
| ↳ `createdAt` | string | When the conversation was created |
17981798
| ↳ `closedAt` | string | When the conversation was closed |
17991799
| ↳ `closedBy` | number | ID of the user who closed it |
@@ -1885,9 +1885,9 @@ Trigger workflow when a note is added to a Help Scout conversation
18851885
| ↳ `subject` | string | Subject |
18861886
| ↳ `preview` | string | Preview text from the most recent thread |
18871887
| ↳ `mailboxId` | number | Inbox ID |
1888-
| ↳ `assignee` | json | User or team the conversation is assigned to |
1889-
| ↳ `createdBy` | json | User or customer who created the conversation |
1890-
| ↳ `primaryCustomer` | json | Primary customer of the conversation |
1888+
| ↳ `assignee` | json | User or team the conversation is assigned to \(id, type, first, last, name, email\) |
1889+
| ↳ `createdBy` | json | User or customer who created the conversation \(id, type, first, last, name, email\) |
1890+
| ↳ `primaryCustomer` | json | Primary customer of the conversation \(id, type, first, last, name, email\) |
18911891
| ↳ `createdAt` | string | When the conversation was created |
18921892
| ↳ `closedAt` | string | When the conversation was closed |
18931893
| ↳ `closedBy` | number | ID of the user who closed it |

‎apps/sim/blocks/blocks/helpscout.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ export const HelpScoutBlock: BlockConfig = {
448448
type: 'short-input',
449449
placeholder: 'vip, billing',
450450
condition: { field: 'operation', value: ['create_conversation', 'set_conversation_tags'] },
451+
required: { field: 'operation', value: 'set_conversation_tags' },
451452
},
452453
{
453454
id: 'createStatus',

‎apps/sim/connectors/helpscout/helpscout.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import { type RetryOptions, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/docum
77
import { DEFAULT_MAX_CONVERSATIONS, helpScoutConnectorMeta } from '@/connectors/helpscout/meta'
88
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
99
import { computeContentHash, htmlToPlainText, joinTagArray, parseTagDate } from '@/connectors/utils'
10+
import { HELPSCOUT_API_BASE, parseIdText } from '@/tools/helpscout/utils'
1011

1112
const logger = createLogger('HelpScoutConnector')
1213

13-
const API_BASE = 'https://api.helpscout.net/v2'
14-
const HELPSCOUT_ID_PATTERN = /^\d{1,16}$/
1514
const STATUSES = new Set(['all', 'active', 'pending', 'closed'])
1615
/** Help Scout caps a conversation at 100 threads, 50 per page, so two pages cover every thread. */
1716
const MAX_THREAD_PAGES = 4
@@ -119,7 +118,7 @@ async function helpScoutGet(
119118
query: Record<string, string> = {},
120119
retryOptions?: RetryOptions
121120
): Promise<unknown> {
122-
const url = new URL(`${API_BASE}${path}`)
121+
const url = new URL(`${HELPSCOUT_API_BASE}${path}`)
123122
for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value)
124123
const response = await fetchWithRetry(
125124
url.toString(),
@@ -140,7 +139,7 @@ function optionalId(value: unknown, label: string): string | undefined {
140139
const text =
141140
typeof value === 'string' ? value.trim() : typeof value === 'number' ? String(value) : ''
142141
if (!text) return undefined
143-
if (!HELPSCOUT_ID_PATTERN.test(text)) throw new Error(`${label} must be a numeric Help Scout ID`)
142+
if (parseIdText(text) === null) throw new Error(`${label} must be a numeric Help Scout ID`)
144143
return text
145144
}
146145

@@ -404,7 +403,7 @@ export const helpScoutConnector: ConnectorConfig = {
404403
sourceConfig: Record<string, unknown>,
405404
externalId: string
406405
): Promise<ExternalDocument | null> => {
407-
if (!HELPSCOUT_ID_PATTERN.test(externalId)) return null
406+
if (parseIdText(externalId) === null) return null
408407
const includeNotes = sourceConfig.includeNotes !== 'exclude'
409408
try {
410409
const conversation = ConversationSchema.parse(

‎apps/sim/lib/credentials/client-credential-accounts/descriptors.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ export interface ClientCredentialAccountDescriptor {
105105
defaultAuthMethod?: string
106106
/** Sim setup guide, docked bottom-left of the connect modal. */
107107
docsUrl: string
108-
/** Optional one-line caveat rendered in the connect modal. */
108+
/**
109+
* Optional one-line caveat, rendered under the `orgId` field of the connect modal. A descriptor
110+
* without an `orgId` field puts its guidance in a field `hint` instead.
111+
*/
109112
helpText?: string
110113
}
111114

@@ -434,6 +437,7 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record<
434437
label: 'App ID',
435438
placeholder: 'Paste the App ID',
436439
secret: false,
440+
hint: 'Create the app in Help Scout under Your Profile > My Apps.',
437441
},
438442
{
439443
id: 'clientSecret',
@@ -443,7 +447,6 @@ export const CLIENT_CREDENTIAL_ACCOUNT_DESCRIPTORS: Record<
443447
},
444448
],
445449
docsUrl: 'https://docs.sim.ai/integrations/helpscout',
446-
helpText: 'Create the app in Help Scout under Your Profile > My Apps.',
447450
},
448451
[VANTA_SERVICE_ACCOUNT_PROVIDER_ID]: {
449452
providerId: VANTA_SERVICE_ACCOUNT_PROVIDER_ID,

‎apps/sim/lib/selectors/server/providers/helpscout.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
requireListRequest,
1919
type ServerSelectorAttachmentMap,
2020
} from '@/lib/selectors/server/types'
21-
import { HELPSCOUT_API_BASE, HELPSCOUT_ID_PATTERN } from '@/tools/helpscout/utils'
21+
import { HELPSCOUT_API_BASE, parseIdText } from '@/tools/helpscout/utils'
2222

2323
type HelpScoutSelectorKey = Extract<
2424
ServerSelectorKey,
@@ -91,9 +91,7 @@ async function helpScoutHeaders(args: ExecuteServerSelectorArgs): Promise<Record
9191

9292
function requireHelpScoutId(value: string): string {
9393
const id = value.trim()
94-
if (!HELPSCOUT_ID_PATTERN.test(id) || !Number.isSafeInteger(Number(id))) {
95-
throw new SelectorContextUnavailableError()
96-
}
94+
if (parseIdText(id) === null) throw new SelectorContextUnavailableError()
9795
return id
9896
}
9997

‎apps/sim/lib/webhooks/providers/helpscout.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ import type {
2626
} from '@/lib/webhooks/providers/types'
2727
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
2828
import { ErrorExtractorId, extractErrorMessage } from '@/tools/error-extractors'
29-
import { HELPSCOUT_API_BASE, projectConversation, projectCustomer } from '@/tools/helpscout/utils'
29+
import {
30+
HELPSCOUT_API_BASE,
31+
parseIdText,
32+
projectConversation,
33+
projectCustomer,
34+
readResourceId,
35+
} from '@/tools/helpscout/utils'
3036
import {
3137
getHelpScoutSubscriptionEvents,
3238
HELPSCOUT_GENERIC_TRIGGER_ID,
@@ -45,7 +51,6 @@ function validateHelpScoutSignature(secret: string, signature: string, rawBody:
4551
return safeCompare(computed, signature.trim())
4652
}
4753

48-
const HELPSCOUT_ID_PATTERN = /^\d{1,16}$/
4954
/** Help Scout accepts a signing secret of at most 40 characters. */
5055
const WEBHOOK_SECRET_LENGTH = 40
5156

@@ -60,8 +65,8 @@ function readEventType(headers: Record<string, string>): string {
6065
function resolveInboxFilter(config: Record<string, unknown>): number | undefined {
6166
const raw = toStringOrNull(config.triggerMailboxId)?.trim()
6267
if (!raw) return undefined
63-
const id = Number(raw)
64-
if (!HELPSCOUT_ID_PATTERN.test(raw) || !Number.isSafeInteger(id) || id <= 0) {
68+
const id = parseIdText(raw)
69+
if (id === null) {
6570
throw new WebhookDeploymentConfigurationError('The Help Scout inbox ID must be numeric.')
6671
}
6772
return id
@@ -94,12 +99,10 @@ async function readHelpScoutErrorDetail(response: Response): Promise<string> {
9499

95100
/** The new webhook ID comes from `Resource-ID`, with the `Location` URL as a fallback. */
96101
function readCreatedWebhookId(response: Response): string | null {
97-
const resourceId = toStringOrNull(response.headers.get('resource-id'))?.trim()
98-
if (resourceId && HELPSCOUT_ID_PATTERN.test(resourceId)) return resourceId
99-
const fromLocation = toStringOrNull(response.headers.get('location'))?.match(
100-
/\/webhooks\/(\d{1,16})\/?$/
101-
)?.[1]
102-
return fromLocation ?? null
102+
const id =
103+
readResourceId(response) ??
104+
parseIdText(response.headers.get('location')?.match(/\/webhooks\/(\d+)\/?$/)?.[1] ?? null)
105+
return id === null ? null : String(id)
103106
}
104107

105108
async function requireAccessToken(config: Record<string, unknown>, requestId: string) {
@@ -176,7 +179,7 @@ export const helpScoutHandler: WebhookProviderHandler = {
176179
logger.info(`[${requestId}] No managed Help Scout webhook to delete`)
177180
return
178181
}
179-
if (!HELPSCOUT_ID_PATTERN.test(externalId)) {
182+
if (parseIdText(externalId) === null) {
180183
if (strict) throw new Error('Invalid Help Scout webhook ID for deletion')
181184
return
182185
}

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/helpscout/set-conversation-tags.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { toStringOrNull } from '@sim/utils/coerce'
12
import { ErrorExtractorId } from '@/tools/error-extractors'
23
import type {
34
HelpScoutConversationChangeResponse,
@@ -14,14 +15,22 @@ import {
1415
} from '@/tools/helpscout/utils'
1516
import type { ToolConfig } from '@/tools/types'
1617

18+
/** An empty list removes every tag, so clearing takes an explicit `[]` and never an omitted value. */
19+
function parseTags(value: unknown): string[] {
20+
if (Array.isArray(value) ? value.length === 0 : toStringOrNull(value)?.trim() === '[]') return []
21+
const tags = toStringList(value)
22+
if (tags.length === 0) throw new Error('tags is empty; pass [] to remove every tag')
23+
return tags
24+
}
25+
1726
export const helpScoutSetConversationTagsTool: ToolConfig<
1827
HelpScoutSetConversationTagsParams,
1928
HelpScoutConversationChangeResponse
2029
> = {
2130
id: 'helpscout_set_conversation_tags',
2231
name: 'Help Scout Set Conversation Tags',
2332
description:
24-
'Replace the full tag list on a Help Scout conversation. Tags left out are removed, an empty list removes every tag, and unknown tags are created.',
33+
'Replace the full tag list on a Help Scout conversation. Tags left out are removed, [] removes every tag, and unknown tags are created.',
2534
version: '1.0.0',
2635
oauth: { required: true, provider: 'helpscout' },
2736
errorExtractor: ErrorExtractorId.HELPSCOUT_ERRORS,
@@ -30,17 +39,17 @@ export const helpScoutSetConversationTagsTool: ToolConfig<
3039
conversationId: CONVERSATION_ID_PARAM,
3140
tags: {
3241
type: 'string',
33-
required: false,
42+
required: true,
3443
visibility: 'user-or-llm',
3544
description:
36-
'Comma-separated tags the conversation should have, including any existing tags to keep. Leave empty to remove every tag.',
45+
'Comma-separated tags the conversation should have, including any existing tags to keep. Pass [] to remove every tag.',
3746
},
3847
},
3948
request: {
4049
url: (params) => conversationUrl(params.conversationId, '/tags'),
4150
method: 'PUT',
4251
headers: (params) => buildHelpScoutHeaders(params.accessToken),
43-
body: (params) => ({ tags: toStringList(params.tags) }),
52+
body: (params) => ({ tags: parseTags(params.tags) }),
4453
},
4554
transformResponse: async (_response, params) => conversationChangeResult(params),
4655
outputs: CONVERSATION_CHANGE_OUTPUTS,

‎apps/sim/tools/helpscout/types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ export interface HelpScoutMoveConversationParams extends HelpScoutConversationPa
781781
export type HelpScoutDeleteConversationParams = HelpScoutConversationParams
782782

783783
export interface HelpScoutSetConversationTagsParams extends HelpScoutConversationParams {
784-
tags?: string | string[]
784+
tags: string | string[]
785785
}
786786

787787
export interface HelpScoutConversationChangeResponse extends ToolResponse {

0 commit comments

Comments
 (0)