#!/usr/bin/env node // Read-only release preparation. Publishing is handled by the serialized operator release workflow. import fs from 'node:fs'; import {createHash} from 'node:crypto'; import {homedir} from 'node:os'; import {join,resolve} from 'node:path'; import {fileURLToPath} from 'node:url'; import ts from 'typescript'; function body(code) { const ast=ts.createSourceFile('gateway.js',code,ts.ScriptTarget.Latest,true,ts.ScriptKind.JS); const nodes=ast.statements.filter(n=>ts.isFunctionDeclaration(n)&&n.name?.text==='contentPage'); if(nodes.length!==1)throw Error('Expected exactly one contentPage function'); const node=nodes[0];return{start:node.getStart(ast),end:node.end,text:code.slice(node.getStart(ast),node.end)}; } export function patchWordClassroomRead(code) { const source=fs.readFileSync(new URL('./deploy-admin-functions.mjs',import.meta.url),'utf8'); const local=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)?.[1]; if(!local)throw Error('Missing local app gateway'); const target=body(local).text,old=body(code); if(old.text===target)return code; // Only accept the known predecessor. Any independent online change requires review. const expected=target .replace(' const modelId = contentModel(input, nodeIds), vocabulary = modelId === 52;\n // Vocabulary IDs are numeric: preserve indexable joins in the high-traffic classroom reads.\n','') .replace("clauses.push(vocabulary ? 'c.\"nodeId\" = ANY($' + values.length + ')' : 'CAST(c.\"nodeId\" AS text) = ANY($' + values.length + '::text[])');", () => "clauses.push('CAST(c.\"nodeId\" AS text) = ANY($' + values.length + '::text[])');") .replace(' const config = CONTENT_ADDONS[modelId]', ' const modelId = contentModel(input, nodeIds); const config = CONTENT_ADDONS[modelId]') .replace("clauses.push((vocabulary ? 'c.\"modelId\" = $' : 'CAST(c.\"modelId\" AS text) = $') + values.length);", () => "clauses.push('CAST(c.\"modelId\" AS text) = $' + values.length);") .replace("a ON a.\"company\" = $1 AND ' + (vocabulary ? 'a.\"id\" = c.\"itemId\"' : 'CAST(a.\"id\" AS text) = CAST(c.\"itemId\" AS text)')", () => "a ON a.\"company\" = $1 AND CAST(a.\"id\" AS text) = CAST(c.\"itemId\" AS text)'"); if(old.text!==expected)throw Error('Online contentPage differs from the reviewed predecessor; inspect drift before release'); const patched=code.slice(0,old.start)+target+code.slice(old.end);new Function(patched);return patched; } async function main() { const mode='prepare'; if(process.argv.some(arg=>['--deploy','--rollback'].includes(arg)))throw Error('Preparation only: use the serialized operator release workflow to publish or roll back'); const master=process.env.XIAOSHU_MASTER_KEY;if(!master)throw Error('Missing XIAOSHU_MASTER_KEY; no production changes made'); const base=(process.env.XIAOSHU_PARSE_URL||'https://server.xiaoshu.pro/parse').replace(/\/$/,''),app=process.env.XIAOSHU_PARSE_APP_ID||'7pIbDBJmKx_main'; const dir=process.env.XIAOSHU_WORD_RELEASE_DIR||join(homedir(),'.local/state/xiaoshu/cloud-releases/2026-11-07-word-classroom-release'); fs.mkdirSync(dir,{recursive:true,mode:0o700}); if(['manifest.json','original.json','candidate.json'].some(name=>fs.existsSync(join(dir,name))))throw Error('Prepared release artifacts already exist; choose a new release directory, never overwrite the rollback baseline'); const hash=x=>createHash('sha256').update(x).digest('hex'),save=(name,data)=>fs.writeFileSync(join(dir,name),JSON.stringify(data,null,2),{mode:0o600,flag:'wx'}); const parse=async(path,method='GET',data)=>{ const response=await fetch(base+path,{method,headers:{'Content-Type':'application/json','X-Parse-Application-Id':app,'X-Parse-Master-Key':master},body:data?JSON.stringify(data):undefined,signal:AbortSignal.timeout(30000)}); const result=await response.json();if(!response.ok||result.error)throw Error('Release API HTTP '+response.status);return result; }; if(mode==='prepare') { const rows=(await parse('/classes/Function?'+new URLSearchParams({where:JSON.stringify({name:'xiaoshu.app.gateway'}),limit:'2'}))).results; if(rows?.length!==1)throw Error('Ambiguous app gateway'); const original=rows[0],candidate=patchWordClassroomRead(original.code); save('original.json',original);save('candidate.json',{code:candidate}); const manifest={objectId:original.objectId,beforeHash:hash(original.code),afterHash:hash(candidate),functions:['contentPage'],preparedAt:new Date().toISOString()}; save('manifest.json',manifest);console.log(JSON.stringify({mode,dir,...manifest})); } } if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url))await main();