All files / src config.ts

85.91% Statements 61/71
23.52% Branches 4/17
100% Functions 4/4
85.91% Lines 61/71

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 1001x                                       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),
  };
}