Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cli/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
import type { Database } from '../types/supabase.types'
import { log } from '@clack/prompts'
import { buildCliRequestHeaders } from '../analytics/cli-headers'
import { CliUserError } from '../shared/cli-user-error'
import { appAddHintMessage, formatCapgoApiErrorBody, getCapgoCliHttpStatus, hasCliPermission, invokeCapgoCliApi, isCapgoManagedSupabaseHost, resolveCapgoPublicApiHost, show2FADeniedError } from '../utils'

export async function checkAppExists(
Expand Down Expand Up @@ -217,10 +218,12 @@ export async function checkAppExistsAndHasPermissionOrgErr(
}

if (!(await hasCliPermission(supabase, apikey, requiredPermissionKey, { appId: appid, channelId: channelId ?? null }))) {
const msg = `Insufficient permissions for app ${appid}. Required RBAC permission for this action: ${requiredPermissionKey}.`
if (!silent)
log.error(msg)
throw new Error(msg)
log.error(`Insufficient permissions for app ${appid}. Required RBAC permission for this action: ${requiredPermissionKey}.`)
// Keep the app id OUT of the CliUserError message so error tracking does not
// fingerprint one issue per app; it goes in context. The permission key is a
// small bounded enum, so it stays in the message (as in currentBundle.ts).
throw new CliUserError(`Insufficient permissions for app. Required RBAC permission for this action: ${requiredPermissionKey}.`, { appId: appid })
}

return true
Expand Down
5 changes: 4 additions & 1 deletion cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2051,7 +2051,10 @@ export async function resolveUserIdFromApiKey(supabase: SupabaseClient<Database>
if (!userId) {
if (!silent)
log.error(`Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.`)
throw new Error('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.')
// Throw a CliUserError so error tracking skips this by type: a bad or
// missing API key is an expected user-configuration failure, not a crash.
// Type classification stays true even if this wording changes later.
throw new CliUserError('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.')
}
return userId
}
Expand Down
12 changes: 12 additions & 0 deletions cli/test/test-posthog-exception.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ try {
// never opens an error tracking issue.
assert.equal(shouldCapturePosthogException(new CliUserError('Login cancelled')), false)
assert.equal(shouldCapturePosthogException(new CliUserError('Upload cancelled by user')), false)
// `resolveUserIdFromApiKey` throws a bad-key failure as CliUserError, so a
// later reword of the message can no longer break the filter by substring.
assert.equal(shouldCapturePosthogException(new CliUserError('Capgo authentication failed: invalid Capgo API key or insufficient Capgo permissions.')), false)
// `checkAppExistsAndHasPermissionOrgErr` throws the RBAC failure as
// CliUserError; the app id lives in context (the permission key is a bounded
// enum and stays in the message), so every app maps to one issue per
// permission instead of one issue per app.
assert.equal(shouldCapturePosthogException(new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: app.write.', { appId: 'com.example.app' })), false)
assert.equal(
new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: channel.delete.', { appId: 'com.a' }).message,
new CliUserError('Insufficient permissions for app. Required RBAC permission for this action: channel.delete.', { appId: 'com.b' }).message,
)
// Two failures on different channels must be treated identically (one issue,
// not one per channel), since the channel name lives in context, not the message.
assert.equal(
Expand Down
Loading