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.3.0

Published

Reusable service framework utilities for SMB Tech Node.js services

Readme

@smb-tech/service-framework-js

Reusable TypeScript utilities for SMB Tech Node.js services.

Intended use: this package is maintained for SMB Tech and QuickFade services. It is published to npm for controlled reuse across BFFs, APIs, microservices, gateways, and internal services. It is not a general-purpose community framework.

Overview

@smb-tech/service-framework-js centralizes cross-cutting service behavior:

  • request tracing and B3-compatible propagation
  • request-scoped MDC backed by one AsyncLocalStorage
  • structured logging through @smb-tech/logger-core and @smb-tech/logger-node
  • deep sensitive-data redaction
  • outbound HTTP clients with timeouts, retries, response limits, and REST logs
  • Bearer token extraction and scope enforcement
  • opaque token validation through introspection or token info
  • JWT validation through static JWKS, remote JWKS, or OAuth discovery
  • OAuth Gateway operations
  • RS256 Client Assertion and JWT Bearer Assertion signing
  • pure Node.js PKCS#12/P12 loading with alias selection and key rotation
  • standard application errors and safe error presentation
  • Next.js, Express, and Fastify adapters
  • optional telemetry hooks that never change request outcomes

The core package has no framework dependency. Next.js, Express, and Fastify are optional peers.

Contents

Requirements

| Requirement | Supported value | | -------------- | ------------------------------------ | | Node.js | >=20 | | TypeScript | Supported; declarations are included | | Module formats | ESM and CommonJS | | Next.js | >=14, Node.js runtime only | | Express | >=4 or >=5 | | Fastify | >=4 or >=5 |

The package uses the native Node.js fetch, Request, Response, Headers, URL, and AbortController APIs.

Runtime P12 signing uses node-forge and Node.js crypto. It does not require Java, keytool, openssl, temporary files, or child processes, so it works in Docker, ECS, Lambda, Render native runtimes, and local Node.js environments.

Installation

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

Install only the framework used by the consuming application:

npm install next
# or
npm install express
# or
npm install fastify

Quick Start

1. Minimal Next.js API Route

This preset provides tracing, structured logging, standardized errors, and an HTTP client without requiring OAuth configuration.

SERVICE_NAME=example-next-api
SERVICE_VERSION=1.0.0
LOGGER_TIMEZONE=America/Santiago
// app/api/health/route.ts
import { createNextApiService } from "@smb-tech/service-framework-js";

export const runtime = "nodejs";

const service = createNextApiService();

export const GET = service.route(async () => {
  service.logger.info("Health check completed", {
    operation: "health.read",
    tags: ["health"]
  });

  return Response.json({ status: "ok" });
});

The route wrapper creates or accepts trace headers, initializes logging once, catches thrown values, returns the standard error body, adds trace response headers, and logs request completion.

2. Protected Next.js BFF Route

SERVICE_NAME=quickfade-bff-web
SERVICE_VERSION=1.0.0

CORE_OAUTH_GATEWAY_BASE_URL=https://oauth.example.com
CORE_OAUTH_GATEWAY_CERTS_URL=https://oauth.example.com/oauth2/v1/certs
CORE_OAUTH_GATEWAY_ISSUER=https://oauth.example.com
CORE_OAUTH_JWT_AUDIENCE=quickfade-bff-web
CORE_OAUTH_ALLOWED_ALGORITHMS=RS256
// app/api/profile/route.ts
import { createNextBffService } from "@smb-tech/service-framework-js";

export const runtime = "nodejs";

const service = createNextBffService();

export const GET = service.authRoute(
  async (_request, { auth }) => {
    return Response.json({
      subject: auth.subject,
      scopes: auth.scopes
    });
  },
  {
    requiredScopes: "cl:core:profile:read"
  }
);

requiredScopes is evaluated per route. A missing or invalid token returns 401; a valid token without the required scope returns 403.

3. Protected Express API

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

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

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

app.get("/profiles/:id", service.auth({ requiredScopes: "cl:core:profile:read" }), (req, res) => {
  res.json({
    subject: req.auth?.subject,
    profileId: req.params.id
  });
});

app.use(service.errorHandler);
app.listen(3000);

Use the same OAuth environment variables shown in the protected Next.js example.

Choose a Service Preset

| Preset | Use when | Includes | | ------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------- | | createNextApiService() | A Next.js API does not validate Bearer tokens | logger, tracing, route wrapper, error policy, HTTP client, lifecycle | | createNextBffService() | A Next.js BFF validates opaque or JWT access tokens | everything above plus OAuth client, token validator, authRoute() | | createExpressApiService() | An Express API exposes protected routes | logger, tracing middleware, auth middleware factory, error handler, HTTP client | | createInternalServiceClient() | A service calls another internal service | base URL resolution, trace propagation, REST logging, timeout/retry controls |

Fastify currently uses the lower-level fastifyTracePlugin() and fastifyErrorHandler() adapters; there is no Fastify service preset.

Basic Usage

Tracing

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
import {
  createTraceContext,
  getTraceRequestHeaders,
  getTraceResponseHeaders,
  runWithTraceContext
} from "@smb-tech/service-framework-js/tracing";

const context = createTraceContext(request.headers, {
  serviceName: "profile-service",
  serviceVersion: "1.0.0"
});

await runWithTraceContext(context, async () => {
  const downstreamHeaders = getTraceRequestHeaders();
  const responseHeaders = getTraceResponseHeaders();
});

Invalid incoming trace/span IDs are replaced. Missing values are generated. By default, an incoming span becomes parentSpanId and a new child span is created. Set createChildSpan: false to reuse a valid incoming span.

Request-Scoped MDC

import { getMdc, setMdcField, setMdcFields, withMdc } from "@smb-tech/service-framework-js";

setMdcFields({
  tenantId: "tenant-1",
  operation: "profile.read"
});

await withMdc({ useCase: "GetProfile" }, async () => {
  console.log(getMdc());
});

setMdcField("cacheHit", true);

Framework-owned fields such as requestId, traceId, and serviceName cannot be overwritten. Sensitive MDC key names are rejected. Custom MDC is limited to 32 fields, 64 characters per key, and 256 characters per value.

Logging

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

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

logger.info("Profile loaded", {
  operation: "profile.read",
  target_service: "profile-store",
  duration_ms: 24,
  tags: ["profile", "read"]
});

try {
  throw new Error("Database unavailable");
} catch (error) {
  logger.error("Profile loading failed", {
    operation: "profile.read",
    error
  });
}

Use meta.error only for exceptions. It is normalized into the canonical exception field and may include native Error, DOMException, AggregateError, AppError, cause chains, primitives, or non-standard thrown values. Use meta.tags for domain tags.

HTTP Client

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

const http = createHttpClient({
  serviceName: "quickfade-bff-web",
  targetService: "core-auth-service",
  profile: "internal",
  timeoutMs: 10_000,
  retry: {
    retries: 1,
    retryDelayMs: 100
  }
});

const profile = await http.getJson<{ display_name: string }>(
  "https://core-auth-service.example.com/profiles/user-1",
  {
    operation: "profile.get"
  }
);

getJson(), postJson(), and postForm() throw normalized errors for non-2xx responses. request() returns the raw Response, including non-2xx responses.

Internal Service Client

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

const authService = createInternalServiceClient({
  serviceName: "quickfade-bff-web",
  targetService: "core-auth-service",
  baseUrl: "https://core-auth-service.example.com"
});

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

Relative paths resolve against baseUrl. Absolute URLs remain absolute.

Bearer Tokens and Scopes

import { extractBearerToken, hasAllScopes, hasAnyScope, requireScopes } from "@smb-tech/service-framework-js";

const bearer = extractBearerToken(request.headers.get("authorization"));
const validated = await tokenValidator.validate(bearer);

requireScopes(validated, ["cl:core:profile:read"]);

const canReadOrWrite = hasAnyScope(validated, ["cl:core:profile:read", "cl:core:profile:write"]);

const canManage = hasAllScopes(validated, ["cl:core:profile:read", "cl:core:profile:write"]);

Advanced Usage

Opaque and JWT Token Validation

Token classification follows the package contract:

  • token contains .: JWT
  • token does not contain .: opaque
import { createOAuthTokenValidatorFromEnv } from "@smb-tech/service-framework-js";

const validator = createOAuthTokenValidatorFromEnv();
const token = await validator.validate(accessToken);

console.log(token.tokenType, token.subject, token.clientId, token.scopes);

Opaque tokens use CORE_OAUTH_OPAQUE_VALIDATION_MODE=introspect|tokeninfo. JWTs validate:

  • alg against an allowlist; none is rejected
  • typ when configured
  • kid
  • signature
  • exp
  • nbf when present
  • iss when configured
  • aud when configured

The normalized result is:

type ValidatedToken = {
  active: boolean;
  tokenType: "opaque" | "jwt";
  subject?: string;
  clientId?: string;
  scopes: string[];
  claims: Record<string, unknown>;
  expiresAt?: number;
  issuedAt?: number;
  jwtId?: string;
};

Use claimNormalizer, jwtClaimNormalizer, or opaqueClaimNormalizer when provider claims do not match the built-in normalization rules.

OAuth Gateway Client

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

const oauth = createOAuthGatewayClientFromEnv({
  enableAssertionSigning: true
});

const serviceToken = await oauth.tokenByClientCredentials({
  scope: "cl:core:profile:read"
});

const delegatedToken = await oauth.tokenByJwtBearer({
  claims: {
    customer_id: "customer-123",
    tenant: {
      id: "tenant-1",
      channel: "web"
    }
  },
  assertionScopes: ["cl:bff:web:profile:read"],
  requestedScopes: ["cl:bff:web:profile:read"]
});

Additional methods:

await oauth.tokenByAuthorizationCode({ code, redirectUri, codeVerifier });
await oauth.tokenByRefreshToken({ refreshToken, scope });
await oauth.introspectToken(accessToken);
await oauth.tokenInfo(accessToken);
await oauth.revokeToken(accessToken);
await oauth.userAuthorize(payload);
await oauth.getJwks();

assertionScopes becomes the signed JWT scope claim. requestedScopes becomes the OAuth form scope parameter. The deprecated scope field on tokenByJwtBearer() aliases requestedScopes. Do not provide both.

The client still accepts a pre-signed assertion, but it cannot be combined with automatic generation fields.

P12 Client Assertions

CORE_OAUTH_GATEWAY_TOKEN_URL=https://oauth.example.com/oauth2/v1/token
CORE_OAUTH_CLIENT_ID=replace-with-client-id
CORE_OAUTH_ASSERTION_AUDIENCE=https://oauth.example.com/oauth2/v1/token
CORE_OAUTH_ASSERTION_TTL_SECONDS=300

CORE_OAUTH_P12_BASE64=replace-with-base64-p12
CORE_OAUTH_P12_PASSWORD_BASE64=replace-with-base64-password
CORE_OAUTH_P12_ALIAS=replace-with-friendly-name

The P12 password is Base64-encoded and decoded before use. The alias must match exactly one private key entry. The associated certificate must contain the matching RSA public key.

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

const signer = createOAuthSignerFromEnv();

const clientAssertion = await signer.signClientAssertion();
const jwtBearerAssertion = await signer.signJwtBearerAssertion({
  claims: {
    account_id: "account-1"
  },
  scopes: ["cl:bff:web:profile:read"]
});

Generated assertions always use RS256 and contain:

iss, sub, aud, iat, exp, jti

JWT bearer custom claims are plain JSON data. They cannot override iss, sub, aud, iat, exp, or jti.

Claim limits:

| Limit | Value | | ----------------------- | ------------- | | Properties per object | 32 | | Nesting depth | 5 | | Items per array | 100 | | Claim name length | 128 | | Total serialized claims | 8 KiB | | Assertion TTL | 1-600 seconds |

Unsafe keys (__proto__, prototype, constructor), accessors, cycles, non-finite numbers, and non-JSON values are rejected.

Signing-Key Cache and Rotation

const current = await oauth.getAssertionSigningKeyInfo();

// Clear the cache. The next signing request reloads the configured provider.
oauth.reloadAssertionSigningKey();

// Validate the candidate before atomically replacing the active key.
const rotated = await oauth.rotateAssertionSigningKey();

console.log(current.keyId, rotated.keyId);

The cache deduplicates concurrent loads. Rotation validates the P12, alias, certificate, RSA pair, fingerprint, and derived kid before activation. A failed rotation preserves the active key.

Environment-backed rotation only sees changes available to the running process. Platforms that inject environment variables only at startup require a restart or redeploy.

Client Credentials Token Cache

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

const tokens = new ClientCredentialsClient({
  oauthGatewayClient: oauth,
  scope: "cl:core:profile:read",
  cacheSkewSeconds: 30
});

const accessToken = await tokens.getAccessToken();

Concurrent cache misses share one request. The default token lifetime is 300 seconds when the token response omits expires_in; the default early-refresh skew is 30 seconds.

Secure Remote JWKS

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

const resolver = createRemoteJwksResolver({
  allowedHosts: ["identity.example.com", "*.trusted.example.com"],
  allowedAlgorithms: ["RS256"],
  allowedKeyTypes: ["RSA"]
});

const jwks = await resolver.resolve("https://identity.example.com/.well-known/jwks.json");

*.trusted.example.com matches subdomains only, not trusted.example.com. Omit allowedHosts or use ["*"] to permit any public host. HTTPS, port validation, private-network blocking, response limits, and public-key-only validation remain active.

Use blockPrivateNetworks: false only for a deliberately trusted private JWKS endpoint.

Fastify

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

const app = Fastify();

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

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

await app.listen({ port: 3000 });

Fastify has tracing and error adapters, but no built-in Bearer/scope adapter. Compose the core extractBearerToken(), OAuthTokenValidator, and ScopeGuard APIs in a hook when authentication is required.

Configuration

Recommended Environment Profiles

Minimal Next API:

SERVICE_NAME=example-next-api
SERVICE_VERSION=1.0.0

JWT validation:

SERVICE_NAME=quickfade-bff-web
CORE_OAUTH_GATEWAY_BASE_URL=https://oauth.example.com
CORE_OAUTH_GATEWAY_CERTS_URL=https://oauth.example.com/oauth2/v1/certs
CORE_OAUTH_GATEWAY_ISSUER=https://oauth.example.com
CORE_OAUTH_JWT_AUDIENCE=quickfade-bff-web

OAuth discovery instead of explicit JWKS and issuer:

CORE_OAUTH_GATEWAY_BASE_URL=https://oauth.example.com
CORE_OAUTH_ISSUER_DISCOVERY_URL=https://oauth.example.com/.well-known/oauth-authorization-server
CORE_OAUTH_JWT_AUDIENCE=quickfade-bff-web

Opaque-only validation:

const validator = createOAuthTokenValidatorFromEnv({
  requireJwt: false,
  requireIssuer: false,
  requireAudience: false
});

Automatic assertion signing:

CORE_OAUTH_GATEWAY_TOKEN_URL=https://oauth.example.com/oauth2/v1/token
CORE_OAUTH_CLIENT_ID=replace-with-client-id
CORE_OAUTH_P12_BASE64=replace-with-secret-manager-value
CORE_OAUTH_P12_PASSWORD_BASE64=replace-with-secret-manager-value
CORE_OAUTH_P12_ALIAS=replace-with-friendly-name

Programmatic Factory Options

createOAuthGatewayClientFromEnv(options)

| Option | Type | Default | Purpose | | -------------------------- | --------------------------------- | ----------------- | ---------------------------------------------- | | env | NodeJS.ProcessEnv | process.env | Alternate environment object; useful for tests | | serviceNameEnv | string | SERVICE_NAME | Alternate service-name variable | | serviceVersionEnv | string | SERVICE_VERSION | Alternate version variable | | requireBaseUrlOrTokenUrl | boolean | true | Require a Gateway base URL or token URL | | enableAssertionSigning | boolean | false | Build an environment-backed P12 signer | | assertionSigner | OAuthAssertionSigner | none | Inject a custom signer | | logger | Logger | generated logger | Logging adapter | | fetcher | typeof fetch | global fetch | Custom fetch implementation or test mock | | traceContextProvider | () => TraceContext \| undefined | current context | Custom trace source | | onHttpClientMetric | hook | none | Outbound HTTP telemetry |

createOAuthTokenValidatorFromEnv(options)

Includes every OAuth Gateway option plus:

| Option | Type | Default | Purpose | | ------------------------- | -------------------- | --------------------- | -------------------------------------------- | | oauthGatewayClient | OAuthGatewayClient | created from env | Reuse an existing client | | requireJwt | boolean | true | Require JWKS/discovery configuration | | requireIssuer | boolean | value of requireJwt | Require an expected issuer without discovery | | requireAudience | boolean | value of requireJwt | Require an expected audience | | onTokenValidationMetric | hook | none | Token validation telemetry | | onJwksRefresh | hook | none | JWKS refresh telemetry | | onAuthFailure | hook | none | Next/Express auth-failure telemetry |

Service Presets

| Option | Applies to | Default | Purpose | | ----------------------------- | -------------------- | ----------------------- | -------------------------------------- | | logger | all presets | generated logger | Inject a shared logger | | initializeLogger | Next/Express presets | true | Initialize runtime logging in adapters | | loggerInitializationOptions | Next/Express presets | environment defaults | Code-based logger setup | | errorPolicy | Next/Express presets | standard policy | Override severity/reporting policy | | contextName | Next API | NextApiService | Logger class/context name | | httpClient | Next API | environment HTTP config | Override client fields |

Route-level options override preset defaults. For example, setting requiredScopes on one route does not affect another route.

Low-Level Option Reference

CreateTraceContextOptions

| Option | Type | Default | Purpose | | ----------------------- | --------- | ------------------------ | ------------------------------------------------------ | | serviceName | string | incoming header or unset | Local service identity | | serviceVersion | string | incoming header or unset | Local service version | | defaultClientChannel | string | "unknown" | Fallback client channel | | defaultClientPlatform | string | "unknown" | Fallback client platform | | defaultClientApp | string | "unknown" | Fallback client application | | defaultClientVersion | string | "unknown" | Fallback client version | | createChildSpan | boolean | true | Create a new span and preserve incoming span as parent |

HttpClientConfig

| Option | Type | Default | Purpose | | ----------------------- | ---------------------- | ----------------------------- | -------------------------------- | | serviceName | string | required | Calling service | | profile | internal \| external | none | Apply profile defaults | | logger | Logger | generated HttpClient logger | REST lifecycle logs | | traceContextProvider | function | current request context | Explicit trace provider | | fetcher | typeof fetch | global fetch | Custom transport/test mock | | timeoutMs | number | profile or 10000 | Per-attempt timeout | | retry | HttpRetryConfig | general retry defaults | Retry policy | | defaultHeaders | HeadersInit | none | Headers applied to every request | | targetService | string | serviceName | Default log/metric target | | propagateTraceHeaders | boolean | profile or true | Add trace headers | | maxResponseBodyBytes | number | profile or unlimited | Buffering limit | | responseBodyLogging | policy | profile or errors | never, errors, or always | | onHttpClientMetric | hook | none | Attempt-level metric callback |

Every request accepts profile, headers, targetService, operation, timeoutMs, retry, traceContext, propagateTraceHeaders, maxResponseBodyBytes, and responseBodyLogging overrides. JSON/form methods also accept parseJson; it defaults to true.

HttpRetryConfig fields:

| Option | Type | Default | | ---------------- | -------------------- | ----------------------------------- | | retries | non-negative integer | 0 | | retryDelayMs | non-negative integer | 100 | | retryStatuses | HTTP status array | 408, 425, 429, 500, 502, 503, 504 | | retryOnTimeout | boolean | true |

OAuthGatewayClientConfig

| Option | Type | Default | Purpose | | ----------------------- | ---------------------- | ---------------------- | ------------------------------------------ | | serviceName | string | "unknown-service" | Calling service | | baseUrl | URL string | none | Base for /oauth2/v1/* fallbacks | | tokenUrl | URL string | derived from baseUrl | Token endpoint | | introspectionUrl | URL string | derived | Introspection endpoint | | tokenInfoUrl | URL string | derived | Token info endpoint | | revokeUrl | URL string | derived | Revocation endpoint | | userAuthorizeUrl | URL string | derived | User authorization endpoint | | jwksUrl | URL string | derived | Certificates/JWKS endpoint | | clientId | string | unset | Default OAuth client | | clientSecret | string | unset | Secret authentication fallback | | assertionSigner | OAuthAssertionSigner | unset | Recommended automatic signer | | clientAssertionSigner | legacy signer | unset | Deprecated Client Credentials-only adapter | | logger | Logger | generated logger | HTTP/signing logs | | traceContextProvider | function | current context | Trace source | | fetcher | typeof fetch | global fetch | Custom transport/test mock | | timeoutMs | number | HTTP client default | Request timeout | | retry | HttpRetryConfig | HTTP client default | Retry policy | | onHttpClientMetric | hook | none | OAuth HTTP telemetry |

OAuthTokenValidatorConfig

| Option | Type | Default | Purpose | | ------------------------- | ------------------------- | ---------------------- | --------------------------- | | opaqueValidationMode | introspect \| tokeninfo | introspect | Opaque-token strategy | | introspectToken | async function | none | Introspection callback | | tokenInfo | async function | none | Token-info callback | | jwt | JwksTokenVerifierConfig | none | Build a JWT verifier | | jwksTokenVerifier | JwksTokenVerifier | none | Inject an existing verifier | | claimNormalizer | function | built-in normalization | Shared claim mapper | | jwtClaimNormalizer | function | shared mapper | JWT-specific mapper | | opaqueClaimNormalizer | function | shared mapper | Opaque-specific mapper | | onTokenValidationMetric | hook | none | Validation telemetry |

JwksTokenVerifierConfig

| Option | Type | Default | Purpose | | ------------------- | ---------------------- | --------------------------------- | ----------------------------- | | jwksUrl | string | none | Direct JWKS URI | | discoveryUrl | string | none | OAuth metadata URI | | discoveryClient | OAuthDiscoveryClient | built from URL | Inject discovery client | | jwks | OAuthJwks | none | Static in-memory JWKS | | expectedIssuer | string | discovery issuer or unchecked | Exact issuer | | expectedAudience | string/string[] | unchecked | Accepted audiences | | expectedType | string/string[] | unchecked | Accepted JWT typ | | allowedAlgorithms | string[] | ["RS256"] | Algorithm allowlist | | cacheTtlMs | number | 300000 | Discovery/JWKS cache | | fetcher | typeof fetch | global fetch | Custom transport | | clockSkewSeconds | number | 0 direct; env factory uses 30 | Temporal tolerance | | timeoutMs | number | 10000 | Remote timeout | | retryCount | number | 1 | Retries after initial attempt | | retryDelayMs | number | 100 | Fixed retry delay | | claimNormalizer | function | built-in | JWT claim mapper | | onJwksRefresh | hook | none | Refresh telemetry | | remoteJwks | resolver config | secure defaults | Remote policy overrides | | jwksResolver | resolver instance | generated | Inject a resolver |

RemoteJwksResolverConfig

| Option | Type | Default | | ------------------------ | -------------- | ----------------------------------------- | | serviceName | string | SERVICE_NAME or "service-framework" | | targetService | string | "remote-jwks" | | logger | Logger | generated HTTP logger | | httpClient | HttpClient | external-profile client | | fetcher | typeof fetch | global fetch | | resolveHostname | async function | DNS lookup | | allowedHosts | string[] | any public host | | allowedPorts | number[] | HTTPS 443; otherwise 80,443 | | allowedKeyTypes | string[] | RSA, EC, OKP; verifier narrows to RSA | | allowedAlgorithms | string[] | unrestricted at resolver level | | requireHttps | boolean | true | | blockPrivateNetworks | boolean | true | | requireJsonContentType | boolean | true | | cacheTtlMs | number | 300000 | | timeoutMs | number | 5000 | | retryCount | number | 1 | | retryDelayMs | number | 100 | | maxResponseBytes | number | 65536 | | maxKeys | number | 20 | | maxRedirects | number | 3 | | onJwksRefresh | hook | none |

OAuthSignerEnvOptions

| Option | Type | Default | | ---------------------- | ------------------- | ---------------------------------- | | env | NodeJS.ProcessEnv | process.env | | p12Base64Env | string | CORE_OAUTH_P12_BASE64 | | p12PasswordBase64Env | string | CORE_OAUTH_P12_PASSWORD_BASE64 | | p12AliasEnv | string | CORE_OAUTH_P12_ALIAS | | clientIdEnv | string | CORE_OAUTH_CLIENT_ID | | audienceEnv | string | standard audience fallback chain | | ttlSecondsEnv | string | CORE_OAUTH_ASSERTION_TTL_SECONDS | | keyIdEnv | string | CORE_OAUTH_KEY_ID | | defaultTtlSeconds | number | 300 | | defaultAudience | string | unset | | logger | Logger | none |

Framework Adapter Options

All trace adapters accept CreateTraceContextOptions.

| Adapter | Additional options | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | nextRouteHandler | logger, initializeLogger=true, loggerInitializationOptions, errorPolicy | | nextAuthRouteHandler | above plus required tokenValidator, requiredScopes, authorizationHeaderName="authorization", onAuthFailure | | createNextTraceMiddleware | initializeLogger=true, loggerInitializationOptions | | expressTraceMiddleware | logger, initializeLogger=true, loggerInitializationOptions, logRequestCompletion=true | | expressAuthMiddleware | required tokenValidator, requiredScopes, authorizationHeaderName="authorization", serviceName, onAuthFailure | | expressErrorHandler | logger, logErrors, errorPolicy | | fastifyTracePlugin | initializeLogger=true, loggerInitializationOptions | | fastifyErrorHandler | logger, logErrors, errorPolicy |

Redaction Options

| Option | Type | Default | | -------------------------- | -------- | ---------------------------------------- | | redactionText | string | [REDACTED] | | sensitiveFields | string[] | mandatory built-in fields plus additions | | sensitiveQueryParameters | string[] | mandatory built-ins plus additions | | maxStringLength | number | 4000 |

LoggerInitializationOptions

Code options override environment variables:

| Option | Type | Default | | ------------------------- | ----------------------- | -------------------------------------------- | | level | logger level | LOG_LEVEL, then INFO | | sampleRate | number | LOGGER_SAMPLE_RATE, then 1 | | timezone | IANA timezone | LOGGER_TIMEZONE, then UTC | | errorStackEnabled | boolean | LOGGER_ERROR_STACK_ENABLED === "true" | | sensitiveKeys | logger mask rules | mandatory defaults | | redactPlaceholder | string | [REDACTED] | | maxRedactionInputLength | number | logger-core default 4096 | | sampledLevels | log levels | logger-core sampled levels | | internalErrorHandler | function | logger-core safe stderr handler | | mode | sync \| async | LOGGER_MODE, then async | | flushIntervalMs | number | LOGGER_FLUSH_INTERVAL_MS, then 10 | | maxQueueSize | number | LOGGER_MAX_QUEUE_SIZE, then 10000 | | overflowStrategy | drop \| sync-fallback | environment, then sync-fallback | | shutdownTimeoutMs | number | LOGGER_SHUTDOWN_TIMEOUT_MS, then 2000 | | metricsEnabled | boolean | LOGGER_INTERNAL_METRICS_ENABLED === "true" |

Configuration Reference

List values accept comma-separated or whitespace-separated text. Boolean values accept true/false, 1/0, yes/no, and y/n.

Service and HTTP

| Variable | Type | Required | Default | Example / notes | | -------------------------------------- | --------------------------- | ------------------------------- | -------------------------------------- | -------------------------------------------- | | SERVICE_NAME | string | Yes for presets/env HTTP config | none | quickfade-bff-web | | SERVICE_VERSION | string | No | unset | 1.0.0 | | SERVICE_HTTP_PROFILE | internal \| external | No | internal | Selects secure HTTP defaults | | SERVICE_HTTP_TIMEOUT_MS | number | No | internal 10000; external 5000 | Positive integer | | SERVICE_HTTP_PROPAGATE_TRACE_HEADERS | boolean | No | internal true; external false | Avoid propagation to untrusted external APIs | | SERVICE_HTTP_MAX_RESPONSE_BODY_BYTES | number | No | internal 5242880; external 1048576 | Positive integer | | SERVICE_HTTP_RESPONSE_BODY_LOGGING | never \| errors \| always | No | internal errors; external never | Bodies are redacted before logging | | SERVICE_HTTP_RETRY_COUNT | number | No | 0 | Non-negative integer | | SERVICE_HTTP_RETRY_DELAY_MS | number | No | 100 | Fixed delay; no exponential backoff |

OAuth Gateway

| Variable | Type | Required | Default | Example / notes | | --------------------------------------- | ------ | -------------------------------------- | --------------------------- | --------------------------------------- | | CORE_OAUTH_GATEWAY_BASE_URL | URL | One base/token URL required by default | none | https://oauth.example.com | | CORE_OAUTH_GATEWAY_TOKEN_URL | URL | Alternative to base URL | derived from base URL | /oauth2/v1/token | | CORE_OAUTH_GATEWAY_INTROSPECTION_URL | URL | No | derived from base/token URL | /oauth2/v1/introspect | | CORE_OAUTH_GATEWAY_TOKENINFO_URL | URL | No | derived from base/token URL | /oauth2/v1/tokeninfo | | CORE_OAUTH_GATEWAY_REVOKE_URL | URL | No | derived from base/token URL | /oauth2/v1/revoke | | CORE_OAUTH_GATEWAY_USER_AUTHORIZE_URL | URL | No | derived from base/token URL | /oauth2/v1/userauthorize | | CORE_OAUTH_GATEWAY_CERTS_URL | URL | Conditional | none | Preferred direct JWKS variable | | CORE_OAUTH_JWKS_URL | URL | Conditional | none | Alias for direct JWKS | | CORE_OAUTH_CLIENT_ID | string | Required for automatic signing | unset | OAuth client identifier | | CORE_OAUTH_CLIENT_SECRET | string | No | unset | Prefer private_key_jwt when supported | | CORE_OAUTH_HTTP_TIMEOUT_MS | number | No | 10000 | OAuth HTTP timeout | | CORE_OAUTH_HTTP_RETRY_COUNT | number | No | 1 | Retry attempts after the first request | | CORE_OAUTH_HTTP_RETRY_DELAY_MS | number | No | 100 | Fixed retry delay |

Token Validation and JWKS

| Variable | Type | Required | Default | Example / notes | | ---------------------------------------- | ------------------------- | ------------------------------------- | ------------------------------------ | ------------------------------------------------------ | | CORE_OAUTH_OPAQUE_VALIDATION_MODE | introspect \| tokeninfo | No | introspect | Used for tokens without . | | CORE_OAUTH_GATEWAY_ISSUER | string | For JWT when discovery is absent | none | Exact iss match | | CORE_OAUTH_JWT_AUDIENCE | string | For JWT by default | none | Expected access-token audience | | CORE_OAUTH_JWT_TYPES | list | No | no typ restriction | Example: JWT at+jwt | | CORE_OAUTH_REQUIRED_SCOPES | list | No | none | Global preset scopes; route-level scopes are preferred | | CORE_OAUTH_ALLOWED_ALGORITHMS | list | No | RS256 | none is always rejected | | CORE_OAUTH_CLOCK_SKEW_SECONDS | number | No | 30 | Applied to exp and nbf | | CORE_OAUTH_JWKS_CACHE_TTL_MS | number | No | 300000 | Five minutes | | CORE_OAUTH_JWKS_TIMEOUT_MS | number | No | 10000 | Remote JWKS/discovery timeout | | CORE_OAUTH_JWKS_RETRY_COUNT | number | No | 1 | Retry attempts after first request | | CORE_OAUTH_JWKS_RETRY_DELAY_MS | number | No | 100 | Fixed delay | | CORE_OAUTH_ISSUER_DISCOVERY_URL | URL | Alternative to direct JWKS and issuer | none | Authorization Server Metadata URL | | CORE_OAUTH_JWKS_ALLOWED_HOSTS | list | No | any public host | identity.example.com *.trusted.example.com | | CORE_OAUTH_JWKS_ALLOWED_PORTS | number list | No | HTTPS: 443; HTTP allowed: 80,443 | Ports 1-65535 | | CORE_OAUTH_JWKS_REQUIRE_HTTPS | boolean | No | true | Keep enabled in production | | CORE_OAUTH_JWKS_BLOCK_PRIVATE_NETWORKS | boolean | No | true | SSRF protection | | CORE_OAUTH_JWKS_MAX_RESPONSE_BYTES | number | No | 65536 | 64 KiB | | CORE_OAUTH_JWKS_MAX_KEYS | number | No | 20 | Maximum accepted public keys | | CORE_OAUTH_JWKS_MAX_REDIRECTS | number | No | 3 | Every redirect target is revalidated |

JWT validation requires either a direct JWKS URL or discovery URL when requireJwt is true. Discovery supplies issuer and jwks_uri; audience remains application-specific.

Assertion Signing

| Variable | Type | Required | Default | Example / notes | | ---------------------------------- | ------------- | -------------------- | ------------------------------------------- | -------------------------------------- | | CORE_OAUTH_P12_BASE64 | Base64 string | When signing enabled | none | Binary .p12/.pfx encoded as Base64 | | CORE_OAUTH_P12_PASSWORD_BASE64 | Base64 string | When signing enabled | none | P12 password encoded as Base64 | | CORE_OAUTH_P12_ALIAS | string | When signing enabled | none | Exact PKCS#12 friendly name | | CORE_OAUTH_CLIENT_ID | string | When signing enabled | none | Used for both iss and sub | | CORE_OAUTH_ASSERTION_AUDIENCE | string | Conditional | token URL, then legacy JWT audience | Token endpoint is recommended | | CORE_OAUTH_ASSERTION_TTL_SECONDS | number | No | 300 | Integer from 1 to 600 | | CORE_OAUTH_KEY_ID | string | No | RFC 7638 thumbprint derived from public key | Explicit JWT header kid override |

CORE_OAUTH_JWT_AUDIENCE is an inbound access-token validation setting. Use CORE_OAUTH_ASSERTION_AUDIENCE for assertions unless both values intentionally match.

Logging

| Variable | Type | Required | Default | Example / notes | | --------------------------------- | ----------------------- | -------- | --------------- | --------------------------------------------------------------------- | | LOG_LEVEL | logger level | No | INFO | TRACE, DEBUG, INFO, WARN, ERROR, and logger-specific levels | | LOGGER_TIMEZONE | IANA timezone | No | UTC | America/Santiago; invalid values fall back to UTC | | LOGGER_MODE | sync \| async | No | async | sync is useful for short examples/tests | | LOGGER_SAMPLE_RATE | number | No | 1 | Clamped by logger-core | | LOGGER_ERROR_STACK_ENABLED | boolean text | No | false | Only exact true enables stack output | | LOGGER_FLUSH_INTERVAL_MS | number | No | 10 | Async sink interval | | LOGGER_MAX_QUEUE_SIZE | number | No | 10000 | Async queue size | | LOGGER_OVERFLOW_STRATEGY | drop \| sync-fallback | No | sync-fallback | Any value except drop selects fallback | | LOGGER_SHUTDOWN_TIMEOUT_MS | number | No | 2000 | Shutdown flush timeout | | LOGGER_INTERNAL_METRICS_ENABLED | boolean text | No | false | Only exact true enables sink metrics |

Default Values

HTTP Profiles

| Behavior | internal | external | | --------------------- | ----------- | ---------- | | Timeout | 10 seconds | 5 seconds | | Trace propagation | enabled | disabled | | Maximum response body | 5 MiB | 1 MiB | | Response body logging | errors only | never |

General HTTP retry defaults:

{
  retries: 0,
  retryDelayMs: 100,
  retryStatuses: [408, 425, 429, 500, 502, 503, 504],
  retryOnTimeout: true
}

OAuth environment clients override retries to 1. Retries use a fixed delay.

Tracing

| Value | Default | | ------------------- | ----------------------------------- | | requestId | UUID v4 | | traceId | 32 lowercase hexadecimal characters | | spanId | 16 lowercase hexadecimal characters | | client metadata | "unknown" | | child span creation | enabled |

OAuth and Security

| Behavior | Default | | -------------------------- | ----------------- | | Opaque validation | introspection | | Allowed JWT algorithms | RS256 | | Environment JWT clock skew | 30 seconds | | JWKS cache | 5 minutes | | Assertion TTL | 300 seconds | | Maximum assertion TTL | 600 seconds | | P12 key selection | exact alias match | | JWKS HTTPS | required | | JWKS private networks | blocked |

API Reference

All public APIs are exported from the package root unless a subpath is shown.

Runtime and Presets

| API | Purpose | | -------------------------------------- | -------------------------------------------- | | initializeServiceFramework(options?) | Idempotently initialize logging | | shutdownServiceFramework(options?) | Flush and stop logging; returns sink metrics | | isServiceFrameworkInitialized() | Report lifecycle state | | createNextApiService(options?) | Non-OAuth Next.js preset | | createNextBffService(options?) | OAuth-aware Next.js BFF preset | | createExpressApiService(options?) | OAuth-aware Express preset | | createInternalServiceClient(options) | Internal downstream client |

Configuration Factories

| API | Purpose | | ------------------------------------------------ | --------------------------------------- | | createServiceMetadataFromEnv() | Read required service identity | | createServiceHttpClientConfigFromEnv() | Build HTTP config from SERVICE_HTTP_* | | createOAuthGatewayConfigFromEnv() | Build OAuthGatewayClientConfig | | createOAuthGatewayClientFromEnv() | Build the OAuth client | | createOAuthTokenValidatorConfigFromEnv() | Build validator config | | createOAuthTokenValidatorFromEnv() | Build the validator | | createNextBffConfigFromEnv() | Build lower-level Next BFF config | | createExpressServiceConfigFromEnv() | Build lower-level Express config | | validateEnv() | Validate a custom environment schema | | requiredEnv(), optionalEnv() | Read string values | | requiredUrlEnv(), optionalUrlEnv() | Read URL values | | numberEnv(), booleanEnv(), stringListEnv() | Read typed values |

Tracing and MDC

| API | Purpose | | ---------------------------------------- | ------------------------------------------------ | | createTraceContext(headers?, options?) | Normalize/generate trace metadata | | getTraceRequestHeaders(context?) | Build downstream propagation headers | | getTraceResponseHeaders(context?) | Build response trace headers | | runWithTraceContext(context, callback) | Run work in request context | | getCurrentTraceContext() | Read current normalized trace context | | traceContextToMdc() | Convert context to canonical camelCase MDC | | traceContextToLogFields() | Convert context to snake_case log fields | | traceContextToMetricLabels() | Produce low-cardinality metric labels by default | | setMdcField(), setMdcFields() | Add custom request fields | | getMdc() | Return an immutable MDC snapshot | | removeMdcField() | Remove one custom field | | withMdc() | Add scoped MDC and restore it afterward |

traceContextToMetricLabels(context, { includeHighCardinality: true }) adds request, trace, and span IDs. Do not use high-cardinality labels in Prometheus-style metrics.

Lower-level tracing exports also include createTraceMiddleware(), applyTraceResponseHeaders(), createRequestTraceContext(), header constants, and generic header readers/writers.

Logging

| API | Purpose | | -------------------------------------- | ------------------------------------------------------- | | initializeLogger(options?) | Initialize logger-core/logger-node directly | | shutdownLogger(options?) | Flush the logger sink | | createLogger(config?) | Create the service logger facade | | redactSensitiveData(value, options?) | Deeply redact values, errors, headers, arrays, and URLs | | normalizeLogException(error) | Normalize any thrown value | | createRestCallLogger(logger?) | Low-level REST lifecycle logger |

HTTP

| API | Purpose | | ----------------------------------------------------------- | ------------------------------------------------------- | | createHttpClient(config) | Create raw/JSON/form HTTP methods | | extractBearerToken(header) | Parse Bearer authorization or throw UnauthorizedError | | isBearerTokenAuthorizationHeader(header) | Boolean Bearer validation | | createHeaders(), withJsonHeaders(), withFormHeaders() | Header utilities | | applyTraceHeaders() | Add trace headers to an existing Headers | | mapResponseToAppError() | Map non-2xx responses to standard errors | | safeResponseBody() | Read and redact a cloned response body |

OAuth

| API | Main methods | | ------------------------- | ------------------------------------------------------------------------------ | | OAuthTokenValidator | validate(token) | | OpaqueTokenIntrospector | validate(token) | | JwksTokenVerifier | verify(token), getJwks() | | OAuthDiscoveryClient | getMetadata({ forceRefresh? }) | | OAuthGatewayClient | token grants, introspection, token info, revoke, authorize, JWKS, key rotation | | ClientCredentialsClient | getAccessToken() with cache | | ScopeGuard | requireScopes, hasScope, hasAnyScope, hasAllScopes |

Function forms of all scope helpers are also exported.

Security and Signing

| API | Purpose | | -------------------------------------- | -------------------------------------------------------- | | createOAuthSignerFromEnv(options?) | Recommended unified environment signer | | OAuthSigner | Unified Client Assertion/JWT Bearer signer with rotation | | PrivateKeyJwtSigner | Lower-level Client Assertion signer | | JwtBearerAssertionSigner | Lower-level JWT Bearer signer | | P12JwtBearerAssertionSigner | Compatibility signer with cached P12 and rotation | | loadKeyMaterialFromP12Base64(config) | Load private/public key, kid, fingerprint, and alias | | loadPrivateKeyFromP12Base64(config) | Load only the private KeyObject | | P12KeyMaterialCache | Cache and atomically rotate verified P12 material | | RemoteJwksResolver | Secure remote JWKS loading and caching |

clientAssertionSigner in OAuthGatewayClientConfig is deprecated. Use assertionSigner. userId remains a compatibility shortcut that creates user_id; new integrations should prefer the claims record.

Low-level JWT utilities (parseJwt, signJwtRs256, verifyJwtRs256, publicKeyFromJwk, privateKeyFromPem, and claim normalization helpers) are public for infrastructure use. Prefer the higher-level verifier and signer classes in application code.

Framework Adapters

Next.js:

nextRouteHandler
nextAuthRouteHandler / withAuthRoute
createNextTraceMiddleware
createNextErrorResponse
createCookieSafeNextJsonResponse

Express:

expressTraceMiddleware
expressAuthMiddleware
expressErrorHandler
runWithExpressTraceContext

Fastify:

fastifyTracePlugin
fastifyErrorHandler

The package exports no decorators.

Key Public Types

| Area | Types | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Tracing | TraceContext, RequestTraceContext, TraceLogFields, TraceMetricLabels, CreateTraceContextOptions, MdcFields | | Logging | Logger, LogMetadata, LoggerInitializationOptions, RedactionOptions | | HTTP | HttpClient, HttpClientConfig, HttpRetryConfig, HttpRequestOptions, JsonRequestOptions | | OAuth | ValidatedToken, OAuthTokenResponse, OAuthGatewayClientConfig, token grant parameter types, OAuthTokenValidatorConfig, JwksTokenVerifierConfig | | Signing | OAuthAssertionSigner, OAuthSignerConfig, OAuthSignerEnvOptions, P12LoaderConfig, P12KeyMaterial, OAuthSigningKeyInfo | | Errors | AppErrorOptions, ErrorBody, ErrorPresentation, PresentErrorOptions, error-policy types | | Observability | ObservabilityHooks, HttpClientMetric, TokenValidationMetric, JwksRefreshEvent, AuthFailureEvent | | Frameworks | route callbacks/contexts and adapter option types under each framework subpath |

Package Subpaths

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 { initializeServiceFramework } from "@smb-tech/service-framework-js/runtime";
import type { ObservabilityHooks } f