verstak/scripts/sign-xpi-api.js

158 lines
5.4 KiB
JavaScript

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