Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
env-health.ts8.4 KB · 217 lines
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/**
 * Environment / feature health — makes silently-disabled features visible.
 *
 * A dozen major features quietly turn off when their env vars are unset
 * (no ANTHROPIC_API_KEY → every AI feature is a no-op, no RESEND_API_KEY →
 * verification emails go to stderr, …). This module turns that implicit
 * state into an explicit, renderable checklist for /admin/env-health.
 *
 *   collectEnvHealth(env?)        — pure + synchronous, env-only checks.
 *   collectEnvHealthWithDb()      — augments the pure result with DB-backed
 *                                   toggles (currently: Google OAuth rows
 *                                   saved via /admin/google-oauth).
 *
 * SECURITY: items only ever carry set/unset booleans — never the values.
 * Keep it that way; this page is rendered into HTML for site admins and
 * the JSON shape may end up in logs or screenshots.
 */

import { getGoogleOauthConfig } from "./sso";

export type EnvHealthSeverity = "critical" | "recommended" | "optional";

export interface EnvHealthItem {
  /** Human-facing feature name, e.g. "AI features (review, incidents, …)". */
  feature: string;
  /** Env vars that control the feature — names only, never values. */
  envVars: string[];
  /** True when the feature is live in the current environment. */
  configured: boolean;
  /** One-liner: what silently turns off when this is missing. */
  impact: string;
  severity: EnvHealthSeverity;
}

/** Render order for the grouped table. */
export const SEVERITY_ORDER: EnvHealthSeverity[] = [
  "critical",
  "recommended",
  "optional",
];

/** Truthy = non-empty after trim. Never returns the value itself. */
function isSet(env: NodeJS.ProcessEnv, name: string): boolean {
  return (env[name] || "").trim().length > 0;
}

/**
 * Pure, synchronous snapshot of every env-gated feature. Pass a synthetic
 * env object in tests; defaults to `process.env` at call time (matching
 * the getter-based pattern in `src/lib/config.ts` — values are read at
 * access time, never cached at import).
 */
export function collectEnvHealth(
  env: NodeJS.ProcessEnv = process.env
): EnvHealthItem[] {
  const appBaseUrl = (env.APP_BASE_URL || "").trim();

  return [
    // ─── Critical — core product surface degrades without these ───
    {
      feature: "AI features (PR review, incidents, commit messages, …)",
      envVars: ["ANTHROPIC_API_KEY"],
      configured: isSet(env, "ANTHROPIC_API_KEY"),
      impact:
        "Every AI surface silently no-ops: AI PR review, incident responder, commit messages, changelogs, test generation.",
      severity: "critical",
    },
    {
      feature: "Email delivery (verification, password reset)",
      envVars: ["EMAIL_PROVIDER", "RESEND_API_KEY"],
      // The provider must be "resend" AND the key present — the default
      // "log" provider only writes outbound mail to stderr (dev mode).
      configured:
        (env.EMAIL_PROVIDER || "").trim().toLowerCase() === "resend" &&
        isSet(env, "RESEND_API_KEY"),
      impact:
        "Outbound email goes to stderr instead of users — email verification, password resets, and digests never arrive.",
      severity: "critical",
    },
    {
      feature: "Canonical base URL (APP_BASE_URL)",
      envVars: ["APP_BASE_URL"],
      // Set AND not pointing at localhost — the default breaks OAuth
      // redirects and every link in outbound email.
      configured: appBaseUrl.length > 0 && !appBaseUrl.includes("localhost"),
      impact:
        "Links in emails/webhooks point at http://localhost:3000 and OAuth fails with redirect_uri_mismatch.",
      severity: "critical",
    },

    // ─── Recommended — feature works but in a degraded mode ───
    {
      feature: "Semantic code search (real embeddings)",
      envVars: ["VOYAGE_API_KEY"],
      configured: isSet(env, "VOYAGE_API_KEY"),
      impact:
        "Code search falls back to the hash-based local embedder instead of voyage-code-3 — noticeably worse relevance.",
      severity: "recommended",
    },
    {
      feature: "GateTest push-time security scans",
      envVars: ["GATETEST_URL", "GATETEST_API_KEY"],
      // GATETEST_URL has a baked-in default in config.ts, so the API key
      // is the real on/off switch.
      configured: isSet(env, "GATETEST_API_KEY"),
      impact:
        "Pushes are not scanned by GateTest; gate enforcement at push time is off.",
      severity: "recommended",
    },
    {
      feature: "Signed deploy webhook (Crontech)",
      envVars: ["GLUECRON_WEBHOOK_SECRET"],
      configured: isSet(env, "GLUECRON_WEBHOOK_SECRET"),
      impact:
        "Outbound deploy webhook fires without an HMAC signature header — the receiver rejects it with 401 (treated as a failed deploy).",
      severity: "recommended",
    },
    {
      feature: "PR preview builds",
      envVars: ["PREVIEW_DOMAIN"],
      configured: isSet(env, "PREVIEW_DOMAIN"),
      impact:
        "PR previews are URL-only — the preview-builder never runs and no static files are served.",
      severity: "recommended",
    },
    {
      feature: "Error tracking",
      envVars: ["SENTRY_DSN", "ERROR_WEBHOOK_URL"],
      // Either sink counts — both are fire-and-forget exporters.
      configured: isSet(env, "SENTRY_DSN") || isSet(env, "ERROR_WEBHOOK_URL"),
      impact:
        "Unhandled errors are only visible in server logs — nothing is exported to Sentry or a webhook.",
      severity: "recommended",
    },
    {
      feature: "Stable SSH host key",
      envVars: ["SSH_HOST_KEY"],
      configured: isSet(env, "SSH_HOST_KEY"),
      impact:
        "An ephemeral host key is generated on every restart — git-over-SSH clients see 'host key changed' warnings.",
      severity: "recommended",
    },

    // ─── Optional — opt-ins and scale-out knobs ───
    {
      feature: "Multi-instance SSE fan-out",
      envVars: ["REDIS_URL", "VALKEY_URL"],
      configured: isSet(env, "REDIS_URL") || isSet(env, "VALKEY_URL"),
      impact:
        "SSE events are delivered in-process only — live updates miss users connected to other instances behind a load balancer.",
      severity: "optional",
    },
    {
      feature: "Google login (env bootstrap)",
      envVars: ["GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET"],
      configured:
        isSet(env, "GOOGLE_OAUTH_CLIENT_ID") &&
        isSet(env, "GOOGLE_OAUTH_CLIENT_SECRET"),
      impact:
        "'Sign in with Google' is unavailable — unless credentials were saved at /admin/google-oauth, which also satisfies this check.",
      severity: "optional",
    },
    {
      feature: "AI auto-issue opener",
      envVars: ["AI_AUTO_ISSUES"],
      configured: (env.AI_AUTO_ISSUES || "").trim() === "1",
      impact:
        "Pushes are not scanned for TODOs / hardcoded secrets / SQL-injection patterns; no issues are auto-opened. Opt-in: set to \"1\".",
      severity: "optional",
    },
    {
      feature: "Dependency CVE scanner",
      envVars: ["DEPENDENCY_SCAN_ENABLED"],
      configured: (env.DEPENDENCY_SCAN_ENABLED || "").trim() === "1",
      impact:
        "Manifest changes (package.json, requirements.txt, …) are not scanned for known CVEs on push. Opt-in: set to \"1\".",
      severity: "optional",
    },
  ];
}

/**
 * Async variant that augments the pure env snapshot with DB-backed toggles.
 * v1 only checks Google OAuth: a row saved via /admin/google-oauth enables
 * Google login even when the env pair is unset. DB failures degrade to the
 * env-only result — this must never take the admin page down.
 */
export async function collectEnvHealthWithDb(
  env: NodeJS.ProcessEnv = process.env
): Promise<EnvHealthItem[]> {
  const items = collectEnvHealth(env);

  try {
    // getGoogleOauthConfig() already merges DB row + env fallback.
    const google = await getGoogleOauthConfig();
    if (google?.clientId && google?.clientSecret) {
      const item = items.find((i) =>
        i.envVars.includes("GOOGLE_OAUTH_CLIENT_ID")
      );
      if (item) item.configured = true;
    }
  } catch {
    // DB unreachable — keep the env-only answer.
  }

  return items;
}

/** Group items by severity in render order (critical → recommended → optional). */
export function groupBySeverity(
  items: EnvHealthItem[]
): Array<{ severity: EnvHealthSeverity; items: EnvHealthItem[] }> {
  return SEVERITY_ORDER.map((severity) => ({
    severity,
    items: items.filter((i) => i.severity === severity),
  })).filter((g) => g.items.length > 0);
}