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+ function normalizeUpstreamRegistry ( raw ) {
10+ if ( ! raw ) {
11+ return undefined ;
12+ }
13+ const url = new URL ( raw ) ;
14+ if (
15+ ( url . protocol !== "http:" && url . protocol !== "https:" ) ||
16+ url . username ||
17+ url . password ||
18+ url . pathname !== "/" ||
19+ url . search ||
20+ url . hash
21+ ) {
22+ throw new Error ( "OPENCLAW_NPM_REGISTRY_UPSTREAM must be an HTTP(S) origin" ) ;
23+ }
24+ return url . origin ;
25+ }
26+
27+ const upstreamRegistry = normalizeUpstreamRegistry ( process . env . OPENCLAW_NPM_REGISTRY_UPSTREAM ) ;
828
929if ( ! portFile || packageArgs . length === 0 || packageArgs . length % 3 !== 0 ) {
1030 console . error (
@@ -14,6 +34,25 @@ if (!portFile || packageArgs.length === 0 || packageArgs.length % 3 !== 0) {
1434}
1535
1636const packages = new Map ( ) ;
37+
38+ function readPackageManifest ( tarballPath , packageName ) {
39+ try {
40+ const packageJson = JSON . parse (
41+ execFileSync ( "tar" , [ "-xOf" , tarballPath , "package/package.json" ] , {
42+ encoding : "utf8" ,
43+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
44+ } ) ,
45+ ) ;
46+ return packageJson && typeof packageJson === "object" && ! Array . isArray ( packageJson )
47+ ? packageJson
48+ : { } ;
49+ } catch {
50+ return packageName === "@openclaw/demo-plugin-npm"
51+ ? { dependencies : { "is-number" : "7.0.0" } }
52+ : { } ;
53+ }
54+ }
55+
1756for ( let index = 0 ; index < packageArgs . length ; index += 3 ) {
1857 const packageName = packageArgs [ index ] ;
1958 const version = packageArgs [ index + 1 ] ;
@@ -28,8 +67,8 @@ for (let index = 0; index < packageArgs.length; index += 3) {
2867 existing . latestVersion = version ;
2968 existing . versions . set ( version , {
3069 archive,
31- dependencies : packageName === "@openclaw/demo-plugin-npm" ? { "is-number" : "7.0.0" } : { } ,
3270 integrity : `sha512-${ crypto . createHash ( "sha512" ) . update ( archive ) . digest ( "base64" ) } ` ,
71+ manifest : readPackageManifest ( tarballPath , packageName ) ,
3372 shasum : crypto . createHash ( "sha1" ) . update ( archive ) . digest ( "hex" ) ,
3473 tarballName : path . basename ( tarballPath ) ,
3574 version,
@@ -44,7 +83,7 @@ const metadataFor = (entry, baseUrl) => ({
4483 [ ...entry . versions . entries ( ) ] . map ( ( [ version , versionEntry ] ) => [
4584 version ,
4685 {
47- dependencies : versionEntry . dependencies ,
86+ ... versionEntry . manifest ,
4887 name : entry . packageName ,
4988 version,
5089 dist : {
@@ -85,9 +124,46 @@ function findTarballForPath(pathname) {
85124 return undefined ;
86125}
87126
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 } ` ;
127+ function resolveUpstreamRequestUrl ( rawRequestUrl ) {
128+ const raw = rawRequestUrl || "/" ;
129+ if ( ! raw . startsWith ( "/" ) || raw . startsWith ( "//" ) || raw . includes ( "\\" ) ) {
130+ throw new Error ( `refusing non-origin registry request URL: ${ JSON . stringify ( raw ) } ` ) ;
131+ }
132+ const requestUrl = new URL ( raw , "http://127.0.0.1" ) ;
133+ return `${ upstreamRegistry } ${ requestUrl . pathname } ${ requestUrl . search } ` ;
134+ }
135+
136+ async function proxyUpstream ( rawRequestUrl , response ) {
137+ if ( ! upstreamRegistry ) {
138+ return false ;
139+ }
140+ try {
141+ const upstreamUrl = resolveUpstreamRequestUrl ( rawRequestUrl ) ;
142+ const upstreamResponse = await fetch ( upstreamUrl , { redirect : "manual" } ) ;
143+ const body = Buffer . from ( await upstreamResponse . arrayBuffer ( ) ) ;
144+ // Fetch decodes compressed bodies but preserves upstream length metadata.
145+ // Emit the decoded size so npm clients do not truncate proxied responses.
146+ const headers = { "content-length" : String ( body . length ) } ;
147+ for ( const name of [ "content-type" , "location" ] ) {
148+ const value = upstreamResponse . headers . get ( name ) ;
149+ if ( value ) {
150+ headers [ name ] = value ;
151+ }
152+ }
153+ response . writeHead ( upstreamResponse . status , headers ) ;
154+ response . end ( body ) ;
155+ } catch ( error ) {
156+ response . writeHead ( 502 , { "content-type" : "text/plain" } ) ;
157+ response . end ( `upstream registry request failed: ${ String ( error ) } ` ) ;
158+ }
159+ return true ;
160+ }
161+
162+ async function handleRequest ( request , response ) {
163+ const fallbackHost = `127.0.0.1:${ server . address ( ) . port } ` ;
164+ const requestHost = request . headers . host || fallbackHost ;
165+ const url = new URL ( request . url ?? "/" , `http://${ requestHost } ` ) ;
166+ const baseUrl = url . origin ;
91167 if ( request . method !== "GET" ) {
92168 response . writeHead ( 405 , { "content-type" : "text/plain" } ) ;
93169 response . end ( "method not allowed" ) ;
@@ -111,10 +187,27 @@ const server = http.createServer((request, response) => {
111187 return ;
112188 }
113189
190+ if ( await proxyUpstream ( request . url , response ) ) {
191+ return ;
192+ }
193+
114194 response . writeHead ( 404 , { "content-type" : "text/plain" } ) ;
115195 response . end ( `not found: ${ url . pathname } ` ) ;
196+ }
197+
198+ const server = http . createServer ( ( request , response ) => {
199+ void handleRequest ( request , response ) . catch ( ( /** @type {unknown } */ error ) => {
200+ if ( ! response . headersSent ) {
201+ response . writeHead ( 500 , { "content-type" : "text/plain" } ) ;
202+ response . end ( `registry request failed: ${ String ( error ) } ` ) ;
203+ return ;
204+ }
205+ response . destroy ( error instanceof Error ? error : new Error ( String ( error ) ) ) ;
206+ } ) ;
116207} ) ;
117208
118- server . listen ( 0 , "127.0.0.1" , ( ) => {
209+ const bindHost = process . env . OPENCLAW_NPM_REGISTRY_BIND_HOST || "127.0.0.1" ;
210+ const requestedPort = Number ( process . env . OPENCLAW_NPM_REGISTRY_PORT || 0 ) ;
211+ server . listen ( requestedPort , bindHost , ( ) => {
119212 fs . writeFileSync ( portFile , String ( server . address ( ) . port ) ) ;
120213} ) ;
0 commit comments