firebase-functions 6.1.0 → 6.1.2
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.
|
@@ -85,6 +85,27 @@ export interface CallableRequest<T = any> {
|
|
|
85
85
|
*/
|
|
86
86
|
rawRequest: Request;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* CallableProxyResponse exposes subset of express.Response object
|
|
90
|
+
* to allow writing partial, streaming responses back to the client.
|
|
91
|
+
*/
|
|
92
|
+
export interface CallableProxyResponse {
|
|
93
|
+
/**
|
|
94
|
+
* Writes a chunk of the response body to the client. This method can be called
|
|
95
|
+
* multiple times to stream data progressively.
|
|
96
|
+
*/
|
|
97
|
+
write: express.Response["write"];
|
|
98
|
+
/**
|
|
99
|
+
* Indicates whether the client has requested and can handle streaming responses.
|
|
100
|
+
* This should be checked before attempting to stream data to avoid compatibility issues.
|
|
101
|
+
*/
|
|
102
|
+
acceptsStreaming: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* An AbortSignal that is triggered when the client disconnects or the
|
|
105
|
+
* request is terminated prematurely.
|
|
106
|
+
*/
|
|
107
|
+
signal: AbortSignal;
|
|
108
|
+
}
|
|
88
109
|
/**
|
|
89
110
|
* The set of Firebase Functions status codes. The codes are the same at the
|
|
90
111
|
* ones exposed by {@link https://github.com/grpc/grpc/blob/master/doc/statuscodes.md | gRPC}.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
// SOFTWARE.
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
|
24
|
+
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;
|
|
25
25
|
const cors = require("cors");
|
|
26
26
|
const logger = require("../../logger");
|
|
27
27
|
// TODO(inlined): Decide whether we want to un-version apps or whether we want a
|
|
@@ -35,6 +35,8 @@ const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
|
|
|
35
35
|
exports.CALLABLE_AUTH_HEADER = "x-callable-context-auth";
|
|
36
36
|
/** @internal */
|
|
37
37
|
exports.ORIGINAL_AUTH_HEADER = "x-original-auth";
|
|
38
|
+
/** @internal */
|
|
39
|
+
exports.DEFAULT_HEARTBEAT_SECONDS = 30;
|
|
38
40
|
/**
|
|
39
41
|
* Standard error codes and HTTP statuses for different ways a request can fail,
|
|
40
42
|
* as defined by:
|
|
@@ -385,8 +387,8 @@ async function checkAppCheckToken(req, ctx, options) {
|
|
|
385
387
|
}
|
|
386
388
|
}
|
|
387
389
|
/** @internal */
|
|
388
|
-
function onCallHandler(options, handler) {
|
|
389
|
-
const wrapped = wrapOnCallHandler(options, handler);
|
|
390
|
+
function onCallHandler(options, handler, version) {
|
|
391
|
+
const wrapped = wrapOnCallHandler(options, handler, version);
|
|
390
392
|
return (req, res) => {
|
|
391
393
|
return new Promise((resolve) => {
|
|
392
394
|
res.on("finish", resolve);
|
|
@@ -397,9 +399,36 @@ function onCallHandler(options, handler) {
|
|
|
397
399
|
};
|
|
398
400
|
}
|
|
399
401
|
exports.onCallHandler = onCallHandler;
|
|
402
|
+
function encodeSSE(data) {
|
|
403
|
+
return `data: ${JSON.stringify(data)}\n`;
|
|
404
|
+
}
|
|
400
405
|
/** @internal */
|
|
401
|
-
function wrapOnCallHandler(options, handler) {
|
|
406
|
+
function wrapOnCallHandler(options, handler, version) {
|
|
402
407
|
return async (req, res) => {
|
|
408
|
+
const abortController = new AbortController();
|
|
409
|
+
let heartbeatInterval = null;
|
|
410
|
+
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
|
|
411
|
+
const clearScheduledHeartbeat = () => {
|
|
412
|
+
if (heartbeatInterval) {
|
|
413
|
+
clearTimeout(heartbeatInterval);
|
|
414
|
+
heartbeatInterval = null;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const scheduleHeartbeat = () => {
|
|
418
|
+
clearScheduledHeartbeat();
|
|
419
|
+
if (!abortController.signal.aborted) {
|
|
420
|
+
heartbeatInterval = setTimeout(() => {
|
|
421
|
+
if (!abortController.signal.aborted) {
|
|
422
|
+
res.write(": ping\n");
|
|
423
|
+
scheduleHeartbeat();
|
|
424
|
+
}
|
|
425
|
+
}, heartbeatSeconds * 1000);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
res.on("close", () => {
|
|
429
|
+
clearScheduledHeartbeat();
|
|
430
|
+
abortController.abort();
|
|
431
|
+
});
|
|
403
432
|
try {
|
|
404
433
|
if (!isValidRequest(req)) {
|
|
405
434
|
logger.error("Invalid request, unable to process.");
|
|
@@ -413,7 +442,7 @@ function wrapOnCallHandler(options, handler) {
|
|
|
413
442
|
// The original monkey-patched code lived in the functionsEmulatorRuntime
|
|
414
443
|
// (link: https://github.com/firebase/firebase-tools/blob/accea7abda3cc9fa6bb91368e4895faf95281c60/src/emulator/functionsEmulatorRuntime.ts#L480)
|
|
415
444
|
// and was not compatible with how monorepos separate out packages (see https://github.com/firebase/firebase-tools/issues/5210).
|
|
416
|
-
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification") &&
|
|
445
|
+
if ((0, debug_1.isDebugFeatureEnabled)("skipTokenVerification") && version === "gcfv1") {
|
|
417
446
|
const authContext = context.rawRequest.header(exports.CALLABLE_AUTH_HEADER);
|
|
418
447
|
if (authContext) {
|
|
419
448
|
logger.debug("Callable functions auth override", {
|
|
@@ -452,9 +481,14 @@ function wrapOnCallHandler(options, handler) {
|
|
|
452
481
|
// pushes with FCM. In that case, the FCM APIs will validate the token.
|
|
453
482
|
context.instanceIdToken = req.header("Firebase-Instance-ID-Token");
|
|
454
483
|
}
|
|
484
|
+
const acceptsStreaming = req.header("accept") === "text/event-stream";
|
|
485
|
+
if (acceptsStreaming && version === "gcfv1") {
|
|
486
|
+
// streaming responses are not supported in v1 callable
|
|
487
|
+
throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
|
|
488
|
+
}
|
|
455
489
|
const data = decode(req.body.data);
|
|
456
490
|
let result;
|
|
457
|
-
if (
|
|
491
|
+
if (version === "gcfv1") {
|
|
458
492
|
result = await handler(data, context);
|
|
459
493
|
}
|
|
460
494
|
else {
|
|
@@ -462,26 +496,79 @@ function wrapOnCallHandler(options, handler) {
|
|
|
462
496
|
...context,
|
|
463
497
|
data,
|
|
464
498
|
};
|
|
499
|
+
const responseProxy = {
|
|
500
|
+
write(chunk) {
|
|
501
|
+
// if client doesn't accept sse-protocol, response.write() is no-op.
|
|
502
|
+
if (!acceptsStreaming) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
// if connection is already closed, response.write() is no-op.
|
|
506
|
+
if (abortController.signal.aborted) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const formattedData = encodeSSE({ message: chunk });
|
|
510
|
+
const wrote = res.write(formattedData);
|
|
511
|
+
// Reset heartbeat timer after successful write
|
|
512
|
+
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
|
|
513
|
+
scheduleHeartbeat();
|
|
514
|
+
}
|
|
515
|
+
return wrote;
|
|
516
|
+
},
|
|
517
|
+
acceptsStreaming,
|
|
518
|
+
signal: abortController.signal,
|
|
519
|
+
};
|
|
520
|
+
if (acceptsStreaming) {
|
|
521
|
+
// SSE always responds with 200
|
|
522
|
+
res.status(200);
|
|
523
|
+
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
|
|
524
|
+
scheduleHeartbeat();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
465
527
|
// For some reason the type system isn't picking up that the handler
|
|
466
528
|
// is a one argument function.
|
|
467
|
-
result = await handler(arg);
|
|
529
|
+
result = await handler(arg, responseProxy);
|
|
530
|
+
clearScheduledHeartbeat();
|
|
531
|
+
}
|
|
532
|
+
if (!abortController.signal.aborted) {
|
|
533
|
+
// Encode the result as JSON to preserve types like Dates.
|
|
534
|
+
result = encode(result);
|
|
535
|
+
// If there was some result, encode it in the body.
|
|
536
|
+
const responseBody = { result };
|
|
537
|
+
if (acceptsStreaming) {
|
|
538
|
+
res.write(encodeSSE(responseBody));
|
|
539
|
+
res.end();
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
res.status(200).send(responseBody);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
res.end();
|
|
468
547
|
}
|
|
469
|
-
// Encode the result as JSON to preserve types like Dates.
|
|
470
|
-
result = encode(result);
|
|
471
|
-
// If there was some result, encode it in the body.
|
|
472
|
-
const responseBody = { result };
|
|
473
|
-
res.status(200).send(responseBody);
|
|
474
548
|
}
|
|
475
549
|
catch (err) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
550
|
+
if (!abortController.signal.aborted) {
|
|
551
|
+
let httpErr = err;
|
|
552
|
+
if (!(err instanceof HttpsError)) {
|
|
553
|
+
// This doesn't count as an 'explicit' error.
|
|
554
|
+
logger.error("Unhandled error", err);
|
|
555
|
+
httpErr = new HttpsError("internal", "INTERNAL");
|
|
556
|
+
}
|
|
557
|
+
const { status } = httpErr.httpErrorCode;
|
|
558
|
+
const body = { error: httpErr.toJSON() };
|
|
559
|
+
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
|
|
560
|
+
res.send(encodeSSE(body));
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
res.status(status).send(body);
|
|
564
|
+
}
|
|
481
565
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
566
|
+
else {
|
|
567
|
+
res.end();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
finally {
|
|
571
|
+
clearScheduledHeartbeat();
|
|
485
572
|
}
|
|
486
573
|
};
|
|
487
574
|
}
|
|
@@ -70,9 +70,8 @@ function _onRequestWithOptions(handler, options) {
|
|
|
70
70
|
exports._onRequestWithOptions = _onRequestWithOptions;
|
|
71
71
|
/** @internal */
|
|
72
72
|
function _onCallWithOptions(handler, options) {
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// in another handler to avoid accidentally triggering the v2 API
|
|
73
|
+
// fix the length of handler to make the call to handler consistent
|
|
74
|
+
// in the onCallHandler
|
|
76
75
|
const fixedLen = (data, context) => {
|
|
77
76
|
return (0, onInit_1.withInit)(handler)(data, context);
|
|
78
77
|
};
|
|
@@ -80,7 +79,7 @@ function _onCallWithOptions(handler, options) {
|
|
|
80
79
|
enforceAppCheck: options.enforceAppCheck,
|
|
81
80
|
consumeAppCheckToken: options.consumeAppCheckToken,
|
|
82
81
|
cors: { origin: true, methods: "POST" },
|
|
83
|
-
}, fixedLen));
|
|
82
|
+
}, fixedLen, "gcfv1"));
|
|
84
83
|
func.__trigger = {
|
|
85
84
|
labels: {},
|
|
86
85
|
...(0, cloud_functions_1.optionsToTrigger)(options),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as express from "express";
|
|
2
2
|
import { ResetValue } from "../../common/options";
|
|
3
|
-
import { CallableRequest, FunctionsErrorCode, HttpsError, Request } from "../../common/providers/https";
|
|
3
|
+
import { CallableRequest, CallableProxyResponse, FunctionsErrorCode, HttpsError, Request } from "../../common/providers/https";
|
|
4
4
|
import { ManifestEndpoint } from "../../runtime/manifest";
|
|
5
5
|
import { GlobalOptions, SupportedRegion } from "../options";
|
|
6
6
|
import { Expression } from "../../params";
|
|
@@ -132,6 +132,13 @@ export interface CallableOptions extends HttpsOptions {
|
|
|
132
132
|
* further decisions, such as requiring additional security checks or rejecting the request.
|
|
133
133
|
*/
|
|
134
134
|
consumeAppCheckToken?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Time in seconds between sending heartbeat messages to keep the connection
|
|
137
|
+
* alive. Set to `null` to disable heartbeats.
|
|
138
|
+
*
|
|
139
|
+
* Defaults to 30 seconds.
|
|
140
|
+
*/
|
|
141
|
+
heartbeatSeconds?: number | null;
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* Handles HTTPS requests.
|
|
@@ -175,10 +182,10 @@ export declare function onRequest(handler: (request: Request, response: express.
|
|
|
175
182
|
* @param handler - A function that takes a {@link https.CallableRequest}.
|
|
176
183
|
* @returns A function that you can export and deploy.
|
|
177
184
|
*/
|
|
178
|
-
export declare function onCall<T = any, Return = any | Promise<any>>(opts: CallableOptions, handler: (request: CallableRequest<T
|
|
185
|
+
export declare function onCall<T = any, Return = any | Promise<any>>(opts: CallableOptions, handler: (request: CallableRequest<T>, response?: CallableProxyResponse) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>>;
|
|
179
186
|
/**
|
|
180
187
|
* Declares a callable method for clients to call using a Firebase SDK.
|
|
181
188
|
* @param handler - A function that takes a {@link https.CallableRequest}.
|
|
182
189
|
* @returns A function that you can export and deploy.
|
|
183
190
|
*/
|
|
184
|
-
export declare function onCall<T = any, Return = any | Promise<any>>(handler: (request: CallableRequest<T
|
|
191
|
+
export declare function onCall<T = any, Return = any | Promise<any>>(handler: (request: CallableRequest<T>, response?: CallableProxyResponse) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>>;
|
|
@@ -130,15 +130,15 @@ function onCall(optsOrHandler, handler) {
|
|
|
130
130
|
if (Array.isArray(origin) && origin.length === 1) {
|
|
131
131
|
origin = origin[0];
|
|
132
132
|
}
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
const fixedLen = (req) => (0, onInit_1.withInit)(handler)(req);
|
|
133
|
+
// fix the length of handler to make the call to handler consistent
|
|
134
|
+
const fixedLen = (req, resp) => (0, onInit_1.withInit)(handler)(req, resp);
|
|
136
135
|
let func = (0, https_1.onCallHandler)({
|
|
137
136
|
cors: { origin, methods: "POST" },
|
|
138
137
|
enforceAppCheck: (_a = opts.enforceAppCheck) !== null && _a !== void 0 ? _a : options.getGlobalOptions().enforceAppCheck,
|
|
139
138
|
consumeAppCheckToken: opts.consumeAppCheckToken,
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
heartbeatSeconds: opts.heartbeatSeconds,
|
|
140
|
+
}, fixedLen, "gcfv2");
|
|
141
|
+
func = (0, trace_1.wrapTraceContext)((0, onInit_1.withInit)(func));
|
|
142
142
|
Object.defineProperty(func, "__trigger", {
|
|
143
143
|
get: () => {
|
|
144
144
|
const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-functions",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"description": "Firebase SDK for Cloud Functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firebase",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
"@types/nock": "^10.0.3",
|
|
288
288
|
"@types/node": "^14.18.24",
|
|
289
289
|
"@types/node-fetch": "^3.0.3",
|
|
290
|
-
"@types/sinon": "^
|
|
290
|
+
"@types/sinon": "^9.0.11",
|
|
291
291
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
|
292
292
|
"@typescript-eslint/parser": "^5.33.1",
|
|
293
293
|
"api-extractor-model-me": "^0.1.1",
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"eslint-config-prettier": "^8.3.0",
|
|
300
300
|
"eslint-plugin-jsdoc": "^39.2.9",
|
|
301
301
|
"eslint-plugin-prettier": "^4.0.0",
|
|
302
|
-
"firebase-admin": "^
|
|
302
|
+
"firebase-admin": "^13.0.0",
|
|
303
303
|
"js-yaml": "^3.13.1",
|
|
304
304
|
"jsdom": "^16.2.1",
|
|
305
305
|
"jsonwebtoken": "^9.0.0",
|
|
@@ -313,13 +313,13 @@
|
|
|
313
313
|
"prettier": "^2.7.1",
|
|
314
314
|
"protobufjs-cli": "^1.1.1",
|
|
315
315
|
"semver": "^7.3.5",
|
|
316
|
-
"sinon": "^
|
|
316
|
+
"sinon": "^9.2.4",
|
|
317
317
|
"ts-node": "^10.4.0",
|
|
318
318
|
"typescript": "^4.3.5",
|
|
319
319
|
"yargs": "^15.3.1"
|
|
320
320
|
},
|
|
321
321
|
"peerDependencies": {
|
|
322
|
-
"firebase-admin": "^11.10.0 || ^12.0.0"
|
|
322
|
+
"firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0"
|
|
323
323
|
},
|
|
324
324
|
"engines": {
|
|
325
325
|
"node": ">=14.10.0"
|