From 8b02a26d472a2fbf622542cac4d601dfddb809c4 Mon Sep 17 00:00:00 2001 From: Sherry-hue <37186915+Sherry-hue@users.noreply.github.com> Date: Thu, 28 May 2026 15:34:53 +0800 Subject: [PATCH] feat(a2ui): publish preview payloads --- .../genui/a2ui-playground/examples/README.md | 36 ++ .../genui/a2ui-playground/rsbuild.config.ts | 7 + .../src/components/PreviewPanel.tsx | 159 +++++- .../src/hooks/useConversation.ts | 44 +- .../a2ui-playground/src/pages/AIChatPage.tsx | 73 ++- .../a2ui-playground/src/pages/DemosPage.tsx | 94 +++- packages/genui/a2ui-playground/src/render.tsx | 94 +++- .../a2ui-playground/src/storage/types.ts | 7 + packages/genui/a2ui-playground/src/styles.css | 5 +- .../a2ui-playground/src/utils/renderUrl.ts | 45 ++ .../server/app/a2ui/action/stream/route.ts | 7 + .../server/app/a2ui/payload-publisher.ts | 114 ++++ .../genui/server/app/a2ui/payload/route.ts | 70 +++ .../genui/server/app/a2ui/stream/route.ts | 7 + packages/genui/server/package.json | 1 + pnpm-lock.yaml | 517 ++++++++++++++++++ 16 files changed, 1238 insertions(+), 42 deletions(-) create mode 100644 packages/genui/server/app/a2ui/payload-publisher.ts create mode 100644 packages/genui/server/app/a2ui/payload/route.ts diff --git a/packages/genui/a2ui-playground/examples/README.md b/packages/genui/a2ui-playground/examples/README.md index 49a07c5d7e..d2a5a01834 100644 --- a/packages/genui/a2ui-playground/examples/README.md +++ b/packages/genui/a2ui-playground/examples/README.md @@ -25,6 +25,42 @@ agent.start(); // streams initial messages into the buffer agent.onAction(action); // pushes the canned response to a user action ``` +## Supabase Storage payload publishing + +The A2UI server keeps AI-generated preview URLs short by uploading final +validated `messages` to Supabase Storage before emitting the `done` SSE event. +The playground still receives the full `messages` for immediate rendering, and +uses `done.preview.messagesUrl` for Web Preview and Native Preview links. + +To test this locally, create a public bucket for preview payloads and start the +server with Supabase S3 credentials: + +```bash +SUPABASE_URL=https://koaijebcyqjpnvxajqhe.supabase.co \ +SUPABASE_S3_ACCESS_KEY_ID= \ +SUPABASE_S3_SECRET_ACCESS_KEY= \ +SUPABASE_STORAGE_BUCKET=genui \ +pnpm dev +``` + +`SUPABASE_STORAGE_BUCKET` defaults to `genui`, and +`SUPABASE_STORAGE_PREFIX` defaults to `a2ui`. +`SUPABASE_STORAGE_REGION` defaults to `us-east-1`. The server writes through +Supabase's S3-compatible Storage endpoint: + +```text +a2ui//messages.json +``` + +Those objects must be in a public bucket and CORS-readable by the preview +runtime. + +In local playground development, generated preview links use the playground +dev server's in-memory payload store by default. Set +`A2UI_PLAYGROUND_CLIENT_PAYLOAD_PUBLISH=0` when you want local development to +exercise the server-side Supabase upload path instead. Production builds do +not enable the dev-server payload store. + ## Multi-turn chat shell pattern For chat UIs, give each turn (user prompt + agent response) its own diff --git a/packages/genui/a2ui-playground/rsbuild.config.ts b/packages/genui/a2ui-playground/rsbuild.config.ts index da82105ec5..25bbaf3e77 100644 --- a/packages/genui/a2ui-playground/rsbuild.config.ts +++ b/packages/genui/a2ui-playground/rsbuild.config.ts @@ -10,6 +10,8 @@ import type { RsbuildPlugin } from '@rsbuild/core'; import { pluginReact } from '@rsbuild/plugin-react'; const PORT = Number(process.env.PORT ?? 3000); +const CLIENT_PAYLOAD_STORE_ENABLED = process.env.NODE_ENV !== 'production' + && process.env.A2UI_PLAYGROUND_CLIENT_PAYLOAD_PUBLISH !== '0'; // In-memory A2UI payload store. Keeps the dev-bundle / render URLs short // enough to fit inside a scannable QR code. @@ -166,6 +168,11 @@ function buildRspeedyBundleUrl(port: number): string { export default defineConfig({ plugins: [pluginReact(), a2uiPayloadPlugin], source: { + define: { + __A2UI_PLAYGROUND_CLIENT_PAYLOAD_STORE__: JSON.stringify( + CLIENT_PAYLOAD_STORE_ENABLED, + ), + }, entry: { index: './src/entry.tsx', render: './src/render.tsx', diff --git a/packages/genui/a2ui-playground/src/components/PreviewPanel.tsx b/packages/genui/a2ui-playground/src/components/PreviewPanel.tsx index c10fd9d003..b33fe18af9 100644 --- a/packages/genui/a2ui-playground/src/components/PreviewPanel.tsx +++ b/packages/genui/a2ui-playground/src/components/PreviewPanel.tsx @@ -21,6 +21,8 @@ import { DEFAULT_A2UI_DEMO_URL } from '../utils/demoUrl.js'; import type { Protocol } from '../utils/protocol.js'; import { buildRenderUrl } from '../utils/renderUrl.js'; +declare const __A2UI_PLAYGROUND_CLIENT_PAYLOAD_STORE__: boolean; + export type PreviewMode = 'phone' | 'full'; export interface PreviewPanelPreviewModeContextValue { @@ -58,7 +60,9 @@ interface A2UIPreviewSource { demoUrl: string; theme: 'light' | 'dark'; messages: unknown; + messagesUrl?: string; actionMocks?: Record; + actionMocksUrl?: string; demoId?: string; /** * When true, build the render URL in playback mode so the Lynx app waits @@ -158,13 +162,6 @@ function useRspeedyDevUrl(): string { return url; } -function formatUrlForDisplay(url: string): string { - if (url.length <= 80) return url; - const head = url.slice(0, 44); - const tail = url.slice(-24); - return `${head}…${tail}`; -} - function buildOpenUIRenderUrl( rawText: string, baseUrl: string, @@ -180,6 +177,18 @@ function buildOpenUIRenderUrl( return url.toString(); } +function absoluteUrl(url: string, origin: string): string { + try { + return new URL(url, origin).toString(); + } catch { + return url; + } +} + +function shouldUseClientPayloadStore(): boolean { + return __A2UI_PLAYGROUND_CLIENT_PAYLOAD_STORE__; +} + export function PreviewPanel(props: PreviewPanelProps) { const { afterBody, @@ -306,11 +315,27 @@ export function PreviewPanel(props: PreviewPanelProps) { } if (previewSource.kind === 'a2ui') { + const useClientPayloadStore = shouldUseClientPayloadStore(); + const canSharePayload = !!previewSource.demoId + || !!previewSource.messagesUrl + || useClientPayloadStore; + const hasInlineMessages = Array.isArray(previewSource.messages) + ? previewSource.messages.length > 0 + : previewSource.messages !== undefined; + if (!canSharePayload && !hasInlineMessages) { + setRenderUrl(''); + setRenderShareUrl(''); + setLynxDevUrl(''); + return; + } + const url = buildRenderUrl( { protocol: previewSource.protocol, demoUrl: previewSource.demoUrl ?? DEFAULT_A2UI_DEMO_URL, + messagesUrl: previewSource.messagesUrl, messages: previewSource.messages, + actionMocksUrl: previewSource.actionMocksUrl, actionMocks: previewSource.actionMocks, theme: previewSource.theme, demoId: previewSource.demoId, @@ -325,7 +350,9 @@ export function PreviewPanel(props: PreviewPanelProps) { { protocol: previewSource.protocol, demoUrl: previewSource.demoUrl ?? DEFAULT_A2UI_DEMO_URL, + messagesUrl: previewSource.messagesUrl, messages: previewSource.messages, + actionMocksUrl: previewSource.actionMocksUrl, actionMocks: previewSource.actionMocks, theme: previewSource.theme, demoId: previewSource.demoId, @@ -334,7 +361,12 @@ export function PreviewPanel(props: PreviewPanelProps) { shareBaseUrl, ); setRenderUrl(url); - setRenderShareUrl(shareUrl); + setRenderShareUrl(canSharePayload ? shareUrl : ''); + + if (!canSharePayload) { + setLynxDevUrl(''); + return; + } if (!rspeedyDevUrl) { setLynxDevUrl(''); @@ -354,6 +386,22 @@ export function PreviewPanel(props: PreviewPanelProps) { 'messagesUrl', new URL(`demos/${previewSource.demoId}.json`, demosBase).toString(), ); + } else if (previewSource.messagesUrl) { + uInline.searchParams.set('messagesUrl', previewSource.messagesUrl); + uInline.searchParams.delete('messages'); + if (previewSource.actionMocksUrl) { + uInline.searchParams.set( + 'actionMocksUrl', + previewSource.actionMocksUrl, + ); + uInline.searchParams.delete('actionMocks'); + } else if (previewSource.actionMocks) { + uInline.searchParams.set( + 'actionMocks', + JSON.stringify(previewSource.actionMocks), + ); + uInline.searchParams.delete('actionMocksUrl'); + } } else { uInline.searchParams.set( 'messages', @@ -367,6 +415,93 @@ export function PreviewPanel(props: PreviewPanelProps) { } } setLynxDevUrl(uInline.toString()); + + if ( + previewSource.demoId + || previewSource.messagesUrl + || !useClientPayloadStore + ) { + return; + } + + void (async () => { + try { + const payloadOrigin = new URL(baseUrl).origin; + const res = await window.fetch(`${payloadOrigin}/__a2ui_payload`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: previewSource.messages, + actionMocks: previewSource.actionMocks, + }), + }); + if (!res.ok) return; + const data = (await res.json()) as { + messagesUrl?: string; + actionMocksUrl?: string; + }; + + if (seq !== buildSeqRef.current) return; + if (typeof data.messagesUrl !== 'string') return; + + const messagesUrl = absoluteUrl(data.messagesUrl, payloadOrigin); + const actionMocksUrl = typeof data.actionMocksUrl === 'string' + ? absoluteUrl(data.actionMocksUrl, payloadOrigin) + : undefined; + + const shortUrl = buildRenderUrl( + { + protocol: previewSource.protocol, + demoUrl: previewSource.demoUrl ?? DEFAULT_A2UI_DEMO_URL, + messagesUrl, + messages: previewSource.messages, + actionMocksUrl, + theme: previewSource.theme, + speed, + playbackMode: previewSource.playbackMode, + }, + baseUrl, + ); + const shortShareUrl = buildRenderUrl( + { + protocol: previewSource.protocol, + demoUrl: previewSource.demoUrl ?? DEFAULT_A2UI_DEMO_URL, + messagesUrl, + messages: previewSource.messages, + actionMocksUrl, + theme: previewSource.theme, + speed, + }, + shareBaseUrl, + ); + setRenderUrl(shortUrl); + setRenderShareUrl(shortShareUrl); + + const u = new URL(rspeedyDevUrl); + if (speed !== 1) { + u.searchParams.set('speed', String(speed)); + } + u.searchParams.set('theme', previewSource.theme); + u.searchParams.set('messagesUrl', messagesUrl); + u.searchParams.delete('messages'); + if (actionMocksUrl) { + u.searchParams.set('actionMocksUrl', actionMocksUrl); + u.searchParams.delete('actionMocks'); + } else if (previewSource.actionMocks) { + u.searchParams.set( + 'actionMocks', + JSON.stringify(previewSource.actionMocks), + ); + u.searchParams.delete('actionMocksUrl'); + } else { + u.searchParams.delete('actionMocksUrl'); + u.searchParams.delete('actionMocks'); + } + setLynxDevUrl(u.toString()); + } catch { + // Keep the inline URLs above if the local dev payload store is unavailable. + } + })(); return; } @@ -460,7 +595,9 @@ export function PreviewPanel(props: PreviewPanelProps) { return []; } - const showQrCode = previewSource.kind !== 'a2ui' || !!previewSource.demoId; + const showQrCode = previewSource.kind !== 'a2ui' + || !!previewSource.demoId + || !!previewSource.messagesUrl; const cards: Array<{ key: string; item: PreviewQrItem }> = []; if (renderShareUrl) { @@ -470,7 +607,7 @@ export function PreviewPanel(props: PreviewPanelProps) { title: 'Web Preview', description: 'Opens in any mobile browser via Lynx for Web.', url: renderShareUrl, - urlTitle: formatUrlForDisplay(renderShareUrl), + urlTitle: renderShareUrl, copyButtonTitle: 'Copy render URL', showQrCode, }, @@ -483,7 +620,7 @@ export function PreviewPanel(props: PreviewPanelProps) { title: 'Native Preview', description: 'Opens in LynxExplorer for native rendering.', url: lynxDevUrl, - urlTitle: formatUrlForDisplay(lynxDevUrl), + urlTitle: lynxDevUrl, copyButtonTitle: 'Copy Lynx dev bundle URL', variant: 'alt', showQrCode, diff --git a/packages/genui/a2ui-playground/src/hooks/useConversation.ts b/packages/genui/a2ui-playground/src/hooks/useConversation.ts index df805557e2..5674e5f108 100644 --- a/packages/genui/a2ui-playground/src/hooks/useConversation.ts +++ b/packages/genui/a2ui-playground/src/hooks/useConversation.ts @@ -18,11 +18,13 @@ import type { ConversationMeta, DataModelSnapshot, PersistedMessage, + PreviewPayloadUrls, } from '../storage/types.js'; export interface ModelChatMessage { role: 'user' | 'assistant' | 'system'; content: string; + previewPayloadUrls?: PreviewPayloadUrls; } export interface ConversationContext { @@ -35,6 +37,7 @@ interface ConversationHotState { dataModel: Record; surfaceIds: Set; previewMessages: unknown[]; + previewPayloadUrls: PreviewPayloadUrls | null; } export interface RecordTurnInput { @@ -42,6 +45,8 @@ export interface RecordTurnInput { assistantContent: string; a2uiMessages: unknown[]; previewMessages?: unknown[]; + previewPayloadUrls?: PreviewPayloadUrls | null; + snapshotPreviewPayloadUrls?: PreviewPayloadUrls | null; } export interface UseConversationReturn { @@ -51,6 +56,7 @@ export interface UseConversationReturn { dataModel: Record; surfaceIds: ReadonlySet; previewMessages: unknown[]; + previewPayloadUrls: PreviewPayloadUrls | null; isReady: boolean; isPersistent: boolean; switchTo: (id: string) => Promise; @@ -186,6 +192,7 @@ function toPersistedMessages( seq: index, role: message.role, content: message.content, + previewPayloadUrls: message.previewPayloadUrls, createdAt: now + index, })); } @@ -196,6 +203,7 @@ function fromPersistedMessages( return messages.map((message) => ({ role: message.role, content: message.content, + previewPayloadUrls: message.previewPayloadUrls, })); } @@ -205,6 +213,7 @@ function createEmptyHotState(): ConversationHotState { dataModel: {}, surfaceIds: new Set(), previewMessages: [], + previewPayloadUrls: null, }; } @@ -214,6 +223,9 @@ function cloneHotState(state: ConversationHotState): ConversationHotState { dataModel: cloneDataModel(state.dataModel), surfaceIds: new Set(state.surfaceIds), previewMessages: state.previewMessages.slice(), + previewPayloadUrls: state.previewPayloadUrls + ? { ...state.previewPayloadUrls } + : null, }; } @@ -226,6 +238,9 @@ export function useConversation(): UseConversationReturn { () => new Set(), ); const [previewMessages, setPreviewMessages] = useState([]); + const [previewPayloadUrls, setPreviewPayloadUrls] = useState< + PreviewPayloadUrls | null + >(null); const [isReady, setIsReady] = useState(false); const [isPersistent, setIsPersistent] = useState(true); @@ -233,6 +248,7 @@ export function useConversation(): UseConversationReturn { const dataModelRef = useRef>({}); const surfaceIdsRef = useRef>(new Set()); const previewMessagesRef = useRef([]); + const previewPayloadUrlsRef = useRef(null); const activeIdRef = useRef(null); const activationTokenRef = useRef(null); const conversationHotStateMapRef = useRef>( @@ -246,15 +262,18 @@ export function useConversation(): UseConversationReturn { nextDataModel: Record, nextSurfaceIds: Set, nextPreviewMessages: unknown[], + nextPreviewPayloadUrls: PreviewPayloadUrls | null, ) => { messagesRef.current = nextMessages; dataModelRef.current = nextDataModel; surfaceIdsRef.current = nextSurfaceIds; previewMessagesRef.current = nextPreviewMessages; + previewPayloadUrlsRef.current = nextPreviewPayloadUrls; setMessages(nextMessages); setDataModel(nextDataModel); setSurfaceIds(new Set(nextSurfaceIds)); setPreviewMessages(nextPreviewMessages); + setPreviewPayloadUrls(nextPreviewPayloadUrls); const id = activeIdRef.current; if (!persistentRef.current && id) { conversationHotStateMapRef.current.set(id, { @@ -262,6 +281,9 @@ export function useConversation(): UseConversationReturn { dataModel: cloneDataModel(nextDataModel), surfaceIds: new Set(nextSurfaceIds), previewMessages: nextPreviewMessages.slice(), + previewPayloadUrls: nextPreviewPayloadUrls + ? { ...nextPreviewPayloadUrls } + : null, }); } }, @@ -284,6 +306,7 @@ export function useConversation(): UseConversationReturn { cloneDataModel(record.snapshot?.dataModel ?? {}), new Set(record.snapshot?.surfaceIds ?? []), record.snapshot?.previewMessages ?? [], + record.snapshot?.previewPayloadUrls ?? null, ); return true; }, @@ -327,7 +350,7 @@ export function useConversation(): UseConversationReturn { activeIdRef.current = meta.id; conversationHotStateMapRef.current.set(meta.id, createEmptyHotState()); setActiveId(meta.id); - syncHotState([], {}, new Set(), []); + syncHotState([], {}, new Set(), [], null); } finally { if (!cancelled) setIsReady(true); } @@ -352,6 +375,7 @@ export function useConversation(): UseConversationReturn { hotState.dataModel, hotState.surfaceIds, hotState.previewMessages, + hotState.previewPayloadUrls, ); return; } @@ -374,6 +398,7 @@ export function useConversation(): UseConversationReturn { hotState.dataModel, hotState.surfaceIds, hotState.previewMessages, + hotState.previewPayloadUrls, ); return meta.id; } @@ -384,7 +409,7 @@ export function useConversation(): UseConversationReturn { activationTokenRef.current = null; activeIdRef.current = meta.id; setActiveId(meta.id); - syncHotState([], {}, new Set(), []); + syncHotState([], {}, new Set(), [], null); return meta.id; }, [syncHotState]); @@ -433,7 +458,11 @@ export function useConversation(): UseConversationReturn { const nextMessages = [ ...messagesRef.current, input.userMessage, - { role: 'assistant' as const, content: input.assistantContent }, + { + role: 'assistant' as const, + content: input.assistantContent, + previewPayloadUrls: input.previewPayloadUrls ?? undefined, + }, ]; const nextDataModel = cloneDataModel(dataModelRef.current); const nextSurfaceIds = new Set(surfaceIdsRef.current); @@ -443,6 +472,9 @@ export function useConversation(): UseConversationReturn { input.a2uiMessages, ); const nextPreviewMessages = input.previewMessages ?? input.a2uiMessages; + const nextPreviewPayloadUrls = input.snapshotPreviewPayloadUrls + ?? input.previewPayloadUrls + ?? null; const now = Date.now(); const existingMeta = conversations.find((item) => item.id === id) @@ -462,6 +494,7 @@ export function useConversation(): UseConversationReturn { dataModel: nextDataModel, surfaceIds: [...nextSurfaceIds], previewMessages: nextPreviewMessages, + previewPayloadUrls: nextPreviewPayloadUrls ?? undefined, updatedAt: now, }; @@ -470,6 +503,7 @@ export function useConversation(): UseConversationReturn { nextDataModel, nextSurfaceIds, nextPreviewMessages, + nextPreviewPayloadUrls, ); setConversations((prev) => { const without = prev.filter((item) => item.id !== id); @@ -495,6 +529,9 @@ export function useConversation(): UseConversationReturn { dataModel: cloneDataModel(nextDataModel), surfaceIds: new Set(nextSurfaceIds), previewMessages: nextPreviewMessages.slice(), + previewPayloadUrls: nextPreviewPayloadUrls + ? { ...nextPreviewPayloadUrls } + : null, }); } } @@ -517,6 +554,7 @@ export function useConversation(): UseConversationReturn { dataModel, surfaceIds, previewMessages, + previewPayloadUrls, isReady, isPersistent, switchTo, diff --git a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx index 8bbd3e7c62..4ca2480166 100644 --- a/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/AIChatPage.tsx @@ -44,6 +44,10 @@ interface A2UIDonePayload { error?: unknown; message?: unknown; usage?: unknown; + preview?: { + messagesUrl?: unknown; + actionMocksUrl?: unknown; + }; } interface TokenUsage { @@ -56,6 +60,11 @@ interface A2UIResponseMessageMeta { final: boolean; } +interface PreviewPayloadUrls { + messagesUrl: string; + actionMocksUrl?: string; +} + interface ProviderSettings { preset: ProviderPresetId; apiKey: string; @@ -467,6 +476,21 @@ function normalizeA2UIMessages(payload: unknown): unknown[] { return []; } +function normalizePreviewPayloadUrls( + payload: unknown, +): PreviewPayloadUrls | null { + if (!payload || typeof payload !== 'object') return null; + const preview = (payload as A2UIDonePayload).preview; + if (!preview || typeof preview !== 'object') return null; + if (typeof preview.messagesUrl !== 'string') return null; + return { + messagesUrl: preview.messagesUrl, + actionMocksUrl: typeof preview.actionMocksUrl === 'string' + ? preview.actionMocksUrl + : undefined, + }; +} + function includesCreateSurface(messages: unknown[]): boolean { return messages.some((message) => Boolean( @@ -551,6 +575,7 @@ async function readA2UIResponse( parseDeltaMessages?: boolean; publishPartialMessages?: boolean; publishText?: boolean; + onPreviewPayload?: (preview: PreviewPayloadUrls) => void; } = {}, ): Promise { const contentType = response.headers.get('content-type') ?? ''; @@ -563,6 +588,8 @@ async function readA2UIResponse( if (payload && typeof payload === 'object') { const usage = parseUsage((payload as A2UIDonePayload).usage); if (usage) onUsage?.(usage); + const preview = normalizePreviewPayloadUrls(payload); + if (preview) options.onPreviewPayload?.(preview); } onMessages(messages, { final: true }); return messages; @@ -619,6 +646,8 @@ async function readA2UIResponse( if (parsed.data && typeof parsed.data === 'object') { const usage = parseUsage((parsed.data as A2UIDonePayload).usage); if (usage) onUsage?.(usage); + const preview = normalizePreviewPayloadUrls(parsed.data); + if (preview) options.onPreviewPayload?.(preview); } if (doneMessages.length > 0) { latestMessages = doneMessages; @@ -804,6 +833,7 @@ export function AIChatPage( isReady, messages: persistedMessages, previewMessages: persistedPreviewMessages, + previewPayloadUrls: persistedPreviewPayloadUrls, recordTurn, remove, rename, @@ -818,6 +848,9 @@ export function AIChatPage( const [previewMessages, setPreviewMessages] = useState( null, ); + const [previewPayloadUrls, setPreviewPayloadUrls] = useState< + PreviewPayloadUrls | null + >(null); const [isGenerating, setIsGenerating] = useState(false); const [deleteConversationId, setDeleteConversationId] = useState< string | null @@ -836,6 +869,7 @@ export function AIChatPage( const actionAbortRef = useRef(null); const hydratedActiveIdRef = useRef(null); const latestPreviewMessagesRef = useRef([]); + const latestPreviewPayloadUrlsRef = useRef(null); const renderUrlRef = useRef(''); const { containerRef: pageRef, @@ -861,6 +895,14 @@ export function AIChatPage( [showCopyToast], ); + const updatePreviewPayloadUrls = useCallback( + (next: PreviewPayloadUrls | null) => { + latestPreviewPayloadUrlsRef.current = next; + setPreviewPayloadUrls(next); + }, + [], + ); + const providerRequestOptions = useMemo( () => toProviderRequestOptions(providerSettings), [providerSettings], @@ -967,8 +1009,10 @@ export function AIChatPage( demoUrl: DEFAULT_A2UI_DEMO_URL, theme, messages: previewMessages, + messagesUrl: previewPayloadUrls?.messagesUrl, + actionMocksUrl: previewPayloadUrls?.actionMocksUrl, }; - }, [previewMessages, protocol, theme]); + }, [previewMessages, previewPayloadUrls, protocol, theme]); const publishPreviewMessages = useCallback( (nextMessages: unknown[]) => { @@ -1011,6 +1055,7 @@ export function AIChatPage( ]; latestPreviewMessagesRef.current = accumulatedMessages; setPreviewMessages(accumulatedMessages); + updatePreviewPayloadUrls(null); const initData = { protocol, @@ -1034,7 +1079,7 @@ export function AIChatPage( return buildRenderUrl(initData, baseUrl); }); }, - [baseUrl, protocol, theme], + [baseUrl, protocol, theme, updatePreviewPayloadUrls], ); const handlePreviewLoad = useCallback(() => { @@ -1061,11 +1106,13 @@ export function AIChatPage( completionTokens: 0, totalTokens: 0, }); + updatePreviewPayloadUrls(persistedPreviewPayloadUrls); if (replayMessages.length > 0) { publishPreviewMessages(replayMessages); } else { latestPreviewMessagesRef.current = []; setPreviewMessages(null); + updatePreviewPayloadUrls(null); setRenderUrl(''); } }, [ @@ -1074,8 +1121,10 @@ export function AIChatPage( isGenerating, persistedMessages, persistedPreviewMessages, + persistedPreviewPayloadUrls, publishPreviewMessages, renderUrl, + updatePreviewPayloadUrls, ]); useEffect(() => { @@ -1104,6 +1153,7 @@ export function AIChatPage( ]); setInputValue(''); setPreviewMessages(null); + updatePreviewPayloadUrls(null); latestPreviewMessagesRef.current = []; setTokenUsage({ promptTokens: 0, @@ -1176,6 +1226,7 @@ export function AIChatPage( { parseDeltaMessages: false, publishText: false, + onPreviewPayload: updatePreviewPayloadUrls, }, ); @@ -1188,6 +1239,7 @@ export function AIChatPage( assistantContent: JSON.stringify(finalMessages), a2uiMessages: finalMessages, previewMessages: finalMessages, + previewPayloadUrls: latestPreviewPayloadUrlsRef.current, }); setMessages((prev) => { const next = prev.slice(); @@ -1229,6 +1281,7 @@ export function AIChatPage( publishStreamingPreviewMessages, providerRequestOptions, recordTurn, + updatePreviewPayloadUrls, ]); useEffect(() => { @@ -1331,6 +1384,7 @@ export function AIChatPage( } let responseMessages: unknown[] = []; + let responsePreviewPayloadUrls: PreviewPayloadUrls | null = null; await readA2UIResponse( response, @@ -1398,6 +1452,11 @@ export function AIChatPage( totalTokens: prev.totalTokens + usage.totalTokens, })); }, + { + onPreviewPayload: (preview) => { + responsePreviewPayloadUrls = preview; + }, + }, ); if (signal.aborted) return; @@ -1413,11 +1472,14 @@ export function AIChatPage( ]; latestPreviewMessagesRef.current = replayMessages; setPreviewMessages(replayMessages); + updatePreviewPayloadUrls(null); await recordTurn({ userMessage: userActionMessage, assistantContent: JSON.stringify(responseMessages), a2uiMessages: responseMessages, previewMessages: replayMessages, + previewPayloadUrls: responsePreviewPayloadUrls, + snapshotPreviewPayloadUrls: null, }); setMessages((prev) => { const next = prev.slice(); @@ -1481,7 +1543,12 @@ export function AIChatPage( actionAbortRef.current?.abort(); actionAbortRef.current = null; }; - }, [buildConversationContext, publishPreviewMessages, recordTurn]); + }, [ + buildConversationContext, + publishPreviewMessages, + recordTurn, + updatePreviewPayloadUrls, + ]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { diff --git a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx index ee8273d28e..af870652f0 100644 --- a/packages/genui/a2ui-playground/src/pages/DemosPage.tsx +++ b/packages/genui/a2ui-playground/src/pages/DemosPage.tsx @@ -26,14 +26,25 @@ interface Scenario { interface PreviewInput { messages: unknown; + messagesUrl?: string; actionMocks?: Record; + actionMocksUrl?: string; demoId?: string; } +interface PublishedPayload { + messagesUrl: string; + actionMocksUrl?: string; +} + type PlayState = 'idle' | 'playing' | 'paused' | 'done'; type PlaybackProgressStatus = 'idle' | 'streaming' | 'paused' | 'done'; const jsonExtensions = [json()]; +const ONLINE_A2UI_SERVER_ORIGIN = 'https://genui-server.vercel.app'; +const LOCAL_A2UI_SERVER_PORT = '3060'; + +declare const __A2UI_PLAYGROUND_CLIENT_PAYLOAD_STORE__: boolean; function formatJson(value: unknown): string { return JSON.stringify(value ?? [], null, 2); @@ -48,6 +59,57 @@ function findScenarioById(id?: string): Scenario | undefined { return ALL_SCENARIOS.find((s) => s.id === id); } +function isDevHost(hostname: string): boolean { + return ( + hostname === 'localhost' + || hostname === '127.0.0.1' + || hostname === '0.0.0.0' + || hostname.startsWith('10.') + || hostname.startsWith('192.168.') + || /^172\.(?:1[6-9]|2\d|3[01])\./u.test(hostname) + ); +} + +function getA2UIPayloadEndpoint(): string { + if ( + window.location.protocol === 'http:' && isDevHost(window.location.hostname) + ) { + return `http://${window.location.hostname}:${LOCAL_A2UI_SERVER_PORT}/a2ui/payload`; + } + return `${ONLINE_A2UI_SERVER_ORIGIN}/a2ui/payload`; +} + +async function publishA2UIPayloadForPreview( + messages: unknown, + actionMocks?: Record, +): Promise { + const res = await window.fetch(getA2UIPayloadEndpoint(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages, actionMocks }), + }); + const payload = await res.json().catch(() => ({})) as { + preview?: { + messagesUrl?: unknown; + actionMocksUrl?: unknown; + }; + error?: unknown; + }; + if (!res.ok || typeof payload.preview?.messagesUrl !== 'string') { + throw new Error( + typeof payload.error === 'string' + ? payload.error + : 'Failed to publish A2UI messages', + ); + } + return { + messagesUrl: payload.preview.messagesUrl, + actionMocksUrl: typeof payload.preview.actionMocksUrl === 'string' + ? payload.preview.actionMocksUrl + : undefined, + }; +} + const ALL_SCENARIOS: Scenario[] = [ ...STATIC_DEMOS.map((d) => ({ ...d, actionMocks: undefined })), ...DYNAMIC_PRESETS, @@ -83,6 +145,7 @@ export function DemosPage(props: { const [error, setError] = useState(''); const [jsonEdited, setJsonEdited] = useState(false); const [previewRenderKey, setPreviewRenderKey] = useState(0); + const [isPublishingPayload, setIsPublishingPayload] = useState(false); const [previewInput, setPreviewInput] = useState(() => initialScenario ? { @@ -220,7 +283,9 @@ export function DemosPage(props: { theme, demoUrl: DEFAULT_A2UI_DEMO_URL, messages: previewInput.messages, + messagesUrl: previewInput.messagesUrl, actionMocks: previewInput.actionMocks, + actionMocksUrl: previewInput.actionMocksUrl, demoId: previewInput.demoId, playbackMode: isPlaybackActive, }; @@ -270,11 +335,33 @@ export function DemosPage(props: { }, [currentScenario, customJson, jsonEdited]); const handleRender = useCallback(() => { - if (commitJson()) { + const committed = commitJson(); + if (committed) { + if (!committed.isKnownDemo && !__A2UI_PLAYGROUND_CLIENT_PAYLOAD_STORE__) { + setIsPublishingPayload(true); + void publishA2UIPayloadForPreview( + committed.parsed, + currentScenario?.actionMocks, + ).then((preview) => { + setPreviewInput({ + messages: committed.parsed, + messagesUrl: preview.messagesUrl, + actionMocks: currentScenario?.actionMocks, + actionMocksUrl: preview.actionMocksUrl, + }); + setPreviewRenderKey((value) => value + 1); + }).catch((err) => { + setError(err instanceof Error ? err.message : String(err)); + }).finally(() => { + setIsPublishingPayload(false); + }); + resetPlayback(); + return; + } resetPlayback(); setPreviewRenderKey((value) => value + 1); } - }, [commitJson, resetPlayback]); + }, [commitJson, currentScenario, resetPlayback]); const handleFillExample = useCallback(() => { setError(''); @@ -742,8 +829,9 @@ export function DemosPage(props: { type='button' className='toolbarBtn primary' onClick={handleRender} + disabled={isPublishingPayload} > - ▶ Render + {isPublishingPayload ? 'Publishing...' : '▶ Render'} diff --git a/packages/genui/a2ui-playground/src/render.tsx b/packages/genui/a2ui-playground/src/render.tsx index 0535570105..ce9464486a 100644 --- a/packages/genui/a2ui-playground/src/render.tsx +++ b/packages/genui/a2ui-playground/src/render.tsx @@ -18,6 +18,7 @@ import '@lynx-js/web-elements/index.css'; import { decodeBase64Url } from './utils/base64url.js'; import { DEFAULT_A2UI_DEMO_URL } from './utils/demoUrl.js'; +import { RENDER_INIT_DATA_QUERY_PARAM } from './utils/renderUrl.js'; interface InitData { protocol?: '0.9' | 'a2ui' | 'openui'; @@ -95,9 +96,64 @@ function parseJsonParam(raw: string): unknown { } } +function readString(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined; +} + +function readBoolean(value: unknown): boolean | undefined { + return typeof value === 'boolean' ? value : undefined; +} + +function readProtocol(value: unknown): InitData['protocol'] { + return value === '0.9' || value === 'a2ui' || value === 'openui' + ? value + : undefined; +} + +function readTheme(value: unknown): InitData['theme'] { + return value === 'dark' ? 'dark' : (value === 'light' ? 'light' : undefined); +} + +function readSpeed(value: unknown): number | undefined { + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + return undefined; + } + return value; +} + +function readInitDataParam(raw: string | null): InitData | null { + if (!raw) return null; + + const parsed = parseJsonParam(raw); + if (!parsed || typeof parsed !== 'object') return null; + + const record = parsed as Record; + const initData: InitData = {}; + + initData.protocol = readProtocol(record.protocol); + initData.messagesUrl = readString(record.messagesUrl); + if ('messages' in record) initData.messages = record.messages; + initData.actionMocksUrl = readString(record.actionMocksUrl); + if ('actionMocks' in record) initData.actionMocks = record.actionMocks; + initData.demoUrl = readString(record.demoUrl); + initData.speed = readSpeed(record.speed); + initData.instant = readBoolean(record.instant); + initData.playbackMode = readBoolean(record.playbackMode); + initData.theme = readTheme(record.theme); + initData.rawText = readString(record.rawText); + initData.rawTextUrl = readString(record.rawTextUrl); + initData.playbackPaused = readBoolean(record.playbackPaused); + initData.liveAction = readBoolean(record.liveAction); + + return initData; +} + function parseInitDataFromQuery(): InitData | null { const params = new URLSearchParams(window.location.search); + const baseInitData = readInitDataParam( + params.get(RENDER_INIT_DATA_QUERY_PARAM), + ); const protocol = params.get('protocol'); const messagesUrl = params.get('messagesUrl'); const demoUrl = params.get('demoUrl'); @@ -113,37 +169,35 @@ function parseInitDataFromQuery(): InitData | null { const rawTextUrl = params.get('rawTextUrl'); if ( - !protocol && !messagesUrl && !messages && !demoUrl && !demo && !rawText - && !rawTextUrl + !baseInitData && !protocol && !messagesUrl && !messages && !demoUrl + && !demo && !rawText && !rawTextUrl ) { return null; } - const protocolValue = protocol === '0.9' || protocol === 'a2ui' - || protocol === 'openui' - ? protocol - : undefined; + const protocolValue = readProtocol(protocol); const speedRaw = params.get('speed'); const speedVal = speedRaw === null ? undefined : Number(speedRaw); const initData: InitData = { - protocol: protocolValue, - messagesUrl: messagesUrl ?? undefined, - actionMocksUrl: actionMocksUrl ?? undefined, - demoUrl: demoUrl ?? undefined, - messages: [], // Default to an empty array + ...baseInitData, + protocol: protocolValue ?? baseInitData?.protocol, + messagesUrl: messagesUrl ?? baseInitData?.messagesUrl, + actionMocksUrl: actionMocksUrl ?? baseInitData?.actionMocksUrl, + demoUrl: demoUrl ?? baseInitData?.demoUrl, + messages: baseInitData?.messages ?? [], // Default to an empty array speed: speedVal !== undefined && Number.isFinite(speedVal) && speedVal >= 0 ? speedVal - : undefined, - instant: instant === '1' ? true : undefined, - playbackMode: playbackMode === '1' ? true : undefined, - theme: theme === 'dark' - ? 'dark' - : (theme === 'light' ? 'light' : undefined), - rawText: rawText ?? undefined, - rawTextUrl: rawTextUrl ?? undefined, - liveAction: params.get('liveAction') === '1' ? true : undefined, + : baseInitData?.speed, + instant: instant === '1' ? true : baseInitData?.instant, + playbackMode: playbackMode === '1' ? true : baseInitData?.playbackMode, + theme: readTheme(theme) ?? baseInitData?.theme, + rawText: rawText ?? baseInitData?.rawText, + rawTextUrl: rawTextUrl ?? baseInitData?.rawTextUrl, + liveAction: params.get('liveAction') === '1' + ? true + : baseInitData?.liveAction, }; if (messages) { diff --git a/packages/genui/a2ui-playground/src/storage/types.ts b/packages/genui/a2ui-playground/src/storage/types.ts index 03c42ab046..14f6a3c258 100644 --- a/packages/genui/a2ui-playground/src/storage/types.ts +++ b/packages/genui/a2ui-playground/src/storage/types.ts @@ -11,11 +11,17 @@ export interface ConversationMeta { previewText: string; } +export interface PreviewPayloadUrls { + messagesUrl: string; + actionMocksUrl?: string; +} + export interface PersistedMessage { conversationId: string; seq: number; role: 'user' | 'assistant' | 'system'; content: string; + previewPayloadUrls?: PreviewPayloadUrls; createdAt: number; } @@ -24,6 +30,7 @@ export interface DataModelSnapshot { dataModel: Record; surfaceIds: string[]; previewMessages?: unknown[]; + previewPayloadUrls?: PreviewPayloadUrls; updatedAt: number; } diff --git a/packages/genui/a2ui-playground/src/styles.css b/packages/genui/a2ui-playground/src/styles.css index 2b40d7f74f..927b276590 100644 --- a/packages/genui/a2ui-playground/src/styles.css +++ b/packages/genui/a2ui-playground/src/styles.css @@ -1102,13 +1102,14 @@ html[data-theme="dark"] .phoneHomeIndicator { .previewQrUrlRow { margin-top: 6px; - display: flex; + display: inline-flex; align-items: center; gap: 8px; + max-width: 100%; } .previewQrUrlText { - flex: 1; + flex: 0 1 auto; min-width: 0; font-size: 11px; font-family: var(--geist-mono); diff --git a/packages/genui/a2ui-playground/src/utils/renderUrl.ts b/packages/genui/a2ui-playground/src/utils/renderUrl.ts index 120461e9be..4b5a7f4c75 100644 --- a/packages/genui/a2ui-playground/src/utils/renderUrl.ts +++ b/packages/genui/a2ui-playground/src/utils/renderUrl.ts @@ -4,10 +4,14 @@ import { encodeBase64Url } from './base64url.js'; import type { Protocol } from './protocol.js'; +export const RENDER_INIT_DATA_QUERY_PARAM = 'initData'; + export interface RenderInit { protocol: Protocol; demoUrl: string; + messagesUrl?: string; messages: unknown; + actionMocksUrl?: string; actionMocks?: unknown; /** Theme forwarded to the preview runtime. */ theme?: 'light' | 'dark'; @@ -27,6 +31,33 @@ export interface RenderInit { playbackMode?: boolean; } +function buildRenderInitData(init: RenderInit): Record { + const initData: Record = { + protocol: init.protocol.name, + demoUrl: init.demoUrl, + }; + + if (init.messagesUrl) { + initData.messagesUrl = init.messagesUrl; + } else if (!init.demoId) { + initData.messages = init.messages; + } + + if (init.actionMocksUrl) { + initData.actionMocksUrl = init.actionMocksUrl; + } else if (init.actionMocks !== undefined) { + initData.actionMocks = init.actionMocks; + } + + if (init.theme) initData.theme = init.theme; + if (init.speed !== undefined && init.speed !== 1) initData.speed = init.speed; + if (init.instant) initData.instant = true; + if (init.liveAction) initData.liveAction = true; + if (init.playbackMode) initData.playbackMode = true; + + return initData; +} + export function buildRenderUrl(init: RenderInit, baseUrl: string): string { const url = new URL('render.html', baseUrl); url.searchParams.set('protocol', init.protocol.name); @@ -38,6 +69,20 @@ export function buildRenderUrl(init: RenderInit, baseUrl: string): string { if (init.demoId) { // Known demo: reference static JSON file by ID instead of inlining payload. url.searchParams.set('demo', init.demoId); + } else if (init.messagesUrl) { + url.searchParams.set( + RENDER_INIT_DATA_QUERY_PARAM, + encodeBase64Url(JSON.stringify(buildRenderInitData(init))), + ); + url.searchParams.set('messagesUrl', init.messagesUrl); + if (init.actionMocksUrl) { + url.searchParams.set('actionMocksUrl', init.actionMocksUrl); + } else if (init.actionMocks !== undefined) { + url.searchParams.set( + 'actionMocks', + encodeBase64Url(JSON.stringify(init.actionMocks)), + ); + } } else { // Custom JSON: inline the payload as base64url. url.searchParams.set( diff --git a/packages/genui/server/app/a2ui/action/stream/route.ts b/packages/genui/server/app/a2ui/action/stream/route.ts index 0879d56d5d..84e7867104 100644 --- a/packages/genui/server/app/a2ui/action/stream/route.ts +++ b/packages/genui/server/app/a2ui/action/stream/route.ts @@ -23,6 +23,7 @@ import { validateConversation, } from '../../_shared'; import { corsHeaders, corsPreflight, jsonWithCors } from '../../cors'; +import { publishA2UIPayload } from '../../payload-publisher'; import { checkRateLimit, rateLimitSseResponse } from '../../rate-limit'; export const runtime = 'nodejs'; @@ -330,10 +331,15 @@ export async function POST(req: Request) { } } + const preview = validation.ok + ? await publishA2UIPayload(validation.messages) + : undefined; + log('done.enqueued', { validationOk: validation.ok, validationErrorCount: validation.errors.length, messageCount: validation.messages.length, + hasPreviewUrl: Boolean(preview?.messagesUrl), repairAttempted: repair?.attempted ?? false, repairOk: repair?.ok, requestId, @@ -343,6 +349,7 @@ export async function POST(req: Request) { usage, finishReason, validation, + preview, repair, }); } catch (err: unknown) { diff --git a/packages/genui/server/app/a2ui/payload-publisher.ts b/packages/genui/server/app/a2ui/payload-publisher.ts new file mode 100644 index 0000000000..247d3713c1 --- /dev/null +++ b/packages/genui/server/app/a2ui/payload-publisher.ts @@ -0,0 +1,114 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; + +const SUPABASE_URL = process.env.SUPABASE_URL; +const SUPABASE_S3_ACCESS_KEY_ID = process.env.SUPABASE_S3_ACCESS_KEY_ID; +const SUPABASE_S3_SECRET_ACCESS_KEY = process.env.SUPABASE_S3_SECRET_ACCESS_KEY; +const SUPABASE_STORAGE_BUCKET = process.env.SUPABASE_STORAGE_BUCKET + ?? 'genui'; +const SUPABASE_STORAGE_PREFIX = process.env.SUPABASE_STORAGE_PREFIX ?? 'a2ui'; +const SUPABASE_STORAGE_REGION = process.env.SUPABASE_STORAGE_REGION + ?? 'us-east-1'; + +export interface A2UIPublishedPayload { + messagesUrl: string; + actionMocksUrl?: string; +} + +function trimSlashes(value: string): string { + return value.replace(/^\/+|\/+$/g, ''); +} + +function buildSupabaseStoragePath(id: string, file: string): string { + const prefix = trimSlashes(SUPABASE_STORAGE_PREFIX); + return prefix ? `${prefix}/${id}/${file}` : `${id}/${file}`; +} + +function buildSupabaseObjectUrl(path: string): string | null { + if (!SUPABASE_URL) return null; + + const base = SUPABASE_URL.replace(/\/+$/, ''); + return `${base}/storage/v1/object/public/${SUPABASE_STORAGE_BUCKET}/${path}`; +} + +function createSupabaseS3Client(): S3Client { + if ( + !SUPABASE_URL + || !SUPABASE_S3_ACCESS_KEY_ID + || !SUPABASE_S3_SECRET_ACCESS_KEY + ) { + throw new Error('Supabase Storage S3 is not configured'); + } + + const base = SUPABASE_URL.replace(/\/+$/, ''); + return new S3Client({ + region: SUPABASE_STORAGE_REGION, + endpoint: `${base}/storage/v1/s3`, + forcePathStyle: true, + credentials: { + accessKeyId: SUPABASE_S3_ACCESS_KEY_ID, + secretAccessKey: SUPABASE_S3_SECRET_ACCESS_KEY, + }, + }); +} + +async function uploadSupabaseJson( + path: string, + payload: unknown, +): Promise { + const client = createSupabaseS3Client(); + await client.send( + new PutObjectCommand({ + Bucket: SUPABASE_STORAGE_BUCKET, + Key: path, + Body: JSON.stringify(payload), + ContentType: 'application/json; charset=utf-8', + CacheControl: 'public, max-age=1800', + }), + ); +} + +export async function publishA2UIPayload( + messages: unknown, + actionMocks?: unknown, +): Promise { + if (messages === undefined) return undefined; + + if ( + !SUPABASE_URL + || !SUPABASE_S3_ACCESS_KEY_ID + || !SUPABASE_S3_SECRET_ACCESS_KEY + ) { + console.warn( + '[a2ui:payload-publisher] Supabase Storage S3 is not configured', + ); + return undefined; + } + + try { + const id = crypto.randomUUID(); + const messagesPath = buildSupabaseStoragePath(id, 'messages.json'); + await uploadSupabaseJson(messagesPath, messages); + const messagesUrl = buildSupabaseObjectUrl(messagesPath); + if (!messagesUrl) return undefined; + + if (actionMocks !== undefined) { + const actionMocksPath = buildSupabaseStoragePath(id, 'actionMocks.json'); + await uploadSupabaseJson(actionMocksPath, actionMocks); + const actionMocksUrl = buildSupabaseObjectUrl(actionMocksPath) + ?? undefined; + return { messagesUrl, actionMocksUrl }; + } + + return { messagesUrl }; + } catch (err) { + console.warn( + '[a2ui:payload-publisher] Supabase Storage upload failed', + err, + ); + return undefined; + } +} diff --git a/packages/genui/server/app/a2ui/payload/route.ts b/packages/genui/server/app/a2ui/payload/route.ts new file mode 100644 index 0000000000..c7f89754d9 --- /dev/null +++ b/packages/genui/server/app/a2ui/payload/route.ts @@ -0,0 +1,70 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { errorMessage, readJsonBodyWithLimit } from '../_shared'; +import { corsPreflight, jsonWithCors } from '../cors'; +import { publishA2UIPayload } from '../payload-publisher'; +import { checkRateLimit } from '../rate-limit'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +interface A2UIPayloadBody { + messages?: unknown; + actionMocks?: unknown; +} + +export function OPTIONS(req: Request) { + return corsPreflight(req); +} + +export async function POST(req: Request) { + const decision = checkRateLimit(req); + if (!decision.ok) { + return jsonWithCors( + req, + { ok: false, error: 'rate limit exceeded' }, + { status: 429 }, + ); + } + + const parsed = await readJsonBodyWithLimit(req); + if (!parsed.ok) { + return jsonWithCors( + req, + { ok: false, error: parsed.error }, + { status: parsed.status }, + ); + } + + if (parsed.body.messages === undefined) { + return jsonWithCors( + req, + { ok: false, error: 'messages is required' }, + { status: 400 }, + ); + } + + try { + const preview = await publishA2UIPayload( + parsed.body.messages, + parsed.body.actionMocks, + ); + if (!preview) { + return jsonWithCors( + req, + { ok: false, error: 'payload publishing is not configured' }, + { status: 503 }, + ); + } + + return jsonWithCors(req, { ok: true, preview }); + } catch (err) { + return jsonWithCors( + req, + { ok: false, error: errorMessage(err).message }, + { status: 500 }, + ); + } +} diff --git a/packages/genui/server/app/a2ui/stream/route.ts b/packages/genui/server/app/a2ui/stream/route.ts index 27ee424dee..c60ff1f815 100644 --- a/packages/genui/server/app/a2ui/stream/route.ts +++ b/packages/genui/server/app/a2ui/stream/route.ts @@ -22,6 +22,7 @@ import { } from '../_shared'; import type { A2UIChatBody } from '../_shared'; import { corsHeaders, corsPreflight, jsonWithCors } from '../cors'; +import { publishA2UIPayload } from '../payload-publisher'; import { checkRateLimit, rateLimitSseResponse } from '../rate-limit'; export const runtime = 'nodejs'; @@ -268,10 +269,15 @@ export async function POST(req: Request) { } } + const preview = validation.ok + ? await publishA2UIPayload(validation.messages) + : undefined; + log('done.enqueued', { validationOk: validation.ok, validationErrorCount: validation.errors.length, messageCount: validation.messages.length, + hasPreviewUrl: Boolean(preview?.messagesUrl), repairAttempted: repair?.attempted ?? false, repairOk: repair?.ok, requestId, @@ -281,6 +287,7 @@ export async function POST(req: Request) { usage, finishReason, validation, + preview, repair, }); } catch (err: unknown) { diff --git a/packages/genui/server/package.json b/packages/genui/server/package.json index 8d6cf80fec..8a8348c374 100644 --- a/packages/genui/server/package.json +++ b/packages/genui/server/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ai-sdk/openai": "^3.0.63", + "@aws-sdk/client-s3": "^3.1053.0", "@mastra/core": "1.13.2", "next": "16.2.6", "react": "19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d59d03e9eb..24afa922bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -793,6 +793,9 @@ importers: '@ai-sdk/openai': specifier: ^3.0.63 version: 3.0.63(zod@3.25.76) + '@aws-sdk/client-s3': + specifier: ^3.1053.0 + version: 3.1053.0 '@mastra/core': specifier: 1.13.2 version: 1.13.2(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.10)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.10)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@3.25.76) @@ -2591,6 +2594,125 @@ packages: resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==} engines: {node: '>= 10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1053.0': + resolution: {integrity: sha512-/oGxoB6p1Nqs935Blt+v1o+anSCEf2n3RjIrcLz84i4cn2Gr+Z7JpDdUkG5+74r5ctqEPG7k/phTGbJ9fNKnHg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.13': + resolution: {integrity: sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.9': + resolution: {integrity: sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.39': + resolution: {integrity: sha512-29wX9zpAvEt1vcj0psha+y6ygBHy2V/S72mp6e7q0KARLWXq+pwE/lR6qGkwknQvruh52lXvlqZIga8Hdxkucw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.41': + resolution: {integrity: sha512-IA3CQTjtJkb6u1H4mE4936c8OPBMa9Jggtwe8U2Mqw/vvb/tZ5Ebd0mcZcX0uKWQhOyYo/+qNIwkV5Xh+FeJJA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.43': + resolution: {integrity: sha512-4mzII+3mZEVXXE1xzrLQrCJL7/r62A63bA6SVzZoNL5rqCJghpf+xgGltVrIBBs0n+mOZBKrQl2tRREtvZ5l6A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.43': + resolution: {integrity: sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.44': + resolution: {integrity: sha512-sDaBIT0yrNNIPfvlsiTCmANm07zKju+ipWODjEXgZlsjMeIJR3LVp7RDyAOzUoAsTbDfYKDWp+i5WrFiQP6rmQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.39': + resolution: {integrity: sha512-2k/amBifLd75eXNwgvPw/2lKYSQ3NhvHQgkVKVjfUq13/eJ3JRtHmznuFenn74OK3sSfp4SMy1YB2w+UVXoKqA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.43': + resolution: {integrity: sha512-LPc3+Y4vhH1T4x6CMqwCM6hk5+SRf/Lwmgm8INm95wxTtIRHcMwQUVkDzWu4Iw/RSncxYM2BC01OrYbxOPZvyg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.43': + resolution: {integrity: sha512-wQtL34lUD/09VXjwAUo2T+I3aEXRDxMB3DKmTJL/Zj0Gi6sLDTrVhae1XVt01yzkquOWajI/sZW72JGDZ1ciTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.15': + resolution: {integrity: sha512-O2HDANa+MrvbxpaRVQDiH3T13uAa9AkMjKyZmDygwauAmmvqZ5B0iRmKW+fuVGW6NPXuyXurFgIx69lSvmAWGA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.13': + resolution: {integrity: sha512-sHiqIFg8o2ipT7t40B89Vj0ubSUtY6OSt/+Ee/OXhHch5K4+81zP2+QX8Lkc/nJ2QSmCySxOke7TEbmX69fe2g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.21': + resolution: {integrity: sha512-alAu9heyiBK/OmRNXVxq8mmPTgeW2AQ6EYjRsI38kPZa1MZvt2Jh+BlGq7/GG9OVXOaEgD7DlGj/Lzfy5OmuEg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.11': + resolution: {integrity: sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.42': + resolution: {integrity: sha512-/xNqNGXv9LaxZd25L9VV4pnSOw9OdDNO4rAHamM+h3KQBSITljIH9vk3dveGga1I2j36lQd0rdG3gjNEXvtNew==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.11': + resolution: {integrity: sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.11': + resolution: {integrity: sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.28': + resolution: {integrity: sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1052.0': + resolution: {integrity: sha512-QqZNB3so7UIDxZtroc85TQaLVxdZRFm0eWM1CSR2N+b06as9TOrilvrlTZuj3guYlxMs6yLOgGxnklJ5qMYtTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.9': + resolution: {integrity: sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.25': + resolution: {integrity: sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -4427,6 +4549,9 @@ packages: cpu: [x64] os: [win32] + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5430,6 +5555,42 @@ packages: resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==} engines: {node: '>=12'} + '@smithy/core@3.24.4': + resolution: {integrity: sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.4': + resolution: {integrity: sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.4': + resolution: {integrity: sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.4': + resolution: {integrity: sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.4': + resolution: {integrity: sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -6674,6 +6835,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -7869,6 +8033,13 @@ packages: fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastest-stable-stringify@2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} @@ -9847,6 +10018,10 @@ packages: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -11215,6 +11390,9 @@ packages: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -12085,6 +12263,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -12330,6 +12512,271 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.37.0 '@ast-grep/napi-win32-x64-msvc': 0.37.0 + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.9 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1053.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.13 + '@aws-sdk/credential-provider-node': 3.972.44 + '@aws-sdk/middleware-bucket-endpoint': 3.972.15 + '@aws-sdk/middleware-expect-continue': 3.972.13 + '@aws-sdk/middleware-flexible-checksums': 3.974.21 + '@aws-sdk/middleware-location-constraint': 3.972.11 + '@aws-sdk/middleware-sdk-s3': 3.972.42 + '@aws-sdk/middleware-ssec': 3.972.11 + '@aws-sdk/signature-v4-multi-region': 3.996.28 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.13': + dependencies: + '@aws-sdk/types': 3.973.9 + '@aws-sdk/xml-builder': 3.972.25 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.4 + '@smithy/signature-v4': 5.4.4 + '@smithy/types': 4.14.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.9': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.39': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.41': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/credential-provider-env': 3.972.39 + '@aws-sdk/credential-provider-http': 3.972.41 + '@aws-sdk/credential-provider-login': 3.972.43 + '@aws-sdk/credential-provider-process': 3.972.39 + '@aws-sdk/credential-provider-sso': 3.972.43 + '@aws-sdk/credential-provider-web-identity': 3.972.43 + '@aws-sdk/nested-clients': 3.997.11 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/credential-provider-imds': 4.3.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/nested-clients': 3.997.11 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.44': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.39 + '@aws-sdk/credential-provider-http': 3.972.41 + '@aws-sdk/credential-provider-ini': 3.972.43 + '@aws-sdk/credential-provider-process': 3.972.39 + '@aws-sdk/credential-provider-sso': 3.972.43 + '@aws-sdk/credential-provider-web-identity': 3.972.43 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/credential-provider-imds': 4.3.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.39': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/nested-clients': 3.997.11 + '@aws-sdk/token-providers': 3.1052.0 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.43': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/nested-clients': 3.997.11 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.972.15': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.21': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.13 + '@aws-sdk/crc64-nvme': 3.972.9 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/signature-v4-multi-region': 3.996.28 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/signature-v4': 5.4.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.11': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.13 + '@aws-sdk/signature-v4-multi-region': 3.996.28 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/fetch-http-handler': 5.4.4 + '@smithy/node-http-handler': 4.7.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.28': + dependencies: + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/signature-v4': 5.4.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1052.0': + dependencies: + '@aws-sdk/core': 3.974.13 + '@aws-sdk/nested-clients': 3.997.11 + '@aws-sdk/types': 3.973.9 + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.9': + dependencies: + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.25': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.2 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -14730,6 +15177,8 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.6': optional: true + '@nodable/entities@2.1.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -15996,6 +16445,54 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + '@smithy/core@3.24.4': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.4': + dependencies: + '@smithy/core': 3.24.4 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/types@4.14.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.10)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': @@ -17266,6 +17763,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -18677,6 +19176,18 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 + fastest-stable-stringify@2.0.2: {} fastq@1.17.1: @@ -21127,6 +21638,8 @@ snapshots: path-exists@5.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -22604,6 +23117,8 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + strnum@2.3.0: {} + style-mod@4.1.3: {} style-to-js@1.1.16: @@ -23739,6 +24254,8 @@ snapshots: xml-name-validator@5.0.0: {} + xml-naming@0.1.0: {} + xmlbuilder@15.1.1: {} xmlchars@2.2.0: {}