Add dynamic open graph sharing metadata and multi-method sharing support - #699
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
🚀 Preview Deployment ReadyVercel is rebuilding the frontend with the new Convex backend URL. Vercel will post the preview URL automatically. Convex Preview Backend
🤖 Deployed automatically by GitHub Actions |
Greptile SummaryThis PR introduces a full chat sharing system: a The implementation is architecturally sound — auth is derived from the Convex session, share IDs use 88 bits of randomness, public queries properly gate on
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Sidebar as AppSidebar / ChatContextMenu
participant Actions as useSidebarActions
participant Convex as Convex (chatShares)
participant SharePage as /share/:shareId
participant OGEndpoint as /api/og/share/:shareId
participant Crawler as Social Media Crawler
User->>Sidebar: Right-click chat → "Share"
Sidebar->>Actions: handleShareFromMenu(chatId)
Actions->>Convex: mutation(createOrGet, { chatId })
Convex-->>Actions: { shareId, createdAt }
Actions->>Actions: setShareUrl(origin + /share/ + shareId)
Actions-->>Sidebar: opens ShareChatDialog
User->>Sidebar: Copies link / shares to X/WhatsApp/Email
Crawler->>SharePage: GET /share/:shareId
SharePage->>Convex: query(getPublicByShareId, { shareId })
Convex-->>SharePage: { title, messages, firstUserPrompt, ... }
SharePage-->>Crawler: HTML with og:image = /api/og/share/:shareId
Crawler->>OGEndpoint: GET /api/og/share/:shareId
OGEndpoint->>Convex: query(getPreviewByShareId, { shareId })
Convex-->>OGEndpoint: { title, firstUserPrompt, firstAssistantResponse }
OGEndpoint-->>Crawler: SVG image (⚠️ unsupported by Facebook/LinkedIn)
User->>Sidebar: Click "Stop sharing"
Sidebar->>Actions: handleRevokeShare()
Actions->>Convex: mutation(revoke, { chatId })
Convex-->>Actions: { revoked: true }
Actions->>Actions: setShareUrl("") — dialog stays open ⚠️
Last reviewed commit: 31d6034 |
|
✅ No security issues found — scanned commits: |
|
@macroscope-app review |
|
Review complete! You can view the results here: |
ApprovabilityVerdict: Needs human review This PR introduces a complete new chat sharing feature with backend mutations, new database schema, UI components, and routes for share pages and OG images. New features introducing user-facing behavior require human review. Additionally, open review comments identify functional issues: SVG og:images won't render on most social platforms, and the share dialog has confusing UX after revoking. You can customize Macroscope's approvability policy. Learn more. |
|
@cubic-dev-ai review |
|
@cubic-dev-ai please re-run review on the latest commit 30e1694. |
…ion 1) Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <greptile-apps[bot]@users.noreply.github.com> Co-authored-by: macroscope-app[bot] <macroscope-app[bot]@users.noreply.github.com>
|
@macroscope-app review |
|
@cubic-dev-ai please re-run review on the latest commit ba6335c. |
|
✅ No security issues found — scanned commits: |
|
@cubic-dev-ai please re-run review on the latest commit b13cac2. |
|
@macroscope-app review |
|
✅ No security issues found — scanned commits: |
|
@macroscope-app review |
|
@cubic-dev-ai please re-run review on the latest commit cbd2ab5. |
|
@cubic-dev-ai review |
|
| Severity | Vulnerability Type | File | Line(s) |
|---|---|---|---|
| Medium | Missing Rate Limiting | apps/server/convex/chatShares.ts |
104–132 |
| Medium | Internal ID Disclosure | apps/server/convex/chatShares.ts |
~204 |
| Low | Missing CSP Header on SVG | apps/web/src/routes/api/og/share/$shareId.ts |
~127 |
| Low | Missing Input Validation | apps/web/src/routes/api/og/share/$shareId.ts, apps/web/src/routes/share/$shareId.tsx |
~119, ~38 |
| Low | CDN Cache Serves Revoked Shares | apps/web/src/routes/api/og/share/$shareId.ts |
~129 |
Total: 2 Medium, 3 Low
Details & Remediation
1. Missing Rate Limiting on createOrGet mutation (Medium)
File: apps/server/convex/chatShares.ts:104–132
The createOrGet mutation creates share records without rate limiting, while other mutations in this codebase (chats.ts) consistently use rateLimiter.limit(). An authenticated user could spam share creation across many chats, generating database load.
Fix: Add rate limiting consistent with existing patterns:
// In the createOrGet handler, before any DB writes:
await rateLimiter.limit(ctx, "shareCreate", { key: userId });2. Internal Convex _id Leaked in Public API Response (Medium)
File: apps/server/convex/chatShares.ts:~204 (inside getPublicByShareId)
The unauthenticated getPublicByShareId query maps message._id (the internal Convex document ID) to the public id field. This exposes internal identifiers to anonymous users, which reveals backend implementation details and could aid enumeration against other endpoints.
Fix: Use a non-revealing identifier instead:
// Before (leaks _id):
.map((message) => ({
id: message._id,
...
}))
// After (use index-based ID):
.map((message, index) => ({
id: `msg-${index}`,
...
}))3. SVG Response Missing Content-Security-Policy Header (Low)
File: apps/web/src/routes/api/og/share/$shareId.ts:~127
The OG image endpoint returns SVGs with user-supplied text. While the encodeSvgText() function correctly escapes XML entities, the response lacks a CSP header. If the SVG URL is opened directly in a browser (not via <img>), a more sophisticated vector could theoretically execute in browsers without strict SVG sandboxing.
Fix: Add CSP header:
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
"Cache-Control": "public, s-maxage=600, stale-while-revalidate=86400",
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
},4. No Input Validation on shareId Route Parameter (Low)
File: apps/web/src/routes/api/og/share/$shareId.ts:~119, apps/web/src/routes/share/$shareId.tsx:~38
The shareId URL parameter is passed directly to Convex queries without format validation. While Convex will return null for non-matching values, an attacker could send extremely long strings, creating unnecessary backend load.
Fix: Add a format guard before calling Convex:
if (!/^[a-f0-9]{22}$/.test(params.shareId)) {
return new Response("Not found", { status: 404 });
}5. CDN Cache Serves Revoked Shares for Up to 10 Minutes (Low)
File: apps/web/src/routes/api/og/share/$shareId.ts:~129
The OG endpoint sets s-maxage=600 (10 minutes). After a user revokes a share link, the OG preview image remains in CDN caches and continues to be served. This is a design tradeoff rather than a bug, but worth noting if instant revocation is a product requirement.
Fix (if needed): Reduce TTL or implement cache purge on revocation:
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",Positive Findings
- Authentication/authorization is correctly implemented — all write operations and owner-facing reads call
requireAuthUserId()andassertOwnsChat(). - SVG text encoding is properly implemented —
encodeSvgText()escapes all 5 critical XML entities. - Deleted chats are properly excluded — both public queries check
chat.deletedAt. - Client-side URL construction is safe — share URLs use
encodeURIComponent()for social sharing links. - Share ID entropy is adequate — 22 hex characters from
crypto.randomUUID()provides ~88 bits of randomness.
…ion 4) Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <greptile-apps[bot]@users.noreply.github.com> Co-authored-by: macroscope-app[bot] <macroscope-app[bot]@users.noreply.github.com>
|
@macroscope-app review |
|
@cubic-dev-ai review |
|
✅ No security issues found — scanned commits: |
| return new Response(svg, { | ||
| status: 200, | ||
| headers: { | ||
| "Content-Type": "image/svg+xml; charset=utf-8", | ||
| "Cache-Control": "public, s-maxage=600, stale-while-revalidate=86400", | ||
| }, |
There was a problem hiding this comment.
SVG og:image unsupported by major social platforms
The OG image endpoint returns image/svg+xml, but Facebook's Open Graph scraper, LinkedIn, Slack, and Discord all require raster images (PNG/JPEG/WebP) for og:image — SVG is silently ignored. This means the entire OG preview image feature will not display on the platforms it is most intended for, despite the significant effort building buildSvg.
Twitter/X can render SVG in modern browsers, but its card validator also frequently rejects SVGs from server-rendered og:image URLs.
To fix this you'd need to convert the SVG to a raster image server-side (e.g., using @resvg/resvg-js, sharp, or a Vercel OG image library like @vercel/og), then return a PNG response:
// Example using @vercel/og (already used in many TanStack Start apps)
import { ImageResponse } from '@vercel/og';
// ...
return new ImageResponse(<ShareCardComponent />, { width: 1200, height: 630 });Confidence: 4/5 — SVG is definitively unsupported by Facebook OG and LinkedIn scrapers; this is a well-documented platform limitation.
| const handleRevokeShare = useCallback(async () => { | ||
| if (!shareChatId || !convexClient || !convexUser?._id) return; | ||
|
|
||
| setIsRevokingShare(true); | ||
| try { | ||
| const result = await convexClient.mutation(api.chatShares.revoke, { | ||
| chatId: shareChatId as Id<"chats">, | ||
| }); | ||
| if (!result.revoked) { | ||
| toast.error("Share link is no longer active"); | ||
| return; | ||
| } | ||
| setShareUrl(""); | ||
| toast.success("Share link revoked"); | ||
| } catch (error) { | ||
| console.warn("[Chat] Failed to revoke share link:", error); | ||
| toast.error("Failed to revoke share link"); | ||
| } finally { | ||
| setIsRevokingShare(false); | ||
| } | ||
| }, [convexClient, convexUser?._id, shareChatId]); |
There was a problem hiding this comment.
Dialog stays open after successful revoke with confusing empty state
After handleRevokeShare succeeds, setShareUrl("") clears the URL but setShowShareDialog(false) is never called. The dialog remains open showing "Share link will appear here" (the pre-share state), with all action buttons disabled (canShare = false, !shareUrl disables "Stop sharing"). The only available action is "Close".
This is confusing: the user can't tell if the revoke worked or if they're expected to do something else. Consider closing the dialog automatically on success, or resetting to a clear "Share revoked" state:
if (!result.revoked) {
toast.error("Share link is no longer active");
return;
}
setShareUrl("");
setShowShareDialog(false); // close on successful revoke
toast.success("Share link revoked");Confidence: 3/5 — the behavior is technically functional (toast appears, state is cleared), but the UX is confusing enough that users may think the revoke failed.
Summary
Testing
Note
Add chat sharing with dynamic Open Graph metadata and multi-method share support
chatSharesConvex table and backend module with queries/mutations to create, retrieve, and revoke share links for chats./share/:shareIdpublic route that renders a read-only view of a shared chat with SEO/OG meta tags./api/og/share/:shareIdendpoint that generates a dynamic SVG Open Graph image using chat title and preview snippets.useSidebarActionshook with share management: generating a link, copying it, invoking native share, and revoking it.share-preview.tscontains a syntax error inencodeSvgText(extraneous+before a.replaceAllcall) that will prevent the app from building.Macroscope summarized 31d6034.