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

@b1-road/node-core

v0.1.0-alpha.1

Published

Framework-agnostic core for the Eduzz Plat Node BFF — OIDC login, session + token store, streaming proxy, permission primitives. Install a driver (@b1-road/express, @b1-road/nestjs) instead of this.

Readme

@b1-road/node-core

The framework-agnostic Eduzz Plat BFF. You probably want a driver, not this package.

| If your server is… | Install | | --- | --- | | Express | @b1-road/express | | NestJS | @b1-road/nestjs | | something else | this package — see Writing a driver |

This package exists so those drivers cannot drift. PKCE, the state round-trip, refresh-with-margin, logout revocation, the proxy's decision chain, the signed CSRF double-submit, the webhook HMAC and the permission algebra are implemented once, here. A fix lands in one place and every driver inherits it; a new framework becomes an adapter rather than a fork.

Status: alpha pre-release, versioned with the drivers that consume it.

The shape of it

                    ┌────────────────────────┐
   @b1-road/express │                        │ @b1-road/nestjs
   ─────────────────┤   @b1-road/node-core   ├─────────────────
   routing only     │                        │ DI + decorators
                    │  OIDC · session · proxy│    only
                    │  CSRF · webhooks · IAM │
                    └───────────┬────────────┘
                                │
                         @b1-road/types
                      (the wire contract)

A driver does two things: turn its framework's request/response into the structural shapes below, and decide which of its routes are protected. It does not re-implement any part of the BFF.

The framework boundary

Everything here is written against two structural types, not against any framework's:

interface RoadRequest {
  method: string;
  url?: string;
  originalUrl?: string;        // set by frameworks that keep a pre-mount copy
  headers: Record<string, string | string[] | undefined>;
  params?: Record<string, string | undefined>;
  query?: Record<string, unknown>;
  body?: unknown;
  rawBody?: Buffer | string;
  readableEnded?: boolean;
}

interface RoadResponse {
  statusCode: number;
  headersSent: boolean;
  setHeader(name, value): unknown;
  getHeader(name): unknown;
  write(chunk): boolean;
  end(chunk?): unknown;
  on(event, listener): unknown;
}

They are deliberately the intersection of Node's http.IncomingMessage / http.ServerResponse and the few extras every Node web framework populates. An Express Request is one of these with fields added; Fastify's request.raw / reply.raw are the same objects underneath. So an adapter is a cast, not a conversion.

Two consequences worth knowing:

  • Nothing here calls res.status(...).json(...). That is Express sugar; the core writes through writeHead/end, which every Node server speaks. Use sendJson / sendHtml / sendEmpty / redirect from this package.
  • If a type would force a framework import, it belongs in http/http.ts as a structural shape instead.

Writing a driver

import {
  createRoadRuntime,
  authenticate,
  authorizeRequest,
  handleProxy,
  runWithAuthContext,
  requestIdOf,
  splitTarget,
  stripPrefix,
  AUTH_ROUTE_PREFIX,
} from '@b1-road/node-core';

// 1. Wire it once, at mount time. This is also where the boot-time safety
//    guards fire, so a misconfigured app fails here rather than on a user's
//    first login.
const runtime = createRoadRuntime(options, {
  context: {
    label: 'myDriver()',                      // prefixes every boot failure
    dotenvHint: 'Load your env before …',     // framework-specific next step
  },
  logger,
});

runtime carries { options, store, oidc, pkce, sessions, flow, client, health, logger, fetcher, warm(), close() }.

// 2. Mount the four auth routes. Their paths are a COMPATIBILITY CONTRACT:
//    the redirect URI registered with the Auth Server names the callback
//    literally, so renaming them breaks login in a way that looks like a Plat
//    outage and isn't. `AUTH_ROUTE_PREFIX` is 'auth/road'.
//      GET  {prefix}/login     -> runtime.flow.login(req, res)
//      GET  {prefix}/callback  -> runtime.flow.callback(req, res)
//      POST {prefix}/logout    -> runtime.flow.logout(req, res)
//      GET  {prefix}/logout    -> runtime.flow.logoutRedirect(req, res)
//
//    `callback` throws on a genuine dead end. Render it with
//    `renderRoadError(req, res, err, { retryHref: runtime.flow.loginUrl() })`
//    so a browser gets a page and a fetch client gets the JSON contract.

// 3. Mount the proxy.
const [path, queryString] = splitTarget(req.url ?? '/');
const subPath = stripPrefix(path, runtime.options.proxy.prefix);
if (subPath !== null) {
  const outcome = await handleProxy(
    { options: runtime.options, sessions: runtime.sessions, fetcher: runtime.fetcher },
    { req, res, subPath, queryString, requestId: requestIdOf(req) },
  );
  if (outcome === 'handled') return;   // 'passthrough' = the proxy is disabled
}

// 4. Authenticate a protected route, and ENTER THE ASYNC CONTEXT around
//    whatever runs next. This is what lets `auth()` work with no argument in
//    the integrator's handler — the property that makes handler code
//    identical across drivers.
const ctx = await authenticate(
  { sessions: runtime.sessions, client: runtime.client, logger: runtime.logger },
  req, res,
);
runWithAuthContext(ctx, () => next());

// 5. Authorize, when the route declares a permission.
await authorizeRequest(
  {
    client: runtime.client,
    scopeResolvers: runtime.options.scopeResolvers,
    logger,
    // Required for `{ from: 'platform' }`. Without it the check throws
    // scope_resolution_failed even when ROAD_PLATFORM_ID is set, because
    // authorizeRequest reads this field, not the runtime options.
    platformId: runtime.options.platformId,
  },
  ctx, req,
  { action: 'read', subject: 'Member', in: 'buId' },
);

in accepts a route-param name, a structured { from: 'body' | 'header' | 'query' | 'param' } source, a resolver function, or { from: 'platform', buParam } — which resolves the platform subscription's scope rather than the business unit's. That last one matters: a platform's permission template and roles are registered on the subscription scope, and grant collection walks up the scope tree and never down, so authorizing one of your own subjects against the business unit denies every caller who is not a BU owner. Pass platformId in the options (or ROAD_PLATFORM_ID) and the checks never repeat it.

Things a driver must get right, because the core cannot do them for you:

  • Enter the ALS frame. Call next() from inside runWithAuthContext, or auth() is empty in the handler.
  • Answer, don't rethrow, an auth failure — or at least make sure a RoadAuthnError becomes a 401 carrying WWW-Authenticate: Session. The @b1-road/react client keys its onUnauthenticated callback off that header.
  • Gate the decision trace on both halves. includeTrace must be runtime.options.debugHeader && isDebugRequested(req). Passing only the first leaks the full authorization decision on every 403 in any non-prod deployment.
  • Await async middleware. Express 4 ignores a returned rejected promise; wrap and forward it yourself.
  • Call runtime.close() on shutdown so the token store lets go.

Test mode is shared too — @b1-road/node-core/testing exports roadScenario() plus the fakes (buildTestNormalizedOptions, seedTokenStoreFromScenario, makeFakeOidcHandler) a driver's own roadTest() composes. A scenario written against one driver is the same object in another.

What lives here

| Area | Exports | | --- | --- | | Assembly | createRoadRuntime, RoadRuntime | | Config + boot guards | normalize, NormalizeContext, RoadCoreOptions, DEFAULT_ALLOW | | HTTP boundary | RoadRequest, RoadResponse, sendJson, redirect, header, query, isDebugRequested | | OIDC | OidcFlow, OidcClient, PkceCookie, safeReturnTo, AUTH_ROUTE_PREFIX | | Session | SessionService, sealSessionId, MemoryTokenStore, RedisTokenStore, RoadTokenStore | | Proxy | handleProxy, stripPrefix, isAllowed, isCsrfValid, isCsrfTokenBoundToSession, deriveCsrfToken | | Authn / authz | authenticate, authorizeRequest, auth, can, runWithAuthContext | | Client | RoadClient and every resource shape it returns | | Errors | the Road*Error hierarchy, renderRoadError, errorBody | | Webhooks | verifyWebhook, webhookVerdictStatus | | Health | RoadHealthChecker, roadHealthUrl | | Bridge | bridgeEnforce (provider side, fail-closed), ROAD_BRIDGE_CONTEXT (the verified grant on the request), and road.bridge.tokenExchange (consumer side, RFC 8693) |

Security properties this package owns

Reviewers and driver authors should know which invariants live here, because a driver cannot weaken them and must not duplicate them:

  • returnTo cannot leave the origin. Validated when sealed and again when used, rejecting //, \, and every control character.
  • The proxy allowlist is a boundary. A path containing a . or .. segment — in any encoding a URL parser would decode — is rejected before any session lookup, so it cannot normalise out of /api/<version>/.
  • CSRF is signed double-submit. The token is an HMAC of the session id under the session secret, and the proxy verifies it was issued for this session — not merely that the cookie and header agree.
  • Webhooks are fail-closed and verified over the raw bytes, in constant time, inside a 5-minute replay window.
  • Refresh is single-flight per session, so concurrent requests never spend a rotating refresh token twice.
  • The proxy never forwards the browser's cookies upstream, and never returns the API's Set-Cookie downstream.

License

MIT