firebase-functions 6.1.2 → 6.2.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.
@@ -84,22 +84,27 @@ export interface CallableRequest<T = any> {
84
84
  * The raw request handled by the callable.
85
85
  */
86
86
  rawRequest: Request;
87
+ /**
88
+ * Whether this is a streaming request.
89
+ * Code can be optimized by not trying to generate a stream of chunks to
90
+ * call response.sendChunk on if request.acceptsStreaming is false.
91
+ * It is always safe, however, to call response.sendChunk as this will
92
+ * noop if acceptsStreaming is false.
93
+ */
94
+ acceptsStreaming: boolean;
87
95
  }
88
96
  /**
89
- * CallableProxyResponse exposes subset of express.Response object
90
- * to allow writing partial, streaming responses back to the client.
97
+ * CallableProxyResponse allows streaming response chunks and listening to signals
98
+ * triggered in events such as a disconnect.
91
99
  */
92
- export interface CallableProxyResponse {
100
+ export interface CallableResponse<T = unknown> {
93
101
  /**
94
102
  * Writes a chunk of the response body to the client. This method can be called
95
103
  * multiple times to stream data progressively.
104
+ * Returns a promise of whether the data was written. This can be false, for example,
105
+ * if the request was not a streaming request. Rejects if there is a network error.
96
106
  */
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;
107
+ sendChunk: (chunk: T) => Promise<boolean>;
103
108
  /**
104
109
  * An AbortSignal that is triggered when the client disconnects or the
105
110
  * request is terminated prematurely.
@@ -283,13 +283,9 @@ async function checkTokens(req, ctx, options) {
283
283
  app: "INVALID",
284
284
  auth: "INVALID",
285
285
  };
286
- await Promise.all([
287
- Promise.resolve().then(async () => {
288
- verifications.auth = await checkAuthToken(req, ctx);
289
- }),
290
- Promise.resolve().then(async () => {
291
- verifications.app = await checkAppCheckToken(req, ctx, options);
292
- }),
286
+ [verifications.auth, verifications.app] = await Promise.all([
287
+ checkAuthToken(req, ctx),
288
+ checkAppCheckToken(req, ctx, options),
293
289
  ]);
294
290
  const logPayload = {
295
291
  verifications,
@@ -405,6 +401,7 @@ function encodeSSE(data) {
405
401
  /** @internal */
406
402
  function wrapOnCallHandler(options, handler, version) {
407
403
  return async (req, res) => {
404
+ var _a;
408
405
  const abortController = new AbortController();
409
406
  let heartbeatInterval = null;
410
407
  const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
@@ -487,6 +484,12 @@ function wrapOnCallHandler(options, handler, version) {
487
484
  throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
488
485
  }
489
486
  const data = decode(req.body.data);
487
+ if (options.authPolicy) {
488
+ const authorized = await options.authPolicy((_a = context.auth) !== null && _a !== void 0 ? _a : null, data);
489
+ if (!authorized) {
490
+ throw new HttpsError("permission-denied", "Permission Denied");
491
+ }
492
+ }
490
493
  let result;
491
494
  if (version === "gcfv1") {
492
495
  result = await handler(data, context);
@@ -495,26 +498,38 @@ function wrapOnCallHandler(options, handler, version) {
495
498
  const arg = {
496
499
  ...context,
497
500
  data,
501
+ acceptsStreaming,
498
502
  };
499
503
  const responseProxy = {
500
- write(chunk) {
504
+ sendChunk(chunk) {
501
505
  // if client doesn't accept sse-protocol, response.write() is no-op.
502
506
  if (!acceptsStreaming) {
503
- return false;
507
+ return Promise.resolve(false);
504
508
  }
505
509
  // if connection is already closed, response.write() is no-op.
506
510
  if (abortController.signal.aborted) {
507
- return false;
511
+ return Promise.resolve(false);
508
512
  }
509
513
  const formattedData = encodeSSE({ message: chunk });
510
- const wrote = res.write(formattedData);
514
+ let resolve;
515
+ let reject;
516
+ const p = new Promise((res, rej) => {
517
+ resolve = res;
518
+ reject = rej;
519
+ });
520
+ const wrote = res.write(formattedData, (error) => {
521
+ if (error) {
522
+ reject(error);
523
+ return;
524
+ }
525
+ resolve(wrote);
526
+ });
511
527
  // Reset heartbeat timer after successful write
512
528
  if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
513
529
  scheduleHeartbeat();
514
530
  }
515
- return wrote;
531
+ return p;
516
532
  },
517
- acceptsStreaming,
518
533
  signal: abortController.signal,
519
534
  };
520
535
  if (acceptsStreaming) {
@@ -40,8 +40,8 @@ async function loadModule(functionsDir) {
40
40
  return require(path.resolve(absolutePath));
41
41
  }
42
42
  catch (e) {
43
- if (e.code === "ERR_REQUIRE_ESM") {
44
- // This is an ESM package!
43
+ if (e.code === "ERR_REQUIRE_ESM" || e.code === "ERR_REQUIRE_ASYNC_MODULE") {
44
+ // This is an ESM package, or one containing top-level awaits!
45
45
  const modulePath = require.resolve(absolutePath);
46
46
  // Resolve module path to file:// URL. Required for windows support.
47
47
  const moduleURL = url.pathToFileURL(modulePath).href;
@@ -1,6 +1,6 @@
1
1
  import * as express from "express";
2
2
  import { ResetValue } from "../../common/options";
3
- import { CallableRequest, CallableProxyResponse, FunctionsErrorCode, HttpsError, Request } from "../../common/providers/https";
3
+ import { CallableRequest, CallableResponse, FunctionsErrorCode, HttpsError, Request, AuthData } from "../../common/providers/https";
4
4
  import { ManifestEndpoint } from "../../runtime/manifest";
5
5
  import { GlobalOptions, SupportedRegion } from "../options";
6
6
  import { Expression } from "../../params";
@@ -101,7 +101,7 @@ export interface HttpsOptions extends Omit<GlobalOptions, "region" | "enforceApp
101
101
  /**
102
102
  * Options that can be set on a callable HTTPS function.
103
103
  */
104
- export interface CallableOptions extends HttpsOptions {
104
+ export interface CallableOptions<T = any> extends HttpsOptions {
105
105
  /**
106
106
  * Determines whether Firebase AppCheck is enforced.
107
107
  * When true, requests with invalid tokens autorespond with a 401
@@ -139,7 +139,22 @@ export interface CallableOptions extends HttpsOptions {
139
139
  * Defaults to 30 seconds.
140
140
  */
141
141
  heartbeatSeconds?: number | null;
142
+ /**
143
+ * Callback for whether a request is authorized.
144
+ *
145
+ * Designed to allow reusable auth policies to be passed as an options object. Two built-in reusable policies exist:
146
+ * isSignedIn and hasClaim.
147
+ */
148
+ authPolicy?: (auth: AuthData | null, data: T) => boolean | Promise<boolean>;
142
149
  }
150
+ /**
151
+ * An auth policy that requires a user to be signed in.
152
+ */
153
+ export declare const isSignedIn: () => (auth: AuthData | null) => boolean;
154
+ /**
155
+ * An auth policy that requires a user to be both signed in and have a specific claim (optionally with a specific value)
156
+ */
157
+ export declare const hasClaim: (claim: string, value?: string) => (auth: AuthData | null) => boolean;
143
158
  /**
144
159
  * Handles HTTPS requests.
145
160
  */
@@ -156,12 +171,16 @@ res: express.Response) => void | Promise<void>) & {
156
171
  /**
157
172
  * Creates a callable method for clients to call using a Firebase SDK.
158
173
  */
159
- export interface CallableFunction<T, Return> extends HttpsFunction {
174
+ export interface CallableFunction<T, Return, Stream = unknown> extends HttpsFunction {
160
175
  /** Executes the handler function with the provided data as input. Used for unit testing.
161
176
  * @param data - An input for the handler function.
162
177
  * @returns The output of the handler function.
163
178
  */
164
- run(data: CallableRequest<T>): Return;
179
+ run(request: CallableRequest<T>): Return;
180
+ stream(request: CallableRequest<T>, response: CallableResponse<Stream>): {
181
+ stream: AsyncIterator<Stream>;
182
+ output: Return;
183
+ };
165
184
  }
166
185
  /**
167
186
  * Handles HTTPS requests.
@@ -182,10 +201,10 @@ export declare function onRequest(handler: (request: Request, response: express.
182
201
  * @param handler - A function that takes a {@link https.CallableRequest}.
183
202
  * @returns A function that you can export and deploy.
184
203
  */
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>>;
204
+ export declare function onCall<T = any, Return = any | Promise<any>, Stream = unknown>(opts: CallableOptions<T>, handler: (request: CallableRequest<T>, response?: CallableResponse<Stream>) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>, Stream>;
186
205
  /**
187
206
  * Declares a callable method for clients to call using a Firebase SDK.
188
207
  * @param handler - A function that takes a {@link https.CallableRequest}.
189
208
  * @returns A function that you can export and deploy.
190
209
  */
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>>;
210
+ export declare function onCall<T = any, Return = any | Promise<any>, Stream = unknown>(handler: (request: CallableRequest<T>, response?: CallableResponse<Stream>) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>>;
@@ -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.onCall = exports.onRequest = exports.HttpsError = void 0;
24
+ exports.onCall = exports.onRequest = exports.hasClaim = exports.isSignedIn = exports.HttpsError = void 0;
25
25
  /**
26
26
  * Cloud functions to handle HTTPS request or callable RPCs.
27
27
  * @packageDocumentation
@@ -35,6 +35,24 @@ Object.defineProperty(exports, "HttpsError", { enumerable: true, get: function (
35
35
  const manifest_1 = require("../../runtime/manifest");
36
36
  const options = require("../options");
37
37
  const onInit_1 = require("../../common/onInit");
38
+ /**
39
+ * An auth policy that requires a user to be signed in.
40
+ */
41
+ const isSignedIn = () => (auth) => !!auth;
42
+ exports.isSignedIn = isSignedIn;
43
+ /**
44
+ * An auth policy that requires a user to be both signed in and have a specific claim (optionally with a specific value)
45
+ */
46
+ const hasClaim = (claim, value) => (auth) => {
47
+ if (!auth) {
48
+ return false;
49
+ }
50
+ if (!(claim in auth.token)) {
51
+ return false;
52
+ }
53
+ return !value || auth.token[claim] === value;
54
+ };
55
+ exports.hasClaim = hasClaim;
38
56
  function onRequest(optsOrHandler, handler) {
39
57
  let opts;
40
58
  if (arguments.length === 1) {
@@ -131,12 +149,13 @@ function onCall(optsOrHandler, handler) {
131
149
  origin = origin[0];
132
150
  }
133
151
  // fix the length of handler to make the call to handler consistent
134
- const fixedLen = (req, resp) => (0, onInit_1.withInit)(handler)(req, resp);
152
+ const fixedLen = (req, resp) => handler(req, resp);
135
153
  let func = (0, https_1.onCallHandler)({
136
154
  cors: { origin, methods: "POST" },
137
155
  enforceAppCheck: (_a = opts.enforceAppCheck) !== null && _a !== void 0 ? _a : options.getGlobalOptions().enforceAppCheck,
138
156
  consumeAppCheckToken: opts.consumeAppCheckToken,
139
157
  heartbeatSeconds: opts.heartbeatSeconds,
158
+ authPolicy: opts.authPolicy,
140
159
  }, fixedLen, "gcfv2");
141
160
  func = (0, trace_1.wrapTraceContext)((0, onInit_1.withInit)(func));
142
161
  Object.defineProperty(func, "__trigger", {
@@ -175,7 +194,18 @@ function onCall(optsOrHandler, handler) {
175
194
  },
176
195
  callableTrigger: {},
177
196
  };
197
+ // TODO: in the next major version, do auth/appcheck in these helper methods too.
178
198
  func.run = (0, onInit_1.withInit)(handler);
199
+ func.stream = () => {
200
+ return {
201
+ stream: {
202
+ next() {
203
+ return Promise.reject("Coming soon");
204
+ },
205
+ },
206
+ output: Promise.reject("Coming soon"),
207
+ };
208
+ };
179
209
  return func;
180
210
  }
181
211
  exports.onCall = onCall;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-functions",
3
- "version": "6.1.2",
3
+ "version": "6.2.0",
4
4
  "description": "Firebase SDK for Cloud Functions",
5
5
  "keywords": [
6
6
  "firebase",