| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import {fileURLToPath} from 'node:url';
- const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
- const sha=s=>crypto.createHash('sha256').update(s).digest('hex');
- const normalized=s=>s.replaceAll('\r\n','\n');
- const source=s=>normalized(s).replace(/^import .*;\n/gm,'').replace(/^export /gm,'');
- export function patchMemberRoster(code){
- const priorCore=fs.readFileSync(path.join(root,'deploy/board-local-20261111/member-core-before.js'),'utf8');
- const priorReader=fs.readFileSync(path.join(root,'deploy/board-local-20261111/member-reader-before.mjs'),'utf8');
- const nextCore=source(fs.readFileSync(path.join(root,'src/core/workshop-roster.js'),'utf8'))+'\n'+source(fs.readFileSync(path.join(root,'src/core/member-groups.js'),'utf8'));
- const nextReader=source(fs.readFileSync(path.join(root,'server/src/member-groups.mjs'),'utf8'));
- let next=normalized(code);
- for(const [before,after]of [[source(priorCore),nextCore],[source(priorReader),nextReader]]){
- if(next.split(before).length!==2)throw Error('MEMBER_PATCH_BASELINE_MISMATCH');next=next.replace(before,after);
- }
- new Function(next);return next;
- }
- export async function deployMemberRoster({request,manifest,apply=false,outDir}){
- const plans=[];
- for(const def of manifest.functions){
- const row=await request('GET','/classes/Function/'+def.objectId);
- if(row.name!==def.name||row.path!==def.path||![def.beforeSha256,def.afterSha256].includes(sha(row.code)))throw Error('UNKNOWN_MEMBER_FUNCTION_BASELINE');
- const same=sha(row.code)===def.afterSha256,code=same?row.code:patchMemberRoster(row.code);
- if(sha(code)!==def.afterSha256)throw Error('MEMBER_PATCH_HASH_MISMATCH');plans.push({def,row,code,same});
- }
- const receipt={ok:true,readOnly:!apply,functions:plans.map(p=>({name:p.def.name,action:p.same?'unchanged':'update',beforeSha256:sha(p.row.code),afterSha256:sha(p.code)})),businessWrites:0,schemaChanges:0,permissionsChanged:false};
- if(!apply||plans.every(p=>p.same))return receipt;
- fs.mkdirSync(outDir,{recursive:true,mode:0o700});
- receipt.backup=path.join(outDir,'member-functions-before-'+crypto.randomUUID()+'.private.json');
- fs.writeFileSync(receipt.backup,JSON.stringify(plans.map(p=>p.row)),{flag:'wx',mode:0o600});
- const attempted=[];
- try{
- for(const p of plans){if(p.same)continue;const route='/classes/Function/'+p.def.objectId;
- if(sha((await request('GET',route)).code)!==p.def.beforeSha256)throw Error('CONCURRENT_MEMBER_FUNCTION_CHANGE');
- attempted.push(p);await request('PUT',route,{code:p.code});
- const read=await request('GET',route);if(read.code!==p.code||read.name!==p.row.name||read.path!==p.row.path||JSON.stringify(read.ACL)!==JSON.stringify(p.row.ACL))throw Error('MEMBER_FUNCTION_READBACK_MISMATCH');
- }
- return{...receipt,readbackVerified:true};
- }catch(error){
- let rollbackComplete=true;for(const p of attempted.reverse())try{
- const route='/classes/Function/'+p.def.objectId,row=await request('GET',route);
- if(row.code===p.row.code)continue;if(row.code!==p.code){rollbackComplete=false;continue;}
- await request('PUT',route,{code:p.row.code});if((await request('GET',route)).code!==p.row.code)rollbackComplete=false;
- }catch{rollbackComplete=false;}
- throw Object.assign(Error('MEMBER_DEPLOYMENT_STOPPED'),{receipt:{...receipt,ok:false,rollbackComplete}});
- }
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- try{
- const action=process.argv[2];if(!['plan','apply'].includes(action)||process.argv.length!==3)throw Error('USAGE_PLAN_OR_APPLY');
- let request;
- if(process.env.KSF_DEPLOY_AUTH_FILE){
- const cfg=JSON.parse(fs.readFileSync(process.env.KSF_DEPLOY_AUTH_FILE,'utf8'));
- if(cfg.appId!=='FKieRbIoGt_main'||!cfg.masterKey)throw Error('INVALID_PRIVATE_APPLICATION_CONFIG');
- const base=cfg.parseUrl||'https://kangshifu.fmode.cn/parse';if(base!=='https://kangshifu.fmode.cn/parse')throw Error('INVALID_PARSE_TARGET');
- request=async(method,route,body)=>{const r=await fetch(base+route,{method,headers:{'content-type':'application/json','X-Parse-Application-Id':cfg.appId,'X-Parse-Master-Key':cfg.masterKey},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(45000),redirect:'error'});if(!r.ok)throw Error('PARSE_DEPLOY_REQUEST_FAILED');return r.json();};
- }else({request}=await import('../server/scripts/platform-client.mjs'));
- const manifest=JSON.parse(fs.readFileSync(path.join(root,'deploy/board-local-20261111/member-functions.json')));
- console.log(JSON.stringify(await deployMemberRoster({request,manifest,apply:action==='apply',outDir:path.join(root,'work/board-local-deploy-private')}),null,2));
- }catch(e){console.error(JSON.stringify(e.receipt||{ok:false,error:e.message.match(/^[A-Z_]+$/)?e.message:'MEMBER_DEPLOYMENT_STOPPED'}));process.exitCode=1;}
- }
|