#!/usr/bin/env node /* global fetch, AbortSignal, TextDecoder, console, process */ // Dependency-free Node 22 publisher. Importing this module never makes requests. import { readFile, writeFile, rename, unlink } from 'node:fs/promises' import { randomUUID, createHash } from 'node:crypto' import { pathToFileURL } from 'node:url' import core from './client-core.cjs' async function readResponse(response) { if (!response.body?.getReader) return response.text() // Mock transport / older response adapter. const reader = response.body.getReader(), decoder = new TextDecoder('utf-8', { fatal: true }) let total = 0, text = '' try { while (true) { const { done, value } = await reader.read() if (done) break total += value.byteLength if (total > core.MAX_BYTES) { await reader.cancel() throw new Error('response_limit') } text += decoder.decode(value, { stream: true }) } return text + decoder.decode() } catch { throw new Error( 'Could not read a bounded UTF-8 response (maximum 256 KiB). Preserve the saved run for receipt recovery.' ) } finally { reader.releaseLock() } } export function createClient({ apiBase, token, fetchImpl = fetch }) { const base = core.apiBase(apiBase) core.credential(token) async function request(path, { body, key } = {}) { core.check( /^\/(?:competitions(?:\/[0-9a-f-]+\/fixtures(?:\?after=[0-9a-f-]+&limit=100)?)?|predictions\/(?:validate|batches(?:\/[0-9a-f-]+)?)|me\/predictions)$/.test( path ), 'Unsupported API path.' ) let result try { result = await fetchImpl(base + path, { method: body ? 'POST' : 'GET', redirect: 'error', signal: AbortSignal.timeout(30_000), headers: { Authorization: 'Bearer ' + token, ...(body ? { 'Content-Type': 'application/json' } : {}), ...(key ? { 'Idempotency-Key': key } : {}), }, ...(body ? { body } : {}), }) } catch { throw new Error( 'Request did not return a receipt. Preserve the saved run and retry explicitly with the same payload and key.' ) } return core.response(result.status, await readResponse(result)) } async function fixtures(competitionId) { core.check(/^[0-9a-f-]{36}$/i.test(competitionId), 'Use a canonical competition UUID.') const all = [], cursors = new Set() let after = null do { const page = await request( '/competitions/' + competitionId + '/fixtures' + (after ? '?after=' + encodeURIComponent(after) + '&limit=100' : '') ) core.check( Array.isArray(page.fixtures) && (page.next_cursor === null || /^[0-9a-f-]{36}$/i.test(page.next_cursor)), 'Unexpected fixture catalog.' ) all.push(...page.fixtures) after = page.next_cursor core.check(!after || !cursors.has(after), 'Fixture catalog repeated a cursor.') cursors.add(after) core.check(all.length <= 10_000, 'Catalog is too large; stop and check the competition.') } while (after) return all } return { request, fixtures } } function fingerprint(run) { return createHash('sha256') .update(run.api_base + '\n' + run.idempotency_key + '\n' + run.payload_text) .digest('hex') } export async function saveRun(file, run, exclusive = false) { run.fingerprint = fingerprint(run) const text = JSON.stringify(run, null, 2) + '\n' if (exclusive) { await writeFile(file, text, { mode: 0o600, flag: 'wx' }) return } const temporary = file + '.pending-' + randomUUID() try { await writeFile(temporary, text, { mode: 0o600, flag: 'wx' }) await rename(temporary, file) } finally { await unlink(temporary).catch(() => {}) } } export async function loadRun(file, base) { const run = JSON.parse(await readFile(file, 'utf8')) core.assertRun(run, base) core.check( run.fingerprint === fingerprint(run), 'Saved payload/key changed; recover the original run rather than inventing a retry.' ) return run } export async function preview(client, run, persist) { await persist(run) // Freeze before even Preview; never overwrite on transport failure. const result = await client.request('/predictions/validate', { body: run.payload_text }) core.previewResult(run, result) await persist(run) return result } export async function publish(client, run, { confirmed, kickoffConsent, persist }) { core.publishReady(run, run.api_base, confirmed, kickoffConsent) run.state = 'uncertain' await persist(run) // This state survives an ambiguous timeout. const result = await client.request('/predictions/batches', { body: run.payload_text, key: run.idempotency_key, }) core.publishResult(run, result) try { await persist(run) } catch { throw new Error( 'Rivory returned a receipt, but it could not be saved. Preserve the original run and explicitly retry the same payload and key to recover it.' ) } return result } function showReceipt(value) { console.log(value.batch_id ? 'Owned batch: ' + value.batch_id : 'Preview (no picks published)') for (const r of value.rows) console.log(`${r.index + 1}: ${r.fixture_id} ${r.status}${r.code ? ' [' + r.code + ']' : ''}`) return !value.rows.some((r) => r.status === 'rejected') } function options(args) { const allowed = new Set([ 'competition-id', 'format', 'round-label', 'out', 'csv', 'model-version', 'run-id', 'run', 'confirm', 'accept-kickoff-line', ]) const o = {} for (let i = 0; i < args.length; i++) { const key = args[i].replace(/^--/, '') core.check( args[i].startsWith('--') && allowed.has(key) && !(key in o), 'Unknown or duplicate option.' ) if (['confirm', 'accept-kickoff-line'].includes(key)) o[key] = true else { core.check(args[i + 1] && !args[i + 1].startsWith('--'), 'Missing option value.') o[key] = args[++i] } } return o } export async function main(args, env = process.env, fetchImpl = fetch) { const [command, ...rest] = args if (!command || command === 'help') { console.log( 'Setup: https://rivory.app/model-connect-guide. Commands: competitions, rounds, template, preview, publish, receipt, predictions. Token: RIVORY_MODEL_TOKEN environment only.' ) return } const o = options(rest), base = core.apiBase(env.RIVORY_MODEL_API_BASE) const client = createClient({ apiBase: base, token: env.RIVORY_MODEL_TOKEN, fetchImpl }) if (command === 'competitions') { const v = await client.request('/competitions') core.check(Array.isArray(v.competitions), 'Unexpected competition catalog.') for (const c of v.competitions) console.log(`${c.id} ${c.slug} ${c.eligibility}`) } else if (command === 'rounds') { core.check(o['competition-id'], 'Rounds requires --competition-id.') console.log( JSON.stringify(core.availableRounds(await client.fixtures(o['competition-id'])), null, 2) ) } else if (command === 'template') { core.check(o.out && o['competition-id'], 'Template requires --out and --competition-id.') const rows = core.template( await client.fixtures(o['competition-id']), o.format || 'straight', o['round-label'] ) await writeFile(o.out, core.toCsv(rows), { mode: 0o600, flag: 'wx' }) console.log( 'Template saved with blank selections. Fill canonical participant IDs, then Preview.' ) } else if (command === 'preview') { core.check( o.out && o.csv && o['competition-id'] && o['model-version'], 'Preview requires --out, --csv, --competition-id and --model-version.' ) const run = core.newRun( { api_base: base, competition_id: o['competition-id'], model_version: o['model-version'] }, core.parseCsv(await readFile(o.csv, 'utf8')), await client.fixtures(o['competition-id']), new Date().toISOString(), o['run-id'] || randomUUID(), randomUUID() ) await saveRun(o.out, run, true) const result = await preview(client, run, (value) => saveRun(o.out, value)) if (!showReceipt(result)) process.exitCode = 2 } else if (command === 'publish') { core.check(o.run, 'Publish requires --run.') const run = await loadRun(o.run, base) const result = await publish(client, run, { confirmed: o.confirm === true, kickoffConsent: o['accept-kickoff-line'] === true, persist: (v) => saveRun(o.run, v), }) if (!showReceipt(result)) process.exitCode = 2 } else if (command === 'receipt') { core.check(o.run, 'Receipt requires --run.') const run = await loadRun(o.run, base) core.check( run.receipt?.batch_id, 'No batch ID was returned. Explicitly retry Publish on this same saved run to recover its receipt.' ) const result = await client.request('/predictions/batches/' + run.receipt.batch_id) core.publishResult(run, result) await saveRun(o.run, run) if (!showReceipt(result)) process.exitCode = 2 } else if (command === 'predictions') { const value = await client.request('/me/predictions') core.check(Array.isArray(value.predictions), 'Unexpected owned predictions.') console.log(JSON.stringify(value.predictions, null, 2)) } else throw new Error('Unknown command.') } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(process.argv.slice(2)).catch((error) => { console.error(error.message) process.exitCode = 1 })