export-recipient.mjs 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import os from 'node:os';
  4. import crypto from 'node:crypto';
  5. import {fileURLToPath} from 'node:url';
  6. import {COMPANY,APP,AGENT,HOST,WRAPPER_SHA,encode,sha,need,plain,privateDirectory,writeNew,readPrivate,recipientInfo,argsOf,errorReceipt} from './common.mjs';
  7. export function exportRecipient({keyDirectory,output},{dataDir='/opt/data',hostname=os.hostname(),platform=process.platform}={}){
  8. need(platform==='linux','LINUX_REQUIRED');need(hostname===HOST,'WRONG_CONTAINER');
  9. const current=JSON.parse(readPrivate(path.join(dataDir,'.fmode/config/enterprise/current.json')));
  10. need(current.companyObjectId===COMPANY&&current.appId===APP&&current.provisionStatus==='success','ENTERPRISE_MISMATCH');
  11. const skill=path.join(dataDir,'skills/ksf-workshop-runtime');
  12. need(fs.readFileSync(path.join(skill,'SKILL.md'),'utf8').includes('version: r13'),'SKILL_VERSION_MISMATCH');
  13. need(sha(fs.readFileSync(path.join(skill,'scripts/ksf-secure.mjs')))===WRAPPER_SHA,'WRAPPER_CHANGED');
  14. output=plain(output);dataDir=plain(dataDir);
  15. need(output.startsWith(dataDir+path.sep)&&!output.startsWith(path.join(dataDir,'skills')+path.sep),'UNSAFE_PUBLIC_OUTPUT');
  16. if(!keyDirectory){
  17. if(fs.existsSync(output))keyDirectory=recipientInfo(JSON.parse(fs.readFileSync(output))).keyDirectory;
  18. else{
  19. const created=fs.mkdtempSync(path.join(dataDir,'ksf-key-receive-20261031-'));
  20. fs.chmodSync(created,0o700);keyDirectory='/opt/data/'+path.basename(created);
  21. }
  22. }
  23. need(/^\/opt\/data\/ksf-key-receive-20261031-[A-Za-z0-9]+$/.test(keyDirectory),'BAD_KEY_DIRECTORY');
  24. const localDirectory=path.join(dataDir,path.posix.basename(keyDirectory));
  25. privateDirectory(localDirectory);
  26. need(!output.startsWith(localDirectory+path.sep),'UNSAFE_PUBLIC_OUTPUT');
  27. const privateFile=path.join(localDirectory,'recipient-private.pem');
  28. let privateKey;
  29. if(fs.existsSync(privateFile)){
  30. const raw=readPrivate(privateFile);try{privateKey=crypto.createPrivateKey(raw);}finally{raw.fill(0);}
  31. }else{
  32. need(fs.readdirSync(localDirectory).length===0,'EXISTING_KEY_DIRECTORY_NOT_EMPTY');
  33. const pair=crypto.generateKeyPairSync('rsa',{modulusLength:3072});privateKey=pair.privateKey;
  34. const bytes=Buffer.from(privateKey.export({type:'pkcs8',format:'pem'}));
  35. try{writeNew(privateFile,bytes);}finally{bytes.fill(0);}
  36. }
  37. const publicKey=crypto.createPublicKey(privateKey);
  38. const value={schema:'ksf.enterprise-workshop-recipient.v1',publicOnly:true,companyObjectId:COMPANY,appId:APP,
  39. agentId:AGENT,host:hostname,keyDirectory,publicKeyPem:publicKey.export({type:'spki',format:'pem'}),
  40. publicKeySha256:sha(publicKey.export({type:'spki',format:'der'})),privateKeyExported:false,tokenExported:false};
  41. recipientInfo(value);
  42. const bytes=encode(value);
  43. if(fs.existsSync(output))need(fs.readFileSync(output).equals(bytes),'PUBLIC_RECIPIENT_CHANGED');
  44. else writeNew(output,bytes);
  45. return{ok:true,publicRecipientFile:output,sha256:sha(bytes),host:hostname,
  46. privateKeyExported:false,tokenExported:false,businessRecordWrites:0};
  47. }
  48. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
  49. try{const a=argsOf(process.argv.slice(2),['--out','--key-directory']);need(a['--out'],'USAGE');
  50. console.log(JSON.stringify(exportRecipient({output:a['--out'],keyDirectory:a['--key-directory']}),null,2));
  51. }catch(error){console.error(JSON.stringify(errorReceipt(error)));process.exitCode=1;}
  52. }