Skip to content

Commit e9f8e4d

Browse files
committed
2 parents 78b9be4 + e16c808 commit e9f8e4d

17 files changed

+370
-184
lines changed

dart_test.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md
2+
tags:
3+
e2e:
4+
# Designates integration tests that require the Firebase Local Emulator Suite.
5+
# These tests spin up external processes (like Node.js and Java emulators)
6+
# and execute full end-to-end request pipelines.
7+
timeout: 5x

lib/logger.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@
4949
/// - **stderr**: WARNING, ERROR, CRITICAL, ALERT, EMERGENCY
5050
library;
5151

52-
export 'src/logger/logger.dart';
52+
export 'src/logger/logger.dart'
53+
hide createLogger, projectIdZoneKey, traceIdZoneKey;

lib/src/builder/manifest.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,11 @@ List<Map<String, String>> _buildRequiredAPIs(
128128
}
129129

130130
/// The base image URI template for Cloud Run deployment.
131+
///
131132
/// The region prefix is substituted at generation time.
133+
/// See: https://cloud.google.com/run/docs/runtime-support#support_schedule
132134
const _baseImageUriSuffix =
133-
'-docker.pkg.dev/serverless-runtimes/google-24/osonly24';
135+
'-docker.pkg.dev/serverless-runtimes/google-24/runtimes/osonly24';
134136

135137
/// Builds a single endpoint entry as a map.
136138
Map<String, dynamic> _buildEndpointMap(EndpointSpec endpoint) {

lib/src/common/environment.dart

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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() : environment = mockEnvironment ?? Platform.environment;
24+
25+
@visibleForTesting
26+
static Map<String, String>? mockEnvironment;
27+
28+
final Map<String, String> environment;
29+
30+
/// Whether running within a Firebase emulator environment.
31+
bool get isEmulator {
32+
// Explicit functions emulator flag
33+
return environment['FUNCTIONS_EMULATOR'] == 'true' ||
34+
// Generic fallback: check if any common emulator hosts are configured
35+
_emulatorHostKeys.any(environment.containsKey);
36+
}
37+
38+
/// Timezone setting.
39+
String get tz => environment['TZ'] ?? 'UTC';
40+
41+
/// Whether debug mode is enabled.
42+
bool get debugMode => environment['FIREBASE_DEBUG_MODE'] == 'true';
43+
44+
/// Whether to skip token verification (emulator only).
45+
bool get skipTokenVerification => _getDebugFeature('skipTokenVerification');
46+
47+
/// Whether CORS is enabled (emulator only).
48+
bool get enableCors => _getDebugFeature('enableCors');
49+
50+
bool _getDebugFeature(String key) {
51+
if (environment['FIREBASE_DEBUG_FEATURES'] case final String json) {
52+
try {
53+
if (jsonDecode(json) case final Map<String, dynamic> m) {
54+
return switch (m[key]) {
55+
final bool value => value,
56+
_ => false,
57+
};
58+
}
59+
} on FormatException {
60+
// ignore
61+
}
62+
}
63+
return false;
64+
}
65+
66+
/// Returns the current Firebase project ID.
67+
///
68+
/// Checks standard environment variables in order:
69+
/// 1. FIREBASE_PROJECT
70+
/// 2. GCLOUD_PROJECT
71+
/// 3. GOOGLE_CLOUD_PROJECT
72+
/// 4. GCP_PROJECT
73+
///
74+
/// If none are set, throws [StateError].
75+
String get projectId {
76+
for (final option in _projectIdEnvKeyOptions) {
77+
final value = environment[option];
78+
if (value != null && value.isNotEmpty) return value;
79+
}
80+
81+
throw StateError(
82+
'No project ID found in environment. Checked: ${_projectIdEnvKeyOptions.join(', ')}',
83+
);
84+
}
85+
86+
/// The port to listen on.
87+
///
88+
/// Uses the [PORT] environment variable, defaulting to 8080.
89+
int get port => int.tryParse(environment['PORT'] ?? '8080') ?? 8080;
90+
91+
/// The name of the Cloud Run service.
92+
///
93+
/// Uses the `K_SERVICE` environment variable.
94+
///
95+
/// See https://cloud.google.com/run/docs/container-contract#env-vars
96+
String? get kService => environment['K_SERVICE'];
97+
98+
/// The name of the target function.
99+
///
100+
/// Uses the `FUNCTION_TARGET` environment variable.
101+
///
102+
/// See https://docs.cloud.google.com/run/docs/configuring/services/environment-variables#additional_reserved_environment_variables_when_deploying_functions
103+
String? get functionTarget => environment['FUNCTION_TARGET'];
104+
105+
/// Whether the functions control API is enabled.
106+
///
107+
/// Uses the `FUNCTIONS_CONTROL_API` environment variable.
108+
///
109+
/// This is part of the contract with `firebase-tools`.
110+
bool get functionsControlApi =>
111+
environment['FUNCTIONS_CONTROL_API'] == 'true';
112+
}
113+
114+
/// Common project ID environment variables checked in order.
115+
const _projectIdEnvKeyOptions = [
116+
'FIREBASE_PROJECT',
117+
'GCLOUD_PROJECT',
118+
'GOOGLE_CLOUD_PROJECT',
119+
'GCP_PROJECT',
120+
];
121+
122+
/// Common emulator host keys used to detect emulator environment.
123+
const _emulatorHostKeys = [
124+
'FIRESTORE_EMULATOR_HOST',
125+
'FIREBASE_AUTH_EMULATOR_HOST',
126+
'FIREBASE_DATABASE_EMULATOR_HOST',
127+
'FIREBASE_STORAGE_EMULATOR_HOST',
128+
];

lib/src/common/params.dart

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -792,11 +792,25 @@ 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> map) {
804+
if (map[_configKey] case final String value) {
805+
return value;
806+
}
807+
}
808+
} on FormatException {
809+
// ignore
810+
}
811+
}
812+
return '';
813+
}
800814

801815
@override
802816
WireParamSpec<String> toSpec() {
@@ -820,45 +834,22 @@ sealed class ParamInput<T extends Object> {
820834

821835
// Internal Firebase expressions
822836

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-
});
837+
static const databaseURL = InternalExpression._(
838+
'DATABASE_URL',
839+
'databaseURL',
840+
);
832841

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-
});
842+
static const projectId = InternalExpression._('PROJECT_ID', 'projectId');
842843

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-
});
844+
static const gcloudProject = InternalExpression._(
845+
'GCLOUD_PROJECT',
846+
'projectId',
847+
);
852848

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-
});
849+
static const storageBucket = InternalExpression._(
850+
'STORAGE_BUCKET',
851+
'storageBucket',
852+
);
862853

863854
// Factory methods for creating input types
864855

lib/src/firebase.dart

Lines changed: 12 additions & 15 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';
@@ -39,7 +39,7 @@ import 'test_lab/test_lab_namespace.dart';
3939
///
4040
/// Provides access to all function namespaces (https, pubsub, firestore, etc.).
4141
class Firebase {
42-
Firebase() {
42+
Firebase() : _env = FirebaseEnv() {
4343
_initializeAdminSDK();
4444
}
4545

@@ -48,18 +48,7 @@ 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+
if (_env.isEmulator) {
6352
// TODO: Implement direct REST API calls to emulator
6453
// For now, we'll skip document fetching in emulator mode
6554
return;
@@ -71,7 +60,7 @@ class Firebase {
7160
_adminApp = FirebaseApp.initializeApp(
7261
options: AppOptions(
7362
credential: Credential.fromApplicationDefaultCredentials(),
74-
projectId: projectId,
63+
projectId: _env.projectId,
7564
),
7665
);
7766

@@ -82,6 +71,8 @@ class Firebase {
8271
}
8372
}
8473

74+
final FirebaseEnv _env;
75+
8576
/// Get the Firestore instance
8677
gfs.Firestore? get firestoreAdmin => _firestoreInstance;
8778

@@ -260,3 +251,9 @@ abstract class FunctionsNamespace {
260251
const FunctionsNamespace(this.firebase);
261252
final Firebase firebase;
262253
}
254+
255+
/// Internal extension to access private members of Firebase.
256+
@internal
257+
extension FirebaseInternal on Firebase {
258+
FirebaseEnv get $env => _env;
259+
}

lib/src/https/https_namespace.dart

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
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';
@@ -26,20 +25,6 @@ import 'callable.dart';
2625
import 'error.dart';
2726
import 'options.dart';
2827

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-
4328
/// HTTPS triggers namespace.
4429
///
4530
/// Provides methods to define HTTP-triggered Cloud Functions.
@@ -115,7 +100,7 @@ class HttpsNamespace extends FunctionsNamespace {
115100
}
116101

117102
// Extract auth and app check tokens
118-
final skipVerification = _shouldSkipTokenVerification();
103+
final skipVerification = firebase.$env.skipTokenVerification;
119104
final tokens = await checkTokens(
120105
request,
121106
skipTokenVerification: skipVerification,
@@ -195,7 +180,7 @@ class HttpsNamespace extends FunctionsNamespace {
195180
final body = await request.json as Map<String, dynamic>?;
196181

197182
// Extract auth and app check tokens
198-
final skipVerification = _shouldSkipTokenVerification();
183+
final skipVerification = firebase.$env.skipTokenVerification;
199184
final tokens = await checkTokens(
200185
request,
201186
skipTokenVerification: skipVerification,

0 commit comments

Comments
 (0)