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

@anchrd/gate

v0.40.0

Published

**gate** — login and permissions as a finished product. A Cloudflare Worker holds the logic, [D1](https://developers.cloudflare.com/d1/) holds everything as data. Deployable per customer, steerable from the outside over **REST** and **MCP**.

Readme

@anchrd/gate

gate — login and permissions as a finished product. A Cloudflare Worker holds the logic, D1 holds everything as data. Deployable per customer, steerable from the outside over REST and MCP.

Built on Better Auth, Hono, Drizzle, and Zod.

Install

npm i @anchrd/gate

What it is

One question matters: can(interface, function) — may this user perform this action? gate holds users, roles with their permissions, registered interfaces, and an append-only audit log. A customer edge asks /api/v1/authorization once per request and gets back { identity, rules } — you never write code inside gate; everything is data.

The most convenient way to steer and consume gate is @anchrd/gate-sdk (client + gate CLI).

Where this package stops. It needs a D1 binding and a rate-limit binding, and it needs BETTER_AUTH_URL and BETTER_AUTH_SECRET; without them it does not come up in a usable state. It is not a library you call in-process — every consumer talks HTTP. It does not run your service's authorization for you: it answers what may this identity do, and the decision is made in your own code by can(). It carries the admin UI but does not serve it unless you ask for it (the assets binding below is optional), and it stores no business data — only principals, roles, permissions, registered interfaces and the audit log.

Setup (Cloudflare Worker)

The core is a factory (createGate(deps)); a thin worker shell injects the bindings. The ready-made ./cloudflare export is exactly that shell — your worker entry re-exports it:

// api.ts — your wrangler `main`
import worker from "@anchrd/gate/cloudflare";
export default worker;
// wrangler.jsonc
{
  "name": "gate",
  "main": "api.ts",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [{ "binding": "DB", "database_name": "gate" }],
  "ratelimits": [{ "name": "RATE_LIMIT", "simple": { "limit": 100, "period": 60 } }],
  "vars": {
    "BETTER_AUTH_URL": "https://gate.your-customer.dev",
    "GATE_ADMIN_EMAIL": "",
    "GATE_OAUTH_AUDIENCES": "https://operations-dwh.example.com/mcp,https://operations-intel.example.com/mcp"
  }
}

One required secret (not in vars):

wrangler secret put BETTER_AUTH_SECRET     # ≥ 32 bytes of randomness

Apply the migrations and deploy. The first human who signs up becomes administrator and can sign in without email verification; every later account must verify its address normally. Between deployment and that first sign-up, the publicly reachable installation can be claimed. Sign up first, then announce or expose the installation to its intended audience.

GATE_ADMIN_EMAIL is an optional emergency exit, not bootstrap configuration. Leave it empty during normal operation. If all administrator roles are lost, set it to the email address of an existing account; the shared admin guard then admits that account so roles can be repaired. Remove the value again after recovery.

GATE_OAUTH_AUDIENCES is an optional, comma-separated allowlist for MCP resource servers hosted outside gate. Add their exact public resource URLs (including /mcp when applicable). Gate then accepts RFC 8707 resource parameters for those URLs and issues access tokens with the requested audience; every URL not on the allowlist remains rejected. Without this value, only BETTER_AUTH_URL is a valid audience.

When registering such a service, set its oauthResource to the same exact URL. The service key then accepts only JWT access tokens minted for that resource; a token for another configured resource is rejected.

Hosting elsewhere means swapping the ./cloudflare shell plus the db/mailer adapters; the core beneath knows no env.

Social sign-in (optional: Google, Microsoft)

Users can sign in with Google or Microsoft (OIDC). A provider is active exactly when all its values are set — there is no second switch. Set none and gate behaves as before (the login page shows only email/password). Credentials are worker configuration, never gate.json: that file lives in your git and is baked into the UI at build time; a client secret has no place there and the worker could not read it anyway.

wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put MICROSOFT_CLIENT_ID
wrangler secret put MICROSOFT_CLIENT_SECRET

Register these redirect URIs at the provider (they are <BETTER_AUTH_URL>/callback/<provider>):

| Provider | Where | Redirect URI | |---|---|---| | Google | Google Cloud Console → OAuth client → Authorized redirect URIs | https://gate.your-customer.dev/callback/google | | Microsoft | Entra ID → App registration → Authentication → Redirect URIs (Web) | https://gate.your-customer.dev/callback/microsoft |

Swap gate.your-customer.dev for your BETTER_AUTH_URL. The public GET /api/v1/auth-options returns only each active provider's name and sign-in path (no client ID or secret), so the UI knows which button to show. The first human account becomes administrator regardless of whether it is created through OIDC or email/password. Every later OIDC account holds no role by default, just like every later email sign-up. Google can supply a verified email; Microsoft is forced to emailVerified: false, so gate's own verification mail must establish ownership before an invitation can be accepted.

Cloudflare Access as a way IN has been removed (#446). CLOUDFLARE_ACCESS_CLIENT_ID, CLOUDFLARE_ACCESS_CLIENT_SECRET and CLOUDFLARE_ACCESS_TEAM_NAME are no longer read; an installation that still sets them is not broken, they are simply ignored. Existing account rows for the provider are kept, and the account screen still lists and unlinks the leftover way.

⚠️ Order matters, and there is no rescue afterwards. An account whose only way in was Access and that carries no password is locked out once the provider is gone: sendResetPassword deliberately sends no reset link to an account without a credential row, only a hint pointing at /account, and /account needs a session. Such an account must set a password through POST /set-password while the provider is still configured.

⚠️ This does not touch the opposite direction: gate as an identity provider for others. /oauth2/authorize and the ES256 key set at /jwks are unchanged. What did go, with #531, are the roles and mcp claims in the ID token: they existed for Cloudflare Access in front of the MCP portal, which is no longer part of the product. A policy that still reads either claim finds nothing and has to be moved to a product-side check (POST /api/v1/authorization).

Second factor (optional, per person)

Anyone with an account can switch on a second factor for themselves under Account: a passkey (WebAuthn, bound to the device) plus a set of backup codes. Nothing is switched on for anyone by gate, and no role or setting can require it.

  • The passkey answers the challenge. Signing in stays password-first: the password is checked, and gate then asks for the passkey before it hands out a session. The passkey is the second factor, not a replacement for the first one.
  • The backup codes are the way back when the device is gone: sign in with the password as usual, then answer the same challenge with one code instead. Each code works once.
  • They are shown once, at setup. gate stores them hashed and has no endpoint that reads them back; POST /two-factor/generate-backup-codes replaces the whole set with a new one.
  • There is no code by email, and no TOTP app. As long as the mailbox can reset the password, a code sent to that same mailbox is the same factor twice, not a second one.

Two refusals exist so that nobody can lock themselves out, and both answer 409:

| You try to | gate answers | |---|---| | switch the factor on without a passkey | refused — the backup codes are the rescue for a passkey, not a factor of their own | | remove your last passkey while the factor is on | refused — switch the factor off first, then remove the passkey |

On the API surface, POST /passkey/verify-authentication does create a session on its own, the way the Better Auth plugin ships it. The interface gate delivers does not offer that as a way in: there is no "sign in with a passkey" button before the password, and the passkey is only ever offered as the second step. Anything built on top of the API should say which of the two it means.

⚠️ A second factor protects the sign-in in a BROWSER. It does not protect the machine door. The challenge hangs on /sign-in/email, /sign-in/username and /sign-in/phone-number and on nothing else — a request carrying an x-api-key, a bearer token or an OAuth access token is never asked for a second factor, and its behaviour is unchanged by this feature.

That is deliberate rather than an omission: a machine principal cannot carry a second factor, because there is nobody present to answer the challenge. Any future rule that makes a second factor mandatory has to exempt machine principals explicitly, or it locks out the very agents the installation runs on.

Admin UI (optional, same-origin)

The admin SPA travels inside this package, under ui/, and is served as Workers Static Assets on this same worker — one deploy per customer, no separate UI worker. Same-origin means the Better Auth session cookie just works: no CORS, no SameSite pain. Add an assets binding pointing at the built UI:

// wrangler.jsonc — add alongside the config above
{
  "assets": {
    "directory": ".gate/ui",  // where `gate build` puts the built UI (see below)
    "binding": "ASSETS"       // exposes env.ASSETS to the shell for the SPA fallback
  }
}

Build the UI, then deploy the worker — wrangler dev/deploy serves UI and API from one process:

npm i @anchrd/gate
npx gate build       # -> .gate/ui (reads your gate.json)
wrangler deploy

The UI ships as Vite/React source, not as a prebuilt bundle: gate build applies your gate.json — theme, logo, favicon, language catalogs — and builds it. A prebuilt dist could not do that. The output belongs to you and is written outside node_modules, which the next npm ci would wipe:

| the UI is found in | output | |---|---| | node_modules/@anchrd/gate/ui (installed) | .gate/ui in your project root | | packages/gate/ui (this workspace) | packages/gate/ui/dist |

You never import from the UI in your own code — building it is the whole interface. The same command mirrors this package's D1 migrations to .gate/migrations, so your wrangler.jsonc needs no path into a package's insides.

Routing is automatic and needs no main changes: Cloudflare's asset router serves existing files (/, /assets/*) before the worker; the API routes (/api/*, /oauth2/*, /.well-known/*, /mcp, /consent, …) keep priority; and a browser navigation to a client route (/grants, /roles, …) falls back to index.html so deep-links and reloads work. The customer picks the domain (e.g. admin.gate.your-customer.dev) in their own wrangler config. Omit the assets binding and gate is a pure API — env.ASSETS is absent and the fallback is skipped.

Why the UI travels in here. Until 0.26.0 it was a package of its own (@anchrd/gate-ui), versioned separately from the API. That bought a leaner tarball for a consumer who runs gate headless — and there has never been one. What it cost was real: an installation had to pin two version numbers for one product, and the two drifted apart. Since 0.28.0 there is one number. ⚠️ The price is named, not hidden: whoever installs this package pulls the UI's build dependencies (vite, react, tailwindcss) even if they never run gate build. The repo's reference consumer (workers/api) points its assets.directory straight at the workspace build (../../packages/gate/ui/dist).

Surface

| Path | | |---|---| | POST /api/v1/authorization | Bearer → { identity, rules }, scoped to the calling service (service key; stamps "last seen"). | | GET /api/v1/schema | The calling service's own interfaces, for codegen (gate init). | | PUT/DELETE /api/v1/service/interfaces/:handle | Idempotent self-management of the calling app service's own interfaces. The service key supplies the scope; no service id is accepted. | | /api/v1/services · /roles · /users | Administration (admin). A service owns its interfaces (/services/:id/interfaces) and carries the one service key. A role owns its permissions: /roles/:role/permissions reads them as a tree (service → interface → function) and sets/unsets a single (role, interfaceId, function). | | /api/v1/users/:id/disable · /enable | Lock a human account and release it again. Sessions are deleted, sign-in is refused, an already issued token stops resolving at the next protected access — identity and roles are kept. Nobody locks themselves, and nobody locks the last operational human admin. | | /api/v1/applications | Machine principals with scoped API keys (CI/CLI), including /:id/disable and /:id/enable. | | /api/v1/oauth-clients | The clients that registered themselves at the open POST /oauth2/register — list them with their registration date, DELETE one together with its tokens and consents. There is no create path here: the open endpoint is the only way in. | | /mcp | The same administration actions as MCP tools. | | /auth/* · /oauth2/* · /.well-known/* | Better Auth: login, OAuth provider, DCR, PKCE, discovery. | | /health | Whether the gate itself is alive. |

Two kinds of auth: authorization, schema, and interface self-management accept the service key; /api/v1/auth-options is public. Everything else under /api/v1 requires a user bearer with admin rights (or an application key holding the admin role).

Every service-key door is scoped to the calling service. The key identifies a service, and gate answers only about that service's own interfaces: /authorization intersects the user's permissions with them (a data warehouse backend never learns what the same user may do on the dashboard MCP), and /schema generates types for those interfaces alone. This is also what keeps a handle unambiguous — two services may carry the same handle, but one response only ever describes one service. The single exception is POST /api/v1/users/invite/accept: an invitation is accepted by the invitee, who is not an admin. It still requires an authenticated session, and the role it grants comes from the stored invitation — never from the request body. Accepting only works on an account whose email is verified, which is what stops a stranger from pre-registering an address about to be invited.

Sign-up stays open, sign-in requires a verified email. A newly created user holds no role and therefore no access; POST /sign-up/email no longer returns a session, and the first sign-in attempt sends the verification mail through the injected mailer port. Sign-in, sign-up and invite are rate limited through the RATE_LIMIT binding.

Related

  • @anchrd/gate-sdk — the client, the shared wire formats and the gate CLI. That is the package a consumer of a gate installs; this one is what an operator installs, and it depends on the SDK.