@agent-native/core 0.108.0 → 0.109.1
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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +162 -17
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +200 -14
- package/corpus/core/src/a2a/index.ts +7 -1
- package/corpus/core/src/a2a/invoke.ts +77 -1
- package/corpus/core/src/a2a/server.ts +13 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +32 -0
- package/corpus/core/src/action.ts +5 -2
- package/corpus/core/src/agent/production-agent.ts +33 -4
- package/corpus/core/src/client/AssistantChat.tsx +648 -603
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -4
- package/corpus/core/src/client/agent-chat-adapter.ts +23 -37
- package/corpus/core/src/client/agent-sidebar-state.ts +4 -0
- package/corpus/core/src/client/analytics.ts +84 -0
- package/corpus/core/src/client/chat/message-components.tsx +94 -22
- package/corpus/core/src/client/chat/tool-call-display.tsx +170 -47
- package/corpus/core/src/client/chat/use-agent-chat-lifecycle-tracking.ts +74 -0
- package/corpus/core/src/client/chat-thread-url.ts +11 -0
- package/corpus/core/src/client/error-capture.ts +12 -0
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +37 -1
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/route-chunk-recovery.ts +20 -8
- package/corpus/core/src/client/session-replay.ts +31 -0
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/extensions/actions.ts +2 -14
- package/corpus/core/src/extensions/store.ts +1 -1
- package/corpus/core/src/localization/default-messages.ts +3 -0
- package/corpus/core/src/mcp/build-server.ts +2 -2
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/progress/actions.ts +4 -2
- package/corpus/core/src/provider-api/index.ts +205 -105
- package/corpus/core/src/scripts/call-agent.ts +190 -19
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +112 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +136 -4
- package/corpus/core/src/styles/agent-conversation.css +7 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +29 -10
- package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +54 -23
- package/corpus/templates/analytics/.builder/skills/apollo/SKILL.md +19 -41
- package/corpus/templates/analytics/.builder/skills/commonroom/SKILL.md +20 -35
- package/corpus/templates/analytics/.builder/skills/github/SKILL.md +23 -67
- package/corpus/templates/analytics/.builder/skills/pylon/SKILL.md +25 -43
- package/corpus/templates/analytics/AGENTS.md +22 -13
- package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
- package/corpus/templates/analytics/actions/configure-gong-native-insights.ts +29 -0
- package/corpus/templates/analytics/actions/content-calendar-schema.ts +8 -4
- package/corpus/templates/analytics/actions/content-calendar.ts +12 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +113 -22
- package/corpus/templates/analytics/actions/gong-native-insights.ts +166 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +190 -46
- package/corpus/templates/analytics/actions/pylon-issues.ts +123 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/actions/slack-messages.ts +179 -27
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +48 -152
- package/corpus/templates/analytics/app/components/ui/chart.tsx +1 -1
- package/corpus/templates/analytics/app/hooks/use-chart-tooltip-flip.ts +21 -6
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/analytics/app/i18n-data.ts +134 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +181 -30
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +4 -3
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-dashboard-editors-can-add-shared-extensions-or-opt-into-per-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-dashboard-extensions-show-a-loading-skeleton.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-errors-now-group-repeat-failures-consistently-and-keep-priva.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-gong-transcript-reviews-now-batch-bounded-call-windows-for-f.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-hubspot-deal-filters-now-run-in-hubspot-before-paging-result.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-notion-content-calendars-now-discover-the-matching-database-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-pylon-dashboard-reads-now-use-the-paginated-issue-search-api.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-slack-reads-no-longer-join-channels-and-report-pagination-coverage.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/handlers/notion.ts +14 -3
- package/corpus/templates/analytics/server/handlers/slack.ts +182 -29
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -6
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +6 -2
- package/corpus/templates/analytics/server/lib/error-capture.ts +75 -8
- package/corpus/templates/analytics/server/lib/gong-native-policy.ts +72 -0
- package/corpus/templates/analytics/server/lib/gong.ts +54 -66
- package/corpus/templates/analytics/server/lib/hubspot.ts +40 -3
- package/corpus/templates/analytics/server/lib/notion.ts +83 -8
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +4 -0
- package/corpus/templates/analytics/server/lib/slack.ts +242 -90
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -3
- package/corpus/templates/analytics/server/routes/api/pylon/accounts.get.ts +42 -1
- package/corpus/templates/analytics/server/routes/api/pylon/issues.get.ts +71 -1
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +44 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +19 -1
- package/corpus/templates/clips/app/components/library/create-folder-dialog.tsx +123 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +42 -4
- package/corpus/templates/clips/app/components/library/recording-card.tsx +30 -0
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/entry.client.tsx +6 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -1
- package/corpus/templates/clips/app/i18n/de-DE.ts +12 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +12 -1
- package/corpus/templates/clips/app/i18n/es-ES.ts +11 -1
- package/corpus/templates/clips/app/i18n/fr-FR.ts +11 -1
- package/corpus/templates/clips/app/i18n/hi-IN.ts +10 -1
- package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +11 -1
- package/corpus/templates/clips/app/i18n/pt-BR.ts +11 -1
- package/corpus/templates/clips/app/i18n/zh-CN.ts +11 -1
- package/corpus/templates/clips/app/i18n/zh-TW.ts +11 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +72 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
- package/corpus/templates/clips/changelog/2026-07-17-choose-which-workspace-receives-new-clips-recordings-and-des.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-create-folders-directly-from-the-move-menu.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-failed-desktop-uploads-now-offer-a-direct-way-to-reopen-clip.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +19 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +45 -18
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
- package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/corpus/templates/content/app/entry.client.tsx +6 -1
- package/corpus/templates/dispatch/AGENTS.md +5 -0
- package/corpus/templates/dispatch/changelog/2026-07-17-apps-page-links-to-other-available-apps.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-17-curated-template-remixes.md +6 -0
- package/corpus/templates/mail/app/entry.client.tsx +6 -1
- package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +11 -7
- package/corpus/templates/slides/.env.example +0 -1
- package/corpus/templates/slides/AGENTS.md +4 -0
- package/corpus/templates/slides/DEVELOPING.md +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +1 -1
- package/corpus/templates/slides/actions/logo-config.ts +6 -7
- package/corpus/templates/slides/actions/search-images.ts +11 -39
- package/corpus/templates/slides/actions/search-logos.ts +5 -52
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/LogoSearchPanel.tsx +16 -26
- package/corpus/templates/slides/app/entry.client.tsx +6 -1
- package/corpus/templates/slides/server/lib/media-search-providers.ts +216 -0
- package/corpus/templates/slides/server/register-secrets.ts +30 -0
- package/dist/a2a/client.d.ts +47 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +94 -12
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +147 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +4 -4
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +2 -2
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +29 -2
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +33 -1
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +14 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +27 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +5 -2
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +14 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +158 -136
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +27 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +21 -39
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-sidebar-state.d.ts +1 -0
- package/dist/client/agent-sidebar-state.d.ts.map +1 -1
- package/dist/client/agent-sidebar-state.js +4 -0
- package/dist/client/agent-sidebar-state.js.map +1 -1
- package/dist/client/analytics.d.ts +14 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +67 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +8 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +62 -19
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +6 -0
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +83 -14
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts +8 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts.map +1 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.js +51 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.js.map +1 -0
- package/dist/client/chat-thread-url.d.ts +2 -0
- package/dist/client/chat-thread-url.d.ts.map +1 -0
- package/dist/client/chat-thread-url.js +12 -0
- package/dist/client/chat-thread-url.js.map +1 -0
- package/dist/client/error-capture.d.ts.map +1 -1
- package/dist/client/error-capture.js +7 -0
- package/dist/client/error-capture.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.d.ts +3 -1
- package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +28 -3
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/route-chunk-recovery.d.ts.map +1 -1
- package/dist/client/route-chunk-recovery.js +14 -9
- package/dist/client/route-chunk-recovery.js.map +1 -1
- package/dist/client/session-replay.d.ts +15 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +20 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +2 -10
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/store.js +1 -1
- package/dist/extensions/store.js.map +1 -1
- package/dist/localization/default-messages.d.ts +3 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +3 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +2 -2
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/actions.d.ts.map +1 -1
- package/dist/progress/actions.js +3 -2
- package/dist/progress/actions.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +91 -8
- package/dist/provider-api/index.js.map +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +140 -17
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts +24 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +62 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +118 -5
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-conversation.css +7 -0
- package/dist/templates/default/app/i18n/ar-SA.ts +20 -0
- package/dist/templates/default/app/i18n/de-DE.ts +20 -0
- package/dist/templates/default/app/i18n/en-US.ts +20 -0
- package/dist/templates/default/app/i18n/es-ES.ts +20 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +19 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +20 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +19 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +20 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +19 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +20 -0
- package/src/templates/default/app/i18n/de-DE.ts +20 -0
- package/src/templates/default/app/i18n/en-US.ts +20 -0
- package/src/templates/default/app/i18n/es-ES.ts +20 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +19 -0
- package/src/templates/default/app/i18n/ja-JP.ts +20 -0
- package/src/templates/default/app/i18n/ko-KR.ts +19 -0
- package/src/templates/default/app/i18n/pt-BR.ts +20 -0
- package/src/templates/default/app/i18n/zh-CN.ts +19 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/server/handlers/github.ts +0 -200
- package/corpus/templates/analytics/server/handlers/pylon.ts +0 -43
- package/corpus/templates/analytics/server/lib/apollo.ts +0 -167
- package/corpus/templates/analytics/server/lib/commonroom.ts +0 -149
- package/corpus/templates/analytics/server/lib/github.ts +0 -660
- package/corpus/templates/analytics/server/lib/mixpanel.ts +0 -156
- package/corpus/templates/analytics/server/lib/posthog.ts +0 -194
- package/corpus/templates/analytics/server/lib/pylon.ts +0 -109
- package/corpus/templates/analytics/server/routes/api/github/graphql.post.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/issue.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/org-prs.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/pr.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/prs.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/search.get.ts +0 -1
- package/corpus/templates/slides/actions/fetch-logos.ts +0 -90
- package/corpus/templates/slides/actions/image-search.ts +0 -64
- package/corpus/templates/slides/actions/import-github.ts +0 -182
- package/corpus/templates/slides/actions/logo-lookup.ts +0 -113
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "h3";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
|
+
import finalizeRecording from "../../../../actions/finalize-recording.js";
|
|
14
15
|
import { ensureRecordingSeekable } from "../../../../actions/lib/ensure-seekable-video.js";
|
|
15
16
|
import requestTranscript from "../../../../actions/request-transcript.js";
|
|
16
17
|
import { getDb, schema } from "../../../db/index.js";
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
|
|
23
24
|
const bodySchema = z.object({
|
|
24
25
|
recordingId: z.string().min(1).max(200),
|
|
25
|
-
kind: z.enum(["seekable", "transcript"]),
|
|
26
|
+
kind: z.enum(["media-ready", "seekable", "transcript"]),
|
|
26
27
|
token: z.string().min(1),
|
|
27
28
|
delayMs: z.number().int().min(0).max(30_000).optional(),
|
|
28
29
|
retryAttempt: z.number().int().min(1).max(10).optional(),
|
|
@@ -61,7 +62,8 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
61
62
|
setResponseStatus(event, 404);
|
|
62
63
|
return { ok: false, error: "Recording not found" };
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
const requiredStatus = kind === "media-ready" ? "processing" : "ready";
|
|
66
|
+
if (recording.status !== requiredStatus) {
|
|
65
67
|
return {
|
|
66
68
|
ok: true,
|
|
67
69
|
recordingId,
|
|
@@ -84,6 +86,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
84
86
|
kind,
|
|
85
87
|
retryAttempt,
|
|
86
88
|
regenerate,
|
|
89
|
+
requireAccepted: kind === "media-ready",
|
|
87
90
|
});
|
|
88
91
|
return {
|
|
89
92
|
ok: true,
|
|
@@ -100,6 +103,13 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
100
103
|
});
|
|
101
104
|
return { ok: true, kind, result };
|
|
102
105
|
}
|
|
106
|
+
if (kind === "media-ready") {
|
|
107
|
+
const result = await finalizeRecording.run({
|
|
108
|
+
id: recordingId,
|
|
109
|
+
mediaVerificationRetryAttempt: retryAttempt ?? 1,
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, kind, result };
|
|
112
|
+
}
|
|
103
113
|
|
|
104
114
|
const result = await requestTranscript.run({
|
|
105
115
|
recordingId,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "../../../shared/transcript-segments.js";
|
|
43
43
|
import { resolveTranscriptPresentation } from "../../../shared/transcript-status.js";
|
|
44
44
|
import { getDb, schema } from "../../db/index.js";
|
|
45
|
+
import { resolvePlayerThumbnailUrl } from "../../lib/player-thumbnail-url.js";
|
|
45
46
|
import { resolvePlayerVideoUrl } from "../../lib/player-video-url.js";
|
|
46
47
|
import {
|
|
47
48
|
getOrganizationRoleForEmail,
|
|
@@ -367,6 +368,10 @@ export default defineEventHandler(async (event) => {
|
|
|
367
368
|
resolvedVideoUrl,
|
|
368
369
|
protectedMediaToken,
|
|
369
370
|
);
|
|
371
|
+
const playbackThumbnailUrl = resolvePlayerThumbnailUrl(rec, {
|
|
372
|
+
accessToken: protectedMediaToken,
|
|
373
|
+
appPath,
|
|
374
|
+
});
|
|
370
375
|
|
|
371
376
|
const canExposeAgentContext =
|
|
372
377
|
(rec.visibility === "public" || tokenAllowsAgentAccess || viewerIsOwner) &&
|
|
@@ -399,7 +404,7 @@ export default defineEventHandler(async (event) => {
|
|
|
399
404
|
id: rec.id,
|
|
400
405
|
title: rec.title,
|
|
401
406
|
description: rec.description,
|
|
402
|
-
thumbnailUrl:
|
|
407
|
+
thumbnailUrl: playbackThumbnailUrl,
|
|
403
408
|
animatedThumbnailUrl: rec.animatedThumbnailUrl,
|
|
404
409
|
sourceAppName: rec.sourceAppName,
|
|
405
410
|
durationMs: rec.durationMs,
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve a recording thumbnail from the same origin as the public player.
|
|
3
|
+
*
|
|
4
|
+
* Thumbnail providers may return expiring or hotlink-protected URLs. Public
|
|
5
|
+
* share pages already proxy video through `/api/video/:recordingId`; using the
|
|
6
|
+
* same contract here keeps embeds and crawler previews reliable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createSsrfSafeDispatcher,
|
|
11
|
+
isBlockedExtensionUrlWithDns,
|
|
12
|
+
} from "@agent-native/core/extensions/url-safety";
|
|
13
|
+
import { getOrgContext } from "@agent-native/core/org";
|
|
14
|
+
import {
|
|
15
|
+
captureRouteError,
|
|
16
|
+
getSession,
|
|
17
|
+
runWithRequestContext,
|
|
18
|
+
signShortLivedToken,
|
|
19
|
+
verifyShortLivedToken,
|
|
20
|
+
} from "@agent-native/core/server";
|
|
21
|
+
import { resolveAccess } from "@agent-native/core/sharing";
|
|
22
|
+
import { eq } from "drizzle-orm";
|
|
23
|
+
import {
|
|
24
|
+
defineEventHandler,
|
|
25
|
+
getCookie,
|
|
26
|
+
getQuery,
|
|
27
|
+
getRequestURL,
|
|
28
|
+
getRouterParam,
|
|
29
|
+
setCookie,
|
|
30
|
+
setResponseStatus,
|
|
31
|
+
type H3Event,
|
|
32
|
+
} from "h3";
|
|
33
|
+
|
|
34
|
+
import { getDb, schema } from "../../../db/index.js";
|
|
35
|
+
import { getOrganizationRoleForEmail } from "../../../lib/recordings.js";
|
|
36
|
+
import { verifySharePassword } from "../../../lib/share-password.js";
|
|
37
|
+
|
|
38
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
39
|
+
const PROTECTED_MEDIA_ACCESS_TTL_SECONDS = 6 * 60 * 60;
|
|
40
|
+
const PROTECTED_MEDIA_COOKIE_PREFIX = "clips_media_";
|
|
41
|
+
const SAFE_RASTER_IMAGE_TYPES = new Set([
|
|
42
|
+
"image/avif",
|
|
43
|
+
"image/bmp",
|
|
44
|
+
"image/gif",
|
|
45
|
+
"image/jpeg",
|
|
46
|
+
"image/png",
|
|
47
|
+
"image/webp",
|
|
48
|
+
"image/x-icon",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
type ThumbnailRecording = {
|
|
52
|
+
id: string;
|
|
53
|
+
thumbnailUrl?: string | null;
|
|
54
|
+
animatedThumbnailUrl?: string | null;
|
|
55
|
+
expiresAt?: string | null;
|
|
56
|
+
organizationId?: string | null;
|
|
57
|
+
password?: string | null;
|
|
58
|
+
visibility?: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function appPath(path: string): string {
|
|
62
|
+
if (!path.startsWith("/")) return path;
|
|
63
|
+
const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
|
|
64
|
+
const base = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
65
|
+
return base ? `/${base}${path}` : path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cookieName(recordingId: string): string {
|
|
69
|
+
return `${PROTECTED_MEDIA_COOKIE_PREFIX}${recordingId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cookiePath(recordingId: string): string {
|
|
73
|
+
return appPath(`/api/thumbnail/${encodeURIComponent(recordingId)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isHttpsRequest(event: H3Event): boolean {
|
|
77
|
+
const requestUrl = getRequestURL(event);
|
|
78
|
+
return (
|
|
79
|
+
requestUrl.protocol === "https:" ||
|
|
80
|
+
process.env.APP_URL?.startsWith("https://") === true
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renewProtectedMediaCookie(event: H3Event, recordingId: string): void {
|
|
85
|
+
const token = signShortLivedToken({
|
|
86
|
+
resourceId: recordingId,
|
|
87
|
+
ttlSeconds: PROTECTED_MEDIA_ACCESS_TTL_SECONDS,
|
|
88
|
+
});
|
|
89
|
+
const secure = isHttpsRequest(event);
|
|
90
|
+
setCookie(event, cookieName(recordingId), token, {
|
|
91
|
+
httpOnly: true,
|
|
92
|
+
sameSite: secure ? "none" : "lax",
|
|
93
|
+
secure,
|
|
94
|
+
...(secure ? { partitioned: true } : {}),
|
|
95
|
+
path: cookiePath(recordingId),
|
|
96
|
+
maxAge: PROTECTED_MEDIA_ACCESS_TTL_SECONDS,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isRecursiveThumbnailUrl(value: string, recordingId: string): boolean {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(value, "http://local.test");
|
|
103
|
+
const expected = `/api/thumbnail/${encodeURIComponent(recordingId)}`;
|
|
104
|
+
return parsed.pathname === expected || parsed.pathname.endsWith(expected);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function dataUrlResponse(sourceUrl: string): Response | null {
|
|
111
|
+
const match = sourceUrl.match(/^data:(image\/[\w.+-]+)(;base64)?,(.*)$/s);
|
|
112
|
+
if (!match) return null;
|
|
113
|
+
|
|
114
|
+
const [, rawMimeType, encoding, payload] = match;
|
|
115
|
+
const mimeType = rawMimeType.toLowerCase();
|
|
116
|
+
if (!SAFE_RASTER_IMAGE_TYPES.has(mimeType)) return null;
|
|
117
|
+
try {
|
|
118
|
+
const bytes = encoding
|
|
119
|
+
? decodeBase64(payload)
|
|
120
|
+
: new TextEncoder().encode(decodeURIComponent(payload));
|
|
121
|
+
return imageResponse(bytes, mimeType);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function decodeBase64(value: string): Uint8Array<ArrayBuffer> {
|
|
128
|
+
const binary = globalThis.atob(value);
|
|
129
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
130
|
+
for (let index = 0; index < binary.length; index++) {
|
|
131
|
+
bytes[index] = binary.charCodeAt(index);
|
|
132
|
+
}
|
|
133
|
+
return bytes;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function imageResponse(
|
|
137
|
+
body: BodyInit | null,
|
|
138
|
+
mimeType: string,
|
|
139
|
+
status = 200,
|
|
140
|
+
): Response {
|
|
141
|
+
const headers = new Headers({
|
|
142
|
+
"Content-Type": mimeType,
|
|
143
|
+
"Cache-Control": "private, max-age=0, no-store",
|
|
144
|
+
"Referrer-Policy": "no-referrer",
|
|
145
|
+
"X-Content-Type-Options": "nosniff",
|
|
146
|
+
});
|
|
147
|
+
return new Response(body, { status, headers });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fetchThumbnail(sourceUrl: string): Promise<Response> {
|
|
151
|
+
let currentUrl = sourceUrl;
|
|
152
|
+
const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
|
|
153
|
+
|
|
154
|
+
for (let redirects = 0; redirects <= 4; redirects++) {
|
|
155
|
+
if (await isBlockedExtensionUrlWithDns(currentUrl)) {
|
|
156
|
+
return imageResponse(null, "text/plain", 403);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
161
|
+
let upstream: Response;
|
|
162
|
+
try {
|
|
163
|
+
const fetchOptions: RequestInit & { dispatcher?: unknown } = {
|
|
164
|
+
redirect: "manual",
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
};
|
|
167
|
+
if (dispatcher) fetchOptions.dispatcher = dispatcher;
|
|
168
|
+
upstream = await fetch(currentUrl, fetchOptions);
|
|
169
|
+
} finally {
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (upstream.status < 300 || upstream.status >= 400) {
|
|
174
|
+
const contentType =
|
|
175
|
+
upstream.headers
|
|
176
|
+
.get("content-type")
|
|
177
|
+
?.split(";", 1)[0]
|
|
178
|
+
?.trim()
|
|
179
|
+
.toLowerCase() ?? "";
|
|
180
|
+
if (!SAFE_RASTER_IMAGE_TYPES.has(contentType)) {
|
|
181
|
+
await upstream.body?.cancel().catch(() => {});
|
|
182
|
+
return imageResponse(
|
|
183
|
+
null,
|
|
184
|
+
"text/plain; charset=utf-8",
|
|
185
|
+
upstream.ok ? 415 : upstream.status,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return imageResponse(upstream.body, contentType, upstream.status);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const location = upstream.headers.get("location");
|
|
192
|
+
await upstream.body?.cancel().catch(() => {});
|
|
193
|
+
if (!location) return imageResponse(null, "text/plain", upstream.status);
|
|
194
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return imageResponse(null, "text/plain", 508);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function loadRecording(recordingId: string, event: H3Event) {
|
|
201
|
+
const access = await resolveAccess("recording", recordingId);
|
|
202
|
+
let recording = (access?.resource as ThumbnailRecording | undefined) ?? null;
|
|
203
|
+
const role = access?.role ?? null;
|
|
204
|
+
const session = await getSession(event).catch(() => null);
|
|
205
|
+
|
|
206
|
+
if (!recording) {
|
|
207
|
+
const [row] = await getDb()
|
|
208
|
+
.select({
|
|
209
|
+
id: schema.recordings.id,
|
|
210
|
+
thumbnailUrl: schema.recordings.thumbnailUrl,
|
|
211
|
+
animatedThumbnailUrl: schema.recordings.animatedThumbnailUrl,
|
|
212
|
+
expiresAt: schema.recordings.expiresAt,
|
|
213
|
+
organizationId: schema.recordings.organizationId,
|
|
214
|
+
password: schema.recordings.password,
|
|
215
|
+
visibility: schema.recordings.visibility,
|
|
216
|
+
})
|
|
217
|
+
.from(schema.recordings)
|
|
218
|
+
.where(eq(schema.recordings.id, recordingId))
|
|
219
|
+
.limit(1);
|
|
220
|
+
|
|
221
|
+
if (!row) return { error: "Not found", status: 404 } as const;
|
|
222
|
+
|
|
223
|
+
let viewerIsOrgMember = false;
|
|
224
|
+
if (session?.email && row.visibility === "org" && row.organizationId) {
|
|
225
|
+
const orgRole = await getOrganizationRoleForEmail(
|
|
226
|
+
row.organizationId,
|
|
227
|
+
session.email,
|
|
228
|
+
).catch(() => null);
|
|
229
|
+
viewerIsOrgMember = Boolean(orgRole);
|
|
230
|
+
}
|
|
231
|
+
if (row.visibility !== "public" && !viewerIsOrgMember) {
|
|
232
|
+
return { error: "Not found", status: 404 } as const;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
recording = row;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { recording, role } as const;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default defineEventHandler(async (event: H3Event) => {
|
|
242
|
+
const recordingId = getRouterParam(event, "recordingId");
|
|
243
|
+
if (!recordingId) {
|
|
244
|
+
setResponseStatus(event, 400);
|
|
245
|
+
return { error: "Missing recordingId" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const session = await getSession(event).catch(() => null);
|
|
249
|
+
const orgCtx = await getOrgContext(event).catch(() => null);
|
|
250
|
+
|
|
251
|
+
return runWithRequestContext(
|
|
252
|
+
{ userEmail: session?.email, orgId: orgCtx?.orgId ?? session?.orgId },
|
|
253
|
+
async () => {
|
|
254
|
+
const loaded = await loadRecording(recordingId, event);
|
|
255
|
+
if ("error" in loaded) {
|
|
256
|
+
setResponseStatus(event, loaded.status);
|
|
257
|
+
return { error: loaded.error };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { recording } = loaded;
|
|
261
|
+
if (recording.expiresAt) {
|
|
262
|
+
const expires = new Date(recording.expiresAt).getTime();
|
|
263
|
+
if (Number.isFinite(expires) && expires < Date.now()) {
|
|
264
|
+
setResponseStatus(event, 410);
|
|
265
|
+
return { error: "Recording has expired" };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const query = getQuery(event) as {
|
|
270
|
+
password?: unknown;
|
|
271
|
+
t?: unknown;
|
|
272
|
+
};
|
|
273
|
+
if (recording.password && loaded.role !== "owner") {
|
|
274
|
+
const queryToken = typeof query.t === "string" ? query.t : "";
|
|
275
|
+
const cookieToken = getCookie(event, cookieName(recordingId)) ?? "";
|
|
276
|
+
const password =
|
|
277
|
+
typeof query.password === "string" ? query.password : "";
|
|
278
|
+
const allowed =
|
|
279
|
+
(queryToken && verifyShortLivedToken(queryToken, recordingId).ok) ||
|
|
280
|
+
(cookieToken && verifyShortLivedToken(cookieToken, recordingId).ok) ||
|
|
281
|
+
(password && verifySharePassword(password, recording.password));
|
|
282
|
+
if (!allowed) {
|
|
283
|
+
setResponseStatus(event, 401);
|
|
284
|
+
return { error: "Password required", passwordRequired: true };
|
|
285
|
+
}
|
|
286
|
+
renewProtectedMediaCookie(event, recordingId);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sourceUrl =
|
|
290
|
+
recording.thumbnailUrl || recording.animatedThumbnailUrl;
|
|
291
|
+
if (!sourceUrl) {
|
|
292
|
+
setResponseStatus(event, 404);
|
|
293
|
+
return { error: "Thumbnail not found" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (sourceUrl.startsWith("data:")) {
|
|
297
|
+
return (
|
|
298
|
+
dataUrlResponse(sourceUrl) ??
|
|
299
|
+
imageResponse(null, "text/plain; charset=utf-8", 415)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (isRecursiveThumbnailUrl(sourceUrl, recordingId)) {
|
|
303
|
+
setResponseStatus(event, 404);
|
|
304
|
+
return { error: "Thumbnail not found" };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let resolvedSourceUrl = sourceUrl;
|
|
308
|
+
if (sourceUrl.startsWith("/")) {
|
|
309
|
+
resolvedSourceUrl = new URL(sourceUrl, getRequestURL(event).origin)
|
|
310
|
+
.href;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const response = await fetchThumbnail(resolvedSourceUrl);
|
|
315
|
+
if (response.status >= 500) {
|
|
316
|
+
captureRouteError(
|
|
317
|
+
new Error(
|
|
318
|
+
`Storage provider returned ${response.status} for thumbnail`,
|
|
319
|
+
),
|
|
320
|
+
{
|
|
321
|
+
route: "api/thumbnail",
|
|
322
|
+
tags: { upstreamStatus: String(response.status) },
|
|
323
|
+
extra: { recordingId },
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
setResponseStatus(event, 502);
|
|
327
|
+
return { error: "The recording thumbnail could not be loaded." };
|
|
328
|
+
}
|
|
329
|
+
return response;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
captureRouteError(error, {
|
|
332
|
+
route: "api/thumbnail",
|
|
333
|
+
extra: { recordingId },
|
|
334
|
+
});
|
|
335
|
+
setResponseStatus(event, 502);
|
|
336
|
+
return { error: "The recording thumbnail could not be loaded." };
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
deleteAppState,
|
|
9
10
|
readAppState,
|
|
10
11
|
writeAppState,
|
|
11
12
|
deleteAppStateByPrefix,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from "h3";
|
|
23
24
|
|
|
24
25
|
import { getDb, schema } from "../../../../db/index.js";
|
|
26
|
+
import { mediaVerificationStateKey } from "../../../../lib/media-verification-state.js";
|
|
25
27
|
import {
|
|
26
28
|
getEventOwnerContext,
|
|
27
29
|
ownerEmailMatches,
|
|
@@ -75,6 +77,25 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
75
77
|
return { ok: true, recordingId, alreadyReady: true, chunksCleared: 0 };
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
const existingUploadStateRaw = await readAppState(
|
|
81
|
+
`recording-upload-${recordingId}`,
|
|
82
|
+
).catch(() => null);
|
|
83
|
+
const existingUploadState =
|
|
84
|
+
existingUploadStateRaw && typeof existingUploadStateRaw === "object"
|
|
85
|
+
? (existingUploadStateRaw as Record<string, unknown>)
|
|
86
|
+
: {};
|
|
87
|
+
if (
|
|
88
|
+
existing.status === "processing" &&
|
|
89
|
+
existingUploadState.pendingMediaVerification === true
|
|
90
|
+
) {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
recordingId,
|
|
94
|
+
verificationPending: true,
|
|
95
|
+
chunksCleared: 0,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
const preserveRecoveryState =
|
|
79
100
|
isStoredButUnservableFinalizeError(failureReason) ||
|
|
80
101
|
isStoredButUnservableFinalizeError(existing.failureReason);
|
|
@@ -104,13 +125,6 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
104
125
|
})
|
|
105
126
|
.where(eq(schema.recordings.id, recordingId));
|
|
106
127
|
|
|
107
|
-
const existingUploadStateRaw = await readAppState(
|
|
108
|
-
`recording-upload-${recordingId}`,
|
|
109
|
-
).catch(() => null);
|
|
110
|
-
const existingUploadState =
|
|
111
|
-
existingUploadStateRaw && typeof existingUploadStateRaw === "object"
|
|
112
|
-
? (existingUploadStateRaw as Record<string, unknown>)
|
|
113
|
-
: {};
|
|
114
128
|
await writeAppState(`recording-upload-${recordingId}`, {
|
|
115
129
|
...existingUploadState,
|
|
116
130
|
recordingId,
|
|
@@ -118,6 +132,9 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
118
132
|
failureReason,
|
|
119
133
|
updatedAt: now,
|
|
120
134
|
});
|
|
135
|
+
await deleteAppState(mediaVerificationStateKey(recordingId)).catch(
|
|
136
|
+
() => {},
|
|
137
|
+
);
|
|
121
138
|
|
|
122
139
|
const cleared = preserveRecoveryState
|
|
123
140
|
? 0
|
|
@@ -101,6 +101,37 @@ function stateNumber(
|
|
|
101
101
|
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function pendingMediaVerificationState(
|
|
105
|
+
value: unknown,
|
|
106
|
+
): Record<string, unknown> | null {
|
|
107
|
+
if (!value || typeof value !== "object") return null;
|
|
108
|
+
const state = value as Record<string, unknown>;
|
|
109
|
+
return state.status === "processing" &&
|
|
110
|
+
state.pendingMediaVerification === true
|
|
111
|
+
? state
|
|
112
|
+
: null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function acceptedProcessingResponse(
|
|
116
|
+
event: H3Event,
|
|
117
|
+
recordingId: string,
|
|
118
|
+
state: Record<string, unknown>,
|
|
119
|
+
) {
|
|
120
|
+
setResponseStatus(event, 202);
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
finalized: false,
|
|
124
|
+
verificationPending: true,
|
|
125
|
+
status: "processing" as const,
|
|
126
|
+
retryAfterMs: 3_000,
|
|
127
|
+
id: recordingId,
|
|
128
|
+
videoUrl: typeof state.videoUrl === "string" ? state.videoUrl : undefined,
|
|
129
|
+
videoSizeBytes: stateNumber(state, "videoSizeBytes"),
|
|
130
|
+
sourceSizeBytes: stateNumber(state, "sourceSizeBytes"),
|
|
131
|
+
durationMs: stateNumber(state, "durationMs"),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
104
135
|
function expectedDataChunksForFinalPost(
|
|
105
136
|
index: number,
|
|
106
137
|
bodySize: number,
|
|
@@ -216,6 +247,15 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
216
247
|
throw createError({ statusCode: 404, message: "Recording not found" });
|
|
217
248
|
}
|
|
218
249
|
|
|
250
|
+
if (isFinal && existing.status === "processing") {
|
|
251
|
+
const pendingState = pendingMediaVerificationState(
|
|
252
|
+
await readAppState(`recording-upload-${recordingId}`).catch(() => null),
|
|
253
|
+
);
|
|
254
|
+
if (pendingState) {
|
|
255
|
+
return acceptedProcessingResponse(event, recordingId, pendingState);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
219
259
|
// Resumable streaming path — forward chunks directly to the provider.
|
|
220
260
|
const resumableSession = await getResumableSession(recordingId);
|
|
221
261
|
if (resumableSession && isStreamingUploadDisabled()) {
|
|
@@ -567,13 +607,17 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
567
607
|
const waitingForStorage =
|
|
568
608
|
(result as any)?.status === "waiting_storage" ||
|
|
569
609
|
(result as any)?.storageSetupRequired === true;
|
|
570
|
-
|
|
610
|
+
const verificationPending =
|
|
611
|
+
(result as any)?.status === "processing" &&
|
|
612
|
+
(result as any)?.verificationPending === true;
|
|
613
|
+
if (waitingForStorage || verificationPending) {
|
|
571
614
|
setResponseStatus(event, 202);
|
|
572
615
|
}
|
|
573
616
|
return {
|
|
574
617
|
ok: true,
|
|
575
|
-
finalized: !waitingForStorage,
|
|
618
|
+
finalized: !waitingForStorage && !verificationPending,
|
|
576
619
|
waitingForStorage,
|
|
620
|
+
...(verificationPending ? { retryAfterMs: 3_000 } : {}),
|
|
577
621
|
...result,
|
|
578
622
|
};
|
|
579
623
|
} catch (err) {
|
|
@@ -650,6 +694,16 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
650
694
|
hasCamera: committed.hasCamera,
|
|
651
695
|
};
|
|
652
696
|
}
|
|
697
|
+
if (committed?.status === "processing" && committed.videoUrl) {
|
|
698
|
+
const pendingState = pendingMediaVerificationState(
|
|
699
|
+
await readAppState(`recording-upload-${recordingId}`).catch(
|
|
700
|
+
() => null,
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
if (pendingState) {
|
|
704
|
+
return acceptedProcessingResponse(event, recordingId, pendingState);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
653
707
|
trackUploadBlockingFailure(ownerEmail, {
|
|
654
708
|
stage: "finalize_recording",
|
|
655
709
|
failureKind: "finalize_error",
|
|
@@ -657,7 +711,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
657
711
|
uploadMode: "buffered",
|
|
658
712
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
659
713
|
});
|
|
660
|
-
await db
|
|
714
|
+
const failed = await db
|
|
661
715
|
.update(schema.recordings)
|
|
662
716
|
.set({
|
|
663
717
|
status: "failed",
|
|
@@ -665,7 +719,17 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
665
719
|
err instanceof Error ? err.message : "Finalize failed",
|
|
666
720
|
updatedAt: new Date().toISOString(),
|
|
667
721
|
})
|
|
668
|
-
.where(
|
|
722
|
+
.where(
|
|
723
|
+
and(
|
|
724
|
+
eq(schema.recordings.id, recordingId),
|
|
725
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
726
|
+
eq(schema.recordings.status, "processing"),
|
|
727
|
+
),
|
|
728
|
+
)
|
|
729
|
+
.returning({ id: schema.recordings.id });
|
|
730
|
+
if (failed.length !== 1) {
|
|
731
|
+
throw err;
|
|
732
|
+
}
|
|
669
733
|
const failedUploadStateRaw = await readAppState(
|
|
670
734
|
`recording-upload-${recordingId}`,
|
|
671
735
|
).catch(() => null);
|
|
@@ -862,7 +926,16 @@ async function handleResumableChunk(
|
|
|
862
926
|
error: "Recording was cancelled before it finished saving.",
|
|
863
927
|
};
|
|
864
928
|
}
|
|
865
|
-
|
|
929
|
+
const verificationPending =
|
|
930
|
+
(result as any)?.status === "processing" &&
|
|
931
|
+
(result as any)?.verificationPending === true;
|
|
932
|
+
if (verificationPending) setResponseStatus(event, 202);
|
|
933
|
+
return {
|
|
934
|
+
ok: true,
|
|
935
|
+
finalized: !verificationPending,
|
|
936
|
+
...(verificationPending ? { retryAfterMs: 3_000 } : {}),
|
|
937
|
+
...result,
|
|
938
|
+
};
|
|
866
939
|
} catch (err) {
|
|
867
940
|
console.error(`[resumable-chunk-${recordingId}] finalize failed:`, err);
|
|
868
941
|
const db = getDb();
|
|
@@ -932,6 +1005,14 @@ async function handleResumableChunk(
|
|
|
932
1005
|
hasCamera: committed.hasCamera,
|
|
933
1006
|
};
|
|
934
1007
|
}
|
|
1008
|
+
if (committed?.status === "processing" && committed.videoUrl) {
|
|
1009
|
+
const pendingState = pendingMediaVerificationState(
|
|
1010
|
+
await readAppState(`recording-upload-${recordingId}`).catch(() => null),
|
|
1011
|
+
);
|
|
1012
|
+
if (pendingState) {
|
|
1013
|
+
return acceptedProcessingResponse(event, recordingId, pendingState);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
935
1016
|
|
|
936
1017
|
trackUploadBlockingFailure(ownerEmail, {
|
|
937
1018
|
stage: "finalize_recording",
|
|
@@ -943,7 +1024,7 @@ async function handleResumableChunk(
|
|
|
943
1024
|
const failureReason =
|
|
944
1025
|
err instanceof Error ? err.message : "Finalize failed";
|
|
945
1026
|
const failedAt = new Date().toISOString();
|
|
946
|
-
await db
|
|
1027
|
+
const failed = await db
|
|
947
1028
|
.update(schema.recordings)
|
|
948
1029
|
.set({
|
|
949
1030
|
status: "failed",
|
|
@@ -954,8 +1035,11 @@ async function handleResumableChunk(
|
|
|
954
1035
|
and(
|
|
955
1036
|
eq(schema.recordings.id, recordingId),
|
|
956
1037
|
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
1038
|
+
eq(schema.recordings.status, "processing"),
|
|
957
1039
|
),
|
|
958
|
-
)
|
|
1040
|
+
)
|
|
1041
|
+
.returning({ id: schema.recordings.id });
|
|
1042
|
+
if (failed.length !== 1) throw err;
|
|
959
1043
|
const failedUploadStateRaw = await readAppState(
|
|
960
1044
|
`recording-upload-${recordingId}`,
|
|
961
1045
|
).catch(() => null);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { readAppState } from "@agent-native/core/application-state";
|
|
1
2
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
2
3
|
import { and, eq } from "drizzle-orm";
|
|
3
4
|
import {
|
|
4
5
|
createError,
|
|
5
6
|
defineEventHandler,
|
|
6
7
|
getRouterParam,
|
|
8
|
+
setResponseHeader,
|
|
7
9
|
setResponseStatus,
|
|
8
10
|
type H3Event,
|
|
9
11
|
} from "h3";
|
|
@@ -23,6 +25,7 @@ function appPath(path: string): string {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export default defineEventHandler(async (event: H3Event) => {
|
|
28
|
+
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
26
29
|
const recordingId = getRouterParam(event, "recordingId");
|
|
27
30
|
if (!recordingId) {
|
|
28
31
|
throw createError({ statusCode: 400, message: "Missing recordingId" });
|
|
@@ -55,10 +58,21 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
55
58
|
return { error: "Not found" };
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
const uploadState = await readAppState(
|
|
62
|
+
`recording-upload-${recordingId}`,
|
|
63
|
+
).catch(() => null);
|
|
64
|
+
const verificationPending = Boolean(
|
|
65
|
+
uploadState &&
|
|
66
|
+
typeof uploadState === "object" &&
|
|
67
|
+
(uploadState as Record<string, unknown>).pendingMediaVerification ===
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
|
|
58
71
|
return {
|
|
59
72
|
recording: {
|
|
60
73
|
id: recording.id,
|
|
61
74
|
status: recording.status,
|
|
75
|
+
verificationPending,
|
|
62
76
|
videoUrl: resolvePlayerVideoUrl(recording, {
|
|
63
77
|
appPath,
|
|
64
78
|
proxyRemoteMedia: true,
|