Skip to content

feat(comments): phase-2 sweep onto core primitives - #145

Merged
olliethedev merged 1 commit into
v3from
feat/comments-phase2-sweep
Aug 19, 2026
Merged

feat(comments): phase-2 sweep onto core primitives#145
olliethedev merged 1 commit into
v3from
feat/comments-phase2-sweep

Conversation

@olliethedev

Copy link
Copy Markdown
Collaborator

Summary

Migrates the comments plugin to the v3 core primitives (#136 plugin sweeps), mirroring the blog/CMS/form-builder sweeps:

  • Resource factory: commentsResources declaration + createCommentsQueryKeys via the server-safe createResourceQueryKeys. Key shapes are byte-identical to the previous lukemorales factory (shared discriminators from api/query-key-defs.ts), so SSR loader prefetch/hydration is unchanged.
  • Mutations: all HTTP calls go through runResourceMutation, which gains an optional headers param in core (same rationale as the existing headers param on createResourceQueryKeys). The optimistic onMutate/onSuccess/onError cache logic for usePostComment/useToggleLike stays hand-written — the public comments hooks take an explicit client config because CommentThread must stay embeddable outside StackProvider, so they can't use the overrides-bound createResource mutation hooks. This is the documented deviation for this plugin.
  • useNotify: all 18 sonner toast sites in the three internal pages replaced.
  • useTranslate: every UI string routed through t("comments.…", "Default") with override-wins precedence (localization prop/override → i18n provider → English default). CommentThread now also honors overrides.localization (prop still wins).
  • CanAccess: permission prop 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.
  • useListState: moderation tab+page and my-comments page are URL-synced (?tab=spam&page=3), back-button friendly, and clamped against mangled URL values.
  • Inline field errors: CommentForm surfaces the StackError.errors.body message inline instead of the generic submit error.
  • Cleanup: plugin-local error-utils.ts and the toError re-export deleted; build-registry.ts updated; btst-comments.json regenerated (no more error-utils/sonner references).

Test plan

  • pnpm build, pnpm typecheck, pnpm lint, pnpm knip green
  • pnpm test — 441 tests / 38 files, including:
    • new comments-query-keys.test.ts parity guard (factory keys vs COMMENTS_QUERY_KEYS/discriminators, _def prefixes, parentId null-vs-undefined segregation)
    • new client-sweep.test.tsx jsdom suite (15 tests: CanAccess allow/deny paths, notify success routing, useListState URL seed/write/clamp, inline StackError field errors, i18n precedence)
  • Registry regenerated and validated

Made with Cursor

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>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview Aug 19, 2026 3:07pm
better-stack-playground Ready Ready Preview Aug 19, 2026 3:07pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • tab is 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.
  • page is passed through Math.max(1, Math.floor(listState.page) || 1), clamping all invalid inputs (non-numeric, NaN, 0, negative) to 1.

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:

"currentUserId is intentionally NOT sent to the server in any query. The server resolves the caller's identity server-side via the resolveCurrentUserId hook. 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 ⚠️ Pre-existing, not fixed
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
Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

@github-actions

Copy link
Copy Markdown
Contributor

Shadcn registry validated — no registry changes detected.

@olliethedev
olliethedev merged commit 8958f0f into v3 Aug 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant