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

@zipbul/cors

v0.1.0

Published

Framework-agnostic CORS library for standard Web APIs (Request/Response)

Readme

@zipbul/cors

English | 한국어

npm coverage

A framework-agnostic CORS handling library. Instead of generating responses directly, it returns a discriminated union result, giving the caller full control over the response.

Uses standard Web APIs (Request / Response).

📦 Installation

bun add @zipbul/cors

💡 Core Concept

handle() does not create a response. It only tells you what to do next.

CorsResult
├── Continue          → Attach CORS headers to the response and continue
├── RespondPreflight  → Return a preflight-only response immediately
└── Reject            → Reject the request (with reason)

This design fits naturally into any environment — middleware pipelines, edge runtimes, custom error formats, and more.

🚀 Quick Start

import { Cors, CorsAction, CorsError } from '@zipbul/cors';

// Cors.create() throws CorsError on invalid options
const cors = Cors.create({
  origin: 'https://my-app.example.com',
  credentials: true,
});

async function handleRequest(request: Request): Promise<Response> {
  // handle() throws CorsError if the origin function fails
  const result = await cors.handle(request);

  if (result.action === CorsAction.Reject) {
    return new Response('Forbidden', { status: 403 });
  }

  if (result.action === CorsAction.RespondPreflight) {
    return new Response(null, {
      status: result.statusCode,
      headers: result.headers,
    });
  }

  // CorsAction.Continue — merge CORS headers into your response
  const response = new Response(JSON.stringify({ ok: true }), {
    headers: { 'Content-Type': 'application/json' },
  });

  for (const [key, value] of result.headers) {
    response.headers.set(key, value);
  }

  return response;
}

⚙️ Options

interface CorsOptions {
  origin?: OriginOptions;              // Default: '*'
  methods?: HttpMethod[];              // Default: GET, HEAD, PUT, PATCH, POST, DELETE
  allowedHeaders?: string[];           // Default: reflects request's ACRH
  exposedHeaders?: string[];           // Default: none
  credentials?: boolean;               // Default: false
  maxAge?: number;                     // Default: none (header not included)
  preflightContinue?: boolean;         // Default: false
  optionsSuccessStatus?: number;       // Default: 204
}

origin

| Value | Behavior | |:------|:---------| | '*' (default) | Allow all origins | | false | Reject all origins | | true | Reflect the request origin | | 'https://example.com' | Allow only the exact match | | /^https:\/\/(.+\.)?example\.com$/ | Regex matching | | ['https://a.com', /^https:\/\/b\./] | Array (mix of strings and regexes) | | (origin, request) => boolean \| string | Function (sync or async) |

When credentials: true, origin: '*' causes a validation error. Use origin: true to reflect the request origin.

RegExp origins are checked for ReDoS safety at creation time using safe-regex2. Patterns with star height ≥ 2 (e.g. /(a+)+$/) are rejected with CorsErrorReason.UnsafeRegExp.

methods

HTTP methods to allow in preflight. Accepts HttpMethod[] — standard methods are autocompleted, and any RFC 9110 §5.6.2 token (e.g. 'PROPFIND') is also valid.

Cors.create({ methods: ['GET', 'POST', 'DELETE'] });
Cors.create({ methods: ['GET', 'PROPFIND'] }); // custom token

A wildcard '*' allows all methods. With credentials: true, the wildcard is replaced by echoing the request method.

allowedHeaders

Request headers to allow in preflight. When not set, the client's Access-Control-Request-Headers value is echoed back.

Cors.create({ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'] });

⚠️ Authorization caveat — Per the Fetch Standard, a wildcard '*' alone does not cover the Authorization header. You must list it explicitly.

Cors.create({ allowedHeaders: ['*', 'Authorization'] });

exposedHeaders

Response headers to expose to browser JavaScript.

Cors.create({ exposedHeaders: ['X-Request-Id', 'X-Rate-Limit-Remaining'] });

With credentials: true, using a wildcard '*' causes the Access-Control-Expose-Headers header to not be set at all.

credentials

Whether to include the Access-Control-Allow-Credentials: true header.

Cors.create({ origin: 'https://app.example.com', credentials: true });

maxAge

How long (in seconds) the browser may cache the preflight result.

Cors.create({ maxAge: 86400 }); // 24 hours

preflightContinue

When set to true, preflight requests are not handled automatically. Instead, CorsAction.Continue is returned, delegating to the next handler.

optionsSuccessStatus

HTTP status code for the preflight response. Defaults to 204. Set to 200 if legacy browser compatibility is needed.

📤 Return Types

handle() returns Promise<CorsResult>. CorsResult is a discriminated union of three interfaces.

CorsContinueResult

{ action: CorsAction.Continue; headers: Headers }

Returned for normal (non-OPTIONS) requests, or preflight when preflightContinue: true. Merge headers into your response directly.

CorsPreflightResult

{ action: CorsAction.RespondPreflight; headers: Headers; statusCode: number }

Returned for OPTIONS requests that include Access-Control-Request-Method. Use headers and statusCode to build a response.

CorsRejectResult

{ action: CorsAction.Reject; reason: CorsRejectionReason }

Returned when CORS validation fails. Use reason to build a detailed error response.

| CorsRejectionReason | Meaning | |:-----------------------|:--------| | NoOrigin | Origin header missing or empty | | OriginNotAllowed | Origin not in the allowed list | | MethodNotAllowed | Request method not in the allowed list | | HeaderNotAllowed | Request header not in the allowed list |

Cors.create() throws CorsError when options fail validation:

| CorsErrorReason | Meaning | |:------------------|:--------| | CredentialsWithWildcardOrigin | credentials:true with origin:'*' (Fetch Standard §3.3.5) | | InvalidMaxAge | maxAge is not a non-negative integer (RFC 9111 §1.2.1) | | InvalidStatusCode | optionsSuccessStatus is not a 2xx integer | | InvalidOrigin | origin is an empty/blank string, empty array, or array with empty/blank entries (RFC 6454) | | InvalidMethods | methods is empty, or contains empty/blank entries (RFC 9110 §5.6.2) | | InvalidAllowedHeaders | allowedHeaders contains empty/blank entries (RFC 9110 §5.6.2) | | InvalidExposedHeaders | exposedHeaders contains empty/blank entries (RFC 9110 §5.6.2) | | OriginFunctionError | Origin function threw at runtime | | UnsafeRegExp | origin RegExp has exponential backtracking risk (ReDoS) |

🔬 Advanced Usage

Origin option patterns

// Single origin
Cors.create({ origin: 'https://app.example.com' });

// Multiple origins (mix of strings and regexes)
Cors.create({
  origin: [
    'https://app.example.com',
    'https://admin.example.com',
    /^https:\/\/preview-\d+\.example\.com$/,
  ],
});

// Regex to allow all subdomains
Cors.create({ origin: /^https:\/\/(.+\.)?example\.com$/ });

Async origin function

Dynamically validate origins via a database or external service.

Cors.create({
  origin: async (origin, request) => {
    const tenant = request.headers.get('X-Tenant-Id');
    const allowed = await db.isOriginAllowed(tenant, origin);

    return allowed ? true : false;
    // true   → reflect the request origin
    // string → use the specified string
    // false  → reject
  },
});

If the origin function throws, handle() throws CorsError with reason: CorsErrorReason.OriginFunctionError.

Wildcards and credentials

Per the Fetch Standard, wildcards (*) cannot be used with credentialed requests (cookies, Authorization). When credentials: true, the library automatically handles the following:

| Option | Behavior with wildcard | |:-------|:-----------------------| | origin: '*' | Validation error — use origin: true to reflect the request origin | | methods: ['*'] | Echoes the request method | | allowedHeaders: ['*'] | Echoes the request headers | | exposedHeaders: ['*'] | Access-Control-Expose-Headers is not set |

// ✅ origin: true + credentials: true → request origin is reflected
Cors.create({ origin: true, credentials: true });

// ✅ Specific domain + credentials
Cors.create({ origin: 'https://app.example.com', credentials: true });

// ❌ origin: '*' + credentials: true → Cors.create() throws CorsError
Cors.create({ origin: '*', credentials: true }); // CorsErrorReason.CredentialsWithWildcardOrigin

Preflight delegation

When another middleware needs to handle OPTIONS requests directly:

const cors = Cors.create({ preflightContinue: true });

async function handle(request: Request): Promise<Response> {
  const result = await cors.handle(request);

  if (result.action === CorsAction.Reject) {
    return new Response('Forbidden', { status: 403 });
  }

  // Continue — both normal and preflight requests arrive here
  const response = await nextHandler(request);

  for (const [key, value] of result.headers) {
    response.headers.set(key, value);
  }

  return response;
}

🔌 Framework Integration Examples

import { Cors, CorsAction } from '@zipbul/cors';

const cors = Cors.create({
  origin: ['https://app.example.com'],
  credentials: true,
  exposedHeaders: ['X-Request-Id'],
});

Bun.serve({
  async fetch(request) {
    const result = await cors.handle(request);

    if (result.action === CorsAction.Reject) {
      return new Response(
        JSON.stringify({ error: 'CORS policy violation', reason: result.reason }),
        { status: 403, headers: { 'Content-Type': 'application/json' } },
      );
    }

    if (result.action === CorsAction.RespondPreflight) {
      return new Response(null, {
        status: result.statusCode,
        headers: result.headers,
      });
    }

    const response = await router.handle(request);

    for (const [key, value] of result.headers) {
      response.headers.set(key, value);
    }

    return response;
  },
  port: 3000,
});
import { Cors, CorsAction } from '@zipbul/cors';
import type { CorsOptions } from '@zipbul/cors';

function corsMiddleware(options?: CorsOptions) {
  // throws CorsError on invalid options
  const cors = Cors.create(options);

  return async (ctx: Context, next: () => Promise<void>) => {
    // throws CorsError if origin function fails
    const result = await cors.handle(ctx.request);

    if (result.action === CorsAction.Reject) {
      ctx.status = 403;
      ctx.body = { error: 'CORS_VIOLATION', reason: result.reason };
      return;
    }

    if (result.action === CorsAction.RespondPreflight) {
      ctx.response = new Response(null, {
        status: result.statusCode,
        headers: result.headers,
      });
      return;
    }

    await next();

    for (const [key, value] of result.headers) {
      ctx.response.headers.set(key, value);
    }
  };
}

📄 License

MIT