deploy-identity.mjs 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import {fileURLToPath} from 'node:url';
  5. const ROOT=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
  6. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  7. const fail=code=>{throw Object.assign(Error(code),{code});};
  8. const targets={ksfStartWorkshopSession:'yKpUay6RDG',ksfGetIngestContract:'dWSrHp4FfQ'};
  9. export async function deployIdentity({root=ROOT,request,apply=false}={}){
  10. const manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/identity-manifest.json'),'utf8'));
  11. 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');
  12. const entries=manifest.functions.map(x=>{
  13. 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');
  14. 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};
  15. });
  16. const beforeSchemas=await request('GET','/schemas');const before=[];
  17. for(const x of entries){
  18. const row=await request('GET','/classes/Function/'+x.objectId);
  19. if(row.name!==x.name||row.path!==x.path||typeof row.code!=='string')fail('FUNCTION_IDENTITY_MISMATCH');
  20. const current=sha(row.code);if(![x.beforeSha256,x.sha256].includes(current))fail('UNKNOWN_FUNCTION_VERSION');before.push(row);
  21. }
  22. 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};
  23. if(!apply)return journal;
  24. const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});const backup=path.join(evidence,'identity-functions-before-'+crypto.randomUUID()+'.json');
  25. fs.writeFileSync(backup,JSON.stringify(before,null,2),{flag:'wx',mode:0o600});journal.backup=backup;
  26. const attempted=[];
  27. try{
  28. for(const [i,x]of entries.entries()){
  29. if(journal.functions[i].action==='unchanged')continue;
  30. const row=await request('GET','/classes/Function/'+x.objectId);if(sha(row.code)!==sha(before[i].code))fail('FUNCTION_CHANGED_CONCURRENTLY');
  31. attempted.push({x,prior:before[i]});await request('PUT','/classes/Function/'+x.objectId,{code:x.code});
  32. const read=await request('GET','/classes/Function/'+x.objectId);if(read.code!==x.code)fail('FUNCTION_READBACK_MISMATCH');journal.functions[i].verified=true;
  33. }
  34. if(JSON.stringify((await request('GET','/schemas')).results)!==JSON.stringify(beforeSchemas.results))fail('SCHEMA_OR_PERMISSIONS_CHANGED');
  35. journal.schemaAndPermissionsPreserved=true;journal.finishedAt=new Date().toISOString();fs.writeFileSync(path.join(evidence,'identity-deployment.json'),JSON.stringify(journal,null,2));return journal;
  36. }catch(error){
  37. let complete=true;
  38. 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;}
  39. 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;
  40. }
  41. }
  42. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  43. try{
  44. if(process.argv.slice(2).some(x=>x!=='--apply')||process.argv.length>3)fail('USAGE');
  45. const {request}=await import('./platform-client.mjs');console.log(JSON.stringify(await deployIdentity({request,apply:process.argv.includes('--apply')}),null,2));
  46. }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;}
  47. }