feat(file): workspace-scoped inline images + public-share cascade#5203
Conversation
Embedded markdown images now resolve only within the document's workspace, and public file shares cascade to the images the shared document embeds. - New /api/workspaces/[id]/files/inline (in-app, workspace-scoped) and /api/files/public/[token]/inline (public cascade) routes; the public one serves an embed only when it is referenced-by-doc, same-workspace, and passes a magic-byte image sniff - Embed srcs (serve-key and view-id forms) rewrite through one scoped inline route; one shared isomorphic parser owns the embed grammar for both the frontend renderer and the server doc scan - Accept wf_ file ids on the view/export routes (were 400ing on .uuid())
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@greptile review |
PR SummaryMedium Risk Overview Embedded A shared isomorphic embed parser ( Reviewed by Cursor Bugbot for commit a007410. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e14499f. Configure here.
Greptile SummaryThis PR adds workspace-scoped inline image serving for markdown embeds and cascades public file shares to their embedded images, so logged-out viewers see images instead of broken icons. It also fixes a schema bug where
Confidence Score: 5/5Safe to merge — the three-gate security model is correctly layered, all gates fail closed with 404/401, and the shared parser eliminates client/server drift risk. The new inline routes are well-structured: the in-app route gates on session + workspace membership before resolving any file, and the public route correctly chains rate-limit → token resolution → deployment auth → referenced-by-doc → same-workspace → magic-byte sniff, with every gate failure returning a non-leaking response. The shared isomorphic parser ensures the reference set the server authorizes exactly matches what the client renders. The export route fix (wf_ IDs, in-app-path embed rewriting) is targeted and correct. Previous reviewer feedback on the per-type cap bug has been addressed in the final commit. Test coverage is thorough across all gates. No files require special attention. Important Files Changed
Reviews (4): Last reviewed commit: "fix(file): export rewrites all embed for..." | Re-trigger Greptile |
Greptile SummaryThis PR introduces workspace-scoped inline image serving for embedded markdown images and a public-share cascade that allows logged-out viewers of a shared document to see its embedded images. Two new API routes are added, a shared isomorphic parser owns the embed grammar for both frontend and server, and the
Confidence Score: 4/5The three-gate public cascade route is correctly structured, the workspace-scoped in-app route properly gates on session + membership, and the schema fix unblocks wf_-format file IDs without regressions. The core security logic is sound across all changed routes, the isomorphic parser removes grammar drift between client and server, and the validation fix is straightforward. All three findings are non-blocking quality observations with no correctness or data-integrity impact on the changed paths. apps/sim/app/api/files/public/[token]/inline/route.ts (per-request document download) and apps/sim/lib/uploads/utils/embedded-image-ref.ts (per-type vs. global image cap) Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Browser
participant PublicInlineRoute as /api/files/public/[token]/inline
participant WorkspaceInlineRoute as /api/workspaces/[id]/files/inline
participant ShareManager
participant Storage
participant DB
Note over Browser,DB: Logged-out viewer — public share cascade
Browser->>PublicInlineRoute: "GET ?fileId=wf_abc (or ?key=…)"
PublicInlineRoute->>ShareManager: resolveActiveShareByToken(token)
ShareManager-->>PublicInlineRoute: "{share, file: doc}"
PublicInlineRoute->>PublicInlineRoute: validateDeploymentAuth()
PublicInlineRoute->>Storage: downloadFile(doc.key) [referenced-by-doc gate]
Storage-->>PublicInlineRoute: doc bytes
PublicInlineRoute->>PublicInlineRoute: extractEmbeddedImageIds/Keys(docText).includes(ref)
alt not referenced
PublicInlineRoute-->>Browser: 404
end
PublicInlineRoute->>DB: resolveWorkspaceInlineImage(doc.workspaceId, ref) [same-workspace gate]
DB-->>PublicInlineRoute: "{key, contentType, filename} or null"
alt cross-workspace or deleted
PublicInlineRoute-->>Browser: 404
end
PublicInlineRoute->>Storage: "downloadFile(image.key) [content-truth gate: sniff=true]"
Storage-->>PublicInlineRoute: image bytes
PublicInlineRoute->>PublicInlineRoute: sniffImageContentType(bytes) — rejects SVG/HTML
PublicInlineRoute-->>Browser: 200 image/png (or jpeg/gif/webp)
Note over Browser,DB: Authenticated viewer — workspace-scoped inline
Browser->>WorkspaceInlineRoute: "GET /api/workspaces/wsId/files/inline?fileId=wf_abc"
WorkspaceInlineRoute->>WorkspaceInlineRoute: getSession() + getUserEntityPermissions()
alt no session or not a member
WorkspaceInlineRoute-->>Browser: 401 / 404
end
WorkspaceInlineRoute->>DB: resolveWorkspaceInlineImage(workspaceId, ref)
DB-->>WorkspaceInlineRoute: "{key, contentType, filename} or null"
WorkspaceInlineRoute->>Storage: "downloadFile(image.key) [sniff=false, stored type]"
Storage-->>WorkspaceInlineRoute: image bytes
WorkspaceInlineRoute-->>Browser: 200 image/…
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Browser
participant PublicInlineRoute as /api/files/public/[token]/inline
participant WorkspaceInlineRoute as /api/workspaces/[id]/files/inline
participant ShareManager
participant Storage
participant DB
Note over Browser,DB: Logged-out viewer — public share cascade
Browser->>PublicInlineRoute: "GET ?fileId=wf_abc (or ?key=…)"
PublicInlineRoute->>ShareManager: resolveActiveShareByToken(token)
ShareManager-->>PublicInlineRoute: "{share, file: doc}"
PublicInlineRoute->>PublicInlineRoute: validateDeploymentAuth()
PublicInlineRoute->>Storage: downloadFile(doc.key) [referenced-by-doc gate]
Storage-->>PublicInlineRoute: doc bytes
PublicInlineRoute->>PublicInlineRoute: extractEmbeddedImageIds/Keys(docText).includes(ref)
alt not referenced
PublicInlineRoute-->>Browser: 404
end
PublicInlineRoute->>DB: resolveWorkspaceInlineImage(doc.workspaceId, ref) [same-workspace gate]
DB-->>PublicInlineRoute: "{key, contentType, filename} or null"
alt cross-workspace or deleted
PublicInlineRoute-->>Browser: 404
end
PublicInlineRoute->>Storage: "downloadFile(image.key) [content-truth gate: sniff=true]"
Storage-->>PublicInlineRoute: image bytes
PublicInlineRoute->>PublicInlineRoute: sniffImageContentType(bytes) — rejects SVG/HTML
PublicInlineRoute-->>Browser: 200 image/png (or jpeg/gif/webp)
Note over Browser,DB: Authenticated viewer — workspace-scoped inline
Browser->>WorkspaceInlineRoute: "GET /api/workspaces/wsId/files/inline?fileId=wf_abc"
WorkspaceInlineRoute->>WorkspaceInlineRoute: getSession() + getUserEntityPermissions()
alt no session or not a member
WorkspaceInlineRoute-->>Browser: 401 / 404
end
WorkspaceInlineRoute->>DB: resolveWorkspaceInlineImage(workspaceId, ref)
DB-->>WorkspaceInlineRoute: "{key, contentType, filename} or null"
WorkspaceInlineRoute->>Storage: "downloadFile(image.key) [sniff=false, stored type]"
Storage-->>WorkspaceInlineRoute: image bytes
WorkspaceInlineRoute-->>Browser: 200 image/…
|
- New /Image slash command uploads an image via a file picker and inserts it at the caret (same upload+insert path as paste/drop) - Inserted src is the workspace serve URL, so it renders in-app and cascades to public shares like any other embed - Per-editor handler wired through slash-command storage (the extension set is a shared singleton); only active when the editor is editable
|
@greptile review |
Addresses PR review: - Markdown export now rewrites the in-app `/workspace/<ws>/files/<id>` embed form too (not just `/api/files/view/<id>`), so a bundled asset never leaves a broken link in an offline export (Bugbot) - extractEmbeddedFileRefs bounds total references (keys + ids) to 50 combined rather than 50 each, matching MAX_EMBEDDED_IMAGES intent
|
@greptile review |

Summary
getWorkspaceFilescope check; cross-workspace embeds previously rendered for dual-workspace members)/api/workspaces/[id]/files/inline(in-app, workspace-scoped) and/api/files/public/[token]/inline(public cascade); embeds rewrite through them via theFileContentSourceseamwf_file ids were 400ing on the view/export routes (the schema required.uuid(), but workspace files usewf_<shortId>)Type of Change
Testing
bun run lint,bun run check:api-validation:strict, andtsc --noEmitall passa618…docwf_Ytm…embeds imagewf_YwDXi8…Checklist