text: "use strict";
  // The MIT License (MIT)
  //
  // Copyright (c) 2017 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 });
  const cors = require("cors");
  const _ = require("lodash");
  const apps_1 = require("../apps");
  const cloud_functions_1 = require("../cloud-functions");
  /**
   * Handle HTTP requests.
   * @param handler A function that takes a request and response object,
   * same signature as an Express app.
   */
  function onRequest(handler) {
      return _onRequestWithOptions(handler, {});
  }
  exports.onRequest = onRequest;
  /**
   * Declares a callable method for clients to call using a Firebase SDK.
   * @param handler A method that takes a data and context and returns a value.
   */
  function onCall(handler) {
      return _onCallWithOptions(handler, {});
  }
  exports.onCall = onCall;
  /** @internal */
  function _onRequestWithOptions(handler, options) {
      // lets us add __trigger without altering handler:
      const cloudFunction = (req, res) => {
          handler(req, res);
      };
      cloudFunction.__trigger = _.assign(cloud_functions_1.optionsToTrigger(options), {
          httpsTrigger: {},
      });
      // TODO parse the options
      return cloudFunction;
  }
  exports._onRequestWithOptions = _onRequestWithOptions;
  /**
   * Standard error codes 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 make sure it's in the supported set.
   */
  const errorCodeMap = {
      ok: 'OK',
      cancelled: 'CANCELLED',
      unknown: 'UNKNOWN',
      'invalid-argument': 'INVALID_ARGUMENT',
      'deadline-exceeded': 'DEADLINE_EXCEEDED',
      'not-found': 'NOT_FOUND',
      'already-exists': 'ALREADY_EXISTS',
      'permission-denied': 'PERMISSION_DENIED',
      unauthenticated: 'UNAUTHENTICATED',
      'resource-exhausted': 'RESOURCE_EXHAUSTED',
      'failed-precondition': 'FAILED_PRECONDITION',
      aborted: 'ABORTED',
      'out-of-range': 'OUT_OF_RANGE',
      unimplemented: 'UNIMPLEMENTED',
      internal: 'INTERNAL',
      unavailable: 'UNAVAILABLE',
      'data-loss': 'DATA_LOSS',
  };
  /**
   * 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);
          if (!errorCodeMap[code]) {
              throw new Error('Unknown error status: ' + code);
          }
          this.code = code;
          this.details = details;
      }
      /**
       * @internal
       * A string representation of the Google error code for this error for HTTP.
       */
      get status() {
          return errorCodeMap[this.code];
      }
      /**
       * @internal
       * Returns the canonical http status code for the given error.
       */
      get httpStatus() {
          switch (this.code) {
              case 'ok':
                  return 200;
              case 'cancelled':
                  return 499;
              case 'unknown':
                  return 500;
              case 'invalid-argument':
                  return 400;
              case 'deadline-exceeded':
                  return 504;
              case 'not-found':
                  return 404;
              case 'already-exists':
                  return 409;
              case 'permission-denied':
                  return 403;
              case 'unauthenticated':
                  return 401;
              case 'resource-exhausted':
                  return 429;
              case 'failed-precondition':
                  return 400;
              case 'aborted':
                  return 409;
              case 'out-of-range':
                  return 400;
              case 'unimplemented':
                  return 501;
              case 'internal':
                  return 500;
              case 'unavailable':
                  return 503;
              case 'data-loss':
                  return 500;
              // This should never happen as long as the type system is doing its job.
              default:
                  throw new Error('Invalid error code: ' + this.code);
          }
      }
      /** @internal */
      toJSON() {
          const json = {
              status: this.status,
              message: this.message,
          };
          if (!_.isUndefined(this.details)) {
              json.details = this.details;
          }
          return json;
      }
  }
  exports.HttpsError = HttpsError;
  // Returns true if req is a properly formatted callable request.
  function isValidRequest(req) {
      // The body must not be empty.
      if (!req.body) {
          console.warn('Request is missing body.');
          return false;
      }
      // Make sure it's a POST.
      if (req.method !== 'POST') {
          console.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.substr(0, semiColon).trim();
      }
      if (contentType !== 'application/json') {
          console.warn('Request has incorrect Content-Type.', contentType);
          return false;
      }
      // The body must have data.
      if (_.isUndefined(req.body.data)) {
          console.warn('Request body is missing data.', req.body);
          return false;
      }
      // TODO(klimt): Allow only whitelisted http headers.
      // Verify that the body does not have any extra fields.
      const extras = _.omit(req.body, 'data');
      if (!_.isEmpty(extras)) {
          console.warn('Request body has extra fields.', extras);
          return false;
      }
      return true;
  }
  const LONG_TYPE = 'type.googleapis.com/google.protobuf.Int64Value';
  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.
   */
  /** @internal */
  function encode(data) {
      if (_.isNull(data) || _.isUndefined(data)) {
          return null;
      }
      // Oddly, _.isFinite(new Number(x)) always returns false, so unwrap Numbers.
      if (data instanceof Number) {
          data = data.valueOf();
      }
      if (_.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 (_.isBoolean(data)) {
          return data;
      }
      if (_.isString(data)) {
          return data;
      }
      if (_.isArray(data)) {
          return _.map(data, encode);
      }
      if (_.isObject(data)) {
          // It's not safe to use _.forEach, because the object might be 'array-like'
          // if it has a key called 'length'. Note that this intentionally overrides
          // any toJSON method that an object may have.
          return _.mapValues(data, encode);
      }
      // If we got this far, the data is not encodable.
      console.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.
   */
  /** @internal */
  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)) {
                      console.error('Data cannot be decoded from JSON.', data);
                      throw new Error('Data cannot be decoded from JSON: ' + data);
                  }
                  return value;
              }
              default: {
                  console.error('Data cannot be decoded from JSON.', data);
                  throw new Error('Data cannot be decoded from JSON: ' + data);
              }
          }
      }
      if (_.isArray(data)) {
          return _.map(data, decode);
      }
      if (_.isObject(data)) {
          // It's not safe to use _.forEach, because the object might be 'array-like'
          // if it has a key called 'length'.
          return _.mapValues(data, decode);
      }
      // Anything else is safe to return.
      return data;
  }
  exports.decode = decode;
  const corsHandler = cors({ origin: true, methods: 'POST' });
  /** @internal */
  function _onCallWithOptions(handler, options) {
      const func = async (req, res) => {
          try {
              if (!isValidRequest(req)) {
                  console.error('Invalid request', req);
                  throw new HttpsError('invalid-argument', 'Bad Request');
              }
              const context = { rawRequest: req };
              const authorization = req.header('Authorization');
              if (authorization) {
                  const match = authorization.match(/^Bearer (.*)$/);
                  if (!match) {
                      throw new HttpsError('unauthenticated', 'Unauthenticated');
                  }
                  const idToken = match[1];
                  try {
                      const authToken = await apps_1.apps()
                          .admin.auth()
                          .verifyIdToken(idToken);
                      context.auth = {
                          uid: authToken.uid,
                          token: authToken,
                      };
                  }
                  catch (e) {
                      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 data = decode(req.body.data);
              let result = await handler(data, context);
              // 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 };
              res.status(200).send(responseBody);
          }
          catch (error) {
              if (!(error instanceof HttpsError)) {
                  // This doesn't count as an 'explicit' error.
                  console.error('Unhandled error', error);
                  error = new HttpsError('internal', 'INTERNAL');
              }
              const status = error.httpStatus;
              const body = { error: error.toJSON() };
              res.status(status).send(body);
          }
      };
      // Wrap the function with a cors handler.
      const corsFunc = (req, res) => {
          return corsHandler(req, res, () => func(req, res));
      };
      corsFunc.__trigger = _.assign(cloud_functions_1.optionsToTrigger(options), {
          httpsTrigger: {},
          labels: { 'deployment-callable': 'true' },
      });
      corsFunc.run = handler;
      return corsFunc;
  }
  exports._onCallWithOptions = _onCallWithOptions;
  