agent-device-proxy 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/ARCHITECTURE.md +263 -0
- package/README.md +43 -159
- package/dist/src/224.js +1 -1
- package/dist/src/36.js +12 -18
- package/dist/src/agent-proxy-server.d.ts +2 -0
- package/dist/src/bin.js +14 -2
- package/dist/src/client.d.ts +2 -0
- package/dist/src/dev.d.ts +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +1 -1
- package/dist/src/server/config.d.ts +15 -0
- package/dist/src/server/daemon-forward.d.ts +12 -0
- package/dist/src/server/daemon-state.d.ts +6 -0
- package/dist/src/server/errors.d.ts +9 -0
- package/dist/src/server/http.d.ts +9 -0
- package/dist/src/server/metro-bridge-descriptor.d.ts +4 -0
- package/dist/src/server/metro-bridge-proxy.d.ts +8 -0
- package/dist/src/server/metro-bridge-types.d.ts +33 -0
- package/dist/src/server/metro-bridge.d.ts +3 -0
- package/dist/src/server/metro.d.ts +25 -0
- package/dist/src/server/routes.d.ts +12 -0
- package/dist/src/server.d.ts +8 -0
- package/dist/src/server.js +1 -1
- package/dist/src/types.d.ts +60 -0
- package/package.json +13 -12
- package/scripts/macos-remote-setup.sh +1117 -0
- package/dist/src/402.js +0 -1
- package/dist/src/403.js +0 -2
- package/dist/src/47.js +0 -1
- package/dist/src/493.js +0 -1
- package/dist/src/metro-runtime.js +0 -1
package/dist/src/36.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import e from"node:http";import t from"node:os";import r from"node:path";import{randomUUID as n,timingSafeEqual as o}from"node:crypto";import i from"node:https";import{accessSync as a,constants as s,existsSync as u,readFileSync as l}from"node:fs";function d(e,t,r){let n=process.env[e];if(void 0===n||""===n)return t;let o=Number.parseInt(n,10);if(!Number.isInteger(o)||o<r.min||o>r.max)throw Error(`${e} must be an integer in range ${r.min}-${r.max}`);return o}function c(e,t){let r=process.env[e];if(void 0===r||""===r)return t;let n=r.trim().toLowerCase();if("1"===n||"true"===n||"yes"===n||"on"===n)return!0;if("0"===n||"false"===n||"no"===n||"off"===n)return!1;throw Error(`${e} must be a boolean-like value`)}class f extends Error{statusCode;code;details;constructor(e,t,r,n=null){super(r),this.statusCode=e,this.code=t,this.details=n}}function m(e,t,r,n){p(e,t,{ok:!0,data:r,request_id:n},n)}function h(e,t,r,n,o,i=null){p(e,t,{ok:!1,error:{code:r,message:n,...i?{details:i}:{}},request_id:o},o)}function p(e,t,r,n){e.statusCode=t,e.setHeader("content-type","application/json; charset=utf-8"),e.setHeader("x-request-id",n),e.end(JSON.stringify(r))}async function _(e,t){let r=(await g(e,t)).toString("utf8");if(!r.trim())throw new f(400,"invalid_request","body must be valid JSON");try{let e=JSON.parse(r);if(!e||"object"!=typeof e||Array.isArray(e))throw new f(400,"invalid_request","body must be valid JSON object");return e}catch(e){if(e instanceof f)throw e;throw new f(400,"invalid_request","body must be valid JSON")}}async function g(e,t){let r=[],n=0;try{for await(let o of e){let e=Buffer.isBuffer(o)?o:Buffer.from(o);if((n+=e.length)>t)throw new f(413,"invalid_request","request body too large");r.push(e)}}catch(e){if(e instanceof f)throw e;throw new f(400,"invalid_request","invalid request body")}return Buffer.concat(r)}async function y({statusUrl:e,timeoutMs:t}){let r=Date.now();try{let n=await fetch(e,{signal:AbortSignal.timeout(t)});return{reachable:n.ok,status_code:n.status,latency_ms:Date.now()-r,detail:n.ok?"ok":`HTTP ${n.status}`}}catch(e){return{reachable:!1,status_code:0,latency_ms:Date.now()-r,detail:e instanceof Error?e.message:String(e)}}}function w(e,t,r){let n,o=b(e,t);if(!o)return null;try{n=new URL(o)}catch{throw new f(400,"invalid_request",`${t} must be an absolute URL`)}if(r&&!r.has(n.protocol))throw new f(400,"invalid_request",`${t} must use ${Array.from(r).join(" or ")}`);return n.toString()}function b(e,t){if(null==e||""===e)return"";if("string"!=typeof e)throw new f(400,"invalid_request",`${t} must be a string`);return e.trim()}function E(e){let t=new URL(e.toString()),r=t.pathname.endsWith("/")&&t.pathname.length>1?t.pathname.slice(0,-1):t.pathname,n=r;if(r.endsWith("/index.bundle"))n=r.slice(0,-13);else if(r.endsWith(".bundle")){let e=r.lastIndexOf("/");n=e>=0?r.slice(0,e):""}return t.pathname=`${n||""}/status`.replace(/\/{2,}/g,"/"),t.search="",t.hash="",t.toString()}async function T(e,t){var r,n,o,i;let a=(r=e.host,n=e.port,`http://${r}:${n}`),s=new URL("/status",a).toString(),u=new URL("/index.bundle?platform=ios&dev=true&minify=false",a).toString(),l=await y({statusUrl:s,timeoutMs:Math.min(e.proxyTimeoutMs,15e3)});return{enabled:!0,base_url:a,status_url:s,bundle_url:u,ios_runtime:{metro_host:e.host,metro_port:e.port,metro_bundle_url:u},android_runtime:{metro_host:e.androidHost,metro_port:e.port,metro_bundle_url:(o=e.androidHost,i=e.port,`http://${o}:${i}/index.bundle?platform=android&dev=true&minify=false`)},upstream:t,probe:l}}let v=new Set(["connection","keep-alive","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade","host","content-length"]);async function $(e,t,r,n){let o=new URL((e.url||"/").replace(/^\/+/,""),r.upstreamBaseUrl),i=new Headers;for(let[t,r]of Object.entries(e.headers))if(!(!r||v.has(t.toLowerCase())))if(Array.isArray(r))for(let e of r)i.append(t,e);else i.set(t,r);let a=e.method||"GET",s="GET"===a||"HEAD"===a?void 0:await g(e,n.maxBodyBytes),u=await fetch(o,{method:a,headers:i,...s?{body:s,duplex:"half"}:{},redirect:"manual",signal:AbortSignal.timeout(n.proxyTimeoutMs)});t.statusCode=u.status,u.headers.forEach((e,r)=>{v.has(r.toLowerCase())||t.setHeader(r,e)}),t.end(Buffer.from(await u.arrayBuffer()))}async function x(t,r,n,o,a){let s=new URL(S(o).upstreamBaseUrl),u=("https:"===s.protocol?i:e).request({protocol:s.protocol,hostname:s.hostname,port:s.port||void 0,method:t.method||"GET",path:t.url||"/",headers:function(e){let t={};for(let[r,n]of Object.entries(e.headers)){if(!n)continue;let e=r.toLowerCase();"host"!==e&&"content-length"!==e&&(t[r]=n)}return t}(t)});u.setTimeout(a.proxyTimeoutMs,()=>{u.destroy(new f(504,"metro_bridge_timeout","Metro bridge websocket upgrade timed out"))}),u.once("response",e=>{B(r,e),e.pipe(r)}),u.once("upgrade",(e,t,o)=>{B(r,e),o.length>0&&r.write(o),n.length>0&&t.write(n),t.pipe(r),r.pipe(t),t.on("error",()=>{r.destroy()}),r.on("error",()=>{t.destroy()})}),u.once("error",e=>{O(r,e)}),u.end()}function S(e){if(!e)throw new f(503,"metro_bridge_unconfigured","Metro bridge target is not configured");return e}function B(e,t){let r=t.statusCode||101,n=t.statusMessage||"Switching Protocols",o=[`HTTP/1.1 ${r} ${n}`];for(let[e,r]of Object.entries(t.headers))if(r)if(Array.isArray(r))for(let t of r)o.push(`${e}: ${t}`);else o.push(`${e}: ${r}`);o.push("",""),e.write(o.join("\r\n"))}function O(e,t){let r=t instanceof f?t.statusCode:502,n=t instanceof Error?t.message:"Metro bridge failed";e.destroyed||e.end(`HTTP/1.1 ${r} ${504===r?"Gateway Timeout":"Bad Gateway"}\r
|
|
2
|
+
content-type: text/plain; charset=utf-8\r
|
|
3
|
+
content-length: ${Buffer.byteLength(n,"utf8")}\r
|
|
4
|
+
connection: close\r
|
|
5
|
+
\r
|
|
6
|
+
`+n)}function R(e){return function(e){try{if(!u(e))return null;let t=JSON.parse(l(e,"utf8")),r=t.httpPort,n="number"==typeof r?r:"string"==typeof r?Number.parseInt(r,10):NaN,o="string"==typeof t.token?t.token.trim():"string"==typeof t.authToken?t.authToken.trim():"";if(!Number.isInteger(n)||n<=0||!o)return null;return{baseUrl:`http://127.0.0.1:${n}`,authToken:o}}catch{return null}}(e.daemonStateFilePath)}let I="/agent-device/",N=["content-type","x-artifact-type","x-artifact-filename"];async function A(e){var t;let r,{req:n,res:o,requestId:i,config:a}=e,s=R(a);if(!s)throw new f(503,"daemon_unavailable",`Upstream daemon is unavailable. Ensure ${a.daemonStateFilePath} points at a live HTTP daemon.`);let u=n.url||"/",l=new URL(u,"http://127.0.0.1").pathname,d=l.startsWith(I)?l.slice(I.length):"",c=new URL(d,`${s.baseUrl.replace(/\/+$/,"")}/`),m=u.includes("?")?u.slice(u.indexOf("?")):"";m&&(c.search=m);let h=n.method||"GET",p="GET"!==h&&"HEAD"!==h,_="rpc"===d&&"POST"===h,y=new Headers;for(let e of N){let t=n.headers[e];"string"==typeof t&&t.trim()&&y.set(e,t)}if(p&&!_){let e=n.headers["content-length"];"string"==typeof e&&e.trim()&&y.set("content-length",e)}let w=s.authToken||("string"==typeof(t=n.headers.authorization)&&t.startsWith("Bearer ")?t.slice(7):"")||("string"==typeof n.headers["x-agent-device-token"]?n.headers["x-agent-device-token"]:"");w&&(y.set("authorization",`Bearer ${w}`),y.set("x-agent-device-token",w)),p&&(r=_?function(e,t){if(!t)return e;try{let r=JSON.parse(e);if(!r||"object"!=typeof r)return e;return r.params={...r.params??{},token:t},JSON.stringify(r)}catch{return e}}((await g(n,a.maxJsonBodyBytes)).toString("utf8"),s.authToken):n);let b=await fetch(c,{method:h,headers:y,...r?{body:r,duplex:"half"}:{}});o.statusCode=b.status;let E=b.headers.get("content-type");E&&o.setHeader("content-type",E),o.setHeader("x-request-id",i);let T=Buffer.from(await b.arrayBuffer());b.status>=400&&process.stderr.write(`[agent-device-proxy] daemon ${h} ${c.pathname}${c.search} → ${b.status} (requestId=${i})
|
|
7
|
+
`),o.end(T)}async function D(e){let{req:t,res:r,requestId:n,config:i,routePath:a}=e;"GET"===t.method&&("/api/health"===a||"/healthz"===a)?function({res:e,requestId:t,routePath:r}){let n={status:"ok",service:"agent-device-proxy",checked_at:new Date().toISOString()};if("/healthz"===r){e.statusCode=200,e.setHeader("content-type","application/json; charset=utf-8"),e.setHeader("x-request-id",t),e.end(JSON.stringify(n));return}m(e,200,n,t)}(e):!function(e,t){if(!t)return!0;let r=e.headers.authorization||"",n=`Bearer ${t}`,i=Buffer.from(r),a=Buffer.from(n);return i.length===a.length&&o(i,a)}(t,i.bearerToken)?h(r,401,"unauthorized","unauthorized",n):"/agent-device"===a||a.startsWith(I)?await A(e):"/api/metro/resolve"===a&&"POST"===t.method?await P(e):"/api/metro/probe"===a&&"POST"===t.method?await C(e):"/api/metro/bridge"===a&&"POST"===t.method?await M(e):h(r,404,"not_found","not found",n)}async function P(e){let t=await k(e);m(e.res,200,t,e.requestId)}async function C(e){let t=await _(e.req,e.config.maxJsonBodyBytes),r=q(t),n=function(e){if(null==e||""===e)return 2500;let t=Number.parseInt(String(e),10);return!Number.isInteger(t)||t<100||t>6e4?2500:t}(t.timeout_ms),o=await y({statusUrl:r.status_url,timeoutMs:n});m(e.res,200,{...r,...o},e.requestId)}async function M(e){let t=await k(e),r=await e.metroBridge.configure(t);m(e.res,200,r,e.requestId)}async function k(e){return q(await _(e.req,e.config.maxJsonBodyBytes))}function q(e){let t=function(e){if(!e)return null;if(e.metro_bundle_url){let t=new URL(e.metro_bundle_url);return{bundle_url:t.toString(),host:t.hostname,port:function(e){if(e.port){let t=Number.parseInt(e.port,10);if(Number.isInteger(t)&&t>=1&&t<=65535)return t}return"https:"===e.protocol?443:80}(t),status_url:E(t)}}if(e.metro_host&&e.metro_port){let t=new URL(`http://${e.metro_host}:${e.metro_port}/index.bundle?platform=ios&dev=true&minify=false`);return{bundle_url:t.toString(),host:t.hostname,port:Number.parseInt(t.port,10),status_url:E(t)}}return null}(function(e){if(null==e||""===e)return null;if(!(e&&"object"==typeof e&&!Array.isArray(e)))throw new f(400,"invalid_request","ios_runtime must be an object");let t=w(e.launch_url,"ios_runtime.launch_url",null),r=w(e.metro_bundle_url,"ios_runtime.metro_bundle_url",new Set(["http:","https:"])),n=function(e,t){let r=b(e,t);if(!r)return null;if(r.includes(":"))throw new f(400,"invalid_request",`${t} must not include a port`);if(!/^[A-Za-z0-9._-]{1,253}$/.test(r))throw new f(400,"invalid_request",`${t} must contain only host-safe characters`);return r}(e.metro_host,"ios_runtime.metro_host"),o=function(e,t){if(null==e||""===e)return null;let r=Number.parseInt(String(e),10);if(!Number.isInteger(r)||r<1||r>65535)throw new f(400,"invalid_request",`${t} must be an integer in range 1..65535`);return r}(e.metro_port,"ios_runtime.metro_port");return t||r||n||o?{launch_url:t||"",metro_bundle_url:r||"",metro_host:n||"",metro_port:o||0}:null}(e.ios_runtime??e));if(!t)throw new f(400,"invalid_metro_runtime","ios_runtime metro hints are required (metro_bundle_url or metro_host+metro_port)");return t}function U(e={}){return L(e).server}function G(e={}){let{server:t,config:r}=L(e);return t.listen(r.port,r.host,()=>{process.stdout.write(`agent-device-proxy listening on http://${r.host}:${r.port}
|
|
8
|
+
`),process.stdout.write(`agent-device daemon bridge: http://${r.host}:${r.port}/agent-device/*
|
|
7
9
|
`),process.stdout.write(`api health endpoint: http://${r.host}:${r.port}/api/health
|
|
8
|
-
`),process.stdout.write(`api
|
|
9
|
-
`),process.stdout.write(`
|
|
10
|
-
`),process.
|
|
11
|
-
`),
|
|
12
|
-
`),process.stdout.write(`api metro resolve endpoint: http://${r.host}:${r.port}/api/metro/resolve
|
|
13
|
-
`),process.stdout.write(`api metro probe endpoint: http://${r.host}:${r.port}/api/metro/probe
|
|
14
|
-
`),process.stdout.write(`agent-device command available: ${W(r.command,r.baseCommandEnv.PATH)?"yes":"no"} (${r.command})
|
|
15
|
-
`),process.stdout.write(`root dir: ${r.rootDir}
|
|
16
|
-
`),process.stdout.write(`artifacts dir: ${r.artifactsDir}
|
|
17
|
-
`),r.workspaceRoot&&process.stdout.write(`workspace root constraint: ${r.workspaceRoot}
|
|
18
|
-
`),V(r)}),i.maybeCleanupArtifacts().catch(t=>{process.stderr.write(`agent-device-proxy artifact cleanup warning: ${t instanceof Error?t.message:String(t)}
|
|
19
|
-
`)}),e}async function V(t){if(t.daemonBaseUrl&&t.validateDaemonStartup)try{let e=new Headers,r=t.daemonAuthToken||t.bearerToken;r&&(e.set("authorization",`Bearer ${r}`),e.set("x-agent-device-token",r));let i=await fetch(new URL("health",`${t.daemonBaseUrl.replace(/\/+$/,"")}/`),{method:"GET",headers:e,signal:AbortSignal.timeout(t.daemonProbeTimeoutMs)});if(!i.ok){let t=await i.text();throw Error(`daemon health probe failed (${i.status}): ${t||"<empty>"}`)}}catch(e){process.stderr.write(["agent-device-proxy startup failed: upstream daemon is not reachable in HTTP mode.",`AGENT_DEVICE_PROXY_DAEMON_BASE_URL=${t.daemonBaseUrl}`,e instanceof Error?e.message:String(e),"If the daemon restarted in socket mode, restart agent-device with AGENT_DEVICE_DAEMON_SERVER_MODE=http and then restart agent-device-proxy."].join("\n")+"\n"),process.exitCode=1,setImmediate(()=>{process.exit(1)})}}function H(n={}){let s,u,d,y,E,g,O,C,N,P,B,M,X,Y,V,W,K,Z,Q,tt=n.config||(s=process.env.AGENT_DEVICE_PROXY_HOST??"127.0.0.1",u=l("AGENT_DEVICE_PROXY_PORT",9123,{min:1,max:65535}),d=(process.env.AGENT_DEVICE_PROXY_DAEMON_BASE_URL??"").trim(),y=(process.env.AGENT_DEVICE_PROXY_DAEMON_AUTH_TOKEN??"").trim(),E=c("AGENT_DEVICE_PROXY_VALIDATE_DAEMON_STARTUP",!0),g=l("AGENT_DEVICE_PROXY_DAEMON_PROBE_TIMEOUT_MS",3e3,{min:100,max:6e4}),O=l("AGENT_DEVICE_PROXY_MAX_BODY_BYTES",1048576,{min:1024,max:0x1000000}),C=l("AGENT_DEVICE_PROXY_ARTIFACT_MAX_BYTES",0x20000000,{min:1048576,max:0xffffffff}),N=l("AGENT_DEVICE_PROXY_EXEC_TIMEOUT_MS",24e4,{min:1e3,max:36e5}),P=l("AGENT_DEVICE_PROXY_COMMAND_MAX_OUTPUT_BYTES",1048576,{min:4096,max:0x4000000}),B=l("AGENT_DEVICE_PROXY_ARTIFACT_UNZIP_TIMEOUT_MS",18e4,{min:1e3,max:18e5}),M=l("AGENT_DEVICE_PROXY_ARTIFACT_TTL_HOURS",24,{min:1,max:720}),X=l("AGENT_DEVICE_PROXY_ARTIFACT_MAX_TOTAL_BYTES",0x140000000,{min:1048576,max:0x1900000000}),Y=c("AGENT_DEVICE_PROXY_STRICT_COMMAND_STATUS",!0),V=(process.env.AGENT_DEVICE_PROXY_BEARER_TOKEN??"").trim(),W=(process.env.AGENT_DEVICE_PROXY_COMMAND??"agent-device").trim()||"agent-device",K=e.resolve(process.env.AGENT_DEVICE_PROXY_ROOT_DIR??process.cwd()),Z=function(t){let r=(t??"").trim();if(!r)return"";if(!e.isAbsolute(r))throw Error("AGENT_DEVICE_PROXY_WORKSPACE_ROOT must be absolute path");return e.resolve(r)}(process.env.AGENT_DEVICE_PROXY_WORKSPACE_ROOT??""),Q=e.resolve(process.env.AGENT_DEVICE_PROXY_ARTIFACTS_DIR??e.join(K,".agent-device-proxy-artifacts")),{host:s,port:u,daemonBaseUrl:d,daemonAuthToken:y,validateDaemonStartup:E,daemonProbeTimeoutMs:g,maxJsonBodyBytes:O,maxArtifactBytes:C,commandTimeoutMs:N,commandMaxOutputBytes:P,unzipTimeoutMs:B,artifactTtlHours:M,artifactMaxTotalBytes:X,strictCommandStatus:Y,bearerToken:V,command:W,rootDir:K,workspaceRoot:Z,artifactsDir:Q,baseCommandEnv:{PATH:function(t,r){let i=[e.dirname(process.execPath),e.join(r,"Library","Android","sdk","platform-tools"),e.join(r,"Library","Android","sdk","emulator"),"/opt/homebrew/bin","/usr/local/bin","/usr/bin","/bin"],a=new Set((t||"").split(e.delimiter).filter(Boolean));for(let t of i)o(t)&&a.add(t);return Array.from(a).join(e.delimiter)}(process.env.PATH||"",process.env.HOME||process.env.USERPROFILE||""),HOME:process.env.HOME||"",USER:process.env.USER||"",TMPDIR:process.env.TMPDIR||"/tmp",LANG:process.env.LANG||"en_US.UTF-8",LC_ALL:process.env.LC_ALL||"",LC_CTYPE:process.env.LC_CTYPE||"",SHELL:process.env.SHELL||"",TERM:process.env.TERM||"",ANDROID_HOME:process.env.ANDROID_HOME||"",ANDROID_SDK_ROOT:process.env.ANDROID_SDK_ROOT||"",JAVA_HOME:process.env.JAVA_HOME||"",XDG_RUNTIME_DIR:process.env.XDG_RUNTIME_DIR||""}});r(tt.artifactsDir,{recursive:!0});let te=function(t,{runCommand:r}){let n=null,s=0;async function u(){let e=Date.now();if(n)return await n;e-s<6e4||(n=v(t).finally(()=>{s=Date.now(),n=null}),await n)}return{storeArtifactFromRequest:async function({req:n,headers:o}){let s;await u();let d=a(),l=e.join(t.artifactsDir,d),c=e.join(l,o.filename);await i.mkdir(l,{recursive:!0});try{s=await m(n,c,t.maxArtifactBytes)}catch(t){throw await A(l),t}if(o.expected_sha256&&o.expected_sha256!==s.sha256)throw await A(l),new f(400,"invalid_request","artifact sha256 mismatch");let v=c,y=!1,E=s.sizeBytes;if("app"===o.artifact_type){let a=e.join(l,"unpacked");await i.mkdir(a,{recursive:!0}),await _({archivePath:c,timeoutMs:t.unzipTimeoutMs,runCommand:r,env:t.baseCommandEnv});let n=await p({archivePath:c,timeoutMs:t.unzipTimeoutMs,runCommand:r,env:t.baseCommandEnv,maxOutputBytes:t.commandMaxOutputBytes});if(n>t.maxArtifactBytes)throw await A(l),new f(413,"invalid_request","unzipped app archive too large",{uncompressed_bytes:n,max_bytes:t.maxArtifactBytes});let o=await r({command:"unzip",argv:["-q",c,"-d",a],cwd:l,env:t.baseCommandEnv,timeoutMs:t.unzipTimeoutMs,maxOutputBytes:t.commandMaxOutputBytes});if(0!==o.exitCode)throw await A(l),new f(400,"invalid_request","failed to unzip app archive",{exit_code:o.exitCode,stdout:o.stdout,stderr:o.stderr});let u=await h(a);if(!u)throw await A(l),new f(400,"invalid_request","zip archive does not contain a .app bundle");let d=await w(a);if(d>t.maxArtifactBytes)throw await A(l),new f(413,"invalid_request","unzipped app archive too large",{uncompressed_bytes:d,max_bytes:t.maxArtifactBytes});E=s.sizeBytes+d,v=u,y=!0}let g={artifact_id:d,artifact_type:o.artifact_type,archive:o.archive,filename:o.filename,size_bytes:E,sha256:s.sha256,created_at:new Date().toISOString(),raw_path:c,ready_path:v,extracted:y};return await i.writeFile(e.join(l,"metadata.json"),JSON.stringify(g,null,2),"utf8"),g},readArtifactMetadata:async function(r){let a,n=String(r||"").trim();if(!/^[a-z0-9-]{8,128}$/i.test(n))throw new f(400,"invalid_request","artifact_id format is invalid");let s=e.join(t.artifactsDir,n,"metadata.json");if(!o(s))throw new f(404,"not_found","artifact_id not found");try{a=JSON.parse(await i.readFile(s,"utf8"))}catch{throw new f(500,"internal_error","artifact metadata is unreadable")}if(!a||"object"!=typeof a||!a.ready_path||!a.artifact_type)throw new f(500,"internal_error","artifact metadata is invalid");if(!o(a.ready_path))throw new f(404,"not_found","artifact file is missing");return a},maybeCleanupArtifacts:u}}(tt,{runCommand:T});return{server:t.createServer(async(t,r)=>{var i,n;let s,u,d,l="string"==typeof(s=t.headers["x-request-id"])&&s.trim()?s.trim().slice(0,128):a(),{path:c,legacy:m}=(d=(u=new URL(t.url||"/","http://127.0.0.1")).pathname.startsWith("/v1/"),{path:x.get(u.pathname)||u.pathname,legacy:d});try{if("GET"===t.method&&"/agent-device/health"===c)return void await J({req:t,res:r,requestId:l,config:tt,endpoint:"health"});if("GET"===t.method&&("/api/health"===c||"/healthz"===c)){let t={status:"ok",service:"agent-device-proxy",checked_at:new Date().toISOString()};if("/healthz"===c){r.statusCode=200,r.setHeader("content-type","application/json; charset=utf-8"),r.setHeader("x-request-id",l),r.end(JSON.stringify(t));return}R(r,200,t,l,m);return}if("POST"!==t.method)return void D(r,404,"not_found","not found",l,m);if((i=tt.bearerToken)&&(t.headers.authorization||"")!==`Bearer ${i}`)return void D(r,401,"unauthorized","unauthorized",l,m);if("/agent-device/rpc"===c)return void await J({req:t,res:r,requestId:l,config:tt,endpoint:"rpc"});if("/agent-device/upload"===c)return void await J({req:t,res:r,requestId:l,config:tt,endpoint:"upload"});if("/api/artifacts/upload"===c){let e=function(t){let e=L(j(t,"x-artifact-type"),"x-artifact-type",["app","apk"]),r=L(j(t,"x-artifact-archive"),"x-artifact-archive",["zip","raw"]),i=(j(t,"x-artifact-filename")||`${e}-${Date.now()}${"zip"===r?".zip":".bin"}`).replace(/[^a-zA-Z0-9._-]/g,"_").slice(-160)||`artifact-${Date.now()}`,a=j(t,"x-artifact-sha256").toLowerCase();if(a&&!/^[a-f0-9]{64}$/.test(a))throw new f(400,"invalid_request","x-artifact-sha256 must be a 64-char lowercase hex sha256");if("app"===e&&"zip"!==r)throw new f(400,"invalid_request","x-artifact-type=app requires x-artifact-archive=zip");if("apk"===e&&"raw"!==r)throw new f(400,"invalid_request","x-artifact-type=apk requires x-artifact-archive=raw");return{artifact_type:e,archive:r,filename:i,expected_sha256:a}}(t.headers),i=await te.storeArtifactFromRequest({req:t,headers:e});R(r,200,i,l,m);return}if("/api/metro/resolve"===c){let e=await S(t,tt.maxJsonBodyBytes),i=I(e?.ios_runtime??e),a=$(i);if(!a)throw new f(400,"invalid_metro_runtime","ios_runtime metro hints are required (metro_bundle_url or metro_host+metro_port)");R(r,200,a,l,m);return}if("/api/metro/probe"===c){let e=await S(t,tt.maxJsonBodyBytes),i=I(e?.ios_runtime??e),a=$(i);if(!a)throw new f(400,"invalid_metro_runtime","ios_runtime metro hints are required (metro_bundle_url or metro_host+metro_port)");let n=function(t){if(null==t||""===t)return 2500;let e=Number.parseInt(String(t),10);return!Number.isInteger(e)||e<100||e>6e4?2500:e}(e?.timeout_ms),o=await q({statusUrl:a.status_url,timeoutMs:n});R(r,200,{...a,...o},l,m);return}if("/api/agent-device/exec"===c){let i;F(tt);let s=await S(t,tt.maxJsonBodyBytes),u=function(t){if(!z(t))throw new f(400,"invalid_request","body must be valid JSON object");if(!Array.isArray(t.argv)||t.argv.some(t=>"string"!=typeof t))throw new f(400,"invalid_request","argv must be an array of strings");if(0===t.argv.length)throw new f(400,"invalid_request","argv must contain at least one argument");let e={argv:t.argv,cwd:U(t.cwd,"cwd"),run_id:U(t.run_id,"run_id"),ios_session_id:U(t.ios_session_id,"ios_session_id"),tenant_id:U(t.tenant_id,"tenant_id"),ios_runtime:function(t,e){if(null==t||""===t)return null;if(!t||"object"!=typeof t||Array.isArray(t))throw new f(400,"invalid_request",`${e} must be an object`);return t}(t.ios_runtime,"ios_runtime")};if(e.tenant_id&&!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(e.tenant_id))throw new f(400,"invalid_request","tenant_id must match ^[a-z0-9][a-z0-9_-]{0,63}$");return e}(s),d={...u,ios_runtime:I(u.ios_runtime)},c=await T({command:tt.command,argv:d.argv,cwd:function(t,r){if(!t)return r.rootDir;if(!e.isAbsolute(t))throw new f(400,"invalid_request","cwd must be absolute path");let i=e.resolve(t);if(r.workspaceRoot&&i!==r.workspaceRoot&&!i.startsWith(`${r.workspaceRoot}${e.sep}`))throw new f(400,"invalid_request","cwd is outside configured workspace root");if(!o(i))throw new f(400,"invalid_request","cwd does not exist");return i}(d.cwd,tt),env:(n=tt.baseCommandEnv,i={...n,AGENT_DEVICE_PROXY_REQUEST_ID:a()},d.tenant_id&&(i.AGENT_DEVICE_PROXY_TENANT_ID=d.tenant_id),d.run_id&&(i.AGENT_DEVICE_PROXY_RUN_ID=d.run_id),d.ios_session_id&&(i.AGENT_DEVICE_PROXY_IOS_SESSION_ID=d.ios_session_id),d.ios_runtime&&(d.ios_runtime.launch_url&&(i.AGENT_DEVICE_PROXY_IOS_LAUNCH_URL=d.ios_runtime.launch_url),d.ios_runtime.metro_bundle_url&&(i.AGENT_DEVICE_PROXY_METRO_BUNDLE_URL=d.ios_runtime.metro_bundle_url),d.ios_runtime.metro_host&&(i.AGENT_DEVICE_PROXY_METRO_HOST=d.ios_runtime.metro_host),d.ios_runtime.metro_port&&(i.AGENT_DEVICE_PROXY_METRO_PORT=String(d.ios_runtime.metro_port))),i),timeoutMs:tt.commandTimeoutMs,maxOutputBytes:tt.commandMaxOutputBytes});b({result:c,action:`agent-device exec (${d.argv[0]||"command"})`,strictCommandStatus:tt.strictCommandStatus}),R(r,200,{exit_code:c.exitCode,stdout:c.stdout,stderr:c.stderr,timed_out:c.timedOut},l,m);return}if("/api/agent-device/install"===c){F(tt);let e=await S(t,tt.maxJsonBodyBytes),i=function(t){if(!z(t))throw new f(400,"invalid_request","body must be valid JSON object");let e=k(t.app,"app"),r=k(t.artifact_id,"artifact_id"),i=L(t.platform,"platform",["ios","android","apple"]),a=U(t.device,"device"),n=U(t.session,"session"),o=U(t.udid,"udid"),s=U(t.serial,"serial"),u=G(t.reinstall,"reinstall",!1);return{app:e,artifact_id:r,platform:i,device:a,session:n,udid:o,serial:s,reinstall:u,json:G(t.json,"json",!0)}}(e),n=await te.readArtifactMetadata(i.artifact_id),o=function(t,e){if("android"===t.platform&&"apk"!==e.artifact_type)throw new f(400,"invalid_request","android install requires artifact_type=apk");if(("ios"===t.platform||"apple"===t.platform)&&"app"!==e.artifact_type)throw new f(400,"invalid_request","ios install requires artifact_type=app");let r=[t.reinstall?"reinstall":"install",t.app,e.ready_path,"--platform",t.platform];return t.device&&r.push("--device",t.device),t.session&&r.push("--session",t.session),t.udid&&r.push("--udid",t.udid),t.serial&&r.push("--serial",t.serial),t.json&&r.push("--json"),r}(i,n),s=await T({command:tt.command,argv:o,cwd:tt.rootDir,env:{...tt.baseCommandEnv,AGENT_DEVICE_PROXY_REQUEST_ID:a(),AGENT_DEVICE_PROXY_ARTIFACT_ID:i.artifact_id},timeoutMs:tt.commandTimeoutMs,maxOutputBytes:tt.commandMaxOutputBytes});b({result:s,action:"agent-device install",strictCommandStatus:tt.strictCommandStatus}),R(r,200,{artifact_id:i.artifact_id,artifact_type:n.artifact_type,artifact_path:n.ready_path,exit_code:s.exitCode,stdout:s.stdout,stderr:s.stderr,output:function(t){try{return JSON.parse(t)}catch{return null}}(s.stdout)},l,m);return}D(r,404,"not_found","not found",l,m)}catch(e){let t;D(r,(t=function(t,e="internal error"){return t instanceof f?t:new f(500,"internal_error",t instanceof Error?t.message:e)}(e)).statusCode,t.code,t.message,l,m,t.details)}}),config:tt,artifactStore:te}}async function J(t){var e,r;let{req:i,res:a,requestId:n,config:o,endpoint:s}=t;if(!o.daemonBaseUrl)throw new f(503,"daemon_unavailable","AGENT_DEVICE_PROXY_DAEMON_BASE_URL is required for daemon forwarding");let u=new URL(s,`${o.daemonBaseUrl.replace(/\/+$/,"")}/`),d=new Headers,l=i.headers["content-type"];"string"==typeof l&&l.trim()&&d.set("content-type",l);let c=o.daemonAuthToken||("string"!=typeof(e=i.headers.authorization)?"":e.startsWith("Bearer ")?e.slice(7):"")||("string"==typeof(r=i.headers["x-agent-device-token"])?r:"");c&&(d.set("authorization",`Bearer ${c}`),d.set("x-agent-device-token",c));let m=i.headers["content-length"];"string"==typeof m&&m.trim()&&d.set("content-length",m);let _=i.headers["x-artifact-type"];"string"==typeof _&&_.trim()&&d.set("x-artifact-type",_);let p=i.headers["x-artifact-filename"];"string"==typeof p&&p.trim()&&d.set("x-artifact-filename",p);let h="rpc"===s?function(t){let{body:e,daemonAuthToken:r}=t;if(!r)return e;try{let t=JSON.parse(e);if(!t||"object"!=typeof t)return e;return t.params={...t.params??{},token:r},JSON.stringify(t)}catch{return e}}({body:Buffer.from(await N(i,o.maxJsonBodyBytes)).toString("utf8"),daemonAuthToken:o.daemonAuthToken}):"upload"===s?i:void 0,w=await fetch(u,{method:i.method||("health"===s?"GET":"POST"),headers:d,...h?{body:h,..."upload"===s?{duplex:"half"}:{}}:{}}),v=await w.text(),y=w.headers.get("content-type");a.statusCode=w.status,y?a.setHeader("content-type",y):a.setHeader("content-type","application/json; charset=utf-8"),a.setHeader("x-request-id",n),a.end(v)}function F(t){if(!W(t.command,t.baseCommandEnv.PATH))throw new f(503,"command_unavailable",`local command unavailable for this endpoint: ${t.command}`,{command:t.command})}function W(t,e){var r;return"string"==typeof t&&!!t.trim()&&0===u("sh",["-lc",`command -v ${(r=t,`'${r.replaceAll("'","'\\''")}'`)} >/dev/null 2>&1`],{stdio:"ignore",env:{...process.env,...e?{PATH:e}:{}}}).status}export{X as createagentDeviceProxyServer,Y as startAgentDeviceProxyServer};
|
|
10
|
+
`),process.stdout.write(`api metro endpoints: http://${r.host}:${r.port}/api/metro/{resolve,probe,bridge}
|
|
11
|
+
`),process.stdout.write(`metro bridge target (lazy): http://${r.metroBridgeHost}:${r.metroBridgePort}
|
|
12
|
+
`),H(r)}),t}async function H(e){if(!e.validateDaemonStartup)return;let t=R(e);if(t)try{let r=new Headers,n=t.authToken||e.bearerToken;n&&(r.set("authorization",`Bearer ${n}`),r.set("x-agent-device-token",n));let o=await fetch(new URL("health",`${t.baseUrl.replace(/\/+$/,"")}/`),{method:"GET",headers:r,signal:AbortSignal.timeout(e.daemonProbeTimeoutMs)});if(!o.ok){let e=await o.text();throw Error(`daemon health probe failed (${o.status}): ${e||"<empty>"}`)}}catch(e){process.stderr.write(["agent-device-proxy startup failed: upstream daemon is not reachable in HTTP mode.",`resolvedDaemonBaseUrl=${t.baseUrl}`,e instanceof Error?e.message:String(e),"If the daemon restarted in socket mode, restart agent-device with AGENT_DEVICE_DAEMON_SERVER_MODE=http and then restart agent-device-proxy."].join("\n")+"\n"),process.exitCode=1,setImmediate(()=>process.exit(1))}}function L(o={}){let i,a,s,u,l,m,p,_,g,y,w,b=o.config||(i=process.env.AGENT_DEVICE_PROXY_HOST??"127.0.0.1",a=d("AGENT_DEVICE_PROXY_PORT",9124,{min:1,max:65535}),s=c("AGENT_DEVICE_PROXY_METRO_BRIDGE_ENABLED",!0),u=(process.env.AGENT_DEVICE_PROXY_METRO_BRIDGE_HOST??"").trim()||"127.0.0.1",l=d("AGENT_DEVICE_PROXY_METRO_BRIDGE_PORT",8081,{min:1,max:65535}),m=(process.env.AGENT_DEVICE_PROXY_METRO_BRIDGE_ANDROID_HOST??"").trim()||"10.0.2.2",p=d("AGENT_DEVICE_PROXY_METRO_BRIDGE_TIMEOUT_MS",12e4,{min:1e3,max:36e5}),_=(process.env.AGENT_DEVICE_STATE_DIR??"").trim()||r.join(t.homedir(),".agent-device"),g=r.join(_,"daemon.json"),y=c("AGENT_DEVICE_PROXY_VALIDATE_DAEMON_STARTUP",!0),w=d("AGENT_DEVICE_PROXY_DAEMON_PROBE_TIMEOUT_MS",3e3,{min:100,max:6e4}),{host:i,port:a,metroBridgeEnabled:s,metroBridgeHost:u,metroBridgePort:l,metroBridgeAndroidHost:m,metroBridgeProxyTimeoutMs:p,daemonStateFilePath:g,validateDaemonStartup:y,daemonProbeTimeoutMs:w,maxJsonBodyBytes:d("AGENT_DEVICE_PROXY_MAX_BODY_BYTES",1048576,{min:1024,max:0x1000000}),bearerToken:(process.env.AGENT_DEVICE_PROXY_BEARER_TOKEN??"").trim()}),E=function(t){let r=null,n=null,o=null;async function i(){if(!t.enabled)throw new f(503,"metro_bridge_disabled","Metro bridge is disabled");if(n?.listening)return;if(o)return void await o;let i=new Promise((o,i)=>{let a=function({config:t,getTarget:r}){let n=e.createServer(async(e,n)=>{try{let o=S(r());await $(e,n,o,t)}catch(r){let e=r instanceof f?r.statusCode:502,t=r instanceof Error?r.message:"Metro bridge failed";n.statusCode=e,n.setHeader("content-type","text/plain; charset=utf-8"),n.end(t)}});return n.on("upgrade",(e,n,o)=>{x(e,n,o,r(),t).catch(e=>{O(n,e)})}),n}({config:t,getTarget:()=>r});a.once("error",e=>{i(new f(502,"metro_bridge_start_failed",`Failed to start Metro bridge on http://${t.host}:${t.port}`,{error:e instanceof Error?e.message:String(e)}))}),a.listen(t.port,t.host,()=>{n=a,o()})}).finally(()=>{o===i&&(o=null)});o=i,await o}return{configure:async e=>(await i(),r={upstreamBaseUrl:function(e){let t=new URL(e),r=t.pathname,n=r;if(r.endsWith("/index.bundle"))n=r.slice(0,-13);else if(r.endsWith(".bundle")){let e=r.lastIndexOf("/");n=e>=0?r.slice(0,e+1):"/"}return new URL((n.endsWith("/")?n:`${n}/`).replace(/^\/+/,""),`${t.origin}/`).toString()}(e.bundle_url)},await T(t,e))}}({enabled:b.metroBridgeEnabled,host:b.metroBridgeHost,port:b.metroBridgePort,androidHost:b.metroBridgeAndroidHost,proxyTimeoutMs:b.metroBridgeProxyTimeoutMs,maxBodyBytes:b.maxJsonBodyBytes}),v=e.createServer(async(e,t)=>{let r,o="string"==typeof(r=e.headers["x-request-id"])&&r.trim()?r.trim().slice(0,128):n(),i=new URL(e.url||"/","http://127.0.0.1").pathname;try{await D({req:e,res:t,requestId:o,config:b,routePath:i,metroBridge:E})}catch(r){let e;h(t,(e=function(e,t="internal error"){return e instanceof f?e:new f(500,"internal_error",e instanceof Error?e.message:t)}(r)).statusCode,e.code,e.message,o,e.details)}});return v.on("connection",e=>{e.on("error",t=>{j(t,e)})}),v.on("clientError",(e,t)=>{j(e,t)}),{server:v,config:b}}function j(e,t){"ECONNRESET"!==e.code&&process.stderr.write(`[agent-device-proxy] client socket error: ${e.message}
|
|
13
|
+
`),t.destroyed||t.destroy()}export{a as accessSync,s as constants,U as createAgentDeviceProxyServer,u as existsSync,t as node_os,r as node_path,l as readFileSync,G as startAgentDeviceProxyServer};
|
package/dist/src/bin.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{fileURLToPath as e}from"node:url";import{
|
|
3
|
-
`)
|
|
2
|
+
import{fileURLToPath as e}from"node:url";import{execFileSync as r,spawnSync as t}from"node:child_process";import{node_os as o,node_path as n,readFileSync as s,constants as i,accessSync as c,existsSync as a,startAgentDeviceProxyServer as E}from"./36.js";function d(e,t,o={}){try{return r(e,t,{encoding:o.encoding??"utf8",stdio:o.stdio??["ignore","pipe","ignore"]}).trim()}catch(r){if(r&&"object"==typeof r&&"code"in r&&"ENOENT"===r.code)throw Error(`Required host utility '${e}' was not found in PATH. This command currently supports macOS hosts with standard system tools installed.`);throw r}}function p(e,r){process.env[e]||(process.env[e]=r)}function u(e,r){if(!a(e))return"";try{let t=JSON.parse(s(e,"utf8"))[r];return null==t?"":String(t)}catch{return""}}function l(e){try{return c(e,i.X_OK),!0}catch{return!1}}function _(e){try{return d("ps",["-p",e,"-o","command="])}catch{return""}}function f(e){return _(e).includes("agent-device")}function v(e){let r=_(e);return r.includes("agent-device-proxy")||r.includes("agent-proxy-server.ts")||r.includes("/dist/src/bin.js")}function T(e,r,t,o){if(!e)return;try{process.kill(Number(e),0)}catch{return}if(!o(e))return;process.stdout.write(`Stopping stale ${t} (${r}, pid ${e})
|
|
3
|
+
`);try{process.kill(Number(e))}catch{}let n=Date.now()+5e3;for(;Date.now()<n;){try{process.kill(Number(e),0)}catch{return}Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,250)}process.stdout.write(`Force killing stale ${t} (${r}, pid ${e})
|
|
4
|
+
`);try{process.kill(Number(e),"SIGKILL")}catch{}}function m(e){if(!e)return[];try{return d("lsof",[`-tiTCP:${e}`,"-sTCP:LISTEN"]).split("\n").map(e=>e.trim()).filter(Boolean)}catch{return[]}}let h=process.argv.slice(2);function N(){let r=n.dirname(e(import.meta.url));for(let e of[n.resolve(r,"..","package.json"),n.resolve(r,"..","..","package.json")]){if(!a(e))continue;let r=JSON.parse(s(e,"utf8"));if("string"==typeof r.version&&r.version.trim())return r.version}throw Error("Unable to determine agent-device-proxy version")}function g(){process.stdout.write("Usage: agent-device-proxy <command>\n\nCommands:\n serve Start the proxy server using current environment variables\n dev Prepare local agent-device HTTP daemon defaults, then start the proxy server\n version Print the package version\n help Show this help\n\nExamples:\n agent-device-proxy serve\n agent-device-proxy dev\n")}function w(e){let r=!1,t=t=>{r||(r=!0,process.stdout.write(`Received ${t}, shutting down agent-device-proxy
|
|
5
|
+
`),e.close(e=>{e&&(process.stderr.write(`Failed to shut down agent-device-proxy cleanly: ${e.message}
|
|
6
|
+
`),process.exitCode=1),process.exit()}),setTimeout(()=>{process.stderr.write("Timed out waiting for agent-device-proxy to shut down cleanly\n"),process.exit(1)},5e3).unref())};process.on("SIGINT",()=>t("SIGINT")),process.on("SIGTERM",()=>t("SIGTERM"))}function I(e){if(!(e instanceof Error))return!1;let r="code"in e?String(e.code):"",t="syscall"in e?String(e.syscall):"";return("ECONNRESET"===r||"EPIPE"===r)&&(""===t||"read"===t||"write"===t)}!async function(){(h.includes("-V")||h.includes("--version"))&&(process.stdout.write(`${N()}
|
|
7
|
+
`),process.exit(0)),(h.includes("-h")||h.includes("--help"))&&(g(),process.exit(0));let e=h[0]??"serve";switch(process.on("uncaughtException",e=>{if(!I(e))throw e}),process.on("unhandledRejection",e=>{if(!I(e))throw e}),e){case"serve":w(E());return;case"dev":!function(){p("AGENT_DEVICE_DAEMON_SERVER_MODE","http"),p("AGENT_DEVICE_DAEMON_TRANSPORT","http"),p("AGENT_DEVICE_STATE_DIR",n.join(o.homedir(),".agent-device")),p("AGENT_DEVICE_PROXY_RESET_DAEMON","1");let e=n.join(process.env.AGENT_DEVICE_STATE_DIR,"daemon.json"),r=function(){let e,r,o=process.env.AGENT_DEVICE_BIN?.trim();if(o){if(!l(o))throw Error(`AGENT_DEVICE_BIN is not executable: ${o}`);if(0===t(o,["--version"],{stdio:"ignore"}).status)return o;throw Error(`AGENT_DEVICE_BIN is not a working agent-device binary: ${o}`)}for(let o of(e=process.env.PATH??"",r="win32"===process.platform?["",".cmd",".exe",".bat"]:[""],e.split(n.delimiter).filter(Boolean).flatMap(e=>r.map(r=>n.join(e,`agent-device${r}`)))))if(l(o)&&0===t(o,["--version"],{stdio:"ignore"}).status)return o;throw Error("Unable to resolve a working agent-device binary. Set AGENT_DEVICE_BIN explicitly.")}();!function(e){if("1"!==process.env.AGENT_DEVICE_PROXY_RESET_DAEMON)return;let r=u(e,"pid"),t=u(e,"httpPort");T(r,`recorded in ${e}`,"agent-device daemon",f);for(let e of m(t))T(e,`listener on port ${t}`,"agent-device daemon",f)}(e);if(0!==t(r,["session","list","--json","--daemon-transport","http"],{stdio:"ignore",env:process.env}).status)throw Error("Failed to start agent-device HTTP daemon");if(!a(e))throw Error(`agent-device daemon.json not found at ${e}`);let s=u(e,"httpPort");if(!s)throw Error(`Failed to resolve daemon httpPort from ${e}`);if(!process.env.AGENT_DEVICE_PROXY_BEARER_TOKEN?.trim())throw Error("AGENT_DEVICE_PROXY_BEARER_TOKEN must be set before starting agent-device-proxy");p("AGENT_DEVICE_PROXY_HOST","127.0.0.1"),p("AGENT_DEVICE_PROXY_PORT","9124");var i=process.env.AGENT_DEVICE_PROXY_PORT;for(let e of m(i))T(e,`listener on port ${i}`,"agent-device-proxy",v);process.stdout.write("Starting agent-device-proxy with:\n"),process.stdout.write(` AGENT_DEVICE_BIN=${r}
|
|
8
|
+
`),process.stdout.write(` upstream daemon: auto (${e}, current port ${s})
|
|
9
|
+
`),process.stdout.write(` AGENT_DEVICE_PROXY_RESET_DAEMON=${process.env.AGENT_DEVICE_PROXY_RESET_DAEMON}
|
|
10
|
+
`),process.stdout.write(` AGENT_DEVICE_PROXY_HOST=${process.env.AGENT_DEVICE_PROXY_HOST}
|
|
11
|
+
`),process.stdout.write(` AGENT_DEVICE_PROXY_PORT=${process.env.AGENT_DEVICE_PROXY_PORT}
|
|
12
|
+
`)}(),w(E());return;case"version":process.stdout.write(`${N()}
|
|
13
|
+
`);return;case"help":g();return;default:process.stderr.write(`Unknown command: ${e}
|
|
14
|
+
|
|
15
|
+
`),g(),process.exit(64)}}();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function prepareDevEnvironment(): void;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createAgentDeviceProxyClient } from "./client";
|
|
2
|
+
export { createAgentDeviceProxyServer, startAgentDeviceProxyServer, } from "./server";
|
|
3
|
+
export type { AgentDeviceProxyClient, AgentDeviceProxyClientConfig, agentDeviceProxyClient, agentDeviceProxyClientConfig, HealthResponse, MetroBridgeResponse, MetroProbeResponse, MetroRequestPayload, MetroResolveResponse, MetroRuntimeHints, } from "./types";
|
package/dist/src/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{createAgentDeviceProxyClient}from"./224.js";export{
|
|
1
|
+
export{createAgentDeviceProxyClient}from"./224.js";export{createAgentDeviceProxyServer,startAgentDeviceProxyServer}from"./36.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ServerConfig {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
metroBridgeEnabled: boolean;
|
|
5
|
+
metroBridgeHost: string;
|
|
6
|
+
metroBridgePort: number;
|
|
7
|
+
metroBridgeAndroidHost: string;
|
|
8
|
+
metroBridgeProxyTimeoutMs: number;
|
|
9
|
+
daemonStateFilePath: string;
|
|
10
|
+
validateDaemonStartup: boolean;
|
|
11
|
+
daemonProbeTimeoutMs: number;
|
|
12
|
+
maxJsonBodyBytes: number;
|
|
13
|
+
bearerToken: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createServerConfig(): ServerConfig;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ServerConfig } from "./config";
|
|
3
|
+
export declare const DAEMON_PREFIX = "/agent-device/";
|
|
4
|
+
interface DaemonForwardContext {
|
|
5
|
+
req: IncomingMessage;
|
|
6
|
+
res: ServerResponse;
|
|
7
|
+
requestId: string;
|
|
8
|
+
config: ServerConfig;
|
|
9
|
+
}
|
|
10
|
+
export declare function isDaemonRoute(routePath: string): boolean;
|
|
11
|
+
export declare function handleDaemonForward(ctx: DaemonForwardContext): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ServerConfig } from "./config";
|
|
2
|
+
export interface ResolvedDaemonConnection {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
authToken: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveDaemonConnection(config: Pick<ServerConfig, "daemonStateFilePath">): ResolvedDaemonConnection | null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class AgentDeviceProxyError extends Error {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
code: string;
|
|
4
|
+
details: unknown;
|
|
5
|
+
constructor(statusCode: number, code: string, message: string, details?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare function asAgentDeviceProxyError(error: unknown, fallbackMessage?: string): AgentDeviceProxyError;
|
|
8
|
+
export declare const agentDeviceProxyError: typeof AgentDeviceProxyError;
|
|
9
|
+
export declare const asagentDeviceProxyError: typeof asAgentDeviceProxyError;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
export declare function normalizeRoute(requestUrl: string): string;
|
|
3
|
+
export declare function resolveRequestId(req: IncomingMessage): string;
|
|
4
|
+
export declare function authorizeBearer(req: IncomingMessage, expectedBearerToken: string): boolean;
|
|
5
|
+
export declare function sendSuccess(res: ServerResponse, statusCode: number, data: unknown, requestId: string): void;
|
|
6
|
+
export declare function sendRouteError(res: ServerResponse, error: unknown, requestId: string): void;
|
|
7
|
+
export declare function sendError(res: ServerResponse, statusCode: number, code: string, message: string, requestId: string, details?: unknown): void;
|
|
8
|
+
export declare function readJsonBody(req: IncomingMessage, maxBodyBytes: number): Promise<Record<string, unknown>>;
|
|
9
|
+
export declare function readBodyBuffer(req: IncomingMessage, maxBodyBytes: number): Promise<Buffer>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ResolvedMetroRuntime } from "./metro";
|
|
2
|
+
import type { MetroBridgeConfig, MetroBridgeDescriptor, MetroBridgeTarget } from "./metro-bridge-types";
|
|
3
|
+
export declare function createMetroBridgeTarget(resolved: ResolvedMetroRuntime): MetroBridgeTarget;
|
|
4
|
+
export declare function createMetroBridgeDescriptor(config: MetroBridgeConfig, resolved: ResolvedMetroRuntime): Promise<MetroBridgeDescriptor>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import type { MetroBridgeConfig, MetroBridgeTarget } from "./metro-bridge-types";
|
|
3
|
+
interface CreateMetroBridgeProxyServerOptions {
|
|
4
|
+
config: MetroBridgeConfig;
|
|
5
|
+
getTarget: () => MetroBridgeTarget | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function createMetroBridgeProxyServer({ config, getTarget, }: CreateMetroBridgeProxyServerOptions): http.Server;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MetroProbeResult, ResolvedMetroRuntime } from "./metro";
|
|
2
|
+
export interface MetroBridgeTarget {
|
|
3
|
+
upstreamBaseUrl: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MetroBridgeConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
androidHost: string;
|
|
10
|
+
proxyTimeoutMs: number;
|
|
11
|
+
maxBodyBytes: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MetroBridgeDescriptor {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
base_url: string;
|
|
16
|
+
status_url: string;
|
|
17
|
+
bundle_url: string;
|
|
18
|
+
ios_runtime: {
|
|
19
|
+
metro_host: string;
|
|
20
|
+
metro_port: number;
|
|
21
|
+
metro_bundle_url: string;
|
|
22
|
+
};
|
|
23
|
+
android_runtime: {
|
|
24
|
+
metro_host: string;
|
|
25
|
+
metro_port: number;
|
|
26
|
+
metro_bundle_url: string;
|
|
27
|
+
};
|
|
28
|
+
upstream: ResolvedMetroRuntime;
|
|
29
|
+
probe: MetroProbeResult;
|
|
30
|
+
}
|
|
31
|
+
export interface MetroBridgeRuntime {
|
|
32
|
+
configure: (resolved: ResolvedMetroRuntime) => Promise<MetroBridgeDescriptor>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ParsedIosRuntime {
|
|
2
|
+
launch_url: string;
|
|
3
|
+
metro_bundle_url: string;
|
|
4
|
+
metro_host: string;
|
|
5
|
+
metro_port: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedMetroRuntime {
|
|
8
|
+
bundle_url: string;
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
status_url: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MetroProbeResult {
|
|
14
|
+
reachable: boolean;
|
|
15
|
+
status_code: number;
|
|
16
|
+
latency_ms: number;
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseIosRuntime(raw: unknown): ParsedIosRuntime | null;
|
|
20
|
+
export declare function resolveMetroRuntime(iosRuntime: ParsedIosRuntime | null): ResolvedMetroRuntime | null;
|
|
21
|
+
export declare function probeMetro({ statusUrl, timeoutMs, }: {
|
|
22
|
+
statusUrl: string;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
}): Promise<MetroProbeResult>;
|
|
25
|
+
export declare function parseProbeTimeoutMs(rawValue: unknown): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { ServerConfig } from "./config";
|
|
3
|
+
import type { MetroBridgeRuntime } from "./metro-bridge";
|
|
4
|
+
export interface RouteContext {
|
|
5
|
+
req: IncomingMessage;
|
|
6
|
+
res: ServerResponse;
|
|
7
|
+
requestId: string;
|
|
8
|
+
config: ServerConfig;
|
|
9
|
+
routePath: string;
|
|
10
|
+
metroBridge: MetroBridgeRuntime;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleRouteRequest(ctx: RouteContext): Promise<void>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
import type { ServerConfig } from "./server/config";
|
|
3
|
+
export declare function createAgentDeviceProxyServer(options?: {
|
|
4
|
+
config?: ServerConfig;
|
|
5
|
+
}): Server;
|
|
6
|
+
export declare function startAgentDeviceProxyServer(options?: {
|
|
7
|
+
config?: ServerConfig;
|
|
8
|
+
}): Server;
|
package/dist/src/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{
|
|
1
|
+
export{createAgentDeviceProxyServer,startAgentDeviceProxyServer}from"./36.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface MetroRuntimeHints {
|
|
2
|
+
metro_host: string;
|
|
3
|
+
metro_port: number;
|
|
4
|
+
metro_bundle_url?: string;
|
|
5
|
+
launch_url?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentDeviceProxyClientConfig {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
bearerToken?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export type agentDeviceProxyClientConfig = AgentDeviceProxyClientConfig;
|
|
13
|
+
export interface MetroRequestPayload {
|
|
14
|
+
ios_runtime: MetroRuntimeHints;
|
|
15
|
+
timeout_ms?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HealthResponse {
|
|
18
|
+
status: string;
|
|
19
|
+
service: string;
|
|
20
|
+
checked_at: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MetroResolveResponse {
|
|
23
|
+
bundle_url: string;
|
|
24
|
+
host: string;
|
|
25
|
+
port: number;
|
|
26
|
+
status_url: string;
|
|
27
|
+
}
|
|
28
|
+
export interface MetroProbeResponse extends MetroResolveResponse {
|
|
29
|
+
reachable: boolean;
|
|
30
|
+
status_code: number;
|
|
31
|
+
latency_ms: number;
|
|
32
|
+
detail: string;
|
|
33
|
+
}
|
|
34
|
+
export interface MetroBridgeResponse {
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
base_url: string;
|
|
37
|
+
status_url: string;
|
|
38
|
+
bundle_url: string;
|
|
39
|
+
ios_runtime: MetroRuntimeHints;
|
|
40
|
+
android_runtime: MetroRuntimeHints;
|
|
41
|
+
upstream: {
|
|
42
|
+
bundle_url: string;
|
|
43
|
+
host: string;
|
|
44
|
+
port: number;
|
|
45
|
+
status_url: string;
|
|
46
|
+
};
|
|
47
|
+
probe: {
|
|
48
|
+
reachable: boolean;
|
|
49
|
+
status_code: number;
|
|
50
|
+
latency_ms: number;
|
|
51
|
+
detail: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface AgentDeviceProxyClient {
|
|
55
|
+
health: () => Promise<HealthResponse>;
|
|
56
|
+
metroResolve: (payload: MetroRequestPayload | MetroRuntimeHints) => Promise<MetroResolveResponse>;
|
|
57
|
+
metroProbe: (payload: MetroRequestPayload | MetroRuntimeHints) => Promise<MetroProbeResponse>;
|
|
58
|
+
metroBridge: (payload: MetroRequestPayload | MetroRuntimeHints) => Promise<MetroBridgeResponse>;
|
|
59
|
+
}
|
|
60
|
+
export type agentDeviceProxyClient = AgentDeviceProxyClient;
|
package/package.json
CHANGED
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"types": "./index.d.ts",
|
|
5
|
+
"types": "./dist/src/index.d.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"types": "./index.d.ts",
|
|
8
|
+
"types": "./dist/src/index.d.ts",
|
|
9
9
|
"default": "./dist/src/index.js"
|
|
10
10
|
},
|
|
11
11
|
"./client": {
|
|
12
|
-
"types": "./
|
|
12
|
+
"types": "./dist/src/client.d.ts",
|
|
13
13
|
"default": "./dist/src/client.js"
|
|
14
14
|
},
|
|
15
15
|
"./server": {
|
|
16
|
-
"types": "./
|
|
16
|
+
"types": "./dist/src/server.d.ts",
|
|
17
17
|
"default": "./dist/src/server.js"
|
|
18
|
-
},
|
|
19
|
-
"./metro-runtime": {
|
|
20
|
-
"types": "./index.d.ts",
|
|
21
|
-
"default": "./dist/src/metro-runtime.js"
|
|
22
18
|
}
|
|
23
19
|
},
|
|
24
20
|
"bin": {
|
|
25
|
-
"agent-device-proxy": "./dist/src/bin.js"
|
|
21
|
+
"agent-device-proxy": "./dist/src/bin.js",
|
|
22
|
+
"agent-device-proxy-macos-remote-setup": "./scripts/macos-remote-setup.sh"
|
|
26
23
|
},
|
|
27
24
|
"scripts": {
|
|
28
25
|
"build": "rslib build",
|
|
26
|
+
"dev": "tsx src/agent-proxy-server.ts dev",
|
|
29
27
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
30
|
-
"
|
|
28
|
+
"serve": "tsx src/agent-proxy-server.ts serve",
|
|
31
29
|
"test": "vitest run",
|
|
32
30
|
"test:watch": "vitest"
|
|
33
31
|
},
|
|
@@ -38,7 +36,10 @@
|
|
|
38
36
|
"vitest": "^4.0.18"
|
|
39
37
|
},
|
|
40
38
|
"files": [
|
|
41
|
-
"
|
|
39
|
+
"ARCHITECTURE.md",
|
|
40
|
+
".env.example",
|
|
41
|
+
"dist",
|
|
42
|
+
"scripts"
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=24"
|