cc-viewer 1.6.274 → 1.6.276
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 +11 -0
- package/cli.js +29 -0
- package/dist/assets/App-VfOsEOzR.css +1 -0
- package/dist/assets/App-g0FMGNbK.js +1 -0
- package/dist/assets/{MdxEditorPanel-DvqnmX-m.css → MdxEditorPanel-1vSx6ymU.css} +1 -1
- package/dist/assets/{MdxEditorPanel-BAnfnKiF.js → MdxEditorPanel-lkBOg8Jh.js} +1 -1
- package/dist/assets/Mobile-CfP2RFHS.js +1 -0
- package/dist/assets/{_baseUniq-CPJrFyUF.js → _baseUniq-IhLiJsz9.js} +1 -1
- package/dist/assets/{arc-BLBrFElt.js → arc-CI5WB-FW.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-CbnBsMiQ.js → architectureDiagram-Q4EWVU46-YOnqcO8r.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-0mYr6-Fl.js → blockDiagram-DXYQGD6D-B6oK5mZJ.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CS7vcr0z.js → c4Diagram-AHTNJAMY-JsynU_tx.js} +1 -1
- package/dist/assets/{channel-CF3zZzSR.js → channel-DLsvHA9V.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-1FZYtnJ7.js → chunk-4BX2VUAB-DGhBdIp1.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-COs1qui5.js → chunk-4TB4RGXK-crHwtw-n.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-p77Qw3wN.js → chunk-55IACEB6-CHTfiCuV.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-5qaIrQKg.js → chunk-EDXVE4YY-buLXKlmZ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DmCR8mDZ.js → chunk-FMBD7UC4-DSfXNwCE.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-D6xDfgW3.js → chunk-OYMX7WX6-16K1lWZl.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B6cmUU0N.js → chunk-QZHKN3VN-DgDuGTt2.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-l-OyOqnn.js → chunk-YZCP3GAM-DHqswL_6.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BCPnChlV.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BCPnChlV.js +1 -0
- package/dist/assets/clone-CJyRHxRw.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CEzaS8XS.js → cose-bilkent-S5V4N54A-D1j0CqN7.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-B3U2njWW.js → dagre-KV5264BT-XKbpcECY.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-BRSDbyBr.js → diagram-5BDNPKRD-CcbZUyrk.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BZfOi9B7.js → diagram-G4DWMVQ6-UxGWOz8H.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CWpb3Cg0.js → diagram-MMDJMWI5-COG8DLtM.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-CTJyBSVj.js → diagram-TYMM5635-_dGuaThe.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CHtTRd5S.js → erDiagram-SMLLAGMA-CNu-hiRP.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-Bda8X-WJ.js → flowDiagram-DWJPFMVM-DQHinAg1.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CPfnGu5V.js → ganttDiagram-T4ZO3ILL-BAySDtwF.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-B5QxesQg.js → gitGraphDiagram-UUTBAWPF-BBO_XdYa.js} +1 -1
- package/dist/assets/{graph-ChltdhTU.js → graph-D9285_n5.js} +1 -1
- package/dist/assets/index-0hGZ2hk-.js +2 -0
- package/dist/assets/{index-BK4sui_O.js → index-6IDcGlHG.js} +1 -1
- package/dist/assets/{index-C2fhupP6.js → index-CXhN926Q.js} +1 -1
- package/dist/assets/{index-CfEkC3bc.js → index-CaKHIO3v.js} +1 -1
- package/dist/assets/{index-yClPXlMf.js → index-D18bphjK.js} +1 -1
- package/dist/assets/{index-B7lK5fJz.js → index-DTi4w1dY.js} +1 -1
- package/dist/assets/{index-C5PA4OJg.js → index-gSNq4ykV.js} +1 -1
- package/dist/assets/{index-DyGa-jNv.js → index-pEgUz6fU.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CD4TS4O2.js → infoDiagram-42DDH7IO-DODFGD-6.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-jhiYabQ7.js → ishikawaDiagram-UXIWVN3A-kn6kHfYH.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BDzIXxxt.js → journeyDiagram-VCZTEJTY-BRaTTmzg.js} +1 -1
- package/dist/assets/{jszip.min-CuGGBMI4.js → jszip.min-CnLSX52a.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-D34MYATQ.js → kanban-definition-6JOO6SKY-CzLQb2u1.js} +1 -1
- package/dist/assets/{layout-D6sLAapX.js → layout-B9TjxURJ.js} +1 -1
- package/dist/assets/{linear-CFV4P-wn.js → linear-Dpck4eFk.js} +1 -1
- package/dist/assets/{mermaid.core-YmJi7T-s.js → mermaid.core-Dd_GyvDR.js} +2 -2
- package/dist/assets/{min-akJQqRMn.js → min-CUkc58GS.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-w5zaJyrN.js → mindmap-definition-QFDTVHPH-BteQHO2X.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-B6EM4Ow6.js → pieDiagram-DEJITSTG-CaOYrVcc.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-2TmBxFy-.js → quadrantDiagram-34T5L4WZ-DTcmYcmQ.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-EOjvbxUy.js → requirementDiagram-MS252O5E-CY8WoU0d.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-BmTQD5eT.js → sankeyDiagram-XADWPNL6-9NPSQPLx.js} +1 -1
- package/dist/assets/seqResourceLoaders-BgEHTs14.js +2 -0
- package/dist/assets/seqResourceLoaders-DPmXT2Hh.css +41 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-CLjtqA1D.js → sequenceDiagram-FGHM5R23-e7V5HhHU.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-Cbq90iZ8.js → stateDiagram-FHFEXIEX-B6R9nxzj.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-sUx4g5Jk.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-f3kEDay0.js → timeline-definition-GMOUNBTQ-CqpCMRBB.js} +1 -1
- package/dist/assets/{vendor-antd-5xE7sz6B.js → vendor-antd-BqzW9CXo.js} +2 -2
- package/dist/assets/{vendor-codemirror-ib-jPbXC.js → vendor-codemirror-B5WQ33Y0.js} +1 -1
- package/dist/assets/vendor-markdown-DOJHsAxX.js +3 -0
- package/dist/assets/{vendor-mdxeditor-BdKMdw6O.js → vendor-mdxeditor-DwzTmNBd.js} +2 -2
- package/dist/assets/{vendor-qrcode-vKlE-WYu.js → vendor-qrcode-C9ZMcovz.js} +1 -1
- package/dist/assets/{vendor-virtuoso-DOIfjLfU.js → vendor-virtuoso-CQj5-1oy.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-0GDSFVnH.js → vennDiagram-DHZGUBPP-CxzTP1Mu.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-B2rj5j7G.js → wardley-RL74JXVD-_Dcq-0iY.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-COVTciJP.js → wardleyDiagram-NUSXRM2D-CltoIcVL.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DXoLnGxX.js → xychartDiagram-5P7HB3ND-DXKa-Cxe.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/i18n.js +186 -2
- package/server/lib/auth.js +348 -0
- package/server/lib/voice-pack-events.js +2 -2
- package/server/routes/_dispatch.js +43 -0
- package/server/routes/ask-perm.js +452 -0
- package/server/routes/auth.js +166 -0
- package/server/routes/events.js +335 -0
- package/server/routes/files-content.js +421 -0
- package/server/routes/files-fs.js +955 -0
- package/server/routes/git.js +228 -0
- package/server/routes/logs.js +201 -0
- package/server/routes/misc.js +22 -0
- package/server/routes/plugins.js +96 -0
- package/server/routes/preferences.js +216 -0
- package/server/routes/project-meta.js +118 -0
- package/server/routes/skills.js +261 -0
- package/server/routes/team.js +169 -0
- package/server/routes/voice-pack.js +235 -0
- package/server/routes/workspaces.js +171 -0
- package/server/server.js +340 -3953
- package/dist/assets/App-BediOgt6.js +0 -1
- package/dist/assets/App-TGGslOeT.css +0 -1
- package/dist/assets/Mobile-C_nqfEXb.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BiCYgTHO.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BiCYgTHO.js +0 -1
- package/dist/assets/clone-ChTCnPsO.js +0 -1
- package/dist/assets/index-CuE3VwXB.js +0 -2
- package/dist/assets/seqResourceLoaders-CgNKpehN.js +0 -2
- package/dist/assets/seqResourceLoaders-Dd_x1M9-.css +0 -41
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BKDg-3t_.js +0 -1
- package/dist/assets/vendor-markdown-BFrYfpb0.js +0 -1
package/server/i18n.js
CHANGED
|
@@ -223,8 +223,8 @@ const i18nData = {
|
|
|
223
223
|
"uk": "\nДля видалення виконайте: ccv --uninstall"
|
|
224
224
|
},
|
|
225
225
|
"cli.help": {
|
|
226
|
-
"zh": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [选项] [命令] [提示词]\n\n默认启动交互式会话 + Web Viewer,所有参数透传给 claude。\n\n参数:\n prompt 你的提示词\n\nCCV 专属选项:\n -SDK, --sdk 使用 Agent SDK 模式(无终端,结构化交互)\n -logger 安装/修复 Claude Code Hook\n --uninstall 移除 CC Viewer 集成\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自定义 JSONL 日志存储目录\n --no-open 启动时不自动打开浏览器\n --user-name <name> 自定义显示名称\n --user-avatar <path|url> 自定义头像图片(文件路径或 URL)\n\nClaude 选项(透传):\n -c, --continue 继续当前目录最近的对话\n -r, --resume [value] 通过会话 ID 恢复对话,或打开交互式选择器\n -p, --print 打印响应后退出(适用于管道)\n -d, --debug [filter] 启用调试模式\n -w, --worktree [name] 为此会话创建新的 git worktree\n --model <model> 当前会话使用的模型\n --effort <level> 努力级别 (low, medium, high, max)\n --dangerously-skip-permissions 跳过所有权限检查\n --allowedTools <tools...> 允许的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 会话系统提示词\n --append-system-prompt <prompt> 追加系统提示词\n --mcp-config <configs...> 加载 MCP 服务器配置\n --permission-mode <mode> 权限模式\n --add-dir <directories...> 额外允许访问的目录\n --max-budget-usd <amount> API 调用最大预算(仅 --print)\n --output-format <format> 输出格式(仅 --print): text, json, stream-json\n --json-schema <schema> 结构化输出的 JSON Schema\n --verbose 详细模式\n -h, --help 显示帮助\n -v, --version 显示版本\n\n 更多选项请参考: claude --help\n\n示例:\n ccv 启动交互模式\n ccv -c 继续上次对话\n ccv -r 恢复对话\n ccv -p \"hello\" 打印模式\n ccv --model opus 指定模型\n ccv --d 跳过权限确认(快捷方式)\n ccv --ad 允许跳过权限(不立即启用,可通过 Shift+Tab 切换)\n ccv -c --model sonnet --effort max 组合使用\n ccv -SDK Agent SDK 模式(无终端)\n ccv -SDK --d SDK 模式 + 跳过权限\n ccv -logger 安装/修复 Hook\n ccv --log-dir ~/my-logs 使用自定义日志目录\n ccv --no-open 启动时不打开浏览器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自定义用户身份\n ccv --log-dir /tmp/test --no-open 组合 CCV 选项",
|
|
227
|
-
"en": "CC Viewer CLI — Web Viewer frontend for Claude Code\n\nUsage: ccv [options] [command] [prompt]\n\nStarts an interactive session with Web Viewer by default. All args are passed\nthrough to claude.\n\nArguments:\n prompt Your prompt\n\nCCV Options:\n -SDK, --sdk Use Agent SDK mode (no terminal, structured interaction)\n -logger Install/repair Claude Code hook\n --uninstall Remove CC Viewer integration\n --d Shortcut for --dangerously-skip-permissions\n --ad Shortcut for --allow-dangerously-skip-permissions\n --log-dir <path> Custom JSONL log storage directory\n --no-open Don't auto-open browser on startup\n --user-name <name> Custom display name for this session\n --user-avatar <path|url> Custom avatar image (file path or URL)\n\nClaude Options (passed through):\n -c, --continue Continue the most recent conversation\n -r, --resume [value] Resume a conversation by session ID, or open picker\n -p, --print Print response and exit (useful for pipes)\n -d, --debug [filter] Enable debug mode\n -w, --worktree [name] Create a new git worktree for this session\n --model <model> Model for the current session\n --effort <level> Effort level (low, medium, high, max)\n --dangerously-skip-permissions Bypass all permission checks\n --allowedTools <tools...> Allowed tool names\n --disallowedTools <tools...> Disallowed tool names\n --system-prompt <prompt> System prompt for the session\n --append-system-prompt <prompt> Append to default system prompt\n --mcp-config <configs...> Load MCP servers from JSON files or strings\n --permission-mode <mode> Permission mode for the session\n --add-dir <directories...> Additional directories to allow access to\n --max-budget-usd <amount> Max dollar amount for API calls (--print only)\n --output-format <format> Output format (--print only): text, json, stream-json\n --json-schema <schema> JSON Schema for structured output\n --verbose Enable verbose mode\n -h, --help Show help\n -v, --version Show version\n\n For all options, see: claude --help\n\nExamples:\n ccv Start interactive mode\n ccv -c Continue last conversation\n ccv -r Resume a conversation\n ccv -p \"hello\" Print mode\n ccv --model opus Specify model\n ccv --d Skip permission prompts (shortcut)\n ccv --ad Allow bypass mode (not active, toggle via Shift+Tab)\n ccv -c --model sonnet --effort max Combine options\n ccv -SDK Agent SDK mode (no terminal)\n ccv -SDK --d SDK mode + bypass permissions\n ccv -logger Install/repair hook\n ccv --log-dir ~/my-logs Use custom log directory\n ccv --no-open Start without opening browser\n ccv --user-name \"Alice\" --user-avatar ./avatar.png Custom user profile\n ccv --log-dir /tmp/test --no-open Combine CCV options",
|
|
226
|
+
"zh": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [选项] [命令] [提示词]\n\n默认启动交互式会话 + Web Viewer,所有参数透传给 claude。\n\n参数:\n prompt 你的提示词\n\nCCV 专属选项:\n -SDK, --sdk 使用 Agent SDK 模式(无终端,结构化交互)\n -logger 安装/修复 Claude Code Hook\n --uninstall 移除 CC Viewer 集成\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自定义 JSONL 日志存储目录\n --no-open 启动时不自动打开浏览器\n --user-name <name> 自定义显示名称\n --user-avatar <path|url> 自定义头像图片(文件路径或 URL)\n --usePassword[=<pwd>] 启用密码登录(远程访问需密码或 token;不带值则随机生成 8 位)\n\nClaude 选项(透传):\n -c, --continue 继续当前目录最近的对话\n -r, --resume [value] 通过会话 ID 恢复对话,或打开交互式选择器\n -p, --print 打印响应后退出(适用于管道)\n -d, --debug [filter] 启用调试模式\n -w, --worktree [name] 为此会话创建新的 git worktree\n --model <model> 当前会话使用的模型\n --effort <level> 努力级别 (low, medium, high, max)\n --dangerously-skip-permissions 跳过所有权限检查\n --allowedTools <tools...> 允许的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 会话系统提示词\n --append-system-prompt <prompt> 追加系统提示词\n --mcp-config <configs...> 加载 MCP 服务器配置\n --permission-mode <mode> 权限模式\n --add-dir <directories...> 额外允许访问的目录\n --max-budget-usd <amount> API 调用最大预算(仅 --print)\n --output-format <format> 输出格式(仅 --print): text, json, stream-json\n --json-schema <schema> 结构化输出的 JSON Schema\n --verbose 详细模式\n -h, --help 显示帮助\n -v, --version 显示版本\n\n 更多选项请参考: claude --help\n\n示例:\n ccv 启动交互模式\n ccv -c 继续上次对话\n ccv -r 恢复对话\n ccv -p \"hello\" 打印模式\n ccv --model opus 指定模型\n ccv --d 跳过权限确认(快捷方式)\n ccv --ad 允许跳过权限(不立即启用,可通过 Shift+Tab 切换)\n ccv -c --model sonnet --effort max 组合使用\n ccv -SDK Agent SDK 模式(无终端)\n ccv -SDK --d SDK 模式 + 跳过权限\n ccv -logger 安装/修复 Hook\n ccv --log-dir ~/my-logs 使用自定义日志目录\n ccv --no-open 启动时不打开浏览器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自定义用户身份\n ccv --log-dir /tmp/test --no-open 组合 CCV 选项",
|
|
227
|
+
"en": "CC Viewer CLI — Web Viewer frontend for Claude Code\n\nUsage: ccv [options] [command] [prompt]\n\nStarts an interactive session with Web Viewer by default. All args are passed\nthrough to claude.\n\nArguments:\n prompt Your prompt\n\nCCV Options:\n -SDK, --sdk Use Agent SDK mode (no terminal, structured interaction)\n -logger Install/repair Claude Code hook\n --uninstall Remove CC Viewer integration\n --d Shortcut for --dangerously-skip-permissions\n --ad Shortcut for --allow-dangerously-skip-permissions\n --log-dir <path> Custom JSONL log storage directory\n --no-open Don't auto-open browser on startup\n --user-name <name> Custom display name for this session\n --user-avatar <path|url> Custom avatar image (file path or URL)\n --usePassword[=<pwd>] Enable password login (remote needs password or token; bare flag → random 8-char)\n\nClaude Options (passed through):\n -c, --continue Continue the most recent conversation\n -r, --resume [value] Resume a conversation by session ID, or open picker\n -p, --print Print response and exit (useful for pipes)\n -d, --debug [filter] Enable debug mode\n -w, --worktree [name] Create a new git worktree for this session\n --model <model> Model for the current session\n --effort <level> Effort level (low, medium, high, max)\n --dangerously-skip-permissions Bypass all permission checks\n --allowedTools <tools...> Allowed tool names\n --disallowedTools <tools...> Disallowed tool names\n --system-prompt <prompt> System prompt for the session\n --append-system-prompt <prompt> Append to default system prompt\n --mcp-config <configs...> Load MCP servers from JSON files or strings\n --permission-mode <mode> Permission mode for the session\n --add-dir <directories...> Additional directories to allow access to\n --max-budget-usd <amount> Max dollar amount for API calls (--print only)\n --output-format <format> Output format (--print only): text, json, stream-json\n --json-schema <schema> JSON Schema for structured output\n --verbose Enable verbose mode\n -h, --help Show help\n -v, --version Show version\n\n For all options, see: claude --help\n\nExamples:\n ccv Start interactive mode\n ccv -c Continue last conversation\n ccv -r Resume a conversation\n ccv -p \"hello\" Print mode\n ccv --model opus Specify model\n ccv --d Skip permission prompts (shortcut)\n ccv --ad Allow bypass mode (not active, toggle via Shift+Tab)\n ccv -c --model sonnet --effort max Combine options\n ccv -SDK Agent SDK mode (no terminal)\n ccv -SDK --d SDK mode + bypass permissions\n ccv -logger Install/repair hook\n ccv --log-dir ~/my-logs Use custom log directory\n ccv --no-open Start without opening browser\n ccv --user-name \"Alice\" --user-avatar ./avatar.png Custom user profile\n ccv --log-dir /tmp/test --no-open Combine CCV options",
|
|
228
228
|
"zh-TW": "CC Viewer CLI — Claude Code 的 Web Viewer 前端\n\n用法: ccv [選項] [命令] [提示詞]\n\n預設啟動互動式會話 + Web Viewer,所有參數透傳給 claude。\n\n參數:\n prompt 你的提示詞\n\nCCV 專屬選項:\n -SDK, --sdk 使用 Agent SDK 模式(無終端,結構化互動)\n -logger 安裝/修復 Claude Code Hook\n --uninstall 移除 CC Viewer 整合\n --d --dangerously-skip-permissions 的快捷方式\n --ad --allow-dangerously-skip-permissions 的快捷方式\n --log-dir <path> 自訂 JSONL 日誌儲存目錄\n --no-open 啟動時不自動開啟瀏覽器\n --user-name <name> 自訂顯示名稱\n --user-avatar <path|url> 自訂頭像圖片(檔案路徑或 URL)\n\nClaude 選項(透傳):\n -c, --continue 繼續當前目錄最近的對話\n -r, --resume [value] 透過會話 ID 恢復對話,或開啟互動式選擇器\n -p, --print 列印回應後退出(適用於管道)\n -d, --debug [filter] 啟用除錯模式\n -w, --worktree [name] 為此會話建立新的 git worktree\n --model <model> 當前會話使用的模型\n --effort <level> 努力級別 (low, medium, high, max)\n --dangerously-skip-permissions 跳過所有權限檢查\n --allowedTools <tools...> 允許的工具列表\n --disallowedTools <tools...> 禁止的工具列表\n --system-prompt <prompt> 會話系統提示詞\n --append-system-prompt <prompt> 追加系統提示詞\n --mcp-config <configs...> 載入 MCP 伺服器設定\n --permission-mode <mode> 權限模式\n --add-dir <directories...> 額外允許存取的目錄\n --max-budget-usd <amount> API 呼叫最大預算(僅 --print)\n --output-format <format> 輸出格式(僅 --print): text, json, stream-json\n --json-schema <schema> 結構化輸出的 JSON Schema\n --verbose 詳細模式\n -h, --help 顯示說明\n -v, --version 顯示版本\n\n 更多選項請參考: claude --help\n\n範例:\n ccv 啟動互動模式\n ccv -c 繼續上次對話\n ccv -r 恢復對話\n ccv -p \"hello\" 列印模式\n ccv --model opus 指定模型\n ccv --d 跳過權限確認(快捷方式)\n ccv --ad 允許跳過權限(不立即啟用,可透過 Shift+Tab 切換)\n ccv -c --model sonnet --effort max 組合使用\n ccv -SDK Agent SDK 模式(無終端)\n ccv -SDK --d SDK 模式 + 跳過權限\n ccv -logger 安裝/修復 Hook\n ccv --log-dir ~/my-logs 使用自訂日誌目錄\n ccv --no-open 啟動時不開啟瀏覽器\n ccv --user-name \"Alice\" --user-avatar ./avatar.png 自訂使用者身份\n ccv --log-dir /tmp/test --no-open 組合 CCV 選項"
|
|
229
229
|
},
|
|
230
230
|
"cli.cMode.notFound": {
|
|
@@ -487,6 +487,146 @@ const i18nData = {
|
|
|
487
487
|
"tr": " ➜ Network: {protocol}://{ip}:{port}?token={token}",
|
|
488
488
|
"uk": " ➜ Network: {protocol}://{ip}:{port}?token={token}"
|
|
489
489
|
},
|
|
490
|
+
"server.passwordActive": {
|
|
491
|
+
"zh": " 🔒 密码保护已开启,密码: {password}",
|
|
492
|
+
"en": " 🔒 Password protection enabled. Password: {password}",
|
|
493
|
+
"zh-TW": " 🔒 密碼保護已開啟,密碼: {password}",
|
|
494
|
+
"ko": " 🔒 비밀번호 보호가 활성화되었습니다. 비밀번호: {password}",
|
|
495
|
+
"ja": " 🔒 パスワード保護が有効です。パスワード: {password}",
|
|
496
|
+
"de": " 🔒 Passwortschutz aktiviert. Passwort: {password}",
|
|
497
|
+
"es": " 🔒 Protección por contraseña activada. Contraseña: {password}",
|
|
498
|
+
"fr": " 🔒 Protection par mot de passe activée. Mot de passe : {password}",
|
|
499
|
+
"it": " 🔒 Protezione con password attivata. Password: {password}",
|
|
500
|
+
"da": " 🔒 Adgangskodebeskyttelse aktiveret. Adgangskode: {password}",
|
|
501
|
+
"pl": " 🔒 Ochrona hasłem włączona. Hasło: {password}",
|
|
502
|
+
"ru": " 🔒 Защита паролем включена. Пароль: {password}",
|
|
503
|
+
"ar": " 🔒 تم تفعيل الحماية بكلمة المرور. كلمة المرور: {password}",
|
|
504
|
+
"no": " 🔒 Passordbeskyttelse aktivert. Passord: {password}",
|
|
505
|
+
"pt-BR": " 🔒 Proteção por senha ativada. Senha: {password}",
|
|
506
|
+
"th": " 🔒 เปิดใช้การป้องกันด้วยรหัสผ่านแล้ว รหัสผ่าน: {password}",
|
|
507
|
+
"tr": " 🔒 Parola koruması etkin. Parola: {password}",
|
|
508
|
+
"uk": " 🔒 Захист паролем увімкнено. Пароль: {password}"
|
|
509
|
+
},
|
|
510
|
+
"server.passwordEmptyWarn": {
|
|
511
|
+
"zh": " ⚠️ 密码为空:远程访问无需任何验证,存在安全风险",
|
|
512
|
+
"en": " ⚠️ Password is empty: remote access requires no verification — security risk",
|
|
513
|
+
"zh-TW": " ⚠️ 密碼為空:遠端存取無需任何驗證,存在安全風險",
|
|
514
|
+
"ko": " ⚠️ 비밀번호가 비어 있음: 원격 접속에 인증이 필요 없어 보안 위험이 있습니다",
|
|
515
|
+
"ja": " ⚠️ パスワードが空です:リモートアクセスに認証が不要でセキュリティリスクがあります",
|
|
516
|
+
"de": " ⚠️ Passwort ist leer: Fernzugriff erfordert keine Verifizierung – Sicherheitsrisiko",
|
|
517
|
+
"es": " ⚠️ La contraseña está vacía: el acceso remoto no requiere verificación; riesgo de seguridad",
|
|
518
|
+
"fr": " ⚠️ Mot de passe vide : l'accès distant ne nécessite aucune vérification — risque de sécurité",
|
|
519
|
+
"it": " ⚠️ Password vuota: l'accesso remoto non richiede verifica — rischio di sicurezza",
|
|
520
|
+
"da": " ⚠️ Adgangskode er tom: fjernadgang kræver ingen verificering – sikkerhedsrisiko",
|
|
521
|
+
"pl": " ⚠️ Hasło jest puste: dostęp zdalny nie wymaga weryfikacji — ryzyko bezpieczeństwa",
|
|
522
|
+
"ru": " ⚠️ Пароль пуст: удалённый доступ не требует проверки — угроза безопасности",
|
|
523
|
+
"ar": " ⚠️ كلمة المرور فارغة: الوصول عن بُعد لا يتطلب أي تحقق — خطر أمني",
|
|
524
|
+
"no": " ⚠️ Passordet er tomt: fjerntilgang krever ingen verifisering – sikkerhetsrisiko",
|
|
525
|
+
"pt-BR": " ⚠️ A senha está vazia: o acesso remoto não exige verificação — risco de segurança",
|
|
526
|
+
"th": " ⚠️ รหัสผ่านว่างเปล่า: การเข้าถึงระยะไกลไม่ต้องยืนยันตัวตน — มีความเสี่ยงด้านความปลอดภัย",
|
|
527
|
+
"tr": " ⚠️ Parola boş: uzaktan erişim doğrulama gerektirmez — güvenlik riski",
|
|
528
|
+
"uk": " ⚠️ Пароль порожній: віддалений доступ не потребує перевірки — загроза безпеці"
|
|
529
|
+
},
|
|
530
|
+
"server.auth.loginTitle": {
|
|
531
|
+
"zh": "请输入访问密码",
|
|
532
|
+
"en": "Enter access password",
|
|
533
|
+
"zh-TW": "請輸入存取密碼",
|
|
534
|
+
"ko": "접속 비밀번호를 입력하세요",
|
|
535
|
+
"ja": "アクセスパスワードを入力してください",
|
|
536
|
+
"de": "Zugangspasswort eingeben",
|
|
537
|
+
"es": "Introduce la contraseña de acceso",
|
|
538
|
+
"fr": "Saisissez le mot de passe d'accès",
|
|
539
|
+
"it": "Inserisci la password di accesso",
|
|
540
|
+
"da": "Indtast adgangskode",
|
|
541
|
+
"pl": "Wprowadź hasło dostępu",
|
|
542
|
+
"ru": "Введите пароль доступа",
|
|
543
|
+
"ar": "أدخل كلمة مرور الوصول",
|
|
544
|
+
"no": "Skriv inn tilgangspassord",
|
|
545
|
+
"pt-BR": "Digite a senha de acesso",
|
|
546
|
+
"th": "กรอกรหัสผ่านเพื่อเข้าถึง",
|
|
547
|
+
"tr": "Erişim parolasını girin",
|
|
548
|
+
"uk": "Введіть пароль доступу"
|
|
549
|
+
},
|
|
550
|
+
"server.auth.loginPlaceholder": {
|
|
551
|
+
"zh": "密码",
|
|
552
|
+
"en": "Password",
|
|
553
|
+
"zh-TW": "密碼",
|
|
554
|
+
"ko": "비밀번호",
|
|
555
|
+
"ja": "パスワード",
|
|
556
|
+
"de": "Passwort",
|
|
557
|
+
"es": "Contraseña",
|
|
558
|
+
"fr": "Mot de passe",
|
|
559
|
+
"it": "Password",
|
|
560
|
+
"da": "Adgangskode",
|
|
561
|
+
"pl": "Hasło",
|
|
562
|
+
"ru": "Пароль",
|
|
563
|
+
"ar": "كلمة المرور",
|
|
564
|
+
"no": "Passord",
|
|
565
|
+
"pt-BR": "Senha",
|
|
566
|
+
"th": "รหัสผ่าน",
|
|
567
|
+
"tr": "Parola",
|
|
568
|
+
"uk": "Пароль"
|
|
569
|
+
},
|
|
570
|
+
"server.auth.loginBtn": {
|
|
571
|
+
"zh": "登录",
|
|
572
|
+
"en": "Sign in",
|
|
573
|
+
"zh-TW": "登入",
|
|
574
|
+
"ko": "로그인",
|
|
575
|
+
"ja": "ログイン",
|
|
576
|
+
"de": "Anmelden",
|
|
577
|
+
"es": "Iniciar sesión",
|
|
578
|
+
"fr": "Se connecter",
|
|
579
|
+
"it": "Accedi",
|
|
580
|
+
"da": "Log ind",
|
|
581
|
+
"pl": "Zaloguj się",
|
|
582
|
+
"ru": "Войти",
|
|
583
|
+
"ar": "تسجيل الدخول",
|
|
584
|
+
"no": "Logg inn",
|
|
585
|
+
"pt-BR": "Entrar",
|
|
586
|
+
"th": "เข้าสู่ระบบ",
|
|
587
|
+
"tr": "Giriş yap",
|
|
588
|
+
"uk": "Увійти"
|
|
589
|
+
},
|
|
590
|
+
"server.auth.loginError": {
|
|
591
|
+
"zh": "密码错误",
|
|
592
|
+
"en": "Incorrect password",
|
|
593
|
+
"zh-TW": "密碼錯誤",
|
|
594
|
+
"ko": "비밀번호가 올바르지 않습니다",
|
|
595
|
+
"ja": "パスワードが正しくありません",
|
|
596
|
+
"de": "Falsches Passwort",
|
|
597
|
+
"es": "Contraseña incorrecta",
|
|
598
|
+
"fr": "Mot de passe incorrect",
|
|
599
|
+
"it": "Password errata",
|
|
600
|
+
"da": "Forkert adgangskode",
|
|
601
|
+
"pl": "Nieprawidłowe hasło",
|
|
602
|
+
"ru": "Неверный пароль",
|
|
603
|
+
"ar": "كلمة المرور غير صحيحة",
|
|
604
|
+
"no": "Feil passord",
|
|
605
|
+
"pt-BR": "Senha incorreta",
|
|
606
|
+
"th": "รหัสผ่านไม่ถูกต้อง",
|
|
607
|
+
"tr": "Parola yanlış",
|
|
608
|
+
"uk": "Невірний пароль"
|
|
609
|
+
},
|
|
610
|
+
"server.auth.togglePassword": {
|
|
611
|
+
"zh": "显示/隐藏密码",
|
|
612
|
+
"en": "Show/hide password",
|
|
613
|
+
"zh-TW": "顯示/隱藏密碼",
|
|
614
|
+
"ko": "비밀번호 표시/숨기기",
|
|
615
|
+
"ja": "パスワードの表示/非表示",
|
|
616
|
+
"de": "Passwort anzeigen/verbergen",
|
|
617
|
+
"es": "Mostrar/ocultar contraseña",
|
|
618
|
+
"fr": "Afficher/masquer le mot de passe",
|
|
619
|
+
"it": "Mostra/nascondi password",
|
|
620
|
+
"da": "Vis/skjul adgangskode",
|
|
621
|
+
"pl": "Pokaż/ukryj hasło",
|
|
622
|
+
"ru": "Показать/скрыть пароль",
|
|
623
|
+
"ar": "إظهار/إخفاء كلمة المرور",
|
|
624
|
+
"no": "Vis/skjul passord",
|
|
625
|
+
"pt-BR": "Mostrar/ocultar senha",
|
|
626
|
+
"th": "แสดง/ซ่อนรหัสผ่าน",
|
|
627
|
+
"tr": "Parolayı göster/gizle",
|
|
628
|
+
"uk": "Показати/сховати пароль"
|
|
629
|
+
},
|
|
490
630
|
"update.updating": {
|
|
491
631
|
"zh": "正在更新到 v{version}...",
|
|
492
632
|
"en": "Updating to v{version}...",
|
|
@@ -2561,5 +2701,49 @@ export function t(key, params) {
|
|
|
2561
2701
|
return text;
|
|
2562
2702
|
}
|
|
2563
2703
|
|
|
2704
|
+
/**
|
|
2705
|
+
* Like t(), but resolves against an explicit locale instead of the process-global
|
|
2706
|
+
* currentLang. Used to render the login page in the remote browser's language
|
|
2707
|
+
* without mutating shared state. Falls back to en → key.
|
|
2708
|
+
*/
|
|
2709
|
+
export function tFor(key, lang, params) {
|
|
2710
|
+
let text = locales[lang]?.[key] || locales['en'][key] || key;
|
|
2711
|
+
if (params) {
|
|
2712
|
+
for (const [k, v] of Object.entries(params)) {
|
|
2713
|
+
text = text.replaceAll(`{${k}}`, v);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return text;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
/**
|
|
2720
|
+
* Pick the best supported locale from an HTTP Accept-Language header
|
|
2721
|
+
* (e.g. "en-US,en;q=0.9,zh;q=0.8"). Returns null if the header is empty/unusable
|
|
2722
|
+
* so the caller can fall back to the server's configured language.
|
|
2723
|
+
*/
|
|
2724
|
+
export function localeFromAcceptLanguage(header) {
|
|
2725
|
+
if (!header || typeof header !== 'string') return null;
|
|
2726
|
+
const tags = header
|
|
2727
|
+
.split(',')
|
|
2728
|
+
.map((part) => {
|
|
2729
|
+
const [tag, ...params] = part.trim().split(';');
|
|
2730
|
+
let q = 1;
|
|
2731
|
+
for (const param of params) {
|
|
2732
|
+
const m = param.trim().match(/^q=([0-9.]+)$/);
|
|
2733
|
+
if (m) q = parseFloat(m[1]);
|
|
2734
|
+
}
|
|
2735
|
+
return { tag: tag.trim(), q: Number.isFinite(q) ? q : 0 };
|
|
2736
|
+
})
|
|
2737
|
+
.filter((x) => x.tag && x.tag !== '*')
|
|
2738
|
+
.sort((a, b) => b.q - a.q);
|
|
2739
|
+
for (const { tag } of tags) {
|
|
2740
|
+
const cleaned = tag.replace(/_/g, '-').toLowerCase();
|
|
2741
|
+
if (LANG_MAP[cleaned]) return LANG_MAP[cleaned];
|
|
2742
|
+
const primary = cleaned.split('-')[0];
|
|
2743
|
+
if (LANG_MAP[primary]) return LANG_MAP[primary];
|
|
2744
|
+
}
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2564
2748
|
// 自动检测并设置语言
|
|
2565
2749
|
setLang(detectLanguage());
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Password-based LAN auth — pure logic, heavily unit-tested.
|
|
2
|
+
//
|
|
3
|
+
// cc-viewer's original LAN gate is URL `?token=<32-hex>` (server.js ACCESS_TOKEN,
|
|
4
|
+
// random per boot, never persisted). This module adds an OPTIONAL second path:
|
|
5
|
+
// a password that, once verified, is stored in a cookie (value = ACCESS_TOKEN).
|
|
6
|
+
// Both paths coexist — token and cookie are equivalent.
|
|
7
|
+
//
|
|
8
|
+
// Everything here is side-effect-light and exported so the decision logic can be
|
|
9
|
+
// unit-tested directly (loopback HTTP tests always look local, so they cannot
|
|
10
|
+
// exercise the remote branch — `decideAuth` covers it as a pure function).
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
15
|
+
import { tFor, localeFromAcceptLanguage } from '../i18n.js';
|
|
16
|
+
|
|
17
|
+
// A-Z + 0-9 (36 chars). No lowercase / ambiguous-pair stripping: kept simple and
|
|
18
|
+
// matches the user-facing "uppercase letters + digits" spec. Login compares
|
|
19
|
+
// case-insensitively (see routes/auth.js), so lowercase input still works.
|
|
20
|
+
const PASSWORD_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Cryptographically uniform password from A-Z0-9 (default 6 chars).
|
|
24
|
+
* Rejection sampling discards bytes ≥ 252 (256 - 256%36) to avoid modulo bias.
|
|
25
|
+
*/
|
|
26
|
+
export function generatePassword(len = 6) {
|
|
27
|
+
const n = PASSWORD_CHARS.length; // 36
|
|
28
|
+
const limit = 256 - (256 % n); // 252
|
|
29
|
+
let out = '';
|
|
30
|
+
while (out.length < len) {
|
|
31
|
+
const buf = randomBytes(len * 2);
|
|
32
|
+
for (let i = 0; i < buf.length && out.length < len; i++) {
|
|
33
|
+
if (buf[i] < limit) out += PASSWORD_CHARS[buf[i] % n];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_CONFIG = { enabled: false, password: '' };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Auth config is persisted as the `auth` key inside the same preferences.json the
|
|
43
|
+
* rest of cc-viewer uses (LOG_DIR/preferences.json) — NOT a separate global file.
|
|
44
|
+
* Path is computed fresh each call: LOG_DIR is a live binding (setLogDir) and tests
|
|
45
|
+
* redirect it via CCV_LOG_DIR before importing.
|
|
46
|
+
*/
|
|
47
|
+
export function getPrefsPath() {
|
|
48
|
+
return join(LOG_DIR, 'preferences.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readPrefs() {
|
|
52
|
+
try {
|
|
53
|
+
const p = getPrefsPath();
|
|
54
|
+
if (!existsSync(p)) return {};
|
|
55
|
+
const obj = JSON.parse(readFileSync(p, 'utf-8'));
|
|
56
|
+
return obj && typeof obj === 'object' ? obj : {};
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeAuth(cfg) {
|
|
63
|
+
return {
|
|
64
|
+
enabled: !!(cfg && cfg.enabled),
|
|
65
|
+
password: cfg && typeof cfg.password === 'string' ? cfg.password : '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The password is kept in memory & over the admin API as plaintext (admin must be able
|
|
70
|
+
// to view/copy it), but is base64-encoded on disk so preferences.json never shows the
|
|
71
|
+
// raw password. This is light obfuscation, NOT real security — base64 is trivially
|
|
72
|
+
// reversible; it only avoids the password sitting in literal plaintext in the file.
|
|
73
|
+
function encodePassword(plain) {
|
|
74
|
+
return plain ? Buffer.from(plain, 'utf-8').toString('base64') : '';
|
|
75
|
+
}
|
|
76
|
+
function decodePassword(stored) {
|
|
77
|
+
if (!stored || typeof stored !== 'string') return '';
|
|
78
|
+
try { return Buffer.from(stored, 'base64').toString('utf-8'); } catch { return ''; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Scoped persistence (global default + optional per-project override) ───
|
|
82
|
+
// preferences.json:
|
|
83
|
+
// auth: { enabled, password(b64) } ← global default
|
|
84
|
+
// authByProject: { "<projectDir>": { enabled, password(b64) } } ← optional overrides
|
|
85
|
+
// A project "has an override" iff a key exists for it (even if disabled). The gate
|
|
86
|
+
// resolves: project override (if the key exists) else global. To inherit global again,
|
|
87
|
+
// the override must be REMOVED (clearProjectOverride), not merely disabled.
|
|
88
|
+
|
|
89
|
+
function decodeStored(a) {
|
|
90
|
+
return { enabled: !!(a && a.enabled), password: decodePassword(a && a.password) };
|
|
91
|
+
}
|
|
92
|
+
function encodeForDisk(normalized) {
|
|
93
|
+
return { enabled: normalized.enabled, password: encodePassword(normalized.password) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Does this project have its own auth override? (key present, regardless of enabled) */
|
|
97
|
+
function hasOverride(prefs, projectDir) {
|
|
98
|
+
return !!(projectDir && prefs.authByProject && Object.prototype.hasOwnProperty.call(prefs.authByProject, projectDir));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the EFFECTIVE auth config the gate should enforce for `projectDir`:
|
|
103
|
+
* the project override if one exists, otherwise the global default.
|
|
104
|
+
* Returns { enabled, password } (plaintext). projectDir null → always global.
|
|
105
|
+
*/
|
|
106
|
+
export function loadAuthConfig(projectDir = null) {
|
|
107
|
+
const prefs = readPrefs();
|
|
108
|
+
const src = hasOverride(prefs, projectDir) ? prefs.authByProject[projectDir] : prefs.auth;
|
|
109
|
+
return decodeStored(src);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Rich state for the admin API: the effective config + which scope produced it +
|
|
114
|
+
* whether a project override exists + the global default (all passwords plaintext;
|
|
115
|
+
* the route masks them for non-admins).
|
|
116
|
+
*/
|
|
117
|
+
export function loadAuthState(projectDir = null) {
|
|
118
|
+
const prefs = readPrefs();
|
|
119
|
+
const overridden = hasOverride(prefs, projectDir);
|
|
120
|
+
return {
|
|
121
|
+
effective: decodeStored(overridden ? prefs.authByProject[projectDir] : prefs.auth),
|
|
122
|
+
global: decodeStored(prefs.auth),
|
|
123
|
+
scope: overridden ? 'project' : 'global',
|
|
124
|
+
hasProjectOverride: overridden,
|
|
125
|
+
projectDir: projectDir || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writePrefs(prefs) {
|
|
130
|
+
const p = getPrefsPath();
|
|
131
|
+
const dir = dirname(p);
|
|
132
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
133
|
+
writeFileSync(p, JSON.stringify(prefs, null, 2), { mode: 0o600 });
|
|
134
|
+
// writeFileSync's mode only applies on creation; a pre-existing file keeps its old
|
|
135
|
+
// perms. Re-assert 0600 — preferences.json now carries the (base64) password.
|
|
136
|
+
try { chmodSync(p, 0o600); } catch { /* best-effort; non-POSIX or race */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Persist auth config (read-merge-write, preserving all other prefs). `opts.scope`
|
|
141
|
+
* 'global' → the `auth` key; 'project' → authByProject[opts.projectDir] (requires
|
|
142
|
+
* projectDir, else falls back to global). Password stored base64; returns the
|
|
143
|
+
* in-memory (plaintext) normalized shape.
|
|
144
|
+
*/
|
|
145
|
+
export function saveAuthConfig(cfg, opts = {}) {
|
|
146
|
+
const normalized = normalizeAuth(cfg);
|
|
147
|
+
const prefs = readPrefs();
|
|
148
|
+
const scope = opts.scope === 'project' && opts.projectDir ? 'project' : 'global';
|
|
149
|
+
if (scope === 'project') {
|
|
150
|
+
if (!prefs.authByProject || typeof prefs.authByProject !== 'object') prefs.authByProject = {};
|
|
151
|
+
prefs.authByProject[opts.projectDir] = encodeForDisk(normalized);
|
|
152
|
+
} else {
|
|
153
|
+
prefs.auth = encodeForDisk(normalized);
|
|
154
|
+
}
|
|
155
|
+
writePrefs(prefs);
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Remove a project's override so it inherits the global default again. No-op if absent. */
|
|
160
|
+
export function clearProjectOverride(projectDir) {
|
|
161
|
+
if (!projectDir) return;
|
|
162
|
+
const prefs = readPrefs();
|
|
163
|
+
if (prefs.authByProject && Object.prototype.hasOwnProperty.call(prefs.authByProject, projectDir)) {
|
|
164
|
+
delete prefs.authByProject[projectDir];
|
|
165
|
+
writePrefs(prefs);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse a Cookie header into an object.
|
|
171
|
+
* - Segments without `=` are skipped (malformed).
|
|
172
|
+
* - Duplicate keys: first occurrence wins (deterministic).
|
|
173
|
+
*/
|
|
174
|
+
export function parseCookies(header) {
|
|
175
|
+
const out = {};
|
|
176
|
+
if (!header || typeof header !== 'string') return out;
|
|
177
|
+
for (const part of header.split(';')) {
|
|
178
|
+
const idx = part.indexOf('=');
|
|
179
|
+
if (idx < 0) continue;
|
|
180
|
+
const key = part.slice(0, idx).trim();
|
|
181
|
+
if (!key || Object.prototype.hasOwnProperty.call(out, key)) continue;
|
|
182
|
+
out[key] = part.slice(idx + 1).trim();
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Pure auth decision, shared by the HTTP request prelude and the WS upgrade handler.
|
|
189
|
+
*
|
|
190
|
+
* ctx = {
|
|
191
|
+
* isStaticAsset, pathname, isLocal,
|
|
192
|
+
* urlToken, cookieToken, accessToken,
|
|
193
|
+
* enabled, password, wantsHtml
|
|
194
|
+
* }
|
|
195
|
+
* → { action: 'allow' | 'login-page' | 'unauthorized' | 'forbidden' }
|
|
196
|
+
*
|
|
197
|
+
* Caller invariants (see plan): 'login-page' / 'unauthorized' / 'forbidden' MUST
|
|
198
|
+
* terminate the request; only 'allow' falls through to the Host allowlist + routing.
|
|
199
|
+
*/
|
|
200
|
+
export function decideAuth(ctx) {
|
|
201
|
+
const {
|
|
202
|
+
isStaticAsset, pathname, isLocal,
|
|
203
|
+
urlToken, cookieToken, accessToken,
|
|
204
|
+
enabled, password, wantsHtml,
|
|
205
|
+
} = ctx;
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
isStaticAsset ||
|
|
209
|
+
pathname === '/api/auth/login' || // login endpoint must be reachable unauthenticated
|
|
210
|
+
isLocal ||
|
|
211
|
+
urlToken === accessToken ||
|
|
212
|
+
cookieToken === accessToken ||
|
|
213
|
+
(enabled && password === '') // empty password = explicitly no protection
|
|
214
|
+
) {
|
|
215
|
+
return { action: 'allow' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (enabled) {
|
|
219
|
+
// Password auth on but no valid credential: HTML navigations get the login page,
|
|
220
|
+
// XHR/asset/WS get a plain 401.
|
|
221
|
+
return { action: wantsHtml ? 'login-page' : 'unauthorized' };
|
|
222
|
+
}
|
|
223
|
+
// Password auth off → preserve original token-only behaviour (403).
|
|
224
|
+
return { action: 'forbidden' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function escapeHtml(s) {
|
|
228
|
+
return String(s)
|
|
229
|
+
.replace(/&/g, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>')
|
|
232
|
+
.replace(/"/g, '"')
|
|
233
|
+
.replace(/'/g, ''');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Self-contained login page (inline CSS + JS, references NO /assets — static assets
|
|
238
|
+
* are auth-exempt but we don't want to surface the SPA bundle to unauthenticated
|
|
239
|
+
* remotes). On submit it POSTs to /api/auth/login and reloads on success.
|
|
240
|
+
*
|
|
241
|
+
* `lang` is a resolved locale code (caller derives it from Accept-Language).
|
|
242
|
+
*/
|
|
243
|
+
export function renderLoginPage({ lang = 'en', error = false } = {}) {
|
|
244
|
+
const dir = lang === 'ar' ? 'rtl' : 'ltr';
|
|
245
|
+
const title = escapeHtml(tFor('server.auth.loginTitle', lang));
|
|
246
|
+
const placeholder = escapeHtml(tFor('server.auth.loginPlaceholder', lang));
|
|
247
|
+
const btn = escapeHtml(tFor('server.auth.loginBtn', lang));
|
|
248
|
+
const errMsg = escapeHtml(tFor('server.auth.loginError', lang));
|
|
249
|
+
const toggleLabel = escapeHtml(tFor('server.auth.togglePassword', lang));
|
|
250
|
+
const errStyle = error ? '' : ' style="display:none"';
|
|
251
|
+
return `<!DOCTYPE html>
|
|
252
|
+
<html lang="${escapeHtml(lang)}" dir="${dir}" data-theme="dark">
|
|
253
|
+
<head>
|
|
254
|
+
<meta charset="utf-8">
|
|
255
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
256
|
+
<meta name="robots" content="noindex">
|
|
257
|
+
<title>${title}</title>
|
|
258
|
+
<style>
|
|
259
|
+
:root { color-scheme: dark; }
|
|
260
|
+
* { box-sizing: border-box; }
|
|
261
|
+
body {
|
|
262
|
+
margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
263
|
+
background: #1a1a1a; color: #e8e8e8;
|
|
264
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
265
|
+
}
|
|
266
|
+
.card {
|
|
267
|
+
width: 100%; max-width: 340px; padding: 32px 28px; margin: 16px;
|
|
268
|
+
background: #242424; border: 1px solid #383838; border-radius: 14px;
|
|
269
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
270
|
+
}
|
|
271
|
+
h1 { margin: 0 0 20px; font-size: 18px; font-weight: 600; text-align: center; }
|
|
272
|
+
input {
|
|
273
|
+
width: 100%; padding: 11px 14px; font-size: 15px; color: #e8e8e8;
|
|
274
|
+
background: #1a1a1a; border: 1px solid #3a3a3a; border-radius: 8px; outline: none;
|
|
275
|
+
}
|
|
276
|
+
input:focus { border-color: #6c8cff; }
|
|
277
|
+
/* Password field + reveal toggle. padding-inline-end leaves room for the eye;
|
|
278
|
+
inset-inline-end keeps it on the correct side under RTL (ar). */
|
|
279
|
+
.field { position: relative; }
|
|
280
|
+
.field input { padding-inline-end: 44px; }
|
|
281
|
+
.toggle {
|
|
282
|
+
position: absolute; top: 50%; inset-inline-end: 6px; transform: translateY(-50%);
|
|
283
|
+
width: 32px; height: 32px; margin: 0; padding: 0;
|
|
284
|
+
display: flex; align-items: center; justify-content: center;
|
|
285
|
+
background: transparent; border: none; border-radius: 6px; cursor: pointer; color: #9a9a9a;
|
|
286
|
+
}
|
|
287
|
+
.toggle:hover { color: #e8e8e8; background: #2e2e2e; }
|
|
288
|
+
.toggle svg { width: 18px; height: 18px; display: block; }
|
|
289
|
+
#b {
|
|
290
|
+
width: 100%; margin-top: 14px; padding: 11px 14px; font-size: 15px; font-weight: 600;
|
|
291
|
+
color: #fff; background: #4a6cf7; border: none; border-radius: 8px; cursor: pointer;
|
|
292
|
+
}
|
|
293
|
+
#b:hover { background: #3a5ce0; }
|
|
294
|
+
#b:disabled { opacity: 0.6; cursor: default; }
|
|
295
|
+
.error { margin-top: 12px; font-size: 13px; color: #ff7b7b; text-align: center; }
|
|
296
|
+
</style>
|
|
297
|
+
</head>
|
|
298
|
+
<body>
|
|
299
|
+
<div class="card">
|
|
300
|
+
<h1>${title}</h1>
|
|
301
|
+
<form id="f" autocomplete="off">
|
|
302
|
+
<div class="field">
|
|
303
|
+
<input id="p" type="password" placeholder="${placeholder}" autofocus autocomplete="current-password">
|
|
304
|
+
<button id="tg" type="button" class="toggle" aria-label="${toggleLabel}" title="${toggleLabel}" aria-pressed="false">
|
|
305
|
+
<svg id="eyeOn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
306
|
+
<svg id="eyeOff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><path d="M3 3l18 18"/><path d="M10.6 10.6a3 3 0 0 0 4.2 4.2"/><path d="M9.9 4.2A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a13.3 13.3 0 0 1-2.2 2.9"/><path d="M6.6 6.6A13.5 13.5 0 0 0 2 12s3.5 7 10 7a10.8 10.8 0 0 0 3.3-.5"/></svg>
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
<button id="b" type="submit">${btn}</button>
|
|
310
|
+
<div class="error" id="e"${errStyle}>${errMsg}</div>
|
|
311
|
+
</form>
|
|
312
|
+
</div>
|
|
313
|
+
<script>
|
|
314
|
+
(function () {
|
|
315
|
+
var f = document.getElementById('f'), p = document.getElementById('p'),
|
|
316
|
+
b = document.getElementById('b'), e = document.getElementById('e'),
|
|
317
|
+
tg = document.getElementById('tg'),
|
|
318
|
+
eOn = document.getElementById('eyeOn'), eOff = document.getElementById('eyeOff');
|
|
319
|
+
tg.addEventListener('click', function () {
|
|
320
|
+
var reveal = p.type === 'password';
|
|
321
|
+
p.type = reveal ? 'text' : 'password';
|
|
322
|
+
eOn.style.display = reveal ? 'none' : '';
|
|
323
|
+
eOff.style.display = reveal ? '' : 'none';
|
|
324
|
+
tg.setAttribute('aria-pressed', reveal ? 'true' : 'false');
|
|
325
|
+
p.focus();
|
|
326
|
+
});
|
|
327
|
+
f.addEventListener('submit', function (ev) {
|
|
328
|
+
ev.preventDefault();
|
|
329
|
+
b.disabled = true; e.style.display = 'none';
|
|
330
|
+
fetch('/api/auth/login', {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
333
|
+
body: JSON.stringify({ password: p.value })
|
|
334
|
+
}).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok, j: j }; }); })
|
|
335
|
+
.then(function (res) {
|
|
336
|
+
if (res.ok && res.j && res.j.ok) { location.reload(); }
|
|
337
|
+
else { e.style.display = ''; b.disabled = false; p.select(); }
|
|
338
|
+
})
|
|
339
|
+
.catch(function () { e.style.display = ''; b.disabled = false; });
|
|
340
|
+
});
|
|
341
|
+
})();
|
|
342
|
+
</script>
|
|
343
|
+
</body>
|
|
344
|
+
</html>`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Re-export for callers that resolve the request's preferred locale. */
|
|
348
|
+
export { localeFromAcceptLanguage };
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// - server/lib/voice-pack-manager.js (EVENT_KEYS for whitelist + reconcile)
|
|
6
6
|
// - server/server.js (preferences merge / reconcile)
|
|
7
7
|
// - src/AppBase.jsx (initial state default)
|
|
8
|
-
// - src/components/VoicePackSettings.jsx (UI rows + reset handler)
|
|
8
|
+
// - src/components/settings/VoicePackSettings.jsx (UI rows + reset handler)
|
|
9
9
|
// - scripts/gen-placeholder-voicepack.js (pattern table keys)
|
|
10
|
-
// - src/components/AskTimeoutCountdown.jsx (threshold list keys)
|
|
10
|
+
// - src/components/chat/AskTimeoutCountdown.jsx (threshold list keys)
|
|
11
11
|
// Adding a 6th event meant editing 5+ files and any miss silently dropped audio
|
|
12
12
|
//(). All consumers now import from here.
|
|
13
13
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Lightweight in-house HTTP route dispatcher (no framework).
|
|
2
|
+
//
|
|
3
|
+
// Replaces the giant `if (url === ... && method === ...)` chain that used to live
|
|
4
|
+
// inside server.js's handleRequest. Each domain module under server/routes/ exports
|
|
5
|
+
// an ordered array of route descriptors; server.js concatenates them IN THE SAME
|
|
6
|
+
// ORDER as the original if-chain and hands the array here. dispatch() walks the list
|
|
7
|
+
// top-to-bottom and invokes the first matching handler — byte-for-byte the same
|
|
8
|
+
// matching semantics as the old chain (order is load-bearing for prefix-vs-exact and
|
|
9
|
+
// method-distinguished duplicates like GET vs POST /api/file-content).
|
|
10
|
+
//
|
|
11
|
+
// Descriptor shape (one of):
|
|
12
|
+
// { method, match: 'exact'|'prefix', path, handler }
|
|
13
|
+
// { predicate: (url, method) => boolean, handler } // for compound conditions
|
|
14
|
+
//
|
|
15
|
+
// Handler signature: (req, res, parsedUrl, isLocal, deps) => void | Promise<void>
|
|
16
|
+
// `deps` is the singleton dependency bag built once in server.js (reassignable state
|
|
17
|
+
// via getters, shared Maps by reference, helpers, constants).
|
|
18
|
+
|
|
19
|
+
function matches(route, url, method) {
|
|
20
|
+
if (route.predicate) return route.predicate(url, method);
|
|
21
|
+
if (route.method !== method) return false;
|
|
22
|
+
return route.match === 'prefix' ? url.startsWith(route.path) : url === route.path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a dispatcher over an ordered list of route descriptors.
|
|
27
|
+
* Returns an async function(req, res, parsedUrl, isLocal, deps) => boolean,
|
|
28
|
+
* resolving true if a route handled the request, false if none matched
|
|
29
|
+
* (caller then falls through to the static-file / 404 path).
|
|
30
|
+
*/
|
|
31
|
+
export function createDispatcher(routes) {
|
|
32
|
+
return async function dispatch(req, res, parsedUrl, isLocal, deps) {
|
|
33
|
+
const url = parsedUrl.pathname;
|
|
34
|
+
const method = req.method;
|
|
35
|
+
for (const route of routes) {
|
|
36
|
+
if (matches(route, url, method)) {
|
|
37
|
+
await route.handler(req, res, parsedUrl, isLocal, deps);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
}
|