From 30107e4e76a454ff7402de9a8bc2e0dd6f422aa8 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 26 Jun 2026 20:36:48 +0200 Subject: [PATCH] fix: gracefully degrade when widget_secret table is missing or access denied The releases widget was crashing because throwIfActionForbiddenAsync or the widgetSecrets query would throw before DB migrations are run. Wrap in try/catch so the widget works without tokens in that case. --- packages/api/src/router/widgets/releases.ts | 36 ++++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index 4bb5cb1b8..ac56e3ec5 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -48,26 +48,30 @@ export const releasesRouter = createTRPCRouter({ let allowedRepoIds: Set | null = null; if (input.itemId) { - 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"); + 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>(item.options); - const repos = options.repositories as Array<{ id?: string }> | undefined; - if (repos) { - allowedRepoIds = new Set(repos.map((r) => r.id).filter((id): id is string => Boolean(id))); - } + const options = SuperJSON.parse>(item.options); + const repos = options.repositories as Array<{ id?: string }> | undefined; + if (repos) { + allowedRepoIds = new Set(repos.map((r) => r.id).filter((id): id is string => Boolean(id))); + } - 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 { - // Skip corrupt secrets + 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 { + // Skip corrupt secrets + } } } + } catch { + // Access denied or table not yet migrated -- proceed without tokens } }