@@ -1290,6 +1290,90 @@ describe("checkup-api", () => {
12901290 }
12911291 expect ( attempts ) . toBe ( 2 ) ; // Should retry on ECONNRESET
12921292 } ) ;
1293+
1294+ // Transport selection — pick http/https by URL protocol, but refuse HTTP
1295+ // to non-loopback hosts unless CHECKUP_ALLOW_HTTP=1 is set (prevents
1296+ // typo-driven plaintext API-key leaks like http://api.postgres.ai/...).
1297+ describe ( "transport selection" , ( ) => {
1298+ test ( "https URL does not trip the guard (network error expected)" , async ( ) => {
1299+ let caught : Error | null = null ;
1300+ try {
1301+ await api . createCheckupReport ( {
1302+ apiKey : "dummy" ,
1303+ apiBaseUrl : "https://127.0.0.1:1/api" , // port 1 — connect refused
1304+ project : "p" ,
1305+ } ) ;
1306+ } catch ( e ) {
1307+ caught = e as Error ;
1308+ }
1309+ expect ( caught ) . not . toBeNull ( ) ;
1310+ expect ( caught ! . message ) . not . toMatch ( / R e f u s i n g t o s e n d A P I k e y / ) ;
1311+ } ) ;
1312+
1313+ test ( "http on loopback does not trip the guard (network error expected)" , async ( ) => {
1314+ // IPv6 loopback is written as `[::1]` in URLs; WHATWG URL preserves
1315+ // the brackets in .hostname, so the guard must strip them before
1316+ // matching the allowlist.
1317+ for ( const host of [ "localhost" , "127.0.0.1" , "[::1]" ] ) {
1318+ let caught : Error | null = null ;
1319+ try {
1320+ await api . createCheckupReport ( {
1321+ apiKey : "dummy" ,
1322+ apiBaseUrl : `http://${ host } :1/api` , // port 1 — connect refused
1323+ project : "p" ,
1324+ } ) ;
1325+ } catch ( e ) {
1326+ caught = e as Error ;
1327+ }
1328+ expect ( caught ) . not . toBeNull ( ) ;
1329+ expect ( caught ! . message ) . not . toMatch ( / R e f u s i n g t o s e n d A P I k e y / ) ;
1330+ }
1331+ } ) ;
1332+
1333+ test ( "http to non-loopback host is refused by the guard" , async ( ) => {
1334+ const saved = process . env . CHECKUP_ALLOW_HTTP ;
1335+ delete process . env . CHECKUP_ALLOW_HTTP ;
1336+ try {
1337+ let caught : Error | null = null ;
1338+ try {
1339+ await api . createCheckupReport ( {
1340+ apiKey : "dummy" ,
1341+ apiBaseUrl : "http://example.com/api" ,
1342+ project : "p" ,
1343+ } ) ;
1344+ } catch ( e ) {
1345+ caught = e as Error ;
1346+ }
1347+ expect ( caught ) . not . toBeNull ( ) ;
1348+ expect ( caught ! . message ) . toMatch ( / R e f u s i n g t o s e n d A P I k e y o v e r p l a i n t e x t H T T P / ) ;
1349+ expect ( caught ! . message ) . toMatch ( / e x a m p l e \. c o m / ) ;
1350+ } finally {
1351+ if ( saved !== undefined ) process . env . CHECKUP_ALLOW_HTTP = saved ;
1352+ }
1353+ } ) ;
1354+
1355+ test ( "CHECKUP_ALLOW_HTTP=1 bypasses the guard for non-loopback hosts" , async ( ) => {
1356+ const saved = process . env . CHECKUP_ALLOW_HTTP ;
1357+ process . env . CHECKUP_ALLOW_HTTP = "1" ;
1358+ try {
1359+ let caught : Error | null = null ;
1360+ try {
1361+ await api . createCheckupReport ( {
1362+ apiKey : "dummy" ,
1363+ apiBaseUrl : "http://127.0.0.2:1/api" , // non-loopback-match hostname, connect refused port
1364+ project : "p" ,
1365+ } ) ;
1366+ } catch ( e ) {
1367+ caught = e as Error ;
1368+ }
1369+ expect ( caught ) . not . toBeNull ( ) ;
1370+ expect ( caught ! . message ) . not . toMatch ( / R e f u s i n g t o s e n d A P I k e y / ) ;
1371+ } finally {
1372+ if ( saved === undefined ) delete process . env . CHECKUP_ALLOW_HTTP ;
1373+ else process . env . CHECKUP_ALLOW_HTTP = saved ;
1374+ }
1375+ } ) ;
1376+ } ) ;
12931377} ) ;
12941378
12951379// Tests for checkup-summary module
@@ -1821,5 +1905,3 @@ describe("checkup-summary", () => {
18211905 expect ( result . message ) . toBe ( "No redundant indexes" ) ;
18221906 } ) ;
18231907} ) ;
1824-
1825-
0 commit comments