Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
7.7 kB · 180 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181// Loads a release-candidate zip under the *store* extension id and checks the// things that only go wrong under that identity.//// Loading dist/ unpacked cannot catch a v1.2.1-class bug. The unpacked build// runs under the dev id pinned by the manifest `key`, and that id was in the// bundled OAuth metadata all along — only the store id was missing, so// sign-in was broken for every store user while every local test passed.//// The store id is a hash of the publisher's public key, and that key is// published inside every CRX the store serves (oauth/store-key.txt, verified// here against oauth/extension-ids.json before it is used). Stamping it into a// release zip's manifest makes Chrome compute the store id for a locally// loaded build, so the release candidate can be exercised under its real// identity before anything is uploaded.//// Usage:// node scripts/rc-load.mjs # newest zip in release/// node scripts/rc-load.mjs release/x.zip # a specific one// node scripts/rc-load.mjs --interactive # headful, stays open to sign in//// Branded Chrome >= 137 ignores --load-extension, so the extension is loaded// over CDP with Extensions.loadUnpacked. The store copy of the extension// cannot be installed at the same time (same id), which is why this always// uses a throwaway profile.
import { execFileSync } from 'node:child_process'import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { join, resolve } from 'node:path'import { withChrome } from './cdp.mjs'import { extensionIdFromKey, redirectUriFor } from './check-oauth-metadata.mjs'
/** Newest release zip by version, not by mtime — rebuilds must not reorder. */export function latestReleaseZip(names) { const parsed = names .map((name) => ({ name, m: /^substandard-v(\d+)\.(\d+)\.(\d+)\.zip$/.exec(name) })) .filter((e) => e.m) .map(({ name, m }) => ({ name, v: [Number(m[1]), Number(m[2]), Number(m[3])] })) if (parsed.length === 0) return undefined parsed.sort((a, b) => b.v[0] - a.v[0] || b.v[1] - a.v[1] || b.v[2] - a.v[2]) return parsed[0].name}
/** * What an `oauth-authorize` probe says about the bundled metadata. The * redirect_uri check in @atproto/oauth-client runs before any identity * lookup, so a deliberately unresolvable handle separates the two failures: * anything that is not "Invalid redirect_uri" means the metadata covered this * build's own id, which is the whole question. */export function classifyAuthProbe(result) { const message = String(result?.settled?.__error ?? result?.threw ?? '') if (/invalid redirect_uri/i.test(message)) return 'invalid-redirect' if (result?.timedOut) return 'inconclusive' return 'ok'}
function extract(zip, dest) { try { execFileSync('unzip', ['-oq', zip, '-d', dest], { stdio: 'pipe' }) return } catch { // no unzip; fall through to python3, as deploy-ext.sh does for zipping } execFileSync('python3', ['-c', 'import sys,zipfile;zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])', zip, dest])}
// Runs inside the extension's own service worker. Creating the offscreen// document and messaging it is exactly what startSignIn does; the bogus handle// stops before any consent window.const PROBE = `(async () => { try { await chrome.offscreen.createDocument({ url: 'offscreen.html', reasons: ['LOCAL_STORAGE'], justification: 'release candidate probe', }) } catch (err) { if (!/single offscreen document/i.test(String(err))) { return JSON.stringify({ id: chrome.runtime.id, offscreenError: String(err) }) } } const ask = chrome.runtime .sendMessage({ target: 'offscreen', type: 'oauth-authorize', handle: 'rc-probe.invalid' }) .then((settled) => ({ settled })) .catch((err) => ({ threw: String(err) })) const timed = new Promise((r) => setTimeout(() => r({ timedOut: true }), 15000)) return JSON.stringify({ id: chrome.runtime.id, authProbe: await Promise.race([ask, timed]) })})()`
async function main(argv) { const root = resolve(import.meta.dirname, '..') const interactive = argv.includes('--interactive') const zipArg = argv.find((a) => !a.startsWith('--'))
const releaseDir = join(root, 'release') let zip = zipArg && resolve(zipArg) if (!zip) { const newest = existsSync(releaseDir) ? latestReleaseZip(readdirSync(releaseDir)) : undefined if (!newest) { console.error('rc-load: no release zip found — run scripts/deploy-ext.sh first') process.exit(1) } zip = join(releaseDir, newest) } if (!existsSync(zip)) { console.error(`rc-load: ${zip} does not exist`) process.exit(1) }
const ids = JSON.parse(readFileSync(join(root, 'oauth/extension-ids.json'), 'utf8')) const storeKey = readFileSync(join(root, 'oauth/store-key.txt'), 'utf8').trim() const derived = extensionIdFromKey(storeKey) if (derived !== ids.store) { console.error( `rc-load: oauth/store-key.txt derives ${derived}, but extension-ids.json declares ${ids.store}.` + ' Re-extract the publisher key from the store CRX.', ) process.exit(1) }
const work = mkdtempSync(join(tmpdir(), 'substandard-rc-')) const extDir = join(work, 'ext') const profile = join(work, 'profile') extract(zip, extDir)
const manifestPath = join(extDir, 'manifest.json') const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) manifest.key = storeKey writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
console.log(`rc-load: ${zip}`) console.log(` version ${manifest.version}, stamped with the store identity ${ids.store}`)
const problems = [] const probe = await withChrome( { extensionDir: extDir, profile, headless: !interactive }, async ({ inWorker, extensionId }) => { if (extensionId !== ids.store) { problems.push(`Chrome loaded this build as ${extensionId}, not the store id ${ids.store}`) } const raw = await inWorker(PROBE) const parsed = typeof raw === 'string' ? JSON.parse(raw) : undefined if (!parsed) problems.push(`the service worker did not answer the probe: ${raw}`) if (interactive) { console.log('\nrc-load: Chrome is open with the release candidate installed.') console.log(' Pin the toolbar icon and sign in for real; Ctrl-C here when done.') await new Promise(() => {}) } return parsed }, )
if (probe?.offscreenError) problems.push(`offscreen document failed: ${probe.offscreenError}`) else if (probe) { const verdict = classifyAuthProbe(probe.authProbe) if (verdict === 'invalid-redirect') { problems.push( `sign-in is broken in this artifact: authorization was refused with "Invalid redirect_uri".` + ` This build does not carry ${redirectUriFor(ids.store)} in its bundled` + ' oauth/client-metadata.json — rebuild from a tree that does.', ) } else if (verdict === 'inconclusive') { problems.push('the authorization probe timed out; run again, or with --interactive') } else { console.log(` sign-in reached identity resolution: ${probe.authProbe?.settled?.__error}`) } }
rmSync(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
if (problems.length > 0) { console.error(`\nrc-load: FAILED (${problems.length} problem(s)):`) for (const p of problems) console.error(` - ${p}`) process.exit(1) } console.log(`\nrc-load: OK — ran as ${ids.store} and authorization got past its own metadata`)}
if (process.argv[1] === import.meta.filename) await main(process.argv.slice(2))