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

@authlink/policy-client

v1.0.1

Published

Official client SDK for requesting policy decisions and effective permissions from the Authlink Policy Decision Point

Readme

🔒 Security stance — read this first

Browser-side checks are UX gating only. This client asks the Policy Decision Point (PDP) what the current user may do so your UI can hide buttons, disable forms, and guard routes — it does not, and cannot, enforce anything. A user with devtools can bypass every check in this package. Enforcement happens at the API policy enforcement points (PEPs) that consume the same policies server-side; the UI's job is to agree with the API, not to replace it.

Everything here fails closed: an unloaded snapshot, a transport error, a Deny, a NotApplicable, and an obligation contract the client does not understand all read as denied. If something goes wrong, the UI under-promises and the API remains the judge.

🚀 Getting Started

Install

npm install @authlink/policy-client

Quickstart

Wire the client to the tokens you already get from @authlink/client (the Authlink identity SDK). getAccessToken is called once per request, so a refreshed token is picked up automatically; getSubjectId supplies the subject (the token's sub claim) so requests can omit it:

import { IdentityProviderClient } from "@authlink/client";
import { PolicyClient, createPolicyContext } from "@authlink/policy-client";

const identity = new IdentityProviderClient("https://identity.example.com");

let tokens: { accessToken: string; sub: string; expiresAt: number } | null = null;

async function getAccessToken(): Promise<string> {
    if (tokens !== null && Date.now() < tokens.expiresAt - 30_000)
        return tokens.accessToken;

    // Silent authorization + code exchange (PKCE handled by @authlink/client).
    // Request the Policy API's audience on the token exchange; the authority is
    // appended automatically, so one token serves identity and policy calls.
    const response = await identity.authorizeSilently({
        clientId,
        redirectUri,
        scope: "openid"
    });
    const exchanged = await identity.token({
        grantType: "authorization_code",
        clientId,
        redirectUri,
        code: response.code!,
        state: response.state,
        audience: "https://policy.example.com"
    });

    const claims = JSON.parse(atob(exchanged.accessToken.split(".")[1]!));
    tokens = {
        accessToken: exchanged.accessToken,
        sub: claims.sub,
        expiresAt: (claims.exp as number) * 1000
    };
    return tokens.accessToken;
}

const client = new PolicyClient({
    baseUrl: "https://policy.example.com",
    getAccessToken,
    getSubjectId: () => {
        if (tokens === null) throw new Error("not signed in");
        return tokens.sub;
    }
});

Then load a permission snapshot once and gate the UI:

const permissions = createPolicyContext(client, { refreshIntervalMs: 5 * 60 * 1000 });
await permissions.load();

// Synchronous — no network per check:
if (permissions.can("document:update")) {
    showEditButton();
}
permissions.canAll(["document:read", "document:update"]);

And gate an attribute-dependent action with a live decision:

import { isAllowed, explainDenial, requiresReason } from "@authlink/policy-client";

const result = await permissions.check("document:delete", {
    resourceType: "document",
    resourceAttributes: { "owner-id": document.ownerId }
});

if (isAllowed(result)) {
    if (requiresReason(result.value)) {
        const reason = await promptForJustification();
        await api.deleteDocument(document.id, { reason });
    } else {
        await api.deleteDocument(document.id);
    }
} else {
    tooltip.show(explainDenial(result)); // deny reason, "no policy permits", or error detail
}

🧭 Snapshot (can) vs live (check)

| | can / canAny / canAll | check / checkMany | |---|---|---| | Answers | "Does the user hold this permission?" | "May the user do this to this resource?" | | Source | Effective-permissions snapshot, loaded once | POST /api/decide per (cached) request | | Cost | Synchronous, no network | Async; 30s TTL cache + in-flight coalescing | | Use for | Menus, buttons, route guards — the bulk of gating | Row/record-level actions that depend on resource attributes, purpose, or context | | Freshness | refresh() / refreshIntervalMs (stale-while-refreshing) | TTL expiry, invalidate(), or automatic clear when a snapshot lands |

Rule of thumb: reach for the snapshot first; use check only when the answer depends on which resource (ownership, tenant, classification) — that is what the resource attributes are for. checkMany evaluates per-row checks in one round-trip via the batch endpoint (POST /api/decisions), falling back to parallel single decisions when the endpoint is not deployed.

📜 Obligations in the UI

An Allow decision may carry obligations — requirements the enforcing PEP must honour. In the browser they translate to UX:

  • RequireReason — requiresReason(response) tells a form to collect a justification before submitting; the API PEP rejects the operation without one.
  • MaskProperties — maskedProperties(response) returns the parsed selector list (ssn, bank_details.iban, prefix wildcards like bank_details.*) for cosmetic field hiding. The API response is already masked server-side; hiding the fields client-side keeps the UI honest about what it never received.
  • RequireAudit — enforced entirely server-side; nothing to do in the UI.
  • RowFilter — compiled and enforced by the API PEP; result sets arrive pre-filtered.

Before trusting any of the above, ask hasUnsupportedObligations(response): it flags unknown obligation kinds, unsupported grammar versions, and unparseable expressions. When it returns true, degrade safely (treat as denied or hide the affected view) — partially honouring obligations is what the fail-closed rule exists to prevent.

⚠️ Error handling

Expected failures never throw. Every call returns PolicyResult<T> = { ok: true; value } | { ok: false; error } with error.code one of validation (HTTP 400, problem details parsed onto error.problem), unauthorized (401 or the token provider failed), forbidden (403), server (5xx, unexpected statuses, malformed bodies), or unavailable (network failure or the 3-second timeout). isAllowed treats them all as denied.

🖼 Framework packages

Both are thin reactive wrappers over this package's PolicyContext.

🛠 Features

  • Policy decisions (permit/deny) against the Authlink Policy Decision Point
  • Effective permission queries
  • Typed error results instead of thrown exceptions (fail-closed friendly)
  • Fail-closed decision guards and obligation helpers (isAllowed, explainDenial, requiresReason, maskedProperties, hasUnsupportedObligations)
  • Permission snapshot (createPolicyContext) with synchronous can/canAny/canAll, stale-while-refreshing updates, and change subscriptions
  • Live per-resource decisions (check/checkMany) with a TTL + LRU decision cache and in-flight coalescing
  • Token-provider auth, request-default merging, and AbortController timeouts
  • Type-safe APIs mirroring the PDP wire contracts
  • Framework-agnostic, zero dependencies
  • Dual ESM + CJS builds with bundled type declarations

🧪 Development

This package is a member of the npm workspace at the repository root:

npm install            # at the repository root
npm run build
npm run test
npm run lint
npm run check:package

📝 License

MIT