diff --git a/example/https/bin/server.dart b/example/https/bin/server.dart index 81ed241..a38e126 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,21 @@ 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(); + final preview = key.length >= 4 ? '${key.substring(0, 4)}...' : key; + return Response.ok('API key starts with: $preview'); + }, + ); + // 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..d0a4e45 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. @@ -581,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/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..fe5474b 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,15 +352,15 @@ void main() { final nodejsEndpoints = nodejsManifest['endpoints'] as Map; expect( - dartEndpoints.keys.length, - equals(56), + dartEndpoints.keys, + 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)', + '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.length, - equals(56), - reason: 'Node.js reference should also have 56 endpoints', + nodejsEndpoints.keys, + hasLength(57), + reason: 'Node.js reference should also have 57 endpoints', ); // Verify both manifests have the same endpoints (normalized via @@ -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; }