| // Load and reconcile the same numbered Codex auth pool used by the Doris | |
| // code-review runner. Secrets stay in RUNNER_TEMP and never enter artifacts. | |
| const assert = require('node:assert/strict'); | |
| const fs = require('node:fs'); | |
| const path = require('node:path'); | |
| const {createHash, randomInt} = require('node:crypto'); | |
| const {execFileSync} = require('node:child_process'); | |
| const endpoint = 'oss-cn-hongkong.aliyuncs.com'; | |
| const prefix = 'oss://doris-community-ci/codex/auth.json.'; | |
| const candidatePattern = /^oss:\/\/doris-community-ci\/codex\/auth\.json\.[0-9]+$/; | |
| // The auth pool serves every translation language, and the adapter signals a | |
| // reused refresh token through this file in the per-run CODEX_HOME. | |
| const homePrefix = 'doris-codex-home-'; | |
| const refreshTokenReusedFile = 'refresh-token-reused'; | |
| const sha256 = value => createHash('sha256').update(value).digest('hex'); | |
| function oss(args) { | |
| assert(process.env.OSS_AK && process.env.OSS_SK, 'OSS credentials are required'); | |
| try {return execFileSync('ossutil', ['-i', process.env.OSS_AK, '-k', process.env.OSS_SK, | |
| '-e', endpoint, ...args], {encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, | |
| stdio: ['ignore', 'pipe', 'ignore']});} | |
| catch {throw new Error('OSS operation failed; inspect object access and retry the workflow');} | |
| } | |
| function listed(object) { | |
| return oss(['ls', object, '-s']).split('\n').some(line => line.trim() === object); | |
| } | |
| function validAuth(raw) { | |
| const data = JSON.parse(raw); | |
| assert(data.auth_mode === 'chatgpt' && typeof data.tokens?.access_token === 'string' | |
| && data.tokens.access_token && typeof data.tokens?.refresh_token === 'string' | |
| && data.tokens.refresh_token, 'Invalid Codex ChatGPT auth snapshot'); | |
| return data; | |
| } | |
| function maskAuth(auth) { | |
| if (!process.env.GITHUB_ACTIONS) return; | |
| for (const value of Object.values(auth.tokens)) { | |
| if (typeof value !== 'string' || !value) continue; | |
| const escaped = value.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'); | |
| process.stdout.write(`::add-mask::${escaped}\n`); | |
| } | |
| } | |
| function candidatesFrom(listing) { | |
| return [...new Set(listing.split('\n').map(line => line.trim()).filter(item => candidatePattern.test(item)))]; | |
| } | |
| function shuffle(items) { | |
| const result = [...items]; | |
| for (let i = result.length - 1; i > 0; i--) { | |
| const j = randomInt(i + 1); | |
| [result[i], result[j]] = [result[j], result[i]]; | |
| } | |
| return result; | |
| } | |
| function appendEnv(name, value) { | |
| assert(process.env.GITHUB_ENV, 'GITHUB_ENV is required'); | |
| fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); | |
| } | |
| function load() { | |
| const runnerTemp = process.env.RUNNER_TEMP; | |
| assert(runnerTemp && fs.existsSync(runnerTemp), 'RUNNER_TEMP is required'); | |
| const home = fs.mkdtempSync(path.join(runnerTemp, homePrefix)); | |
| fs.chmodSync(home, 0o700); | |
| const candidates = shuffle(candidatesFrom(oss(['ls', prefix, '-s']))); | |
| assert(candidates.length, 'No numbered Codex auth snapshots found'); | |
| for (const object of candidates) { | |
| const contextObject = `${object}.context`; | |
| if (listed(contextObject)) { | |
| const contextFile = path.join(home, 'context.json'); | |
| oss(['cp', '-f', contextObject, contextFile]); | |
| try { | |
| const context = JSON.parse(fs.readFileSync(contextFile, 'utf8')); | |
| if (context.version === 1 && context.state === 'usage_limited' | |
| && Number.isInteger(context.retry_after_epoch) | |
| && context.retry_after_epoch > Math.floor(Date.now() / 1000)) continue; | |
| } catch { /* An unusable advisory context cannot select a credential. */ continue; } | |
| } | |
| const candidateFile = path.join(home, 'candidate-auth.json'); | |
| oss(['cp', '-f', object, candidateFile]); | |
| const raw = fs.readFileSync(candidateFile, 'utf8'); | |
| const auth = validAuth(raw); | |
| maskAuth(auth); | |
| const refreshHash = sha256(auth.tokens.refresh_token); | |
| if (listed(`${object}.invalid.${refreshHash}`)) continue; | |
| const authFile = path.join(home, 'auth.json'); | |
| fs.renameSync(candidateFile, authFile); | |
| fs.chmodSync(authFile, 0o600); | |
| fs.writeFileSync(path.join(home, 'config.toml'), | |
| 'cli_auth_credentials_store = "file"\napproval_policy = "never"\n' | |
| + '[features]\nshell_tool = false\nunified_exec = false\n', {mode: 0o600}); | |
| appendEnv('CODEX_HOME', home); | |
| appendEnv('I18N_CODEX_AUTH_OBJECT', object); | |
| appendEnv('I18N_CODEX_AUTH_SHA', sha256(raw)); | |
| appendEnv('I18N_CODEX_REFRESH_SHA', refreshHash); | |
| process.stdout.write(`Selected Codex auth snapshot ${object.slice(prefix.length)}\n`); | |
| return; | |
| } | |
| throw new Error('No eligible Codex auth snapshot; all are usage-limited or invalid'); | |
| } | |
| function sync() { | |
| const home = process.env.CODEX_HOME; | |
| const object = process.env.I18N_CODEX_AUTH_OBJECT; | |
| const originalHash = process.env.I18N_CODEX_AUTH_SHA; | |
| const refreshHash = process.env.I18N_CODEX_REFRESH_SHA; | |
| assert(home && object && candidatePattern.test(object) && /^[a-f0-9]{64}$/.test(originalHash ?? ''), | |
| 'Missing selected Codex auth identity'); | |
| assert(/^[a-f0-9]{64}$/.test(refreshHash ?? ''), 'Missing selected refresh-token identity'); | |
| if (fs.existsSync(path.join(home, refreshTokenReusedFile))) { | |
| const invalidFile = path.join(home, 'invalid-token.json'); | |
| fs.writeFileSync(invalidFile, JSON.stringify({version: 1, state: 'auth_invalid', | |
| reason: 'refresh_token_reused', refresh_token_sha256: refreshHash}) + '\n', {mode: 0o600}); | |
| oss(['cp', '-f', invalidFile, `${object}.invalid.${refreshHash}`]); | |
| process.stdout.write('Recorded reused Codex refresh token for this snapshot\n'); | |
| return; | |
| } | |
| const authFile = path.join(home, 'auth.json'); | |
| const raw = fs.readFileSync(authFile, 'utf8'); | |
| maskAuth(validAuth(raw)); | |
| if (sha256(raw) === originalHash) { | |
| process.stdout.write('Codex auth snapshot did not change\n'); | |
| return; | |
| } | |
| const remoteFile = path.join(home, `remote-auth-${randomInt(1_000_000)}.json`); | |
| try { | |
| oss(['cp', '-f', object, remoteFile]); | |
| if (sha256(fs.readFileSync(remoteFile)) !== originalHash) { | |
| process.stdout.write('Codex auth changed remotely; skipped stale refresh upload\n'); | |
| return; | |
| } | |
| oss(['cp', '-f', authFile, object]); | |
| process.stdout.write('Synced refreshed Codex auth snapshot\n'); | |
| } finally {fs.rmSync(remoteFile, {force: true});} | |
| } | |
| if (require.main === module) { | |
| try { | |
| assert(['load', 'sync'].includes(process.argv[2]) && process.argv.length === 3, | |
| 'Usage: codex-auth.cjs load|sync'); | |
| if (process.argv[2] === 'load') load(); else sync(); | |
| } catch (error) {console.error(error.message); process.exitCode = 1;} | |
| } | |
| module.exports = {candidatesFrom, validAuth, load, sync, refreshTokenReusedFile}; |