Skip to content

Commit b61f577

Browse files
committed
Merge PR_26171_018 local API startup diagnostics
2 parents 06d4a0f + 25db7f2 commit b61f577

2 files changed

Lines changed: 190 additions & 13 deletions

File tree

scripts/start-local-api-server.mjs

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,81 @@ import { startLocalApiServer } from "../src/dev-runtime/server/local-api-server.
77

88
const RUNTIME_ENV_FILE = ".env";
99
const 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

1145
function 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+
63184
export 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})`}`,

tests/dev-runtime/LocalApiStartupLogging.test.mjs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,76 @@ test("local API startup log separates bind URL from configured public URLs", ()
1717
GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api",
1818
GAMEFOUNDRY_DATABASE_URL: "postgres://secret-user:secret-pass@example.invalid/db",
1919
GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500",
20+
GAMEFOUNDRY_STORAGE_ENDPOINT: "http://127.0.0.1:9000",
2021
GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY: "storage-secret",
2122
GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: "service-role-secret",
23+
GAMEFOUNDRY_SUPABASE_URL: "https://example.supabase.co",
2224
},
2325
localServer: {
2426
baseUrl: "http://127.0.0.1:5501",
2527
},
2628
runtimeEnv: {
2729
loaded: true,
2830
loadedKeys: 12,
31+
variables: [
32+
{
33+
applied: false,
34+
key: "GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY",
35+
value: "service-role-secret",
36+
},
37+
{
38+
applied: true,
39+
key: "GAMEFOUNDRY_SUPABASE_URL",
40+
value: "https://example.supabase.co",
41+
},
42+
{
43+
applied: true,
44+
key: "GAMEFOUNDRY_DATABASE_URL",
45+
value: "postgres://secret-user:secret-pass@example.invalid/db",
46+
},
47+
{
48+
applied: true,
49+
key: "GAMEFOUNDRY_STORAGE_ENDPOINT",
50+
value: "http://127.0.0.1:9000",
51+
},
52+
{
53+
applied: false,
54+
key: "GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY",
55+
value: "storage-secret",
56+
},
57+
],
2958
},
3059
});
3160

3261
assert.deepEqual(lines, [
3362
"GameFoundry API runtime server running at http://127.0.0.1:5501",
3463
"Configured site URL: http://127.0.0.1:5500",
3564
"Configured API URL: http://127.0.0.1:5501/api",
36-
".env loaded for API runtime (12 key(s) applied).",
65+
"=========================================",
66+
"Environment Variables",
67+
"=========================================",
68+
"+ GAMEFOUNDRY_DATABASE_URL=postgres://********:********@example.invalid/db",
69+
"+ GAMEFOUNDRY_STORAGE_ENDPOINT=http://127.0.0.1:9000",
70+
"- GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY=********",
71+
"- GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY=********",
72+
"+ GAMEFOUNDRY_SUPABASE_URL=https://example.supabase.co",
73+
"=========================================",
74+
"All Runtime Ports being used by Service",
75+
"=========================================",
76+
"live server port: 5500",
77+
"API server port: 5501",
78+
"configured API URL port: 5501",
79+
"DB/Postgres port: 5432",
80+
"Supabase service port: 443",
81+
"Storage service port: 9000",
3782
"Configured auth connection: configured.",
3883
"Configured database connection: configured.",
3984
"Database SSL mode: require",
4085
"Press Ctrl+C to stop.",
4186
]);
4287
assert.equal(lines.join("\n").includes("/account/sign-in.html"), false);
88+
assert.equal(lines.join("\n").includes(".env loaded for API runtime"), false);
89+
assert.equal(lines.join("\n").includes("secret-user"), false);
4390
assert.equal(lines.join("\n").includes("secret-pass"), false);
4491
assert.equal(lines.join("\n").includes("storage-secret"), false);
4592
assert.equal(lines.join("\n").includes("service-role-secret"), false);
@@ -64,6 +111,7 @@ test("local API startup log shows missing site URL and derives API URL from bind
64111
runtimeEnv: {
65112
loaded: false,
66113
loadedKeys: 0,
114+
variables: [],
67115
},
68116
});
69117

@@ -72,6 +120,15 @@ test("local API startup log shows missing site URL and derives API URL from bind
72120
"Configured site URL: (not configured)",
73121
"Configured API URL: http://127.0.0.1:5599/api",
74122
".env was not found for API runtime.",
123+
"=========================================",
124+
"All Runtime Ports being used by Service",
125+
"=========================================",
126+
"live server port: not configured",
127+
"API server port: 5599",
128+
"configured API URL port: 5599",
129+
"DB/Postgres port: not configured",
130+
"Supabase service port: not configured",
131+
"Storage service port: not configured",
75132
"Configured auth connection: missing GAMEFOUNDRY_SUPABASE_URL.",
76133
"Configured database connection: missing GAMEFOUNDRY_DATABASE_URL.",
77134
"Database SSL mode: invalid (GAMEFOUNDRY_DATABASE_SSL is missing.)",

0 commit comments

Comments
 (0)