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

@aiquants/auth-directory-core

v0.2.1

Published

Provider-agnostic core for external directory group synchronization: the read-only DirectoryProvider port, a pure reconciler with a removal circuit breaker, and a pure tenant-membership rule. No network, no database, no vendor SDK.

Readme

@aiquants/auth-directory-core

Provider-agnostic core for synchronizing group membership from an external identity directory (Google Workspace, LDAP, …) into a local RBAC schema.

It contains no network calls, no database access, and no vendor SDK — only the read-only port the provider adapters implement, and the two pure functions that make the dangerous decisions: what to delete, and who may act as a tenant.

Why the pure core exists

A directory sync has exactly one irreversible operation: removal. A partial read, an emptied upstream group, or a misconfigured filter all look identical to "everyone left" — and acting on that revokes access for the whole company. Isolating the set arithmetic here means every one of those cases is reproducible in a unit test, with no directory and no database.

Exports

DirectoryProvider (port)

type DirectoryProvider = {
    getGroup(externalId: string): Promise<DirectoryGroup | null>
    listGroupMembers(externalId: string, mode: "direct" | "transitive"): Promise<DirectoryMember[]>
}

Read-only by contract. There is deliberately no write method, so no code path exists in which a sync can modify the upstream directory.

⚠️ listGroupMembers must return a complete list or throw. The caller treats what it returns as the whole upstream truth and deletes everything not in it, so a part-way listing becomes a mass revocation. If paging cannot be completed, throw — never return the partial array.

externalId is the stable upstream id, never the group address: an address can be renamed upstream, and anchoring on it makes the link silently break the moment someone renames the group.

reconcile(local, remote, policy)

Turns "what upstream says" into "what to write locally":

| Output | Meaning | | --- | --- | | ledgerUpserts / ledgerDeletes | The member ledger, which records every upstream member — including people who have never signed in and therefore have no local user row | | projectionAdds / projectionRemoves | The real membership table, which only ever holds resolvable users | | unresolvedEmails | Upstream members with no local user. Not an error — it is the visible answer to "why doesn't this person have the permission yet?" | | aborted | Non-null when the plan must not be applied |

Removal is guarded by a circuit breaker (BlastRadiusPolicy) with two independent axes, each with its own required limits:

| Axis | Counts | Denominator | | --- | --- | --- | | membership | people losing real membership, i.e. losing granted permissions | memberships held before the run | | ledger | ledger rows removed, i.e. people losing the ability to sign in at all | ledger rows before the run |

The axes are separate because the ledger deliberately holds every member observed upstream, including people who have never signed in and hold no membership. Mixing them into one denominator dilutes the ratio in proportion to how many such people a group has: for a group with 1,000 ledger rows and 40 memberships, a run that removes all 40 memberships measures as a 4% blast and sails through — the breaker falls silent in exactly the failure it exists to catch. Separate limits also mean that relaxing one axis for a routine event never quietly relaxes the other.

minimumPopulation exists on each axis because below it the ratio is too twitchy — a five-person group trips on an ordinary transfer, and a warning nobody can act on is a warning nobody reads. maxRemovalRatio is measured against the local pre-sync population, never the upstream count: when upstream returns zero the upstream-based ratio diverges, exactly when the guard matters most.

An abort clears every write, and carries the withheld identities — withheldLedgerDeletes and withheldProjectionRemoves. Asking an operator to approve a removal while showing only a count leaves them no way to decide except to raise the ceiling.

evaluateTenantMembership(input)

Decides whether a caller may act as the tenant it named, and returns the reason alongside the verdict — a denial caused by "tenant not served" and one caused by "not in any group" send an investigation in opposite directions.

1. named tenant is not one this deployment serves → deny  (tenant-not-served)
2. anonymous caller                               → allow (anonymous)
3. the tenant's policy is "no-proof-required"     → allow (proof-not-required)
4. caller is in one of the proof groups           → allow (group-member)
                              otherwise           → deny  (not-in-any-membership-group)

Rule 2 is load-bearing: @anonymous grants belong to the tenant, so rejecting anonymous callers here makes every public resource unreadable.

Rule 3 is an explicit policy, never inferred from an empty list. TenantMembershipPolicy is a discriminated union, so "we require no proof", "somebody forgot to declare the groups" and "the query returned nothing because of a transient fault" cannot collapse into the same outcome — making an isolation axis optional is what turns unspecified into everything.

A require-proof policy must also name at least one break-glass group, and every break-glass group must itself be a proof group. If every proof path is upstream-owned, one accident in the directory locks out every caller including the people who could repair it. assertTenantMembershipPolicy refuses such a policy; call it at wiring time so the deployment fails to start rather than at the first request. Whether a break-glass group is genuinely locally managed is a fact about storage, so that half of the check belongs to the adapter.

Evidence comes from two disjoint sources — local membership (for locally-managed groups) and the member ledger (for externally-sourced ones). Use the ledger, not the projected membership: on a first sign-in the user row exists before the next sync projects it, and reading the projection would lock a legitimate user out of everything for one sync interval.

Blank tenant ids raise AuthzTenantError rather than denying, and malformed group ids raise TypeError rather than being dropped — a silently dropped id produces a false "not a member" with nothing in the logs to explain it.

Tenant identifiers

Validation and comparison are delegated to @aiquants/authz-core (assertTenantId / isSameTenant). This package deliberately does not reimplement them: the definition of "blank" is an explicit Unicode set rather than any language's trim, and a second implementation would drift from the authorization core it has to agree with.

Address comparison

Every address is compared through normalizeAuthEmail from @aiquants/auth-core — the same function the login allowlist uses. Two normalizers would let an address be present in the ledger and absent from the allowlist with nothing raising an error.

Install

pnpm add @aiquants/auth-directory-core @aiquants/auth-core @aiquants/authz-core zod

Both peers are pure logic packages with no I/O, and must resolve to the same instance the host uses. @aiquants/authz-core itself depends on zod, so a host that installs these three by hand needs it too — pnpm add zod — or an ESM import of the authz core fails with ERR_MODULE_NOT_FOUND.