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

@procedures/better-auth

v0.48.2

Published

Better Auth for a procedures server: the sign-in with GitHub, Google and passkeys over MongoDB, and a request hook that gives the principal of a session or of an agent token.

Readme

@procedures/better-auth

npm

npm install @procedures/better-auth @procedures/client mongodb

The sign-in of a procedures server with Better Auth over MongoDB, and the request hook that gives the principal of each request.

  • The people sign in with GitHub, Google or a passkey. The app stores no password.
  • Better Auth keeps the sessions in httpOnly cookies, and does the OAuth state, PKCE and the CSRF checks.
  • The organizations of Better Auth are the groups of a user.
  • An agent token lets a program act as the agent, or for one user with the rights of that user.

The package has no procedures. The package root registers nothing.

The server

betterAuthConfig reads the environment. createBetterAuth makes the server. Give Better Auth its own database: its user collection must not mix with the data of the app.

import { betterAuthConfig, betterAuthHandler, createBetterAuth } from "@procedures/better-auth";

const origins = ["https://app.example.com"];
const config = betterAuthConfig(process.env, { port: 9091, trustedOrigins: origins, appName: "Example" });
if (typeof config === "string") throw new Error(config);
const auth = createBetterAuth(client.db("example-auth"), client, config);
app.use(betterAuthHandler(auth, { allowedOrigins: origins }).middleware); // the routes belong to the app

| Variable | Use | | --- | --- | | BETTER_AUTH_SECRET | The secret that signs the session cookies, with 16 characters or more. It is necessary. | | AUTH_BASE_URL | The public origin of the server. The default is http://localhost:<port>. | | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET | The sign-in with GitHub. | | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | The sign-in with Google. | | PASSKEY_RP_ID | The domain of the passkeys. The default is the host of the first trusted origin. | | AUTH_TEST_ACCOUNTS=1 | Accounts with an email and a password, for the tests. Their email counts as verified. Production refuses it. | | AUTH_CROSS_SITE_COOKIES=1 | The cookies get SameSite=None and Secure, for a page on another site. Refer to A page on another origin. |

A page on another origin

The handler of Better Auth sends no CORS headers. betterAuthHandler(auth, options) gives the Node handler of the auth routes with credentialed CORS. Use the allowedOrigins of the host, the list of the browser checks of procedureHandler. Thus one list of trusted origins drives the procedures and the auth routes.

  • A response for a trusted origin names that one origin, with Access-Control-Allow-Credentials: true and Vary: Origin.
  • The handler answers a preflight (OPTIONS) with 204. For a trusted origin, it names the methods (GET, POST) and the headers (Content-Type, Authorization).
  • An origin that is not trusted gets no CORS header. The CSRF checks of Better Auth also refuse its writes.
  • Each response has Vary: Origin, also a response without CORS headers. Thus a shared cache keeps one answer for each origin.
  • The handler refuses "*", because each site could then send the cookies of the user (CSRF).
  • The option cors changes the methods, the headers, the exposed headers and maxAge. The option basePath is /auth by default.
  • Mount the handler on the mount list of a listener. For Express, give handler.middleware to the app.

betterAuthFace(auth) gives the same handler as a face of a procedures host (createProcedureHost() of @procedures/peer). The face takes browser.allowedOrigins of the policy of the host. Each request of the routes goes through the request tracking of the host, on the route pattern of the face (auth *):

  • A request after the stop of the intake gets 503, with the CORS headers. Thus the page can read the refusal.
  • The drain waits for a request in flight. Thus a sign-in at the stop writes its session and sends its answer.
  • The readiness of the controller of the host applies: controller.requires can name the database of Better Auth.
const policy = hostPolicy({ browser: { allowedOrigins: origins } });
const host = createProcedureHost({ registry, policy, faces: { http: true, auth: betterAuthFace(auth) }, listen });

The cookies need more for a page on another site, for example https://app.example.com and https://api.other.example:

  • A browser sends a cookie to another site only when the cookie has SameSite=None. It keeps such a cookie only when it is Secure, so the server needs HTTPS (or localhost).
  • Set AUTH_CROSS_SITE_COOKIES=1, or give crossSiteCookies: true in the configuration. The cookies then get SameSite=None; Secure.
  • Two ports of one host are the same site, for example the dev ports localhost:5173 and localhost:9091. They need CORS, but not SameSite=None.

The browser

@procedures/better-auth/browser gives the calls of a page to the auth routes of the server. It has no framework and no global state. Each function gives data, and the app decides what the page does next.

import { createBrowserAuth } from "@procedures/better-auth/browser";

// The page and the server have one origin (or a proxy gives the server under the origin of the page)
const auth = createBrowserAuth({ baseURL: "" });
const user = await auth.session(); // undefined when signed out
const out = await auth.signInSocial("github", location.href);
if ("url" in out) location.assign(out.url);

| Function | What it does | | --- | --- | | session() | The signed-in user of this browser: id, name and email. A server that does not answer in time gives undefined. | | signInSocial(provider, callbackURL) | The page of the provider. The provider comes back to callbackURL. | | signOut() | This function ends the session. | | addPasskey(name) | This function makes a passkey of this device for the signed-in user. | | signInWithPasskey() | This function signs in with a passkey of this device. | | signInWithPassword(input) | A test server only (AUTH_TEST_ACCOUNTS): a sign-in, or a sign-up with signUp: true. |

  • Each call sends the cookies. The session cookie is httpOnly, so a script of the page cannot read it.
  • Give the page and the server one origin. A proxy under the origin of the page also works. This is the simplest form.
  • A server on another origin needs CORS that names exactly one trusted origin, with Access-Control-Allow-Credentials: true. betterAuthHandler sends these headers. Without them, session() gives undefined and the other calls fail. Refer to A page on another origin.
  • A server on another site also needs the cookie attributes SameSite=None and Secure (AUTH_CROSS_SITE_COOKIES=1).
  • A refused call gives { error }. The functions do not throw for an answer of the server.
  • The passkeys use @simplewebauthn/browser, which loads only when a passkey function operates.

The principal of a request

SessionPrincipals gives the user of a session cookie: the id, the verified email, the name and the groups. A short memo keeps the boot of a page to one session lookup.

betterAuthPrincipalOf gives the request hook (principalOf) of a procedures server. The app maps each kind of principal to a capability:

import { betterAuthPrincipalOf, SessionPrincipals, VIA } from "@procedures/better-auth";

const READ_ONLY = { effects: { writes: [] } };
const principals = new SessionPrincipals({ auth, db: client.db("example-auth") });
const server = createProcedureServer({
  // No `token`: the hook checks the agent token, and a cookie session has no bearer token
  principalOf: betterAuthPrincipalOf({
    principals,
    agentToken: process.env.AGENT_TOKEN,
    // The agent acts only for users who own no group
    actAs: (user) => !user.groups.some((group) => group.role === "owner"),
    // A visitor without a session can read, and change nothing
    anonymous: READ_ONLY,
  }),
  capability: (request) => (request.principal?.via === VIA.anonymous ? READ_ONLY : {}),
});

| The request has | The principal | | --- | --- | | The agent token as the bearer token | { id: "agent", via: "agent-token" } | | The agent token and the header x-act-as: <user id> | The user, with via: "agent-as" and the claims agent: true and actAs. The actAs policy must permit the user. Without the policy, the hook refuses the request. | | A session cookie | The user, with via: "better-auth" and the claims email, name and groups | | Nothing of the above | With anonymous: { id: "anonymous", via: "anonymous" } and that narrowing. Without it (the default): no principal, and the server refuses the request. |

  • An act-as request for an unknown user gets the anonymous principal (via: "anonymous"), or a refusal without anonymous. A refused act-as gets no principal. It does not get the principal of the agent.
  • A bearer token that is not the agent token goes on to the token check of the server.
  • Do not give the server a token with this hook. The token check of the server operates after the hook, and a cookie session has no bearer token.
  • The procedures read the principal as ctx.principal. The rights of the app stay in the app.
  • The hook compares the agent token in constant time with tokenMatches of @procedures/client.