Skip to content

Commit 29274cf

Browse files
committed
fix(deps): address review edge cases
1 parent 69979ba commit 29274cf

13 files changed

Lines changed: 225 additions & 23 deletions

File tree

apps/sim/app/(landing)/demo/components/demo-booking/demo-booking.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ import { chipBorderShadowRing, cn } from '@sim/emcn'
55
import dynamic from 'next/dynamic'
66
import { preconnect } from 'react-dom'
77
import { DemoForm, type DemoLead } from '@/app/(landing)/demo/components/demo-form'
8+
import { CAL_ORIGIN } from '@/app/(landing)/demo/components/demo-scheduler/cal-config'
89
import { applyLegacyInertFallback } from '@/app/(landing)/demo/components/legacy-inert-fallback'
910

1011
const importScheduler = () => import('@/app/(landing)/demo/components/demo-scheduler')
1112

1213
/**
1314
* Warm the entire booking path while the visitor fills the form: preconnect to
14-
* app.cal.com, then load the scheduler chunk, Cal.com's embed.js, and the
15+
* the configured Cal origin, then load the scheduler chunk and the
1516
* booker iframe assets (via the embed's `preload` instruction). Fired on first
1617
* form focus so nothing Cal.com-related competes with initial page load — the
1718
* connection handshake overlaps the chunk import, and it all finishes long
1819
* before the visitor submits.
1920
*/
2021
function preloadScheduler() {
21-
preconnect('https://app.cal.com')
22+
preconnect(CAL_ORIGIN)
2223
return importScheduler().then((m) => m.preloadCalEmbed())
2324
}
2425

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const DEFAULT_CAL_ORIGIN = 'https://app.cal.com'
2+
const DEFAULT_CAL_LINK = 'team/sim/demo'
3+
4+
/** Resolves a hosted or self-hosted Cal event link and rejects non-HTTP embed targets. */
5+
export function resolveCalLink(configuredLink?: string): URL {
6+
const link = configuredLink?.trim() || DEFAULT_CAL_LINK
7+
let url: URL
8+
9+
try {
10+
url = new URL(link)
11+
} catch {
12+
url = new URL(link.replace(/^\/+/, ''), `${DEFAULT_CAL_ORIGIN}/`)
13+
}
14+
15+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
16+
throw new Error('NEXT_PUBLIC_CAL_LINK must be an HTTP(S) URL or a Cal.com event path')
17+
}
18+
19+
url.hash = ''
20+
return url
21+
}
22+
23+
const calLinkUrl = resolveCalLink(process.env.NEXT_PUBLIC_CAL_LINK)
24+
25+
/** Exact origin used for iframe navigation, preconnect, and postMessage validation. */
26+
export const CAL_ORIGIN = calLinkUrl.origin
27+
28+
/** Returns a fresh URL so callers can safely add embed-specific paths and parameters. */
29+
export function createConfiguredCalUrl(): URL {
30+
return new URL(calLinkUrl)
31+
}

apps/sim/app/(landing)/demo/components/demo-scheduler/demo-scheduler.test.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ vi.mock('@/lib/consent/tracking-consent', () => ({
1616
useTrackingConsent: () => mockConsent,
1717
}))
1818

19+
import { resolveCalLink } from '@/app/(landing)/demo/components/demo-scheduler/cal-config'
1920
import {
2021
createCalEmbedUrl,
2122
DemoScheduler,
@@ -75,6 +76,22 @@ describe('DemoScheduler', () => {
7576
})
7677
})
7778

79+
it('derives the trusted origin from a self-hosted event URL', () => {
80+
const url = resolveCalLink('https://calendar.example.com/team/sim/demo')
81+
82+
expect(url.origin).toBe('https://calendar.example.com')
83+
expect(url.pathname).toBe('/team/sim/demo')
84+
})
85+
86+
it('rejects unsafe Cal embed protocols and credential-bearing URLs', () => {
87+
expect(() => resolveCalLink('javascript:alert(1)')).toThrow(
88+
'NEXT_PUBLIC_CAL_LINK must be an HTTP(S) URL or a Cal.com event path'
89+
)
90+
expect(() => resolveCalLink('https://user:secret@calendar.example.com/demo')).toThrow(
91+
'NEXT_PUBLIC_CAL_LINK must be an HTTP(S) URL or a Cal.com event path'
92+
)
93+
})
94+
7895
it('warms the hosted booker only once while the preload frame remains mounted', () => {
7996
preloadCalEmbed()
8097
preloadCalEmbed()

apps/sim/app/(landing)/demo/components/demo-scheduler/demo-scheduler.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { trackGoogleEvent } from '@/lib/analytics/google'
55
import { X_DEMO_BOOKED_EVENT_ID } from '@/lib/consent/scripts'
66
import { useTrackingConsent } from '@/lib/consent/tracking-consent'
77
import type { DemoLead } from '@/app/(landing)/demo/components/demo-form'
8+
import {
9+
CAL_ORIGIN,
10+
createConfiguredCalUrl,
11+
} from '@/app/(landing)/demo/components/demo-scheduler/cal-config'
812

9-
/** The Cal.com event the demo books - set `NEXT_PUBLIC_CAL_LINK` to override. */
10-
const CAL_ORIGIN = 'https://app.cal.com'
1113
const CAL_NAMESPACE = 'demo'
12-
const CAL_LINK = process.env.NEXT_PUBLIC_CAL_LINK ?? 'team/sim/demo'
1314

1415
/** Sim's brand color, matching the `--brand-agent` token. */
1516
const CAL_BRAND_COLOR = '#6f3dfa'
@@ -38,9 +39,9 @@ function isCalMessage(data: unknown): data is CalMessage {
3839
* Query parameters keep the lead prefill and light, month-view presentation.
3940
*/
4041
export function createCalEmbedUrl(lead: DemoLead): string {
41-
const normalizedLink = CAL_LINK.replace(/^\/+|\/+$/g, '')
42-
const path = normalizedLink.endsWith('/embed') ? normalizedLink : `${normalizedLink}/embed`
43-
const url = new URL(path, `${CAL_ORIGIN}/`)
42+
const url = createConfiguredCalUrl()
43+
const normalizedPath = url.pathname.replace(/\/+$/, '')
44+
url.pathname = normalizedPath.endsWith('/embed') ? normalizedPath : `${normalizedPath}/embed`
4445
url.searchParams.set('embed', CAL_NAMESPACE)
4546
url.searchParams.set('name', lead.name)
4647
url.searchParams.set('email', lead.email)
@@ -53,8 +54,7 @@ export function createCalEmbedUrl(lead: DemoLead): string {
5354
}
5455

5556
function createCalPreloadUrl(): string {
56-
const normalizedLink = CAL_LINK.replace(/^\/+|\/+$/g, '')
57-
const url = new URL(normalizedLink, `${CAL_ORIGIN}/`)
57+
const url = createConfiguredCalUrl()
5858
url.searchParams.set('preload', 'true')
5959
return url.toString()
6060
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import type { Node, ReactFlowInstance, Viewport } from '@xyflow/react'
6+
import { createRoot, type Root } from 'react-dom/client'
7+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
8+
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
9+
10+
const mountedRoots: Root[] = []
11+
12+
function renderCanvasViewport() {
13+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
14+
const root = createRoot(document.createElement('div'))
15+
mountedRoots.push(root)
16+
17+
const setViewport = vi.fn()
18+
const reactFlowInstance = {
19+
getNodes: () => [],
20+
screenToFlowPosition: (position: { x: number; y: number }) => position,
21+
setViewport,
22+
} as unknown as ReactFlowInstance
23+
let fitViewToBounds: ReturnType<typeof useCanvasViewport>['fitViewToBounds'] | undefined
24+
25+
function Probe() {
26+
fitViewToBounds = useCanvasViewport(reactFlowInstance).fitViewToBounds
27+
return null
28+
}
29+
30+
act(() => root.render(<Probe />))
31+
32+
return {
33+
applyFit(nodes: Node[]) {
34+
fitViewToBounds?.({ nodes, padding: 0, minZoom: 0.01, maxZoom: 10, duration: 0 })
35+
},
36+
get viewport(): Viewport | undefined {
37+
return setViewport.mock.calls.at(-1)?.[0]
38+
},
39+
}
40+
}
41+
42+
describe('useCanvasViewport', () => {
43+
beforeEach(() => {
44+
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1000 })
45+
Object.defineProperty(window, 'innerHeight', { configurable: true, value: 800 })
46+
})
47+
48+
afterEach(() => {
49+
act(() => {
50+
for (const root of mountedRoots.splice(0)) root.unmount()
51+
})
52+
})
53+
54+
it('falls back from nonpositive measured dimensions to configured dimensions', () => {
55+
const probe = renderCanvasViewport()
56+
57+
probe.applyFit([
58+
{
59+
id: 'node-1',
60+
position: { x: 100, y: 200 },
61+
data: {},
62+
measured: { width: 0, height: -1 },
63+
width: 250,
64+
height: 100,
65+
},
66+
])
67+
68+
expect(probe.viewport).toEqual({ x: -400, y: -600, zoom: 4 })
69+
})
70+
71+
it('uses block defaults when measured and configured dimensions are not usable', () => {
72+
const probe = renderCanvasViewport()
73+
74+
probe.applyFit([
75+
{
76+
id: 'node-1',
77+
position: { x: 0, y: 0 },
78+
data: {},
79+
measured: { width: 0, height: 0 },
80+
width: -1,
81+
height: -1,
82+
},
83+
])
84+
85+
expect(probe.viewport).toEqual({ x: 0, y: 200, zoom: 4 })
86+
})
87+
})

apps/sim/hooks/use-canvas-viewport.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ interface FitViewToBoundsOptions {
9494
nodes?: Node[]
9595
}
9696

97+
function resolveNodeDimension(
98+
measuredDimension: number | undefined,
99+
configuredDimension: number | undefined,
100+
fallback: number
101+
): number {
102+
if (typeof measuredDimension === 'number' && measuredDimension > 0) {
103+
return measuredDimension
104+
}
105+
if (typeof configuredDimension === 'number' && configuredDimension > 0) {
106+
return configuredDimension
107+
}
108+
return fallback
109+
}
110+
97111
/**
98112
* Hook providing canvas viewport utilities that account for sidebar, panel, and terminal overlays.
99113
*/
@@ -150,8 +164,16 @@ export function useCanvasViewport(
150164
let maxY = Number.NEGATIVE_INFINITY
151165

152166
nodes.forEach((node) => {
153-
const nodeWidth = node.measured?.width ?? node.width ?? BLOCK_DIMENSIONS.FIXED_WIDTH
154-
const nodeHeight = node.measured?.height ?? node.height ?? BLOCK_DIMENSIONS.MIN_HEIGHT
167+
const nodeWidth = resolveNodeDimension(
168+
node.measured?.width,
169+
node.width,
170+
BLOCK_DIMENSIONS.FIXED_WIDTH
171+
)
172+
const nodeHeight = resolveNodeDimension(
173+
node.measured?.height,
174+
node.height,
175+
BLOCK_DIMENSIONS.MIN_HEIGHT
176+
)
155177

156178
minX = Math.min(minX, node.position.x)
157179
minY = Math.min(minY, node.position.y)

apps/sim/lib/audio/extractor.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* @vitest-environment node
33
*/
44
import fs from 'node:fs'
5+
import path from 'node:path'
56
import { beforeEach, describe, expect, it, vi } from 'vitest'
67

78
const { calls, probe } = vi.hoisted(() => ({
@@ -72,6 +73,21 @@ describe('audio FFmpeg execution', () => {
7273
})
7374
})
7475

76+
it('keeps MIME-derived input filenames inside the temporary directory', async () => {
77+
await getAudioMetadata(Buffer.from('audio'), 'audio/../../../../escaped')
78+
79+
const inputFile = calls[0].args.at(-1)
80+
expect(inputFile).toBeDefined()
81+
expect(path.basename(inputFile as string)).toBe('input.dat')
82+
expect(inputFile).toMatch(/audio-ffprobe-[^/]+\/input\.dat$/)
83+
})
84+
85+
it('normalizes MIME parameters before selecting a known extension', async () => {
86+
await getAudioMetadata(Buffer.from('audio'), ' Audio/MPEG; codecs=mp3 ')
87+
88+
expect(calls[0].args.at(-1)).toMatch(/input\.mp3$/)
89+
})
90+
7591
it('converts with a shell-free argument vector and preserves audio options', async () => {
7692
const result = await extractAudioFromVideo(Buffer.from('video'), 'video/mp4', {
7793
bitrate: '128',

apps/sim/lib/audio/extractor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,12 @@ function getExtensionFromMimeType(mimeType: string): string {
239239
'audio/opus': 'opus',
240240
}
241241

242-
return mimeToExt[mimeType] || mimeType.split('/')[1] || 'dat'
242+
const normalizedMimeType = mimeType.split(';', 1)[0].trim().toLowerCase()
243+
const knownExtension = mimeToExt[normalizedMimeType]
244+
if (knownExtension) return knownExtension
245+
246+
const subtype = normalizedMimeType.split('/')[1]
247+
return subtype && /^[a-z0-9][a-z0-9.+_-]{0,63}$/.test(subtype) ? subtype : 'dat'
243248
}
244249

245250
function getAudioCodec(format: string): string {

apps/sim/scripts/build-pi-e2b-template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929

3030
const DEFAULT_TEMPLATE_NAME = 'sim-pi'
3131

32-
/** Pi 0.80 requires Node >=22.19; E2B's code-interpreter base currently ships Node 20. */
32+
/** Pi 0.84 requires Node >=22.19; E2B's code-interpreter base currently ships Node 20. */
3333
const INSTALL_NODE_COMMAND = `curl -fsSL https://deb.nodesource.com/setup_${PI_NODE_MAJOR}.x | bash - && apt-get install -y nodejs && ${PI_NODE_VERSION_ASSERT}`
3434

3535
/** Pi uses the command and filesystem APIs, so the inherited Jupyter service is unnecessary. */
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import packageJson from '@/package.json'
6+
import {
7+
PI_BUN_VERSION,
8+
PI_GLOBAL_NPM_PACKAGES,
9+
PI_PACKAGE_VERSION,
10+
} from '@/scripts/pi-sandbox-packages'
11+
12+
describe('Pi sandbox package contract', () => {
13+
it('keeps the sandbox Pi runtime aligned with the app SDK', () => {
14+
expect(PI_PACKAGE_VERSION).toBe(packageJson.dependencies['@earendil-works/pi-ai'])
15+
expect(PI_PACKAGE_VERSION).toBe(packageJson.dependencies['@earendil-works/pi-coding-agent'])
16+
expect(PI_GLOBAL_NPM_PACKAGES).toEqual([
17+
`bun@${PI_BUN_VERSION}`,
18+
`@earendil-works/pi-coding-agent@${PI_PACKAGE_VERSION}`,
19+
`@earendil-works/pi-agent-core@${PI_PACKAGE_VERSION}`,
20+
`@earendil-works/pi-ai@${PI_PACKAGE_VERSION}`,
21+
`@earendil-works/pi-tui@${PI_PACKAGE_VERSION}`,
22+
])
23+
})
24+
})

0 commit comments

Comments
 (0)