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.builtins

v0.14.0

Published

Last updated: 2026-09-24

Readme

@o3co/auth.policy-verifier.builtins

Last updated: 2026-09-24

Built-in attribute collectors, rule collectors, and resource parser for auth.policy-verifier.

Runtime: Server- and edge-side JavaScript runtimes that support BigInt and Map.groupBy — Node.js 22+ (declared via engines.node so older Node installs are blocked at install time), Cloudflare Workers, Vercel Edge, Deno, Bun. Browsers are out of scope by design: authorization decisions must be enforced server-side. The server companion package remains Node-only.

Responsibility

A small, ready-made set of implementations of core's interfaces: attribute collectors, rules, rule collectors, a resource parser, and builtinCollectorsModule, which registers them for a host such as the server's createApp. It depends on @o3co/auth.policy-verifier.core alone; templates/standalone wires it in, and server uses it only in its tests (devDependencies).

  • Owns what these implementations read, write and match: the mapping from JWT claims (sub, azp, scope, declared claims) to attribute keys (#170), the matching rules of HasScope / HasPermission and the comparison rules, and the dot-notation resource grammar.
  • Does not own the pipelines, the grouping or the decision (core), authentication or HTTP (server), policy engines or AsyncRules (cedar), or collectors that do I/O — those are the consumer's to write (docs/extending.md).
  • Why a separate package from core: core is the engine-neutral contract and names no field of the subject (#170); the claim vocabulary and the concrete matching live here, on the other side of that line (AGENTS.md — Core Vocabulary Scope). It is also optional: builtins is a deliberately basic set, not a catalog (docs/extending.md — Positioning), so a deployment that writes its own collectors and rules does not have to install it, and core does not grow with it.

Responsibility, role and invariants of each source directory: src/collectors/README.md (attribute collectors) and src/rules/README.md (rules and rule collectors).

Install

npm install @o3co/auth.policy-verifier.builtins

Attribute Collectors

All collectors implement AttributeCollector.

| Name | Reads from | Emits | Constructor args | | --- | --- | --- | --- | | PayloadScopeCollector | the scope claim — subject.scope by default, a space-separated string or an array of strings | ATTR_SCOPES: string[] | { claim?: string } (#219: "scp" for Okta, "permissions" for Auth0) | | PayloadSubjectIdCollector | subject.sub, subject.azp | ATTR_USER_ID, ATTR_CLIENT_ID | none | | StaticPermissionCollector | — | ATTR_PERMISSIONS: string[] | { permissions: string[] } | | StaticRoleCollector | — | ATTR_ROLES: Role[] | { roles: Role[] } | | RequestContextAttributeCollector | declared fields of requestContext | the operator's own keys | { attributes: Mapping[] } | | PayloadClaimAttributeCollector | declared claims of the verified subject | the operator's own keys, or core's five | { attributes: Mapping[] } (#219) |

StaticPermissionCollector and StaticRoleCollector always emit the values supplied at construction time, regardless of request context. They copy what they were given at construction — the array, and for StaticRoleCollector each Role and its permissions — so mutating the config afterwards changes nothing they emit (#255). permissions and roles must be arrays: anything else, a string included (permissions = "posts.*" where ["posts.*"] was meant), is refused with a TypeError at construction, so the deployment fails at boot (#264).

PayloadClaimAttributeCollector

Promotes declared claims of the verified subject into attributes (#219) — the operator-declared way to turn an external IdP's claims into what the rules read, without a bespoke collector:

{ collector = "PayloadClaimAttributeCollector"
  attributes = [
    { from = "o.rol", to = "roles", type = "string[]" }                      # Clerk: org role, by dot path
    { from = "https://example.com/roles", to = "roles", type = "string[]" }  # Auth0: a namespaced claim is one key, not a path
    { from = "tid", to = "tenantId" }
  ] }

Same mapping shape as RequestContextAttributeCollector ({ from, to?, type? }, an exact key winning over a dot path, own properties only). What differs is the source, and therefore the trust: the subject bag is what the authenticator verified, so a mapping may land on core's five keys — scopes, permissions, roles, userId, clientId — where the request-context collector refuses them. Two collectors writing one list key union it; that is the deployment composing two issuer-derived sources, and it says so in config. A scalar key written by two collectors with different values throws AttributeConflictError and denies every request — do not map onto userId / clientId while PayloadSubjectIdCollector also writes them. Keys another package reserved (cedar's request*) stay refused, because they are derived from the request, not from the subject. And map only claims the IdP populates from its own registration or admin data: a claim minted from user-editable metadata (Clerk's unsafe_metadata, Auth0's user_metadata) is signed, not trusted.

For the scope claim specifically, prefer PayloadScopeCollector { claim = "scp" }, which also reads the space-delimited string form and pairs with ResourceActionScopeRuleCollector { claim = "scp" } — set on both, since each keeps its own claim and nothing checks they match — so both look at the same claim. The rule collector checks only that the claim is present: a claim holding no usable scope list ("", a number) yields no scopes but still counts as scoped under scopeless = "skip".

RequestContextAttributeCollector

Promotes declared fields of CollectorContext.requestContext into attributes:

{ collector = "RequestContextAttributeCollector"
  attributes = [
    { from = "tenant.id", to = "tenantId" }     # dot path; `to` defaults to `from`
    { from = "groups", type = "string[]" }
  ] }

Each mapping is { from: string; to?: string; type?: "string" | "number" | "boolean" | "string[]" }, type defaulting to "string". An exact key on context wins over walking a dot path: a literal "tenant.id" field is read before tenant → id. A malformed mapping throws at construction; an unusable value does not — requestContext is caller-supplied request data, so a field that is missing, empty, or not of its declared type is simply not promoted.

The declaration is the trust boundary — this collector is the ready-made way to stay on the right side of the one described in docs/extending.md. requestContext is free-form and unvalidated, so nothing undeclared becomes an attribute and a configured dot path traverses own properties only (constructor.name reads nothing). This collector invents no vocabulary of its own: the operator names both the fields and the keys, which is what keeps AGENTS.md — Core Vocabulary Scope intact while still shipping something usable. For anything beyond read-check-write — deriving a value, calling out to a store — write a focused project-side AttributeCollector as that section describes.

Reserved vocabulary is not a valid destination

A mapping's to may not name a reserved attribute key. Naming one is a configuration error, refused at construction, so a deployment that writes it fails at boot rather than on the first request:

# refused at boot — core's own vocabulary
{ from = "groups", to = "scopes" }

# refused at boot — @o3co/auth.policy-verifier.cedar's vocabulary,
# reserved as soon as that package is loaded
{ from = "rid", to = "requestResourceId" }

# fine — the field may be called anything; only the attribute key is reserved
{ from = "groups", to = "requestGroups", type = "string[]" }
{ from = "scopes", to = "requestedScopes", type = "string[]" }

The reserved set is not a list this package keeps. It is core's registry (RESERVED_ATTRIBUTE_KEYS, reserveAttributeKeys, attributeKeyReservation): core reserves its own five — scopes, permissions, roles, userId, clientId — and every package that owns attribute vocabulary reserves its own at module load. @o3co/auth.policy-verifier.cedar reserves requestAction, requestResourceType, requestResourceId and requestResourceRaw; a project-side collector should reserve the keys it writes the same way. A composition that can name a package's collectors in config has already imported that package, so its keys are registered before this collector is constructed — which is why the guard covers vocabulary core cannot see. The refusal names the owning package, and suggests a rename that is not itself reserved.

context is the caller's, and those keys are the deployment's. Under the default server scopes, userId and clientId are read out of the signature-verified token, permissions / roles carry the entitlements the builtin rules decide from, and cedar's four carry the parsed request — so the two sides of such a mapping carry entirely different trust, and the request body must not join them in one bucket.

What makes it worth refusing rather than documenting is the merge: AttributePipeline unions array-valued attributes across collectors. A mapping onto scopes therefore does not overwrite what PayloadScopeCollector produced and lose an argument with it — it extends it. A caller sending context.groups = ["admin:write"] would be authorized for a scope its token never carried, and nothing in the decision, the logs or the metrics would tell that apart from an issuer that granted it. See AttributePipeline's merge doc comment.

A scalar key is no safer, in two ways. Where both sides write it the values disagree and AttributeConflictError denies the request — fail-closed, but an unannounced denial rather than a refusal at boot. And where the owning collector writes its key only sometimes there is no second writer at all: cedar's RequestFactsCollector omits requestResourceId for an id-less resource such as "document", so { from = "rid", to = "requestResourceId" } would land unopposed and the Cedar resource entity would be built from the caller's own request body.

Rules

HasPermission

new HasPermission(permission: string)
  • ruleType: "permission", code: "no_permission"
  • Checks ATTR_PERMISSIONS (direct) and ATTR_ROLES[].permissions (via roles).
  • Matching is exact and case-sensitive — the same discipline HasScope applies to scopes and DotNotationResourceParser applies to resources: compare what was written, never a normalized guess at what was meant. The parser preserves case, so Project:1.perm:read and project:1.perm:read are different permissions, exactly as Project:1 and project:1 are different resources to a scope rule.
  • Wildcards in a granted permission are honoured — written match structure, not normalization; the literal halves around the * still compare exactly:
    • "*" matches any permission.
    • "foo*" matches any permission with prefix foo.
    • "*bar" matches any permission with suffix bar.
    • "foo*bar" matches any permission starting with foo and ending with bar.
    • More than one * in a granted permission never matches (rejecting beats silently over-granting).

HasScope

new HasScope(scope: string, options?: { allowBareScopeRewrite?: boolean })
  • ruleType: "scope", code: "invalid_scope"
  • Checks ATTR_SCOPES.
  • Matching is exact and case-sensitive. OAuth 2.0 scope values are case-sensitive opaque strings (RFC 6749 §3.3), so read:PROJECT does not satisfy read:project.
  • A scope containing more than one : is a value in its own right — nothing is split off at the second :. read:project:restricted does not satisfy read:project (a deliberately narrowed grant must not collapse into the broader one), and read:project does not satisfy read:project:restricted.
  • allowBareScopeRewrite (default false) opts in to treating a bare granted scope "resource" as "read:resource" as well as literally. Only a scope with no : is ever rewritten; "project:restricted" is left alone, because which segment is the action is unknowable and guessing over-grants. Leave it off unless your issuer emits bare resource names.
  • Non-string entries in ATTR_SCOPES never match and never throw.

AttrMatchRule

Deprecated. Use AttrPairEqual instead. AttrMatchRule is kept as a thin wrapper class that extends AttrPairEqual and preserves the legacy ruleType (attr_match:${a}:${b}) and legacy message wording for backward compatibility. The type AttrMatchRuleConfig is a type alias of AttrPairEqualConfig. Deprecated since v0.3; it may be removed in a future release (subject to change).

new AttrMatchRule({ a: string, b: string, group?: string })
  • code: "attr_mismatch".
  • Passes when attrs.get(a) and attrs.get(b) are both non-empty strings and equal. Any other case returns false (fail closed).
  • Pure predicate — does not read CollectorContext. Consuming projects provide the two values to compare through upstream AttributeCollectors and wire the rule through their own RuleCollector.
  • ruleType defaults to "attr_match:${a}:${b}". The evaluator ORs rules within a ruleType and ANDs across different ruleTypes, so the default ensures two independent comparisons are AND-combined (required together). Pass group explicitly when you want two comparisons to be OR-combined (for example, "identify by DID or by email") — both rules then share the provided group as their ruleType.

Attribute Comparison Rules

The attribute comparison rules form a 2 × 5 matrix over two axes: family (Literal vs. Pair) and operator (Equal, NotEqual, In, NotIn, Compare).

  • Literal rules compare a single named attribute against a static value (or set of values) supplied at construction time.
  • Pair rules compare two named attributes resolved from the Attributes map at evaluation time.
  • In / NotIn variants exist for the Literal family only. A pair-over-set operation does not generalize cleanly to a finite list, so AttrPairIn / AttrPairNotIn are intentionally absent.

| Family | Equal | NotEqual | In | NotIn | Compare | | ------- | ------------------ | --------------------- | --------------- | ------------------ | -------------------- | | Literal | AttrLiteralEqual | AttrLiteralNotEqual | AttrLiteralIn | AttrLiteralNotIn | AttrLiteralCompare | | Pair | AttrPairEqual | AttrPairNotEqual | — | — | AttrPairCompare |

AttrLiteralEqual

new AttrLiteralEqual({ a: string, v: string | number | boolean, group?: string })
  • code: "attr_not_equal".
  • Default ruleType: `attr_literal_equal:${a}:${typeof v}:${String(v)}`. The typeof v segment prevents silent collisions between distinct-type literals that stringify the same way (e.g. true vs "true").
  • Passes when attrs.get(a) is the same type and strictly equal to v. No type coercion.

AttrLiteralNotEqual

new AttrLiteralNotEqual({ a: string, v: string | number | boolean, group?: string })
  • code: "attr_equal".
  • Default ruleType: `attr_literal_not_equal:${a}:${typeof v}:${String(v)}`. The typeof v segment prevents silent collisions between distinct-type literals (same rationale as AttrLiteralEqual).
  • Passes when attrs.get(a) is the same type as v and strictly not equal to it. Missing, wrong-type or NaN attributes return false (safe-deny): NaN is unequal to every number, and a restriction must not pass on a value that is not a number at all (#254).

AttrLiteralIn

new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: string })
  • code: "attr_not_in_set".
  • Default ruleType: `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` — where count is the post-deduplication element count and hashPrefix is a 16-hex-character FNV-1a 64-bit hash over the deduplicated, sorted, stringified values. The hash is non-cryptographic but the 64-bit width makes accidental and adversarial collisions vanishingly unlikely for any realistic policy size; the package has no node:* dependency and loads in any supported server/edge runtime (see "Runtime" above). Two instances with the same a and logically equivalent values (duplicates and order do not matter) share the same ruleType and are OR-combined by the evaluator.
  • values must be a non-empty, homogeneous array (string[], number[], or boolean[]). Passes when attrs.get(a) is in the set. Duplicate elements in values are ignored (the rule uses Set semantics internally).

AttrLiteralNotIn

new AttrLiteralNotIn({ a: string, values: (string | number | boolean)[], group?: string })
  • code: "attr_in_set".
  • Default ruleType: `attr_literal_not_in:${a}:${type}:${count}:${hashPrefix}` — same stable, deduplication-aware hash scheme as AttrLiteralIn.
  • values must be a non-empty, homogeneous array. Passes when attrs.get(a) is the same type as the values and NOT in the set. Missing, wrong-type or NaN attributes return false (safe-deny); NaN is in no set, so it would otherwise pass (#254). Duplicate elements in values are ignored.

AttrLiteralCompare

new AttrLiteralCompare({ a: string, op: "lt" | "le" | "gt" | "ge", v: number, group?: string })
  • code: "attr_compare_violated".
  • Default ruleType: `attr_literal_compare:${a}:${op}:${String(v)}`.
  • Passes when attrs.get(a) is a number satisfying a op v. NaN as v is rejected at construction time. NaN attributes always return false.

AttrPairEqual

new AttrPairEqual({ a: string, b: string, group?: string })
  • code: "attr_mismatch".
  • Default ruleType: `attr_pair_equal:${a}:${b}`.
  • Passes when both attrs.get(a) and attrs.get(b) are non-empty strings and strictly equal. This is the successor to the deprecated AttrMatchRule.

AttrPairNotEqual

new AttrPairNotEqual({ a: string, b: string, group?: string })
  • code: "attr_match".
  • Default ruleType: `attr_pair_not_equal:${a}:${b}`.
  • Passes when both attrs.get(a) and attrs.get(b) are non-empty strings and strictly not equal. Missing, empty, or non-string attributes return false (safe-deny).

AttrPairCompare

new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group?: string })
  • code: "attr_compare_violated".
  • Default ruleType: `attr_pair_compare:${a}:${op}:${b}`.
  • Passes when both attrs.get(a) and attrs.get(b) are numbers satisfying a op b. NaN on either side returns false (JS comparison semantics).

Grouping: AND by default, group for OR

All attribute comparison rules follow the same grouping semantics described for AttrMatchRule above. By default, each rule's ruleType is derived from its distinguishing parameters so that distinct requirements are AND-combined by the evaluator. Pass the same group string to two rules to give them the same ruleType — the evaluator then OR-combines them (either condition satisfies the requirement).

Configuration is copied at construction

Every attribute comparison rule reads each field of its config once, validates it, and keeps its own copy (#255). Mutating the config object afterwards — replacing a, b, op or v, or editing values — changes neither the rule's answers nor its ruleType and message, and a value the constructor would refuse (such as a NaN literal) cannot be installed after it.

Rule Collectors

| Name | Derives permission/scope | Returns | | --- | --- | --- | | ResourceActionPermissionRuleCollector | "<resource.raw>.perm:<action>" | [HasPermission(...)] | | ResourceActionScopeRuleCollector | "<action>:<resource.resourceType>" | [HasScope(...)] |

ResourceActionPermissionRuleCollector takes no constructor arguments. ResourceActionScopeRuleCollector accepts { scopeless?: "deny" | "skip", allowBareScopeRewrite?: boolean, claim?: string } — claim names the claim whose presence says the token asserted scopes (default scope; set it to what PayloadScopeCollector reads, e.g. scp).

  • scopeless (default "deny"): it emits the HasScope rule for every request, so a token carrying no scope claim (scope, or the claim claim names) fails it. "skip" emits no rule for a scopeless token — only use it in a pipeline where another rule group authorizes the request, since a request that collects no rule at all is denied.
  • allowBareScopeRewrite (default false): forwarded to HasScope. Set it to true only if your issuer emits bare resource names (project) rather than {action}:{resourceType} scopes (read:project).

Resource Parser

DotNotationResourceParser

Parses a dot-notation string into a Resource.

new DotNotationResourceParser()

Grammar:

resource = segment *( "." segment )
segment  = type [ ":" id ]
type     = 1*tchar
id       = 1*tchar
tchar    = %x21 / %x23-2D / %x2F-39 / %x3B-5B / %x5D-7E
           ; RFC 6749 NQCHAR less "." and ":"
           ; i.e. printable ASCII except space, `"`, `\`, `.` and `:`

Example: "foo.bar:123" → { raw: "foo.bar:123", resourceType: "foo.bar", resourceId: "123" }

  • Segments are split by .. Each segment may include :id.
  • resourceType is the segment types joined with . — the separator is preserved, not rewritten.
  • resourceId is the id of the last segment, if present.
  • raw is the input verbatim.

Anything the grammar does not accept raises ResourceParseError (from @o3co/auth.policy-verifier.core); the parser never repairs its input. The server answers such a request 400 invalid_request, not a decision. Refused, among others:

| Input | Why | | --- | --- | | "", a..b, .a, a. | an empty segment — every segment needs a type | | a:, :1 | an empty type or id | | a:1:2 | more than one : in a segment — the tail is refused, not truncated away | | a:1 , a : 1 | whitespace — it is refused, not trimmed | | プロジェクト, a"b, a\b | a character no OAuth scope value may carry |

resourceType is the authorization namespace: ResourceActionScopeRuleCollector turns it into the {action}:{resourceType} scope that must be granted. Two distinct resources that parse to the same type are therefore authorized identically, so the grammar is built to make that impossible — . is reserved as the separator, which keeps the nested type a.b distinct from the flat type literally named a_b (both were a_b before). This is the same principle HasScope applies to scope values: compare what was written, never a normalized guess at what was meant.

An id that needs ., : or a character outside the set must be encoded by the caller (percent-encoding round-trips through this grammar) or handled by a ResourceParser written for that syntax.

builtinCollectorsModule

builtinCollectorsModule is a Module (name: "builtin-collectors") that registers all built-in implementations into their respective registries.

import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";

Registrations as of this writing; the source of truth is src/module.mts.

| Registry | Name | Factory | | --- | --- | --- | | attributeCollector | "PayloadScopeCollector" | (config) => new PayloadScopeCollector(config) | | attributeCollector | "PayloadSubjectIdCollector" | () => new PayloadSubjectIdCollector() | | attributeCollector | "StaticPermissionCollector" | (config) => new StaticPermissionCollector(config) | | attributeCollector | "StaticRoleCollector" | (config) => new StaticRoleCollector(config) | | attributeCollector | "RequestContextAttributeCollector" | (config) => new RequestContextAttributeCollector(config) | | attributeCollector | "PayloadClaimAttributeCollector" | (config) => new PayloadClaimAttributeCollector(config) | | ruleCollector | "ResourceActionScopeRuleCollector" | (config) => new ResourceActionScopeRuleCollector(config) | | ruleCollector | "ResourceActionPermissionRuleCollector" | () => new ResourceActionPermissionRuleCollector() | | resourceParser | "DotNotationResourceParser" | () => new DotNotationResourceParser() |

See Also