From f71a76fcdd464521d7c4dcb0c84c204c624b6aa3 Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 18 May 2026 13:27:39 +0100 Subject: [PATCH 1/4] fix: resolve secret name from defineSecret argument instead of variable name # Conflicts: # test/snapshots/manifest_snapshot_test.dart --- example/https/bin/server.dart | 16 +++ lib/src/builder/spec.dart | 13 ++- test/fixtures/dart_reference/bin/server.dart | 12 +++ .../nodejs_reference/extract-manifest.js | 9 ++ test/fixtures/nodejs_reference/index.js | 14 ++- test/snapshots/manifest_snapshot_test.dart | 101 +++++++++++++++--- 6 files changed, 145 insertions(+), 20 deletions(-) diff --git a/example/https/bin/server.dart b/example/https/bin/server.dart index 81ed241..6e54785 100644 --- a/example/https/bin/server.dart +++ b/example/https/bin/server.dart @@ -42,6 +42,8 @@ final isProduction = defineBoolean( ), ); +final apiKey = defineSecret('API_KEY'); + void main(List args) async { await runFunctions((firebase) { // Basic callable function - untyped data @@ -126,6 +128,20 @@ void main(List args) async { }, ); + // HTTPS function using a secret from Cloud Secret Manager + firebase.https.onRequest( + name: 'secretExample', + // ignore: non_const_argument_for_const_parameter + options: HttpsOptions( + secrets: [apiKey], + invoker: const Invoker.private(), + ), + (request) async { + final key = apiKey.value(); + return Response.ok('API key starts with: ${key.substring(0, 4)}...'); + }, + ); + // Conditional configuration based on boolean parameter firebase.https.onRequest( name: 'configuredEndpoint', diff --git a/lib/src/builder/spec.dart b/lib/src/builder/spec.dart index 4dfe460..50b0772 100644 --- a/lib/src/builder/spec.dart +++ b/lib/src/builder/spec.dart @@ -466,11 +466,14 @@ class EndpointSpec { /// Extracts secret name from SecretParam instance. String? _extractSecretName(Expression expression) { - if (expression is! SimpleIdentifier) return null; - - // The identifier references a SecretParam variable - // We need to find its definition, but for now just use the variable name - return expression.name; + final name = switch (expression) { + SimpleIdentifier() => expression.name, + PrefixedIdentifier() => expression.identifier.name, + PropertyAccess() => expression.propertyName.name, + _ => null, + }; + if (name == null) return null; + return variableToParamName[name] ?? toUpperSnakeCase(name); } /// Extracts labels map. diff --git a/test/fixtures/dart_reference/bin/server.dart b/test/fixtures/dart_reference/bin/server.dart index 4d02fe5..93c5fc0 100644 --- a/test/fixtures/dart_reference/bin/server.dart +++ b/test/fixtures/dart_reference/bin/server.dart @@ -50,6 +50,10 @@ final isProduction = defineBoolean( ), ); +final apiKey = defineSecret('API_KEY'); + +final apiConfig = defineJsonSecret>('API_CONFIG'); + void main(List args) async { FirebaseApp.initializeApp(); @@ -761,6 +765,14 @@ void main(List args) async { (request) async => Response.ok('Custom invoker'), ); + // HTTPS onRequest with secrets — tests variableToParamName resolution + firebase.https.onRequest( + name: 'httpsWithSecrets', + // ignore: non_const_argument_for_const_parameter + options: HttpsOptions(secrets: [apiKey, apiConfig]), + (request) async => Response.ok('HTTPS with secrets'), + ); + // Pub/Sub with options firebase.pubsub.onMessagePublished( topic: optionsTopic, diff --git a/test/fixtures/nodejs_reference/extract-manifest.js b/test/fixtures/nodejs_reference/extract-manifest.js index 75d8167..a178357 100644 --- a/test/fixtures/nodejs_reference/extract-manifest.js +++ b/test/fixtures/nodejs_reference/extract-manifest.js @@ -54,6 +54,7 @@ function extractParams() { BooleanParam: "boolean", FloatParam: "float", ListParam: "list", + SecretParam: "secret", }; const result = { name: p.name }; @@ -142,6 +143,14 @@ function normalizeEndpoint(name, endpoint) { } } + // Secrets — normalize to {key, secret} pairs to match Dart manifest format + if (Array.isArray(endpoint.secretEnvironmentVariables) && endpoint.secretEnvironmentVariables.length > 0) { + result.secretEnvironmentVariables = endpoint.secretEnvironmentVariables.map((s) => ({ + key: s.key, + secret: s.secret ?? s.key, + })); + } + // Trigger types if (endpoint.callableTrigger !== undefined) { result.callableTrigger = endpoint.callableTrigger || {}; diff --git a/test/fixtures/nodejs_reference/index.js b/test/fixtures/nodejs_reference/index.js index 3a67b05..1df9951 100644 --- a/test/fixtures/nodejs_reference/index.js +++ b/test/fixtures/nodejs_reference/index.js @@ -34,7 +34,7 @@ const { onConfigUpdated } = require("firebase-functions/v2/remoteConfig"); const { onObjectFinalized, onObjectArchived, onObjectDeleted, onObjectMetadataUpdated } = require("firebase-functions/v2/storage"); const { onCustomEventPublished } = require("firebase-functions/v2/eventarc"); const { onTestMatrixCompleted } = require("firebase-functions/v2/testLab"); -const { defineString, defineInt, defineBoolean } = require("firebase-functions/params"); +const { defineString, defineInt, defineBoolean, defineSecret } = require("firebase-functions/params"); // ============================================================================= // Parameterized Configuration Examples @@ -59,6 +59,10 @@ const isProduction = defineBoolean("IS_PRODUCTION", { description: "Whether this is a production deployment", }); +const apiKey = defineSecret("API_KEY"); + +const apiConfig = defineSecret("API_CONFIG"); + // ============================================================================= // HTTPS Callable Functions (onCall) // ============================================================================= @@ -673,6 +677,14 @@ exports.httpsCustomInvoker = onRequest( } ); +// HTTPS onRequest with secrets +exports.httpsWithSecrets = onRequest( + { secrets: [apiKey, apiConfig] }, + (request, response) => { + response.send("HTTPS with secrets"); + } +); + // Pub/Sub with options exports.onMessagePublished_optionstopic = onMessagePublished( { diff --git a/test/snapshots/manifest_snapshot_test.dart b/test/snapshots/manifest_snapshot_test.dart index d2d84fd..7045ae8 100644 --- a/test/snapshots/manifest_snapshot_test.dart +++ b/test/snapshots/manifest_snapshot_test.dart @@ -98,11 +98,11 @@ void main() { final nodejsParams = nodejsManifest['params'] as List; expect( - dartParams.length, - equals(nodejsParams.length), + dartParams, + hasLength(nodejsParams.length), reason: 'Should have same number of params', ); - expect(dartParams.length, equals(3)); + expect(dartParams, hasLength(5)); }); test('should have WELCOME_MESSAGE string param', () { @@ -157,6 +157,32 @@ void main() { expect(dartParam['description'], equals(nodejsParam['description'])); }); + test('should have API_KEY secret param', () { + final dartParam = _getParam(dartManifest, 'API_KEY'); + final nodejsParam = _getParam(nodejsManifest, 'API_KEY'); + + expect(dartParam, isNotNull); + expect(nodejsParam, isNotNull); + + expect(dartParam!['type'], equals('secret')); + expect(nodejsParam!['type'], equals('secret')); + }); + + test('should have API_CONFIG JSON secret param', () { + final dartParam = _getParam(dartManifest, 'API_CONFIG'); + final nodejsParam = _getParam(nodejsManifest, 'API_CONFIG'); + + expect(dartParam, isNotNull); + expect(nodejsParam, isNotNull); + + expect(dartParam!['type'], equals('secret')); + expect(nodejsParam!['type'], equals('secret')); + + // format: json is Dart-specific (defineJsonSecret has no Node.js equivalent) + expect(dartParam['format'], equals('json')); + expect(nodejsParam.containsKey('format'), isFalse); + }); + // ========================================================================= // RequiredAPIs Tests // ========================================================================= @@ -166,11 +192,11 @@ void main() { final nodejsAPIs = nodejsManifest['requiredAPIs'] as List; expect( - dartAPIs.length, - equals(nodejsAPIs.length), + dartAPIs, + hasLength(nodejsAPIs.length), reason: 'Should have same number of requiredAPIs', ); - expect(dartAPIs.length, equals(5)); + expect(dartAPIs, hasLength(5)); // Check each API matches for (final nodejsApi in nodejsAPIs) { @@ -326,14 +352,14 @@ void main() { final nodejsEndpoints = nodejsManifest['endpoints'] as Map; expect( - dartEndpoints.keys.length, - equals(56), + dartEndpoints.keys, + hasLength(56), reason: - 'Should discover 56 functions (7 Callable + 7 HTTPS + 1 Pub/Sub + 5 Firestore + 4 Firestore WithAuthContext + 5 Database + 3 Alerts + 4 Identity + 1 Remote Config + 4 Storage + 2 Eventarc + 2 Scheduler + 2 Tasks + 1 Test Lab + 5 Options + 2 Variable Options + 1 Cross-file Options)', + 'Should discover 56 functions (7 Callable + 7 HTTPS + 1 Pub/Sub + 5 Firestore + 4 Firestore WithAuthContext + 5 Database + 3 Alerts + 4 Identity + 1 Remote Config + 4 Storage + 2 Eventarc + 2 Scheduler + 2 Tasks + 1 Test Lab + 5 Options + 2 Variable Options + 1 Cross-file Options + 1 Secrets)', ); expect( - nodejsEndpoints.keys.length, - equals(56), + nodejsEndpoints.keys, + hasLength(56), reason: 'Node.js reference should also have 56 endpoints', ); @@ -1968,6 +1994,54 @@ void main() { expect(dartFunc['availableMemoryMb'], equals(512)); expect(nodejsFunc['availableMemoryMb'], equals(512)); }); + + // ========================================================================= + // Secrets Tests + // ========================================================================= + + test('should resolve secret variable names via variableToParamName', () { + final dartFunc = _getEndpoint(dartManifest, 'httpsWithSecrets')!; + final nodejsFunc = _getEndpoint(nodejsManifest, 'httpsWithSecrets')!; + + expect(dartFunc['secretEnvironmentVariables'], isNotNull); + expect(nodejsFunc['secretEnvironmentVariables'], isNotNull); + + final dartSecrets = dartFunc['secretEnvironmentVariables'] as List; + final nodejsSecrets = nodejsFunc['secretEnvironmentVariables'] as List; + + expect(dartSecrets, hasLength(2)); + expect(nodejsSecrets, hasLength(2)); + + // Variable 'apiKey' bound to defineSecret('API_KEY') resolves to 'API_KEY' + final dartApiKey = dartSecrets.firstWhere( + (s) => (s as Map)['key'] == 'API_KEY', + orElse: () => null, + ); + expect(dartApiKey, isNotNull); + expect((dartApiKey as Map)['secret'], equals('API_KEY')); + + final nodejsApiKey = nodejsSecrets.firstWhere( + (s) => (s as Map)['key'] == 'API_KEY', + orElse: () => null, + ); + expect(nodejsApiKey, isNotNull); + expect((nodejsApiKey as Map)['secret'], equals('API_KEY')); + + // Variable 'apiConfig' bound to defineJsonSecret('API_CONFIG') resolves to 'API_CONFIG' + final dartApiConfig = dartSecrets.firstWhere( + (s) => (s as Map)['key'] == 'API_CONFIG', + orElse: () => null, + ); + expect(dartApiConfig, isNotNull); + expect((dartApiConfig as Map)['secret'], equals('API_CONFIG')); + + final nodejsApiConfig = nodejsSecrets.firstWhere( + (s) => (s as Map)['key'] == 'API_CONFIG', + orElse: () => null, + ); + expect(nodejsApiConfig, isNotNull); + expect((nodejsApiConfig as Map)['secret'], equals('API_CONFIG')); + }); }); } @@ -1995,9 +2069,8 @@ Map? _getParam(Map manifest, String name) { final params = manifest['params'] as List?; if (params == null) return null; for (final param in params) { - if ((param as Map)['name'] == name) { - return param as Map; - } + final typedParam = param as Map; + if (typedParam['name'] == name) return typedParam; } return null; } From 64cc0ad0ebf10022f8aab7bb1e1db8a0529951f8 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 18 Jun 2026 17:10:27 +0100 Subject: [PATCH 2/4] fix CI --- test/snapshots/manifest_snapshot_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/snapshots/manifest_snapshot_test.dart b/test/snapshots/manifest_snapshot_test.dart index 7045ae8..fe5474b 100644 --- a/test/snapshots/manifest_snapshot_test.dart +++ b/test/snapshots/manifest_snapshot_test.dart @@ -353,14 +353,14 @@ void main() { expect( dartEndpoints.keys, - hasLength(56), + hasLength(57), reason: - 'Should discover 56 functions (7 Callable + 7 HTTPS + 1 Pub/Sub + 5 Firestore + 4 Firestore WithAuthContext + 5 Database + 3 Alerts + 4 Identity + 1 Remote Config + 4 Storage + 2 Eventarc + 2 Scheduler + 2 Tasks + 1 Test Lab + 5 Options + 2 Variable Options + 1 Cross-file Options + 1 Secrets)', + 'Should discover 57 functions (7 Callable + 7 HTTPS + 1 Pub/Sub + 5 Firestore + 4 Firestore WithAuthContext + 5 Database + 3 Alerts + 4 Identity + 1 Remote Config + 4 Storage + 2 Eventarc + 2 Scheduler + 2 Tasks + 1 Test Lab + 5 Options + 2 Variable Options + 1 Cross-file Options + 1 Secrets)', ); expect( nodejsEndpoints.keys, - hasLength(56), - reason: 'Node.js reference should also have 56 endpoints', + hasLength(57), + reason: 'Node.js reference should also have 57 endpoints', ); // Verify both manifests have the same endpoints (normalized via From 9ce0552960fbc1edb070856b69b20e182f55f2f4 Mon Sep 17 00:00:00 2001 From: demolaf Date: Fri, 19 Jun 2026 11:12:28 +0100 Subject: [PATCH 3/4] updates --- example/https/bin/server.dart | 3 ++- lib/src/builder/spec.dart | 6 +++--- test/snapshots/manifest_snapshot_test.dart | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/example/https/bin/server.dart b/example/https/bin/server.dart index 6e54785..a38e126 100644 --- a/example/https/bin/server.dart +++ b/example/https/bin/server.dart @@ -138,7 +138,8 @@ void main(List args) async { ), (request) async { final key = apiKey.value(); - return Response.ok('API key starts with: ${key.substring(0, 4)}...'); + final preview = key.length >= 4 ? '${key.substring(0, 4)}...' : key; + return Response.ok('API key starts with: $preview'); }, ); diff --git a/lib/src/builder/spec.dart b/lib/src/builder/spec.dart index 50b0772..d0a4e45 100644 --- a/lib/src/builder/spec.dart +++ b/lib/src/builder/spec.dart @@ -584,8 +584,8 @@ class EndpointSpec { /// Converts a camelCase or PascalCase string to UPPER_SNAKE_CASE. String toUpperSnakeCase(String input) { - return input + final result = input .replaceAllMapped(RegExp(r'[A-Z]'), (match) => '_${match.group(0)}') - .toUpperCase() - .replaceFirst('_', ''); + .toUpperCase(); + return result.startsWith('_') ? result.substring(1) : result; } diff --git a/test/snapshots/manifest_snapshot_test.dart b/test/snapshots/manifest_snapshot_test.dart index fe5474b..9601544 100644 --- a/test/snapshots/manifest_snapshot_test.dart +++ b/test/snapshots/manifest_snapshot_test.dart @@ -179,8 +179,8 @@ void main() { expect(nodejsParam!['type'], equals('secret')); // format: json is Dart-specific (defineJsonSecret has no Node.js equivalent) - expect(dartParam['format'], equals('json')); - expect(nodejsParam.containsKey('format'), isFalse); + expect(dartParam!['format'], equals('json')); + expect(nodejsParam!.containsKey('format'), isFalse); }); // ========================================================================= From 83062d6f1b76434f04e48d0826dd2b644a40a47d Mon Sep 17 00:00:00 2001 From: demolaf Date: Fri, 19 Jun 2026 11:14:35 +0100 Subject: [PATCH 4/4] chore: fix lint errors --- test/snapshots/manifest_snapshot_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/snapshots/manifest_snapshot_test.dart b/test/snapshots/manifest_snapshot_test.dart index 9601544..fe5474b 100644 --- a/test/snapshots/manifest_snapshot_test.dart +++ b/test/snapshots/manifest_snapshot_test.dart @@ -179,8 +179,8 @@ void main() { expect(nodejsParam!['type'], equals('secret')); // format: json is Dart-specific (defineJsonSecret has no Node.js equivalent) - expect(dartParam!['format'], equals('json')); - expect(nodejsParam!.containsKey('format'), isFalse); + expect(dartParam['format'], equals('json')); + expect(nodejsParam.containsKey('format'), isFalse); }); // =========================================================================