Skip to content

refactor(firebase): replace REST API clients with googleapis library#22

Open
pitzcarraldo wants to merge 4 commits intomainfrom
feat/refactor-firebase-apis
Open

refactor(firebase): replace REST API clients with googleapis library#22
pitzcarraldo wants to merge 4 commits intomainfrom
feat/refactor-firebase-apis

Conversation

@pitzcarraldo
Copy link
Contributor

@pitzcarraldo pitzcarraldo commented Feb 6, 2026

Summary

Replace direct Firebase Management and IAM REST API implementations with @googleapis/firebase and @googleapis/iam packages, reducing code by 57% while maintaining full backward compatibility.

Details

This refactoring leverages the official googleapis library for Firebase and IAM APIs instead of maintaining custom REST clients. Benefits include:

  • Code reduction: 57% fewer lines (1,042 → 448 lines in API clients)
  • Type safety: Auto-generated types from googleapis instead of manual definitions
  • Maintenance: Reduces custom token handling and pagination logic
  • Compatibility: Existing public APIs remain unchanged; downstreams unaffected
  • OAuth integration: Cleaner token refresh via OAuth2Client

Key metrics:

  • firebase-api.ts: 366 → 223 lines (-39%)
  • iam-api.ts: 219 → 126 lines (-42%)
  • types.ts: 428 → 80 lines (-81%, removed Response/Operation types)

How to Validate

  1. Run tests: bun test (all 588 unit tests pass)
  2. Run E2E: bun run build && bun test tests/e2e/ (all 20 E2E tests pass)
  3. Type check: bun run typecheck (no errors)
  4. Lint: bun run lint (no warnings)

Try Firebase operations in CLI:

  • DEBUG=1 bun run dev/firebase command
  • Authenticate with Google
  • List projects
  • Create app
  • Download config files

Related Issues

Related to #21 (project-local storage migration)

Summary by CodeRabbit

  • New Features

    • Added installation preparation UI to check Firebase and iOS setup status before installation.
    • Introduced service account configuration support for Firebase.
    • Enhanced Firebase setup with GCP project linking and selection.
  • Bug Fixes

    • Improved OAuth error logging and token refresh handling.
  • Chores

    • Removed Firebase and iOS setup as standalone CLI commands; functionality integrated into installation workflow.
    • Added Google Cloud and Firebase API dependencies.

- Add IAM API client for Service Account operations (list, create, download key)
- Add GCP project API for listing available projects and adding Firebase
- Extend OAuth scopes to include iam and cloud-platform permissions
- Create Zod-based Service Account JSON validator with real-time feedback
- Extend FirebaseDownloader with SA management methods
- Add new wizard phases: no_projects, service_account_menu, paste_json
- Support JSON paste with real-time validation and error display
- Save service account keys to .clix/service-account.json
Replace direct Firebase Management and IAM REST API implementations with
@googleapis/firebase and @googleapis/iam packages. This reduces code by 57%
while maintaining full backward compatibility through existing public APIs.

Key improvements:
- Reduced firebase-api.ts from 366 to 223 lines (-39%)
- Reduced iam-api.ts from 219 to 126 lines (-42%)
- Removed duplicate pagination and token handling logic
- Type safety improved with generated types from googleapis
- Automatic token refresh via OAuth2Client
- Cleaner error handling with googleapis's built-in support

All 588 unit tests and 20 E2E tests pass.
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Walkthrough

This PR removes Firebase and iOS setup commands, migrates the Firebase API to use googleapis libraries, and introduces a comprehensive installation preparation workflow system. It adds setup status tracking via project config, restructures Firebase credential handling, and enhances the Firebase wizard with new phases and service account management.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added @googleapis/cloudresourcemanager and @googleapis/firebase dependencies.
Firebase & iOS Command Removal
src/cli.tsx, src/commands/firebase.tsx, src/commands/ios-setup/index.tsx, src/lib/commands/firebase.tsx, src/lib/commands/ios-setup.tsx, src/lib/commands/registry.ts, src/lib/commands/__tests__/registry.test.ts
Removed firebase and ios-setup command implementations from CLI, command registry, and related tests.
Installation Preparation Flow
src/commands/skill/preparation.ts, src/commands/skill/index.tsx, src/commands/skill/__tests__/preparation.test.ts, src/ui/components/InstallPreparationUI.tsx
Introduced new preparation workflow with PreparationContext, Firebase and iOS status checks, and UI components for guiding initial setup.
Project Configuration & Setup Status
src/lib/config/project-config-schema.ts, src/lib/config/project-config-manager.ts, src/lib/config/index.ts
Extended ProjectConfig schema with setup status tracking for Firebase, iOS, and APNS; added updateSetup method to manager; implemented version migrations.
Firebase API Modernization
src/lib/services/firebase/api/firebase-api.ts, src/lib/services/firebase/api/types.ts, src/lib/services/firebase/api/index.ts
Rewrote Firebase API client to use @googleapis libraries with OAuth2 auth; restructured type definitions (removed response types, added GcpProject and ServiceAccountJson); implemented GCP project operations and service account support.
Firebase Service Enhancement
src/lib/services/firebase/downloader.ts, src/lib/services/firebase/index.ts, src/lib/services/firebase/service-account-validator.ts, src/lib/services/firebase/types.ts
Added async API client initialization with credential caching, GCP project management APIs, comprehensive service account validation with Zod schemas, and new credential action type.
Firebase OAuth & Authentication
src/lib/services/firebase/oauth/auth-client.ts, src/lib/services/firebase/oauth/config.ts, src/lib/utils/oauth.ts
Enhanced OAuth error logging via dedicated oauthLogger, improved token exchange/refresh error handling, added structured logging to project-scoped debug log file.
FirebaseWizard Refactor
src/ui/components/FirebaseWizard.tsx, src/ui/components/firebase-wizard-transitions.ts, src/ui/components/__tests__/firebase-wizard-transitions.test.ts
Introduced centralized phase transition system with ExtendedWizardPhase types; added GCP project linking, service account setup flows, and sender config verification; expanded wizard to handle no-projects and Firebase-to-GCP scenarios.
Organizations & Projects Service
src/lib/services/organization-projects.ts, src/lib/services/__tests__/organization-projects.test.ts
New service for concurrent fetching of organizations with projects; includes concurrency controls, retry logic, and timing instrumentation.
Internal API Extensions
src/lib/api/internal-client.ts, src/lib/api/types.ts, src/lib/api/index.ts
Extended API client with retry logic, timeout handling, and new methods (getProject, createOrUpdateSenderConfig); added AppPushSenderConfig and SenderConfig types to support push configuration.
Auth Credentials & Debugging
src/lib/auth/credentials.ts, src/lib/debug/logger.ts
Refactored Firebase token clearing to preserve credentials file; added file-based debug logging with oauthLogger instance.
Skills Integration
src/lib/skills.ts, src/lib/__tests__/skills.test.ts
Added PreparationContext support to SkillOptions; integrated pre-configured setup sections into install skill prompts; added comprehensive tests for context-aware prompt generation.
Chat UI & Hooks
src/ui/chat/ChatApp.tsx, src/ui/chat/hooks/useCommandHandler.ts, src/ui/chat/hooks/useMessageSending.ts, src/ui/chat/hooks/useOverlays.ts
Integrated InstallPreparationUI overlay system; added install-preparation flow to command handler and overlay management; passed clixProjectId to FirebaseWizard; extended skill execution with preparationContext.
Project Selector
src/ui/components/ProjectSelector.tsx
Exported interfaces and component; added search/filter capabilities with locale-aware sorting via Intl.Collator; implemented filtered project navigation and empty-state handling.
Documentation
AGENTS.md, plan.md
Added FirebaseWizard state machine documentation and high-level planning notes for API/UI changes.

Possibly related PRs

Suggested reviewers

  • nyanxyz
  • JeongwooYoo
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: replacing REST API clients with the googleapis library, which is the primary focus of this refactoring PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, detailed benefits, validation steps, and related issues. However, it is missing the pre-merge checklist section required by the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/refactor-firebase-apis

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c184418fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 32 to 36
compactCommand,
agentCommand,
debugCommand,
firebaseCommand,
iosSetupCommand,
transferCommand,
resumeCommand,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve slash registrations for firebase setup flows

/firebase and /ios-setup are no longer in BUILT_IN_COMMANDS, but command execution still gates on getCommand(...) before the switch in useCommandHandler, so both commands now resolve as unknown and their existing handlers become unreachable. This breaks the interactive setup paths that are still referenced elsewhere (including embedded skill guidance), so users can no longer invoke these setup flows from chat.

Useful? React with 👍 / 👎.

const updatedContext: PreparationContext = {
...context,
firebase: updatedFirebase,
missing: context.missing.filter((m) => !m.includes('Firebase')),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep Firebase items in missing list when setup is skipped

In handleFirebaseComplete, Firebase-related missing items are always removed even when result.completed is false, so skipping or failing Firebase setup can still produce a context that no longer reports missing Firebase prerequisites. Because this context is forwarded to install skill prompting, the agent can proceed without being told required Firebase config is absent, leading to incomplete or broken installation runs.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 52-54: Update the invalid dependency versions in package.json:
change "@googleapis/cloudresourcemanager" from ^6.0.1 to ^3.0.1,
"@googleapis/firebase" from ^12.0.1 to ^10.0.1, and "@googleapis/iam" from
^36.0.0 to ^30.1.0 so they match published npm releases; leave
"google-auth-library" at ^10.5.0 as-is, then run npm install (or yarn) and a
quick build/test to verify no further dependency conflicts.

In `@src/commands/skill/preparation.ts`:
- Around line 159-169: The readiness check incorrectly treats projectType.target
=== 'unknown' as not needing Firebase/iOS setup; update the logic in the
preparation routine so that when projectType.target === 'unknown' it is treated
as needing setup (i.e., needed = true or explicitly block readiness) instead of
returning configured true. Modify the branch around the needed variable and the
blocks that set configured, androidConfigured, and iosConfigured (and the
similar checks referenced later) so unknown target forces configured:false (or
prevents ready:true) and flows through normal setup checks for Firebase and iOS.

In `@src/lib/services/firebase/downloader.ts`:
- Around line 417-427: The saveServiceAccountJson function currently writes the
service-account.json with default permissions; change it to enforce owner-only
read/write permissions like credentials.json does by creating the .clix dir as
before, writing the file, then calling fs.chmod(savePath, 0o600) (and
handle/await any errors) so the file is restricted; reference
saveServiceAccountJson and mimic the chmod pattern used in
src/lib/auth/credentials.ts for credentials.json.

In `@src/lib/services/firebase/oauth/config.ts`:
- Around line 157-168: Update the OAuth scopes array in the scopes property so
it uses least-privilege Cloud Resource Manager scope(s) instead of the broad
cloud-platform scope: replace 'https://www.googleapis.com/auth/cloud-platform'
in the scopes array with the more specific
'https://www.googleapis.com/auth/cloudplatformprojects' (or
'https://www.googleapis.com/auth/cloudplatformprojects.readonly' if only
listing/reading projects) while keeping the existing firebase and iam scopes;
locate this change in the scopes definition in config.ts.

In `@src/ui/components/FirebaseWizard.tsx`:
- Around line 208-215: The menu item for "setup-service-account" is incorrectly
gated only on Android; change the condition in the FirebaseWizard component so
the item is shown when OAuth is configured AND a project ID or valid platform
config exists (use getProjectIdFromResult(result) or check result.ios?.valid)
instead of relying solely on result.android?.valid; update the conditional that
calls items.push({ id: 'setup-service-account', ... }) to use
isOAuthConfigured() && (getProjectIdFromResult(result) != null ||
result.ios?.valid || result.android?.valid) so iOS-only projects with a project
ID will see the menu entry.

In `@src/ui/components/InstallPreparationUI.tsx`:
- Around line 418-423: The render branch for phase 'ios_setup' calls
handleIosSkip() directly which triggers setPhase during render; instead remove
that direct call and create a useEffect that watches the phase (or a derived
boolean), context, and handleIosSkip, and invokes handleIosSkip() inside the
effect when phase === 'ios_setup' and context is present; keep the component
returning <CheckingPhase /> from the render path and ensure the effect's
dependency array includes handleIosSkip and context to avoid stale closures.

package.json Outdated
Comment on lines 52 to 54
"@googleapis/cloudresourcemanager": "^6.0.1",
"@googleapis/firebase": "^12.0.1",
"@googleapis/iam": "^36.0.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check the package.json to see the context and google-auth-library version
head -60 package.json | tail -20

Repository: clix-so/clix-cli

Length of output: 679


🌐 Web query:

Latest npm versions peer requirements @googleapis/firebase @googleapis/iam @googleapis/cloudresourcemanager google-auth-library compatibility

💡 Result:

Latest npm package versions (as of Feb 6, 2026)

  • @googleapis/firebase: 10.0.1 [1]
  • @googleapis/iam: 30.1.0 [2]
  • @googleapis/cloudresourcemanager: 3.0.1 [3]
  • google-auth-library: 10.5.0 [4]

Peer requirements / compatibility (what to align)

The @googleapis/* API packages are generated from the googleapis Node client ecosystem, which (for current releases) is built to use:

  • google-auth-library: ^10.2.0 (i.e., any compatible 10.x, including 10.5.0) [5]
  • googleapis-common: ^8.0.0 [5]

google-auth-library 10.x requires Node.js >= 18. [6]

So the practical compatibility target is:

  • Node >= 18
  • google-auth-library 10.x (10.5.0 is fine), satisfying ^10.2.0.

Sources: [1] [2] [3] [4] [5] [6]


Update googleapis package versions to available releases on npm.

The specified versions do not match current releases: @googleapis/firebase should be ^10.0.1 (not ^12.0.1), @googleapis/iam should be ^30.1.0 (not ^36.0.0), and @googleapis/cloudresourcemanager should be ^3.0.1 (not ^6.0.1). The current specification will cause npm installation to fail as these versions do not exist on npm. The google-auth-library ^10.5.0 requirement is compatible with the @googleapis packages, but only with corrected versions.

🤖 Prompt for AI Agents
In `@package.json` around lines 52 - 54, Update the invalid dependency versions in
package.json: change "@googleapis/cloudresourcemanager" from ^6.0.1 to ^3.0.1,
"@googleapis/firebase" from ^12.0.1 to ^10.0.1, and "@googleapis/iam" from
^36.0.0 to ^30.1.0 so they match published npm releases; leave
"google-auth-library" at ^10.5.0 as-is, then run npm install (or yarn) and a
quick build/test to verify no further dependency conflicts.

Comment on lines +159 to +169
// Determine if Firebase is needed based on project type
const needed = projectType.target !== 'unknown';

if (!needed) {
return {
configured: true,
androidConfigured: true,
iosConfigured: true,
needed: false,
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unknown project targets can falsely mark preparation as ready.
Both Firebase and iOS status checks treat target === 'unknown' as “not needed,” which can produce ready = true even when setup is missing. Consider treating unknown as needing setup (or blocking readiness) to avoid false positives.

🛠️ Suggested behavior for unknown targets
-  // Determine if Firebase is needed based on project type
-  const needed = projectType.target !== 'unknown';
-
-  if (!needed) {
-    return {
-      configured: true,
-      androidConfigured: true,
-      iosConfigured: true,
-      needed: false,
-    };
-  }
+  if (projectType.target === 'unknown') {
+    return {
+      configured: false,
+      androidConfigured: false,
+      iosConfigured: false,
+      needed: true,
+    };
+  }
-  // iOS setup is only needed for iOS or cross-platform targets
-  const needed = projectType.target === 'ios' || projectType.target === 'ios-android';
-
-  if (!needed) {
-    return {
-      needed: false,
-      entitlementsConfigured: true,
-      nseConfigured: true,
-    };
-  }
+  if (projectType.target === 'unknown') {
+    return {
+      needed: true,
+      entitlementsConfigured: false,
+      nseConfigured: false,
+    };
+  }
+  const needed = projectType.target === 'ios' || projectType.target === 'ios-android';
+  if (!needed) {
+    return {
+      needed: false,
+      entitlementsConfigured: true,
+      nseConfigured: true,
+    };
+  }

Also applies to: 224-233

🤖 Prompt for AI Agents
In `@src/commands/skill/preparation.ts` around lines 159 - 169, The readiness
check incorrectly treats projectType.target === 'unknown' as not needing
Firebase/iOS setup; update the logic in the preparation routine so that when
projectType.target === 'unknown' it is treated as needing setup (i.e., needed =
true or explicitly block readiness) instead of returning configured true. Modify
the branch around the needed variable and the blocks that set configured,
androidConfigured, and iosConfigured (and the similar checks referenced later)
so unknown target forces configured:false (or prevents ready:true) and flows
through normal setup checks for Firebase and iOS.

Comment on lines +417 to +427
async saveServiceAccountJson(
projectPath: string,
serviceAccountJson: ServiceAccountJson,
): Promise<string> {
const clixDir = path.join(projectPath, '.clix');
const savePath = path.join(clixDir, 'service-account.json');

await fs.mkdir(clixDir, { recursive: true });
await fs.writeFile(savePath, JSON.stringify(serviceAccountJson, null, 2), 'utf-8');

return savePath;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check .gitignore for .clix exclusion
find . -name '.gitignore' -type f | head -10

Repository: clix-so/clix-cli

Length of output: 72


🏁 Script executed:

# Search for .clix related entries in .gitignore files
rg -n '\.(clix|clix/)' --glob '.gitignore'

Repository: clix-so/clix-cli

Length of output: 106


🏁 Script executed:

# Look for other credential/session storage patterns in the codebase
rg -n 'saveServiceAccountJson|credentials\.json|sessions/' src/lib --type ts --type tsx -A 3 -B 1

Repository: clix-so/clix-cli

Length of output: 87


🏁 Script executed:

# Check if fs.mkdir or fs.writeFile with mode options are used elsewhere
rg -n 'fs\.(mkdir|writeFile).*mode' src/ --type ts --type tsx

Repository: clix-so/clix-cli

Length of output: 87


🏁 Script executed:

# Search for fs.mkdir or fs.writeFile with mode across ts files
rg -n 'fs\.(mkdir|writeFile)' src/ -A 2 | head -50

Repository: clix-so/clix-cli

Length of output: 2695


🏁 Script executed:

# Look at how credentials.json is being saved
rg -n 'credentials\.json' src/lib -A 3 -B 1 -g '*.ts'

Repository: clix-so/clix-cli

Length of output: 1097


🏁 Script executed:

# Read the downloader.ts file to see the full context
cat -n src/lib/services/firebase/downloader.ts | sed -n '410,435p'

Repository: clix-so/clix-cli

Length of output: 1073


🏁 Script executed:

# Read credentials.ts to see how it saves the credentials file
cat -n src/lib/auth/credentials.ts | head -150

Repository: clix-so/clix-cli

Length of output: 5099


🏁 Script executed:

# Look for the actual save method in credentials.ts
rg -n 'saveCredentials|writeFile|mkdir' src/lib/auth/credentials.ts -A 4 -B 1

Repository: clix-so/clix-cli

Length of output: 734


Harden service-account key storage permissions.

The private key is written with default permissions, creating a security risk on shared machines. The credentials.json file already uses restricted permissions (0o600) and chmod() enforcement in src/lib/auth/credentials.ts—apply the same pattern here.

🔐 Proposed hardening for key storage
-    await fs.mkdir(clixDir, { recursive: true });
-    await fs.writeFile(savePath, JSON.stringify(serviceAccountJson, null, 2), 'utf-8');
+    await fs.mkdir(clixDir, { recursive: true, mode: 0o700 });
+    await fs.writeFile(
+      savePath,
+      JSON.stringify(serviceAccountJson, null, 2),
+      { encoding: 'utf-8', mode: 0o600 },
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async saveServiceAccountJson(
projectPath: string,
serviceAccountJson: ServiceAccountJson,
): Promise<string> {
const clixDir = path.join(projectPath, '.clix');
const savePath = path.join(clixDir, 'service-account.json');
await fs.mkdir(clixDir, { recursive: true });
await fs.writeFile(savePath, JSON.stringify(serviceAccountJson, null, 2), 'utf-8');
return savePath;
async saveServiceAccountJson(
projectPath: string,
serviceAccountJson: ServiceAccountJson,
): Promise<string> {
const clixDir = path.join(projectPath, '.clix');
const savePath = path.join(clixDir, 'service-account.json');
await fs.mkdir(clixDir, { recursive: true, mode: 0o700 });
await fs.writeFile(
savePath,
JSON.stringify(serviceAccountJson, null, 2),
{ encoding: 'utf-8', mode: 0o600 },
);
return savePath;
}
🤖 Prompt for AI Agents
In `@src/lib/services/firebase/downloader.ts` around lines 417 - 427, The
saveServiceAccountJson function currently writes the service-account.json with
default permissions; change it to enforce owner-only read/write permissions like
credentials.json does by creating the .clix dir as before, writing the file,
then calling fs.chmod(savePath, 0o600) (and handle/await any errors) so the file
is restricted; reference saveServiceAccountJson and mimic the chmod pattern used
in src/lib/auth/credentials.ts for credentials.json.

Comment on lines 157 to 168
/**
* OAuth scopes required for Firebase Management API.
* Using full firebase scope for listing projects/apps, downloading configs, and creating apps.
*
* - firebase: List projects/apps, download configs, create apps
* - iam: Service Account management (create, list, generate keys)
* - cloud-platform: GCP project access (list projects, add Firebase to existing projects)
*/
scopes: ['https://www.googleapis.com/auth/firebase'],
scopes: [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/iam',
'https://www.googleapis.com/auth/cloud-platform',
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the file to understand what operations use these scopes
cat -n src/lib/services/firebase/oauth/config.ts

Repository: clix-so/clix-cli

Length of output: 7806


🏁 Script executed:

# Search for how these scopes are used in the codebase
rg -n "GOOGLE_OAUTH_CONFIG" --type ts --type tsx -B 3 -A 3

Repository: clix-so/clix-cli

Length of output: 87


🏁 Script executed:

# Look for Firebase Admin API calls and GCP project operations
rg -n "firebase|iam|cloud-platform|projects\.|apps\." src/lib/services/firebase/ --type ts --type tsx -A 2

Repository: clix-so/clix-cli

Length of output: 87


🌐 Web query:

Google Cloud OAuth scopes Firebase Management API IAM service account GCP projects minimum required permissions

💡 Result:

OAuth scopes (Firebase Management API)

Firebase Management API methods accept either Firebase-specific scopes or the broader Cloud Platform scopes:

  • Read/write: https://www.googleapis.com/auth/firebase or https://www.googleapis.com/auth/cloud-platform [1]
  • Read-only (for read methods like projects.get / projects.list): https://www.googleapis.com/auth/firebase.readonly or https://www.googleapis.com/auth/cloud-platform.read-only [2][3]

Minimum IAM permissions (example: projects.addFirebase)

To call projects.addFirebase (turn an existing GCP project into a Firebase project), the calling principal (user or service account) must have:

  • firebase.projects.update
  • resourcemanager.projects.get
  • serviceusage.services.enable
  • serviceusage.services.get [1][4]

Google notes that Project Editor and Project Owner include these permissions (but you can also build a custom role with exactly these). [1]

“Baseline” permissions commonly required for Firebase project access (custom roles)

Firebase’s IAM docs also list permissions that are required broadly to retrieve Firebase project info and run many Firebase workflows/CLI commands, including:

  • firebase.projects.get (and other firebase.* reads like firebase.clients.get/list, firebase.links.list)
  • resourcemanager.projects.get, resourcemanager.projects.getIamPolicy, resourcemanager.projects.list
  • various serviceusage.* and API Keys reads/list/lookup [5]

If your automation also needs to grant roles / change IAM policy, you additionally need resourcemanager.projects.setIamPolicy (called out as required to manage role assignments). [6]

Sources: [1] Firebase projects.addFirebase (scopes + required permissions), [2] Firebase projects.list (scopes), [3] Firebase projects.get (scopes), [4] Firebase “workflow set up/manage project” (required permissions), [5] Firebase IAM permissions (required permissions tables), [6] Firebase IAM roles (permission needed to assign roles).


🏁 Script executed:

# Search for usage of the GOOGLE_OAUTH_CONFIG and related API calls
rg -n "GOOGLE_OAUTH_CONFIG\|firebase|firebaseapi\|cloudresourcemanager\|projects\.addFirebase" src/lib/services/firebase/ -A 2 -B 2

Repository: clix-so/clix-cli

Length of output: 42


🏁 Script executed:

# Find where OAuth token is used and what APIs are called
rg -n "getOAuthCredentials\|oauth\|bearer\|authorization" src/lib/services/firebase/ --type ts -A 3

Repository: clix-so/clix-cli

Length of output: 42


🏁 Script executed:

# Check for any Firebase Admin SDK or Google Cloud client usage
rg -n "firebase-admin|google-cloud|googleapis" . -A 2

Repository: clix-so/clix-cli

Length of output: 4954


🌐 Web query:

Firebase Management API scopes narrower alternatives to cloud-platform GCP project operations minimum required scope

💡 Result:

For Firebase Management API calls, you usually don’t need the broad GCP scope https://www.googleapis.com/auth/cloud-platform—Firebase provides narrower scopes you can use instead.

Firebase Management API (firebase.googleapis.com) — narrower scopes

  • Read Firebase project metadata (example: projects.get): use https://www.googleapis.com/auth/firebase.readonly (narrowest) instead of cloud-platform.read-only. [1]
  • Write / manage Firebase project (example: projects.addFirebase, projects.patch): use https://www.googleapis.com/auth/firebase (narrowest write scope).
    • projects.addFirebase accepts firebase or cloud-platform (not read-only). [2]

If you truly need “GCP project operations” (Cloud Resource Manager)

If your code is calling Cloud Resource Manager (cloudresourcemanager.googleapis.com) endpoints like projects.get / projects.list, the narrower alternatives to cloud-platform are:

  • Read-only: https://www.googleapis.com/auth/cloudplatformprojects.readonly (or cloud-platform.read-only) [3]
  • Read/write: https://www.googleapis.com/auth/cloudplatformprojects (or cloud-platform) [3]

Practical “minimum required scope” rule of thumb

  • If you only call Firebase Management API: request firebase.readonly (read) or firebase (write). [1][2]
  • Only request cloudplatformprojects(.readonly) if you are directly calling Cloud Resource Manager project APIs. [3]

Sources: Firebase projects.get scopes [1], Firebase projects.addFirebase scopes [2], Cloud Resource Manager projects.get scopes (includes cloudplatformprojects*) [3].


Replace cloud-platform with narrower scopes aligned to actual API usage.

The cloud-platform scope grants access across all GCP services. Based on the code's use of separate Google API clients (@googleapis/firebase, @googleapis/iam, @googleapis/cloudresourcemanager), consider these narrower alternatives:

  • https://www.googleapis.com/auth/firebase: Write operations on Firebase Management API (create apps, download configs, add Firebase to projects)
  • https://www.googleapis.com/auth/cloudplatformprojects or cloudplatformprojects.readonly: Access to Cloud Resource Manager (list/get projects) instead of the broad cloud-platform scope
  • https://www.googleapis.com/auth/iam: Already appropriately scoped for service account management

Replace the cloud-platform scope with the specific cloudplatformprojects scope to follow least-privilege principles.

🤖 Prompt for AI Agents
In `@src/lib/services/firebase/oauth/config.ts` around lines 157 - 168, Update the
OAuth scopes array in the scopes property so it uses least-privilege Cloud
Resource Manager scope(s) instead of the broad cloud-platform scope: replace
'https://www.googleapis.com/auth/cloud-platform' in the scopes array with the
more specific 'https://www.googleapis.com/auth/cloudplatformprojects' (or
'https://www.googleapis.com/auth/cloudplatformprojects.readonly' if only
listing/reading projects) while keeping the existing firebase and iam scopes;
locate this change in the scopes definition in config.ts.

Remove backward compatibility code for V1→V2 migration:
- Unify ProjectConfigV1Schema and ProjectConfigV2Schema into single ProjectConfigSchema
- Change version field from z.literal(2) to z.number() for flexibility
- Remove isConfigV1(), isConfigV2(), migrateV1ToV2() functions
- Keep ensureLatestVersion() framework for future migrations
- Remove loadV2() method, use load() directly
- Update CURRENT_PROJECT_CONFIG_VERSION to 1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/lib/config/project-config-manager.ts`:
- Around line 167-192: The updateSetup method currently merges a
Partial<SetupStatus> and saves without validating, which can produce an invalid
ProjectConfig; modify updateSetup to merge updates into config.setup and then
validate or normalize the resulting config before calling save: use an existing
config validation/normalization utility (or add one) to fill missing default
fields for SetupStatus (e.g., getDefaultSetup or schema validate) and run it
against the merged object, and if validation fails throw a ConfigError; ensure
you reference updateSetup, load, save, SetupStatus and ProjectConfig when
locating and changing the code.
🧹 Nitpick comments (1)
src/lib/config/project-config-schema.ts (1)

123-123: Constrain config version to a non‑negative integer.
Currently z.number() allows decimals (e.g., 1.5), which complicates migrations and version comparisons. Versions should be integers.

♻️ Proposed fix
-  version: z.number(),
+  version: z.number().int().nonnegative(),

Comment on lines +167 to +192
/**
* Update the setup status in config.
* Creates setup object if it doesn't exist.
*
* @param updates - Partial setup status to merge
*/
async updateSetup(updates: Partial<SetupStatus>): Promise<void> {
const config = await this.load();
if (!config) {
throw new ConfigError(
'Project config not found. Run "clix login" first.',
ERROR_CODES.PROJECT_CONFIG_NOT_FOUND,
this.configFilePath,
);
}

const updatedConfig: ProjectConfig = {
...config,
setup: {
...config.setup,
...updates,
},
};

await this.save(updatedConfig);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent saving invalid partial setup objects.
updateSetup accepts Partial<SetupStatus>; if callers pass incomplete sub-objects (e.g., only projectId), the saved config can fail schema validation and later loads will break. Validate the merged config (or fill defaults) before saving.

🛠️ Suggested fix: validate before save
   const updatedConfig: ProjectConfig = {
     ...config,
     setup: {
       ...config.setup,
       ...updates,
     },
   };
-
-  await this.save(updatedConfig);
+  const validated = safeValidateProjectConfig(updatedConfig);
+  if (!validated) {
+    throw new ConfigError(
+      'Invalid project configuration',
+      ERROR_CODES.PROJECT_CONFIG_INVALID,
+      this.configFilePath,
+    );
+  }
+
+  await this.save(validated);
🤖 Prompt for AI Agents
In `@src/lib/config/project-config-manager.ts` around lines 167 - 192, The
updateSetup method currently merges a Partial<SetupStatus> and saves without
validating, which can produce an invalid ProjectConfig; modify updateSetup to
merge updates into config.setup and then validate or normalize the resulting
config before calling save: use an existing config validation/normalization
utility (or add one) to fill missing default fields for SetupStatus (e.g.,
getDefaultSetup or schema validate) and run it against the merged object, and if
validation fails throw a ConfigError; ensure you reference updateSetup, load,
save, SetupStatus and ProjectConfig when locating and changing the code.

- Migrate Firebase API clients from REST to googleapis library
- Remove IAM API (SA keys now downloaded from Firebase Console)
- Add Sender Config API integration (check/register push notification config)
- Extract FirebaseWizard PHASE_TRANSITIONS state machine with transition() validation
- Add Organization/Projects concurrent fetching service
- Add ProjectSelector search/filter functionality
- Improve InstallPreparationUI with explicit action menus
- Add OAuth debug logging infrastructure
- Simplify OAuth scopes to firebase-only
- Add exponential backoff retry logic for API calls
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@src/lib/debug/logger.ts`:
- Around line 201-226: The writeToFile method currently calls JSON.stringify on
optional data in both the file-write branch (building variable line) and the
stderr fallback, which can throw on circular or unserializable values; change
writeToFile (and any helper uses of this.namespace within it) to first attempt
JSON.stringify(data) inside a try/catch (or use a small safeStringify helper)
and on failure replace with a safe placeholder (e.g., "[unserializable data]" or
inspect fallback) so that neither the appendFileSync branch nor the
console.error fallback can throw — update the construction of line and the
console.error call to use the safe stringified value instead of calling
JSON.stringify directly.

In `@src/lib/services/firebase/api/firebase-api.ts`:
- Around line 91-97: The code currently type-asserts p.state to 'ACTIVE' |
'DELETED' which can let unexpected strings through; instead normalize p.state
explicitly before pushing into projects (e.g., compute const normalizedState =
p.state === 'DELETED' ? 'DELETED' : 'ACTIVE' and use that). Update the push that
builds the FirebaseProject (the projects.push block using p.state) to use this
normalized value, and apply the same change to the other occurrence (the similar
block around lines 273-277) so the FirebaseProject union is always respected.

In `@src/lib/services/firebase/oauth/auth-client.ts`:
- Around line 336-345: The current refresh error handler in the auth client
unconditionally rewraps any refreshAccessToken failure as "invalid_grant",
causing unnecessary re-auth flows; update the catch in the token refresh block
(where tokenStore.isExpired(tokens) is checked and refreshAccessToken is called)
to inspect the caught error (e.g., error.message or error.code) and only throw a
new Error prefixed with "invalid_grant: " when the underlying error actually
indicates "invalid_grant"; for other errors rethrow the original error (or throw
a generic refresh error without the invalid_grant prefix) so transient
network/IO errors are not misclassified.

In `@src/lib/services/organization-projects.ts`:
- Around line 22-27: The normalizeConcurrency function currently floors
fractional inputs which can produce 0 for values like 0.5; update
normalizeConcurrency to first validate the input, then Math.floor the value and
clamp the result to a minimum of 1 (and fall back to
DEFAULT_PROJECT_FETCH_CONCURRENCY when input is missing/invalid) so the returned
concurrency is always at least 1; reference the normalizeConcurrency function
and DEFAULT_PROJECT_FETCH_CONCURRENCY constant when making the change.

In `@src/lib/utils/oauth.ts`:
- Around line 261-283: The debug write currently persists full_url and
all_params which can leak secrets; before calling oauthLogger.writeToFile (from
within the OAuth error branch), sanitize the data by redacting sensitive
parameter values (e.g., code, state, access_token, refresh_token, client_secret,
refresh_token, id_token) and replace them with a presence flag or "[REDACTED]"
and reconstruct a sanitized full_url without secret values; then pass
sanitized_all_params and sanitized_full_url to oauthLogger.writeToFile (keep use
of findProjectRoot() and existing metadata) so only non-sensitive info or
presence indicators are persisted.

In `@src/ui/components/FirebaseWizard.tsx`:
- Around line 2087-2091: The setTimeout in the FirebaseWizard component
schedules setPhase(...) and onComplete(...) after 1500ms but doesn't clear the
timer, which can lead to state updates after unmount; capture the timeout id
when calling setTimeout (e.g., store it in a ref or local variable tied to the
component), and ensure you call clearTimeout(timeoutId) in a useEffect cleanup
or when the component is unmounted/cancelled so that the scheduled setPhase and
onComplete are not invoked on an unmounted component.

In `@src/ui/components/InstallPreparationUI.tsx`:
- Around line 62-67: getPostFirebasePhase currently only checks
ios.entitlementsConfigured and skips ios_setup if entitlements are present,
which ignores missing NSE; update getPostFirebasePhase (and the related
handleContinue logic) to require both ios.entitlementsConfigured and
ios.nseConfigured to be true before returning 'ready' when ios.needed is true so
that missing NSE keeps the flow in 'ios_setup'. Locate the PreparationContext
usage in getPostFirebasePhase and the function/method named handleContinue and
change their conditional from checking ios.entitlementsConfigured to checking
(ios.entitlementsConfigured && ios.nseConfigured), preserving the ios.needed
guard and returning 'ios_setup' otherwise.
🧹 Nitpick comments (5)
src/ui/components/ProjectSelector.tsx (2)

6-9: Avoid duplicating OrgWithProjects.

This type already exists in src/lib/services/organization-projects.ts; redefining it here risks drift. Consider importing and re-exporting the shared type.

♻️ Suggested refactor
-import type { Organization, Project } from '@/lib/api';
+import type { Organization, Project } from '@/lib/api';
+import type { OrgWithProjects } from '@/lib/services/organization-projects';
@@
-export interface OrgWithProjects {
-  org: Organization;
-  projects: Project[];
-}
+export type { OrgWithProjects };

84-159: Align selector input handling with GenericSelector + useCancelInput.

This is a selector component but handles ESC/Ctrl+C manually; the UI guidelines prefer the shared selector abstraction and cancellation hook.

As per coding guidelines, “Import useCancelInput from @/ui/hooks for ESC/Ctrl+C cancellation in new components; for selector-based components, extend GenericSelector.”

src/lib/services/firebase/api/firebase-api.ts (1)

49-68: Consider replacing the class with a plain-object factory.

The codebase guidelines prefer plain objects with interfaces over classes for TS modules.

As per coding guidelines, “Prefer plain objects with interfaces over classes; use ES module exports for encapsulation in TypeScript code.”

src/lib/api/internal-client.ts (1)

71-73: Token resolved once before retry loop may become stale during long retry sequences.

The access token is resolved once at line 71, but if maxRetries is high and the server returns 5xx repeatedly, the token could expire during the retry attempts. Consider refreshing the token before each retry attempt, or at least after a 401 response within the loop.

However, since the default maxRetries is 0 and the total retry window is short (150ms + 300ms + ... backoff), this is unlikely to be a practical issue for most use cases.

src/ui/components/FirebaseWizard.tsx (1)

799-823: Clipboard reading lacks Windows support.

The readClipboard function supports macOS (pbpaste) and Linux (xclip/xsel) but returns null on Windows. If Windows users are expected, consider adding powershell -command "Get-Clipboard" as a fallback.

🪟 Optional: Add Windows clipboard support
     if (platform === 'linux') {
       try {
         const { stdout } = await execFileAsync('xclip', ['-selection', 'clipboard', '-o']);
         return stdout;
       } catch {
         const { stdout } = await execFileAsync('xsel', ['--clipboard', '--output']);
         return stdout;
       }
     }
+    if (platform === 'win32') {
+      const { stdout } = await execFileAsync('powershell', ['-command', 'Get-Clipboard']);
+      return stdout;
+    }
     return null;

Comment on lines +201 to +226
writeToFile(message: string, data?: unknown, projectRoot?: string): void {
// Try multiple locations to ensure logging works
const locations = [
projectRoot,
process.cwd(),
process.env.HOME ? join(process.env.HOME, '.clix') : null,
].filter((loc): loc is string => loc !== null && loc !== undefined);

for (const root of locations) {
try {
const clixDir = root.endsWith('.clix') ? root : join(root, '.clix');
const logFile = join(clixDir, 'debug.log');

mkdirSync(clixDir, { recursive: true });

const timestamp = new Date().toISOString();
const line = `${timestamp} ${this.namespace} ${message}${data !== undefined ? ` ${JSON.stringify(data)}` : ''}\n`;
appendFileSync(logFile, line);
return; // Success, exit loop
} catch {
// Try next location
}
}
// All locations failed, log to stderr as last resort
console.error(`[${this.namespace}] ${message}`, data !== undefined ? JSON.stringify(data) : '');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get its size
wc -l src/lib/debug/logger.ts

Repository: clix-so/clix-cli

Length of output: 87


🏁 Script executed:

# Read the writeToFile method around the specified lines (201-226)
sed -n '195,230p' src/lib/debug/logger.ts

Repository: clix-so/clix-cli

Length of output: 1364


🏁 Script executed:

# Check if there are any other stringify calls in the file
rg 'JSON\.stringify|safeStringify' src/lib/debug/logger.ts -n

Repository: clix-so/clix-cli

Length of output: 356


🏁 Script executed:

# Check if there are any circular data patterns or objects that might be passed as data
rg 'writeToFile.*\(' src/lib/debug/logger.ts -A 2 -B 2

Repository: clix-so/clix-cli

Length of output: 306


Harden file logging against unserializable context

JSON.stringify can throw on circular data. While line 217 is protected by a try-catch that attempts fallback locations, the stderr fallback on line 225 is unprotected. If all file writes fail and the data is unserializable, JSON.stringify will throw an uncaught exception, crashing the logging mechanism entirely. Wrap JSON.stringify calls in safe error handling.

🔧 Proposed fix
  writeToFile(message: string, data?: unknown, projectRoot?: string): void {
+   const safeStringify = (value: unknown): string => {
+     try {
+       return JSON.stringify(value);
+     } catch {
+       return '[unserializable]';
+     }
+   };
    // Try multiple locations to ensure logging works
    const locations = [
      projectRoot,
      process.cwd(),
      process.env.HOME ? join(process.env.HOME, '.clix') : null,
    ].filter((loc): loc is string => loc !== null && loc !== undefined);

    for (const root of locations) {
      try {
        const clixDir = root.endsWith('.clix') ? root : join(root, '.clix');
        const logFile = join(clixDir, 'debug.log');

        mkdirSync(clixDir, { recursive: true });

        const timestamp = new Date().toISOString();
-       const line = `${timestamp} ${this.namespace} ${message}${data !== undefined ? ` ${JSON.stringify(data)}` : ''}\n`;
+       const serialized = data !== undefined ? safeStringify(data) : '';
+       const line = `${timestamp} ${this.namespace} ${message}${
+         data !== undefined ? ` ${serialized}` : ''
+       }\n`;
        appendFileSync(logFile, line);
        return; // Success, exit loop
      } catch {
        // Try next location
      }
    }
    // All locations failed, log to stderr as last resort
-   console.error(`[${this.namespace}] ${message}`, data !== undefined ? JSON.stringify(data) : '');
+   const fallback = data !== undefined ? safeStringify(data) : '';
+   console.error(`[${this.namespace}] ${message}`, fallback);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
writeToFile(message: string, data?: unknown, projectRoot?: string): void {
// Try multiple locations to ensure logging works
const locations = [
projectRoot,
process.cwd(),
process.env.HOME ? join(process.env.HOME, '.clix') : null,
].filter((loc): loc is string => loc !== null && loc !== undefined);
for (const root of locations) {
try {
const clixDir = root.endsWith('.clix') ? root : join(root, '.clix');
const logFile = join(clixDir, 'debug.log');
mkdirSync(clixDir, { recursive: true });
const timestamp = new Date().toISOString();
const line = `${timestamp} ${this.namespace} ${message}${data !== undefined ? ` ${JSON.stringify(data)}` : ''}\n`;
appendFileSync(logFile, line);
return; // Success, exit loop
} catch {
// Try next location
}
}
// All locations failed, log to stderr as last resort
console.error(`[${this.namespace}] ${message}`, data !== undefined ? JSON.stringify(data) : '');
}
writeToFile(message: string, data?: unknown, projectRoot?: string): void {
const safeStringify = (value: unknown): string => {
try {
return JSON.stringify(value);
} catch {
return '[unserializable]';
}
};
// Try multiple locations to ensure logging works
const locations = [
projectRoot,
process.cwd(),
process.env.HOME ? join(process.env.HOME, '.clix') : null,
].filter((loc): loc is string => loc !== null && loc !== undefined);
for (const root of locations) {
try {
const clixDir = root.endsWith('.clix') ? root : join(root, '.clix');
const logFile = join(clixDir, 'debug.log');
mkdirSync(clixDir, { recursive: true });
const timestamp = new Date().toISOString();
const serialized = data !== undefined ? safeStringify(data) : '';
const line = `${timestamp} ${this.namespace} ${message}${
data !== undefined ? ` ${serialized}` : ''
}\n`;
appendFileSync(logFile, line);
return; // Success, exit loop
} catch {
// Try next location
}
}
// All locations failed, log to stderr as last resort
const fallback = data !== undefined ? safeStringify(data) : '';
console.error(`[${this.namespace}] ${message}`, fallback);
}
🤖 Prompt for AI Agents
In `@src/lib/debug/logger.ts` around lines 201 - 226, The writeToFile method
currently calls JSON.stringify on optional data in both the file-write branch
(building variable line) and the stderr fallback, which can throw on circular or
unserializable values; change writeToFile (and any helper uses of this.namespace
within it) to first attempt JSON.stringify(data) inside a try/catch (or use a
small safeStringify helper) and on failure replace with a safe placeholder
(e.g., "[unserializable data]" or inspect fallback) so that neither the
appendFileSync branch nor the console.error fallback can throw — update the
construction of line and the console.error call to use the safe stringified
value instead of calling JSON.stringify directly.

Comment on lines +91 to +97
projects.push({
name: p.name ?? '',
projectId: p.projectId ?? '',
projectNumber: p.projectNumber ?? '',
displayName: p.displayName ?? '',
state: (p.state as 'ACTIVE' | 'DELETED') ?? 'ACTIVE',
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize Firebase project state instead of type-asserting.

The current cast doesn’t constrain values; unexpected strings can leak and violate the FirebaseProject contract. Normalize explicitly to the supported union.

🔧 Suggested fix
-            state: (p.state as 'ACTIVE' | 'DELETED') ?? 'ACTIVE',
+            state: p.state === 'DELETED' ? 'DELETED' : 'ACTIVE',
@@
-      state: (project.state as 'ACTIVE' | 'DELETED') ?? 'ACTIVE',
+      state: project.state === 'DELETED' ? 'DELETED' : 'ACTIVE',

Also applies to: 273-277

🤖 Prompt for AI Agents
In `@src/lib/services/firebase/api/firebase-api.ts` around lines 91 - 97, The code
currently type-asserts p.state to 'ACTIVE' | 'DELETED' which can let unexpected
strings through; instead normalize p.state explicitly before pushing into
projects (e.g., compute const normalizedState = p.state === 'DELETED' ?
'DELETED' : 'ACTIVE' and use that). Update the push that builds the
FirebaseProject (the projects.push block using p.state) to use this normalized
value, and apply the same change to the other occurrence (the similar block
around lines 273-277) so the FirebaseProject union is always respected.

Comment on lines 336 to +345
// Check if token is expired and we have a refresh token
if (this.tokenStore.isExpired(tokens) && tokens.refresh_token) {
tokens = await this.refreshAccessToken(tokens.refresh_token);
try {
tokens = await this.refreshAccessToken(tokens.refresh_token);
} catch (error) {
// If refresh failed (e.g., invalid_grant), tokens are already cleared
// Re-throw with indication that re-authentication is needed
const message = error instanceof Error ? error.message : 'Token refresh failed';
throw new Error(`invalid_grant: ${message}`);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Only prefix invalid_grant when that error actually occurs

Right now any refresh failure gets wrapped as invalid_grant, which can force unnecessary re-auth on transient errors. Consider only prefixing when the underlying error is actually invalid_grant.

🔧 Proposed fix
       try {
         tokens = await this.refreshAccessToken(tokens.refresh_token);
       } catch (error) {
         // If refresh failed (e.g., invalid_grant), tokens are already cleared
         // Re-throw with indication that re-authentication is needed
         const message = error instanceof Error ? error.message : 'Token refresh failed';
-        throw new Error(`invalid_grant: ${message}`);
+        if (message.includes('invalid_grant')) {
+          throw new Error(`invalid_grant: ${message}`);
+        }
+        throw new Error(message);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if token is expired and we have a refresh token
if (this.tokenStore.isExpired(tokens) && tokens.refresh_token) {
tokens = await this.refreshAccessToken(tokens.refresh_token);
try {
tokens = await this.refreshAccessToken(tokens.refresh_token);
} catch (error) {
// If refresh failed (e.g., invalid_grant), tokens are already cleared
// Re-throw with indication that re-authentication is needed
const message = error instanceof Error ? error.message : 'Token refresh failed';
throw new Error(`invalid_grant: ${message}`);
}
// Check if token is expired and we have a refresh token
if (this.tokenStore.isExpired(tokens) && tokens.refresh_token) {
try {
tokens = await this.refreshAccessToken(tokens.refresh_token);
} catch (error) {
// If refresh failed (e.g., invalid_grant), tokens are already cleared
// Re-throw with indication that re-authentication is needed
const message = error instanceof Error ? error.message : 'Token refresh failed';
if (message.includes('invalid_grant')) {
throw new Error(`invalid_grant: ${message}`);
}
throw new Error(message);
}
🤖 Prompt for AI Agents
In `@src/lib/services/firebase/oauth/auth-client.ts` around lines 336 - 345, The
current refresh error handler in the auth client unconditionally rewraps any
refreshAccessToken failure as "invalid_grant", causing unnecessary re-auth
flows; update the catch in the token refresh block (where
tokenStore.isExpired(tokens) is checked and refreshAccessToken is called) to
inspect the caught error (e.g., error.message or error.code) and only throw a
new Error prefixed with "invalid_grant: " when the underlying error actually
indicates "invalid_grant"; for other errors rethrow the original error (or throw
a generic refresh error without the invalid_grant prefix) so transient
network/IO errors are not misclassified.

Comment on lines +22 to +27
function normalizeConcurrency(input?: number): number {
if (!input || !Number.isFinite(input) || input <= 0) {
return DEFAULT_PROJECT_FETCH_CONCURRENCY;
}
return Math.floor(input);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp fractional concurrency to at least 1.

Values like 0.5 currently floor to 0, yielding zero workers and empty results. Clamp after flooring.

🛠️ Proposed fix
 function normalizeConcurrency(input?: number): number {
-  if (!input || !Number.isFinite(input) || input <= 0) {
-    return DEFAULT_PROJECT_FETCH_CONCURRENCY;
-  }
-  return Math.floor(input);
+  const normalized = Math.floor(input ?? NaN);
+  if (!Number.isFinite(normalized) || normalized <= 0) {
+    return DEFAULT_PROJECT_FETCH_CONCURRENCY;
+  }
+  return normalized;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function normalizeConcurrency(input?: number): number {
if (!input || !Number.isFinite(input) || input <= 0) {
return DEFAULT_PROJECT_FETCH_CONCURRENCY;
}
return Math.floor(input);
}
function normalizeConcurrency(input?: number): number {
const normalized = Math.floor(input ?? NaN);
if (!Number.isFinite(normalized) || normalized <= 0) {
return DEFAULT_PROJECT_FETCH_CONCURRENCY;
}
return normalized;
}
🤖 Prompt for AI Agents
In `@src/lib/services/organization-projects.ts` around lines 22 - 27, The
normalizeConcurrency function currently floors fractional inputs which can
produce 0 for values like 0.5; update normalizeConcurrency to first validate the
input, then Math.floor the value and clamp the result to a minimum of 1 (and
fall back to DEFAULT_PROJECT_FETCH_CONCURRENCY when input is missing/invalid) so
the returned concurrency is always at least 1; reference the
normalizeConcurrency function and DEFAULT_PROJECT_FETCH_CONCURRENCY constant
when making the change.

Comment on lines 261 to 283
// Handle OAuth error
if (error) {
const errorMsg = errorDescription || error;
const errorMsg = errorDescription ? `${error}: ${errorDescription}` : error;

// Write debug info to .clix/debug.log
oauthLogger.writeToFile(
'OAuth callback error',
{
type: 'oauth_callback_error',
error,
error_description: errorDescription,
full_url: req.url,
all_params: Object.fromEntries(url.searchParams.entries()),
},
findProjectRoot(),
);

res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(this.options.errorHtml(errorMsg));
clearTimeout(timeout);
this.stop();
reject(new Error(`OAuth error: ${errorMsg}`));
reject(new Error(errorMsg));
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact sensitive OAuth parameters before persisting to debug.log

writeToFile always writes, and full_url + all_params can include secrets like code and state. Please redact or log only presence flags before persisting.

🛡️ Proposed fix
         const error = url.searchParams.get('error');
         const errorDescription = url.searchParams.get('error_description');
+        const allParams = Object.fromEntries(url.searchParams.entries());
+        const redactedParams = { ...allParams };
+        if (redactedParams.code) redactedParams.code = '[redacted]';
+        if (redactedParams.state) redactedParams.state = '[redacted]';
+        const sanitizedUrl = (req.url ?? '')
+          .replace(/code=[^&]+/g, 'code=[redacted]')
+          .replace(/state=[^&]+/g, 'state=[redacted]');

         // Handle OAuth error
         if (error) {
           const errorMsg = errorDescription ? `${error}: ${errorDescription}` : error;

           // Write debug info to .clix/debug.log
           oauthLogger.writeToFile(
             'OAuth callback error',
             {
               type: 'oauth_callback_error',
               error,
               error_description: errorDescription,
-              full_url: req.url,
-              all_params: Object.fromEntries(url.searchParams.entries()),
+              full_url: sanitizedUrl,
+              all_params: redactedParams,
             },
             findProjectRoot(),
           );
🤖 Prompt for AI Agents
In `@src/lib/utils/oauth.ts` around lines 261 - 283, The debug write currently
persists full_url and all_params which can leak secrets; before calling
oauthLogger.writeToFile (from within the OAuth error branch), sanitize the data
by redacting sensitive parameter values (e.g., code, state, access_token,
refresh_token, client_secret, refresh_token, id_token) and replace them with a
presence flag or "[REDACTED]" and reconstruct a sanitized full_url without
secret values; then pass sanitized_all_params and sanitized_full_url to
oauthLogger.writeToFile (keep use of findProjectRoot() and existing metadata) so
only non-sensitive info or presence indicators are persisted.

Comment on lines +2087 to +2091
// Auto-complete after brief display
setTimeout(() => {
setPhase(transition('registering_sender_config', 'complete'));
onComplete({ completed: true, skipped: false, detection: result });
}, 1500);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing cleanup for setTimeout may cause state update on unmounted component.

The setTimeout at line 2088 schedules a state update after 1500ms, but if the component unmounts before then (e.g., user cancels), this could cause a React warning about updating unmounted components.

🧹 Proposed fix with cleanup
   useEffect(() => {
     if (phase !== 'registering_sender_config' || !clixProjectId) return;

+    let timeoutId: NodeJS.Timeout | undefined;
+
     const registerSenderConfig = async () => {
       try {
         // ... existing registration logic ...
         setSenderConfigResult('success');
       } catch {
         setSenderConfigResult('failed');
       }

       // Auto-complete after brief display
-      setTimeout(() => {
+      timeoutId = setTimeout(() => {
         setPhase(transition('registering_sender_config', 'complete'));
         onComplete({ completed: true, skipped: false, detection: result });
       }, 1500);
     };

     registerSenderConfig();
+
+    return () => {
+      if (timeoutId) clearTimeout(timeoutId);
+    };
   }, [phase, clixProjectId, projectPath, onComplete, result]);
🤖 Prompt for AI Agents
In `@src/ui/components/FirebaseWizard.tsx` around lines 2087 - 2091, The
setTimeout in the FirebaseWizard component schedules setPhase(...) and
onComplete(...) after 1500ms but doesn't clear the timer, which can lead to
state updates after unmount; capture the timeout id when calling setTimeout
(e.g., store it in a ref or local variable tied to the component), and ensure
you call clearTimeout(timeoutId) in a useEffect cleanup or when the component is
unmounted/cancelled so that the scheduled setPhase and onComplete are not
invoked on an unmounted component.

Comment on lines +62 to +67
export function getPostFirebasePhase(context: PreparationContext): PreparationPhase {
if (context.ios.needed && !context.ios.entitlementsConfigured) {
return 'ios_setup';
}
return 'ready';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

iOS phase transitions ignore missing NSE configuration

getPostFirebasePhase and handleContinue only check entitlementsConfigured. If NSE is missing but entitlements are present, the flow skips ios_setup and jumps to ready. Consider including nseConfigured in the checks.

🔧 Proposed fix
 export function getPostFirebasePhase(context: PreparationContext): PreparationPhase {
-  if (context.ios.needed && !context.ios.entitlementsConfigured) {
+  if (
+    context.ios.needed &&
+    (!context.ios.entitlementsConfigured || !context.ios.nseConfigured)
+  ) {
     return 'ios_setup';
   }
   return 'ready';
 }
@@
     if (phase === 'firebase_check') {
       if (!context.firebase.configured && context.firebase.needed) {
         setPhase('firebase_setup');
-      } else if (context.ios.needed && !context.ios.entitlementsConfigured) {
+      } else if (
+        context.ios.needed &&
+        (!context.ios.entitlementsConfigured || !context.ios.nseConfigured)
+      ) {
         setPhase('ios_setup');
       } else {
         setPhase('ready');
       }
     } else if (phase === 'ios_check') {
-      if (!context.ios.entitlementsConfigured && context.ios.needed) {
+      if (
+        context.ios.needed &&
+        (!context.ios.entitlementsConfigured || !context.ios.nseConfigured)
+      ) {
         setPhase('ios_setup');
       } else {
         setPhase('ready');
       }

Also applies to: 404-420

🤖 Prompt for AI Agents
In `@src/ui/components/InstallPreparationUI.tsx` around lines 62 - 67,
getPostFirebasePhase currently only checks ios.entitlementsConfigured and skips
ios_setup if entitlements are present, which ignores missing NSE; update
getPostFirebasePhase (and the related handleContinue logic) to require both
ios.entitlementsConfigured and ios.nseConfigured to be true before returning
'ready' when ios.needed is true so that missing NSE keeps the flow in
'ios_setup'. Locate the PreparationContext usage in getPostFirebasePhase and the
function/method named handleContinue and change their conditional from checking
ios.entitlementsConfigured to checking (ios.entitlementsConfigured &&
ios.nseConfigured), preserving the ios.needed guard and returning 'ios_setup'
otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant