@@ -7,36 +7,81 @@ import { startLocalApiServer } from "../src/dev-runtime/server/local-api-server.
77
88const RUNTIME_ENV_FILE = ".env" ;
99const NOT_CONFIGURED = "(not configured)" ;
10+ const PORT_NOT_CONFIGURED = "not configured" ;
11+ const SECTION_DIVIDER = "=========================================" ;
12+ const MASKED_ENV_VALUE = "********" ;
13+ const SECRET_ENV_KEY_PARTS = Object . freeze ( [
14+ "PASSWORD" ,
15+ "SECRET" ,
16+ "TOKEN" ,
17+ "KEY" ,
18+ "SERVICE_ROLE" ,
19+ "JWT" ,
20+ ] ) ;
21+ const DEFAULT_PORT_BY_PROTOCOL = Object . freeze ( {
22+ "http:" : "80" ,
23+ "https:" : "443" ,
24+ "postgres:" : "5432" ,
25+ "postgresql:" : "5432" ,
26+ } ) ;
27+
28+ function parseRuntimeEnvLine ( line ) {
29+ const trimmed = line . trim ( ) ;
30+ if ( ! trimmed || trimmed . startsWith ( "#" ) || ! trimmed . includes ( "=" ) ) {
31+ return null ;
32+ }
33+ const index = trimmed . indexOf ( "=" ) ;
34+ const key = trimmed . slice ( 0 , index ) . trim ( ) ;
35+ let value = trimmed . slice ( index + 1 ) . trim ( ) ;
36+ if ( ! key ) {
37+ return null ;
38+ }
39+ if ( ( value . startsWith ( "\"" ) && value . endsWith ( "\"" ) ) || ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) ) {
40+ value = value . slice ( 1 , - 1 ) ;
41+ }
42+ return { key, value } ;
43+ }
1044
1145function loadRuntimeEnv ( ) {
1246 const envPath = path . resolve ( process . cwd ( ) , RUNTIME_ENV_FILE ) ;
1347 if ( ! existsSync ( envPath ) ) {
1448 return {
1549 loaded : false ,
1650 loadedKeys : 0 ,
51+ variables : [ ] ,
1752 } ;
1853 }
54+ const envValuesBeforeLoad = new Map (
55+ Object . keys ( process . env )
56+ . filter ( ( key ) => process . env [ key ] !== undefined )
57+ . map ( ( key ) => [ key , process . env [ key ] ] )
58+ ) ;
59+ const variablesByKey = new Map ( ) ;
1960 let loadedKeys = 0 ;
2061 readFileSync ( envPath , "utf8" ) . split ( / \r ? \n / ) . forEach ( ( line ) => {
21- const trimmed = line . trim ( ) ;
22- if ( ! trimmed || trimmed . startsWith ( "#" ) || ! trimmed . includes ( "=" ) ) {
62+ const parsed = parseRuntimeEnvLine ( line ) ;
63+ if ( ! parsed ) {
2364 return ;
2465 }
25- const index = trimmed . indexOf ( "=" ) ;
26- const key = trimmed . slice ( 0 , index ) . trim ( ) ;
27- let value = trimmed . slice ( index + 1 ) . trim ( ) ;
28- if ( ! key || process . env [ key ] !== undefined ) {
29- return ;
66+ const { key, value } = parsed ;
67+ const wasAlreadySet = envValuesBeforeLoad . has ( key ) ;
68+ if ( ! variablesByKey . has ( key ) ) {
69+ variablesByKey . set ( key , {
70+ applied : ! wasAlreadySet ,
71+ key,
72+ value : wasAlreadySet ? envValuesBeforeLoad . get ( key ) : value ,
73+ } ) ;
3074 }
31- if ( ( value . startsWith ( "\"" ) && value . endsWith ( "\"" ) ) || ( value . startsWith ( "'" ) && value . endsWith ( "'" ) ) ) {
32- value = value . slice ( 1 , - 1 ) ;
75+ if ( process . env [ key ] !== undefined ) {
76+ return ;
3377 }
3478 process . env [ key ] = value ;
3579 loadedKeys += 1 ;
3680 } ) ;
3781 return {
3882 loaded : true ,
3983 loadedKeys,
84+ variables : Array . from ( variablesByKey . values ( ) ) . sort ( ( left , right ) => left . key . localeCompare ( right . key ) ) ,
4085 } ;
4186}
4287
@@ -60,6 +105,82 @@ function connectionStatusLine(label, connection) {
60105 return `Configured ${ label } connection: ${ connection . ready ? "configured" : `missing ${ connection . missingKeys . join ( ", " ) } ` } .` ;
61106}
62107
108+ function shouldMaskEnvValue ( key ) {
109+ const normalizedKey = String ( key || "" ) . toUpperCase ( ) ;
110+ return SECRET_ENV_KEY_PARTS . some ( ( part ) => normalizedKey . includes ( part ) ) ;
111+ }
112+
113+ function redactUrlCredentials ( value ) {
114+ const rawValue = String ( value ?? "" ) ;
115+ try {
116+ const url = new URL ( rawValue ) ;
117+ if ( ! url . username && ! url . password ) {
118+ return rawValue ;
119+ }
120+ if ( url . username ) {
121+ url . username = MASKED_ENV_VALUE ;
122+ }
123+ if ( url . password ) {
124+ url . password = MASKED_ENV_VALUE ;
125+ }
126+ return url . toString ( ) ;
127+ } catch {
128+ return rawValue ;
129+ }
130+ }
131+
132+ function formatEnvValue ( key , value ) {
133+ if ( shouldMaskEnvValue ( key ) ) {
134+ return MASKED_ENV_VALUE ;
135+ }
136+ return redactUrlCredentials ( value ) ;
137+ }
138+
139+ function formatEnvironmentVariableLogLines ( runtimeEnv ) {
140+ if ( ! runtimeEnv . loaded ) {
141+ return [ ".env was not found for API runtime." ] ;
142+ }
143+ return [
144+ SECTION_DIVIDER ,
145+ "Environment Variables" ,
146+ SECTION_DIVIDER ,
147+ ...( runtimeEnv . variables || [ ] )
148+ . slice ( )
149+ . sort ( ( left , right ) => left . key . localeCompare ( right . key ) )
150+ . map (
151+ ( { applied, key, value } ) => `${ applied ? "+" : "-" } ${ key } =${ formatEnvValue ( key , value ) } `
152+ ) ,
153+ ] ;
154+ }
155+
156+ function portFromUrl ( value ) {
157+ const trimmed = String ( value || "" ) . trim ( ) ;
158+ if ( ! trimmed ) {
159+ return PORT_NOT_CONFIGURED ;
160+ }
161+ try {
162+ const url = new URL ( trimmed ) ;
163+ return url . port || DEFAULT_PORT_BY_PROTOCOL [ url . protocol ] || PORT_NOT_CONFIGURED ;
164+ } catch {
165+ return PORT_NOT_CONFIGURED ;
166+ }
167+ }
168+
169+ function formatRuntimePortLogLines ( { env, localServer } ) {
170+ const configuredApiUrl = String ( env . GAMEFOUNDRY_API_URL || "" ) . trim ( ) || defaultApiUrl ( localServer . baseUrl ) ;
171+ return [
172+ SECTION_DIVIDER ,
173+ "All Runtime Ports being used by Service" ,
174+ SECTION_DIVIDER ,
175+ `live server port: ${ portFromUrl ( env . GAMEFOUNDRY_SITE_URL ) } ` ,
176+ `API server port: ${ portFromUrl ( localServer . baseUrl ) } ` ,
177+ `configured API URL port: ${ portFromUrl ( configuredApiUrl ) } ` ,
178+ `DB/Postgres port: ${ portFromUrl ( env . GAMEFOUNDRY_DATABASE_URL ) } ` ,
179+ `Supabase service port: ${ portFromUrl ( env . GAMEFOUNDRY_SUPABASE_URL ) } ` ,
180+ `Storage service port: ${ portFromUrl ( env . GAMEFOUNDRY_STORAGE_ENDPOINT ) } ` ,
181+ ] ;
182+ }
183+
63184export function formatStartupLogLines ( {
64185 accountConnection,
65186 configuredDatabaseSslMode,
@@ -73,9 +194,8 @@ export function formatStartupLogLines({
73194 `GameFoundry API runtime server running at ${ localServer . baseUrl } ` ,
74195 `Configured site URL: ${ configuredValue ( env . GAMEFOUNDRY_SITE_URL ) } ` ,
75196 `Configured API URL: ${ String ( env . GAMEFOUNDRY_API_URL || "" ) . trim ( ) || defaultApiUrl ( localServer . baseUrl ) } ` ,
76- runtimeEnv . loaded
77- ? `.env loaded for API runtime (${ runtimeEnv . loadedKeys } key(s) applied).`
78- : ".env was not found for API runtime." ,
197+ ...formatEnvironmentVariableLogLines ( runtimeEnv ) ,
198+ ...formatRuntimePortLogLines ( { env, localServer } ) ,
79199 connectionStatusLine ( "auth" , accountConnection ) ,
80200 connectionStatusLine ( "database" , databaseConnection ) ,
81201 `Database SSL mode: ${ configuredDatabaseSslMode || `invalid (${ databaseSslModeError } )` } ` ,
0 commit comments