From 8357b106323f0b71c4c2fdbbcbfbd8a9ac8d2086 Mon Sep 17 00:00:00 2001 From: "Kevin R. Whitley" Date: Mon, 25 Mar 2024 17:39:38 -0500 Subject: [PATCH] v5.x DRAFT: CORS rewrite (#226) * flowrouter is the new router and ittyrouter is the old router * further evolving the standard * added shared router tests for feature parity between IttyRouter and Rotuer * take that, linter! * cleaned up example * added AutoRouter test coverage * lint fix * types for before/after/onError * released v4.3.0-next.0 - releasing v4.3 as next to determine sizes * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * catch is a single handler * v4.3 DRAFT: rename after stage to finally (#225) * replaced after stage with finally * fixed potential bug in createResponse to prevent pollution if passed a Request as second param, like in v4.3 stages * createResponse simplified and safe against using as ResponseHandler * staging changes for cors revamp * modifying cors * ready to begin test suite for cors * starting basic framework for testsg * cors complete? * additional preflight test * can handle multiple cookies in corsified responses * merge conflicts in example file * lint passing * last merge conflict? * credentials: true should reflect with * origin * released v4.3.0-next.1 - releasing as next for bundle size comparison * released v4.3.0-next.2 - next version bump * save a couple bytes by using allowMethods default of string * released v4.3.0-next.3 - byte shaving --- .eslintrc | 1 + .gitignore | 1 + README.md | 9 +- {test => lib}/index.ts | 4 +- package.json | 10 +- src/AutoRouter.spec.ts | 2 +- src/Router.spec.ts | 8 +- src/SharedRouter.spec.ts | 2 +- src/cors.spec.ts | 229 ++++++++++++++++++++++++++ src/cors.ts | 80 ++++++++++ src/createCors.spec.ts | 239 ---------------------------- src/createCors.ts | 92 ----------- src/index.ts | 2 +- vitest.config.ts | 2 +- yarn.lock | 336 +++++++++++++++++++++++++++------------ 15 files changed, 566 insertions(+), 451 deletions(-) rename {test => lib}/index.ts (91%) create mode 100644 src/cors.spec.ts create mode 100644 src/cors.ts delete mode 100644 src/createCors.spec.ts delete mode 100644 src/createCors.ts diff --git a/.eslintrc b/.eslintrc index 1f177dcd..8cb32d2b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,6 +19,7 @@ "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", "linebreak-style": ["error", "unix"], "prefer-const": "off", "quotes": ["error", "single", { "allowTemplateLiterals": true }], diff --git a/.gitignore b/.gitignore index 6ae4e4c3..3ab7ecd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # OS ignores .DS_Store +.vscode # Logs logs diff --git a/README.md b/README.md index b2e9240c..c2523b32 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,15 @@ An ultra-tiny API microrouter, for use when [size matters](https://github.com/Ti ```js import { AutoRouter } from 'itty-router' // ~1kB -export default AutoRouter() +const router = AutoRouter() + +router .get('/hello/:name', ({ name }) => `Hello, ${name}!`) .get('/json', () => [1,2,3]) .get('/promises', () => Promise.resolve('foo')) +export default router + // that's it ^-^ ``` @@ -82,6 +86,9 @@ Join us on [Discord](https://discord.gg/53vyrZAu9u)! These folks are the real heroes, making open source the powerhouse that it is! Help out and get your name added to this list! <3 +#### Constant Feedback, Suggestions, Moral Support & Community Building +- TBD + #### Core Concepts - [@mvasigh](https://github.com/mvasigh) - proxy hack wizard behind itty, coding partner in crime, maker of the entire doc site, etc, etc. diff --git a/test/index.ts b/lib/index.ts similarity index 91% rename from test/index.ts rename to lib/index.ts index 10749ecd..46992a90 100644 --- a/test/index.ts +++ b/lib/index.ts @@ -4,14 +4,14 @@ import { expect, it, vi } from 'vitest' // generates a request from a string like: // GET /whatever // /foo -export const toReq = (methodAndPath: string) => { +export const toReq = (methodAndPath: string, options: RequestInit = {}) => { let [method, path] = methodAndPath.split(' ') if (!path) { path = method method = 'GET' } - return new Request(`https://example.com${path}`, { method }) + return new Request(`https://example.com${path}`, { method, ...options }) } export const extract = ({ params, query }) => ({ params, query }) diff --git a/package.json b/package.json index 552cbbc8..957ad72f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "itty-router", - "version": "4.3.0-next.0", + "version": "4.3.0-next.3", "description": "A tiny, zero-dependency router, designed to make beautiful APIs in any environment.", "main": "./index.js", "module": "./index.mjs", @@ -16,10 +16,10 @@ "require": "./AutoRouter.js", "types": "./AutoRouter.d.ts" }, - "./createCors": { - "import": "./createCors.mjs", - "require": "./createCors.js", - "types": "./createCors.d.ts" + "./cors": { + "import": "./cors.mjs", + "require": "./cors.js", + "types": "./cors.d.ts" }, "./createResponse": { "import": "./createResponse.mjs", diff --git a/src/AutoRouter.spec.ts b/src/AutoRouter.spec.ts index 636871f3..2a5e37ec 100644 --- a/src/AutoRouter.spec.ts +++ b/src/AutoRouter.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { toReq } from '../test' +import { toReq } from '../lib' import { AutoRouter } from './AutoRouter' import { text } from './text' import { error } from './error' diff --git a/src/Router.spec.ts b/src/Router.spec.ts index e4f28538..96666be6 100644 --- a/src/Router.spec.ts +++ b/src/Router.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { toReq } from '../test' +import { toReq } from '../lib' import { Router } from './Router' import { json } from './json' import { error } from './error' @@ -115,11 +115,11 @@ describe(`SPECIFIC TESTS: Router`, () => { }) // manipulate - router.finally.push(() => true) + router.finally?.push(() => true) const response = await router.fetch(toReq('/')) - expect(router.before.length).toBe(2) - expect(router.finally.length).toBe(3) + expect(router.before?.length).toBe(2) + expect(router.finally?.length).toBe(3) expect(response).toBe(true) }) diff --git a/src/SharedRouter.spec.ts b/src/SharedRouter.spec.ts index d719fbb2..ee2e68c3 100644 --- a/src/SharedRouter.spec.ts +++ b/src/SharedRouter.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { createTestRunner, extract, toReq } from '../test' +import { createTestRunner, extract, toReq } from '../lib' import { IttyRouter } from './IttyRouter' import { Router as FlowRouter } from './Router' diff --git a/src/cors.spec.ts b/src/cors.spec.ts new file mode 100644 index 00000000..fc8a9ce5 --- /dev/null +++ b/src/cors.spec.ts @@ -0,0 +1,229 @@ +import { describe, expect, it } from 'vitest' +import { toReq } from '../lib' +import { Router } from './Router' +import { CorsOptions, cors } from './cors' +import { text } from './text' + +// outputs a router with a single route at index +const corsRouter = (options?: CorsOptions) => { + const { preflight, corsify } = cors(options) + + return Router({ + before: [preflight], + finally: [text, corsify], + }).get('/', () => TEST_STRING) +} + +const DEFAULT_ROUTER = corsRouter() +const HEADERS_AS_ARRAY = [ 'x-foo', 'x-bar' ] +const HEADERS_AS_STRING = HEADERS_AS_ARRAY.join(',') +const TEST_STRING = 'Hello World' +const TEST_ORIGIN = 'https://foo.bar' +const REGEXP_DENY_ORIGIN = /^https:\/\/google.com$/ +const BASIC_OPTIONS_REQUEST = toReq('OPTIONS /', { + headers: { origin: TEST_ORIGIN }, +}) +const BASIC_REQUEST = toReq('/', { + headers: { origin: TEST_ORIGIN }, +}) + +describe('cors(options?: CorsOptions)', () => { + describe('BEHAVIOR', () => { + it('returns a { preflight, corsify } handler set', () => { + const { preflight, corsify } = cors() + + expect(typeof preflight).toBe('function') + expect(typeof corsify).toBe('function') + }) + }) + + describe('OPTIONS', () => { + describe('origin', () => { + it('defaults to *', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe('*') + }) + + it('can accept a string', async () => { + const response = await corsRouter({ origin: TEST_ORIGIN }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('can accept a RegExp object (if test passes, reflect origin)', async () => { + const response = await corsRouter({ origin: /oo.bar$/ }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('can accept a RegExp object (undefined if fails)', async () => { + const response = await corsRouter({ origin: REGEXP_DENY_ORIGIN }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + }) + + it('can accept true (reflect origin)', async () => { + const response = await corsRouter({ origin: true }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('can accept a function (reflect origin if passes)', async () => { + const response = await corsRouter({ origin: () => TEST_ORIGIN.toUpperCase() }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN.toUpperCase()) + }) + + it('can accept a function (undefined if fails)', async () => { + const response = await corsRouter({ origin: () => undefined }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + }) + + it('can accept an array of strings (reflect origin if passes)', async () => { + const response = await corsRouter({ origin: [TEST_ORIGIN] }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('can accept an array of strings (undefined if fails)', async () => { + const response = await corsRouter({ origin: [] }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + }) + }) + + describe('allowMethods', () => { + it('defaults to *', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-methods')).toBe('*') + }) + + it('can accept a string', async () => { + const response = await corsRouter({ allowMethods: 'GET,POST' }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-methods')).toBe('GET,POST') + }) + + it('can accept a an array of strings', async () => { + const response = await corsRouter({ allowMethods: ['GET', 'POST'] }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-methods')).toBe('GET,POST') + }) + }) + + describe('allowHeaders', () => { + it('defaults to undefined/null', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-headers')).toBeNull() + }) + + it('can accept a string', async () => { + const response = await corsRouter({ allowHeaders: HEADERS_AS_STRING }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-headers')).toBe(HEADERS_AS_STRING) + }) + + it('can accept a an array of strings', async () => { + const response = await corsRouter({ allowHeaders: HEADERS_AS_ARRAY }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-headers')).toBe(HEADERS_AS_STRING) + }) + }) + + describe('exposeHeaders', () => { + it('defaults to undefined/null', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-expose-headers')).toBeNull() + }) + + it('can accept a string', async () => { + const response = await corsRouter({ exposeHeaders: HEADERS_AS_STRING }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-expose-headers')).toBe(HEADERS_AS_STRING) + }) + + it('can accept a an array of strings', async () => { + const response = await corsRouter({ exposeHeaders: HEADERS_AS_ARRAY }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-expose-headers')).toBe(HEADERS_AS_STRING) + }) + }) + + describe('credentials', () => { + it('defaults to undefined/null', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-credentials')).toBeNull() + }) + + it('can accept true', async () => { + const response = await corsRouter({ credentials: true }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-credentials')).toBe('true') + }) + + it('reflect domain if origin is *', async () => { + const response = await corsRouter({ credentials: true }).fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + }) + }) + + describe('preflight', () => { + describe('BEHAVIOR', () => { + it('responds to OPTIONS requests', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe('*') + }) + + it('ignores non-OPTIONS requests (does not return)', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_REQUEST) + expect(response.status).toBe(200) + }) + + it('responds with status 204', async () => { + const response = await DEFAULT_ROUTER.fetch(BASIC_OPTIONS_REQUEST) + expect(response.status).toBe(204) + }) + }) + }) + + describe('corsify', () => { + describe('BEHAVIOR', () => { + it('adds cors headers to Response', async () => { + const { corsify } = cors() + const response = corsify(new Response(null)) + expect(response.headers.get('access-control-allow-origin')).toBe('*') + expect(response.headers.get('access-control-allow-methods')).toBe('*') + }) + + it('will reflect origin (from request) if origin: true', async () => { + const { corsify } = cors({ origin: true }) + const response = corsify(new Response(null)) + const response2 = corsify(new Response(null), BASIC_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + expect(response2.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('will reflect origin (from request) if origin is in array of origins', async () => { + const { corsify } = cors({ origin: [TEST_ORIGIN] }) + const response = corsify(new Response(null)) + const response2 = corsify(new Response(null), BASIC_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + expect(response2.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('will reflect origin (from request) if origin passes RegExp origin test', async () => { + const { corsify } = cors({ origin: /oo.bar$/ }) + const response = corsify(new Response(null)) + const response2 = corsify(new Response(null), BASIC_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBeNull() + expect(response2.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('will pass origin as string if given', async () => { + const { corsify } = cors({ origin: TEST_ORIGIN }) + const response = corsify(new Response(null)) + const response2 = corsify(new Response(null), BASIC_REQUEST) + expect(response.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + expect(response2.headers.get('access-control-allow-origin')).toBe(TEST_ORIGIN) + }) + + it('will safely preserve multiple cookies (or other identical header names)', async () => { + const { corsify } = cors() + const response = new Response(null) + response.headers.append('Set-Cookie', 'cookie1=value1; Path=/; HttpOnly') + response.headers.append('Set-Cookie', 'cookie2=value2; Path=/; Secure') + const corsified = corsify(response.clone()) + + expect(response.headers.getSetCookie().length).toBe(2) + expect(corsified.headers.getSetCookie().length).toBe(2) + }) + }) + }) +}) diff --git a/src/cors.ts b/src/cors.ts new file mode 100644 index 00000000..bca4a62e --- /dev/null +++ b/src/cors.ts @@ -0,0 +1,80 @@ +export type CorsOptions = { + credentials?: true + origin?: boolean | string | string[] | RegExp | ((origin: string) => string | void) + maxAge?: number + allowMethods?: string | string[] + allowHeaders?: any + exposeHeaders?: string | string[] +} + +// Create CORS function with default options. +export const cors = (options: CorsOptions = {}) => { + // Destructure and set defaults for options. + const { + origin = '*', + credentials = false, + allowMethods = '*', + allowHeaders, + exposeHeaders, + maxAge, + } = options + + // create generic CORS headers + const corsHeaders: Record = { + 'access-control-allow-headers': allowHeaders?.join?.(',') ?? allowHeaders, // include allowed headers + // @ts-expect-error + 'access-control-expose-headers': exposeHeaders?.join?.(',') ?? exposeHeaders, // include allowed headers + // @ts-expect-error + 'access-control-allow-methods': allowMethods?.join?.(',') ?? allowMethods, // include allowed methods + 'access-control-max-age': maxAge, + 'access-control-allow-credentials': credentials, + } + + const getAccessControlOrigin = (request?: Request): string => { + const requestOrigin = request?.headers.get('origin') // may be null if no request passed + + // @ts-expect-error + if (origin === true) return requestOrigin + // @ts-expect-error + if (origin instanceof RegExp) return origin.test(requestOrigin) ? requestOrigin : undefined + // @ts-expect-error + if (Array.isArray(origin)) return origin.includes(requestOrigin) ? requestOrigin : undefined + // @ts-expect-error + if (origin instanceof Function) return origin(requestOrigin) + + // @ts-expect-error + return origin == '*' && credentials + ? requestOrigin + : origin + } + + const preflight = (request: Request) => { + if (request.method == 'OPTIONS') { + return new Response(null, { + status: 204, + headers: Object.entries({ + 'access-control-allow-origin': getAccessControlOrigin(request), + ...corsHeaders, + }).filter(v => v[1]), + }) + } // otherwise ignore + } + + const corsify = (response: Response, request?: Request) => { + // ignore if already has CORS headers + if (response?.headers?.get('access-control-allow-origin')) return response + + return new Response(response.body, { + ...response, + // @ts-expect-error + headers: [ + ...response.headers, + ['access-control-allow-origin', getAccessControlOrigin(request)], + ...Object.entries(corsHeaders), + ].filter(v => v[1]), + }) + } + + // Return corsify and preflight methods. + return { corsify, preflight } +} diff --git a/src/createCors.spec.ts b/src/createCors.spec.ts deleted file mode 100644 index 1b3068a3..00000000 --- a/src/createCors.spec.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { createCors } from './createCors' -import { json } from './json' -import { Router } from './Router' - -class WebSocketResponse { - constructor(body, options = {}) { - this.body = body - this.status = options.status || 101 - this.statusText = options.statusText || 'Switching Protocols' - this.headers = options.headers || new Headers() - } -} - -describe('createCors(options)', () => { - it('returns { preflight, corsify }', async () => { - const { preflight, corsify } = createCors() - - expect(typeof preflight).toBe('function') - expect(typeof corsify).toBe('function') - }) - - describe('options', () => { - it('maxAge', async () => { - const { preflight } = createCors({ - maxAge: 60, - }) - const router = Router().all('*', preflight) - const request = new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin: 'http://localhost:3000', - }, - }) - const response = await router.fetch(request) - - expect(response.headers.get('Access-Control-Max-Age')).toBe('60') - }) - - it('origins should be array of string', async () => { - const { preflight } = createCors({ - origins: ['http://localhost:3000', 'http://localhost:4000'] - }) - const router = Router().all('*', preflight) - - const generateRequest = (origin: string) => new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin, - } - }) - - const response1 = await router.fetch(generateRequest('http://localhost:3000')) - expect(response1.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:3000') - const response2 = await router.fetch(generateRequest('http://localhost:4000')) - expect(response2.headers.get('Access-Control-Allow-Origin')).toBe('http://localhost:4000') - const response3 = await router.fetch(generateRequest('http://localhost:5000')) - expect(response3.headers.get('Access-Control-Allow-Origin')).toBe(null) - }) - - it('origins should be function returns boolean', async () => { - const { preflight } = createCors({ - origins: (origin) => origin.startsWith('https://') && origin.endsWith('.example.com') - }) - const router = Router().all('*', preflight) - - const generateRequest = (origin: string) => new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin, - } - }) - - const response1 = await router.fetch(generateRequest('https://secure.example.com')) - expect(response1.headers.get('Access-Control-Allow-Origin')).toBe('https://secure.example.com') - const response2 = await router.fetch(generateRequest('https://another-secure.example.com')) - expect(response2.headers.get('Access-Control-Allow-Origin')).toBe('https://another-secure.example.com') - const response3 = await router.fetch(generateRequest('http://unsecure.example.com')) - expect(response3.headers.get('Access-Control-Allow-Origin')).toBe(null) - }) - }) - - describe('preflight (middleware)', () => { - it('should handle options requests', async () => { - const { preflight } = createCors() - const router = Router().all('*', preflight) - const request = new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin: 'http://localhost:3000', - }, - }) - const response = await router.fetch(request) - - expect(response.headers.get('Access-Control-Allow-Origin')).toBe( - 'http://localhost:3000' - ) - }) - - it('should handle OPTIONS requests without standard headers via Allow (methods) header', async () => { - const { preflight } = createCors() - const router = Router().all('*', preflight) - const request = new Request('https://foo.bar', { method: 'OPTIONS' }) - const response = await router.fetch(request) - - expect((response.headers.get('Allow') || '').includes('GET')).toBe(true) - }) - }) - - describe('corsify(response: Response): Response', () => { - it('should throw if no Response passed as argument', async () => { - const { preflight, corsify } = createCors() - const catchError = vi.fn() - const router = Router() - .all('*', preflight) - .get('/foo', () => json(13)) - const request = new Request('https://foo.bar/miss') - await router.fetch(request).then(corsify).catch(catchError) - - expect(catchError).toHaveBeenCalled() - }) - - it('should handle options requests', async () => { - const { preflight, corsify } = createCors() - const router = Router() - .all('*', preflight) - .get('/foo', () => json(13)) - const request = new Request('https://foo.bar/foo', { - headers: { - origin: 'http://localhost:3000', - }, - }) - const response = await router.fetch(request).then(corsify) - - expect(response.headers.get('Access-Control-Allow-Origin')).toBe( - 'http://localhost:3000' - ) - expect( - (response.headers.get('Access-Control-Allow-Methods') || '').includes( - 'GET' - ) - ).toBe(true) - }) - - it('will not modify responses with existing CORS headers', async () => { - const { preflight, corsify } = createCors() - const router = Router() - .all('*', preflight) - .get( - '/foo', - () => - new Response(null, { - headers: { - 'access-control-allow-origin': '*', - }, - }) - ) - const request = new Request('https://foo.bar/foo', { - headers: { - origin: 'http://localhost:3000', - }, - }) - const response = await router.fetch(request).then(corsify) - - expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*') - }) - - it('will not modify redirects or 101 status responses', async () => { - const { preflight, corsify } = createCors() - const router = Router() - .all('*', preflight) - .get('/foo', () => new WebSocketResponse(null, { status: 101 })) - const request = new Request('https://foo.bar/foo', { - headers: { - origin: 'http://localhost:3000', - }, - }) - const response = await router.fetch(request).then(corsify) - - expect(response.headers.get('Access-Control-Allow-Origin')).toBe(null) - }) - - describe('repeated use', () => { - const { preflight } = createCors() - const router = Router().all('*', preflight) - const origin = 'http://localhost:3000' - - const generateRequest = () => - new Request('https://foo.bar', { - method: 'OPTIONS', - headers: { - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'content-type', - origin, - }, - }) - - it('will work multiple times in a row', async () => { - const response1 = await router.fetch(generateRequest()) - expect(response1.status).toBe(200) - expect(response1.headers.get('Access-Control-Allow-Origin')).toBe( - origin - ) - - const response2 = await router.fetch(generateRequest()) - expect(response2.status).toBe(200) - expect(response2.headers.get('Access-Control-Allow-Origin')).toBe( - origin - ) - }) - }) - }) - - // it('returns { preflight, corsify }', async () => { - // const router = Router() - // const handler = vi.fn(({ content }) => content) - // const request = new Request('https://foo.bar', { - // method: 'POST', - // headers: { - // 'content-type': 'application/json' - // }, - // body: JSON.stringify({ foo: 'bar' }) - // }) - - // await router - // .post('/', withContent, handler) - // .fetch(request) - - // expect(handler).toHaveReturnedWith({ foo: 'bar' }) - // }) -}) diff --git a/src/createCors.ts b/src/createCors.ts deleted file mode 100644 index 2ef2f424..00000000 --- a/src/createCors.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { IRequest } from './IttyRouter' - -export type CorsOptions = { - origins?: string[] | ((origin: string) => boolean) - maxAge?: number - methods?: string[] - headers?: any -} - -// Create CORS function with default options. -export const createCors = (options: CorsOptions = {}) => { - // Destructure and set defaults for options. - const { origins = ['*'], maxAge, methods = ['GET'], headers = {} } = options - - let allowOrigin: any - const isAllowOrigin = typeof origins === 'function' - ? origins - : (origin: string) => (origins.includes(origin) || origins.includes('*')) - - // Initial response headers. - const rHeaders = { - 'content-type': 'application/json', - 'Access-Control-Allow-Methods': methods.join(', '), - ...headers, - } - - // Set max age if provided. - if (maxAge) rHeaders['Access-Control-Max-Age'] = maxAge - - // Pre-flight function. - const preflight = (r: IRequest) => { - // Use methods set. - const useMethods = [...new Set(['OPTIONS', ...methods])] - const origin = r.headers.get('origin') || '' - - // set allowOrigin globally - allowOrigin = isAllowOrigin(origin) && { 'Access-Control-Allow-Origin': origin } - - // Check if method is OPTIONS. - if (r.method === 'OPTIONS') { - const reqHeaders = { - ...rHeaders, - 'Access-Control-Allow-Methods': useMethods.join(', '), - 'Access-Control-Allow-Headers': r.headers.get( - 'Access-Control-Request-Headers' - ), - ...allowOrigin, - } - - // Handle CORS pre-flight request. - return new Response(null, { - headers: - r.headers.get('Origin') && - r.headers.get('Access-Control-Request-Method') && - r.headers.get('Access-Control-Request-Headers') - ? reqHeaders - : { Allow: useMethods.join(', ') }, - }) - } - } - - // Corsify function. - const corsify = (response: Response): Response => { - if (!response) - throw new Error( - 'No fetch handler responded and no upstream to proxy to specified.' - ) - - const { headers, status, body } = response - - // Bypass for protocol shifts or redirects, or if CORS is already set. - if ( - [101, 301, 302, 308].includes(status) || - headers.get('access-control-allow-origin') - ) - return response - - // Return new response with CORS headers. - return new Response(body, { - status, - headers: { - ...Object.fromEntries(headers), - ...rHeaders, - ...allowOrigin, - 'content-type': headers.get('content-type'), - }, - }) - } - - // Return corsify and preflight methods. - return { corsify, preflight } -} diff --git a/src/index.ts b/src/index.ts index 9886f0c0..a83a321c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,4 +25,4 @@ export * from './withCookies' export * from './withParams' // CORS -export * from './createCors' +export * from './cors' diff --git a/vitest.config.ts b/vitest.config.ts index 97b7fd5e..c325e39b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ reporter: ['text', 'lcov'], exclude: [ '**/example/**', - '**/test/**', + '**/lib/**', 'src/index.ts', 'src/websocket.ts', ], diff --git a/yarn.lock b/yarn.lock index 9dab2c1e..e762dfeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -220,11 +220,121 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== + +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== + +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== + "@esbuild/darwin-arm64@0.19.12": version "0.19.12" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== + +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== + +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== + +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== + +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== + +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== + +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== + +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== + +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== + +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== + +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== + +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== + +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== + +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== + +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== + +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== + +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== + +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -309,16 +419,16 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.1" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" - integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== - "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" @@ -332,7 +442,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@1.4.14": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== @@ -342,14 +452,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -358,6 +460,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@kamilkisiela/fast-url-parser@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@kamilkisiela/fast-url-parser/-/fast-url-parser-1.1.4.tgz" @@ -379,7 +489,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.4": +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": version "2.0.4" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz" integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== @@ -449,11 +559,71 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@rollup/rollup-android-arm-eabi@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b" + integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg== + +"@rollup/rollup-android-arm64@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e" + integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q== + "@rollup/rollup-darwin-arm64@4.13.0": version "4.13.0" resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz" integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g== +"@rollup/rollup-darwin-x64@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455" + integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg== + +"@rollup/rollup-linux-arm-gnueabihf@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4" + integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ== + +"@rollup/rollup-linux-arm64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143" + integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w== + +"@rollup/rollup-linux-arm64-musl@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef" + integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw== + +"@rollup/rollup-linux-riscv64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6" + integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA== + +"@rollup/rollup-linux-x64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778" + integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA== + +"@rollup/rollup-linux-x64-musl@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89" + integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw== + +"@rollup/rollup-win32-arm64-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b" + integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA== + +"@rollup/rollup-win32-ia32-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2" + integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw== + +"@rollup/rollup-win32-x64-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10" + integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -497,7 +667,7 @@ resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/estree@^1.0.0", "@types/estree@1.0.5": +"@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -866,7 +1036,7 @@ asn1@~0.2.3: dependencies: safer-buffer "~2.1.0" -assert-plus@^1.0.0, assert-plus@1.0.0: +assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= @@ -1027,16 +1197,7 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^2.4.2: +chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1108,16 +1269,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colorette@^1.1.0: version "1.4.0" resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz" @@ -1223,7 +1384,7 @@ data-urls@^5.0.0: whatwg-mimetype "^4.0.0" whatwg-url "^14.0.0" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@4: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1553,16 +1714,16 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - extsprintf@1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + fast-decode-uri-component@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" @@ -1573,7 +1734,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3, fast-glob@^3.2.9, fast-glob@3.2.12: +fast-glob@3.2.12, fast-glob@^3.0.3, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -1892,6 +2053,20 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globby@10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz" + integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" @@ -1916,20 +2091,6 @@ globby@^14.0.1: slash "^5.1.0" unicorn-magic "^0.1.0" -globby@10.0.1: - version "10.0.1" - resolved "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz" - integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - graceful-fs@^4.1.2: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" @@ -2072,13 +2233,6 @@ human-signals@^5.0.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" @@ -2086,6 +2240,13 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" @@ -2640,26 +2801,19 @@ mimic-fn@^4.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.1: +minimatch@9.0.3, minimatch@^9.0.1: version "9.0.3" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: - brace-expansion "^2.0.1" + brace-expansion "^1.1.7" minimist@^1.2.5: version "1.2.7" @@ -3241,7 +3395,7 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -safer-buffer@^2.0.2, safer-buffer@^2.1.0, "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -3253,7 +3407,7 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -3277,11 +3431,6 @@ semver@^7.5.4: dependencies: lru-cache "^6.0.0" -"semver@2 || 3 || 4 || 5": - version "5.7.2" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" @@ -3337,12 +3486,7 @@ signal-exit@^3.0.2: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -signal-exit@^4.1.0: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -3495,7 +3639,7 @@ string.prototype.trimstart@^1.0.5: define-properties "^1.1.4" es-abstract "^1.19.5" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3509,13 +3653,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -3978,16 +4115,7 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==