jamdesk 1.1.7 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +5 -2
  2. package/dist/lib/deps.js +2 -2
  3. package/dist/lib/openapi/types.d.ts +11 -6
  4. package/dist/lib/openapi/types.d.ts.map +1 -1
  5. package/dist/lib/path-security.d.ts +3 -0
  6. package/dist/lib/path-security.d.ts.map +1 -1
  7. package/dist/lib/path-security.js +14 -1
  8. package/dist/lib/path-security.js.map +1 -1
  9. package/package.json +13 -10
  10. package/vendored/app/[[...slug]]/page.tsx +48 -13
  11. package/vendored/app/api/assets/[...path]/route.ts +2 -0
  12. package/vendored/components/layout/LayoutWrapper.tsx +3 -4
  13. package/vendored/components/mdx/ApiEndpoint.tsx +13 -2
  14. package/vendored/components/mdx/MDXComponents.tsx +16 -0
  15. package/vendored/components/mdx/OpenApiEndpoint.tsx +76 -36
  16. package/vendored/components/mdx/Tabs.tsx +1 -1
  17. package/vendored/components/mdx/Video.tsx +82 -0
  18. package/vendored/components/navigation/Header.tsx +3 -3
  19. package/vendored/components/navigation/Sidebar.tsx +3 -3
  20. package/vendored/components/ui/CodePanel.tsx +5 -5
  21. package/vendored/components/ui/CodePanelModal.tsx +3 -3
  22. package/vendored/components/ui/DevOnlyNotice.tsx +78 -0
  23. package/vendored/hooks/useChatPanel.tsx +21 -2
  24. package/vendored/hooks/useMediaQuery.ts +27 -0
  25. package/vendored/lib/build-endpoint-from-mdx.ts +66 -0
  26. package/vendored/lib/isr-build-executor.ts +1 -1
  27. package/vendored/lib/middleware-helpers.ts +6 -1
  28. package/vendored/lib/openapi/code-examples.ts +479 -99
  29. package/vendored/lib/openapi/index.ts +9 -1
  30. package/vendored/lib/openapi/types.ts +29 -5
  31. package/vendored/lib/preprocess-mdx.ts +103 -36
  32. package/vendored/lib/process-mdx-with-exports.ts +22 -14
  33. package/vendored/lib/remark-extract-param-fields.ts +134 -0
  34. package/vendored/lib/shiki-client.ts +12 -0
  35. package/vendored/lib/static-artifacts.ts +2 -0
  36. package/vendored/lib/url-safety.ts +122 -0
  37. package/vendored/next.config.js +7 -0
  38. package/vendored/schema/docs-schema.json +17 -4
  39. package/vendored/scripts/copy-files.cjs +60 -54
  40. package/vendored/scripts/validate-links.cjs +1 -1
  41. package/vendored/shared/path-security.ts +17 -1
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef } from 'react';
4
+
5
+ /** Shared with preprocess-mdx.ts — keep in sync or extract to shared module */
6
+ export const VIDEO_EXTENSIONS = ['.mp4', '.webm'];
7
+
8
+ interface VideoProps {
9
+ src: string;
10
+ title?: string;
11
+ /** Default: true */
12
+ controls?: boolean;
13
+ /** Browsers block unmuted autoplay — pair with muted */
14
+ autoPlay?: boolean;
15
+ muted?: boolean;
16
+ loop?: boolean;
17
+ width?: string;
18
+ height?: string;
19
+ }
20
+
21
+ /**
22
+ * HTML5 video player for embedded video files.
23
+ *
24
+ * Markdown ![alt](/videos/demo.mp4) is converted to <Video> JSX by the
25
+ * preprocessor (transformMarkdownVideos). The img handler in MDXComponents
26
+ * has a fallback isVideoUrl check for raw <img src="video.mp4"> in MDX.
27
+ *
28
+ * muted is set via useRef/useEffect — React doesn't reflect it as a DOM
29
+ * attribute (https://github.com/facebook/react/issues/6544).
30
+ */
31
+ export function Video({
32
+ src,
33
+ title,
34
+ controls = true,
35
+ autoPlay,
36
+ muted,
37
+ loop,
38
+ width,
39
+ height,
40
+ }: VideoProps): React.ReactElement | null {
41
+ const videoRef = useRef<HTMLVideoElement>(null);
42
+
43
+ useEffect(() => {
44
+ if (videoRef.current) {
45
+ if (muted) {
46
+ videoRef.current.setAttribute('muted', '');
47
+ } else {
48
+ videoRef.current.removeAttribute('muted');
49
+ }
50
+ }
51
+ }, [muted]);
52
+
53
+ if (!src) {
54
+ console.warn('Video component requires a "src" prop');
55
+ return null;
56
+ }
57
+
58
+ // Append #t=0.1 to force browsers to decode and display a preview frame.
59
+ // Without it, Chrome/Safari often show a black rectangle with preload="metadata".
60
+ const srcWithPreview = src.includes('#') ? src : `${src}#t=0.1`;
61
+
62
+ // No <div> wrapper — markdown syntax can place this inside a <p>, and
63
+ // <p><div> is invalid HTML (causes hydration errors).
64
+ return (
65
+ <video
66
+ ref={videoRef}
67
+ data-testid="video-player"
68
+ src={srcWithPreview}
69
+ controls={controls}
70
+ autoPlay={autoPlay}
71
+ loop={loop}
72
+ width={width}
73
+ height={height}
74
+ preload="metadata"
75
+ playsInline
76
+ aria-label={title}
77
+ className="my-6 rounded-xl w-full border border-theme-border"
78
+ >
79
+ Your browser does not support the video tag.
80
+ </video>
81
+ );
82
+ }
@@ -16,7 +16,7 @@ import { LanguageSelector } from './LanguageSelector';
16
16
  import { resolveNavigation } from '@/lib/navigation-resolver';
17
17
  import { getIconClass } from '@/lib/icon-utils';
18
18
  import { useLinkPrefix } from '@/lib/link-prefix-context';
19
- import { useChatPanel } from '@/hooks/useChatPanel';
19
+ import { useChatOpenHandler } from '@/hooks/useChatPanel';
20
20
 
21
21
  interface HeaderProps {
22
22
  config: DocsConfig;
@@ -36,8 +36,7 @@ const MOBILE_ICON_BUTTON = 'lg:hidden p-2 text-[var(--color-text-muted)] hover:t
36
36
  const COMPACT_ICON_BUTTON = 'hidden md:flex p-2 hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg transition-colors cursor-pointer';
37
37
 
38
38
  export function Header({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isSidebarOpen, onToggleSidebar }: HeaderProps) {
39
- const { chatEnabled: chatContextEnabled, setIsChatOpen } = useChatPanel();
40
- const onChatOpen = chatContextEnabled ? () => setIsChatOpen(true) : undefined;
39
+ const { onChatOpen, devNotice } = useChatOpenHandler();
41
40
  const [isSearchOpen, setIsSearchOpen] = useState(false);
42
41
  const [logoError, setLogoError] = useState(false);
43
42
  const [isDark, setIsDark] = useState(false);
@@ -542,6 +541,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
542
541
  onClose={() => setIsSearchOpen(false)}
543
542
  popularPages={config.search?.popularPages}
544
543
  />
544
+ {devNotice}
545
545
  </>
546
546
  );
547
547
  }
@@ -30,7 +30,7 @@ import { ThemeToggle, ThemeToggleCycle } from '@/components/theme/ThemeToggle';
30
30
  import { getIconClass } from '@/lib/icon-utils';
31
31
  import { getTabsFromConfig } from '@/lib/navigation-utils';
32
32
  import { useLinkPrefix } from '@/lib/link-prefix-context';
33
- import { useChatPanel } from '@/hooks/useChatPanel';
33
+ import { useChatOpenHandler } from '@/hooks/useChatPanel';
34
34
 
35
35
  interface SidebarProps {
36
36
  config: DocsConfig;
@@ -150,8 +150,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
150
150
  const linkPrefix = useLinkPrefix();
151
151
 
152
152
  // Chat panel state — used for Pulsar sidebar AI button
153
- const { chatEnabled, setIsChatOpen } = useChatPanel();
154
- const onChatOpen = chatEnabled ? () => setIsChatOpen(true) : undefined;
153
+ const { onChatOpen, devNotice } = useChatOpenHandler();
155
154
 
156
155
  // Optimistic navigation: immediately highlight clicked link while page loads.
157
156
  // Strips hash fragments so anchor links (e.g. /page#section) match pathname.
@@ -750,6 +749,7 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
750
749
 
751
750
  {/* Search Modal - shown when search is opened from sidebar */}
752
751
  <SearchModal isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} onNavigate={handleNavigate} />
752
+ {devNotice}
753
753
  </>
754
754
  );
755
755
  }
@@ -279,7 +279,7 @@ export function CodePanel({
279
279
  {enableFullscreen && (
280
280
  <button
281
281
  onClick={() => setIsModalOpen(true)}
282
- className="p-1.5 rounded-md transition-colors flex-shrink-0"
282
+ className="p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer"
283
283
  style={{ color: codePanelColors.textMuted }}
284
284
  onMouseEnter={(e) => {
285
285
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -298,7 +298,7 @@ export function CodePanel({
298
298
  {/* Copy Button */}
299
299
  <button
300
300
  onClick={handleCopy}
301
- className={`p-1.5 rounded-md transition-colors flex-shrink-0 ${enableFullscreen ? 'ml-1' : ''}`}
301
+ className={`p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer ${enableFullscreen ? 'ml-1' : ''}`}
302
302
  style={{ color: codePanelColors.textMuted }}
303
303
  onMouseEnter={(e) => {
304
304
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -348,7 +348,7 @@ export function CodePanel({
348
348
  <button
349
349
  key={index}
350
350
  onClick={() => handleTabClick(index)}
351
- className="px-1 py-1 font-medium transition-colors whitespace-nowrap flex items-center"
351
+ className="px-1 py-1 font-medium transition-colors whitespace-nowrap flex items-center cursor-pointer"
352
352
  style={{
353
353
  color: isActive ? activeColor : codePanelColors.textMuted,
354
354
  cursor: 'pointer',
@@ -407,7 +407,7 @@ export function CodePanel({
407
407
  {enableFullscreen && (
408
408
  <button
409
409
  onClick={() => setIsModalOpen(true)}
410
- className="p-1.5 rounded-md transition-colors flex-shrink-0 ml-auto"
410
+ className="p-1.5 rounded-md transition-colors flex-shrink-0 ml-auto cursor-pointer"
411
411
  style={{ color: codePanelColors.textMuted }}
412
412
  onMouseEnter={(e) => {
413
413
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -426,7 +426,7 @@ export function CodePanel({
426
426
  {/* Fixed copy button */}
427
427
  <button
428
428
  onClick={handleCopy}
429
- className={`p-1.5 rounded-md transition-colors flex-shrink-0 ${enableFullscreen ? 'ml-1' : 'ml-auto'}`}
429
+ className={`p-1.5 rounded-md transition-colors flex-shrink-0 cursor-pointer ${enableFullscreen ? 'ml-1' : 'ml-auto'}`}
430
430
  style={{ color: codePanelColors.textMuted }}
431
431
  onMouseEnter={(e) => {
432
432
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -198,7 +198,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
198
198
  <button
199
199
  key={index}
200
200
  onClick={() => setActiveTab(index)}
201
- className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors"
201
+ className="px-3 py-1.5 rounded-md text-sm font-medium transition-colors cursor-pointer"
202
202
  style={{
203
203
  backgroundColor: activeTab === index ? codePanelColors.tabActiveBg : 'transparent',
204
204
  color: activeTab === index ? codePanelColors.text : codePanelColors.textMuted,
@@ -226,7 +226,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
226
226
  {/* Copy Button */}
227
227
  <button
228
228
  onClick={handleCopy}
229
- className="p-2 rounded-md transition-colors"
229
+ className="p-2 rounded-md transition-colors cursor-pointer"
230
230
  style={{ color: codePanelColors.textMuted }}
231
231
  onMouseEnter={(e) => {
232
232
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -249,7 +249,7 @@ export function CodePanelModal({ isOpen, onClose, tabs, title, initialTabIndex =
249
249
  {/* Close Button */}
250
250
  <button
251
251
  onClick={handleClose}
252
- className="p-2 rounded-md transition-colors"
252
+ className="p-2 rounded-md transition-colors cursor-pointer"
253
253
  style={{ color: codePanelColors.textMuted }}
254
254
  onMouseEnter={(e) => {
255
255
  e.currentTarget.style.backgroundColor = codePanelColors.tabHoverBg;
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * DevOnlyNotice
5
+ *
6
+ * A toast-like popup shown when users click features that are only available
7
+ * in production (e.g., API playground, AI chat). Appears briefly, then
8
+ * auto-dismisses. Used in local dev / CLI mode where the backend
9
+ * infrastructure (proxy endpoints, Upstash Vector, etc.) isn't available.
10
+ */
11
+
12
+ import { useState, useEffect, useCallback } from 'react';
13
+
14
+ /** Feature names for production-only features. */
15
+ export const DEV_FEATURE = {
16
+ AI_CHAT: 'AI Chat',
17
+ API_PLAYGROUND: 'API Playground',
18
+ } as const;
19
+
20
+ interface DevOnlyNoticeProps {
21
+ feature: string;
22
+ onDismiss: () => void;
23
+ }
24
+
25
+ function DevOnlyNotice({ feature, onDismiss }: DevOnlyNoticeProps) {
26
+ useEffect(() => {
27
+ const timer = setTimeout(onDismiss, 4000);
28
+ return () => clearTimeout(timer);
29
+ }, [onDismiss]);
30
+
31
+ return (
32
+ <div
33
+ role="alert"
34
+ style={{
35
+ position: 'fixed',
36
+ bottom: '1.5rem',
37
+ left: '50%',
38
+ transform: 'translateX(-50%)',
39
+ zIndex: 1000010,
40
+ background: 'var(--color-bg-primary, #fff)',
41
+ border: '1px solid var(--color-border, #e2e8f0)',
42
+ borderRadius: '8px',
43
+ padding: '0.75rem 1.25rem',
44
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
45
+ fontSize: '0.8125rem',
46
+ color: 'var(--color-text-primary, #1e293b)',
47
+ maxWidth: '400px',
48
+ textAlign: 'center',
49
+ animation: 'devNoticeIn 0.2s ease-out',
50
+ }}
51
+ >
52
+ <div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>
53
+ {feature} is available in production
54
+ </div>
55
+ <div style={{ color: 'var(--color-text-muted, #64748b)', fontSize: '0.75rem' }}>
56
+ Deploy your docs to see this feature live.
57
+ </div>
58
+ <style>{`@keyframes devNoticeIn { from { opacity: 0; transform: translateX(-50%) translateY(8px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }`}</style>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Hook that returns a trigger function and the notice element.
65
+ * Call `showNotice()` to display the toast for a given feature.
66
+ */
67
+ export function useDevOnlyNotice() {
68
+ const [activeFeature, setActiveFeature] = useState<string | null>(null);
69
+ const dismiss = useCallback(() => setActiveFeature(null), []);
70
+
71
+ const showNotice = setActiveFeature;
72
+
73
+ const notice = activeFeature ? (
74
+ <DevOnlyNotice feature={activeFeature} onDismiss={dismiss} />
75
+ ) : null;
76
+
77
+ return { showNotice, notice };
78
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
4
+ import { useDevOnlyNotice, DEV_FEATURE } from '@/components/ui/DevOnlyNotice';
4
5
 
5
6
  const MIN_WIDTH = 350;
6
7
  const MAX_WIDTH = 500;
@@ -61,8 +62,9 @@ export function ChatPanelProvider({ children, chatEnabled, chatEndpoint, starter
61
62
 
62
63
  // Keyboard shortcuts: Cmd+I / Ctrl+I to toggle, Escape to close
63
64
  // Uses capture phase for Escape so it fires before SearchModal's bubble-phase handler
65
+ const isDevMode = process.env.NODE_ENV !== 'production';
64
66
  useEffect(() => {
65
- if (!chatEnabled) return;
67
+ if (!chatEnabled || isDevMode) return;
66
68
 
67
69
  const handler = (e: KeyboardEvent) => {
68
70
  // Cmd+I / Ctrl+I — toggle chat
@@ -81,7 +83,7 @@ export function ChatPanelProvider({ children, chatEnabled, chatEndpoint, starter
81
83
  };
82
84
  document.addEventListener('keydown', handler, true); // capture phase
83
85
  return () => document.removeEventListener('keydown', handler, true);
84
- }, [chatEnabled, isChatOpen]);
86
+ }, [chatEnabled, isChatOpen, isDevMode]);
85
87
 
86
88
  return (
87
89
  <ChatPanelContext.Provider value={{ isChatOpen, setIsChatOpen, chatWidth, setChatWidth, chatEnabled, chatEndpoint, starterQuestions }}>
@@ -99,3 +101,20 @@ export function useChatPanel(): ChatPanelContextValue {
99
101
  }
100
102
 
101
103
  export { MIN_WIDTH, MAX_WIDTH, DEFAULT_WIDTH };
104
+
105
+ /**
106
+ * Returns a chat-open handler that shows DevOnlyNotice in dev mode
107
+ * instead of opening the panel. Used by Header and Sidebar.
108
+ */
109
+ export function useChatOpenHandler() {
110
+ const { chatEnabled, setIsChatOpen } = useChatPanel();
111
+ const { showNotice, notice } = useDevOnlyNotice();
112
+ const isDevMode = process.env.NODE_ENV !== 'production';
113
+
114
+ const onChatOpen = chatEnabled ? () => {
115
+ if (isDevMode) { showNotice(DEV_FEATURE.AI_CHAT); return; }
116
+ setIsChatOpen(true);
117
+ } : undefined;
118
+
119
+ return { onChatOpen, devNotice: notice };
120
+ }
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useSyncExternalStore } from 'react';
4
+
5
+ /**
6
+ * SSR-safe media query hook using useSyncExternalStore.
7
+ *
8
+ * Returns true when the viewport matches the given CSS media query string,
9
+ * false otherwise. Returns false on the server (getServerSnapshot).
10
+ */
11
+ export function useMediaQuery(query: string): boolean {
12
+ const subscribe = useCallback((callback: () => void) => {
13
+ if (typeof window === 'undefined') return () => {};
14
+ const mql = window.matchMedia(query);
15
+ mql.addEventListener('change', callback);
16
+ return () => mql.removeEventListener('change', callback);
17
+ }, [query]);
18
+
19
+ const getSnapshot = useCallback(() => {
20
+ if (typeof window === 'undefined') return false;
21
+ return window.matchMedia(query).matches;
22
+ }, [query]);
23
+
24
+ const getServerSnapshot = useCallback(() => false, []);
25
+
26
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
27
+ }
@@ -0,0 +1,66 @@
1
+ import type { OpenApiEndpointData, ParsedParameter, ParsedRequestBody } from './openapi/types';
2
+ import type { ExtractedParam } from './remark-extract-param-fields';
3
+
4
+ /**
5
+ * Build an OpenApiEndpointData from extracted ParamField data and api: frontmatter.
6
+ *
7
+ * Bridges `api:` pages (MDX-defined endpoints) into the same data structure
8
+ * that the playground consumes for `openapi:` pages.
9
+ *
10
+ * Known limitation: only top-level <ParamField> components are supported.
11
+ * Nested ParamFields (inside <Expandable> etc.) are extracted as flat fields.
12
+ */
13
+ export function buildEndpointFromMdx(
14
+ method: string,
15
+ path: string,
16
+ params: ExtractedParam[],
17
+ serverUrl?: string,
18
+ ): OpenApiEndpointData {
19
+ const bodyParams = params.filter(p => p.in === 'body');
20
+ const nonBodyParams = params.filter(p => p.in !== 'body');
21
+
22
+ const parameters: ParsedParameter[] = nonBodyParams.map(p => ({
23
+ name: p.name,
24
+ in: p.in as 'path' | 'query' | 'header' | 'cookie',
25
+ required: p.required,
26
+ schema: {
27
+ type: p.type,
28
+ ...(p.default !== undefined ? { default: p.default } : {}),
29
+ },
30
+ }));
31
+
32
+ let requestBody: ParsedRequestBody | undefined;
33
+ if (bodyParams.length > 0) {
34
+ const properties: Record<string, { type?: string }> = {};
35
+ const required: string[] = [];
36
+ for (const p of bodyParams) {
37
+ properties[p.name] = {
38
+ type: p.type,
39
+ ...(p.default !== undefined ? { default: p.default } : {}),
40
+ };
41
+ if (p.required) required.push(p.name);
42
+ }
43
+ requestBody = {
44
+ required: required.length > 0,
45
+ content: {
46
+ 'application/json': {
47
+ schema: {
48
+ type: 'object',
49
+ properties,
50
+ ...(required.length > 0 ? { required } : {}),
51
+ },
52
+ },
53
+ },
54
+ };
55
+ }
56
+
57
+ return {
58
+ method: method as OpenApiEndpointData['method'],
59
+ path,
60
+ parameters,
61
+ requestBody,
62
+ responses: {},
63
+ servers: serverUrl ? [{ url: serverUrl }] : [],
64
+ security: [],
65
+ };
66
+ }
@@ -62,7 +62,7 @@ export async function collectMdxFiles(
62
62
  * @returns Array of asset file paths relative to projectDir
63
63
  */
64
64
  export async function collectAssetFiles(projectDir: string): Promise<string[]> {
65
- const files = await glob('**/*.{png,jpg,jpeg,gif,svg,webp,ico}', {
65
+ const files = await glob('**/*.{png,jpg,jpeg,gif,svg,webp,ico,mp4,webm}', {
66
66
  cwd: projectDir,
67
67
  ignore: ['node_modules/**', '.git/**'],
68
68
  });
@@ -491,7 +491,7 @@ export function buildProjectHeaders(
491
491
  /**
492
492
  * Asset extensions that need to be routed to the assets API.
493
493
  */
494
- const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.pdf'];
494
+ const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.pdf', '.mp4', '.webm'];
495
495
 
496
496
  /**
497
497
  * Project-specific files that need to be fetched from R2 via assets API.
@@ -522,6 +522,11 @@ export function isAssetRequest(pathname: string): boolean {
522
522
  return true;
523
523
  }
524
524
 
525
+ // Project videos under /_jd/videos/ are served from R2
526
+ if (pathname.startsWith(`${ASSET_PREFIX}/videos/`)) {
527
+ return true;
528
+ }
529
+
525
530
  // Project-specific files from R2, or files with asset extensions
526
531
  return (
527
532
  PROJECT_SPECIFIC_FILES.includes(pathname) ||