
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@emeryld/rrroutes-server
Advanced tools
<!-- Summary: - Added full quick start for binding finalized contracts to Express with typed controllers, ctx builders, derived upload middleware, and output validation. - Documented debug options, per-route overrides, partial/complete registration helper
Express/Socket.IO bindings for RRRoutes contracts. Map finalized leaves to an Express router with Zod-validated params/query/body/output, typed controller maps, ctx-aware middleware, optional upload derivation, and structured debug logging. Socket helpers add validated events, heartbeat, room hooks, and lifecycle debugging.
pnpm add @emeryld/rrroutes-server express socket.io
# or
npm install @emeryld/rrroutes-server express socket.io
This package peers with @emeryld/rrroutes-contract and bundles zod.
import express from 'express'
import { finalize, resource } from '@emeryld/rrroutes-contract'
import { createRRRoute, defineControllers } from '@emeryld/rrroutes-server'
import multer from 'multer'
import { z } from 'zod'
// 1) Build & finalize contracts (usually elsewhere in your app)
const leaves = resource('/api')
.sub(
resource('profiles')
.get({
outputSchema: z.array(
z.object({ id: z.string().uuid(), name: z.string() }),
),
description: 'List profiles',
})
.sub(
resource(':profileId', undefined, z.string().uuid())
.patch({
bodySchema: z.object({ name: z.string().min(1) }),
outputSchema: z.object({ id: z.string().uuid(), name: z.string() }),
})
.sub(
resource('avatar')
.put({
bodyFiles: [{ name: 'avatar', maxCount: 1 }], // derive upload middleware
bodySchema: z.object({ avatar: z.instanceof(Blob) }),
outputSchema: z.object({ ok: z.literal(true) }),
})
.done(),
)
.done(),
)
.done(),
)
.done()
const registry = finalize(leaves)
// 2) Wire Express with ctx + derived upload middleware
const app = express()
const server = createRRRoute(app, {
buildCtx: async (req) => ({
user: await loadUser(req),
routesLogger: console,
}), // ctx lives on res.locals[CTX_SYMBOL]
middleware: {
postCtx: [
({ ctx, next }) => {
if (!ctx.user) throw new Error('unauthorized')
next()
},
],
},
multerOptions: (files) =>
files && files.length > 0 ? { storage: multer.memoryStorage() } : undefined,
validateOutput: true, // parse handler returns with outputSchema (default true)
debug: {
request: true,
handler: true,
verbose: true,
logger: (e) => console.debug(e),
},
})
// 3) Author controllers with enforced keys/types
const controllers = defineControllers<
typeof registry,
{ user: { id: string } }
>()({
'GET /api/profiles': {
handler: async ({ ctx }) => {
return fetchProfilesFor(ctx.user.id)
},
},
'PATCH /api/profiles/:profileId': {
before: [
({ ctx, params, next }) =>
params.profileId === ctx.user.id
? next()
: next(new Error('Forbidden')),
],
handler: async ({ params, body }) => {
return updateProfile(params.profileId, body)
},
},
'PUT /api/profiles/:profileId/avatar': {
handler: async ({ req, params }) => {
const avatar = (req.files as any)?.avatar?.[0]
await storeAvatar(params.profileId, avatar?.buffer)
return { ok: true }
},
},
})
server.registerControllers(registry, controllers)
server.warnMissingControllers(registry, console) // warns in dev about unhandled leaves
app.listen(3000)
import { defineControllers, bindExpressRoutes } from '@emeryld/rrroutes-server'
const controllers = defineControllers<typeof registry, Ctx>()({
'POST /v1/articles': {
handler: async ({ body, ctx }) => createArticle(ctx.user.id, body),
},
})
// register only the controllers provided (missing keys are ignored)
bindExpressRoutes(app, registry, controllers, {
buildCtx: () => ({ user: { id: '123' } }),
})
// or enforce every key is present at compile time
bindExpressRoutes(
app,
registry,
controllers as { [K in keyof typeof registry.byKey]: any },
{ buildCtx },
)
If you need access to the parsed params/query/body inside buildCtx, destructure them from the single argument:
const server = createRRRoute(app, {
buildCtx: ({ params, query, body }) => ({
user: lookupUser(params.id),
verbose: query?.verbose === 'yes',
}),
})
buildCtxnow receives the{ req, res, params, query, body }object; the legacy(req, res)signature is no longer supported.
defineControllers<Registry, Ctx>()(map) keeps literal "METHOD /path" keys accurate and infers params/query/body/output types per leaf.registerControllers accepts partial maps (missing routes are skipped); bindAll enforces completeness at compile time.warnMissingControllers(router, registry, logger) inspects the Express stack and warns for any leaf without a handler.batchLeaf)Use batchLeaf to register one endpoint that dispatches multiple already-registered controllers by alias, while each entry carries its encoded route key.
import { batchLeaf } from '@emeryld/rrroutes-server'
const server = createRRRoute(app, { buildCtx })
server.registerControllers(registry, controllers)
// Defaults to POST. You can override with { method: 'put' } etc.
batchLeaf(server, '/v1/batch', registry)
Client request body shape:
{
getUser: {
encodedLeaf: encodeURIComponent('GET /v1/users/:userId'),
params: { userId: 'u_1' },
},
updateUser: {
encodedLeaf: encodeURIComponent('PATCH /v1/users/:userId'),
params: { userId: 'u_1' },
body: { name: 'Emery' },
},
}
Response shape (same alias keys):
{
getUser: { out: { ... }, meta: ... },
updateUser: { out: { ... }, meta: ... },
}
Notes:
batchLeaf(...); unknown keys fail at runtime.server.invoke(...) in parallel for each entry, so each sub-call runs per-leaf parsing, buildCtx, route.before, handler execution, and output validation.sanitizer, preCtx, postCtx, Multer).Order: sanitizer → preCtx → resolve → ctx → postCtx → route.before → handler.
import { getCtx, CtxRequestHandler } from '@emeryld/rrroutes-server'
const audit: CtxRequestHandler<Ctx> = ({ ctx, req, next }) => {
ctx.routesLogger?.info?.('audit', { user: ctx.user?.id, path: req.path })
next()
}
const server = createRRRoute(app, {
buildCtx: (req, res) => ({ user: res.locals.user, routesLogger: console }),
middleware: { postCtx: [audit] },
})
const routeBefore = ({ params, query, body, ctx, next }) => {
ctx.routesLogger?.debug?.('route.before payload', { params, query, body })
next()
}
// Inside any Express middleware (even outside route.before), use getCtx to retrieve typed ctx:
app.use((req, res, next) => {
const ctx = getCtx<Ctx>(res)
ctx?.routesLogger?.debug?.('in arbitrary middleware')
next()
})
CtxRequestHandler receives { req, res, next, ctx } with your typed ctx.route.before handlers now receive the same parsed params, query, and body payload as the handler, alongside req, res, and ctx.res.on('finish', handler) inside route.before/middleware.postCtx instead of relying on a dedicated "after" stage.Use middleware.sanitizer when you want to sanitize raw request data before RRRoutes parses params/query/body.
const server = createRRRoute(app, {
buildCtx,
middleware: {
sanitizer: {
trimStrings: true,
customSanitizer: (value, context) => {
if (context.target === 'query' && typeof value === 'string') {
return value.toLowerCase()
}
return value
},
},
},
})
By default, the sanitizer:
__proto__, prototype, constructor) unless stripBlockedKeys: falsetrimStrings: true is setprofiler: trueblockedKeys exists to prevent prototype-pollution payloads from surviving into downstream object merges.
For full sanitizer docs/options, see ./SANITIZER.md.
Routes that declare bodyFiles automatically run Multer before ctx using shared memory storage. Override or disable that behavior with multerOptions.
import multer from 'multer'
import { FileField } from '@emeryld/rrroutes-contract'
const diskStorage = multer.diskStorage({
destination: 'tmp/uploads',
filename: (_req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`),
})
const server = createRRRoute(app, {
buildCtx,
multerOptions: (files: FileField[] | undefined) =>
files?.length
? {
storage: diskStorage,
limits: { fileSize: 5 * 1024 * 1024 },
}
: false,
})
Return false from multerOptions when you want to skip Multer for a specific route even if bodyFiles are declared.
validateOutput: true parses handler return values with the leaf outputSchema. Set to false to skip.send to change response behavior (e.g., res.status(201).json(data)).const server = createRRRoute(app, {
buildCtx,
send: (res, data) => res.status(201).json({ data }),
})
Global debug options:
const server = createRRRoute(app, {
buildCtx,
debug: {
request: true, // register/request/handler/buildCtx event toggles
handler: true,
verbose: true, // include params/query/body/output/errors
only: ['users:list'], // filter by RouteDef.debug?.debugName
logger: (event) => console.log('[route-debug]', event),
},
})
Per-route overrides:
server.register(registry.byKey['GET /api/profiles'], {
debug: { handler: true, debugName: 'profiles:list' },
handler: async () => [],
})
Context logger passthrough: if buildCtx provides routesLogger, handler debug events also flow to that logger (useful for request-scoped loggers).
finalize([...usersLeaves, ...projectsLeaves]), then register once.bindAll(...) for compile-time coverage or call warnMissingControllers(...) during startup to surface missing routes.route.before per controller (e.g., role checks) and keep middleware.postCtx minimal (auth/session parsing).@emeryld/rrroutes-server also ships a typed Socket.IO wrapper that pairs with defineSocketEvents from the contract package.
import { Server } from 'socket.io'
import { defineSocketEvents } from '@emeryld/rrroutes-contract'
import {
createSocketConnections,
createConnectionLoggingMiddleware,
} from '@emeryld/rrroutes-server'
import { z } from 'zod'
const { config, events } = defineSocketEvents(
{
joinMetaMessage: z.object({ room: z.string() }),
leaveMetaMessage: z.object({ room: z.string() }),
pingPayload: z.object({ sentAt: z.string() }),
pongPayload: z.object({
sentAt: z.string(),
sinceMs: z.number().optional(),
}),
},
{
'chat:message': {
message: z.object({
roomId: z.string(),
text: z.string(),
userId: z.string(),
}),
},
},
)
const io = new Server(3000, { cors: { origin: '*', credentials: true } })
io.use(createConnectionLoggingMiddleware({ includeHeaders: false }))
const sockets = createSocketConnections(io, events, {
config,
heartbeat: { enabled: true }, // enables sys:ping/sys:pong using config schemas
sys: {
'sys:connect': async ({ socket, complete }) => {
socket.data.user = await loadUserFromHandshake(socket.handshake)
await complete() // attach built-ins (ping/pong, join/leave)
},
'sys:ping': async ({ socket, ping }) => ({
sentAt: ping.sentAt,
sinceMs: Date.now() - Date.parse(ping.sentAt),
}),
},
debug: {
register: true,
handler: true,
emit: true,
verbose: true,
logger: (e) => console.debug('[socket-debug]', e),
},
})
// Validate inbound payloads + emit envelopes
sockets.on('chat:message', async (payload, ctx) => {
await saveMessage(payload, ctx.user)
// broadcast to room participants
sockets.emit('chat:message', payload, payload.roomId)
})
// Graceful shutdown
process.on('SIGTERM', () => sockets.destroy())
<event>:error with Zod issues.sys:connect, sys:disconnect, sys:ping, sys:pong, sys:room_join, sys:room_leave.heartbeat.enabled !== false) and uses config.pingPayload / config.pongPayload schemas.destroy() removes listeners, room handlers, and connection hooks—safe for test teardown.res.on('finish', handler) from a middleware in the normal pipeline if you need to observe completed responses.compilePath/param parsing exceptions bubble to Express error handlers; wrap buildCtx/middleware in try/catch if you need custom error shapes.validateOutput is true and no outputSchema exists, raw handler output is passed through.multerOptions runs only when leaf.cfg.bodyFiles is a non-empty array; return false to disable the upload middleware for that route.emit will throw on invalid payloads; handle errors around broadcast loops.Run from repo root:
pnpm --filter @emeryld/rrroutes-server build # tsup + d.ts
pnpm --filter @emeryld/rrroutes-server typecheck
pnpm --filter @emeryld/rrroutes-server test
FAQs
<!-- Summary: - Added full quick start for binding finalized contracts to Express with typed controllers, ctx builders, derived upload middleware, and output validation. - Documented debug options, per-route overrides, partial/complete registration helper
We found that @emeryld/rrroutes-server demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.