@basenative/middleware

中文文档
3.3.0发布于 2026/11/03Apache-2.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 1926

Request pipeline middleware — security headers, CORS, rate limiting, CSRF, and adapters

原始使用文档
文档目录9 节
  1. Install
  2. Quick Start
  3. API
  4. Pipeline
  5. Context Shape
  6. Built-in Middlewares
  7. Security Headers
  8. Framework Adapters
  9. License

@basenative/middleware

Server-agnostic middleware pipeline with CORS, rate limiting, CSRF, and framework adapters

Part of the BaseNative ecosystem — a signal-based web runtime over native HTML.

Install

npm install @basenative/middleware

Quick Start

import { createPipeline, cors, rateLimit, csrf, logger } from '@basenative/middleware';
import { toExpressMiddleware } from '@basenative/middleware';

const pipeline = createPipeline()
  .use(cors({ origins: ['https://example.com'] }))
  .use(rateLimit({ max: 100, windowMs: 60_000 }))
  .use(csrf())
  .use(logger());

// Use with Express
const app = express();
app.use(toExpressMiddleware(pipeline));

API

Pipeline

  • createPipeline() — Creates a middleware pipeline. Returns an object with:
    • .use(middleware) — Appends a middleware function (ctx, next) => Promise<void>.
    • .run(ctx) — Executes the pipeline with a context object.
    • .toHandler() — Returns the pipeline as a single handler function.
    • .stack — Read-only array of registered middlewares.
  • compose(...middlewares) — Combines multiple middleware functions into one.

Context Shape

Each middleware receives a context object:

{ request: { method, url, headers, body }, response: { status, headers, body }, state: {} }

Built-in Middlewares

  • cors(options?) — Sets CORS headers. Options: origins, methods, headers, credentials, maxAge.
  • rateLimit(options?) — In-process rate limiter. Options: max, windowMs, keyFn, message.
  • csrf(options?) — Double-submit cookie CSRF protection. Options: cookieName, headerName.
  • logger(options?) — Request/response logging middleware. Options: format, skip.
  • securityHeaders(options?) — Response hardening headers (CSP, HSTS, Permissions-Policy, COOP/COEP/CORP, cache stance) for Workers-style handlers. See below.

Security Headers

import { securityHeaders } from '@basenative/middleware/security-headers';

One hardened baseline, shared. Products supply what is genuinely theirs — which hosts they talk to, whether they want to be indexed, how long a response may be cached — and the package supplies everything else, so adding a CDN to script-src can never silently drop frame-ancestors 'none'.

Adopting it in a new product

import { securityHeaders } from '@basenative/middleware/security-headers';

// Configure once, at module scope. Options are validated eagerly: a bad
// directive throws at boot, not on the first request that needs it.
const harden = securityHeaders({
  csp: {
    // Merged INTO the baseline. 'self' stays; this adds to it.
    'script-src': ['https://cdn.example.com'],
    'connect-src': ['https://api.example.com'],
  },
  hsts: { preload: true },
  cache: ({ contentType }) =>
    contentType.startsWith('application/json') ? 'private, no-store' : null,
});

export default {
  async fetch(request, env, ctx) {
    const response = await handle(request, env, ctx);
    return harden(response, { request });
  },
};

That is the whole adoption. harden accepts any Response — including the immutable one env.ASSETS.fetch() returns — and gives back a new one with the status, statusText, body and every unrelated header intact.

What you get without asking

HeaderDefaultWhy this value
Content-Security-Policydefault-src 'self', script-src 'self', style-src 'self', style-src-attr 'none', img-src 'self' data:, font-src 'self', connect-src 'self', object-src 'none', frame-ancestors 'none', base-uri 'self', form-action 'self', upgrade-insecure-requestsDeny by default; every allowance is something a product asked for by name
Strict-Transport-Securitymax-age=31536000; includeSubDomainsOne year. preload is opt-in — it is a submission to a browser-vendor list that is slow and painful to leave
X-Frame-OptionsDENYRedundant with frame-ancestors, still graded by scanners and honoured by older engines
X-Content-Type-OptionsnosniffNot configurable. There is no response this is wrong for
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=(), usb=()Deny the high-risk features; opt one back in per feature
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Resource-Policyomittedsame-origin breaks any product serving assets to a sibling subdomain. Set it deliberately (corp: 'same-site') rather than discovering it in an outage
Cross-Origin-Embedder-PolicyomittedRequires every cross-origin subresource to opt in; enable it when you have audited them
X-Robots-Tagomittednoindex: true on an internal tool; wrong on a marketing site, so never a default

Merge semantics — the part worth reading twice

CSP directives merge. A caller source list is unioned into the baseline's, deduplicated, baseline first. csp: { 'connect-src': ['https://api.example.com'] } yields connect-src 'self' https://api.example.com. A directive the baseline does not have is added as given.

Permissions-Policy features replace. Unioning a deny with an allow is meaningless, so permissions: { camera: ['self'] } opts that one feature back in and leaves every other default denial standing.

null removes. In either map, null drops that entry. It is the only way to lose a baseline protection, and it has to be written out:

securityHeaders({
  csp: { 'upgrade-insecure-requests': null },  // local dev over plain http
  permissions: { usb: null },                   // do not send the feature at all
});

csp: false sends no CSP at all, so a product that has never had one can adopt the package for the other seven headers today and write its policy later. Same for hsts: false, coop: false, frameOptions: false.

Nonces

nonce: true generates a fresh base64 nonce per response and appends it to script-src (configurable via nonceDirectives). Use it when something injects scripts into your markup after you return — a Cloudflare Web Analytics beacon, a Bot Fight Mode snippet — so those tags run without 'unsafe-inline'.

When the body itself carries the nonce, generate it first and hand it back:

import { securityHeaders, createNonce } from '@basenative/middleware/security-headers';

const harden = securityHeaders({ nonce: true });

const nonce = createNonce();
const html = renderPage({ nonce });           // <script nonce="...">
return harden(new Response(html, { headers }), { nonce });

Passing a nonce without nonce: true throws. Silently dropping it would block the inline script in production with nothing failing locally.

Per-request policy

When the policy varies by request — a per-tenant camera allow-list, a preview-only CORP — build the record instead of decorating a response:

import { buildSecurityHeaders } from '@basenative/middleware/security-headers';

const headers = buildSecurityHeaders({
  permissions: { camera: tenant.allowsScanner ? ['self'] : [] },
  corp: env.ENVIRONMENT === 'production' ? 'same-site' : 'cross-origin',
});

return new Response(html, { headers: { ...headers, 'Content-Type': 'text/html' } });

buildSecurityHeaders never includes Cache-Control; that is derived from a response, so only securityHeaders() applies it.

Cache stance

cache is called with { contentType, status, request, response }. Return a Cache-Control value to set it, or null/undefined to leave whatever the handler already chose:

cache: ({ contentType, status }) => {
  if (status !== 200) return null;
  if (contentType.startsWith('text/html')) return 'public, max-age=300';
  return null;
},

Inside a pipeline

securityHeadersMiddleware(options) is the same headers as a createPipeline() stage. It runs downstream middleware first so cache sees the content type the handler chose, and publishes the nonce on ctx.state.cspNonce.

Errors it will throw at you

Every one of these is a failure that has actually shipped somewhere, now caught at construction:

  • hsts: { preload: true, includeSubDomains: false } — the preload list rejects the submission, so the directive claims eligibility it does not have.
  • permissions: { bluetooth: [] } — not a registered feature. Browsers ignore it and log a console warning on every response.
  • an unknown option key — a typo silently drops a header, so it is rejected.
  • a CSP source containing ; — it would inject a second directive.
  • a nonce with no CSP, or targeting a directive that was removed.

API

  • securityHeaders(options?) → (response, context?) => Response
  • buildSecurityHeaders(options?, context?) → Record<string, string>
  • securityHeadersMiddleware(options?) → pipeline MiddlewareFn
  • createNonce(byteLength = 16) → base64 string
  • DEFAULT_CSP, DEFAULT_PERMISSIONS — frozen, for inspection

context is { request?, nonce? }. request is passed through to cache and is never inspected for header construction.

Framework Adapters

  • toExpressMiddleware(pipeline) — Wraps a pipeline for use as Express middleware.
  • toHonoMiddleware(pipeline) — Wraps a pipeline for use as Hono middleware.
  • toFastifyPlugin(pipeline) — Wraps a pipeline as a Fastify plugin.
  • toCloudflareHandler(pipeline) — Wraps a pipeline as a Cloudflare Workers fetch handler.

License

Apache-2.0

版本与发布记录

@basenative/middleware 共发布 14 个版本,首次发布于 2026/03/20;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.3.0latest2026/11/035 天前
3.2.02026/11/035 天前
3.1.02026/11/017 天前
3.0.02026/10/317 天前
2.4.02026/10/2613 天前
2.3.22026/10/2217 天前

关于 @basenative/middleware 的常见问题

@basenative/middleware 是什么?

Request pipeline middleware — security headers, CORS, rate limiting, CSRF, and adapters

如何安装 @basenative/middleware?

在项目目录运行 npm install @basenative/middleware,或使用 pnpm add / yarn add / bun add。

@basenative/middleware 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@basenative/middleware 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@basenative/middleware 可以商用吗?

@basenative/middleware 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@basenative/middleware 的下载量有多少?

根据 npm 官方下载统计,@basenative/middleware 在 2026/10/08 – 2026/11/06 这 30 天内被下载 1,926 次,日均约 64 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@basenative/middleware 还在维护吗?

最新版本 3.3.0 发布于 2026/11/03(5 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/03/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。