You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
356 lines
13 KiB
356 lines
13 KiB
"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;
|
|
}
|