npm.io
0.3.0 • Published yesterdayCLI

@smb-tech/service-framework-js

Licence
MIT
Version
0.3.0
Deps
3
Size
2.5 MB
Vulns
0
Weekly
0

@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

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 } 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";

Available subpaths: /tracing, /logging, /http, /oauth, /security, /errors, /config, /observability, /presets, /runtime, /framework/next, /framework/express, and /framework/fastify.

Examples

Repository examples:

Directory Demonstrates
examples/next-bff Next route wrapper, Bearer validation, tracing, safe response
examples/express-service Express tracing, protected route, scopes, errors
examples/fastify-service Fastify trace plugin and error handler
examples/oauth-validation Opaque/JWT validation and scopes
examples/client-credentials Automatic RS256 client_assertion
examples/jwt-bearer-assertion Automatic JWT bearer assertion with custom claims
examples/rest-client-logging Success/error REST logs and redaction

Demo examples:

npm run example:next
npm run example:express
npm run example:fastify

OAuth signing examples require real non-committed configuration:

cp examples/client-credentials/.env.example examples/client-credentials/.env.local
cp examples/jwt-bearer-assertion/.env.example examples/jwt-bearer-assertion/.env.local

# Replace placeholders:
npm run example:client-credentials
npm run example:jwt-bearer

These examples print only public key metadata and non-sensitive token response metadata.

Error Handling

All standard errors produce:

{
  "error": "forbidden",
  "error_description": "Missing required scope: cl:core:profile:read"
}
Class Status Code Exposed by default
InvalidRequestError 400 invalid_request Yes
UnauthorizedError 401 unauthorized Yes
ForbiddenError 403 forbidden Yes
NotFoundError 404 not_found Yes
ConflictError 409 invalid_state Yes
UnprocessableEntityError 422 unprocessable_entity Yes
InternalServerError 500 internal_server_error No
ExternalServiceError 502 external_service_error No

OAuth signing also exports OAuthConfigurationError, P12LoadError, P12AliasNotFoundError, P12AliasConflictError, P12CertificateError, OAuthAssertionSigningError, and OAuthAssertionSignerNotConfiguredError.

import { AppError, NotFoundError } from "@smb-tech/service-framework-js/errors";

throw new NotFoundError("Profile was not found");

throw AppError.externalService("Profile provider failed", {
  cause: providerError
});

Internal messages and stacks are hidden by default. presentError(error, { includeStack: true, production: false }) may expose an internal message only in an explicitly non-production path.

Custom policy:

import { createErrorPolicyResolver, ExternalServiceError } from "@smb-tech/service-framework-js/errors";

const errorPolicy = createErrorPolicyResolver({
  rules: [
    {
      errorType: ExternalServiceError,
      policy: {
        logLevel: "error",
        operational: true,
        shouldReport: true
      }
    }
  ]
});

Pass the resolver to a preset or framework error adapter.

Logging and Observability

Canonical Log Shape

Request fields are stored in mdc; operation fields remain in data:

{
  "ts": "2026-07-02T10:30:00.000-04:00",
  "type": "INFO",
  "msg": "External service call completed",
  "mdc": {
    "requestId": "request-id",
    "traceId": "trace-id",
    "spanId": "span-id",
    "serviceName": "quickfade-bff-web"
  },
  "data": {
    "target_service": "core-auth-service",
    "operation": "profile.get",
    "status_code": 200,
    "duration_ms": 42
  },
  "tags": ["service-framework", "profile"]
}

LOGGER_TIMEZONE controls the timestamp offset. It does not change the instant being logged.

Automatic Redaction

Redaction covers nested objects, arrays, native errors, headers, cookies, URL credentials, query parameters, fragments, Bearer values, binary content, tokens, assertions, P12/PFX values, passwords, private keys, API keys, and OAuth signatures.

Custom fields extend mandatory defaults; they cannot disable built-in protection:

redactSensitiveData(payload, {
  sensitiveFields: ["national_id"],
  sensitiveQueryParameters: ["invite_code"],
  maxStringLength: 2_000
});
Optional Hooks
const service = createNextBffService({
  onHttpClientMetric(event) {
    metrics.httpClient.observe(event);
  },
  onTokenValidationMetric(event) {
    metrics.tokenValidation.observe(event);
  },
  onJwksRefresh(event) {
    metrics.jwksRefresh.increment(event);
  },
  onAuthFailure(event) {
    metrics.authFailure.increment(event);
  }
});

Hooks are optional, receive frozen copies, and are failure-isolated. Synchronous throws and rejected promises are ignored so telemetry cannot alter authentication or request behavior.

Security

  • Never commit P12/PFX content, passwords, tokens, cookies, or assertions.
  • Store CORE_OAUTH_P12_BASE64 and CORE_OAUTH_P12_PASSWORD_BASE64 in a secret manager.
  • Use private_key_jwt instead of a client secret when supported.
  • Keep JWT algorithms allowlisted; the environment default is RS256.
  • Keep JWKS HTTPS and private-network blocking enabled.
  • Use the external HTTP profile for third-party hosts.
  • Avoid responseBodyLogging: "always" in production.
  • Do not put tokens, user identifiers, or request IDs in metric labels.
  • Do not place sensitive values in custom MDC.
  • Call shutdownServiceFramework() on graceful process termination.
  • Rotate any credential that has appeared in a terminal transcript, chat, issue, or commit.

createCookieSafeNextJsonResponse() adds Cache-Control: no-store, Pragma: no-cache, and Vary: Cookie, Authorization.

Troubleshooting

SERVICE_NAME is required

Environment-based presets require SERVICE_NAME.

SERVICE_NAME=example-service npm run example:express

For repository examples, use their .env.example as the starting point.

JWT validation is not configured

The token contains ., so it is treated as JWT, but no direct JWKS or discovery URL was configured. Set one of:

CORE_OAUTH_GATEWAY_CERTS_URL=https://oauth.example.com/oauth2/v1/certs
# or
CORE_OAUTH_JWKS_URL=https://oauth.example.com/oauth2/v1/certs
# or
CORE_OAUTH_ISSUER_DISCOVERY_URL=https://oauth.example.com/.well-known/oauth-authorization-server
Invalid JWT issuer or Invalid JWT audience

Decode the token payload only for diagnosis; do not log the complete token. Compare iss and aud with CORE_OAUTH_GATEWAY_ISSUER and CORE_OAUTH_JWT_AUDIENCE. Assertion audience and access-token audience are different settings.

JWT algorithm is not allowed

The token header alg is outside CORE_OAUTH_ALLOWED_ALGORITHMS. Do not add an algorithm merely to silence the error. Confirm the OAuth issuer's signing policy first. alg=none is always rejected.

P12 alias not found

CORE_OAUTH_P12_ALIAS must match the key entry friendly name, not merely a certificate subject. The error includes available aliases but never includes key material or passwords.

P12 loads locally but fails in production

Confirm that:

  1. the .p12 binary was Base64-encoded without accidental quoting
  2. the password itself was separately Base64-encoded
  3. the alias matches exactly, including case
  4. all three values belong to the same P12 deployment
  5. the P12 contains an RSA private key and matching certificate

No external binary is needed in production.

Rotation does not use the new environment value

Many hosting platforms do not mutate environment variables inside an already-running process. Redeploy/restart, or inject a runtime P12KeyMaterialProvider. Failed rotations intentionally keep the old key active.

Remote JWKS host is blocked

Check HTTPS, host allowlist, port allowlist, DNS resolution, redirects, and private/reserved IP blocking. A wildcard such as *.example.com excludes the apex example.com; list both when needed.

HTTP call times out or retries unexpectedly

Check the selected profile, effective timeoutMs, retry.retries, and retry status list. The retries value is the number of retries after the initial attempt.

Logs do not appear before process exit

Async mode buffers briefly. Call:

await shutdownServiceFramework();

Use LOGGER_MODE=sync only for short-lived scripts or diagnosis.

Next.js Edge runtime failure

Use:

export const runtime = "nodejs";

P12, Node.js crypto, logger-node, and AsyncLocalStorage are not Edge-runtime APIs.

Best Practices

  1. Prefer a service preset over assembling adapters manually.
  2. Configure requiredScopes per endpoint; use global scopes only when every endpoint shares them.
  3. Reuse one OAuthGatewayClient and one signer per process.
  4. Reuse ClientCredentialsClient when downstream tokens should be cached.
  5. Name every outbound operation (profile.get, oauth.token.introspect) for stable metrics.
  6. Use internal HTTP only for trusted service-to-service calls.
  7. Inject fetcher and an explicit env object in tests.
  8. Keep domain behavior in the service; use this package for cross-cutting infrastructure.
  9. Validate a replacement P12 before activating it and retain the previous registration during rollout.
  10. Run npm run prepublishOnly before publishing.

Migration Guide

From Local Cross-Cutting Utilities
Local responsibility Replacement
trace header parsing createTraceContext or framework adapters
custom AsyncLocalStorage framework MDC (getMdc, withMdc)
JSON console logger createLogger
custom redaction redactSensitiveData
fetch wrapper createHttpClient or createInternalServiceClient
Bearer parser extractBearerToken
introspection/JWKS validation OAuthTokenValidator
scope middleware route-level requiredScopes / ScopeGuard
assertion shell scripts environment-enabled OAuthGatewayClient
error JSON AppError and framework error handlers

Delete local code only after all imports and behavior have migrated. Keep controllers, use cases, repositories, domain validation, and authorization-server-specific protocol logic.

From JKS and Manual Assertions
Previous 0.3.0
JKS file/Base64 CORE_OAUTH_P12_BASE64
store/key password CORE_OAUTH_P12_PASSWORD_BASE64
JKS alias CORE_OAUTH_P12_ALIAS
--client-id CORE_OAUTH_CLIENT_ID
--aud CORE_OAUTH_ASSERTION_AUDIENCE
manual client_assertion tokenByClientCredentials()
manual JWT bearer assertion tokenByJwtBearer()
export const oauthGateway = createOAuthGatewayClientFromEnv({
  enableAssertionSigning: true
});

Remove JKS loaders, keytool, openssl, temporary-file handling, execFile, and shell assertion invocations after both grants pass against the target environment.

Recommended service migration order:

Service First replacement
quickfade-bff-web Next BFF preset, route scopes, internal HTTP
core-login-provider Next adapters, OAuth Gateway client, P12 assertions
core-auth-service protected routes, scope guards, downstream HTTP
core-messaging-engine Express tracing/errors, internal HTTP
core-oauth-gateway tracing, logging, redaction, HTTP; keep authorization-server domain logic

CLI

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

Client Assertion:

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

JWT Bearer Assertion:

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

Encoding:

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

Supported assertion flags:

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

CLI-only environment fallbacks also include OAUTH_ASSERTION_USER_ID and OAUTH_ASSERTION_CLAIMS_JSON.

The CLI prints generated assertions because that is its explicit purpose. Treat stdout as secret material and avoid CI logs or shell history.

AI Usage Notes

When generating or modifying a service that uses this package:

  1. Target Node.js >=20. For Next.js, emit export const runtime = "nodejs".
  2. Start with createNextApiService, createNextBffService, or createExpressApiService; do not duplicate tracing, error, or auth wrappers.
  3. Treat SERVICE_NAME as required for all presets.
  4. Do not configure OAuth for createNextApiService() unless the application explicitly needs an OAuth client separately.
  5. For protected Next routes, use service.authRoute(handler, { requiredScopes }).
  6. For Express, preserve middleware order: JSON parser, trace middleware, route auth, handlers, error handler.
  7. Distinguish CORE_OAUTH_JWT_AUDIENCE (incoming tokens) from CORE_OAUTH_ASSERTION_AUDIENCE (outgoing assertions).
  8. For JWT bearer, place domain data in claims; use assertionScopes for the signed claim and requestedScopes for the OAuth form.
  9. Never create or override iss, sub, aud, iat, exp, or jti inside custom claims.
  10. Never output tokens, assertions, P12, passwords, cookies, private keys, or complete Authorization headers in examples or logs.
  11. Use profile: "external" for public third-party APIs and do not propagate trace headers unless explicitly approved.
  12. Observability hooks are optional and must remain side-effect-only.
  13. Use AppError subclasses for expected service failures. Do not expose internal stacks.
  14. Do not introduce JKS, Java, keytool, openssl, temporary key files, or child_process.
  15. Do not assume a P12 has one key; always require and use p12Alias.
  16. Before suggesting a new API, verify it exists in the package exports or source.

Canonical protected Next pattern:

const service = createNextBffService();

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

Canonical automatic signing pattern:

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

await oauth.tokenByClientCredentials({ scope });
await oauth.tokenByJwtBearer({ claims, assertionScopes, requestedScopes });

Versioning and Development

Current package version: 0.3.0.

See CHANGELOG.md for release details. Coordinate breaking API changes with SMB Tech service owners before publishing.

npm install
npm run lint
npm run typecheck
npm test
npm run test:unit
npm run test:integration
npm run test:package
npm run build
npm run prepublishOnly

npm run test:package verifies ESM, CommonJS, package subpaths, declarations, CLI resolution, and installation of the generated npm tarball in a temporary consumer.

Source maps are intentionally not published in 0.3.0.

Keywords