The outer catch at releases.ts:79 silently swallowed all errors when loading widget secrets, including the case where the widget_secret table didn't exist (migration not applied). This caused tokens to silently become undefined, leading to unauthenticated API calls and rate limit errors. Now logs a warning with the itemId and error so the issue is diagnosable in server logs instead of failing silently.
122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
import SuperJSON from "superjson";
|
|
import { escapeForRegEx } from "@tiptap/react";
|
|
import { z } from "zod/v4";
|
|
|
|
import { decryptSecret } from "@homarr/common/server";
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import { eq } from "@homarr/db";
|
|
import { boards, items, widgetSecrets } from "@homarr/db/schema";
|
|
import { releaseProviderKinds } from "@homarr/definitions";
|
|
import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
|
|
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
|
import { throwIfActionForbiddenAsync } from "../board/board-access";
|
|
|
|
const logger = createLogger({ module: "releasesRouter" });
|
|
|
|
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
|
|
if (!versionFilter) return undefined;
|
|
|
|
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
|
|
const precision = "[0-9]+\\.".repeat(versionFilter.precision).slice(0, -2);
|
|
const escapedSuffix = versionFilter.suffix ? escapeForRegEx(versionFilter.suffix) : "";
|
|
|
|
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
|
|
};
|
|
|
|
const releaseVersionFilterSchema = z.object({
|
|
prefix: z.string().optional(),
|
|
precision: z.number(),
|
|
suffix: z.string().optional(),
|
|
});
|
|
|
|
export const releasesRouter = createTRPCRouter({
|
|
getLatest: publicProcedure
|
|
.input(
|
|
z.object({
|
|
itemId: z.string().optional(),
|
|
repositories: z.array(
|
|
z.object({
|
|
id: z.string().optional(),
|
|
provider: z.enum(releaseProviderKinds),
|
|
identifier: z.string(),
|
|
versionFilter: releaseVersionFilterSchema.optional(),
|
|
providerUrl: z.string().url().optional(),
|
|
}),
|
|
),
|
|
}),
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const tokensByProvider = new Map<string, string>();
|
|
const savedRepos = new Map<string, { providerUrl?: string }>();
|
|
|
|
if (input.itemId) {
|
|
try {
|
|
const item = await ctx.db.query.items.findFirst({ where: eq(items.id, input.itemId) });
|
|
if (item && item.kind === "releases") {
|
|
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "view");
|
|
|
|
const options = SuperJSON.parse<Record<string, unknown>>(item.options);
|
|
const repos = options.repositories as
|
|
| Array<{ provider?: string; identifier: string; providerUrl?: string }>
|
|
| undefined;
|
|
if (repos) {
|
|
for (const repo of repos) {
|
|
if (repo.provider && repo.identifier) {
|
|
savedRepos.set(`${repo.provider}:${repo.identifier}`, { providerUrl: repo.providerUrl });
|
|
}
|
|
}
|
|
}
|
|
|
|
const secrets = await ctx.db.query.widgetSecrets.findMany({
|
|
where: eq(widgetSecrets.itemId, input.itemId),
|
|
});
|
|
for (const secret of secrets) {
|
|
try {
|
|
tokensByProvider.set(secret.kind, decryptSecret(secret.value));
|
|
} catch (error) {
|
|
logger.warn("Failed to decrypt widget secret", { itemId: input.itemId, kind: secret.kind, error });
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.warn("Failed to load release widget tokens", { itemId: input.itemId, error });
|
|
}
|
|
}
|
|
|
|
return await Promise.all(
|
|
input.repositories.map(async (repository) => {
|
|
const repositoryId = repository.id ?? repository.identifier;
|
|
const savedRepo = savedRepos.get(`${repository.provider}:${repository.identifier}`);
|
|
try {
|
|
const response = await releasesRequestHandler
|
|
.handler({
|
|
id: repositoryId,
|
|
provider: repository.provider,
|
|
identifier: repository.identifier,
|
|
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
|
providerUrl: savedRepo?.providerUrl ?? repository.providerUrl,
|
|
token: savedRepo ? tokensByProvider.get(repository.provider) : undefined,
|
|
})
|
|
.getDataAsync();
|
|
|
|
return {
|
|
id: repositoryId,
|
|
provider: repository.provider,
|
|
timestamp: response.timestamp,
|
|
...response.data,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
id: repositoryId,
|
|
provider: repository.provider,
|
|
timestamp: new Date(),
|
|
success: false as const,
|
|
error: { code: "unexpected" as const, message: String(error) },
|
|
};
|
|
}
|
|
}),
|
|
);
|
|
}),
|
|
});
|