Ctrl K
ComunidadGuías…

lib/capabilities.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Pure catalog of infrastructures on which minddy can depend.
 *
 * It does not read `process.env` itself: the server passes it its environment,
 * and the tests can thus prove that an empty installation does not activate anything.
 */
export type CapabilityRequirement = "required" | "replaceable" | "optional";
export type CapabilityState = "ready" | "disabled" | "incomplete" | "external";

export type CapabilityId =
  | "supabase"
  | "storage"
  | "managedBilling"
  | "managedAi"
  | "scheduler"
  | "analytics"
  | "transactionalEmail"
  | "webPush"
  | "apns"
  | "wns"
  | "github"
  | "gitlab";

export interface CapabilityStatus {
  id: CapabilityId;
  requirement: CapabilityRequirement;
  state: CapabilityState;
  configured: boolean;
  missing: string[];
  diagnostic: string;
}

export type CapabilityEnvironment = Record<string, string | undefined>;

import { resolveDeploymentEdition } from "@/lib/env";
import {
  isForgeRelayOptedOut,
  resolveForgeRelayConfig,
} from "@/lib/forge-relay";

const present = (env: CapabilityEnvironment, key: string): boolean =>
  Boolean(env[key]?.trim());

function validVapidSubject(value: string | undefined): boolean {
  const subject = value?.trim();
  return Boolean(subject?.startsWith("mailto:") || subject?.startsWith("https://"));
}

function missing(env: CapabilityEnvironment, keys: string[]): string[] {
  return keys.filter((key) => !present(env, key));
}

function status(params: {
  id: CapabilityId;
  requirement: CapabilityRequirement;
  state: CapabilityState;
  missing?: string[];
  diagnostic: string;
}): CapabilityStatus {
  return {
    id: params.id,
    requirement: params.requirement,
    state: params.state,
    configured: params.state === "ready" || params.state === "external",
    missing: params.missing ?? [],
    diagnostic: params.diagnostic,
  };
}

function optIn(
  env: CapabilityEnvironment,
  params: {
    id: CapabilityId;
    requirement: CapabilityRequirement;
    flag: string;
    keys: string[];
    ready: string;
  },
): CapabilityStatus {
  const explicit = env[params.flag]?.trim();
  if (explicit !== "1") {
    return status({
      ...params,
      state: "disabled",
      diagnostic: `${params.flag}=1 is required to enable this capability.`,
    });
  }
  const absent = missing(env, params.keys);
  return absent.length === 0
    ? status({ ...params, state: "ready", diagnostic: params.ready })
    : status({
        ...params,
        state: "incomplete",
        missing: absent,
        diagnostic: `Configuration incomplete; missing: ${absent.join(", ")}.`,
      });
}

function cloudOptIn(
  env: CapabilityEnvironment,
  edition: ReturnType<typeof resolveDeploymentEdition>,
  params: Parameters<typeof optIn>[1],
): CapabilityStatus {
  if (edition !== "cloud") {
    return status({
      ...params,
      state: "disabled",
      diagnostic: `${params.id} is only available when MINDDY_EDITION=cloud.`,
    });
  }
  return optIn(env, params);
}

/** Resolves all capabilities, without SDK import or network calls. */
export function resolveCapabilities(env: CapabilityEnvironment): Record<CapabilityId, CapabilityStatus> {
  const edition = resolveDeploymentEdition(env);
  const supabaseMissing = missing(env, [
    "MINDDY_PUBLIC_SUPABASE_URL",
    "MINDDY_PUBLIC_SUPABASE_ANON_KEY",
    "SUPABASE_SERVICE_ROLE_KEY",
  ]);
  const supabase = status({
    id: "supabase",
    requirement: "required",
    state: supabaseMissing.length === 0 ? "ready" : "incomplete",
    missing: supabaseMissing,
    diagnostic:
      supabaseMissing.length === 0
        ? "Supabase database, Auth and Realtime are configured."
        : `Core unavailable; missing: ${supabaseMissing.join(", ")}.`,
  });

  const billing = cloudOptIn(env, edition, {
    id: "managedBilling",
    requirement: "optional",
    flag: "MINDDY_MANAGED_BILLING",
    keys: [
      "STRIPE_SECRET_KEY",
      "STRIPE_WEBHOOK_SECRET",
      "STRIPE_PRICE_ID_GO",
      "STRIPE_PRICE_ID_PRO",
      "STRIPE_PRICE_ID_GO_YEARLY",
      "STRIPE_PRICE_ID_PRO_YEARLY",
    ],
    ready: "Managed Stripe billing is enabled.",
  });
  const sharedAi = cloudOptIn(env, edition, {
    id: "managedAi",
    requirement: "replaceable",
    flag: "MINDDY_MANAGED_AI",
    keys: ["OPENROUTER_API_KEY"],
    ready: "Managed OpenRouter quota is enabled; BYOK remains an alternative.",
  });
  // Billed plans pay with per-account OpenRouter keys (MIN-761), which the
  // management key issues; without it, billed AI calls are refused.
  const managedAi = sharedAi.state === "ready" && billing.state === "ready" &&
    !present(env, "OPENROUTER_MANAGEMENT_KEY") && !present(env, "OPENROUTER_PROVISIONING_KEY")
    ? status({
        id: "managedAi",
        requirement: "replaceable",
        state: "incomplete",
        missing: ["OPENROUTER_MANAGEMENT_KEY"],
        diagnostic: "Configuration incomplete; missing: OPENROUTER_MANAGEMENT_KEY (per-account keys for billed plans).",
      })
    : sharedAi;

  const clientAnalytics = present(env, "MINDDY_PUBLIC_POSTHOG_KEY") &&
    present(env, "MINDDY_PUBLIC_POSTHOG_HOST");
  const serverAnalytics = present(env, "POSTHOG_API_KEY") && present(env, "POSTHOG_HOST");
  const analyticsMissing = clientAnalytics || serverAnalytics
    ? []
    : ["MINDDY_PUBLIC_POSTHOG_KEY + MINDDY_PUBLIC_POSTHOG_HOST or POSTHOG_API_KEY + POSTHOG_HOST"];
  const analytics = status({
    id: "analytics",
    requirement: "optional",
    state: analyticsMissing.length === 0 ? "ready" : "disabled",
    missing: analyticsMissing,
    diagnostic:
      analyticsMissing.length === 0
        ? "PostHog analytics is configured."
        : "Analytics is disabled; no PostHog SDK is initialized and no event is sent.",
  });

  const emailProvider = env.EMAIL_PROVIDER?.trim();
  const emailMissing = missing(env, ["RESEND_API_KEY"]);
  if (!present(env, "FEEDBACK_EMAIL_FROM")) emailMissing.push("FEEDBACK_EMAIL_FROM");
  if (!present(env, "INVITATION_EMAIL_FROM")) emailMissing.push("INVITATION_EMAIL_FROM");
  const transactionalEmail =
    emailProvider !== "resend"
      ? status({
          id: "transactionalEmail",
          requirement: "replaceable",
          state: "disabled",
          diagnostic:
            "Application email is disabled. Set EMAIL_PROVIDER=resend with explicit senders, or configure SMTP in Supabase for Auth mail.",
        })
      : emailMissing.length === 0
        ? status({
            id: "transactionalEmail",
            requirement: "replaceable",
            state: "ready",
            diagnostic: "Resend application email is explicitly enabled.",
          })
        : status({
            id: "transactionalEmail",
            requirement: "replaceable",
            state: "incomplete",
            missing: emailMissing,
            diagnostic: `Resend selected but configuration is missing: ${emailMissing.join(", ")}.`,
          });

  const webPushMissing = missing(env, [
    "MINDDY_PUBLIC_VAPID_PUBLIC_KEY",
    "VAPID_PRIVATE_KEY",
  ]);
  const vapidSubject = env.VAPID_SUBJECT;
  if (!validVapidSubject(vapidSubject)) webPushMissing.push("VAPID_SUBJECT (mailto: or https:)");
  const apnsMissing = missing(env, [
    "APNS_TEAM_ID",
    "APNS_KEY_ID",
    "APNS_PRIVATE_KEY",
  ]);
  if (!present(env, "APNS_BUNDLE_ID")) {
    apnsMissing.push("APNS_BUNDLE_ID");
  }
  const wnsMissing = missing(env, [
    "WNS_TENANT_ID",
    "WNS_APP_ID",
    "WNS_CLIENT_SECRET",
  ]);
  const gitStateMissing = missing(env, ["GIT_STATE_SECRET"]);
  const githubMissing = missing(env, [
    "GITHUB_APP_ID",
    "GITHUB_APP_SLUG",
    "GITHUB_APP_PRIVATE_KEY",
    "GIT_STATE_SECRET",
  ]);
  const forgeTokenSecret = (
    env.GIT_TOKEN_ENCRYPTION_SECRET ?? env.GITLAB_TOKEN_ENCRYPTION_SECRET
  )?.trim();
  const gitlabSecret = Boolean(forgeTokenSecret && forgeTokenSecret.length >= 32);
  const relayMissing = [
    ...gitStateMissing,
    ...(gitlabSecret ? [] : ["GIT_TOKEN_ENCRYPTION_SECRET (at least 32 characters)"]),
  ];
  const gitlabMissing = [
    ...missing(env, ["GITLAB_OAUTH_CLIENT_ID", "GITLAB_OAUTH_CLIENT_SECRET", "GIT_STATE_SECRET"]),
    ...(gitlabSecret ? [] : ["GIT_TOKEN_ENCRYPTION_SECRET"]),
  ];

  const optional = (
    id: CapabilityId,
    absent: string[],
    ready: string,
    off: string,
  ): CapabilityStatus =>
    status({
      id,
      requirement: "optional",
      state: absent.length === 0 ? "ready" : "disabled",
      missing: absent,
      diagnostic:
        absent.length === 0 ? ready : `${off} Missing: ${absent.join(", ")}.`,
    });

  /**
   * A git provider served either by the operator-owned app (local
   * credentials, precedence for new connections when both are configured) or
   * by the managed forge relay (docs/managed-forge-relay-plan.md). The relay
   * is a REPLACEABLE provider with three activation shapes, in order:
   * explicit relay variables (pinned control plane), then automatic
   * provisioning — the DEFAULT on a self-hosted edition without local app,
   * where credentials are issued on first connect and stored in the instance
   * database — and finally `MINDDY_FORGE_RELAY=0`, which disables it exactly
   * like a missing local app. Note this is the CONFIGURATION-level view:
   * each connection also carries a `source` marker, and token routing
   * follows the channel the connection was actually established through
   * until it is reconnected.
   */
  const gitProvider = (
    id: CapabilityId,
    localMissing: string[],
    relayMissing: string[],
    localReady: string,
    localOff: string,
    relayReady: string,
    autoReady: string,
  ): CapabilityStatus => {
    if (localMissing.length === 0) {
      return status({
        id,
        requirement: "optional",
        state: "ready",
        diagnostic: localReady,
      });
    }
    if (resolveForgeRelayConfig(env)) {
      return relayMissing.length === 0
        ? status({
            id,
            requirement: "replaceable",
            state: "ready",
            diagnostic: relayReady,
          })
        : status({
            id,
            requirement: "replaceable",
            state: "incomplete",
            missing: relayMissing,
            diagnostic: `Managed forge relay selected but configuration is missing: ${relayMissing.join(", ")}.`,
          });
    }
    if (
      resolveDeploymentEdition(env) === "self-hosted" &&
      !isForgeRelayOptedOut(env)
    ) {
      // Automatic provisioning removes only the CLOUD-side app credentials:
      // the instance-side secrets (git state signing, token encryption) stay
      // required — relayed tokens and claim states use them exactly like the
      // pinned-relay path.
      return relayMissing.length === 0
        ? status({
            id,
            requirement: "replaceable",
            state: "ready",
            diagnostic: autoReady,
          })
        : status({
            id,
            requirement: "replaceable",
            state: "incomplete",
            missing: relayMissing,
            diagnostic: `Managed forge relay is selected by default but configuration is missing: ${relayMissing.join(", ")}.`,
          });
    }
    return status({
      id,
      requirement: "optional",
      state: "disabled",
      missing: localMissing,
      diagnostic: `${localOff} Missing: ${localMissing.join(", ")}.`,
    });
  };

  return {
    supabase,
    storage: status({
      id: "storage",
      requirement: "required",
      state: supabase.state,
      missing: supabase.missing,
      diagnostic:
        supabase.state === "ready"
          ? "Supabase Storage is the core storage backend."
          : "Storage unavailable until the required Supabase stack is configured.",
    }),
    managedBilling: billing,
    managedAi,
    scheduler:
      !present(env, "CRON_SECRET")
          ? status({
              id: "scheduler",
              requirement: "replaceable",
              state: "disabled",
              missing: ["CRON_SECRET"],
              diagnostic:
                "Background jobs are disabled; configure CRON_SECRET and an HTTP scheduler to enable them.",
            })
          : status({
              id: "scheduler",
              requirement: "replaceable",
              state: "external",
              diagnostic:
                "Cron routes are enabled and authenticated for the configured external scheduler.",
            }),
    analytics,
    transactionalEmail,
    webPush: optional(
      "webPush",
      webPushMissing,
      "Web Push is configured.",
      "Web Push is disabled; inbox notifications continue without network delivery.",
    ),
    apns: optional(
      "apns",
      apnsMissing,
      "APNs is configured.",
      "APNs is disabled; no connection to Apple is opened.",
    ),
    wns: optional(
      "wns",
      wnsMissing,
      "WNS is configured.",
      "WNS is disabled; no connection to Microsoft is opened.",
    ),
    github: gitProvider(
      "github",
      githubMissing,
      relayMissing,
      "GitHub App integration is configured (operator-owned app).",
      "GitHub integration is hidden because its configuration is absent or incomplete.",
      "GitHub integration is served by the managed forge relay.",
      "GitHub integration connects through the managed forge relay; credentials are provisioned automatically on first connect.",
    ),
    gitlab: gitProvider(
      "gitlab",
      gitlabMissing,
      // Relayed tokens are stored instance-side (encrypted at rest), so the
      // relay path requires the same encryption secret as the local one —
      // without it the connect flow would fail at encrypt time after Cloud
      // had already consumed the brokered delivery.
      relayMissing,
      "GitLab OAuth integration is configured (operator-owned app).",
      "GitLab integration is hidden because its configuration is absent or incomplete.",
      "GitLab integration is served by the managed forge relay.",
      "GitLab integration connects through the managed forge relay; credentials are provisioned automatically on first connect.",
    ),
  };
}