@akira-tl/forgerelay 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.3] - 2026-09-14
8
+
9
+ ### Fixed
10
+
11
+ - MCP App 静态资源现在在 API Origin validation 之前进入专用静态资源路由,使 ChatGPT `*.web-sandbox.oaiusercontent.com` iframe 可以跨域加载 JavaScript/CSS;MCP、OAuth 与其他 API 路由仍继续拒绝未授权 Origin,不扩大 API 信任边界。
12
+ - 交互式 `7677` debug 实例现在复用生产 ForgeRelay 的 Skill source(默认 `~/.forgerelay/skills`),同时继续隔离 debug 的 config、auth、state 与端口,使 Activity Panel / Workspace Skill 展示与生产发现语义一致。
13
+
7
14
  ## [1.2.2] - 2026-09-13
8
15
 
9
16
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { checkResourceAllowed, createMcpHandler, isInitializeRequest, isLegacyRequest, localhostAllowedOrigins, resourceUrlFromServerUrl, } from "@modelcontextprotocol/server";
3
- import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBearerAuth, } from "@modelcontextprotocol/express";
3
+ import { getOAuthProtectedResourceMetadataUrl, hostHeaderValidation, localhostHostValidation, originValidation, requireBearerAuth, } from "@modelcontextprotocol/express";
4
4
  import { NodeStreamableHTTPServerTransport, toNodeHandler, toWebRequest, } from "@modelcontextprotocol/node";
5
5
  import express from "express";
6
6
  import { ActivityAuditStore } from "../../../activity/history/audit-store.js";
@@ -40,11 +40,14 @@ export function createHttpServer(config, options, createMcpServer) {
40
40
  ...localhostAllowedOrigins(),
41
41
  ...routeBaseUrls.map((baseUrl) => baseUrl.hostname),
42
42
  ]));
43
- const app = createMcpExpressApp({
44
- host: config.host,
45
- ...(allowedHosts ? { allowedHosts } : {}),
46
- allowedOrigins,
47
- });
43
+ const app = express();
44
+ app.use(express.json());
45
+ if (allowedHosts) {
46
+ app.use(hostHeaderValidation(allowedHosts));
47
+ }
48
+ else if (["127.0.0.1", "localhost", "::1"].includes(config.host)) {
49
+ app.use(localhostHostValidation());
50
+ }
48
51
  const transports = new McpTransportRegistry({
49
52
  maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
50
53
  });
@@ -193,16 +196,10 @@ export function createHttpServer(config, options, createMcpServer) {
193
196
  });
194
197
  next();
195
198
  });
196
- app.use(createForgeRelayAuthRouter({
197
- provider: oauthProvider,
198
- cliAuthenticationProvider: oauthProvider,
199
- instanceId: config.instanceId,
200
- issuerUrl: new URL(config.publicBaseUrl),
201
- resourceServerUrl,
202
- routeBaseUrls,
203
- scopesSupported: config.oauth.scopes,
204
- resourceName: "ForgeRelay",
205
- }));
199
+ // MCP App assets are intentionally public, cross-origin resources. ChatGPT
200
+ // renders an app on a dedicated `*.web-sandbox.oaiusercontent.com` origin,
201
+ // so these routes must run before MCP/API Origin validation. Host validation
202
+ // still applies above, while MCP, OAuth, and health routes remain protected.
206
203
  app.options(activityPanelAssetsPaths.map((assetPath) => `${assetPath}/{*asset}`), (_req, res) => {
207
204
  setActivityPanelAssetHeaders(res);
208
205
  res.sendStatus(204);
@@ -213,6 +210,17 @@ export function createHttpServer(config, options, createMcpServer) {
213
210
  fallthrough: false,
214
211
  setHeaders: setActivityPanelAssetHeaders,
215
212
  }));
213
+ app.use(originValidation(allowedOrigins));
214
+ app.use(createForgeRelayAuthRouter({
215
+ provider: oauthProvider,
216
+ cliAuthenticationProvider: oauthProvider,
217
+ instanceId: config.instanceId,
218
+ issuerUrl: new URL(config.publicBaseUrl),
219
+ resourceServerUrl,
220
+ routeBaseUrls,
221
+ scopesSupported: config.oauth.scopes,
222
+ resourceName: "ForgeRelay",
223
+ }));
216
224
  app.get(healthPaths, (_req, res) => {
217
225
  res.json({ ok: true, name: "forgerelay" });
218
226
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -67,6 +67,16 @@ export function createInteractiveDebugEnvironment({
67
67
  const configDir = interactiveDebugConfigDir({ env, home });
68
68
  const ownerToken = interactiveDebugOwnerToken(configDir);
69
69
  const { baseUrl, mcpUrl } = interactiveDebugUrls(configDir);
70
+ const productConfigDir = env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR
71
+ ? resolve(env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.startsWith("~/")
72
+ ? join(home, env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR.slice(2))
73
+ : env.FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR)
74
+ : resolve(join(home, ".forgerelay"));
75
+ const productSkillPaths = [
76
+ join(productConfigDir, "skills"),
77
+ env.FORGERELAY_SKILL_PATHS,
78
+ ].filter((value) => typeof value === "string" && value.trim());
79
+ const productSkillsEnabled = env.FORGERELAY_SKILLS;
70
80
  mkdirSync(debugRoot, { recursive: true });
71
81
 
72
82
  const debugEnv = { ...env };
@@ -84,6 +94,10 @@ export function createInteractiveDebugEnvironment({
84
94
  }
85
95
  }
86
96
  debugEnv.FORGERELAY_CONFIG_DIR = configDir;
97
+ debugEnv.FORGERELAY_SKILL_PATHS = productSkillPaths.join(",");
98
+ if (productSkillsEnabled !== undefined) {
99
+ debugEnv.FORGERELAY_SKILLS = productSkillsEnabled;
100
+ }
87
101
 
88
102
  return { ownerToken, configDir, baseUrl, mcpUrl, env: debugEnv };
89
103
  }
@@ -40,6 +40,8 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
40
40
  FORGERELAY_ALLOWED_HOSTS: "wrong.example.test",
41
41
  FORGERELAY_OAUTH_OWNER_TOKEN: "wrong-owner-password-123456",
42
42
  FORGERELAY_WIDGETS: "off",
43
+ FORGERELAY_SKILL_PATHS: "/extra/product/skills",
44
+ FORGERELAY_SKILLS: "1",
43
45
  FORGERELAY_LOG_LEVEL: "debug",
44
46
  },
45
47
  home,
@@ -62,6 +64,11 @@ test("interactive debug uses one dedicated persisted config under ~/.forgerelay/
62
64
  assert.equal(result.env.FORGERELAY_ALLOWED_HOSTS, undefined);
63
65
  assert.equal(result.env.FORGERELAY_OAUTH_OWNER_TOKEN, undefined);
64
66
  assert.equal(result.env.FORGERELAY_WIDGETS, undefined);
67
+ assert.equal(
68
+ result.env.FORGERELAY_SKILL_PATHS,
69
+ [join(home, ".forgerelay", "skills"), "/extra/product/skills"].join(","),
70
+ );
71
+ assert.equal(result.env.FORGERELAY_SKILLS, "1");
65
72
  assert.equal(result.env.FORGERELAY_LOG_LEVEL, "debug");
66
73
  });
67
74
 
@@ -80,8 +87,12 @@ test("interactive debug config directory may be explicitly relocated without fal
80
87
  await writeFile(join(customDir, "config.json"), JSON.stringify({ host: "127.0.0.1", port: 6768 }) + "\n");
81
88
  await writeFile(join(customDir, "auth.json"), JSON.stringify({ ownerToken: "custom-debug-password-123456" }) + "\n");
82
89
 
90
+ const productConfigDir = join(home, "custom-product-config");
83
91
  const result = createInteractiveDebugEnvironment({
84
- env: { FORGERELAY_DEBUG_CONFIG_DIR: customDir },
92
+ env: {
93
+ FORGERELAY_DEBUG_CONFIG_DIR: customDir,
94
+ FORGERELAY_DEBUG_PRODUCT_CONFIG_DIR: productConfigDir,
95
+ },
85
96
  home,
86
97
  });
87
98
 
@@ -90,4 +101,5 @@ test("interactive debug config directory may be explicitly relocated without fal
90
101
  assert.equal(result.env.FORGERELAY_CONFIG_DIR, customDir);
91
102
  assert.equal(result.baseUrl, "http://127.0.0.1:6768");
92
103
  assert.equal(result.mcpUrl, "http://127.0.0.1:6768/mcp");
104
+ assert.equal(result.env.FORGERELAY_SKILL_PATHS, join(productConfigDir, "skills"));
93
105
  });