← back to Bhv Review Poller
check.mjs
72 lines
// Beverly Hills Videos — App Store review-state poller (READ-ONLY against Apple).
// Emits one JSON line to stdout: {state, prev, changed, versionString, reviewState, stamp, meaning}
// Persists last state to data/last-state.txt and a heartbeat to data/latest.json.
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const DIR = path.dirname(fileURLToPath(import.meta.url));
const KEY_ID = '72Y2TZT54R';
const ISS = 'cfbd63ed-301b-465c-aad7-49e94420ad70';
const APP = '6798763975';
const P8 = '/Users/macstudio3/.appstoreconnect/private_keys/AuthKey_72Y2TZT54R.p8';
// Human meaning for each terminal/interesting state
const MEANING = {
WAITING_FOR_REVIEW: 'In Apple’s queue, not yet picked up.',
IN_REVIEW: 'Apple has STARTED reviewing the app.',
PENDING_DEVELOPER_RELEASE: 'APPROVED — waiting for you to release it.',
PENDING_APPLE_RELEASE: 'APPROVED — scheduled for Apple release.',
READY_FOR_SALE: 'APPROVED and LIVE on the App Store.',
REJECTED: 'REJECTED by Apple — needs changes + resubmit.',
METADATA_REJECTED: 'Metadata REJECTED — fix listing text/screenshots + resubmit.',
DEVELOPER_REJECTED: 'You rejected the build (removed from review).',
INVALID_BINARY: 'Binary invalid — needs a new build.',
PENDING_CONTRACT: 'Blocked on an Apple agreement/contract.',
};
const APPROVED = new Set(['PENDING_DEVELOPER_RELEASE', 'PENDING_APPLE_RELEASE', 'READY_FOR_SALE']);
const REJECTED = new Set(['REJECTED', 'METADATA_REJECTED', 'INVALID_BINARY']);
function jwt() {
const b64u = o => Buffer.from(typeof o === 'string' ? o : JSON.stringify(o)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const si = `${b64u({ alg: 'ES256', kid: KEY_ID, typ: 'JWT' })}.${b64u({ iss: ISS, iat: now, exp: now + 600, aud: 'appstoreconnect-v1' })}`;
const sig = crypto.sign('sha256', Buffer.from(si), { key: fs.readFileSync(P8, 'utf8'), dsaEncoding: 'ieee-p1363' });
return `${si}.${sig.toString('base64url')}`;
}
async function main() {
const t = jwt();
const g = async u => {
const r = await fetch('https://api.appstoreconnect.apple.com' + u, { headers: { Authorization: `Bearer ${t}` } });
return await r.json();
};
const vresp = await g(`/v1/apps/${APP}/appStoreVersions?limit=1&fields[appStoreVersions]=versionString,appStoreState`);
const v = vresp.data && vresp.data[0];
if (!v) throw new Error('no appStoreVersion returned: ' + JSON.stringify(vresp).slice(0, 200));
const state = v.attributes.appStoreState;
const rs = ((await g(`/v1/reviewSubmissions?filter[app]=${APP}&filter[platform]=IOS`)).data || []).find(x => x.attributes.submittedDate);
const stateFile = path.join(DIR, 'data', 'last-state.txt');
let prev = null;
try { prev = fs.readFileSync(stateFile, 'utf8').trim(); } catch {}
const changed = !!prev && prev !== state;
fs.writeFileSync(stateFile, state);
const stamp = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' });
const out = {
state, prev, changed,
firstRun: !prev,
versionString: v.attributes.versionString,
reviewState: rs ? rs.attributes.state : null,
approved: APPROVED.has(state),
rejected: REJECTED.has(state),
meaning: MEANING[state] || state,
stamp,
};
fs.writeFileSync(path.join(DIR, 'data', 'latest.json'), JSON.stringify(out, null, 2));
process.stdout.write(JSON.stringify(out) + '\n');
}
main().catch(e => { process.stderr.write('ERR ' + e.message + '\n'); process.exit(1); });