#!/usr/bin/env node // Node 22+. Download waveform-client.mjs beside this file. Never prints credentials. import {open,readFile,writeFile,rename,unlink} from 'node:fs/promises'; import {createHash,randomUUID} from 'node:crypto'; import {WaveformClient} from './waveform-client.mjs'; const operations={"identity_get":{"method":"GET","path":"/api/v1/identity","keyed":false,"binary":false},"generations_list":{"method":"GET","path":"/api/v1/generations","keyed":false,"binary":false},"generation_get":{"method":"GET","path":"/api/v1/generations/:id","keyed":false,"binary":false},"generation_request_get":{"method":"GET","path":"/api/v1/generation-requests/:requestKey","keyed":false,"binary":false},"artifact_manifest":{"method":"GET","path":"/api/v1/generations/:id/artifact-manifest","keyed":false,"binary":false},"source_get":{"method":"GET","path":"/api/v1/generations/:id/source","keyed":false,"binary":false},"quality_get":{"method":"GET","path":"/api/v1/generations/:id/quality","keyed":false,"binary":false},"generation_create":{"method":"POST","path":"/api/v1/generations","keyed":true,"binary":false},"designs_review":{"method":"POST","path":"/api/v1/generations/:id/review-designs","keyed":true,"binary":false},"design_edit":{"method":"POST","path":"/api/v1/generations/:id/edit-design","keyed":true,"binary":false},"design_select":{"method":"POST","path":"/api/v1/generations/:id/select-design","keyed":true,"binary":false},"source_edit":{"method":"POST","path":"/api/v1/generations/:id/edit","keyed":true,"binary":false},"generation_finalize":{"method":"POST","path":"/api/v1/generations/:id/finalize","keyed":true,"binary":false},"generation_continue":{"method":"POST","path":"/api/v1/generations/:id/continue","keyed":true,"binary":false},"generation_cancel":{"method":"POST","path":"/api/v1/generations/:id/cancel","keyed":false,"binary":false},"hosting_get":{"method":"GET","path":"/api/v1/pages/:pageId/hosting","keyed":false,"binary":false},"page_publish":{"method":"POST","path":"/api/v1/generations/:id/publish","keyed":false,"binary":false},"page_unpublish":{"method":"POST","path":"/api/v1/pages/:pageId/unpublish","keyed":false,"binary":false},"site_task_start":{"method":"POST","path":"/api/v1/site-tasks","keyed":true,"binary":false},"site_task_get":{"method":"GET","path":"/api/v1/site-tasks/:id","keyed":false,"binary":false},"site_tasks_list":{"method":"GET","path":"/api/v1/pages/:pageId/site-tasks","keyed":false,"binary":false},"site_task_cancel":{"method":"POST","path":"/api/v1/site-tasks/:id/cancel","keyed":true,"binary":false},"site_for_page":{"method":"GET","path":"/api/v1/pages/:pageId/site","keyed":false,"binary":false},"site_create":{"method":"POST","path":"/api/v1/sites","keyed":true,"binary":false},"site_get":{"method":"GET","path":"/api/v1/sites/:id","keyed":false,"binary":false},"site_history":{"method":"GET","path":"/api/v1/sites/:id/history","keyed":false,"binary":false},"site_edit":{"method":"PUT","path":"/api/v1/sites/:id/scope","keyed":true,"binary":false},"site_facts":{"method":"POST","path":"/api/v1/sites/:id/facts","keyed":true,"binary":false},"site_approve":{"method":"POST","path":"/api/v1/sites/:id/approve","keyed":true,"binary":false},"site_propose":{"method":"POST","path":"/api/v1/sites/:id/proposals","keyed":true,"binary":false},"site_proposals":{"method":"GET","path":"/api/v1/sites/:id/proposals","keyed":false,"binary":false},"site_job_get":{"method":"GET","path":"/api/v1/site-jobs/:id","keyed":false,"binary":false},"site_job_cancel":{"method":"POST","path":"/api/v1/site-jobs/:id/cancel","keyed":true,"binary":false},"site_proposal_apply":{"method":"POST","path":"/api/v1/site-jobs/:id/apply","keyed":true,"binary":false},"site_posts":{"method":"GET","path":"/api/v1/sites/:id/posts","keyed":false,"binary":false},"site_post_get":{"method":"GET","path":"/api/v1/sites/:id/posts/:postId","keyed":false,"binary":false},"site_post_history":{"method":"GET","path":"/api/v1/sites/:id/posts/:postId/history","keyed":false,"binary":false},"site_post_create":{"method":"POST","path":"/api/v1/sites/:id/posts","keyed":true,"binary":false},"site_post_edit":{"method":"PUT","path":"/api/v1/sites/:id/posts/:postId","keyed":true,"binary":false},"site_posts_select":{"method":"POST","path":"/api/v1/sites/:id/blog-selection","keyed":true,"binary":false},"site_build":{"method":"POST","path":"/api/v1/sites/:id/builds","keyed":true,"binary":false},"site_revision_options":{"method":"GET","path":"/api/v1/sites/:id/revision-options","keyed":false,"binary":false},"site_revise":{"method":"POST","path":"/api/v1/sites/:id/revisions","keyed":true,"binary":false},"site_builds":{"method":"GET","path":"/api/v1/sites/:id/builds","keyed":false,"binary":false},"site_build_get":{"method":"GET","path":"/api/v1/site-builds/:id","keyed":false,"binary":false},"billing_summary":{"method":"GET","path":"/api/v1/billing/summary","keyed":false,"binary":false},"billing_receipt":{"method":"GET","path":"/api/v1/billing/usage/:id","keyed":false,"binary":false},"billing_quote":{"method":"POST","path":"/api/v1/billing/quote","keyed":false,"binary":false},"site_release_options":{"method":"GET","path":"/api/v1/sites/:id/release-options","keyed":false,"binary":false},"site_releases":{"method":"GET","path":"/api/v1/sites/:id/releases","keyed":false,"binary":false},"site_release_create":{"method":"POST","path":"/api/v1/sites/:id/releases","keyed":true,"binary":false},"site_release_get":{"method":"GET","path":"/api/v1/sites/:id/releases/:releaseId","keyed":false,"binary":false},"site_release_download":{"method":"GET","path":"/api/v1/sites/:id/releases/:releaseId/download","keyed":false,"binary":true},"site_publication_get":{"method":"GET","path":"/api/v1/sites/:id/publication","keyed":false,"binary":false},"site_publish":{"method":"POST","path":"/api/v1/sites/:id/publication","keyed":true,"binary":false},"webhooks_list":{"method":"GET","path":"/api/v1/integrations/webhooks","keyed":false,"binary":false},"webhook_get":{"method":"GET","path":"/api/v1/integrations/webhooks/:id","keyed":false,"binary":false},"webhook_create":{"method":"POST","path":"/api/v1/integrations/webhooks","keyed":true,"binary":false},"webhook_update":{"method":"PUT","path":"/api/v1/integrations/webhooks/:id","keyed":false,"binary":false},"webhook_deliveries":{"method":"GET","path":"/api/v1/integrations/webhooks/:id/deliveries","keyed":false,"binary":false},"webhook_retry":{"method":"POST","path":"/api/v1/integrations/webhooks/:id/deliveries/:deliveryId/retry","keyed":false,"binary":false},"domain_connect":{"method":"POST","path":"/api/v1/pages/:pageId/domains","keyed":false,"binary":false},"domain_check":{"method":"POST","path":"/api/v1/pages/:pageId/domains/:domainId/check","keyed":false,"binary":false},"domain_disconnect":{"method":"DELETE","path":"/api/v1/pages/:pageId/domains/:domainId","keyed":false,"binary":false},"forms_list":{"method":"GET","path":"/api/v1/pages/:pageId/forms","keyed":false,"binary":false},"form_create":{"method":"POST","path":"/api/v1/pages/:pageId/forms","keyed":false,"binary":false},"form_update":{"method":"PUT","path":"/api/v1/pages/:pageId/forms/:formId","keyed":false,"binary":false},"form_copy":{"method":"POST","path":"/api/v1/pages/:pageId/forms/:formId/copy","keyed":false,"binary":false},"leads_list":{"method":"GET","path":"/api/v1/pages/:pageId/forms/:formId/leads","keyed":false,"binary":false},"form_test":{"method":"POST","path":"/api/v1/pages/:pageId/forms/:formId/test","keyed":true,"binary":false},"lead_retry":{"method":"POST","path":"/api/v1/pages/:pageId/forms/:formId/leads/:leadId/retry","keyed":false,"binary":false},"reference_read":{"method":"GET","path":"/api/v1/generations/:id/reference","keyed":false,"binary":true},"design_image_read":{"method":"GET","path":"/api/v1/generations/:id/designs/:candidateId/image","keyed":false,"binary":true},"brand_logo_read":{"method":"GET","path":"/api/v1/generations/:id/brand/logo","keyed":false,"binary":true},"download_read":{"method":"GET","path":"/api/v1/generations/:id/download","keyed":false,"binary":true}}; let origin;try{origin=new URL(process.env.WAVEFORM_ORIGIN??'');if(origin.username||origin.password||origin.pathname!=='/'||origin.search||origin.hash||!(origin.protocol==='https:'||origin.protocol==='http:'&&['localhost','127.0.0.1','[::1]'].includes(origin.hostname)))throw Error();}catch{console.error('Use the trusted HTTPS studio origin (HTTP loopback is allowed locally).');process.exit(1);} const key=process.env.WAVEFORM_API_KEY;if(!key)throw Error('Set WAVEFORM_API_KEY privately.'); const client=new WaveformClient(origin.origin,()=>({Authorization:'Bearer '+key})); const call=(name,input,timeout=30000)=>client.call(name,input,AbortSignal.timeout(timeout)); const load=async file=>JSON.parse(await readFile(file,'utf8')); const fingerprint=v=>createHash('sha256').update(JSON.stringify([v.origin,v.workspaceId,v.operation,v.input])).digest('hex'); async function save(file,value,fresh=false){const bytes=JSON.stringify(value,null,2)+'\n';if(fresh)return writeFile(file,bytes,{flag:'wx',mode:0o600});const temp=file+'.'+randomUUID()+'.tmp';try{await writeFile(temp,bytes,{flag:'wx',mode:0o600});await rename(temp,file);}finally{await unlink(temp).catch(()=>{});}} async function locked(file,run){const lock=await open(file+'.lock','wx',0o600);try{return await run();}finally{await lock.close();await unlink(file+'.lock');}} const [command,a,b]=process.argv.slice(2); try{ if(command==='prepare'){ if(!a||!b)throw Error('prepare REQUEST.json INTENT.json'); const request=await load(a),op=operations[request.operation];if(!op||op.method==='GET')throw Error('Choose one installed bearer mutation. Use read for JSON reads.'); const input=request.input;if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Request needs input object.'); if(op.keyed&&!input.idempotencyKey)input.idempotencyKey=randomUUID(); const identity=await call('identity_get',{}),intent={version:1,driver:'javascript',origin:origin.origin,workspaceId:identity.workspaceId,operation:request.operation,input,attempted:false}; await save(b,{...intent,fingerprint:fingerprint(intent)},true);console.log('Prepared '+b+' for workspace '+identity.workspaceId+'. No mutation sent.'); }else if(command==='send'||command==='recover'){ if(!a)throw Error('send/recover INTENT.json'); await locked(a,async()=>{const intent=await load(a),op=operations[intent.operation]; if(intent.version!==1||intent.driver!=='javascript'||intent.origin!==origin.origin||fingerprint(intent)!==intent.fingerprint||!op)throw Error('Intent changed or belongs to another origin/runner.'); const identity=await call('identity_get',{});if(identity.workspaceId!==intent.workspaceId)throw Error('Credential workspace differs from saved intent.'); if(Object.hasOwn(intent,'result')){console.log(JSON.stringify(intent.result,null,2));return;} if(intent.attempted&&(command!=='recover'||!op.keyed||!intent.input.idempotencyKey))throw Error('Acceptance is uncertain. Read saved state; only the identical keyed intent can use recover.'); intent.attempted=true;await save(a,intent);const result=await call(intent.operation,intent.input);intent.result=result;await save(a,intent);console.log(JSON.stringify(result,null,2)); }); }else if(command==='read'){ if(!a)throw Error('read REQUEST.json');const request=await load(a),op=operations[request.operation];if(!op||op.method!=='GET'||op.binary)throw Error('read accepts only an installed JSON GET operation.');console.log(JSON.stringify(await call(request.operation,request.input),null,2)); }else if(command==='status'||command==='watch'){ if(!a)throw Error('status/watch GENERATION_ID');const deadline=Date.now()+300000; while(true){const result=await call('generation_get',{params:{id:a}},Math.min(30000,Math.max(1,deadline-Date.now())));console.log(JSON.stringify(result));if(command==='status'||!['queued','running'].includes(result.state))break;if(Date.now()>=deadline){console.error('Local wait ended; saved job continues.');break;}await new Promise(resolve=>setTimeout(resolve,Math.min(3000,deadline-Date.now())));if(Date.now()>=deadline){console.error('Local wait ended; saved job continues.');break;}} }else if(command==='cancel'){if(!a)throw Error('cancel GENERATION_ID');console.log(JSON.stringify(await call('generation_cancel',{params:{id:a}})));} else if(command==='download'){if(!a||!b)throw Error('download GENERATION_ID OUTPUT.zip');const response=await call('download_read',{params:{id:a}});await writeFile(b,new Uint8Array(await response.arrayBuffer()),{flag:'wx',mode:0o600});console.log('Saved '+b);} else throw Error('Use prepare REQUEST.json INTENT.json | send/recover INTENT.json | read REQUEST.json | status/watch/cancel GENERATION_ID | download GENERATION_ID OUTPUT.zip'); }catch(error){console.error(error.code?error.code+': '+error.message:error.message);process.exitCode=1;}