@agent-native/core 0.69.0 → 0.70.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/README.md +15 -15
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +109 -0
- package/corpus/core/docs/design/durable-agent-runs.md +217 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +89 -0
- package/corpus/core/src/agent/model-config.ts +0 -2
- package/corpus/core/src/agent/run-loop-with-resume.ts +48 -0
- package/corpus/core/src/changelog/parse.ts +245 -0
- package/corpus/core/src/cli/changelog.ts +199 -0
- package/corpus/core/src/cli/index.ts +19 -0
- package/corpus/core/src/cli/recap.ts +24 -1
- package/corpus/core/src/cli/skills.ts +74 -47
- package/corpus/core/src/client/AgentPanel.tsx +5 -3
- package/corpus/core/src/client/AssistantChat.tsx +8 -0
- package/corpus/core/src/client/CommandMenu.tsx +183 -75
- package/corpus/core/src/client/agent-chat-adapter.ts +44 -7
- package/corpus/core/src/client/analytics.ts +181 -0
- package/corpus/core/src/client/changelog/Changelog.tsx +321 -0
- package/corpus/core/src/client/client-surface.ts +41 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +123 -12
- package/corpus/core/src/client/feedback-context.ts +4 -0
- package/corpus/core/src/client/index.ts +12 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +54 -0
- package/corpus/core/src/client/settings/useBuilderStatus.ts +82 -20
- package/corpus/core/src/client/sse-event-processor.ts +7 -1
- package/corpus/core/src/mcp/build-server.ts +57 -1
- package/corpus/core/src/mcp/embed-app.ts +197 -7
- package/corpus/core/src/mcp/oauth-route.ts +109 -13
- package/corpus/core/src/server/attribution.ts +211 -0
- package/corpus/core/src/server/auth.ts +6 -0
- package/corpus/core/src/server/better-auth-instance.ts +49 -5
- package/corpus/core/src/server/builder-browser.ts +27 -1
- package/corpus/core/src/server/core-routes-plugin.ts +136 -0
- package/corpus/core/src/server/google-oauth.ts +19 -6
- package/corpus/core/src/server/onboarding-html.ts +35 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
- package/corpus/core/src/styles/agent-conversation.css +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +22 -0
- package/corpus/templates/analytics/AGENTS.md +8 -1
- package/corpus/templates/analytics/CHANGELOG.md +10 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +99 -4
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +39 -8
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +30 -3
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +180 -144
- package/corpus/templates/analytics/app/components/layout/Header.tsx +2 -3
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +7 -5
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +221 -215
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +6 -6
- package/corpus/templates/analytics/app/lib/demo-dashboard-path.ts +1 -1
- package/corpus/templates/analytics/app/lib/last-opened.ts +2 -1
- package/corpus/templates/analytics/app/pages/Settings.tsx +4 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +2 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +101 -99
- package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +1 -1
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +18 -5
- package/corpus/templates/analytics/app/routes/catalog.tsx +1 -1
- package/corpus/templates/analytics/app/routes/dashboards.$id.tsx +9 -0
- package/corpus/templates/analytics/app/routes/traffic.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-06-23-long-dashboard-and-analysis-names-in-the-sidebar-now-truncat.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-open-any-dashboard-chart-full-screen-in-a-modal-from-the-pan.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-23-the-dashboards-and-analyses-sidebar-settings-buttons-now-sta.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +145 -0
- package/corpus/templates/analytics/server/db/index.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -2
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +1 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -2
- package/corpus/templates/analytics/server/plugins/core-routes.ts +6 -6
- package/corpus/templates/assets/CHANGELOG.md +10 -0
- package/corpus/templates/assets/actions/generate-image.ts +24 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +2 -2
- package/corpus/templates/assets/app/global.css +17 -1
- package/corpus/templates/assets/app/root.tsx +7 -1
- package/corpus/templates/assets/app/routes/_index.tsx +94 -14
- package/corpus/templates/assets/app/routes/settings.tsx +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-chat-dates-in-the-sidebar-no-longer-wrap-onto-two-lines.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-clicking-image-video-or-refine-on-the-start-screen-now-drops.md +6 -0
- package/corpus/templates/assets/changelog/2026-06-23-pick-your-image-generation-model-right-from-the-chat-model-m.md +6 -0
- package/corpus/templates/brain/CHANGELOG.md +10 -0
- package/corpus/templates/brain/app/root.tsx +7 -1
- package/corpus/templates/brain/app/routes/settings.tsx +8 -1
- package/corpus/templates/calendar/CHANGELOG.md +10 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +4 -0
- package/corpus/templates/calendar/app/root.tsx +7 -1
- package/corpus/templates/chat/CHANGELOG.md +10 -0
- package/corpus/templates/chat/app/root.tsx +7 -1
- package/corpus/templates/clips/CHANGELOG.md +10 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -20
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +33 -7
- package/corpus/templates/clips/app/components/player/sign-in-prompt-dialog.tsx +8 -0
- package/corpus/templates/clips/app/components/player/video-player.tsx +150 -20
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +72 -48
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +7 -11
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +31 -0
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +71 -0
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +35 -17
- package/corpus/templates/clips/app/root.tsx +7 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +15 -1
- package/corpus/templates/clips/app/routes/record.tsx +78 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +86 -5
- package/corpus/templates/clips/changelog/2026-06-23-clips-shared-in-slack-no-longer-show-a-could-not-start-playb.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-made-the-play-button-on-shared-and-embedded-clips-scale-with.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clip-share-links-now-show-whether-they-ll-play-inline.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-removed-select-button-from-library-hover-a-clip-to-select-it.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-camera-recordings-now-keep-the-presenter-s-face-in-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-shared-video-links-now-play-for-anyone-without-signing-in.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-skip-or-cancel-the-recording-countdown-with-buttons-beside-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-camera-bubble-now-looks-sharp-the-instant-it-ope.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-desktop-recording-widget-now-grows-downward-only-when-yo.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-the-recording-start-sound-is-now-a-softer-more-modern-chime.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +17 -8
- package/corpus/templates/clips/chrome-extension/src/background.ts +769 -180
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +177 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +466 -329
- package/corpus/templates/clips/chrome-extension/src/overlay.css +377 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.html +12 -0
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +330 -0
- package/corpus/templates/clips/chrome-extension/src/permission.html +227 -0
- package/corpus/templates/clips/chrome-extension/src/permission.ts +105 -0
- package/corpus/templates/clips/chrome-extension/src/popup.html +3 -0
- package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -2
- package/corpus/templates/clips/chrome-extension/src/styles.css +50 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +3 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +41 -24
- package/corpus/templates/clips/desktop/src/lib/bubble-webrtc.ts +119 -2
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +21 -2
- package/corpus/templates/clips/desktop/src/overlays/region-record-border.tsx +76 -0
- package/corpus/templates/clips/desktop/src/styles.css +119 -2
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +140 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +85 -6
- package/corpus/templates/clips/shared/share-attribution.ts +79 -0
- package/corpus/templates/content/AGENTS.md +20 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_database-utils.ts +15 -0
- package/corpus/templates/content/actions/_property-utils.ts +371 -4
- package/corpus/templates/content/actions/configure-document-property.ts +35 -1
- package/corpus/templates/content/actions/create-content-database.ts +5 -1
- package/corpus/templates/content/actions/delete-document-property.ts +52 -2
- package/corpus/templates/content/actions/duplicate-document-property.ts +34 -17
- package/corpus/templates/content/actions/reorder-document-property.ts +79 -0
- package/corpus/templates/content/actions/set-document-property.ts +39 -0
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +806 -0
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +226 -68
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +59 -30
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +25 -1
- package/corpus/templates/content/app/components/editor/blockFieldSaveController.ts +180 -0
- package/corpus/templates/content/app/components/editor/blockFieldSaveRegistry.ts +179 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +244 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +132 -0
- package/corpus/templates/content/app/global.css +9 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +21 -0
- package/corpus/templates/content/app/root.tsx +7 -1
- package/corpus/templates/content/server/db/schema.ts +30 -0
- package/corpus/templates/content/server/plugins/db.ts +87 -0
- package/corpus/templates/content/shared/api.ts +7 -0
- package/corpus/templates/content/shared/properties.ts +109 -0
- package/corpus/templates/design/CHANGELOG.md +10 -0
- package/corpus/templates/design/actions/generate-design.ts +4 -0
- package/corpus/templates/design/app/root.tsx +7 -1
- package/corpus/templates/design/app/routes/settings.tsx +10 -2
- package/corpus/templates/dispatch/CHANGELOG.md +10 -0
- package/corpus/templates/dispatch/app/root.tsx +7 -1
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +10 -0
- package/corpus/templates/forms/CHANGELOG.md +10 -0
- package/corpus/templates/forms/actions/export-responses.ts +6 -0
- package/corpus/templates/forms/actions/list-responses.ts +2 -0
- package/corpus/templates/forms/actions/response-insights.ts +2 -0
- package/corpus/templates/forms/app/pages/ResponsesPage.tsx +138 -1
- package/corpus/templates/forms/app/root.tsx +7 -1
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-the-page-each-submission-came-from-.md +6 -0
- package/corpus/templates/forms/changelog/2026-06-23-response-tables-now-show-whether-feedback-came-from-the-web-.md +6 -0
- package/corpus/templates/forms/server/db/schema.ts +7 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +19 -2
- package/corpus/templates/forms/server/lib/integrations.ts +76 -2
- package/corpus/templates/forms/server/plugins/db.ts +20 -0
- package/corpus/templates/forms/shared/types.ts +12 -0
- package/corpus/templates/macros/CHANGELOG.md +10 -0
- package/corpus/templates/macros/app/root.tsx +7 -1
- package/corpus/templates/mail/CHANGELOG.md +10 -0
- package/corpus/templates/mail/app/components/layout/CommandPalette.tsx +3 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +31 -17
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -4
- package/corpus/templates/plan/.agents/skills/visual-recap/SKILL.md +34 -24
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -4
- package/corpus/templates/plan/AGENTS.md +4 -0
- package/corpus/templates/plan/CHANGELOG.md +10 -0
- package/corpus/templates/plan/actions/view-screen.ts +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +98 -1
- package/corpus/templates/plan/app/components/plan/wireframe/html-artboard.css +3 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +107 -7
- package/corpus/templates/plan/app/root.tsx +7 -1
- package/corpus/templates/plan/changelog/2026-06-23-large-diagrams-in-a-plan-now-scroll-within-their-block-inste.md +6 -0
- package/corpus/templates/plan/shared/share-attribution.ts +72 -0
- package/corpus/templates/slides/CHANGELOG.md +10 -0
- package/corpus/templates/slides/app/root.tsx +7 -1
- package/corpus/templates/videos/CHANGELOG.md +4 -151
- package/corpus/templates/videos/app/root.tsx +7 -1
- package/dist/action.js +87 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/model-config.d.ts +2 -2
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +0 -2
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/run-loop-with-resume.d.ts +13 -0
- package/dist/agent/run-loop-with-resume.d.ts.map +1 -1
- package/dist/agent/run-loop-with-resume.js +44 -0
- package/dist/agent/run-loop-with-resume.js.map +1 -1
- package/dist/changelog/parse.d.ts +70 -0
- package/dist/changelog/parse.d.ts.map +1 -0
- package/dist/changelog/parse.js +200 -0
- package/dist/changelog/parse.js.map +1 -0
- package/dist/cli/changelog.d.ts +4 -0
- package/dist/cli/changelog.d.ts.map +1 -0
- package/dist/cli/changelog.js +166 -0
- package/dist/cli/changelog.js.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +22 -1
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +74 -47
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/CommandMenu.d.ts +17 -1
- package/dist/client/CommandMenu.d.ts.map +1 -1
- package/dist/client/CommandMenu.js +49 -13
- package/dist/client/CommandMenu.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +43 -6
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/analytics.d.ts +18 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +177 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/changelog/Changelog.d.ts +47 -0
- package/dist/client/changelog/Changelog.d.ts.map +1 -0
- package/dist/client/changelog/Changelog.js +138 -0
- package/dist/client/changelog/Changelog.js.map +1 -0
- package/dist/client/client-surface.d.ts +17 -0
- package/dist/client/client-surface.d.ts.map +1 -0
- package/dist/client/client-surface.js +24 -0
- package/dist/client/client-surface.js.map +1 -0
- package/dist/client/composer/TiptapComposer.d.ts +26 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +27 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/feedback-context.d.ts +3 -0
- package/dist/client/feedback-context.d.ts.map +1 -1
- package/dist/client/feedback-context.js +2 -0
- package/dist/client/feedback-context.js.map +1 -1
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +42 -1
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +2 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +58 -10
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +21 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +43 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +197 -7
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/oauth-route.d.ts.map +1 -1
- package/dist/mcp/oauth-route.js +95 -12
- package/dist/mcp/oauth-route.js.map +1 -1
- package/dist/server/attribution.d.ts +83 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +178 -0
- package/dist/server/attribution.js.map +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +7 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +9 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +32 -2
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/builder-browser.d.ts +4 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +20 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +101 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +16 -2
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/styles/agent-conversation.css +21 -0
- package/dist/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/dist/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/docs/design/durable-agent-runs.md +217 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/changelog/SKILL.md +111 -0
- package/src/templates/workspace-core/.agents/skills/reliable-mutations/SKILL.md +72 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +46 -14
- package/corpus/templates/analytics/app/pages/About.tsx +0 -108
- package/corpus/templates/analytics/app/routes/about.tsx +0 -9
- /package/corpus/templates/analytics/app/routes/{dashboards.tsx → dashboards._index.tsx} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-touch referral attribution — server side.
|
|
3
|
+
*
|
|
4
|
+
* The browser captures an anonymous visitor's *first* landing context (referral
|
|
5
|
+
* source, UTM params, referring host, landing path) and persists it across the
|
|
6
|
+
* signup boundary in a first-party cookie named `an_ft` (see
|
|
7
|
+
* `client/analytics.ts`). On signup, the auth hook reads that cookie off the
|
|
8
|
+
* request and enriches the canonical server-side `signup` event so we can
|
|
9
|
+
* measure where new users came from and how apps spread (virality).
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally pure and dependency-free so it is trivially
|
|
12
|
+
* unit-testable and can never throw into the signup path. The single hard rule:
|
|
13
|
+
* parsing untrusted cookie input must NEVER throw — every accessor is defensive.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The decoded first-touch attribution object. Mirrors the compact JSON the
|
|
18
|
+
* client writes into the `an_ft` cookie / `an_attribution` localStorage key.
|
|
19
|
+
* Every field is optional — the client omits empty fields to keep the cookie
|
|
20
|
+
* small, and a malformed/absent cookie yields `null`.
|
|
21
|
+
*/
|
|
22
|
+
export interface FirstTouchAttribution {
|
|
23
|
+
/** Referral source bucket, e.g. "clip_share", "plan_share". */
|
|
24
|
+
ref?: string;
|
|
25
|
+
/** Referrer's stable user id (the clip/plan owner who shared the link). */
|
|
26
|
+
via?: string;
|
|
27
|
+
utm_source?: string;
|
|
28
|
+
utm_medium?: string;
|
|
29
|
+
utm_campaign?: string;
|
|
30
|
+
utm_content?: string;
|
|
31
|
+
utm_term?: string;
|
|
32
|
+
/** `window.location.pathname` of the first page the visitor landed on. */
|
|
33
|
+
landing_path?: string;
|
|
34
|
+
/** Host of `document.referrer` (scrubbed; host only, never a full URL). */
|
|
35
|
+
landing_referrer?: string;
|
|
36
|
+
/** ISO timestamp of when the visitor first landed. */
|
|
37
|
+
landed_at?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Cookie name written by the client (non-HttpOnly; non-sensitive). */
|
|
41
|
+
export const FIRST_TOUCH_COOKIE_NAME = "an_ft";
|
|
42
|
+
|
|
43
|
+
const STRING_FIELDS: Array<keyof FirstTouchAttribution> = [
|
|
44
|
+
"ref",
|
|
45
|
+
"via",
|
|
46
|
+
"utm_source",
|
|
47
|
+
"utm_medium",
|
|
48
|
+
"utm_campaign",
|
|
49
|
+
"utm_content",
|
|
50
|
+
"utm_term",
|
|
51
|
+
"landing_path",
|
|
52
|
+
"landing_referrer",
|
|
53
|
+
"landed_at",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse a raw `Cookie:` header into a flat name→value map. Tolerates missing
|
|
58
|
+
* input, extra whitespace, `=` inside values, and malformed pairs. Never throws.
|
|
59
|
+
*/
|
|
60
|
+
export function parseCookieHeader(
|
|
61
|
+
cookieHeader: string | null | undefined,
|
|
62
|
+
): Record<string, string> {
|
|
63
|
+
const out: Record<string, string> = {};
|
|
64
|
+
if (!cookieHeader || typeof cookieHeader !== "string") return out;
|
|
65
|
+
for (const part of cookieHeader.split(";")) {
|
|
66
|
+
const eq = part.indexOf("=");
|
|
67
|
+
if (eq === -1) continue;
|
|
68
|
+
const name = part.slice(0, eq).trim();
|
|
69
|
+
if (!name) continue;
|
|
70
|
+
const rawValue = part.slice(eq + 1).trim();
|
|
71
|
+
// First write wins so a duplicate cookie name can't clobber the first.
|
|
72
|
+
if (name in out) continue;
|
|
73
|
+
out[name] = rawValue;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decode a single cookie value into a `FirstTouchAttribution`. The value is the
|
|
80
|
+
* URL-encoded compact JSON written by the client. Returns `null` for empty,
|
|
81
|
+
* malformed, or non-object input. Only known string fields are copied through,
|
|
82
|
+
* each clamped to a sane max length as a defense against an oversized cookie.
|
|
83
|
+
*/
|
|
84
|
+
export function decodeFirstTouchValue(
|
|
85
|
+
value: string | null | undefined,
|
|
86
|
+
): FirstTouchAttribution | null {
|
|
87
|
+
if (!value || typeof value !== "string") return null;
|
|
88
|
+
let decoded = value;
|
|
89
|
+
try {
|
|
90
|
+
decoded = decodeURIComponent(value);
|
|
91
|
+
} catch {
|
|
92
|
+
// Not valid percent-encoding — fall back to the raw value and let the JSON
|
|
93
|
+
// parse below decide whether it's usable.
|
|
94
|
+
decoded = value;
|
|
95
|
+
}
|
|
96
|
+
let parsed: unknown;
|
|
97
|
+
try {
|
|
98
|
+
parsed = JSON.parse(decoded);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const source = parsed as Record<string, unknown>;
|
|
106
|
+
const result: FirstTouchAttribution = {};
|
|
107
|
+
let any = false;
|
|
108
|
+
for (const field of STRING_FIELDS) {
|
|
109
|
+
const raw = source[field];
|
|
110
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
111
|
+
result[field] = raw.slice(0, 120);
|
|
112
|
+
any = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return any ? result : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the `an_ft` first-touch attribution out of a raw `Cookie:` header.
|
|
120
|
+
* Returns `null` when the cookie is absent or unparseable. Never throws.
|
|
121
|
+
*/
|
|
122
|
+
export function readFirstTouchAttribution(
|
|
123
|
+
cookieHeader: string | null | undefined,
|
|
124
|
+
): FirstTouchAttribution | null {
|
|
125
|
+
try {
|
|
126
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
127
|
+
return decodeFirstTouchValue(cookies[FIRST_TOUCH_COOKIE_NAME]);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExternalReferrerHost(host: string | undefined): boolean {
|
|
134
|
+
const trimmed = host?.trim();
|
|
135
|
+
return !!trimmed && trimmed.length > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Derive the `referral_source` bucket from first-touch attribution per the
|
|
140
|
+
* contract:
|
|
141
|
+
* 1. explicit `ref` wins;
|
|
142
|
+
* 2. landing path under `/share/` => "clip_share";
|
|
143
|
+
* 3. landing path that looks like a public plan page
|
|
144
|
+
* (contains `/p/`, `/plan/`, or `/share-plan/`) => "plan_share";
|
|
145
|
+
* 4. a non-empty external referring host => "external";
|
|
146
|
+
* 5. otherwise => "direct".
|
|
147
|
+
*/
|
|
148
|
+
export function deriveReferralSource(ft: FirstTouchAttribution | null): string {
|
|
149
|
+
if (ft?.ref && ft.ref.trim()) return ft.ref.trim();
|
|
150
|
+
const path = ft?.landing_path ?? "";
|
|
151
|
+
if (path.startsWith("/share/")) return "clip_share";
|
|
152
|
+
if (
|
|
153
|
+
path.includes("/p/") ||
|
|
154
|
+
path.includes("/plan/") ||
|
|
155
|
+
path.includes("/share-plan/")
|
|
156
|
+
) {
|
|
157
|
+
return "plan_share";
|
|
158
|
+
}
|
|
159
|
+
if (isExternalReferrerHost(ft?.landing_referrer)) return "external";
|
|
160
|
+
return "direct";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compute the snake_case signup-event properties from first-touch attribution.
|
|
165
|
+
* Returns a clean object with `undefined` values omitted, ready to merge into
|
|
166
|
+
* the `signup` track call. Always sets `referral_source` (defaults to "direct").
|
|
167
|
+
*
|
|
168
|
+
* Pure and total — given any (or no) input it returns a well-formed object and
|
|
169
|
+
* never throws.
|
|
170
|
+
*/
|
|
171
|
+
export function deriveSignupAttribution(
|
|
172
|
+
ft: FirstTouchAttribution | null,
|
|
173
|
+
): Record<string, string> {
|
|
174
|
+
const out: Record<string, string> = {
|
|
175
|
+
referral_source: deriveReferralSource(ft),
|
|
176
|
+
};
|
|
177
|
+
if (!ft) return out;
|
|
178
|
+
|
|
179
|
+
const setIf = (key: string, value: string | undefined) => {
|
|
180
|
+
const trimmed = value?.trim();
|
|
181
|
+
if (trimmed) out[key] = trimmed;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
setIf("referrer_user", ft.via);
|
|
185
|
+
setIf("referral_medium", ft.utm_medium);
|
|
186
|
+
setIf("referral_campaign", ft.utm_campaign);
|
|
187
|
+
setIf("utm_source", ft.utm_source);
|
|
188
|
+
setIf("utm_medium", ft.utm_medium);
|
|
189
|
+
setIf("utm_campaign", ft.utm_campaign);
|
|
190
|
+
setIf("utm_content", ft.utm_content);
|
|
191
|
+
setIf("utm_term", ft.utm_term);
|
|
192
|
+
setIf("first_touch_path", ft.landing_path);
|
|
193
|
+
setIf("landing_referrer", ft.landing_referrer);
|
|
194
|
+
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convenience: read the cookie header and derive signup attribution in one
|
|
200
|
+
* call. Never throws; falls back to `{ referral_source: "direct" }` on any
|
|
201
|
+
* error.
|
|
202
|
+
*/
|
|
203
|
+
export function signupAttributionFromCookieHeader(
|
|
204
|
+
cookieHeader: string | null | undefined,
|
|
205
|
+
): Record<string, string> {
|
|
206
|
+
try {
|
|
207
|
+
return deriveSignupAttribution(readFirstTouchAttribution(cookieHeader));
|
|
208
|
+
} catch {
|
|
209
|
+
return { referral_source: "direct" };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -1796,6 +1796,12 @@ function createAuthGuardFn(): (
|
|
|
1796
1796
|
// route tree, no per-user data.
|
|
1797
1797
|
if (p === "/__manifest") return;
|
|
1798
1798
|
if (p === "/_agent-native/speculation-rules.json") return;
|
|
1799
|
+
// Liveness probe: always public so uptime monitors and the keep-warm cron
|
|
1800
|
+
// can reach the DB-warmup route without a session. It exposes no per-user
|
|
1801
|
+
// data (just ok/db/ms) and runs a trivial `SELECT 1`. Without this bypass
|
|
1802
|
+
// the gate below 401s anonymous /_agent-native/* requests before any DB
|
|
1803
|
+
// query, so the database would never get warmed.
|
|
1804
|
+
if (p === "/_agent-native/health") return;
|
|
1799
1805
|
if (isPublicPath(normalizedUrl, publicPaths)) return;
|
|
1800
1806
|
if (shouldBypassAuthForBuilderConnect(event, p)) return;
|
|
1801
1807
|
if (isPublicWorkspacePageRequest(event, p, config)) {
|
|
@@ -23,6 +23,7 @@ import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
|
23
23
|
import { autoJoinDomainMatchingOrgs } from "../org/auto-join-domain.js";
|
|
24
24
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
25
25
|
import { flushTracking, identify, track } from "../tracking/index.js";
|
|
26
|
+
import { signupAttributionFromCookieHeader } from "./attribution.js";
|
|
26
27
|
import { TEMPLATES } from "../cli/templates-meta.js";
|
|
27
28
|
import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
|
|
28
29
|
import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
|
|
@@ -72,23 +73,41 @@ export async function trackSignupEvent({
|
|
|
72
73
|
authUserId,
|
|
73
74
|
email,
|
|
74
75
|
name,
|
|
76
|
+
attribution,
|
|
75
77
|
}: {
|
|
76
78
|
authProvider: string;
|
|
77
79
|
authUserId?: string;
|
|
78
80
|
email: string;
|
|
79
81
|
name?: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* First-touch referral attribution derived from the visitor's `an_ft`
|
|
84
|
+
* cookie (see `server/attribution.ts`). Snake_case keys such as
|
|
85
|
+
* `referral_source`, `referrer_user`, and the UTM passthrough are merged
|
|
86
|
+
* into the `signup` event so we can measure where new users came from.
|
|
87
|
+
* `undefined` values are dropped; a missing object is a clean no-op.
|
|
88
|
+
*/
|
|
89
|
+
attribution?: Record<string, string | undefined>;
|
|
80
90
|
}): Promise<void> {
|
|
81
91
|
identify(email, {
|
|
82
92
|
email,
|
|
83
93
|
name: name ?? undefined,
|
|
84
94
|
authUserId,
|
|
85
95
|
});
|
|
96
|
+
const cleanAttribution: Record<string, string> = {};
|
|
97
|
+
if (attribution) {
|
|
98
|
+
for (const [key, value] of Object.entries(attribution)) {
|
|
99
|
+
if (typeof value === "string" && value.length > 0) {
|
|
100
|
+
cleanAttribution[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
86
104
|
track(
|
|
87
105
|
"signup",
|
|
88
106
|
{
|
|
89
107
|
...resolveSignupTrackingProperties(),
|
|
90
108
|
auth_provider: authProvider,
|
|
91
109
|
...(authUserId ? { auth_user_id: authUserId } : {}),
|
|
110
|
+
...cleanAttribution,
|
|
92
111
|
},
|
|
93
112
|
{ userId: email },
|
|
94
113
|
);
|
|
@@ -929,22 +948,47 @@ async function createBetterAuthInstance(
|
|
|
929
948
|
databaseHooks: {
|
|
930
949
|
user: {
|
|
931
950
|
create: {
|
|
932
|
-
after: async (
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
951
|
+
after: async (
|
|
952
|
+
user: {
|
|
953
|
+
id?: string;
|
|
954
|
+
email?: string;
|
|
955
|
+
name?: string | null;
|
|
956
|
+
},
|
|
957
|
+
// Better Auth (1.6.x) passes the endpoint context as the 2nd arg.
|
|
958
|
+
// It carries the originating request's headers (and on OAuth
|
|
959
|
+
// signups the callback request's headers), which is where the
|
|
960
|
+
// browser's `an_ft` first-touch cookie rides in.
|
|
961
|
+
context?: {
|
|
962
|
+
headers?: Headers | null;
|
|
963
|
+
request?: { headers?: Headers | null } | null;
|
|
964
|
+
} | null,
|
|
965
|
+
) => {
|
|
937
966
|
// When a newly-created user's email has pending org invitations
|
|
938
967
|
// (common when someone is invited *before* they've signed up),
|
|
939
968
|
// auto-accept them so the user lands in the org on their very
|
|
940
969
|
// first page load instead of a blank-slate workspace.
|
|
941
970
|
const email = user?.email;
|
|
942
971
|
if (!email) return;
|
|
972
|
+
// Derive first-touch referral attribution from the request's
|
|
973
|
+
// cookie header. Never let attribution parsing throw or block
|
|
974
|
+
// signup — on any error fall back to `direct`.
|
|
975
|
+
let attribution: Record<string, string> | undefined;
|
|
976
|
+
try {
|
|
977
|
+
const cookieHeader =
|
|
978
|
+
context?.headers?.get("cookie") ??
|
|
979
|
+
context?.request?.headers?.get("cookie") ??
|
|
980
|
+
null;
|
|
981
|
+
attribution = signupAttributionFromCookieHeader(cookieHeader);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
console.error("[auth] failed to derive signup attribution", err);
|
|
984
|
+
attribution = undefined;
|
|
985
|
+
}
|
|
943
986
|
await trackSignupEvent({
|
|
944
987
|
authProvider: "better-auth",
|
|
945
988
|
authUserId: user.id,
|
|
946
989
|
email,
|
|
947
990
|
name: user.name,
|
|
991
|
+
attribution,
|
|
948
992
|
});
|
|
949
993
|
try {
|
|
950
994
|
await acceptPendingInvitationsForEmail(email);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import type { H3Event } from "h3";
|
|
3
3
|
import { getHeader } from "h3";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getAuthSecret,
|
|
6
|
+
resolveSignupTrackingIdentity,
|
|
7
|
+
} from "./better-auth-instance.js";
|
|
5
8
|
import { getAppBasePath, getOrigin } from "./google-oauth.js";
|
|
6
9
|
|
|
7
10
|
const DEFAULT_BUILDER_APP_HOST = "https://builder.io";
|
|
@@ -43,6 +46,8 @@ export const BUILDER_SIGNUP_SOURCE_PARAM = "signupSource";
|
|
|
43
46
|
export const BUILDER_AGENT_NATIVE_FLOW_PARAM = "agentNativeFlow";
|
|
44
47
|
export const BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM =
|
|
45
48
|
"agentNativeConnectSource";
|
|
49
|
+
export const BUILDER_AGENT_NATIVE_APP_PARAM = "agentNativeApp";
|
|
50
|
+
export const BUILDER_AGENT_NATIVE_TEMPLATE_PARAM = "agentNativeTemplate";
|
|
46
51
|
|
|
47
52
|
const BUILDER_STATE_TTL_MS = 10 * 60 * 1000;
|
|
48
53
|
const BUILDER_SIGNUP_SOURCE = "agent-native";
|
|
@@ -51,6 +56,8 @@ export interface BuilderConnectTrackingParams {
|
|
|
51
56
|
signupSource?: string;
|
|
52
57
|
agentNativeFlow?: string;
|
|
53
58
|
agentNativeConnectSource?: string;
|
|
59
|
+
agentNativeApp?: string;
|
|
60
|
+
agentNativeTemplate?: string;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
function cleanTrackingParam(value: unknown): string | undefined {
|
|
@@ -72,6 +79,12 @@ export function getBuilderConnectTrackingParams(
|
|
|
72
79
|
agentNativeConnectSource: cleanTrackingParam(
|
|
73
80
|
params.get(BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM),
|
|
74
81
|
),
|
|
82
|
+
agentNativeApp: cleanTrackingParam(
|
|
83
|
+
params.get(BUILDER_AGENT_NATIVE_APP_PARAM),
|
|
84
|
+
),
|
|
85
|
+
agentNativeTemplate: cleanTrackingParam(
|
|
86
|
+
params.get(BUILDER_AGENT_NATIVE_TEMPLATE_PARAM),
|
|
87
|
+
),
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
|
|
@@ -86,6 +99,12 @@ export function builderConnectTrackingProperties(
|
|
|
86
99
|
if (tracking.agentNativeConnectSource) {
|
|
87
100
|
properties.agent_native_connect_source = tracking.agentNativeConnectSource;
|
|
88
101
|
}
|
|
102
|
+
if (tracking.agentNativeApp) {
|
|
103
|
+
properties.agent_native_app = tracking.agentNativeApp;
|
|
104
|
+
}
|
|
105
|
+
if (tracking.agentNativeTemplate) {
|
|
106
|
+
properties.agent_native_template = tracking.agentNativeTemplate;
|
|
107
|
+
}
|
|
89
108
|
return properties;
|
|
90
109
|
}
|
|
91
110
|
|
|
@@ -101,6 +120,10 @@ function applyBuilderConnectTrackingParams(
|
|
|
101
120
|
if (flow) params.set(BUILDER_AGENT_NATIVE_FLOW_PARAM, flow);
|
|
102
121
|
const source = cleanTrackingParam(tracking.agentNativeConnectSource);
|
|
103
122
|
if (source) params.set(BUILDER_AGENT_NATIVE_CONNECT_SOURCE_PARAM, source);
|
|
123
|
+
const app = cleanTrackingParam(tracking.agentNativeApp);
|
|
124
|
+
if (app) params.set(BUILDER_AGENT_NATIVE_APP_PARAM, app);
|
|
125
|
+
const template = cleanTrackingParam(tracking.agentNativeTemplate);
|
|
126
|
+
if (template) params.set(BUILDER_AGENT_NATIVE_TEMPLATE_PARAM, template);
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
export interface BuilderBrowserStatus {
|
|
@@ -524,8 +547,11 @@ export function buildBuilderCliAuthUrl(
|
|
|
524
547
|
) {
|
|
525
548
|
callbackUrl.searchParams.set(BUILDER_OPENER_PARAM, requestedPreviewOrigin);
|
|
526
549
|
}
|
|
550
|
+
const identity = resolveSignupTrackingIdentity();
|
|
527
551
|
const tracking = {
|
|
528
552
|
signupSource: BUILDER_SIGNUP_SOURCE,
|
|
553
|
+
agentNativeApp: identity.app,
|
|
554
|
+
agentNativeTemplate: identity.template,
|
|
529
555
|
...options.tracking,
|
|
530
556
|
};
|
|
531
557
|
applyBuilderConnectTrackingParams(callbackUrl.searchParams, tracking);
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
MCP_EMBED_CORS_ALLOW_HEADERS,
|
|
33
33
|
shouldAllowMcpEmbedCredentials,
|
|
34
34
|
} from "../shared/mcp-embed-headers.js";
|
|
35
|
+
import { captureError } from "./capture-error.js";
|
|
35
36
|
import { handleMcpConnect } from "../mcp/connect-route.js";
|
|
36
37
|
import {
|
|
37
38
|
handleMcpOAuth,
|
|
@@ -145,6 +146,7 @@ import { isEnvVarWriteAllowed } from "./env-var-writes.js";
|
|
|
145
146
|
import { llmConnectionTrackingProperties } from "../shared/llm-connection.js";
|
|
146
147
|
import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js";
|
|
147
148
|
import { mountDbAdminRoutes } from "../db-admin/routes.js";
|
|
149
|
+
import { getDbExec } from "../db/client.js";
|
|
148
150
|
import {
|
|
149
151
|
DEFAULT_SSR_CACHE_HEADERS,
|
|
150
152
|
EMPTY_SPECULATION_RULES,
|
|
@@ -158,6 +160,40 @@ import {
|
|
|
158
160
|
export const FRAMEWORK_ROUTE_PREFIX = "/_agent-native";
|
|
159
161
|
export const FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/events`;
|
|
160
162
|
export const LEGACY_FRAMEWORK_EVENTS_ROUTE = `${FRAMEWORK_ROUTE_PREFIX}/poll-events`;
|
|
163
|
+
|
|
164
|
+
/** Result of the `/_agent-native/health` liveness + DB-warmup probe. */
|
|
165
|
+
export interface DbHealthProbeResult {
|
|
166
|
+
/** The serverless function is live and served the request. */
|
|
167
|
+
ok: true;
|
|
168
|
+
/** A trivial `SELECT 1` reached the database (false = no DB or unreachable). */
|
|
169
|
+
db: boolean;
|
|
170
|
+
/** Round-trip time of the probe in milliseconds. */
|
|
171
|
+
ms: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Run a trivial `SELECT 1` to confirm the database is reachable and, as a side
|
|
176
|
+
* effect, keep a scale-to-zero serverless database (e.g. Neon) warm. Touching
|
|
177
|
+
* the DB on a schedule prevents the multi-second cold-start that otherwise
|
|
178
|
+
* stalls the next real user request.
|
|
179
|
+
*
|
|
180
|
+
* Always resolves: an app with no database (or a momentarily unreachable one)
|
|
181
|
+
* is still live, so the probe reports `db: false` rather than throwing. The
|
|
182
|
+
* `exec` parameter is injectable purely for tests.
|
|
183
|
+
*/
|
|
184
|
+
export async function runDbHealthProbe(
|
|
185
|
+
exec: () => { execute: (sql: string) => Promise<unknown> } = getDbExec,
|
|
186
|
+
): Promise<DbHealthProbeResult> {
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
let db = false;
|
|
189
|
+
try {
|
|
190
|
+
await exec().execute("SELECT 1");
|
|
191
|
+
db = true;
|
|
192
|
+
} catch {
|
|
193
|
+
// Live even when the DB is unreachable or the app has no database.
|
|
194
|
+
}
|
|
195
|
+
return { ok: true, db, ms: Date.now() - startedAt };
|
|
196
|
+
}
|
|
161
197
|
const DEFAULT_BUILDER_WAITLIST_FORM_ID = "DYTHuM0jlV";
|
|
162
198
|
const DEFAULT_BUILDER_WAITLIST_FORMS_ORIGIN = "https://forms.agent-native.com";
|
|
163
199
|
const BUILDER_WAITLIST_FORM_SOURCE = "connect_builder_card";
|
|
@@ -550,6 +586,8 @@ export interface CoreRoutesPluginOptions {
|
|
|
550
586
|
disableSSE?: boolean;
|
|
551
587
|
/** Disable the /_agent-native/ping health check. */
|
|
552
588
|
disablePing?: boolean;
|
|
589
|
+
/** Disable the /_agent-native/health DB liveness + warmup probe. */
|
|
590
|
+
disableHealth?: boolean;
|
|
553
591
|
/** Disable the /_agent-native/application-state routes. */
|
|
554
592
|
disableAppState?: boolean;
|
|
555
593
|
/** Disable the /_agent-native/open deep-link route. */
|
|
@@ -594,6 +632,7 @@ export interface CoreRoutesPluginOptions {
|
|
|
594
632
|
* GET /_agent-native/poll — polling endpoint for change detection
|
|
595
633
|
* GET /_agent-native/events (or custom) — SSE endpoint for real-time sync
|
|
596
634
|
* GET /_agent-native/ping — health check
|
|
635
|
+
* GET /_agent-native/health — DB liveness probe + scale-to-zero warmup
|
|
597
636
|
* GET /_agent-native/env-status — env key configuration status (when envKeys provided)
|
|
598
637
|
* POST /_agent-native/env-vars — save env vars to .env (when envKeys provided)
|
|
599
638
|
* GET /_agent-native/application-state/:key — read application state
|
|
@@ -925,6 +964,20 @@ export function createCoreRoutesPlugin(
|
|
|
925
964
|
);
|
|
926
965
|
}
|
|
927
966
|
|
|
967
|
+
// Health + DB warmup — liveness probe that touches the database so
|
|
968
|
+
// uptime monitors and the keep-warm cron prevent a scale-to-zero
|
|
969
|
+
// serverless DB (e.g. Neon) from cold-starting on the next real
|
|
970
|
+
// request. Public, side-effect free, and never cached.
|
|
971
|
+
if (!options.disableHealth) {
|
|
972
|
+
getH3App(nitroApp).use(
|
|
973
|
+
`${P}/health`,
|
|
974
|
+
defineEventHandler(async (event) => {
|
|
975
|
+
setResponseHeader(event, "cache-control", "no-store");
|
|
976
|
+
return runDbHealthProbe();
|
|
977
|
+
}),
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
928
981
|
getH3App(nitroApp).use(
|
|
929
982
|
`${P}/speculation-rules.json`,
|
|
930
983
|
defineEventHandler((event) => {
|
|
@@ -1691,6 +1744,12 @@ export function createCoreRoutesPlugin(
|
|
|
1691
1744
|
agentNativeConnectSource:
|
|
1692
1745
|
connectTracking.agentNativeConnectSource ??
|
|
1693
1746
|
pending.tracking.agentNativeConnectSource,
|
|
1747
|
+
agentNativeApp:
|
|
1748
|
+
connectTracking.agentNativeApp ??
|
|
1749
|
+
pending.tracking.agentNativeApp,
|
|
1750
|
+
agentNativeTemplate:
|
|
1751
|
+
connectTracking.agentNativeTemplate ??
|
|
1752
|
+
pending.tracking.agentNativeTemplate,
|
|
1694
1753
|
};
|
|
1695
1754
|
}
|
|
1696
1755
|
if (
|
|
@@ -3103,6 +3162,83 @@ export function createCoreRoutesPlugin(
|
|
|
3103
3162
|
`${P}/embed/start`,
|
|
3104
3163
|
createEmbedStartRouteHandler({ getExistingSession: getSession }),
|
|
3105
3164
|
);
|
|
3165
|
+
|
|
3166
|
+
// POST /_agent-native/mcp/embed-error — telemetry sink for MCP App
|
|
3167
|
+
// embed shells. The shell runs in a sandboxed, opaque-origin iframe
|
|
3168
|
+
// (Codex, Cursor, ChatGPT, Claude) with no session cookie or CSRF
|
|
3169
|
+
// token, so this endpoint is intentionally unauthenticated and
|
|
3170
|
+
// CORS-open to the SAME sandbox origins as /embed/start. It forwards a
|
|
3171
|
+
// small, bounded diagnostic payload to Sentry via captureError so we
|
|
3172
|
+
// can see *why* an inline embed failed (handshake timeout, transplant
|
|
3173
|
+
// fetch status/CORS, auth, CSP) per host. Best-effort: always 204,
|
|
3174
|
+
// never throws, body capped, no client-trusted identity.
|
|
3175
|
+
getH3App(nitroApp).use(
|
|
3176
|
+
`${P}/mcp/embed-error`,
|
|
3177
|
+
defineEventHandler(async (event: H3Event) => {
|
|
3178
|
+
const origin = getHeader(event, "origin");
|
|
3179
|
+
if (origin && isMcpEmbedCorsOrigin(origin)) {
|
|
3180
|
+
setResponseHeader(event, "Access-Control-Allow-Origin", origin);
|
|
3181
|
+
setResponseHeader(event, "Vary", "Origin");
|
|
3182
|
+
setResponseHeader(
|
|
3183
|
+
event,
|
|
3184
|
+
"Access-Control-Allow-Methods",
|
|
3185
|
+
"POST,OPTIONS",
|
|
3186
|
+
);
|
|
3187
|
+
setResponseHeader(
|
|
3188
|
+
event,
|
|
3189
|
+
"Access-Control-Allow-Headers",
|
|
3190
|
+
MCP_EMBED_CORS_ALLOW_HEADERS,
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
const method = getMethod(event);
|
|
3194
|
+
if (method === "OPTIONS") {
|
|
3195
|
+
setResponseStatus(event, 204);
|
|
3196
|
+
return "";
|
|
3197
|
+
}
|
|
3198
|
+
if (method !== "POST") {
|
|
3199
|
+
setResponseStatus(event, 405);
|
|
3200
|
+
return { error: "Method not allowed" };
|
|
3201
|
+
}
|
|
3202
|
+
const body = await readBody(event).catch(() => undefined);
|
|
3203
|
+
const rec =
|
|
3204
|
+
body && typeof body === "object" && !Array.isArray(body)
|
|
3205
|
+
? (body as Record<string, unknown>)
|
|
3206
|
+
: {};
|
|
3207
|
+
const str = (value: unknown, max: number): string | undefined =>
|
|
3208
|
+
typeof value === "string" && value
|
|
3209
|
+
? value.slice(0, max)
|
|
3210
|
+
: undefined;
|
|
3211
|
+
const message = str(rec.message, 500) ?? "MCP embed failed";
|
|
3212
|
+
try {
|
|
3213
|
+
captureError(new Error(message), {
|
|
3214
|
+
route: `${P}/mcp/embed-error`,
|
|
3215
|
+
method: "POST",
|
|
3216
|
+
userAgent:
|
|
3217
|
+
str(rec.userAgent, 300) ?? getHeader(event, "user-agent"),
|
|
3218
|
+
tags: {
|
|
3219
|
+
source: "mcp-embed-shell",
|
|
3220
|
+
embed_stage: str(rec.stage, 60),
|
|
3221
|
+
embed_render_mode: str(rec.renderMode, 40),
|
|
3222
|
+
embed_host: str(rec.host, 160),
|
|
3223
|
+
embed_bridge: str(rec.bridge, 40),
|
|
3224
|
+
},
|
|
3225
|
+
extra: {
|
|
3226
|
+
embedUrl: str(rec.url, 600),
|
|
3227
|
+
httpStatus:
|
|
3228
|
+
typeof rec.status === "number"
|
|
3229
|
+
? rec.status
|
|
3230
|
+
: str(rec.status, 40),
|
|
3231
|
+
detail: str(rec.detail, 1200),
|
|
3232
|
+
origin,
|
|
3233
|
+
},
|
|
3234
|
+
});
|
|
3235
|
+
} catch {
|
|
3236
|
+
// Observability must never throw back into the request path.
|
|
3237
|
+
}
|
|
3238
|
+
setResponseStatus(event, 204);
|
|
3239
|
+
return "";
|
|
3240
|
+
}),
|
|
3241
|
+
);
|
|
3106
3242
|
}
|
|
3107
3243
|
|
|
3108
3244
|
if (!options.disableAppState) {
|
|
@@ -841,14 +841,27 @@ export function oauthCallbackResponse(
|
|
|
841
841
|
// same-origin; Builder desktop workspace returns may point back to the
|
|
842
842
|
// local loopback gateway and carry the short-lived `_session` bridge so
|
|
843
843
|
// the local app can promote the newly created hosted OAuth session.
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
"Location",
|
|
848
|
-
appendSessionToOAuthReturnUrl(opts.returnUrl, opts.sessionToken),
|
|
844
|
+
const location = appendSessionToOAuthReturnUrl(
|
|
845
|
+
opts.returnUrl,
|
|
846
|
+
opts.sessionToken,
|
|
849
847
|
);
|
|
848
|
+
setResponseStatus(event, 302);
|
|
849
|
+
setResponseHeader(event, "Location", location);
|
|
850
850
|
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
851
|
-
return
|
|
851
|
+
// Return a real 302 so the browser lands on the clean return URL instead of
|
|
852
|
+
// lingering on the provider callback URL with its `code`/`state` query
|
|
853
|
+
// params. But h3 hands a non-2xx web `Response` straight back WITHOUT merging
|
|
854
|
+
// the `Set-Cookie` staged earlier in the callback (the framework session
|
|
855
|
+
// cookie), so mirror those staged cookies onto the redirect Response —
|
|
856
|
+
// otherwise the sign-in succeeds but the browser arrives back logged out.
|
|
857
|
+
const headers = new Headers({
|
|
858
|
+
Location: location,
|
|
859
|
+
"Referrer-Policy": "no-referrer",
|
|
860
|
+
});
|
|
861
|
+
for (const cookie of event.res?.headers?.getSetCookie?.() ?? []) {
|
|
862
|
+
headers.append("set-cookie", cookie);
|
|
863
|
+
}
|
|
864
|
+
return new Response(null, { status: 302, headers });
|
|
852
865
|
}
|
|
853
866
|
|
|
854
867
|
/** HTML error page for OAuth failures. The message is HTML-escaped — most
|