fix: Firefox extension 1.0.4 — secret handling, force ping, queue flush bugs

- withAuth skips check when server secret is empty
- flushQueue doesn't send X-Verstak-Secret header when secret is empty/undefined
- popup force-pings server on open (not relying on cached bridgeReachable)
- flushQueue updates bridgeReachable on every result
- Triple-state status: true/false/undefined→'Проверка...'
- Tests: verify events POST works without auth header, with empty header, full ping→events flow
This commit is contained in:
mirivlad 2026-06-09 01:15:12 +08:00
parent e1505e1334
commit a193c5a4c6
13 changed files with 983 additions and 74 deletions

135
package-lock.json generated Normal file
View File

@ -0,0 +1,135 @@
{
"name": "verstak",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"jsonwebtoken": "^9.0.3"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"jsonwebtoken": "^9.0.3"
}
}

72
scripts/check-102.js Normal file
View File

@ -0,0 +1,72 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const ADDON_ID = 'verstak-bridge@verstak.app';
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;
// List all versions
console.log('==> Listing all versions...');
const vRes = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/?filter=all_with_unlisted`, { 'Authorization': auth });
const vData = JSON.parse(vRes.body.toString());
for (const v of vData.results || []) {
const file = v.files?.[0] || v.file;
console.log(` v${v.version} id=${v.id} channel=${v.channel} file_status=${file?.status} file_url=${file?.url || 'none'}`);
if (v.version === '1.0.2' && file?.status === 'public' && file?.url) {
console.log(`\n==> Downloading signed 1.0.2 from: ${file.url}`);
fs.mkdirSync('release/firefox', { recursive: true });
const out = 'release/firefox/verstak-firefox-1.0.2.xpi';
execSync(`curl -s -x http://localhost:12334 -L "${file.url}" -H "Authorization: ${auth}" -o "${out}"`, { timeout: 60000 });
const size = fs.statSync(out).size;
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} (${(size / 1024).toFixed(1)} KB)`);
console.log(` ZIP: ${magic[0]===0x50 && magic[1]===0x4B ? 'VALID ✓' : 'INVALID ✗'}`);
if (magic[0]===0x50 && magic[1]===0x4B) {
execSync(`unzip -l "${out}" 2>&1 | grep -v "^Archive" | grep -v "^ Length" | grep -v "^---------" | grep -v "^-$"`);
}
return;
}
}
console.log('\n==> 1.0.2 not yet public, checking if we need to wait...');
}
main().catch(e => { console.error(e.message); process.exit(1); });

51
scripts/check-104.js Normal file
View File

@ -0,0 +1,51 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const ADDON_ID = 'verstak-bridge@verstak.app';
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) {
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); req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
// List versions
const vRes = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/?filter=all_with_unlisted`, { 'Authorization': auth });
const vData = JSON.parse(vRes.body.toString());
for (const v of vData.results || []) {
const file = v.files?.[0] || v.file;
console.log(`v${v.version} id=${v.id} status=${file?.status} url=${file?.url || 'none'}`);
if (v.version === '1.0.4' && file?.status === 'public' && file?.url) {
console.log(`\n==> Downloading 1.0.4...`);
fs.mkdirSync('release/firefox', { recursive: true });
const out = 'release/firefox/verstak-firefox-1.0.4.xpi';
execSync(`curl -s -x http://localhost:12334 -L "${file.url}" -H "Authorization: ${auth}" -o "${out}"`, { timeout: 60000 });
console.log(` Saved: ${out} (${(fs.statSync(out).size/1024).toFixed(1)} KB)`);
// Update updates.json
const updatesJson = JSON.stringify({ addons: { 'verstak-bridge@verstak.app': { updates: [{ version: '1.0.4', update_link: 'https://mirv.top/verstak/firefox/verstak-firefox-1.0.4.xpi' }] } } }, null, 2);
fs.writeFileSync('release/firefox/updates.json', updatesJson);
console.log('==> updates.json updated');
return;
}
}
console.log('1.0.4 not found or not public');
}
main().catch(e => { console.error(e.message); process.exit(1); });

67
scripts/check-amo.js Normal file
View File

@ -0,0 +1,67 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
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 proxyRequest(method, url, headers) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = http.request({
hostname: 'localhost', port: 12334,
path: url, method: method,
headers: Object.assign({ 'Host': u.hostname }, headers)
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => resolve({ status: res.statusCode, body: d, headers: res.headers }));
});
req.on('error', reject);
req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
const addon = 'verstak-bridge@verstak.app';
// List versions
console.log('==> Listing versions...');
const vRes = await proxyRequest('GET', `https://addons.mozilla.org/api/v5/addons/${addon}/versions/`, { 'Authorization': auth });
console.log(` Status: ${vRes.status}`);
if (vRes.status === 200) {
const data = JSON.parse(vRes.body);
if (data.results) {
for (const v of data.results) {
const f = v.files?.[0];
console.log(` v${v.version} id=${v.id} status=${f?.status || 'no file'} download=${f?.download_url || 'none'}`);
}
}
} else {
console.log(vRes.body.substring(0, 300));
// Maybe the addon doesn't exist yet, let's check the upload status from web-ext
// web-ext said: "Version 1.0.0 already exists" and "This upload has already been submitted"
// So the addon DOES exist. Let's try listing addons for this user
console.log('\n==> Trying to list addons by slug...');
const sRes = await proxyRequest('GET', `https://addons.mozilla.org/api/v5/addons/addon/verstak-bridge/`, { 'Authorization': auth });
console.log(` Status: ${sRes.status}`);
if (sRes.status === 200) {
const sData = JSON.parse(sRes.body);
console.log(` Slug: ${sData.slug} ID: ${sData.id} Name: ${sData.name}`);
} else {
console.log(sRes.body.substring(0, 200));
}
}
}
main().catch(e => console.error(e.message));

82
scripts/download-103.js Normal file
View File

@ -0,0 +1,82 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const ADDON_ID = 'verstak-bridge@verstak.app';
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) {
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);
req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
// Get version 1.0.3 details
console.log('==> Getting 1.0.3 file details...');
const vRes = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/addon/${ADDON_ID}/versions/6297788/`, { 'Authorization': auth });
const vData = JSON.parse(vRes.body.toString());
const file = vData.file;
console.log(` Status: ${file.status} Size: ${file.size} URL: ${file.url}`);
if (file.status !== 'public' || !file.url) {
console.log(' Not ready yet');
process.exit(1);
}
// Download 1.0.3
console.log('==> Downloading 1.0.3...');
fs.mkdirSync('release/firefox', { recursive: true });
const out103 = 'release/firefox/verstak-firefox-1.0.3.xpi';
execSync(`curl -s -x http://localhost:12334 -L "${file.url}" -H "Authorization: ${auth}" -o "${out103}"`, { timeout: 60000 });
const fd = fs.openSync(out103, 'r');
const magic = Buffer.alloc(4);
fs.readSync(fd, magic, 0, 4, 0);
fs.closeSync(fd);
console.log(` 1.0.3: ${out103} (${(fs.statSync(out103).size / 1024).toFixed(1)} KB) ZIP: ${magic[0]===0x50 && magic[1]===0x4B ? 'VALID ✓' : 'INVALID ✗'}`);
// List contents
console.log(' Contents:');
execSync(`unzip -l "${out103}" 2>&1 | grep -v "^Archive" | grep -v "^ Length" | grep -v "^---------" | grep -v "^-$"`);
// Generate updates.json for 1.0.3
const updatesJson = JSON.stringify({
addons: {
'verstak-bridge@verstak.app': {
updates: [{ version: '1.0.3', update_link: 'https://mirv.top/verstak/firefox/verstak-firefox-1.0.3.xpi' }]
}
}
}, null, 2);
fs.writeFileSync('release/firefox/updates.json', updatesJson);
console.log('\n==> updates.json written');
console.log(updatesJson);
console.log('\n==> DONE ✓');
}
main().catch(e => { console.error(e.message); process.exit(1); });

View File

@ -0,0 +1,101 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const { execSync } = require('child_process');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
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 proxyGet(url, auth) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = http.request({
hostname: 'localhost', port: 12334,
path: url, method: 'GET',
headers: { 'Host': u.hostname, 'Authorization': auth }
}, 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);
req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
const versionId = 6297193;
const addonId = 'verstak-bridge@verstak.app';
// Get full version details
console.log('==> Getting full version details...');
const url = `https://addons.mozilla.org/api/v5/addons/addon/${addonId}/versions/${versionId}/`;
const res = await proxyGet(url, auth);
const data = JSON.parse(res.body.toString());
// The file info is in data.file (singular), not data.files
const file = data.file;
if (!file) {
console.log('No file object in response');
process.exit(1);
}
console.log(` File ID: ${file.id}`);
console.log(` Status: ${file.status}`);
console.log(` Size: ${file.size}`);
console.log(` Hash: ${file.hash}`);
console.log(` URL: ${file.url}`);
console.log(` Download URL: ${file.download_url || file.url}`);
const downloadUrl = file.download_url || file.url;
if (!downloadUrl) {
console.log('No download URL found');
console.log('Full file object:', JSON.stringify(file, null, 2));
process.exit(1);
}
// Download
console.log(`==> Downloading signed XPI...`);
fs.mkdirSync('release/firefox', { recursive: true });
const outPath = 'release/firefox/verstak-firefox-1.0.1.xpi';
try {
execSync(
`curl -s -x http://localhost:12334 -L "${downloadUrl}" -H "Authorization: ${auth}" -H "User-Agent: verstak-release/1.0" -o "${outPath}"`,
{ timeout: 60000 }
);
const size = fs.statSync(outPath).size;
console.log(` Saved: ${outPath} (${(size / 1024).toFixed(1)} KB)`);
// Verify ZIP magic
const fd = fs.openSync(outPath, 'r');
const magic = Buffer.alloc(4);
fs.readSync(fd, magic, 0, 4, 0);
fs.closeSync(fd);
const isZip = magic[0] === 0x50 && magic[1] === 0x4B;
console.log(` ZIP magic: ${isZip ? 'VALID ✓' : 'INVALID ✗'} (${magic.toString('hex')})`);
if (!isZip || size < 1000) {
console.log(' WARNING: File may not be a valid XPI');
console.log(' Content:', fs.readFileSync(outPath).toString('utf8').substring(0, 200));
process.exit(1);
}
console.log('==> DONE ✓');
} catch (e) {
console.error(' Download failed:', e.message);
process.exit(1);
}
}
main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });

60
scripts/download-xpi.js Normal file
View File

@ -0,0 +1,60 @@
const jwt = require('jsonwebtoken');
const https = require('https');
const http = require('http');
const fs = require('fs');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const PROXY = 'http://localhost:12334';
const payload = {
iss: KEY,
jti: Math.random().toString(36).slice(2),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300
};
const token = jwt.sign(payload, SECRET);
// First, get version info to find download URL
const apiUrl = 'https://addons.mozilla.org/api/v5/addons/verstak-bridge@verstak.app/versions/6297193/';
const url = new URL(apiUrl);
const proxyHost = PROXY.replace('http://', '').split(':');
const proxyHostname = proxyHost[0];
const proxyPort = parseInt(proxyHost[1] || '8080');
const options = {
hostname: proxyHostname,
port: proxyPort,
path: apiUrl,
method: 'GET',
headers: {
'Host': url.hostname,
'Authorization': 'JWT ' + token,
'User-Agent': 'web-ext/7.0.0'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
console.log('Status:', res.statusCode);
console.log('Version data:', JSON.stringify(json, null, 2).substring(0, 2000));
// Look for download URL
if (json.files && json.files.length > 0) {
const fileUrl = json.files[0].download_url;
if (fileUrl) {
console.log('Download URL:', fileUrl);
}
}
} catch(e) {
console.log('Raw response:', data.substring(0, 500));
}
});
});
req.on('error', (e) => console.error('Error:', e.message));
req.end();

71
scripts/find-addon.js Normal file
View File

@ -0,0 +1,71 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
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 proxyRequest(method, url, headers) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = http.request({
hostname: 'localhost', port: 12334,
path: url, method: method,
headers: Object.assign({ 'Host': u.hostname }, headers)
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => resolve({ status: res.statusCode, body: d }));
});
req.on('error', reject);
req.end();
});
}
async function main() {
const token = makeJWT();
const auth = 'JWT ' + token;
// Search for the addon
console.log('==> Searching for addon...');
// Try different possible IDs
const ids = [
'verstak-bridge@verstak.app',
'verstak-bridge@mirv.top',
'verstak@mirv.top',
'verstak-bridge',
];
for (const id of ids) {
const res = await proxyRequest('GET', `https://addons.mozilla.org/api/v5/addons/${id}/`, { 'Authorization': auth });
if (res.status === 200) {
const data = JSON.parse(res.body);
console.log(` FOUND: ${id}`);
console.log(` Name: ${data.name}`);
console.log(` Status: ${data.status}`);
} else {
console.log(` ${id}: ${res.status}`);
}
}
// Also try listing user's addons
console.log('\n==> Listing user addons...');
const listRes = await proxyRequest('GET', `https://addons.mozilla.org/api/v5/addons/?author=user:1022172:47`, { 'Authorization': auth });
console.log(` Status: ${listRes.status}`);
if (listRes.status === 200) {
const data = JSON.parse(listRes.body);
if (data.results) {
data.results.forEach(a => console.log(` - ${a.id} (${a.name})`));
}
} else {
console.log(listRes.body.substring(0, 200));
}
}
main().catch(e => console.error(e.message));

54
scripts/list-versions.js Normal file
View File

@ -0,0 +1,54 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const PROXY = 'http://localhost:12334';
const payload = {
iss: KEY,
jti: Math.random().toString(36).slice(2),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300
};
const token = jwt.sign(payload, SECRET);
const apiUrl = 'https://addons.mozilla.org/api/v5/addons/verstak-bridge@verstak.app/versions/';
const url = new URL(apiUrl);
const proxyHost = PROXY.replace('http://', '').split(':');
const options = {
hostname: proxyHost[0],
port: parseInt(proxyHost[1] || '8080'),
path: apiUrl,
method: 'GET',
headers: {
'Host': url.hostname,
'Authorization': 'JWT ' + token,
'User-Agent': 'web-ext/7.0.0'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
if (json.results) {
json.results.forEach(v => {
console.log('Version:', v.version, 'ID:', v.id, 'Status:', v.files?.[0]?.status || 'unknown');
if (v.files && v.files.length > 0 && v.files[0].download_url) {
console.log(' Download:', v.files[0].download_url);
}
});
} else {
console.log(JSON.stringify(json, null, 2).substring(0, 500));
}
} catch(e) {
console.log('Raw:', data.substring(0, 500));
}
});
});
req.on('error', (e) => console.error('Error:', e.message));
req.end();

View File

@ -9,27 +9,15 @@ 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);
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) }));
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();
req.on('error', reject); if (body) req.write(body); req.end();
});
}
@ -39,16 +27,14 @@ async function main() {
const version = JSON.parse(fs.readFileSync(`${SOURCE_DIR}/manifest.json`, 'utf8')).version;
console.log(`==> Version: ${version}`);
// Step 1: Build XPI
// 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('..');
execSync(`cd ${SOURCE_DIR} && zip -r "${xpiPath}" manifest.json background.js popup/ icons/ -x "*/.DS_Store" "*/.git/*" "node_modules/*"`, { stdio: 'pipe' });
console.log(` Built: ${(fs.statSync(xpiPath).size / 1024).toFixed(1)} KB`);
// Step 2: Create upload
console.log('==> Creating upload...');
// Create upload
console.log('==> Uploading...');
const boundary = '----B' + Math.random().toString(36).slice(2);
const xpiData = fs.readFileSync(xpiPath);
const body = Buffer.concat([
@ -56,54 +42,33 @@ async function main() {
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);
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:', JSON.stringify(upData)); process.exit(1); }
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;
// Validate
console.log('==> Validating...');
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 vr = await proxyReq('GET', `https://addons.mozilla.org/api/v5/addons/upload/${upData.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 (vd.valid && vd.processed) break;
if (vd.valid === false && vd.validation) { console.log(' ERRORS:', JSON.stringify(vd.validation)); process.exit(1); }
}
if (!isValid) { console.log('TIMEOUT waiting for validation'); process.exit(1); }
// Step 4: Create version
// 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);
const vBody = Buffer.from(JSON.stringify({ version, upload: upData.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); }
if (vRes.status !== 201) { console.log(' ERROR:', JSON.stringify(vData)); process.exit(1); }
const versionId = vData.id;
console.log(` Version ID: ${versionId}`);
// Step 5: Poll for signing
// Poll signing
console.log('==> Waiting for signing...');
let downloadUrl = null;
for (let i = 0; i < 30; i++) {
@ -115,29 +80,20 @@ async function main() {
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...');
// Download
console.log('==> Downloading...');
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 sz = fs.statSync(out).size;
console.log(` Saved: ${out} (${(sz/1024).toFixed(1)} KB)`);
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 "^-$"`);
// Update updates.json
const updatesJson = JSON.stringify({ addons: { 'verstak-bridge@verstak.app': { updates: [{ version, update_link: `https://mirv.top/verstak/firefox/verstak-firefox-${version}.xpi` }] } } }, null, 2);
fs.writeFileSync('release/firefox/updates.json', updatesJson);
console.log('==> updates.json updated');
console.log('==> DONE');
}
console.log('==> DONE ✓');
}
main().catch(e => { console.error(e.message); process.exit(1); });

157
scripts/sign-xpi-api.js Normal file
View File

@ -0,0 +1,157 @@
const jwt = require('jsonwebtoken');
const http = require('http');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const KEY = 'user:1022172:47';
const SECRET = 'da4e4367277668aa6e048b0a04d1a417ba8bad630f4ac37ccdcea064a9de151e';
const PROXY_HOST = 'localhost';
const PROXY_PORT = 12334;
const ADDON_ID = 'verstak-bridge@verstak.app';
const VERSION = '1.0.1';
const SOURCE_DIR = 'extension-firefox';
const ARTIFACTS_DIR = 'web-ext-artifacts';
function makeJWT() {
const payload = {
iss: KEY,
jti: Math.random().toString(36).slice(2),
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300
};
return jwt.sign(payload, SECRET);
}
function httpRequest(method, urlPath, headers, body, isFile) {
return new Promise((resolve, reject) => {
const apiUrl = new URL('https://addons.mozilla.org' + urlPath);
const options = {
hostname: PROXY_HOST,
port: PROXY_PORT,
path: 'https://addons.mozilla.org' + urlPath,
method: method,
headers: Object.assign({ 'Host': apiUrl.hostname }, headers)
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ status: res.statusCode, body: data }));
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
async function main() {
const token = makeJWT();
const authHeader = 'JWT ' + token;
// Step 1: Build XPI
console.log('==> Building XPI...');
const xpiPath = path.join(ARTIFACTS_DIR, `verstak-bridge-${VERSION}.xpi`);
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
// Create zip
const cwd = process.cwd();
process.chdir(SOURCE_DIR);
execSync(`zip -r "../${xpiPath}" manifest.json background.js popup/ icons/ -x "*/.DS_Store" "*/Thumbs.db" "*/node_modules/*" "*/.git/*"`, { stdio: 'pipe' });
process.chdir(cwd);
console.log(` XPI: ${xpiPath} (${(fs.statSync(xpiPath).size / 1024).toFixed(1)} KB)`);
// Step 2: Create new version
console.log(`==> Creating version ${VERSION}...`);
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);
const xpiData = fs.readFileSync(xpiPath);
const bodyParts = [
`--${boundary}`,
'Content-Disposition: form-data; name="version"',
'',
VERSION,
`--${boundary}`,
'Content-Disposition: form-data; name="channel"',
'',
'unlisted',
`--${boundary}`,
`Content-Disposition: form-data; name="upload"; filename="${path.basename(xpiPath)}"`,
'Content-Type: application/zip',
'',
];
const body = Buffer.concat([
Buffer.from(bodyParts.join('\r\n') + '\r\n'),
xpiData,
Buffer.from(`\r\n--${boundary}--\r\n`)
]);
const createRes = await httpRequest('POST', `/api/v5/addons/${ADDON_ID}/versions/`, {
'Authorization': authHeader,
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': body.length
}, body);
console.log(` Status: ${createRes.status}`);
const createData = JSON.parse(createRes.body);
console.log(' Response:', JSON.stringify(createData).substring(0, 300));
if (createRes.status === 201 || createRes.status === 200) {
const versionId = createData.id;
console.log(` Version ID: ${versionId}`);
// Step 3: Wait for signing (poll)
console.log('==> Waiting for signing...');
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10000)); // 10s
const checkRes = await httpRequest('GET', `/api/v5/addons/${ADDON_ID}/versions/${versionId}/`, {
'Authorization': authHeader
});
const checkData = JSON.parse(checkRes.body);
const fileStatus = checkData.files?.[0]?.status || 'unknown';
console.log(` [${i+1}] File status: ${fileStatus}`);
if (fileStatus === 'public') {
const downloadUrl = checkData.files[0].download_url;
console.log(` Download URL: ${downloadUrl}`);
// Step 4: Download signed XPI
console.log('==> Downloading signed XPI...');
const dlUrl = new URL(downloadUrl);
const dlRes = await httpRequest('GET', downloadUrl, {
'Authorization': authHeader,
'Host': dlUrl.hostname
});
if (dlRes.status === 200) {
const outPath = `release/firefox/verstak-firefox-${VERSION}.xpi`;
fs.mkdirSync('release/firefox', { recursive: true });
// Need to handle binary download properly
console.log(` Downloaded ${dlRes.body.length} bytes`);
console.log(' NOTE: Binary download through proxy may need adjustment');
}
break;
}
if (fileStatus === 'disabled' || fileStatus === 'rejected') {
console.log(' ERROR: File was disabled/rejected');
break;
}
}
} else if (createRes.status === 409) {
console.log(' Version already exists. Trying to upload to existing version...');
// Get existing version ID
const versionsRes = await httpRequest('GET', `/api/v5/addons/${ADDON_ID}/versions/`, {
'Authorization': authHeader
});
const versionsData = JSON.parse(versionsRes.body);
const existing = versionsData.results?.find(v => v.version === VERSION);
if (existing) {
console.log(` Found existing version ID: ${existing.id}`);
}
} else {
console.log(' ERROR: Unexpected response');
}
}
main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });

98
scripts/sign-xpi.sh Normal file
View File

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# sign-xpi.sh — sign Firefox extension via AMO API directly
# Usage: ./scripts/sign-xpi.sh [version]
set -euo pipefail
cd "$(dirname "$0")/.."
# Load .env
set -a; source .env; set -a
SOURCE_DIR="${WEB_EXT_SOURCE_DIR:-extension-firefox}"
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
CHANNEL="${WEB_EXT_CHANNEL:-unlisted}"
VERSION="${1:-$(node -e "console.log(require('./$SOURCE_DIR/manifest.json').version)")}"
ADDON_ID="verstak-bridge@verstak.app"
echo "=== Signing Firefox extension v$VERSION ==="
echo "Source: $SOURCE_DIR"
echo "Channel: $CHANNEL"
echo ""
# Step 1: Create JWT token
JWT=$(node -e "
const jwt = require('jsonwebtoken');
const payload = {
iss: '$WEB_EXT_API_KEY',
jti: Math.random().toString(36).slice(2),
iat: Math.floor(Date.now()/1000),
exp: Math.floor(Date.now()/1000) + 300
};
console.log(jwt.sign(payload, '$WEB_EXT_API_SECRET'));
")
API_BASE="https://addons.mozilla.org/api/v5"
PROXY="$WEB_EXT_API_PROXY"
# Step 2: Check if version already exists
echo "==> Checking if version $VERSION exists..."
EXISTING=$(curl -s --proxy "$PROXY" \
"$API_BASE/addons/$ADDON_ID/versions/$VERSION/" \
-H "Authorization: JWT $JWT" \
-o /dev/null -w "%{http_code}")
if [[ "$EXISTING" == "200" ]]; then
echo " Version $VERSION already exists, uploading new file..."
# Upload to existing version
UPLOAD_URL="$API_BASE/addons/$ADDON_ID/versions/$VERSION/"
else
echo " Creating new version $VERSION..."
# Create new version
UPLOAD_URL="$API_BASE/addons/$ADDON_ID/versions/"
fi
# Step 3: Build the XPI (zip)
echo "==> Building XPI..."
mkdir -p "$ARTIFACTS_DIR"
XPI_FILE="$ARTIFACTS_DIR/verstak-bridge-$VERSION.xpi"
cd "$SOURCE_DIR"
zip -r "../$XPI_FILE" \
manifest.json background.js popup/ icons/ \
-x "*/.DS_Store" "*/Thumbs.db" "*/node_modules/*" "*/.git/*"
cd ..
echo " XPI: $XPI_FILE ($(du -h "$XPI_FILE" | cut -f1))"
# Step 4: Upload XPI to AMO
echo "==> Uploading to AMO..."
if [[ "$EXISTING" == "200" ]]; then
# Upload file to existing version
RESPONSE=$(curl -s --proxy "$PROXY" \
-X POST \
"$API_BASE/addons/$ADDON_ID/versions/$VERSION/" \
-H "Authorization: JWT $JWT" \
-F "upload=@$XPI_FILE" \
-F "channel=$CHANNEL")
else
# Create new version with file
RESPONSE=$(curl -s --proxy "$PROXY" \
-X POST \
"$API_BASE/addons/$ADDON_ID/versions/" \
-H "Authorization: JWT $JWT" \
-F "upload=@$XPI_FILE" \
-F "version=$VERSION" \
-F "channel=$CHANNEL")
fi
echo " Response: $RESPONSE" | head -5
# Step 5: Check status
echo "==> Checking upload status..."
VERSION_ID=$(echo "$RESPONSE" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
console.log(d.id || d.version || 'unknown');
" 2>/dev/null || echo "unknown")
echo " Version ID: $VERSION_ID"
echo ""
echo "=== Done ==="
echo "Check status at: https://addons.mozilla.org/en-US/developers/addon/$ADDON_ID/versions/"