| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- const FILE = 'src/core/kfs/employee.js', RELEASE = 'ksf.employee-name.20261103.v1';
- const PRIOR = 'd2b312ac7ae6c749279a9c04c9e87e697b86c269e503ab3009c6e1b69c852eb1';
- const sha = b => crypto.createHash('sha256').update(b).digest('hex');
- const fail = code => { throw Object.assign(Error(code), { code }); };
- function plain(file) { const s = fs.lstatSync(file); if (s.isSymbolicLink() || !s.isFile()) fail('NOT_PLAIN_FILE'); return s; }
- function site(targetDir) {
- if (!path.isAbsolute(targetDir)) fail('ABSOLUTE_TARGET_REQUIRED');
- const root = path.resolve(targetDir);
- if (fs.realpathSync(root) !== root || root === path.parse(root).root) fail('UNSAFE_TARGET');
- for (let p = path.join(root, FILE); p !== path.dirname(p); p = path.dirname(p)) if (fs.lstatSync(p).isSymbolicLink()) fail('UNSAFE_TARGET');
- plain(path.join(root, FILE)); plain(path.join(root, 'index.html'));
- return root;
- }
- function replace(file, bytes, mode) {
- const temp = file + '.name-' + crypto.randomUUID();
- try { fs.writeFileSync(temp, bytes, { flag: 'wx', mode }); fs.renameSync(temp, file); }
- finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
- }
- export function publishEmployeeName({ sourceDir, targetDir, apply = false, backupFile = null }) {
- const root = site(targetDir), target = path.join(root, FILE), homepage = sha(fs.readFileSync(path.join(root, 'index.html')));
- const before = fs.readFileSync(target), current = sha(before), mode = fs.statSync(target).mode & 0o777;
- let bytes, journal;
- if (backupFile) {
- if (!path.isAbsolute(backupFile) || path.resolve(backupFile).startsWith(root + path.sep)) fail('UNSAFE_BACKUP');
- plain(backupFile); journal = JSON.parse(fs.readFileSync(backupFile));
- if (journal.release !== RELEASE || journal.root !== root || journal.homepage !== homepage || journal.prior !== PRIOR ||
- current !== journal.after || sha(Buffer.from(journal.before, 'base64')) !== PRIOR) fail('ROLLBACK_VERSION_CONFLICT');
- bytes = Buffer.from(journal.before, 'base64');
- } else {
- if (!path.isAbsolute(sourceDir) || path.resolve(sourceDir) === root || path.resolve(sourceDir).startsWith(root + path.sep) || root.startsWith(path.resolve(sourceDir) + path.sep)) fail('UNSAFE_SOURCE');
- const input = path.join(sourceDir, 'payload', FILE); plain(input); bytes = fs.readFileSync(input);
- const manifest = JSON.parse(fs.readFileSync(path.join(sourceDir, 'deploy/employee-name-static.json')));
- if (manifest.release !== RELEASE || manifest.path !== FILE || manifest.prior !== PRIOR || manifest.sha256 !== sha(bytes)) fail('PAYLOAD_HASH_MISMATCH');
- if (![PRIOR, sha(bytes)].includes(current)) fail('UNKNOWN_TARGET_CONTENT');
- }
- const changed = current !== sha(bytes), result = { ok: true, readOnly: !apply, release: RELEASE, filesChecked: 1,
- changes: changed ? [FILE] : [], homepageModified: false, networkRequests: 0, businessWrites: 0, browserVerified: false };
- if (!apply || !changed) return result;
- const lock = path.join(path.dirname(root), '.' + path.basename(root) + '.ksf-static.lock');
- const fd = fs.openSync(lock, 'wx', 0o600); let backup = backupFile;
- try {
- if (sha(fs.readFileSync(target)) !== current || sha(fs.readFileSync(path.join(root, 'index.html'))) !== homepage) fail('CONCURRENT_CHANGE');
- if (!backupFile) {
- backup = path.join(path.dirname(root), 'ksf-employee-name-backup-' + crypto.randomUUID() + '.json');
- fs.writeFileSync(backup, JSON.stringify({ release: RELEASE, root, homepage, prior: current, after: sha(bytes), before: before.toString('base64') }), { flag: 'wx', mode: 0o600 });
- }
- replace(target, bytes, mode);
- if (sha(fs.readFileSync(target)) !== sha(bytes)) fail('READBACK_MISMATCH');
- return { ...result, backup, filesystemVerified: true };
- } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
- }
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
- try {
- const args = process.argv.slice(2), options = { sourceDir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') };
- const seen = new Set();
- for (let i = 0; i < args.length; i++) {
- const flag = args[i]; if (seen.has(flag)) fail('USAGE'); seen.add(flag);
- if (flag === '--apply') options.apply = true;
- else if (['--target-dir', '--rollback'].includes(flag) && args[i + 1] && !args[i + 1].startsWith('--')) options[flag === '--target-dir' ? 'targetDir' : 'backupFile'] = args[++i];
- else fail('USAGE');
- }
- if (!options.targetDir) fail('USAGE');
- console.log(JSON.stringify(publishEmployeeName(options), null, 2));
- } catch (e) { console.error(JSON.stringify({ ok: false, code: e.code || 'PUBLISH_STOPPED' })); process.exitCode = 1; }
- }
|