@axhub/genie 0.2.8 → 0.2.10
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/LICENSE +21 -675
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CYCCsgwf.js +264 -0
- package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
- package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
- package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
- package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
- package/dist/assets/channel-BMhScXFe.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
- package/dist/assets/clone-BPqOt4r3.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
- package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
- package/dist/assets/index-C514cLyb.js +2 -0
- package/dist/assets/index-h1DBl_g3.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
- package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
- package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
- package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +8 -7
- package/server/acp-runtime/client.js +129 -16
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +79 -5
- package/server/cli.js +55 -10
- package/server/database/db.js +20 -0
- package/server/external-agent/service.js +24 -6
- package/server/external-agent/ws.js +540 -27
- package/server/index.js +112 -151
- package/server/lan-access/core.js +79 -0
- package/server/lan-access/state.js +102 -0
- package/server/middleware/auth.js +57 -14
- package/server/projects.js +930 -667
- package/server/routes/auth.js +24 -4
- package/server/routes/cli-auth.js +21 -25
- package/server/routes/codex.js +84 -298
- package/server/routes/commands.js +322 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +160 -91
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +60 -20
- package/server/session-core/providerAdapters.js +75 -38
- package/server/session-core/runtimeState.js +8 -0
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +174 -15
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-CTKZtqB1.js +0 -460
- package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
- package/dist/assets/channel-1oJBvF-0.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
- package/dist/assets/clone-CinxIlEu.js +0 -1
- package/dist/assets/index-DFxzgWoO.js +0 -2
- package/dist/assets/index-YCFGDVKw.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/server/_legacy-providers/README.md +0 -30
- package/server/_legacy-providers/claude-sdk.js +0 -956
- package/server/_legacy-providers/gemini-cli.js +0 -368
- package/server/_legacy-providers/openai-codex.js +0 -705
- package/server/_legacy-providers/opencode-cli.js +0 -674
- package/server/routes/git.js +0 -1110
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -536
- package/server/routes/taskmaster.js +0 -1963
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import QRCode from 'qrcode';
|
|
3
|
+
|
|
4
|
+
import { authenticateToken, generateToken, verifySignedToken } from '../middleware/auth.js';
|
|
5
|
+
import { userDb } from '../database/db.js';
|
|
6
|
+
import { collectLanIPv4Addresses } from '../lan-access/core.js';
|
|
7
|
+
import {
|
|
8
|
+
getLanSessionVersion,
|
|
9
|
+
hasLanAccessPassword,
|
|
10
|
+
setLanAccessPassword,
|
|
11
|
+
verifyLanAccessPassword,
|
|
12
|
+
} from '../lan-access/state.js';
|
|
13
|
+
|
|
14
|
+
const apiRouter = express.Router();
|
|
15
|
+
const pageRouter = express.Router();
|
|
16
|
+
|
|
17
|
+
function getDefaultUser() {
|
|
18
|
+
return userDb.ensureDefaultUser();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getRequestProtocol(req) {
|
|
22
|
+
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').trim();
|
|
23
|
+
if (forwardedProto) {
|
|
24
|
+
return forwardedProto.split(',')[0].trim() || 'http';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return req.protocol || 'http';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildAppUrl(req, address) {
|
|
31
|
+
const protocol = getRequestProtocol(req);
|
|
32
|
+
const requestUrl = new URL(`${protocol}://${req.headers.host || 'localhost'}`);
|
|
33
|
+
requestUrl.hostname = address;
|
|
34
|
+
requestUrl.pathname = '/';
|
|
35
|
+
requestUrl.search = '';
|
|
36
|
+
requestUrl.hash = '';
|
|
37
|
+
return requestUrl.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildExchangeUrl(req, address, exchangeToken) {
|
|
41
|
+
const protocol = getRequestProtocol(req);
|
|
42
|
+
const requestUrl = new URL(`${protocol}://${req.headers.host || 'localhost'}`);
|
|
43
|
+
requestUrl.hostname = address;
|
|
44
|
+
requestUrl.pathname = '/lan-access/exchange';
|
|
45
|
+
requestUrl.search = '';
|
|
46
|
+
requestUrl.searchParams.set('token', exchangeToken);
|
|
47
|
+
return requestUrl.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderExchangeDocument(token) {
|
|
51
|
+
const serializedToken = JSON.stringify(String(token || ''));
|
|
52
|
+
return `<!DOCTYPE html>
|
|
53
|
+
<html lang="zh-CN">
|
|
54
|
+
<head>
|
|
55
|
+
<meta charset="utf-8" />
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
57
|
+
<title>Axhub Genie</title>
|
|
58
|
+
<style>
|
|
59
|
+
body {
|
|
60
|
+
margin: 0;
|
|
61
|
+
min-height: 100vh;
|
|
62
|
+
display: grid;
|
|
63
|
+
place-items: center;
|
|
64
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
65
|
+
background: linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%);
|
|
66
|
+
color: #0f172a;
|
|
67
|
+
}
|
|
68
|
+
.card {
|
|
69
|
+
width: min(92vw, 420px);
|
|
70
|
+
border-radius: 24px;
|
|
71
|
+
padding: 28px;
|
|
72
|
+
background: rgba(255, 255, 255, 0.92);
|
|
73
|
+
box-shadow: 0 30px 80px rgba(15, 23, 42, 0.12);
|
|
74
|
+
text-align: center;
|
|
75
|
+
}
|
|
76
|
+
h1 {
|
|
77
|
+
margin: 0 0 10px;
|
|
78
|
+
font-size: 22px;
|
|
79
|
+
}
|
|
80
|
+
p {
|
|
81
|
+
margin: 0;
|
|
82
|
+
color: #475569;
|
|
83
|
+
font-size: 14px;
|
|
84
|
+
}
|
|
85
|
+
.dots {
|
|
86
|
+
margin: 18px auto 0;
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
gap: 8px;
|
|
89
|
+
}
|
|
90
|
+
.dot {
|
|
91
|
+
width: 8px;
|
|
92
|
+
height: 8px;
|
|
93
|
+
border-radius: 999px;
|
|
94
|
+
background: #2563eb;
|
|
95
|
+
animation: bounce 1s infinite ease-in-out;
|
|
96
|
+
}
|
|
97
|
+
.dot:nth-child(2) { animation-delay: 0.12s; }
|
|
98
|
+
.dot:nth-child(3) { animation-delay: 0.24s; }
|
|
99
|
+
@keyframes bounce {
|
|
100
|
+
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
101
|
+
40% { transform: translateY(-4px); opacity: 1; }
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<div class="card">
|
|
107
|
+
<h1>正在进入 Axhub Genie</h1>
|
|
108
|
+
<p>验证通过后会自动跳转。</p>
|
|
109
|
+
<div class="dots" aria-hidden="true">
|
|
110
|
+
<span class="dot"></span>
|
|
111
|
+
<span class="dot"></span>
|
|
112
|
+
<span class="dot"></span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<script>
|
|
116
|
+
try {
|
|
117
|
+
localStorage.setItem('auth-token', ${serializedToken});
|
|
118
|
+
window.location.replace('/');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
document.body.innerHTML = '<div class="card"><h1>进入失败</h1><p>无法写入本地访问凭证,请检查浏览器隐私设置后重试。</p></div>';
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
</body>
|
|
124
|
+
</html>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
apiRouter.get('/summary', authenticateToken, async (_req, res) => {
|
|
128
|
+
res.json({
|
|
129
|
+
success: true,
|
|
130
|
+
hasPassword: hasLanAccessPassword(),
|
|
131
|
+
addresses: collectLanIPv4Addresses(),
|
|
132
|
+
sessionVersion: getLanSessionVersion(),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
apiRouter.post('/password', authenticateToken, async (req, res) => {
|
|
137
|
+
try {
|
|
138
|
+
const password = String(req.body?.password || '');
|
|
139
|
+
const state = await setLanAccessPassword(password);
|
|
140
|
+
res.json({
|
|
141
|
+
success: true,
|
|
142
|
+
hasPassword: true,
|
|
143
|
+
sessionVersion: state.sessionVersion,
|
|
144
|
+
message: '局域网访问密码已更新。'
|
|
145
|
+
});
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const message = error?.message || 'Failed to update LAN access password';
|
|
148
|
+
const statusCode = message.includes('required') ? 400 : 500;
|
|
149
|
+
res.status(statusCode).json({ success: false, error: message });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
apiRouter.post('/token', authenticateToken, async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const address = String(req.body?.address || '').trim();
|
|
156
|
+
if (!address) {
|
|
157
|
+
return res.status(400).json({ success: false, error: 'Address is required' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!hasLanAccessPassword()) {
|
|
161
|
+
return res.status(412).json({ success: false, error: '请先设置局域网访问密码。' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const user = getDefaultUser();
|
|
165
|
+
const exchangeToken = generateToken(user, {
|
|
166
|
+
expiresIn: '8m',
|
|
167
|
+
purpose: 'lan-exchange',
|
|
168
|
+
});
|
|
169
|
+
const appUrl = buildAppUrl(req, address);
|
|
170
|
+
const exchangeUrl = buildExchangeUrl(req, address, exchangeToken);
|
|
171
|
+
const qrCodeDataUrl = await QRCode.toDataURL(exchangeUrl, {
|
|
172
|
+
margin: 1,
|
|
173
|
+
width: 256,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
res.json({
|
|
177
|
+
success: true,
|
|
178
|
+
address,
|
|
179
|
+
accessUrl: appUrl,
|
|
180
|
+
exchangeUrl,
|
|
181
|
+
qrCodeDataUrl,
|
|
182
|
+
expiresInSeconds: 8 * 60,
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('LAN access token error:', error);
|
|
186
|
+
res.status(500).json({ success: false, error: 'Failed to generate LAN access link' });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
apiRouter.post('/login', async (req, res) => {
|
|
191
|
+
try {
|
|
192
|
+
if (!hasLanAccessPassword()) {
|
|
193
|
+
return res.status(412).json({ success: false, error: '局域网访问密码尚未设置。' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const password = String(req.body?.password || '');
|
|
197
|
+
const isValidPassword = await verifyLanAccessPassword(password);
|
|
198
|
+
if (!isValidPassword) {
|
|
199
|
+
return res.status(401).json({ success: false, error: '访问密码不正确。' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const user = getDefaultUser();
|
|
203
|
+
const token = generateToken(user);
|
|
204
|
+
res.json({ success: true, token, user });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('LAN access login error:', error);
|
|
207
|
+
res.status(500).json({ success: false, error: 'Failed to sign in with LAN access password' });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pageRouter.get('/exchange', async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const exchangeToken = String(req.query?.token || '');
|
|
214
|
+
if (!exchangeToken) {
|
|
215
|
+
return res.status(400).send(renderExchangeDocument(''));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const decoded = verifySignedToken(exchangeToken, { expectedPurpose: 'lan-exchange' });
|
|
219
|
+
const user = userDb.getUserById(decoded.userId) || getDefaultUser();
|
|
220
|
+
const token = generateToken(user);
|
|
221
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
222
|
+
res.send(renderExchangeDocument(token));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
res.status(403).send(`<!DOCTYPE html><html lang="zh-CN"><body style="font-family: ui-sans-serif, sans-serif; padding: 32px;"><h1 style="margin:0 0 12px;">链接已失效</h1><p style="margin:0;color:#475569;">请重新生成局域网访问链接或二维码。</p></body></html>`);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
apiRouter as lanAccessApiRoutes,
|
|
230
|
+
pageRouter as lanAccessPageRoutes,
|
|
231
|
+
};
|
|
@@ -19,39 +19,116 @@ export {
|
|
|
19
19
|
|
|
20
20
|
const router = express.Router();
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'/',
|
|
26
|
-
'/
|
|
27
|
-
'/
|
|
28
|
-
'/
|
|
29
|
-
'/
|
|
30
|
-
'/
|
|
31
|
-
'/
|
|
32
|
-
'/
|
|
33
|
-
'/
|
|
34
|
-
'/
|
|
35
|
-
'/
|
|
36
|
-
'/
|
|
37
|
-
'/
|
|
38
|
-
'/
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
'C:\\
|
|
43
|
-
'C:\\
|
|
44
|
-
'C
|
|
45
|
-
'C:\\ProgramData',
|
|
46
|
-
'C:\\System Volume Information',
|
|
47
|
-
'C:\\$Recycle.Bin'
|
|
22
|
+
const SYSTEM_PATH_RULES = [
|
|
23
|
+
{ label: '/', kind: 'exact' },
|
|
24
|
+
{ label: '/etc', kind: 'prefix' },
|
|
25
|
+
{ label: '/bin', kind: 'prefix' },
|
|
26
|
+
{ label: '/sbin', kind: 'prefix' },
|
|
27
|
+
{ label: '/usr', kind: 'prefix' },
|
|
28
|
+
{ label: '/dev', kind: 'prefix' },
|
|
29
|
+
{ label: '/proc', kind: 'prefix' },
|
|
30
|
+
{ label: '/sys', kind: 'prefix' },
|
|
31
|
+
{ label: '/var', kind: 'prefix' },
|
|
32
|
+
{ label: '/boot', kind: 'prefix' },
|
|
33
|
+
{ label: '/root', kind: 'prefix' },
|
|
34
|
+
{ label: '/lib', kind: 'prefix' },
|
|
35
|
+
{ label: '/lib64', kind: 'prefix' },
|
|
36
|
+
{ label: '/opt', kind: 'prefix' },
|
|
37
|
+
{ label: '/tmp', kind: 'exact' },
|
|
38
|
+
{ label: '/run', kind: 'prefix' },
|
|
39
|
+
{ label: 'C:\\Windows', kind: 'prefix' },
|
|
40
|
+
{ label: 'C:\\Program Files', kind: 'prefix' },
|
|
41
|
+
{ label: 'C:\\Program Files (x86)', kind: 'prefix' },
|
|
42
|
+
{ label: 'C:\\ProgramData', kind: 'prefix' },
|
|
43
|
+
{ label: 'C:\\System Volume Information', kind: 'prefix' },
|
|
44
|
+
{ label: 'C:\\$Recycle.Bin', kind: 'prefix' }
|
|
48
45
|
];
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
export const FORBIDDEN_PATHS = Object.freeze(SYSTEM_PATH_RULES.map((rule) => rule.label));
|
|
48
|
+
|
|
49
|
+
function normalizeCandidatePath(candidatePath) {
|
|
50
|
+
return path.normalize(path.resolve(candidatePath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isVarException(candidatePath) {
|
|
54
|
+
return candidatePath.startsWith('/var/tmp') || candidatePath.startsWith('/var/folders');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findSystemPathViolation(candidatePath) {
|
|
58
|
+
for (const rule of SYSTEM_PATH_RULES) {
|
|
59
|
+
if (rule.label === '/var' && isVarException(candidatePath)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (rule.kind === 'exact' && candidatePath === rule.label) {
|
|
64
|
+
return rule.label;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (rule.kind === 'prefix' && (candidatePath === rule.label || candidatePath.startsWith(`${rule.label}${path.sep}`))) {
|
|
68
|
+
return rule.label;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolvePathForValidation(candidatePath, fsImpl) {
|
|
76
|
+
try {
|
|
77
|
+
await fsImpl.access(candidatePath);
|
|
78
|
+
return await fsImpl.realpath(candidatePath);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error.code !== 'ENOENT') {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parentPath = path.dirname(candidatePath);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const resolvedParent = await fsImpl.realpath(parentPath);
|
|
88
|
+
return path.join(resolvedParent, path.basename(candidatePath));
|
|
89
|
+
} catch (parentError) {
|
|
90
|
+
if (parentError.code === 'ENOENT') {
|
|
91
|
+
return candidatePath;
|
|
92
|
+
}
|
|
93
|
+
throw parentError;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function assertSymlinkDoesNotEscape(candidatePath, options) {
|
|
99
|
+
const {
|
|
100
|
+
fsImpl,
|
|
101
|
+
hasWorkspaceRootRestriction,
|
|
102
|
+
allowedWorkspaceRoots,
|
|
103
|
+
resolvedWorkspaceRoots
|
|
104
|
+
} = options;
|
|
105
|
+
|
|
106
|
+
if (!hasWorkspaceRootRestriction || allowedWorkspaceRoots.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const stat = await fsImpl.lstat(candidatePath);
|
|
112
|
+
if (!stat.isSymbolicLink()) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const linkTarget = await fsImpl.readlink(candidatePath);
|
|
117
|
+
const resolvedTarget = path.resolve(path.dirname(candidatePath), linkTarget);
|
|
118
|
+
const realTarget = await fsImpl.realpath(resolvedTarget);
|
|
119
|
+
|
|
120
|
+
if (!isPathWithinAllowedRoots(realTarget, resolvedWorkspaceRoots)) {
|
|
121
|
+
return 'Symlink target is outside the allowed workspace root';
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error.code !== 'ENOENT') {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
55
132
|
export async function validateWorkspacePath(requestedPath, options = {}) {
|
|
56
133
|
try {
|
|
57
134
|
const {
|
|
@@ -60,73 +137,29 @@ export async function validateWorkspacePath(requestedPath, options = {}) {
|
|
|
60
137
|
allowedWorkspaceRoots = WORKSPACES_ROOTS
|
|
61
138
|
} = options;
|
|
62
139
|
|
|
63
|
-
|
|
64
|
-
|
|
140
|
+
const absoluteCandidate = normalizeCandidatePath(requestedPath);
|
|
141
|
+
const violation = findSystemPathViolation(absoluteCandidate);
|
|
65
142
|
|
|
66
|
-
|
|
67
|
-
const normalizedPath = path.normalize(absolutePath);
|
|
68
|
-
if (FORBIDDEN_PATHS.includes(normalizedPath) || normalizedPath === '/') {
|
|
143
|
+
if (violation) {
|
|
69
144
|
return {
|
|
70
145
|
valid: false,
|
|
71
|
-
error:
|
|
146
|
+
error: violation === '/'
|
|
147
|
+
? 'Cannot use system-critical directories as workspace locations'
|
|
148
|
+
: `Cannot create workspace in system directory: ${violation}`
|
|
72
149
|
};
|
|
73
150
|
}
|
|
74
151
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (normalizedPath === forbidden ||
|
|
78
|
-
normalizedPath.startsWith(forbidden + path.sep)) {
|
|
79
|
-
// Exception: /var/tmp and similar user-accessible paths might be allowed
|
|
80
|
-
// but /var itself and most /var subdirectories should be blocked
|
|
81
|
-
if (forbidden === '/var' &&
|
|
82
|
-
(normalizedPath.startsWith('/var/tmp') ||
|
|
83
|
-
normalizedPath.startsWith('/var/folders'))) {
|
|
84
|
-
continue; // Allow these specific cases
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
valid: false,
|
|
89
|
-
error: `Cannot create workspace in system directory: ${forbidden}`
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Try to resolve the real path (following symlinks)
|
|
95
|
-
let realPath;
|
|
96
|
-
try {
|
|
97
|
-
// Check if path exists to resolve real path
|
|
98
|
-
await fsImpl.access(absolutePath);
|
|
99
|
-
realPath = await fsImpl.realpath(absolutePath);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
if (error.code === 'ENOENT') {
|
|
102
|
-
// Path doesn't exist yet - check parent directory
|
|
103
|
-
let parentPath = path.dirname(absolutePath);
|
|
104
|
-
try {
|
|
105
|
-
const parentRealPath = await fsImpl.realpath(parentPath);
|
|
106
|
-
|
|
107
|
-
// Reconstruct the full path with real parent
|
|
108
|
-
realPath = path.join(parentRealPath, path.basename(absolutePath));
|
|
109
|
-
} catch (parentError) {
|
|
110
|
-
if (parentError.code === 'ENOENT') {
|
|
111
|
-
// Parent doesn't exist either - use the absolute path as-is
|
|
112
|
-
// We'll validate it's within allowed root
|
|
113
|
-
realPath = absolutePath;
|
|
114
|
-
} else {
|
|
115
|
-
throw parentError;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
152
|
+
const resolvedPath = await resolvePathForValidation(absoluteCandidate, fsImpl);
|
|
153
|
+
const normalizedResolvedPath = normalizeCandidatePath(resolvedPath);
|
|
122
154
|
|
|
155
|
+
let resolvedWorkspaceRoots = [];
|
|
123
156
|
if (hasWorkspaceRootRestriction && allowedWorkspaceRoots.length > 0) {
|
|
124
|
-
|
|
157
|
+
resolvedWorkspaceRoots = await resolveAllowedWorkspaceRoots({
|
|
125
158
|
fsImpl,
|
|
126
159
|
allowedWorkspaceRoots
|
|
127
160
|
});
|
|
128
161
|
|
|
129
|
-
if (!isPathWithinAllowedRoots(
|
|
162
|
+
if (!isPathWithinAllowedRoots(normalizedResolvedPath, resolvedWorkspaceRoots)) {
|
|
130
163
|
return {
|
|
131
164
|
valid: false,
|
|
132
165
|
error: `Workspace path must be within one of the allowed workspace roots: ${formatAllowedWorkspaceRoots(allowedWorkspaceRoots)}`
|
|
@@ -134,40 +167,24 @@ export async function validateWorkspacePath(requestedPath, options = {}) {
|
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
150
|
-
const realTarget = await fsImpl.realpath(resolvedTarget);
|
|
151
|
-
|
|
152
|
-
if (!isPathWithinAllowedRoots(realTarget, resolvedWorkspaceRoots)) {
|
|
153
|
-
return {
|
|
154
|
-
valid: false,
|
|
155
|
-
error: 'Symlink target is outside the allowed workspace root'
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
} catch (error) {
|
|
160
|
-
if (error.code !== 'ENOENT') {
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
// Path doesn't exist - that's fine for new workspace creation
|
|
170
|
+
const symlinkViolation = await assertSymlinkDoesNotEscape(absoluteCandidate, {
|
|
171
|
+
fsImpl,
|
|
172
|
+
hasWorkspaceRootRestriction,
|
|
173
|
+
allowedWorkspaceRoots,
|
|
174
|
+
resolvedWorkspaceRoots
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (symlinkViolation) {
|
|
178
|
+
return {
|
|
179
|
+
valid: false,
|
|
180
|
+
error: symlinkViolation
|
|
181
|
+
};
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
return {
|
|
167
185
|
valid: true,
|
|
168
|
-
resolvedPath:
|
|
186
|
+
resolvedPath: normalizedResolvedPath
|
|
169
187
|
};
|
|
170
|
-
|
|
171
188
|
} catch (error) {
|
|
172
189
|
return {
|
|
173
190
|
valid: false,
|
|
@@ -176,38 +193,30 @@ export async function validateWorkspacePath(requestedPath, options = {}) {
|
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
|
|
179
|
-
/**
|
|
180
|
-
* Create a new workspace
|
|
181
|
-
* POST /api/projects/create-workspace
|
|
182
|
-
*
|
|
183
|
-
* Body:
|
|
184
|
-
* - workspaceType: 'existing' | 'new'
|
|
185
|
-
* - path: string (workspace path)
|
|
186
|
-
*/
|
|
187
196
|
router.post('/create-workspace', async (req, res) => {
|
|
188
197
|
try {
|
|
189
|
-
const {
|
|
198
|
+
const {
|
|
199
|
+
workspaceType,
|
|
200
|
+
path: workspacePath,
|
|
201
|
+
githubUrl,
|
|
202
|
+
githubTokenId,
|
|
203
|
+
newGithubToken
|
|
204
|
+
} = req.body;
|
|
190
205
|
|
|
191
|
-
// Validate required fields
|
|
192
206
|
if (!workspaceType || !workspacePath) {
|
|
193
207
|
return res.status(400).json({ error: 'workspaceType and path are required' });
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
if (
|
|
197
|
-
githubUrl !== undefined
|
|
198
|
-
|| githubTokenId !== undefined
|
|
199
|
-
|| newGithubToken !== undefined
|
|
200
|
-
) {
|
|
210
|
+
if (githubUrl !== undefined || githubTokenId !== undefined || newGithubToken !== undefined) {
|
|
201
211
|
return res.status(400).json({
|
|
202
212
|
error: 'GitHub-based workspace creation has been removed. Create an empty workspace or add an existing local workspace instead.'
|
|
203
213
|
});
|
|
204
214
|
}
|
|
205
215
|
|
|
206
|
-
if (
|
|
216
|
+
if (workspaceType !== 'existing' && workspaceType !== 'new') {
|
|
207
217
|
return res.status(400).json({ error: 'workspaceType must be "existing" or "new"' });
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
// Validate path safety before any operations
|
|
211
220
|
const validation = await validateWorkspacePath(workspacePath);
|
|
212
221
|
if (!validation.valid) {
|
|
213
222
|
return res.status(400).json({
|
|
@@ -216,16 +225,12 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
216
225
|
});
|
|
217
226
|
}
|
|
218
227
|
|
|
219
|
-
const
|
|
228
|
+
const targetPath = validation.resolvedPath;
|
|
220
229
|
|
|
221
|
-
// Handle existing workspace
|
|
222
230
|
if (workspaceType === 'existing') {
|
|
223
|
-
// Check if the path exists
|
|
224
231
|
try {
|
|
225
|
-
await fs.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!stats.isDirectory()) {
|
|
232
|
+
const stat = await fs.stat(targetPath);
|
|
233
|
+
if (!stat.isDirectory()) {
|
|
229
234
|
return res.status(400).json({ error: 'Path exists but is not a directory' });
|
|
230
235
|
}
|
|
231
236
|
} catch (error) {
|
|
@@ -235,9 +240,7 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
235
240
|
throw error;
|
|
236
241
|
}
|
|
237
242
|
|
|
238
|
-
|
|
239
|
-
const project = await addProjectManually(absolutePath);
|
|
240
|
-
|
|
243
|
+
const project = await addProjectManually(targetPath);
|
|
241
244
|
return res.json({
|
|
242
245
|
success: true,
|
|
243
246
|
project,
|
|
@@ -245,26 +248,19 @@ router.post('/create-workspace', async (req, res) => {
|
|
|
245
248
|
});
|
|
246
249
|
}
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Create the directory if it doesn't exist
|
|
251
|
-
await fs.mkdir(absolutePath, { recursive: true });
|
|
252
|
-
|
|
253
|
-
// Add the new workspace to the project list (no clone)
|
|
254
|
-
const project = await addProjectManually(absolutePath);
|
|
255
|
-
|
|
256
|
-
return res.json({
|
|
257
|
-
success: true,
|
|
258
|
-
project,
|
|
259
|
-
message: 'New workspace created successfully'
|
|
260
|
-
});
|
|
261
|
-
}
|
|
251
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
252
|
+
const project = await addProjectManually(targetPath);
|
|
262
253
|
|
|
254
|
+
res.json({
|
|
255
|
+
success: true,
|
|
256
|
+
project,
|
|
257
|
+
message: 'New workspace created successfully'
|
|
258
|
+
});
|
|
263
259
|
} catch (error) {
|
|
264
260
|
console.error('Error creating workspace:', error);
|
|
265
261
|
res.status(500).json({
|
|
266
|
-
error:
|
|
267
|
-
details:
|
|
262
|
+
error: 'Failed to create workspace',
|
|
263
|
+
details: error.message
|
|
268
264
|
});
|
|
269
265
|
}
|
|
270
266
|
});
|