import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import crypto from 'node:crypto'; import {fileURLToPath} from 'node:url'; import {COMPANY,APP,AGENT,HOST,WRAPPER_SHA,encode,sha,need,plain,privateDirectory,writeNew,readPrivate,recipientInfo,argsOf,errorReceipt} from './common.mjs'; export function exportRecipient({keyDirectory,output},{dataDir='/opt/data',hostname=os.hostname(),platform=process.platform}={}){ need(platform==='linux','LINUX_REQUIRED');need(hostname===HOST,'WRONG_CONTAINER'); const current=JSON.parse(readPrivate(path.join(dataDir,'.fmode/config/enterprise/current.json'))); need(current.companyObjectId===COMPANY&¤t.appId===APP&¤t.provisionStatus==='success','ENTERPRISE_MISMATCH'); const skill=path.join(dataDir,'skills/ksf-workshop-runtime'); need(fs.readFileSync(path.join(skill,'SKILL.md'),'utf8').includes('version: r13'),'SKILL_VERSION_MISMATCH'); need(sha(fs.readFileSync(path.join(skill,'scripts/ksf-secure.mjs')))===WRAPPER_SHA,'WRAPPER_CHANGED'); output=plain(output);dataDir=plain(dataDir); need(output.startsWith(dataDir+path.sep)&&!output.startsWith(path.join(dataDir,'skills')+path.sep),'UNSAFE_PUBLIC_OUTPUT'); if(!keyDirectory){ if(fs.existsSync(output))keyDirectory=recipientInfo(JSON.parse(fs.readFileSync(output))).keyDirectory; else{ const created=fs.mkdtempSync(path.join(dataDir,'ksf-key-receive-20261031-')); fs.chmodSync(created,0o700);keyDirectory='/opt/data/'+path.basename(created); } } need(/^\/opt\/data\/ksf-key-receive-20261031-[A-Za-z0-9]+$/.test(keyDirectory),'BAD_KEY_DIRECTORY'); const localDirectory=path.join(dataDir,path.posix.basename(keyDirectory)); privateDirectory(localDirectory); need(!output.startsWith(localDirectory+path.sep),'UNSAFE_PUBLIC_OUTPUT'); const privateFile=path.join(localDirectory,'recipient-private.pem'); let privateKey; if(fs.existsSync(privateFile)){ const raw=readPrivate(privateFile);try{privateKey=crypto.createPrivateKey(raw);}finally{raw.fill(0);} }else{ need(fs.readdirSync(localDirectory).length===0,'EXISTING_KEY_DIRECTORY_NOT_EMPTY'); const pair=crypto.generateKeyPairSync('rsa',{modulusLength:3072});privateKey=pair.privateKey; const bytes=Buffer.from(privateKey.export({type:'pkcs8',format:'pem'})); try{writeNew(privateFile,bytes);}finally{bytes.fill(0);} } const publicKey=crypto.createPublicKey(privateKey); const value={schema:'ksf.enterprise-workshop-recipient.v1',publicOnly:true,companyObjectId:COMPANY,appId:APP, agentId:AGENT,host:hostname,keyDirectory,publicKeyPem:publicKey.export({type:'spki',format:'pem'}), publicKeySha256:sha(publicKey.export({type:'spki',format:'der'})),privateKeyExported:false,tokenExported:false}; recipientInfo(value); const bytes=encode(value); if(fs.existsSync(output))need(fs.readFileSync(output).equals(bytes),'PUBLIC_RECIPIENT_CHANGED'); else writeNew(output,bytes); return{ok:true,publicRecipientFile:output,sha256:sha(bytes),host:hostname, privateKeyExported:false,tokenExported:false,businessRecordWrites:0}; } if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){ try{const a=argsOf(process.argv.slice(2),['--out','--key-directory']);need(a['--out'],'USAGE'); console.log(JSON.stringify(exportRecipient({output:a['--out'],keyDirectory:a['--key-directory']}),null,2)); }catch(error){console.error(JSON.stringify(errorReceipt(error)));process.exitCode=1;} }