## 发布说明 本次发布将 CloudSSH 升级至 `v1.9.0`,新增默认关闭、可选启用的一次性 SSH 授权分享与分享会话专用审计。 ## 主要变更 - 使用仅含 256 位随机能力凭证的分享链接,持久层只保存哈希,链接只能领取一次。 - 支持独立的领取有效期、最长会话时间、所有者实时撤销和连接到期关闭。 - 接收者明确同意审计后进入受限会话,仅可使用终端与 SFTP。 - 分享会话禁用 AI Agent、自动重连、OS 检测、元数据更新、主机指纹替换及全链路 `keyboard-interactive`。 - 新增 `SSHShareDO`,记录会话生命周期、SFTP 请求/结果和终端 PTY 输出;审计失败或达到容量上限时关闭会话。 - 增加 `ENABLE_SSH_SHARING` 部署配置、中英文说明和完整安全回归。 ## 安全边界 - 原始分享凭证不会进入 SSH Durable Object 或持久层,领取后仅签发 60 秒有效的连接票据。 - 分享链要求目标及全部跳板节点已有可信路径级主机指纹,并始终启用严格主机密钥签名验证。 - PTY 输出审计不是目标机级强审计,文档已明确其覆盖范围和限制。 ## 验证 - Worker 与前端类型检查:通过 - 单元与集成测试:584 项通过 - Chromium/WebKit 浏览器回归:通过(1 项按测试条件跳过) - 前端生产构建:通过 - Wrangler 干运行:通过,正确识别 3 个 Durable Object 绑定 - `git diff --check`:通过 Refs #84 Issue #84 保持开启,用于继续收集分享功能的实际使用反馈和后续需求。
97 lines
3.8 KiB
JavaScript
97 lines
3.8 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const frontendDir = path.join(rootDir, 'frontend');
|
|
const distDir = path.join(frontendDir, 'dist');
|
|
const workerDir = path.join(rootDir, 'src', 'worker');
|
|
const htmlTsFile = path.join(workerDir, 'html.ts');
|
|
|
|
function inlineRequiredAsset(html, patterns, replacement, assetType) {
|
|
for (const pattern of patterns) {
|
|
if (pattern.test(html)) {
|
|
return html.replace(pattern, replacement);
|
|
}
|
|
}
|
|
throw new Error(`Unable to find the ${assetType} reference in the generated frontend HTML.`);
|
|
}
|
|
|
|
console.log('Building frontend...');
|
|
try {
|
|
execFileSync(process.execPath, ['scripts/sync-theme-editor.js'], { cwd: rootDir, stdio: 'inherit' });
|
|
// Dependencies are installed explicitly by developers/CI. Keeping installs out
|
|
// of the build makes production artifacts deterministic and offline-buildable.
|
|
const executableSuffix = process.platform === 'win32' ? '.cmd' : '';
|
|
execFileSync(path.join(frontendDir, 'node_modules', '.bin', `tsc${executableSuffix}`), [], {
|
|
cwd: frontendDir,
|
|
stdio: 'inherit',
|
|
});
|
|
execFileSync(path.join(frontendDir, 'node_modules', '.bin', `vite${executableSuffix}`), ['build'], {
|
|
cwd: frontendDir,
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
console.log('Inlining assets...');
|
|
// 2. Read dist/index.html
|
|
let html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf8');
|
|
|
|
// 3. Find assets in dist/assets
|
|
const assetsDir = path.join(distDir, 'assets');
|
|
const files = fs.readdirSync(assetsDir).sort();
|
|
const jsFiles = files.filter((file) => file.endsWith('.js'));
|
|
const cssFiles = files.filter((file) => file.endsWith('.css'));
|
|
|
|
if (jsFiles.length !== 1 || cssFiles.length !== 1) {
|
|
throw new Error(
|
|
`Expected exactly one JS and one CSS bundle for Worker inlining, got ${jsFiles.length} JS and ${cssFiles.length} CSS.`
|
|
);
|
|
}
|
|
|
|
const jsContent = fs.readFileSync(path.join(assetsDir, jsFiles[0]), 'utf8');
|
|
const cssContent = fs.readFileSync(path.join(assetsDir, cssFiles[0]), 'utf8');
|
|
|
|
// 4. Inline favicon.svg as base64 data URL
|
|
const faviconPath = path.join(frontendDir, 'public', 'favicon.svg');
|
|
if (fs.existsSync(faviconPath)) {
|
|
const faviconContent = fs.readFileSync(faviconPath, 'utf8');
|
|
const faviconBase64 = Buffer.from(faviconContent).toString('base64');
|
|
html = html.replace(
|
|
/href=["']\/favicon\.svg["']/i,
|
|
`href="data:image/svg+xml;base64,${faviconBase64}"`
|
|
);
|
|
}
|
|
|
|
// 5. Inline CSS (replace <link rel="stylesheet" ...>)
|
|
html = inlineRequiredAsset(html, [
|
|
/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/assets\/[^"']+["'][^>]*>/i,
|
|
/<link[^>]*rel=["']stylesheet["'][^>]*href=["']\/src\/[^"']+["'][^>]*>/i,
|
|
], () => `<style>${cssContent}</style>`, 'stylesheet');
|
|
|
|
// 6. Inline JS (replace <script type="module" src="/src/main.ts"></script> or similar)
|
|
html = inlineRequiredAsset(html, [
|
|
/<script[^>]*type=["']module["'][^>]*src=["']\/assets\/[^"']+["'][^>]*><\/script>/i,
|
|
/<script[^>]*type=["']module["'][^>]*src=["']\/src\/[^"']+["'][^>]*><\/script>/i,
|
|
], () => `<script type="module">${jsContent}</script>`, 'module script');
|
|
|
|
// 7. Write to src/worker/html.ts
|
|
const escapedHtml = html
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(/`/g, '\\`')
|
|
.replace(/\${/g, '\\${');
|
|
|
|
const htmlTsContent = `// Automatically generated by scripts/build-html.js. Do not edit directly.
|
|
export const HTML = \`${escapedHtml}\`;
|
|
`;
|
|
|
|
if (!fs.existsSync(workerDir)) {
|
|
fs.mkdirSync(workerDir, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(htmlTsFile, htmlTsContent, 'utf8');
|
|
console.log('Frontend built and inlined successfully to src/worker/html.ts!');
|
|
} catch (error) {
|
|
console.error('Failed to build and inline frontend:', error);
|
|
process.exit(1);
|
|
}
|