import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {fileURLToPath} from 'node:url'; const ROOT=path.dirname(path.dirname(fileURLToPath(import.meta.url))); const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); const fail=code=>{throw Object.assign(Error(code),{code});}; const targets={ksfStartWorkshopSession:'yKpUay6RDG',ksfGetIngestContract:'dWSrHp4FfQ'}; export async function deployIdentity({root=ROOT,request,apply=false}={}){ const manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/identity-manifest.json'),'utf8')); if(manifest.appId!=='FKieRbIoGt_main'||manifest.version!=='ksf.identity-deployment.v1'||JSON.stringify(manifest.functions.map(x=>x.name).sort())!==JSON.stringify(Object.keys(targets).sort()))fail('INVALID_DEPLOY_MANIFEST'); const entries=manifest.functions.map(x=>{ if(x.objectId!==targets[x.name]||x.path!=='/'+x.name||x.file!=='dist/'+x.name+'.js'||!/^[a-f0-9]{64}$/.test(x.beforeSha256))fail('INVALID_DEPLOY_MANIFEST'); const code=fs.readFileSync(path.join(root,x.file),'utf8');if(sha(code)!==x.sha256)fail('PACKAGE_HASH_MISMATCH');new Function(code);return{...x,code}; }); const beforeSchemas=await request('GET','/schemas');const before=[]; for(const x of entries){ const row=await request('GET','/classes/Function/'+x.objectId); if(row.name!==x.name||row.path!==x.path||typeof row.code!=='string')fail('FUNCTION_IDENTITY_MISMATCH'); const current=sha(row.code);if(![x.beforeSha256,x.sha256].includes(current))fail('UNKNOWN_FUNCTION_VERSION');before.push(row); } const journal={ok:true,at:new Date().toISOString(),readOnly:!apply,operation:apply?'identity-functions-published':'read-only-plan',functions:entries.map((x,i)=>({name:x.name,objectId:x.objectId,beforeSha256:sha(before[i].code),afterSha256:x.sha256,action:sha(before[i].code)===x.sha256?'unchanged':'update'})),schemaChanges:0,permissionChanges:0,businessWrites:0,frontendChanged:false,gatewayModified:false}; if(!apply)return journal; const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});const backup=path.join(evidence,'identity-functions-before-'+crypto.randomUUID()+'.json'); fs.writeFileSync(backup,JSON.stringify(before,null,2),{flag:'wx',mode:0o600});journal.backup=backup; const attempted=[]; try{ for(const [i,x]of entries.entries()){ if(journal.functions[i].action==='unchanged')continue; const row=await request('GET','/classes/Function/'+x.objectId);if(sha(row.code)!==sha(before[i].code))fail('FUNCTION_CHANGED_CONCURRENTLY'); attempted.push({x,prior:before[i]});await request('PUT','/classes/Function/'+x.objectId,{code:x.code}); const read=await request('GET','/classes/Function/'+x.objectId);if(read.code!==x.code)fail('FUNCTION_READBACK_MISMATCH');journal.functions[i].verified=true; } if(JSON.stringify((await request('GET','/schemas')).results)!==JSON.stringify(beforeSchemas.results))fail('SCHEMA_OR_PERMISSIONS_CHANGED'); journal.schemaAndPermissionsPreserved=true;journal.finishedAt=new Date().toISOString();fs.writeFileSync(path.join(evidence,'identity-deployment.json'),JSON.stringify(journal,null,2));return journal; }catch(error){ let complete=true; for(const {x,prior}of attempted.reverse())try{const row=await request('GET','/classes/Function/'+x.objectId);if(row.code===prior.code)continue;if(row.code!==x.code){complete=false;continue;}await request('PUT','/classes/Function/'+x.objectId,{code:prior.code});if((await request('GET','/classes/Function/'+x.objectId)).code!==prior.code)complete=false;}catch{complete=false;} const failure={...journal,ok:false,error:{code:error.code||'DEPLOY_FAILED'},rollbackComplete:complete};fs.writeFileSync(path.join(evidence,'identity-deployment.json'),JSON.stringify(failure,null,2));error.receipt=failure;throw error; } } if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ try{ if(process.argv.slice(2).some(x=>x!=='--apply')||process.argv.length>3)fail('USAGE'); const {request}=await import('./platform-client.mjs');console.log(JSON.stringify(await deployIdentity({request,apply:process.argv.includes('--apply')}),null,2)); }catch(e){console.error(JSON.stringify(e.receipt||{ok:false,error:{code:/^[A-Z][A-Z0-9_]+$/.test(e.code||'')?e.code:'DEPLOY_STOPPED',message:'停止部署;核对配置、版本和备份,未输出凭据或原始异常。'}}));process.exitCode=1;} }