@smb-tech/service-framework-js
v0.2.0
Published
Reusable service framework utilities for SMB Tech Node.js services
Maintainers
Readme
@smb-tech/service-framework-js
Reusable Node.js and TypeScript service utilities for SMB Tech services.
This package is published to npm for SMB Tech distribution convenience. It is not a general-purpose community framework. The APIs, defaults, naming conventions, headers, OAuth assumptions and operational behavior are designed for SMB Tech and QuickFade services.
What This Package Is
@smb-tech/service-framework-js centralizes cross-cutting service logic used across SMB Tech Node.js services:
- Next.js BFFs
- Express APIs
- Fastify APIs
- OAuth-aware gateways
- Core services
- Internal microservices
- Background or integration services that call SMB Tech APIs
It provides shared building blocks for:
- tracing headers
- structured logging
- sensitive data redaction
- outbound REST logging
- Bearer token extraction
- opaque token validation with introspection or tokeninfo
- JWT validation with JWKS/certs
- scope guards
- OAuth Gateway client flows
private_key_jwtclient assertions- JWT bearer grant assertions
- PKCS#12/P12 key loading from Base64
- standard error responses
- Next.js and Express adapters
- Fastify adapters
- environment-based service configuration
- first-class Next.js, Express and internal-client presets
- OAuth Authorization Server Metadata discovery
- optional vendor-neutral observability hooks
- published assertion and P12 CLI
Intended Audience
Use this package when you are building or maintaining SMB Tech or QuickFade services.
If you are outside SMB Tech, the package may still be installable, but the defaults are intentionally opinionated around SMB Tech infrastructure:
X-Request-Id- B3 trace headers
X-Client-*headersX-Service-*headers- OAuth Gateway endpoints
RS256JWT validation- SMB Tech error payloads
- SMB Tech scope naming
- SMB Tech environment variable names
Installation
npm install @smb-tech/service-framework-jsRequirements:
- Node.js
>=20 - TypeScript supported
- ESM and CommonJS builds are published
- Type declarations are exported
Quick Start: Next.js BFF
This is the most common use case for QuickFade BFFs.
import {
createCookieSafeNextJsonResponse,
createInternalServiceClient,
createNextBffService
} from "@smb-tech/service-framework-js";
const service = createNextBffService();
const coreAuthClient = createInternalServiceClient({
targetService: "core-auth-service",
baseUrl: process.env.CORE_AUTH_SERVICE_BASE_URL!
});
export const GET = service.authRoute(
async (_request, { bearerToken, traceContext, auth }) => {
const profile = await coreAuthClient.getJson("/v1/auth/me", {
traceContext,
headers: {
Authorization: `Bearer ${process.env.CORE_AUTH_CLIENT_CREDENTIALS_TOKEN}`,
"X-Access-Token": bearerToken
}
});
return createCookieSafeNextJsonResponse({
subject: auth.subject,
profile
});
},
{ requiredScopes: "cl:core:profile:read" }
);With this setup the route automatically:
- reads and propagates trace headers
- extracts the
Authorization: Bearer ...token - validates opaque tokens or JWTs
- validates configured required scopes
- logs the request outcome
- returns standard error payloads
- injects response trace headers
- prevents cache leaks with cookie-safe response headers
Quick Start: Express Service
import express from "express";
import { createExpressApiService, type ExpressAuthenticatedRequest } from "@smb-tech/service-framework-js";
const service = createExpressApiService();
const app = express();
app.use(express.json());
app.use(service.traceMiddleware);
app.get(
"/notes",
service.auth({ requiredScopes: "cl:quickfade:notes:read" }),
(req: ExpressAuthenticatedRequest, res) => {
res.status(200).json({
subject: req.auth?.subject,
scopes: req.auth?.scopes,
notes: []
});
}
);
app.use(service.errorHandler);
app.listen(3001, () => {
console.log("express-service listening on http://localhost:3001");
});Service Presets
The 0.2.0 presets keep framework wiring in one place:
createNextBffService()returnsauthRoute,route,http, OAuth clients and normalized configuration.createExpressApiService()returnstraceMiddleware, route-levelauth,errorHandler,httpand OAuth clients.createInternalServiceClient()binds a source service, target service and base URL to an HTTP client.
const profiles = createInternalServiceClient({
targetService: "core-auth-service",
baseUrl: process.env.CORE_AUTH_SERVICE_BASE_URL!
});
const profile = await profiles.getJson("/v1/profiles/user-1", {
operation: "profile.get"
});All existing low-level factories remain available when a service needs custom composition.
Environment Preset
For most BFFs and services, start with these variables.
SERVICE_NAME=quickfade-bff-web
SERVICE_VERSION=1.0.0
SERVICE_HTTP_TIMEOUT_MS=10000
SERVICE_HTTP_RETRY_COUNT=1
SERVICE_HTTP_RETRY_DELAY_MS=100
CORE_OAUTH_GATEWAY_BASE_URL=https://core-oauth-gateway.onrender.com
CORE_OAUTH_ISSUER_DISCOVERY_URL=https://core-oauth-gateway.onrender.com/.well-known/oauth-authorization-server
CORE_OAUTH_JWT_AUDIENCE=quickfade-bff-web
CORE_OAUTH_REQUIRED_SCOPES=cl:core:profile:read
CORE_OAUTH_OPAQUE_VALIDATION_MODE=introspect
CORE_OAUTH_ALLOWED_ALGORITHMS=RS256
CORE_OAUTH_CLOCK_SKEW_SECONDS=30
CORE_OAUTH_JWKS_CACHE_TTL_MS=300000
CORE_OAUTH_JWKS_TIMEOUT_MS=10000
CORE_OAUTH_JWKS_RETRY_COUNT=1
CORE_OAUTH_JWKS_RETRY_DELAY_MS=100
CORE_OAUTH_HTTP_TIMEOUT_MS=10000
CORE_OAUTH_HTTP_RETRY_COUNT=1
CORE_OAUTH_HTTP_RETRY_DELAY_MS=100
CORE_AUTH_SERVICE_BASE_URL=https://core-auth-service.onrender.com
CORE_AUTH_CLIENT_CREDENTIALS_TOKEN=replace-at-runtimeCORE_OAUTH_REQUIRED_SCOPES may contain one or more scopes separated by spaces or commas.
Discovery is the recommended JWT configuration. The metadata response must contain valid issuer and jwks_uri values. For gateways without discovery, configure both values directly:
CORE_OAUTH_GATEWAY_CERTS_URL=https://core-oauth-gateway.onrender.com/oauth2/v1/certs
CORE_OAUTH_GATEWAY_ISSUER=https://core-oauth-gateway.onrender.comScope Validation Example
Use route-level scopes when endpoints have different permissions. A route-level value overrides the preset from CORE_OAUTH_REQUIRED_SCOPES.
const service = createNextBffService();
export const GET = service.authRoute(handler, {
requiredScopes: "cl:core:profile:read"
});Express uses the same endpoint-level pattern:
app.get("/notes", service.auth({ requiredScopes: "cl:quickfade:notes:read" }), handler);Behavior:
- missing or invalid token returns
401 - expired token returns
401 - valid token without the required scope returns
403
Tracing
import {
createTraceContext,
getTraceRequestHeaders,
getTraceResponseHeaders,
traceContextToLogFields,
traceContextToMetricLabels,
runWithTraceContext
} from "@smb-tech/service-framework-js";
const traceContext = createTraceContext(request.headers, {
serviceName: "core-auth-service",
serviceVersion: "1.0.0"
});
await runWithTraceContext(traceContext, async () => {
const downstreamHeaders = getTraceRequestHeaders(traceContext);
const responseHeaders = getTraceResponseHeaders(traceContext);
const logFields = traceContextToLogFields(traceContext);
const metricLabels = traceContextToMetricLabels(traceContext);
console.log({ downstreamHeaders, responseHeaders, logFields, metricLabels });
});Supported headers:
X-Request-IdX-B3-TraceIdX-B3-SpanIdX-B3-ParentSpanIdX-Client-ChannelX-Client-PlatformX-Client-AppX-Client-VersionX-Service-NameX-Service-Version
Logging And Redaction
import { createLogger, redactSensitiveData } from "@smb-tech/service-framework-js";
const logger = createLogger({
contextName: "CoreAuthService",
serviceName: "core-auth-service"
});
logger.info("Token request accepted", {
client_id: "core-auth-service",
client_assertion: "secret.jwt.value"
});
const safePayload = redactSensitiveData({
authorization: "Bearer secret",
cookie: "session=secret",
nested: {
p12Base64: "base64-secret",
query: "https://api.test/path?access_token=secret"
}
});Sensitive values are deeply redacted from:
- objects
- arrays
HeadersURLURLSearchParams- URL strings
Errorinstances- nested payloads
Sensitive keys include:
access_tokenrefresh_tokenid_tokentokenauthorizationcookiepasswordsecretclient_secretassertionclient_assertionjwtp12pfxprivate_keyapi_keyapikeyx-api-key
REST Client Logging
import { createHttpClient } from "@smb-tech/service-framework-js";
const httpClient = createHttpClient({
serviceName: "core-auth-service",
timeoutMs: 10_000,
retry: {
retries: 1,
retryDelayMs: 100,
retryOnTimeout: true
}
});
const token = await httpClient.postJson(
"https://core-oauth-gateway.onrender.com/oauth2/v1/token",
{
grant_type: "client_credentials",
scope: "cl:system:auth:internal"
},
{
targetService: "core-oauth-gateway",
operation: "oauth.token.client_credentials"
}
);Outbound logs include:
service_nametarget_serviceoperationmethodurlstatus_codeduration_msresponse_time_mstrace_idrequest_id
For non-2xx responses, redacted response_headers, response_body and error_message are also logged.
Optional Observability Hooks
Metrics are completely optional. Without hooks, runtime behavior is unchanged. Hooks are vendor-neutral and may forward events to OpenTelemetry, Prometheus, Datadog or another application-owned collector.
const service = createNextBffService({
onHttpClientMetric(metric) {
telemetry.recordHttpDuration(metric.durationMs, {
target_service: metric.targetService,
operation: metric.operation,
outcome: metric.outcome
});
},
onTokenValidationMetric(metric) {
telemetry.increment("oauth.token.validation", {
token_type: metric.tokenType,
outcome: metric.outcome
});
},
onJwksRefresh(event) {
telemetry.increment("oauth.jwks.refresh", { outcome: event.outcome });
},
onAuthFailure(event) {
telemetry.increment("http.auth.failure", {
framework: event.framework,
reason: event.reason
});
}
});Hook failures are isolated and never change HTTP, OAuth or authorization results. Events never contain bearer tokens, assertions, cookies, P12 content, passwords or private keys. Avoid using request IDs, trace IDs or raw paths as Prometheus labels because they create high cardinality.
OAuth Token Validation
Token type detection is automatic:
- token without
.is treated as opaque - token with
.is treated as JWT
Opaque Token
const validator = new OAuthTokenValidator({
opaqueValidationMode: "introspect",
introspectToken: (token) => oauthGateway.introspectToken(token)
});
const result = await validator.validate("opaque-token");JWT With JWKS
const validator = new OAuthTokenValidator({
jwt: {
jwksUrl: "https://core-oauth-gateway.onrender.com/oauth2/v1/certs",
expectedIssuer: "https://core-oauth-gateway.onrender.com",
expectedAudience: "quickfade-bff-web",
allowedAlgorithms: ["RS256"],
clockSkewSeconds: 30,
cacheTtlMs: 300_000,
timeoutMs: 10_000,
retryCount: 1,
retryDelayMs: 100
}
});
const result = await validator.validate(jwt);JWT With OAuth Discovery
const validator = new OAuthTokenValidator({
jwt: {
discoveryUrl: "https://core-oauth-gateway.onrender.com/.well-known/oauth-authorization-server",
expectedAudience: "quickfade-bff-web",
allowedAlgorithms: ["RS256"]
}
});The discovery document supplies issuer and jwks_uri. Metadata and JWKS responses are cached and fetched with bounded timeouts and retries.
JWT validation checks:
- signature
kidissaudexpnbf- allowed algorithms
alg=none is rejected.
OAuth Gateway Client
import { OAuthGatewayClient, createOAuthGatewayConfigFromEnv } from "@smb-tech/service-framework-js";
const oauthGateway = new OAuthGatewayClient(createOAuthGatewayConfigFromEnv());
await oauthGateway.tokenByClientCredentials({
scope: "cl:system:auth:internal"
});
await oauthGateway.tokenByJwtBearer({
assertion,
scope: "cl:bff:web:profile:read"
});
await oauthGateway.tokenByAuthorizationCode({
code,
redirectUri,
codeVerifier
});
await oauthGateway.tokenByRefreshToken({ refreshToken });
await oauthGateway.introspectToken(accessToken);
await oauthGateway.tokenInfo(accessToken);
await oauthGateway.revokeToken(accessToken);
await oauthGateway.userAuthorize({ oauth_key, user_jwt });
await oauthGateway.getJwks();PKCS#12 / P12 Runtime Signing
Runtime signing uses PKCS#12/P12 Base64 key material and node-forge.
No JKS, Java, keytool, openssl, temporary files or child processes are required.
CORE_OAUTH_P12_BASE64=...
CORE_OAUTH_P12_PASSWORD_BASE64=...
CORE_OAUTH_P12_ALIAS=core-auth-service
CORE_OAUTH_CLIENT_ID=core-auth-service
CORE_OAUTH_JWT_AUDIENCE=https://core-oauth-gateway.onrender.com/oauth2/v1/token
CORE_OAUTH_KEY_ID=optional-kid
CORE_OAUTH_ASSERTION_TTL_SECONDS=300Rules:
CORE_OAUTH_P12_BASE64is the Base64-encoded.p12or.pfxfile.CORE_OAUTH_P12_PASSWORD_BASE64is the Base64-encoded P12 password.CORE_OAUTH_P12_ALIASmust match the PKCS#12 key entry friendly name.- The loader fails if the alias does not exist.
- The loader fails if multiple private key entries match the alias.
- Private key material is never written to disk.
- The resulting
KeyObjectis cached in memory.
Client Assertion
Use this for OAuth private_key_jwt client authentication.
import { createOAuthSignerFromEnv } from "@smb-tech/service-framework-js";
const signer = createOAuthSignerFromEnv();
const clientAssertion = await signer.signClientAssertion({
jwtId: crypto.randomUUID(),
ttlSeconds: 300
});JWT structure:
{
"iss": "core-auth-service",
"sub": "core-auth-service",
"aud": "https://core-oauth-gateway.onrender.com/oauth2/v1/token",
"iat": 1700000000,
"exp": 1700000300,
"jti": "uuid"
}JWT Bearer Assertion
Use this for the grant:
urn:ietf:params:oauth:grant-type:jwt-bearerRecommended usage with explicit claims:
const assertion = await signer.signJwtBearerAssertion({
claims: {
user_id: "user-1",
tenant_id: "quickfade",
account_id: "acc-123"
},
scopes: ["cl:bff:web:profile:read"],
ttlSeconds: 300
});Legacy shortcut, still supported:
const assertion = await signer.signJwtBearerAssertion({
userId: "user-1",
scopes: "cl:bff:web:profile:read"
});Reserved claims cannot be overridden from claims:
isssubaudiatexpjti
Standard Errors
import { AppError, UnauthorizedError, ForbiddenError, presentError } from "@smb-tech/service-framework-js";
throw new ForbiddenError("Missing required scope: cl:core:profile:read");Error response:
{
"error": "forbidden",
"error_description": "Missing required scope: cl:core:profile:read"
}Provided errors:
InvalidRequestErrorUnauthorizedErrorForbiddenErrorNotFoundErrorConflictErrorUnprocessableEntityErrorExternalServiceErrorInternalServerError
Stack traces are not exposed by default.
Fastify Adapter
import Fastify from "fastify";
import { fastifyErrorHandler, fastifyTracePlugin } from "@smb-tech/service-framework-js";
const app = Fastify();
await app.register(
fastifyTracePlugin({
serviceName: "core-messaging-engine",
serviceVersion: "1.0.0"
})
);
app.get("/health", async () => ({
status: "ok"
}));
app.setErrorHandler(fastifyErrorHandler());Running Examples
The Git repository includes runnable examples. They are development references and are not required at runtime by the published npm package.
examples/next-bffexamples/express-serviceexamples/fastify-serviceexamples/oauth-validationexamples/client-credentialsexamples/jwt-bearer-assertionexamples/rest-client-logging
Demo mode:
npm run example:next
npm run example:expressThe Next and Express examples can run without environment variables in demo mode and accept:
Authorization: Bearer demo-tokenExpress demo:
npm run example:express
curl -i -H "Authorization: Bearer demo-token" http://127.0.0.1:3001/notesRun a script with a real .env.local file:
node --env-file=examples/next-bff/.env.local --import tsx examples/next-bff/route.tsCLI Scripts
Version 0.2.0 publishes a formal CLI. It can run through npx without cloning the repository:
npx @smb-tech/service-framework-js --helpCreate a client assertion:
npx @smb-tech/service-framework-js client-assertion \
--client-id core-auth-service \
--p12 ./client.p12 \
--p12-password secret \
--p12-alias core-auth-service \
--aud https://core-oauth-gateway.onrender.com/oauth2/v1/tokenCreate a JWT bearer assertion with repeated claims:
npx @smb-tech/service-framework-js jwt-bearer-assertion \
--client-id core-auth-service \
--claim user_id=user-1 \
--claim tenant_id=quickfade \
--p12-base64 "$CORE_OAUTH_P12_BASE64" \
--p12-password-base64 "$CORE_OAUTH_P12_PASSWORD_BASE64" \
--p12-alias "$CORE_OAUTH_P12_ALIAS" \
--aud https://core-oauth-gateway.onrender.com/oauth2/v1/token \
--scope cl:bff:web:profile:readCreate a JWT bearer assertion with JSON claims:
npx @smb-tech/service-framework-js jwt-bearer-assertion \
--client-id core-auth-service \
--claims-json '{"user_id":"user-1","tenant_id":"quickfade"}' \
--p12-base64 "$CORE_OAUTH_P12_BASE64" \
--p12-password-base64 "$CORE_OAUTH_P12_PASSWORD_BASE64" \
--p12-alias "$CORE_OAUTH_P12_ALIAS" \
--aud https://core-oauth-gateway.onrender.com/oauth2/v1/token \
--scope cl:bff:web:profile:readEncode local files and passwords:
npx @smb-tech/service-framework-js p12-base64 --p12 ./client.p12
npx @smb-tech/service-framework-js password-base64 --password secretThe repository keeps the equivalent npm run client:assertion, npm run jwt-bearer:assertion, npm run p12:base64 and npm run password:base64 scripts for maintainers.
Supported flags:
--client-id--user-id--claim--claims-json--p12--pfx--p12-base64--p12-password--p12-password-base64--p12-alias--aud--scope--ttl-seconds
Security Guarantees
The package is designed to reduce accidental production leaks:
- tokens are not logged in full
- assertions are not logged in full
- P12/PFX content is not logged
- passwords are not logged
- cookies are not logged
- private keys are never written to disk
- outbound HTTP has timeouts by default
- JWT
alg=noneis rejected - JWT algorithms are allowlisted
- JWT
iss,aud,expandnbfare validated - stack traces are not exposed by default
Production Notes
For Render, Docker, ECS, Lambda and local environments, prefer the P12 path:
const signer = createOAuthSignerFromEnv();This path only requires Node.js and node-forge. It does not require:
- Java
keytoolopenssl- temp files
child_process
Package Exports
Root import:
import {
createTraceContext,
createHttpClient,
OAuthTokenValidator,
ScopeGuard
} from "@smb-tech/service-framework-js";Subpath imports are also available:
import { createHttpClient } from "@smb-tech/service-framework-js/http";
import { OAuthTokenValidator } from "@smb-tech/service-framework-js/oauth";
import { createOAuthSignerFromEnv } from "@smb-tech/service-framework-js/security";
import { createNextBffService } from "@smb-tech/service-framework-js/presets";
import type { ObservabilityHooks } from "@smb-tech/service-framework-js/observability";
import { nextAuthRouteHandler } from "@smb-tech/service-framework-js/framework/next";
import { expressAuthMiddleware } from "@smb-tech/service-framework-js/framework/express";Source Maps
Source maps are intentionally not published in 0.2.0. The package publishes JavaScript output and TypeScript declarations, but omits .map files to keep the npm tarball smaller and avoid shipping TypeScript source content in public npm artifacts.
Development
npm install
npm run lint
npm run typecheck
npm run test
npm run test:package
npm run buildnpm run test:package builds the package and verifies:
- ESM import from
dist/index.js - package self-reference imports such as
@smb-tech/service-framework-js/oauth - CommonJS
require(...) - TypeScript declaration resolution from the generated
.d.tsfiles - installation of the real
npm packtarball in a temporary consumer project - ESM, CommonJS, subpath, declaration and CLI resolution from that installed tarball
Support Policy
This package is maintained for SMB Tech service development. API changes should be coordinated with SMB Tech service owners before publishing new versions.
