This commit is contained in:
girinb
2025-05-29 15:41:51 +09:00
parent a1bd4f87a1
commit 485098cd1a
5611 changed files with 881685 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
import { App } from "firebase-admin/app";
import * as database from "firebase-admin/database";
/**
* Pulled from @firebase/database-types, make sure the interface is updated on dependencies upgrades.
* Represents a child snapshot of a `Reference` that is being iterated over. The key will never be undefined.
*/
interface IteratedDataSnapshot extends DataSnapshot {
key: string;
}
/**
* Interface representing a Firebase Realtime database data snapshot.
*/
export declare class DataSnapshot implements database.DataSnapshot {
private app?;
instance: string;
/** @hidden */
private _ref;
/** @hidden */
private _path;
/** @hidden */
private _data;
/** @hidden */
private _childPath;
constructor(data: any, path?: string, // path is undefined for the database root
app?: App, instance?: string);
/**
* Returns a [`Reference`](/docs/reference/admin/node/admin.database.Reference)
* to the database location where the triggering write occurred. Has
* full read and write access.
*/
get ref(): database.Reference;
/**
* The key (last part of the path) of the location of this `DataSnapshot`.
*
* The last token in a database location is considered its key. For example,
* "ada" is the key for the `/users/ada/` node. Accessing the key on any
* `DataSnapshot` returns the key for the location that generated it.
* However, accessing the key on the root URL of a database returns `null`.
*/
get key(): string | null;
/**
* Extracts a JavaScript value from a `DataSnapshot`.
*
* Depending on the data in a `DataSnapshot`, the `val()` method may return a
* scalar type (string, number, or boolean), an array, or an object. It may also
* return `null`, indicating that the `DataSnapshot` is empty (contains no
* data).
*
* @return The snapshot's contents as a JavaScript value (Object,
* Array, string, number, boolean, or `null`).
*/
val(): any;
/**
* Exports the entire contents of the `DataSnapshot` as a JavaScript object.
*
* @return The contents of the `DataSnapshot` as a JavaScript value
* (Object, Array, string, number, boolean, or `null`).
*/
exportVal(): any;
/**
* Gets the priority value of the data in this `DataSnapshot`.
*
* As an alternative to using priority, applications can order collections by
* ordinary properties. See [Sorting and filtering
* data](/docs/database/web/lists-of-data#sorting_and_filtering_data).
*
* @return The priority value of the data.
*/
getPriority(): string | number | null;
/**
* Returns `true` if this `DataSnapshot` contains any data. It is slightly more
* efficient than using `snapshot.val() !== null`.
*
* @return `true` if this `DataSnapshot` contains any data; otherwise, `false`.
*/
exists(): boolean;
/**
* Gets a `DataSnapshot` for the location at the specified relative path.
*
* The relative path can either be a simple child name (for example, "ada") or
* a deeper slash-separated path (for example, "ada/name/first").
*
* @param path A relative path from this location to the desired child
* location.
* @return The specified child location.
*/
child(childPath: string): DataSnapshot;
/**
* Enumerates the `DataSnapshot`s of the children items.
*
* Because of the way JavaScript objects work, the ordering of data in the
* JavaScript object returned by `val()` is not guaranteed to match the ordering
* on the server nor the ordering of `child_added` events. That is where
* `forEach()` comes in handy. It guarantees the children of a `DataSnapshot`
* can be iterated in their query order.
*
* If no explicit `orderBy*()` method is used, results are returned
* ordered by key (unless priorities are used, in which case, results are
* returned by priority).
*
* @param action A function that is called for each child `DataSnapshot`.
* The callback can return `true` to cancel further enumeration.
*
* @return `true` if enumeration was canceled due to your callback
* returning `true`.
*/
forEach(action: (a: IteratedDataSnapshot) => boolean | void): boolean;
/**
* Returns `true` if the specified child path has (non-`null`) data.
*
* @param path A relative path to the location of a potential child.
* @return `true` if data exists at the specified child path; otherwise,
* `false`.
*/
hasChild(childPath: string): boolean;
/**
* Returns whether or not the `DataSnapshot` has any non-`null` child
* properties.
*
* You can use `hasChildren()` to determine if a `DataSnapshot` has any
* children. If it does, you can enumerate them using `forEach()`. If it
* doesn't, then either this snapshot contains a primitive value (which can be
* retrieved with `val()`) or it is empty (in which case, `val()` returns
* `null`).
*
* @return `true` if this snapshot has any children; else `false`.
*/
hasChildren(): boolean;
/**
* Returns the number of child properties of this `DataSnapshot`.
*
* @return Number of child properties of this `DataSnapshot`.
*/
numChildren(): number;
/**
* Returns a JSON-serializable representation of this object.
*
* @return A JSON-serializable representation of this object.
*/
toJSON(): Record<string, unknown>;
/** Recursive function to check if keys are numeric & convert node object to array if they are
*
* @hidden
*/
private _checkAndConvertToArray;
/** @hidden */
private _dup;
/** @hidden */
private _fullPath;
}
export {};

View File

@@ -0,0 +1,298 @@
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2022 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataSnapshot = void 0;
const database = require("firebase-admin/database");
const config_1 = require("../../common/config");
const path_1 = require("../../common/utilities/path");
/**
* Interface representing a Firebase Realtime database data snapshot.
*/
class DataSnapshot {
constructor(data, path, // path is undefined for the database root
app, instance) {
this.app = app;
const config = (0, config_1.firebaseConfig)();
if (instance) {
// SDK always supplies instance, but user's unit tests may not
this.instance = instance;
}
else if (app) {
this.instance = app.options.databaseURL;
}
else if (config.databaseURL) {
this.instance = config.databaseURL;
}
else if (process.env.GCLOUD_PROJECT) {
this.instance = "https://" + process.env.GCLOUD_PROJECT + "-default-rtdb.firebaseio.com";
}
this._path = path;
this._data = data;
}
/**
* Returns a [`Reference`](/docs/reference/admin/node/admin.database.Reference)
* to the database location where the triggering write occurred. Has
* full read and write access.
*/
get ref() {
if (!this.app) {
// may be unpopulated in user's unit tests
throw new Error("Please supply a Firebase app in the constructor for DataSnapshot" +
" in order to use the .ref method.");
}
if (!this._ref) {
let db;
if (this.instance) {
db = database.getDatabaseWithUrl(this.instance, this.app);
}
else {
db = database.getDatabase(this.app);
}
this._ref = db.ref(this._fullPath());
}
return this._ref;
}
/**
* The key (last part of the path) of the location of this `DataSnapshot`.
*
* The last token in a database location is considered its key. For example,
* "ada" is the key for the `/users/ada/` node. Accessing the key on any
* `DataSnapshot` returns the key for the location that generated it.
* However, accessing the key on the root URL of a database returns `null`.
*/
get key() {
const segments = (0, path_1.pathParts)(this._fullPath());
const last = segments[segments.length - 1];
return !last || last === "" ? null : last;
}
/**
* Extracts a JavaScript value from a `DataSnapshot`.
*
* Depending on the data in a `DataSnapshot`, the `val()` method may return a
* scalar type (string, number, or boolean), an array, or an object. It may also
* return `null`, indicating that the `DataSnapshot` is empty (contains no
* data).
*
* @return The snapshot's contents as a JavaScript value (Object,
* Array, string, number, boolean, or `null`).
*/
val() {
const parts = (0, path_1.pathParts)(this._childPath);
let source = this._data;
if (parts.length) {
for (const part of parts) {
if (source[part] === undefined) {
return null;
}
source = source[part];
}
}
const node = source !== null && source !== void 0 ? source : null;
return this._checkAndConvertToArray(node);
}
/**
* Exports the entire contents of the `DataSnapshot` as a JavaScript object.
*
* @return The contents of the `DataSnapshot` as a JavaScript value
* (Object, Array, string, number, boolean, or `null`).
*/
exportVal() {
return this.val();
}
/**
* Gets the priority value of the data in this `DataSnapshot`.
*
* As an alternative to using priority, applications can order collections by
* ordinary properties. See [Sorting and filtering
* data](/docs/database/web/lists-of-data#sorting_and_filtering_data).
*
* @return The priority value of the data.
*/
getPriority() {
return 0;
}
/**
* Returns `true` if this `DataSnapshot` contains any data. It is slightly more
* efficient than using `snapshot.val() !== null`.
*
* @return `true` if this `DataSnapshot` contains any data; otherwise, `false`.
*/
exists() {
const val = this.val();
if (typeof val === "undefined" || val === null) {
return false;
}
if (typeof val === "object" && Object.keys(val).length === 0) {
return false;
}
return true;
}
/**
* Gets a `DataSnapshot` for the location at the specified relative path.
*
* The relative path can either be a simple child name (for example, "ada") or
* a deeper slash-separated path (for example, "ada/name/first").
*
* @param path A relative path from this location to the desired child
* location.
* @return The specified child location.
*/
child(childPath) {
if (!childPath) {
return this;
}
return this._dup(childPath);
}
/**
* Enumerates the `DataSnapshot`s of the children items.
*
* Because of the way JavaScript objects work, the ordering of data in the
* JavaScript object returned by `val()` is not guaranteed to match the ordering
* on the server nor the ordering of `child_added` events. That is where
* `forEach()` comes in handy. It guarantees the children of a `DataSnapshot`
* can be iterated in their query order.
*
* If no explicit `orderBy*()` method is used, results are returned
* ordered by key (unless priorities are used, in which case, results are
* returned by priority).
*
* @param action A function that is called for each child `DataSnapshot`.
* The callback can return `true` to cancel further enumeration.
*
* @return `true` if enumeration was canceled due to your callback
* returning `true`.
*/
forEach(action) {
const val = this.val() || {};
if (typeof val === "object") {
return Object.keys(val).some((key) => action(this.child(key)) === true);
}
return false;
}
/**
* Returns `true` if the specified child path has (non-`null`) data.
*
* @param path A relative path to the location of a potential child.
* @return `true` if data exists at the specified child path; otherwise,
* `false`.
*/
hasChild(childPath) {
return this.child(childPath).exists();
}
/**
* Returns whether or not the `DataSnapshot` has any non-`null` child
* properties.
*
* You can use `hasChildren()` to determine if a `DataSnapshot` has any
* children. If it does, you can enumerate them using `forEach()`. If it
* doesn't, then either this snapshot contains a primitive value (which can be
* retrieved with `val()`) or it is empty (in which case, `val()` returns
* `null`).
*
* @return `true` if this snapshot has any children; else `false`.
*/
hasChildren() {
const val = this.val();
return val !== null && typeof val === "object" && Object.keys(val).length > 0;
}
/**
* Returns the number of child properties of this `DataSnapshot`.
*
* @return Number of child properties of this `DataSnapshot`.
*/
numChildren() {
const val = this.val();
return val !== null && typeof val === "object" ? Object.keys(val).length : 0;
}
/**
* Returns a JSON-serializable representation of this object.
*
* @return A JSON-serializable representation of this object.
*/
toJSON() {
return this.val();
}
/** Recursive function to check if keys are numeric & convert node object to array if they are
*
* @hidden
*/
_checkAndConvertToArray(node) {
if (node === null || typeof node === "undefined") {
return null;
}
if (typeof node !== "object") {
return node;
}
const obj = {};
let numKeys = 0;
let maxKey = 0;
let allIntegerKeys = true;
for (const key in node) {
if (!node.hasOwnProperty(key)) {
continue;
}
const childNode = node[key];
const v = this._checkAndConvertToArray(childNode);
if (v === null) {
// Empty child node
continue;
}
obj[key] = v;
numKeys++;
const integerRegExp = /^(0|[1-9]\d*)$/;
if (allIntegerKeys && integerRegExp.test(key)) {
maxKey = Math.max(maxKey, Number(key));
}
else {
allIntegerKeys = false;
}
}
if (numKeys === 0) {
// Empty node
return null;
}
if (allIntegerKeys && maxKey < 2 * numKeys) {
// convert to array.
const array = [];
for (const key of Object.keys(obj)) {
array[key] = obj[key];
}
return array;
}
return obj;
}
/** @hidden */
_dup(childPath) {
const dup = new DataSnapshot(this._data, undefined, this.app, this.instance);
[dup._path, dup._childPath] = [this._path, this._childPath];
if (childPath) {
dup._childPath = (0, path_1.joinPath)(dup._childPath, childPath);
}
return dup;
}
/** @hidden */
_fullPath() {
return (this._path || "") + "/" + (this._childPath || "");
}
}
exports.DataSnapshot = DataSnapshot;

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,110 @@
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2023 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBeforeSnapshotFromJson = exports.createSnapshotFromJson = exports.createBeforeSnapshotFromProtobuf = exports.createSnapshotFromProtobuf = void 0;
const firestore = require("firebase-admin/firestore");
const logger = require("../../logger");
const app_1 = require("../../common/app");
const compiledFirestore_1 = require("../../../protos/compiledFirestore");
const encoder_1 = require("../../common/utilities/encoder");
/** static-complied protobufs */
const DocumentEventData = compiledFirestore_1.google.events.cloud.firestore.v1.DocumentEventData;
let firestoreInstance;
/** @hidden */
function _getValueProto(data, resource, valueFieldName) {
const value = data === null || data === void 0 ? void 0 : data[valueFieldName];
if (typeof value === "undefined" ||
value === null ||
(typeof value === "object" && !Object.keys(value).length)) {
// Firestore#snapshot_ takes resource string instead of proto for a non-existent snapshot
return resource;
}
const proto = {
fields: (value === null || value === void 0 ? void 0 : value.fields) || {},
createTime: (0, encoder_1.dateToTimestampProto)(value === null || value === void 0 ? void 0 : value.createTime),
updateTime: (0, encoder_1.dateToTimestampProto)(value === null || value === void 0 ? void 0 : value.updateTime),
name: (value === null || value === void 0 ? void 0 : value.name) || resource,
};
return proto;
}
/** @internal */
function createSnapshotFromProtobuf(data, path, databaseId) {
if (!firestoreInstance) {
firestoreInstance = firestore.getFirestore((0, app_1.getApp)(), databaseId);
}
try {
const dataBuffer = Buffer.from(data);
const firestoreDecoded = DocumentEventData.decode(dataBuffer);
return firestoreInstance.snapshot_(firestoreDecoded.value || path, null, "protobufJS");
}
catch (err) {
logger.error("Failed to decode protobuf and create a snapshot.");
throw err;
}
}
exports.createSnapshotFromProtobuf = createSnapshotFromProtobuf;
/** @internal */
function createBeforeSnapshotFromProtobuf(data, path, databaseId) {
if (!firestoreInstance) {
firestoreInstance = firestore.getFirestore((0, app_1.getApp)(), databaseId);
}
try {
const dataBuffer = Buffer.from(data);
const firestoreDecoded = DocumentEventData.decode(dataBuffer);
return firestoreInstance.snapshot_(firestoreDecoded.oldValue || path, null, "protobufJS");
}
catch (err) {
logger.error("Failed to decode protobuf and create a before snapshot.");
throw err;
}
}
exports.createBeforeSnapshotFromProtobuf = createBeforeSnapshotFromProtobuf;
/** @internal */
function createSnapshotFromJson(data, source, createTime, updateTime, databaseId) {
if (!firestoreInstance) {
firestoreInstance = databaseId
? firestore.getFirestore((0, app_1.getApp)(), databaseId)
: firestore.getFirestore((0, app_1.getApp)());
}
const valueProto = _getValueProto(data, source, "value");
let timeString = createTime || updateTime;
if (!timeString) {
logger.warn("Snapshot has no readTime. Using now()");
timeString = new Date().toISOString();
}
const readTime = (0, encoder_1.dateToTimestampProto)(timeString);
return firestoreInstance.snapshot_(valueProto, readTime, "json");
}
exports.createSnapshotFromJson = createSnapshotFromJson;
/** @internal */
function createBeforeSnapshotFromJson(data, source, createTime, updateTime, databaseId) {
if (!firestoreInstance) {
firestoreInstance = databaseId
? firestore.getFirestore((0, app_1.getApp)(), databaseId)
: firestore.getFirestore((0, app_1.getApp)());
}
const oldValueProto = _getValueProto(data, source, "oldValue");
const oldReadTime = (0, encoder_1.dateToTimestampProto)(createTime || updateTime);
return firestoreInstance.snapshot_(oldValueProto, oldReadTime, "json");
}
exports.createBeforeSnapshotFromJson = createBeforeSnapshotFromJson;

View File

@@ -0,0 +1,240 @@
/// <reference types="node" />
import * as express from "express";
import { DecodedAppCheckToken } from "firebase-admin/app-check";
import { DecodedIdToken } from "firebase-admin/auth";
import { TaskContext } from "./tasks";
/** An express request with the wire format representation of the request body. */
export interface Request extends express.Request {
/** The wire format representation of the request body. */
rawBody: Buffer;
}
/**
* The interface for AppCheck tokens verified in Callable functions
*/
export interface AppCheckData {
/**
* The app ID of a Firebase App attested by the App Check token.
*/
appId: string;
/**
* Decoded App Check token.
*/
token: DecodedAppCheckToken;
/**
* Indicates if the token has been consumed.
*
* @remarks
* `false` value indicates that this is the first time the App Check service has seen this token and marked the
* token as consumed for future use of the token.
*
* `true` value indicates the token has previously been marked as consumed by the App Check service. In this case,
* consider taking extra precautions, such as rejecting the request or requiring additional security checks.
*/
alreadyConsumed?: boolean;
}
/**
* The interface for Auth tokens verified in Callable functions
*/
export interface AuthData {
uid: string;
token: DecodedIdToken;
}
/**
* The interface for metadata for the API as passed to the handler.
*/
export interface CallableContext {
/**
* The result of decoding and verifying a Firebase AppCheck token.
*/
app?: AppCheckData;
/**
* The result of decoding and verifying a Firebase Auth ID token.
*/
auth?: AuthData;
/**
* An unverified token for a Firebase Instance ID.
*/
instanceIdToken?: string;
/**
* The raw request handled by the callable.
*/
rawRequest: Request;
}
/**
* The request used to call a callable function.
*/
export interface CallableRequest<T = any> {
/**
* The parameters used by a client when calling this function.
*/
data: T;
/**
* The result of decoding and verifying a Firebase App Check token.
*/
app?: AppCheckData;
/**
* The result of decoding and verifying a Firebase Auth ID token.
*/
auth?: AuthData;
/**
* An unverified token for a Firebase Instance ID.
*/
instanceIdToken?: string;
/**
* The raw request handled by the callable.
*/
rawRequest: Request;
/**
* Whether this is a streaming request.
* Code can be optimized by not trying to generate a stream of chunks to
* call `response.sendChunk` if `request.acceptsStreaming` is false.
* It is always safe, however, to call `response.sendChunk` as this will
* noop if `acceptsStreaming` is false.
*/
acceptsStreaming: boolean;
}
/**
* `CallableProxyResponse` allows streaming response chunks and listening to signals
* triggered in events such as a disconnect.
*/
export interface CallableResponse<T = unknown> {
/**
* Writes a chunk of the response body to the client. This method can be called
* multiple times to stream data progressively.
* Returns a promise of whether the data was written. This can be false, for example,
* if the request was not a streaming request. Rejects if there is a network error.
*/
sendChunk: (chunk: T) => Promise<boolean>;
/**
* An `AbortSignal` that is triggered when the client disconnects or the
* request is terminated prematurely.
*/
signal: AbortSignal;
}
/**
* The set of Firebase Functions status codes. The codes are the same at the
* ones exposed by {@link https://github.com/grpc/grpc/blob/master/doc/statuscodes.md | gRPC}.
*
* @remarks
* Possible values:
*
* - `cancelled`: The operation was cancelled (typically by the caller).
*
* - `unknown`: Unknown error or an error from a different error domain.
*
* - `invalid-argument`: Client specified an invalid argument. Note that this
* differs from `failed-precondition`. `invalid-argument` indicates
* arguments that are problematic regardless of the state of the system
* (e.g. an invalid field name).
*
* - `deadline-exceeded`: Deadline expired before operation could complete.
* For operations that change the state of the system, this error may be
* returned even if the operation has completed successfully. For example,
* a successful response from a server could have been delayed long enough
* for the deadline to expire.
*
* - `not-found`: Some requested document was not found.
*
* - `already-exists`: Some document that we attempted to create already
* exists.
*
* - `permission-denied`: The caller does not have permission to execute the
* specified operation.
*
* - `resource-exhausted`: Some resource has been exhausted, perhaps a
* per-user quota, or perhaps the entire file system is out of space.
*
* - `failed-precondition`: Operation was rejected because the system is not
* in a state required for the operation's execution.
*
* - `aborted`: The operation was aborted, typically due to a concurrency
* issue like transaction aborts, etc.
*
* - `out-of-range`: Operation was attempted past the valid range.
*
* - `unimplemented`: Operation is not implemented or not supported/enabled.
*
* - `internal`: Internal errors. Means some invariants expected by
* underlying system has been broken. If you see one of these errors,
* something is very broken.
*
* - `unavailable`: The service is currently unavailable. This is most likely
* a transient condition and may be corrected by retrying with a backoff.
*
* - `data-loss`: Unrecoverable data loss or corruption.
*
* - `unauthenticated`: The request does not have valid authentication
* credentials for the operation.
*/
export type FunctionsErrorCode = "ok" | "cancelled" | "unknown" | "invalid-argument" | "deadline-exceeded" | "not-found" | "already-exists" | "permission-denied" | "resource-exhausted" | "failed-precondition" | "aborted" | "out-of-range" | "unimplemented" | "internal" | "unavailable" | "data-loss" | "unauthenticated";
/** @hidden */
export type CanonicalErrorCodeName = "OK" | "CANCELLED" | "UNKNOWN" | "INVALID_ARGUMENT" | "DEADLINE_EXCEEDED" | "NOT_FOUND" | "ALREADY_EXISTS" | "PERMISSION_DENIED" | "UNAUTHENTICATED" | "RESOURCE_EXHAUSTED" | "FAILED_PRECONDITION" | "ABORTED" | "OUT_OF_RANGE" | "UNIMPLEMENTED" | "INTERNAL" | "UNAVAILABLE" | "DATA_LOSS";
/** @hidden */
interface HttpErrorCode {
canonicalName: CanonicalErrorCodeName;
status: number;
}
/** @hidden */
interface HttpErrorWireFormat {
details?: unknown;
message: string;
status: CanonicalErrorCodeName;
}
/**
* An explicit error that can be thrown from a handler to send an error to the
* client that called the function.
*/
export declare class HttpsError extends Error {
/**
* A standard error code that will be returned to the client. This also
* determines the HTTP status code of the response, as defined in code.proto.
*/
readonly code: FunctionsErrorCode;
/**
* Extra data to be converted to JSON and included in the error response.
*/
readonly details: unknown;
/**
* A wire format representation of a provided error code.
*
* @hidden
*/
readonly httpErrorCode: HttpErrorCode;
constructor(code: FunctionsErrorCode, message: string, details?: unknown);
/**
* Returns a JSON-serializable representation of this object.
*/
toJSON(): HttpErrorWireFormat;
}
/** @hidden */
interface HttpRequest extends Request {
body: {
data: any;
};
}
/** @hidden */
export declare function isValidRequest(req: Request): req is HttpRequest;
/**
* Encodes arbitrary data in our special format for JSON.
* This is exposed only for testing.
*/
/** @hidden */
export declare function encode(data: any): any;
/**
* Decodes our special format for JSON into native types.
* This is exposed only for testing.
*/
/** @hidden */
export declare function decode(data: any): any;
/**
* Be careful when changing token status values.
*
* Users are encouraged to setup log-based metric based on these values, and
* changing their values may cause their metrics to break.
*
*/
/** @hidden */
type TokenStatus = "MISSING" | "VALID" | "INVALID";
/** @interanl */
export declare function checkAuthToken(req: Request, ctx: CallableContext | TaskContext): Promise<TokenStatus>;
export {};

View File

@@ -0,0 +1,590 @@
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2021 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.DEFAULT_HEARTBEAT_SECONDS = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
const cors = require("cors");
const logger = require("../../logger");
// TODO(inlined): Decide whether we want to un-version apps or whether we want a
// different strategy
const app_check_1 = require("firebase-admin/app-check");
const auth_1 = require("firebase-admin/auth");
const app_1 = require("../app");
const debug_1 = require("../debug");
const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
/** @internal */
exports.CALLABLE_AUTH_HEADER = "x-callable-context-auth";
/** @internal */
exports.ORIGINAL_AUTH_HEADER = "x-original-auth";
/** @internal */
exports.DEFAULT_HEARTBEAT_SECONDS = 30;
/**
* Standard error codes and HTTP statuses for different ways a request can fail,
* as defined by:
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
*
* This map is used primarily to convert from a client error code string to
* to the HTTP format error code string and status, and make sure it's in the
* supported set.
*/
const errorCodeMap = {
ok: { canonicalName: "OK", status: 200 },
cancelled: { canonicalName: "CANCELLED", status: 499 },
unknown: { canonicalName: "UNKNOWN", status: 500 },
"invalid-argument": { canonicalName: "INVALID_ARGUMENT", status: 400 },
"deadline-exceeded": { canonicalName: "DEADLINE_EXCEEDED", status: 504 },
"not-found": { canonicalName: "NOT_FOUND", status: 404 },
"already-exists": { canonicalName: "ALREADY_EXISTS", status: 409 },
"permission-denied": { canonicalName: "PERMISSION_DENIED", status: 403 },
unauthenticated: { canonicalName: "UNAUTHENTICATED", status: 401 },
"resource-exhausted": { canonicalName: "RESOURCE_EXHAUSTED", status: 429 },
"failed-precondition": { canonicalName: "FAILED_PRECONDITION", status: 400 },
aborted: { canonicalName: "ABORTED", status: 409 },
"out-of-range": { canonicalName: "OUT_OF_RANGE", status: 400 },
unimplemented: { canonicalName: "UNIMPLEMENTED", status: 501 },
internal: { canonicalName: "INTERNAL", status: 500 },
unavailable: { canonicalName: "UNAVAILABLE", status: 503 },
"data-loss": { canonicalName: "DATA_LOSS", status: 500 },
};
/**
* An explicit error that can be thrown from a handler to send an error to the
* client that called the function.
*/
class HttpsError extends Error {
constructor(code, message, details) {
super(message);
// A sanity check for non-TypeScript consumers.
if (code in errorCodeMap === false) {
throw new Error(`Unknown error code: ${code}.`);
}
this.code = code;
this.details = details;
this.httpErrorCode = errorCodeMap[code];
}
/**
* Returns a JSON-serializable representation of this object.
*/
toJSON() {
const { details, httpErrorCode: { canonicalName: status }, message, } = this;
return {
...(details === undefined ? {} : { details }),
message,
status,
};
}
}
exports.HttpsError = HttpsError;
/** @hidden */
// Returns true if req is a properly formatted callable request.
function isValidRequest(req) {
// The body must not be empty.
if (!req.body) {
logger.warn("Request is missing body.");
return false;
}
// Make sure it's a POST.
if (req.method !== "POST") {
logger.warn("Request has invalid method.", req.method);
return false;
}
// Check that the Content-Type is JSON.
let contentType = (req.header("Content-Type") || "").toLowerCase();
// If it has a charset, just ignore it for now.
const semiColon = contentType.indexOf(";");
if (semiColon >= 0) {
contentType = contentType.slice(0, semiColon).trim();
}
if (contentType !== "application/json") {
logger.warn("Request has incorrect Content-Type.", contentType);
return false;
}
// The body must have data.
if (typeof req.body.data === "undefined") {
logger.warn("Request body is missing data.", req.body);
return false;
}
// TODO(klimt): Allow only specific http headers.
// Verify that the body does not have any extra fields.
const extraKeys = Object.keys(req.body).filter((field) => field !== "data");
if (extraKeys.length !== 0) {
logger.warn("Request body has extra fields: ", extraKeys.join(", "));
return false;
}
return true;
}
exports.isValidRequest = isValidRequest;
/** @hidden */
const LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value";
/** @hidden */
const UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value";
/**
* Encodes arbitrary data in our special format for JSON.
* This is exposed only for testing.
*/
/** @hidden */
function encode(data) {
if (data === null || typeof data === "undefined") {
return null;
}
if (data instanceof Number) {
data = data.valueOf();
}
if (Number.isFinite(data)) {
// Any number in JS is safe to put directly in JSON and parse as a double
// without any loss of precision.
return data;
}
if (typeof data === "boolean") {
return data;
}
if (typeof data === "string") {
return data;
}
if (Array.isArray(data)) {
return data.map(encode);
}
if (typeof data === "object" || typeof data === "function") {
// Sadly we don't have Object.fromEntries in Node 10, so we can't use a single
// list comprehension
const obj = {};
for (const [k, v] of Object.entries(data)) {
obj[k] = encode(v);
}
return obj;
}
// If we got this far, the data is not encodable.
logger.error("Data cannot be encoded in JSON.", data);
throw new Error(`Data cannot be encoded in JSON: ${data}`);
}
exports.encode = encode;
/**
* Decodes our special format for JSON into native types.
* This is exposed only for testing.
*/
/** @hidden */
function decode(data) {
if (data === null) {
return data;
}
if (data["@type"]) {
switch (data["@type"]) {
case LONG_TYPE:
// Fall through and handle this the same as unsigned.
case UNSIGNED_LONG_TYPE: {
// Technically, this could work return a valid number for malformed
// data if there was a number followed by garbage. But it's just not
// worth all the extra code to detect that case.
const value = parseFloat(data.value);
if (isNaN(value)) {
logger.error("Data cannot be decoded from JSON.", data);
throw new Error(`Data cannot be decoded from JSON: ${data}`);
}
return value;
}
default: {
logger.error("Data cannot be decoded from JSON.", data);
throw new Error(`Data cannot be decoded from JSON: ${data}`);
}
}
}
if (Array.isArray(data)) {
return data.map(decode);
}
if (typeof data === "object") {
const obj = {};
for (const [k, v] of Object.entries(data)) {
obj[k] = decode(v);
}
return obj;
}
// Anything else is safe to return.
return data;
}
exports.decode = decode;
/** @internal */
function unsafeDecodeToken(token) {
if (!JWT_REGEX.test(token)) {
return {};
}
const components = token.split(".").map((s) => Buffer.from(s, "base64").toString());
let payload = components[1];
if (typeof payload === "string") {
try {
const obj = JSON.parse(payload);
if (typeof obj === "object") {
payload = obj;
}
}
catch (e) {
// ignore error
}
}
return payload;
}
exports.unsafeDecodeToken = unsafeDecodeToken;
/**
* Decode, but not verify, a Auth ID token.
*
* Do not use in production. Token should always be verified using the Admin SDK.
*
* This is exposed only for testing.
*/
/** @internal */
function unsafeDecodeIdToken(token) {
const decoded = unsafeDecodeToken(token);
decoded.uid = decoded.sub;
return decoded;
}
exports.unsafeDecodeIdToken = unsafeDecodeIdToken;
/**
* Decode, but not verify, an App Check token.
*
* Do not use in production. Token should always be verified using the Admin SDK.
*
* This is exposed only for testing.
*/
/** @internal */
function unsafeDecodeAppCheckToken(token) {
const decoded = unsafeDecodeToken(token);
decoded.app_id = decoded.sub;
return decoded;
}
exports.unsafeDecodeAppCheckToken = unsafeDecodeAppCheckToken;
/**
* Check and verify tokens included in the requests. Once verified, tokens
* are injected into the callable context.
*
* @param {Request} req - Request sent to the Callable function.
* @param {CallableContext} ctx - Context to be sent to callable function handler.
* @returns {CallableTokenStatus} Status of the token verifications.
*/
/** @internal */
async function checkTokens(req, ctx, options) {
const verifications = {
app: "INVALID",
auth: "INVALID",
};
[verifications.auth, verifications.app] = await Promise.all([
checkAuthToken(req, ctx),
checkAppCheckToken(req, ctx, options),
]);
const logPayload = {
verifications,
"logging.googleapis.com/labels": {
"firebase-log-type": "callable-request-verification",
},
};
const errs = [];
if (verifications.app === "INVALID") {
errs.push("AppCheck token was rejected.");
}
if (verifications.auth === "INVALID") {
errs.push("Auth token was rejected.");
}
if (errs.length === 0) {
logger.debug("Callable request verification passed", logPayload);
}
else {
logger.warn(`Callable request verification failed: ${errs.join(" ")}`, logPayload);
}
return verifications;
}
/** @interanl */
async function checkAuthToken(req, ctx) {
const authorization = req.header("Authorization");
if (!authorization) {
return "MISSING";
}
const match = authorization.match(/^Bearer (.*)$/i);
if (!match) {
return "INVALID";
}
const idToken = match[1];
try {
let authToken;
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")) {
authToken = unsafeDecodeIdToken(idToken);
}
else {
authToken = await (0, auth_1.getAuth)((0, app_1.getApp)()).verifyIdToken(idToken);
}
ctx.auth = {
uid: authToken.uid,
token: authToken,
};
return "VALID";
}
catch (err) {
logger.warn("Failed to validate auth token.", err);
return "INVALID";
}
}
exports.checkAuthToken = checkAuthToken;
/** @internal */
async function checkAppCheckToken(req, ctx, options) {
var _a;
const appCheckToken = req.header("X-Firebase-AppCheck");
if (!appCheckToken) {
return "MISSING";
}
try {
let appCheckData;
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")) {
const decodedToken = unsafeDecodeAppCheckToken(appCheckToken);
appCheckData = { appId: decodedToken.app_id, token: decodedToken };
if (options.consumeAppCheckToken) {
appCheckData.alreadyConsumed = false;
}
}
else {
const appCheck = (0, app_check_1.getAppCheck)((0, app_1.getApp)());
if (options.consumeAppCheckToken) {
if (((_a = appCheck.verifyToken) === null || _a === void 0 ? void 0 : _a.length) === 1) {
const errorMsg = "Unsupported version of the Admin SDK." +
" App Check token will not be consumed." +
" Please upgrade the firebase-admin to the latest version.";
logger.error(errorMsg);
throw new HttpsError("internal", "Internal Error");
}
appCheckData = await (0, app_check_1.getAppCheck)((0, app_1.getApp)()).verifyToken(appCheckToken, { consume: true });
}
else {
appCheckData = await (0, app_check_1.getAppCheck)((0, app_1.getApp)()).verifyToken(appCheckToken);
}
}
ctx.app = appCheckData;
return "VALID";
}
catch (err) {
logger.warn("Failed to validate AppCheck token.", err);
if (err instanceof HttpsError) {
throw err;
}
return "INVALID";
}
}
/** @internal */
function onCallHandler(options, handler, version) {
const wrapped = wrapOnCallHandler(options, handler, version);
return (req, res) => {
return new Promise((resolve) => {
res.on("finish", resolve);
cors(options.cors)(req, res, () => {
resolve(wrapped(req, res));
});
});
};
}
exports.onCallHandler = onCallHandler;
function encodeSSE(data) {
return `data: ${JSON.stringify(data)}\n\n`;
}
/** @internal */
function wrapOnCallHandler(options, handler, version) {
return async (req, res) => {
var _a;
const abortController = new AbortController();
let heartbeatInterval = null;
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
const clearScheduledHeartbeat = () => {
if (heartbeatInterval) {
clearTimeout(heartbeatInterval);
heartbeatInterval = null;
}
};
const scheduleHeartbeat = () => {
clearScheduledHeartbeat();
if (!abortController.signal.aborted) {
heartbeatInterval = setTimeout(() => {
if (!abortController.signal.aborted) {
res.write(": ping\n\n");
scheduleHeartbeat();
}
}, heartbeatSeconds * 1000);
}
};
res.on("close", () => {
clearScheduledHeartbeat();
abortController.abort();
});
try {
if (!isValidRequest(req)) {
logger.error("Invalid request, unable to process.");
throw new HttpsError("invalid-argument", "Bad Request");
}
const context = { rawRequest: req };
// TODO(colerogers): yank this when we release a breaking change of the CLI that removes
// our monkey-patching code referenced below and increases the minimum supported SDK version.
//
// Note: This code is needed to fix v1 callable functions in the emulator with a monorepo setup.
// The original monkey-patched code lived in the functionsEmulatorRuntime
// (link: https://github.com/firebase/firebase-tools/blob/accea7abda3cc9fa6bb91368e4895faf95281c60/src/emulator/functionsEmulatorRuntime.ts#L480)
// and was not compatible with how monorepos separate out packages (see https://github.com/firebase/firebase-tools/issues/5210).
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification") && version === "gcfv1") {
const authContext = context.rawRequest.header(exports.CALLABLE_AUTH_HEADER);
if (authContext) {
logger.debug("Callable functions auth override", {
key: exports.CALLABLE_AUTH_HEADER,
value: authContext,
});
context.auth = JSON.parse(decodeURIComponent(authContext));
delete context.rawRequest.headers[exports.CALLABLE_AUTH_HEADER];
}
const originalAuth = context.rawRequest.header(exports.ORIGINAL_AUTH_HEADER);
if (originalAuth) {
context.rawRequest.headers["authorization"] = originalAuth;
delete context.rawRequest.headers[exports.ORIGINAL_AUTH_HEADER];
}
}
const tokenStatus = await checkTokens(req, context, options);
if (tokenStatus.auth === "INVALID") {
throw new HttpsError("unauthenticated", "Unauthenticated");
}
if (tokenStatus.app === "INVALID") {
if (options.enforceAppCheck) {
throw new HttpsError("unauthenticated", "Unauthenticated");
}
else {
logger.warn("Allowing request with invalid AppCheck token because enforcement is disabled");
}
}
if (tokenStatus.app === "MISSING" && options.enforceAppCheck) {
throw new HttpsError("unauthenticated", "Unauthenticated");
}
const instanceId = req.header("Firebase-Instance-ID-Token");
if (instanceId) {
// Validating the token requires an http request, so we don't do it.
// If the user wants to use it for something, it will be validated then.
// Currently, the only real use case for this token is for sending
// pushes with FCM. In that case, the FCM APIs will validate the token.
context.instanceIdToken = req.header("Firebase-Instance-ID-Token");
}
const acceptsStreaming = req.header("accept") === "text/event-stream";
if (acceptsStreaming && version === "gcfv1") {
// streaming responses are not supported in v1 callable
throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
}
const data = decode(req.body.data);
if (options.authPolicy) {
const authorized = await options.authPolicy((_a = context.auth) !== null && _a !== void 0 ? _a : null, data);
if (!authorized) {
throw new HttpsError("permission-denied", "Permission Denied");
}
}
let result;
if (version === "gcfv1") {
result = await handler(data, context);
}
else {
const arg = {
...context,
data,
acceptsStreaming,
};
const responseProxy = {
sendChunk(chunk) {
// if client doesn't accept sse-protocol, response.write() is no-op.
if (!acceptsStreaming) {
return Promise.resolve(false);
}
// if connection is already closed, response.write() is no-op.
if (abortController.signal.aborted) {
return Promise.resolve(false);
}
const formattedData = encodeSSE({ message: chunk });
let resolve;
let reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const wrote = res.write(formattedData, (error) => {
if (error) {
reject(error);
return;
}
resolve(wrote);
});
// Reset heartbeat timer after successful write
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
scheduleHeartbeat();
}
return p;
},
signal: abortController.signal,
};
if (acceptsStreaming) {
// SSE always responds with 200
res.status(200);
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
scheduleHeartbeat();
}
}
// For some reason the type system isn't picking up that the handler
// is a one argument function.
result = await handler(arg, responseProxy);
clearScheduledHeartbeat();
}
if (!abortController.signal.aborted) {
// Encode the result as JSON to preserve types like Dates.
result = encode(result);
// If there was some result, encode it in the body.
const responseBody = { result };
if (acceptsStreaming) {
res.write(encodeSSE(responseBody));
res.end();
}
else {
res.status(200).send(responseBody);
}
}
else {
res.end();
}
}
catch (err) {
if (!abortController.signal.aborted) {
let httpErr = err;
if (!(err instanceof HttpsError)) {
// This doesn't count as an 'explicit' error.
logger.error("Unhandled error", err);
httpErr = new HttpsError("internal", "INTERNAL");
}
const { status } = httpErr.httpErrorCode;
const body = { error: httpErr.toJSON() };
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
res.write(encodeSSE(body));
res.end();
}
else {
res.status(status).send(body);
}
}
else {
res.end();
}
}
finally {
clearScheduledHeartbeat();
}
};
}

View File

@@ -0,0 +1,258 @@
import * as auth from "firebase-admin/auth";
import { EventContext } from "../../v1/cloud-functions";
import { HttpsError } from "./https";
export { HttpsError };
/**
* Shorthand auth blocking events from GCIP.
* @hidden
* @alpha
*/
export type AuthBlockingEventType = "beforeCreate" | "beforeSignIn" | "beforeSendEmail" | "beforeSendSms";
/**
* The `UserRecord` passed to Cloud Functions is the same
* {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.userrecord | UserRecord}
* that is returned by the Firebase Admin SDK.
*/
export type UserRecord = auth.UserRecord;
/**
* `UserInfo` that is part of the `UserRecord`.
*/
export type UserInfo = auth.UserInfo;
/**
* Helper class to create the user metadata in a `UserRecord` object.
*/
export declare class UserRecordMetadata implements auth.UserMetadata {
creationTime: string;
lastSignInTime: string;
constructor(creationTime: string, lastSignInTime: string);
/** Returns a plain JavaScript object with the properties of UserRecordMetadata. */
toJSON(): AuthUserMetadata;
}
/**
* Helper function that creates a `UserRecord` class from data sent over the wire.
* @param wireData data sent over the wire
* @returns an instance of `UserRecord` with correct toJSON functions
*/
export declare function userRecordConstructor(wireData: Record<string, unknown>): UserRecord;
/**
* User info that is part of the `AuthUserRecord`.
*/
export interface AuthUserInfo {
/**
* The user identifier for the linked provider.
*/
uid: string;
/**
* The display name for the linked provider.
*/
displayName: string;
/**
* The email for the linked provider.
*/
email: string;
/**
* The photo URL for the linked provider.
*/
photoURL: string;
/**
* The linked provider ID (for example, "google.com" for the Google provider).
*/
providerId: string;
/**
* The phone number for the linked provider.
*/
phoneNumber: string;
}
/**
* Additional metadata about the user.
*/
export interface AuthUserMetadata {
/**
* The date the user was created, formatted as a UTC string.
*/
creationTime: string;
/**
* The date the user last signed in, formatted as a UTC string.
*/
lastSignInTime: string;
}
/**
* Interface representing the common properties of a user-enrolled second factor.
*/
export interface AuthMultiFactorInfo {
/**
* The ID of the enrolled second factor. This ID is unique to the user.
*/
uid: string;
/**
* The optional display name of the enrolled second factor.
*/
displayName?: string;
/**
* The type identifier of the second factor. For SMS second factors, this is `phone`.
*/
factorId: string;
/**
* The optional date the second factor was enrolled, formatted as a UTC string.
*/
enrollmentTime?: string;
/**
* The phone number associated with a phone second factor.
*/
phoneNumber?: string;
}
/**
* The multi-factor related properties for the current user, if available.
*/
export interface AuthMultiFactorSettings {
/**
* List of second factors enrolled with the current user.
*/
enrolledFactors: AuthMultiFactorInfo[];
}
/**
* The `UserRecord` passed to auth blocking functions from the identity platform.
*/
export interface AuthUserRecord {
/**
* The user's `uid`.
*/
uid: string;
/**
* The user's primary email, if set.
*/
email?: string;
/**
* Whether or not the user's primary email is verified.
*/
emailVerified: boolean;
/**
* The user's display name.
*/
displayName?: string;
/**
* The user's photo URL.
*/
photoURL?: string;
/**
* The user's primary phone number, if set.
*/
phoneNumber?: string;
/**
* Whether or not the user is disabled: `true` for disabled; `false` for
* enabled.
*/
disabled: boolean;
/**
* Additional metadata about the user.
*/
metadata: AuthUserMetadata;
/**
* An array of providers (for example, Google, Facebook) linked to the user.
*/
providerData: AuthUserInfo[];
/**
* The user's hashed password (base64-encoded).
*/
passwordHash?: string;
/**
* The user's password salt (base64-encoded).
*/
passwordSalt?: string;
/**
* The user's custom claims object if available, typically used to define
* user roles and propagated to an authenticated user's ID token.
*/
customClaims?: Record<string, any>;
/**
* The ID of the tenant the user belongs to, if available.
*/
tenantId?: string | null;
/**
* The date the user's tokens are valid after, formatted as a UTC string.
*/
tokensValidAfterTime?: string;
/**
* The multi-factor related properties for the current user, if available.
*/
multiFactor?: AuthMultiFactorSettings;
}
/** The additional user info component of the auth event context */
export interface AdditionalUserInfo {
providerId?: string;
profile?: any;
username?: string;
isNewUser: boolean;
recaptchaScore?: number;
email?: string;
phoneNumber?: string;
}
/** The credential component of the auth event context */
export interface Credential {
claims?: {
[key: string]: any;
};
idToken?: string;
accessToken?: string;
refreshToken?: string;
expirationTime?: string;
secret?: string;
providerId: string;
signInMethod: string;
}
/**
* Possible types of emails as described by the GCIP backend, which can be:
* - A sign-in email
* - A password reset email
*/
export type EmailType = "EMAIL_SIGN_IN" | "PASSWORD_RESET";
/**
* The type of SMS message, which can be:
* - A sign-in or sign up SMS message
* - A multi-factor sign-in SMS message
* - A multi-factor enrollment SMS message
*/
export type SmsType = "SIGN_IN_OR_SIGN_UP" | "MULTI_FACTOR_SIGN_IN" | "MULTI_FACTOR_ENROLLMENT";
/** Defines the auth event context for blocking events */
export interface AuthEventContext extends EventContext {
locale?: string;
ipAddress: string;
userAgent: string;
additionalUserInfo?: AdditionalUserInfo;
credential?: Credential;
emailType?: EmailType;
smsType?: SmsType;
}
/** Defines the auth event for 2nd gen blocking events */
export interface AuthBlockingEvent extends AuthEventContext {
data?: AuthUserRecord;
}
/** The reCAPTCHA action options. */
export type RecaptchaActionOptions = "ALLOW" | "BLOCK";
/** The handler response type for `beforeEmailSent` blocking events */
export interface BeforeEmailResponse {
recaptchaActionOverride?: RecaptchaActionOptions;
}
/** The handler response type for `beforeSmsSent` blocking events */
export interface BeforeSmsResponse {
recaptchaActionOverride?: RecaptchaActionOptions;
}
/** The handler response type for `beforeCreate` blocking events */
export interface BeforeCreateResponse {
displayName?: string;
disabled?: boolean;
emailVerified?: boolean;
photoURL?: string;
customClaims?: object;
recaptchaActionOverride?: RecaptchaActionOptions;
}
/** The handler response type for `beforeSignIn` blocking events */
export interface BeforeSignInResponse extends BeforeCreateResponse {
sessionClaims?: object;
}
export type MaybeAsync<T> = T | Promise<T>;
export type HandlerV1 = (userOrContext: AuthUserRecord | AuthEventContext, context?: AuthEventContext) => MaybeAsync<BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void>;
export type HandlerV2 = (event: AuthBlockingEvent) => MaybeAsync<BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void>;
export type AuthBlockingEventHandler = (HandlerV1 | HandlerV2) & {
platform: "gcfv1" | "gcfv2";
};

View File

@@ -0,0 +1,494 @@
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2022 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapHandler = exports.getUpdateMask = exports.validateAuthResponse = exports.parseAuthEventContext = exports.generateResponsePayload = exports.parseAuthUserRecord = exports.parseMultiFactor = exports.parseDate = exports.parseProviderData = exports.parseMetadata = exports.isValidRequest = exports.userRecordConstructor = exports.UserRecordMetadata = exports.HttpsError = void 0;
const auth = require("firebase-admin/auth");
const logger = require("../../logger");
const app_1 = require("../app");
const debug_1 = require("../debug");
const https_1 = require("./https");
Object.defineProperty(exports, "HttpsError", { enumerable: true, get: function () { return https_1.HttpsError; } });
const DISALLOWED_CUSTOM_CLAIMS = [
"acr",
"amr",
"at_hash",
"aud",
"auth_time",
"azp",
"cnf",
"c_hash",
"exp",
"iat",
"iss",
"jti",
"nbf",
"nonce",
"firebase",
];
const CLAIMS_MAX_PAYLOAD_SIZE = 1000;
const EVENT_MAPPING = {
beforeCreate: "providers/cloud.auth/eventTypes/user.beforeCreate",
beforeSignIn: "providers/cloud.auth/eventTypes/user.beforeSignIn",
beforeSendEmail: "providers/cloud.auth/eventTypes/user.beforeSendEmail",
beforeSendSms: "providers/cloud.auth/eventTypes/user.beforeSendSms",
};
/**
* Helper class to create the user metadata in a `UserRecord` object.
*/
class UserRecordMetadata {
constructor(creationTime, lastSignInTime) {
this.creationTime = creationTime;
this.lastSignInTime = lastSignInTime;
}
/** Returns a plain JavaScript object with the properties of UserRecordMetadata. */
toJSON() {
return {
creationTime: this.creationTime,
lastSignInTime: this.lastSignInTime,
};
}
}
exports.UserRecordMetadata = UserRecordMetadata;
/**
* Helper function that creates a `UserRecord` class from data sent over the wire.
* @param wireData data sent over the wire
* @returns an instance of `UserRecord` with correct toJSON functions
*/
function userRecordConstructor(wireData) {
// Falsey values from the wire format proto get lost when converted to JSON, this adds them back.
const falseyValues = {
email: null,
emailVerified: false,
displayName: null,
photoURL: null,
phoneNumber: null,
disabled: false,
providerData: [],
customClaims: {},
passwordSalt: null,
passwordHash: null,
tokensValidAfterTime: null,
};
const record = { ...falseyValues, ...wireData };
const meta = record.metadata;
if (meta) {
record.metadata = new UserRecordMetadata(meta.createdAt || meta.creationTime, meta.lastSignedInAt || meta.lastSignInTime);
}
else {
record.metadata = new UserRecordMetadata(null, null);
}
record.toJSON = () => {
const { uid, email, emailVerified, displayName, photoURL, phoneNumber, disabled, passwordHash, passwordSalt, tokensValidAfterTime, } = record;
const json = {
uid,
email,
emailVerified,
displayName,
photoURL,
phoneNumber,
disabled,
passwordHash,
passwordSalt,
tokensValidAfterTime,
};
json.metadata = record.metadata.toJSON();
json.customClaims = JSON.parse(JSON.stringify(record.customClaims));
json.providerData = record.providerData.map((entry) => {
const newEntry = { ...entry };
newEntry.toJSON = () => entry;
return newEntry;
});
return json;
};
return record;
}
exports.userRecordConstructor = userRecordConstructor;
/**
* Checks for a valid identity platform web request, otherwise throws an HttpsError.
* @internal
*/
function isValidRequest(req) {
var _a, _b;
if (req.method !== "POST") {
logger.warn(`Request has invalid method "${req.method}".`);
return false;
}
const contentType = (req.header("Content-Type") || "").toLowerCase();
if (!contentType.includes("application/json")) {
logger.warn("Request has invalid header Content-Type.");
return false;
}
if (!((_b = (_a = req.body) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.jwt)) {
logger.warn("Request has an invalid body.");
return false;
}
return true;
}
exports.isValidRequest = isValidRequest;
/**
* Decode, but not verify, an Auth Blocking token.
*
* Do not use in production. Token should always be verified using the Admin SDK.
*
* This is exposed only for testing.
*/
function unsafeDecodeAuthBlockingToken(token) {
const decoded = (0, https_1.unsafeDecodeToken)(token);
decoded.uid = decoded.sub;
return decoded;
}
/**
* Helper function to parse the decoded metadata object into a `UserMetaData` object
* @internal
*/
function parseMetadata(metadata) {
const creationTime = (metadata === null || metadata === void 0 ? void 0 : metadata.creation_time)
? new Date(metadata.creation_time).toUTCString()
: null;
const lastSignInTime = (metadata === null || metadata === void 0 ? void 0 : metadata.last_sign_in_time)
? new Date(metadata.last_sign_in_time).toUTCString()
: null;
return {
creationTime,
lastSignInTime,
};
}
exports.parseMetadata = parseMetadata;
/**
* Helper function to parse the decoded user info array into an `AuthUserInfo` array.
* @internal
*/
function parseProviderData(providerData) {
const providers = [];
for (const provider of providerData) {
providers.push({
uid: provider.uid,
displayName: provider.display_name,
email: provider.email,
photoURL: provider.photo_url,
providerId: provider.provider_id,
phoneNumber: provider.phone_number,
});
}
return providers;
}
exports.parseProviderData = parseProviderData;
/**
* Helper function to parse the date into a UTC string.
* @internal
*/
function parseDate(tokensValidAfterTime) {
if (!tokensValidAfterTime) {
return null;
}
tokensValidAfterTime = tokensValidAfterTime * 1000;
try {
const date = new Date(tokensValidAfterTime);
if (!isNaN(date.getTime())) {
return date.toUTCString();
}
}
catch {
// ignore error
}
return null;
}
exports.parseDate = parseDate;
/**
* Helper function to parse the decoded enrolled factors into a valid MultiFactorSettings
* @internal
*/
function parseMultiFactor(multiFactor) {
if (!multiFactor) {
return null;
}
const parsedEnrolledFactors = [];
for (const factor of multiFactor.enrolled_factors || []) {
if (!factor.uid) {
throw new https_1.HttpsError("internal", "INTERNAL ASSERT FAILED: Invalid multi-factor info response");
}
const enrollmentTime = factor.enrollment_time
? new Date(factor.enrollment_time).toUTCString()
: null;
parsedEnrolledFactors.push({
uid: factor.uid,
factorId: factor.phone_number ? factor.factor_id || "phone" : factor.factor_id,
displayName: factor.display_name,
enrollmentTime,
phoneNumber: factor.phone_number,
});
}
if (parsedEnrolledFactors.length > 0) {
return {
enrolledFactors: parsedEnrolledFactors,
};
}
return null;
}
exports.parseMultiFactor = parseMultiFactor;
/**
* Parses the decoded user record into a valid UserRecord for use in the handler
* @internal
*/
function parseAuthUserRecord(decodedJWTUserRecord) {
if (!decodedJWTUserRecord.uid) {
throw new https_1.HttpsError("internal", "INTERNAL ASSERT FAILED: Invalid user response");
}
const disabled = decodedJWTUserRecord.disabled || false;
const metadata = parseMetadata(decodedJWTUserRecord.metadata);
const providerData = parseProviderData(decodedJWTUserRecord.provider_data);
const tokensValidAfterTime = parseDate(decodedJWTUserRecord.tokens_valid_after_time);
const multiFactor = parseMultiFactor(decodedJWTUserRecord.multi_factor);
return {
uid: decodedJWTUserRecord.uid,
email: decodedJWTUserRecord.email,
emailVerified: decodedJWTUserRecord.email_verified,
displayName: decodedJWTUserRecord.display_name,
photoURL: decodedJWTUserRecord.photo_url,
phoneNumber: decodedJWTUserRecord.phone_number,
disabled,
metadata,
providerData,
passwordHash: decodedJWTUserRecord.password_hash,
passwordSalt: decodedJWTUserRecord.password_salt,
customClaims: decodedJWTUserRecord.custom_claims,
tenantId: decodedJWTUserRecord.tenant_id,
tokensValidAfterTime,
multiFactor,
};
}
exports.parseAuthUserRecord = parseAuthUserRecord;
/** Helper to get the `AdditionalUserInfo` from the decoded JWT */
function parseAdditionalUserInfo(decodedJWT) {
let profile;
let username;
if (decodedJWT.raw_user_info) {
try {
profile = JSON.parse(decodedJWT.raw_user_info);
}
catch (err) {
logger.debug(`Parse Error: ${err.message}`);
}
}
if (profile) {
if (decodedJWT.sign_in_method === "github.com") {
username = profile.login;
}
if (decodedJWT.sign_in_method === "twitter.com") {
username = profile.screen_name;
}
}
return {
providerId: decodedJWT.sign_in_method === "emailLink" ? "password" : decodedJWT.sign_in_method,
profile,
username,
isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false,
recaptchaScore: decodedJWT.recaptcha_score,
email: decodedJWT.email,
phoneNumber: decodedJWT.phone_number,
};
}
/**
* Helper to generate a response from the blocking function to the Firebase Auth backend.
* @internal
*/
function generateResponsePayload(authResponse) {
if (!authResponse) {
return {};
}
const { recaptchaActionOverride, ...formattedAuthResponse } = authResponse;
const result = {};
const updateMask = getUpdateMask(formattedAuthResponse);
if (updateMask.length !== 0) {
result.userRecord = {
...formattedAuthResponse,
updateMask,
};
}
if (recaptchaActionOverride !== undefined) {
result.recaptchaActionOverride = recaptchaActionOverride;
}
return result;
}
exports.generateResponsePayload = generateResponsePayload;
/** Helper to get the Credential from the decoded JWT */
function parseAuthCredential(decodedJWT, time) {
if (!decodedJWT.sign_in_attributes &&
!decodedJWT.oauth_id_token &&
!decodedJWT.oauth_access_token &&
!decodedJWT.oauth_refresh_token) {
return null;
}
return {
claims: decodedJWT.sign_in_attributes,
idToken: decodedJWT.oauth_id_token,
accessToken: decodedJWT.oauth_access_token,
refreshToken: decodedJWT.oauth_refresh_token,
expirationTime: decodedJWT.oauth_expires_in
? new Date(time + decodedJWT.oauth_expires_in * 1000).toUTCString()
: undefined,
secret: decodedJWT.oauth_token_secret,
providerId: decodedJWT.sign_in_method === "emailLink" ? "password" : decodedJWT.sign_in_method,
signInMethod: decodedJWT.sign_in_method,
};
}
/**
* Parses the decoded jwt into a valid AuthEventContext for use in the handler
* @internal
*/
function parseAuthEventContext(decodedJWT, projectId, time = new Date().getTime()) {
const eventType = (EVENT_MAPPING[decodedJWT.event_type] || decodedJWT.event_type) +
(decodedJWT.sign_in_method ? `:${decodedJWT.sign_in_method}` : "");
return {
locale: decodedJWT.locale,
ipAddress: decodedJWT.ip_address,
userAgent: decodedJWT.user_agent,
eventId: decodedJWT.event_id,
eventType,
authType: decodedJWT.user_record ? "USER" : "UNAUTHENTICATED",
resource: {
// TODO(colerogers): figure out the correct service
service: "identitytoolkit.googleapis.com",
name: decodedJWT.tenant_id
? `projects/${projectId}/tenants/${decodedJWT.tenant_id}`
: `projects/${projectId}`,
},
timestamp: new Date(decodedJWT.iat * 1000).toUTCString(),
additionalUserInfo: parseAdditionalUserInfo(decodedJWT),
credential: parseAuthCredential(decodedJWT, time),
emailType: decodedJWT.email_type,
smsType: decodedJWT.sms_type,
params: {},
};
}
exports.parseAuthEventContext = parseAuthEventContext;
/**
* Checks the handler response for invalid customClaims & sessionClaims objects
* @internal
*/
function validateAuthResponse(eventType, authRequest) {
if (!authRequest) {
authRequest = {};
}
if (authRequest.customClaims) {
const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.customClaims.hasOwnProperty(claim));
if (invalidClaims.length > 0) {
throw new https_1.HttpsError("invalid-argument", `The customClaims claims "${invalidClaims.join(",")}" are reserved and cannot be specified.`);
}
if (JSON.stringify(authRequest.customClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
throw new https_1.HttpsError("invalid-argument", `The customClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
}
}
if (eventType === "beforeSignIn" && authRequest.sessionClaims) {
const invalidClaims = DISALLOWED_CUSTOM_CLAIMS.filter((claim) => authRequest.sessionClaims.hasOwnProperty(claim));
if (invalidClaims.length > 0) {
throw new https_1.HttpsError("invalid-argument", `The sessionClaims claims "${invalidClaims.join(",")}" are reserved and cannot be specified.`);
}
if (JSON.stringify(authRequest.sessionClaims).length >
CLAIMS_MAX_PAYLOAD_SIZE) {
throw new https_1.HttpsError("invalid-argument", `The sessionClaims payload should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters.`);
}
const combinedClaims = {
...authRequest.customClaims,
...authRequest.sessionClaims,
};
if (JSON.stringify(combinedClaims).length > CLAIMS_MAX_PAYLOAD_SIZE) {
throw new https_1.HttpsError("invalid-argument", `The customClaims and sessionClaims payloads should not exceed ${CLAIMS_MAX_PAYLOAD_SIZE} characters combined.`);
}
}
}
exports.validateAuthResponse = validateAuthResponse;
/**
* Helper function to generate the update mask for the identity platform changed values
* @internal
*/
function getUpdateMask(authResponse) {
if (!authResponse) {
return "";
}
const updateMask = [];
for (const key in authResponse) {
if (authResponse.hasOwnProperty(key) && typeof authResponse[key] !== "undefined") {
updateMask.push(key);
}
}
return updateMask.join(",");
}
exports.getUpdateMask = getUpdateMask;
/** @internal */
function wrapHandler(eventType, handler) {
return async (req, res) => {
try {
const projectId = process.env.GCLOUD_PROJECT;
if (!isValidRequest(req)) {
logger.error("Invalid request, unable to process");
throw new https_1.HttpsError("invalid-argument", "Bad Request");
}
if (!auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken) {
throw new Error("Cannot validate Auth Blocking token. Please update Firebase Admin SDK to >= v10.1.0");
}
const decodedPayload = (0, debug_1.isDebugFeatureEnabled)("skipTokenVerification")
? unsafeDecodeAuthBlockingToken(req.body.data.jwt)
: handler.platform === "gcfv1"
? await auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken(req.body.data.jwt)
: await auth.getAuth((0, app_1.getApp)())._verifyAuthBlockingToken(req.body.data.jwt, "run.app");
let authUserRecord;
if (decodedPayload.event_type === "beforeCreate" ||
decodedPayload.event_type === "beforeSignIn") {
authUserRecord = parseAuthUserRecord(decodedPayload.user_record);
}
const authEventContext = parseAuthEventContext(decodedPayload, projectId);
let authResponse;
if (handler.platform === "gcfv1") {
authResponse = authUserRecord
? (await handler(authUserRecord, authEventContext)) || undefined
: (await handler(authEventContext)) || undefined;
}
else {
authResponse =
(await handler({
...authEventContext,
data: authUserRecord,
})) || undefined;
}
validateAuthResponse(eventType, authResponse);
const result = generateResponsePayload(authResponse);
res.status(200);
res.setHeader("Content-Type", "application/json");
res.send(JSON.stringify(result));
}
catch (err) {
let httpErr = err;
if (!(httpErr instanceof https_1.HttpsError)) {
// This doesn't count as an 'explicit' error.
logger.error("Unhandled error", err);
httpErr = new https_1.HttpsError("internal", "An unexpected error occurred.");
}
const { status } = httpErr.httpErrorCode;
const body = { error: httpErr.toJSON() };
res.setHeader("Content-Type", "application/json");
res.status(status).send(body);
}
};
}
exports.wrapHandler = wrapHandler;

View File

@@ -0,0 +1,112 @@
import { DecodedIdToken } from "firebase-admin/auth";
import { Expression } from "../../params";
import { ResetValue } from "../options";
/** How a task should be retried in the event of a non-2xx return. */
export interface RetryConfig {
/**
* Maximum number of times a request should be attempted.
* If left unspecified, will default to 3.
*/
maxAttempts?: number | Expression<number> | ResetValue;
/**
* Maximum amount of time for retrying failed task.
* If left unspecified will retry indefinitely.
*/
maxRetrySeconds?: number | Expression<number> | ResetValue;
/**
* The maximum amount of time to wait between attempts.
* If left unspecified will default to 1hr.
*/
maxBackoffSeconds?: number | Expression<number> | ResetValue;
/**
* The maximum number of times to double the backoff between
* retries. If left unspecified will default to 16.
*/
maxDoublings?: number | Expression<number> | ResetValue;
/**
* The minimum time to wait between attempts. If left unspecified
* will default to 100ms.
*/
minBackoffSeconds?: number | Expression<number> | ResetValue;
}
/** How congestion control should be applied to the function. */
export interface RateLimits {
/**
* The maximum number of requests that can be processed at a time.
* If left unspecified, will default to 1000.
*/
maxConcurrentDispatches?: number | Expression<number> | ResetValue;
/**
* The maximum number of requests that can be invoked per second.
* If left unspecified, will default to 500.
*/
maxDispatchesPerSecond?: number | Expression<number> | ResetValue;
}
/** Metadata about the authorization used to invoke a function. */
export interface AuthData {
uid: string;
token: DecodedIdToken;
}
/** Metadata about a call to a Task Queue function. */
export interface TaskContext {
/**
* The result of decoding and verifying an ODIC token.
*/
auth?: AuthData;
/**
* The name of the queue.
* Populated via the `X-CloudTasks-QueueName` header.
*/
queueName: string;
/**
* The "short" name of the task, or, if no name was specified at creation, a unique
* system-generated id.
* This is the "my-task-id" value in the complete task name, such as "task_name =
* projects/my-project-id/locations/my-location/queues/my-queue-id/tasks/my-task-id."
* Populated via the `X-CloudTasks-TaskName` header.
*/
id: string;
/**
* The number of times this task has been retried.
* For the first attempt, this value is 0. This number includes attempts where the task failed
* due to 5XX error codes and never reached the execution phase.
* Populated via the `X-CloudTasks-TaskRetryCount` header.
*/
retryCount: number;
/**
* The total number of times that the task has received a response from the handler.
* Since Cloud Tasks deletes the task once a successful response has been received, all
* previous handler responses were failures. This number does not include failures due to 5XX
* error codes.
* Populated via the `X-CloudTasks-TaskExecutionCount` header.
*/
executionCount: number;
/**
* The schedule time of the task, as an RFC 3339 string in UTC time zone.
* Populated via the `X-CloudTasks-TaskETA` header, which uses seconds since January 1 1970.
*/
scheduledTime: string;
/**
* The HTTP response code from the previous retry.
* Populated via the `X-CloudTasks-TaskPreviousResponse` header
*/
previousResponse?: number;
/**
* The reason for retrying the task.
* Populated via the `X-CloudTasks-TaskRetryReason` header.
*/
retryReason?: string;
/**
* Raw request headers.
*/
headers?: Record<string, string>;
}
/**
* The request used to call a task queue function.
*/
export type Request<T = any> = TaskContext & {
/**
* The parameters used by a client when calling this function.
*/
data: T;
};

View File

@@ -0,0 +1,100 @@
"use strict";
// The MIT License (MIT)
//
// Copyright (c) 2022 Firebase
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
Object.defineProperty(exports, "__esModule", { value: true });
exports.onDispatchHandler = void 0;
const logger = require("../../logger");
const https = require("./https");
/** @internal */
function onDispatchHandler(handler) {
return async (req, res) => {
var _a;
try {
if (!https.isValidRequest(req)) {
logger.error("Invalid request, unable to process.");
throw new https.HttpsError("invalid-argument", "Bad Request");
}
const headers = {};
for (const [key, value] of Object.entries(req.headers)) {
if (!Array.isArray(value)) {
headers[key] = value;
}
}
const context = {
queueName: req.header("X-CloudTasks-QueueName"),
id: req.header("X-CloudTasks-TaskName"),
retryCount: req.header("X-CloudTasks-TaskRetryCount")
? Number(req.header("X-CloudTasks-TaskRetryCount"))
: undefined,
executionCount: req.header("X-CloudTasks-TaskExecutionCount")
? Number(req.header("X-CloudTasks-TaskExecutionCount"))
: undefined,
scheduledTime: req.header("X-CloudTasks-TaskETA"),
previousResponse: req.header("X-CloudTasks-TaskPreviousResponse")
? Number(req.header("X-CloudTasks-TaskPreviousResponse"))
: undefined,
retryReason: req.header("X-CloudTasks-TaskRetryReason"),
headers,
};
if (!process.env.FUNCTIONS_EMULATOR) {
const authHeader = req.header("Authorization") || "";
const token = (_a = authHeader.match(/^Bearer (.*)$/)) === null || _a === void 0 ? void 0 : _a[1];
// Note: this should never happen since task queue functions are guarded by IAM.
if (!token) {
throw new https.HttpsError("unauthenticated", "Unauthenticated");
}
// We skip authenticating the token since tq functions are guarded by IAM.
const authToken = https.unsafeDecodeIdToken(token);
context.auth = {
uid: authToken.uid,
token: authToken,
};
}
const data = https.decode(req.body.data);
if (handler.length === 2) {
await handler(data, context);
}
else {
const arg = {
...context,
data,
};
// For some reason the type system isn't picking up that the handler
// is a one argument function.
await handler(arg);
}
res.status(204).end();
}
catch (err) {
let httpErr = err;
if (!(err instanceof https.HttpsError)) {
// This doesn't count as an 'explicit' error.
logger.error("Unhandled error", err);
httpErr = new https.HttpsError("internal", "INTERNAL");
}
const { status } = httpErr.httpErrorCode;
const body = { error: httpErr.toJSON() };
res.status(status).send(body);
}
};
}
exports.onDispatchHandler = onDispatchHandler;