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

@entitle/next

v0.2.0

Published

Next.js HTTP adapter for the Entitle policy engine

Readme

@entitle/next

Next.js App Router adapter for Entitle, a strongly-typed feature entitlements and policy engine for TypeScript.

It builds the route handlers for a catch-all App Router segment.

These routes can change who is entitled to what. /assign, /revoke and /override grant any policy, or any limit, to any subject. authorize is a required option on every Entitle adapter and there is no way to mount one without it: 0.0.1 served these routes with no authentication, which was an anonymous privilege-escalation API rather than a missing feature, and 0.1.0 made the hook required in order to close it. expose defaults to 'read-only', so the four mutating routes are not registered at all until you pass expose: 'all'.

Read Security in @entitle/core's README before you deploy this. It is the same text as node_modules/@entitle/core/README.md, which this package installs alongside itself.

Install

pnpm add @entitle/next @entitle/core

Usage

Create a catch-all route at app/api/entitle/[...path]/route.ts:

import { createNextHandlers } from '@entitle/next'

const handlers = createNextHandlers({
  engine,
  // Omit `expose` for a read-only mount: check / explain / resolve only.
  // `'all'` additionally serves assign / revoke / override / override/remove.
  expose: 'all',
  authorize: async ({ access, request }) => {
    const session = await getSession(request.headers.get('cookie') ?? undefined)
    if (!session) return { ok: false, code: 'UNAUTHENTICATED' }
    if (access === 'write' && !session.user.isAdmin) {
      return { ok: false, code: 'FORBIDDEN' }
    }
    return { ok: true, actorId: session.user.id }
  },
})

export const POST = handlers.POST

Requests to /api/entitle/check, /api/entitle/explain and the rest are routed to the corresponding handler. A path that is not a mounted operation is a 404.

Options

| Option | Required | Default | Meaning | | ----------- | -------- | ------------- | ----------------------------------------------------- | | engine | yes | -- | A configured PolicyEngine | | authorize | yes | -- | Decides whether one request may perform one operation | | expose | no | 'read-only' | Which routes exist: the three reads, or all seven |

authorize

Called once per request, before the operation runs, with:

| Field | Type | Meaning | | ----------- | -------------------- | ------------------------------------------------ | | operation | AdminOperationName | Canonical operation name, e.g. 'assign_policy' | | access | 'read' \| 'write' | Whether the operation observes or changes state | | input | unknown | The parsed request body | | request | Web Request | For request.headers and cookies |

Return { ok: true, actorId } to allow -- actorId is what the write is attributed to. Return { ok: false, code: 'UNAUTHENTICATED' } for a 401 or { ok: false, code: 'FORBIDDEN' } for a 403; a bare { ok: false } is a 403. A hook that throws or rejects denies the request: it fails closed, and the handler is never reached.

A body that is not valid JSON is reported before the hook runs, which matches how Express and Fastify behave -- both parse upstream of any route handler.

expose

Defaults to 'read-only', which serves check, explain and resolve. The four mutating paths are not in the route table at all until expose: 'all', so until then they 404 rather than 403 -- they genuinely do not exist on that mount. This is the option most likely to surprise you on an upgrade, because a missing authorize is a compile error while a missing expose is a silent 404.

Routes

All four Entitle adapters serve the same POST routes at the same paths:

| Route | Access | Description | | ----------------------- | ------- | ------------------------------------ | | POST /check | read | Check a single feature for a subject | | POST /explain | read | Full evaluation trace for a feature | | POST /resolve | read | Resolve all features for a subject | | POST /assign | write | Assign a policy to a subject | | POST /revoke | write | Revoke a policy from a subject | | POST /override | write | Set a per-subject feature override | | POST /override/remove | write | Remove a per-subject override |

Every body names the subject by subjectId and subjectKind, removals included.

Responses

Every success is the same body on all four adapters, with data always present and null for the four operations that return nothing:

{
  "ok": true,
  "data": null
}

All four adapters agree on this. Before 0.1.1 @entitle/express omitted the data key and @entitle/fastify sent data: undefined, which its serializer drops, so a client written against this adapter broke on a data === null check when moved to either of those. The contract, the idempotent removals, and the three failures the envelope does not cover are What a success response contains in @entitle/core's README.

Errors

Every failure is the same body on all four adapters:

{
  "ok": false,
  "error": {
    "code": "STORE_ERROR",
    "message": "Failed to assign policy",
    "correlationId": "0b9c1f8e-6f2a-4b51-9a2e-2d3f8f0f7c11"
  }
}

code decides the status; message is a constant chosen by the code path; correlationId is present exactly when something was withheld, and matches the logger.error line that carries the cause in full. A driver's error fields, a store's SQL or file path, a feature's schema and an internal exception never reach a response. The full contract is What an error response may contain in @entitle/core's README.

Serving your own users their own grants

In a Next.js app you usually do not want this adapter at all for the read path: you already have an authenticated route, so serve the bundle from there and take the subject from the session rather than from the request body.

// app/api/entitlements/route.ts
export async function GET(request: Request): Promise<Response> {
  const session = await getSession(request.headers.get('cookie') ?? undefined)
  if (!session) return Response.json({ error: 'unauthenticated' }, { status: 401 })

  const bundle = await engine.resolveAll(session.user.id, 'user')
  return Response.json(bundle)
}

That removes the body-supplied subjectId entirely, and with it the enumeration hole that comes from trusting one. See Serving a subject its own grants in @entitle/core's README.

Serving the API open, on purpose

For a sidecar bound to loopback, a mesh that authorizes at the edge, or a test harness, pass UNSAFE_allowUnauthenticated as authorize rather than writing your own always-allow hook. It warns once per process, and the name is deliberately greppable. The deployment assumptions that have to hold first are under UNSAFE_allowUnauthenticated in @entitle/core's README.

Testing

@entitle/testing exports createAuthorizeStub, which is the hook plus a record of what the adapter asked it -- so a test can assert that a refused request never reached the operation.

Links

  • @entitle/core -- the engine, and the guidance the sections above point at: what the routes are and which of them write, expose, serving a subject its own grants, explain as information disclosure, actor attribution, UNSAFE_allowUnauthenticated and the preconditions it needs, and what an error response may contain. Also at node_modules/@entitle/core/README.md.
  • Reporting a vulnerability -- privately, to https://github.com/fponticelli, and not in a public issue. 0.0.1 is deprecated and unsupported: it served these routes with no authentication.

License

MIT