Skip to content

Commit afc697c

Browse files
committed
fix(credentials): classify auth-shaped Shopify GraphQL errors as invalid credentials
Shopify can reject invalid or revoked shpat_ tokens with HTTP 200 and a GraphQL error body instead of a 401; those now map to invalid_credentials instead of a provider-outage message
1 parent dda7dad commit afc697c

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,21 @@ describe('validateShopifyServiceAccount', () => {
139139
status: 502,
140140
})
141141
})
142+
143+
it('maps an auth-shaped GraphQL error in a 200 response to invalid_credentials', async () => {
144+
mockFetch.mockResolvedValueOnce(
145+
jsonResponse(200, {
146+
errors: [
147+
{ message: 'Invalid API key or access token (unrecognized login or wrong password)' },
148+
],
149+
})
150+
)
151+
await expect(
152+
validateShopifyServiceAccount({ apiToken: 'shpat_bad', domain: 'my-store.myshopify.com' })
153+
).rejects.toMatchObject({
154+
name: 'TokenServiceAccountValidationError',
155+
code: 'invalid_credentials',
156+
status: 401,
157+
})
158+
})
142159
})

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,32 @@ const SHOPIFY_HOST_REGEX = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/
2727

2828
const SHOP_QUERY = '{ shop { name myshopifyDomain } }'
2929

30+
interface ShopifyGraphqlError {
31+
message?: string
32+
extensions?: { code?: string }
33+
}
34+
3035
interface ShopifyShopResponse {
3136
data?: {
3237
shop?: {
3338
name?: string
3439
myshopifyDomain?: string
3540
}
3641
}
37-
errors?: unknown
42+
errors?: ShopifyGraphqlError[] | unknown
43+
}
44+
45+
/**
46+
* Shopify can reject invalid or revoked `shpat_` tokens with HTTP 200 and a
47+
* GraphQL error body instead of a 401, so auth-shaped GraphQL errors must map
48+
* to `invalid_credentials` rather than a provider outage.
49+
*/
50+
function hasShopifyAuthError(errors: unknown): boolean {
51+
if (!Array.isArray(errors)) return false
52+
return errors.some((error: ShopifyGraphqlError) => {
53+
const haystack = `${error?.message ?? ''} ${error?.extensions?.code ?? ''}`
54+
return /access.?denied|unauthorized|invalid api key or access token|401/i.test(haystack)
55+
})
3856
}
3957

4058
/** Strips the protocol and trailing slashes and lowercases the store domain. */
@@ -89,6 +107,13 @@ export async function validateShopifyServiceAccount(
89107
const payload = await parseProviderJson<ShopifyShopResponse>(res, 'shop_query')
90108

91109
const shop = payload.data?.shop
110+
if (hasShopifyAuthError(payload.errors)) {
111+
throw new TokenServiceAccountValidationError('invalid_credentials', 401, {
112+
step: 'shop_query',
113+
domain,
114+
reason: 'auth-shaped GraphQL error in 200 response',
115+
})
116+
}
92117
if (payload.errors || !shop) {
93118
throw new TokenServiceAccountValidationError('provider_unavailable', 502, {
94119
step: 'shop_query',

0 commit comments

Comments
 (0)