npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arraypress/security-headers

v2.0.0

Published

Security response headers for static hosts — CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy. Generates a Cloudflare/Netlify _headers file, with an Astro integration. Zero dependencies.

Downloads

361

Readme

@arraypress/security-headers

Security response headers for static hosts — CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy. Generates a Cloudflare/Netlify _headers file, with an Astro integration. Zero dependencies.

Install

npm install @arraypress/security-headers

Astro

// astro.config.mjs
import { defineConfig } from 'astro/config';
import headers from '@arraypress/security-headers/astro';

export default defineConfig({
  security: { csp: true },    // Astro owns CSP — see below
  integrations: [headers()],   // everything else, written to dist/_headers
});

The integration writes _headers on astro:build:done, so there's no separate build script to remember. It logs what it wrote:

[@arraypress/security-headers] wrote _headers — 7 headers on /*

Why a file and not middleware

On Cloudflare, a static-assets deploy with no server script serves requests for free. Adding middleware to set headers adds a script and makes every request billable. _headers is applied at the edge for nothing.

Why CSP defaults to off here

Astro has its own security.csp, and it can hash the inline <script> and <style> blocks Astro itself emits — the theme flash-guard, scoped component styles. A static _headers file can't hash them, so expressing the same policy there means 'unsafe-inline' on both directives, which is most of what CSP was protecting you from.

So Astro owns CSP, and this owns what Astro doesn't do: HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-Content-Type-Options, Cross-Origin-Opener-Policy and X-Permitted-Cross-Domain-Policies.

Opt back in — for a host where Astro's CSP isn't in play:

integrations: [headers({ csp: { defaultSrc: ["'self'"] } })]

Options

headers(config?, options?)

  • config — a SecurityHeadersConfig (below). csp defaults to false here.
  • options.path — path pattern the headers apply to. Default '/*'.
  • options.filename — output name. Default '_headers'.

Cloudflare caps a _headers file at 100 rules; one path costs one rule however many headers it carries.

Anywhere else

The generators are plain functions with no framework attached — use them from a build script, a Worker, or a test.

import { headersFile, buildHeaders, buildCSP, buildHSTS } from '@arraypress/security-headers';

// A _headers file, as a string.
writeFileSync('dist/_headers', headersFile({ csp: { scriptSrc: ["'self'"] } }));

// The same values as a plain object — for a Response you build yourself.
return new Response(body, { headers: buildHeaders() });

// Or one header at a time.
buildCSP({ defaultSrc: ["'self'"] });
buildHSTS({ maxAge: 31536000, includeSubDomains: true });

headersFile() renders the Cloudflare/Netlify format — a path line, then each header indented two spaces:

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: SAMEORIGIN
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Cross-Origin-Opener-Policy: same-origin
  X-Permitted-Cross-Domain-Policies: none
  Content-Security-Policy: default-src 'self'; script-src 'self'; …
  Strict-Transport-Security: max-age=31536000; includeSubDomains

Under the Astro integration the CSP line is absent, since Astro owns it.

Configuration

| Option | Default | Notes | |---|---|---| | csp | strict defaults | CSPConfig or false. Defaults to false in the Astro integration. | | cspReportOnly | false | A stricter policy sent as …-Report-Only, to trial before enforcing. | | hsts | true | HSTSConfig, true for defaults, or false to skip. | | xContentTypeOptions | true | Emits nosniff. | | xFrameOptions | 'SAMEORIGIN' | 'DENY', 'SAMEORIGIN' or false. | | referrerPolicy | 'strict-origin-when-cross-origin' | Any policy string, or false. | | permissionsPolicy | camera=(), microphone=(), geolocation=() | Any policy string, or false. | | crossOriginOpenerPolicy | 'same-origin' | Use 'same-origin-allow-popups' for OAuth popups. | | crossOriginEmbedderPolicy | false | 'require-corp' / 'credentialless'. See isolation below. | | crossOriginResourcePolicy | false | 'same-origin' / 'same-site' / 'cross-origin'. | | permittedCrossDomainPolicies | 'none' | Legacy Flash/Acrobat crossdomain.xml opt-out. | | originAgentCluster | false | Emits Origin-Agent-Cluster: ?1. | | reportingEndpoints | null | { csp: 'https://…' }Reporting-Endpoints. |

Every header is independently togglable — pass false to skip it.

Cross-origin isolation

Cross-Origin-Opener-Policy is on by default: it severs window.opener across origins and needs nothing from the resources you load, so it costs you nothing. The one gotcha is OAuth popups that talk back via window.opener — those want 'same-origin-allow-popups'.

Cross-Origin-Embedder-Policy and Cross-Origin-Resource-Policy are off by default, deliberately. COEP blocks every cross-origin resource that hasn't opted in, and CORP stops other sites embedding your images and fonts. Turn them on together, with COOP 'same-origin', when you actually need crossOriginIsolated — that is, SharedArrayBuffer or wasm threads:

headers({
  crossOriginOpenerPolicy: 'same-origin',
  crossOriginEmbedderPolicy: 'require-corp',
  crossOriginResourcePolicy: 'same-origin',
})

An app using AudioWorklets or Workers does not need this on its own — only shared memory does.

Rolling out a CSP

Send a strict policy as report-only alongside a permissive enforced one, watch the reports, then promote it:

headers({
  csp: { scriptSrc: ["'self'", "'unsafe-inline'"] },   // enforced today
  cspReportOnly: { scriptSrc: ["'self'"] },             // the goal
  reportingEndpoints: { csp: 'https://example.com/csp-report' },
})

CSP directives are camelCase and become kebab-case on the wire: defaultSrc, scriptSrc, styleSrc, imgSrc, fontSrc, connectSrc, frameSrc, and the rest.

buildHeaders({
  csp: { scriptSrc: ["'self'", 'https://challenges.cloudflare.com'] },
  xFrameOptions: 'DENY',
  hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
  permissionsPolicy: false,
});

Gotchas

Two defaults are strict on purpose and will bite if your site is the exception. Both fail silently in the browser, so they're worth knowing before you deploy.

The microphone and camera are off

Permissions-Policy defaults to camera=(), microphone=(), geolocation=(), which disables getUserMedia() outright — the call rejects, and nothing in your own code looks wrong. Right for a marketing or directory site; wrong for an app that records audio.

// An app with a record-from-mic button:
headers({ permissionsPolicy: 'camera=(), microphone=(self), geolocation=()' })

The default CSP blocks Google Fonts

font-src defaults to 'self' and style-src to 'self' 'unsafe-inline', so a <link> to fonts.googleapis.com and the files it pulls from fonts.gstatic.com are both blocked:

headers({
  csp: {
    styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
    fontSrc: ["'self'", 'https://fonts.gstatic.com'],
  },
})

The same applies to any third-party origin — analytics, embeds, a CDN. The default assumes a site that serves everything itself; add origins as you add dependencies rather than loosening default-src.

This one doesn't arise under the Astro integration, where CSP is off and Astro owns the policy.

Security notes

X-Frame-Options is superseded by CSP's frame-ancestors but is still emitted for older browsers — they don't cost each other anything.

The Permissions-Policy default is a tight baseline suited to admin surfaces. If your site legitimately uses the camera, microphone or geolocation, extend it rather than dropping the header.

HSTS only takes effect over HTTPS, and preload is a one-way door — browsers cache it for a long time, so don't enable it until you're certain every subdomain can serve HTTPS.

License

MIT