Skip to content

Commit c94b4e7

Browse files
committed
fix(credentials): close batch-2 audit findings
Restricted keys that name their missing permission (Ashby, Resend, ElevenLabs) are accepted through a provesKey check; Typeform rejects every 403; WhatsApp keeps the phone number ID in its own path segment; Semrush is held back because its free balance check accepts unknown keys.
1 parent 6940383 commit c94b4e7

67 files changed

Lines changed: 130 additions & 232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/components/ui/icon-mapping.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
694694
secrets_manager: SecretsManagerIcon,
695695
secrets_manager_v2: SecretsManagerIcon,
696696
semrush: SemrushIcon,
697-
semrush_v2: SemrushIcon,
698697
sendblue: SendblueIcon,
699698
sendgrid: SendgridIcon,
700699
sendgrid_v2: SendgridIcon,

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

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

‎apps/sim/blocks/blocks/grain.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export const GrainBlock: BlockConfig = {
2626
description: 'Access meeting recordings, transcripts, and AI summaries',
2727
authMode: AuthMode.ApiKey,
2828
triggerAllowed: true,
29-
// Superseded by grain_v2 (Grain API v1 sunsets 2026-09-07); existing blocks
30-
// keep rendering, new blocks come from the v2 entry.
29+
// Superseded (Grain API v1 sunsets 2026-09-07); existing blocks keep rendering,
30+
// new blocks come from the current version.
3131
hideFromToolbar: true,
3232
sunset: { status: 'legacy', replacedBy: 'grain_v3' },
3333
longDescription:

‎apps/sim/blocks/blocks/semrush.ts‎

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SemrushIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
4-
import { withSavedCredential } from '@/blocks/utils'
54

65
const DATABASE_OPTIONS = [
76
{ label: 'United States (us)', id: 'us' },
@@ -349,8 +348,6 @@ Return ONLY the 8-character value - no explanations, no quotes, no extra text.`,
349348

350349
export const SemrushBlock: BlockConfig = {
351350
type: 'semrush',
352-
hideFromToolbar: true,
353-
sunset: { status: 'legacy', replacedBy: 'semrush_v2' },
354351
name: 'Semrush',
355352
description: 'Research SEO and paid search data with Semrush',
356353
authMode: AuthMode.ApiKey,
@@ -1051,17 +1048,3 @@ export const SemrushBlockMeta = {
10511048
},
10521049
],
10531050
} as const satisfies BlockMeta
1054-
1055-
export const SemrushV2Block: BlockConfig = {
1056-
...SemrushBlock,
1057-
type: 'semrush_v2',
1058-
authMode: AuthMode.OAuth,
1059-
hideFromToolbar: false,
1060-
sunset: undefined,
1061-
...withSavedCredential(SemrushBlock, {
1062-
id: 'credential',
1063-
type: 'oauth-input',
1064-
serviceId: 'semrush',
1065-
title: 'Semrush Credential',
1066-
}),
1067-
}

‎apps/sim/blocks/registry-maps.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ import {
362362
SecretsManagerBlockMeta,
363363
SecretsManagerV2Block,
364364
} from '@/blocks/blocks/secrets_manager'
365-
import { SemrushBlock, SemrushBlockMeta, SemrushV2Block } from '@/blocks/blocks/semrush'
365+
import { SemrushBlock, SemrushBlockMeta } from '@/blocks/blocks/semrush'
366366
import { SendblueBlock, SendblueBlockMeta } from '@/blocks/blocks/sendblue'
367367
import { SendGridBlock, SendGridBlockMeta, SendGridV2Block } from '@/blocks/blocks/sendgrid'
368368
import { SentryBlock, SentryBlockMeta, SentryV2Block } from '@/blocks/blocks/sentry'
@@ -820,7 +820,6 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
820820
secrets_manager: SecretsManagerBlock,
821821
secrets_manager_v2: SecretsManagerV2Block,
822822
semrush: SemrushBlock,
823-
semrush_v2: SemrushV2Block,
824823
sendblue: SendblueBlock,
825824
sendgrid: SendGridBlock,
826825
sendgrid_v2: SendGridV2Block,

‎apps/sim/lib/block-metadata/names.generated.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ export const BLOCK_NAMES: Readonly<Record<string, string>> = {
252252
schedule: 'Schedule',
253253
search: 'Search',
254254
secrets_manager_v2: 'AWS Secrets Manager',
255-
semrush_v2: 'Semrush',
255+
semrush: 'Semrush',
256256
sendblue: 'Sendblue',
257257
sendgrid_v2: 'SendGrid',
258258
sentry_v2: 'Sentry',

‎apps/sim/lib/credentials/token-service-accounts/descriptors.ts‎

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ export const TYPEFORM_SERVICE_ACCOUNT_PROVIDER_ID = 'typeform-service-account' a
9191
export const TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID = 'telegram-service-account' as const
9292
export const STRIPE_SERVICE_ACCOUNT_PROVIDER_ID = 'stripe-service-account' as const
9393
export const SENTRY_SERVICE_ACCOUNT_PROVIDER_ID = 'sentry-service-account' as const
94-
export const SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID = 'semrush-service-account' as const
9594
export const ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID = 'rootly-service-account' as const
9695
export const RESEND_SERVICE_ACCOUNT_PROVIDER_ID = 'resend-service-account' as const
9796
export const PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID = 'pagerduty-service-account' as const
@@ -198,7 +197,6 @@ export type TokenServiceAccountProviderId =
198197
| typeof TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID
199198
| typeof STRIPE_SERVICE_ACCOUNT_PROVIDER_ID
200199
| typeof SENTRY_SERVICE_ACCOUNT_PROVIDER_ID
201-
| typeof SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID
202200
| typeof ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID
203201
| typeof RESEND_SERVICE_ACCOUNT_PROVIDER_ID
204202
| typeof PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID
@@ -1520,7 +1518,7 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record<
15201518
],
15211519
docsUrl: 'https://docs.sim.ai/integrations/ashby',
15221520
helpText:
1523-
'Create an API key in Ashby under Settings > API Keys and grant read and write access to the modules your workflows use.',
1521+
'Create an API key in Ashby under Admin > API Keys and grant read and write access to the modules your workflows use.',
15241522
invalidCredentialsHelp:
15251523
'Ashby rejected this key. Check that it was copied in full and has not been deactivated.',
15261524
},
@@ -1666,20 +1664,6 @@ export const TOKEN_SERVICE_ACCOUNT_DESCRIPTORS: Record<
16661664
invalidCredentialsHelp:
16671665
'Rootly rejected this key. Check that it was copied in full and has not been revoked.',
16681666
},
1669-
[SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID]: {
1670-
providerId: SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID,
1671-
serviceLabel: 'Semrush',
1672-
tokenNoun: 'API key',
1673-
connectNoun: 'API key',
1674-
fields: [
1675-
{ id: 'apiToken', label: 'API key', placeholder: 'Paste your Semrush API key', secret: true },
1676-
],
1677-
docsUrl: 'https://docs.sim.ai/integrations/semrush',
1678-
helpText:
1679-
'API access needs a Semrush subscription with API units. Checking the key uses no units.',
1680-
invalidCredentialsHelp:
1681-
'Semrush rejected this key. Check that it was copied in full and belongs to an account with API access.',
1682-
},
16831667
[SENTRY_SERVICE_ACCOUNT_PROVIDER_ID]: {
16841668
providerId: SENTRY_SERVICE_ACCOUNT_PROVIDER_ID,
16851669
serviceLabel: 'Sentry',

‎apps/sim/lib/credentials/token-service-accounts/server.ts‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import {
7373
RIPPLING_SERVICE_ACCOUNT_PROVIDER_ID,
7474
ROCKETLANE_SERVICE_ACCOUNT_PROVIDER_ID,
7575
ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID,
76-
SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID,
7776
SENDGRID_SERVICE_ACCOUNT_PROVIDER_ID,
7877
SENTRY_SERVICE_ACCOUNT_PROVIDER_ID,
7978
SHOPIFY_SERVICE_ACCOUNT_PROVIDER_ID,
@@ -176,7 +175,6 @@ import {
176175
validateRipplingServiceAccount,
177176
validateRocketlaneServiceAccount,
178177
validateRootlyServiceAccount,
179-
validateSemrushServiceAccount,
180178
validateSendGridServiceAccount,
181179
validateSentryServiceAccount,
182180
validateSimilarwebServiceAccount,
@@ -332,7 +330,6 @@ const TOKEN_SERVICE_ACCOUNT_VALIDATORS: Record<
332330
[TELEGRAM_SERVICE_ACCOUNT_PROVIDER_ID]: validateTelegramServiceAccount,
333331
[STRIPE_SERVICE_ACCOUNT_PROVIDER_ID]: validateStripeServiceAccount,
334332
[SENTRY_SERVICE_ACCOUNT_PROVIDER_ID]: validateSentryServiceAccount,
335-
[SEMRUSH_SERVICE_ACCOUNT_PROVIDER_ID]: validateSemrushServiceAccount,
336333
[ROOTLY_SERVICE_ACCOUNT_PROVIDER_ID]: validateRootlyServiceAccount,
337334
[RESEND_SERVICE_ACCOUNT_PROVIDER_ID]: validateResendServiceAccount,
338335
[PAGERDUTY_SERVICE_ACCOUNT_PROVIDER_ID]: validatePagerDutyServiceAccount,

‎apps/sim/lib/credentials/token-service-accounts/validators/http.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ interface TokenCheck {
4646
* before {@link forbiddenMeansScoped} accepts a 403.
4747
*/
4848
rejectsKey?: (errorBody: unknown) => boolean
49+
/**
50+
* For a vendor that answers a valid but restricted key with an error naming
51+
* the missing permission (Resend's `restricted_api_key` 401): returns whether
52+
* the error body proves the key authenticated. Checked after
53+
* {@link rejectsKey}.
54+
*/
55+
provesKey?: (errorBody: unknown) => boolean
4956
}
5057

5158
const MAX_CHECK_RESPONSE_BYTES = 64 * 1024
@@ -82,12 +89,13 @@ export function tokenCheckValidator(check: TokenCheck): TokenServiceAccountValid
8289
check.forbiddenMeansScoped &&
8390
response.status === 403 &&
8491
(response.headers.get('content-type') ?? '').includes('application/json')
85-
if (check.rejectsKey && !response.ok) {
92+
if ((check.rejectsKey || check.provesKey) && !response.ok) {
8693
const unreadable = Symbol('unreadable')
8794
const errorBody = await readJsonBody(response, check.serviceLabel).catch(() => unreadable)
88-
if (errorBody !== unreadable && check.rejectsKey(errorBody)) {
95+
if (errorBody !== unreadable && check.rejectsKey?.(errorBody)) {
8996
throw new TokenServiceAccountValidationError('invalid_credentials', 400, { step: 'verify' })
9097
}
98+
if (errorBody !== unreadable && check.provesKey?.(errorBody)) return accepted
9199
if (scopedForbidden) {
92100
// Only a body that was read and doesn't name a bad key proves the token.
93101
if (errorBody === unreadable) {

‎apps/sim/lib/credentials/token-service-accounts/validators/token-checks.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
33
import {
44
validateAshbyServiceAccount,
55
validateBrexServiceAccount,
6+
validateElevenLabsServiceAccount,
67
validateFlintServiceAccount,
78
validateInstantlyServiceAccount,
89
validateResendServiceAccount,
@@ -238,3 +239,38 @@ describe('Vercel saved token verification', () => {
238239
})
239240
})
240241
})
242+
243+
describe('ElevenLabs saved API key verification', () => {
244+
it('accepts a valid key without the user permission', async () => {
245+
vi.stubGlobal(
246+
'fetch',
247+
vi
248+
.fn()
249+
.mockResolvedValue(
250+
jsonResponse({ detail: { status: 'missing_permissions', message: 'user_read' } }, 401)
251+
)
252+
)
253+
254+
await expect(
255+
validateElevenLabsServiceAccount({ apiToken: 'sk_scoped' })
256+
).resolves.toMatchObject({ displayName: 'ElevenLabs' })
257+
})
258+
259+
it('rejects an unknown key', async () => {
260+
vi.stubGlobal(
261+
'fetch',
262+
vi
263+
.fn()
264+
.mockResolvedValue(
265+
jsonResponse(
266+
{ detail: { status: 'invalid_api_key', message: 'API key is invalid.' } },
267+
400
268+
)
269+
)
270+
)
271+
272+
await expect(validateElevenLabsServiceAccount({ apiToken: 'sk_bad' })).rejects.toMatchObject({
273+
code: 'invalid_credentials',
274+
})
275+
})
276+
})

0 commit comments

Comments
 (0)