npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@o3co/auth.policy-verifier.cedar

v0.14.0

Published

Last updated: 2026-09-25

Readme

@o3co/auth.policy-verifier.cedar

Last updated: 2026-09-25

Co-resident Cedar policy evaluation for auth.policy-verifier, as an optional plugin package.

The whole Cedar policy set is evaluated by a real Cedar evaluator and enters the engine's AND-evaluation as one rule in one group. TypeScript rule groups keep working beside it: TS collectors gather the facts, Cedar policies write judgment over them — no entity store to build or sync.

Which evaluator is a deployment decision, made by dependency rather than by config: this package owns policy loading, the attribute-to-entity mapping and the rule, and hands the request to whichever CedarEngine is registered. @o3co/auth.policy-verifier.cedar-wasm is the in-process one (official @cedar-policy/cedar-wasm, ~15µs per decision, no network); importing it is all it takes. See Engines.

This decouples policy language from PDP topology. Native logic stays TypeScript — no DSL is ever required — and a deployment that adopts Cedar here is not binding itself to this verifier: the same .cedar files load unchanged into an embedded evaluator later, or into a cedar-agent when laid out one policy per file (see Running out of process). Design: #185.

Responsibility

Role. An optional plugin. A composition passes cedarPolicyModule to the server's createApp, which registers RequestFactsCollector and CedarPolicyRuleCollector for config to name. It depends on @o3co/auth.policy-verifier.core only; neither core nor the server depends on it, and engine packages such as cedar-wasm depend on it.

Owns.

  • Policy loading and the policy revision (loadPolicySource, computePolicyRevision).
  • The mapping from merged attributes to a Cedar request, with entities synthesized inline.
  • The rule: how an engine's answer becomes pass, fail or a logged deny (onNoDeterminingPolicy, evaluation errors, the revision check).
  • The CedarEngine port and the process-wide engine registry and selection.
  • The four attribute keys it reserves (see Reserved attribute keys).
  • The out-of-process http engine (a cedar-agent client).

Does not own.

  • An in-process evaluator. It has no Cedar dependency; cedar-wasm supplies one and pins its version.
  • An entity store. Entities are built per request, one hop deep.
  • How rule groups combine. That is core's AND-evaluation; the Cedar policy set is one group in it.
  • Parsing the request. The server does that, through the configured resource parser, before any collector runs; RequestFactsCollector only copies the result into attributes.

Why a separate package. Cedar is opt-in: nothing of it loads unless a deployment imports this package. Its vocabulary (the request* attribute keys, entity mapping) stays out of core, whose ATTR_* constants are reserved for OAuth/OIDC/RBAC concepts. The evaluator lives one package further out so that which evaluator runs is a dependency choice, not a config change.

Responsibility, role and invariants of the source directory: src/README.md.

Usage

import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
import { cedarPolicyModule } from "@o3co/auth.policy-verifier.cedar";
import "@o3co/auth.policy-verifier.cedar-wasm"; // registers the in-process engine
import { builtinKeyResolversModule, createApp } from "@o3co/auth.policy-verifier.server";

const app = await createApp({
	pathResolver: import.meta.resolve,
	config,
	modules: [builtinCollectorsModule, builtinKeyResolversModule, cedarPolicyModule],
});
attribute {
  collectors = [
    { collector = "PayloadSubjectIdCollector" }
    # Promotes action / resourceType / resourceId into attributes — the rule
    # pipeline never sees the request, so the Cedar request is built from these.
    { collector = "RequestFactsCollector" }
  ]
}
rule {
  collectors = [
    { collector = "CedarPolicyRuleCollector"
      # *.cedar files, sorted and concatenated — byte-identical to what a
      # Cedar agent would load. XOR an inline `policies = "..."` string.
      policyDir = "config/policies"

      # What the group answers when no policy determined the request.
      # "deny" is the default; see "No determining policy" below.
      onNoDeterminingPolicy = "deny"

      # Which registered engine evaluates the set. Optional: absent, the
      # preferred registered one — "wasm" whenever cedar-wasm is imported.
      # engine = "wasm"

      principal {
        # type = "User"          # default
        # idAttribute = "userId" # default (ATTR_USER_ID)
        attributes { dept = "department" }   # principal.dept == "eng"
        parents { Group = "groups" }         # principal in Group::"admins"
      }
      resource {
        attributes {
          # entity-reference form: resource.owner == principal
          owner = { attribute = "resourceOwner", entityType = "User" }
        }
      }
      context { mfa = "mfaVerified" }        # context.mfa
    }
  ]
}

Request-context fields reach Cedar the same way everything else does — as attributes. Promote them with the builtins' RequestContextAttributeCollector (the declared-allowlist trust boundary, #123) and map them here; nothing undeclared can reach a policy.

Reserved attribute keys

Loading this package reserves the four keys it owns — requestAction, requestResourceType, requestResourceId, requestResourceRaw — in core's attribute key registry (reserveAttributeKeys, exported by @o3co/auth.policy-verifier.core). A RequestContextAttributeCollector mapping whose to names one of them is then refused at boot, naming this package.

That matters most for requestResourceId, which RequestFactsCollector writes only when the parsed resource carried an id. For an id-less resource such as document nothing else writes it, so a mapping { from = "rid", to = "requestResourceId" } would have met no competing writer: {"resource":"document","action":"read","context":{"rid":"someone-elses-doc"}} would have been decided as document::"someone-elses-doc", with the entity chosen by the caller's own request body. Where the resource does carry an id the two writers collide instead and AttributeConflictError denies — fail-closed, but an unannounced denial rather than a refusal at boot.

The reservation happens at module scope, so it is in place before any collector of any package is constructed: a composition can only name RequestFactsCollector or CedarPolicyRuleCollector in config by importing this package. If you write a collector that promotes caller-supplied data, consult RESERVED_ATTRIBUTE_KEYS (live, not a snapshot) and reserve your own keys the same way — see docs/extending.md.

Semantics

  • Layered PDP. Cedar's own semantics (forbid overrides permit) hold inside the group; the group ANDs with every TypeScript group. During migration both are active and compose only toward strictness.

  • No determining policy denies by default. When no permit and no forbid matched, onNoDeterminingPolicy decides what the group answers:

    | value | the group | choose it when | | --- | --- | --- | | "deny" (default) | fails | Cedar is authoritative over the surface it is asked about — including the common case where it is the only rule group | | "abstain" | passes | Cedar is one group beside TypeScript rules that own the rest of the surface, and having no opinion outside its own coverage is the intent |

    "deny" is Cedar's own implicit deny, and it is the default because the default is what a first deployment gets: with Cedar as the only rule group, an abstention passes the group and therefore passes the request — the one composition where abstaining is indistinguishable from allowing, and also the simplest one to assemble. The surrounding engine composes rule groups with default-deny; this matches it.

    "abstain" is the migration posture and stays a first-class choice: while the policy set covers part of the surface and the TypeScript rules hold the rest, a request outside Cedar's coverage should be decided by the group that does cover it. Selecting it is a statement that another group will decide — so a pipeline whose only rule group is Cedar should not.

    "abstain" is refused at boot with an out-of-process engine (engine = "http"): an agent that restarted comes back with no policies and answers every request "no determining policy" — exactly what a covered request that matched nothing answers — so under "abstain" every forbid would silently stop applying.

    Neither value affects an evaluation error, which always denies (below).

  • Evaluation errors always deny, and log. Cedar reports a policy that reads a missing attribute as deny with the cause only in diagnostics — and an erroring forbid stops forbidding, so the top-level decision can read allow exactly when it is least trustworthy. The rule checks diagnostics.errors first: any error is a deny regardless of onNoDeterminingPolicy, and is logged unless logEvaluationErrors = false.

  • Entity synthesis is one hop. Principal and resource entities are synthesized per request from the attribute map — attributes, group membership, entity references. Multi-hop dereference and hierarchy walks need an entity store; needing them is the signal to move to a full Cedar deployment, which the same .cedar files already fit.

Policy revision: which policies decided

Every answer of the rule reports the evaluation behind it (#244). The report goes to the reporter core hands verify / decide for that one call, and core carries it onto the decision: into the decision log event always, and into the response under verify.evaluationInResponse = "include". The rule itself still answers a boolean, so an evaluator that predates the reporter reads a deny as a deny.

A denial is cedar_deny whether or not a policy produced it; this is what tells them apart:

| the rule answered because | evaluation.status | the revision | | --- | --- | --- | | Cedar answered without errors — a permit, a forbid, or no policy determining the request | completed | revision, when the engine vouches for it | | Cedar answered with evaluation errors | failed | revision, when the engine vouches for it | | the call itself failed | failed | revision: null — nothing answered, so nothing vouched | | the engine named a revision other than the one loaded | failed | revision: null — it answered from a policy set this verifier did not load | | the engine named no revision, under requireConfirmedRevision | failed | revision: null | | the request could not be built from the attributes, so Cedar was not asked | not_invoked | no revision key at all |

What the revision is. sha256: and the lowercase hex SHA-256 of

auth.policy-verifier.cedar/policy-set/v1\n
<bytes>:<name>,<bytes>:<text>,          ← once per *.cedar file, in load order

where <bytes> is the decimal UTF-8 byte length of what follows it, <name> is the file's bare name (policies for the inline set) and <text> its contents. loadPolicySource({ policyDir }).revision gives it for a directory — the same filter, sort and decoding the collector uses — and computePolicyRevision(files) for a list already in hand, so CI can compute the revision of what it is about to ship and compare it with what production reports. It is computed once, at boot, from the very files handed to the engine.

  • Same contents, same revision — on any replica, at any mount path. The directory is deliberately not part of it, and no path ever appears in a decision.
  • A changed policy changes it even when its policy id does not. Policy ids are positional (policy0) under wasm and file names under http; neither moves when a policy's text is edited. The revision does.
  • A rename changes it, because under the http engine the file name is the policy id the agent is given.
  • The framing is there because concatenation is not injective: "X\n" + "Y" and "X" + "\nY" are one policy text and two policy sets.

It is the text as loaded that is hashed, so what changes the text changes the revision, policy for policy identical or not. Three things do that across machines: line endings (a checkout with core.autocrlf turns \n into \r\n), a byte-order mark (kept as U+FEFF by the UTF-8 decoder), and the Unicode normalization of a non-ASCII file name (NFC on one filesystem, NFD on another). Pin line endings for *.cedar in .gitattributes, and keep file names ASCII, if replicas built on different machines must agree.

What it does not cover. The collector's mapping, onNoDeterminingPolicy, the engine and its version, and the attributes the request was decided over all shape an answer too. The revision says which policies were evaluated. It is not a promise that evaluating them again gives the same answer; to explain a decision later, keep the deployed version and its config beside it.

revision versus loadedRevision. revision is a claim about what was evaluated, so it is set only when the engine vouches for that answer. The wasm engine does: the set is compiled in-process from the files that were hashed, under an id nothing else holds. The http engine cannot: cedar-agent answers { decision, diagnostics } and does not say which policies it holds, and a restarted agent comes back empty. There the rule reports revision: null and the digest of what it pushed at boot as loadedRevision — worth recording, and not proof of what ran. An engine that names a revision other than the loaded one is answering from a policy set this verifier did not load, and the rule fails it closed — and logs it whatever logEvaluationErrors says, because that is a fault of the deployment and not a policy reading a missing attribute.

requireConfirmedRevision = true is for a deployment whose audit has to name the policies behind every decision: an answer nobody vouched for becomes a deny — always logged, like the mismatch above — instead of a permit of unknown origin. It is refused at boot over an engine that does not declare confirmsRevision — today, engine = "http" — because there every answer would be that deny.

Engines

This package has no evaluator of its own. CedarPolicyRuleCollector loads the policy set, builds the Cedar request — principal, action, resource, context and the synthesized entities, inline — from the merged attributes, and hands both to a CedarEngine. The port and its types are defined, with their documentation, in src/engine.mts; the contract in short:

  • An engine has a name (the registry key and the config value that selects it), declares up front whether its policy sets answer asynchronously (async), and may declare that every answer names the revision it evaluated (confirmsRevision, #244). Both declarations come before load, so the collector can refuse an unsafe configuration before loading has side effects.
  • load is called at boot with the PolicySource and a load context carrying the collector's whole config entry (an engine reads and validates its own keys there, such as the http engine's endpoint) and a logger. It returns the loaded set, possibly as a promise, or throws CedarEngineError to refuse the set.
  • A loaded set answers either synchronously (in-process), or asynchronously with an AbortSignal for the rule's deadline (over I/O). Its async must match the engine's declaration.
  • An answer is a CedarDecision: the decision, the determining policies and the evaluation errors as text, and optionally the revision it was evaluated against. A call that failed outright rejects with CedarEngineError.

CedarDecision.revision is the port's confirmation contract (see Policy revision): an engine names source.revision on an answer only if that answer provably came from the set compiled from that source. It is per answer, not per load, because that is the only moment the claim is true of a remote engine.

load may be asynchronous — a remote engine takes the policy set over the network — and a set that cannot be loaded still refuses to start: the collector's factory (CedarPolicyRuleCollector.create, what the module registers) awaits it, and createApp awaits the factory.

The kind of policy set the engine returns decides the kind of rule the collector builds — a Rule asked through verify, or an AsyncRule asked through decide under the server's verify.ruleTimeoutMs — and nothing else changes: config, mapping, the answer table above and the cedar_deny the decision reports are identical across engines. Switching engines is a dependency change, not a config change (#225).

An engine package registers itself when imported (registerCedarEngine, at module scope), and the collector picks one by its config engine key (resolveCedarEngine in src/engine.mts):

| config engine | result | | --- | --- | | absent | the first registered of wasm, http — so wasm whenever @o3co/auth.policy-verifier.cedar-wasm is imported | | a registered name | that engine; an explicit choice wins over the preference | | "wasm", package not imported | refuses to start, naming @o3co/auth.policy-verifier.cedar-wasm | | anything else | refuses to start, listing what is registered |

Name the engine. Left absent, which process decides authorization is settled by what the dependency graph happens to import — a transitive dependency that pulls in the wasm package flips a deployment from out-of-process to in-process — and nothing in the config shows it. The collector therefore logs a warning at boot when engine is absent (cedar engine selected by default — set engine …), and states the engine at info when it is named. The registry is process-wide, not per copy of this package, so two copies on the graph still see one set of engines.

The engines that ship today:

| engine | package | runs | when | | --- | --- | --- | --- | | wasm | @o3co/auth.policy-verifier.cedar-wasm | in-process, synchronous, tens of µs per decision on a small set; ~12 MB of wasm instantiated at import | the policy set is cheaper to evaluate than a loopback hop — most of them; see Sizing | | http | this package (cedarHttpEngine, registered by importing it) | out of process: a cedar-agent over HTTP, asynchronous, one loopback hop per decision | the policy set is large enough that evaluating it in-process competes with request handling, or the evaluator should scale and upgrade apart from the verifier |

Both engines see the same request — principal, action, resource, context and the synthesized entities, inline — and answer through the same table. What differs is where the evaluator runs and what the deployment carries: the wasm package, or a second process.

Running out of process

engine = "http" (or simply not importing the wasm package) sends every decision to a cedar-agent. The standalone template ships it as a compose profile that shares the verifier's network namespace, so the agent listens on loopback and nothing outside the container pair can reach it — the same trust boundary the verifier's own bind address draws:

echo "CEDAR_AUTHENTICATION=$(openssl rand -hex 32)" >> .env   # the agent's token, required
docker compose --profile cedar up --build
  • Where the agent is. endpoint in the collector's config entry, else the CEDAR_ENDPOINT environment variable. Neither refuses to start with no cedar engine endpoint is configured, naming both ways out — this engine is also what a deployment gets when it has not imported the wasm package, and that is the mistake worth naming. The template's compose file sets CEDAR_ENDPOINT=http://127.0.0.1:8180, the profile's address. A base URL: the agent's /v1/policies and /v1/is_authorized are appended. Redirects are not followed, so this must be the URL that answers those calls itself: behind an ingress that answers with a 3xx, every decision is a deny and a 3xx to the policy load fails boot. Plain http:// is accepted for loopback hosts only; a routable agent must be https:// (the rule jwksUri follows, and for the same reason: the request carries the subject's attributes and the answer is an authorization). authentication in config, else CEDAR_AUTHENTICATION, is sent verbatim as the Authorization header when the agent was started with one. A token that cannot be sent as a header — an ASCII control character other than a tab inside it, a character above U+00FF — fails boot, naming where it came from but not its value.
  • Authenticate the agent. An agent without --authentication is a write oracle over the policy set: anything that reaches its port can PUT /v1/policies a permit(principal, action, resource); and every later decision allows. Loopback in a shared network namespace narrows who can reach it; it does not make the token optional, and outside a private namespace it MUST be set. The template's compose profile starts the agent with CEDAR_AGENT_AUTHENTICATION from the same CEDAR_AUTHENTICATION the app sends. A load the agent refuses as unauthenticated fails boot naming CEDAR_AUTHENTICATION.
  • The verifier owns the policies. At boot the engine PUTs the policy set to the agent, one entry per .cedar file with the file's name as the policy id, so the agent holds exactly config/policies and nothing is converted or mounted twice. Boot retries an unreachable agent for 10 s (a compose sibling may be a few hundred milliseconds behind) and then refuses to start. The error names the cause — connect ECONNREFUSED …, getaddrinfo ENOTFOUND …, a TLS error — or, when the deadline passes while an attempt is still waiting, says no answer came in time, with how the attempt before failed if one did. A set the agent refuses fails boot at once, with the agent's message.
  • One policy per file. cedar-agent stores policies one by one, so each .cedar file — and an inline policies string — must hold exactly one policy; a file with two is refused at boot. The wasm engine concatenates and does not care, so a corpus laid out one policy per file runs under both, and reads better under this one: diagnostics.reason then names files (10-permit-eng) rather than policy0.
  • One collector per agent. PUT /v1/policies replaces the agent's whole set, so a second CedarPolicyRuleCollector pointed at the same agent is refused at boot rather than silently overwriting the first. Every loopback spelling of a host (localhost, 127.0.0.1, [::1]) on one port counts as the same agent.
  • Connections are not capped. The engine uses the process's global fetch dispatcher with keep-alive, so concurrent decisions map one-to-one onto concurrent agent connections. Each call is bounded by verify.ruleTimeoutMs and the decision by verify.evaluateDeadlineMs, and a batch by verify.batchConcurrency; a deployment expecting floods on the verifier should size the agent for that concurrency, or put a connection-limiting proxy in front of it.
  • An answer is bounded. At most maxAnswerBytes of one answer is read, and held until it is parsed, for each concurrent call. Unset, it is 1 MiB — the default, exported as CEDAR_ANSWER_MAX_BYTES; there is no environment variable for it. A longer answer is refused rather than held for the rule deadline, since a process out of memory takes every route down. An answer's determining-policy and error lists grow with the policy set, so a large set can answer honestly past 1 MiB: the refusal says so, and the fix is a higher maxAnswerBytes in the collector's entry — a whole number of bytes from 1 KiB to 256 MiB, written as a number (maxAnswerBytes = 4194304) or a numeric string, which is what a HOCON env substitution of a variable of your own naming delivers (maxAnswerBytes = ${?MY_ANSWER_BYTES}). Anything else — 4 MiB unquoted is a string — refuses to start. An error's body is read up to the smaller of maxAnswerBytes and 1 MiB, since it becomes the log line.
  • Failure after boot is a deny. An agent that is unreachable, answers non-2xx, breaks off its answer, answers more than maxAnswerBytes, or answers something that is not a decision makes the rule fail and log (cedar authorization call failed); the log line's reason names the cause, as the boot error does. An agent that is up but has lost the policy set — restarted, or recreated by docker compose up — is not a failure it can see: it answers "deny, no determining policy" to every request, which is why onNoDeterminingPolicy = "abstain" is refused with this engine; under the default it denies everything until the verifier is restarted and pushes the set again. Each call runs under the server's verify.ruleTimeoutMs (default 2000 ms), answering rule_timeout when the agent is slower than that.
  • Two Cedar versions. cedar-agent 0.2.2 evaluates with cedar-policy 2.4; the wasm package with the Cedar 4 release it pins (see Version pinning). Policies written to the older grammar run under both; a policy using a newer construct will be refused by the agent at boot, which is the right place to find out.
  • The response contract is cedar-agent 0.2.x's. POST /v1/is_authorized must answer { decision, diagnostics: { reason: [...], errors: [...] } }; an answer missing either list is refused, and the call denies. The items of both lists are read as text — an agent on a newer Cedar that reports structured errors still has its errors seen (and denied on), rather than every answer refused as malformed.
  • Entities travel inline, and the agent reads them. Each call carries the request's entities in entities; nothing is written to the agent's own /v1/data store. #225 left open whether cedar-agent honours inline entities rather than its store; the v0.10.0 release audit confirmed it against the pinned permitio/cedar-agent:0.2.2: a policy reading an inline attribute decides on it as the wasm engine does, and one reading an attribute the entity lacks answers Deny with the error in diagnostics.errors, which the rule denies on.

Sizing: which engine

Measured, not guessed: one request of the shape the collector sends (two entities inline, one context field), against policy sets in which every policy has to be considered, 2000 decisions each. Apple M3, Node 26, @cedar-policy/cedar-wasm 4.12.0 in-process; permitio/cedar-agent:0.2.2 in Docker Desktop on the same machine, reached through a published loopback port. Microseconds per decision:

| policies | wasm p50 | wasm p99 | agent p50 | agent p99 | | ---: | ---: | ---: | ---: | ---: | | 1 | 34 | 90 | 346 | 1281 | | 10 | 45 | 78 | 328 | 1897 | | 100 | 134 | 238 | 395 | 791 | | 1000 | 1111 | 1409 | 969 | 1595 |

How to read it:

  • The wasm figure is time the verifier's event loop is blocked; the agent figure is mostly waiting, and costs the loop only the fetch overhead (on the order of 100 µs of it).
  • On this setup the hop costs roughly 300 µs, so in-process wins outright up to a few hundred policies, and the two cross near a thousand policies of this shape — where the agent's native evaluator is already faster than the wasm one and the verifier stops paying for evaluation on its own loop.
  • Docker Desktop on macOS routes the published port through a virtual machine; a Linux host with the compose profile's shared network namespace pays less for the hop, which moves the crossover down. Policies with heavier conditions or larger entity sets move it down too. Measure your own corpus before deciding: the script is three fetch calls and one statefulIsAuthorized loop.
  • Below the crossover, choose the agent anyway when the evaluator should scale or upgrade apart from the verifier, or when the 12 MB wasm should not be in the image. Above it, choose wasm anyway when a second process is not worth operating. The switch is a dependency change — config, mapping and policies stay put.

Version pinning

The Cedar evaluator's version is the engine package's concern: @o3co/auth.policy-verifier.cedar-wasm pins @cedar-policy/cedar-wasm exactly (the version is in its package.json), because Cedar minor releases can carry policy-language changes and upgrades should be deliberate. This package depends on no evaluator.