@michengai/dsh-codex-ui 0.2.111 → 0.2.113

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/lib/index.mjs CHANGED
@@ -5,6 +5,16 @@ import { homedir } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { randomBytes, randomUUID } from "node:crypto";
8
+ //#region src/business-api.ts
9
+ const CODEX_UI_API_BASE = "/api/dsh-codex-ui";
10
+ const CODEX_UI_API_ENDPOINTS = Object.freeze({
11
+ connectors: `${CODEX_UI_API_BASE}/connectors`,
12
+ dependencies: `${CODEX_UI_API_BASE}/dependencies`,
13
+ openInExplorer: `${CODEX_UI_API_BASE}/open-in-explorer`,
14
+ preferences: `${CODEX_UI_API_BASE}/preferences`,
15
+ sessionMove: `${CODEX_UI_API_BASE}/session-move`
16
+ });
17
+ //#endregion
8
18
  //#region src/dependencies.ts
9
19
  const SUITE_PACKAGE = "@michengai/dsh-codex-suite";
10
20
  const SUITE_MEMBER_PACKAGES = [
@@ -1558,41 +1568,17 @@ function writeWorkspacePreferences(pinnedWorkspaceIds, workspaceGroups = [], pat
1558
1568
  }
1559
1569
  //#endregion
1560
1570
  //#region src/index.ts
1561
- const connectorsEndpoint = "/api/michengai/codex-ui/connectors";
1562
- const dependenciesEndpoint = "/api/michengai/codex-ui/dependencies";
1563
- const explorerEndpoint = "/api/michengai/codex-ui/open-in-explorer";
1564
- const preferencesEndpoint = "/api/michengai/codex-ui/preferences";
1565
- const sessionMoveEndpoint = "/api/michengai/codex-ui/session-move";
1571
+ const connectorsEndpoint = CODEX_UI_API_ENDPOINTS.connectors;
1572
+ const dependenciesEndpoint = CODEX_UI_API_ENDPOINTS.dependencies;
1573
+ const explorerEndpoint = CODEX_UI_API_ENDPOINTS.openInExplorer;
1574
+ const preferencesEndpoint = CODEX_UI_API_ENDPOINTS.preferences;
1575
+ const sessionMoveEndpoint = CODEX_UI_API_ENDPOINTS.sessionMove;
1566
1576
  const maxPreferencesBodyBytes = 32768;
1567
- function isLoopbackAddress(value) {
1568
- const address = value?.toLowerCase().replace(/^\[|\]$/g, "");
1569
- return address === "localhost" || address === "localhost." || address === "::1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
1570
- }
1571
1577
  function headerValue(headers, name) {
1572
1578
  const value = headers[name];
1573
1579
  if (typeof value === "string") return value;
1574
1580
  if (Array.isArray(value)) return value[0];
1575
1581
  }
1576
- /**
1577
- * 依赖安装会改写用户配置并拉起子进程,只允许回环地址上的同源浏览器请求。
1578
- * Origin、Host 和 Sec-Fetch-Site 都可被非浏览器客户端伪造,不能单独作为授权依据。
1579
- */
1580
- function crossSiteRequest(request) {
1581
- if (!isLoopbackAddress(request.socket?.remoteAddress)) return true;
1582
- const headers = request.headers;
1583
- if (headers === void 0) return true;
1584
- const site = headerValue(headers, "sec-fetch-site");
1585
- if (site !== void 0 && site !== "same-origin") return true;
1586
- const origin = headerValue(headers, "origin");
1587
- const host = headerValue(headers, "host");
1588
- if (origin === void 0 || host === void 0) return true;
1589
- try {
1590
- const url = new URL(origin);
1591
- return url.protocol !== "http:" && url.protocol !== "https:" || !isLoopbackAddress(url.hostname) || url.host !== host;
1592
- } catch {
1593
- return true;
1594
- }
1595
- }
1596
1582
  /** 把安装错误收成可给浏览器看的文案:我们自己的中文说明保留,带本地路径的底层错误脱敏。 */
1597
1583
  function publicDependencyError(error) {
1598
1584
  const message = error instanceof Error ? error.message : "依赖管理暂不可用。";
@@ -1615,6 +1601,35 @@ async function readRequestBody(request, maxBytes = maxPreferencesBodyBytes) {
1615
1601
  }
1616
1602
  return Buffer.concat(chunks).toString("utf8");
1617
1603
  }
1604
+ /**
1605
+ * 所有业务 REST 依赖 connection >=0.1.2-rc.1 的 Host/Origin/Fetch Metadata 与登录检查。
1606
+ * 必须在读取请求体及执行副作用前调用;不得将 undefined 以外的拒绝结果当作放行。
1607
+ * 发布包信任边界由 tests/business-rest-auth.assert.ts 的真实宿主契约用例验证。
1608
+ */
1609
+ function authenticateBusinessRequest(ctx, request, response, errorBody = (_status, message) => ({ error: message })) {
1610
+ const reject = (status, message) => {
1611
+ response.writeHead(status, {
1612
+ "content-type": "application/json; charset=utf-8",
1613
+ "cache-control": "no-store"
1614
+ });
1615
+ response.end(JSON.stringify(errorBody(status, message)));
1616
+ return false;
1617
+ };
1618
+ try {
1619
+ const connection = ctx.get("connection");
1620
+ if (typeof connection?.requestRejection !== "function") {
1621
+ ctx.logger.warn("Codex UI 业务 REST 认证不可用:缺少 connection.requestRejection。");
1622
+ return reject(503, "宿主认证服务暂不可用。");
1623
+ }
1624
+ const rejection = connection.requestRejection(request);
1625
+ if (rejection !== void 0) return reject(rejection, rejection === 401 ? "请先登录 DSH。" : "已拒绝不可信或跨站请求。");
1626
+ return true;
1627
+ } catch (error) {
1628
+ const reason = error instanceof Error ? `${error.name}: ${error.message}` : typeof error;
1629
+ ctx.logger.warn("Codex UI 业务 REST 认证调用失败:%s", reason);
1630
+ return reject(503, "宿主认证服务暂不可用。");
1631
+ }
1632
+ }
1618
1633
  const inject = [
1619
1634
  "webServer",
1620
1635
  "agents",
@@ -1650,6 +1665,13 @@ function publicSessionMoveError(error) {
1650
1665
  }[code] ?? "暂时无法移动该会话,请稍后重试。"
1651
1666
  };
1652
1667
  }
1668
+ function sessionMoveAuthenticationError(status, error) {
1669
+ return {
1670
+ ok: false,
1671
+ code: status === 401 ? "session-move/unauthorized" : status === 403 ? "session-move/forbidden" : "session-move/service-unavailable",
1672
+ error
1673
+ };
1674
+ }
1653
1675
  /** 提供不泄露地址、命令和凭证的连接器目录。 */
1654
1676
  function apply(ctx) {
1655
1677
  const host = hostServices(ctx);
@@ -1660,6 +1682,7 @@ function apply(ctx) {
1660
1682
  kind: "exact",
1661
1683
  path: connectorsEndpoint,
1662
1684
  handler: async (request, response) => {
1685
+ if (!authenticateBusinessRequest(ctx, request, response)) return;
1663
1686
  if (request.method !== "GET" && request.method !== "HEAD") {
1664
1687
  response.writeHead(405);
1665
1688
  response.end();
@@ -1702,6 +1725,7 @@ function apply(ctx) {
1702
1725
  kind: "exact",
1703
1726
  path: dependenciesEndpoint,
1704
1727
  handler: async (request, response) => {
1728
+ if (!authenticateBusinessRequest(ctx, request, response)) return;
1705
1729
  const url = new URL(request.url ?? "/", "http://localhost");
1706
1730
  try {
1707
1731
  if (request.method === "GET") {
@@ -1731,14 +1755,6 @@ function apply(ctx) {
1731
1755
  return;
1732
1756
  }
1733
1757
  if (request.method === "POST") {
1734
- if (crossSiteRequest(request)) {
1735
- response.writeHead(403, {
1736
- "content-type": "application/json; charset=utf-8",
1737
- "cache-control": "no-store"
1738
- });
1739
- response.end(JSON.stringify({ error: "已拒绝非本机同源请求。" }));
1740
- return;
1741
- }
1742
1758
  if (url.searchParams.get("action") === "update-all") {
1743
1759
  const runtime = resolveDependencyRuntime(ctx);
1744
1760
  const notifyParent = canRequestParentReload(runtime);
@@ -1800,6 +1816,7 @@ function apply(ctx) {
1800
1816
  kind: "exact",
1801
1817
  path: preferencesEndpoint,
1802
1818
  handler: async (request, response) => {
1819
+ if (!authenticateBusinessRequest(ctx, request, response)) return;
1803
1820
  try {
1804
1821
  if (request.method === "GET" || request.method === "HEAD") {
1805
1822
  const preferences = await readWorkspacePreferences();
@@ -1811,14 +1828,6 @@ function apply(ctx) {
1811
1828
  return;
1812
1829
  }
1813
1830
  if (request.method === "PUT") {
1814
- if (crossSiteRequest(request)) {
1815
- response.writeHead(403, {
1816
- "content-type": "application/json; charset=utf-8",
1817
- "cache-control": "no-store"
1818
- });
1819
- response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
1820
- return;
1821
- }
1822
1831
  const body = JSON.parse(await readRequestBody(request));
1823
1832
  const record = body !== null && typeof body === "object" ? body : void 0;
1824
1833
  const pinnedWorkspaceIds = record === void 0 ? void 0 : parsePinnedWorkspaceIds(record.pinnedWorkspaceIds);
@@ -1878,23 +1887,12 @@ function apply(ctx) {
1878
1887
  kind: "exact",
1879
1888
  path: sessionMoveEndpoint,
1880
1889
  handler: async (request, response) => {
1890
+ if (!authenticateBusinessRequest(ctx, request, response, sessionMoveAuthenticationError)) return;
1881
1891
  if (request.method !== "POST") {
1882
1892
  response.writeHead(405, { allow: "POST" });
1883
1893
  response.end();
1884
1894
  return;
1885
1895
  }
1886
- if (crossSiteRequest(request)) {
1887
- response.writeHead(403, {
1888
- "content-type": "application/json; charset=utf-8",
1889
- "cache-control": "no-store"
1890
- });
1891
- response.end(JSON.stringify({
1892
- ok: false,
1893
- code: "session-move/cross-site",
1894
- error: "已拒绝跨站请求。"
1895
- }));
1896
- return;
1897
- }
1898
1896
  try {
1899
1897
  const body = JSON.parse(await readRequestBody(request));
1900
1898
  const record = body !== null && typeof body === "object" ? body : void 0;
@@ -1951,19 +1949,12 @@ function apply(ctx) {
1951
1949
  kind: "exact",
1952
1950
  path: explorerEndpoint,
1953
1951
  handler: async (request, response) => {
1952
+ if (!authenticateBusinessRequest(ctx, request, response)) return;
1954
1953
  if (request.method !== "POST") {
1955
1954
  response.writeHead(405, { allow: "POST" });
1956
1955
  response.end();
1957
1956
  return;
1958
1957
  }
1959
- if (crossSiteRequest(request)) {
1960
- response.writeHead(403, {
1961
- "content-type": "application/json; charset=utf-8",
1962
- "cache-control": "no-store"
1963
- });
1964
- response.end(JSON.stringify({ error: "已拒绝跨站请求。" }));
1965
- return;
1966
- }
1967
1958
  if (process.platform !== "win32") {
1968
1959
  response.writeHead(501, {
1969
1960
  "content-type": "application/json; charset=utf-8",
@@ -2022,4 +2013,4 @@ function apply(ctx) {
2022
2013
  }, "michengai-codex-ui: catalogs");
2023
2014
  }
2024
2015
  //#endregion
2025
- export { RequestBodyTooLargeError, apply, crossSiteRequest, inject, publicDependencyError, readRequestBody };
2016
+ export { RequestBodyTooLargeError, apply, inject, publicDependencyError, readRequestBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-codex-ui",
3
- "version": "0.2.111",
3
+ "version": "0.2.113",
4
4
  "description": "以 Codex 风格重构 DSH Web 侧栏的独立客户端插件",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -62,10 +62,12 @@
62
62
  "scripts": {
63
63
  "build": "tsc --noEmit && tsdown",
64
64
  "typecheck": "tsc --noEmit",
65
- "test": "pnpm run typecheck && vitest run && tsdown && node tests/run-assertions.mjs"
65
+ "test": "pnpm run typecheck && vitest run && tsdown && node tests/run-assertions.mjs && pnpm run test:layout",
66
+ "test:layout": "node scripts/verify-workspace-drop-layout.mjs --run && node scripts/verify-sidebar-transition.mjs"
66
67
  },
67
68
  "peerDependencies": {
68
- "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
69
+ "@deepseek-ai/cordis": ">=4.0.2 <5.0.0",
70
+ "@deepseek-ai/dsh-client-connection": ">=0.1.2-rc.1 <0.2.0",
69
71
  "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5 <0.2.0",
70
72
  "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5 <0.2.0",
71
73
  "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.5 <0.2.0",
@@ -119,12 +121,14 @@
119
121
  "@types/node": "^26.0.0",
120
122
  "@types/react": "~18.3.1",
121
123
  "jsdom": "^29.1.1",
124
+ "playwright": "1.63.0",
122
125
  "react": "^18.3.1",
123
126
  "react-dom": "^18.3.1",
124
127
  "tsdown": "^0.22.2",
125
128
  "tsx": "^4.22.4",
126
129
  "typescript": "^6.0.3",
127
- "vitest": "^4.1.8"
130
+ "vitest": "^4.1.8",
131
+ "workflow-yaml": "npm:yaml@2.9.0"
128
132
  },
129
133
  "dependencies": {
130
134
  "lucide-react": "^1.39.0"