Connect durable canvas runtime to canonical Foundry protocol
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/** Compile and exercise the actual Next route in a disposable frontend project. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'node:http';
|
||||
import { mkdtemp, mkdir, copyFile, writeFile, symlink, rm } from 'node:fs/promises';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
|
||||
const source=resolve(import.meta.dirname,'../..'),directory=await mkdtemp(join(tmpdir(),'endoleap-proxy-check-')),received=[];
|
||||
const account='10000000-0000-4000-8000-000000000004',key='90000000-0000-4000-8000-000000000002';
|
||||
const responseBytes=' { "protocol_version":2, "request_id":"'+key+'", "status":"not-applied", "terminal":true, "error":"fixture_rejection", "extension":{"retained":true} }\n';
|
||||
const upstream=createServer(async(request,response)=>{
|
||||
const chunks=[];for await(const chunk of request)chunks.push(chunk);
|
||||
received.push({path:request.url,body:Buffer.concat(chunks),headers:request.headers});
|
||||
response.writeHead(request.url.endsWith('/model/operations')?409:200,{'Content-Type':'application/json'});
|
||||
response.end(request.url.endsWith('/model/operations')?responseBytes:'{"id":"fixture"}');
|
||||
});
|
||||
await new Promise(resolve=>upstream.listen(0,'127.0.0.1',resolve));
|
||||
const reservation=createServer();await new Promise(resolve=>reservation.listen(0,'127.0.0.1',resolve));const port=reservation.address().port;await new Promise(resolve=>reservation.close(resolve));
|
||||
let next,logs='',launchError;
|
||||
try{
|
||||
for(const filename of ['package.json','tsconfig.json','src/app/api/foundry/[...path]/route.ts','src/lib/canvas-response.ts','src/lib/session-cookie.ts']){
|
||||
const target=join(directory,filename);await mkdir(resolve(target,'..'),{recursive:true});await copyFile(join(source,filename),target);
|
||||
}
|
||||
await symlink(join(source,'node_modules'),join(directory,'node_modules'));
|
||||
await writeFile(join(directory,'src/app/layout.tsx'),'export default function Layout({children}:{children:React.ReactNode}){return <html><body>{children}</body></html>}');
|
||||
await writeFile(join(directory,'src/app/page.tsx'),'export default function Page(){return <main>Proxy verification fixture</main>}');
|
||||
next=spawn(process.execPath,[join(source,'node_modules/next/dist/bin/next'),'dev','--webpack','--hostname','127.0.0.1','--port',String(port)],{cwd:directory,detached:true,env:{...process.env,NEXT_TELEMETRY_DISABLED:'1',FOUNDRY_API_ORIGIN:`http://127.0.0.1:${upstream.address().port}`,NEXT_PUBLIC_FOUNDRY_PROJECT:'fixture'},stdio:['ignore','pipe','pipe']});
|
||||
next.stdout.on('data',chunk=>{logs=(logs+chunk).slice(-12000)});next.stderr.on('data',chunk=>{logs=(logs+chunk).slice(-12000)});next.on('error',error=>{launchError=error});
|
||||
let ready=false;
|
||||
for(let attempt=0;attempt<90;attempt++){
|
||||
if(launchError)throw launchError;if(next.exitCode!==null)throw new Error('Next exited during startup:\n'+logs);
|
||||
try{const response=await fetch(`http://127.0.0.1:${port}/api/foundry/profile`,{signal:AbortSignal.timeout(1500)});await response.text();if(response.status===200){ready=true;break}}catch{}
|
||||
await pause(250);
|
||||
}
|
||||
if(!ready)throw new Error('Next did not become ready:\n'+logs);
|
||||
const path='workspaces/10000000-0000-4000-8000-000000000003/materials/10000000-0000-4000-8000-000000000001/canvas/model/operations';
|
||||
// More than the generic 1 MiB proxy cap, below the canonical 5 MiB cap.
|
||||
const body=' {\n "protocol_version":2,"request_id":"'+key+'","fixture":"'+ 'x'.repeat(1100000)+'"\n} ';
|
||||
const response=await fetch(`http://127.0.0.1:${port}/api/foundry/${path}`,{method:'POST',body,headers:{'Content-Type':'application/json','Idempotency-Key':key,'X-Expected-Account':account,Cookie:'foundry_session=fixture-session',Origin:`http://127.0.0.1:${port}`},signal:AbortSignal.timeout(20000)});
|
||||
assert.equal(response.status,409);assert.equal(await response.text(),responseBytes);
|
||||
const request=received.find(item=>item.path.endsWith('/model/operations'));
|
||||
assert.ok(request);assert.equal(request.body.toString(),body);assert.equal(request.headers['idempotency-key'],key);assert.equal(request.headers['x-expected-account'],account);assert.equal(request.headers.authorization,'Bearer fixture-session');
|
||||
const before=received.length,invalid=await fetch(`http://127.0.0.1:${port}/api/foundry/${path}/unexpected/extra`,{signal:AbortSignal.timeout(10000)});await invalid.text();assert.equal(invalid.status,404);assert.equal(received.length,before);
|
||||
console.log('PASS actual Next proxy preserves frozen request bytes/key/account and raw terminal receipt above 1 MiB');
|
||||
console.log('PASS canonical route allowlist rejects unexpected paths without contacting the upstream');
|
||||
}finally{
|
||||
if(next?.pid){const exited=new Promise(resolve=>{if(next.exitCode!==null)resolve();else next.once('exit',resolve)});try{process.kill(-next.pid,'SIGTERM')}catch{}await Promise.race([exited,pause(3000)]);if(next.exitCode===null){try{process.kill(-next.pid,'SIGKILL')}catch{}}}
|
||||
upstream.closeAllConnections();await new Promise(resolve=>upstream.close(resolve));await rm(directory,{recursive:true,force:true,maxRetries:5});
|
||||
}
|
||||
Reference in New Issue
Block a user