Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@
/// - **stderr**: WARNING, ERROR, CRITICAL, ALERT, EMERGENCY
library;

export 'src/logger/logger.dart';
export 'src/logger/logger.dart'
hide createLogger, projectIdZoneKey, traceIdZoneKey;
128 changes: 128 additions & 0 deletions lib/src/common/environment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2026 Firebase
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:convert';
import 'dart:io';

import 'package:meta/meta.dart';

/// Provides unified access to environment variables, emulator checks, and
/// Google Cloud / Firebase configuration.
class FirebaseEnv {
FirebaseEnv() : environment = mockEnvironment ?? Platform.environment;

@visibleForTesting
static Map<String, String>? mockEnvironment;

final Map<String, String> environment;

/// Whether running within a Firebase emulator environment.
bool get isEmulator {
// Explicit functions emulator flag
return environment['FUNCTIONS_EMULATOR'] == 'true' ||
// Generic fallback: check if any common emulator hosts are configured
_emulatorHostKeys.any(environment.containsKey);
}

/// Timezone setting.
String get tz => environment['TZ'] ?? 'UTC';

/// Whether debug mode is enabled.
bool get debugMode => environment['FIREBASE_DEBUG_MODE'] == 'true';

/// Whether to skip token verification (emulator only).
bool get skipTokenVerification => _getDebugFeature('skipTokenVerification');

/// Whether CORS is enabled (emulator only).
bool get enableCors => _getDebugFeature('enableCors');

bool _getDebugFeature(String key) {
if (environment['FIREBASE_DEBUG_FEATURES'] case final String json) {
try {
if (jsonDecode(json) case final Map<String, dynamic> m) {
return switch (m[key]) {
final bool value => value,
_ => false,
};
}
} on FormatException {
// ignore
}
}
return false;
}

/// Returns the current Firebase project ID.
///
/// Checks standard environment variables in order:
/// 1. FIREBASE_PROJECT
/// 2. GCLOUD_PROJECT
/// 3. GOOGLE_CLOUD_PROJECT
/// 4. GCP_PROJECT
///
/// If none are set, throws [StateError].
String get projectId {
for (final option in _projectIdEnvKeyOptions) {
final value = environment[option];
if (value != null && value.isNotEmpty) return value;
}

throw StateError(
'No project ID found in environment. Checked: ${_projectIdEnvKeyOptions.join(', ')}',
);
}

/// The port to listen on.
///
/// Uses the [PORT] environment variable, defaulting to 8080.
int get port => int.tryParse(environment['PORT'] ?? '8080') ?? 8080;

/// The name of the Cloud Run service.
///
/// Uses the `K_SERVICE` environment variable.
///
/// See https://cloud.google.com/run/docs/container-contract#env-vars
String? get kService => environment['K_SERVICE'];

/// The name of the target function.
///
/// Uses the `FUNCTION_TARGET` environment variable.
///
/// See https://docs.cloud.google.com/run/docs/configuring/services/environment-variables#additional_reserved_environment_variables_when_deploying_functions
String? get functionTarget => environment['FUNCTION_TARGET'];

/// Whether the functions control API is enabled.
///
/// Uses the `FUNCTIONS_CONTROL_API` environment variable.
///
/// This is part of the contract with `firebase-tools`.
bool get functionsControlApi =>
environment['FUNCTIONS_CONTROL_API'] == 'true';
}

/// Common project ID environment variables checked in order.
const _projectIdEnvKeyOptions = [
'FIREBASE_PROJECT',
'GCLOUD_PROJECT',
'GOOGLE_CLOUD_PROJECT',
'GCP_PROJECT',
];

/// Common emulator host keys used to detect emulator environment.
const _emulatorHostKeys = [
'FIRESTORE_EMULATOR_HOST',
'FIREBASE_AUTH_EMULATOR_HOST',
'FIREBASE_DATABASE_EMULATOR_HOST',
'FIREBASE_STORAGE_EMULATOR_HOST',
];
27 changes: 12 additions & 15 deletions lib/src/firebase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// limitations under the License.

import 'dart:async';
import 'dart:io';

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

import 'alerts/alerts_namespace.dart';
import 'common/cloud_run_id.dart';
import 'common/environment.dart';
import 'database/database_namespace.dart';
import 'eventarc/eventarc_namespace.dart';
import 'firestore/firestore_namespace.dart';
Expand All @@ -39,7 +39,7 @@ import 'test_lab/test_lab_namespace.dart';
///
/// Provides access to all function namespaces (https, pubsub, firestore, etc.).
class Firebase {
Firebase() {
Firebase() : _env = FirebaseEnv() {
_initializeAdminSDK();
}

Expand All @@ -48,18 +48,7 @@ class Firebase {

/// Initialize the Firebase Admin SDK
void _initializeAdminSDK() {
// Get project ID from environment
final projectId =
Platform.environment['GCLOUD_PROJECT'] ??
Platform.environment['GCP_PROJECT'] ??
'demo-test'; // Fallback for emulator

// Check if running in emulator
final firestoreEmulatorHost =
Platform.environment['FIRESTORE_EMULATOR_HOST'];
final isEmulator = firestoreEmulatorHost != null;

if (isEmulator) {
if (_env.isEmulator) {
// TODO: Implement direct REST API calls to emulator
// For now, we'll skip document fetching in emulator mode
return;
Expand All @@ -71,7 +60,7 @@ class Firebase {
_adminApp = FirebaseApp.initializeApp(
options: AppOptions(
credential: Credential.fromApplicationDefaultCredentials(),
projectId: projectId,
projectId: _env.projectId,
),
);

Expand All @@ -82,6 +71,8 @@ class Firebase {
}
}

final FirebaseEnv _env;

/// Get the Firestore instance
gfs.Firestore? get firestoreAdmin => _firestoreInstance;

Expand Down Expand Up @@ -260,3 +251,9 @@ abstract class FunctionsNamespace {
const FunctionsNamespace(this.firebase);
final Firebase firebase;
}

/// Internal extension to access private members of Firebase.
@internal
extension FirebaseInternal on Firebase {
FirebaseEnv get $env => _env;
}
19 changes: 2 additions & 17 deletions lib/src/https/https_namespace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart';
Expand All @@ -26,20 +25,6 @@ import 'callable.dart';
import 'error.dart';
import 'options.dart';

/// Checks if token verification should be skipped (emulator mode).
bool _shouldSkipTokenVerification() {
final debugFeatures = Platform.environment['FIREBASE_DEBUG_FEATURES'];
if (debugFeatures == null) {
return false;
}
try {
final features = jsonDecode(debugFeatures);
return features['skipTokenVerification'] as bool? ?? false;
} catch (_) {
return false;
}
}

/// HTTPS triggers namespace.
///
/// Provides methods to define HTTP-triggered Cloud Functions.
Expand Down Expand Up @@ -115,7 +100,7 @@ class HttpsNamespace extends FunctionsNamespace {
}

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

// Extract auth and app check tokens
final skipVerification = _shouldSkipTokenVerification();
final skipVerification = firebase.$env.skipTokenVerification;
final tokens = await checkTokens(
request,
skipTokenVerification: skipVerification,
Expand Down
30 changes: 4 additions & 26 deletions lib/src/identity/identity_namespace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ library;

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart';
Expand Down Expand Up @@ -276,45 +275,24 @@ class IdentityNamespace extends FunctionsNamespace {
/// certificates. In emulator mode (when FUNCTIONS_EMULATOR=true or
/// skipTokenVerification debug feature is enabled), verification is skipped.
Future<Map<String, dynamic>> _decodeAndVerifyJwt(String jwt) async {
// Get environment configuration
final env = Platform.environment;
final isEmulator = env['FUNCTIONS_EMULATOR'] == 'true';
final skipVerification = _shouldSkipTokenVerification(env);

// Get project ID
final projectId =
env['GCLOUD_PROJECT'] ??
env['GCP_PROJECT'] ??
env['FIREBASE_PROJECT'] ??
'demo-test';
final projectId = firebase.$env.projectId;

// Create verifier
final verifier = AuthBlockingTokenVerifier(
projectId: projectId,
isEmulator: isEmulator || skipVerification,
isEmulator:
firebase.$env.isEmulator || firebase.$env.skipTokenVerification,
);

// Determine audience based on platform
// Cloud Run uses "run.app", GCF v1 uses default
final kService = env['K_SERVICE']; // Cloud Run service name
final kService = firebase.$env.kService; // Cloud Run service name
final audience = kService != null ? 'run.app' : null;

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

/// Checks if token verification should be skipped based on debug features.
bool _shouldSkipTokenVerification(Map<String, String> env) {
final debugFeatures = env['FIREBASE_DEBUG_FEATURES'];
if (debugFeatures == null) return false;

try {
final features = jsonDecode(debugFeatures) as Map<String, dynamic>;
return features['skipTokenVerification'] as bool? ?? false;
} on FormatException {
return false;
}
}

/// Validates the auth response for invalid claims.
void _validateAuthResponse(
AuthBlockingEventType eventType,
Expand Down
Loading
Loading