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
17 changes: 17 additions & 0 deletions example/https/bin/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ final isProduction = defineBoolean(
),
);

final apiKey = defineSecret('API_KEY');

void main(List<String> args) async {
await runFunctions((firebase) {
// Basic callable function - untyped data
Expand Down Expand Up @@ -126,6 +128,21 @@ void main(List<String> 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',
Expand Down
19 changes: 11 additions & 8 deletions lib/src/builder/spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
demolaf marked this conversation as resolved.
}

/// Extracts labels map.
Expand Down Expand Up @@ -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;
}
12 changes: 12 additions & 0 deletions test/fixtures/dart_reference/bin/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ final isProduction = defineBoolean(
),
);

final apiKey = defineSecret('API_KEY');

final apiConfig = defineJsonSecret<Map<String, dynamic>>('API_CONFIG');

void main(List<String> args) async {
FirebaseApp.initializeApp();

Expand Down Expand Up @@ -761,6 +765,14 @@ void main(List<String> 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,
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/nodejs_reference/extract-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ function extractParams() {
BooleanParam: "boolean",
FloatParam: "float",
ListParam: "list",
SecretParam: "secret",
};

const result = { name: p.name };
Expand Down Expand Up @@ -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 || {};
Expand Down
14 changes: 13 additions & 1 deletion test/fixtures/nodejs_reference/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
// =============================================================================
Expand Down Expand Up @@ -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(
{
Expand Down
103 changes: 88 additions & 15 deletions test/snapshots/manifest_snapshot_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -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');
Comment thread
demolaf marked this conversation as resolved.
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
// =========================================================================
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'));
});
});
}

Expand Down Expand Up @@ -1995,9 +2069,8 @@ Map<String, dynamic>? _getParam(Map<String, dynamic> 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<String, dynamic>;
}
final typedParam = param as Map<String, dynamic>;
if (typedParam['name'] == name) return typedParam;
}
return null;
}
Expand Down
Loading