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:
parent
e1505e1334
commit
a193c5a4c6
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -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); });
|
||||
|
|
@ -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); });
|
||||
|
|
@ -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));
|
||||
|
|
@ -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); });
|
||||
|
|
@ -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); });
|
||||
|
|
@ -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();
|
||||
|
|
@ -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));
|
||||
|
|
@ -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();
|
||||
|
|
@ -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 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 ✓');
|
||||
}
|
||||
const sz = fs.statSync(out).size;
|
||||
console.log(` Saved: ${out} (${(sz/1024).toFixed(1)} KB)`);
|
||||
|
||||
// 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');
|
||||
}
|
||||
main().catch(e => { console.error(e.message); process.exit(1); });
|
||||
|
|
|
|||
|
|
@ -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); });
|
||||
|
|
@ -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/"
|
||||
Loading…
Reference in New Issue