From 6fd70630155abb3182a9e5af1f3a75af9f0edb1e Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Wed, 22 Apr 2026 17:22:31 +0200 Subject: [PATCH] feat(apps): declare envMappings for every app; make field required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only the Google Workspace family declared envMappings, so granting GitHub, YouTube, Resend, or Spotify in a consumer (e.g. Humr) produced no pod env — the consumer would run the agent with no env contract, despite the gateway already knowing how to inject auth for those hosts. The omission was easy to miss because the field was optional. - Make `envMappings` required on `AppDefinition` so a new app cannot ship without declaring the env contract its CLI/SDK consumers expect. - Add `githubEnvMappings` (GH_TOKEN) shared by github + github-enterprise. - YouTube reuses googleWorkspaceEnvMappings (same Google OAuth token). - Resend declares RESEND_API_KEY; Spotify declares SPOTIFY_ACCESS_TOKEN. No route changes — `/api/connections` already joins `envMappings` from the registry, so every connection row will now carry a non-null value for known providers. Signed-off-by: Tomas Dvorak --- apps/web/src/lib/apps/github-enterprise.ts | 7 ++++++- apps/web/src/lib/apps/github-oauth.ts | 12 ++++++++++++ apps/web/src/lib/apps/github.ts | 2 ++ apps/web/src/lib/apps/resend.ts | 6 ++++++ apps/web/src/lib/apps/spotify.ts | 6 ++++++ apps/web/src/lib/apps/types.ts | 7 +++++-- apps/web/src/lib/apps/youtube.ts | 2 ++ 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/apps/github-enterprise.ts b/apps/web/src/lib/apps/github-enterprise.ts index c2427fe..e4ba06a 100644 --- a/apps/web/src/lib/apps/github-enterprise.ts +++ b/apps/web/src/lib/apps/github-enterprise.ts @@ -1,5 +1,9 @@ import type { AppDefinition } from "./types"; -import { buildGithubAuthUrl, exchangeGithubCode } from "./github-oauth"; +import { + buildGithubAuthUrl, + exchangeGithubCode, + githubEnvMappings, +} from "./github-oauth"; const cfgFromConfig = (config: Record) => { const baseUrl = config.baseUrl; @@ -115,4 +119,5 @@ export const githubEnterprise: AppDefinition = { clientSecret: "GITHUB_ENTERPRISE_CLIENT_SECRET", }, }, + envMappings: githubEnvMappings, }; diff --git a/apps/web/src/lib/apps/github-oauth.ts b/apps/web/src/lib/apps/github-oauth.ts index 419c119..d368eef 100644 --- a/apps/web/src/lib/apps/github-oauth.ts +++ b/apps/web/src/lib/apps/github-oauth.ts @@ -1,3 +1,4 @@ +import type { EnvMapping } from "@/lib/env-mapping"; import type { OAuthBuildAuthUrlParams, OAuthExchangeCodeParams, @@ -9,6 +10,17 @@ export interface GithubOAuthConfig { apiBase: string; } +/** + * Env var contract for GitHub OAuth connections (github.com + Enterprise). + * `gh` CLI, hub, and octokit-based tools read `GH_TOKEN` by default; the + * gateway swaps the sentinel for the real OAuth access token on outbound + * requests to `api.github.com`, `github.com`, and `raw.githubusercontent.com` + * (see gateway `apps.rs`). + */ +export const githubEnvMappings: EnvMapping[] = [ + { envName: "GH_TOKEN", placeholder: "humr:sentinel" }, +]; + export const buildGithubAuthUrl = ( cfg: GithubOAuthConfig, params: OAuthBuildAuthUrlParams, diff --git a/apps/web/src/lib/apps/github.ts b/apps/web/src/lib/apps/github.ts index 4d17bec..745c000 100644 --- a/apps/web/src/lib/apps/github.ts +++ b/apps/web/src/lib/apps/github.ts @@ -2,6 +2,7 @@ import type { AppDefinition } from "./types"; import { buildGithubAuthUrl, exchangeGithubCode, + githubEnvMappings, type GithubOAuthConfig, } from "./github-oauth"; @@ -94,4 +95,5 @@ export const github: AppDefinition = { clientSecret: "GITHUB_CLIENT_SECRET", }, }, + envMappings: githubEnvMappings, }; diff --git a/apps/web/src/lib/apps/resend.ts b/apps/web/src/lib/apps/resend.ts index 009ad7b..6b53e3f 100644 --- a/apps/web/src/lib/apps/resend.ts +++ b/apps/web/src/lib/apps/resend.ts @@ -1,5 +1,10 @@ +import type { EnvMapping } from "@/lib/env-mapping"; import type { AppDefinition } from "./types"; +const resendEnvMappings: EnvMapping[] = [ + { envName: "RESEND_API_KEY", placeholder: "humr:sentinel" }, +]; + export const resend: AppDefinition = { id: "resend", name: "Resend", @@ -18,4 +23,5 @@ export const resend: AppDefinition = { ], }, available: true, + envMappings: resendEnvMappings, }; diff --git a/apps/web/src/lib/apps/spotify.ts b/apps/web/src/lib/apps/spotify.ts index 4a94765..2a8c345 100644 --- a/apps/web/src/lib/apps/spotify.ts +++ b/apps/web/src/lib/apps/spotify.ts @@ -1,5 +1,10 @@ +import type { EnvMapping } from "@/lib/env-mapping"; import type { AppDefinition } from "./types"; +const spotifyEnvMappings: EnvMapping[] = [ + { envName: "SPOTIFY_ACCESS_TOKEN", placeholder: "humr:sentinel" }, +]; + export const spotify: AppDefinition = { id: "spotify", name: "Spotify", @@ -193,4 +198,5 @@ export const spotify: AppDefinition = { clientSecret: "SPOTIFY_CLIENT_SECRET", }, }, + envMappings: spotifyEnvMappings, }; diff --git a/apps/web/src/lib/apps/types.ts b/apps/web/src/lib/apps/types.ts index 9c296d4..49569ca 100644 --- a/apps/web/src/lib/apps/types.ts +++ b/apps/web/src/lib/apps/types.ts @@ -83,7 +83,10 @@ export interface AppDefinition { }; /** * Env vars a consumer should inject into any agent granted this connection. - * Omitted for apps with no known CLI integration. + * Required — every app must declare the env contract its CLI/SDK consumers + * expect so agents can call the provider without per-provider wiring. Apps + * in the same OAuth family can share an exported mapping (see + * `googleWorkspaceEnvMappings`, `githubEnvMappings`). */ - envMappings?: EnvMapping[]; + envMappings: EnvMapping[]; } diff --git a/apps/web/src/lib/apps/youtube.ts b/apps/web/src/lib/apps/youtube.ts index 1b949af..2d44dee 100644 --- a/apps/web/src/lib/apps/youtube.ts +++ b/apps/web/src/lib/apps/youtube.ts @@ -4,6 +4,7 @@ import { exchangeGoogleCode, googleConfigFields, googleEnvDefaults, + googleWorkspaceEnvMappings, } from "./google-oauth"; export const youtube: AppDefinition = { @@ -48,4 +49,5 @@ export const youtube: AppDefinition = { fields: googleConfigFields, envDefaults: googleEnvDefaults, }, + envMappings: googleWorkspaceEnvMappings, };