From 04fa4f10eedee38102157cd4911fa253e756c656 Mon Sep 17 00:00:00 2001 From: "xuping.wang" Date: Thu, 27 Aug 2026 23:57:48 +0800 Subject: [PATCH 1/2] chore: migrate egg-cors into monorepo --- plugins/cors/LICENSE | 21 +++ plugins/cors/README.md | 44 ++++++ plugins/cors/package.json | 67 ++++++++++ plugins/cors/src/app.ts | 36 +++++ plugins/cors/src/app/middleware/cors.ts | 3 + plugins/cors/src/config/config.default.ts | 10 ++ plugins/cors/src/index.ts | 14 ++ plugins/cors/src/types.ts | 7 + plugins/cors/test/cors.test.ts | 125 ++++++++++++++++++ .../apps/cors-origin-function/app/router.js | 10 ++ .../config/config.default.js | 8 ++ .../cors-origin-function/config/plugin.js | 5 + .../apps/cors-origin-function/package.json | 3 + .../fixtures/apps/cors-origin/app/router.js | 5 + .../apps/cors-origin/config/config.default.js | 8 ++ .../apps/cors-origin/config/plugin.js | 5 + .../fixtures/apps/cors-origin/package.json | 3 + .../apps/cors-private-network/app/router.js | 5 + .../config/config.default.js | 6 + .../cors-private-network/config/plugin.js | 5 + .../apps/cors-private-network/package.json | 3 + .../test/fixtures/apps/cors/app/router.js | 5 + .../apps/cors/config/config.default.js | 5 + .../test/fixtures/apps/cors/config/plugin.js | 5 + .../cors/test/fixtures/apps/cors/package.json | 3 + plugins/cors/tsconfig.json | 3 + plugins/cors/vitest.config.ts | 3 + pnpm-workspace.yaml | 2 + tsconfig.json | 3 + wiki/index.md | 1 + wiki/log.md | 6 + wiki/packages/cors.md | 21 +++ 32 files changed, 450 insertions(+) create mode 100644 plugins/cors/LICENSE create mode 100644 plugins/cors/README.md create mode 100644 plugins/cors/package.json create mode 100644 plugins/cors/src/app.ts create mode 100644 plugins/cors/src/app/middleware/cors.ts create mode 100644 plugins/cors/src/config/config.default.ts create mode 100644 plugins/cors/src/index.ts create mode 100644 plugins/cors/src/types.ts create mode 100644 plugins/cors/test/cors.test.ts create mode 100644 plugins/cors/test/fixtures/apps/cors-origin-function/app/router.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin-function/config/config.default.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin-function/config/plugin.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin-function/package.json create mode 100644 plugins/cors/test/fixtures/apps/cors-origin/app/router.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin/config/config.default.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin/config/plugin.js create mode 100644 plugins/cors/test/fixtures/apps/cors-origin/package.json create mode 100644 plugins/cors/test/fixtures/apps/cors-private-network/app/router.js create mode 100644 plugins/cors/test/fixtures/apps/cors-private-network/config/config.default.js create mode 100644 plugins/cors/test/fixtures/apps/cors-private-network/config/plugin.js create mode 100644 plugins/cors/test/fixtures/apps/cors-private-network/package.json create mode 100644 plugins/cors/test/fixtures/apps/cors/app/router.js create mode 100644 plugins/cors/test/fixtures/apps/cors/config/config.default.js create mode 100644 plugins/cors/test/fixtures/apps/cors/config/plugin.js create mode 100644 plugins/cors/test/fixtures/apps/cors/package.json create mode 100644 plugins/cors/tsconfig.json create mode 100644 plugins/cors/vitest.config.ts create mode 100644 wiki/packages/cors.md diff --git a/plugins/cors/LICENSE b/plugins/cors/LICENSE new file mode 100644 index 0000000000..8363a1e45b --- /dev/null +++ b/plugins/cors/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Alibaba Group Holding Limited and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cors/README.md b/plugins/cors/README.md new file mode 100644 index 0000000000..4e00153230 --- /dev/null +++ b/plugins/cors/README.md @@ -0,0 +1,44 @@ +# @eggjs/cors + +[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) plugin for Egg, +based on [@koa/cors](https://github.com/koajs/cors). + +## Install + +```bash +npm install @eggjs/cors +``` + +## Usage + +```ts +// config/plugin.ts +import corsPlugin from '@eggjs/cors'; + +export default { + ...corsPlugin(), +}; +``` + +When no custom `origin` is configured, the plugin uses the Security plugin's +`domainWhiteList`. Without the Security plugin, the request origin is allowed. + +## Configuration + +All [@koa/cors options](https://github.com/koajs/cors#corsoptions) are supported. + +```ts +// config/config.default.ts +export default { + cors: { + origin: 'https://example.com', + credentials: true, + }, +}; +``` + +A custom `origin` takes precedence over `security.domainWhiteList`. + +## License + +[MIT](LICENSE) diff --git a/plugins/cors/package.json b/plugins/cors/package.json new file mode 100644 index 0000000000..fd06c278c9 --- /dev/null +++ b/plugins/cors/package.json @@ -0,0 +1,67 @@ +{ + "name": "@eggjs/cors", + "version": "4.0.0-beta.26", + "description": "CORS plugin for Egg", + "keywords": [ + "cors", + "egg", + "egg-plugin" + ], + "homepage": "https://github.com/eggjs/egg/tree/next/plugins/cors", + "bugs": { + "url": "https://github.com/eggjs/egg/issues" + }, + "license": "MIT", + "author": "dead_horse", + "repository": { + "type": "git", + "url": "git+https://github.com/eggjs/egg.git", + "directory": "plugins/cors" + }, + "files": [ + "dist" + ], + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./src/index.ts", + "./app": "./src/app.ts", + "./app/middleware/cors": "./src/app/middleware/cors.ts", + "./config/config.default": "./src/config/config.default.ts", + "./types": "./src/types.ts", + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": "./dist/index.js", + "./app": "./dist/app.js", + "./app/middleware/cors": "./dist/app/middleware/cors.js", + "./config/config.default": "./dist/config/config.default.js", + "./types": "./dist/types.js", + "./package.json": "./package.json" + } + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@koa/cors": "catalog:" + }, + "devDependencies": { + "@eggjs/mock": "workspace:*", + "@eggjs/tsconfig": "workspace:*", + "@types/koa__cors": "catalog:", + "@types/node": "catalog:", + "egg": "workspace:*", + "typescript": "catalog:" + }, + "peerDependencies": { + "egg": "workspace:*" + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/plugins/cors/src/app.ts b/plugins/cors/src/app.ts new file mode 100644 index 0000000000..b85599ab65 --- /dev/null +++ b/plugins/cors/src/app.ts @@ -0,0 +1,36 @@ +import type { Application, Context, ILifecycleBoot } from 'egg'; +import type { Context as KoaContext } from 'koa'; + +export default class AppBoot implements ILifecycleBoot { + #app: Application; + + constructor(app: Application) { + this.#app = app; + } + + configWillLoad(): void { + const { config } = this.#app; + config.coreMiddleware.unshift('cors'); + + config.cors.hasCustomOriginHandler = Boolean(config.cors.origin); + config.cors.origin ??= function corsOrigin(ctx: KoaContext): string { + const origin = ctx.get('origin'); + if (!origin) return ''; + + const eggContext = ctx as unknown as Context; + if (typeof eggContext.isSafeDomain !== 'function') return origin; + + let parsedUrl: URL; + try { + parsedUrl = new URL(origin); + } catch { + return ''; + } + + if (eggContext.isSafeDomain(parsedUrl.hostname) || eggContext.isSafeDomain(origin)) { + return origin; + } + return ''; + }; + } +} diff --git a/plugins/cors/src/app/middleware/cors.ts b/plugins/cors/src/app/middleware/cors.ts new file mode 100644 index 0000000000..1490b654e6 --- /dev/null +++ b/plugins/cors/src/app/middleware/cors.ts @@ -0,0 +1,3 @@ +import cors from '@koa/cors'; + +export default cors; diff --git a/plugins/cors/src/config/config.default.ts b/plugins/cors/src/config/config.default.ts new file mode 100644 index 0000000000..8cfba024f2 --- /dev/null +++ b/plugins/cors/src/config/config.default.ts @@ -0,0 +1,10 @@ +import type { Options as KoaCorsOptions } from '@koa/cors'; + +export interface CorsConfig extends KoaCorsOptions { + /** Whether the application supplied its own origin handler. */ + hasCustomOriginHandler?: boolean; +} + +export default { + cors: {} as CorsConfig, +}; diff --git a/plugins/cors/src/index.ts b/plugins/cors/src/index.ts new file mode 100644 index 0000000000..92929795d7 --- /dev/null +++ b/plugins/cors/src/index.ts @@ -0,0 +1,14 @@ +import './types.ts'; +import { definePluginFactory, type EggPluginFactory } from 'egg'; + +/** + * CORS plugin. + * + * @since 4.1.0 + */ +export default definePluginFactory({ + name: 'cors', + enable: true, + path: import.meta.dirname, + optionalDependencies: ['security'], +}) as EggPluginFactory; diff --git a/plugins/cors/src/types.ts b/plugins/cors/src/types.ts new file mode 100644 index 0000000000..2653947de0 --- /dev/null +++ b/plugins/cors/src/types.ts @@ -0,0 +1,7 @@ +import type { CorsConfig } from './config/config.default.ts'; + +declare module 'egg' { + interface EggAppConfig { + cors: CorsConfig; + } +} diff --git a/plugins/cors/test/cors.test.ts b/plugins/cors/test/cors.test.ts new file mode 100644 index 0000000000..29739b9b4d --- /dev/null +++ b/plugins/cors/test/cors.test.ts @@ -0,0 +1,125 @@ +import { strict as assert } from 'node:assert'; +import path from 'node:path'; + +import { mm, type MockApplication } from '@eggjs/mock'; +import { afterAll, afterEach, beforeAll, describe, it } from 'vitest'; + +function createApp(name: string): MockApplication { + return mm.app({ + baseDir: path.join(import.meta.dirname, 'fixtures', 'apps', name), + }); +} + +describe('@eggjs/cors', () => { + let app: MockApplication; + + beforeAll(async () => { + app = createApp('cors'); + await app.ready(); + }); + afterAll(() => app.close()); + afterEach(() => mm.restore()); + + it('does not set an origin header when the request has no origin', async () => { + await app + .httpRequest() + .get('/') + .expect({ foo: 'bar' }) + .expect((res) => assert.equal(res.headers['access-control-allow-origin'], undefined)) + .expect(200); + }); + + it('allows origins in the security domain whitelist', async () => { + await app + .httpRequest() + .get('/') + .set('Origin', 'http://test.eggjs.org') + .expect('Access-Control-Allow-Origin', 'http://test.eggjs.org') + .expect('Access-Control-Allow-Credentials', 'true') + .expect(200); + + await app + .httpRequest() + .get('/') + .set('Origin', 'https://b.com:1234') + .expect('Access-Control-Allow-Origin', 'https://b.com:1234') + .expect(200); + }); + + it('rejects origins outside the security domain whitelist', async () => { + await app + .httpRequest() + .get('/') + .set('Origin', 'http://eggjs.org!.evil.com') + .expect((res) => { + assert.equal(res.headers['access-control-allow-origin'], undefined); + assert.equal(res.headers['access-control-allow-credentials'], undefined); + }) + .expect(200); + }); +}); + +describe('@eggjs/cors with a string origin', () => { + let app: MockApplication; + + beforeAll(async () => { + app = createApp('cors-origin'); + await app.ready(); + }); + afterAll(() => app.close()); + afterEach(() => mm.restore()); + + it('uses the configured origin instead of the whitelist', async () => { + await app + .httpRequest() + .get('/') + .set('Origin', 'http://not-in-the-whitelist.example') + .expect('Access-Control-Allow-Origin', 'eggjs.org') + .expect('Access-Control-Allow-Credentials', 'true') + .expect(200); + }); +}); + +describe('@eggjs/cors with an origin function', () => { + let app: MockApplication; + + beforeAll(async () => { + app = createApp('cors-origin-function'); + await app.ready(); + }); + afterAll(() => app.close()); + afterEach(() => mm.restore()); + + it('marks and invokes the custom origin handler', async () => { + await app.httpRequest().get('/config').expect({ hasCustomOriginHandler: true }).expect(200); + await app + .httpRequest() + .get('/') + .set('Origin', 'http://example.com') + .expect('Access-Control-Allow-Origin', 'eggjs.org') + .expect(200); + }); +}); + +describe('@eggjs/cors private network access', () => { + let app: MockApplication; + + beforeAll(async () => { + app = createApp('cors-private-network'); + await app.ready(); + }); + afterAll(() => app.close()); + afterEach(() => mm.restore()); + + it('sets the private network header on a preflight request', async () => { + await app + .httpRequest() + .options('/') + .set('Origin', 'https://eggjs.org') + .set('Access-Control-Request-Method', 'POST') + .set('Access-Control-Request-Private-Network', 'true') + .expect('Access-Control-Allow-Origin', 'https://eggjs.org') + .expect('Access-Control-Allow-Private-Network', 'true') + .expect(204); + }); +}); diff --git a/plugins/cors/test/fixtures/apps/cors-origin-function/app/router.js b/plugins/cors/test/fixtures/apps/cors-origin-function/app/router.js new file mode 100644 index 0000000000..e026153e86 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin-function/app/router.js @@ -0,0 +1,10 @@ +module.exports = (app) => { + app.get('/', async (ctx) => { + ctx.body = { foo: 'bar' }; + }); + app.get('/config', async (ctx) => { + ctx.body = { + hasCustomOriginHandler: ctx.app.config.cors.hasCustomOriginHandler, + }; + }); +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin-function/config/config.default.js b/plugins/cors/test/fixtures/apps/cors-origin-function/config/config.default.js new file mode 100644 index 0000000000..4dae047c31 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin-function/config/config.default.js @@ -0,0 +1,8 @@ +exports.keys = 'cors-origin-function-test'; +exports.cors = { + async origin(ctx) { + if (!ctx.get('origin')) return ''; + return 'eggjs.org'; + }, + credentials: true, +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin-function/config/plugin.js b/plugins/cors/test/fixtures/apps/cors-origin-function/config/plugin.js new file mode 100644 index 0000000000..c1e1b74d70 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin-function/config/plugin.js @@ -0,0 +1,5 @@ +import corsPlugin from '../../../../../src/index.ts'; + +module.exports = { + ...corsPlugin(), +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin-function/package.json b/plugins/cors/test/fixtures/apps/cors-origin-function/package.json new file mode 100644 index 0000000000..83dec6e108 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin-function/package.json @@ -0,0 +1,3 @@ +{ + "name": "cors-origin-function-test" +} diff --git a/plugins/cors/test/fixtures/apps/cors-origin/app/router.js b/plugins/cors/test/fixtures/apps/cors-origin/app/router.js new file mode 100644 index 0000000000..cb27a67cff --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin/app/router.js @@ -0,0 +1,5 @@ +module.exports = (app) => { + app.get('/', async (ctx) => { + ctx.body = { foo: 'bar' }; + }); +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin/config/config.default.js b/plugins/cors/test/fixtures/apps/cors-origin/config/config.default.js new file mode 100644 index 0000000000..3e491dc063 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin/config/config.default.js @@ -0,0 +1,8 @@ +exports.keys = 'cors-origin-test'; +exports.cors = { + origin: 'eggjs.org', + credentials: true, +}; +exports.security = { + domainWhiteList: ['eggjs-white.org'], +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin/config/plugin.js b/plugins/cors/test/fixtures/apps/cors-origin/config/plugin.js new file mode 100644 index 0000000000..c1e1b74d70 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin/config/plugin.js @@ -0,0 +1,5 @@ +import corsPlugin from '../../../../../src/index.ts'; + +module.exports = { + ...corsPlugin(), +}; diff --git a/plugins/cors/test/fixtures/apps/cors-origin/package.json b/plugins/cors/test/fixtures/apps/cors-origin/package.json new file mode 100644 index 0000000000..2a0dc79172 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-origin/package.json @@ -0,0 +1,3 @@ +{ + "name": "cors-origin-test" +} diff --git a/plugins/cors/test/fixtures/apps/cors-private-network/app/router.js b/plugins/cors/test/fixtures/apps/cors-private-network/app/router.js new file mode 100644 index 0000000000..cb27a67cff --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-private-network/app/router.js @@ -0,0 +1,5 @@ +module.exports = (app) => { + app.get('/', async (ctx) => { + ctx.body = { foo: 'bar' }; + }); +}; diff --git a/plugins/cors/test/fixtures/apps/cors-private-network/config/config.default.js b/plugins/cors/test/fixtures/apps/cors-private-network/config/config.default.js new file mode 100644 index 0000000000..a4a374af36 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-private-network/config/config.default.js @@ -0,0 +1,6 @@ +exports.keys = 'cors-private-network-test'; +exports.cors = { privateNetworkAccess: true }; +exports.security = { + csrf: false, + domainWhiteList: ['.eggjs.org'], +}; diff --git a/plugins/cors/test/fixtures/apps/cors-private-network/config/plugin.js b/plugins/cors/test/fixtures/apps/cors-private-network/config/plugin.js new file mode 100644 index 0000000000..c1e1b74d70 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-private-network/config/plugin.js @@ -0,0 +1,5 @@ +import corsPlugin from '../../../../../src/index.ts'; + +module.exports = { + ...corsPlugin(), +}; diff --git a/plugins/cors/test/fixtures/apps/cors-private-network/package.json b/plugins/cors/test/fixtures/apps/cors-private-network/package.json new file mode 100644 index 0000000000..aebe0b2df3 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors-private-network/package.json @@ -0,0 +1,3 @@ +{ + "name": "cors-private-network-test" +} diff --git a/plugins/cors/test/fixtures/apps/cors/app/router.js b/plugins/cors/test/fixtures/apps/cors/app/router.js new file mode 100644 index 0000000000..cb27a67cff --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors/app/router.js @@ -0,0 +1,5 @@ +module.exports = (app) => { + app.get('/', async (ctx) => { + ctx.body = { foo: 'bar' }; + }); +}; diff --git a/plugins/cors/test/fixtures/apps/cors/config/config.default.js b/plugins/cors/test/fixtures/apps/cors/config/config.default.js new file mode 100644 index 0000000000..bdb741a7a5 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors/config/config.default.js @@ -0,0 +1,5 @@ +exports.keys = 'cors-test'; +exports.cors = { credentials: true }; +exports.security = { + domainWhiteList: ['.eggjs.org', 'https://b.com:1234'], +}; diff --git a/plugins/cors/test/fixtures/apps/cors/config/plugin.js b/plugins/cors/test/fixtures/apps/cors/config/plugin.js new file mode 100644 index 0000000000..c1e1b74d70 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors/config/plugin.js @@ -0,0 +1,5 @@ +import corsPlugin from '../../../../../src/index.ts'; + +module.exports = { + ...corsPlugin(), +}; diff --git a/plugins/cors/test/fixtures/apps/cors/package.json b/plugins/cors/test/fixtures/apps/cors/package.json new file mode 100644 index 0000000000..29987930d3 --- /dev/null +++ b/plugins/cors/test/fixtures/apps/cors/package.json @@ -0,0 +1,3 @@ +{ + "name": "cors-test" +} diff --git a/plugins/cors/tsconfig.json b/plugins/cors/tsconfig.json new file mode 100644 index 0000000000..4082f16a5d --- /dev/null +++ b/plugins/cors/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/plugins/cors/vitest.config.ts b/plugins/cors/vitest.config.ts new file mode 100644 index 0000000000..ebb0784a70 --- /dev/null +++ b/plugins/cors/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({}); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8029106a4b..ab01b6df31 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ packages: - tegg/standalone/* catalog: + '@koa/cors': ^5.0.0 '@clack/prompts': ^0.11.0 '@eggjs/compressible': ^3.0.0 '@eggjs/cookies': ^3.1.0 @@ -42,6 +43,7 @@ catalog: '@types/js-beautify': ^1.14.3 '@types/js-yaml': ^4.0.9 '@types/koa-bodyparser': ^4.3.12 + '@types/koa__cors': ^5.0.1 '@types/koa-compose': ^3.2.8 '@types/koa-range': ^0.3.5 '@types/lodash': ^4.17.20 diff --git a/tsconfig.json b/tsconfig.json index db00302e83..384f1bd0ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -65,6 +65,9 @@ { "path": "./plugins/jsonp" }, + { + "path": "./plugins/cors" + }, { "path": "./plugins/watcher" }, diff --git a/wiki/index.md b/wiki/index.md index 87c7e0d8f7..43583d577d 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -25,6 +25,7 @@ Read this file before exploring raw sources. ## Packages - [Core Package](./packages/core.md) - Loader, lifecycle, and application core primitives used by Egg runtime packages. +- [CORS Plugin](./packages/cors.md) - Applies @koa/cors with optional Security domain checks. - [Egg Bundler](./packages/egg-bundler.md) - Bundles Egg applications for Node startup snapshots and tegg standalone service workers. - [Loader FS Package](./packages/loader-fs.md) - Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes. - [Onerror Plugin](./packages/onerror.md) - Default Egg error-handling plugin and configurable response negotiation layer. diff --git a/wiki/log.md b/wiki/log.md index 6b14bffa48..6e61a144f5 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -2,6 +2,12 @@ Dates use the workspace-local Asia/Shanghai calendar date. +## [2026-08-27] package | migrate egg-cors into the monorepo + +- sources touched: `plugins/cors` +- pages updated: `wiki/index.md`, `wiki/log.md`, `wiki/packages/cors.md` +- note: Added the scoped `@eggjs/cors` plugin with the existing Security whitelist fallback, typed @koa/cors configuration, modern plugin metadata, and Vitest coverage. + ## [2026-08-06] fix | restore standalone public dynamic injection - sources touched: `tegg/standalone/{standalone,service-worker-runtime}` diff --git a/wiki/packages/cors.md b/wiki/packages/cors.md new file mode 100644 index 0000000000..8be71980cd --- /dev/null +++ b/wiki/packages/cors.md @@ -0,0 +1,21 @@ +--- +title: CORS Plugin +type: package +summary: Egg plugin that applies @koa/cors with optional Security domain checks. +source_files: + - plugins/cors/src/index.ts + - plugins/cors/src/app.ts + - plugins/cors/src/config/config.default.ts +updated_at: 2026-08-27 +status: active +--- + +# CORS Plugin + +`@eggjs/cors` installs `@koa/cors` at the front of Egg's core middleware list. +Applications can use the full upstream CORS option surface. + +When an application does not configure `cors.origin`, the plugin consults the +optional Security plugin's `ctx.isSafeDomain()` check. If Security is not +enabled, it permits the request origin. A custom origin string or function +takes precedence and is recorded by `cors.hasCustomOriginHandler`. From 6f9b2b4dc99639fd6dcbcf714859c10753c60242 Mon Sep 17 00:00:00 2001 From: "xuping.wang" <2232455931@qq.com> Date: Fri, 28 Aug 2026 00:09:08 +0800 Subject: [PATCH 2/2] fix: address cors migration review feedback --- plugins/cors/package.json | 4 ++-- plugins/cors/src/app.ts | 6 +++++- plugins/cors/test/cors.test.ts | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/cors/package.json b/plugins/cors/package.json index fd06c278c9..43c64a5b72 100644 --- a/plugins/cors/package.json +++ b/plugins/cors/package.json @@ -48,12 +48,12 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { - "@koa/cors": "catalog:" + "@koa/cors": "catalog:", + "@types/koa__cors": "catalog:" }, "devDependencies": { "@eggjs/mock": "workspace:*", "@eggjs/tsconfig": "workspace:*", - "@types/koa__cors": "catalog:", "@types/node": "catalog:", "egg": "workspace:*", "typescript": "catalog:" diff --git a/plugins/cors/src/app.ts b/plugins/cors/src/app.ts index b85599ab65..f018cdccb5 100644 --- a/plugins/cors/src/app.ts +++ b/plugins/cors/src/app.ts @@ -10,7 +10,11 @@ export default class AppBoot implements ILifecycleBoot { configWillLoad(): void { const { config } = this.#app; - config.coreMiddleware.unshift('cors'); + const coreMiddleware = config.coreMiddleware; + for (let index = coreMiddleware.lastIndexOf('cors'); index >= 0; index = coreMiddleware.lastIndexOf('cors')) { + coreMiddleware.splice(index, 1); + } + coreMiddleware.unshift('cors'); config.cors.hasCustomOriginHandler = Boolean(config.cors.origin); config.cors.origin ??= function corsOrigin(ctx: KoaContext): string { diff --git a/plugins/cors/test/cors.test.ts b/plugins/cors/test/cors.test.ts index 29739b9b4d..a5d5c229a7 100644 --- a/plugins/cors/test/cors.test.ts +++ b/plugins/cors/test/cors.test.ts @@ -2,8 +2,11 @@ import { strict as assert } from 'node:assert'; import path from 'node:path'; import { mm, type MockApplication } from '@eggjs/mock'; +import type { Application } from 'egg'; import { afterAll, afterEach, beforeAll, describe, it } from 'vitest'; +import AppBoot from '../src/app.ts'; + function createApp(name: string): MockApplication { return mm.app({ baseDir: path.join(import.meta.dirname, 'fixtures', 'apps', name), @@ -59,6 +62,22 @@ describe('@eggjs/cors', () => { }); }); +describe('@eggjs/cors middleware registration', () => { + it('moves an existing cors middleware to the front without duplicating it', () => { + const coreMiddleware = ['bodyParser', 'cors', 'overrideMethod', 'cors']; + const app = { + config: { + coreMiddleware, + cors: {}, + }, + } as unknown as Application; + + new AppBoot(app).configWillLoad(); + + assert.deepEqual(coreMiddleware, ['cors', 'bodyParser', 'overrideMethod']); + }); +}); + describe('@eggjs/cors with a string origin', () => { let app: MockApplication;