diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9c3c2b2..e523b43 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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.
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f734a05..2365375 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -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 }})
@@ -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
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
index 9cf9495..1e54ebc 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1 +1 @@
-package-lock=false
\ No newline at end of file
+package-lock=true
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 247be91..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: node_js
-node_js:
- - "24"
-
-script:
- - npx standard
- - npm run test
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..92b7b8a
--- /dev/null
+++ b/CHANGELOG.md
@@ -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).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..316ef47
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/README.md b/README.md
index e80dcc0..3e4fdfd 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://www.npmjs.com/package/restana)
[](https://www.npmjs.com/package/restana)
[](https://www.npmjs.com/package/restana)
-[](https://github.com/jkyberneees/restana)
+[](https://github.com/BackendStack21/restana)
@@ -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
@@ -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()`:
@@ -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)
diff --git a/demos/static/app-cache.js b/demos/static/app-cache.js
index 7aefd94..841fd62 100644
--- a/demos/static/app-cache.js
+++ b/demos/static/app-cache.js
@@ -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'), {
diff --git a/docs/README.md b/docs/README.md
index 571a436..e969b5a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,7 +3,7 @@
[](https://www.npmjs.com/package/restana)
[](https://www.npmjs.com/package/restana)
[](https://www.npmjs.com/package/restana)
-[](https://github.com/jkyberneees/restana)
+[](https://github.com/BackendStack21/restana)
@@ -34,7 +34,7 @@ Install
```bash
npm i restana
```
-Create unsecure API service:
+Create an HTTP API service:
```js
const restana = require('restana')
@@ -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.
@@ -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
@@ -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:
@@ -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) => {
@@ -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
@@ -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)
}
@@ -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
@@ -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.
diff --git a/docs/index.html b/docs/index.html
index 7b6aa9f..258e3da 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -199,6 +199,19 @@
}
.scroll-top.visible { opacity: 1; transform: translateY(0); }
.scroll-top:hover { border-color: var(--accent); color: var(--accent); }
+ .skip-link {
+ position: fixed; left: 12px; top: -80px; z-index: 1000;
+ padding: 10px 14px; background: var(--bg-card); color: var(--text-primary);
+ }
+ .skip-link:focus { top: 12px; }
+
+ @media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ }
+ }
/* ── Responsive ── */
@media (max-width: 768px) {
@@ -212,6 +225,7 @@
Restana 6.1 strengthens production behavior without trading away routing performance.
+
+
+ ✓
+
Reliable lifecycle
+
service.start() now rejects cleanly on socket and port conflicts, making startup failures observable and recoverable.
+
+
+ 🔐
+
Explicit trust
+
Error details are opt-in for local debugging, while forwarded TLS headers require an explicitly trusted proxy.
+
+
+ ⚙
+
Stronger API contracts
+
Boolean bodies, array-valued headers, cache disabling, and TypeScript declarations now match documented behavior.
+
+
+ ↗
+
Performance guarded
+
Response and routing benchmarks now run with CI budgets, including a direct router-integration overhead check.
+
+
+
+
+
@@ -267,7 +314,7 @@
Blazing Fast
🛡
Secure by Default
-
Production error masking, header injection protection, security headers, TRACE disabled, and deep-frozen config — all on by default.
+
Default error masking, header injection protection, security headers, TRACE disabled, and isolated config snapshots — all on by default.
🔗
@@ -298,13 +345,13 @@
Minimal Footprint
Security
Safe Out of the Box
-
restana v6.0 ships with hardened security defaults. No extra packages needed.
+
restana v6.1 ships with hardened security defaults. No extra packages needed.
🔒
-
Production Error Masking
-
Default error handler returns generic Internal Server Error. In NODE_ENV=production, res.send(err) strips stack traces and internal details. Customize with your own errorHandler.
+
Safe Error Masking
+
Error details are hidden in every environment by default. Local development can explicitly opt in with debugErrors: true; production always masks details.
@@ -318,7 +365,7 @@
Header Injection Protection
🛡
Default Security Headers
-
Every response gets X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and X-XSS-Protection: 0. Strict-Transport-Security is set automatically on TLS connections. Disable with securityHeaders: false.
+
Every response gets X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and X-XSS-Protection: 0. HSTS is set for direct TLS or explicitly trusted proxies.
@@ -332,7 +379,7 @@
TRACE Disabled by Default
❄️
Immutable Configuration
-
getConfigOptions() returns a deep-frozen copy. Nested plain objects are frozen too — middleware can't mutate internal framework options at runtime.
+
getConfigOptions() returns an isolated snapshot. Nested plain objects and arrays are cloned and frozen so middleware cannot mutate framework options.
@@ -452,7 +499,8 @@
1. HTTP Routing
2. The res.send() Method
One method handles every response type. No res.json(), res.text(), or res.stream() needed.