| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 |
- 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=x=>crypto.createHash('sha256').update(x).digest('hex');
- const source=s=>s.replaceAll('\r\n','\n').replace(/^import .*;\n/gm,'').replace(/^export /gm,'');
- const baseline={
- ksfStartWorkshopSession:['yKpUay6RDG','08fb43393d88351e50e7b73d296d4675bbc22d266bc73ed22db57446fd702c68'],
- ksfSaveWorkshopTask:['f0faHuk5Br','5ff81e847857dd869522a065423978fc7744b3a80dad900bd18d58850d3fa531'],
- ksfGetPublicBoard:['inPfC51rvn','e5067736cd5c5f6d8b2244e2a7757ed23719dc0a85b9b2fba2c0e0b83cc437fb'],
- ksfGetIngestContract:['dWSrHp4FfQ','84aa7bfd086308f6d59711c4c4cd6e7a068688f7b98c7b4773ba0ef1580a3586']
- };
- export function patchAccountBinding(code){
- if(code.includes('// KSF_ACCOUNT_BINDING_V1'))return code;
- let next=code.replaceAll('\r\n','\n');
- const replace=(a,b)=>{if(next.split(a).length!==2)throw Error('ACCOUNT_PATCH_BASELINE_MISMATCH');next=next.replace(a,b);};
- replace('attribution:{participantName:p.participant.name,participantKey:',"attribution:{...(participantAccount?.platform==='fmode'?{accountProfileKey:p.participant.key}:{}),participantName:p.participant.name,participantKey:");
- const bootstrap=fs.readFileSync(path.join(root,'server/src/bootstrap.mjs'),'utf8').replaceAll('\r\n','\n');
- const page=bootstrap.slice(bootstrap.indexOf(" if(name==='ksfWorkshopAccount'"),bootstrap.indexOf(' attachBoardActionHandlers(app'));
- replace(" if(name==='ksfConfirmationPage'){"," attachAccountBindingHandlers(app,{store,settings});\n"+page+" if(name==='ksfConfirmationPage'){");
- replace(' const actor=await app.authenticate(request.headers);\n return response.json'," const actor=name==='ksfWorkshopAccount'&¶ms.action==='confirm'\n ?await app.authenticateWorkshopPlatform(request.headers):await app.authenticate(request.headers);\n return response.json");
- next='// KSF_ACCOUNT_BINDING_V1\n'+source(fs.readFileSync(path.join(root,'src/core/workshop-roster.js'),'utf8'))+'\nconst ACCOUNT_LINK_HTML='+JSON.stringify(fs.readFileSync(path.join(root,'server/web/account-link.html'),'utf8').replaceAll('\r\n','\n'))+';\n'+source(fs.readFileSync(path.join(root,'server/src/account-binding.mjs'),'utf8'))+'\n'+next;
- new Function(next);return next;
- }
- export async function deployAccountBinding({request,apply=false,outDir=path.join(root,'work/account-binding-private')}){
- const manifest=JSON.parse(fs.readFileSync(path.join(root,'deploy/account-binding-20261112/functions.json'),'utf8'));
- const plans=[];
- for(const[name,[id,hash]]of Object.entries(baseline)){
- const row=await request('GET','/classes/Function/'+id);
- if(row.name!==name||row.path!=='/'+name)throw Error('FUNCTION_IDENTITY_MISMATCH');
- const expected=manifest.functions.find(p=>p.name===name);
- if(!expected||expected.objectId!==id||expected.beforeSha256!==hash||![hash,expected.afterSha256].includes(sha(row.code)))throw Error('UNKNOWN_ACCOUNT_FUNCTION_VERSION');
- const code=patchAccountBinding(row.code);if(sha(code)!==expected.afterSha256)throw Error('ACCOUNT_PAYLOAD_HASH_MISMATCH');
- plans.push({row,code});
- }
- const name='ksfWorkshopAccount',existing=(await request('GET','/classes/Function?'+new URLSearchParams({where:JSON.stringify({name})}))).results;
- if(existing.length>1)throw Error('DUPLICATE_ACCOUNT_FUNCTION');
- const code=plans[0].code.replace('runDatabaseFunction("ksfStartWorkshopSession",request','runDatabaseFunction("ksfWorkshopAccount",request');
- if(!code.includes('runDatabaseFunction("ksfWorkshopAccount",request'))throw Error('NEW_FUNCTION_HANDLER_MISMATCH');new Function(code);
- if(sha(code)!==manifest.functions.find(p=>p.name===name)?.afterSha256)throw Error('ACCOUNT_PAYLOAD_HASH_MISMATCH');
- if(existing[0]&&existing[0].code!==code)throw Error('ACCOUNT_ENDPOINT_ALREADY_EXISTS');
- plans.push({row:existing[0]||{name,path:'/'+name,ACL:{}},code});
- const receipt={ok:true,readOnly:!apply,schemaChanges:0,permissionsChanged:false,staticBoardDeployed:false,functions:plans.map(p=>({name:p.row.name,objectId:p.row.objectId||null,beforeSha256:p.row.code?sha(p.row.code):null,afterSha256:sha(p.code),action:p.row.code===p.code?'unchanged':p.row.objectId?'update':'create'}))};
- if(!apply)return receipt;
- fs.mkdirSync(outDir,{recursive:true,mode:0o700});
- const backup=path.join(outDir,'deployment-before-'+crypto.randomUUID()+'.private.json');fs.writeFileSync(backup,JSON.stringify(plans.map(p=>p.row)),{mode:0o600,flag:'wx'});receipt.backup=backup;
- const done=[];
- try{
- // Publish the binding endpoint before enabling the new-session guard.
- for(const p of [...plans].reverse()){
- if(p.row.code===p.code)continue;
- if(p.row.objectId){const current=await request('GET','/classes/Function/'+p.row.objectId);if(current.code!==p.row.code)throw Error('CONCURRENT_FUNCTION_CHANGE');}
- const route='/classes/Function'+(p.row.objectId?'/'+p.row.objectId:'');
- const result=await request(p.row.objectId?'PUT':'POST',route,p.row.objectId?{code:p.code}:{...p.row,code:p.code});
- p.appliedId=p.row.objectId||result.objectId;done.push(p);
- const read=await request('GET','/classes/Function/'+p.appliedId);
- 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('FUNCTION_READBACK_MISMATCH');
- }
- receipt.readbackVerified=true;receipt.functions.forEach(p=>{p.objectId||=plans.find(x=>x.row.name===p.name).appliedId;});return receipt;
- }catch(e){
- let rollbackComplete=true;
- for(const p of done.reverse())try{const route='/classes/Function/'+p.appliedId,r=await request('GET',route);if(r.code!==p.code){rollbackComplete=false;continue;}if(p.row.objectId)await request('PUT',route,{code:p.row.code});else await request('DELETE',route);}catch{rollbackComplete=false;}
- throw Object.assign(Error('ACCOUNT_DEPLOYMENT_STOPPED'),{receipt:{...receipt,ok:false,rollbackComplete}});
- }
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
- try{if(!['plan','apply'].includes(process.argv[2])||process.argv.length!==3)throw Error('USAGE_PLAN_OR_APPLY');const{request}=await import('../server/scripts/platform-client.mjs');const r=await deployAccountBinding({request,apply:process.argv[2]==='apply'});fs.mkdirSync(path.join(root,'work/account-binding-private'),{recursive:true,mode:0o700});fs.writeFileSync(path.join(root,'work/account-binding-private/deploy-'+process.argv[2]+'.json'),JSON.stringify(r,null,2),{mode:0o600});console.log(JSON.stringify(r,null,2));}
- catch(e){console.error(JSON.stringify(e.receipt||{ok:false,error:/^[A-Z_]+$/.test(e.message)?e.message:'ACCOUNT_DEPLOYMENT_STOPPED'}));process.exitCode=1;}
- }
|