Files
CloudSSH/scripts/sync-theme-editor.js
T
TanXinandTanXin 52fec4e00f release: 发布 v1.12.0 分享审计保留与清理完善版本 (#103)
## 版本内容

### 分享审计保留与清理
- 审计记录保留与清理能力:终态会话由所有者整体清空(写入墓碑事件保留追责线索)或导出完整 JSON 归档;终态满默认 90 天由 alarm
自动清理
- 审计保留期创建时自定义(7–365 天,白名单校验,默认 90)
-
清理留痕集中展示:清理时间与方式(手动/自动)同步用户库(`audit_purged_at`/`audit_purge_type`),折叠面板展示;已清理分享等同删除效果,不再提供查看审计入口
- 手动清空后取消已排期的自动清理闹钟;保留期服务端校验统一为 7–365 天;修复闹钟设置失败导致"有排期无闹钟"的幽灵状态

### 工程与质量
- 新增审计清理回归测试 8 用例(墓碑隔离、归属/终态校验、同步失败容错、排期回滚、留痕接缝、迁移幂等)
- UserDBDO SQL 查询类型化封装(`query<T>`/`one<T>`),消除 22 处 `as unknown as`
- 测试类型检查纳入门禁:`tests/` 拆分
`tsconfig.worker`/`tsconfig.frontend`(workers-types 与 DOM lib 的 Element
接口冲突无法单配置共存)
- 弱随机数加固(会话名 `crypto.randomUUID`);pnpm
供应链策略(`blockExoticSubdeps`/`minimumReleaseAge` 7 天/`no-downgrade`);CI
权限收敛 `contents: read`;`.gitleaks.toml` 误报豁免
- pi-lens 项目策略口径(AGENTS.md #31):XSS 类规则刻意不做项目级禁用,仅行内逐处豁免

## 提交列表

- 1c38c7a `release: 发布 v1.12.0 分享审计保留与清理完善版本`
- 4dd7c63 `feat: 审计清理回归测试、UserDBDO 类型化查询与测试类型门禁`
- 50389f3 `fix: 消除 pi-lens 剩余告警——SAFETY 注释/测试类型/XSS 抑制/空 catch 说明`
- 23a3e8e `chore: 代码质量审查整改——弱随机数/测试类型检查/供应链加固等 7 项`
- 1cf0ef9 `chore: 配置 pi-lens 项目策略降低风格类噪音并清理用户库遗留项`
- 0f27a50 `fix: 审计清理后完全隐藏分享卡片,仅保留集中式清理记录`
- 7c42e8e `feat: 审计清理改为删除效果并新增集中式清理记录留痕区`
- a3847ee `fix: 审计清理后改用折叠面板展示清理记录并补齐中英文词条`
- b730219 `fix: 审计保留期服务端校验统一为 7–365 天并修复清理闹钟静默失败`
- a68a6c8 `feat: 分享审计保留期支持创建时自定义(7–365 天)`
- 158b205 `fix: 手动清空审计后取消已排期的自动清理唤醒`
- 618f63c `feat: 分享审计记录保留与清理能力(清空/导出/自动保留期)`

## 验证结果

- typecheck 全绿(src + tests.worker + tests.frontend + frontend 四段)
- 全量测试 644/644(54 文件)
- build:frontend 构建成功并内联
- 关联 Issue/PR:无(基于 v1.11.0 #102)

---------

Co-authored-by: TanXin <tanxin@example.com>
2026-08-26 12:35:58 +08:00

109 lines
4.0 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const rootDir = path.resolve(__dirname, '..');
const themeSourcePath = path.join(rootDir, 'frontend', 'src', 'theme.ts');
const editorPath = path.join(rootDir, 'docs', 'theme-editor', 'index.html');
const themeSource = fs.readFileSync(themeSourcePath, 'utf8');
let editorHtml = fs.readFileSync(editorPath, 'utf8');
const terminalThemes = readObjectLiteral(themeSource, 'export const THEMES =');
const uiThemes = readObjectLiteral(themeSource, 'export const UI_THEMES:');
const stylePresets = readObjectLiteral(themeSource, 'export const UI_STYLE_PRESETS:');
const builtInAppearances = readObjectLiteral(themeSource, 'export const BUILT_IN_APPEARANCE:');
const colorSchemes = readObjectLiteral(themeSource, 'const COLOR_SCHEMES:');
const labels = {
'standard-dark': 'Standard Dark',
'standard-light': 'Standard Light',
cyberpunk: 'Cyberpunk',
glacier: 'Glacier',
gruvbox: 'Gruvbox',
};
const presets = Object.fromEntries(
Object.keys(terminalThemes).map((themeName) => {
const appearance = builtInAppearances[themeName] || {};
const style = appearance.style || 'cyberpunk';
const stylePreset = stylePresets[style] || stylePresets.cyberpunk;
return [
themeName,
{
name: labels[themeName] || themeName,
colorScheme: colorSchemes[themeName],
appearance: {
...stylePreset,
...appearance,
components: {
...stylePreset.components,
...(appearance.components || {}),
},
},
terminal: terminalThemes[themeName],
ui: uiThemes[themeName],
},
];
}),
);
const generatedBlock = `const PRESETS = /* THEME_PRESETS_START */ ${JSON.stringify(presets, null, 2)} /* THEME_PRESETS_END */;`;
const markerStart = editorHtml.indexOf('/* THEME_PRESETS_START */');
const markerEnd = editorHtml.indexOf('/* THEME_PRESETS_END */');
if (markerStart >= 0 && markerEnd > markerStart) {
const declarationStart = editorHtml.lastIndexOf('const PRESETS =', markerStart);
const declarationEnd = editorHtml.indexOf(';', markerEnd);
editorHtml = `${editorHtml.slice(0, declarationStart)}${generatedBlock}${editorHtml.slice(declarationEnd + 1)}`;
} else {
const declarationStart = editorHtml.indexOf('const PRESETS =');
if (declarationStart < 0) throw new Error('Could not find PRESETS declaration in theme editor');
const objectStart = editorHtml.indexOf('{', declarationStart);
const objectEnd = findMatchingBrace(editorHtml, objectStart);
const declarationEnd = editorHtml.indexOf(';', objectEnd);
editorHtml = `${editorHtml.slice(0, declarationStart)}${generatedBlock}${editorHtml.slice(declarationEnd + 1)}`;
}
fs.writeFileSync(editorPath, editorHtml, 'utf8');
console.log('Theme editor presets synchronized from frontend/src/theme.ts');
function readObjectLiteral(source, marker) {
const markerIndex = source.indexOf(marker);
if (markerIndex < 0) throw new Error(`Could not find ${marker}`);
const objectStart = source.indexOf('{', markerIndex + marker.length);
const objectEnd = findMatchingBrace(source, objectStart);
const literal = source.slice(objectStart, objectEnd + 1);
// 仓库源码属于受信任输入;这里只执行抽取出的纯数据对象,供静态 Pages 使用。
// pi-lens-ignore: no-global-eval-js
return Function(`"use strict"; return (${literal});`)();
}
function findMatchingBrace(source, startIndex) {
let depth = 0;
let quote = null;
let escaped = false;
for (let index = startIndex; index < source.length; index++) {
const character = source[index];
if (quote) {
if (escaped) {
escaped = false;
} else if (character === '\\') {
escaped = true;
} else if (character === quote) {
quote = null;
}
continue;
}
if (character === '"' || character === "'" || character === '`') {
quote = character;
continue;
}
if (character === '{') depth++;
if (character === '}') {
depth--;
if (depth === 0) return index;
}
}
throw new Error('Unterminated object literal');
}