firebase-functions 6.6.0 → 7.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/_virtual/rolldown_runtime.js +34 -0
- package/lib/bin/firebase-functions.js +78 -103
- package/lib/common/app.js +35 -55
- package/lib/common/change.js +54 -75
- package/lib/common/config.js +41 -41
- package/lib/common/debug.js +23 -47
- package/lib/common/encoding.js +59 -82
- package/lib/common/onInit.js +26 -28
- package/lib/common/options.js +22 -42
- package/lib/common/params.d.ts +6 -6
- package/lib/common/params.js +0 -23
- package/lib/common/providers/database.js +270 -300
- package/lib/common/providers/firestore.js +66 -92
- package/lib/common/providers/https.d.ts +0 -1
- package/lib/common/providers/https.js +537 -539
- package/lib/common/providers/identity.js +393 -444
- package/lib/common/providers/tasks.js +64 -98
- package/lib/common/timezone.js +544 -542
- package/lib/common/trace.d.ts +0 -1
- package/lib/common/trace.js +63 -55
- package/lib/common/utilities/assertions.d.ts +11 -0
- package/lib/common/utilities/assertions.js +18 -0
- package/lib/common/utilities/encoder.js +20 -37
- package/lib/common/utilities/path-pattern.js +106 -132
- package/lib/common/utilities/path.js +28 -27
- package/lib/common/utilities/utils.js +23 -45
- package/lib/esm/_virtual/rolldown_runtime.mjs +16 -0
- package/lib/esm/bin/firebase-functions.mjs +91 -0
- package/lib/esm/common/app.mjs +39 -0
- package/lib/esm/common/change.mjs +57 -0
- package/lib/esm/common/config.mjs +45 -0
- package/lib/esm/common/debug.mjs +28 -0
- package/lib/esm/common/encoding.mjs +69 -0
- package/lib/esm/common/onInit.mjs +33 -0
- package/lib/esm/common/options.mjs +22 -0
- package/lib/esm/common/params.mjs +1 -0
- package/lib/esm/common/providers/database.mjs +269 -0
- package/lib/esm/common/providers/firestore.mjs +78 -0
- package/lib/esm/common/providers/https.mjs +573 -0
- package/lib/esm/common/providers/identity.mjs +428 -0
- package/lib/esm/common/providers/tasks.mjs +67 -0
- package/lib/esm/common/timezone.mjs +544 -0
- package/lib/esm/common/trace.mjs +73 -0
- package/lib/esm/common/utilities/assertions.mjs +17 -0
- package/lib/esm/common/utilities/encoder.mjs +21 -0
- package/lib/esm/common/utilities/path-pattern.mjs +116 -0
- package/lib/esm/common/utilities/path.mjs +35 -0
- package/lib/esm/common/utilities/utils.mjs +29 -0
- package/lib/esm/function-configuration.mjs +1 -0
- package/lib/esm/logger/common.mjs +23 -0
- package/lib/esm/logger/compat.mjs +25 -0
- package/lib/esm/logger/index.mjs +131 -0
- package/lib/esm/params/index.mjs +160 -0
- package/lib/esm/params/types.mjs +400 -0
- package/lib/esm/runtime/loader.mjs +132 -0
- package/lib/esm/runtime/manifest.mjs +134 -0
- package/lib/esm/types/global.d.mjs +1 -0
- package/lib/esm/v1/cloud-functions.mjs +206 -0
- package/lib/esm/v1/config.mjs +14 -0
- package/lib/esm/v1/function-builder.mjs +252 -0
- package/lib/esm/v1/function-configuration.mjs +72 -0
- package/lib/esm/v1/index.mjs +27 -0
- package/lib/esm/v1/providers/analytics.mjs +212 -0
- package/lib/esm/v1/providers/auth.mjs +156 -0
- package/lib/esm/v1/providers/database.mjs +243 -0
- package/lib/esm/v1/providers/firestore.mjs +131 -0
- package/lib/esm/v1/providers/https.mjs +82 -0
- package/lib/esm/v1/providers/pubsub.mjs +175 -0
- package/lib/esm/v1/providers/remoteConfig.mjs +64 -0
- package/lib/esm/v1/providers/storage.mjs +163 -0
- package/lib/esm/v1/providers/tasks.mjs +63 -0
- package/lib/esm/v1/providers/testLab.mjs +94 -0
- package/lib/esm/v2/core.mjs +4 -0
- package/lib/esm/v2/index.mjs +28 -0
- package/lib/esm/v2/options.mjs +102 -0
- package/lib/esm/v2/providers/alerts/alerts.mjs +85 -0
- package/lib/esm/v2/providers/alerts/appDistribution.mjs +75 -0
- package/lib/esm/v2/providers/alerts/billing.mjs +51 -0
- package/lib/esm/v2/providers/alerts/crashlytics.mjs +122 -0
- package/lib/esm/v2/providers/alerts/index.mjs +22 -0
- package/lib/esm/v2/providers/alerts/performance.mjs +66 -0
- package/lib/esm/v2/providers/database.mjs +197 -0
- package/lib/esm/v2/providers/dataconnect.mjs +130 -0
- package/lib/esm/v2/providers/eventarc.mjs +51 -0
- package/lib/esm/v2/providers/firestore.mjs +294 -0
- package/lib/esm/v2/providers/https.mjs +210 -0
- package/lib/esm/v2/providers/identity.mjs +103 -0
- package/lib/esm/v2/providers/pubsub.mjs +148 -0
- package/lib/esm/v2/providers/remoteConfig.mjs +52 -0
- package/lib/esm/v2/providers/scheduler.mjs +84 -0
- package/lib/esm/v2/providers/storage.mjs +155 -0
- package/lib/esm/v2/providers/tasks.mjs +65 -0
- package/lib/esm/v2/providers/testLab.mjs +53 -0
- package/lib/esm/v2/trace.mjs +20 -0
- package/lib/function-configuration.d.ts +0 -0
- package/lib/function-configuration.js +0 -0
- package/lib/logger/common.js +21 -41
- package/lib/logger/compat.js +18 -33
- package/lib/logger/index.js +119 -130
- package/lib/params/index.d.ts +4 -2
- package/lib/params/index.js +150 -144
- package/lib/params/types.js +389 -423
- package/lib/runtime/loader.js +114 -148
- package/lib/runtime/manifest.js +106 -126
- package/lib/types/global.d.js +0 -0
- package/lib/v1/cloud-functions.d.ts +2 -2
- package/lib/v1/cloud-functions.js +193 -241
- package/lib/v1/config.d.ts +4 -7
- package/lib/v1/config.js +13 -75
- package/lib/v1/function-builder.js +239 -368
- package/lib/v1/function-configuration.js +70 -63
- package/lib/v1/index.js +118 -73
- package/lib/v1/providers/analytics.js +188 -235
- package/lib/v1/providers/auth.d.ts +2 -1
- package/lib/v1/providers/auth.js +159 -164
- package/lib/v1/providers/database.js +237 -242
- package/lib/v1/providers/firestore.js +131 -130
- package/lib/v1/providers/https.d.ts +2 -1
- package/lib/v1/providers/https.js +79 -86
- package/lib/v1/providers/pubsub.js +175 -172
- package/lib/v1/providers/remoteConfig.js +64 -68
- package/lib/v1/providers/storage.js +161 -163
- package/lib/v1/providers/tasks.d.ts +1 -1
- package/lib/v1/providers/tasks.js +65 -80
- package/lib/v1/providers/testLab.js +94 -94
- package/lib/v2/core.d.ts +1 -1
- package/lib/v2/core.js +5 -32
- package/lib/v2/index.d.ts +6 -3
- package/lib/v2/index.js +123 -75
- package/lib/v2/options.js +88 -114
- package/lib/v2/providers/alerts/alerts.js +76 -95
- package/lib/v2/providers/alerts/appDistribution.js +73 -78
- package/lib/v2/providers/alerts/billing.js +49 -53
- package/lib/v2/providers/alerts/crashlytics.js +110 -102
- package/lib/v2/providers/alerts/index.js +56 -53
- package/lib/v2/providers/alerts/performance.js +64 -74
- package/lib/v2/providers/database.js +177 -180
- package/lib/v2/providers/dataconnect.d.ts +95 -0
- package/lib/v2/providers/dataconnect.js +137 -0
- package/lib/v2/providers/eventarc.js +55 -77
- package/lib/v2/providers/firestore.js +262 -260
- package/lib/v2/providers/https.d.ts +3 -2
- package/lib/v2/providers/https.js +210 -247
- package/lib/v2/providers/identity.d.ts +2 -1
- package/lib/v2/providers/identity.js +96 -105
- package/lib/v2/providers/pubsub.js +149 -167
- package/lib/v2/providers/remoteConfig.js +54 -63
- package/lib/v2/providers/scheduler.js +84 -96
- package/lib/v2/providers/storage.js +147 -162
- package/lib/v2/providers/tasks.d.ts +1 -1
- package/lib/v2/providers/tasks.js +68 -95
- package/lib/v2/providers/testLab.js +55 -64
- package/lib/v2/trace.js +18 -19
- package/package.json +290 -226
- package/protos/compiledFirestore.mjs +3512 -0
- package/protos/update.sh +28 -7
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { error, warn } from "../../logger/index.mjs";
|
|
2
|
+
import { getApp } from "../app.mjs";
|
|
3
|
+
import { dateToTimestampProto } from "../utilities/encoder.mjs";
|
|
4
|
+
import * as firestore from "firebase-admin/firestore";
|
|
5
|
+
import { google } from "../../../../protos/compiledFirestore.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/common/providers/firestore.ts
|
|
8
|
+
/** static-complied protobufs */
|
|
9
|
+
const DocumentEventData = google.events.cloud.firestore.v1.DocumentEventData;
|
|
10
|
+
let firestoreInstance;
|
|
11
|
+
/** @hidden */
|
|
12
|
+
function _getValueProto(data, resource, valueFieldName) {
|
|
13
|
+
const value = data?.[valueFieldName];
|
|
14
|
+
if (typeof value === "undefined" || value === null || typeof value === "object" && !Object.keys(value).length) {
|
|
15
|
+
return resource;
|
|
16
|
+
}
|
|
17
|
+
const proto = {
|
|
18
|
+
fields: value?.fields || {},
|
|
19
|
+
createTime: dateToTimestampProto(value?.createTime),
|
|
20
|
+
updateTime: dateToTimestampProto(value?.updateTime),
|
|
21
|
+
name: value?.name || resource
|
|
22
|
+
};
|
|
23
|
+
return proto;
|
|
24
|
+
}
|
|
25
|
+
/** @internal */
|
|
26
|
+
function createSnapshotFromProtobuf(data, path, databaseId) {
|
|
27
|
+
if (!firestoreInstance) {
|
|
28
|
+
firestoreInstance = firestore.getFirestore(getApp(), databaseId);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const dataBuffer = Buffer.from(data);
|
|
32
|
+
const firestoreDecoded = DocumentEventData.decode(dataBuffer);
|
|
33
|
+
return firestoreInstance.snapshot_(firestoreDecoded.value || path, null, "protobufJS");
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error("Failed to decode protobuf and create a snapshot.");
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** @internal */
|
|
40
|
+
function createBeforeSnapshotFromProtobuf(data, path, databaseId) {
|
|
41
|
+
if (!firestoreInstance) {
|
|
42
|
+
firestoreInstance = firestore.getFirestore(getApp(), databaseId);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const dataBuffer = Buffer.from(data);
|
|
46
|
+
const firestoreDecoded = DocumentEventData.decode(dataBuffer);
|
|
47
|
+
return firestoreInstance.snapshot_(firestoreDecoded.oldValue || path, null, "protobufJS");
|
|
48
|
+
} catch (err) {
|
|
49
|
+
error("Failed to decode protobuf and create a before snapshot.");
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** @internal */
|
|
54
|
+
function createSnapshotFromJson(data, source, createTime, updateTime, databaseId) {
|
|
55
|
+
if (!firestoreInstance) {
|
|
56
|
+
firestoreInstance = databaseId ? firestore.getFirestore(getApp(), databaseId) : firestore.getFirestore(getApp());
|
|
57
|
+
}
|
|
58
|
+
const valueProto = _getValueProto(data, source, "value");
|
|
59
|
+
let timeString = createTime || updateTime;
|
|
60
|
+
if (!timeString) {
|
|
61
|
+
warn("Snapshot has no readTime. Using now()");
|
|
62
|
+
timeString = new Date().toISOString();
|
|
63
|
+
}
|
|
64
|
+
const readTime = dateToTimestampProto(timeString);
|
|
65
|
+
return firestoreInstance.snapshot_(valueProto, readTime, "json");
|
|
66
|
+
}
|
|
67
|
+
/** @internal */
|
|
68
|
+
function createBeforeSnapshotFromJson(data, source, createTime, updateTime, databaseId) {
|
|
69
|
+
if (!firestoreInstance) {
|
|
70
|
+
firestoreInstance = databaseId ? firestore.getFirestore(getApp(), databaseId) : firestore.getFirestore(getApp());
|
|
71
|
+
}
|
|
72
|
+
const oldValueProto = _getValueProto(data, source, "oldValue");
|
|
73
|
+
const oldReadTime = dateToTimestampProto(createTime || updateTime);
|
|
74
|
+
return firestoreInstance.snapshot_(oldValueProto, oldReadTime, "json");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { createBeforeSnapshotFromJson, createBeforeSnapshotFromProtobuf, createSnapshotFromJson, createSnapshotFromProtobuf };
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { debug, error, warn } from "../../logger/index.mjs";
|
|
2
|
+
import { getApp } from "../app.mjs";
|
|
3
|
+
import { isDebugFeatureEnabled } from "../debug.mjs";
|
|
4
|
+
import { getAuth } from "firebase-admin/auth";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
import { getAppCheck } from "firebase-admin/app-check";
|
|
7
|
+
|
|
8
|
+
//#region src/common/providers/https.ts
|
|
9
|
+
const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
|
|
10
|
+
/** @internal */
|
|
11
|
+
const CALLABLE_AUTH_HEADER = "x-callable-context-auth";
|
|
12
|
+
/** @internal */
|
|
13
|
+
const ORIGINAL_AUTH_HEADER = "x-original-auth";
|
|
14
|
+
/** @internal */
|
|
15
|
+
const DEFAULT_HEARTBEAT_SECONDS = 30;
|
|
16
|
+
/**
|
|
17
|
+
* Standard error codes and HTTP statuses for different ways a request can fail,
|
|
18
|
+
* as defined by:
|
|
19
|
+
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
|
|
20
|
+
*
|
|
21
|
+
* This map is used primarily to convert from a client error code string to
|
|
22
|
+
* to the HTTP format error code string and status, and make sure it's in the
|
|
23
|
+
* supported set.
|
|
24
|
+
*/
|
|
25
|
+
const errorCodeMap = {
|
|
26
|
+
ok: {
|
|
27
|
+
canonicalName: "OK",
|
|
28
|
+
status: 200
|
|
29
|
+
},
|
|
30
|
+
cancelled: {
|
|
31
|
+
canonicalName: "CANCELLED",
|
|
32
|
+
status: 499
|
|
33
|
+
},
|
|
34
|
+
unknown: {
|
|
35
|
+
canonicalName: "UNKNOWN",
|
|
36
|
+
status: 500
|
|
37
|
+
},
|
|
38
|
+
"invalid-argument": {
|
|
39
|
+
canonicalName: "INVALID_ARGUMENT",
|
|
40
|
+
status: 400
|
|
41
|
+
},
|
|
42
|
+
"deadline-exceeded": {
|
|
43
|
+
canonicalName: "DEADLINE_EXCEEDED",
|
|
44
|
+
status: 504
|
|
45
|
+
},
|
|
46
|
+
"not-found": {
|
|
47
|
+
canonicalName: "NOT_FOUND",
|
|
48
|
+
status: 404
|
|
49
|
+
},
|
|
50
|
+
"already-exists": {
|
|
51
|
+
canonicalName: "ALREADY_EXISTS",
|
|
52
|
+
status: 409
|
|
53
|
+
},
|
|
54
|
+
"permission-denied": {
|
|
55
|
+
canonicalName: "PERMISSION_DENIED",
|
|
56
|
+
status: 403
|
|
57
|
+
},
|
|
58
|
+
unauthenticated: {
|
|
59
|
+
canonicalName: "UNAUTHENTICATED",
|
|
60
|
+
status: 401
|
|
61
|
+
},
|
|
62
|
+
"resource-exhausted": {
|
|
63
|
+
canonicalName: "RESOURCE_EXHAUSTED",
|
|
64
|
+
status: 429
|
|
65
|
+
},
|
|
66
|
+
"failed-precondition": {
|
|
67
|
+
canonicalName: "FAILED_PRECONDITION",
|
|
68
|
+
status: 400
|
|
69
|
+
},
|
|
70
|
+
aborted: {
|
|
71
|
+
canonicalName: "ABORTED",
|
|
72
|
+
status: 409
|
|
73
|
+
},
|
|
74
|
+
"out-of-range": {
|
|
75
|
+
canonicalName: "OUT_OF_RANGE",
|
|
76
|
+
status: 400
|
|
77
|
+
},
|
|
78
|
+
unimplemented: {
|
|
79
|
+
canonicalName: "UNIMPLEMENTED",
|
|
80
|
+
status: 501
|
|
81
|
+
},
|
|
82
|
+
internal: {
|
|
83
|
+
canonicalName: "INTERNAL",
|
|
84
|
+
status: 500
|
|
85
|
+
},
|
|
86
|
+
unavailable: {
|
|
87
|
+
canonicalName: "UNAVAILABLE",
|
|
88
|
+
status: 503
|
|
89
|
+
},
|
|
90
|
+
"data-loss": {
|
|
91
|
+
canonicalName: "DATA_LOSS",
|
|
92
|
+
status: 500
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* An explicit error that can be thrown from a handler to send an error to the
|
|
97
|
+
* client that called the function.
|
|
98
|
+
*/
|
|
99
|
+
var HttpsError = class extends Error {
|
|
100
|
+
constructor(code, message, details) {
|
|
101
|
+
super(message);
|
|
102
|
+
if (code in errorCodeMap === false) {
|
|
103
|
+
throw new Error(`Unknown error code: ${code}.`);
|
|
104
|
+
}
|
|
105
|
+
this.code = code;
|
|
106
|
+
this.details = details;
|
|
107
|
+
this.httpErrorCode = errorCodeMap[code];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Returns a JSON-serializable representation of this object.
|
|
111
|
+
*/
|
|
112
|
+
toJSON() {
|
|
113
|
+
const { details, httpErrorCode: { canonicalName: status }, message } = this;
|
|
114
|
+
return {
|
|
115
|
+
...details === undefined ? {} : { details },
|
|
116
|
+
message,
|
|
117
|
+
status
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** @hidden */
|
|
122
|
+
function isValidRequest(req) {
|
|
123
|
+
if (!req.body) {
|
|
124
|
+
warn("Request is missing body.");
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (req.method !== "POST") {
|
|
128
|
+
warn("Request has invalid method.", req.method);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
let contentType = (req.header("Content-Type") || "").toLowerCase();
|
|
132
|
+
const semiColon = contentType.indexOf(";");
|
|
133
|
+
if (semiColon >= 0) {
|
|
134
|
+
contentType = contentType.slice(0, semiColon).trim();
|
|
135
|
+
}
|
|
136
|
+
if (contentType !== "application/json") {
|
|
137
|
+
warn("Request has incorrect Content-Type.", contentType);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (typeof req.body.data === "undefined") {
|
|
141
|
+
warn("Request body is missing data.", req.body);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const extraKeys = Object.keys(req.body).filter((field) => field !== "data");
|
|
145
|
+
if (extraKeys.length !== 0) {
|
|
146
|
+
warn("Request body has extra fields: ", extraKeys.join(", "));
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/** @hidden */
|
|
152
|
+
const LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value";
|
|
153
|
+
/** @hidden */
|
|
154
|
+
const UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value";
|
|
155
|
+
/**
|
|
156
|
+
* Encodes arbitrary data in our special format for JSON.
|
|
157
|
+
* This is exposed only for testing.
|
|
158
|
+
*/
|
|
159
|
+
/** @hidden */
|
|
160
|
+
function encode(data) {
|
|
161
|
+
if (data === null || typeof data === "undefined") {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (data instanceof Number) {
|
|
165
|
+
data = data.valueOf();
|
|
166
|
+
}
|
|
167
|
+
if (Number.isFinite(data)) {
|
|
168
|
+
return data;
|
|
169
|
+
}
|
|
170
|
+
if (typeof data === "boolean") {
|
|
171
|
+
return data;
|
|
172
|
+
}
|
|
173
|
+
if (typeof data === "string") {
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(data)) {
|
|
177
|
+
return data.map(encode);
|
|
178
|
+
}
|
|
179
|
+
if (typeof data === "object" || typeof data === "function") {
|
|
180
|
+
const obj = {};
|
|
181
|
+
for (const [k, v] of Object.entries(data)) {
|
|
182
|
+
obj[k] = encode(v);
|
|
183
|
+
}
|
|
184
|
+
return obj;
|
|
185
|
+
}
|
|
186
|
+
error("Data cannot be encoded in JSON.", data);
|
|
187
|
+
throw new Error(`Data cannot be encoded in JSON: ${data}`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Decodes our special format for JSON into native types.
|
|
191
|
+
* This is exposed only for testing.
|
|
192
|
+
*/
|
|
193
|
+
/** @hidden */
|
|
194
|
+
function decode(data) {
|
|
195
|
+
if (data === null) {
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
if (data["@type"]) {
|
|
199
|
+
switch (data["@type"]) {
|
|
200
|
+
case LONG_TYPE:
|
|
201
|
+
case UNSIGNED_LONG_TYPE: {
|
|
202
|
+
const value = parseFloat(data.value);
|
|
203
|
+
if (isNaN(value)) {
|
|
204
|
+
error("Data cannot be decoded from JSON.", data);
|
|
205
|
+
throw new Error(`Data cannot be decoded from JSON: ${data}`);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
default: {
|
|
210
|
+
error("Data cannot be decoded from JSON.", data);
|
|
211
|
+
throw new Error(`Data cannot be decoded from JSON: ${data}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(data)) {
|
|
216
|
+
return data.map(decode);
|
|
217
|
+
}
|
|
218
|
+
if (typeof data === "object") {
|
|
219
|
+
const obj = {};
|
|
220
|
+
for (const [k, v] of Object.entries(data)) {
|
|
221
|
+
obj[k] = decode(v);
|
|
222
|
+
}
|
|
223
|
+
return obj;
|
|
224
|
+
}
|
|
225
|
+
return data;
|
|
226
|
+
}
|
|
227
|
+
/** @internal */
|
|
228
|
+
function unsafeDecodeToken(token) {
|
|
229
|
+
if (!JWT_REGEX.test(token)) {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
const components = token.split(".").map((s) => Buffer.from(s, "base64").toString());
|
|
233
|
+
let payload = components[1];
|
|
234
|
+
if (typeof payload === "string") {
|
|
235
|
+
try {
|
|
236
|
+
const obj = JSON.parse(payload);
|
|
237
|
+
if (typeof obj === "object") {
|
|
238
|
+
payload = obj;
|
|
239
|
+
}
|
|
240
|
+
} catch (_e) {}
|
|
241
|
+
}
|
|
242
|
+
return payload;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Decode, but not verify, a Auth ID token.
|
|
246
|
+
*
|
|
247
|
+
* Do not use in production. Token should always be verified using the Admin SDK.
|
|
248
|
+
*
|
|
249
|
+
* This is exposed only for testing.
|
|
250
|
+
*/
|
|
251
|
+
/** @internal */
|
|
252
|
+
function unsafeDecodeIdToken(token) {
|
|
253
|
+
const decoded = unsafeDecodeToken(token);
|
|
254
|
+
decoded.uid = decoded.sub;
|
|
255
|
+
return decoded;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Decode, but not verify, an App Check token.
|
|
259
|
+
*
|
|
260
|
+
* Do not use in production. Token should always be verified using the Admin SDK.
|
|
261
|
+
*
|
|
262
|
+
* This is exposed only for testing.
|
|
263
|
+
*/
|
|
264
|
+
/** @internal */
|
|
265
|
+
function unsafeDecodeAppCheckToken(token) {
|
|
266
|
+
const decoded = unsafeDecodeToken(token);
|
|
267
|
+
decoded.app_id = decoded.sub;
|
|
268
|
+
return decoded;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check and verify tokens included in the requests. Once verified, tokens
|
|
272
|
+
* are injected into the callable context.
|
|
273
|
+
*
|
|
274
|
+
* @param {Request} req - Request sent to the Callable function.
|
|
275
|
+
* @param {CallableContext} ctx - Context to be sent to callable function handler.
|
|
276
|
+
* @returns {CallableTokenStatus} Status of the token verifications.
|
|
277
|
+
*/
|
|
278
|
+
/** @internal */
|
|
279
|
+
async function checkTokens(req, ctx, options) {
|
|
280
|
+
const verifications = {
|
|
281
|
+
app: "INVALID",
|
|
282
|
+
auth: "INVALID"
|
|
283
|
+
};
|
|
284
|
+
[verifications.auth, verifications.app] = await Promise.all([checkAuthToken(req, ctx), checkAppCheckToken(req, ctx, options)]);
|
|
285
|
+
const logPayload = {
|
|
286
|
+
verifications,
|
|
287
|
+
"logging.googleapis.com/labels": { "firebase-log-type": "callable-request-verification" }
|
|
288
|
+
};
|
|
289
|
+
const errs = [];
|
|
290
|
+
if (verifications.app === "INVALID") {
|
|
291
|
+
errs.push("AppCheck token was rejected.");
|
|
292
|
+
}
|
|
293
|
+
if (verifications.auth === "INVALID") {
|
|
294
|
+
errs.push("Auth token was rejected.");
|
|
295
|
+
}
|
|
296
|
+
if (errs.length === 0) {
|
|
297
|
+
debug("Callable request verification passed", logPayload);
|
|
298
|
+
} else {
|
|
299
|
+
warn(`Callable request verification failed: ${errs.join(" ")}`, logPayload);
|
|
300
|
+
}
|
|
301
|
+
return verifications;
|
|
302
|
+
}
|
|
303
|
+
/** @interanl */
|
|
304
|
+
async function checkAuthToken(req, ctx) {
|
|
305
|
+
const authorization = req.header("Authorization");
|
|
306
|
+
if (!authorization) {
|
|
307
|
+
return "MISSING";
|
|
308
|
+
}
|
|
309
|
+
const match = authorization.match(/^Bearer (.*)$/i);
|
|
310
|
+
if (!match) {
|
|
311
|
+
return "INVALID";
|
|
312
|
+
}
|
|
313
|
+
const idToken = match[1];
|
|
314
|
+
try {
|
|
315
|
+
let authToken;
|
|
316
|
+
if (isDebugFeatureEnabled("skipTokenVerification")) {
|
|
317
|
+
authToken = unsafeDecodeIdToken(idToken);
|
|
318
|
+
} else {
|
|
319
|
+
authToken = await getAuth(getApp()).verifyIdToken(idToken);
|
|
320
|
+
}
|
|
321
|
+
ctx.auth = {
|
|
322
|
+
uid: authToken.uid,
|
|
323
|
+
token: authToken,
|
|
324
|
+
rawToken: idToken
|
|
325
|
+
};
|
|
326
|
+
return "VALID";
|
|
327
|
+
} catch (err) {
|
|
328
|
+
warn("Failed to validate auth token.", err);
|
|
329
|
+
return "INVALID";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** @internal */
|
|
333
|
+
async function checkAppCheckToken(req, ctx, options) {
|
|
334
|
+
const appCheckToken = req.header("X-Firebase-AppCheck");
|
|
335
|
+
if (!appCheckToken) {
|
|
336
|
+
return "MISSING";
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
let appCheckData;
|
|
340
|
+
if (isDebugFeatureEnabled("skipTokenVerification")) {
|
|
341
|
+
const decodedToken = unsafeDecodeAppCheckToken(appCheckToken);
|
|
342
|
+
appCheckData = {
|
|
343
|
+
appId: decodedToken.app_id,
|
|
344
|
+
token: decodedToken
|
|
345
|
+
};
|
|
346
|
+
if (options.consumeAppCheckToken) {
|
|
347
|
+
appCheckData.alreadyConsumed = false;
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
const appCheck = getAppCheck(getApp());
|
|
351
|
+
if (options.consumeAppCheckToken) {
|
|
352
|
+
if (appCheck.verifyToken?.length === 1) {
|
|
353
|
+
const errorMsg = "Unsupported version of the Admin SDK." + " App Check token will not be consumed." + " Please upgrade the firebase-admin to the latest version.";
|
|
354
|
+
error(errorMsg);
|
|
355
|
+
throw new HttpsError("internal", "Internal Error");
|
|
356
|
+
}
|
|
357
|
+
appCheckData = await getAppCheck(getApp()).verifyToken(appCheckToken, { consume: true });
|
|
358
|
+
} else {
|
|
359
|
+
appCheckData = await getAppCheck(getApp()).verifyToken(appCheckToken);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
ctx.app = appCheckData;
|
|
363
|
+
return "VALID";
|
|
364
|
+
} catch (err) {
|
|
365
|
+
warn("Failed to validate AppCheck token.", err);
|
|
366
|
+
if (err instanceof HttpsError) {
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
return "INVALID";
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** @internal */
|
|
373
|
+
function onCallHandler(options, handler, version) {
|
|
374
|
+
const wrapped = wrapOnCallHandler(options, handler, version);
|
|
375
|
+
return (req, res) => {
|
|
376
|
+
return new Promise((resolve) => {
|
|
377
|
+
res.on("finish", resolve);
|
|
378
|
+
cors(options.cors)(req, res, () => {
|
|
379
|
+
resolve(wrapped(req, res));
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function encodeSSE(data) {
|
|
385
|
+
return `data: ${JSON.stringify(data)}\n\n`;
|
|
386
|
+
}
|
|
387
|
+
/** @internal */
|
|
388
|
+
function wrapOnCallHandler(options, handler, version) {
|
|
389
|
+
return async (req, res) => {
|
|
390
|
+
const abortController = new AbortController();
|
|
391
|
+
let heartbeatInterval = null;
|
|
392
|
+
const heartbeatSeconds = options.heartbeatSeconds === undefined ? DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
|
|
393
|
+
const clearScheduledHeartbeat = () => {
|
|
394
|
+
if (heartbeatInterval) {
|
|
395
|
+
clearTimeout(heartbeatInterval);
|
|
396
|
+
heartbeatInterval = null;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const scheduleHeartbeat = () => {
|
|
400
|
+
clearScheduledHeartbeat();
|
|
401
|
+
if (!abortController.signal.aborted) {
|
|
402
|
+
heartbeatInterval = setTimeout(() => {
|
|
403
|
+
if (!abortController.signal.aborted) {
|
|
404
|
+
res.write(": ping\n\n");
|
|
405
|
+
scheduleHeartbeat();
|
|
406
|
+
}
|
|
407
|
+
}, heartbeatSeconds * 1e3);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
res.on("close", () => {
|
|
411
|
+
clearScheduledHeartbeat();
|
|
412
|
+
abortController.abort();
|
|
413
|
+
});
|
|
414
|
+
try {
|
|
415
|
+
if (!isValidRequest(req)) {
|
|
416
|
+
error("Invalid request, unable to process.");
|
|
417
|
+
throw new HttpsError("invalid-argument", "Bad Request");
|
|
418
|
+
}
|
|
419
|
+
const context = { rawRequest: req };
|
|
420
|
+
if (isDebugFeatureEnabled("skipTokenVerification") && version === "gcfv1") {
|
|
421
|
+
const authContext = context.rawRequest.header(CALLABLE_AUTH_HEADER);
|
|
422
|
+
if (authContext) {
|
|
423
|
+
debug("Callable functions auth override", {
|
|
424
|
+
key: CALLABLE_AUTH_HEADER,
|
|
425
|
+
value: authContext
|
|
426
|
+
});
|
|
427
|
+
context.auth = JSON.parse(decodeURIComponent(authContext));
|
|
428
|
+
delete context.rawRequest.headers[CALLABLE_AUTH_HEADER];
|
|
429
|
+
}
|
|
430
|
+
const originalAuth = context.rawRequest.header(ORIGINAL_AUTH_HEADER);
|
|
431
|
+
if (originalAuth) {
|
|
432
|
+
context.rawRequest.headers["authorization"] = originalAuth;
|
|
433
|
+
delete context.rawRequest.headers[ORIGINAL_AUTH_HEADER];
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const tokenStatus = await checkTokens(req, context, options);
|
|
437
|
+
if (tokenStatus.auth === "INVALID") {
|
|
438
|
+
throw new HttpsError("unauthenticated", "Unauthenticated");
|
|
439
|
+
}
|
|
440
|
+
if (tokenStatus.app === "INVALID") {
|
|
441
|
+
if (options.enforceAppCheck) {
|
|
442
|
+
throw new HttpsError("unauthenticated", "Unauthenticated");
|
|
443
|
+
} else {
|
|
444
|
+
warn("Allowing request with invalid AppCheck token because enforcement is disabled");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (tokenStatus.app === "MISSING" && options.enforceAppCheck) {
|
|
448
|
+
throw new HttpsError("unauthenticated", "Unauthenticated");
|
|
449
|
+
}
|
|
450
|
+
const instanceId = req.header("Firebase-Instance-ID-Token");
|
|
451
|
+
if (instanceId) {
|
|
452
|
+
context.instanceIdToken = req.header("Firebase-Instance-ID-Token");
|
|
453
|
+
}
|
|
454
|
+
const acceptsStreaming = req.header("accept") === "text/event-stream";
|
|
455
|
+
if (acceptsStreaming && version === "gcfv1") {
|
|
456
|
+
throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
|
|
457
|
+
}
|
|
458
|
+
const data = decode(req.body.data);
|
|
459
|
+
if (options.authPolicy) {
|
|
460
|
+
const authorized = await options.authPolicy(context.auth ?? null, data);
|
|
461
|
+
if (!authorized) {
|
|
462
|
+
throw new HttpsError("permission-denied", "Permission Denied");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
let result;
|
|
466
|
+
if (version === "gcfv1") {
|
|
467
|
+
result = await handler(data, context);
|
|
468
|
+
} else {
|
|
469
|
+
const arg = {
|
|
470
|
+
...context,
|
|
471
|
+
data,
|
|
472
|
+
acceptsStreaming
|
|
473
|
+
};
|
|
474
|
+
const responseProxy = {
|
|
475
|
+
sendChunk(chunk) {
|
|
476
|
+
if (!acceptsStreaming) {
|
|
477
|
+
return Promise.resolve(false);
|
|
478
|
+
}
|
|
479
|
+
if (abortController.signal.aborted) {
|
|
480
|
+
return Promise.resolve(false);
|
|
481
|
+
}
|
|
482
|
+
const formattedData = encodeSSE({ message: chunk });
|
|
483
|
+
let resolve;
|
|
484
|
+
let reject;
|
|
485
|
+
const p = new Promise((res$1, rej) => {
|
|
486
|
+
resolve = res$1;
|
|
487
|
+
reject = rej;
|
|
488
|
+
});
|
|
489
|
+
const wrote = res.write(formattedData, (error$1) => {
|
|
490
|
+
if (error$1) {
|
|
491
|
+
reject(error$1);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
resolve(wrote);
|
|
495
|
+
});
|
|
496
|
+
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
|
|
497
|
+
scheduleHeartbeat();
|
|
498
|
+
}
|
|
499
|
+
return p;
|
|
500
|
+
},
|
|
501
|
+
signal: abortController.signal
|
|
502
|
+
};
|
|
503
|
+
if (acceptsStreaming) {
|
|
504
|
+
res.status(200);
|
|
505
|
+
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
|
|
506
|
+
scheduleHeartbeat();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
result = await handler(arg, responseProxy);
|
|
510
|
+
clearScheduledHeartbeat();
|
|
511
|
+
}
|
|
512
|
+
if (!abortController.signal.aborted) {
|
|
513
|
+
result = encode(result);
|
|
514
|
+
const responseBody = { result };
|
|
515
|
+
if (acceptsStreaming) {
|
|
516
|
+
res.write(encodeSSE(responseBody));
|
|
517
|
+
res.end();
|
|
518
|
+
} else {
|
|
519
|
+
res.status(200).send(responseBody);
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
res.end();
|
|
523
|
+
}
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (!abortController.signal.aborted) {
|
|
526
|
+
let httpErr = err;
|
|
527
|
+
if (!(err instanceof HttpsError)) {
|
|
528
|
+
error("Unhandled error", err);
|
|
529
|
+
httpErr = new HttpsError("internal", "INTERNAL");
|
|
530
|
+
}
|
|
531
|
+
const { status } = httpErr.httpErrorCode;
|
|
532
|
+
const body = { error: httpErr.toJSON() };
|
|
533
|
+
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
|
|
534
|
+
res.write(encodeSSE(body));
|
|
535
|
+
res.end();
|
|
536
|
+
} else {
|
|
537
|
+
res.status(status).send(body);
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
res.end();
|
|
541
|
+
}
|
|
542
|
+
} finally {
|
|
543
|
+
clearScheduledHeartbeat();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Wraps an HTTP handler with a safety net for unhandled errors.
|
|
549
|
+
*
|
|
550
|
+
* This wrapper catches both synchronous errors and rejected Promises from `async` handlers.
|
|
551
|
+
* Without this, an unhandled error in an `async` handler would cause the request to hang
|
|
552
|
+
* until the platform timeout, as Express (v4) does not await handlers.
|
|
553
|
+
*
|
|
554
|
+
* It logs the error and returns a 500 Internal Server Error to the client if the response
|
|
555
|
+
* headers have not yet been sent.
|
|
556
|
+
*
|
|
557
|
+
* @internal
|
|
558
|
+
*/
|
|
559
|
+
function withErrorHandler(handler) {
|
|
560
|
+
return async (req, res) => {
|
|
561
|
+
try {
|
|
562
|
+
await handler(req, res);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
error("Unhandled error", err);
|
|
565
|
+
if (!res.headersSent) {
|
|
566
|
+
res.status(500).send("Internal Server Error");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
//#endregion
|
|
573
|
+
export { CALLABLE_AUTH_HEADER, DEFAULT_HEARTBEAT_SECONDS, HttpsError, ORIGINAL_AUTH_HEADER, checkAuthToken, decode, encode, isValidRequest, onCallHandler, unsafeDecodeAppCheckToken, unsafeDecodeIdToken, unsafeDecodeToken, withErrorHandler };
|