UNPKG

@cerbos/embedded

Version:

Client library for interacting with embedded Cerbos policy decision points generated by Cerbos Hub from server-side Node.js and browser-based applications

257 lines 8.85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AutoUpdatingLoader = exports.Loader = exports.LoadError = void 0; const core_1 = require("@cerbos/core"); const bundle_1 = require("./bundle"); const interval_1 = require("./interval"); const logger_1 = require("./logger"); const response_1 = require("./response"); const transport_1 = require("./transport"); const { version } = require("../package.json"); const defaultUserAgent = `cerbos-sdk-javascript-embedded/${version}`; async function resolve(result) { const { bundle, error } = await result; if (error) { throw error; } return bundle; } /** * Error thrown when a {@link Loader} fails to load an embedded policy decision point bundle. * * @public */ class LoadError extends Error { cause; constructor( /** * The error that caused loading the embedded policy decision point bundle to fail. */ cause) { const message = "Failed to load embedded policy decision point bundle"; super(cause instanceof Error ? `${message}: ${cause.message}` : message, { cause, }); this.cause = cause; (0, core_1._setErrorNameAndStack)(this); } } exports.LoadError = LoadError; /** * Loads an embedded policy decision point bundle from a given source. * * @public */ class Loader { /** @internal */ _active; /** @internal */ _options; /** @internal */ _userAgent; logger; /** * Load an embedded policy decision point (PDP) bundle from a given source. * * @param source - WebAssembly binary code of an embedded PDP bundle, or a URL or HTTP response from which to stream it. * @param options - additional settings. * * @remarks * Bundle download URLs are available in the "Embedded" section of the "Decision points" page of your Cerbos Hub workspace. * * The bundle will be loaded in the background when the loader is created. * If loading fails, then the first request from the client using the loader will throw an error. * To detect failure to load the bundle before making any requests, provide an {@link Options.onError} callback or await the {@link Loader.active} method. * * @example * Fetch an embedded PDP bundle via HTTP in a {@link https://caniuse.com/wasm | supported browser} or Node.js: * * ```typescript * const loader = new Loader("https://lite.cerbos.cloud/bundle?workspace=...&label=..."); * ``` * * @example * Read an embedded PDP bundle from disk in Node.js: * * ```typescript * import { readFile } from "fs/promises"; * * const loader = new Loader(readFile("policies.wasm")); * ``` * * @example * Load an embedded PDP bundle from a precompiled WebAssembly module (requires a bundler): * * ```typescript * import bundle from "bundle.wasm"; * * const loader = new Loader(bundle); * ``` */ constructor(source, options = {}) { this._options = options; this._userAgent = `${options.userAgent ? `${options.userAgent} ` : ""}${defaultUserAgent}`; if (options.onDecision) { this.logger = new logger_1.DecisionLogger(options.onDecision, this._userAgent); } this._active = this._load(source, true); } /** * Resolves to the metadata of the loaded bundle, or rejects with the error that was encountered when loading the bundle. */ async active() { return (await resolve(this._active)).metadata; } /** @internal */ _transport = new transport_1.Transport(async () => await resolve(this._active)); /** @internal */ async _load(source, initial = false) { try { const bundle = await bundle_1.Bundle.from(source, this.logger, this._userAgent, this._options); await this._onLoad(bundle, initial); return { bundle }; } catch (cause) { const error = new LoadError(cause); await this._onError(error); return { error }; } } /** @internal */ async _onLoad(bundle, _initial) { await this._options.onLoad?.(bundle.metadata); } /** @internal */ async _onError(error) { await this._options.onError?.(error); } } exports.Loader = Loader; const notModified = new Error("HTTP 304"); /** * Loads an embedded policy decision point bundle from a given URL, and polls for updates. * * @public */ class AutoUpdatingLoader extends Loader { url; activateOnLoad; interval; _pending; etag; running = true; abortController; timeout; /** * Load an embedded policy decision point bundle from a given URL. * * @param url - URL from which to stream bundles. * @param options - additional settings. * * @remarks * Bundle download URLs are available in the "Embedded" section of the "Decision points" page of your Cerbos Hub workspace. * * The bundle will be loaded in the background when the loader is created. * If initial loading fails, then the first request from the client using the loader will throw an error. * To detect failure to load the bundle before making any requests, provide an {@link Options.onError} callback or await the {@link Loader.active} method. * * Failure to load updates after the initial load will not cause requests from the client to throw errors, * but errors will be passed to the {@link Options.onError} callback. */ constructor(url, { activateOnLoad = true, interval, ...options } = {}) { super(url, options); this.url = url; this.activateOnLoad = activateOnLoad; this.interval = (0, interval_1.constrainAutoUpdateInterval)(interval); this.scheduleUpdate(); } /** * The metadata of a new embedded policy decision point bundle that has been downloaded but is not yet being used to evaluate policy decisions. * * @remarks * Only set if {@link AutoUpdateOptions.activateOnLoad} is `false` and an update has been downloaded. * * Use {@link AutoUpdatingLoader.activate} to start using the pending bundle to evaluate policy decisions. */ get pending() { return this._pending?.metadata; } /** * Promote the {@link AutoUpdatingLoader.pending | pending} embedded policy decision point bundle (if any) to active, so that it is used to evaluate policy decisions. * * @remarks * This method is a no-op if an update has not been downloaded, or if {@link AutoUpdateOptions.activateOnLoad} is `true` (the default). */ activate() { if (this._pending) { this._active = { bundle: this._pending }; this._pending = undefined; } } /** * Stops polling for new embedded policy decision point bundles, and aborts any in-flight updates. */ stop() { this.running = false; this.abortController?.abort(); if (this.timeout) { clearTimeout(this.timeout); } } /** @internal */ async _onLoad(bundle, initial) { this.etag = bundle.etag; if (!initial) { this._pending = bundle; if (this.activateOnLoad) { this.activate(); } } await super._onLoad(bundle, initial); } /** @internal */ async _onError(error) { if (!this.suppressError(error.cause)) { await super._onError(error); } } scheduleUpdate() { if (!this.running) { return; } if (this.timeout?.refresh) { this.timeout.refresh(); } else { this.timeout = setTimeout(() => { void this.update(); }, this.interval); } } async update() { this.abortController?.abort(); this.abortController = new AbortController(); await this._load(this.download(this.abortController.signal)); this.scheduleUpdate(); } async download(signal) { const request = { signal }; if (this.etag) { request.headers = { "If-None-Match": this.etag }; } const response = await (0, bundle_1.download)(this.url, this._userAgent, request); if (response.status === 304) { (0, response_1.cancelBody)(response); throw notModified; } return response; } suppressError(cause) { return cause === notModified || (isAbortError(cause) && !this.running); } } exports.AutoUpdatingLoader = AutoUpdatingLoader; function isAbortError(error) { return error instanceof DOMException && error.name === "AbortError"; } //# sourceMappingURL=loader.js.map