UNPKG

firebase-tools

Version:
513 lines (512 loc) 20.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = exports.CLI_OAUTH_PROJECT_NUMBER = exports.GOOG_USER_PROJECT_HEADER = exports.standardHeaders = void 0; exports.setRefreshToken = setRefreshToken; exports.setAccessToken = setAccessToken; exports.getAccessToken = getAccessToken; exports.noKeepAliveAgent = noKeepAliveAgent; const url_1 = require("url"); const stream_1 = require("stream"); const undici_1 = require("undici"); const retry = require("retry"); const http = require("http"); const https = require("https"); const util_1 = require("util"); const auth = require("./auth"); const error_1 = require("./error"); const env_1 = require("./env"); const logger_1 = require("./logger"); const responseToError_1 = require("./responseToError"); const streamUtils_1 = require("./streamUtils"); const FormData = require("form-data"); const pkg = require("../package.json"); const CLI_VERSION = pkg.version; const standardHeaders = () => { const agent = (0, env_1.detectAIAgent)(); const agentStr = agent === "unknown" ? "" : ` agent-name/${agent}`; const platform = (0, env_1.isFirebaseMcp)() ? "FirebaseMCP" : "FirebaseCLI"; const clientVersion = `${platform}/${CLI_VERSION}${agentStr}`; return { Connection: "keep-alive", "User-Agent": clientVersion, "X-Client-Version": clientVersion, }; }; exports.standardHeaders = standardHeaders; const GOOG_QUOTA_USER_HEADER = "x-goog-quota-user"; exports.GOOG_USER_PROJECT_HEADER = "x-goog-user-project"; exports.CLI_OAUTH_PROJECT_NUMBER = "563584335869"; let accessToken = ""; let refreshToken = ""; function setRefreshToken(token = "") { refreshToken = token; } function setAccessToken(token = "") { accessToken = token; } async function getAccessToken() { const valid = auth.haveValidTokens(refreshToken, []); const usingADC = !auth.loggedIn(); if (accessToken && (valid || usingADC)) { return accessToken; } const data = await auth.getAccessToken(refreshToken, []); return data.access_token; } function proxyURIFromEnv() { const uri = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined; if (uri === "undefined" || uri === "null") { return undefined; } return uri; } const httpAgentNoKeepAlive = new http.Agent({ keepAlive: false }); const httpsAgentNoKeepAlive = new https.Agent({ keepAlive: false }); function noKeepAliveAgent(parsedURL) { return parsedURL.protocol === "https:" ? httpsAgentNoKeepAlive : httpAgentNoKeepAlive; } function isPrematureCloseError(err) { for (const candidate of [err, err?.original]) { if (!candidate) { continue; } const e = candidate; const code = e.code || e.errno; const message = typeof e.message === "string" ? e.message : ""; if (code === "ERR_STREAM_PREMATURE_CLOSE" || code === "ECONNRESET" || /premature close/i.test(message) || /socket hang up/i.test(message)) { return true; } } return false; } 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); } options(path, options = {}) { const reqOptions = Object.assign(options, { method: "OPTIONS", 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 Headers(reqOptions.headers), }); internalReqOptions = this.addRequestHeaders(internalReqOptions); if (this.opts.auth) { internalReqOptions = await this.addAuthHeader(internalReqOptions); } try { return await this.doRequest(internalReqOptions); } catch (thrown) { const originalErrorMessage = thrown.original?.message || thrown.message || ""; if (originalErrorMessage.includes(exports.CLI_OAUTH_PROJECT_NUMBER)) { throw new error_1.FirebaseError("An Internal error has occurred. Please try again in a few minutes. If this error persists, please open an issue at https://github.com/firebase/firebase-tools", { original: 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 Headers(); } for (const [h, v] of Object.entries((0, exports.standardHeaders)())) { if (!reqOptions.headers.has(h)) { reqOptions.headers.set(h, v); } } if (!reqOptions.headers.has("Content-Type")) { if (reqOptions.responseType === "json") { reqOptions.headers.set("Content-Type", "application/json"); } } if (!reqOptions.ignoreQuotaProject && process.env.GOOGLE_CLOUD_QUOTA_PROJECT && process.env.GOOGLE_CLOUD_QUOTA_PROJECT !== "") { reqOptions.headers.set(exports.GOOG_USER_PROJECT_HEADER, process.env.GOOGLE_CLOUD_QUOTA_PROJECT); } return reqOptions; } async addAuthHeader(reqOptions) { if (!reqOptions.headers) { reqOptions.headers = new Headers(); } let token; if (isLocalInsecureRequest(this.opts.urlPrefix)) { token = "owner"; } else { token = await getAccessToken(); } reqOptions.headers.set("Authorization", `Bearer ${token}`); return reqOptions; } requestURL(options) { const versionPath = this.opts.apiVersion ? `/${this.opts.apiVersion}` : ""; return `${this.opts.urlPrefix}${versionPath}${options.path}`; } async doRequest(options) { 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, }; const proxyURI = proxyURIFromEnv(); if (proxyURI) { fetchOptions.dispatcher = new undici_1.ProxyAgent({ uri: proxyURI }); } if (options.signal) { fetchOptions.signal = options.signal; } let reqTimeout; if (options.timeout) { const controller = new AbortController(); reqTimeout = setTimeout(() => { controller.abort(); }, options.timeout); fetchOptions.signal = controller.signal; } let bodyReplayable = true; if (typeof options.body === "string" || Buffer.isBuffer(options.body)) { fetchOptions.body = options.body; } else if (options.body instanceof FormData) { fetchOptions.body = options.body.getBuffer(); } else if (isStream(options.body)) { fetchOptions.body = options.body; fetchOptions.duplex = "half"; bodyReplayable = false; } else if (options.body !== undefined) { fetchOptions.body = JSON.stringify(options.body); } const operationOptions = { retries: options.retryCodes?.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); let disabledKeepAlive = false; return await new Promise((resolve, reject) => { operation.attempt(async (currentAttempt) => { let res; let body; try { if (currentAttempt > 1) { logger_1.logger.debug(`*** [apiv2] Attempting the request again. Attempt number ${currentAttempt}`); } this.logRequest(options); try { if (options.compress === false) { const undiciOptions = { method: fetchOptions.method, headers: {}, decompress: false, }; if (fetchOptions.dispatcher) { undiciOptions.dispatcher = fetchOptions.dispatcher; } if (fetchOptions.signal) { undiciOptions.signal = fetchOptions.signal; } if (fetchOptions.body) { undiciOptions.body = fetchOptions.body; } if (fetchOptions.headers) { for (const [key, value] of fetchOptions.headers.entries()) { undiciOptions.headers[key] = value; } } const undiciRes = await (0, undici_1.request)(fetchURL, undiciOptions); res = new UndiciResponseCompat(undiciRes); } else { res = await fetch(fetchURL, fetchOptions); } } catch (thrown) { const err = thrown && typeof thrown === "object" && thrown.cause instanceof Error ? thrown.cause : thrown instanceof Error ? thrown : new Error(thrown); logger_1.logger.debug(`*** [apiv2] error from fetch(${fetchURL}, ${JSON.stringify(fetchOptions)}): ${err}`); 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") { if (res.body) { if (typeof res.body.getReader === "function") { body = stream_1.Readable.fromWeb(res.body); } else { body = res.body; } } else { const emptyStream = new stream_1.Readable(); emptyStream.push(null); body = emptyStream; } } else { throw new error_1.FirebaseError(`Unable to interpret response. Please set responseType.`, { exit: 2, }); } } catch (err) { if (!disabledKeepAlive && bodyReplayable && !proxyURIFromEnv() && isPrematureCloseError(err)) { disabledKeepAlive = true; fetchOptions.agent = noKeepAliveAgent; const closeHeaders = new Headers(fetchOptions.headers); closeHeaders.set("Connection", "close"); fetchOptions.headers = closeHeaders; logger_1.logger.debug(`*** [apiv2] retrying ${fetchURL} without keep-alive after a premature close error`); if (operation.retry(err instanceof Error ? err : new Error(`${err}`))) { return; } } return err instanceof error_1.FirebaseError ? reject(err) : reject(new error_1.FirebaseError(`${err}`)); } this.logResponse(res, body, options); if (res.status >= 400) { if (res.status === 401 && this.opts.auth) { logger_1.logger.debug("Got a 401 Unauthenticated error for a call that required authentication. Refreshing tokens."); setAccessToken(); setAccessToken(await getAccessToken()); } if (options.retryCodes?.includes(res.status)) { const err = (0, responseToError_1.responseToError)({ statusCode: res.status }, body, fetchURL) || undefined; if (operation.retry(err)) { return; } } if (!options.resolveOnHTTPError) { return reject((0, responseToError_1.responseToError)({ statusCode: res.status }, body, fetchURL)); } } resolve({ status: res.status, response: res, body, }); }); }); } logRequest(options) { let queryParamsLog = "[none]"; if (options.queryParams) { queryParamsLog = "[omitted]"; if (!options.skipLog?.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_HEADER) || headers.has(exports.GOOG_USER_PROJECT_HEADER))) { const userHeader = headers.has(GOOG_QUOTA_USER_HEADER) ? `${GOOG_QUOTA_USER_HEADER}=${headers.get(GOOG_QUOTA_USER_HEADER)}` : ""; const projectHeader = headers.has(exports.GOOG_USER_PROJECT_HEADER) ? `${exports.GOOG_USER_PROJECT_HEADER}=${headers.get(exports.GOOG_USER_PROJECT_HEADER)}` : ""; logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} ${userHeader} ${projectHeader}`); } if (options.body !== undefined) { let logBody = "[omitted]"; if (!options.skipLog?.body) { logBody = bodyToString(options.body); } logger_1.logger.debug(`>>> [apiv2][body] ${options.method} ${logURL} ${logBody}`); } } logResponse(res, body, options) { const logURL = this.requestURL(options); logger_1.logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`); let logBody = "[omitted]"; if (!options.skipLog?.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:"; } class UndiciResponseCompat { constructor(undiciRes) { this.undiciRes = undiciRes; this.status = undiciRes.statusCode; this.ok = undiciRes.statusCode >= 200 && undiciRes.statusCode < 300; this.headers = new Headers(); for (const [key, value] of Object.entries(undiciRes.headers)) { if (Array.isArray(value)) { for (const v of value) { this.headers.append(key, v); } } else if (value !== undefined) { this.headers.set(key, String(value)); } } this.body = undiciRes.body; } async text() { if (!this.body) { return ""; } return (0, streamUtils_1.streamToString)(this.body); } } 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; }