Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { validateEnv } from "@ontrack/backend-common";
export type AppConfig = {
port: number;
nodeEnv: string;
catalogServiceUrl: string;
/** Internal geo-service (GPX simplification + metrics) base URL. */
geoServiceUrl: string;
/** Internal games-service (coverage game) base URL. */
gamesServiceUrl: string;
serviceToken: string;
/** Firebase project id whose ID tokens this gateway accepts. */
firebaseProjectId: string;
/** Путь к JSON service-account для Firebase Admin SDK (опц.); живая сверка email_verified. */
firebaseServiceAccountPath?: string;
upstreamTimeoutMs: number;
/** Allowed browser origins for CORS (empty = cross-origin disabled). */
corsOrigins: string[];
};
const envSchema = {
type: "object",
properties: {
NODE_ENV: {
type: "string",
enum: ["development", "production", "test"],
default: "development",
},
PORT: { type: "string", default: "3002" },
CATALOG_SERVICE_URL: { type: "string", minLength: 1, default: "http://localhost:3001" },
GEO_SERVICE_URL: { type: "string", minLength: 1, default: "http://localhost:3003" },
GAMES_SERVICE_URL: { type: "string", minLength: 1, default: "http://localhost:3004" },
SERVICE_TOKEN: { type: "string", minLength: 16 },
FIREBASE_PROJECT_ID: { type: "string", minLength: 1 },
FIREBASE_SERVICE_ACCOUNT_PATH: { type: "string" },
UPSTREAM_TIMEOUT_MS: { type: "string", default: "5000" },
CORS_ORIGINS: { type: "string", default: "" },
},
required: ["SERVICE_TOKEN", "FIREBASE_PROJECT_ID"],
additionalProperties: true,
} as const;
function parsePort(raw: string): number {
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
throw new Error(`Invalid PORT: ${raw}`);
}
return parsed;
}
function parseTimeout(raw: string): number {
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed < 100 || parsed > 30000) {
throw new Error(`Invalid UPSTREAM_TIMEOUT_MS: ${raw}`);
}
return parsed;
}
/** Comma-separated list of allowed origins, e.g. "https://app.web.app,http://localhost:4200". */
function parseCorsOrigins(raw: string | undefined): string[] {
if (!raw) {
return [];
}
return raw
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin.length > 0);
}
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
const data = validateEnv<NodeJS.ProcessEnv & Record<string, string>>(envSchema, env);
const catalogServiceUrl = new URL(data.CATALOG_SERVICE_URL ?? "http://localhost:3001")
.toString()
.replace(/\/$/, "");
const geoServiceUrl = new URL(data.GEO_SERVICE_URL ?? "http://localhost:3003")
.toString()
.replace(/\/$/, "");
const gamesServiceUrl = new URL(data.GAMES_SERVICE_URL ?? "http://localhost:3004")
.toString()
.replace(/\/$/, "");
return {
port: parsePort(data.PORT ?? "3002"),
nodeEnv: data.NODE_ENV ?? "development",
catalogServiceUrl,
geoServiceUrl,
gamesServiceUrl,
serviceToken: data.SERVICE_TOKEN ?? "",
firebaseProjectId: data.FIREBASE_PROJECT_ID ?? "",
...(data.FIREBASE_SERVICE_ACCOUNT_PATH && data.FIREBASE_SERVICE_ACCOUNT_PATH.trim().length > 0
? { firebaseServiceAccountPath: data.FIREBASE_SERVICE_ACCOUNT_PATH.trim() }
: {}),
upstreamTimeoutMs: parseTimeout(data.UPSTREAM_TIMEOUT_MS ?? "5000"),
corsOrigins: parseCorsOrigins(data.CORS_ORIGINS),
};
}
|