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); });