"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = exports.setAccessToken = exports.setRefreshToken = void 0; const url_1 = require("url"); const stream_1 = require("stream"); const ProxyAgent = require("proxy-agent"); const retry = require("retry"); const abort_controller_1 = require("abort-controller"); const node_fetch_1 = require("node-fetch"); const util_1 = require("util"); const auth = require("./auth"); const error_1 = require("./error"); const logger_1 = require("./logger"); const responseToError_1 = require("./responseToError"); const FormData = require("form-data"); const pkg = require("../package.json"); const CLI_VERSION = pkg.version; const GOOG_QUOTA_USER = "x-goog-quota-user"; let accessToken = ""; let refreshToken = ""; function setRefreshToken(token = "") { refreshToken = token; } exports.setRefreshToken = setRefreshToken; function setAccessToken(token = "") { accessToken = token; } exports.setAccessToken = setAccessToken; function proxyURIFromEnv() { return (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined); } class Client { constructor(opts) { this.opts = opts; if (this.opts.auth === undefined) { this.opts.auth = true; } if (this.opts.urlPrefix.endsWith("/")) { this.opts.urlPrefix = this.opts.urlPrefix.substring(0, this.opts.urlPrefix.length - 1); } } get(path, options = {}) { const reqOptions = Object.assign(options, { method: "GET", path, }); return this.request(reqOptions); } post(path, json, options = {}) { const reqOptions = Object.assign(options, { method: "POST", path, body: json, }); return this.request(reqOptions); } patch(path, json, options = {}) { const reqOptions = Object.assign(options, { method: "PATCH", path, body: json, }); return this.request(reqOptions); } put(path, json, options = {}) { const reqOptions = Object.assign(options, { method: "PUT", path, body: json, }); return this.request(reqOptions); } delete(path, options = {}) { const reqOptions = Object.assign(options, { method: "DELETE", path, }); return this.request(reqOptions); } async request(reqOptions) { if (!reqOptions.responseType) { reqOptions.responseType = "json"; } if (reqOptions.responseType === "stream" && !reqOptions.resolveOnHTTPError) { throw new error_1.FirebaseError("apiv2 will not handle HTTP errors while streaming and you must set `resolveOnHTTPError` and check for res.status >= 400 on your own", { exit: 2 }); } let internalReqOptions = Object.assign(reqOptions, { headers: new node_fetch_1.Headers(reqOptions.headers), }); internalReqOptions = this.addRequestHeaders(internalReqOptions); if (this.opts.auth) { internalReqOptions = await this.addAuthHeader(internalReqOptions); } try { return await this.doRequest(internalReqOptions); } catch (thrown) { if (thrown instanceof error_1.FirebaseError) { throw thrown; } let err; if (thrown instanceof Error) { err = thrown; } else { err = new Error(thrown); } throw new error_1.FirebaseError(`Failed to make request: ${err.message}`, { original: err }); } } addRequestHeaders(reqOptions) { if (!reqOptions.headers) { reqOptions.headers = new node_fetch_1.Headers(); } reqOptions.headers.set("Connection", "keep-alive"); if (!reqOptions.headers.has("User-Agent")) { reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`); } reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`); if (!reqOptions.headers.has("Content-Type")) { if (reqOptions.responseType === "json") { reqOptions.headers.set("Content-Type", "application/json"); } } return reqOptions; } async addAuthHeader(reqOptions) { if (!reqOptions.headers) { reqOptions.headers = new node_fetch_1.Headers(); } let token; if (isLocalInsecureRequest(this.opts.urlPrefix)) { token = "owner"; } else { token = await this.getAccessToken(); } reqOptions.headers.set("Authorization", `Bearer ${token}`); return reqOptions; } async getAccessToken() { if (accessToken) { return accessToken; } const data = (await auth.getAccessToken(refreshToken, [])); return data.access_token; } requestURL(options) { const versionPath = this.opts.apiVersion ? `/${this.opts.apiVersion}` : ""; return `${this.opts.urlPrefix}${versionPath}${options.path}`; } async doRequest(options) { var _a; if (!options.path.startsWith("/")) { options.path = "/" + options.path; } let fetchURL = this.requestURL(options); if (options.queryParams) { if (!(options.queryParams instanceof url_1.URLSearchParams)) { const sp = new url_1.URLSearchParams(); for (const key of Object.keys(options.queryParams)) { const value = options.queryParams[key]; sp.append(key, `${value}`); } options.queryParams = sp; } const queryString = options.queryParams.toString(); if (queryString) { fetchURL += `?${queryString}`; } } const fetchOptions = { headers: options.headers, method: options.method, redirect: options.redirect, compress: options.compress, }; if (this.opts.proxy) { fetchOptions.agent = new ProxyAgent(this.opts.proxy); } const envProxy = proxyURIFromEnv(); if (envProxy) { fetchOptions.agent = new ProxyAgent(envProxy); } if (options.signal) { fetchOptions.signal = options.signal; } let reqTimeout; if (options.timeout) { const controller = new abort_controller_1.default(); reqTimeout = setTimeout(() => { controller.abort(); }, options.timeout); fetchOptions.signal = controller.signal; } if (typeof options.body === "string" || isStream(options.body)) { fetchOptions.body = options.body; } else if (options.body !== undefined) { fetchOptions.body = JSON.stringify(options.body); } const operationOptions = { retries: ((_a = options.retryCodes) === null || _a === void 0 ? void 0 : _a.length) ? 1 : 2, minTimeout: 1 * 1000, maxTimeout: 5 * 1000, }; if (typeof options.retries === "number") { operationOptions.retries = options.retries; } if (typeof options.retryMinTimeout === "number") { operationOptions.minTimeout = options.retryMinTimeout; } if (typeof options.retryMaxTimeout === "number") { operationOptions.maxTimeout = options.retryMaxTimeout; } const operation = retry.operation(operationOptions); return await new Promise((resolve, reject) => { operation.attempt(async (currentAttempt) => { var _a; let res; let body; try { if (currentAttempt > 1) { logger_1.logger.debug(`*** [apiv2] Attempting the request again. Attempt number ${currentAttempt}`); } this.logRequest(options); try { res = await (0, node_fetch_1.default)(fetchURL, fetchOptions); } catch (thrown) { const err = thrown instanceof Error ? thrown : new Error(thrown); const isAbortError = err.name.includes("AbortError"); if (isAbortError) { throw new error_1.FirebaseError(`Timeout reached making request to ${fetchURL}`, { original: err, }); } throw new error_1.FirebaseError(`Failed to make request to ${fetchURL}`, { original: err }); } finally { if (reqTimeout) { clearTimeout(reqTimeout); } } if (options.responseType === "json") { const text = await res.text(); if (!text.length) { body = undefined; } else { try { body = JSON.parse(text); } catch (err) { this.logResponse(res, text, options); throw new error_1.FirebaseError(`Unable to parse JSON: ${err}`); } } } else if (options.responseType === "xml") { body = (await res.text()); } else if (options.responseType === "stream") { body = res.body; } else { throw new error_1.FirebaseError(`Unable to interpret response. Please set responseType.`, { exit: 2, }); } } catch (err) { return err instanceof error_1.FirebaseError ? reject(err) : reject(new error_1.FirebaseError(`${err}`)); } this.logResponse(res, body, options); if (res.status >= 400) { if ((_a = options.retryCodes) === null || _a === void 0 ? void 0 : _a.includes(res.status)) { const err = (0, responseToError_1.responseToError)({ statusCode: res.status }, body) || undefined; if (operation.retry(err)) { return; } } if (!options.resolveOnHTTPError) { return reject((0, responseToError_1.responseToError)({ statusCode: res.status }, body)); } } resolve({ status: res.status, response: res, body, }); }); }); } logRequest(options) { var _a, _b; let queryParamsLog = "[none]"; if (options.queryParams) { queryParamsLog = "[omitted]"; if (!((_a = options.skipLog) === null || _a === void 0 ? void 0 : _a.queryParams)) { queryParamsLog = options.queryParams instanceof url_1.URLSearchParams ? options.queryParams.toString() : JSON.stringify(options.queryParams); } } const logURL = this.requestURL(options); logger_1.logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`); const headers = options.headers; if (headers && headers.has(GOOG_QUOTA_USER)) { logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${headers.get(GOOG_QUOTA_USER) || ""}`); } if (options.body !== undefined) { let logBody = "[omitted]"; if (!((_b = options.skipLog) === null || _b === void 0 ? void 0 : _b.body)) { logBody = bodyToString(options.body); } logger_1.logger.debug(`>>> [apiv2][body] ${options.method} ${logURL} ${logBody}`); } } logResponse(res, body, options) { var _a; const logURL = this.requestURL(options); logger_1.logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`); let logBody = "[omitted]"; if (!((_a = options.skipLog) === null || _a === void 0 ? void 0 : _a.resBody)) { logBody = bodyToString(body); } logger_1.logger.debug(`<<< [apiv2][body] ${options.method} ${logURL} ${logBody}`); } } exports.Client = Client; function isLocalInsecureRequest(urlPrefix) { const u = new url_1.URL(urlPrefix); return u.protocol === "http:"; } function bodyToString(body) { if (isStream(body)) { return "[stream]"; } else { try { return JSON.stringify(body); } catch (_) { return util_1.default.inspect(body); } } } function isStream(o) { return o instanceof stream_1.Readable || o instanceof FormData; }