feat(comments): phase-2 sweep onto core primitives - #145
Conversation
Migrate the comments plugin to the v3 core primitives, mirroring the form-builder sweep: - Declare commentsResources and generate the query-key factory via the server-safe createResourceQueryKeys; key shapes are unchanged (shared discriminators from api/query-key-defs.ts) - Route all mutation HTTP calls through runResourceMutation (new optional headers param in core, matching createResourceQueryKeys); the optimistic onMutate/onSuccess/onError cache logic for post/like stays hand-written because the public hooks take an explicit client config — required by the embeddable CommentThread - Replace sonner with useNotify and route all UI strings through useTranslate with override-wins localization precedence - Add a permission prop to the moderation route and CanAccess around approve/spam/delete controls (moderation page, resource pending queue) - Move moderation tab/page and my-comments page state to useListState (URL-synced, back-button friendly, clamped against mangled URLs) - Surface StackError body field errors inline in CommentForm - Delete plugin-local error-utils.ts re-export; update build-registry - Tests: query-key parity guard, client-sweep jsdom suite; registry regenerated Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Security Review — feat/comments-phase2-sweep
Result: No high-confidence, newly-introduced vulnerabilities found.
The diff was evaluated against the threat checklist (injection, authn/authz bypasses, secrets, deserialization, XSS, SSRF, supply-chain). Findings are separated into improvements, one confirmed pre-existing concern, and one design observation.
✅ Security Improvements in This PR
1. URL-parameter input sanitization (moderation and my-comments pages)
The migration from useState to useListState reads tab and page from the URL. Both are sanitized before use:
tabis whitelisted to known values ("approved"|"spam"), falling back to"pending"for any other value — unknown strings cannot be used as query parameters or rendered as-is.pageis passed throughMath.max(1, Math.floor(listState.page) || 1), clamping all invalid inputs (non-numeric,NaN,0, negative) to1.
These values flow only into server query params (not into HTML or SQL), so there is no injection risk.
2. CanAccess authorization wrappers on destructive actions
Approve, spam, and delete buttons across ModerationPage, ResourceCommentsPage, and UserCommentsPage are now wrapped in CanAccess guards. This adds a client-side permission check that was previously absent, providing defense-in-depth even though server-side enforcement should remain the primary control.
⚠️ Pre-existing Concern (not introduced by this PR)
Client-supplied authorId in the like mutation
The like mutation sends authorId in the request body (unchanged from the previous implementation):
// packages/stack/src/plugins/comments/query-keys.ts
like: {
path: "@post/comments/:id/like",
method: "POST" as const,
input: (vars: { commentId: string; authorId: string }) => ({
params: { id: vars.commentId },
body: { authorId: vars.authorId }, // ← client-controlled
}),This is inconsistent with the codebase's stated principle for read queries:
"
currentUserIdis intentionally NOT sent to the server in any query. The server resolves the caller's identity server-side via theresolveCurrentUserIdhook. Sending it would allow any caller to impersonate another user."
If the backend handler for POST /comments/:id/like accepts body.authorId at face value without verifying it matches the authenticated session, any authenticated user can record likes as any other user. The PR does not fix or worsen this — it preserves the pre-existing behavior via runResourceMutation. Recommendation: the backend like handler should resolve the author identity from the session, not from the request body, consistent with the read-query pattern already in place.
ℹ️ Design Observation
CanAccess is UI-only gating
CanAccess hides action buttons on the client but does not enforce permissions on the server. The API routes for moderate and delete actions must independently enforce the same permission check. If they do (expected given the backend plugin pattern), the UI guard is correct defense-in-depth. If they do not, removing the CanAccess wrapper or calling the API directly bypasses all access control. This is an architecture observation about layering, not a defect introduced by this PR.
Checklist Summary
| Category | Finding | Severity | Status |
|---|---|---|---|
| Injection (SQL/path/template) | None found | — | ✅ Clean |
| URL parameter sanitization | Tab whitelist + page clamp | — | ✅ Improved |
| Authn/authz bypass | CanAccess UI-only (server must also enforce) |
Low | ℹ️ Observation |
| Client-supplied identity in like mutation | authorId from request body |
Medium | |
| Secret / token leakage | None found | — | ✅ Clean |
| XSS | All comment content rendered via React text nodes | — | ✅ Clean |
| Dependency / supply-chain | No new third-party deps added | — | ✅ Clean |
Sent by Cursor Automation: Find vulnerabilities
|
✅ Shadcn registry validated — no registry changes detected. |


Summary
Migrates the comments plugin to the v3 core primitives (
#136plugin sweeps), mirroring the blog/CMS/form-builder sweeps:commentsResourcesdeclaration +createCommentsQueryKeysvia the server-safecreateResourceQueryKeys. Key shapes are byte-identical to the previous lukemorales factory (shared discriminators fromapi/query-key-defs.ts), so SSR loader prefetch/hydration is unchanged.runResourceMutation, which gains an optionalheadersparam in core (same rationale as the existingheadersparam oncreateResourceQueryKeys). The optimisticonMutate/onSuccess/onErrorcache logic forusePostComment/useToggleLikestays hand-written — the public comments hooks take an explicit client config becauseCommentThreadmust stay embeddable outsideStackProvider, so they can't use the overrides-boundcreateResourcemutation hooks. This is the documented deviation for this plugin.t("comments.…", "Default")with override-wins precedence (localizationprop/override → i18n provider → English default).CommentThreadnow also honorsoverrides.localization(prop still wins).permissionprop on the moderation route (comments:comment/moderate) plus per-control gates on approve/spam (moderate) and delete (delete) across the moderation table, view dialog, bulk toolbar, and the resource-page pending queue. Own-content actions (my-comments delete, thread edit/delete) stay identity-scoped as before.tab+pageand my-commentspageare URL-synced (?tab=spam&page=3), back-button friendly, and clamped against mangled URL values.CommentFormsurfaces theStackError.errors.bodymessage inline instead of the generic submit error.error-utils.tsand thetoErrorre-export deleted;build-registry.tsupdated;btst-comments.jsonregenerated (no moreerror-utils/sonnerreferences).Test plan
pnpm build,pnpm typecheck,pnpm lint,pnpm knipgreenpnpm test— 441 tests / 38 files, including:comments-query-keys.test.tsparity guard (factory keys vsCOMMENTS_QUERY_KEYS/discriminators,_defprefixes,parentIdnull-vs-undefined segregation)client-sweep.test.tsxjsdom suite (15 tests: CanAccess allow/deny paths, notify success routing, useListState URL seed/write/clamp, inline StackError field errors, i18n precedence)Made with Cursor