Skip to content

Commit 666e489

Browse files
committed
refactor: centralize environment variable lookups
Introduced FirebaseEnv to unify emulator configuration and project ID resolution across namespaces, replacing scattered Platform.environment lookups. Also made `projecId` not nullable and moved the `demo-test` logic into a mock setting Also refactored InternalExpression
1 parent e5c7f02 commit 666e489

File tree

12 files changed

+184
-148
lines changed

12 files changed

+184
-148
lines changed

lib/src/common/environment.dart

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2026 Firebase
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:convert';
16+
import 'dart:io';
17+
18+
import 'package:meta/meta.dart';
19+
20+
/// Provides unified access to environment variables, emulator checks, and
21+
/// Google Cloud / Firebase configuration.
22+
class FirebaseEnv {
23+
FirebaseEnv({Map<String, String>? environment})
24+
: environment = environment ?? mockEnvironment ?? Platform.environment;
25+
26+
@visibleForTesting
27+
static Map<String, String>? mockEnvironment;
28+
29+
final Map<String, String> environment;
30+
31+
/// Whether running within a Firebase emulator environment.
32+
bool get isEmulator {
33+
// Explicit functions emulator flag
34+
if (environment['FUNCTIONS_EMULATOR'] == 'true') {
35+
return true;
36+
}
37+
38+
// Generic fallback: check if any common emulator hosts are configured
39+
if (environment.containsKey('FIRESTORE_EMULATOR_HOST') ||
40+
environment.containsKey('FIREBASE_AUTH_EMULATOR_HOST') ||
41+
environment.containsKey('FIREBASE_DATABASE_EMULATOR_HOST') ||
42+
environment.containsKey('FIREBASE_STORAGE_EMULATOR_HOST')) {
43+
return true;
44+
}
45+
46+
return false;
47+
}
48+
49+
/// Timezone setting.
50+
String get tz => environment['TZ'] ?? 'UTC';
51+
52+
/// Whether debug mode is enabled.
53+
bool get debugMode => environment['FIREBASE_DEBUG_MODE'] == 'true';
54+
55+
/// Whether to skip token verification (emulator only).
56+
bool get skipTokenVerification => _getDebugFeature('skipTokenVerification');
57+
58+
/// Whether CORS is enabled (emulator only).
59+
bool get enableCors => _getDebugFeature('enableCors');
60+
61+
bool _getDebugFeature(String key) {
62+
if (environment['FIREBASE_DEBUG_FEATURES'] case final String json) {
63+
try {
64+
if (jsonDecode(json) case final Map<String, dynamic> m) {
65+
return switch (m[key]) {
66+
final bool value => value,
67+
_ => false,
68+
};
69+
}
70+
} on FormatException {
71+
// ignore
72+
}
73+
}
74+
return false;
75+
}
76+
77+
/// Returns the current Firebase project ID.
78+
///
79+
/// Checks standard environment variables in order:
80+
/// 1. FIREBASE_PROJECT
81+
/// 2. GCLOUD_PROJECT
82+
/// 3. GOOGLE_CLOUD_PROJECT
83+
/// 4. GCP_PROJECT
84+
///
85+
/// If none are set, throws [StateError].
86+
String get projectId {
87+
for (final option in _options) {
88+
final value = environment[option];
89+
if (value != null) return value;
90+
}
91+
92+
throw StateError(
93+
'No project ID found in environment. Checked: ${_options.join(', ')}',
94+
);
95+
}
96+
}
97+
98+
const _options = [
99+
'FIREBASE_PROJECT',
100+
'GCLOUD_PROJECT',
101+
'GOOGLE_CLOUD_PROJECT',
102+
'GCP_PROJECT',
103+
];

lib/src/common/params.dart

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,26 @@ class _EnumSelectParamInput<T extends Enum> extends ParamInput<List<T>> {
792792
/// These are special expressions that read from Firebase environment
793793
/// variables and are always available without being defined by the user.
794794
class InternalExpression extends Param<String> {
795-
const InternalExpression._(String name, this.getter) : super(name, null);
796-
final String Function(Map<String, String>) getter;
795+
const InternalExpression._(String name, this._configKey) : super(name, null);
796+
797+
final String _configKey;
797798

798799
@override
799-
String runtimeValue() => getter(Platform.environment);
800+
String runtimeValue() {
801+
if (Platform.environment['FIREBASE_CONFIG'] case final String config) {
802+
try {
803+
if (jsonDecode(config) case final Map<String, dynamic> m) {
804+
return switch (m[_configKey]) {
805+
final String value => value,
806+
_ => '',
807+
};
808+
}
809+
} on FormatException {
810+
// ignore
811+
}
812+
}
813+
return '';
814+
}
800815

801816
@override
802817
WireParamSpec<String> toSpec() {
@@ -820,45 +835,22 @@ sealed class ParamInput<T extends Object> {
820835

821836
// Internal Firebase expressions
822837

823-
static final databaseURL = InternalExpression._('DATABASE_URL', (env) {
824-
if (!env.containsKey('FIREBASE_CONFIG')) return '';
825-
try {
826-
final config = jsonDecode(env['FIREBASE_CONFIG']!);
827-
return config['databaseURL'] as String? ?? '';
828-
} on FormatException {
829-
return '';
830-
}
831-
});
838+
static const databaseURL = InternalExpression._(
839+
'DATABASE_URL',
840+
'databaseURL',
841+
);
832842

833-
static final projectId = InternalExpression._('PROJECT_ID', (env) {
834-
if (!env.containsKey('FIREBASE_CONFIG')) return '';
835-
try {
836-
final config = jsonDecode(env['FIREBASE_CONFIG']!);
837-
return config['projectId'] as String? ?? '';
838-
} on FormatException {
839-
return '';
840-
}
841-
});
843+
static const projectId = InternalExpression._('PROJECT_ID', 'projectId');
842844

843-
static final gcloudProject = InternalExpression._('GCLOUD_PROJECT', (env) {
844-
if (!env.containsKey('FIREBASE_CONFIG')) return '';
845-
try {
846-
final config = jsonDecode(env['FIREBASE_CONFIG']!);
847-
return config['projectId'] as String? ?? '';
848-
} on FormatException {
849-
return '';
850-
}
851-
});
845+
static const gcloudProject = InternalExpression._(
846+
'GCLOUD_PROJECT',
847+
'projectId',
848+
);
852849

853-
static final storageBucket = InternalExpression._('STORAGE_BUCKET', (env) {
854-
if (!env.containsKey('FIREBASE_CONFIG')) return '';
855-
try {
856-
final config = jsonDecode(env['FIREBASE_CONFIG']!);
857-
return config['storageBucket'] as String? ?? '';
858-
} on FormatException {
859-
return '';
860-
}
861-
});
850+
static const storageBucket = InternalExpression._(
851+
'STORAGE_BUCKET',
852+
'storageBucket',
853+
);
862854

863855
// Factory methods for creating input types
864856

lib/src/firebase.dart

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// limitations under the License.
1414

1515
import 'dart:async';
16-
import 'dart:io';
1716

1817
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
1918
import 'package:google_cloud_firestore/google_cloud_firestore.dart' as gfs;
@@ -22,6 +21,7 @@ import 'package:shelf/shelf.dart';
2221

2322
import 'alerts/alerts_namespace.dart';
2423
import 'common/cloud_run_id.dart';
24+
import 'common/environment.dart';
2525
import 'database/database_namespace.dart';
2626
import 'eventarc/eventarc_namespace.dart';
2727
import 'firestore/firestore_namespace.dart';
@@ -48,18 +48,8 @@ class Firebase {
4848

4949
/// Initialize the Firebase Admin SDK
5050
void _initializeAdminSDK() {
51-
// Get project ID from environment
52-
final projectId =
53-
Platform.environment['GCLOUD_PROJECT'] ??
54-
Platform.environment['GCP_PROJECT'] ??
55-
'demo-test'; // Fallback for emulator
56-
57-
// Check if running in emulator
58-
final firestoreEmulatorHost =
59-
Platform.environment['FIRESTORE_EMULATOR_HOST'];
60-
final isEmulator = firestoreEmulatorHost != null;
61-
62-
if (isEmulator) {
51+
final env = FirebaseEnv();
52+
if (env.isEmulator) {
6353
// TODO: Implement direct REST API calls to emulator
6454
// For now, we'll skip document fetching in emulator mode
6555
return;
@@ -71,7 +61,7 @@ class Firebase {
7161
_adminApp = FirebaseApp.initializeApp(
7262
options: AppOptions(
7363
credential: Credential.fromApplicationDefaultCredentials(),
74-
projectId: projectId,
64+
projectId: env.projectId,
7565
),
7666
);
7767

lib/src/https/https_namespace.dart

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,18 @@
1414

1515
import 'dart:async';
1616
import 'dart:convert';
17-
import 'dart:io';
1817

1918
import 'package:meta/meta.dart';
2019
import 'package:shelf/shelf.dart';
2120

21+
import '../common/environment.dart';
2222
import '../common/utilities.dart';
2323
import '../firebase.dart';
2424
import 'auth.dart';
2525
import 'callable.dart';
2626
import 'error.dart';
2727
import 'options.dart';
2828

29-
/// Checks if token verification should be skipped (emulator mode).
30-
bool _shouldSkipTokenVerification() {
31-
final debugFeatures = Platform.environment['FIREBASE_DEBUG_FEATURES'];
32-
if (debugFeatures == null) {
33-
return false;
34-
}
35-
try {
36-
final features = jsonDecode(debugFeatures);
37-
return features['skipTokenVerification'] as bool? ?? false;
38-
} catch (_) {
39-
return false;
40-
}
41-
}
42-
4329
/// HTTPS triggers namespace.
4430
///
4531
/// Provides methods to define HTTP-triggered Cloud Functions.
@@ -115,7 +101,7 @@ class HttpsNamespace extends FunctionsNamespace {
115101
}
116102

117103
// Extract auth and app check tokens
118-
final skipVerification = _shouldSkipTokenVerification();
104+
final skipVerification = FirebaseEnv().skipTokenVerification;
119105
final tokens = await checkTokens(
120106
request,
121107
skipTokenVerification: skipVerification,
@@ -195,7 +181,7 @@ class HttpsNamespace extends FunctionsNamespace {
195181
final body = await request.json as Map<String, dynamic>?;
196182

197183
// Extract auth and app check tokens
198-
final skipVerification = _shouldSkipTokenVerification();
184+
final skipVerification = FirebaseEnv().skipTokenVerification;
199185
final tokens = await checkTokens(
200186
request,
201187
skipTokenVerification: skipVerification,

lib/src/identity/identity_namespace.dart

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ library;
1717

1818
import 'dart:async';
1919
import 'dart:convert';
20-
import 'dart:io';
2120

2221
import 'package:meta/meta.dart';
2322
import 'package:shelf/shelf.dart';
2423

24+
import '../common/environment.dart';
2525
import '../common/utilities.dart';
2626
import '../firebase.dart';
2727
import '../https/error.dart';
@@ -277,44 +277,25 @@ class IdentityNamespace extends FunctionsNamespace {
277277
/// skipTokenVerification debug feature is enabled), verification is skipped.
278278
Future<Map<String, dynamic>> _decodeAndVerifyJwt(String jwt) async {
279279
// Get environment configuration
280-
final env = Platform.environment;
281-
final isEmulator = env['FUNCTIONS_EMULATOR'] == 'true';
282-
final skipVerification = _shouldSkipTokenVerification(env);
280+
final env = FirebaseEnv();
283281

284282
// Get project ID
285-
final projectId =
286-
env['GCLOUD_PROJECT'] ??
287-
env['GCP_PROJECT'] ??
288-
env['FIREBASE_PROJECT'] ??
289-
'demo-test';
283+
final projectId = env.projectId;
290284

291285
// Create verifier
292286
final verifier = AuthBlockingTokenVerifier(
293287
projectId: projectId,
294-
isEmulator: isEmulator || skipVerification,
288+
isEmulator: env.isEmulator || env.skipTokenVerification,
295289
);
296290

297291
// Determine audience based on platform
298292
// Cloud Run uses "run.app", GCF v1 uses default
299-
final kService = env['K_SERVICE']; // Cloud Run service name
293+
final kService = env.environment['K_SERVICE']; // Cloud Run service name
300294
final audience = kService != null ? 'run.app' : null;
301295

302296
return verifier.verifyToken(jwt, audience: audience);
303297
}
304298

305-
/// Checks if token verification should be skipped based on debug features.
306-
bool _shouldSkipTokenVerification(Map<String, String> env) {
307-
final debugFeatures = env['FIREBASE_DEBUG_FEATURES'];
308-
if (debugFeatures == null) return false;
309-
310-
try {
311-
final features = jsonDecode(debugFeatures) as Map<String, dynamic>;
312-
return features['skipTokenVerification'] as bool? ?? false;
313-
} on FormatException {
314-
return false;
315-
}
316-
}
317-
318299
/// Validates the auth response for invalid claims.
319300
void _validateAuthResponse(
320301
AuthBlockingEventType eventType,

lib/src/logger/logger.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'dart:async';
1616
import 'dart:convert';
1717
import 'dart:io' as io;
1818

19+
import '../common/environment.dart';
20+
1921
/// Log severity levels for Cloud Logging.
2022
///
2123
/// See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity).
@@ -177,11 +179,9 @@ class Logger {
177179
// Add trace context if available.
178180
final traceId = Zone.current[traceIdKey] as String?;
179181
if (traceId != null) {
180-
final project = io.Platform.environment['GCLOUD_PROJECT'];
181-
if (project != null) {
182-
entry['logging.googleapis.com/trace'] =
183-
'projects/$project/traces/$traceId';
184-
}
182+
final project = FirebaseEnv().projectId;
183+
entry['logging.googleapis.com/trace'] =
184+
'projects/$project/traces/$traceId';
185185
}
186186

187187
final sanitized = removeCircular(entry);

0 commit comments

Comments
 (0)