forge-remote 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-remote",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Desktop relay for Forge Remote — monitor and control Claude Code sessions from your phone",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -80,6 +80,47 @@ export async function getAccessToken() {
80
80
  return data.access_token;
81
81
  }
82
82
 
83
+ // ─── Web App SDK Config ─────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Fetch the Firebase web app SDK config via REST API.
87
+ * This bypasses the `firebase apps:sdkconfig` CLI command, which sometimes
88
+ * returns 403 on newly created projects.
89
+ *
90
+ * Returns an object with apiKey, authDomain, projectId, storageBucket,
91
+ * messagingSenderId, and appId — or throws on failure.
92
+ */
93
+ export async function getWebAppConfig(projectId, appId) {
94
+ const token = await getAccessToken();
95
+
96
+ const response = await fetch(
97
+ `https://firebase.googleapis.com/v1beta1/projects/${projectId}/webApps/${appId}/config`,
98
+ {
99
+ headers: {
100
+ Authorization: `Bearer ${token}`,
101
+ "X-Goog-User-Project": projectId,
102
+ },
103
+ },
104
+ );
105
+
106
+ if (!response.ok) {
107
+ const err = await response.json().catch(() => ({}));
108
+ throw new Error(
109
+ `Failed to fetch web app config: ${err.error?.message || response.statusText}`,
110
+ );
111
+ }
112
+
113
+ const data = await response.json();
114
+ return {
115
+ apiKey: data.apiKey || null,
116
+ authDomain: data.authDomain || null,
117
+ projectId: data.projectId || null,
118
+ storageBucket: data.storageBucket || null,
119
+ messagingSenderId: data.messagingSenderId || null,
120
+ appId: data.appId || null,
121
+ };
122
+ }
123
+
83
124
  // ─── Anonymous Authentication ───────────────────────────────────────────────
84
125
 
85
126
  /**
package/src/init.js CHANGED
@@ -15,6 +15,7 @@ import chalk from "chalk";
15
15
 
16
16
  import {
17
17
  readRefreshToken,
18
+ getWebAppConfig,
18
19
  enableAnonymousAuth,
19
20
  enableApi,
20
21
  findFirebaseAdminSdkAccount,
@@ -426,20 +427,17 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
426
427
 
427
428
  console.log(chalk.green(` ✓ App ID: ${appId}`));
428
429
 
429
- // Get SDK config (with retry newly created apps may take a few seconds to propagate).
430
+ // Get SDK config via REST API (bypasses flaky `firebase apps:sdkconfig` CLI).
430
431
  let sdkConfig = {};
431
432
 
432
433
  try {
433
- let sdkOutput = null;
434
+ // Primary method: REST API with retry for newly created apps.
434
435
  const maxSdkRetries = 4;
435
436
  const retryDelayMs = 5000; // 5 seconds between retries.
436
437
 
437
438
  for (let attempt = 1; attempt <= maxSdkRetries; attempt++) {
438
439
  try {
439
- sdkOutput = runOrFail(
440
- ["firebase", "apps:sdkconfig", "web", appId, "--project", projectId],
441
- "Failed to get SDK config.",
442
- );
440
+ sdkConfig = await getWebAppConfig(projectId, appId);
443
441
  break; // Success — exit retry loop.
444
442
  } catch (e) {
445
443
  if (attempt < maxSdkRetries) {
@@ -451,23 +449,23 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
451
449
  );
452
450
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
453
451
  } else {
454
- // All retries exhausted — throw with helpful instructions.
455
- const fallbackErr = new Error(
456
- `Could not retrieve SDK config after ${maxSdkRetries} attempts.\n\n` +
457
- chalk.bold(
458
- " This usually means the Firebase web app hasn't fully propagated yet.\n",
459
- ) +
452
+ throw new Error(
453
+ `Could not retrieve SDK config after ${maxSdkRetries} attempts.\n` +
454
+ ` Last error: ${e.message}\n\n` +
460
455
  chalk.bold(" Try these steps:\n\n") +
461
456
  chalk.cyan(
462
457
  ` 1. Wait 30 seconds, then re-run: forge-remote init\n`,
463
458
  ) +
464
- chalk.cyan(` 2. Or run manually:\n`) +
459
+ chalk.cyan(
460
+ ` 2. Or get the config from the Firebase Console:\n`,
461
+ ) +
465
462
  chalk.dim(
466
- ` firebase apps:sdkconfig web ${appId} --project ${projectId}\n`,
463
+ ` https://console.firebase.google.com/project/${projectId}/settings/general\n`,
467
464
  ) +
468
465
  chalk.dim(
469
- ` Then copy the config into: ~/.forge-remote/sdk-config.json\n`,
466
+ ` Scroll to "Your apps" Web app → Config snippet\n`,
470
467
  ) +
468
+ chalk.dim(` Save it to: ~/.forge-remote/sdk-config.json\n`) +
471
469
  chalk.dim(
472
470
  ` Format: { "apiKey": "...", "authDomain": "...", "projectId": "...",\n`,
473
471
  ) +
@@ -475,46 +473,12 @@ export async function runInit({ projectId: overrideProjectId } = {}) {
475
473
  ` "storageBucket": "...", "messagingSenderId": "...", "appId": "..." }\n`,
476
474
  ),
477
475
  );
478
- throw fallbackErr;
479
476
  }
480
477
  }
481
478
  }
482
479
 
483
- // Parse the config — output may be JSON or JS object.
484
- const jsonMatch = sdkOutput.match(/\{[\s\S]*\}/);
485
- if (jsonMatch) {
486
- try {
487
- const parsed = JSON.parse(jsonMatch[0]);
488
- sdkConfig = {
489
- apiKey: parsed.apiKey || null,
490
- authDomain: parsed.authDomain || null,
491
- projectId: parsed.projectId || null,
492
- storageBucket: parsed.storageBucket || null,
493
- messagingSenderId: parsed.messagingSenderId || null,
494
- appId: parsed.appId || null,
495
- };
496
- } catch {
497
- // Not valid JSON — fall back to regex.
498
- const parseField = (field) => {
499
- const regex = new RegExp(`"?${field}"?:\\s*"([^"]+)"`);
500
- const match = sdkOutput.match(regex);
501
- return match ? match[1] : null;
502
- };
503
- sdkConfig = {
504
- apiKey: parseField("apiKey"),
505
- authDomain: parseField("authDomain"),
506
- projectId: parseField("projectId"),
507
- storageBucket: parseField("storageBucket"),
508
- messagingSenderId: parseField("messagingSenderId"),
509
- appId: parseField("appId"),
510
- };
511
- }
512
- }
513
-
514
480
  if (!sdkConfig.apiKey || !sdkConfig.projectId) {
515
- throw new Error(
516
- "Could not parse apiKey or projectId from SDK config output.",
517
- );
481
+ throw new Error("SDK config is missing apiKey or projectId.");
518
482
  }
519
483
 
520
484
  console.log(chalk.green(" ✓ SDK config retrieved"));