diff --git a/.changeset/remove-shopify-api-key-secret-check.md b/.changeset/remove-shopify-api-key-secret-check.md new file mode 100644 index 00000000000..fec7aa19efd --- /dev/null +++ b/.changeset/remove-shopify-api-key-secret-check.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Remove app doctor detection of generic 32-character Shopify API keys. diff --git a/packages/app/src/cli/services/app-doctor-engine/rules/secret-rules.ts b/packages/app/src/cli/services/app-doctor-engine/rules/secret-rules.ts index 28882451233..8d145604b1c 100644 --- a/packages/app/src/cli/services/app-doctor-engine/rules/secret-rules.ts +++ b/packages/app/src/cli/services/app-doctor-engine/rules/secret-rules.ts @@ -25,11 +25,6 @@ interface SecretPattern { } export const SECRET_PATTERNS: SecretPattern[] = [ - // Shopify API key (32 hex chars) - { - regex: /(?:api[_-]?key|client[_-]?id|SHOPIFY_API_KEY)\s*[:=]\s*['"]([a-f0-9]{32})['"]/i, - name: 'Shopify API key', - }, // Shopify API secret (shpss_ prefix or 32 hex) { regex: /(?:api[_-]?secret|SHOPIFY_API_SECRET)\s*[:=]\s*['"](shpss_[a-f0-9]+|[a-f0-9]{32})['"]/i, @@ -132,13 +127,6 @@ export function redactText(text: string): string { const ENV_FILE_PATTERN = /(^|\/)\.env(?:\.[^/]+)?$/ const NAMED_SECRET_FILE_PATTERN = /(^|\/)(?:\.env\.(?:secrets|keys)|(?:secrets|credentials)\.json)$/ -const SHOPIFY_APP_TOML = /(^|\/)shopify\.app(?:\.[^/]+)?\.toml$/ - -/** `client_id` in shopify.app.toml is the public app client ID the CLI writes. */ -function isPublicAppTomlClientId(filePath: string, pattern: SecretPattern): boolean { - return pattern.name === 'Shopify API key' && SHOPIFY_APP_TOML.test(filePath) -} - const SECRET_ASSIGNMENT_PATTERN = /(?:api[_-]?key|api[_-]?secret|access[_-]?token|secret[_-]?key|private[_-]?key|password|SHOPIFY_API_KEY|SHOPIFY_API_SECRET)\s*[:=]\s*(?:"[^"\r\n]+"|'[^'\r\n]+'|[^\s#'"\r\n][^#\r\n]*)/i @@ -201,7 +189,6 @@ export async function scanCommittedSecrets(secretEvidenceFiles: SourceFile[], ap for (const pattern of SECRET_PATTERNS) { // Patterns are non-global so `.test()` has no lastIndex state to leak // between iterations. Do not add the /g flag here. - if (isPublicAppTomlClientId(file.path, pattern)) continue if (!pattern.regex.test(line)) continue issues.push({ id: 'COMMITTED_SECRET', diff --git a/packages/app/src/cli/services/app-doctor-engine/tests/secret-safety.test.ts b/packages/app/src/cli/services/app-doctor-engine/tests/secret-safety.test.ts index 4a8efa9a4c8..257b298c870 100644 --- a/packages/app/src/cli/services/app-doctor-engine/tests/secret-safety.test.ts +++ b/packages/app/src/cli/services/app-doctor-engine/tests/secret-safety.test.ts @@ -100,7 +100,6 @@ describe('redaction never emits the secret it detected', () => { ['GitHub token', `const k = "${PROBES.githubToken}";`], ['Google API key', `const k = "${PROBES.googleKey}";`], ['Slack token', `const k = "${PROBES.slackToken}";`], - ['Shopify API key', `const apiKey = "${HEX32}";`], ] for (const [label, line] of samples) { @@ -268,7 +267,7 @@ describe('git status drives severity, not .gitignore text', () => { }) describe('secret evidence coverage', () => { - test('does not flag the public client_id in shopify.app.toml', async () => { + test('does not flag generic 32-character hex client IDs as Shopify API keys', async () => { const dir = makeApp({ 'app.js': `const client_id = "${HEX32}";\n`, }) @@ -279,11 +278,11 @@ describe('secret evidence coverage', () => { writeFileSync(join(dir, 'shopify.app.staging.toml'), `name = "s"\nclient_id = "${HEX32}"\n`) const result = await scan(dir) const secrets = result.issues.filter((issue) => issue.id === 'COMMITTED_SECRET') - expect( - secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('Shopify API key')), - ).toBe(false) + expect(secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('API key'))).toBe( + false, + ) expect(secrets.some((issue) => issue.location.file === 'shopify.app.staging.toml')).toBe(false) - expect(secrets.some((issue) => issue.location.file === 'app.js')).toBe(true) + expect(secrets.some((issue) => issue.location.file === 'app.js')).toBe(false) expect( secrets.some((issue) => issue.location.file === 'shopify.app.toml' && issue.title.includes('Shopify API secret')), ).toBe(true) @@ -344,8 +343,6 @@ describe('incomplete coverage is reported, not hidden', () => { /** Probe strings with realistic shape, assembled at runtime. See note above. */ function probeFor(name: string): string | undefined { switch (name) { - case 'Shopify API key': - return `api_key = "${HEX32}"` case 'Shopify API secret': return `api_secret = "${PROBES.shopifySecret}"` case 'Shopify access token':