oso-cloud
Version:
Oso Cloud Node.js Client SDK
504 lines • 20.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Api = void 0;
const url_1 = require("url");
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const fs_1 = require("fs");
const crypto_1 = require("crypto");
const ip_num_1 = require("ip-num");
const cross_fetch_1 = __importDefault(require("cross-fetch"));
const package_json_1 = require("../package.json");
const fetch_retry_1 = __importDefault(require("fetch-retry"));
const _1 = require(".");
const promises_1 = __importDefault(require("dns/promises"));
const retryOptions = {
retries: 2,
retryOn: [400, 429, 500, 501, 502, 503, 504],
retryDelay: function (attempt, _error, _response) {
return Math.pow(2, attempt) * 10;
},
};
function withTimeout(fetch, timeoutMillis) {
return (input, init) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMillis);
init = Object.assign({ signal: controller.signal }, init);
if (timeoutMillis == 0) {
// 'setTimeout(..., 0)' can still happen after the 'fetch' completes.
// For consistent test results, make sure we abort immediately for timeout=0.
controller.abort();
}
return fetch(input, init).finally(() => clearTimeout(timeout));
};
}
// 10 MiB
const maxBodySizeBytes = 10485760;
/**
* Returns a custom resolver that uses the given list of servers as the DNS
* servers. These can either be domain names or IP addresses. After the first
* call, caches the resolver.
*/
let dnsResolver = undefined;
function resolverWithDnsServers(servers) {
return __awaiter(this, void 0, void 0, function* () {
if (dnsResolver) {
return dnsResolver;
}
if (servers) {
return Promise.all(servers.map((server) => {
if (ip_num_1.Validator.isValidIPv4String(server)[0] ||
ip_num_1.Validator.isValidIPv6String(server)[0]) {
return Promise.resolve([server]);
}
else {
return promises_1.default.resolve4(server);
}
})).then((results) => {
const resolver = new promises_1.default.Resolver();
resolver.setServers(results.flat());
dnsResolver = resolver;
return resolver;
});
}
else {
dnsResolver = new promises_1.default.Resolver();
return dnsResolver;
}
});
}
/**
* Returns a function that can be used to lookup a given hostname. If dns
* servers are passed in, creates a resolver that uses those servers.
*/
const lookupWithDnsServers = (servers) => (hostname, _, cb) => {
resolverWithDnsServers(servers).then((resolver) => resolver.resolve4(hostname).then((ips) => {
if (ips.length === 0) {
throw new Error(`Unable to resolve ${hostname}`);
}
cb(null, ips.map((ip) => ({ address: ip, family: 4 })));
}));
};
class Api {
constructor(url, apiKey, options) {
if (typeof url !== "string" && url !== undefined)
throw new TypeError(`'url' should be a string`);
this.url = url || "https://api.osohq.com";
this.token = apiKey;
this.debug = options.debug;
this.clientId = (0, crypto_1.randomUUID)();
if (options.userAgent) {
this.userAgent = options.userAgent;
}
else if (typeof process === "object") {
this.userAgent = `Oso Cloud (nodejs ${process.version}; rv:${package_json_1.version})`;
}
else {
this.userAgent = `Oso Cloud (browser; rv:${package_json_1.version})`;
}
const agentOptions = {
keepAlive: true,
};
if (options.dnsServerEndpoints && options.dnsServerEndpoints.length > 0) {
agentOptions.lookup = lookupWithDnsServers(options.dnsServerEndpoints);
}
const parsed = new url_1.URL(this.url);
if (parsed.protocol === "https:") {
this.agent = new https_1.default.Agent(agentOptions);
}
else if (parsed.protocol === "http:") {
this.agent = new http_1.default.Agent(agentOptions);
}
else {
throw new TypeError(`Invalid protocol. Expected 'http' or 'https'; received: ${parsed.protocol}`);
}
this.lastOffset = null;
if (options.fallbackUrl) {
this.fallbackUrl = options.fallbackUrl;
const parsed = new url_1.URL(this.fallbackUrl);
if (parsed.protocol === "https:") {
this.fallbackAgent = new https_1.default.Agent({ keepAlive: true });
}
else if (parsed.protocol === "http:") {
this.fallbackAgent = new http_1.default.Agent({ keepAlive: true });
}
else {
throw new TypeError(`Invalid protocol for fallbackUrl. Expected 'http' or 'https'; received: ${parsed.protocol}`);
}
}
if (options.dataBindings) {
this.dataBindings = fs_1.promises.readFile(options.dataBindings, "utf-8");
}
this.fetchTimeoutMillis = options.fetchTimeoutMillis;
this.fetchBuilder = options.fetchBuilder;
}
fallbackEligible(method, path) {
let eligiblePath = false;
switch (method.toLowerCase()) {
case "post": {
eligiblePath = [
"/authorize",
"/list",
"/actions",
// distributed check api's
"/actions_query",
"/authorize_query",
"/list_query",
"/evaluate_query_local",
// query builder api's
"/evaluate_query",
].includes(path);
break;
}
case "get": {
eligiblePath = ["/facts", "/policy_metadata"].includes(path);
break;
}
}
return this.fallbackAgent && this.fallbackUrl && eligiblePath;
}
fallbackEligibleStatusCode(statusCode) {
// HTTP400 and HTTP5xx indicates errors experienced by the server in
// handling the request, so these should be retried against fallback
return statusCode == 400 || statusCode >= 500;
}
printDebugLogs(msg, metadata) {
return __awaiter(this, void 0, void 0, function* () {
const debug = this.debug;
if (debug === undefined) {
return;
}
if (debug.logger !== undefined) {
debug.logger(_1.LogLevel.debug, msg, metadata ? metadata : {});
}
else if (debug.print !== undefined) {
console.log("[oso] " + msg);
}
const file = debug.file;
if (file !== undefined) {
try {
yield fs_1.promises.appendFile(file, msg + "\n");
}
catch (error) {
if (debug.logger !== undefined) {
debug.logger(_1.LogLevel.error, "error writing to debug file: " + error, { error: error });
}
else {
console.error("error writing to debug file: ", error);
}
}
}
});
}
printRequestTimingInfo(result, startTime, path) {
return __awaiter(this, void 0, void 0, function* () {
const endTime = Date.now();
const totalMs = endTime - startTime;
const serverTiming = result.headers.get("Server-Timing");
const osoServerTiming = serverTiming === null || serverTiming === void 0 ? void 0 : serverTiming.split(",").filter((s) => s.startsWith("oso;"))[0];
const osoServerDur = osoServerTiming === null || osoServerTiming === void 0 ? void 0 : osoServerTiming.split(";").filter((s) => s.startsWith("dur="))[0];
const serverTime = osoServerDur === null || osoServerDur === void 0 ? void 0 : osoServerDur.split("dur=")[1];
const serverMs = serverTime ? parseInt(serverTime) : null;
if (serverMs !== null && Number.isInteger(serverMs)) {
yield this.printDebugLogs(`${path} ${result.status} total: ${totalMs}ms, server: ${serverMs}ms network: ${totalMs - serverMs}ms`, {
path: path,
status: result.status,
totalMs: totalMs,
serverMs: serverMs,
networkMs: totalMs - serverMs,
});
}
else {
yield this.printDebugLogs(`${path} ${result.status} total: ${totalMs}ms`, {
path: path,
status: result.status,
totalMs: totalMs,
});
}
});
}
_req(path, method, params, body, isMutation) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
let url = `${this.url}/api${path}`;
if (params)
url += `?${new url_1.URLSearchParams(params)}`;
let request = {
method,
headers: this._headers(),
agent: this.agent,
};
if (body !== null) {
request.body = JSON.stringify(body);
const bodySizeBytes = Buffer.byteLength(request.body);
if (bodySizeBytes >= maxBodySizeBytes) {
throw new Error(`Oso Cloud error: Request payload too large (bodySizeBytes: ${bodySizeBytes}, maxBodySize: ${maxBodySizeBytes})`);
}
}
const startTime = Date.now();
let result;
let fetch = cross_fetch_1.default;
if (this.fetchTimeoutMillis !== undefined) {
fetch = withTimeout(fetch, this.fetchTimeoutMillis);
}
if (this.fetchBuilder !== undefined) {
fetch = this.fetchBuilder(fetch);
}
else {
fetch = (0, fetch_retry_1.default)(fetch, retryOptions);
}
try {
result = yield fetch(url, request);
}
catch (e) {
if ((["ECONNREFUSED", "ETIMEDOUT"].includes(e.code) ||
e.name === "AbortError") &&
this.fallbackEligible(method, path)) {
let url = `${this.fallbackUrl}/api${path}`;
request.agent = this.fallbackAgent;
result = yield (0, cross_fetch_1.default)(url, request);
}
else {
return Promise.reject(e);
}
}
this.printRequestTimingInfo(result, startTime, path);
if (this.fallbackEligibleStatusCode(result.status) &&
this.fallbackEligible(method, path)) {
const fallbackStartTime = Date.now();
let url = `${this.fallbackUrl}/api${path}`;
request.agent = this.fallbackAgent;
result = yield (0, cross_fetch_1.default)(url, request);
this.printRequestTimingInfo(result, fallbackStartTime, path);
}
if (result.status < 200 || result.status >= 300) {
// Deserialize as text in case the response is not valid JSON.
const responseBody = yield result.text();
let message = "";
try {
const error = JSON.parse(responseBody);
message = (_a = error.message) !== null && _a !== void 0 ? _a : responseBody;
}
catch (_) {
message = responseBody;
}
const requestId = result.headers.get("X-Request-ID");
throw new Error(`Oso Cloud error: ${message} (Request ID: ${requestId})`);
}
if (isMutation) {
const offset = result.headers.get("OsoOffset");
this.lastOffset = offset;
}
// Deserialize as text in case the response is not valid JSON.
const text = yield result.text();
try {
const parsed = JSON.parse(text);
const requestId = result.headers.get("X-Request-ID");
return { result: parsed, requestId };
}
catch (_) {
const requestId = result.headers.get("X-Request-ID");
throw new Error(`Oso Cloud error: Unexpected response: ${text} (Request ID: ${requestId})`);
}
});
}
_get(path, params, _body) {
return __awaiter(this, void 0, void 0, function* () {
return yield this._req(path, "GET", params, null, false);
});
}
_post(path, params, body, isMutation) {
return __awaiter(this, void 0, void 0, function* () {
return yield this._req(path, "POST", params, body, isMutation);
});
}
_delete(path, params, body) {
return __awaiter(this, void 0, void 0, function* () {
return this._req(path, "DELETE", params, body, true);
});
}
_headers(request_id) {
const id = request_id || (0, crypto_1.randomUUID)();
return Object.assign(Object.assign({ "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, "User-Agent": this.userAgent, "X-OsoApiVersion": "0" }, (this.lastOffset ? { OsoOffset: this.lastOffset } : {})), { "X-Request-ID": id, Accept: "application/json", "X-Oso-Instance-Id": this.clientId });
}
getPolicy() {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = null;
const { result } = yield this._get(`/policy`, params, data);
return result;
});
}
getPolicyMetadata(version) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
if (version) {
params["version"] = version;
}
const data = null;
const { result } = yield this._get(`/policy_metadata`, params, data);
return result;
});
}
postPolicy(data) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/policy`, params, data, true);
return result;
});
}
postBatch(data) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/batch`, params, data, true);
return result;
});
}
postAuthorize(data, parityHandle) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { requestId, result } = yield this._post("/authorize", params, data, false);
if (parityHandle) {
if (requestId === null) {
throw new Error("Request ID is null");
}
parityHandle.set(requestId, this);
}
return result;
});
}
postList(data) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/list`, params, data, false);
return result;
});
}
postActions(data) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/actions`, params, data, false);
return result;
});
}
postQuery(data) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/evaluate_query`, params, data, false);
return result;
});
}
getStats() {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = null;
const { result } = yield this._get(`/stats`, params, data);
return result;
});
}
readDataBindings() {
return __awaiter(this, void 0, void 0, function* () {
if (!this.dataBindings)
throw new Error("Data bindings file missing or empty. See https://www.osohq.com/docs/guides/integrate/filter-lists#configuration for more details");
return this.dataBindings;
});
}
postActionsQuery(query) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = {
query,
data_bindings: yield this.readDataBindings(),
};
const { result } = yield this._post(`/actions_query`, params, data, false);
return result;
});
}
postAuthorizeQuery(query, parityHandle) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = {
query,
data_bindings: yield this.readDataBindings(),
};
const { requestId, result } = yield this._post("/authorize_query", params, data, false);
if (parityHandle) {
if (requestId === null) {
throw new Error("Request ID is null");
}
parityHandle.set(requestId, this);
}
return result;
});
}
postListQuery(query, column) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = {
query,
column,
data_bindings: yield this.readDataBindings(),
};
const { result } = yield this._post(`/list_query`, params, data, false);
return result;
});
}
postQueryLocal(query, mode) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = {
query,
data_bindings: yield this.readDataBindings(),
mode,
};
const { result } = yield this._post(`/evaluate_query_local`, params, data, false);
return result;
});
}
clearData() {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const data = null;
const { result } = yield this._post(`/clear_data`, params, data, true);
return result;
});
}
getFacts(predicate, args) {
return __awaiter(this, void 0, void 0, function* () {
let params = {};
args.forEach((value, i) => {
if (value.type) {
params[`args.${i}.type`] = value.type;
}
if (value.id) {
params[`args.${i}.id`] = value.id;
}
});
params["predicate"] = predicate;
const data = null;
const { result } = yield this._get(`/facts`, params, data);
return result;
});
}
postExpectedResult(expectedResult) {
return __awaiter(this, void 0, void 0, function* () {
const params = {};
const { result } = yield this._post(`/expect`, params, expectedResult, false);
return result;
});
}
}
exports.Api = Api;
//# sourceMappingURL=api.js.map