Skip to content

Commit 7a1b0cc

Browse files
gxklclaude
andcommitted
refactor(service-worker): source app config from entry module.yml
Expose the entry app module's module.yml (the module scanned from baseDir) as the framework-owned app-wide `config` inner object, with a programmatic `config` override merged on top. Subsystems read their own slice (config.backgroundTask.timeout, config.mcp.*), so the app's module.yml is the single user config surface. Drop the `mcp` facade option and the `mcpTransportOptions` inner object from ServiceWorkerApp; ServiceWorkerMcpRouter now reads DNS-rebinding options from config.mcp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 756adc3 commit 7a1b0cc

11 files changed

Lines changed: 97 additions & 19 deletions

File tree

tegg/standalone/service-worker-controller/src/mcp/ServiceWorkerMcpRouter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export class ServiceWorkerMcpRouter implements McpRouter {
6262
@Inject()
6363
private readonly mcpAuthHandler: MCPAuthHandler;
6464

65+
// The app-wide config inner object (the app's module.yml); MCP transport
66+
// options live under its `mcp` key.
6567
@Inject()
66-
private readonly mcpTransportOptions: MCPTransportOptions;
68+
private readonly config: { mcp?: MCPTransportOptions };
6769

6870
#registrations: McpServerRegistration[] = [];
6971
#mounted = false;
@@ -121,7 +123,7 @@ export class ServiceWorkerMcpRouter implements McpRouter {
121123
allowedOrigins?: string[];
122124
enableDnsRebindingProtection?: boolean;
123125
} {
124-
const { allowedHosts, allowedOrigins, enableDnsRebindingProtection } = this.mcpTransportOptions ?? {};
126+
const { allowedHosts, allowedOrigins, enableDnsRebindingProtection } = this.config?.mcp ?? {};
125127
if (!allowedHosts?.length && !allowedOrigins?.length) {
126128
return {};
127129
}

tegg/standalone/service-worker-controller/src/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ export interface MCPAuthHandler {
3737

3838
/**
3939
* DNS-rebinding protection for the MCP transport, forwarded to the SDK's
40-
* web-standard transport. When `allowedHosts`/`allowedOrigins` are configured
41-
* the SDK validates the request Host/Origin; enable-protection defaults on once
42-
* either list is set. Left empty (the default) there is no host/origin gate —
43-
* set it before exposing the MCP endpoint beyond loopback.
40+
* web-standard transport. Read from the app-wide `config` inner object under the
41+
* `mcp` key (the app's `module.yml`, or a programmatic `config` override). When
42+
* `allowedHosts`/`allowedOrigins` are configured the SDK validates the request
43+
* Host/Origin; enable-protection defaults on once either list is set. Left empty
44+
* (the default) there is no host/origin gate — set it before exposing the MCP
45+
* endpoint beyond loopback.
4446
*/
4547
export interface MCPTransportOptions {
4648
allowedHosts?: string[];

tegg/standalone/service-worker/src/ServiceWorkerApp.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Readable } from 'node:stream';
44
import { pipeline } from 'node:stream/promises';
55
import { fileURLToPath } from 'node:url';
66

7-
import { FetchEventImpl, type MCPAuthHandler, type MCPTransportOptions } from '@eggjs/service-worker-controller';
7+
import { FetchEventImpl, type MCPAuthHandler } from '@eggjs/service-worker-controller';
88
import { ContextProtoProperty } from '@eggjs/service-worker-runtime';
99
import {
1010
StandaloneApp,
@@ -14,12 +14,14 @@ import {
1414
} from '@eggjs/standalone';
1515

1616
export interface ServiceWorkerAppOptions extends StandaloneAppOptions {
17-
/** Injected as the `config` inner object (BackgroundTaskHelper reads `config.backgroundTask.timeout`). */
17+
/**
18+
* Programmatic overrides for the app-wide `config` inner object. The app's own
19+
* `module.yml` is the primary config source; values here merge on top — e.g.
20+
* `config.backgroundTask.timeout`, `config.mcp.allowedHosts`.
21+
*/
1822
config?: Record<string, any>;
1923
/** Auth hook for MCP routes; the default lets every request through. */
2024
mcpAuthHandler?: MCPAuthHandler;
21-
/** DNS-rebinding protection for the MCP transport (Host/Origin allow-lists). */
22-
mcp?: MCPTransportOptions;
2325
}
2426

2527
const PASS_THROUGH_MCP_AUTH_HANDLER: MCPAuthHandler = {
@@ -45,7 +47,7 @@ export class ServiceWorkerApp {
4547
#initialized = false;
4648

4749
constructor(cwd: string, options?: ServiceWorkerAppOptions) {
48-
const { config, mcpAuthHandler, mcp, ...standaloneOptions } = options ?? {};
50+
const { config, mcpAuthHandler, ...standaloneOptions } = options ?? {};
4951
// Scan this package's own root so its framework-module deps
5052
// (service-worker-runtime + -controller) are auto-discovered via the
5153
// node_modules eggModule convention. `!test/**` keeps their test fixtures out.
@@ -63,10 +65,11 @@ export class ServiceWorkerApp {
6365
frameworkDeps,
6466
dump: standaloneOptions.dump,
6567
logger: standaloneOptions.logger,
68+
// The app-wide `config` inner object is the app's own module.yml; these
69+
// programmatic values merge on top.
70+
config,
6671
innerObjects: {
67-
config: [{ obj: config ?? {} }],
6872
mcpAuthHandler: [{ obj: mcpAuthHandler ?? PASS_THROUGH_MCP_AUTH_HANDLER }],
69-
mcpTransportOptions: [{ obj: mcp ?? {} }],
7073
...standaloneOptions.innerObjectHandlers,
7174
},
7275
});

tegg/standalone/service-worker/test/MCP.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('standalone/service-worker/test/MCP.test.ts mcpAuthHandler', () => {
179179
describe('standalone/service-worker/test/MCP.test.ts hardening', () => {
180180
it('should reject a request whose Host is not in allowedHosts', async () => {
181181
const app = new ServiceWorkerApp(path.join(__dirname, 'fixtures/hello-app'), {
182-
mcp: { allowedHosts: ['allowed.example'] },
182+
config: { mcp: { allowedHosts: ['allowed.example'] } },
183183
});
184184
const server = await app.serve();
185185
const { address, port } = server.address() as AddressInfo;

tegg/standalone/service-worker/test/ServiceWorkerApp.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ describe('standalone/service-worker/test/ServiceWorkerApp.test.ts', () => {
140140
assert.deepEqual(await res.json(), { features: { greeting: 'howdy' } });
141141
});
142142

143+
it('should expose the entry app module.yml as the app-wide config', async () => {
144+
const res = await fetch(`${base}/hello/app-config`);
145+
assert.deepEqual(await res.json(), { features: { greeting: 'howdy' } });
146+
});
147+
143148
it('should return the unified error shape for unknown routes', async () => {
144149
const res = await fetch(`${base}/nope`);
145150
assert.equal(res.status, 404);

tegg/standalone/service-worker/test/fixtures/hello-app/HelloController.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export class HelloController {
3636
@Inject()
3737
private readonly moduleConfigs: ModuleConfigs;
3838

39+
@Inject()
40+
private readonly config: Record<string, unknown>;
41+
3942
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/' })
4043
async index() {
4144
return { message: this.helloService.hello('tegg') };
@@ -75,6 +78,11 @@ export class HelloController {
7578
return this.moduleConfigs.get('helloApp');
7679
}
7780

81+
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/app-config' })
82+
async appConfig() {
83+
return this.config;
84+
}
85+
7886
@HTTPMethod({ method: HTTPMethodEnum.GET, path: '/background' })
7987
async background() {
8088
this.backgroundTaskHelper.run(async () => {

tegg/standalone/standalone/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ export class Foo implements MainRunner<string> {
4545
- options:
4646
- innerObjectHandlers: 当前运行环境中内置的对象
4747
- logger: standalone 框架及模块注入使用的 logger;不要放入 innerObjectHandlers
48+
- config: 应用级配置的程序化覆盖,会合并到 `config` 内置对象之上(见「配置」一节)
4849

49-
`moduleConfigs``moduleConfig``runtimeConfig` 由 standalone 框架维护;
50+
`config``moduleConfigs``moduleConfig``runtimeConfig` 由 standalone 框架维护;
5051
`innerObjectHandlers` 中的同名项会被忽略。
5152

5253
```
@@ -65,6 +66,10 @@ await main(cwd, {
6566

6667
module 支持通过 module.yml 来定义配置,在代码中可以通过注入 moduleConfigs 获取全局配置,通过注入 moduleConfig 来获取单 module 的配置。
6768

69+
入口应用 module(从 baseDir 扫描到的那个 module,其 module 目录即 cwd)的 `module.yml`
70+
会作为应用级的 `config` 内置对象暴露出来,通过 `@Inject() config` 注入即可读取;
71+
构造时传入的 `config` 选项会合并到其之上。子系统各取所需(如 `config.backgroundTask.timeout`)。
72+
6873
```yaml
6974
# module.yml
7075
# module 根目录中

tegg/standalone/standalone/src/StandaloneApp.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ export interface StandaloneAppInit {
4747
frameworkDeps?: (string | ModuleDependency)[];
4848
dump?: boolean;
4949
/**
50-
* Host-provided inner objects. Framework-owned `moduleConfigs`, `moduleConfig`,
51-
* and `runtimeConfig` entries are ignored. `logger` is reserved; use the
52-
* dedicated `logger` option instead.
50+
* Host-provided inner objects. Framework-owned `config`, `moduleConfigs`,
51+
* `moduleConfig`, and `runtimeConfig` entries are ignored. `logger` is
52+
* reserved; use the dedicated `logger` option instead.
5353
*/
5454
innerObjects?: Record<string, InnerObject[]>;
5555
/** User-facing option name for diagnostics. Defaults to `innerObjects`. */
@@ -60,6 +60,13 @@ export interface StandaloneAppInit {
6060
* to console.
6161
*/
6262
logger?: Logger;
63+
/**
64+
* Programmatic overrides for the app-wide `config` inner object. The entry
65+
* app module's `module.yml` (the module scanned from baseDir) is the primary
66+
* source; values here merge on top, so a host can supply config without a
67+
* file (tests, embedding).
68+
*/
69+
config?: Record<string, unknown>;
6370
}
6471

6572
/** init()-time binding: which app to load and its runtime identity. */
@@ -83,6 +90,8 @@ export interface StandaloneAppOptions {
8390
env?: string;
8491
name?: string;
8592
logger?: Logger;
93+
/** Programmatic overrides merged on top of the entry app module's `module.yml` config. */
94+
config?: Record<string, unknown>;
8695
/** Host-provided objects other than `logger`; use the dedicated logger option. */
8796
innerObjectHandlers?: Record<string, InnerObject[]>;
8897
dependencies?: (string | ModuleDependency)[];
@@ -103,6 +112,12 @@ export class StandaloneApp {
103112
// In the constructor there is no runtime config yet — pre-create the object
104113
// so the runtimeConfig inner object can hold it; init() fills the values.
105114
readonly #runtimeConfig = {} as RuntimeConfig;
115+
// The app-wide `config` inner object: the entry app module's module.yml with
116+
// the programmatic override on top. Pre-created so the inner object can hold
117+
// it; #loadModuleConfigs fills it once the entry module is known.
118+
readonly #config: Record<string, unknown> = {};
119+
readonly #configOverride?: Record<string, unknown>;
120+
#appModuleName?: string;
106121
#moduleReferences: readonly ModuleReference[] = [];
107122
#state: StandaloneAppState = 'new';
108123
#runnerProto?: EggPrototype;
@@ -123,6 +138,7 @@ export class StandaloneApp {
123138
if (init?.innerObjects && Object.hasOwn(init.innerObjects, 'logger')) {
124139
throw new Error(`[tegg/standalone] ${innerObjectsName}.logger is reserved; use the logger option instead`);
125140
}
141+
this.#configOverride = init?.config;
126142
this.#innerObjects = this.#createInnerObjects(init);
127143
this.scopeBag = TeggScope.createBag();
128144
}
@@ -139,6 +155,7 @@ export class StandaloneApp {
139155
// init() — the inner objects hold these same references unless the caller
140156
// supplies objects with other names.
141157
logger: [{ obj: this.#logger }],
158+
config: [{ obj: this.#config }],
142159
moduleConfigs: [{ obj: new ModuleConfigs(this.#moduleConfigs) }],
143160
moduleConfig: [] as InnerObject[],
144161
runtimeConfig: [{ obj: this.#runtimeConfig }],
@@ -162,6 +179,7 @@ export class StandaloneApp {
162179

163180
/** Load every module's config and expose it as a qualified `moduleConfig` inner object. */
164181
#loadModuleConfigs(): void {
182+
const baseDir = path.resolve(this.#runtimeConfig.baseDir);
165183
const resolvedReferences: ModuleReference[] = [];
166184
for (const reference of this.#moduleReferences) {
167185
const resolved = ModuleConfigUtil.resolveModuleConfigTolerant(reference, this.#runtimeConfig.baseDir);
@@ -177,9 +195,21 @@ export class StandaloneApp {
177195
reference: resolvedRef,
178196
config: resolved.config,
179197
};
198+
// The entry app module is the one scanned from baseDir (its module dir IS
199+
// the cwd); its module.yml is exposed as the app-wide `config`.
200+
if (path.resolve(resolved.path) === baseDir) {
201+
this.#appModuleName = resolved.name;
202+
}
180203
resolvedReferences.push(resolvedRef);
181204
}
182205
this.#moduleReferences = resolvedReferences;
206+
// app-wide config: entry app module.yml as the base, programmatic override on top.
207+
if (this.#appModuleName) {
208+
Object.assign(this.#config, this.#moduleConfigs[this.#appModuleName].config);
209+
}
210+
if (this.#configOverride) {
211+
Object.assign(this.#config, this.#configOverride);
212+
}
183213
for (const moduleConfig of Object.values(this.#moduleConfigs)) {
184214
this.#innerObjects.moduleConfig.push({
185215
obj: moduleConfig.config,

tegg/standalone/standalone/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export async function main<T = void>(cwd: string, options?: StandaloneAppOptions
6666
{
6767
frameworkDeps: options?.frameworkDeps,
6868
dump: options?.dump,
69+
config: options?.config,
6970
innerObjects: options?.innerObjectHandlers,
7071
innerObjectsName: 'innerObjectHandlers',
7172
logger: options?.logger,

wiki/log.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Dates use the workspace-local Asia/Shanghai calendar date.
44

5+
## [2026-07-14] behavior | app-wide `config` inner object = entry app module.yml
6+
7+
- sources touched: `tegg/standalone/standalone/src/StandaloneApp.ts`, `tegg/standalone/service-worker/src/ServiceWorkerApp.ts`, `tegg/standalone/service-worker-controller/src/{mcp/ServiceWorkerMcpRouter.ts,types.ts}`, `tegg/standalone/service-worker/test/{ServiceWorkerApp.test.ts,MCP.test.ts,fixtures/hello-app/HelloController.ts}`
8+
- pages updated: `wiki/packages/service-worker.md`, `wiki/log.md`
9+
- note: `StandaloneApp` now exposes the entry app module's `module.yml` (the module scanned from `baseDir`) as the framework-owned app-wide `config` inner object, with the programmatic `StandaloneAppInit.config` merged on top. This is the single user config surface: subsystems read their slice (`config.backgroundTask.timeout`, `config.mcp.*`). Removed the `mcp` facade option and the `mcpTransportOptions` inner object from `ServiceWorkerApp`; `ServiceWorkerMcpRouter` now reads DNS-rebinding options from `config.mcp`. Mirrors the internal standalone's `runner.config = moduleConfigs[appName].config`.
10+
511
## [2026-07-14] package | service-worker framework-module auto-discovery (drop hand-ordered frameworkDeps)
612

713
- sources touched: `tegg/standalone/service-worker-runtime/src/StandaloneEggObjectFactory.ts`, `tegg/standalone/service-worker/src/{ServiceWorkerApp.ts,index.ts}`, `tegg/standalone/service-worker/test/ServiceWorkerApp.test.ts`

0 commit comments

Comments
 (0)