@tom2012/cc-web 2026.4.19-s → 2026.4.19-t
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 +1 -1
- package/backend/dist/adapters/claude-adapter.d.ts +1 -1
- package/backend/dist/adapters/claude-adapter.d.ts.map +1 -1
- package/backend/dist/adapters/claude-adapter.js +62 -10
- package/backend/dist/adapters/claude-adapter.js.map +1 -1
- package/backend/dist/adapters/types.d.ts +1 -1
- package/backend/dist/adapters/types.d.ts.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +4 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/claude.d.ts.map +1 -1
- package/backend/dist/routes/claude.js +23 -1
- package/backend/dist/routes/claude.js.map +1 -1
- package/backend/dist/routes/projects.d.ts.map +1 -1
- package/backend/dist/routes/projects.js +26 -0
- package/backend/dist/routes/projects.js.map +1 -1
- package/backend/dist/routes/sync.d.ts +3 -0
- package/backend/dist/routes/sync.d.ts.map +1 -0
- package/backend/dist/routes/sync.js +183 -0
- package/backend/dist/routes/sync.js.map +1 -0
- package/backend/dist/sync-config.d.ts +62 -0
- package/backend/dist/sync-config.d.ts.map +1 -0
- package/backend/dist/sync-config.js +207 -0
- package/backend/dist/sync-config.js.map +1 -0
- package/backend/dist/sync-scheduler.d.ts +7 -0
- package/backend/dist/sync-scheduler.d.ts.map +1 -0
- package/backend/dist/sync-scheduler.js +157 -0
- package/backend/dist/sync-scheduler.js.map +1 -0
- package/backend/dist/sync-service.d.ts +51 -0
- package/backend/dist/sync-service.d.ts.map +1 -0
- package/backend/dist/sync-service.js +344 -0
- package/backend/dist/sync-service.js.map +1 -0
- package/backend/dist/user-prefs.d.ts +3 -0
- package/backend/dist/user-prefs.d.ts.map +1 -0
- package/backend/dist/user-prefs.js +87 -0
- package/backend/dist/user-prefs.js.map +1 -0
- package/frontend/dist/assets/AssistantMessageContent-D1-3VpWE.js +13 -0
- package/frontend/dist/assets/{GraphPreview-DqnUioLI.js → GraphPreview-CeI4sbtV.js} +1 -1
- package/frontend/dist/assets/MobilePage-BP7c3W3D.js +14 -0
- package/frontend/dist/assets/{OfficePreview-DsfkiPFS.js → OfficePreview-Dgs4aiYi.js} +2 -2
- package/frontend/dist/assets/{ProjectPage-eUYt7NVa.js → ProjectPage-DPv57nD9.js} +5 -6
- package/frontend/dist/assets/SettingsPage-CFSmKovg.js +13 -0
- package/frontend/dist/assets/{SkillHubPage-CRFSy-Cg.js → SkillHubPage-4g29B-Hr.js} +2 -2
- package/frontend/dist/assets/{chevron-down-9-G7S6iK.js → chevron-down-CyDTNuVw.js} +1 -1
- package/frontend/dist/assets/{chevron-up-BnktQ0Ve.js → chevron-up-BURz786o.js} +1 -1
- package/frontend/dist/assets/{index-Dn-YLcfD.js → index-BFWZX3Hz.js} +7 -7
- package/frontend/dist/assets/index-D0NB3R9q.css +1 -0
- package/frontend/dist/assets/index-Uy-V98k3.js +13 -0
- package/frontend/dist/assets/{index-B7AIJuGG.js → index-z4qRGojt.js} +1 -1
- package/frontend/dist/assets/{jszip.min-wfpG-66r.js → jszip.min-CPG_u-b-.js} +1 -1
- package/frontend/dist/assets/{search-Dq8YTmIg.js → search-CZ83atP-.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/AssistantMessageContent-DayURBLZ.js +0 -13
- package/frontend/dist/assets/MobilePage-CIA-DkuB.js +0 -14
- package/frontend/dist/assets/SettingsPage-CCk3EySa.js +0 -13
- package/frontend/dist/assets/index-CboghLS5.js +0 -7
- package/frontend/dist/assets/index-pG308FQn.css +0 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const express_1 = require("express");
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const sync_config_1 = require("../sync-config");
|
|
6
|
+
const sync_scheduler_1 = require("../sync-scheduler");
|
|
7
|
+
const sync_service_1 = require("../sync-service");
|
|
8
|
+
const router = (0, express_1.Router)();
|
|
9
|
+
const VALID_DIRECTIONS = ['push', 'pull', 'bidirectional'];
|
|
10
|
+
const VALID_AUTH = ['key', 'password'];
|
|
11
|
+
function requireUser(req, res) {
|
|
12
|
+
const u = req.user?.username;
|
|
13
|
+
if (!u) {
|
|
14
|
+
res.status(401).json({ error: 'Unauthenticated' });
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return u;
|
|
18
|
+
}
|
|
19
|
+
// GET /api/sync/config → current user's config (password redacted)
|
|
20
|
+
router.get('/config', (req, res) => {
|
|
21
|
+
const user = requireUser(req, res);
|
|
22
|
+
if (!user)
|
|
23
|
+
return;
|
|
24
|
+
res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
|
|
25
|
+
});
|
|
26
|
+
// PUT /api/sync/config body: Partial<SyncConfig> with plain `password` for pw auth
|
|
27
|
+
router.put('/config', (req, res) => {
|
|
28
|
+
const user = requireUser(req, res);
|
|
29
|
+
if (!user)
|
|
30
|
+
return;
|
|
31
|
+
const body = (req.body ?? {});
|
|
32
|
+
const existing = (0, sync_config_1.getSyncConfig)(user);
|
|
33
|
+
const next = { ...existing };
|
|
34
|
+
if (typeof body.host === 'string')
|
|
35
|
+
next.host = body.host.trim();
|
|
36
|
+
if (typeof body.port === 'number' && body.port > 0 && body.port < 65536)
|
|
37
|
+
next.port = body.port;
|
|
38
|
+
if (typeof body.user === 'string')
|
|
39
|
+
next.user = body.user.trim();
|
|
40
|
+
if (body.authMethod && VALID_AUTH.includes(body.authMethod))
|
|
41
|
+
next.authMethod = body.authMethod;
|
|
42
|
+
if (typeof body.keyPath === 'string') {
|
|
43
|
+
const candidate = body.keyPath.trim();
|
|
44
|
+
if (candidate && !(0, sync_config_1.isValidKeyPath)(candidate)) {
|
|
45
|
+
res.status(400).json({ error: 'keyPath 不能包含空格、引号、反斜杠、null 字节,或以 `-` 开头(防止 ssh 参数注入)' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
next.keyPath = candidate || undefined;
|
|
49
|
+
}
|
|
50
|
+
if (typeof body.remoteRoot === 'string') {
|
|
51
|
+
const rr = body.remoteRoot.trim().replace(/\/+$/, '');
|
|
52
|
+
// Remote root should be an absolute path; relative would be interpreted
|
|
53
|
+
// relative to the ssh user's home, which is easy to misconfigure.
|
|
54
|
+
if (rr && !rr.startsWith('/')) {
|
|
55
|
+
res.status(400).json({ error: 'remoteRoot 必须是绝对路径(/ 开头)' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
next.remoteRoot = rr;
|
|
59
|
+
}
|
|
60
|
+
if (body.direction && VALID_DIRECTIONS.includes(body.direction))
|
|
61
|
+
next.direction = body.direction;
|
|
62
|
+
if (Array.isArray(body.defaultExcludes)) {
|
|
63
|
+
next.defaultExcludes = body.defaultExcludes
|
|
64
|
+
.filter((v) => typeof v === 'string')
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.slice(0, 200); // prevent DoS via 10K-item excludes
|
|
68
|
+
}
|
|
69
|
+
if (body.schedule && typeof body.schedule === 'object') {
|
|
70
|
+
const sch = body.schedule;
|
|
71
|
+
const cron = typeof sch.cron === 'string' && sch.cron.trim() ? sch.cron.trim() : existing.schedule.cron;
|
|
72
|
+
// Validate cron now so silent "enabled but never fires" doesn't happen.
|
|
73
|
+
const cronErr = (0, sync_scheduler_1.validateCron)(cron);
|
|
74
|
+
if (cronErr) {
|
|
75
|
+
res.status(400).json({ error: `cron 表达式无效: ${cronErr}` });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
next.schedule = { enabled: !!sch.enabled, cron };
|
|
79
|
+
}
|
|
80
|
+
if (body.projectExcludes && typeof body.projectExcludes === 'object') {
|
|
81
|
+
const pe = {};
|
|
82
|
+
for (const [k, v] of Object.entries(body.projectExcludes)) {
|
|
83
|
+
if (Array.isArray(v)) {
|
|
84
|
+
pe[k] = v
|
|
85
|
+
.filter((x) => typeof x === 'string')
|
|
86
|
+
.map((s) => s.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.slice(0, 200);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
next.projectExcludes = pe;
|
|
92
|
+
}
|
|
93
|
+
// Password: only update if explicitly provided
|
|
94
|
+
if (typeof body.password === 'string') {
|
|
95
|
+
if (body.password === '') {
|
|
96
|
+
next.passwordEnc = undefined;
|
|
97
|
+
next.passwordFp = undefined;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const { enc, fp } = (0, sync_config_1.encryptPassword)(body.password);
|
|
101
|
+
next.passwordEnc = enc;
|
|
102
|
+
next.passwordFp = fp;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
(0, sync_config_1.setSyncConfig)(next);
|
|
106
|
+
res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
|
|
107
|
+
});
|
|
108
|
+
// POST /api/sync/reset — revert to defaults (keeps nothing)
|
|
109
|
+
router.post('/reset', (req, res) => {
|
|
110
|
+
const user = requireUser(req, res);
|
|
111
|
+
if (!user)
|
|
112
|
+
return;
|
|
113
|
+
(0, sync_config_1.setSyncConfig)({ username: user, ...sync_config_1.DEFAULT_CONFIG });
|
|
114
|
+
res.json((0, sync_config_1.publicConfig)((0, sync_config_1.getSyncConfig)(user)));
|
|
115
|
+
});
|
|
116
|
+
// POST /api/sync/test — ssh <host> true
|
|
117
|
+
router.post('/test', async (req, res) => {
|
|
118
|
+
const user = requireUser(req, res);
|
|
119
|
+
if (!user)
|
|
120
|
+
return;
|
|
121
|
+
const result = await (0, sync_service_1.testConnection)(user);
|
|
122
|
+
res.json(result);
|
|
123
|
+
});
|
|
124
|
+
// POST /api/sync/project/:id — sync one project using configured direction
|
|
125
|
+
// Optional body: { direction: 'push'|'pull' } to override for this call.
|
|
126
|
+
router.post('/project/:id', async (req, res) => {
|
|
127
|
+
const user = requireUser(req, res);
|
|
128
|
+
if (!user)
|
|
129
|
+
return;
|
|
130
|
+
const project = (0, config_1.getProject)(req.params.id);
|
|
131
|
+
if (!project) {
|
|
132
|
+
res.status(404).json({ error: 'Project not found' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!(0, config_1.isProjectOwner)(project, user)) {
|
|
136
|
+
res.status(403).json({ error: 'Forbidden' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const body = (req.body ?? {});
|
|
140
|
+
const override = body.direction && VALID_DIRECTIONS.includes(body.direction)
|
|
141
|
+
? body.direction
|
|
142
|
+
: undefined;
|
|
143
|
+
const result = await (0, sync_service_1.syncProject)(user, project.id, project.name, project.folderPath, override);
|
|
144
|
+
res.json(result);
|
|
145
|
+
});
|
|
146
|
+
// POST /api/sync/all — sync every owned, non-archived project in sequence.
|
|
147
|
+
// Sequential (not parallel) because rsync is bandwidth-bound; parallel
|
|
148
|
+
// streams just contend for the same pipe.
|
|
149
|
+
router.post('/all', async (req, res) => {
|
|
150
|
+
const user = requireUser(req, res);
|
|
151
|
+
if (!user)
|
|
152
|
+
return;
|
|
153
|
+
const projects = (0, config_1.getProjects)().filter((p) => !p.archived && (0, config_1.isProjectOwner)(p, user));
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const p of projects) {
|
|
156
|
+
const r = await (0, sync_service_1.syncProject)(user, p.id, p.name, p.folderPath);
|
|
157
|
+
results.push({
|
|
158
|
+
projectId: p.id,
|
|
159
|
+
name: p.name,
|
|
160
|
+
ok: r.ok,
|
|
161
|
+
skipped: r.skipped,
|
|
162
|
+
reason: r.reason,
|
|
163
|
+
bytes: r.bytes,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
res.json({ total: projects.length, results });
|
|
167
|
+
});
|
|
168
|
+
// GET /api/sync/status → { inFlight: string[] }
|
|
169
|
+
router.get('/status', (req, res) => {
|
|
170
|
+
const user = requireUser(req, res);
|
|
171
|
+
if (!user)
|
|
172
|
+
return;
|
|
173
|
+
res.json({ inFlight: (0, sync_service_1.listInFlight)(user) });
|
|
174
|
+
});
|
|
175
|
+
// GET /api/sync/status/:id → boolean for a single project
|
|
176
|
+
router.get('/status/:id', (req, res) => {
|
|
177
|
+
const user = requireUser(req, res);
|
|
178
|
+
if (!user)
|
|
179
|
+
return;
|
|
180
|
+
res.json({ syncing: (0, sync_service_1.isSyncing)(user, req.params.id) });
|
|
181
|
+
});
|
|
182
|
+
exports.default = router;
|
|
183
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user rsync sync configuration.
|
|
3
|
+
*
|
|
4
|
+
* Layout: one file per user at `<DATA_DIR>/sync-config/<sha1(username)>.json`.
|
|
5
|
+
* The filename is a hash of the real username so usernames containing `.`,
|
|
6
|
+
* spaces, or any non-[A-Za-z0-9_-] character don't round-trip lossily (the
|
|
7
|
+
* earlier simple-sanitize approach collided `tom.admin` with `tom_admin` and
|
|
8
|
+
* broke scheduler lookup). The real username is stored inside the JSON as
|
|
9
|
+
* `username` so `listUsersWithSyncConfig()` can return the actual strings.
|
|
10
|
+
*
|
|
11
|
+
* Passwords are encrypted with AES-256-GCM using a key derived from the
|
|
12
|
+
* server's JWT secret. A 4-byte fingerprint of the derived key is stored
|
|
13
|
+
* alongside `passwordEnc` so a jwtSecret rotation (e.g. user re-runs setup,
|
|
14
|
+
* config.json regenerated) is detected at read time and the public config
|
|
15
|
+
* exposes `passwordNeedsReset: true` instead of silently returning the
|
|
16
|
+
* wedged ciphertext.
|
|
17
|
+
*/
|
|
18
|
+
export type SyncDirection = 'push' | 'pull' | 'bidirectional';
|
|
19
|
+
export type AuthMethod = 'key' | 'password';
|
|
20
|
+
export interface SyncConfig {
|
|
21
|
+
username: string;
|
|
22
|
+
host: string;
|
|
23
|
+
port: number;
|
|
24
|
+
user: string;
|
|
25
|
+
authMethod: AuthMethod;
|
|
26
|
+
keyPath?: string;
|
|
27
|
+
passwordEnc?: string;
|
|
28
|
+
passwordFp?: string;
|
|
29
|
+
remoteRoot: string;
|
|
30
|
+
direction: SyncDirection;
|
|
31
|
+
defaultExcludes: string[];
|
|
32
|
+
schedule: {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
cron: string;
|
|
35
|
+
};
|
|
36
|
+
projectExcludes: Record<string, string[]>;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_CONFIG: Omit<SyncConfig, 'username'>;
|
|
39
|
+
export declare function encryptPassword(plain: string): {
|
|
40
|
+
enc: string;
|
|
41
|
+
fp: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function decryptPassword(blob: string, expectedFp?: string): string;
|
|
44
|
+
export declare function isValidKeyPath(p: string | undefined | null): boolean;
|
|
45
|
+
/** A project folder name used on the remote must not contain path separators
|
|
46
|
+
* or start with `.`/`-` so `rsync ... remote:root/<name>/` stays inside
|
|
47
|
+
* `remoteRoot`. `path.posix.join` does not protect against `..`. */
|
|
48
|
+
export declare function sanitizeFolderName(raw: string): string | null;
|
|
49
|
+
export declare function getSyncConfig(username: string): SyncConfig;
|
|
50
|
+
export declare function setSyncConfig(cfg: SyncConfig): void;
|
|
51
|
+
export declare function listUsersWithSyncConfig(): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Client-safe projection. Never leaks the password ciphertext. Sets
|
|
54
|
+
* `passwordNeedsReset` when the stored ciphertext was encrypted with a key
|
|
55
|
+
* the server no longer holds (jwtSecret rotated) — the UI should prompt for
|
|
56
|
+
* re-entry instead of silently rendering `passwordSet: true`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function publicConfig(cfg: SyncConfig): Omit<SyncConfig, 'passwordEnc' | 'passwordFp'> & {
|
|
59
|
+
passwordSet: boolean;
|
|
60
|
+
passwordNeedsReset: boolean;
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=sync-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-config.d.ts","sourceRoot":"","sources":["../src/sync-config.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,eAAe,CAAC;AAC9D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,UAAU,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,aAAa,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC3C;AAeD,eAAO,MAAM,cAAc,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAUvD,CAAC;AA0BF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAU1E;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAezE;AASD,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAGpE;AAED;;sEAEsE;AACtE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO7D;AAID,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAc1D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAInD;AAED,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,CAgBlD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,GAAG,YAAY,CAAC,GAAG;IAC9F,WAAW,EAAE,OAAO,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAKA"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
37
|
+
exports.encryptPassword = encryptPassword;
|
|
38
|
+
exports.decryptPassword = decryptPassword;
|
|
39
|
+
exports.isValidKeyPath = isValidKeyPath;
|
|
40
|
+
exports.sanitizeFolderName = sanitizeFolderName;
|
|
41
|
+
exports.getSyncConfig = getSyncConfig;
|
|
42
|
+
exports.setSyncConfig = setSyncConfig;
|
|
43
|
+
exports.listUsersWithSyncConfig = listUsersWithSyncConfig;
|
|
44
|
+
exports.publicConfig = publicConfig;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
const config_1 = require("./config");
|
|
49
|
+
const DEFAULT_EXCLUDES = [
|
|
50
|
+
'.git/', // full .git (prev only excluded objects/, which left a broken repo)
|
|
51
|
+
'node_modules/',
|
|
52
|
+
'dist/',
|
|
53
|
+
'build/',
|
|
54
|
+
'.next/',
|
|
55
|
+
'.venv/',
|
|
56
|
+
'__pycache__/',
|
|
57
|
+
'.DS_Store',
|
|
58
|
+
'*.log',
|
|
59
|
+
'*.tmp',
|
|
60
|
+
];
|
|
61
|
+
exports.DEFAULT_CONFIG = {
|
|
62
|
+
host: '',
|
|
63
|
+
port: 22,
|
|
64
|
+
user: '',
|
|
65
|
+
authMethod: 'key',
|
|
66
|
+
remoteRoot: '',
|
|
67
|
+
direction: 'push',
|
|
68
|
+
defaultExcludes: DEFAULT_EXCLUDES,
|
|
69
|
+
schedule: { enabled: false, cron: '0 3 * * *' },
|
|
70
|
+
projectExcludes: {},
|
|
71
|
+
};
|
|
72
|
+
const SYNC_DIR = path.join(config_1.DATA_DIR, 'sync-config');
|
|
73
|
+
function ensureDir() {
|
|
74
|
+
if (!fs.existsSync(SYNC_DIR))
|
|
75
|
+
fs.mkdirSync(SYNC_DIR, { recursive: true, mode: 0o700 });
|
|
76
|
+
}
|
|
77
|
+
function userFile(username) {
|
|
78
|
+
// Hash-based filenames: usernames may contain any unicode. sha1 is fine
|
|
79
|
+
// here because this is a lookup key, not a security token.
|
|
80
|
+
const hash = crypto.createHash('sha1').update(`ccweb-sync-user:${username}`).digest('hex');
|
|
81
|
+
return path.join(SYNC_DIR, `${hash}.json`);
|
|
82
|
+
}
|
|
83
|
+
// ── Crypto ───────────────────────────────────────────────────────────────────
|
|
84
|
+
function getKey() {
|
|
85
|
+
const secret = (0, config_1.getConfig)().jwtSecret;
|
|
86
|
+
return crypto.createHash('sha256').update(`ccweb-sync:${secret}`).digest();
|
|
87
|
+
}
|
|
88
|
+
function keyFingerprint() {
|
|
89
|
+
return getKey().subarray(0, 4).toString('hex');
|
|
90
|
+
}
|
|
91
|
+
function encryptPassword(plain) {
|
|
92
|
+
if (!plain)
|
|
93
|
+
return { enc: '', fp: '' };
|
|
94
|
+
const iv = crypto.randomBytes(12);
|
|
95
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', getKey(), iv);
|
|
96
|
+
const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
|
|
97
|
+
const tag = cipher.getAuthTag();
|
|
98
|
+
return {
|
|
99
|
+
enc: Buffer.concat([iv, tag, enc]).toString('base64'),
|
|
100
|
+
fp: keyFingerprint(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function decryptPassword(blob, expectedFp) {
|
|
104
|
+
if (!blob)
|
|
105
|
+
return '';
|
|
106
|
+
if (expectedFp && expectedFp !== keyFingerprint())
|
|
107
|
+
return ''; // key rotated — refuse
|
|
108
|
+
try {
|
|
109
|
+
const buf = Buffer.from(blob, 'base64');
|
|
110
|
+
if (buf.length < 28)
|
|
111
|
+
return '';
|
|
112
|
+
const iv = buf.subarray(0, 12);
|
|
113
|
+
const tag = buf.subarray(12, 28);
|
|
114
|
+
const enc = buf.subarray(28);
|
|
115
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', getKey(), iv);
|
|
116
|
+
decipher.setAuthTag(tag);
|
|
117
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── Validation ───────────────────────────────────────────────────────────────
|
|
124
|
+
/** Reject paths that would let a user inject ssh options through rsync's
|
|
125
|
+
* `-e` string tokenization or ssh's own getopt (keyPath beginning with `-`
|
|
126
|
+
* is read as another option). */
|
|
127
|
+
const INVALID_KEYPATH = /[\s'"\\\x00]|^-/;
|
|
128
|
+
function isValidKeyPath(p) {
|
|
129
|
+
if (!p)
|
|
130
|
+
return true; // missing is OK (caller decides whether required)
|
|
131
|
+
return !INVALID_KEYPATH.test(p) && p.length < 512;
|
|
132
|
+
}
|
|
133
|
+
/** A project folder name used on the remote must not contain path separators
|
|
134
|
+
* or start with `.`/`-` so `rsync ... remote:root/<name>/` stays inside
|
|
135
|
+
* `remoteRoot`. `path.posix.join` does not protect against `..`. */
|
|
136
|
+
function sanitizeFolderName(raw) {
|
|
137
|
+
if (!raw)
|
|
138
|
+
return null;
|
|
139
|
+
if (raw.includes('/') || raw.includes('\\') || raw.includes('\0'))
|
|
140
|
+
return null;
|
|
141
|
+
if (raw === '.' || raw === '..')
|
|
142
|
+
return null;
|
|
143
|
+
if (raw.startsWith('.') || raw.startsWith('-'))
|
|
144
|
+
return null;
|
|
145
|
+
if (raw.length > 128)
|
|
146
|
+
return null;
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
// ── Read / Write ─────────────────────────────────────────────────────────────
|
|
150
|
+
function getSyncConfig(username) {
|
|
151
|
+
ensureDir();
|
|
152
|
+
const file = userFile(username);
|
|
153
|
+
if (!fs.existsSync(file))
|
|
154
|
+
return { username, ...exports.DEFAULT_CONFIG };
|
|
155
|
+
try {
|
|
156
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
157
|
+
const parsed = JSON.parse(raw);
|
|
158
|
+
// username comes from the argument (authoritative) — if the file's
|
|
159
|
+
// `username` field is missing / stale / tampered, the argument wins.
|
|
160
|
+
const { username: _ignored, ...rest } = parsed;
|
|
161
|
+
return { ...exports.DEFAULT_CONFIG, ...rest, username };
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return { username, ...exports.DEFAULT_CONFIG };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function setSyncConfig(cfg) {
|
|
168
|
+
ensureDir();
|
|
169
|
+
(0, config_1.atomicWriteSync)(userFile(cfg.username), JSON.stringify(cfg, null, 2));
|
|
170
|
+
try {
|
|
171
|
+
fs.chmodSync(userFile(cfg.username), 0o600);
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
function listUsersWithSyncConfig() {
|
|
176
|
+
ensureDir();
|
|
177
|
+
try {
|
|
178
|
+
const files = fs.readdirSync(SYNC_DIR).filter((f) => f.endsWith('.json'));
|
|
179
|
+
const users = [];
|
|
180
|
+
for (const f of files) {
|
|
181
|
+
try {
|
|
182
|
+
const raw = fs.readFileSync(path.join(SYNC_DIR, f), 'utf-8');
|
|
183
|
+
const obj = JSON.parse(raw);
|
|
184
|
+
if (typeof obj.username === 'string' && obj.username)
|
|
185
|
+
users.push(obj.username);
|
|
186
|
+
}
|
|
187
|
+
catch { /* skip corrupt entries */ }
|
|
188
|
+
}
|
|
189
|
+
return users;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Client-safe projection. Never leaks the password ciphertext. Sets
|
|
197
|
+
* `passwordNeedsReset` when the stored ciphertext was encrypted with a key
|
|
198
|
+
* the server no longer holds (jwtSecret rotated) — the UI should prompt for
|
|
199
|
+
* re-entry instead of silently rendering `passwordSet: true`.
|
|
200
|
+
*/
|
|
201
|
+
function publicConfig(cfg) {
|
|
202
|
+
const { passwordEnc, passwordFp, ...rest } = cfg;
|
|
203
|
+
const set = !!passwordEnc;
|
|
204
|
+
const wedged = set && !!passwordFp && passwordFp !== keyFingerprint();
|
|
205
|
+
return { ...rest, passwordSet: set && !wedged, passwordNeedsReset: wedged };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=sync-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-config.js","sourceRoot":"","sources":["../src/sync-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Human-readable error or `null` if valid. Consumed by `routes/sync.ts` at
|
|
2
|
+
* save-time so invalid crons are rejected with a 400 instead of silently
|
|
3
|
+
* being "enabled but never fires". */
|
|
4
|
+
export declare function validateCron(expr: string): string | null;
|
|
5
|
+
export declare function startSyncScheduler(): void;
|
|
6
|
+
export declare function stopSyncScheduler(): void;
|
|
7
|
+
//# sourceMappingURL=sync-scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-scheduler.d.ts","sourceRoot":"","sources":["../src/sync-scheduler.ts"],"names":[],"mappings":"AAkFA;;uCAEuC;AACvC,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOxD;AAkDD,wBAAgB,kBAAkB,IAAI,IAAI,CAWzC;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateCron = validateCron;
|
|
4
|
+
exports.startSyncScheduler = startSyncScheduler;
|
|
5
|
+
exports.stopSyncScheduler = stopSyncScheduler;
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const sync_config_1 = require("./sync-config");
|
|
8
|
+
const sync_service_1 = require("./sync-service");
|
|
9
|
+
/** Return parsed cron or `null` if invalid. Refuses empty-Set fields (e.g.
|
|
10
|
+
* `9-5` with A>B) and invalid step (`*\/0`) rather than silently accepting
|
|
11
|
+
* them as "never fires" / "every minute". */
|
|
12
|
+
function parseField(raw, min, max) {
|
|
13
|
+
const out = new Set();
|
|
14
|
+
for (const segment of raw.split(',')) {
|
|
15
|
+
const part = segment.trim();
|
|
16
|
+
if (!part)
|
|
17
|
+
return null;
|
|
18
|
+
let step = 1;
|
|
19
|
+
let rangePart = part;
|
|
20
|
+
const slashIdx = part.indexOf('/');
|
|
21
|
+
if (slashIdx >= 0) {
|
|
22
|
+
const stepRaw = part.slice(slashIdx + 1);
|
|
23
|
+
const parsed = parseInt(stepRaw, 10);
|
|
24
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
25
|
+
return null;
|
|
26
|
+
step = parsed;
|
|
27
|
+
rangePart = part.slice(0, slashIdx);
|
|
28
|
+
}
|
|
29
|
+
let start = min;
|
|
30
|
+
let end = max;
|
|
31
|
+
if (rangePart === '*') {
|
|
32
|
+
/* full range */
|
|
33
|
+
}
|
|
34
|
+
else if (rangePart.includes('-')) {
|
|
35
|
+
const [aStr, bStr] = rangePart.split('-');
|
|
36
|
+
const a = parseInt(aStr, 10);
|
|
37
|
+
const b = parseInt(bStr, 10);
|
|
38
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
39
|
+
return null;
|
|
40
|
+
if (a > b)
|
|
41
|
+
return null; // A>B would yield empty Set
|
|
42
|
+
if (a < min || b > max)
|
|
43
|
+
return null; // out-of-range
|
|
44
|
+
start = a;
|
|
45
|
+
end = b;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const n = parseInt(rangePart, 10);
|
|
49
|
+
if (!Number.isFinite(n) || n < min || n > max)
|
|
50
|
+
return null;
|
|
51
|
+
start = end = n;
|
|
52
|
+
}
|
|
53
|
+
for (let v = start; v <= end; v += step) {
|
|
54
|
+
if (v >= min && v <= max)
|
|
55
|
+
out.add(v);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (out.size === 0)
|
|
59
|
+
return null;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function parseCron(expr) {
|
|
63
|
+
const parts = expr.trim().split(/\s+/);
|
|
64
|
+
if (parts.length !== 5)
|
|
65
|
+
return null;
|
|
66
|
+
const [m, h, dom, mo, dow] = parts;
|
|
67
|
+
const minute = parseField(m, 0, 59);
|
|
68
|
+
const hour = parseField(h, 0, 23);
|
|
69
|
+
const domS = parseField(dom, 1, 31);
|
|
70
|
+
const month = parseField(mo, 1, 12);
|
|
71
|
+
const dowS = parseField(dow, 0, 6);
|
|
72
|
+
if (!minute || !hour || !domS || !month || !dowS)
|
|
73
|
+
return null;
|
|
74
|
+
return { minute, hour, dom: domS, month, dow: dowS };
|
|
75
|
+
}
|
|
76
|
+
/** Human-readable error or `null` if valid. Consumed by `routes/sync.ts` at
|
|
77
|
+
* save-time so invalid crons are rejected with a 400 instead of silently
|
|
78
|
+
* being "enabled but never fires". */
|
|
79
|
+
function validateCron(expr) {
|
|
80
|
+
if (!expr || !expr.trim())
|
|
81
|
+
return '空表达式';
|
|
82
|
+
const parts = expr.trim().split(/\s+/);
|
|
83
|
+
if (parts.length !== 5)
|
|
84
|
+
return `必须是 5 段(分 时 日 月 周),当前 ${parts.length} 段`;
|
|
85
|
+
const parsed = parseCron(expr);
|
|
86
|
+
if (!parsed)
|
|
87
|
+
return '包含无效字段、空范围(如 9-5)、或步长为 0';
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function matches(cron, now) {
|
|
91
|
+
return (cron.minute.has(now.getMinutes()) &&
|
|
92
|
+
cron.hour.has(now.getHours()) &&
|
|
93
|
+
cron.dom.has(now.getDate()) &&
|
|
94
|
+
cron.month.has(now.getMonth() + 1) &&
|
|
95
|
+
cron.dow.has(now.getDay()));
|
|
96
|
+
}
|
|
97
|
+
// ── Scheduler state ─────────────────────────────────────────────────────────
|
|
98
|
+
let interval = null;
|
|
99
|
+
let timeoutHandle = null;
|
|
100
|
+
/** Keyed by username — the last `YYYYMMDDHHMM` key at which we fired. Guards
|
|
101
|
+
* against DST fall-back double-fires (same minute + hour twice in one day)
|
|
102
|
+
* and any scheduler-tick drift. */
|
|
103
|
+
const lastRunKey = new Map();
|
|
104
|
+
function minuteKey(d) {
|
|
105
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
106
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
107
|
+
}
|
|
108
|
+
async function runScheduledOnce(now) {
|
|
109
|
+
const currentKey = minuteKey(now);
|
|
110
|
+
const users = (0, sync_config_1.listUsersWithSyncConfig)();
|
|
111
|
+
for (const username of users) {
|
|
112
|
+
const cfg = (0, sync_config_1.getSyncConfig)(username);
|
|
113
|
+
if (!cfg.schedule.enabled || !cfg.schedule.cron)
|
|
114
|
+
continue;
|
|
115
|
+
const parsed = parseCron(cfg.schedule.cron);
|
|
116
|
+
if (!parsed || !matches(parsed, now))
|
|
117
|
+
continue;
|
|
118
|
+
// De-dupe per-minute (DST + tick jitter)
|
|
119
|
+
if (lastRunKey.get(username) === currentKey)
|
|
120
|
+
continue;
|
|
121
|
+
lastRunKey.set(username, currentKey);
|
|
122
|
+
const projects = (0, config_1.getProjects)().filter((p) => !p.archived && (0, config_1.isProjectOwner)(p, username));
|
|
123
|
+
for (const p of projects) {
|
|
124
|
+
try {
|
|
125
|
+
await (0, sync_service_1.syncProject)(username, p.id, p.name, p.folderPath);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error(`[sync-scheduler] ${username}/${p.name} failed:`, err.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function startSyncScheduler() {
|
|
134
|
+
if (interval || timeoutHandle)
|
|
135
|
+
return;
|
|
136
|
+
// Align to top of minute: wait until seconds == 0, then fire every 60s.
|
|
137
|
+
const alignDelay = (60 - new Date().getSeconds()) * 1000;
|
|
138
|
+
timeoutHandle = setTimeout(() => {
|
|
139
|
+
timeoutHandle = null;
|
|
140
|
+
void runScheduledOnce(new Date()).catch(() => { });
|
|
141
|
+
interval = setInterval(() => {
|
|
142
|
+
void runScheduledOnce(new Date()).catch(() => { });
|
|
143
|
+
}, 60000);
|
|
144
|
+
}, alignDelay);
|
|
145
|
+
}
|
|
146
|
+
function stopSyncScheduler() {
|
|
147
|
+
if (timeoutHandle) {
|
|
148
|
+
clearTimeout(timeoutHandle);
|
|
149
|
+
timeoutHandle = null;
|
|
150
|
+
}
|
|
151
|
+
if (interval) {
|
|
152
|
+
clearInterval(interval);
|
|
153
|
+
interval = null;
|
|
154
|
+
}
|
|
155
|
+
lastRunKey.clear();
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=sync-scheduler.js.map
|