UNPKG

oso-cloud

Version:

Oso Cloud Node.js Client SDK

504 lines 20.6 kB
"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