feat: encrypted account-level secrets (#501)

* feat: add encrypted account-level secrets with UI for adding API keys

Adds AccountSecret model with AES-256-GCM encryption, API routes for
CRUD, "Add secret" button in model costs section, and injects decrypted
secrets into action env (YAML secrets take precedence).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: repo secrets, sidebar icons, lazy learnings history

- add repo-level secrets with inheritance from org secrets
- add icons to console sidebar sections
- fix learnings history modal: lazy fetch with hover prefetch,
  strip content from list response, load content per-expansion

Made-with: Cursor

* add input validation bounds for secrets and fix client-side name filter

Made-with: Cursor

* refactor: migrate all client-side data fetching to TanStack Query

Replace manual useState/useEffect/fetch patterns and the custom
usePolling hook with useQuery, useInfiniteQuery, and useMutation
across the entire frontend for consistent caching, background
refetching, and reactive invalidation.

- ActiveWorkflowRunsSection: useQuery + refetchInterval
- WorkflowRunHistory: useInfiniteQuery + polling query
- LearningsSection: useQuery per revision (lazy)
- FlagsSettings: self-contained useQuery + useMutation
- SecretsCard: useMutation for delete
- AddWorkflowButton, VerifyWorkflowButton: useMutation
- EmailSignupForm, email-waitlist: useMutation
- providers.tsx: enable refetchOnWindowFocus
- Delete usePolling.ts (no remaining consumers)

Made-with: Cursor

* address PR review: squash migrations, rename accountSecrets → dbSecrets

Squash the two separate secrets migrations into a single migration.
Rename the wire format field from accountSecrets to dbSecrets since
it now carries merged account + repo secrets.

Made-with: Cursor

* fix: update proxyKeys.ts imports after cache.ts -> yes package migration

Made-with: Cursor

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Colin McDonnell
2026-03-31 05:02:38 +00:00
committed by pullfrog[bot]
parent 0055aef618
commit ba1966f17c
6 changed files with 34 additions and 5 deletions
+15 -3
View File
@@ -107721,7 +107721,7 @@ var providers = {
}), }),
google: provider({ google: provider({
displayName: "Google", displayName: "Google",
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
models: { models: {
"gemini-pro": { "gemini-pro": {
displayName: "Gemini Pro", displayName: "Gemini Pro",
@@ -151108,7 +151108,8 @@ async function fetchRunContext(params) {
}, },
apiToken: data.apiToken, apiToken: data.apiToken,
oss: data.oss ?? false, oss: data.oss ?? false,
proxyModel: data.proxyModel proxyModel: data.proxyModel,
dbSecrets: data.dbSecrets
}; };
} catch { } catch {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@@ -151133,7 +151134,8 @@ async function resolveRunContextData(params) {
repoSettings: runContext.settings, repoSettings: runContext.settings,
apiToken: runContext.apiToken, apiToken: runContext.apiToken,
oss: runContext.oss, oss: runContext.oss,
proxyModel: runContext.proxyModel proxyModel: runContext.proxyModel,
dbSecrets: runContext.dbSecrets
}; };
} }
@@ -151457,6 +151459,16 @@ async function main() {
const initialOctokit = createOctokit(jobToken); const initialOctokit = createOctokit(jobToken);
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken }); const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
timer.checkpoint("runContextData"); timer.checkpoint("runContextData");
if (runContext.dbSecrets) {
for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
if (!process.env[key]) {
process.env[key] = value2;
core5.setSecret(value2);
}
}
const count = Object.keys(runContext.dbSecrets).length;
if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
}
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
toolState.model = payload.model; toolState.model = payload.model;
if (payload.event.trigger === "pull_request_synchronize") { if (payload.event.trigger === "pull_request_synchronize") {
+12
View File
@@ -169,6 +169,18 @@ export async function main(): Promise<MainResult> {
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken }); const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
timer.checkpoint("runContextData"); timer.checkpoint("runContextData");
// inject account-level secrets into process.env (YAML secrets take precedence)
if (runContext.dbSecrets) {
for (const [key, value] of Object.entries(runContext.dbSecrets)) {
if (!process.env[key]) {
process.env[key] = value;
core.setSecret(value);
}
}
const count = Object.keys(runContext.dbSecrets).length;
if (count > 0) log.info(`» ${count} db secret(s) loaded`);
}
// resolve payload to determine shell permission // resolve payload to determine shell permission
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings); const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
toolState.model = payload.model; toolState.model = payload.model;
+1 -1
View File
@@ -93,7 +93,7 @@ export const providers = {
}), }),
google: provider({ google: provider({
displayName: "Google", displayName: "Google",
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
models: { models: {
"gemini-pro": { "gemini-pro": {
displayName: "Gemini Pro", displayName: "Gemini Pro",
+1 -1
View File
@@ -37559,7 +37559,7 @@ var providers = {
}), }),
google: provider({ google: provider({
displayName: "Google", displayName: "Google",
envVars: ["GOOGLE_GENERATIVE_AI_API_KEY", "GEMINI_API_KEY"], envVars: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
models: { models: {
"gemini-pro": { "gemini-pro": {
displayName: "Gemini Pro", displayName: "Gemini Pro",
+3
View File
@@ -27,6 +27,7 @@ export interface RunContext {
apiToken: string; apiToken: string;
oss: boolean; oss: boolean;
proxyModel?: string | undefined; proxyModel?: string | undefined;
dbSecrets?: Record<string, string> | undefined;
} }
const defaultSettings: RepoSettings = { const defaultSettings: RepoSettings = {
@@ -82,6 +83,7 @@ export async function fetchRunContext(params: {
apiToken: string; apiToken: string;
oss?: boolean; oss?: boolean;
proxyModel?: string; proxyModel?: string;
dbSecrets?: Record<string, string>;
} | null; } | null;
if (data === null) { if (data === null) {
@@ -100,6 +102,7 @@ export async function fetchRunContext(params: {
apiToken: data.apiToken, apiToken: data.apiToken,
oss: data.oss ?? false, oss: data.oss ?? false,
proxyModel: data.proxyModel, proxyModel: data.proxyModel,
dbSecrets: data.dbSecrets,
}; };
} catch { } catch {
clearTimeout(timeoutId); clearTimeout(timeoutId);
+2
View File
@@ -14,6 +14,7 @@ export interface RunContextData {
apiToken: string; apiToken: string;
oss: boolean; oss: boolean;
proxyModel?: string | undefined; proxyModel?: string | undefined;
dbSecrets?: Record<string, string> | undefined;
} }
interface ResolveRunContextDataParams { interface ResolveRunContextDataParams {
@@ -46,5 +47,6 @@ export async function resolveRunContextData(
apiToken: runContext.apiToken, apiToken: runContext.apiToken,
oss: runContext.oss, oss: runContext.oss,
proxyModel: runContext.proxyModel, proxyModel: runContext.proxyModel,
dbSecrets: runContext.dbSecrets,
}; };
} }