Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,19 @@ A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
1. Include a minimal Restana service.
2. Include the request that triggers the issue.
3. Include the actual response, error, or stack trace.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]

**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Environment**
- Restana version:
- Node.js version:
- Package manager and version:
- Operating system:
- Relevant middleware or reverse proxy:

**Additional context**
Add any other context about the problem here.
32 changes: 27 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
name: tests
on: [push, pull_request]
on:
push:
branches: [master]
pull_request:

concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
testing:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [24.x]
node-version: [24.x, 26.x]
steps:
- uses: actions/checkout@v4
- name: Setup Environment (Using NodeJS ${{ matrix.node-version }})
Expand All @@ -15,10 +26,21 @@ jobs:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm install
run: npm ci

- name: Linting
run: npx standard
run: npm run lint

- name: Check TypeScript declarations
run: npm run test:types

- name: Run tests
run: npm run test
run: npm run test

- name: Run performance smoke test
if: matrix.node-version == '24.x'
run: npm run bench:ci

- name: Audit production dependencies
if: matrix.node-version == '24.x'
run: npm audit --omit=dev
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
package-lock=false
package-lock=true
7 changes: 0 additions & 7 deletions .travis.yml

This file was deleted.

27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

## 6.1.0

### Added
- Explicit `trustProxy` and `debugErrors` configuration.
- TypeScript declaration tests and performance smoke checks.
- Reproducible dependency installs through `package-lock.json`.

### Changed
- Listen failures now reject `service.start()` instead of escaping as unhandled errors.
- Error details are masked by default in every environment.
- Boolean bodies, array-valued headers, and `routerCacheSize: 0` behave as documented.
- Configuration snapshots clone and freeze nested arrays and circular plain objects.
- Stream failures use the configured error handler when a response can still be sent.
- Forwarded protocol headers require explicit proxy trust.

### Removed
- Obsolete `disableResponseEvent` references.
- Install-time survey output, legacy Travis configuration, and a broken performance demo.

## 6.0.0

Security-focused release that introduced safe default errors, response-header validation,
default browser security headers, deeply frozen configuration, and opt-in TRACE support.

Earlier release notes remain available in [the full documentation](docs/README.md#breaking-changes).
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Contributing

Restana requires Node.js 24 or newer.

```bash
npm ci
npm run check
```

Tests must listen on an ephemeral loopback port:

```js
const server = await service.start(0, '127.0.0.1')
```

Run response-path benchmarks before and after hot-path changes:

```bash
npm run bench
```

When changing the public API, update `index.d.ts`, `specs/types.test.ts`, the root
README, and `docs/README.md`. Security-sensitive behavior requires a regression
test. Performance claims require a repeatable benchmark rather than an isolated
micro-optimization result.

Keep changes focused and use Conventional Commit-style messages where practical.
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![NPM Total Downloads](https://badgen.net/npm/dt/restana)](https://www.npmjs.com/package/restana)
[![License](https://badgen.net/npm/license/restana)](https://www.npmjs.com/package/restana)
[![TypeScript support](https://badgen.net/npm/types/restana)](https://www.npmjs.com/package/restana)
[![Github stars](https://badgen.net/github/stars/jkyberneees/restana?icon=github)](https://github.com/jkyberneees/restana)
[![Github stars](https://badgen.net/github/stars/BackendStack21/restana?icon=github)](https://github.com/BackendStack21/restana)

<img src="docs/restana-logo.svg" width="400">

Expand All @@ -21,14 +21,14 @@ Install
```bash
npm i restana
```
Create unsecure API service:
Create an HTTP API service:
```js
const restana = require('restana')

const service = restana()
service.get('/hi', (req, res) => res.send('Hello World!'))

service.start(3000);
service.start(3000)
```
Creating secure API service:
```js
Expand All @@ -43,7 +43,7 @@ const service = restana({
})
service.get('/hi', (req, res) => res.send('Hello World!'))

service.start(3000);
service.start(3000)
```

Using `http.createServer()`:
Expand All @@ -57,11 +57,36 @@ service.get('/hi', (req, res) => res.send('Hello World!'))
http.createServer(service).listen(3000, '0.0.0.0')
```

# Security Defaults
# Security defaults
Restana ships with secure defaults out of the box:
- **Error handling**: The default error handler returns a generic `Internal Server Error` message, preventing internal details (stack traces, database errors, file paths) from leaking to clients. Provide a custom `errorHandler` to control what gets exposed.
- **Stream safety**: Stream errors are handled gracefully, preventing connection leaks.
- **Immutable config**: `getConfigOptions()` returns a frozen copy, preventing middleware from mutating internal framework options.
- **Response headers**: Browser hardening headers are enabled by default, and connection-specific or cookie headers cannot be injected through `res.send()`.
- **Proxy safety**: Forwarded protocol headers are ignored unless `trustProxy: true` is explicitly configured.

For local-only development, bind explicitly to loopback:
```js
service.start(3000, '127.0.0.1')
```

When TLS terminates at a trusted reverse proxy:
```js
const service = restana({ trustProxy: true })
```

Error details are hidden by default in every environment. For local debugging only, opt in with `debugErrors: true`; production mode always masks details.

## 6.1 highlights
- Reliable `start()` rejection on port and socket errors.
- Isolated, deeply frozen configuration snapshots.
- Boolean response bodies and array-valued headers.
- Correct `routerCacheSize: 0` behavior.
- Updated TypeScript API, reproducible installs, and route-scaling benchmarks.
- Expanded security and performance regression coverage.

# More
- Website and documentation: https://restana.21no.de
- [Full API guide](docs/README.md)
- [Changelog](CHANGELOG.md)
- [Contributing](CONTRIBUTING.md)
4 changes: 1 addition & 3 deletions demos/static/app-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
const files = require('serve-static')
const path = require('path')

const app = require('../../index')({
disableResponseEvent: true
})
const app = require('../../index')()
app.use(require('http-cache-middleware')())

const serve = files(path.join(__dirname, 'src'), {
Expand Down
45 changes: 36 additions & 9 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![NPM Total Downloads](https://badgen.net/npm/dt/restana)](https://www.npmjs.com/package/restana)
[![License](https://badgen.net/npm/license/restana)](https://www.npmjs.com/package/restana)
[![TypeScript support](https://badgen.net/npm/types/restana)](https://www.npmjs.com/package/restana)
[![Github stars](https://badgen.net/github/stars/jkyberneees/restana?icon=github)](https://github.com/jkyberneees/restana)
[![Github stars](https://badgen.net/github/stars/BackendStack21/restana?icon=github)](https://github.com/BackendStack21/restana)

<img src="restana-logo.svg" width="400">

Expand Down Expand Up @@ -34,7 +34,7 @@ Install
```bash
npm i restana
```
Create unsecure API service:
Create an HTTP API service:
```js
const restana = require('restana')

Expand Down Expand Up @@ -84,12 +84,14 @@ Optionally, learn through examples:
- `routerCacheSize`: The router matching cache size, indicates how many request matches will be kept in memory. Default value: `2000`
- `enableTrace`: When `TRUE`, the `TRACE` HTTP method handler is available for debugging purposes. Default value: `FALSE`. ⚠️ Not recommended for production deployments.
- `securityHeaders`: When `TRUE`, default security headers are set on every response. Set to `FALSE` to disable (e.g. when using Helmet or serving non-browser clients). Default value: `TRUE`.
- `trustProxy`: When `TRUE`, trust the first `X-Forwarded-Proto` value when deciding whether to send HSTS. Only enable this behind a reverse proxy that replaces forwarded headers. Default value: `FALSE`.
- `debugErrors`: When `TRUE`, `res.send(error)` includes `error.message` and `error.data` outside production. Use only for local debugging. Default value: `FALSE`.

### Security defaults (v6.0+)
Restana now ships with these security hardening measures enabled by default:
- **Header injection protection**: Security-sensitive and hop-by-hop headers are blocked from the `res.send()` headers parameter.
- **Production error masking**: In `NODE_ENV=production`, `res.send(err)` masks the error message and strips `err.data` to prevent internal details from leaking.
- **Default security headers**: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection: 0`, and `Strict-Transport-Security` (on HTTPS) are set on every response. Disable with `securityHeaders: false`.
- **Error masking**: `res.send(err)` masks the error message and strips `err.data` by default. Local development can opt in with `debugErrors: true`; production always masks details.
- **Default security headers**: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `X-XSS-Protection: 0`, and `Strict-Transport-Security` (on direct HTTPS or a trusted HTTPS proxy) are set on responses. Disable with `securityHeaders: false`.
- **TRACE method disabled by default**: Eliminates Cross-Site Tracing attack surface. Re-enable for debugging via `enableTrace: true` (not recommended in production).
- **Deep frozen config**: `getConfigOptions()` now freezes nested plain objects, not just the top-level copy.

Expand Down Expand Up @@ -155,7 +157,7 @@ service.close().then(()=> {})
```js
const opts = service.getConfigOptions()
```
> `getConfigOptions()` returns a frozen copy of the configuration options. Top-level properties and nested plain objects are frozen, preventing third-party middleware from accidentally or maliciously modifying internal framework options at runtime. The `server` reference is a live object and is excluded from deep freezing.
> `getConfigOptions()` returns an isolated configuration snapshot. Plain objects and arrays are recursively cloned and frozen, preventing third-party middleware from modifying internal framework options. The `server` and other custom class instances remain live references and should not contain secrets.

## Async / Await support
```js
Expand All @@ -170,7 +172,8 @@ service.post('/star/:username', async (req, res) => {
## Sending custom headers
```js
res.send('Hello World', 200, {
'x-response-time': 100
'x-response-time': 100,
vary: ['accept', 'origin']
})
```
> ⚠️ Security-sensitive and hop-by-hop headers are blocked from the `headers` parameter for security reasons:
Expand All @@ -189,6 +192,8 @@ Supported datatypes are:
- Stream (errors on the stream are handled gracefully, terminating the response instead of leaving the connection hanging)
- Promise (recursive promise resolution is capped at a depth of 3 to prevent event loop starvation)

Boolean payloads are serialized as JSON. A number passed as the first argument remains the shorthand for an HTTP status code.

Example usage:
```js
service.get('/promise', (req, res) => {
Expand Down Expand Up @@ -230,7 +235,7 @@ service.get('/throw', (req, res) => {
throw new Error('Upps!')
})
```
> **Note:** When using `res.send(err)` in a custom error handler, the error's `message` and `data` properties will be serialized and sent to the client (in non-production environments). In `NODE_ENV=production`, `res.send(err)` masks the error message and strips `err.data` to prevent internal details from leaking.
> **Note:** `res.send(err)` masks the error's `message` and `data` by default. Set `debugErrors: true` only for local development when detailed responses are required. Production mode always masks details.
### errorHandler not being called?
> Issue: https://github.com/jkyberneees/ana/issues/81

Expand Down Expand Up @@ -378,7 +383,7 @@ service.get('/hello', (req, res) => {
})

// lambda integration
const handler = serverless(app);
const handler = serverless(service)
module.exports.handler = async (event, context) => {
return await handler(event, context)
}
Expand All @@ -401,7 +406,7 @@ service.get('/hello', (req, res) => {
})

// lambda integration
exports = module.exports = functions.https.onRequest(app.callback());
exports = module.exports = functions.https.onRequest(service.callback())
```

## Serving static files
Expand Down Expand Up @@ -484,6 +489,28 @@ service.get('/hello', (req, res) => {
https://goo.gl/forms/qlBwrf5raqfQwteH3

# Breaking changes
## 6.1
> Restana 6.1 improves lifecycle reliability, secure defaults, performance tooling, and TypeScript support.

Added:
- `trustProxy` explicitly enables forwarded-protocol handling for HSTS.
- `debugErrors` explicitly enables detailed local error responses.
- TypeScript coverage for lifecycle, events, callback integration, and TRACE opt-in.
- Reproducible installs and performance smoke checks in CI.

Changed:
- `start()` now rejects on listen errors such as `EADDRINUSE`.
- Error details are masked by default in every environment and remain masked in production.
- Boolean response bodies are serialized as JSON; array-valued response headers are supported.
- `routerCacheSize: 0` now correctly disables route caching.
- Configuration snapshots recursively clone and freeze arrays and support circular plain objects.
- Forwarded protocol headers are ignored unless `trustProxy: true` is configured.
- Connection-specific, proxy-authentication, upgrade, and cookie headers are blocked from the `res.send()` header map.

Removed:
- The obsolete `disableResponseEvent` example and install-time survey.
- Legacy Travis CI configuration and the broken low-level performance demo.

## 6.0
> Restana version 6.0 focuses on security hardening and reducing attack surface.

Expand Down
Loading
Loading