Source: utils/envScope.js

/**
 * @module utils/envScope
 * @description DIF-012 — Single source of truth for the
 * "env-scoped project" shape used by every execution entry point that
 * accepts an optional `environmentId` override.
 *
 * Replaces four copy-pasted local helpers that previously lived in
 * `routes/runs.js`, `routes/tests.js`, `routes/trigger.js`, and
 * `workers/runWorker.js`. Consolidating them here means any future change
 * to the credential-override rule, the `canonicalUrl` stamp, or the
 * preview-URL precedence happens in exactly one place — drift between
 * the four entry points caused real bugs during DIF-012 development.
 *
 * ### Contract
 * - `environment === null` → returns `project` unchanged (zero-regression
 *   for projects without environments).
 * - `previewUrl` (Vercel/Netlify webhook path) takes precedence over
 *   `environment.baseUrl`, matching the existing AUTO-015 deploy-preview
 *   behaviour.
 * - `environment.credentials` is already in the same encrypted shape as
 *   `project.credentials` — `routes/projects.js` encrypts on POST/PATCH
 *   and the env repo only JSON-parses on read (see `rowToEnv` in
 *   `environmentRepo.js`). Assign verbatim — re-encrypting would
 *   double-encrypt and silently break login. Falls back to
 *   `project.credentials` when the env has no credentials of its own.
 * - `canonicalUrl` preserves the original production URL so the
 *   AUTO-015 baseline guard in `crawler.js` treats this as a
 *   preview-style crawl and doesn't overwrite production fingerprints.
 * - The project row in the DB is NEVER mutated; this returns a
 *   shallow-cloned scoped copy used only for the lifetime of one run.
 *
 * @param {Object} project - Persisted project row (credentials encrypted).
 * @param {Object|null} [environment] - Optional env row from
 *   `environmentRepo.getById()`. `undefined` is treated the same as
 *   `null` (e.g. when the worker looks up an env that was deleted
 *   between enqueue and pickup).
 * @param {Object} [opts]
 * @param {string|null} [opts.previewUrl] - Optional deploy-preview URL
 *   override that wins over `environment.baseUrl`.
 * @returns {Object} the scoped project.
 */
export function envScopedProject(project, environment, { previewUrl = null } = {}) {
  if (!environment && !previewUrl) return project;
  const url = previewUrl || environment?.baseUrl || project.url;
  let credentials = project.credentials;
  if (environment?.credentials && (environment.credentials.username || environment.credentials.password)) {
    credentials = environment.credentials;
  }
  return { ...project, url, credentials, canonicalUrl: project.url };
}