firebase-tools
Version:
Command-Line Interface for Firebase
513 lines (512 loc) • 20.7 kB
JavaScript
;
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;
}