cc-viewer 1.8.9 → 1.8.11
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/README.md +13 -0
- package/dist/assets/App-vpm6omRY.js +2 -0
- package/dist/assets/{MdxEditorPanel-BznPWiig.js → MdxEditorPanel-CpknU7SR.js} +1 -1
- package/dist/assets/{Mobile-ph7aKFZy.js → Mobile-BHIBdqHs.js} +1 -1
- package/dist/assets/{ProxyStatsModal-B7iew2Tv.js → ProxyStatsModal-DdV7wN2V.js} +1 -1
- package/dist/assets/{_baseUniq-D2aO8mv4.js → _baseUniq-C2FCldN4.js} +1 -1
- package/dist/assets/{arc-DOjcnBTh.js → arc-CSzd_bdT.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-Drea2H7X.js → architectureDiagram-Q4EWVU46-CjEbBe2R.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Bc9eijBD.js → blockDiagram-DXYQGD6D-BYzl2ukW.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-D-umzFnM.js → c4Diagram-AHTNJAMY-0x0_8O_X.js} +1 -1
- package/dist/assets/{channel-DUff3FBt.js → channel-xMmQtLq9.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-CFnsROfz.js → chunk-4BX2VUAB-DnFoMzQG.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-C95jMPjB.js → chunk-4TB4RGXK-Dc5reuwv.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CeWf2Y2b.js → chunk-55IACEB6-DTKTLKcJ.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-ClUoTJxv.js → chunk-EDXVE4YY-BNqgeN9r.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Dk62kqb8.js → chunk-FMBD7UC4-DzJVZ11t.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CZuykSsu.js → chunk-OYMX7WX6-CjYwzMor.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DYtx68Um.js → chunk-QZHKN3VN-C0KVKecr.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-rLSq0tSC.js → chunk-YZCP3GAM-3VhNeNVm.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DdUtH789.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DdUtH789.js +1 -0
- package/dist/assets/clone-C6d2XDvR.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-B5H6ewon.js → cose-bilkent-S5V4N54A-DGE02sJS.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-BO2R2WP7.js → dagre-KV5264BT-CY4eCcwS.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-Y3GNif-g.js → diagram-5BDNPKRD-DHq2THYn.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Du3_TylQ.js → diagram-G4DWMVQ6-DD_76oJg.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DB_Ynyjc.js → diagram-MMDJMWI5-BvQxk1jl.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-_Rfjm82W.js → diagram-TYMM5635-B_tMXPJl.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-D5XhBiiV.js → erDiagram-SMLLAGMA-DpuGbN9e.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CxWJ7sAQ.js → flowDiagram-DWJPFMVM-BW0xGFBf.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-D-a8fh00.js → ganttDiagram-T4ZO3ILL-BSzTiT93.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-q5WMk0Ce.js → gitGraphDiagram-UUTBAWPF-1tsgBlcn.js} +1 -1
- package/dist/assets/{graph-Bi8xMAuo.js → graph-CwiXMdRR.js} +1 -1
- package/dist/assets/{index-BRtbpMRz.js → index-9e6BGquk.js} +1 -1
- package/dist/assets/{index-BWPHHS6P.js → index-BmpXfVQ8.js} +1 -1
- package/dist/assets/{index-O2Mx9uLd.js → index-CZWNaOo8.js} +1 -1
- package/dist/assets/{index-BBEH6Ppx.js → index-Cixs7aRD.js} +1 -1
- package/dist/assets/{index-C3wsDB-y.js → index-Ct6Fi103.js} +1 -1
- package/dist/assets/index-D1yGak8I.css +1 -0
- package/dist/assets/{index-3Z3ucdlB.js → index-DIAMN9QO.js} +1 -1
- package/dist/assets/{index-BB66PLo6.js → index-DhjwHRxk.js} +1 -1
- package/dist/assets/{index-BbF89uTe.js → index-tnWldUm3.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-CE2mUuLF.js → infoDiagram-42DDH7IO-Dk1ESZFk.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-C5sNQIVL.js → ishikawaDiagram-UXIWVN3A-DcPyNwkI.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-g7HXOeyX.js → journeyDiagram-VCZTEJTY-CZWieu1U.js} +1 -1
- package/dist/assets/{jszip.min-x3xOZxY_.js → jszip.min-Bq1JxqcN.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-D3Mus6hc.js → kanban-definition-6JOO6SKY-S1sSSTKx.js} +1 -1
- package/dist/assets/{layout-05qDuweB.js → layout-BQFiWq71.js} +1 -1
- package/dist/assets/{linear-DQEG_N-F.js → linear-BNV4po81.js} +1 -1
- package/dist/assets/{mermaid.core-DcaXqkk5.js → mermaid.core-DvwEdatr.js} +2 -2
- package/dist/assets/{min-DSpvw5zG.js → min-DFik8k4L.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-DomUIop7.js → mindmap-definition-QFDTVHPH-CebeX76u.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-COTLcC5G.js → pieDiagram-DEJITSTG-DMB5JbMP.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DMg8C3Up.js → quadrantDiagram-34T5L4WZ-BAYzoLVo.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-DF9beYd7.js → requirementDiagram-MS252O5E-CUQoAuOc.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-MvERK2UP.js → sankeyDiagram-XADWPNL6-Dt0AQuLW.js} +1 -1
- package/dist/assets/seqResourceLoaders-C-QHRkUq.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-B8k_nyzc.js → sequenceDiagram-FGHM5R23-HyZFUlMU.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-D-9fTYas.js → stateDiagram-FHFEXIEX-B8x24fuD.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-2qtHlnwO.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-CbK1g9CO.js → timeline-definition-GMOUNBTQ-TnbjDVtB.js} +1 -1
- package/dist/assets/{vendor-antd-FhK5cQx7.js → vendor-antd-BTBRwbax.js} +1 -1
- package/dist/assets/{vendor-codemirror-DL3yyKRQ.js → vendor-codemirror-CPWlhuQ1.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-DgyrvHzS.js → vendor-mdxeditor-Cy5Xg_90.js} +2 -2
- package/dist/assets/{vendor-qrcode-BAwwP1Nd.js → vendor-qrcode-E9ld0lSz.js} +1 -1
- package/dist/assets/{vendor-virtuoso-B_7lc2Ot.js → vendor-virtuoso-Bq6QMhgG.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-DYjGtMA5.js → vennDiagram-DHZGUBPP-BqgIjkBO.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-Djh5alV9.js → wardley-RL74JXVD-BFcIn2-Q.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-al50Jlxp.js → wardleyDiagram-NUSXRM2D-CYDWQLb6.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Bl969-3U.js → xychartDiagram-5P7HB3ND-zfusE3BC.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/lib/auth.js +20 -4
- package/server/lib/is-admin.js +27 -0
- package/server/lib/skills-api.js +48 -26
- package/server/routes/auth.js +11 -9
- package/server/routes/dingtalk.js +23 -8
- package/server/routes/files-fs.js +76 -1
- package/server/routes/im.js +36 -20
- package/server/routes/preferences.js +27 -15
- package/server/routes/project-prefs.js +6 -5
- package/server/routes/skills.js +50 -29
- package/server/routes/voice-pack.js +7 -6
- package/server/server.js +12 -2
- package/dist/assets/App-DuJsCzTH.js +0 -2
- package/dist/assets/classDiagram-6PBFFD2Q-DT8RQXlO.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DT8RQXlO.js +0 -1
- package/dist/assets/clone-_dhVVwEt.js +0 -1
- package/dist/assets/index-D3GEPgde.css +0 -1
- package/dist/assets/seqResourceLoaders-76uKbVqf.js +0 -2
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-CO0tv8rD.js +0 -1
package/server/routes/skills.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, sep, dirname } from 'node:path';
|
|
4
4
|
import { getClaudeConfigDir } from '../../findcc.js';
|
|
5
|
-
import { listSkills, moveSkill, deleteSkill, validateSkillName } from '../lib/skills-api.js';
|
|
5
|
+
import { listSkills, moveSkill, deleteSkill, validateSkillName, parseSkillFrontmatter } from '../lib/skills-api.js';
|
|
6
|
+
import { isAdminReq } from '../lib/is-admin.js';
|
|
6
7
|
|
|
7
8
|
async function skillsList(req, res) {
|
|
8
9
|
try {
|
|
@@ -44,10 +45,12 @@ function skillsToggle(req, res) {
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
48
|
+
// Permanently delete a skill — rmSync the skill folder from skills/ or skills-skip/ (irreversible).
|
|
49
|
+
// admin-only: an irreversible rmSync is not exposed to unauthenticated LAN clients (toggle is a
|
|
50
|
+
// reversible move — this is stricter); an authenticated remote admin (container/cloud) is allowed
|
|
51
|
+
// the same as the loopback admin.
|
|
49
52
|
function skillsDelete(req, res, parsedUrl, isLocal) {
|
|
50
|
-
if (!isLocal) {
|
|
53
|
+
if (!isAdminReq(req, isLocal)) {
|
|
51
54
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
52
55
|
res.end(JSON.stringify({ error: 'Loopback only' }));
|
|
53
56
|
return;
|
|
@@ -78,22 +81,35 @@ function skillsDelete(req, res, parsedUrl, isLocal) {
|
|
|
78
81
|
// 设计要点(与原实现一致):
|
|
79
82
|
// · 扩展名白名单只放 zip / md(忽略大小写);其他类型 415;
|
|
80
83
|
// · zip 内必须含 SKILL.md(任意子目录、忽略大小写),取最浅的那个所在目录作为 skill 根;
|
|
81
|
-
// ·
|
|
84
|
+
// · Strict spec check (unified across all three entries): SKILL.md frontmatter must contain
|
|
85
|
+
// a name (must pass validateSkillName) and a description; violations reject with
|
|
86
|
+
// INVALID_FRONTMATTER / MISSING_NAME / INVALID_NAME / MISSING_DESCRIPTION — no more
|
|
87
|
+
// filename / zip-root-dir fallback;
|
|
82
88
|
// · zip bomb 防护:单文件 ≤50MB,总解压 ≤200MB;拒绝 symlink entry。
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const nm = /^name\s*:\s*(.*)$/m.exec(m[1]);
|
|
88
|
-
if (!nm) return null;
|
|
89
|
-
return nm[1].trim().replace(/^["']|["']$/g, '');
|
|
90
|
-
};
|
|
90
|
+
// Shared size caps for the zip path (declared-size pass + actual-size re-check).
|
|
91
|
+
const MAX_SKILL_FILE = 50 * 1024 * 1024;
|
|
92
|
+
const MAX_SKILL_TOTAL = 200 * 1024 * 1024;
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// Strict skill-spec validation shared by all three import entries (folder / zip / SKILL.md).
|
|
95
|
+
// Check order is fixed (tests depend on it): no frontmatter block → INVALID_FRONTMATTER;
|
|
96
|
+
// missing name → MISSING_NAME; invalid name → INVALID_NAME; missing description → MISSING_DESCRIPTION.
|
|
97
|
+
function requireSkillSpec(text) {
|
|
98
|
+
const fm = parseSkillFrontmatter(text);
|
|
99
|
+
if (!fm) {
|
|
100
|
+
throw Object.assign(new Error('SKILL.md is missing valid frontmatter'), { status: 400, code: 'INVALID_FRONTMATTER' });
|
|
101
|
+
}
|
|
102
|
+
if (!fm.name) {
|
|
103
|
+
throw Object.assign(new Error('SKILL.md frontmatter is missing a name'), { status: 400, code: 'MISSING_NAME' });
|
|
104
|
+
}
|
|
105
|
+
if (!validateSkillName(fm.name)) {
|
|
106
|
+
throw Object.assign(new Error(`Invalid skill name: ${fm.name}`), { status: 400, code: 'INVALID_NAME' });
|
|
107
|
+
}
|
|
108
|
+
if (!fm.description) {
|
|
109
|
+
throw Object.assign(new Error('SKILL.md frontmatter is missing a description'), { status: 400, code: 'MISSING_DESCRIPTION' });
|
|
110
|
+
}
|
|
111
|
+
return fm;
|
|
112
|
+
}
|
|
97
113
|
|
|
98
114
|
/**
|
|
99
115
|
* 解析一段 multipart body(已组装好的 Buffer)→ { skillName, files: [{relPath, data}] }。
|
|
@@ -133,7 +149,7 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
133
149
|
|
|
134
150
|
if (isMd) {
|
|
135
151
|
const text = fileData.toString('utf8');
|
|
136
|
-
skillName =
|
|
152
|
+
skillName = requireSkillSpec(text).name;
|
|
137
153
|
skillFiles = [{ relPath: 'SKILL.md', data: fileData }];
|
|
138
154
|
} else {
|
|
139
155
|
const AdmZip = (await import('adm-zip')).default;
|
|
@@ -144,8 +160,6 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
144
160
|
throw Object.assign(new Error('Invalid zip archive'), { status: 400, code: 'INVALID_ZIP' });
|
|
145
161
|
}
|
|
146
162
|
const entries = zip.getEntries();
|
|
147
|
-
const MAX_PER_FILE = 50 * 1024 * 1024;
|
|
148
|
-
const MAX_TOTAL_UNCOMPRESSED = 200 * 1024 * 1024;
|
|
149
163
|
let totalUncompressed = 0;
|
|
150
164
|
for (const e of entries) {
|
|
151
165
|
if (e.isDirectory) continue;
|
|
@@ -154,11 +168,11 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
154
168
|
throw Object.assign(new Error('Symlinks not allowed in zip'), { status: 400, code: 'INVALID_ZIP' });
|
|
155
169
|
}
|
|
156
170
|
const sizeRaw = e.header?.size || 0;
|
|
157
|
-
if (sizeRaw >
|
|
171
|
+
if (sizeRaw > MAX_SKILL_FILE) {
|
|
158
172
|
throw Object.assign(new Error('File too large in archive'), { status: 400, code: 'ZIP_BOMB' });
|
|
159
173
|
}
|
|
160
174
|
totalUncompressed += sizeRaw;
|
|
161
|
-
if (totalUncompressed >
|
|
175
|
+
if (totalUncompressed > MAX_SKILL_TOTAL) {
|
|
162
176
|
throw Object.assign(new Error('Archive expands too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
163
177
|
}
|
|
164
178
|
}
|
|
@@ -176,12 +190,16 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
176
190
|
if (!bestSkillEntry) {
|
|
177
191
|
throw Object.assign(new Error('SKILL.md not found in zip'), { status: 400, code: 'MISSING_SKILL_MD' });
|
|
178
192
|
}
|
|
193
|
+
// Guard BEFORE inflating: header.size is attacker-controlled, but a declared oversize at
|
|
194
|
+
// least lets us reject before spending memory on getData() (actual-size re-check below
|
|
195
|
+
// still catches a lying small header).
|
|
196
|
+
if ((bestSkillEntry.header?.size || 0) > MAX_SKILL_FILE) {
|
|
197
|
+
throw Object.assign(new Error('File too large in archive'), { status: 400, code: 'ZIP_BOMB' });
|
|
198
|
+
}
|
|
179
199
|
const lastSlash = bestSkillEntry.entryName.lastIndexOf('/');
|
|
180
200
|
const skillRootPrefix = lastSlash >= 0 ? bestSkillEntry.entryName.slice(0, lastSlash + 1) : '';
|
|
181
201
|
const skillMdText = bestSkillEntry.getData().toString('utf8');
|
|
182
|
-
skillName =
|
|
183
|
-
|| (skillRootPrefix ? skillRootPrefix.replace(/\/$/, '').split('/').pop() : null)
|
|
184
|
-
|| fallbackBaseName(originalName, true);
|
|
202
|
+
skillName = requireSkillSpec(skillMdText).name;
|
|
185
203
|
|
|
186
204
|
// 二次校验:header.size 来自 zip 中央目录是攻击者可控的(可谎报 size=0),用真实 data.length 复核。
|
|
187
205
|
let actualTotal = 0;
|
|
@@ -191,11 +209,11 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
191
209
|
const rel = skillRootPrefix ? e.entryName.slice(skillRootPrefix.length) : e.entryName;
|
|
192
210
|
if (!rel || rel.includes('..')) continue;
|
|
193
211
|
const data = e.getData();
|
|
194
|
-
if (data.length >
|
|
212
|
+
if (data.length > MAX_SKILL_FILE) {
|
|
195
213
|
throw Object.assign(new Error('File actual size too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
196
214
|
}
|
|
197
215
|
actualTotal += data.length;
|
|
198
|
-
if (actualTotal >
|
|
216
|
+
if (actualTotal > MAX_SKILL_TOTAL) {
|
|
199
217
|
throw Object.assign(new Error('Archive actual size too large'), { status: 400, code: 'ZIP_BOMB' });
|
|
200
218
|
}
|
|
201
219
|
const finalRel = rel.split('/').pop().toLowerCase() === 'skill.md'
|
|
@@ -205,6 +223,8 @@ export async function parseSkillUpload(buf, boundary, windowsReservedRe) {
|
|
|
205
223
|
}
|
|
206
224
|
}
|
|
207
225
|
|
|
226
|
+
// defense-in-depth: requireSkillSpec already validated the name (and would have
|
|
227
|
+
// thrown first); keep this re-check so future refactors can't silently skip it.
|
|
208
228
|
if (!validateSkillName(skillName)) {
|
|
209
229
|
throw Object.assign(new Error(`Invalid skill name: ${skillName}`), { status: 400, code: 'INVALID_NAME' });
|
|
210
230
|
}
|
|
@@ -261,7 +281,8 @@ export function importSkillTo(req, res, { skillsRoot, windowsReserved }) {
|
|
|
261
281
|
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
262
282
|
if (contentLength > MAX_UPLOAD) {
|
|
263
283
|
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
264
|
-
|
|
284
|
+
// code 字段供前端 skillImportErrorKey 映射成 i18n 文案,而不是裸英文
|
|
285
|
+
res.end(JSON.stringify({ error: 'File too large (max 100MB)', code: 'TOO_LARGE' }));
|
|
265
286
|
return;
|
|
266
287
|
}
|
|
267
288
|
const boundary = boundaryMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
@@ -273,7 +294,7 @@ export function importSkillTo(req, res, { skillsRoot, windowsReserved }) {
|
|
|
273
294
|
if (totalSize > MAX_UPLOAD) {
|
|
274
295
|
aborted = true;
|
|
275
296
|
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
276
|
-
res.end(JSON.stringify({ error: 'File too large (max 100MB)' }));
|
|
297
|
+
res.end(JSON.stringify({ error: 'File too large (max 100MB)', code: 'TOO_LARGE' }));
|
|
277
298
|
req.destroy();
|
|
278
299
|
return;
|
|
279
300
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Voice-pack routes (moved verbatim from server.js handleRequest).
|
|
2
2
|
// Manages user-uploaded audio + serves the bundled "皇上系列" default pack.
|
|
3
|
-
// Uploads are
|
|
3
|
+
// Uploads/deletes are admin-only (loopback, or authenticated remote admin) — other clients can play but not write.
|
|
4
4
|
import { lstatSync, statSync, createReadStream } from 'node:fs';
|
|
5
5
|
import { LOG_DIR } from '../../findcc.js';
|
|
6
6
|
import {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
EVENT_KEYS as VP_EVENT_KEYS,
|
|
19
19
|
MAX_AUDIO_BYTES as VP_MAX_BYTES,
|
|
20
20
|
} from '../lib/voice-pack-manager.js';
|
|
21
|
+
import { isAdminReq } from '../lib/is-admin.js';
|
|
21
22
|
|
|
22
23
|
function voicePackList(req, res) {
|
|
23
24
|
try {
|
|
@@ -45,10 +46,10 @@ function voicePackList(req, res) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function voicePackUpload(req, res, parsedUrl, isLocal) {
|
|
48
|
-
//
|
|
49
|
-
// The token already gates LAN access but voice-pack writes touch the local FS
|
|
50
|
-
//
|
|
51
|
-
if (!isLocal) {
|
|
49
|
+
// Admin-only — refuse unauthenticated LAN clients even if they hold a valid token.
|
|
50
|
+
// The token already gates LAN access but voice-pack writes touch the local FS and end up
|
|
51
|
+
// reachable from every client; an authenticated remote admin (container/cloud) is allowed.
|
|
52
|
+
if (!isAdminReq(req, isLocal)) {
|
|
52
53
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
53
54
|
res.end(JSON.stringify({ error: 'Upload allowed from loopback only' }));
|
|
54
55
|
return;
|
|
@@ -106,7 +107,7 @@ function voicePackUpload(req, res, parsedUrl, isLocal) {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function voicePackDelete(req, res, parsedUrl, isLocal) {
|
|
109
|
-
if (!isLocal) {
|
|
110
|
+
if (!isAdminReq(req, isLocal)) {
|
|
110
111
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
111
112
|
res.end(JSON.stringify({ error: 'Delete allowed from loopback only' }));
|
|
112
113
|
return;
|
package/server/server.js
CHANGED
|
@@ -757,17 +757,27 @@ async function handleRequest(req, res) {
|
|
|
757
757
|
const isLocal = remoteIp === '127.0.0.1' || remoteIp === '::1' || remoteIp === '::ffff:127.0.0.1';
|
|
758
758
|
const isStaticAsset = url.startsWith('/assets/') || url === '/favicon.ico';
|
|
759
759
|
const wantsHtml = method === 'GET' && ((req.headers.accept || '').includes('text/html') || url === '/');
|
|
760
|
+
// Hoisted so decideAuth and the isAdmin computation share the SAME credential values.
|
|
761
|
+
const urlToken = parsedUrl.searchParams.get('token');
|
|
762
|
+
const cookieToken = parseCookies(req.headers.cookie).ccv_auth;
|
|
760
763
|
const authDecision = decideAuth({
|
|
761
764
|
isStaticAsset,
|
|
762
765
|
pathname: url,
|
|
763
766
|
isLocal,
|
|
764
|
-
urlToken
|
|
765
|
-
cookieToken
|
|
767
|
+
urlToken,
|
|
768
|
+
cookieToken,
|
|
766
769
|
accessToken: ACCESS_TOKEN,
|
|
767
770
|
enabled: authConfig.enabled,
|
|
768
771
|
password: authConfig.password,
|
|
769
772
|
wantsHtml,
|
|
770
773
|
});
|
|
774
|
+
// Remote-admin elevation (container/cloud deploy): an authenticated remote caller is
|
|
775
|
+
// treated as admin for management routes (isAdminReq in lib/is-admin.js). isAdmin comes
|
|
776
|
+
// straight from decideAuth (single source of truth — it mirrors the remote-allow branches
|
|
777
|
+
// and can never drift from them). Loopback is always admin. This MUST NOT relax the
|
|
778
|
+
// internal machine bridges (events.js hook notifies, ask-perm.js streamChunk) — those
|
|
779
|
+
// stay strictly loopback + INTERNAL_TOKEN.
|
|
780
|
+
req.ccvIsAdmin = authDecision.isAdmin === true;
|
|
771
781
|
if (authDecision.action === 'login-page') {
|
|
772
782
|
const lang = localeFromAcceptLanguage(req.headers['accept-language']) || getLang();
|
|
773
783
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|