@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 {
|
|
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 =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
@@ -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: {
|
|
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
|
});
|