@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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/backend/dist/adapters/claude-adapter.d.ts +1 -1
  3. package/backend/dist/adapters/claude-adapter.d.ts.map +1 -1
  4. package/backend/dist/adapters/claude-adapter.js +62 -10
  5. package/backend/dist/adapters/claude-adapter.js.map +1 -1
  6. package/backend/dist/adapters/types.d.ts +1 -1
  7. package/backend/dist/adapters/types.d.ts.map +1 -1
  8. package/backend/dist/index.d.ts.map +1 -1
  9. package/backend/dist/index.js +4 -0
  10. package/backend/dist/index.js.map +1 -1
  11. package/backend/dist/routes/claude.d.ts.map +1 -1
  12. package/backend/dist/routes/claude.js +23 -1
  13. package/backend/dist/routes/claude.js.map +1 -1
  14. package/backend/dist/routes/projects.d.ts.map +1 -1
  15. package/backend/dist/routes/projects.js +26 -0
  16. package/backend/dist/routes/projects.js.map +1 -1
  17. package/backend/dist/routes/sync.d.ts +3 -0
  18. package/backend/dist/routes/sync.d.ts.map +1 -0
  19. package/backend/dist/routes/sync.js +183 -0
  20. package/backend/dist/routes/sync.js.map +1 -0
  21. package/backend/dist/sync-config.d.ts +62 -0
  22. package/backend/dist/sync-config.d.ts.map +1 -0
  23. package/backend/dist/sync-config.js +207 -0
  24. package/backend/dist/sync-config.js.map +1 -0
  25. package/backend/dist/sync-scheduler.d.ts +7 -0
  26. package/backend/dist/sync-scheduler.d.ts.map +1 -0
  27. package/backend/dist/sync-scheduler.js +157 -0
  28. package/backend/dist/sync-scheduler.js.map +1 -0
  29. package/backend/dist/sync-service.d.ts +51 -0
  30. package/backend/dist/sync-service.d.ts.map +1 -0
  31. package/backend/dist/sync-service.js +344 -0
  32. package/backend/dist/sync-service.js.map +1 -0
  33. package/backend/dist/user-prefs.d.ts +3 -0
  34. package/backend/dist/user-prefs.d.ts.map +1 -0
  35. package/backend/dist/user-prefs.js +87 -0
  36. package/backend/dist/user-prefs.js.map +1 -0
  37. package/frontend/dist/assets/AssistantMessageContent-D1-3VpWE.js +13 -0
  38. package/frontend/dist/assets/{GraphPreview-DqnUioLI.js → GraphPreview-CeI4sbtV.js} +1 -1
  39. package/frontend/dist/assets/MobilePage-BP7c3W3D.js +14 -0
  40. package/frontend/dist/assets/{OfficePreview-DsfkiPFS.js → OfficePreview-Dgs4aiYi.js} +2 -2
  41. package/frontend/dist/assets/{ProjectPage-eUYt7NVa.js → ProjectPage-DPv57nD9.js} +5 -6
  42. package/frontend/dist/assets/SettingsPage-CFSmKovg.js +13 -0
  43. package/frontend/dist/assets/{SkillHubPage-CRFSy-Cg.js → SkillHubPage-4g29B-Hr.js} +2 -2
  44. package/frontend/dist/assets/{chevron-down-9-G7S6iK.js → chevron-down-CyDTNuVw.js} +1 -1
  45. package/frontend/dist/assets/{chevron-up-BnktQ0Ve.js → chevron-up-BURz786o.js} +1 -1
  46. package/frontend/dist/assets/{index-Dn-YLcfD.js → index-BFWZX3Hz.js} +7 -7
  47. package/frontend/dist/assets/index-D0NB3R9q.css +1 -0
  48. package/frontend/dist/assets/index-Uy-V98k3.js +13 -0
  49. package/frontend/dist/assets/{index-B7AIJuGG.js → index-z4qRGojt.js} +1 -1
  50. package/frontend/dist/assets/{jszip.min-wfpG-66r.js → jszip.min-CPG_u-b-.js} +1 -1
  51. package/frontend/dist/assets/{search-Dq8YTmIg.js → search-CZ83atP-.js} +1 -1
  52. package/frontend/dist/index.html +2 -2
  53. package/package.json +1 -1
  54. package/frontend/dist/assets/AssistantMessageContent-DayURBLZ.js +0 -13
  55. package/frontend/dist/assets/MobilePage-CIA-DkuB.js +0 -14
  56. package/frontend/dist/assets/SettingsPage-CCk3EySa.js +0 -13
  57. package/frontend/dist/assets/index-CboghLS5.js +0 -7
  58. 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