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

@padosoft/laravel-iam-node

v1.3.0

Published

Thin, fail-closed TypeScript/Node client for the Laravel IAM control plane (PDP decisions/check + JWKS token verification).

Readme

@padosoft/laravel-iam-node

Thin, fail-closed TypeScript/Node client for the Laravel IAM control plane.

tests npm license docs

Ask the IAM server "is this subject allowed to do this?" and verify its tokens — from any Node service, with the exact same wire contract and guarantees as the PHP client. No policy logic lives here: every decision is the server's.

📚 Full documentation: doc.laravel-iam-node.padosoft.com — theory of fail-closed, decision-flow & token-verification diagrams, ADRs, Express/Fastify quickstarts, and the complete API reference.

Why

Laravel IAM is an Identity & Authorization control plane (PDP) for multi-application ecosystems. Your non-PHP services still need to ask it for decisions. This SDK is the JavaScript core of that story:

  • Fail-closed by construction. Any network error, timeout, 5xx, 4xx, malformed body, or unverifiable token resolves to deny — never allow. There is no fail-open switch. An unreachable PDP must never open the doors.
  • No PDP logic client-side. The verdict always comes from the server's decisions/check. The client never interprets grants or policies.
  • Drop-in parity. Same endpoint, payload, Bearer auth and response handling as the PHP HttpDecider — the server can't tell the callers apart.
  • Zero heavy deps. Native fetch (Node 18+) and jose for JWKS verification. ESM + CJS + types.

Install

npm install @padosoft/laravel-iam-node

Requires Node 18+ (native fetch).

Quick start

import { IamClient } from '@padosoft/laravel-iam-node';

const iam = new IamClient({
  baseUrl: 'https://iam.example.com/api/iam/v1', // full API base, incl. route prefix
  token: process.env.IAM_SERVICE_TOKEN,          // OAuth2 Client Credentials service token
  timeoutMs: 2000,
  cache: { ttlMs: 5000 },                        // optional, OFF by default
});

const decision = await iam.check({
  subject: { type: 'user', id: 'usr_123' },
  application: 'warehouse',
  permission: 'stock.adjust',
  resource: { type: 'warehouse', id: 'wh_milan' },
  context: { amount: 300 },
});

if (!decision.allowed) throw new Forbidden(decision);     // fail-closed
if (decision.requiresStepUp) promptStepUp(decision.requiredAal);

Prefer a single boolean? iam.can(query) returns true only when the PDP allowed and no step-up is pending:

if (!(await iam.can(query))) return res.status(403).end();

Fail-closed: read this

allowed === true alone is not permission. When requiresStepUp is true, the action is only permitted at a higher AAL — treat it as not yet allowed. Use iam.can() / isGranted() for the fail-safe interpretation, and only inspect requiresStepUp when you intend to drive a step-up challenge.

The cache (opt-in, off by default) never turns a deny into an allow: it stores the server's verdict verbatim, expires on a short TTL, never caches transport errors, and flushes the whole cache when the server reports a newer policy_version. Correctness before latency.

API

new IamClient(config)

| Option | Default | Description | | --- | --- | --- | | baseUrl | — (required) | Full API base, e.g. https://iam.example.com/api/iam/v1. | | token | — | Static service token sent as Authorization: Bearer. | | clientId + privateKey (+ privateKeyKid) | — | private_key_jwt (RFC 7523) — asymmetric, no shared secret. privateKey is an ES256 PKCS#8 PEM; the client signs a short-lived assertion per token request. Highest precedence. Register the matching public key (JWKS) in IAM. | | clientId + clientSecret | — | Self-managed client_credentials: the client mints/refreshes the token itself and auto-follows IAM's client-secret rotation (self-fetch), so a long-running service never breaks on a rotation and you never handle a secret by hand. Takes precedence over token. | | oauthUrl | <origin>/oauth | OAuth base for the token + self-fetch endpoints. | | timeoutMs | 2000 | Per-request timeout. | | retries | 0 | Retries for idempotent network errors only (never on 4xx/5xx). | | cache | off | { ttlMs, maxEntries? } short-TTL decision cache. | | verify | — | { issuer?, audience?, jwksUri? } defaults for verifyToken. | | fetch | global | Inject a custom fetch (tests, proxies). |

Auto-rotating credentials (recommended for long-lived services): pass clientId + clientSecret instead of a static token. The client obtains an access token via client_credentials and, when IAM auto-rotates the secret, self-fetches the new one from POST /oauth/client-secret (authenticating with the still-valid secret during the grace) and hot-swaps it — zero downtime. Enable the endpoint server-side with IAM_OAUTH_CLIENT_SELFFETCH=true. See Application credentials & lifecycle.

check(query): Promise<Decision>

POST {baseUrl}/decisions/check. Returns a normalised Decision (allowed, decisionId, policyVersion, requiresStepUp, requiredAal, matched[], explanation[]). Never throws.

can(query): Promise<boolean>

check() reduced to the fail-safe boolean (allowed and not step-up-pending).

listResources(subject, relation): Promise<Resource[]>

ReBAC list-resources (M16): the resources on which subject has relation. Fail-closed — returns [] on any error.

verifyToken(jwt, options?): Promise<Claims>

Verifies an access/ID token's ES256 signature and iss / aud / exp / nbf against the server JWKS (/.well-known/jwks.json). Resolves to the verified claims, or rejects with TokenVerificationError (the fail-closed signal — treat a rejection as deny). JWKS are cached and refetched on key rotation.

try {
  const claims = await iam.verifyToken(bearer, { audience: 'warehouse' });
  // trust claims.sub / claims.org / claims.scope
} catch {
  return res.status(401).end(); // fail-closed
}

validateManifest(manifest) / submitManifest(manifest, opts)

A node service that owns a permission catalog declares it in a manifest (a versioned file — it is your source of truth) and pushes it to IAM. validateManifest checks it locally against the laravel-iam.manifest.v2 rules (mirrors the server + the published schema at /.well-known/iam-manifest-schema.json); submitManifest POSTs it to the Admin API.

import { validateManifest, submitManifest } from '@padosoft/laravel-iam-node';

const manifest = JSON.parse(await readFile('iam.manifest.json', 'utf8'));

const { valid, errors } = validateManifest(manifest);
if (!valid) throw new Error(errors.join('; '));

const res = await submitManifest(manifest, {
  baseUrl: 'https://your-iam.example.com/api/iam/v1',
  token: process.env.IAM_TOKEN!, // needs iam:manifests.submit
});
// res.ok / res.status / res.data — IAM diffs it: additive changes apply, a removal is gated for approval
// and the removed role/permission is DEPRECATED (kept, disabled), never deleted.

Run it in CI on deploy for hands-off sync. See Keeping IAM in sync.

Middleware (Express / Fastify)

import { requirePermission } from '@padosoft/laravel-iam-node/middleware';

app.post(
  '/warehouses/:id/stock',
  requirePermission(iam, 'stock.adjust', {
    resource: (req) => ({ type: 'warehouse', id: req.params.id }),
    context: (req) => ({ amount: req.body.amount }),
  }),
  stockHandler,
);

The subject defaults to req.user.id / req.auth.sub. A missing subject, an unreachable PDP, or a pending step-up all respond 403 (fail-closed) and never call next().

Endpoint contract

This SDK speaks the canonical decision contract (01-architecture.md §12), mirroring the PHP client:

  • Endpoint: POST {baseUrl}/decisions/check
  • Auth: Authorization: Bearer <service token>, Accept: application/json
  • Body: { subject:{type,id}, permission, organization, application, resource, context, current_aal, explain }
  • Response: the server's Decision (a { "data": { … } } envelope is unwrapped transparently)

Ecosystem

This SDK is one client in the Laravel IAM family — a server (the PDP) plus a polyglot fleet of thin, fail-closed clients sharing one wire contract and one audit trail:

Documentation

Full docs — fail-closed theory, mermaid decision/token-verification diagrams, ADRs, Express & Fastify quickstarts, and a complete API reference — live at doc.laravel-iam-node.padosoft.com.

License

MIT © Padosoft · npm