{"version":3,"sources":["../../src/product-telemetry.ts"],"sourcesContent":["import { FARM_VERSION } from \"./version\";\n\nexport const FARM_PRODUCTION_SITE_TELEMETRY_SCHEMA_VERSION = 1 as const;\nexport const FARM_PRODUCTION_SITE_TELEMETRY_EVENT_TYPE = \"production_site_active\" as const;\nexport const FARM_PRODUCTION_SITE_TELEMETRY_PACKAGE_NAME = \"@farm.js/core\" as const;\nexport const FARM_PRODUCTION_SITE_ATTESTATION_PATH = \"/.well-known/farm-telemetry\" as const;\nexport const FARM_PRODUCTION_SITE_ATTESTATION_EVENT_TYPE = \"production_site_attestation\" as const;\n\nconst DEFAULT_SITE_TELEMETRY_ENDPOINT = \"https://farmjs.dev/api/telemetry/v1/sites\";\nconst REQUEST_TIMEOUT_MS = 3_000;\nconst REPORT_INTERVAL_MS = 24 * 60 * 60 * 1_000;\nconst RETRY_INTERVAL_MS = 5 * 60 * 1_000;\nconst MAX_ORIGINS_PER_INSTANCE = 32;\nconst SAFE_DETAIL_PATTERN = /^[0-9A-Za-z._-]{1,64}$/;\n\nexport interface FarmProductionSiteTelemetryPayload {\n  schemaVersion: typeof FARM_PRODUCTION_SITE_TELEMETRY_SCHEMA_VERSION;\n  eventType: typeof FARM_PRODUCTION_SITE_TELEMETRY_EVENT_TYPE;\n  siteUrl: string;\n  packageName: typeof FARM_PRODUCTION_SITE_TELEMETRY_PACKAGE_NAME;\n  packageVersion: string;\n  renderer: string;\n  deployTarget: string;\n}\n\nexport interface FarmProductionSiteAttestation {\n  schemaVersion: typeof FARM_PRODUCTION_SITE_TELEMETRY_SCHEMA_VERSION;\n  eventType: typeof FARM_PRODUCTION_SITE_ATTESTATION_EVENT_TYPE;\n  packageName: typeof FARM_PRODUCTION_SITE_TELEMETRY_PACKAGE_NAME;\n  packageVersion: string;\n  renderer: string;\n  deployTarget: string;\n}\n\ninterface FarmProductionSiteReporterOptions {\n  renderer: string;\n  deployTarget?: string;\n  endpoint?: string;\n  fetch?: typeof fetch;\n  now?: () => number;\n  reportIntervalMs?: number;\n  retryIntervalMs?: number;\n}\n\ninterface OriginReportState {\n  pending?: Promise<void>;\n  nextReportAt: number;\n}\n\nexport interface FarmProductionSiteReporter {\n  handleAttestation(request: Request): Response | null;\n  report(requestUrl: string | URL, waitUntil?: (promise: Promise<unknown>) => void): void;\n}\n\n/**\n * Reduce an automatically observed request URL to a public HTTPS origin.\n * Request paths, query strings, hashes, and credentials are never returned.\n */\nexport function detectFarmProductionSiteOrigin(value: string | URL): string | undefined {\n  try {\n    const url = value instanceof URL ? value : new URL(value);\n    return normalizeFarmProductionSiteOrigin(url.origin);\n  } catch {\n    return undefined;\n  }\n}\n\n/** Normalize and validate the origin-only value accepted by the ingestion service. */\nexport function normalizeFarmProductionSiteOrigin(value: string): string | undefined {\n  const candidate = value.trim();\n  if (!candidate || candidate.length > 2_048) return undefined;\n\n  try {\n    const url = new URL(candidate);\n    const hostname = url.hostname.toLowerCase();\n    const isIpv4 = /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(hostname);\n    const isIpv6 = hostname.startsWith(\"[\") && hostname.endsWith(\"]\");\n    const isLocal =\n      hostname === \"localhost\" || hostname.endsWith(\".localhost\") || hostname.endsWith(\".local\");\n    const isSingleLabel = !hostname.includes(\".\");\n\n    if (\n      url.protocol !== \"https:\" ||\n      url.username ||\n      url.password ||\n      url.search ||\n      url.hash ||\n      url.pathname !== \"/\" ||\n      isIpv4 ||\n      isIpv6 ||\n      isLocal ||\n      isSingleLabel ||\n      hostname.endsWith(\".\")\n    ) {\n      return undefined;\n    }\n\n    return url.origin;\n  } catch {\n    return undefined;\n  }\n}\n\nexport function createFarmProductionSiteReporter(\n  options: FarmProductionSiteReporterOptions,\n): FarmProductionSiteReporter {\n  const now = options.now ?? Date.now;\n  const reportIntervalMs = options.reportIntervalMs ?? REPORT_INTERVAL_MS;\n  const retryIntervalMs = options.retryIntervalMs ?? RETRY_INTERVAL_MS;\n  const send = options.fetch ?? globalThis.fetch;\n  const reportStates = new Map<string, OriginReportState>();\n  const attestation: FarmProductionSiteAttestation = {\n    schemaVersion: FARM_PRODUCTION_SITE_TELEMETRY_SCHEMA_VERSION,\n    eventType: FARM_PRODUCTION_SITE_ATTESTATION_EVENT_TYPE,\n    packageName: FARM_PRODUCTION_SITE_TELEMETRY_PACKAGE_NAME,\n    packageVersion: sanitizeDetail(FARM_VERSION, \"unknown\"),\n    renderer: sanitizeDetail(options.renderer, \"custom\"),\n    deployTarget: sanitizeDetail(options.deployTarget, \"custom\"),\n  };\n\n  return {\n    handleAttestation(request) {\n      if (productionTelemetryDisabled()) return null;\n\n      let pathname: string;\n      try {\n        pathname = new URL(request.url).pathname;\n      } catch {\n        return null;\n      }\n      if (pathname !== FARM_PRODUCTION_SITE_ATTESTATION_PATH) return null;\n\n      if (request.method !== \"GET\" && request.method !== \"HEAD\") {\n        return new Response(\"Method Not Allowed\", {\n          status: 405,\n          headers: {\n            allow: \"GET, HEAD\",\n            \"cache-control\": \"no-store\",\n            \"content-type\": \"text/plain; charset=utf-8\",\n            \"x-content-type-options\": \"nosniff\",\n          },\n        });\n      }\n\n      return new Response(request.method === \"HEAD\" ? null : JSON.stringify(attestation), {\n        status: 200,\n        headers: {\n          \"cache-control\": \"no-store\",\n          \"content-type\": \"application/json; charset=utf-8\",\n          \"x-content-type-options\": \"nosniff\",\n        },\n      });\n    },\n    report(requestUrl, waitUntil) {\n      if (productionTelemetryDisabled()) return;\n      if (!isProductionDeploymentEnvironment()) {\n        debug(\"production-site check-in skipped outside a production deployment\");\n        return;\n      }\n\n      const siteUrl = detectFarmProductionSiteOrigin(requestUrl);\n      if (!siteUrl) return;\n\n      let state = reportStates.get(siteUrl);\n      if (!state) {\n        if (reportStates.size >= MAX_ORIGINS_PER_INSTANCE) return;\n        state = { nextReportAt: 0 };\n        reportStates.set(siteUrl, state);\n      }\n\n      const attemptedAt = now();\n      if (state.pending || attemptedAt < state.nextReportAt) return;\n\n      state.nextReportAt = attemptedAt + reportIntervalMs;\n      const payload: FarmProductionSiteTelemetryPayload = {\n        schemaVersion: FARM_PRODUCTION_SITE_TELEMETRY_SCHEMA_VERSION,\n        eventType: FARM_PRODUCTION_SITE_TELEMETRY_EVENT_TYPE,\n        siteUrl,\n        packageName: FARM_PRODUCTION_SITE_TELEMETRY_PACKAGE_NAME,\n        packageVersion: attestation.packageVersion,\n        renderer: attestation.renderer,\n        deployTarget: attestation.deployTarget,\n      };\n      state.pending = deliver(send, resolveSiteTelemetryEndpoint(options.endpoint), payload)\n        .then((delivered) => {\n          if (!delivered) state.nextReportAt = now() + retryIntervalMs;\n        })\n        .catch(() => {\n          state.nextReportAt = now() + retryIntervalMs;\n        })\n        .finally(() => {\n          state.pending = undefined;\n        });\n\n      if (waitUntil) {\n        try {\n          waitUntil(state.pending);\n        } catch {\n          // Delivery remains best-effort if a platform rejects background work.\n        }\n      } else {\n        void state.pending;\n      }\n    },\n  };\n}\n\nasync function deliver(\n  send: typeof fetch,\n  endpoint: string | undefined,\n  payload: FarmProductionSiteTelemetryPayload,\n): Promise<boolean> {\n  if (!endpoint) {\n    debug(\"invalid or insecure production-site telemetry endpoint; check-in skipped\");\n    return false;\n  }\n\n  const controller = new AbortController();\n  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n  timeout.unref?.();\n  try {\n    const response = await send(endpoint, {\n      method: \"POST\",\n      headers: { \"content-type\": \"application/json\" },\n      body: JSON.stringify(payload),\n      signal: controller.signal,\n    });\n    if (!response.ok) {\n      debug(`production-site check-in rejected with HTTP ${response.status}`);\n      return false;\n    }\n    try {\n      const result = (await response.json()) as { stored?: unknown };\n      if (result && result.stored === false) {\n        debug(\"production-site check-in was accepted but not stored\");\n        return false;\n      }\n    } catch {\n      // A successful empty response is also a valid acknowledgement.\n    }\n    return true;\n  } catch {\n    debug(\"production-site check-in failed\");\n    return false;\n  } finally {\n    clearTimeout(timeout);\n  }\n}\n\nfunction resolveSiteTelemetryEndpoint(explicit?: string): string | undefined {\n  const candidate = explicit || process.env.FARM_TELEMETRY_SITE_ENDPOINT;\n  if (!candidate) return DEFAULT_SITE_TELEMETRY_ENDPOINT;\n\n  try {\n    const url = new URL(candidate);\n    const hostname = url.hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n    const isLocal = [\"localhost\", \"127.0.0.1\", \"::1\"].includes(hostname);\n    if (url.protocol === \"https:\" || (url.protocol === \"http:\" && isLocal)) {\n      return url.toString();\n    }\n  } catch {\n    // Invalid explicit overrides fail closed below.\n  }\n  return undefined;\n}\n\nfunction productionTelemetryDisabled(): boolean {\n  if (process.env.DO_NOT_TRACK !== undefined && !isFalse(process.env.DO_NOT_TRACK)) return true;\n  if (isTrue(process.env.FARM_TELEMETRY_DISABLED)) return true;\n  return isFalse(process.env.FARM_TELEMETRY);\n}\n\n/**\n * Only a provider's production environment represents a production site;\n * preview, branch, and development deployments must not create dashboard\n * entries. Each provider exposes this differently, so every signal we can read\n * is checked. A deployment whose provider exposes no environment metadata stays\n * eligible, so self-hosted sites continue to report.\n *\n * Cloudflare Pages is deliberately absent: it exposes `CF_PAGES_BRANCH` for both\n * production and preview deployments and no environment flag that distinguishes\n * them, so there is nothing here that could be read without guessing.\n */\nfunction isProductionDeploymentEnvironment(): boolean {\n  // Vercel exposes the deployment environment at build and runtime.\n  const environment = normalizeEnvironment(process.env.VERCEL_ENV);\n  const targetEnvironment = normalizeEnvironment(process.env.VERCEL_TARGET_ENV);\n  if (environment && environment !== \"production\") return false;\n  if (targetEnvironment && targetEnvironment !== \"production\") return false;\n\n  // Netlify: CONTEXT is production | deploy-preview | branch-deploy | dev.\n  // CONTEXT is a generic name, so it is only trusted when NETLIFY marks the\n  // build as Netlify's.\n  if (isTrue(process.env.NETLIFY)) {\n    const context = normalizeEnvironment(process.env.CONTEXT);\n    if (context && context !== \"production\") return false;\n  }\n\n  // Render marks pull-request preview services.\n  if (isTrue(process.env.IS_PULL_REQUEST)) return false;\n\n  return true;\n}\n\nfunction normalizeEnvironment(value: string | undefined): string | undefined {\n  const normalized = value?.trim().toLowerCase();\n  return normalized || undefined;\n}\n\nfunction sanitizeDetail(value: string | undefined, fallback: string): string {\n  return value && SAFE_DETAIL_PATTERN.test(value) ? value : fallback;\n}\n\nfunction isTrue(value: string | undefined): boolean {\n  return value !== undefined && [\"1\", \"true\", \"yes\", \"on\"].includes(value.toLowerCase());\n}\n\nfunction isFalse(value: string | undefined): boolean {\n  return value !== undefined && [\"0\", \"false\", \"no\", \"off\"].includes(value.toLowerCase());\n}\n\nfunction debug(message: string): void {\n  if (!isTrue(process.env.FARM_TELEMETRY_DEBUG)) return;\n  process.stderr.write(`[farm.telemetry] ${message}\\n`);\n}\n"],"mappings":"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","names":[]}