Gathering detailed insights and metrics for firebase-function
Gathering detailed insights and metrics for firebase-function
Gathering detailed insights and metrics for firebase-function
Gathering detailed insights and metrics for firebase-function
firebase-function-tools
firebase-function-tools
@hieroglyphics/firebase-function-tools
@hieroglyphics/firebase-function-tools
@ugrc/firebase-auth-arcgis-server-proxy
An authenticated Firebase function for proxying requests to ArcGIS Server services
@seibert/firestore-backup-function
A small helper function to initialize a Firebase Function which creates Firestore backups according to the given schedule
npm install firebase-function
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
51 Commits
1 Watching
1 Branches
2 Contributors
Updated on 17 Jun 2024
TypeScript (97.7%)
JavaScript (2.3%)
Cumulative downloads
Total Downloads
Last day
-30.2%
67
Compared to previous day
Last week
20.6%
614
Compared to previous week
Last month
3.5%
1,763
Compared to previous month
Last year
119.2%
20,233
Compared to previous year
14
Used to manage a backend api with firebase functions by creating a single firebase function for an api functionallity, these functions also handles security be encrypting / decrypting the parameters and return type. Also allows easy, secure and type-save access to the firebase realtime-database.
You can create a new firebase function by declaring a new class that conforms to FirebaseFunction<MyFunctionType>
interface.
1import { type FirebaseFunction, type ILogger, ParameterContainer, ParameterParser, type FunctionType } from 'firebase-function'; 2import { type AuthData } from 'firebase-functions/lib/common/providers/tasks'; 3 4export class MyFunction implements FirebaseFunction<MyFunctionType> { 5 public readonly parameters: FunctionType.Parameters<MyFunctionType> & { databaseType: DatabaseType }; 6 7 public constructor(data: Record<string, unknown> & { databaseType: DatabaseType }, auth: AuthData | undefined, private readonly logger: ILogger) { 8 this.logger.log('MyFunction.constructor', { data: data, auth: auth }, 'notice'); 9 const parameterContainer = new ParameterContainer(data, getPrivateKeys, this.logger.nextIndent); 10 const parameterParser = new ParameterParser<FunctionType.Parameters<EventEditFunctionType>>({}, this.logger.nextIndent); 11 parameterParser.parseParameters(parameterContainer); 12 this.parameters = parameterParser.parameters; 13 } 14 15 public async executeFunction(): Promise<FunctionType.ReturnType<MyFunctionType>> { 16 this.logger.log('MyFunction.executeFunction', {}, 'info'); 17 // Do whatever your function should do 18 } 19} 20 21export type MyFunctionType = FunctionType<Record<string, never>, void>;
This is the skeleton of a empty firebase function with no parameters. To add parameters to your firebase function see Function Parameters. If you want to return a value back to the frontend, see the Return type Chapter.
You can get this skeleton via autocompletition in VS-Code, just add the functions.code-snippets
file to your .vscode
folder.
To deploy your firebase functions, add them in a seperate file such that they satisfies the FirebaseFunctionsType
. Important: The satisties
keyword requiers at least Typescript 4.9.
1import { FirebaseFunctionDescriptor, type FirebaseFunctionsType } from 'firebase-function'; 2import { MyFunction, type MyFunctionType } from './MyFunction'; 3import { MyOtherFunction, type MyOtherFunctionType } from './MyOtherFunction'; 4import { MyThirdFunction, type MyThirdFunctionType } from './MyThirdFunction'; 5 6export const firebaseFunctions = { 7 myFunction: FirebaseFunctionDescriptor.create<MyFunctionType>(MyFunction), 8 myNamespace: { 9 myOtherFunction: FirebaseFunctionDescriptor.create<MyOtherFunctionType>(MyOtherFunction), 10 myThirdFunction: FirebaseFunctionDescriptor.create<MyThirdFunctionType>(MyThirdFunction) 11 } 12} satisfies FirebaseFunctionsType;
You can nest multiple functions in a namespace by adding them in a nested object. These functions than have the prefix of the namespace, seperated by a -
. You can nest functions in as many namespaces as you wish. In this example you deploy three functions with names: myFunction
, myNamespace-myOtherFunction
and myNamespace-myThirdFunction
.
Then in your index.ts
file you initialize the app and make the firebase functions deployable:
1import * as admin from 'firebase-admin'; 2import { createFirebaseFunctions } from 'firebase-function'; 3import { firebaseFunctions } from './firebaseFunctions'; 4import { getPrivateKeys } from './privateKeys'; 5 6admin.initializeApp(); 7 8export = createFirebaseFunctions(firebaseFunctions, getPrivateKeys);
Learn more about the private keys and how the functions are secured in Security / Private Keys.
At the end deploy your functions as always by running firebase deploy --only functions
in your firebase directory.
The firebase function can have multiple parameters of different types:
boolean
A boolean value: true
or false
.string
Represents textual values like "Hello, World!"
number
Floating point number or integer value.object
Keyed value pairs.array
List of multiple values.null
The null
value.undefined
Absent of a value.You define the parameters as the first type parameter of the FunctionType
:
1export type MyFunctionType = FunctionType<{ 2 name: string; 3 gender: 'male' | 'female'; 4 address: Address; 5}, void>; 6 7interface Address { 8 city: string; 9 street: string; 10}
Now you have to implement the parameterParser
in the function constructor:
1const parameterParser = new ParameterParser<FunctionType.Parameters<EventEditFunctionType>>({ 2 name: ParameterBuilder.value('string'), 3 gender: ParameterBuilder.guard('string', isValidGender), 4 address: ParameterBuilder.build('object', parseAddress) 5}, this.logger.nextIndent); 6 7 8function isValidGender(value: string) value is 'male' | 'female' { 9 return value === 'male' || value === 'female'; 10} 11 12function parseAddress(value: object | null, logger: ILogger): Address { 13 logger.log('parseAdress', { value: value }); 14 15 if (value === null) 16 throw HttpsError('internal', 'Couldn\'t get address from null.', logger); 17 18 if (!('city' in value) || typeof value.city !== 'string') 19 throw HttpsError('internal', 'Couldn\'t get city for address.', logger); 20 21 if (!('street' in value) || typeof value.street !== 'string') 22 throw HttpsError('internal', 'Couldn\'t get street for address.', logger); 23 24 return { 25 city: value.city, 26 street: value.street 27 }; 28}
You can access the parameters via this.parameters
property. There are always a databaseType
parameter which specifies the database type: release
, debug
or testing
.
You can specify the return type in the second type parameter of the FunctionType
:
1export type MyFunctionType = FunctionType<{ 2 name: string; 3 gender: 'male' | 'female'; 4 address: Address; 5}, string>;
You can check whether an user is signed in that called this function by checking the auth
parameter passed to the firebase function constructor.
Why are there three database types, that are always part of the parameters? As the values of the database type says, the are different realtime databases for debug and for the tests, so they don't interfere with the actual data of the release database.
The private keys contains three values:
databaseUrl
This is the url to the realtime database.callSecretKey
This is a key used to identify that the call was made from an authorized frontend.cryptionKeys
These keys are used to decrypt the parameters and encrypt the return value. Also used to en- / decrypt the data on the database.It's recommended that for each database different keys are used. That's like an example would look like:
1export function getPrivateKeys(databaseType: DatabaseType): PrivateKeys { 2 switch (databaseType.value) { 3 case 'release': return { 4 cryptionKeys: { 5 encryptionKey: new FixedLength(Uint8Array.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]), 32), 6 initialisationVector: new FixedLength(Uint8Array.from([0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f]), 16), 7 vernamKey: new FixedLength(Uint8Array.from([0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f]), 32) 8 }, 9 callSecretKey: '!^xil&hAGv8&NDFWie$*jrmO!1#8Sf7tt%5a^zx%R6j0cvjkP$TCwxh573$OG6FF', 10 databaseUrl: 'https://my-database-release.firebasedatabase.app/' 11 }; 12 case 'debug': return { 13 cryptionKeys: { 14 encryptionKey: new FixedLength(Uint8Array.from([0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f]), 32), 15 initialisationVector: new FixedLength(Uint8Array.from([0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f]), 16), 16 vernamKey: new FixedLength(Uint8Array.from([0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f]), 32) 17 }, 18 callSecretKey: 'NAKM*sQFVI6gIJ48oiKNZpHyoI!hkTT68wcwl2baejhBa!GGHAE$zk5vfF8Ua07G', 19 databaseUrl: 'https://my-database-debug.firebasedatabase.app/' 20 }; 21 case 'testing': return { 22 cryptionKeys: { 23 encryptionKey: new FixedLength(Uint8Array.from([0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf]), 32), 24 initialisationVector: new FixedLength(Uint8Array.from([0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf]), 16), 25 vernamKey: new FixedLength(Uint8Array.from([0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef]), 32) 26 }, 27 callSecretKey: 'LI9OxnKdHEuShIS@40%oM2@F2deFzomiaxd7!7TFrFrAIKFFEu&#qoL#ziLj@myo', 28 databaseUrl: 'https://my-database-testing.firebasedatabase.app/' 29 }; 30 } 31}
Make sure to not add the private keys to your version control system like git
.
Errors are handled as a HttpsError
which takes a FunctionsErrorCode
and a message as input. If you throw any other error, your frontend will just receive an unknown
error code.
To call your functions from your frontend, pass this parameters
verbose
The verbose level of the logger.databaseType
Database type of the function.callSecret
expiresAt
Utc date when the call secret expires.hashedData
Sha512 hashed expiresAtUtcDate
with call secret key as key.parameters
Encrypted parameters.and decrypt the return value, like in this example:
1const crypter = new Crypter(this.cryptionKeys); 2const expiresAtUtcDate = UtcDate.now.advanced({ minute: 1 }); 3const callableFunction = functions.httpsCallable<{ 4 verbose: VerboseType; 5 databaseType: DatabaseType.Value; 6 callSecret: CallSecret; 7 parameters: string; 8}, string>(functionName); 9const httpsCallableResult = await callableFunction({ 10 verbose: 'verbose', 11 databaseType: databaseType, 12 callSecret: { 13 expiresAt: expiresAtUtcDate.encoded, 14 hashedData: Crypter.sha512(expiresAtUtcDate.encoded, this.callSecretKey) 15 }, 16 parameters: crypter.encodeEncrypt(parameters) 17}); 18const result = await crypter.decryptDecode(httpsCallableResult.data);
For now it's only possible to call the firebase functions with typescript / javascript and swift as this are the only languages I have implemented the crypter. If you would like to use a different language, feel free to implement the crypter yourself. For that see the crypter in /src/crypter/Crypter.ts
.
This package allows easy, secure and type-save access to the firebase realtime-database.
Therefore you need to define a scheme for your database, e.g.:
1import { type CryptedScheme, type DatabaseSchemeType } from 'firebase-function'; 2 3export type DatabaseScheme = DatabaseSchemeType<{ 4 myValue: string; 5 myOtherData: { 6 value1: boolean; 7 value2: number | null; 8 }; 9 sensibleData: { 10 [UserId in string]: CryptedScheme<User>; 11 }; 12}>;
This scheme defines the database as follows:
myValue
, myOtherData
and sensibleData
.myValue
is just a plain string value.myOtherData
is a nested object, that contains a boolean value value1
and an optional number value value2
.sensibleData
is a collection of users, keyed by the user ids. As user informations are sensible data, we want to encrypt the data on the database. This can be defined by CryptedScheme<User>
in the database scheme.First you need to create a reference to the database:
1const reference = DatabaseReference.base<DatabaseScheme>(getPrivateKeys(this.parameters.databaseType));
With this reference you can remove
data from the database:
1await reference.child('sensibleData').child('aUserId').remove();
or setting / update the data:
1await reference.child('myValue').set('this is a value'); // Not crypted data 2await reference.child('sensibleData').child('aUserId').set(aUser, 'encrypt'); // Crypted data
To check whether there is data at all, check if the snapshot exists:
1const snapshot = await reference.child('sensibleData').child('aUserId').snapshot(); 2if (snapshot.exists) 3 // Do something with the data
and then you can get the value of the snapshot:
1const snapshot = await reference.child('myOtherData').snapshot(); 2const myValue1 = snapshot.child('value1').get(); // Not crypted data 3 4const snapshot = await reference.child('sensibleData').child('aUserId').snapshot(); 5const aUser = snapshot.get('decrypt'); // Crypted data
First create a firebase app with all the configuration and private keys:
1import { FirebaseApp } from 'firebase-function/lib/src/testUtils'; 2 3export const firebaseApp = new FirebaseApp<typeof firebaseFunctions, DatabaseScheme>(firebaseConfig, cryptionKeys, callSecretKey, { 4 name: 'my-database'; // optional 5 functionsRegion: 'europe-west1', // optional 6 databaseUrl: 'https://my-database-testing.firebasedatabase.app/' // optional 7});
The parameters passed to the function has to be primitive, like a json object. For example if you pass an id with a Uuid
as parameter, you don't want to pass the Uuid
object, but just the id as string. For this you need to define this flatten version of parameters as third type parameter of the FunctionType
in your firebase function definition:
1export type MyOtherFunctionType = FunctionType<{ 2 id: Uuid; 3}, string, { 4 id: string; 5}>;
Now you can call this function in your tests:
1const id = new Uuid(); 2const result = await firebaseApp.functions.function('myNamespcea').function('myOtherFunction').call({ 3 id: id.rawValue 4});
You can check if the call succeeds and the return value by get the result value of the function call:
1result.success.equal('my expected value');
Alternative if you expect that your function will throw an error, you can check the result to failure and also the FunctionsErrorCode
and the message:
1result.failure.equal({ 2 code: 'unavailable', 3 message: 'Expected message' 4});
To set the initial test data in the database there are convenient methods:
1await firebaseApp.database.child('myValue').set('this is a value'); // Not crypted data 2await firebaseApp.database.child('sensibleData').child('aUserId').set(aUser, 'encrypt'); // Crypted data 3await firebaseApp.database.child('sensibleData').child('anotherUserId').remove(); 4const existsUser = await firebaseApp.database.child('sensibleData').child('aUserId').exists; 5const myValue = await firebaseApp.database.child('myValue').get(); // Not crypted data 6const aUser = await firebaseApp.database.child('sensibleData').child('aUserId').get('decrypt'); // Crypted data
If you need an user to be signed in while calling the functions, you can use:
1await firebaseApp.auth.signIn(user.email, user.password); 2await firebaseApp.auth.signOut();
No vulnerabilities found.
No security vulnerabilities found.