File tree Expand file tree Collapse file tree
apps/sim/app/api/files/export/[id] Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -161,6 +161,29 @@ describe('markdown export bundling', () => {
161161 expect ( zip . file ( 'assets/bad.png' ) ) . toBeNull ( )
162162 } )
163163
164+ /**
165+ * The two id representations have to stay distinct: metadata resolves by the stored id, while the
166+ * rewrite finds the embed by the spelling the document used. Collapsing them either drops the
167+ * asset or bundles it behind a link still pointing at the API.
168+ */
169+ it ( 'resolves and rewrites an embed whose id is percent-encoded in the document' , async ( ) => {
170+ embeds ( 'wf%5Fa' )
171+ assetsResolveTo ( ( id ) => ( id === 'wf_a' ? assetRecord ( id , 1 * MB ) : null ) )
172+ mockDownloadFile . mockImplementation ( async ( { key } : { key : string } ) =>
173+ key . endsWith ( 'doc.md' )
174+ ? Buffer . from ( '# Doc\n\n' )
175+ : Buffer . from ( 'png-bytes' )
176+ )
177+
178+ const response = await GET ( request ( ) , context )
179+
180+ const zip = await JSZip . loadAsync ( Buffer . from ( await response . arrayBuffer ( ) ) )
181+ expect ( zip . file ( 'assets/wf_a.png' ) ) . not . toBeNull ( )
182+ const md = await zip . file ( 'doc.md' ) ?. async ( 'string' )
183+ expect ( md ) . toContain ( './assets/wf_a.png' )
184+ expect ( md ) . not . toContain ( '/api/files/view/' )
185+ } )
186+
164187 it ( 'skips an asset the caller cannot read' , async ( ) => {
165188 embeds ( 'secret' )
166189 mockVerifyFileAccess . mockImplementation ( async ( key : string ) => ! key . endsWith ( 'secret' ) )
Original file line number Diff line number Diff line change @@ -51,6 +51,19 @@ function safeFilename(name: string): string {
5151 . replace ( / [ \r \n \t ] / g, '' )
5252}
5353
54+ /**
55+ * The stored id behind an embed's spelling. Ids arrive spelled as the document writes them, because
56+ * the rewrite below locates each embed by searching for that spelling; metadata lookup needs the
57+ * decoded id instead, so the two representations are kept distinct rather than reconciled.
58+ */
59+ function storedId ( spelledId : string ) : string {
60+ try {
61+ return decodeURIComponent ( spelledId )
62+ } catch {
63+ return spelledId
64+ }
65+ }
66+
5467function deduplicatedFilename ( preferred : string , existing : Set < string > , imageId : string ) : string {
5568 if ( ! existing . has ( preferred ) ) return preferred
5669 const ext = path . extname ( preferred )
@@ -160,7 +173,7 @@ export const GET = withRouteHandler(
160173 const assetTargets = (
161174 await mapWithConcurrency ( imageIds , MATERIALIZE_CONCURRENCY , async ( imageId ) => {
162175 try {
163- const imgRecord = await getFileMetadataById ( imageId )
176+ const imgRecord = await getFileMetadataById ( storedId ( imageId ) )
164177 if ( ! imgRecord ) return null
165178 if ( ! ( await verifyFileAccess ( imgRecord . key , userId ) ) ) return null
166179 return { imageId, record : imgRecord }
You can’t perform that action at this time.
0 commit comments