diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b07e4c9 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..256d43d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "jsonwebtoken": "^9.0.3" + } +} diff --git a/scripts/check-102.js b/scripts/check-102.js new file mode 100644 index 0000000..7a5885a --- /dev/null +++ b/scripts/check-102.js @@ -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); }); diff --git a/scripts/check-104.js b/scripts/check-104.js new file mode 100644 index 0000000..5a9f51e --- /dev/null +++ b/scripts/check-104.js @@ -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); }); diff --git a/scripts/check-amo.js b/scripts/check-amo.js new file mode 100644 index 0000000..5af1bc5 --- /dev/null +++ b/scripts/check-amo.js @@ -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)); diff --git a/scripts/download-103.js b/scripts/download-103.js new file mode 100644 index 0000000..b5dbb7c --- /dev/null +++ b/scripts/download-103.js @@ -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); }); diff --git a/scripts/download-signed-xpi.js b/scripts/download-signed-xpi.js new file mode 100644 index 0000000..854b3e5 --- /dev/null +++ b/scripts/download-signed-xpi.js @@ -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); }); diff --git a/scripts/download-xpi.js b/scripts/download-xpi.js new file mode 100644 index 0000000..a0d8ace --- /dev/null +++ b/scripts/download-xpi.js @@ -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(); diff --git a/scripts/find-addon.js b/scripts/find-addon.js new file mode 100644 index 0000000..986481b --- /dev/null +++ b/scripts/find-addon.js @@ -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)); diff --git a/scripts/list-versions.js b/scripts/list-versions.js new file mode 100644 index 0000000..682c973 --- /dev/null +++ b/scripts/list-versions.js @@ -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(); diff --git a/scripts/sign-and-download.js b/scripts/sign-and-download.js index f6bcdbe..b6ccfb9 100644 --- a/scripts/sign-and-download.js +++ b/scripts/sign-and-download.js @@ -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); }); diff --git a/scripts/sign-xpi-api.js b/scripts/sign-xpi-api.js new file mode 100644 index 0000000..c236c46 --- /dev/null +++ b/scripts/sign-xpi-api.js @@ -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); }); diff --git a/scripts/sign-xpi.sh b/scripts/sign-xpi.sh new file mode 100644 index 0000000..53657f8 --- /dev/null +++ b/scripts/sign-xpi.sh @@ -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/"