@kylewadegrove/cutline-mcp-cli 0.2.0 → 0.3.0

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.
@@ -1,4 +1,5 @@
1
1
  export declare function loginCommand(options: {
2
2
  staging?: boolean;
3
3
  signup?: boolean;
4
+ email?: string;
4
5
  }): Promise<void>;
@@ -41,15 +41,25 @@ async function loginCommand(options) {
41
41
  if (options.staging) {
42
42
  console.log(chalk_1.default.yellow(' ⚠️ Using STAGING environment\n'));
43
43
  }
44
- if (!config.FIREBASE_API_KEY) {
45
- console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
46
- console.error(chalk_1.default.gray('Please set it before running this command:'));
47
- console.error(chalk_1.default.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
48
- console.error(chalk_1.default.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
49
- process.exit(1);
44
+ if (options.email) {
45
+ console.log(chalk_1.default.gray(` Requesting sign-in as: ${options.email}\n`));
50
46
  }
51
47
  const spinner = (0, ora_1.default)('Starting authentication flow...').start();
52
48
  try {
49
+ // Fetch Firebase API key from web app endpoint
50
+ spinner.text = 'Fetching configuration...';
51
+ let firebaseApiKey;
52
+ try {
53
+ firebaseApiKey = await (0, config_js_1.fetchFirebaseApiKey)(options);
54
+ }
55
+ catch (error) {
56
+ spinner.fail(chalk_1.default.red('Failed to fetch Firebase configuration'));
57
+ if (error instanceof Error) {
58
+ console.error(chalk_1.default.red(` ${error.message}`));
59
+ }
60
+ console.error(chalk_1.default.gray('\n You can also set the FIREBASE_API_KEY environment variable manually.\n'));
61
+ process.exit(1);
62
+ }
53
63
  // Start callback server
54
64
  spinner.text = 'Waiting for authentication...';
55
65
  const serverPromise = (0, callback_js_1.startCallbackServer)();
@@ -58,6 +68,9 @@ async function loginCommand(options) {
58
68
  if (options.signup) {
59
69
  authUrl += '&mode=signup';
60
70
  }
71
+ if (options.email) {
72
+ authUrl += `&email=${encodeURIComponent(options.email)}`;
73
+ }
61
74
  await (0, open_1.default)(authUrl);
62
75
  spinner.text = options.signup
63
76
  ? 'Browser opened - please create your account'
@@ -66,7 +79,7 @@ async function loginCommand(options) {
66
79
  const result = await serverPromise;
67
80
  // Exchange custom token for refresh token
68
81
  spinner.text = 'Exchanging token...';
69
- const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
82
+ const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
70
83
  // Store refresh token
71
84
  try {
72
85
  await (0, keychain_js_1.storeRefreshToken)(refreshToken);
@@ -74,12 +87,11 @@ async function loginCommand(options) {
74
87
  catch (error) {
75
88
  console.warn(chalk_1.default.yellow(' ⚠️ Could not save to Keychain (skipping)'));
76
89
  }
77
- // Always save to file config (cross-platform)
90
+ // Save to file config (cross-platform) - don't store API key anymore
78
91
  try {
79
92
  (0, config_store_js_1.saveConfig)({
80
93
  refreshToken,
81
94
  environment: options.staging ? 'staging' : 'production',
82
- firebaseApiKey: config.FIREBASE_API_KEY
83
95
  });
84
96
  }
85
97
  catch (error) {
@@ -44,16 +44,20 @@ async function statusCommand(options) {
44
44
  console.log(chalk_1.default.gray(' Run'), chalk_1.default.cyan('cutline-mcp login'), chalk_1.default.gray('to authenticate\n'));
45
45
  return;
46
46
  }
47
- // Get config for API key
48
- const config = (0, config_js_1.getConfig)(options);
49
- if (!config.FIREBASE_API_KEY) {
47
+ // Get Firebase API key
48
+ spinner.text = 'Fetching configuration...';
49
+ let firebaseApiKey;
50
+ try {
51
+ firebaseApiKey = await (0, config_js_1.fetchFirebaseApiKey)(options);
52
+ }
53
+ catch (error) {
50
54
  spinner.fail(chalk_1.default.red('Configuration error'));
51
- console.error(chalk_1.default.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
55
+ console.error(chalk_1.default.red(` ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
52
56
  process.exit(1);
53
57
  }
54
58
  // Exchange refresh token for ID token
55
59
  spinner.text = 'Verifying credentials...';
56
- const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
60
+ const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
57
61
  // Initialize Firebase Admin with correct project ID
58
62
  if (firebase_admin_1.default.apps.length === 0) {
59
63
  const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
@@ -36,8 +36,13 @@ async function upgradeCommand(options) {
36
36
  if (options.staging) {
37
37
  console.log(chalk_1.default.yellow(' ⚠️ Using STAGING environment\n'));
38
38
  }
39
- if (!config.FIREBASE_API_KEY) {
40
- console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY environment variable is required.`));
39
+ // Fetch Firebase API key
40
+ let firebaseApiKey;
41
+ try {
42
+ firebaseApiKey = await (0, config_js_1.fetchFirebaseApiKey)(options);
43
+ }
44
+ catch (error) {
45
+ console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
41
46
  process.exit(1);
42
47
  }
43
48
  // Determine upgrade URL based on environment
@@ -59,7 +64,7 @@ async function upgradeCommand(options) {
59
64
  const result = await serverPromise;
60
65
  // Exchange custom token for refresh token
61
66
  spinner.text = 'Refreshing your session...';
62
- const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
67
+ const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
63
68
  // Store refresh token
64
69
  try {
65
70
  await (0, keychain_js_1.storeRefreshToken)(refreshToken);
@@ -67,12 +72,11 @@ async function upgradeCommand(options) {
67
72
  catch (error) {
68
73
  console.warn(chalk_1.default.yellow(' ⚠️ Could not save to Keychain (skipping)'));
69
74
  }
70
- // Save to file config
75
+ // Save to file config (API key is fetched at runtime, not stored)
71
76
  try {
72
77
  (0, config_store_js_1.saveConfig)({
73
78
  refreshToken,
74
79
  environment: options.staging ? 'staging' : 'production',
75
- firebaseApiKey: config.FIREBASE_API_KEY
76
80
  });
77
81
  }
78
82
  catch (error) {
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ program
16
16
  .description('Authenticate with Cutline and store credentials')
17
17
  .option('--staging', 'Use staging environment')
18
18
  .option('--signup', 'Open sign-up page instead of sign-in')
19
+ .option('--email <address>', 'Request sign-in with specific email address')
19
20
  .action(login_js_1.loginCommand);
20
21
  program
21
22
  .command('logout')
@@ -1,7 +1,6 @@
1
1
  export interface McpConfig {
2
2
  refreshToken?: string;
3
3
  environment?: 'production' | 'staging';
4
- firebaseApiKey?: string;
5
4
  }
6
5
  export declare function saveConfig(config: McpConfig): void;
7
6
  export declare function loadConfig(): McpConfig;
@@ -1,8 +1,22 @@
1
1
  export interface Config {
2
2
  AUTH_URL: string;
3
3
  CALLBACK_URL: string;
4
+ BASE_URL: string;
5
+ }
6
+ export interface ConfigWithApiKey extends Config {
4
7
  FIREBASE_API_KEY: string;
5
8
  }
9
+ /**
10
+ * Get static config (URLs only, no API key)
11
+ */
6
12
  export declare function getConfig(options?: {
7
13
  staging?: boolean;
8
14
  }): Config;
15
+ /**
16
+ * Fetch Firebase API key from the web app's public endpoint.
17
+ * This avoids hardcoding API keys in source code.
18
+ * Falls back to environment variables if fetch fails.
19
+ */
20
+ export declare function fetchFirebaseApiKey(options?: {
21
+ staging?: boolean;
22
+ }): Promise<string>;
@@ -1,21 +1,52 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getConfig = getConfig;
4
+ exports.fetchFirebaseApiKey = fetchFirebaseApiKey;
5
+ /**
6
+ * Get static config (URLs only, no API key)
7
+ */
4
8
  function getConfig(options = {}) {
5
9
  if (options.staging) {
6
10
  return {
7
11
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
8
12
  CALLBACK_URL: 'http://localhost:8765',
9
- // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
10
- // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
11
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
13
+ BASE_URL: 'https://cutline-staging.web.app',
12
14
  };
13
15
  }
14
16
  return {
15
17
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
16
18
  CALLBACK_URL: 'http://localhost:8765',
17
- // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
18
- // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
19
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
19
+ BASE_URL: 'https://thecutline.ai',
20
20
  };
21
21
  }
22
+ /**
23
+ * Fetch Firebase API key from the web app's public endpoint.
24
+ * This avoids hardcoding API keys in source code.
25
+ * Falls back to environment variables if fetch fails.
26
+ */
27
+ async function fetchFirebaseApiKey(options = {}) {
28
+ // First check environment variables
29
+ const envKey = process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
30
+ if (envKey) {
31
+ return envKey;
32
+ }
33
+ // Fetch from web app endpoint
34
+ const config = getConfig(options);
35
+ try {
36
+ const response = await fetch(`${config.BASE_URL}/api/firebase-config`, {
37
+ headers: { 'Accept': 'application/json' },
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(`HTTP ${response.status}`);
41
+ }
42
+ const data = await response.json();
43
+ if (!data.apiKey) {
44
+ throw new Error('No apiKey in response');
45
+ }
46
+ return data.apiKey;
47
+ }
48
+ catch (error) {
49
+ throw new Error(`Failed to fetch Firebase config from ${config.BASE_URL}/api/firebase-config. ` +
50
+ `Please set FIREBASE_API_KEY environment variable or ensure network connectivity.`);
51
+ }
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylewadegrove/cutline-mcp-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for authenticating with Cutline MCP servers",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import { startCallbackServer } from '../auth/callback.js';
5
5
  import { storeRefreshToken } from '../auth/keychain.js';
6
6
  import { saveConfig } from '../utils/config-store.js';
7
- import { getConfig } from '../utils/config.js';
7
+ import { getConfig, fetchFirebaseApiKey } from '../utils/config.js';
8
8
 
9
9
  async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
10
10
  const response = await fetch(
@@ -31,7 +31,7 @@ async function exchangeCustomToken(customToken: string, apiKey: string): Promise
31
31
  };
32
32
  }
33
33
 
34
- export async function loginCommand(options: { staging?: boolean; signup?: boolean }) {
34
+ export async function loginCommand(options: { staging?: boolean; signup?: boolean; email?: string }) {
35
35
  const config = getConfig(options);
36
36
 
37
37
  if (options.signup) {
@@ -44,17 +44,28 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
44
44
  console.log(chalk.yellow(' ⚠️ Using STAGING environment\n'));
45
45
  }
46
46
 
47
- if (!config.FIREBASE_API_KEY) {
48
- console.error(chalk.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
49
- console.error(chalk.gray('Please set it before running this command:'));
50
- console.error(chalk.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
51
- console.error(chalk.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
52
- process.exit(1);
47
+ if (options.email) {
48
+ console.log(chalk.gray(` Requesting sign-in as: ${options.email}\n`));
53
49
  }
54
50
 
51
+
55
52
  const spinner = ora('Starting authentication flow...').start();
56
53
 
57
54
  try {
55
+ // Fetch Firebase API key from web app endpoint
56
+ spinner.text = 'Fetching configuration...';
57
+ let firebaseApiKey: string;
58
+ try {
59
+ firebaseApiKey = await fetchFirebaseApiKey(options);
60
+ } catch (error) {
61
+ spinner.fail(chalk.red('Failed to fetch Firebase configuration'));
62
+ if (error instanceof Error) {
63
+ console.error(chalk.red(` ${error.message}`));
64
+ }
65
+ console.error(chalk.gray('\n You can also set the FIREBASE_API_KEY environment variable manually.\n'));
66
+ process.exit(1);
67
+ }
68
+
58
69
  // Start callback server
59
70
  spinner.text = 'Waiting for authentication...';
60
71
  const serverPromise = startCallbackServer();
@@ -64,6 +75,9 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
64
75
  if (options.signup) {
65
76
  authUrl += '&mode=signup';
66
77
  }
78
+ if (options.email) {
79
+ authUrl += `&email=${encodeURIComponent(options.email)}`;
80
+ }
67
81
  await open(authUrl);
68
82
  spinner.text = options.signup
69
83
  ? 'Browser opened - please create your account'
@@ -74,7 +88,7 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
74
88
 
75
89
  // Exchange custom token for refresh token
76
90
  spinner.text = 'Exchanging token...';
77
- const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
91
+ const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
78
92
 
79
93
  // Store refresh token
80
94
  try {
@@ -83,12 +97,11 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
83
97
  console.warn(chalk.yellow(' ⚠️ Could not save to Keychain (skipping)'));
84
98
  }
85
99
 
86
- // Always save to file config (cross-platform)
100
+ // Save to file config (cross-platform) - don't store API key anymore
87
101
  try {
88
102
  saveConfig({
89
103
  refreshToken,
90
104
  environment: options.staging ? 'staging' : 'production',
91
- firebaseApiKey: config.FIREBASE_API_KEY
92
105
  });
93
106
  } catch (error) {
94
107
  console.error(chalk.red(' ✗ Failed to save config file:'), error);
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import admin from 'firebase-admin';
4
4
  import { getRefreshToken } from '../auth/keychain.js';
5
- import { getConfig } from '../utils/config.js';
5
+ import { fetchFirebaseApiKey } from '../utils/config.js';
6
6
 
7
7
  async function exchangeRefreshToken(refreshToken: string, apiKey: string): Promise<string> {
8
8
  const response = await fetch(
@@ -48,18 +48,20 @@ export async function statusCommand(options: { staging?: boolean }) {
48
48
  return;
49
49
  }
50
50
 
51
- // Get config for API key
52
- const config = getConfig(options);
53
-
54
- if (!config.FIREBASE_API_KEY) {
51
+ // Get Firebase API key
52
+ spinner.text = 'Fetching configuration...';
53
+ let firebaseApiKey: string;
54
+ try {
55
+ firebaseApiKey = await fetchFirebaseApiKey(options);
56
+ } catch (error) {
55
57
  spinner.fail(chalk.red('Configuration error'));
56
- console.error(chalk.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
58
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
57
59
  process.exit(1);
58
60
  }
59
61
 
60
62
  // Exchange refresh token for ID token
61
63
  spinner.text = 'Verifying credentials...';
62
- const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
64
+ const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
63
65
 
64
66
  // Initialize Firebase Admin with correct project ID
65
67
  if (admin.apps.length === 0) {
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import { startCallbackServer } from '../auth/callback.js';
5
5
  import { storeRefreshToken } from '../auth/keychain.js';
6
6
  import { saveConfig } from '../utils/config-store.js';
7
- import { getConfig } from '../utils/config.js';
7
+ import { getConfig, fetchFirebaseApiKey } from '../utils/config.js';
8
8
 
9
9
  async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
10
10
  const response = await fetch(
@@ -40,8 +40,12 @@ export async function upgradeCommand(options: { staging?: boolean }) {
40
40
  console.log(chalk.yellow(' ⚠️ Using STAGING environment\n'));
41
41
  }
42
42
 
43
- if (!config.FIREBASE_API_KEY) {
44
- console.error(chalk.red(`Error: FIREBASE_API_KEY environment variable is required.`));
43
+ // Fetch Firebase API key
44
+ let firebaseApiKey: string;
45
+ try {
46
+ firebaseApiKey = await fetchFirebaseApiKey(options);
47
+ } catch (error) {
48
+ console.error(chalk.red(`Error: ${error instanceof Error ? error.message : 'Failed to get Firebase API key'}`));
45
49
  process.exit(1);
46
50
  }
47
51
 
@@ -71,7 +75,7 @@ export async function upgradeCommand(options: { staging?: boolean }) {
71
75
 
72
76
  // Exchange custom token for refresh token
73
77
  spinner.text = 'Refreshing your session...';
74
- const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
78
+ const { refreshToken, email } = await exchangeCustomToken(result.token, firebaseApiKey);
75
79
 
76
80
  // Store refresh token
77
81
  try {
@@ -80,12 +84,11 @@ export async function upgradeCommand(options: { staging?: boolean }) {
80
84
  console.warn(chalk.yellow(' ⚠️ Could not save to Keychain (skipping)'));
81
85
  }
82
86
 
83
- // Save to file config
87
+ // Save to file config (API key is fetched at runtime, not stored)
84
88
  try {
85
89
  saveConfig({
86
90
  refreshToken,
87
91
  environment: options.staging ? 'staging' : 'production',
88
- firebaseApiKey: config.FIREBASE_API_KEY
89
92
  });
90
93
  } catch (error) {
91
94
  console.error(chalk.red(' ✗ Failed to save config file:'), error);
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ program
17
17
  .description('Authenticate with Cutline and store credentials')
18
18
  .option('--staging', 'Use staging environment')
19
19
  .option('--signup', 'Open sign-up page instead of sign-in')
20
+ .option('--email <address>', 'Request sign-in with specific email address')
20
21
  .action(loginCommand);
21
22
 
22
23
  program
@@ -9,7 +9,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
  export interface McpConfig {
10
10
  refreshToken?: string;
11
11
  environment?: 'production' | 'staging';
12
- firebaseApiKey?: string; // Store the API key used during login for reference
12
+ // Note: firebaseApiKey is no longer stored - it's fetched from the web app at runtime
13
13
  }
14
14
 
15
15
  function ensureConfigDir() {
@@ -1,25 +1,65 @@
1
1
  export interface Config {
2
2
  AUTH_URL: string;
3
3
  CALLBACK_URL: string;
4
+ BASE_URL: string;
5
+ }
6
+
7
+ export interface ConfigWithApiKey extends Config {
4
8
  FIREBASE_API_KEY: string;
5
9
  }
6
10
 
11
+ /**
12
+ * Get static config (URLs only, no API key)
13
+ */
7
14
  export function getConfig(options: { staging?: boolean } = {}): Config {
8
15
  if (options.staging) {
9
16
  return {
10
17
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
11
18
  CALLBACK_URL: 'http://localhost:8765',
12
- // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
13
- // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
14
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
19
+ BASE_URL: 'https://cutline-staging.web.app',
15
20
  };
16
21
  }
17
22
 
18
23
  return {
19
24
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
20
25
  CALLBACK_URL: 'http://localhost:8765',
21
- // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
22
- // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
23
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
26
+ BASE_URL: 'https://thecutline.ai',
24
27
  };
25
28
  }
29
+
30
+ /**
31
+ * Fetch Firebase API key from the web app's public endpoint.
32
+ * This avoids hardcoding API keys in source code.
33
+ * Falls back to environment variables if fetch fails.
34
+ */
35
+ export async function fetchFirebaseApiKey(options: { staging?: boolean } = {}): Promise<string> {
36
+ // First check environment variables
37
+ const envKey = process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY;
38
+ if (envKey) {
39
+ return envKey;
40
+ }
41
+
42
+ // Fetch from web app endpoint
43
+ const config = getConfig(options);
44
+ try {
45
+ const response = await fetch(`${config.BASE_URL}/api/firebase-config`, {
46
+ headers: { 'Accept': 'application/json' },
47
+ });
48
+
49
+ if (!response.ok) {
50
+ throw new Error(`HTTP ${response.status}`);
51
+ }
52
+
53
+ const data = await response.json();
54
+ if (!data.apiKey) {
55
+ throw new Error('No apiKey in response');
56
+ }
57
+
58
+ return data.apiKey;
59
+ } catch (error) {
60
+ throw new Error(
61
+ `Failed to fetch Firebase config from ${config.BASE_URL}/api/firebase-config. ` +
62
+ `Please set FIREBASE_API_KEY environment variable or ensure network connectivity.`
63
+ );
64
+ }
65
+ }