Skip to content

Commit c3696fa

Browse files
committed
fix(changelog): harden feed rendering and media validation
1 parent 1652012 commit c3696fa

5 files changed

Lines changed: 44 additions & 21 deletions

File tree

‎apps/sim/app/changelog.xml/route.ts‎

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { getAllEntryMeta } from '@/lib/changelog'
33
import { SITE_URL } from '@/lib/core/utils/urls'
44
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
55

6-
export const dynamic = 'force-static'
7-
export const revalidate = 3600
6+
/** Request-scoped logging needs dynamic handling; successful feeds are cached by the CDN. */
7+
export const dynamic = 'force-dynamic'
88

99
function escapeXml(value: string): string {
1010
return value
@@ -16,31 +16,40 @@ function escapeXml(value: string): string {
1616
}
1717

1818
/** RSS is a public XML protocol; its summaries share the website's published content source. */
19-
export const GET = withRouteHandler(async () => {
20-
const entries = (await getAllEntryMeta()).slice(0, 50)
21-
const items = entries
22-
.map(
23-
(entry) => `
19+
export const GET = withRouteHandler(
20+
async () => {
21+
const entries = (await getAllEntryMeta()).slice(0, 50)
22+
const items = entries
23+
.map(
24+
(entry) => `
2425
<item>
2526
<title>${escapeXml(entry.title)}</title>
2627
<link>${escapeXml(entry.canonical)}</link>
2728
<guid isPermaLink="true">${escapeXml(entry.release?.url ?? entry.canonical)}</guid>
2829
<pubDate>${new Date(entry.date).toUTCString()}</pubDate>
2930
<description>${escapeXml(entry.description)}</description>
3031
</item>`
31-
)
32-
.join('')
33-
const xml = `<?xml version="1.0" encoding="UTF-8"?>
32+
)
33+
.join('')
34+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
3435
<rss version="2.0"><channel>
3536
<title>Sim Changelog</title>
3637
<link>${SITE_URL}/changelog</link>
3738
<description>New features, improvements, and fixes in Sim.</description>
3839
<language>en-us</language>${items}
3940
</channel></rss>`
40-
return new NextResponse(xml, {
41-
headers: {
42-
'Content-Type': 'application/rss+xml; charset=utf-8',
43-
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=3600',
44-
},
45-
})
46-
})
41+
return new NextResponse(xml, {
42+
headers: {
43+
'Content-Type': 'application/rss+xml; charset=utf-8',
44+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=3600',
45+
},
46+
})
47+
},
48+
{
49+
unhandledErrorResponse: () =>
50+
new NextResponse('Service Unavailable', {
51+
status: 503,
52+
headers: { 'Cache-Control': 'no-store', 'Retry-After': '60' },
53+
}),
54+
}
55+
)

‎apps/sim/content/changelog/power-bi-for-agents/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Add [Power BI](/integrations/power-bi) to a workflow to bring business data into
1717

1818
The integration includes eight actions for finding workspaces, reading reports and semantic models, running queries, requesting refreshes, and checking refresh history. A refresh request starts the work; use the history action to check its progress.
1919

20-
For example, summarize weekly revenue by region and use those results to prepare a sales update. The sample below shows query configuration; connect your Power BI account and choose a model before running it.
20+
For example, summarize revenue by region and use those results to prepare a weekly sales update. The sample below shows query configuration; connect your Power BI account and choose a model before running it.
2121

2222
<ChangelogImage
2323
src="/changelog/power-bi-query-v1.jpg"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { isChangelogMediaSource } from '@/lib/changelog/media'
3+
4+
describe('changelog media origins', () => {
5+
it.each([
6+
{ kind: 'tab padding', source: '/\t/example.test/demo.mp4' },
7+
{ kind: 'newline padding', source: '/\n//example.test/demo.mp4' },
8+
{ kind: 'carriage-return padding', source: '/\r/example.test/demo.mp4' },
9+
{ kind: 'backslash separators', source: '/\\example.test/demo.mp4' },
10+
])('rejects $kind that browsers resolve to another origin', ({ source }) => {
11+
expect(new URL(source, 'https://www.sim.ai').origin).toBe('https://example.test')
12+
expect(isChangelogMediaSource(source)).toBe(false)
13+
})
14+
})

‎apps/sim/lib/changelog/media.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const CHANGELOG_MEDIA_ORIGIN = 'https://nnjgp7vypgx4myuq.public.blob.verc
33

44
/** Shared by rendering and content validation; CSP imports this leaf before aliases resolve. */
55
export function isChangelogMediaSource(source: string): boolean {
6-
if (typeof source !== 'string') return false
6+
if (typeof source !== 'string' || /[\r\n\t\\]/.test(source)) return false
77
if (source.startsWith('/') && !source.startsWith('//')) return true
88
try {
99
const url = new URL(source)

‎scripts/check-library-content.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,11 @@ function checkChangelogBody(
270270
)
271271
const asset = (field: string, localOnly: boolean) => {
272272
const value = literal(field)
273-
if (!localOnly && value && !value.startsWith('/') && isChangelogMediaSource(value)) return
274-
if (!value?.startsWith('/') || value.startsWith('//')) {
273+
if (!value || !isChangelogMediaSource(value) || (localOnly && !value.startsWith('/'))) {
275274
fail(field, 'must be a literal same-origin path or approved media CDN URL')
276275
return
277276
}
277+
if (!value.startsWith('/')) return
278278
const target = path.resolve(config.publicDir, `.${value}`)
279279
const relative = path.relative(config.publicDir, target)
280280
if (relative.startsWith('..') || path.isAbsolute(relative)) {

0 commit comments

Comments
 (0)