@agent-native/core 0.92.7 → 0.92.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/docs/content/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/corpus/core/docs/content/pr-visual-recap.mdx +3 -3
- package/corpus/core/docs/content/pure-agent-apps.mdx +22 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/engine/builtin.ts +1 -1
- package/corpus/core/src/agent/model-config.ts +30 -30
- package/corpus/core/src/agent/production-agent.ts +129 -7
- package/corpus/core/src/cli/design-connect.ts +438 -83
- package/corpus/core/src/cli/recap.ts +2 -2
- package/corpus/core/src/client/AgentPanel.tsx +8 -1
- package/corpus/core/src/client/agent-chat-adapter.ts +12 -7
- package/corpus/core/src/client/composer/TiptapComposer.tsx +1 -1
- package/corpus/core/src/client/index.ts +4 -0
- package/corpus/core/src/client/org/TeamPage.tsx +41 -26
- package/corpus/core/src/client/use-action.ts +113 -1
- package/corpus/core/src/db/client.ts +12 -2
- package/corpus/core/src/db/create-get-db.ts +4 -1
- package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +1 -1
- package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +1 -1
- package/corpus/core/src/server/transcribe-voice.ts +1 -1
- package/corpus/core/src/usage/store.ts +13 -4
- package/corpus/core/src/vite/client.ts +38 -1
- package/corpus/templates/analytics/AGENTS.md +3 -0
- package/corpus/templates/analytics/actions/get-data-program.ts +78 -0
- package/corpus/templates/analytics/actions/list-data-programs.ts +49 -0
- package/corpus/templates/analytics/app/global.css +11 -0
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +163 -17
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +20 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +96 -3
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +61 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +40 -2
- package/corpus/templates/analytics/changelog/2026-07-09-analytics-no-longer-forgets-successful-data-queries-when-a-l.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-keep-consistent-spacing-when-they-stack-ver.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-dashboard-charts-tables-and-extensions-can-be-selected-and-d.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-09-long-running-analytics-requests-now-recover-automatically-wh.md +6 -0
- package/corpus/templates/analytics/server/db/index.ts +6 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +1 -7
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +102 -15
- package/corpus/templates/content/app/hooks/use-content-database.ts +52 -0
- package/corpus/templates/content/app/hooks/use-document-properties.ts +50 -6
- package/corpus/templates/content/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/content/app/i18n-data.ts +28 -0
- package/corpus/templates/content/changelog/2026-07-09-notion-conflict-warnings-no-longer-flash-during-normal-synce.md +6 -0
- package/corpus/templates/content/changelog/2026-07-09-property-menus-and-delete-confirmations-now-open-visibly-and.md +6 -0
- package/corpus/templates/content/server/lib/notion-sync.ts +130 -28
- package/corpus/templates/design/actions/add-breakpoint.ts +48 -68
- package/corpus/templates/design/actions/add-localhost-screens.ts +496 -61
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +2 -18
- package/corpus/templates/design/actions/apply-design-token-edit.ts +51 -82
- package/corpus/templates/design/actions/apply-tweaks.ts +63 -41
- package/corpus/templates/design/actions/capture-design-state.ts +2 -2
- package/corpus/templates/design/actions/connect-localhost.ts +58 -12
- package/corpus/templates/design/actions/create-component.ts +2 -18
- package/corpus/templates/design/actions/create-design-branch.ts +38 -18
- package/corpus/templates/design/actions/create-fusion-app.ts +15 -11
- package/corpus/templates/design/actions/delete-file.ts +19 -34
- package/corpus/templates/design/actions/deploy-design-preview.ts +59 -27
- package/corpus/templates/design/actions/deploy-fusion-app.ts +22 -15
- package/corpus/templates/design/actions/export-pdf.ts +2 -1
- package/corpus/templates/design/actions/export-zip.ts +9 -3
- package/corpus/templates/design/actions/generate-design.ts +107 -70
- package/corpus/templates/design/actions/get-component-details.ts +2 -18
- package/corpus/templates/design/actions/get-design-branch-diff.ts +2 -3
- package/corpus/templates/design/actions/get-design-surface-index.ts +2 -18
- package/corpus/templates/design/actions/get-design.ts +2 -1
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +7 -1
- package/corpus/templates/design/actions/import-design-tokens.ts +114 -76
- package/corpus/templates/design/actions/index-components.ts +2 -18
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +23 -28
- package/corpus/templates/design/actions/list-local-files.ts +9 -2
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +82 -98
- package/corpus/templates/design/actions/open-component-source.ts +2 -18
- package/corpus/templates/design/actions/open-visual-edit.ts +19 -1
- package/corpus/templates/design/actions/present-design-variants.ts +70 -70
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +4 -14
- package/corpus/templates/design/actions/read-local-file.ts +9 -2
- package/corpus/templates/design/actions/remove-breakpoint.ts +40 -61
- package/corpus/templates/design/actions/rename-screen.ts +425 -0
- package/corpus/templates/design/actions/request-localhost-write-consent.ts +12 -2
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +9 -2
- package/corpus/templates/design/actions/sync-fusion-app.ts +56 -39
- package/corpus/templates/design/actions/update-design.ts +376 -61
- package/corpus/templates/design/actions/update-file.ts +455 -188
- package/corpus/templates/design/actions/write-local-file.ts +10 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +685 -608
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +43 -15
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +576 -2015
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +62 -14
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +263 -25
- package/corpus/templates/design/app/components/design/design-canvas/annotation-submit.ts +34 -0
- package/corpus/templates/design/app/components/design/design-canvas/coordinate-transforms.ts +40 -0
- package/corpus/templates/design/app/components/design/design-canvas/creation.ts +47 -0
- package/corpus/templates/design/app/components/design/design-canvas/element-payload.ts +27 -0
- package/corpus/templates/design/app/components/design/design-canvas/embedded-frame.ts +62 -0
- package/corpus/templates/design/app/components/design/design-canvas/external-preview.ts +137 -0
- package/corpus/templates/design/app/components/design/design-canvas/file-drop.ts +10 -0
- package/corpus/templates/design/app/components/design/design-canvas/hit-test.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +23 -0
- package/corpus/templates/design/app/components/design/design-canvas/iframe-pan.ts +177 -0
- package/corpus/templates/design/app/components/design/design-canvas/motion-types.ts +7 -0
- package/corpus/templates/design/app/components/design/design-canvas/pending-text-edit.ts +62 -0
- package/corpus/templates/design/app/components/design/index.ts +2 -2
- package/corpus/templates/design/app/components/design/multi-screen/canvas-tools.ts +100 -0
- package/corpus/templates/design/app/components/design/multi-screen/coordinate-transforms.ts +81 -0
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +23 -13
- package/corpus/templates/design/app/components/design/multi-screen/culling.ts +238 -19
- package/corpus/templates/design/app/components/design/multi-screen/draft-primitives.ts +424 -0
- package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +341 -0
- package/corpus/templates/design/app/components/design/multi-screen/iframe-targeting.ts +52 -0
- package/corpus/templates/design/app/components/design/multi-screen/overview-layout.ts +62 -0
- package/corpus/templates/design/app/components/design/multi-screen/primitive-drop-target.ts +283 -0
- package/corpus/templates/design/app/components/design/multi-screen/screen-content-cache.ts +272 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +5 -1
- package/corpus/templates/design/app/components/design/multi-screen/wheel-gesture-state.ts +12 -0
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +325 -84
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +15 -1
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +119 -47
- package/corpus/templates/design/app/i18n/zh-TW.ts +9 -0
- package/corpus/templates/design/app/i18n-data.ts +223 -0
- package/corpus/templates/design/app/lib/design-save-outbox.ts +379 -0
- package/corpus/templates/design/app/lib/png-clipboard.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/canvas-primitives.ts +229 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +918 -0
- package/corpus/templates/design/app/pages/design-editor/collab-sync.ts +43 -0
- package/corpus/templates/design/app/pages/design-editor/cross-screen-text-color.ts +336 -0
- package/corpus/templates/design/app/pages/design-editor/data-operations.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/dom-guards.ts +24 -0
- package/corpus/templates/design/app/pages/design-editor/dom-utils.ts +11 -0
- package/corpus/templates/design/app/pages/design-editor/editor-session.ts +20 -0
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +408 -0
- package/corpus/templates/design/app/pages/design-editor/export-capture.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/geometry-persistence.ts +93 -0
- package/corpus/templates/design/app/pages/design-editor/history.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/layout-operations.ts +185 -0
- package/corpus/templates/design/app/pages/design-editor/motion-state.ts +309 -0
- package/corpus/templates/design/app/pages/design-editor/overview-camera.ts +438 -0
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +510 -0
- package/corpus/templates/design/app/pages/design-editor/portable-style.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +337 -0
- package/corpus/templates/design/app/pages/design-editor/style-utils.ts +5 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +92 -0
- package/corpus/templates/design/app/pages/design-editor/tweak-save.ts +87 -0
- package/corpus/templates/design/app/pages/design-editor/types.ts +36 -0
- package/corpus/templates/design/app/routes/design.$id.tsx +11 -1
- package/corpus/templates/design/changelog/2026-07-09-design-edits-canvas-positions-tools-and-tab-context-now-pers.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-figma-style-shortcuts-drawing-annotations-and-cross-screen-d.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-hand-and-scale-tools-now-keep-their-active-toolbar-identity-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-large-multi-screen-designs-now-stay-responsive-during-long-c.md +6 -0
- package/corpus/templates/design/changelog/2026-07-09-layer-drags-now-land-exactly-where-the-insertion-indicator-s.md +6 -0
- package/corpus/templates/design/server/db/index.ts +2 -52
- package/corpus/templates/design/server/db/schema.ts +16 -0
- package/corpus/templates/design/server/lib/design-data-access.ts +70 -0
- package/corpus/templates/design/server/lib/design-data-mutation.ts +249 -0
- package/corpus/templates/design/server/lib/fusion-screens.ts +89 -76
- package/corpus/templates/design/server/lib/import-design-files.ts +76 -35
- package/corpus/templates/design/server/lib/verify-write-grant.ts +7 -2
- package/corpus/templates/design/server/plugins/db.ts +19 -0
- package/corpus/templates/design/server/source-workspace.ts +12 -13
- package/corpus/templates/design/shared/resolve-tweaks.ts +15 -0
- package/corpus/templates/design/shared/screen-rename.ts +42 -0
- package/corpus/templates/design/shared/source-mode.ts +84 -4
- package/dist/agent/engine/builder-engine.d.ts +1 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builtin.js +1 -1
- package/dist/agent/engine/builtin.js.map +1 -1
- package/dist/agent/model-config.d.ts +9 -9
- package/dist/agent/model-config.d.ts.map +1 -1
- package/dist/agent/model-config.js +30 -30
- package/dist/agent/model-config.js.map +1 -1
- package/dist/agent/production-agent.d.ts +60 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +106 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/design-connect.d.ts +30 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +359 -75
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/recap.js +2 -2
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +6 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +10 -5
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/composer/TiptapComposer.js +1 -1
- package/dist/client/composer/TiptapComposer.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/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +6 -5
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/use-action.d.ts +32 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +68 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -2
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/scripts/agent-engines/manage-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js +1 -1
- package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.js +1 -1
- package/dist/server/transcribe-voice.js.map +1 -1
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +13 -4
- package/dist/usage/store.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +36 -1
- package/dist/vite/client.js.map +1 -1
- package/docs/content/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ar-SA/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/de-DE/a2a-protocol.mdx +12 -0
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/de-DE/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/es-ES/a2a-protocol.mdx +12 -0
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/es-ES/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/fr-FR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/fr-FR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/hi-IN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/hi-IN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ja-JP/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ja-JP/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/ko-KR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/ko-KR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/pt-BR/a2a-protocol.mdx +12 -0
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/pt-BR/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-CN/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-CN/pure-agent-apps.mdx +4 -0
- package/docs/content/locales/zh-TW/a2a-protocol.mdx +12 -0
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +3 -3
- package/docs/content/locales/zh-TW/pure-agent-apps.mdx +4 -0
- package/docs/content/pr-visual-recap.mdx +3 -3
- package/docs/content/pure-agent-apps.mdx +22 -0
- package/package.json +2 -2
|
@@ -64,6 +64,11 @@ export interface DesignConnectArgs {
|
|
|
64
64
|
* matches the token already stored on the user's connection row (no
|
|
65
65
|
* self-registration). Also read from AGENT_NATIVE_BRIDGE_TOKEN. */
|
|
66
66
|
bridgeToken?: string;
|
|
67
|
+
/** Read-only token used by Design browser previews. This is deliberately
|
|
68
|
+
* distinct from `bridgeToken`, which unlocks local filesystem reads/writes.
|
|
69
|
+
* When omitted it is derived one-way from bridgeToken for compatibility
|
|
70
|
+
* with existing /visual-edit launch commands. */
|
|
71
|
+
previewToken?: string;
|
|
67
72
|
json: boolean;
|
|
68
73
|
once: boolean;
|
|
69
74
|
dryRun: boolean;
|
|
@@ -77,6 +82,8 @@ export interface DesignConnectRoute {
|
|
|
77
82
|
title: string;
|
|
78
83
|
sourceFile?: string;
|
|
79
84
|
sourceKind: "react-router" | "html" | "manual";
|
|
85
|
+
screenshotUrl?: string;
|
|
86
|
+
metadata?: Record<string, unknown>;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export interface DesignConnectManifest {
|
|
@@ -112,6 +119,34 @@ export interface DesignConnectBridge {
|
|
|
112
119
|
* the manifest JSON so it is not exposed over the network via GET /manifest.
|
|
113
120
|
* The server-side grant action reads it from the running bridge instance. */
|
|
114
121
|
bridgeToken: string;
|
|
122
|
+
/** Read-only token accepted by manifest, route, snapshot, proxy, and editor
|
|
123
|
+
* bridge-registration endpoints. Safe to hand to the Design browser, but
|
|
124
|
+
* never accepted by filesystem endpoints. */
|
|
125
|
+
previewToken: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DesignConnectBridgeOptions {
|
|
129
|
+
bridgeToken?: string;
|
|
130
|
+
previewToken?: string;
|
|
131
|
+
/** Extra exact browser origins allowed to make CORS requests to the bridge.
|
|
132
|
+
* The production Design origin and loopback development origins are always
|
|
133
|
+
* recognized; custom deployments should pass their app origin here. */
|
|
134
|
+
allowedOrigins?: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const PREVIEW_TOKEN_DOMAIN = "agent-native-design-preview-v1\0";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Derive a read-only preview credential from the stronger filesystem token.
|
|
141
|
+
* The one-way hash keeps old `--bridge-token` launch commands compatible while
|
|
142
|
+
* ensuring a leaked preview credential cannot be promoted into write access.
|
|
143
|
+
*/
|
|
144
|
+
export function deriveDesignPreviewToken(bridgeToken: string): string {
|
|
145
|
+
return crypto
|
|
146
|
+
.createHash("sha256")
|
|
147
|
+
.update(PREVIEW_TOKEN_DOMAIN)
|
|
148
|
+
.update(bridgeToken)
|
|
149
|
+
.digest("hex");
|
|
115
150
|
}
|
|
116
151
|
|
|
117
152
|
function stringFlagValue(argv: string[], index: number, flag: string) {
|
|
@@ -183,6 +218,11 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
|
|
|
183
218
|
index += 1;
|
|
184
219
|
} else if (arg.startsWith("--bridge-token=")) {
|
|
185
220
|
parsed.bridgeToken = arg.slice("--bridge-token=".length);
|
|
221
|
+
} else if (arg === "--preview-token") {
|
|
222
|
+
parsed.previewToken = stringFlagValue(args, index, arg);
|
|
223
|
+
index += 1;
|
|
224
|
+
} else if (arg.startsWith("--preview-token=")) {
|
|
225
|
+
parsed.previewToken = arg.slice("--preview-token=".length);
|
|
186
226
|
} else if (arg === "--json") {
|
|
187
227
|
parsed.json = true;
|
|
188
228
|
parsed.once = true;
|
|
@@ -214,13 +254,36 @@ export function parseDesignConnectArgs(argv: string[]): DesignConnectArgs {
|
|
|
214
254
|
}
|
|
215
255
|
|
|
216
256
|
function routeId(routePath: string): string {
|
|
217
|
-
|
|
218
|
-
const slug =
|
|
257
|
+
const normalized = routePath.trim() || "/";
|
|
258
|
+
const slug = normalized
|
|
219
259
|
.replace(/^\/+/, "")
|
|
260
|
+
.replace(/\*/g, "w")
|
|
261
|
+
.replace(/:/g, "p")
|
|
262
|
+
.replace(/[[\]]/g, "")
|
|
220
263
|
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
221
264
|
.replace(/^-+|-+$/g, "")
|
|
222
265
|
.toLowerCase();
|
|
223
|
-
|
|
266
|
+
// Slugs are intentionally readable but necessarily lossy: `/foo/bar`,
|
|
267
|
+
// `/foo-bar`, and `/foo_bar` all collapse to the same text, while `/` and
|
|
268
|
+
// `/*` can collide with literal `/root` and `/wildcard` paths. Suffix every
|
|
269
|
+
// route with a stable hash of its normalized path so URL/query states and
|
|
270
|
+
// router patterns can never silently replace one another in the manifest.
|
|
271
|
+
const readable =
|
|
272
|
+
normalized === "/"
|
|
273
|
+
? "root"
|
|
274
|
+
: /^\/\*+$/.test(normalized) || !slug
|
|
275
|
+
? "wildcard"
|
|
276
|
+
: slug;
|
|
277
|
+
return `route-${readable}-${stableRoutePathHash(normalized)}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stableRoutePathHash(value: string): string {
|
|
281
|
+
let hash = 0xcbf29ce484222325n;
|
|
282
|
+
for (const character of value) {
|
|
283
|
+
hash ^= BigInt(character.codePointAt(0)!);
|
|
284
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
285
|
+
}
|
|
286
|
+
return hash.toString(36);
|
|
224
287
|
}
|
|
225
288
|
|
|
226
289
|
function titleFromRoutePath(routePath: string): string {
|
|
@@ -384,8 +447,12 @@ function isDesignConnectManifest(
|
|
|
384
447
|
|
|
385
448
|
async function fetchRunningBridgeManifest(
|
|
386
449
|
bridgeUrl: string,
|
|
450
|
+
previewToken?: string,
|
|
387
451
|
): Promise<DesignConnectManifest | null> {
|
|
388
|
-
const manifestUrl = new URL("/manifest.json", bridgeUrl)
|
|
452
|
+
const manifestUrl = new URL("/manifest.json", bridgeUrl);
|
|
453
|
+
if (previewToken) {
|
|
454
|
+
manifestUrl.searchParams.set("previewToken", previewToken);
|
|
455
|
+
}
|
|
389
456
|
const controller = new AbortController();
|
|
390
457
|
const timeout = setTimeout(() => controller.abort(), 800);
|
|
391
458
|
try {
|
|
@@ -421,6 +488,48 @@ export function designConnectManifestsTargetSameApp(
|
|
|
421
488
|
);
|
|
422
489
|
}
|
|
423
490
|
|
|
491
|
+
/** Non-sensitive stable identifier used by /health so daemon reruns can detect
|
|
492
|
+
* an already-running bridge for the same app without exposing its root path or
|
|
493
|
+
* route manifest. */
|
|
494
|
+
export function designConnectAppFingerprint(
|
|
495
|
+
manifest: Pick<DesignConnectManifest, "devServerUrl" | "rootPath">,
|
|
496
|
+
): string {
|
|
497
|
+
let devServerUrl = manifest.devServerUrl;
|
|
498
|
+
try {
|
|
499
|
+
devServerUrl = normalizeHttpUrl(devServerUrl);
|
|
500
|
+
} catch {
|
|
501
|
+
// Hash the original string for malformed legacy manifests.
|
|
502
|
+
}
|
|
503
|
+
return crypto
|
|
504
|
+
.createHash("sha256")
|
|
505
|
+
.update(`${devServerUrl}\n${path.resolve(manifest.rootPath)}`)
|
|
506
|
+
.digest("base64url")
|
|
507
|
+
.slice(0, 24);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function fetchRunningBridgeFingerprint(
|
|
511
|
+
bridgeUrl: string,
|
|
512
|
+
): Promise<string | null> {
|
|
513
|
+
const healthUrl = new URL("/health", bridgeUrl).toString();
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
const timeout = setTimeout(() => controller.abort(), 800);
|
|
516
|
+
try {
|
|
517
|
+
const response = await fetch(healthUrl, {
|
|
518
|
+
method: "GET",
|
|
519
|
+
signal: controller.signal,
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) return null;
|
|
522
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
523
|
+
return typeof body["appFingerprint"] === "string"
|
|
524
|
+
? body["appFingerprint"]
|
|
525
|
+
: null;
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
} finally {
|
|
529
|
+
clearTimeout(timeout);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
424
533
|
async function resolveDevServerUrl(url?: string): Promise<string> {
|
|
425
534
|
if (url) return normalizeHttpUrl(url);
|
|
426
535
|
for (const candidate of DEFAULT_DEV_SERVER_CANDIDATES) {
|
|
@@ -435,12 +544,76 @@ async function ensureRouteManifest(options: {
|
|
|
435
544
|
routes: DesignConnectRoute[];
|
|
436
545
|
devServerUrl: string;
|
|
437
546
|
dryRun: boolean;
|
|
438
|
-
}): Promise<{ path: string; created: boolean }> {
|
|
547
|
+
}): Promise<{ path: string; created: boolean; routes: DesignConnectRoute[] }> {
|
|
439
548
|
const manifestPath = path.isAbsolute(options.routeManifestPath)
|
|
440
549
|
? options.routeManifestPath
|
|
441
550
|
: path.join(options.root, options.routeManifestPath);
|
|
442
551
|
if (fsSync.existsSync(manifestPath)) {
|
|
443
|
-
|
|
552
|
+
try {
|
|
553
|
+
const parsed = JSON.parse(
|
|
554
|
+
await fs.readFile(manifestPath, "utf8"),
|
|
555
|
+
) as Record<string, unknown>;
|
|
556
|
+
const savedRoutes = Array.isArray(parsed.routes)
|
|
557
|
+
? parsed.routes.flatMap((value): DesignConnectRoute[] => {
|
|
558
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
559
|
+
return [];
|
|
560
|
+
}
|
|
561
|
+
const route = value as Record<string, unknown>;
|
|
562
|
+
if (typeof route.path !== "string" || !route.path.trim()) return [];
|
|
563
|
+
const sourceKind =
|
|
564
|
+
route.sourceKind === "react-router" ||
|
|
565
|
+
route.sourceKind === "html" ||
|
|
566
|
+
route.sourceKind === "manual"
|
|
567
|
+
? route.sourceKind
|
|
568
|
+
: "manual";
|
|
569
|
+
return [
|
|
570
|
+
{
|
|
571
|
+
id:
|
|
572
|
+
typeof route.id === "string" && route.id.trim()
|
|
573
|
+
? route.id
|
|
574
|
+
: routeId(route.path),
|
|
575
|
+
path: route.path,
|
|
576
|
+
title:
|
|
577
|
+
typeof route.title === "string" && route.title.trim()
|
|
578
|
+
? route.title
|
|
579
|
+
: titleFromRoutePath(route.path),
|
|
580
|
+
sourceFile:
|
|
581
|
+
typeof route.sourceFile === "string"
|
|
582
|
+
? route.sourceFile
|
|
583
|
+
: undefined,
|
|
584
|
+
sourceKind,
|
|
585
|
+
screenshotUrl:
|
|
586
|
+
typeof route.screenshotUrl === "string"
|
|
587
|
+
? route.screenshotUrl
|
|
588
|
+
: undefined,
|
|
589
|
+
metadata:
|
|
590
|
+
route.metadata &&
|
|
591
|
+
typeof route.metadata === "object" &&
|
|
592
|
+
!Array.isArray(route.metadata)
|
|
593
|
+
? (route.metadata as Record<string, unknown>)
|
|
594
|
+
: undefined,
|
|
595
|
+
},
|
|
596
|
+
];
|
|
597
|
+
})
|
|
598
|
+
: [];
|
|
599
|
+
if (savedRoutes.length > 0) {
|
|
600
|
+
const savedPaths = new Set(savedRoutes.map((route) => route.path));
|
|
601
|
+
return {
|
|
602
|
+
path: manifestPath,
|
|
603
|
+
created: false,
|
|
604
|
+
// Keep manual/custom route order and metadata while still surfacing
|
|
605
|
+
// newly discovered routes on subsequent bridge starts.
|
|
606
|
+
routes: [
|
|
607
|
+
...savedRoutes,
|
|
608
|
+
...options.routes.filter((route) => !savedPaths.has(route.path)),
|
|
609
|
+
],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
// Never overwrite a malformed/custom file. Route discovery remains a
|
|
614
|
+
// safe runtime fallback and the user can repair the manifest in place.
|
|
615
|
+
}
|
|
616
|
+
return { path: manifestPath, created: false, routes: options.routes };
|
|
444
617
|
}
|
|
445
618
|
if (!options.dryRun) {
|
|
446
619
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
@@ -461,7 +634,11 @@ async function ensureRouteManifest(options: {
|
|
|
461
634
|
"utf8",
|
|
462
635
|
);
|
|
463
636
|
}
|
|
464
|
-
return {
|
|
637
|
+
return {
|
|
638
|
+
path: manifestPath,
|
|
639
|
+
created: !options.dryRun,
|
|
640
|
+
routes: options.routes,
|
|
641
|
+
};
|
|
465
642
|
}
|
|
466
643
|
|
|
467
644
|
export async function prepareDesignConnectManifest(
|
|
@@ -471,15 +648,16 @@ export async function prepareDesignConnectManifest(
|
|
|
471
648
|
const port = options.port ?? DEFAULT_BRIDGE_PORT;
|
|
472
649
|
const devServerUrl = await resolveDevServerUrl(options.url);
|
|
473
650
|
const bridgeUrl = `http://127.0.0.1:${port}`;
|
|
474
|
-
const
|
|
651
|
+
const discoveredRoutes = discoverDesignRoutes(root);
|
|
475
652
|
const routeManifest = await ensureRouteManifest({
|
|
476
653
|
root,
|
|
477
654
|
routeManifestPath: options.routeManifest ?? ROUTE_MANIFEST_FILE,
|
|
478
|
-
routes,
|
|
655
|
+
routes: discoveredRoutes,
|
|
479
656
|
devServerUrl,
|
|
480
657
|
dryRun: Boolean(options.dryRun),
|
|
481
658
|
});
|
|
482
659
|
const generatedAt = new Date().toISOString();
|
|
660
|
+
const routes = routeManifest.routes;
|
|
483
661
|
|
|
484
662
|
return {
|
|
485
663
|
version: 1,
|
|
@@ -511,6 +689,73 @@ export async function prepareDesignConnectManifest(
|
|
|
511
689
|
};
|
|
512
690
|
}
|
|
513
691
|
|
|
692
|
+
const BRIDGE_CORS_HEADERS = Symbol("agent-native-design-bridge-cors");
|
|
693
|
+
|
|
694
|
+
type CorsAwareResponse = ServerResponse & {
|
|
695
|
+
[BRIDGE_CORS_HEADERS]?: Record<string, string>;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
function isLoopbackOrigin(parsed: URL): boolean {
|
|
699
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
700
|
+
return (
|
|
701
|
+
hostname === "localhost" ||
|
|
702
|
+
hostname === "::1" ||
|
|
703
|
+
/^127(?:\.\d{1,3}){3}$/.test(hostname)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function isApprovedDesignOrigin(
|
|
708
|
+
rawOrigin: string,
|
|
709
|
+
configuredOrigins: ReadonlySet<string>,
|
|
710
|
+
): boolean {
|
|
711
|
+
let parsed: URL;
|
|
712
|
+
try {
|
|
713
|
+
parsed = new URL(rawOrigin);
|
|
714
|
+
} catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
if (parsed.origin !== rawOrigin) return false;
|
|
718
|
+
if (configuredOrigins.has(parsed.origin)) return true;
|
|
719
|
+
if (
|
|
720
|
+
(parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
721
|
+
isLoopbackOrigin(parsed)
|
|
722
|
+
) {
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
return (
|
|
726
|
+
parsed.protocol === "https:" &&
|
|
727
|
+
(parsed.hostname === "design.agent-native.com" ||
|
|
728
|
+
parsed.hostname.endsWith(".design.agent-native.com"))
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function configureBridgeCors(
|
|
733
|
+
req: IncomingMessage,
|
|
734
|
+
res: ServerResponse,
|
|
735
|
+
configuredOrigins: ReadonlySet<string>,
|
|
736
|
+
): boolean {
|
|
737
|
+
const origin =
|
|
738
|
+
typeof req.headers.origin === "string" ? req.headers.origin : "";
|
|
739
|
+
const approved = origin
|
|
740
|
+
? isApprovedDesignOrigin(origin, configuredOrigins)
|
|
741
|
+
: false;
|
|
742
|
+
(res as CorsAwareResponse)[BRIDGE_CORS_HEADERS] = approved
|
|
743
|
+
? {
|
|
744
|
+
"access-control-allow-origin": origin,
|
|
745
|
+
"access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
|
|
746
|
+
"access-control-allow-headers":
|
|
747
|
+
"content-type, x-bridge-token, x-design-preview-token",
|
|
748
|
+
"access-control-allow-private-network": "true",
|
|
749
|
+
vary: "Origin",
|
|
750
|
+
}
|
|
751
|
+
: {};
|
|
752
|
+
return approved;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function bridgeCorsHeaders(res: ServerResponse): Record<string, string> {
|
|
756
|
+
return (res as CorsAwareResponse)[BRIDGE_CORS_HEADERS] ?? {};
|
|
757
|
+
}
|
|
758
|
+
|
|
514
759
|
function sendJson(
|
|
515
760
|
res: ServerResponse,
|
|
516
761
|
statusCode: number,
|
|
@@ -518,10 +763,7 @@ function sendJson(
|
|
|
518
763
|
) {
|
|
519
764
|
res.writeHead(statusCode, {
|
|
520
765
|
"content-type": "application/json; charset=utf-8",
|
|
521
|
-
|
|
522
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
523
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
524
|
-
"access-control-allow-private-network": "true",
|
|
766
|
+
...bridgeCorsHeaders(res),
|
|
525
767
|
});
|
|
526
768
|
res.end(`${JSON.stringify(body, null, 2)}\n`);
|
|
527
769
|
}
|
|
@@ -534,10 +776,7 @@ function sendText(
|
|
|
534
776
|
) {
|
|
535
777
|
res.writeHead(statusCode, {
|
|
536
778
|
"content-type": contentType,
|
|
537
|
-
|
|
538
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
539
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
540
|
-
"access-control-allow-private-network": "true",
|
|
779
|
+
...bridgeCorsHeaders(res),
|
|
541
780
|
});
|
|
542
781
|
res.end(body);
|
|
543
782
|
}
|
|
@@ -549,10 +788,7 @@ function sendBytes(
|
|
|
549
788
|
headers: Headers,
|
|
550
789
|
) {
|
|
551
790
|
const responseHeaders: Record<string, string> = {
|
|
552
|
-
|
|
553
|
-
"access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
|
|
554
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
555
|
-
"access-control-allow-private-network": "true",
|
|
791
|
+
...bridgeCorsHeaders(res),
|
|
556
792
|
};
|
|
557
793
|
for (const name of [
|
|
558
794
|
"content-type",
|
|
@@ -575,6 +811,28 @@ function sameOrigin(a: string, b: string): boolean {
|
|
|
575
811
|
}
|
|
576
812
|
}
|
|
577
813
|
|
|
814
|
+
function constantTimeTokenMatches(
|
|
815
|
+
providedToken: string,
|
|
816
|
+
expectedToken: string,
|
|
817
|
+
): boolean {
|
|
818
|
+
try {
|
|
819
|
+
return (
|
|
820
|
+
providedToken.length === expectedToken.length &&
|
|
821
|
+
crypto.timingSafeEqual(
|
|
822
|
+
Buffer.from(providedToken, "utf8"),
|
|
823
|
+
Buffer.from(expectedToken, "utf8"),
|
|
824
|
+
)
|
|
825
|
+
);
|
|
826
|
+
} catch {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function readHeader(req: IncomingMessage, name: string): string {
|
|
832
|
+
const value = req.headers[name];
|
|
833
|
+
return typeof value === "string" ? value : "";
|
|
834
|
+
}
|
|
835
|
+
|
|
578
836
|
function resolvePreviewSnapshotUrl(
|
|
579
837
|
devServerUrl: string,
|
|
580
838
|
rawUrl: string | null,
|
|
@@ -1224,34 +1482,83 @@ async function walkBridgeFiles(rootPath: string): Promise<ListFilesResult> {
|
|
|
1224
1482
|
|
|
1225
1483
|
export async function startDesignConnectBridge(
|
|
1226
1484
|
manifest: DesignConnectManifest,
|
|
1227
|
-
|
|
1485
|
+
seedOrOptions?: string | DesignConnectBridgeOptions,
|
|
1228
1486
|
): Promise<DesignConnectBridge> {
|
|
1229
1487
|
// Shared secret the browser sends (x-bridge-token) to unlock live-edit/read/
|
|
1230
1488
|
// write. Bridge and the user's connection row must agree on it. Adopt a
|
|
1231
1489
|
// server-minted seed when given (MCP flow); otherwise mint one and rely on
|
|
1232
1490
|
// --app-url self-registration to push it up. Kept in-process, never served.
|
|
1491
|
+
const options: DesignConnectBridgeOptions =
|
|
1492
|
+
typeof seedOrOptions === "string"
|
|
1493
|
+
? { bridgeToken: seedOrOptions }
|
|
1494
|
+
: (seedOrOptions ?? {});
|
|
1233
1495
|
const bridgeToken =
|
|
1234
|
-
|
|
1496
|
+
options.bridgeToken ||
|
|
1235
1497
|
process.env["AGENT_NATIVE_BRIDGE_TOKEN"] ||
|
|
1236
1498
|
crypto.randomBytes(32).toString("hex");
|
|
1499
|
+
const previewToken =
|
|
1500
|
+
options.previewToken ||
|
|
1501
|
+
process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
|
|
1502
|
+
deriveDesignPreviewToken(bridgeToken);
|
|
1503
|
+
const configuredOrigins = new Set(
|
|
1504
|
+
(options.allowedOrigins ?? []).flatMap((raw): string[] => {
|
|
1505
|
+
try {
|
|
1506
|
+
return [new URL(raw).origin];
|
|
1507
|
+
} catch {
|
|
1508
|
+
return [];
|
|
1509
|
+
}
|
|
1510
|
+
}),
|
|
1511
|
+
);
|
|
1237
1512
|
let liveEditBridgeScript = "";
|
|
1513
|
+
// One bridge process serves every URL-backed screen in an overview. The
|
|
1514
|
+
// editor script carries screen-specific state (notably screenId), so a
|
|
1515
|
+
// single global slot lets parallel iframe registrations overwrite each
|
|
1516
|
+
// other and boot a frame with another frame's identity. Keep keyed scripts
|
|
1517
|
+
// for modern clients while retaining the unkeyed slot for older clients.
|
|
1518
|
+
const liveEditBridgeScripts = new Map<string, string>();
|
|
1238
1519
|
|
|
1239
1520
|
const server = http.createServer(
|
|
1240
1521
|
(req: IncomingMessage, res: ServerResponse) => {
|
|
1522
|
+
const corsApproved = configureBridgeCors(req, res, configuredOrigins);
|
|
1241
1523
|
if (req.method === "OPTIONS") {
|
|
1242
|
-
sendJson(
|
|
1524
|
+
sendJson(
|
|
1525
|
+
res,
|
|
1526
|
+
corsApproved ? 204 : 403,
|
|
1527
|
+
corsApproved
|
|
1528
|
+
? {}
|
|
1529
|
+
: { ok: false, error: "origin is not allowed by this bridge" },
|
|
1530
|
+
);
|
|
1243
1531
|
return;
|
|
1244
1532
|
}
|
|
1245
1533
|
|
|
1246
|
-
const
|
|
1534
|
+
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1535
|
+
const pathname = requestUrl.pathname;
|
|
1536
|
+
const providedPreviewToken =
|
|
1537
|
+
readHeader(req, "x-design-preview-token") ||
|
|
1538
|
+
requestUrl.searchParams.get("previewToken") ||
|
|
1539
|
+
"";
|
|
1540
|
+
const previewTokenValid = constantTimeTokenMatches(
|
|
1541
|
+
providedPreviewToken,
|
|
1542
|
+
previewToken,
|
|
1543
|
+
);
|
|
1544
|
+
const rejectInvalidPreviewToken = (): boolean => {
|
|
1545
|
+
if (previewTokenValid) return false;
|
|
1546
|
+
sendJson(res, 401, {
|
|
1547
|
+
ok: false,
|
|
1548
|
+
error: "invalid or missing preview token",
|
|
1549
|
+
});
|
|
1550
|
+
return true;
|
|
1551
|
+
};
|
|
1247
1552
|
|
|
1248
|
-
// ──
|
|
1553
|
+
// ── Read-only preview routes (preview token required) ────────────────
|
|
1249
1554
|
|
|
1250
1555
|
if (pathname === "/" || pathname === "/manifest.json") {
|
|
1556
|
+
if (rejectInvalidPreviewToken()) return;
|
|
1251
1557
|
sendJson(res, 200, manifest as unknown as Record<string, unknown>);
|
|
1252
1558
|
return;
|
|
1253
1559
|
}
|
|
1254
1560
|
if (pathname === "/routes.json") {
|
|
1561
|
+
if (rejectInvalidPreviewToken()) return;
|
|
1255
1562
|
sendJson(res, 200, {
|
|
1256
1563
|
version: 1,
|
|
1257
1564
|
sourceType: "localhost",
|
|
@@ -1263,7 +1570,11 @@ export async function startDesignConnectBridge(
|
|
|
1263
1570
|
return;
|
|
1264
1571
|
}
|
|
1265
1572
|
if (pathname === "/health") {
|
|
1266
|
-
sendJson(res, 200, {
|
|
1573
|
+
sendJson(res, 200, {
|
|
1574
|
+
ok: true,
|
|
1575
|
+
source: manifest.source,
|
|
1576
|
+
appFingerprint: designConnectAppFingerprint(manifest),
|
|
1577
|
+
});
|
|
1267
1578
|
return;
|
|
1268
1579
|
}
|
|
1269
1580
|
if (pathname === "/live-edit-bridge") {
|
|
@@ -1271,42 +1582,54 @@ export async function startDesignConnectBridge(
|
|
|
1271
1582
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1272
1583
|
return;
|
|
1273
1584
|
}
|
|
1274
|
-
|
|
1275
|
-
const providedToken =
|
|
1276
|
-
typeof tokenHeader === "string" ? tokenHeader : "";
|
|
1277
|
-
let tokenValid = false;
|
|
1278
|
-
try {
|
|
1279
|
-
tokenValid =
|
|
1280
|
-
providedToken.length === bridgeToken.length &&
|
|
1281
|
-
crypto.timingSafeEqual(
|
|
1282
|
-
Buffer.from(providedToken, "utf8"),
|
|
1283
|
-
Buffer.from(bridgeToken, "utf8"),
|
|
1284
|
-
);
|
|
1285
|
-
} catch {
|
|
1286
|
-
tokenValid = false;
|
|
1287
|
-
}
|
|
1288
|
-
if (!tokenValid) {
|
|
1289
|
-
sendJson(res, 401, {
|
|
1290
|
-
ok: false,
|
|
1291
|
-
error: "invalid or missing bridge token",
|
|
1292
|
-
});
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1585
|
+
if (rejectInvalidPreviewToken()) return;
|
|
1295
1586
|
void (async () => {
|
|
1296
1587
|
try {
|
|
1297
1588
|
const raw = await readRequestBody(req);
|
|
1298
1589
|
const body = JSON.parse(raw) as Record<string, unknown>;
|
|
1299
1590
|
const script =
|
|
1300
1591
|
typeof body["script"] === "string" ? body["script"] : "";
|
|
1301
|
-
|
|
1592
|
+
const bridgeKey =
|
|
1593
|
+
typeof body["bridgeKey"] === "string"
|
|
1594
|
+
? body["bridgeKey"].trim()
|
|
1595
|
+
: "";
|
|
1596
|
+
const installsSupportedDesignBridge =
|
|
1597
|
+
script.includes("agent-native:editor-chrome-ready") ||
|
|
1598
|
+
script.includes("embedded-canvas-pan");
|
|
1599
|
+
if (!installsSupportedDesignBridge) {
|
|
1600
|
+
sendJson(res, 400, {
|
|
1601
|
+
ok: false,
|
|
1602
|
+
error:
|
|
1603
|
+
"script must install an approved Agent Native editor or canvas-pan bridge",
|
|
1604
|
+
});
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (
|
|
1608
|
+
bridgeKey &&
|
|
1609
|
+
(!/^[a-zA-Z0-9:._-]+$/.test(bridgeKey) || bridgeKey.length > 128)
|
|
1610
|
+
) {
|
|
1302
1611
|
sendJson(res, 400, {
|
|
1303
1612
|
ok: false,
|
|
1304
|
-
error: "
|
|
1613
|
+
error: "bridgeKey must be 1-128 safe identifier characters",
|
|
1305
1614
|
});
|
|
1306
1615
|
return;
|
|
1307
1616
|
}
|
|
1308
1617
|
liveEditBridgeScript = script;
|
|
1309
|
-
|
|
1618
|
+
if (bridgeKey) {
|
|
1619
|
+
liveEditBridgeScripts.delete(bridgeKey);
|
|
1620
|
+
liveEditBridgeScripts.set(bridgeKey, script);
|
|
1621
|
+
// Bound the in-memory cache. Normal editor usage has one key per
|
|
1622
|
+
// visible screen; 128 also leaves ample room for mode changes.
|
|
1623
|
+
while (liveEditBridgeScripts.size > 128) {
|
|
1624
|
+
const oldest = liveEditBridgeScripts.keys().next().value;
|
|
1625
|
+
if (typeof oldest !== "string") break;
|
|
1626
|
+
liveEditBridgeScripts.delete(oldest);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
sendJson(res, 200, {
|
|
1630
|
+
ok: true,
|
|
1631
|
+
...(bridgeKey ? { bridgeKey } : {}),
|
|
1632
|
+
});
|
|
1310
1633
|
} catch (err: unknown) {
|
|
1311
1634
|
sendJson(res, 400, {
|
|
1312
1635
|
ok: false,
|
|
@@ -1321,9 +1644,9 @@ export async function startDesignConnectBridge(
|
|
|
1321
1644
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1322
1645
|
return;
|
|
1323
1646
|
}
|
|
1647
|
+
if (rejectInvalidPreviewToken()) return;
|
|
1324
1648
|
void (async () => {
|
|
1325
1649
|
try {
|
|
1326
|
-
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1327
1650
|
const targetUrl = resolvePreviewSnapshotUrl(
|
|
1328
1651
|
manifest.devServerUrl,
|
|
1329
1652
|
requestUrl.searchParams.get("url") ??
|
|
@@ -1335,6 +1658,23 @@ export async function startDesignConnectBridge(
|
|
|
1335
1658
|
);
|
|
1336
1659
|
const includeEditorBridge =
|
|
1337
1660
|
requestUrl.searchParams.get("bridge") !== "0";
|
|
1661
|
+
const requestedBridgeKey =
|
|
1662
|
+
requestUrl.searchParams.get("bridgeKey")?.trim() ?? "";
|
|
1663
|
+
const editorBridgeScript = requestedBridgeKey
|
|
1664
|
+
? (liveEditBridgeScripts.get(requestedBridgeKey) ?? "")
|
|
1665
|
+
: liveEditBridgeScript;
|
|
1666
|
+
if (
|
|
1667
|
+
includeEditorBridge &&
|
|
1668
|
+
requestedBridgeKey &&
|
|
1669
|
+
!editorBridgeScript
|
|
1670
|
+
) {
|
|
1671
|
+
sendJson(res, 409, {
|
|
1672
|
+
ok: false,
|
|
1673
|
+
error:
|
|
1674
|
+
"The requested live-edit bridge script is not registered. Reload the Design frame to register it again.",
|
|
1675
|
+
});
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1338
1678
|
// The dev server route the SPA must boot on (e.g. "/todo"), taken
|
|
1339
1679
|
// from the resolved snapshot target rather than the bridge's own
|
|
1340
1680
|
// "/live-edit" request path.
|
|
@@ -1344,7 +1684,7 @@ export async function startDesignConnectBridge(
|
|
|
1344
1684
|
const html = injectLiveEditBridge(
|
|
1345
1685
|
snapshot.html,
|
|
1346
1686
|
new URL("/", manifest.bridgeUrl).toString(),
|
|
1347
|
-
includeEditorBridge ?
|
|
1687
|
+
includeEditorBridge ? editorBridgeScript : "",
|
|
1348
1688
|
targetPath,
|
|
1349
1689
|
);
|
|
1350
1690
|
sendText(
|
|
@@ -1369,9 +1709,9 @@ export async function startDesignConnectBridge(
|
|
|
1369
1709
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1370
1710
|
return;
|
|
1371
1711
|
}
|
|
1712
|
+
if (rejectInvalidPreviewToken()) return;
|
|
1372
1713
|
void (async () => {
|
|
1373
1714
|
try {
|
|
1374
|
-
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1375
1715
|
const targetUrl = resolvePreviewSnapshotUrl(
|
|
1376
1716
|
manifest.devServerUrl,
|
|
1377
1717
|
requestUrl.searchParams.get("url") ??
|
|
@@ -1413,20 +1753,8 @@ export async function startDesignConnectBridge(
|
|
|
1413
1753
|
}
|
|
1414
1754
|
|
|
1415
1755
|
// Authenticate with constant-time comparison to prevent timing attacks.
|
|
1416
|
-
const
|
|
1417
|
-
const providedToken
|
|
1418
|
-
typeof tokenHeader === "string" ? tokenHeader : "";
|
|
1419
|
-
let tokenValid = false;
|
|
1420
|
-
try {
|
|
1421
|
-
tokenValid =
|
|
1422
|
-
providedToken.length === bridgeToken.length &&
|
|
1423
|
-
crypto.timingSafeEqual(
|
|
1424
|
-
Buffer.from(providedToken, "utf8"),
|
|
1425
|
-
Buffer.from(bridgeToken, "utf8"),
|
|
1426
|
-
);
|
|
1427
|
-
} catch {
|
|
1428
|
-
tokenValid = false;
|
|
1429
|
-
}
|
|
1756
|
+
const providedToken = readHeader(req, "x-bridge-token");
|
|
1757
|
+
const tokenValid = constantTimeTokenMatches(providedToken, bridgeToken);
|
|
1430
1758
|
if (!tokenValid) {
|
|
1431
1759
|
sendJson(res, 401, {
|
|
1432
1760
|
ok: false,
|
|
@@ -1644,6 +1972,15 @@ export async function startDesignConnectBridge(
|
|
|
1644
1972
|
}
|
|
1645
1973
|
|
|
1646
1974
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
1975
|
+
const sameOriginPreviewSubresource =
|
|
1976
|
+
readHeader(req, "sec-fetch-site") === "same-origin";
|
|
1977
|
+
if (!previewTokenValid && !sameOriginPreviewSubresource) {
|
|
1978
|
+
sendJson(res, 401, {
|
|
1979
|
+
ok: false,
|
|
1980
|
+
error: "invalid or missing preview token",
|
|
1981
|
+
});
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1647
1984
|
void (async () => {
|
|
1648
1985
|
try {
|
|
1649
1986
|
const targetUrl = resolvePreviewProxyUrl(
|
|
@@ -1682,7 +2019,7 @@ export async function startDesignConnectBridge(
|
|
|
1682
2019
|
});
|
|
1683
2020
|
});
|
|
1684
2021
|
|
|
1685
|
-
return { server, manifest, bridgeToken };
|
|
2022
|
+
return { server, manifest, bridgeToken, previewToken };
|
|
1686
2023
|
}
|
|
1687
2024
|
|
|
1688
2025
|
/**
|
|
@@ -1741,7 +2078,7 @@ export async function registerConnectionWithServer(
|
|
|
1741
2078
|
authToken?: string,
|
|
1742
2079
|
): Promise<void> {
|
|
1743
2080
|
const endpoint = `${appUrl}/_agent-native/actions/connect-localhost`;
|
|
1744
|
-
const { manifest, bridgeToken } = bridge;
|
|
2081
|
+
const { manifest, bridgeToken, previewToken } = bridge;
|
|
1745
2082
|
const payload = {
|
|
1746
2083
|
devServerUrl: manifest.devServerUrl,
|
|
1747
2084
|
bridgeUrl: manifest.bridgeUrl,
|
|
@@ -1761,6 +2098,7 @@ export async function registerConnectionWithServer(
|
|
|
1761
2098
|
// row. grant-localhost-write-consent then reads it from the row instead of
|
|
1762
2099
|
// minting its own unrelated token, which would always produce a 401.
|
|
1763
2100
|
bridgeToken,
|
|
2101
|
+
previewToken,
|
|
1764
2102
|
status: "connected" as const,
|
|
1765
2103
|
};
|
|
1766
2104
|
|
|
@@ -1814,6 +2152,10 @@ Options:
|
|
|
1814
2152
|
/visual-edit flow so the bridge and the user's stored
|
|
1815
2153
|
connection token match with no self-registration.
|
|
1816
2154
|
(also reads AGENT_NATIVE_BRIDGE_TOKEN env)
|
|
2155
|
+
--preview-token <token> Adopt the paired read-only browser preview token.
|
|
2156
|
+
Optional when --bridge-token is present: compatible
|
|
2157
|
+
clients derive the same one-way token automatically.
|
|
2158
|
+
(also reads AGENT_NATIVE_PREVIEW_TOKEN env)
|
|
1817
2159
|
--daemon Start the bridge detached, wait for /health, then exit
|
|
1818
2160
|
--json Print the manifest JSON and exit
|
|
1819
2161
|
--once Prepare/scaffold the manifest and exit
|
|
@@ -1869,19 +2211,24 @@ function resolveCurrentCliInvocation(argv: string[]): {
|
|
|
1869
2211
|
async function startDetachedDesignBridge(
|
|
1870
2212
|
argv: string[],
|
|
1871
2213
|
manifest: DesignConnectManifest,
|
|
2214
|
+
previewToken?: string,
|
|
1872
2215
|
): Promise<number> {
|
|
1873
2216
|
if (await waitForBridgeHealth(manifest.bridgeUrl, 800)) {
|
|
1874
|
-
const runningManifest = await
|
|
1875
|
-
manifest.bridgeUrl,
|
|
1876
|
-
|
|
2217
|
+
const [runningManifest, runningFingerprint] = await Promise.all([
|
|
2218
|
+
fetchRunningBridgeManifest(manifest.bridgeUrl, previewToken),
|
|
2219
|
+
fetchRunningBridgeFingerprint(manifest.bridgeUrl),
|
|
2220
|
+
]);
|
|
2221
|
+
const fingerprintMatches =
|
|
2222
|
+
runningFingerprint === designConnectAppFingerprint(manifest);
|
|
1877
2223
|
if (
|
|
1878
|
-
runningManifest &&
|
|
1879
|
-
|
|
2224
|
+
(runningManifest &&
|
|
2225
|
+
designConnectManifestsTargetSameApp(runningManifest, manifest)) ||
|
|
2226
|
+
fingerprintMatches
|
|
1880
2227
|
) {
|
|
1881
2228
|
console.error(
|
|
1882
2229
|
`Design localhost bridge already running at ${manifest.bridgeUrl}`,
|
|
1883
2230
|
);
|
|
1884
|
-
console.log(JSON.stringify(runningManifest, null, 2));
|
|
2231
|
+
console.log(JSON.stringify(runningManifest ?? manifest, null, 2));
|
|
1885
2232
|
return 0;
|
|
1886
2233
|
}
|
|
1887
2234
|
|
|
@@ -1942,24 +2289,33 @@ export async function runDesign(argv: string[]) {
|
|
|
1942
2289
|
}
|
|
1943
2290
|
|
|
1944
2291
|
const manifest = await prepareDesignConnectManifest(parsed);
|
|
2292
|
+
const seedBridgeToken =
|
|
2293
|
+
parsed.bridgeToken || process.env["AGENT_NATIVE_BRIDGE_TOKEN"] || undefined;
|
|
2294
|
+
const seedPreviewToken =
|
|
2295
|
+
parsed.previewToken ||
|
|
2296
|
+
process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
|
|
2297
|
+
(seedBridgeToken ? deriveDesignPreviewToken(seedBridgeToken) : undefined);
|
|
2298
|
+
const appUrl = resolveAppUrl(parsed.appUrl);
|
|
1945
2299
|
if (parsed.daemon) {
|
|
1946
|
-
return startDetachedDesignBridge(argv, manifest);
|
|
2300
|
+
return startDetachedDesignBridge(argv, manifest, seedPreviewToken);
|
|
1947
2301
|
}
|
|
1948
2302
|
if (parsed.json || parsed.once || parsed.dryRun) {
|
|
1949
2303
|
console.log(JSON.stringify(manifest, null, 2));
|
|
1950
2304
|
return 0;
|
|
1951
2305
|
}
|
|
1952
2306
|
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
2307
|
+
const bridge = await startDesignConnectBridge(manifest, {
|
|
2308
|
+
bridgeToken: seedBridgeToken,
|
|
2309
|
+
previewToken: seedPreviewToken,
|
|
2310
|
+
allowedOrigins: appUrl ? [appUrl] : [],
|
|
2311
|
+
});
|
|
1956
2312
|
console.error("Design localhost bridge running");
|
|
1957
2313
|
console.error(`Bridge: ${manifest.bridgeUrl}`);
|
|
1958
2314
|
console.error(`Manifest: ${manifest.bridgeUrl}/manifest.json`);
|
|
1959
2315
|
console.error(`Routes: ${manifest.routeCount}`);
|
|
1960
2316
|
console.error(`Dev URL: ${manifest.devServerUrl}`);
|
|
1961
2317
|
|
|
1962
|
-
if (
|
|
2318
|
+
if (seedBridgeToken) {
|
|
1963
2319
|
// Server already stored this token on the row; bridge matches it, so no
|
|
1964
2320
|
// self-registration needed. Zero-config path for the remote-MCP flow.
|
|
1965
2321
|
console.error(
|
|
@@ -1968,13 +2324,12 @@ export async function runDesign(argv: string[]) {
|
|
|
1968
2324
|
} else {
|
|
1969
2325
|
// No seed: fall back to self-registration — POST the minted token to
|
|
1970
2326
|
// connect-localhost. Needs an auth token in env or it 401s (the old gap).
|
|
1971
|
-
const appUrl = resolveAppUrl(parsed.appUrl);
|
|
1972
2327
|
if (appUrl) {
|
|
1973
2328
|
void registerConnectionWithServer(appUrl, bridge, resolveAuthToken());
|
|
1974
2329
|
} else {
|
|
1975
2330
|
// No token source at all — warn rather than 401 silently at edit time.
|
|
1976
2331
|
console.error(
|
|
1977
|
-
"[design connect] No bridge token or app URL resolved (pass --bridge-token, or --app-url / AGENT_NATIVE_URL); skipping self-registration — live-edit will fail to authorize.",
|
|
2332
|
+
"[design connect] No bridge token or app URL resolved (pass --bridge-token, or --app-url / AGENT_NATIVE_URL); skipping self-registration — browser preview and live-edit will fail to authorize.",
|
|
1978
2333
|
);
|
|
1979
2334
|
}
|
|
1980
2335
|
}
|