resolveCloudflareFirebaseServiceAccount function
Future<String>
resolveCloudflareFirebaseServiceAccount(
- ExecContext context, {
- required String projectId,
Resolves one Firebase Admin SDK credential for a Cloudflare Worker flavor.
Explicit Authentication and Messaging values must identify the same key. File discovery only accepts a unique key for the selected Firebase project.
Implementation
Future<String> resolveCloudflareFirebaseServiceAccount(
ExecContext context, {
required String projectId,
}) async {
final authYaml = context.yaml
.getAsMap("cloudflare")
.getAsMap("authentication")
.getAsMap("delete_user")
.get<Object?>("service_account", null);
final authSecrets = context.secrets
.getAsMap("cloudflare")
.getAsMap("authentication")
.getAsMap("delete_user")
.get<Object?>("service_account", null);
final messagingYaml = context.yaml
.getAsMap("firebase")
.getAsMap("messaging")
.get<Object?>("service_account", null);
final messagingSecrets = context.secrets
.getAsMap("firebase")
.getAsMap("messaging")
.get<Object?>("service_account", null);
if ([authSecrets, authYaml, messagingSecrets, messagingYaml]
.any((value) => value != null && value is! String)) {
throw StateError(
"Firebase service account must be a JSON string resolved for the selected flavor.",
);
}
final auth = authSecrets is String && authSecrets.trim().isNotEmpty
? authSecrets
: authYaml;
final messaging =
messagingSecrets is String && messagingSecrets.trim().isNotEmpty
? messagingSecrets
: messagingYaml;
final configured = <String>[
if (auth is String && auth.trim().isNotEmpty) auth,
if (messaging is String && messaging.trim().isNotEmpty) messaging,
];
if (configured.isNotEmpty) {
final selected = _parseFirebaseServiceAccount(configured.first);
if (selected == null || selected["project_id"] != projectId) {
throw StateError(
"The configured Firebase service account does not match firebase.project_id for this flavor.",
);
}
for (final value in configured.skip(1)) {
final candidate = _parseFirebaseServiceAccount(value);
if (candidate == null ||
candidate["project_id"] != projectId ||
candidate["client_email"] != selected["client_email"] ||
candidate["private_key"] != selected["private_key"]) {
throw StateError(
"Authentication and Messaging configure different Firebase service accounts for one Worker secret.",
);
}
}
return configured.first;
}
final matches = <String, String>{};
final jsonNamePattern = RegExp(r"^([a-zA-Z0-9_-]+)\.json$");
for (final directoryName in ["cloudflare", "android"]) {
final directory = Directory(directoryName);
if (!directory.existsSync()) {
continue;
}
final files = await directory
.list(recursive: false, followLinks: false)
.where((entity) => entity is File)
.cast<File>()
.toList();
files.sort((a, b) => a.path.compareTo(b.path));
for (final file in files) {
final name = file.path.split(Platform.pathSeparator).last;
if (!jsonNamePattern.hasMatch(name)) {
continue;
}
try {
final content = await file.readAsString();
final parsed = _parseFirebaseServiceAccount(content);
if (parsed?["project_id"] == projectId) {
final identity = jsonEncode([
parsed!["client_email"],
parsed["private_key"],
]);
matches.putIfAbsent(identity, () => content);
}
} on FileSystemException {
continue;
}
}
}
if (matches.length != 1) {
throw StateError(
matches.isEmpty
? "No Firebase service account matches firebase.project_id for this flavor."
: "Multiple Firebase service accounts match firebase.project_id for this flavor. Configure one explicitly.",
);
}
return matches.values.single;
}