Changelog
All notable changes to shapeguard are documented here. Format follows Keep a Changelog. Versioning follows Semantic Versioning.
[Unreleased]
No unreleased changes.
[0.9.0] — 2026-04-05
Minor release — logger singleton + multi-UI docs system + security fixes. Zero breaking changes.
Security fixes (from senior architect audit)
specJsonXSS in doc UIs — escaped</script>sequences in all HTML templatespatchResponseStripdata leak — sends 500 instead of unstripped sensitive data on schema failurejoiAdapter+yupAdapterallErrorsinverted — fixed:!(opts.allErrors ?? true)is correct- Dockerfile
CMD—src/index.js→dist/index.js(was crashing on every container start) - Rate limiter TOCTOU race — synchronous
Mapoperations eliminate concurrent bypass serveDocsbarerequire()— usescreateRequire(import.meta.url)for ESM safety- CI audit gate — removed
continue-on-error, level set to--audit-level=critical - Docker hardcoded
HTTP_PASSWORD— uses${REDIS_COMMANDER_PASSWORD:-changeme}+.env.example onErrorhook — logs failures instead of silently swallowing themMath.random()for IDs — usescrypto.randomBytes()intoInsomnia/toBruno- Unbounded rate limit Map — periodic
setIntervalcleanup with.unref() tsconfig.json— addedisolatedModules: truedependabot.yml— added pino/pino-pretty major-version protectionERRORS.md— removed brokenshapeguard-error-types.svgimage referenceCONFIGURATION.md— added trust proxy and multi-process rate limit warnings- Lint step added to CI
- Post-publish smoke test added to release.yml
Minor release — new logger export + multi-UI docs system. Zero breaking changes.
Added
import { logger } from 'shapeguard' — shared logger singleton
import { logger } from 'shapeguard'
logger.info('Server started on port 3000')
logger.info({ userId }, 'User logged in')
logger.warn({ attempts: 3 }, 'Rate limit approaching')
logger.error(err as object, 'Payment service failed')
The same instance used by shapeguard() middleware. Auto-selects:
pino (if installed) → winston (if installed) → built-in fallback.
Works without shapeguard() middleware — standalone import.
import { configureLogger } from 'shapeguard'
configureLogger({ level: 'warn', silent: process.env.NODE_ENV === 'test' })
Multi-UI docs system — all CDN-based, zero npm install
Scalar UI (default — modern, beautiful, client code snippets):
import { serveScalar } from 'shapeguard/openapi'
app.use('/docs', serveScalar(spec))
Swagger UI (classic, enhanced with persistent auth + code snippets + dark mode):
import { serveSwaggerUI } from 'shapeguard/openapi'
app.use('/docs', serveSwaggerUI(spec, { theme: 'dark', snippets: true, persist: true }))
Redoc (read-only public portal — Stripe-style):
import { serveRedoc } from 'shapeguard/openapi'
app.use('/api-reference', serveRedoc(spec))
serveDocs() — mount everything at once:
import { serveDocs } from 'shapeguard/openapi'
app.use('/docs', serveDocs(spec, {
ui: 'scalar',
exports: {
json: '/docs/openapi.json',
postman: '/docs/postman.json',
insomnia: '/docs/insomnia.json',
bruno: '/docs/bruno.json',
}
}))
API client exports — pure functions, no dependencies
import { toPostman, toInsomnia, toBruno } from 'shapeguard/openapi'
app.get('/docs/postman.json', (_req, res) => res.json(toPostman(spec)))
app.get('/docs/insomnia.json', (_req, res) => res.json(toInsomnia(spec)))
app.get('/docs/bruno.json', (_req, res) => res.json(toBruno(spec)))
Fixed
console.warninsrc/router/with-shape.ts→process.stderr.write(no console in prod)console.warninsrc/openapi/index.ts→process.stderr.writeconsole.loginsrc/logging/logger.tsis intentional (it IS the fallback logger) — documented
Architecture note
Logger resolution order (automatic):
logger.instanceprovided in config → use it- pino installed → use pino
- winston installed → use winston adapter
- none → built-in console fallback
[0.8.3] — 2026-03-31
CI/CD simplification + test coverage to 90%. No code changes, no breaking changes.
Changed
-
CI/CD workflows reduced from 12 → 4 (ci.yml, release.yml, codeql.yml, auto-merge.yml)
pr-check.ymlremoved — ci.yml already runs on PRsbenchmark.ymlremoved — not useful for a solo developerscorecard.ymlremoved — score is ~60; enable branch protection in GitHub settings to improve itrelease-drafter.yml(workflow + config) removed — manual CHANGELOG.md is the chosen strategygreet.ymlremoved — no community yetlabeler.yml(workflow + config) removed — overhead not worth it for solo devlock.ymlremoved — issues don't accumulate at solo-dev scalestale.ymlremoved — same reasonci.ymlnow handles both push-to-main and PR quality gate (merged from pr-check.yml)ci.ymlchangelog check and bundle size report added for PRsci.ymlnpm audit raised to--audit-level=high(was moderate)
-
Coverage thresholds raised to 90% (was 85%)
Added
-
Tests for previously-uncovered source files:
joiAdapter()— 9 test cases covering parse, safeParse, strip, allErrors, error mappingyupAdapter()— 10 test cases covering parse, safeParse, strip, inner error flatteningwinstonAdapter()— 6 test cases covering arg-order flip, invalid logger detectioncreateDTO()— 10 test cases covering all methods, _isDTO flag, non-Zod rejectionhandle()— 3 test cases covering return shape and error forwardingmockRequest()— 9 test cases covering all options, get(), ip, socketmockResponse()— 10 test cases covering status, json, end, setHeader, shapeguard helpersmockNext()— 5 test cases covering called, error captureisDev— basic type check
-
CHANGELOG strategy is MANUAL (Option A) — single source of truth in CHANGELOG.md. release-drafter auto-notes are not used. Reason: for a solo developer, maintaining one clean CHANGELOG.md is cleaner than having two sources of release notes.
[0.8.2] — 2026-03-31
Patch: Asset cleanup, example updates, docs hygiene. No code changes.
Changed
examples/with-openapi/: updated to demonstrate all v0.8.x features —verifyWebhook(),res.cursorPaginated(),AppError.define(). Stale version comment removed.examples/README.md: updated table withwith-webhookexample.README.md: "What's new" updated to v0.8.1; examples table updated.docs/LOGGING.md: removed stale version reference.
Added
examples/with-webhook/: new example demonstratingverifyWebhook()with Stripe, GitHub, Shopify, and custom provider presets. IncludesGET /demo/signaturefor generating valid test signatures.
Removed
- 14 orphaned SVG assets removed from
assets/— they were not referenced in README or any doc. Active assets:shapeguard-comparison.svg,shapeguard-versions.svg,shapeguard-security.svg.
[0.8.1] — 2026-03-31
Patch: Docker reorganisation, standalone Swagger docs, test fixes, and enterprise CI. Fully backwards-compatible — no breaking changes.
Fixed
- 8 test failures in v0.8.0-features.test.ts and v0.6.1-bugfixes.test.ts
zodAdapter()now exposesschemaproperty —generateOpenAPI()Zod type mapping now works (ZodLiteral, ZodUnion, ZodNumber checks, ZodString format,required[]array)makeReq()test mock now includesheaders: {}andsocket— shapeguard() unit tests no longer crash- BUG#5 logger precedence test rewritten to correctly distinguish errorHandler calls from request-logger calls
Changed
- Docker files moved to
docker/folder —docker/Dockerfile,docker/docker-compose.yml,docker/.dockerignore docker-compose.ymlnow uses 3-stage build (deps/builder/example), named services, Redis auth on Commander- All
npm run docker:*scripts updated to use-f docker/docker-compose.yml docker:cleanscript added to remove volumes and local images
Added
createDocs()/generateOpenAPI()standalone docs — README and OPENAPI.md now lead with the 3-line minimum case. NodefineRoute(), noshapeguard()middleware required.- Docker badge in README
auto-merge.yml— auto-merges Dependabot patch/minor PRs after CI passeslock.yml— locks closed issues/PRs after 30 days inactivitygreet.yml— welcomes first-time contributors with next-steps guidancerelease-drafter.yml+.github/release-drafter.yml— auto-drafts release notes from merged PR titlesrelease.ymlimprovements: pre-release npm tag (next), failure notification with recovery instructions, bundle size guard before publishci.ymlfix: coverage collected (npm run test:coverage) before Codecov uploadvalidate-release.ps1now referenced inCONTRIBUTING.mdrelease processshapeguard-versions.svgupdated — includes v0.6.1, v0.7.0, v0.8.0shapeguard-comparison.svgupdated — includes verifyWebhook, cursorPaginated, AppError.define()
Removed
release.ps1andsetup-project.ps1— replaced byvalidate-release.ps1- Stale images removed from README (
shapeguard-logging.svg,shapeguard-response-shapes.svg)
[0.8.0] — 2026-03-28
Theme: Enterprise completeness. Production-grade createDocs(), cursor pagination, webhook verification, and typed error factories. shapeguard now covers every feature gap vs NestJS/tsoa/Hono in the areas it targets. Fully backwards-compatible — no breaking changes.
Added
createDocs() — enterprise Swagger UI (major upgrade from v0.7.0)
validatorUrl: 'none'— disables external validator.swagger.io calls (all competitor libraries do this; we now do too — eliminates noisy console warnings in browser)docExpansion—'none' | 'list' | 'full'— controls how operations render on load (default:'list')defaultModelsExpandDepth— controls how deeply schema models expand (default: 1; set -1 to collapse all)defaultModelExpandDepth— controls individual model expansion depthoperationsSorter—'alpha' | 'method' | 'none'— sort operations alphabetically or by HTTP methodtagsSorter—'alpha' | 'none'— sort tag groupsshowExtensions— showx-*vendor extensions in the UI (default: false)showCommonExtensions— showx-nullable,x-example, etc.displayOperationId— show operationId badges on each operationmaxDisplayedTags— limit visible tag groupsrequestInterceptor— JavaScript function string injected as Swagger UI'srequestInterceptor. Use to auto-inject auth headers, request IDs, or log outgoing requests. Example:"request.headers['X-Trace'] = crypto.randomUUID(); return request;"responseInterceptor— JavaScript function string for response inspection/loggingwithCredentials— send cookies on Try-It-Out requestsoauth2RedirectUrl— OAuth2 redirect callback URLlogo—{ url, altText?, backgroundColor? }— custom logo above the Swagger UI topbarheadHtml— raw HTML injected before</head>— use for analytics scripts, custom fontscsp— Content-Security-Policy header value. Default: auto-generated safe policy covering CDN scripts and styles. Passfalseto disable. Production APIs should leave this as default.- Security headers —
X-Content-Type-Options: nosniffandX-Frame-Options: DENYset on every docs response
generateOpenAPI() — spec generation improvements
deprecatedflag — setdeprecated: trueon any route definition; renders as a strikethrough in Swagger UIdescriptionper route — separate fromsummary; shown as expanded operation descriptionexternalDocsper route — link to external documentation from any operationextensions—Record<string, unknown>ofx-*vendor extensions merged onto the operation objectbodyType—'json' | 'multipart' | 'form'— controls therequestBodycontent type:'multipart'generatesmultipart/form-datawith automatic file field detection (fields namedfile,image,avatar,attachment, etc. getformat: binary)'form'generatesapplication/x-www-form-urlencoded'json'(default) is unchanged
responseHeaders— document response headers in the 200 schema (e.g.X-Request-Id,Retry-After)- Top-level
tagsarray — define tag objects with descriptions and externalDocs at the spec level - Top-level
externalDocs— link to external API documentation at the spec level termsOfService,contact,licensein specinfoblock- Extended number/integer schemas —
z.number().min().max()andz.number().multipleOf()now produceminimum,maximum,multipleOfin the schema ZodReadonly— producesreadOnly: trueZodTuplewith rest element — variadic tuples map correctly toprefixItems+items- All-literal union optimization —
z.union([z.literal('a'), z.literal('b')])produces{ enum: ['a', 'b'] }instead of{ oneOf: [...] } ZodPipeline— maps to theoutschema (what consumers receive)ZodSymbol,ZodFunction— safe fallbacks instead of crashes- String format additions:
base64→byte,jwt(pattern),nanoid(pattern),cidr,includes→ pattern
res.cursorPaginated() — cursor-based pagination
Cursor pagination is the enterprise standard for large datasets and infinite scroll. Offset pagination (res.paginated()) breaks when data changes between pages — cursors don't.
res.cursorPaginated({
data: users,
nextCursor: users.at(-1)?.id ?? null,
prevCursor: req.query.cursor ?? null,
hasMore: users.length === limit,
total: 1000, // optional
})
// Response:
// { success: true, data: { items: [...], nextCursor: 'user_abc', prevCursor: null, hasMore: true } }
verifyWebhook() — HMAC webhook signature middleware
Zero-dependency webhook verification. Uses crypto.timingSafeEqual() to prevent timing attacks. Supports replay attack prevention (timestamp tolerance window).
import { verifyWebhook } from 'shapeguard'
router.post('/webhooks/stripe',
verifyWebhook({ provider: 'stripe', secret: process.env.STRIPE_SECRET! }),
handler,
)
Built-in presets: stripe (timestamp + replay protection), github (sha256=), shopify (base64 HMAC), twilio (sha1 base64), svix (timestamp + replay protection).
Custom providers:
verifyWebhook({
secret: process.env.MY_SECRET!,
algorithm: 'sha256',
headerName: 'x-my-signature',
prefix: 'sha256=',
encoding: 'hex',
onFailure: (req, reason) => alerting.notify(reason),
})
AppError.define() — typed error factory
Define reusable, TypeScript-safe error constructors once. No more Record<string, unknown> guessing.
const RateLimitError = AppError.define<{ retryAfter: number; limit: number }>(
'RATE_LIMIT_EXCEEDED', 429, 'Too many requests'
)
throw RateLimitError({ retryAfter: 30, limit: 100 })
// ^-- TypeScript error if fields wrong or missing
const PaymentError = AppError.define<{ amount: number; currency: string }>(
'PAYMENT_FAILED', 402
)
throw PaymentError({ amount: 9.99, currency: 'USD' }, 'Payment declined')
Exported
verifyWebhookandWebhookConfigfrom mainshapeguardentryCursorPaginatedDataandResCursorPaginatedOptstypes fromshapeguard
[0.7.0] — 2026-03-28
Theme: Swagger docs that actually work. Two P0 feature gaps closed — security schemes so the padlock button functions, and a built-in
createDocs()endpoint so zero extra packages are needed. Extended Zod type coverage and automatic 400/401/403/429 responses round out enterprise-grade OpenAPI output. Fully backwards-compatible — no breaking changes.
Added
-
securityoption ingenerateOpenAPI()— define named security schemes once (bearer JWT, API key, basic, OAuth2); the Swagger UI padlock button is now fully functional. Previously the padlock rendered but did nothing.generateOpenAPI({
security: {
bearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
apiKey: { type: 'apiKey', in: 'header', name: 'X-API-Key' },
},
defaultSecurity: ['bearer'],
routes: { ... },
}) -
defaultSecurityoption — applies the listed schemes to every operation automatically; override per-route viaroute.security: ['otherScheme']or mark as public withroute.security: []. -
Per-route
securityoverride on inline route definitions —security: string[] | nullon any route definition;[]generatessecurity: []in the spec (explicit public endpoint). -
createDocs()— built-in Swagger UI endpoint — mounts a fully themed, auth-enabled Swagger UI at any path. Noswagger-ui-expressor other extra package needed. CDN-loaded assets, persistent authorization, dark/light/auto theme.import { createDocs } from 'shapeguard'
app.use('/docs', createDocs({ spec, title: 'My API', theme: 'dark' }))
// → http://localhost:3000/docs — works immediately -
DocsConfig,SecuritySchemeType,InlineRouteDefinitionexported from bothshapeguardandshapeguard/openapi. -
Automatic 400 response on all operations — pre-parse guard errors (repeated query param, body too deep, string too long) now appear in the spec.
-
Automatic 401 + 403 responses on secured operations — generated whenever
defaultSecurityor per-routesecurityincludes at least one scheme. -
Automatic 429 response on rate-limited routes — generated whenever the route definition includes a
rateLimitoption; schema includes theretryAfterfield. -
Extended Zod type mapping —
toJsonSchema()now covers:ZodInteger/ZodInt,ZodBigInt(int64),ZodNull,ZodLiteral(withconst+enum),ZodUnion/ZodDiscriminatedUnion(oneOf),ZodIntersection(allOf),ZodTuple(prefixItems),ZodRecord(additionalProperties),ZodSet(uniqueItems),ZodNaN,ZodAny,ZodUnknown,ZodVoid,ZodNever(not: {}),ZodBranded,ZodPipeline,ZodCatch,ZodLazy. Previously these all fell back to{ type: 'string' }. -
requiredarray in object schemas — properties that are notZodOptionalorZodDefaultare now listed in the JSON Schemarequiredarray, making validators and SDK generators behave correctly. -
Extended string format mapping —
z.string().date()→format: date,.time()→format: time,.ip()→format: ipv4,.cuid(),.cuid2(),.ulid(),.startsWith(),.endsWith(),.emoji()all produce correct schema annotations. -
createDocsexported fromshapeguard/openapisubpath — importable from both the main entry and the subpath.
Changed
-
with-openapiexample updated — now usescreateDocs()andsecurityschemes; shows public vs protected route split; demonstratesrateLimitproducing a 429 entry in the spec. -
docs/OPENAPI.mdrewritten — new sections: Security schemes, createDocs() API reference, per-route security override, supported scheme types, extended response table.
[0.6.1] — 2026-03-28
Theme: Security and correctness patch. All 12 confirmed bugs from the v0.6.0 audit fixed. Zero breaking changes — all existing APIs remain compatible.
Security
-
[CRITICAL] PARAM_POLLUTION now actually thrown (
validate.ts) — thePARAM_POLLUTIONerror code was declared, documented, and mapped to HTTP 400, but never fired. Express parses?role=admin&role=userasrole: ['admin','user']— a scalar field receiving an unexpected array. Shapeguard now walks allreq.queryentries before schema validation and throwsPARAM_POLLUTION(400) on the first array-valued parameter. Closes the query-pollution attack vector that previously fell through to a generic 422. -
[CRITICAL] Response stripping no longer silently disabled when shape config renames
data(validate.ts,shapeguard.ts) — whenresponse.shapewas configured to rename thedataenvelope key (e.g.result: '{data}'),patchResponseStripchecked for'data' in bodywhich always failed on the already-shaped response. Sensitive fields (passwordHash,stripeId, etc.) leaked to the client with no error or warning. Fixed by threadingResponseConfigthrough topatchResponseStripand resolving the actual data key via a newgetDataKey()helper.
Fixed
-
[HIGH]
./openapisubpath import now resolves (package.json) —import { generateOpenAPI } from 'shapeguard/openapi'previously threwMODULE_NOT_FOUNDat runtime despite the entry point being built by tsup. Added the missing"./openapi"export condition pointing todist/openapi/index.*. -
[HIGH] Rate limit in-memory store no longer leaks memory (
validate.ts) — expired entries were never removed from_rlStore. On a long-running server with many unique client IPs the Map grew without bound. Stale entries are now deleted before a fresh window entry is written. -
[HIGH]
errorHandler()auto-discoversshapeguard()'s logger (shapeguard.ts,error-handler.ts) —shapeguard()now stores its logger onreq.app.locals['__sg_logger__'].errorHandler()reads it as a fallback when no explicitloggeroption is passed, so 5xx errors are logged through the same structured logger without any manual wiring. Existing explicitlogger:option still takes precedence — zero API change. -
[MEDIUM] Cache-Control headers no longer set before validation result is known (
validate.ts) —applyCacheHeaders()was called beforevalidateRequest()ran, so CDNs (Cloudflare, Fastly, CloudFront) could cache 422 validation-error responses. Headers are now set only aftervalidateRequest()resolves successfully. -
[MEDIUM] Rate limit store isolated per route (
validate.ts) —_rlStorewas a module-level singleton shared across everyvalidate()call in the same process. Two app instances (e.g. dev + prod in integration tests) shared rate limit counters. Eachvalidate()call now closes over its ownMap, fully isolating counters per route and per app instance._clearRateLimitStore()kept for backward compatibility. -
[MEDIUM]
logResponseBodycaptures post-strip body (request-log.ts) — clarified and documented the capture ordering:captureResponseBodyregisters as the inner wrapper;patchResponseStripregisters as the outer wrapper. The inner wrapper is called from inside the strip.then(), so the captured body is always the already-stripped payload (what the client receives), not the pre-strip data. -
[LOW]
winstonadded to tsupexternallist (tsup.config.ts) — previously absent, which meant downstream bundlers could accidentally inline the entire winston package into their output bundle. -
[LOW] Route-level
allErrorsnow controls Joi/Yup error collection (validate.ts) —validate({ allErrors: true })now correctly threads through to Joi/Yup adapter instances vianormalise(). A newmakeAllErrorsAdapter()wrapper respects the route-level flag regardless of how the adapter was created. -
[LOW]
winstonmoved frompeerDependenciestooptionalDependencies(package.json) — winston was listed as a peer dependency (causingnpm installwarnings for users who don't use it). Moved tooptionalDependenciesalongside joi, yup, pino. -
[LOW]
withShape()+validate()middleware ordering documented (docs/RESPONSE.md) — the required mount order (validate()beforewithShape()) is now documented with working and broken examples. Includes an explanation of why the wrong order silently skips field stripping.
Improved
-
Retry-AfterHTTP header set on 429 responses (validate.ts) — the retry window was previously only in the response body (details.retryAfter). RFC 7231 requires theRetry-Afterheader on 429 responses. Load balancers, API gateways, and retry libraries (axios-retry, etc.) read this header natively. Both the header and body field are now set. -
cacheoption: discriminated union —noStoreno longer requiresmaxAge(validate.ts,define-route.ts) —cache: { noStore: true }is now the complete and correct way to disable caching. Previously TypeScript requiredmaxAgeeven though it was ignored whennoStorewas set. -
cacheoption: CDN directivessMaxAgeandstaleWhileRevalidatesupported (validate.ts,define-route.ts) — teams using CDN-fronted APIs can now set separate browser and CDN TTLs:cache: { maxAge: 60, sMaxAge: 300, staleWhileRevalidate: 60 }producesCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60. -
Testing: async strip behaviour documented (
docs/TESTING.md) — added a dedicated section explaining why unit tests asserting on stripped response bodies mustawait Promise.resolve()after calling the handler, with correct and incorrect examples, and a note that supertest integration tests are unaffected.
[0.6.0] — 2026-03-17
Theme: Logger control. Four new options giving teams precise control over what appears in terminal and log files. Every option is independent — use one, some, or all. Zero config change needed for existing apps.
Added
logIncoming: false(LoggerConfig) — hides the>>request arrival lines entirely while keeping<<response lines; useful when you want response times and status codes but not the extra arrival noise in busy terminalsshortRequestId: true(LoggerConfig) — shows only the last 8 characters of the request ID on log lines (e.g.[req_019cfa6f...]→[3a3045a]); the full ID is still generated and forwarded in headers, only the terminal display is shortenedlogClientIp: true(LoggerConfig) — logs the client IP address on each response line; readsx-forwarded-forfirst (load balancer / proxy), then falls back tosocket.remoteAddress; IP is also included in the structured JSON payload asiplineColor: 'level'(LoggerConfig) — colours the entire log line (method + status) based on the response status level (2xx=green,4xx=yellow,5xx=red) instead of the default HTTP method colour (GET=green,POST=cyan,DELETE=red); only affects dev/pretty output — JSON prod logs are unaffected
[0.5.0] — 2026-03-17
Theme: OpenAPI overhaul. Five bugs fixed, three new capabilities added. Fully backwards-compatible — no breaking changes.
Added
prefixoption ingenerateOpenAPI()— passprefix: '/api/v1'once and it is prepended to every route path automatically; no more repeating the prefix on every keyoperationIdauto-generated — every operation now gets a stable, SDK-friendlyoperationIdderived from its method and path (e.g.POST /users/:id→postUsersId); SDK generators no longer produce unnamed operationstagsandsummaryper route — addtagsandsummarydirectly to anydefineRoute()result or inline route definition; Swagger UI groups and labels operations correctly- Inline route definitions (
InlineRouteDefinition) — existing Express apps can now describe schemas directly insidegenerateOpenAPI()without usingdefineRoute()at all; unlocks Swagger for apps that don't want to change their routes
Fixed
- 422 and 500 responses now include the full error envelope schema — previously both had only a
descriptionstring; now each includes the complete{ success, message, error: { code, message, details } }shape thaterrorHandler()actually sends - Duplicate route keys warned and skipped — two routes resolving to the same method + path now emit a
console.warnand keep the first definition; previously the second silently overwrote the first with no indication - Trailing slash creates duplicate paths —
GET /usersandGET /users/now normalise to the same/userspath in the spec; previously they appeared as two separate paths ZodBoolean,ZodNumber,ZodArray,ZodEnum,ZodObjecttype mapping — all Zod types now map correctly to their JSON Schema equivalents (already fixed in v0.4.0 codebase, confirmed and tested in v0.5.0)- Response schema used in 200 envelope — the
responsefield indefineRoute()now populates thedataproperty of the 200 response schema (already fixed in v0.4.0 codebase, confirmed and tested in v0.5.0)
[0.4.0] — 2026-03-17
Theme: Correctness and extensibility. Eight bugs fixed, Winston adapter shipped. Fully backwards-compatible — no breaking changes.
Added
shapeguard/adapters/winston— ships awinstonAdapter()function that bridges Winston's argument order (msg, meta) to shapeguard's Logger interface (meta, msg); import and pass tologger.instance— no manual wrapper needed
Fixed
- Logger instance validated at mount time (
logger.ts) — passing a logger without.debug(),.info(),.warn(), or.error()now throws a clear error immediately listing the missing methods, rather than crashing with aTypeErroron the first request; error message explicitly mentionsshapeguard/adapters/winston withShapewarns on undefined tokens (with-shape.ts) — in development, aconsole.warnis emitted when a template token (e.g.{data.uptime}) does not exist in the response; catches path typos immediately rather than silently sendingundefinedto clients- Global config no longer shared between
shapeguard()instances (validate.ts,shapeguard.ts) — removed thesetFallbackValidationConfigmodule-level singleton; config is now scoped exclusively viares.localsper request, so two app instances running in the same process (e.g. integration tests with dev + prod apps) can no longer overwrite each other's validation config - Joi/Yup
allErrorsoption (adapters/joi.ts,adapters/yup.ts) — both adapters now respect theallErrorsoption passed tojoiAdapter()andyupAdapter(); previouslyabortEarlywas hardcoded totruesoallErrorshad zero effect (already fixed in v0.3.1 codebase, confirmed and tested in v0.4.0) router.route()405 tracking (router/create-router.ts) —router.route('/users').get().post()pattern is now intercepted by the proxy and tracked for 405 Method Not Allowed responses (already fixed in v0.3.1 codebase, confirmed and tested in v0.4.0)Object.freezescoped to envelope only (core/response.ts) —res.created({ data: user })no longer deep-freezes the caller'suservariable; only the response envelope wrapper is frozen (already fixed in v0.3.1 codebase, confirmed and tested in v0.4.0)mockRequestsocket, ip, andreq.get(testing/index.ts) —socket.remoteAddress,ip, andget(header)are now present; rate limiter tests no longer share a single bucket due to unknown IP (already fixed in v0.3.1 codebase, confirmed and tested in v0.4.0)
[0.3.1] — 2026-03-17
Theme: Bug fixes. Six correctness issues found in v0.3.0 audit. Fully backwards-compatible — no API changes.
Fixed
- CJS support (
package.json) — added"require"condition to allexportsentries and a top-level"main"field pointing todist/index.cjs; CJS users no longer receiveERR_REQUIRE_ESMwhen callingrequire('shapeguard') allErrors:truenow returns all issues (AppError.validation()) — previously only the first issue was stored indetails; now the full array is stored when more than one issue is provided, giving clients visibility into every validation failurecreateDTO()docs examples (README.md,docs/VALIDATION.md) — examples showed a plain object being passed tocreateDTO(); the function requires az.object(...)call; all examples corrected- Transform hook no longer swallows
AppError(validate.ts) — athrow AppError.conflict()(or anyAppError) inside atransformfunction was being caught and re-thrown as a generic 500; it is now re-thrown as-is so the correct status and code reach the client slowThresholddefault fixed in dev (request-log.ts) — default was0msin development, making0 > 0always false and theSLOWbadge never visible; default is now500msin dev (1000msin prod unchanged)setMaxListenersleak removed (logger.ts) —process.setMaxListeners(getMaxListeners() + 1)was called on everyshapeguard()mount; with 10+ test instances this causedMaxListenersExceededWarning; pino v8 does not require this call and the line has been removed
[0.3.0] — 2026-03-16
Theme: Production power. Features serious apps need in production. Existing v0.2.x code is fully compatible — no breaking changes.
Added
generateOpenAPI()— auto-generate an OpenAPI 3.1 spec fromdefineRoute()definitions; zero manual schema duplication; serve as/docs/openapi.jsonshapeguard/testing—mockRequest(),mockResponse(),mockNext()helpers; unit-test controllers without spinning up Express or making HTTP requests- Per-route
rateLimitondefineRoute()— built-in rate limiting, no extra package; in-memory per-IP store with configurable window and max requests - Per-route
cacheondefineRoute()— declarativeCache-Controlheaders (maxAge,private,noStore) ErrorCode.RATE_LIMIT_EXCEEDED— new stable error code for rate limit responsesexamples/with-openapi— working example showing OpenAPI generation + swagger-ui-expressexamples/with-testing— working example showing controller unit tests with test helpersdocs/OPENAPI.md— full OpenAPI generation docsdocs/TESTING.md— full testing utilities docs
Changed
joiandyupremoved fromdevDependencies— they are optional peer deps, not dev deps- Repository URL corrected in
package.json tsup.config.ts— addedtesting/indexas separate entry point for tree-shaking
[0.2.0] — 2026-03-16
Theme: Developer experience. Same power, significantly less code. Existing v0.1.x code is fully compatible — no breaking changes.
Added
handle(route, handler)— combinesvalidate()+asyncHandler()into a single function; eliminates the two-element array pattern on every routecreateDTO(fields)— thin wrapper aroundz.object()that auto-infers the TypeScript input type; removes manualz.infer<typeof ...>on every schema definition- Transform hook on
defineRoute()— optionaltransform(data) => dataasync function that runs after validation and before the handler; use for password hashing, field normalisation, sanitization — keeps service layer pure - Global string transforms config —
validation.strings.trimandvalidation.strings.lowercaseoptions inshapeguard(); apply.trim()/.toLowerCase()to all string fields without repeating per-field in every schema logger.silent: true— suppresses all log output; designed for test environments sonpm testoutput is cleanexamples/basic-crud-api/— complete working Express + shapeguard app showing all v0.2.0 features end-to-end:handle(),createDTO(), transform hook,createRouter(),AppError,res.paginated()MIGRATION.md— upgrade guide from v0.1.x to v0.2.0
Changed
- Joi and Yup adapters now documented with full usage examples in
README.mdanddocs/VALIDATION.md; previously only mentioned in the types export res.paginated()now documented with a full example inREADME.md; previously only existed as a type (PaginatedData) with no visible usage exampledocs/VALIDATION.md— new sections forhandle(),createDTO(), transform hook, global string transforms, and params/query/headers examples promoted to Quick Start leveldocs/CONFIGURATION.md— newvalidation.stringssection documenting global string transform config
[0.1.0] — 2026-03-13 — Initial public release
Core middleware
shapeguard()— main middleware factory, mount once inapp.ts- Auto-detects
NODE_ENV— no manualdebugflag needed requestIdconfig block — full control over request ID generation:enabled— disable entirely (default:true)header— read trace ID from upstream first, e.g. load balancer'sx-request-id(default:'x-request-id')generator— custom ID function, e.g.() => crypto.randomUUID()
Validation
validate()— validatesreq.body,req.params,req.query,req.headersvalidate({ allErrors: true })— collect all field issues in one responsevalidate({ limits })— per-route pre-parse limit overridesvalidate({ sanitize })— per-route error exposure configdefineRoute()— bundle all schemas into one reusable definition- Auto-wraps raw Zod schemas — no manual
zodAdapter()call needed zodAdapter(),joiAdapter(),yupAdapter()— explicit adaptersisZodSchema()— detect zod schemas at runtime
Type inference
InferBody<T>,InferParams<T>,InferQuery<T>,InferHeaders<T>— infer types fromdefineRoute()output
Pre-parse guards (always on, before schema)
- Proto pollution blocking —
__proto__,constructor,prototypestripped - Unicode sanitization — null bytes (
\u0000), zero-width chars (\u200B), RTL override (\u202E) removed - Object depth limit — default 20 levels, configurable
- Array length limit — default 1000 items, configurable
- String length limit — default 10,000 chars, configurable
- Content-Type enforcement — POST/PUT/PATCH with a body requires valid Content-Type
Errors
AppError— single error class withisOperationalflag (operational vs programmer errors)AppError.notFound(),.unauthorized(),.forbidden(),.conflict(),.validation(),.internal(),.custom(),.fromLegacy()isAppError()— type guard, works across module boundarieserrorHandler()— centralised error middleware, always mount lastnotFoundHandler()— 404 for unmatched routesasyncHandler()— catches async errors in Express 4
Logging
- FastAPI-style request logging — one clean line per event
>>= request arriving,<<= response leaving — pure ASCII, safe on all terminals including Windows- Color-coded level badges:
[DEBUG]cyan ·[INFO]green ·[WARN]yellow ·[ERROR]red - Colors only activate when
process.stdout.isTTY— no escape codes in CI pipes or file redirects logRequestId— toggle[req_id]on/off in log lines (default:true)- Built-in pino integration (optional peer dep — auto-detected, no crash if absent)
- Console fallback logger with identical format and redaction when pino is not installed
logRequestBody/logResponseBody— include sanitized body in logs (off by default)slowThreshold— SLOW warning on responses over N ms (default: disabled in dev, 1000ms in prod)logAllRequests— log every request, not just errors (default: true in dev, false in prod)- Structured JSON payload field:
duration_ms(self-documenting units) - Always-redacted:
password,passwordHash,token,secret,accessToken,refreshToken,apiKey,cardNumber,cvv,ssn,pin,authorizationheader,cookieheader - Production JSON output: one line per event — Datadog / CloudWatch / Loki ready
Response helpers
res.ok(),res.created(),res.accepted(),res.noContent(),res.paginated(),res.fail()— injected on every routewithShape()— per-route response shape override ('raw'or field map)response.shape— global envelope field renamingresponse.statusCodes— configurable default status per HTTP method- Consistent envelope:
{ success, message, data }/{ success, message, error }
Router
createRouter()— drop-in forexpress.Router()- Automatic 405 Method Not Allowed with
Allowheader - Works correctly for parameterised routes (
/users/:id405s for wrong method)
Types
- Full TypeScript types exported:
ShapeguardConfig,RequestIdConfig,LoggerConfig,ValidationConfig,ResponseConfig,ErrorsConfig,SchemaAdapter,RouteSchema,SuccessEnvelope,ErrorEnvelope,Envelope,PaginatedData,Logger,LogLevel,HttpMethod,ValidationIssue,SafeParseResult - Express augmentation:
req.idtyped asstring, allres.*helpers typed
Error codes
VALIDATION_ERROR · NOT_FOUND · UNAUTHORIZED · FORBIDDEN · CONFLICT · INTERNAL_ERROR · METHOD_NOT_ALLOWED · BODY_TOO_DEEP · BODY_ARRAY_TOO_LARGE · STRING_TOO_LONG · INVALID_CONTENT_TYPE · PARAM_POLLUTION · PROTO_POLLUTION
Build
- ESM output (
dist/index.mjs) - TypeScript declarations for all exports (
dist/index.d.ts) sideEffects: false— fully tree-shakeable- Zero runtime dependencies — pino, joi, yup lazy-loaded only if installed
- Node.js 18+