revert(secrets): drop fork's usages-tracking + delete guard
Now that upstream's #5429 provides provider-based secrets management with formal bindings (companySecretBindings table populated at config-save time) plus the /secrets/:id/usage endpoint backed by listBindingReferences(), the fork's parallel usages() scan is redundant for agent/routine bindings. The fork's scan did cover one path upstream doesn't track: skill metadata sourceAuthSecretId references. Dropping this means accidental deletion of a skill's auth secret is no longer rejected — accepted as a chase-upstream tradeoff. - server/src/services/secrets.ts: drop usages(), SecretUsage* types, in-use guard in remove(), and companySkills/agentService imports - server/src/routes/secrets.ts: drop GET /secrets/:id/usages route - ui/src/api/secrets.ts: drop usages() client method Typechecks clean on server and ui.
This commit is contained in:
@@ -419,19 +419,6 @@ export function secretRoutes(db: Db) {
|
|||||||
res.json(updated);
|
res.json(updated);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/secrets/:id/usages", async (req, res) => {
|
|
||||||
assertBoard(req);
|
|
||||||
const id = req.params.id as string;
|
|
||||||
const existing = await svc.getById(id);
|
|
||||||
if (!existing) {
|
|
||||||
res.status(404).json({ error: "Secret not found" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
assertCompanyAccess(req, existing.companyId);
|
|
||||||
const used = await svc.usages(existing.companyId, id);
|
|
||||||
res.json({ agents: used.agents, skills: used.skills });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/secrets/:id/usage", async (req, res) => {
|
router.get("/secrets/:id/usage", async (req, res) => {
|
||||||
assertBoard(req);
|
assertBoard(req);
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
companySecretProviderConfigs,
|
companySecretProviderConfigs,
|
||||||
companySecrets,
|
companySecrets,
|
||||||
companySecretVersions,
|
companySecretVersions,
|
||||||
companySkills,
|
|
||||||
environments,
|
environments,
|
||||||
heartbeatRuns,
|
heartbeatRuns,
|
||||||
issues,
|
issues,
|
||||||
@@ -53,7 +52,6 @@ import type {
|
|||||||
SecretProviderWriteContext,
|
SecretProviderWriteContext,
|
||||||
} from "../secrets/types.js";
|
} from "../secrets/types.js";
|
||||||
import { isSecretProviderClientError } from "../secrets/types.js";
|
import { isSecretProviderClientError } from "../secrets/types.js";
|
||||||
import { agentService } from "./agents.js";
|
|
||||||
|
|
||||||
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||||
const SENSITIVE_ENV_KEY_RE =
|
const SENSITIVE_ENV_KEY_RE =
|
||||||
@@ -261,10 +259,6 @@ export function secretService(db: Db) {
|
|||||||
fieldPath?: string;
|
fieldPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SecretUsageAgent = { id: string; name: string; envKeys: string[] };
|
|
||||||
type SecretUsageSkill = { id: string; name: string; slug: string };
|
|
||||||
type SecretUsages = { agents: SecretUsageAgent[]; skills: SecretUsageSkill[] };
|
|
||||||
|
|
||||||
async function getById(id: string) {
|
async function getById(id: string) {
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
@@ -273,48 +267,6 @@ export function secretService(db: Db) {
|
|||||||
.then((rows) => rows[0] ?? null);
|
.then((rows) => rows[0] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function usages(companyId: string, secretId: string): Promise<SecretUsages> {
|
|
||||||
const agents = agentService(db);
|
|
||||||
const allAgents = await agents.list(companyId);
|
|
||||||
const agentRefs: SecretUsageAgent[] = [];
|
|
||||||
for (const agent of allAgents) {
|
|
||||||
const config = asRecord(agent.adapterConfig);
|
|
||||||
if (!config) continue;
|
|
||||||
const env = asRecord(config.env);
|
|
||||||
if (!env) continue;
|
|
||||||
const matchingKeys: string[] = [];
|
|
||||||
for (const [key, rawBinding] of Object.entries(env)) {
|
|
||||||
const binding = asRecord(rawBinding);
|
|
||||||
if (!binding) continue;
|
|
||||||
if (binding.type === "secret_ref" && binding.secretId === secretId) {
|
|
||||||
matchingKeys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matchingKeys.length > 0) {
|
|
||||||
agentRefs.push({ id: agent.id, name: agent.name, envKeys: matchingKeys });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillRows = await db
|
|
||||||
.select({
|
|
||||||
id: companySkills.id,
|
|
||||||
name: companySkills.name,
|
|
||||||
slug: companySkills.slug,
|
|
||||||
})
|
|
||||||
.from(companySkills)
|
|
||||||
.where(and(
|
|
||||||
eq(companySkills.companyId, companyId),
|
|
||||||
sql`${companySkills.metadata} ->> 'sourceAuthSecretId' = ${secretId}`,
|
|
||||||
));
|
|
||||||
const skillRefs: SecretUsageSkill[] = skillRows.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
slug: row.slug,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { agents: agentRefs, skills: skillRefs };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getByName(companyId: string, name: string) {
|
async function getByName(companyId: string, name: string) {
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
@@ -2083,16 +2035,6 @@ export function secretService(db: Db) {
|
|||||||
remove: async (secretId: string) => {
|
remove: async (secretId: string) => {
|
||||||
const secret = await getById(secretId);
|
const secret = await getById(secretId);
|
||||||
if (!secret) return null;
|
if (!secret) return null;
|
||||||
const used = await usages(secret.companyId, secretId);
|
|
||||||
if (used.agents.length > 0 || used.skills.length > 0) {
|
|
||||||
const agentNames = used.agents.map((agent) => agent.name);
|
|
||||||
const skillNames = used.skills.map((skill) => skill.name);
|
|
||||||
const names = [...agentNames, ...skillNames].sort((left, right) => left.localeCompare(right));
|
|
||||||
throw unprocessable(
|
|
||||||
`Cannot delete secret "${secret.name}" while it is still used by ${names.join(", ")}. Detach it from those references first.`,
|
|
||||||
{ secretId, usedByAgents: used.agents, usedBySkills: used.skills },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const versionRow = await getSecretVersion(secret.id, secret.latestVersion);
|
const versionRow = await getSecretVersion(secret.id, secret.latestVersion);
|
||||||
const providerId = secret.provider as SecretProvider;
|
const providerId = secret.provider as SecretProvider;
|
||||||
const provider = getSecretProvider(providerId);
|
const provider = getSecretProvider(providerId);
|
||||||
@@ -2139,8 +2081,6 @@ export function secretService(db: Db) {
|
|||||||
return secret;
|
return secret;
|
||||||
},
|
},
|
||||||
|
|
||||||
usages,
|
|
||||||
|
|
||||||
normalizeAdapterConfigForPersistence: async (
|
normalizeAdapterConfigForPersistence: async (
|
||||||
companyId: string,
|
companyId: string,
|
||||||
adapterConfig: Record<string, unknown>,
|
adapterConfig: Record<string, unknown>,
|
||||||
|
|||||||
@@ -126,11 +126,6 @@ export const secretsApi = {
|
|||||||
archive: (id: string) =>
|
archive: (id: string) =>
|
||||||
api.patch<CompanySecret>(`/secrets/${id}`, { status: "archived" satisfies SecretStatus }),
|
api.patch<CompanySecret>(`/secrets/${id}`, { status: "archived" satisfies SecretStatus }),
|
||||||
remove: (id: string) => api.delete<{ ok: true }>(`/secrets/${id}`),
|
remove: (id: string) => api.delete<{ ok: true }>(`/secrets/${id}`),
|
||||||
usages: (id: string) =>
|
|
||||||
api.get<{
|
|
||||||
agents: { id: string; name: string; envKeys: string[] }[];
|
|
||||||
skills: { id: string; name: string; slug: string }[];
|
|
||||||
}>(`/secrets/${id}/usages`),
|
|
||||||
usage: (id: string) => api.get<SecretUsageResponse>(`/secrets/${id}/usage`),
|
usage: (id: string) => api.get<SecretUsageResponse>(`/secrets/${id}/usage`),
|
||||||
accessEvents: (id: string) => api.get<SecretAccessEvent[]>(`/secrets/${id}/access-events`),
|
accessEvents: (id: string) => api.get<SecretAccessEvent[]>(`/secrets/${id}/access-events`),
|
||||||
remoteImportPreview: (companyId: string, data: RemoteImportPreviewInput) =>
|
remoteImportPreview: (companyId: string, data: RemoteImportPreviewInput) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user