fix(firefox): v1.0.2 signed XPI, proper contents without node_modules

This commit is contained in:
mirivlad 2026-06-08 21:55:06 +08:00
parent b4de2dec7a
commit 58751945eb
2 changed files with 144 additions and 1 deletions

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Verstak Bridge", "name": "Verstak Bridge",
"version": "1.0.1", "version": "1.0.2",
"description": "Отслеживает активные вкладки и отправляет события в Verstak", "description": "Отслеживает активные вкладки и отправляет события в Verstak",
"author": "Verstak", "author": "Verstak",
"homepage_url": "https://git.mirv.top/mirivlad/verstak", "homepage_url": "https://git.mirv.top/mirivlad/verstak",

View File

@ -0,0 +1,143 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const fs = require('fs');
const { execSync } = require('child_process');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const ADDON_ID = 'verstak-bridge@verstak.app';
const SOURCE_DIR = 'extension-firefox';
function makeJWT() {
return jwt.sign({
iss: KEY, jti: Math.random().toString(36).slice(2),
iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 300
}, SECRET);
}
function proxyReq(method, url, headers, body) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = http.request({
hostname: 'localhost', port: 12334,
path: url, method,
headers: Object.assign({ 'Host': u.hostname }, headers)
}, res => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
const version = JSON.parse(fs.readFileSync(`${SOURCE_DIR}/manifest.json`, 'utf8')).version;
console.log(`==> Version: ${version}`);
// Step 1: Build XPI
console.log('==> Building XPI...');
const xpiPath = `/tmp/verstak-bridge-${version}.xpi`;
process.chdir(SOURCE_DIR);
execSync(`zip -r "${xpiPath}" manifest.json background.js popup/ icons/ -x "*/.DS_Store" "*/.git/*"`, { stdio: 'pipe' });
process.chdir('..');
console.log(` Built: ${(fs.statSync(xpiPath).size / 1024).toFixed(1)} KB`);
// Step 2: Create upload
console.log('==> Creating upload...');
const boundary = '----B' + Math.random().toString(36).slice(2);
const xpiData = fs.readFileSync(xpiPath);
const body = Buffer.concat([
Buffer.from([`--${boundary}\r\n`, `Content-Disposition: form-data; name="upload"; filename="xpi.zip"\r\n`, 'Content-Type: application/zip\r\n\r\n'].join('')),
xpiData,
Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="channel"\r\n\r\nunlisted\r\n--${boundary}--\r\n`)
]);
const upRes = await proxyReq('POST', 'https://addons.mozilla.org/api/v5/addons/upload/', {
'Authorization': auth, 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': body.length
}, body);
console.log(` Upload: ${upRes.status}`);
const upData = JSON.parse(upRes.body.toString());
console.log(` ${JSON.stringify(upData).substring(0, 300)}`);
if (upRes.status !== 201) { console.log(' ERROR'); process.exit(1); }
const uuid = upData.uuid;
console.log(` UUID: ${uuid}`);
// Step 3: Poll upload until valid
console.log('==> Waiting for upload validation...');
let isValid = false;
for (let i = 0; i < 20; i++) {
await new Promise(r => setTimeout(r, 5000));
const vr = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/upload/${uuid}/`, { 'Authorization': auth });
const vd = JSON.parse(vr.body.toString());
console.log(` [${i+1}] valid=${vd.valid} processed=${vd.processed}`);
if (vd.valid && vd.processed) { isValid = true; break; }
if (vd.valid === false && vd.validation) {
console.log(` Validation errors: ${JSON.stringify(vd.validation).substring(0, 300)}`);
process.exit(1);
}
}
if (!isValid) { console.log('TIMEOUT waiting for validation'); process.exit(1); }
// Step 4: Create version
console.log('==> Creating version...');
const vBody = Buffer.from(JSON.stringify({ version, upload: uuid }));
const vRes = await proxyReq('POST', `https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/`, {
'Authorization': auth, 'Content-Type': 'application/json', 'Content-Length': vBody.length
}, vBody);
console.log(` Status: ${vRes.status}`);
const vData = JSON.parse(vRes.body.toString());
console.log(` ${JSON.stringify(vData).substring(0, 400)}`);
if (vRes.status !== 201 && vRes.status !== 200) { console.log(' ERROR'); process.exit(1); }
const versionId = vData.id;
console.log(` Version ID: ${versionId}`);
// Step 5: Poll for signing
console.log('==> Waiting for signing...');
let downloadUrl = null;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000));
const vr = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/${versionId}/`, { 'Authorization': auth });
const vd = JSON.parse(vr.body.toString());
const f = vd.file;
console.log(` [${i+1}] status=${f?.status} size=${f?.size}`);
if (f?.status === 'public' && f?.url) { downloadUrl = f.url; break; }
if (f?.status === 'disabled' || f?.status === 'rejected') { console.log(' REJECTED'); process.exit(1); }
}
if (!downloadUrl) { console.log('TIMEOUT'); process.exit(1); }
// Step 6: Download
console.log('==> Downloading signed XPI...');
fs.mkdirSync('release/firefox', { recursive: true });
const out = `release/firefox/verstak-firefox-${version}.xpi`;
execSync(`curl -s -x http://localhost:12334 -L "${downloadUrl}" -H "Authorization: ${auth}" -o "${out}"`, { timeout: 60000 });
const fd = fs.openSync(out, 'r');
const magic = Buffer.alloc(4);
fs.readSync(fd, magic, 0, 4, 0);
fs.closeSync(fd);
console.log(` Saved: ${out} (${(fs.statSync(out).size / 1024).toFixed(1)} KB)`);
console.log(` ZIP: ${magic[0]===0x50 && magic[1]===0x4B ? 'VALID ✓' : 'INVALID ✗'}`);
if (magic[0]===0x50 && magic[1]===0x4B) {
console.log(' Contents:');
execSync(`unzip -l "${out}" 2>&1 | grep -v "^Archive" | grep -v "^ Length" | grep -v "^---------" | grep -v "^-$"`);
}
console.log('==> DONE ✓');
}
main().catch(e => { console.error(e.message); process.exit(1); });