@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
262 lines • 9.08 kB
JavaScript
import { setErrorNameAndStack, userAgent } from "@cerbos/core/~internal";
import pkg from "../package.json" with { type: "json" };
import { Bundle, download } from "./bundle.js";
import { constrainAutoUpdateInterval } from "./interval.js";
import { DecisionLogger } from "./logger.js";
import { cancelBody } from "./response.js";
import { Transport } from "./transport.js";
const defaultUserAgent = `cerbos-sdk-javascript-embedded/${pkg.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.
*/
export class LoadError extends Error {
cause;
/** @internal */
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;
setErrorNameAndStack(this);
}
}
/**
* Loads an embedded policy decision point bundle from a given source.
*/
export class Loader {
/** @internal */
["~options"];
/** @internal */
["~userAgent"];
/** @internal */
["~transport"] = new Transport(async () => await resolve(this["~active"]));
/** @internal */
["~active"];
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"] = userAgent(options.userAgent, defaultUserAgent);
if (options.onDecision) {
this.logger = new DecisionLogger(options.onDecision, this["~userAgent"]);
}
this["~active"] = this["~load"](source, url(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 */
get ["~updateSignal"]() {
return undefined;
}
/** @internal */
async ["~load"](source, url, initial = false) {
try {
const bundle = await Bundle.from(source, url, 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);
}
}
const notModified = new Error("HTTP 304");
/**
* Loads an embedded policy decision point bundle from a given URL, and polls for updates.
*/
export class AutoUpdatingLoader extends Loader {
url;
activateOnLoad;
interval;
activations = 0;
_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 = 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.activations++;
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 */
get ["~updateSignal"]() {
return this.activations;
}
/** @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.url.toString());
this.scheduleUpdate();
}
async download(signal) {
const request = { signal };
if (this.etag) {
request.headers = { "If-None-Match": this.etag };
}
const response = await download(this.url, this["~userAgent"], request);
if (response.status === 304) {
cancelBody(response);
throw notModified;
}
return response;
}
suppressError(cause) {
return cause === notModified || (isAbortError(cause) && !this.running);
}
}
function isAbortError(error) {
return error instanceof DOMException && error.name === "AbortError";
}
function url(source) {
if (typeof source === "string" || source instanceof URL) {
return source.toString();
}
return undefined;
}
//# sourceMappingURL=loader.js.map