diff --git a/docs/2.utils/98.advanced.md b/docs/2.utils/98.advanced.md index a3552869..a65aa01a 100644 --- a/docs/2.utils/98.advanced.md +++ b/docs/2.utils/98.advanced.md @@ -192,7 +192,7 @@ router.use("/", async (event) => { ### `isCorsOriginAllowed(origin, options)` -Check if the incoming request is a CORS request. +Check if the origin is allowed. ### `isPreflightRequest(event)` diff --git a/src/types/utils/cors.ts b/src/types/utils/cors.ts index d8aba97a..cc289bad 100644 --- a/src/types/utils/cors.ts +++ b/src/types/utils/cors.ts @@ -1,11 +1,53 @@ import type { HTTPMethod } from ".."; export interface H3CorsOptions { + /** + * This determines the value of the "access-control-allow-origin" response header. + * If "*", it can be used to allow all origins. + * If an array of strings or regular expressions, it can be used with origin matching. + * If a custom function, it's used to validate the origin. It takes the origin as an argument and returns `true` if allowed. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin + * @default "*" + */ origin?: "*" | "null" | (string | RegExp)[] | ((origin: string) => boolean); + /** + * This determines the value of the "access-control-allow-methods" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods + * @default "*" + * @example ["GET", "HEAD", "PUT", "POST"] + */ methods?: "*" | HTTPMethod[]; + /** + * This determines the value of the "access-control-allow-headers" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + * @default "*" + */ allowHeaders?: "*" | string[]; + /** + * This determines the value of the "access-control-expose-headers" response header. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers + * @default "*" + */ exposeHeaders?: "*" | string[]; + /** + * This determines the value of the "access-control-allow-credentials" response header. + * When request with credentials, the options that `origin`, `methods`, `exposeHeaders` and `allowHeaders` should not be set "*". + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials + * @see https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + * @default false + */ credentials?: boolean; + /** + * This determines the value of the "access-control-max-age" response header of a preflight request. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age + * @default false + */ maxAge?: string | false; preflight?: { statusCode?: number; diff --git a/src/utils/internal/cors.ts b/src/utils/internal/cors.ts index 4648e440..03785480 100644 --- a/src/utils/internal/cors.ts +++ b/src/utils/internal/cors.ts @@ -39,23 +39,26 @@ export function resolveCorsOptions( } /** - * Check if the incoming request is a CORS request. + * Check if the origin is allowed. */ export function isCorsOriginAllowed( - origin: string | undefined, + origin: string | null | undefined, options: H3CorsOptions, ): boolean { const { origin: originOption } = options; - if ( - !origin || - !originOption || - originOption === "*" || - originOption === "null" - ) { + if (!origin) { + return false; + } + + if (!originOption || originOption === "*") { return true; } + if (typeof originOption === "function") { + return originOption(origin); + } + if (Array.isArray(originOption)) { return originOption.some((_origin) => { if (_origin instanceof RegExp) { @@ -66,7 +69,7 @@ export function isCorsOriginAllowed( }); } - return originOption(origin); + return originOption === origin; } /** @@ -79,17 +82,19 @@ export function createOriginHeaders( const { origin: originOption } = options; const origin = event.request.headers.get("origin"); - if (!origin || !originOption || originOption === "*") { + if (!originOption || originOption === "*") { return { "access-control-allow-origin": "*" }; } - if (typeof originOption === "string") { - return { "access-control-allow-origin": originOption, vary: "origin" }; + if (originOption === "null") { + return { "access-control-allow-origin": "null", vary: "origin" }; } - return isCorsOriginAllowed(origin, options) - ? { "access-control-allow-origin": origin, vary: "origin" } - : {}; + if (isCorsOriginAllowed(origin, options)) { + return { "access-control-allow-origin": origin!, vary: "origin" }; + } + + return {}; } /** diff --git a/test/unit/cors.test.ts b/test/unit/cors.test.ts index ebe85e7e..76803302 100644 --- a/test/unit/cors.test.ts +++ b/test/unit/cors.test.ts @@ -106,11 +106,11 @@ describe("cors (unit)", () => { }); describe("isCorsOriginAllowed", () => { - it("returns `true` if `origin` header is not defined", () => { + it("returns `false` if `origin` header is not defined", () => { const origin = undefined; const options: H3CorsOptions = {}; - expect(isCorsOriginAllowed(origin, options)).toEqual(true); + expect(isCorsOriginAllowed(origin, options)).toEqual(false); }); it("returns `true` if `origin` option is not defined", () => { @@ -129,13 +129,13 @@ describe("cors (unit)", () => { expect(isCorsOriginAllowed(origin, options)).toEqual(true); }); - it('returns `true` if `origin` option is `"null"`', () => { + it('returns `false` if `origin` option is `"null"`', () => { const origin = "https://example.com"; const options: H3CorsOptions = { origin: "null", }; - expect(isCorsOriginAllowed(origin, options)).toEqual(true); + expect(isCorsOriginAllowed(origin, options)).toEqual(false); }); it("can detect allowed origin (string)", () => { @@ -180,33 +180,35 @@ describe("cors (unit)", () => { describe("createOriginHeaders", () => { it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` option is not defined, or `"*"`', () => { - const eventMock = mockEvent("/", { + const hasOriginEventMock = mockEvent("/", { method: "OPTIONS", headers: { origin: "https://example.com", }, }); - const options1: H3CorsOptions = {}; - const options2: H3CorsOptions = { + const noOriginEventMock = mockEvent("/", { + method: "OPTIONS", + headers: {}, + }); + const defaultOptions: H3CorsOptions = {}; + const originWildcardOptions: H3CorsOptions = { origin: "*", }; - expect(createOriginHeaders(eventMock, options1)).toEqual({ + expect(createOriginHeaders(hasOriginEventMock, defaultOptions)).toEqual({ "access-control-allow-origin": "*", }); - expect(createOriginHeaders(eventMock, options2)).toEqual({ + expect( + createOriginHeaders(hasOriginEventMock, originWildcardOptions), + ).toEqual({ "access-control-allow-origin": "*", }); - }); - - it('returns an object whose `access-control-allow-origin` is `"*"` if `origin` header is not defined', () => { - const eventMock = mockEvent("/", { - method: "OPTIONS", - headers: {}, + expect(createOriginHeaders(noOriginEventMock, defaultOptions)).toEqual({ + "access-control-allow-origin": "*", }); - const options: H3CorsOptions = {}; - - expect(createOriginHeaders(eventMock, options)).toEqual({ + expect( + createOriginHeaders(noOriginEventMock, originWildcardOptions), + ).toEqual({ "access-control-allow-origin": "*", }); }); @@ -235,6 +237,12 @@ describe("cors (unit)", () => { origin: "http://example.com", }, }); + const noMatchEventMock = mockEvent("/", { + method: "OPTIONS", + headers: { + origin: "http://example.test", + }, + }); const options1: H3CorsOptions = { origin: ["http://example.com"], }; @@ -246,10 +254,12 @@ describe("cors (unit)", () => { "access-control-allow-origin": "http://example.com", vary: "origin", }); + expect(createOriginHeaders(noMatchEventMock, options1)).toEqual({}); expect(createOriginHeaders(eventMock, options2)).toEqual({ "access-control-allow-origin": "http://example.com", vary: "origin", }); + expect(createOriginHeaders(noMatchEventMock, options2)).toEqual({}); }); it("returns an empty object if `origin` option is one that is not allowed", () => { @@ -269,6 +279,22 @@ describe("cors (unit)", () => { expect(createOriginHeaders(eventMock, options1)).toEqual({}); expect(createOriginHeaders(eventMock, options2)).toEqual({}); }); + + it("returns an empty object if `origin` option is not wildcard and `origin` header is not defined", () => { + const eventMock = mockEvent("/", { + method: "OPTIONS", + headers: {}, + }); + const options1: H3CorsOptions = { + origin: ["http://example.com"], + }; + const options2: H3CorsOptions = { + origin: () => false, + }; + + expect(createOriginHeaders(eventMock, options1)).toEqual({}); + expect(createOriginHeaders(eventMock, options2)).toEqual({}); + }); }); describe("createMethodsHeaders", () => {