From 58751945ebb74a85db7d5c9f0f59175c8582e9e1 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Mon, 8 Jun 2026 21:55:06 +0800 Subject: [PATCH] fix(firefox): v1.0.2 signed XPI, proper contents without node_modules --- extension-firefox/manifest.json | 2 +- scripts/sign-and-download.js | 143 ++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 scripts/sign-and-download.js diff --git a/extension-firefox/manifest.json b/extension-firefox/manifest.json index 73a799a..0494a23 100644 --- a/extension-firefox/manifest.json +++ b/extension-firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Verstak Bridge", - "version": "1.0.1", + "version": "1.0.2", "description": "Отслеживает активные вкладки и отправляет события в Verstak", "author": "Verstak", "homepage_url": "https://git.mirv.top/mirivlad/verstak", diff --git a/scripts/sign-and-download.js b/scripts/sign-and-download.js new file mode 100644 index 0000000..f6bcdbe --- /dev/null +++ b/scripts/sign-and-download.js @@ -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); });