lib/pane-asset-manifest.js
Rama main · réplica de lectura del repositorio original.
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var MAX_BYTES = 512 * 1024;
function digest(bytes) { return crypto.createHash('sha256').update(bytes).digest('hex'); }
function safeName(name) {
return typeof name === 'string' && name.length < 240 && /^[a-zA-Z0-9_./-]+$/.test(name) && name.split('/').every(function (part) { return part && part !== '.' && part !== '..'; });
}
function stamp(file) {
var stat = fs.lstatSync(file, { bigint: true });
if (!stat.isFile() || stat.size > 32n * 1024n * 1024n) throw new Error('Invalid asset');
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':');
}
function validate(manifest) {
if (!manifest || manifest.version !== 1) throw new Error('Invalid manifest');
['inputs', 'outputs'].forEach(function (group) {
var entries = manifest[group];
if (!entries || Array.isArray(entries) || typeof entries !== 'object') throw new Error('Invalid entries');
var names = Object.keys(entries);
if (!names.length || names.length > (group === 'inputs' ? 1500 : 256)) throw new Error('Asset count exceeded');
names.forEach(function (name) {
if (!safeName(name) || !/^[a-f0-9]{64}$/.test(entries[name]) ||
(group === 'outputs' && !/^pane-assets\/[a-zA-Z0-9_-]+\.(js|css)$/.test(name)) ||
(group === 'inputs' && name.indexOf('pane-assets/') === 0)) throw new Error('Invalid asset path');
});
});
if (!manifest.inputs['app.js'] || !manifest.inputs['style.css'] || !manifest.inputs['index.html'] ||
!manifest.outputs[manifest.js] || !manifest.outputs[manifest.css] ||
!/^pane-assets\/app-[\w-]+\.js$/.test(manifest.js) || !/^pane-assets\/style-[\w-]+\.css$/.test(manifest.css)) throw new Error('Invalid entry');
return manifest;
}
// One fixed manifest per public root; metadata checks on every pane selection,
// hashes only on first use or changed metadata. ctime detects same-length edits
// even when an editor restores mtime. Deployment timestamps are not build dates.
function createPaneAssets(publicDir) {
var manifestPath = path.join(publicDir, 'pane-assets/manifest.json');
var previous = null, manifest = null, verified = Object.create(null);
return function () {
try {
var current = stamp(manifestPath);
if (current !== previous) {
manifest = null; verified = Object.create(null);
if (fs.statSync(manifestPath).size > MAX_BYTES) return null;
manifest = validate(JSON.parse(fs.readFileSync(manifestPath, 'utf8')));
previous = current;
}
if (!manifest) return null;
var entries = Object.assign({}, manifest.inputs, manifest.outputs);
var names = Object.keys(entries);
for (var i = 0; i < names.length; i++) {
var name = names[i], file = path.join(publicDir, name);
if (fs.realpathSync(file) !== file) return null;
var signature = stamp(file);
if (verified[name] !== signature) {
if (digest(fs.readFileSync(file)) !== entries[name]) return null;
verified[name] = signature;
}
}
return manifest;
} catch (error) { return null; }
};
}
module.exports = { createPaneAssets: createPaneAssets, digest: digest, validate: validate, maxBytes: MAX_BYTES };