Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"test:ios-marketing-version": "bun test/test-ios-marketing-version.mjs",
"test:platform-flow-contract": "bun test/test-platform-flow-contract.mjs",
"test:tail-engine-shared": "bun test/test-tail-engine-shared.mjs",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:credentials && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:build-platform-selection && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:init-guardrails && bun run test:init-replay && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:organization-set-api-host && bun run test:trial-warning",
"test": "bun run build && bun run test:helper-dce && bun run test:version-detection:setup && bun run test:bundle && bun run test:functional && bun run test:semver && bun run test:version-edge-cases && bun run test:regex && bun run test:upload && bun run test:fail-on-incompatible && bun run test:native-dependencies && bun run test:credentials && bun run test:credentials-validation && bun run test:android-service-account-validation && bun run test:build-zip-filter && bun run test:checksum && bun run test:build-needed && bun run test:ci-prompts && bun run test:ci-secrets && bun run test:android-onboarding-progress && bun run test:onboarding-telemetry && bun run test:v2-event-migration && bun run test:analytics && bun run test:analytics-error-category && bun run test:analytics-org-resolver && bun run test:supabase-perf && bun run test:preview-qr && bun run test:app-set-options && bun run test:mcp-analytics && bun run test:mcp-instructions && bun run test:mcp-live-update-onboarding && bun run test:mcp-stdout-guard && bun run test:mcp-platform-select && bun run test:mcp-explain-scopes && bun run test:mcp-oauth-reopen && bun run test:mcp-broker-oauth && bun run test:mcp-broker-session && bun run test:mcp-credentials-manage && bun run test:mcp-resume-prompt && bun run test:mcp-build-job && bun run test:mcp-build-tools && bun run test:app-created-source && bun run test:doctor-analytics && bun run test:posthog-exception && bun run test:build-platform-selection && bun run test:onboarding-recovery && bun run test:onboarding-progress && bun run test:onboarding-run-targets && bun run test:run-device-command && bun run test:init-monorepo-targeting && bun run test:init-app-conflict && bun run test:init-guardrails && bun run test:init-replay && bun run test:prompt-preferences && bun run test:esm-sdk && bun run test:mcp && bun run test:mcp-no-key-handshake && bun run test:auth-session && bun run test:version-detection && bun run test:platform-paths && bun run test:project-type-detection && bun run test:payload-split && bun run test:manifest-path-encoding && bun run test:macos-signing && bun run test:asc-key-protocol && bun run test:apple-api-import-helpers && bun run test:apple-api-verify-key && bun run test:bundle-id-detector && bun run test:apple-api-app-list && bun run test:app-verification && bun run test:pbxproj-parser && bun run test:ai-log-capture && bun run test:ai-analyze-flow && bun run test:cicd-failure-help && bun run test:ai-sse-parser && bun run test:ai-render-markdown && bun run test:ai-stream-markdown && bun run test:ai-onboarding-mode && bun run test:ai-fit && bun run test:platform-layout && bun run test:frame-fit && bun run test:onboarding-min-size && bun run test:min-size-gate && bun run test:shell-size-gate && bun run test:build-log-sanitize && bun run test:build-output-viewport && bun run test:diff-viewer-viewport && bun run test:build-complete-exit && bun run test:ai-analyze-stream && bun run test:support-mailto && bun run test:support-redact && bun run test:support-internal-log && bun run test:support-help-menu && bun run test:support-contact && bun run test:support-upload-prompt && bun run test:support-bundle-files && bun run test:self-update && bun run test:update-prompt && bun run test:apple-api-cert-create && bun run test:android-tail-engine && bun run test:android-tail-render && bun run test:android-tail-routing && bun run test:dev-gate-stripped && bun run test:frame-fit-ios-shared && bun run test:ios-confirm-app-id && bun run test:ios-create-new && bun run test:ios-e2e && bun run test:ios-flow-contract && bun run test:ios-import-discovery && bun run test:ios-import-export && bun run test:ios-import-pickers && bun run test:ios-import-recovery && bun run test:ios-recovery && bun run test:ios-resume && bun run test:ios-tail-handoff && bun run test:ios-tui-render && bun run test:p8-error && bun run test:ios-tui-routing && bun run test:ios-updater-sync-validation && bun run test:ios-verify-app && bun run test:ios-marketing-version && bun run test:platform-flow-contract && bun run test:tail-engine-shared && bun run test:prescan && bun run test:android-reporting-api && bun run test:android-app-verification && bun run test:android-rename && bun run test:appflow-auth && bun run test:appflow-api-map && bun run test:appflow-validate && bun run test:appflow-flow && bun run test:appflow-gapfill && bun run test:appflow-engine && bun run test:appflow-tail && bun run test:appflow-fetch && bun run test:appflow-sa-decode && bun run test:app-permission-helper && bun run test:organization-set-api-host && bun run test:trial-warning && bun run test:plan-validation",
"test:build-platform-selection": "bun test/test-build-platform-selection.mjs",
"test:ai-log-capture": "bun test/test-ai-log-capture.mjs",
"test:ai-analyze-flow": "bun test/test-ai-analyze-flow.mjs",
Expand Down Expand Up @@ -203,7 +203,8 @@
"test:appflow-sa-decode": "bun test/test-appflow-sa-decode.mjs",
"test:app-permission-helper": "bun test/test-app-permission-helper.mjs",
"test:organization-set-api-host": "bun test/test-organization-set-api-host.mjs",
"test:trial-warning": "bun test/test-trial-warning.mjs"
"test:trial-warning": "bun test/test-trial-warning.mjs",
"test:plan-validation": "bun test/test-plan-validation.mjs"
},
"dependencies": {
"@inkjs/ui": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/bundle/unlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export async function unlinkDeviceInternal(
true,
)

await checkPlanValid(supabase, orgId, enrichedOptions.apikey, resolvedAppId)
await checkPlanValid(supabase, orgId, resolvedAppId)

const versionData = await getVersionData(supabase, resolvedAppId, bundle, { silent })
await checkVersionNotUsedInChannel(supabase, resolvedAppId, versionData, {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/bundle/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl
if (options.verbose)
log.info(`[Verbose] Remote CLI messages checked`)

await checkPlanValidUpload(supabase, orgId, apikey, appid, true)
await checkPlanValidUpload(supabase, orgId, appid, true)
if (options.verbose)
log.info(`[Verbose] Plan validation passed`)
if (options.verbose)
Expand Down
2 changes: 1 addition & 1 deletion cli/src/channel/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export async function setChannelInternal(channel: string, appId: string, options
throw new Error(`Cannot get organization id for channel ${channel}`)
}

await checkPlanValid(supabase, orgId, options.apikey, appId)
await checkPlanValid(supabase, orgId, appId)
Comment thread
WcaleNieWolny marked this conversation as resolved.

const channelPayload: Database['public']['Tables']['channels']['Insert'] = {
created_by: userId,
Expand Down
24 changes: 15 additions & 9 deletions cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,12 +843,19 @@ export async function isAllowedActionOrg(supabase: SupabaseClient<Database>, org
return !!data
}

export async function isAllowedActionAppIdApiKey(supabase: SupabaseClient<Database>, appId: string, apikey: string): Promise<boolean> {
const { data } = await supabase
.rpc('is_allowed_action', { apikey, appid: appId })
.single()
export async function isAllowedActionForApp(supabase: SupabaseClient<Database>, orgId: string, appId: string): Promise<boolean> {
// Check every metered action to preserve the general "good plan" gate while
// giving app-scoped API keys the app context required by RBAC.
const args = {
orgid: orgId,
appid: appId,
actions: ['mau', 'storage', 'bandwidth', 'build_time'],
} as never
const { data, error } = await supabase.rpc('is_allowed_action_org_action', args)
Comment thread
WcaleNieWolny marked this conversation as resolved.
Outdated
if (error)
Comment thread
WcaleNieWolny marked this conversation as resolved.
Outdated
throw new Error(`Cannot validate plan: ${formatError(error)}`)

return !!data
return data === true
}

export async function checkRemoteCliMessages(supabase: SupabaseClient<Database>, orgId: string, cliVersion: string) {
Expand Down Expand Up @@ -882,11 +889,10 @@ export async function checkRemoteCliMessages(supabase: SupabaseClient<Database>,
}
}

export async function checkPlanValid(supabase: SupabaseClient<Database>, orgId: string, apikey: string, appId?: string, warning = true) {
export async function checkPlanValid(supabase: SupabaseClient<Database>, orgId: string, appId?: string, warning = true) {
const config = await getRemoteConfig()

// isAllowedActionAppIdApiKey was updated in the orgs_v3 migration to work with the new system
const validPlan = await (appId ? isAllowedActionAppIdApiKey(supabase, appId, apikey) : isAllowedActionOrg(supabase, orgId))
const validPlan = await (appId ? isAllowedActionForApp(supabase, orgId, appId) : isAllowedActionOrg(supabase, orgId))
if (!validPlan) {
log.error(`You need to upgrade your plan to continue to use capgo.\n Upgrade here: ${config.hostWeb}/settings/organization/plans\n`)
wait(100)
Expand All @@ -906,7 +912,7 @@ export async function checkPlanValid(supabase: SupabaseClient<Database>, orgId:
log.warn(`WARNING !!\nTrial expires in ${trialDays} days, upgrade here: ${config.hostWeb}/settings/organization/plans\n`)
}

export async function checkPlanValidUpload(supabase: SupabaseClient<Database>, orgId: string, apikey: string, appId?: string, warning = true) {
export async function checkPlanValidUpload(supabase: SupabaseClient<Database>, orgId: string, appId?: string, warning = true) {
const config = await getRemoteConfig()

// Pass appid so RBAC evaluates the app scope. Without it,
Expand Down
87 changes: 87 additions & 0 deletions cli/test/test-plan-validation.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env node

import process from 'node:process'
import * as utils from '../src/utils.ts'

console.log('🧪 Testing app-aware plan validation...\n')

let testsPassed = 0
let testsFailed = 0

async function test(name, fn) {
try {
console.log(`\n🔍 ${name}`)
await fn()
console.log(`✅ PASSED: ${name}`)
testsPassed++
}
catch (error) {
console.error(`❌ FAILED: ${name}`)
console.error(` Error: ${error.message}`)
testsFailed++
}
}

function assert(condition, message) {
if (!condition)
throw new Error(message)
}

function assertEquals(actual, expected, message) {
if (JSON.stringify(actual) !== JSON.stringify(expected))
throw new Error(message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}

await test('checks an app-scoped key through the app-aware plan RPC', async () => {
assert(typeof utils.isAllowedActionForApp === 'function', 'Expected isAllowedActionForApp to be exported')

const calls = []
const supabase = {
rpc: async (name, args) => {
calls.push({ name, args })
return { data: true, error: null }
},
}

const allowed = await utils.isAllowedActionForApp(
supabase,
'org-id',
'com.example.app',
)

assertEquals(allowed, true)
assertEquals(calls, [{
name: 'is_allowed_action_org_action',
args: {
orgid: 'org-id',
appid: 'com.example.app',
actions: ['mau', 'storage', 'bandwidth', 'build_time'],
},
}])
})

await test('surfaces plan RPC errors instead of reporting an invalid plan', async () => {
assert(typeof utils.isAllowedActionForApp === 'function', 'Expected isAllowedActionForApp to be exported')

const supabase = {
rpc: async () => ({
data: null,
error: { message: 'permission lookup failed' },
}),
}

let thrown
try {
await utils.isAllowedActionForApp(supabase, 'org-id', 'com.example.app')
}
catch (error) {
thrown = error
}

assert(thrown instanceof Error, 'Expected the RPC error to be thrown')
assert(thrown.message.includes('Cannot validate plan'), `Unexpected error: ${thrown.message}`)
})

console.log(`\n📊 Results: ${testsPassed} passed, ${testsFailed} failed`)
if (testsFailed > 0)
process.exit(1)
Loading