1+ import { execFileSync } from "node:child_process" ;
12// Fixture npm registry server for plugin E2E scenarios.
23import crypto from "node:crypto" ;
34import fs from "node:fs" ;
45import http from "node:http" ;
56import path from "node:path" ;
67
78const [ portFile , ...packageArgs ] = process . argv . slice ( 2 ) ;
9+ const upstreamRegistry = process . env . OPENCLAW_NPM_REGISTRY_UPSTREAM ?. replace ( / \/ + $ / u, "" ) ;
810
911if ( ! portFile || packageArgs . length === 0 || packageArgs . length % 3 !== 0 ) {
1012 console . error (
@@ -14,6 +16,25 @@ if (!portFile || packageArgs.length === 0 || packageArgs.length % 3 !== 0) {
1416}
1517
1618const packages = new Map ( ) ;
19+
20+ function readPackageManifest ( tarballPath , packageName ) {
21+ try {
22+ const packageJson = JSON . parse (
23+ execFileSync ( "tar" , [ "-xOf" , tarballPath , "package/package.json" ] , {
24+ encoding : "utf8" ,
25+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
26+ } ) ,
27+ ) ;
28+ return packageJson && typeof packageJson === "object" && ! Array . isArray ( packageJson )
29+ ? packageJson
30+ : { } ;
31+ } catch {
32+ return packageName === "@openclaw/demo-plugin-npm"
33+ ? { dependencies : { "is-number" : "7.0.0" } }
34+ : { } ;
35+ }
36+ }
37+
1738for ( let index = 0 ; index < packageArgs . length ; index += 3 ) {
1839 const packageName = packageArgs [ index ] ;
1940 const version = packageArgs [ index + 1 ] ;
@@ -28,8 +49,8 @@ for (let index = 0; index < packageArgs.length; index += 3) {
2849 existing . latestVersion = version ;
2950 existing . versions . set ( version , {
3051 archive,
31- dependencies : packageName === "@openclaw/demo-plugin-npm" ? { "is-number" : "7.0.0" } : { } ,
3252 integrity : `sha512-${ crypto . createHash ( "sha512" ) . update ( archive ) . digest ( "base64" ) } ` ,
53+ manifest : readPackageManifest ( tarballPath , packageName ) ,
3354 shasum : crypto . createHash ( "sha1" ) . update ( archive ) . digest ( "hex" ) ,
3455 tarballName : path . basename ( tarballPath ) ,
3556 version,
@@ -44,7 +65,7 @@ const metadataFor = (entry, baseUrl) => ({
4465 [ ...entry . versions . entries ( ) ] . map ( ( [ version , versionEntry ] ) => [
4566 version ,
4667 {
47- dependencies : versionEntry . dependencies ,
68+ ... versionEntry . manifest ,
4869 name : entry . packageName ,
4970 version,
5071 dist : {
@@ -85,9 +106,37 @@ function findTarballForPath(pathname) {
85106 return undefined ;
86107}
87108
88- const server = http . createServer ( ( request , response ) => {
89- const url = new URL ( request . url ?? "/" , "http://127.0.0.1" ) ;
90- const baseUrl = `http://127.0.0.1:${ server . address ( ) . port } ` ;
109+ async function proxyUpstream ( url , response ) {
110+ if ( ! upstreamRegistry ) {
111+ return false ;
112+ }
113+ try {
114+ const upstreamUrl = new URL ( `${ url . pathname } ${ url . search } ` , `${ upstreamRegistry } /` ) ;
115+ const upstreamResponse = await fetch ( upstreamUrl , { redirect : "manual" } ) ;
116+ const body = Buffer . from ( await upstreamResponse . arrayBuffer ( ) ) ;
117+ // Fetch decodes compressed bodies but preserves upstream length metadata.
118+ // Emit the decoded size so npm clients do not truncate proxied responses.
119+ const headers = { "content-length" : String ( body . length ) } ;
120+ for ( const name of [ "content-type" , "location" ] ) {
121+ const value = upstreamResponse . headers . get ( name ) ;
122+ if ( value ) {
123+ headers [ name ] = value ;
124+ }
125+ }
126+ response . writeHead ( upstreamResponse . status , headers ) ;
127+ response . end ( body ) ;
128+ } catch ( error ) {
129+ response . writeHead ( 502 , { "content-type" : "text/plain" } ) ;
130+ response . end ( `upstream registry request failed: ${ String ( error ) } ` ) ;
131+ }
132+ return true ;
133+ }
134+
135+ async function handleRequest ( request , response ) {
136+ const fallbackHost = `127.0.0.1:${ server . address ( ) . port } ` ;
137+ const requestHost = request . headers . host || fallbackHost ;
138+ const url = new URL ( request . url ?? "/" , `http://${ requestHost } ` ) ;
139+ const baseUrl = url . origin ;
91140 if ( request . method !== "GET" ) {
92141 response . writeHead ( 405 , { "content-type" : "text/plain" } ) ;
93142 response . end ( "method not allowed" ) ;
@@ -111,10 +160,27 @@ const server = http.createServer((request, response) => {
111160 return ;
112161 }
113162
163+ if ( await proxyUpstream ( url , response ) ) {
164+ return ;
165+ }
166+
114167 response . writeHead ( 404 , { "content-type" : "text/plain" } ) ;
115168 response . end ( `not found: ${ url . pathname } ` ) ;
169+ }
170+
171+ const server = http . createServer ( ( request , response ) => {
172+ void handleRequest ( request , response ) . catch ( ( /** @type {unknown } */ error ) => {
173+ if ( ! response . headersSent ) {
174+ response . writeHead ( 500 , { "content-type" : "text/plain" } ) ;
175+ response . end ( `registry request failed: ${ String ( error ) } ` ) ;
176+ return ;
177+ }
178+ response . destroy ( error instanceof Error ? error : new Error ( String ( error ) ) ) ;
179+ } ) ;
116180} ) ;
117181
118- server . listen ( 0 , "127.0.0.1" , ( ) => {
182+ const bindHost = process . env . OPENCLAW_NPM_REGISTRY_BIND_HOST || "127.0.0.1" ;
183+ const requestedPort = Number ( process . env . OPENCLAW_NPM_REGISTRY_PORT || 0 ) ;
184+ server . listen ( requestedPort , bindHost , ( ) => {
119185 fs . writeFileSync ( portFile , String ( server . address ( ) . port ) ) ;
120186} ) ;
0 commit comments