Conversation
📝 WalkthroughWalkthroughAdds a new work-queue feature (UI pages, dialogs, charts) backed by a new TRPC reconcile router and DB queries; removes legacy migration logic from workspace store Restore and a related test; minor main.go import/use removal and chart typing/formatting edits. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser as "Browser (UI)"
participant TRPC as "trpc.reconcile (Server)"
participant DB as "Database"
User->>Browser: Open CreateWorkItemDialog / submit form
Browser->>TRPC: reconcile.create(input: workspaceId, kind, scopeId, priority, notBefore, payload?)
TRPC->>DB: upsert reconcileWorkScope (insert or update scope row)
DB-->>TRPC: scope record (id, createdAt...)
alt payload provided
TRPC->>DB: upsert reconcileWorkPayload (link to scope id)
DB-->>TRPC: payload record
end
TRPC-->>Browser: created scope response
Browser->>Browser: invalidate caches, close dialog, reset form
Browser-->>User: show success / updated list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… for work queue in settings
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
packages/trpc/src/routes/reconcile.ts (1)
140-149: Inconsistent pagination API compared tolistWorkScopes.
listWorkScopesreturns{ items, total }for proper pagination support, butlistWorkPayloadsreturns only the items array without a total count. This inconsistency may cause issues for frontend pagination components expecting a uniform response shape.Suggested fix for consistency
.query(async ({ ctx, input }) => { - const items = await ctx.db - .select() - .from(schema.reconcileWorkPayload) - .where(eq(schema.reconcileWorkPayload.scopeRef, input.scopeId)) - .orderBy(desc(schema.reconcileWorkPayload.createdAt)) - .limit(input.limit) - .offset(input.offset); + const condition = eq(schema.reconcileWorkPayload.scopeRef, input.scopeId); + + const [items, [total]] = await Promise.all([ + ctx.db + .select() + .from(schema.reconcileWorkPayload) + .where(condition) + .orderBy(desc(schema.reconcileWorkPayload.createdAt)) + .limit(input.limit) + .offset(input.offset), + ctx.db + .select({ count: count() }) + .from(schema.reconcileWorkPayload) + .where(condition), + ]); - return items; + return { items, total: total?.count ?? 0 }; }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/trpc/src/routes/reconcile.ts` around lines 140 - 149, The endpoint currently returning only the array from the reconcile payload query (the .query handler that selects from schema.reconcileWorkPayload) must be changed to match listWorkScopes' pagination shape: run a separate count query using the same where clause (eq(schema.reconcileWorkPayload.scopeRef, input.scopeId)) to compute total, keep the existing ordered, limited and offset items query, and return an object { items, total } instead of just items so frontend pagination consumers receive a consistent response.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/routes/ws/settings/work-queue.tsx`:
- Around line 211-255: The TableRow click is not keyboard-accessible; make the
row focusable and activate on Enter/Space by adding tabIndex={0} and
role="button" to the TableRow and implementing an onKeyDown handler that calls
setSelectedScopeId(scope.id) when the user presses Enter or Space (while
preserving the existing onClick). Update the TableRow JSX (the TableRow
component rendering each scope) to include these attributes and the onKeyDown
logic so keyboard users can open the drawer.
- Around line 105-120: The live-refreshing scopesQuery can leave offset pointing
past a shrinking total, causing empty pages; update the component to clamp
offset whenever scopesQuery.data?.total (total) changes: compute maxOffset =
Math.max(0, Math.floor((total - 1) / ITEMS_PER_PAGE) * ITEMS_PER_PAGE) and if
offset > maxOffset setOffset(maxOffset); ensure this logic runs after
scopesQuery resolves (e.g., in a useEffect that watches scopesQuery.data?.total)
and apply the same clamp for the other identical pagination block referenced
(lines ~261-279) so hasNext/hasPrev and displayed ranges remain valid.
- Around line 96-103: The stats cards are not refreshed on the same 10s loop as
the chart and list, so update the trpc.reconcile.stats.useQuery call
(statsQuery) to use the same polling options as chartQuery (e.g., pass {
refetchInterval: 10_000 } or the shared options object) so stats auto-refetches
every 10 seconds; also apply the same options to listWorkScopes if it exists to
keep all summary/data views in sync.
In `@apps/workspace-engine/main.go`:
- Line 68: The commented-out desiredrelease.New(WorkerID, db.GetPool(ctx)) call
unregisters the DesiredReleaseKind worker and leaves "desired-release" items
unprocessed; either re-enable the worker by restoring the
desiredrelease.New(WorkerID, db.GetPool(ctx)) registration so the
DesiredReleaseKind queue is consumed, or (if retiring support intentionally)
remove the "desired-release" producer/UI entry and any code that enqueues
DesiredReleaseKind work so no items are created; update whichever of
desiredrelease.New, DesiredReleaseKind, or the new work-queue
dialog/"desired-release" producers you change so producer and consumer remain
consistent.
In `@packages/trpc/src/routes/reconcile.ts`:
- Around line 132-150: listWorkPayloads currently accepts only scopeId and
returns payloads without verifying the scope belongs to a workspace the caller
can access; update the procedure (listWorkPayloads) to enforce workspace-scoped
authorization by either (A) requiring workspaceId in the input and adding
.where(eq(schema.reconcileWorkPayload.workspaceRef, input.workspaceId)) to the
query, or (B) first loading the scope row (select from the scope table that
relates to schema.reconcileWorkPayload, e.g. schema.scope) using the provided
scopeId and checking its workspaceId against the caller's permitted workspace(s)
from ctx (session/permissions), and only then running the payload query filtered
by that workspace; ensure the check rejects/throws when the caller lacks access
before returning items.
- Around line 17-84: All procedures in reconcileRouter (create, listWorkScopes,
listWorkPayloads, stats, chartData) need per-workspace authorization meta: add
.meta({ authorizationCheck: ({ canUser, input }) =>
canUser.perform(Permission.ReconcileXXX).on({ type: "workspace", id:
input.workspaceId }) }) to each procedure using the appropriate Permission
(e.g., ReconcileCreate for create, ReconcileList for
listWorkScopes/stats/chartData). For listWorkPayloads, because input only
contains scopeId, first resolve the scope's workspaceId (via
schema.reconcileWorkScope lookup in the handler or add a pre-check) and use that
workspaceId in the authorizationCheck so the check enforces access to the
scope's workspace. Ensure you reference the reconcileRouter procedures (create,
listWorkScopes, listWorkPayloads, stats, chartData) and the Permission enum when
implementing.
---
Nitpick comments:
In `@packages/trpc/src/routes/reconcile.ts`:
- Around line 140-149: The endpoint currently returning only the array from the
reconcile payload query (the .query handler that selects from
schema.reconcileWorkPayload) must be changed to match listWorkScopes' pagination
shape: run a separate count query using the same where clause
(eq(schema.reconcileWorkPayload.scopeRef, input.scopeId)) to compute total, keep
the existing ordered, limited and offset items query, and return an object {
items, total } instead of just items so frontend pagination consumers receive a
consistent response.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ff44f1c4-95bf-4622-bcec-07c48127a89c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
apps/web/app/components/ui/chart.tsxapps/web/app/routes.tsapps/web/app/routes/ws/settings/_components/work-queue/CreateWorkItemDialog.tsxapps/web/app/routes/ws/settings/_components/work-queue/WorkPayloadDrawer.tsxapps/web/app/routes/ws/settings/_components/work-queue/WorkQueueCharts.tsxapps/web/app/routes/ws/settings/_layout.tsxapps/web/app/routes/ws/settings/work-queue.tsxapps/workspace-engine/main.goapps/workspace-engine/pkg/workspace/store/restore_test.gopackages/trpc/src/root.tspackages/trpc/src/routes/reconcile.ts
💤 Files with no reviewable changes (1)
- apps/workspace-engine/pkg/workspace/store/restore_test.go
✅ Files skipped from review due to trivial changes (1)
- apps/web/app/components/ui/chart.tsx
| const statsQuery = trpc.reconcile.stats.useQuery({ | ||
| workspaceId: workspace.id, | ||
| }); | ||
|
|
||
| const chartQuery = trpc.reconcile.chartData.useQuery( | ||
| { workspaceId: workspace.id }, | ||
| { refetchInterval: 10_000 }, | ||
| ); |
There was a problem hiding this comment.
Keep the stats cards on the same refresh loop.
chartData and listWorkScopes refresh every 10 seconds, but stats only loads once. After claim/expiry/retry transitions, the summary cards drift out of sync with the rest of the page until a manual reload.
♻️ Suggested fix
- const statsQuery = trpc.reconcile.stats.useQuery({
- workspaceId: workspace.id,
- });
+ const statsQuery = trpc.reconcile.stats.useQuery(
+ { workspaceId: workspace.id },
+ { refetchInterval: 10_000 },
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const statsQuery = trpc.reconcile.stats.useQuery({ | |
| workspaceId: workspace.id, | |
| }); | |
| const chartQuery = trpc.reconcile.chartData.useQuery( | |
| { workspaceId: workspace.id }, | |
| { refetchInterval: 10_000 }, | |
| ); | |
| const statsQuery = trpc.reconcile.stats.useQuery( | |
| { workspaceId: workspace.id }, | |
| { refetchInterval: 10_000 }, | |
| ); | |
| const chartQuery = trpc.reconcile.chartData.useQuery( | |
| { workspaceId: workspace.id }, | |
| { refetchInterval: 10_000 }, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/routes/ws/settings/work-queue.tsx` around lines 96 - 103, The
stats cards are not refreshed on the same 10s loop as the chart and list, so
update the trpc.reconcile.stats.useQuery call (statsQuery) to use the same
polling options as chartQuery (e.g., pass { refetchInterval: 10_000 } or the
shared options object) so stats auto-refetches every 10 seconds; also apply the
same options to listWorkScopes if it exists to keep all summary/data views in
sync.
| const scopesQuery = trpc.reconcile.listWorkScopes.useQuery( | ||
| { | ||
| workspaceId: workspace.id, | ||
| limit: ITEMS_PER_PAGE, | ||
| offset, | ||
| kind: kindFilter === "all" ? undefined : kindFilter, | ||
| claimed: claimedFilter, | ||
| }, | ||
| { refetchInterval: 10_000 }, | ||
| ); | ||
|
|
||
| const kinds = statsQuery.data?.byKind ?? []; | ||
| const scopes = scopesQuery.data?.items ?? []; | ||
| const total = scopesQuery.data?.total ?? 0; | ||
| const hasNext = offset + ITEMS_PER_PAGE < total; | ||
| const hasPrev = offset > 0; |
There was a problem hiding this comment.
Clamp pagination against a shrinking live total.
listWorkScopes auto-refreshes every 10 seconds, but offset is never adjusted when total drops. On the last page, that can strand the user on an empty table and render invalid ranges like 51–50 of 50 until they page back manually.
♻️ Suggested fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const scopes = scopesQuery.data?.items ?? [];
const total = scopesQuery.data?.total ?? 0;
const hasNext = offset + ITEMS_PER_PAGE < total;
const hasPrev = offset > 0;
+
+ useEffect(() => {
+ if (total === 0) {
+ setOffset(0);
+ return;
+ }
+
+ if (offset >= total) {
+ setOffset(Math.floor((total - 1) / ITEMS_PER_PAGE) * ITEMS_PER_PAGE);
+ }
+ }, [offset, total]);
@@
- Showing {offset + 1}–{Math.min(offset + ITEMS_PER_PAGE, total)} of{" "}
- {total}
+ {total === 0
+ ? "Showing 0 of 0"
+ : `Showing ${offset + 1}–${Math.min(offset + ITEMS_PER_PAGE, total)} of ${total}`}Also applies to: 261-279
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/routes/ws/settings/work-queue.tsx` around lines 105 - 120, The
live-refreshing scopesQuery can leave offset pointing past a shrinking total,
causing empty pages; update the component to clamp offset whenever
scopesQuery.data?.total (total) changes: compute maxOffset = Math.max(0,
Math.floor((total - 1) / ITEMS_PER_PAGE) * ITEMS_PER_PAGE) and if offset >
maxOffset setOffset(maxOffset); ensure this logic runs after scopesQuery
resolves (e.g., in a useEffect that watches scopesQuery.data?.total) and apply
the same clamp for the other identical pagination block referenced (lines
~261-279) so hasNext/hasPrev and displayed ranges remain valid.
| {scopes.map((scope) => ( | ||
| <TableRow | ||
| key={scope.id} | ||
| className="cursor-pointer" | ||
| onClick={() => setSelectedScopeId(scope.id)} | ||
| > | ||
| <TableCell> | ||
| <Badge | ||
| variant="outline" | ||
| className="max-w-[180px] truncate" | ||
| title={scope.kind} | ||
| > | ||
| {scope.kind} | ||
| </Badge> | ||
| </TableCell> | ||
| <TableCell className="font-mono text-xs"> | ||
| {scope.scopeType || "-"} | ||
| </TableCell> | ||
| <TableCell | ||
| className="max-w-[200px] truncate font-mono text-xs" | ||
| title={scope.scopeId} | ||
| > | ||
| {scope.scopeId || "-"} | ||
| </TableCell> | ||
| <TableCell>{scope.priority}</TableCell> | ||
| <TableCell> | ||
| <ClaimStatusBadge | ||
| claimedBy={scope.claimedBy} | ||
| claimedUntil={scope.claimedUntil} | ||
| /> | ||
| </TableCell> | ||
| <TableCell className="max-w-[150px] truncate text-xs"> | ||
| {scope.claimedBy ?? "-"} | ||
| </TableCell> | ||
| <TableCell className="text-xs text-muted-foreground"> | ||
| {formatDistanceToNowStrict(new Date(scope.notBefore), { | ||
| addSuffix: true, | ||
| })} | ||
| </TableCell> | ||
| <TableCell className="text-xs text-muted-foreground"> | ||
| {formatDistanceToNowStrict(new Date(scope.eventTs), { | ||
| addSuffix: true, | ||
| })} | ||
| </TableCell> | ||
| </TableRow> |
There was a problem hiding this comment.
Make payload inspection keyboard-accessible.
The drawer is only reachable through an onClick on TableRow, which leaves the action untabbable and unusable from the keyboard. That blocks keyboard users from inspecting payloads.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/routes/ws/settings/work-queue.tsx` around lines 211 - 255, The
TableRow click is not keyboard-accessible; make the row focusable and activate
on Enter/Space by adding tabIndex={0} and role="button" to the TableRow and
implementing an onKeyDown handler that calls setSelectedScopeId(scope.id) when
the user presses Enter or Space (while preserving the existing onClick). Update
the TableRow JSX (the TableRow component rendering each scope) to include these
attributes and the onKeyDown logic so keyboard users can open the drawer.
| jobverificationmetric.New(WorkerID, db.GetPool(ctx)), | ||
| relationshipeval.New(WorkerID, db.GetPool(ctx)), | ||
| desiredrelease.New(WorkerID, db.GetPool(ctx)), | ||
| // desiredrelease.New(WorkerID, db.GetPool(ctx)), |
There was a problem hiding this comment.
Commenting out desiredrelease.New strands desired-release items.
From the provided context, this is the worker that processes DesiredReleaseKind, and the new work-queue dialog still exposes "desired-release" creation. With this registration removed, those scopes will sit pending unless another consumer exists. If retirement is intentional, remove the kind from the UI/producers in the same PR.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/workspace-engine/main.go` at line 68, The commented-out
desiredrelease.New(WorkerID, db.GetPool(ctx)) call unregisters the
DesiredReleaseKind worker and leaves "desired-release" items unprocessed; either
re-enable the worker by restoring the desiredrelease.New(WorkerID,
db.GetPool(ctx)) registration so the DesiredReleaseKind queue is consumed, or
(if retiring support intentionally) remove the "desired-release" producer/UI
entry and any code that enqueues DesiredReleaseKind work so no items are
created; update whichever of desiredrelease.New, DesiredReleaseKind, or the new
work-queue dialog/"desired-release" producers you change so producer and
consumer remain consistent.
| export const reconcileRouter = router({ | ||
| create: protectedProcedure | ||
| .input( | ||
| z.object({ | ||
| workspaceId: z.string().uuid(), | ||
| kind: z.string().min(1), | ||
| scopeType: z.string().default(""), | ||
| scopeId: z.string().default(""), | ||
| priority: z.number().int().min(0).max(32767).default(100), | ||
| notBefore: z.coerce.date().optional(), | ||
| payload: z | ||
| .object({ | ||
| payloadType: z.string().default(""), | ||
| payloadKey: z.string().default(""), | ||
| payload: z.record(z.string(), z.any()).default({}), | ||
| }) | ||
| .optional(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ ctx, input }) => { | ||
| const [scope] = await ctx.db | ||
| .insert(schema.reconcileWorkScope) | ||
| .values({ | ||
| workspaceId: input.workspaceId, | ||
| kind: input.kind, | ||
| scopeType: input.scopeType, | ||
| scopeId: input.scopeId, | ||
| priority: input.priority, | ||
| notBefore: input.notBefore ?? new Date(), | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: [ | ||
| schema.reconcileWorkScope.workspaceId, | ||
| schema.reconcileWorkScope.kind, | ||
| schema.reconcileWorkScope.scopeType, | ||
| schema.reconcileWorkScope.scopeId, | ||
| ], | ||
| set: { | ||
| eventTs: new Date(), | ||
| priority: input.priority, | ||
| notBefore: input.notBefore ?? new Date(), | ||
| }, | ||
| }) | ||
| .returning(); | ||
|
|
||
| if (input.payload && scope) { | ||
| await ctx.db | ||
| .insert(schema.reconcileWorkPayload) | ||
| .values({ | ||
| scopeRef: scope.id, | ||
| payloadType: input.payload.payloadType, | ||
| payloadKey: input.payload.payloadKey, | ||
| payload: input.payload.payload, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: [ | ||
| schema.reconcileWorkPayload.scopeRef, | ||
| schema.reconcileWorkPayload.payloadType, | ||
| schema.reconcileWorkPayload.payloadKey, | ||
| ], | ||
| set: { | ||
| payload: input.payload.payload, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return scope; | ||
| }), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check how other TRPC routers handle workspace authorization
echo "=== Authorization patterns in other routers ==="
rg -n "authorizationCheck" packages/trpc/src/routes/ -A 3 -B 1Repository: ctrlplanedev/ctrlplane
Length of output: 6683
🏁 Script executed:
#!/bin/bash
# Check the full reconcile.ts file to see all procedures
echo "=== Full reconcile.ts file ==="
wc -l packages/trpc/src/routes/reconcile.tsRepository: ctrlplanedev/ctrlplane
Length of output: 138
🏁 Script executed:
#!/bin/bash
# Read the reconcile.ts file to see all procedures
cat -n packages/trpc/src/routes/reconcile.tsRepository: ctrlplanedev/ctrlplane
Length of output: 11543
🏁 Script executed:
#!/bin/bash
# Check how protectedProcedure is defined in trpc.ts
echo "=== protectedProcedure definition ==="
rg -n "protectedProcedure" packages/trpc/src/trpc.ts -B 2 -A 2Repository: ctrlplanedev/ctrlplane
Length of output: 166
Add authorization checks to all procedures.
All five procedures (create, listWorkScopes, listWorkPayloads, stats, chartData) use protectedProcedure but lack authorizationCheck meta. This allows any authenticated user to access or modify reconcile data for any workspace.
Follow the pattern used throughout the codebase (e.g., workspace.ts, systems.ts, deployments.ts):
.meta({
authorizationCheck: ({ canUser, input }) =>
canUser
.perform(Permission.ReconcileList) // or appropriate permission
.on({ type: "workspace", id: input.workspaceId }),
})listWorkPayloads is particularly vulnerable—it only accepts scopeId with no workspace validation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/trpc/src/routes/reconcile.ts` around lines 17 - 84, All procedures
in reconcileRouter (create, listWorkScopes, listWorkPayloads, stats, chartData)
need per-workspace authorization meta: add .meta({ authorizationCheck: ({
canUser, input }) => canUser.perform(Permission.ReconcileXXX).on({ type:
"workspace", id: input.workspaceId }) }) to each procedure using the appropriate
Permission (e.g., ReconcileCreate for create, ReconcileList for
listWorkScopes/stats/chartData). For listWorkPayloads, because input only
contains scopeId, first resolve the scope's workspaceId (via
schema.reconcileWorkScope lookup in the handler or add a pre-check) and use that
workspaceId in the authorizationCheck so the check enforces access to the
scope's workspace. Ensure you reference the reconcileRouter procedures (create,
listWorkScopes, listWorkPayloads, stats, chartData) and the Permission enum when
implementing.
| listWorkPayloads: protectedProcedure | ||
| .input( | ||
| z.object({ | ||
| scopeId: z.number(), | ||
| limit: z.number().min(1).max(500).default(100), | ||
| offset: z.number().min(0).default(0), | ||
| }), | ||
| ) | ||
| .query(async ({ ctx, input }) => { | ||
| const items = await ctx.db | ||
| .select() | ||
| .from(schema.reconcileWorkPayload) | ||
| .where(eq(schema.reconcileWorkPayload.scopeRef, input.scopeId)) | ||
| .orderBy(desc(schema.reconcileWorkPayload.createdAt)) | ||
| .limit(input.limit) | ||
| .offset(input.offset); | ||
|
|
||
| return items; | ||
| }), |
There was a problem hiding this comment.
listWorkPayloads lacks workspace-scoped authorization.
This procedure accepts only a scopeId (the numeric primary key) without verifying that the scope belongs to a workspace the caller has access to. An authenticated user could enumerate scope IDs to access payloads from any workspace.
Either:
- Add a join/subquery to verify workspace ownership, or
- Require
workspaceIdin input and filter accordingly, or - Verify the scope's workspace against user permissions before querying.
Suggested approach: verify workspace access
listWorkPayloads: protectedProcedure
.input(
z.object({
scopeId: z.number(),
+ workspaceId: z.string().uuid(),
limit: z.number().min(1).max(500).default(100),
offset: z.number().min(0).default(0),
}),
)
+ .meta({
+ authorizationCheck: async ({ canUser, input }) =>
+ canUser.access("workspace", input.workspaceId),
+ })
.query(async ({ ctx, input }) => {
const items = await ctx.db
.select()
.from(schema.reconcileWorkPayload)
- .where(eq(schema.reconcileWorkPayload.scopeRef, input.scopeId))
+ .innerJoin(
+ schema.reconcileWorkScope,
+ eq(schema.reconcileWorkPayload.scopeRef, schema.reconcileWorkScope.id),
+ )
+ .where(
+ and(
+ eq(schema.reconcileWorkPayload.scopeRef, input.scopeId),
+ eq(schema.reconcileWorkScope.workspaceId, input.workspaceId),
+ ),
+ )
.orderBy(desc(schema.reconcileWorkPayload.createdAt))
.limit(input.limit)
.offset(input.offset);
return items;
}),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/trpc/src/routes/reconcile.ts` around lines 132 - 150,
listWorkPayloads currently accepts only scopeId and returns payloads without
verifying the scope belongs to a workspace the caller can access; update the
procedure (listWorkPayloads) to enforce workspace-scoped authorization by either
(A) requiring workspaceId in the input and adding
.where(eq(schema.reconcileWorkPayload.workspaceRef, input.workspaceId)) to the
query, or (B) first loading the scope row (select from the scope table that
relates to schema.reconcileWorkPayload, e.g. schema.scope) using the provided
scopeId and checking its workspaceId against the caller's permitted workspace(s)
from ctx (session/permissions), and only then running the payload query filtered
by that workspace; ensure the check rejects/throws when the caller lacks access
before returning items.
Summary by CodeRabbit