| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 |
- 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={ksfRegisterSource:'czT35BZZlx',ksfValidateImport:'ra9OWsmbtC',ksfSubmitImport:'gPjyoZbHhU',ksfGetIngestContract:'dWSrHp4FfQ',ksfCheckDuplicates:'JDNsVUCXSo'};
- export async function deployDedup({root=ROOT,request,apply=false}={}){
- const manifest=JSON.parse(fs.readFileSync(path.join(root,'dist/dedup-manifest.json'),'utf8'));
- if(manifest.version!=='ksf.dedup-deployment.v1'||manifest.appId!=='FKieRbIoGt_main'||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'||!Array.isArray(x.beforeSha256)||x.beforeSha256.some(h=>!/^[a-f0-9]{64}$/.test(h)))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 find=async x=>{
- const query=new URLSearchParams({where:JSON.stringify({$or:[{name:x.name},{path:x.path}]}),limit:'3'});
- const rows=(await request('GET','/classes/Function?'+query)).results;
- if(!Array.isArray(rows)||rows.length>1)fail('FUNCTION_IDENTITY_MISMATCH');
- const row=rows[0];if(row&&(row.name!==x.name||row.path!==x.path||typeof row.code!=='string'||x.objectId&&row.objectId!==x.objectId))fail('FUNCTION_IDENTITY_MISMATCH');return row||null;
- };
- const schemas=await request('GET','/schemas'),before=[];
- for(const x of entries){const row=await find(x);if(x.objectId&&!row)fail('FUNCTION_IDENTITY_MISMATCH');if(row&&![...x.beforeSha256,x.sha256].includes(sha(row.code)))fail('UNKNOWN_FUNCTION_VERSION');before.push(row);}
- const journal={ok:true,readOnly:!apply,operation:apply?'dedup-functions-published':'read-only-plan',schemaChanges:0,permissionChanges:0,businessWrites:0,functionCodeWrites:0,frontendChanged:false,gatewayModified:false,functions:entries.map((x,i)=>({name:x.name,objectId:before[i]?.objectId||null,beforeSha256:before[i]?sha(before[i].code):null,afterSha256:x.sha256,action:before[i]?(before[i].code===x.code?'unchanged':'update'):'create'}))};
- if(!apply)return journal;
- const evidence=path.join(root,'evidence');fs.mkdirSync(evidence,{recursive:true});journal.backup=path.join(evidence,'dedup-functions-before-'+crypto.randomUUID()+'.json');fs.writeFileSync(journal.backup,JSON.stringify(before,null,2),{flag:'wx',mode:0o600});
- const attempted=[];
- try{
- for(const [i,x]of entries.entries()){
- if(journal.functions[i].action==='unchanged')continue;
- const current=await find(x),prior=before[i];if((current?.code||null)!==(prior?.code||null))fail('FUNCTION_CHANGED_CONCURRENTLY');
- const item={x,prior,id:prior?.objectId||null};attempted.push(item);
- if(prior)await request('PUT','/classes/Function/'+item.id,{code:x.code});
- else item.id=(await request('POST','/classes/Function',{name:x.name,path:x.path,code:x.code,ACL:{}})).objectId;
- if(!item.id)fail('FUNCTION_CREATE_UNCERTAIN');
- journal.functionCodeWrites++;const read=await request('GET','/classes/Function/'+item.id);if(read.code!==x.code||read.name!==x.name||read.path!==x.path)fail('FUNCTION_READBACK_MISMATCH');Object.assign(journal.functions[i],{objectId:item.id,verified:true});
- }
- if(JSON.stringify((await request('GET','/schemas')).results)!==JSON.stringify(schemas.results))fail('SCHEMA_OR_PERMISSIONS_CHANGED');
- journal.schemaAndPermissionsPreserved=true;journal.finishedAt=new Date().toISOString();fs.writeFileSync(path.join(evidence,'dedup-deployment.json'),JSON.stringify(journal,null,2),{mode:0o600});return journal;
- }catch(error){
- let complete=true;
- for(const item of attempted.reverse())try{
- const {x,prior}=item;
- // A lost create response has no trusted objectId: preserve it for manual reconciliation.
- if(!item.id){complete=false;continue;}
- const current=await request('GET','/classes/Function/'+item.id);
- if(prior&¤t.code===prior.code)continue;
- if(current.name!==x.name||current.path!==x.path||current.code!==x.code){complete=false;continue;}
- if(prior){await request('PUT','/classes/Function/'+item.id,{code:prior.code});if((await request('GET','/classes/Function/'+item.id)).code!==prior.code)complete=false;}
- else await request('DELETE','/classes/Function/'+item.id);
- }catch{complete=false;}
- error.receipt={...journal,ok:false,error:{code:error.code||'DEPLOY_FAILED'},rollbackComplete:complete};fs.writeFileSync(path.join(evidence,'dedup-deployment.json'),JSON.stringify(error.receipt,null,2),{mode:0o600});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 deployDedup({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;}
- }
|