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

@smb-tech/service-framework-js

v0.2.0

Published

Reusable service framework utilities for SMB Tech Node.js services

Readme

@smb-tech/service-framework-js

Reusable Node.js and TypeScript service utilities for SMB Tech services.

This package is published to npm for SMB Tech distribution convenience. It is not a general-purpose community framework. The APIs, defaults, naming conventions, headers, OAuth assumptions and operational behavior are designed for SMB Tech and QuickFade services.

What This Package Is

@smb-tech/service-framework-js centralizes cross-cutting service logic used across SMB Tech Node.js services:

  • Next.js BFFs
  • Express APIs
  • Fastify APIs
  • OAuth-aware gateways
  • Core services
  • Internal microservices
  • Background or integration services that call SMB Tech APIs

It provides shared building blocks for:

  • tracing headers
  • structured logging
  • sensitive data redaction
  • outbound REST logging
  • Bearer token extraction
  • opaque token validation with introspection or tokeninfo
  • JWT validation with JWKS/certs
  • scope guards
  • OAuth Gateway client flows
  • private_key_jwt client assertions
  • JWT bearer grant assertions
  • PKCS#12/P12 key loading from Base64
  • standard error responses
  • Next.js and Express adapters
  • Fastify adapters
  • environment-based service configuration
  • first-class Next.js, Express and internal-client presets
  • OAuth Authorization Server Metadata discovery
  • optional vendor-neutral observability hooks
  • published assertion and P12 CLI

Intended Audience

Use this package when you are building or maintaining SMB Tech or QuickFade services.

If you are outside SMB Tech, the package may still be installable, but the defaults are intentionally opinionated around SMB Tech infrastructure:

  • X-Request-Id
  • B3 trace headers
  • X-Client-* headers
  • X-Service-* headers
  • OAuth Gateway endpoints
  • RS256 JWT validation
  • SMB Tech error payloads
  • SMB Tech scope naming
  • SMB Tech environment variable names

Installation

npm install @smb-tech/service-framework-js

Requirements:

  • Node.js >=20
  • TypeScript supported
  • ESM and CommonJS builds are published
  • Type declarations are exported

Quick Start: Next.js BFF

This is the most common use case for QuickFade BFFs.

import {
  createCookieSafeNextJsonResponse,
  createInternalServiceClient,
  createNextBffService
} from "@smb-tech/service-framework-js";

const service = createNextBffService();
const coreAuthClient = createInternalServiceClient({
  targetService: "core-auth-service",
  baseUrl: process.env.CORE_AUTH_SERVICE_BASE_URL!
});

export const GET = service.authRoute(
  async (_request, { bearerToken, traceContext, auth }) => {
    const profile = await coreAuthClient.getJson("/v1/auth/me", {
      traceContext,
      headers: {
        Authorization: `Bearer ${process.env.CORE_AUTH_CLIENT_CREDENTIALS_TOKEN}`,
        "X-Access-Token": bearerToken
      }
    });

    return createCookieSafeNextJsonResponse({
      subject: auth.subject,
      profile
    });
  },
  { requiredScopes: "cl:core:profile:read" }
);

With this setup the route automatically:

  • reads and propagates trace headers
  • extracts the Authorization: Bearer ... token
  • validates opaque tokens or JWTs
  • validates configured required scopes
  • logs the request outcome
  • returns standard error payloads
  • injects response trace headers
  • prevents cache leaks with cookie-safe response headers

Quick Start: Express Service

import express from "express";
import { createExpressApiService, type ExpressAuthenticatedRequest } from "@smb-tech/service-framework-js";

const service = createExpressApiService();
const app = express();

app.use(express.json());
app.use(service.traceMiddleware);

app.get(
  "/notes",
  service.auth({ requiredScopes: "cl:quickfade:notes:read" }),
  (req: ExpressAuthenticatedRequest, res) => {
    res.status(200).json({
      subject: req.auth?.subject,
      scopes: req.auth?.scopes,
      notes: []
    });
  }
);

app.use(service.errorHandler);

app.listen(3001, () => {
  console.log("express-service listening on http://localhost:3001");
});

Service Presets

The 0.2.0 presets keep framework wiring in one place:

  • createNextBffService() returns authRoute, route, http, OAuth clients and normalized configuration.
  • createExpressApiService() returns traceMiddleware, route-level auth, errorHandler, http and OAuth clients.
  • createInternalServiceClient() binds a source service, target service and base URL to an HTTP client.
const profiles = createInternalServiceClient({
  targetService: "core-auth-service",
  baseUrl: process.env.CORE_AUTH_SERVICE_BASE_URL!
});

const profile = await profiles.getJson("/v1/profiles/user-1", {
  operation: "profile.get"
});

All existing low-level factories remain available when a service needs custom composition.

Environment Preset

For most BFFs and services, start with these variables.

SERVICE_NAME=quickfade-bff-web
SERVICE_VERSION=1.0.0
SERVICE_HTTP_TIMEOUT_MS=10000
SERVICE_HTTP_RETRY_COUNT=1
SERVICE_HTTP_RETRY_DELAY_MS=100

CORE_OAUTH_GATEWAY_BASE_URL=https://core-oauth-gateway.onrender.com
CORE_OAUTH_ISSUER_DISCOVERY_URL=https://core-oauth-gateway.onrender.com/.well-known/oauth-authorization-server
CORE_OAUTH_JWT_AUDIENCE=quickfade-bff-web
CORE_OAUTH_REQUIRED_SCOPES=cl:core:profile:read
CORE_OAUTH_OPAQUE_VALIDATION_MODE=introspect
CORE_OAUTH_ALLOWED_ALGORITHMS=RS256
CORE_OAUTH_CLOCK_SKEW_SECONDS=30
CORE_OAUTH_JWKS_CACHE_TTL_MS=300000
CORE_OAUTH_JWKS_TIMEOUT_MS=10000
CORE_OAUTH_JWKS_RETRY_COUNT=1
CORE_OAUTH_JWKS_RETRY_DELAY_MS=100
CORE_OAUTH_HTTP_TIMEOUT_MS=10000
CORE_OAUTH_HTTP_RETRY_COUNT=1
CORE_OAUTH_HTTP_RETRY_DELAY_MS=100

CORE_AUTH_SERVICE_BASE_URL=https://core-auth-service.onrender.com
CORE_AUTH_CLIENT_CREDENTIALS_TOKEN=replace-at-runtime

CORE_OAUTH_REQUIRED_SCOPES may contain one or more scopes separated by spaces or commas.

Discovery is the recommended JWT configuration. The metadata response must contain valid issuer and jwks_uri values. For gateways without discovery, configure both values directly:

CORE_OAUTH_GATEWAY_CERTS_URL=https://core-oauth-gateway.onrender.com/oauth2/v1/certs
CORE_OAUTH_GATEWAY_ISSUER=https://core-oauth-gateway.onrender.com

Scope Validation Example

Use route-level scopes when endpoints have different permissions. A route-level value overrides the preset from CORE_OAUTH_REQUIRED_SCOPES.

const service = createNextBffService();

export const GET = service.authRoute(handler, {
  requiredScopes: "cl:core:profile:read"
});

Express uses the same endpoint-level pattern:

app.get("/notes", service.auth({ requiredScopes: "cl:quickfade:notes:read" }), handler);

Behavior:

  • missing or invalid token returns 401
  • expired token returns 401
  • valid token without the required scope returns 403

Tracing

import {
  createTraceContext,
  getTraceRequestHeaders,
  getTraceResponseHeaders,
  traceContextToLogFields,
  traceContextToMetricLabels,
  runWithTraceContext
} from "@smb-tech/service-framework-js";

const traceContext = createTraceContext(request.headers, {
  serviceName: "core-auth-service",
  serviceVersion: "1.0.0"
});

await runWithTraceContext(traceContext, async () => {
  const downstreamHeaders = getTraceRequestHeaders(traceContext);
  const responseHeaders = getTraceResponseHeaders(traceContext);
  const logFields = traceContextToLogFields(traceContext);
  const metricLabels = traceContextToMetricLabels(traceContext);

  console.log({ downstreamHeaders, responseHeaders, logFields, metricLabels });
});

Supported headers:

  • X-Request-Id
  • X-B3-TraceId
  • X-B3-SpanId
  • X-B3-ParentSpanId
  • X-Client-Channel
  • X-Client-Platform
  • X-Client-App
  • X-Client-Version
  • X-Service-Name
  • X-Service-Version

Logging And Redaction

import { createLogger, redactSensitiveData } from "@smb-tech/service-framework-js";

const logger = createLogger({
  contextName: "CoreAuthService",
  serviceName: "core-auth-service"
});

logger.info("Token request accepted", {
  client_id: "core-auth-service",
  client_assertion: "secret.jwt.value"
});

const safePayload = redactSensitiveData({
  authorization: "Bearer secret",
  cookie: "session=secret",
  nested: {
    p12Base64: "base64-secret",
    query: "https://api.test/path?access_token=secret"
  }
});

Sensitive values are deeply redacted from:

  • objects
  • arrays
  • Headers
  • URL
  • URLSearchParams
  • URL strings
  • Error instances
  • nested payloads

Sensitive keys include:

  • access_token
  • refresh_token
  • id_token
  • token
  • authorization
  • cookie
  • password
  • secret
  • client_secret
  • assertion
  • client_assertion
  • jwt
  • p12
  • pfx
  • private_key
  • api_key
  • apikey
  • x-api-key

REST Client Logging

import { createHttpClient } from "@smb-tech/service-framework-js";

const httpClient = createHttpClient({
  serviceName: "core-auth-service",
  timeoutMs: 10_000,
  retry: {
    retries: 1,
    retryDelayMs: 100,
    retryOnTimeout: true
  }
});

const token = await httpClient.postJson(
  "https://core-oauth-gateway.onrender.com/oauth2/v1/token",
  {
    grant_type: "client_credentials",
    scope: "cl:system:auth:internal"
  },
  {
    targetService: "core-oauth-gateway",
    operation: "oauth.token.client_credentials"
  }
);

Outbound logs include:

  • service_name
  • target_service
  • operation
  • method
  • url
  • status_code
  • duration_ms
  • response_time_ms
  • trace_id
  • request_id

For non-2xx responses, redacted response_headers, response_body and error_message are also logged.

Optional Observability Hooks

Metrics are completely optional. Without hooks, runtime behavior is unchanged. Hooks are vendor-neutral and may forward events to OpenTelemetry, Prometheus, Datadog or another application-owned collector.

const service = createNextBffService({
  onHttpClientMetric(metric) {
    telemetry.recordHttpDuration(metric.durationMs, {
      target_service: metric.targetService,
      operation: metric.operation,
      outcome: metric.outcome
    });
  },
  onTokenValidationMetric(metric) {
    telemetry.increment("oauth.token.validation", {
      token_type: metric.tokenType,
      outcome: metric.outcome
    });
  },
  onJwksRefresh(event) {
    telemetry.increment("oauth.jwks.refresh", { outcome: event.outcome });
  },
  onAuthFailure(event) {
    telemetry.increment("http.auth.failure", {
      framework: event.framework,
      reason: event.reason
    });
  }
});

Hook failures are isolated and never change HTTP, OAuth or authorization results. Events never contain bearer tokens, assertions, cookies, P12 content, passwords or private keys. Avoid using request IDs, trace IDs or raw paths as Prometheus labels because they create high cardinality.

OAuth Token Validation

Token type detection is automatic:

  • token without . is treated as opaque
  • token with . is treated as JWT

Opaque Token

const validator = new OAuthTokenValidator({
  opaqueValidationMode: "introspect",
  introspectToken: (token) => oauthGateway.introspectToken(token)
});

const result = await validator.validate("opaque-token");

JWT With JWKS

const validator = new OAuthTokenValidator({
  jwt: {
    jwksUrl: "https://core-oauth-gateway.onrender.com/oauth2/v1/certs",
    expectedIssuer: "https://core-oauth-gateway.onrender.com",
    expectedAudience: "quickfade-bff-web",
    allowedAlgorithms: ["RS256"],
    clockSkewSeconds: 30,
    cacheTtlMs: 300_000,
    timeoutMs: 10_000,
    retryCount: 1,
    retryDelayMs: 100
  }
});

const result = await validator.validate(jwt);

JWT With OAuth Discovery

const validator = new OAuthTokenValidator({
  jwt: {
    discoveryUrl: "https://core-oauth-gateway.onrender.com/.well-known/oauth-authorization-server",
    expectedAudience: "quickfade-bff-web",
    allowedAlgorithms: ["RS256"]
  }
});

The discovery document supplies issuer and jwks_uri. Metadata and JWKS responses are cached and fetched with bounded timeouts and retries.

JWT validation checks:

  • signature
  • kid
  • iss
  • aud
  • exp
  • nbf
  • allowed algorithms

alg=none is rejected.

OAuth Gateway Client

import { OAuthGatewayClient, createOAuthGatewayConfigFromEnv } from "@smb-tech/service-framework-js";

const oauthGateway = new OAuthGatewayClient(createOAuthGatewayConfigFromEnv());

await oauthGateway.tokenByClientCredentials({
  scope: "cl:system:auth:internal"
});

await oauthGateway.tokenByJwtBearer({
  assertion,
  scope: "cl:bff:web:profile:read"
});

await oauthGateway.tokenByAuthorizationCode({
  code,
  redirectUri,
  codeVerifier
});

await oauthGateway.tokenByRefreshToken({ refreshToken });
await oauthGateway.introspectToken(accessToken);
await oauthGateway.tokenInfo(accessToken);
await oauthGateway.revokeToken(accessToken);
await oauthGateway.userAuthorize({ oauth_key, user_jwt });
await oauthGateway.getJwks();

PKCS#12 / P12 Runtime Signing

Runtime signing uses PKCS#12/P12 Base64 key material and node-forge.

No JKS, Java, keytool, openssl, temporary files or child processes are required.

CORE_OAUTH_P12_BASE64=...
CORE_OAUTH_P12_PASSWORD_BASE64=...
CORE_OAUTH_P12_ALIAS=core-auth-service
CORE_OAUTH_CLIENT_ID=core-auth-service
CORE_OAUTH_JWT_AUDIENCE=https://core-oauth-gateway.onrender.com/oauth2/v1/token
CORE_OAUTH_KEY_ID=optional-kid
CORE_OAUTH_ASSERTION_TTL_SECONDS=300

Rules:

  • CORE_OAUTH_P12_BASE64 is the Base64-encoded .p12 or .pfx file.
  • CORE_OAUTH_P12_PASSWORD_BASE64 is the Base64-encoded P12 password.
  • CORE_OAUTH_P12_ALIAS must match the PKCS#12 key entry friendly name.
  • The loader fails if the alias does not exist.
  • The loader fails if multiple private key entries match the alias.
  • Private key material is never written to disk.
  • The resulting KeyObject is cached in memory.

Client Assertion

Use this for OAuth private_key_jwt client authentication.

import { createOAuthSignerFromEnv } from "@smb-tech/service-framework-js";

const signer = createOAuthSignerFromEnv();

const clientAssertion = await signer.signClientAssertion({
  jwtId: crypto.randomUUID(),
  ttlSeconds: 300
});

JWT structure:

{
  "iss": "core-auth-service",
  "sub": "core-auth-service",
  "aud": "https://core-oauth-gateway.onrender.com/oauth2/v1/token",
  "iat": 1700000000,
  "exp": 1700000300,
  "jti": "uuid"
}

JWT Bearer Assertion

Use this for the grant:

urn:ietf:params:oauth:grant-type:jwt-bearer

Recommended usage with explicit claims:

const assertion = await signer.signJwtBearerAssertion({
  claims: {
    user_id: "user-1",
    tenant_id: "quickfade",
    account_id: "acc-123"
  },
  scopes: ["cl:bff:web:profile:read"],
  ttlSeconds: 300
});

Legacy shortcut, still supported:

const assertion = await signer.signJwtBearerAssertion({
  userId: "user-1",
  scopes: "cl:bff:web:profile:read"
});

Reserved claims cannot be overridden from claims:

  • iss
  • sub
  • aud
  • iat
  • exp
  • jti

Standard Errors

import { AppError, UnauthorizedError, ForbiddenError, presentError } from "@smb-tech/service-framework-js";

throw new ForbiddenError("Missing required scope: cl:core:profile:read");

Error response:

{
  "error": "forbidden",
  "error_description": "Missing required scope: cl:core:profile:read"
}

Provided errors:

  • InvalidRequestError
  • UnauthorizedError
  • ForbiddenError
  • NotFoundError
  • ConflictError
  • UnprocessableEntityError
  • ExternalServiceError
  • InternalServerError

Stack traces are not exposed by default.

Fastify Adapter

import Fastify from "fastify";
import { fastifyErrorHandler, fastifyTracePlugin } from "@smb-tech/service-framework-js";

const app = Fastify();

await app.register(
  fastifyTracePlugin({
    serviceName: "core-messaging-engine",
    serviceVersion: "1.0.0"
  })
);

app.get("/health", async () => ({
  status: "ok"
}));

app.setErrorHandler(fastifyErrorHandler());

Running Examples

The Git repository includes runnable examples. They are development references and are not required at runtime by the published npm package.

  • examples/next-bff
  • examples/express-service
  • examples/fastify-service
  • examples/oauth-validation
  • examples/client-credentials
  • examples/jwt-bearer-assertion
  • examples/rest-client-logging

Demo mode:

npm run example:next
npm run example:express

The Next and Express examples can run without environment variables in demo mode and accept:

Authorization: Bearer demo-token

Express demo:

npm run example:express
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:3001/notes

Run a script with a real .env.local file:

node --env-file=examples/next-bff/.env.local --import tsx examples/next-bff/route.ts

CLI Scripts

Version 0.2.0 publishes a formal CLI. It can run through npx without cloning the repository:

npx @smb-tech/service-framework-js --help

Create a client assertion:

npx @smb-tech/service-framework-js client-assertion \
  --client-id core-auth-service \
  --p12 ./client.p12 \
  --p12-password secret \
  --p12-alias core-auth-service \
  --aud https://core-oauth-gateway.onrender.com/oauth2/v1/token

Create a JWT bearer assertion with repeated claims:

npx @smb-tech/service-framework-js jwt-bearer-assertion \
  --client-id core-auth-service \
  --claim user_id=user-1 \
  --claim tenant_id=quickfade \
  --p12-base64 "$CORE_OAUTH_P12_BASE64" \
  --p12-password-base64 "$CORE_OAUTH_P12_PASSWORD_BASE64" \
  --p12-alias "$CORE_OAUTH_P12_ALIAS" \
  --aud https://core-oauth-gateway.onrender.com/oauth2/v1/token \
  --scope cl:bff:web:profile:read

Create a JWT bearer assertion with JSON claims:

npx @smb-tech/service-framework-js jwt-bearer-assertion \
  --client-id core-auth-service \
  --claims-json '{"user_id":"user-1","tenant_id":"quickfade"}' \
  --p12-base64 "$CORE_OAUTH_P12_BASE64" \
  --p12-password-base64 "$CORE_OAUTH_P12_PASSWORD_BASE64" \
  --p12-alias "$CORE_OAUTH_P12_ALIAS" \
  --aud https://core-oauth-gateway.onrender.com/oauth2/v1/token \
  --scope cl:bff:web:profile:read

Encode local files and passwords:

npx @smb-tech/service-framework-js p12-base64 --p12 ./client.p12
npx @smb-tech/service-framework-js password-base64 --password secret

The repository keeps the equivalent npm run client:assertion, npm run jwt-bearer:assertion, npm run p12:base64 and npm run password:base64 scripts for maintainers.

Supported flags:

  • --client-id
  • --user-id
  • --claim
  • --claims-json
  • --p12
  • --pfx
  • --p12-base64
  • --p12-password
  • --p12-password-base64
  • --p12-alias
  • --aud
  • --scope
  • --ttl-seconds

Security Guarantees

The package is designed to reduce accidental production leaks:

  • tokens are not logged in full
  • assertions are not logged in full
  • P12/PFX content is not logged
  • passwords are not logged
  • cookies are not logged
  • private keys are never written to disk
  • outbound HTTP has timeouts by default
  • JWT alg=none is rejected
  • JWT algorithms are allowlisted
  • JWT iss, aud, exp and nbf are validated
  • stack traces are not exposed by default

Production Notes

For Render, Docker, ECS, Lambda and local environments, prefer the P12 path:

const signer = createOAuthSignerFromEnv();

This path only requires Node.js and node-forge. It does not require:

  • Java
  • keytool
  • openssl
  • temp files
  • child_process

Package Exports

Root import:

import {
  createTraceContext,
  createHttpClient,
  OAuthTokenValidator,
  ScopeGuard
} from "@smb-tech/service-framework-js";

Subpath imports are also available:

import { createHttpClient } from "@smb-tech/service-framework-js/http";
import { OAuthTokenValidator } from "@smb-tech/service-framework-js/oauth";
import { createOAuthSignerFromEnv } from "@smb-tech/service-framework-js/security";
import { createNextBffService } from "@smb-tech/service-framework-js/presets";
import type { ObservabilityHooks } from "@smb-tech/service-framework-js/observability";
import { nextAuthRouteHandler } from "@smb-tech/service-framework-js/framework/next";
import { expressAuthMiddleware } from "@smb-tech/service-framework-js/framework/express";

Source Maps

Source maps are intentionally not published in 0.2.0. The package publishes JavaScript output and TypeScript declarations, but omits .map files to keep the npm tarball smaller and avoid shipping TypeScript source content in public npm artifacts.

Development

npm install
npm run lint
npm run typecheck
npm run test
npm run test:package
npm run build

npm run test:package builds the package and verifies:

  • ESM import from dist/index.js
  • package self-reference imports such as @smb-tech/service-framework-js/oauth
  • CommonJS require(...)
  • TypeScript declaration resolution from the generated .d.ts files
  • installation of the real npm pack tarball in a temporary consumer project
  • ESM, CommonJS, subpath, declaration and CLI resolution from that installed tarball

Support Policy

This package is maintained for SMB Tech service development. API changes should be coordinated with SMB Tech service owners before publishing new versions.