{"version":3,"file":"infra.mjs","names":[],"sources":["../../src/services/infra.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { RuntimeConfig } from \"../types\";\nimport { frameworkOwnedHeaderComment } from \"./scaffold/ownership\";\n\nconst POSTGRES_USER = \"everythingdev\";\nconst POSTGRES_PASSWORD = \"everythingdev\";\nconst AUTH_DATABASE_SECRET = \"AUTH_DATABASE_URL\";\nconst HOST_SECRET = \"CORS_ORIGIN\";\nconst REDIS_PORT = 6379;\nconst TEST_AUTH_SECRET = \"regression-test-secret-do-not-use-in-production\";\nconst VESTIGIAL_SECRETS = new Set([\n  \"NEAR_RELAYER_PRIVATE_KEY_MAINNET\",\n  \"NEAR_RELAYER_PRIVATE_KEY_TESTNET\",\n]);\n\n/**\n * Fixed local-development database conventions. The committed\n * docker-compose.yml provisions exactly these services/ports — nothing is\n * derived from runtime config. Every non-auth `*_DATABASE_URL` secret shares\n * the api database (schema-isolated per plugin); auth gets its own.\n */\nconst DEV_API_DB_PORT = 5432;\nconst DEV_AUTH_DB_PORT = 5433;\nconst TEST_API_DB_PORT = 5434;\nconst TEST_AUTH_DB_PORT = 5435;\n\nexport interface DatabaseConvention {\n  secret: string;\n  slug: string;\n  port: number;\n  databaseName: string;\n  url: string;\n}\n\nexport interface RedisConvention {\n  secret: string;\n  slug: string;\n  port: number;\n  url: string;\n}\n\nexport interface SecretGroup {\n  section: string;\n  secrets: string[];\n}\n\ninterface DevPortState {\n  host?: number;\n  api?: number;\n  ui?: number;\n  auth?: number;\n  pluginPortStart?: number;\n}\n\nexport type { DevPortState };\n\nexport interface PortState {\n  devPorts?: DevPortState;\n}\n\nfunction uniqueSecrets(values: Array<string | undefined>): string[] {\n  const secrets: string[] = [];\n  const seen = new Set<string>();\n\n  for (const value of values) {\n    if (!value || seen.has(value)) continue;\n    seen.add(value);\n    secrets.push(value);\n  }\n\n  return secrets;\n}\n\nexport function loadPortState(configDir?: string): PortState {\n  if (!configDir) return {};\n  const statePath = join(configDir, \".bos\", \"infra-state.json\");\n  if (!existsSync(statePath)) return {};\n  try {\n    const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Partial<PortState>;\n    return { devPorts: raw.devPorts };\n  } catch {\n    return {};\n  }\n}\n\nexport function savePortState(configDir: string, state: PortState): void {\n  const statePath = join(configDir, \".bos\", \"infra-state.json\");\n  mkdirSync(dirname(statePath), { recursive: true });\n  writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nexport function normalizeDatabaseSlug(secret: string): string {\n  return secret.replace(/_DATABASE_URL$/, \"\").toLowerCase();\n}\n\nfunction normalizeRedisSlug(secret: string): string {\n  return secret.replace(/_REDIS_URL$/, \"\").toLowerCase();\n}\n\nfunction databaseUrl(port: number, databaseName: string): string {\n  return `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${databaseName}`;\n}\n\nexport function buildConventionalDatabases(secrets: string[]): DatabaseConvention[] {\n  const databaseSecrets = uniqueSecrets(\n    secrets.filter((secret) => secret.endsWith(\"_DATABASE_URL\")),\n  );\n\n  return databaseSecrets\n    .sort((a, b) => normalizeDatabaseSlug(a).localeCompare(normalizeDatabaseSlug(b)))\n    .map((secret) => {\n      const slug = normalizeDatabaseSlug(secret);\n      const isAuth = secret === AUTH_DATABASE_SECRET;\n      const port = isAuth ? DEV_AUTH_DB_PORT : DEV_API_DB_PORT;\n      const databaseName = isAuth ? \"auth_db\" : \"api_db\";\n      return { secret, slug, port, databaseName, url: databaseUrl(port, databaseName) };\n    });\n}\n\nexport function buildConventionalRedis(secrets: string[]): RedisConvention[] {\n  const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith(\"_REDIS_URL\")));\n\n  return redisSecrets\n    .sort((a, b) => normalizeRedisSlug(a).localeCompare(normalizeRedisSlug(b)))\n    .map((secret) => ({\n      secret,\n      slug: normalizeRedisSlug(secret),\n      port: REDIS_PORT,\n      url: `redis://localhost:${REDIS_PORT}`,\n    }));\n}\n\nfunction testDatabaseUrl(secret: string): string {\n  const isAuth = secret === AUTH_DATABASE_SECRET;\n  const port = isAuth ? TEST_AUTH_DB_PORT : TEST_API_DB_PORT;\n  const databaseName = isAuth ? \"auth_test_db\" : \"api_test_db\";\n  return databaseUrl(port, databaseName);\n}\n\nexport function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] {\n  const groups: SecretGroup[] = [];\n  const seen = new Set<string>();\n\n  const addGroup = (section: string, secrets: string[]) => {\n    const filtered = secrets.filter((s) => {\n      if (VESTIGIAL_SECRETS.has(s)) return false;\n      if (seen.has(s)) return false;\n      seen.add(s);\n      return true;\n    });\n    if (filtered.length > 0) {\n      groups.push({ section, secrets: filtered });\n    }\n  };\n\n  addGroup(\"app.host\", uniqueSecrets([...(runtimeConfig.host.secrets ?? []), HOST_SECRET]));\n\n  addGroup(\"app.api\", uniqueSecrets(runtimeConfig.api.secrets ?? []));\n\n  if (runtimeConfig.auth) {\n    addGroup(\"app.auth\", uniqueSecrets(runtimeConfig.auth.secrets ?? []));\n  }\n\n  if (runtimeConfig.plugins) {\n    for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) {\n      if (plugin.secrets && plugin.secrets.length > 0) {\n        addGroup(`plugins.${pluginKey}`, plugin.secrets);\n      }\n    }\n  }\n\n  return groups;\n}\n\nfunction defaultSecretValue(\n  secret: string,\n  databases: Map<string, DatabaseConvention>,\n  redisConfigs: Map<string, RedisConvention>,\n  options: { forExample: boolean; devHostPort?: number },\n): string {\n  if (secret === \"BETTER_AUTH_SECRET\") {\n    return options.forExample ? \"\" : randomBytes(32).toString(\"base64url\");\n  }\n\n  if (secret === \"CORS_ORIGIN\") {\n    if (options.forExample) {\n      return \"http://localhost:3000\";\n    }\n    if (typeof options.devHostPort === \"number\") {\n      return `http://localhost:${options.devHostPort}`;\n    }\n    return \"http://localhost:3000\";\n  }\n\n  // Zero-config dev (plan 055 ticket 11): substrate URLs are opt-in — the\n  // example renders them empty so the consumers' pglite boot defaults apply;\n  // the operator sets a postgres:// URL to demand compose Postgres.\n  if (options.forExample) {\n    return \"\";\n  }\n\n  return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? \"\";\n}\n\n/**\n * Plugin `*_DATABASE_URL` secrets share the API database (the platform's\n * two-database contract), so they are not rendered as template entries —\n * the host falls back to `API_DATABASE_URL` when they are unset.\n */\nfunction isFallbackDatabaseSecret(secret: string): boolean {\n  return (\n    secret.endsWith(\"_DATABASE_URL\") &&\n    secret !== \"API_DATABASE_URL\" &&\n    secret !== \"AUTH_DATABASE_URL\"\n  );\n}\n\nfunction renderEnvFile(\n  groups: SecretGroup[],\n  options: { forExample: boolean; devHostPort?: number },\n): string {\n  const rendered = groups\n    .map((group) => ({\n      section: group.section,\n      secrets: group.secrets.filter((secret) => !isFallbackDatabaseSecret(secret)),\n    }))\n    .filter((group) => group.secrets.length > 0);\n  const allSecrets = rendered.flatMap((group) => group.secrets);\n  const databaseMap = new Map(\n    buildConventionalDatabases(allSecrets).map((entry) => [entry.secret, entry]),\n  );\n  const redisMap = new Map(\n    buildConventionalRedis(allSecrets).map((entry) => [entry.secret, entry]),\n  );\n  const lines: string[] = options.forExample\n    ? [\n        ...frameworkOwnedHeaderComment(\"hash\").trimEnd().split(\"\\n\"),\n        \"\",\n        \"# Generated from configured bos secrets\",\n        \"# Update values as needed for your local environment\",\n        \"\",\n      ]\n    : [\n        \"# Generated from configured bos secrets\",\n        \"# Update values as needed for your local environment\",\n        \"\",\n      ];\n\n  for (const group of rendered) {\n    lines.push(`# ${group.section}`);\n    for (const secret of group.secrets) {\n      if (\n        options.forExample &&\n        (secret.endsWith(\"_DATABASE_URL\") || secret.endsWith(\"_REDIS_URL\"))\n      ) {\n        lines.push(\n          secret.endsWith(\"_REDIS_URL\")\n            ? \"# Leave empty for dev defaults; set a redis:// URL to opt into a real Redis.\"\n            : \"# Leave empty to run dev on ephemeral pglite; set a postgres:// URL to opt into compose Postgres.\",\n        );\n      }\n      lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, redisMap, options)}`);\n    }\n    lines.push(\"\");\n  }\n\n  return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvFile };\n\nfunction renderEnvTestFile(groups: SecretGroup[]): string {\n  const lines: string[] = [\n    \"# Generated test environment — loaded by test suites instead of .env\",\n    \"# Test databases run on dedicated *-test docker-compose services\",\n    \"\",\n  ];\n\n  for (const group of groups) {\n    const entries: string[] = [];\n    for (const secret of group.secrets) {\n      if (secret === \"BETTER_AUTH_SECRET\") {\n        entries.push(`${secret}=${TEST_AUTH_SECRET}`);\n      } else if (secret.endsWith(\"_DATABASE_URL\")) {\n        entries.push(`${secret}=${testDatabaseUrl(secret)}`);\n      }\n    }\n    if (entries.length > 0) {\n      lines.push(`# ${group.section}`);\n      lines.push(...entries);\n      lines.push(\"\");\n    }\n  }\n\n  return `${lines.join(\"\\n\")}\\n`;\n}\n\nexport { renderEnvTestFile };\n"],"mappings":"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"}