@agent-native/core 0.92.6 → 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 +18 -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/cli/sync-builder-starter-manifest.ts +1 -1
- 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/cli/sync-builder-starter-manifest.js +1 -1
- package/dist/cli/sync-builder-starter-manifest.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/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/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/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- 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
|
@@ -39,6 +39,19 @@ const MANIFEST_CAPABILITIES = {
|
|
|
39
39
|
readTextFiles: true,
|
|
40
40
|
writeTextFiles: true,
|
|
41
41
|
};
|
|
42
|
+
const PREVIEW_TOKEN_DOMAIN = "agent-native-design-preview-v1\0";
|
|
43
|
+
/**
|
|
44
|
+
* Derive a read-only preview credential from the stronger filesystem token.
|
|
45
|
+
* The one-way hash keeps old `--bridge-token` launch commands compatible while
|
|
46
|
+
* ensuring a leaked preview credential cannot be promoted into write access.
|
|
47
|
+
*/
|
|
48
|
+
export function deriveDesignPreviewToken(bridgeToken) {
|
|
49
|
+
return crypto
|
|
50
|
+
.createHash("sha256")
|
|
51
|
+
.update(PREVIEW_TOKEN_DOMAIN)
|
|
52
|
+
.update(bridgeToken)
|
|
53
|
+
.digest("hex");
|
|
54
|
+
}
|
|
42
55
|
function stringFlagValue(argv, index, flag) {
|
|
43
56
|
const value = argv[index + 1];
|
|
44
57
|
if (!value || value.startsWith("-")) {
|
|
@@ -117,6 +130,13 @@ export function parseDesignConnectArgs(argv) {
|
|
|
117
130
|
else if (arg.startsWith("--bridge-token=")) {
|
|
118
131
|
parsed.bridgeToken = arg.slice("--bridge-token=".length);
|
|
119
132
|
}
|
|
133
|
+
else if (arg === "--preview-token") {
|
|
134
|
+
parsed.previewToken = stringFlagValue(args, index, arg);
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
else if (arg.startsWith("--preview-token=")) {
|
|
138
|
+
parsed.previewToken = arg.slice("--preview-token=".length);
|
|
139
|
+
}
|
|
120
140
|
else if (arg === "--json") {
|
|
121
141
|
parsed.json = true;
|
|
122
142
|
parsed.once = true;
|
|
@@ -149,14 +169,34 @@ export function parseDesignConnectArgs(argv) {
|
|
|
149
169
|
return parsed;
|
|
150
170
|
}
|
|
151
171
|
function routeId(routePath) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const slug = routePath
|
|
172
|
+
const normalized = routePath.trim() || "/";
|
|
173
|
+
const slug = normalized
|
|
155
174
|
.replace(/^\/+/, "")
|
|
175
|
+
.replace(/\*/g, "w")
|
|
176
|
+
.replace(/:/g, "p")
|
|
177
|
+
.replace(/[[\]]/g, "")
|
|
156
178
|
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
157
179
|
.replace(/^-+|-+$/g, "")
|
|
158
180
|
.toLowerCase();
|
|
159
|
-
|
|
181
|
+
// Slugs are intentionally readable but necessarily lossy: `/foo/bar`,
|
|
182
|
+
// `/foo-bar`, and `/foo_bar` all collapse to the same text, while `/` and
|
|
183
|
+
// `/*` can collide with literal `/root` and `/wildcard` paths. Suffix every
|
|
184
|
+
// route with a stable hash of its normalized path so URL/query states and
|
|
185
|
+
// router patterns can never silently replace one another in the manifest.
|
|
186
|
+
const readable = normalized === "/"
|
|
187
|
+
? "root"
|
|
188
|
+
: /^\/\*+$/.test(normalized) || !slug
|
|
189
|
+
? "wildcard"
|
|
190
|
+
: slug;
|
|
191
|
+
return `route-${readable}-${stableRoutePathHash(normalized)}`;
|
|
192
|
+
}
|
|
193
|
+
function stableRoutePathHash(value) {
|
|
194
|
+
let hash = 0xcbf29ce484222325n;
|
|
195
|
+
for (const character of value) {
|
|
196
|
+
hash ^= BigInt(character.codePointAt(0));
|
|
197
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
198
|
+
}
|
|
199
|
+
return hash.toString(36);
|
|
160
200
|
}
|
|
161
201
|
function titleFromRoutePath(routePath) {
|
|
162
202
|
if (routePath === "/")
|
|
@@ -315,8 +355,11 @@ function isDesignConnectManifest(value) {
|
|
|
315
355
|
typeof manifest.bridgeUrl === "string" &&
|
|
316
356
|
typeof manifest.rootPath === "string");
|
|
317
357
|
}
|
|
318
|
-
async function fetchRunningBridgeManifest(bridgeUrl) {
|
|
319
|
-
const manifestUrl = new URL("/manifest.json", bridgeUrl)
|
|
358
|
+
async function fetchRunningBridgeManifest(bridgeUrl, previewToken) {
|
|
359
|
+
const manifestUrl = new URL("/manifest.json", bridgeUrl);
|
|
360
|
+
if (previewToken) {
|
|
361
|
+
manifestUrl.searchParams.set("previewToken", previewToken);
|
|
362
|
+
}
|
|
320
363
|
const controller = new AbortController();
|
|
321
364
|
const timeout = setTimeout(() => controller.abort(), 800);
|
|
322
365
|
try {
|
|
@@ -349,6 +392,46 @@ export function designConnectManifestsTargetSameApp(running, requested) {
|
|
|
349
392
|
return (runningUrl === requestedUrl &&
|
|
350
393
|
path.resolve(running.rootPath) === path.resolve(requested.rootPath));
|
|
351
394
|
}
|
|
395
|
+
/** Non-sensitive stable identifier used by /health so daemon reruns can detect
|
|
396
|
+
* an already-running bridge for the same app without exposing its root path or
|
|
397
|
+
* route manifest. */
|
|
398
|
+
export function designConnectAppFingerprint(manifest) {
|
|
399
|
+
let devServerUrl = manifest.devServerUrl;
|
|
400
|
+
try {
|
|
401
|
+
devServerUrl = normalizeHttpUrl(devServerUrl);
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// Hash the original string for malformed legacy manifests.
|
|
405
|
+
}
|
|
406
|
+
return crypto
|
|
407
|
+
.createHash("sha256")
|
|
408
|
+
.update(`${devServerUrl}\n${path.resolve(manifest.rootPath)}`)
|
|
409
|
+
.digest("base64url")
|
|
410
|
+
.slice(0, 24);
|
|
411
|
+
}
|
|
412
|
+
async function fetchRunningBridgeFingerprint(bridgeUrl) {
|
|
413
|
+
const healthUrl = new URL("/health", bridgeUrl).toString();
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
const timeout = setTimeout(() => controller.abort(), 800);
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(healthUrl, {
|
|
418
|
+
method: "GET",
|
|
419
|
+
signal: controller.signal,
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok)
|
|
422
|
+
return null;
|
|
423
|
+
const body = (await response.json());
|
|
424
|
+
return typeof body["appFingerprint"] === "string"
|
|
425
|
+
? body["appFingerprint"]
|
|
426
|
+
: null;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
clearTimeout(timeout);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
352
435
|
async function resolveDevServerUrl(url) {
|
|
353
436
|
if (url)
|
|
354
437
|
return normalizeHttpUrl(url);
|
|
@@ -363,7 +446,65 @@ async function ensureRouteManifest(options) {
|
|
|
363
446
|
? options.routeManifestPath
|
|
364
447
|
: path.join(options.root, options.routeManifestPath);
|
|
365
448
|
if (fsSync.existsSync(manifestPath)) {
|
|
366
|
-
|
|
449
|
+
try {
|
|
450
|
+
const parsed = JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
451
|
+
const savedRoutes = Array.isArray(parsed.routes)
|
|
452
|
+
? parsed.routes.flatMap((value) => {
|
|
453
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
const route = value;
|
|
457
|
+
if (typeof route.path !== "string" || !route.path.trim())
|
|
458
|
+
return [];
|
|
459
|
+
const sourceKind = route.sourceKind === "react-router" ||
|
|
460
|
+
route.sourceKind === "html" ||
|
|
461
|
+
route.sourceKind === "manual"
|
|
462
|
+
? route.sourceKind
|
|
463
|
+
: "manual";
|
|
464
|
+
return [
|
|
465
|
+
{
|
|
466
|
+
id: typeof route.id === "string" && route.id.trim()
|
|
467
|
+
? route.id
|
|
468
|
+
: routeId(route.path),
|
|
469
|
+
path: route.path,
|
|
470
|
+
title: typeof route.title === "string" && route.title.trim()
|
|
471
|
+
? route.title
|
|
472
|
+
: titleFromRoutePath(route.path),
|
|
473
|
+
sourceFile: typeof route.sourceFile === "string"
|
|
474
|
+
? route.sourceFile
|
|
475
|
+
: undefined,
|
|
476
|
+
sourceKind,
|
|
477
|
+
screenshotUrl: typeof route.screenshotUrl === "string"
|
|
478
|
+
? route.screenshotUrl
|
|
479
|
+
: undefined,
|
|
480
|
+
metadata: route.metadata &&
|
|
481
|
+
typeof route.metadata === "object" &&
|
|
482
|
+
!Array.isArray(route.metadata)
|
|
483
|
+
? route.metadata
|
|
484
|
+
: undefined,
|
|
485
|
+
},
|
|
486
|
+
];
|
|
487
|
+
})
|
|
488
|
+
: [];
|
|
489
|
+
if (savedRoutes.length > 0) {
|
|
490
|
+
const savedPaths = new Set(savedRoutes.map((route) => route.path));
|
|
491
|
+
return {
|
|
492
|
+
path: manifestPath,
|
|
493
|
+
created: false,
|
|
494
|
+
// Keep manual/custom route order and metadata while still surfacing
|
|
495
|
+
// newly discovered routes on subsequent bridge starts.
|
|
496
|
+
routes: [
|
|
497
|
+
...savedRoutes,
|
|
498
|
+
...options.routes.filter((route) => !savedPaths.has(route.path)),
|
|
499
|
+
],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Never overwrite a malformed/custom file. Route discovery remains a
|
|
505
|
+
// safe runtime fallback and the user can repair the manifest in place.
|
|
506
|
+
}
|
|
507
|
+
return { path: manifestPath, created: false, routes: options.routes };
|
|
367
508
|
}
|
|
368
509
|
if (!options.dryRun) {
|
|
369
510
|
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
@@ -376,22 +517,27 @@ async function ensureRouteManifest(options) {
|
|
|
376
517
|
generatedAt: new Date().toISOString(),
|
|
377
518
|
}, null, 2)}\n`, "utf8");
|
|
378
519
|
}
|
|
379
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
path: manifestPath,
|
|
522
|
+
created: !options.dryRun,
|
|
523
|
+
routes: options.routes,
|
|
524
|
+
};
|
|
380
525
|
}
|
|
381
526
|
export async function prepareDesignConnectManifest(options = {}) {
|
|
382
527
|
const root = path.resolve(options.root ?? process.cwd());
|
|
383
528
|
const port = options.port ?? DEFAULT_BRIDGE_PORT;
|
|
384
529
|
const devServerUrl = await resolveDevServerUrl(options.url);
|
|
385
530
|
const bridgeUrl = `http://127.0.0.1:${port}`;
|
|
386
|
-
const
|
|
531
|
+
const discoveredRoutes = discoverDesignRoutes(root);
|
|
387
532
|
const routeManifest = await ensureRouteManifest({
|
|
388
533
|
root,
|
|
389
534
|
routeManifestPath: options.routeManifest ?? ROUTE_MANIFEST_FILE,
|
|
390
|
-
routes,
|
|
535
|
+
routes: discoveredRoutes,
|
|
391
536
|
devServerUrl,
|
|
392
537
|
dryRun: Boolean(options.dryRun),
|
|
393
538
|
});
|
|
394
539
|
const generatedAt = new Date().toISOString();
|
|
540
|
+
const routes = routeManifest.routes;
|
|
395
541
|
return {
|
|
396
542
|
version: 1,
|
|
397
543
|
source: "agent-native-design-connect",
|
|
@@ -420,32 +566,69 @@ export async function prepareDesignConnectManifest(options = {}) {
|
|
|
420
566
|
})),
|
|
421
567
|
};
|
|
422
568
|
}
|
|
569
|
+
const BRIDGE_CORS_HEADERS = Symbol("agent-native-design-bridge-cors");
|
|
570
|
+
function isLoopbackOrigin(parsed) {
|
|
571
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
572
|
+
return (hostname === "localhost" ||
|
|
573
|
+
hostname === "::1" ||
|
|
574
|
+
/^127(?:\.\d{1,3}){3}$/.test(hostname));
|
|
575
|
+
}
|
|
576
|
+
function isApprovedDesignOrigin(rawOrigin, configuredOrigins) {
|
|
577
|
+
let parsed;
|
|
578
|
+
try {
|
|
579
|
+
parsed = new URL(rawOrigin);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (parsed.origin !== rawOrigin)
|
|
585
|
+
return false;
|
|
586
|
+
if (configuredOrigins.has(parsed.origin))
|
|
587
|
+
return true;
|
|
588
|
+
if ((parsed.protocol === "http:" || parsed.protocol === "https:") &&
|
|
589
|
+
isLoopbackOrigin(parsed)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
return (parsed.protocol === "https:" &&
|
|
593
|
+
(parsed.hostname === "design.agent-native.com" ||
|
|
594
|
+
parsed.hostname.endsWith(".design.agent-native.com")));
|
|
595
|
+
}
|
|
596
|
+
function configureBridgeCors(req, res, configuredOrigins) {
|
|
597
|
+
const origin = typeof req.headers.origin === "string" ? req.headers.origin : "";
|
|
598
|
+
const approved = origin
|
|
599
|
+
? isApprovedDesignOrigin(origin, configuredOrigins)
|
|
600
|
+
: false;
|
|
601
|
+
res[BRIDGE_CORS_HEADERS] = approved
|
|
602
|
+
? {
|
|
603
|
+
"access-control-allow-origin": origin,
|
|
604
|
+
"access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
|
|
605
|
+
"access-control-allow-headers": "content-type, x-bridge-token, x-design-preview-token",
|
|
606
|
+
"access-control-allow-private-network": "true",
|
|
607
|
+
vary: "Origin",
|
|
608
|
+
}
|
|
609
|
+
: {};
|
|
610
|
+
return approved;
|
|
611
|
+
}
|
|
612
|
+
function bridgeCorsHeaders(res) {
|
|
613
|
+
return res[BRIDGE_CORS_HEADERS] ?? {};
|
|
614
|
+
}
|
|
423
615
|
function sendJson(res, statusCode, body) {
|
|
424
616
|
res.writeHead(statusCode, {
|
|
425
617
|
"content-type": "application/json; charset=utf-8",
|
|
426
|
-
|
|
427
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
428
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
429
|
-
"access-control-allow-private-network": "true",
|
|
618
|
+
...bridgeCorsHeaders(res),
|
|
430
619
|
});
|
|
431
620
|
res.end(`${JSON.stringify(body, null, 2)}\n`);
|
|
432
621
|
}
|
|
433
622
|
function sendText(res, statusCode, body, contentType) {
|
|
434
623
|
res.writeHead(statusCode, {
|
|
435
624
|
"content-type": contentType,
|
|
436
|
-
|
|
437
|
-
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
438
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
439
|
-
"access-control-allow-private-network": "true",
|
|
625
|
+
...bridgeCorsHeaders(res),
|
|
440
626
|
});
|
|
441
627
|
res.end(body);
|
|
442
628
|
}
|
|
443
629
|
function sendBytes(res, statusCode, body, headers) {
|
|
444
630
|
const responseHeaders = {
|
|
445
|
-
|
|
446
|
-
"access-control-allow-methods": "GET, HEAD, POST, OPTIONS",
|
|
447
|
-
"access-control-allow-headers": "content-type, x-bridge-token",
|
|
448
|
-
"access-control-allow-private-network": "true",
|
|
631
|
+
...bridgeCorsHeaders(res),
|
|
449
632
|
};
|
|
450
633
|
for (const name of [
|
|
451
634
|
"content-type",
|
|
@@ -468,6 +651,19 @@ function sameOrigin(a, b) {
|
|
|
468
651
|
return false;
|
|
469
652
|
}
|
|
470
653
|
}
|
|
654
|
+
function constantTimeTokenMatches(providedToken, expectedToken) {
|
|
655
|
+
try {
|
|
656
|
+
return (providedToken.length === expectedToken.length &&
|
|
657
|
+
crypto.timingSafeEqual(Buffer.from(providedToken, "utf8"), Buffer.from(expectedToken, "utf8")));
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function readHeader(req, name) {
|
|
664
|
+
const value = req.headers[name];
|
|
665
|
+
return typeof value === "string" ? value : "";
|
|
666
|
+
}
|
|
471
667
|
function resolvePreviewSnapshotUrl(devServerUrl, rawUrl) {
|
|
472
668
|
const base = normalizeHttpUrl(devServerUrl);
|
|
473
669
|
const parsed = new URL(rawUrl?.trim() || "/", `${base}/`);
|
|
@@ -1011,27 +1207,68 @@ async function walkBridgeFiles(rootPath) {
|
|
|
1011
1207
|
await walk(resolvedRoot, "");
|
|
1012
1208
|
return { files, truncated };
|
|
1013
1209
|
}
|
|
1014
|
-
export async function startDesignConnectBridge(manifest,
|
|
1210
|
+
export async function startDesignConnectBridge(manifest, seedOrOptions) {
|
|
1015
1211
|
// Shared secret the browser sends (x-bridge-token) to unlock live-edit/read/
|
|
1016
1212
|
// write. Bridge and the user's connection row must agree on it. Adopt a
|
|
1017
1213
|
// server-minted seed when given (MCP flow); otherwise mint one and rely on
|
|
1018
1214
|
// --app-url self-registration to push it up. Kept in-process, never served.
|
|
1019
|
-
const
|
|
1215
|
+
const options = typeof seedOrOptions === "string"
|
|
1216
|
+
? { bridgeToken: seedOrOptions }
|
|
1217
|
+
: (seedOrOptions ?? {});
|
|
1218
|
+
const bridgeToken = options.bridgeToken ||
|
|
1020
1219
|
process.env["AGENT_NATIVE_BRIDGE_TOKEN"] ||
|
|
1021
1220
|
crypto.randomBytes(32).toString("hex");
|
|
1221
|
+
const previewToken = options.previewToken ||
|
|
1222
|
+
process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
|
|
1223
|
+
deriveDesignPreviewToken(bridgeToken);
|
|
1224
|
+
const configuredOrigins = new Set((options.allowedOrigins ?? []).flatMap((raw) => {
|
|
1225
|
+
try {
|
|
1226
|
+
return [new URL(raw).origin];
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
}));
|
|
1022
1232
|
let liveEditBridgeScript = "";
|
|
1233
|
+
// One bridge process serves every URL-backed screen in an overview. The
|
|
1234
|
+
// editor script carries screen-specific state (notably screenId), so a
|
|
1235
|
+
// single global slot lets parallel iframe registrations overwrite each
|
|
1236
|
+
// other and boot a frame with another frame's identity. Keep keyed scripts
|
|
1237
|
+
// for modern clients while retaining the unkeyed slot for older clients.
|
|
1238
|
+
const liveEditBridgeScripts = new Map();
|
|
1023
1239
|
const server = http.createServer((req, res) => {
|
|
1240
|
+
const corsApproved = configureBridgeCors(req, res, configuredOrigins);
|
|
1024
1241
|
if (req.method === "OPTIONS") {
|
|
1025
|
-
sendJson(res, 204,
|
|
1242
|
+
sendJson(res, corsApproved ? 204 : 403, corsApproved
|
|
1243
|
+
? {}
|
|
1244
|
+
: { ok: false, error: "origin is not allowed by this bridge" });
|
|
1026
1245
|
return;
|
|
1027
1246
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1247
|
+
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1248
|
+
const pathname = requestUrl.pathname;
|
|
1249
|
+
const providedPreviewToken = readHeader(req, "x-design-preview-token") ||
|
|
1250
|
+
requestUrl.searchParams.get("previewToken") ||
|
|
1251
|
+
"";
|
|
1252
|
+
const previewTokenValid = constantTimeTokenMatches(providedPreviewToken, previewToken);
|
|
1253
|
+
const rejectInvalidPreviewToken = () => {
|
|
1254
|
+
if (previewTokenValid)
|
|
1255
|
+
return false;
|
|
1256
|
+
sendJson(res, 401, {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
error: "invalid or missing preview token",
|
|
1259
|
+
});
|
|
1260
|
+
return true;
|
|
1261
|
+
};
|
|
1262
|
+
// ── Read-only preview routes (preview token required) ────────────────
|
|
1030
1263
|
if (pathname === "/" || pathname === "/manifest.json") {
|
|
1264
|
+
if (rejectInvalidPreviewToken())
|
|
1265
|
+
return;
|
|
1031
1266
|
sendJson(res, 200, manifest);
|
|
1032
1267
|
return;
|
|
1033
1268
|
}
|
|
1034
1269
|
if (pathname === "/routes.json") {
|
|
1270
|
+
if (rejectInvalidPreviewToken())
|
|
1271
|
+
return;
|
|
1035
1272
|
sendJson(res, 200, {
|
|
1036
1273
|
version: 1,
|
|
1037
1274
|
sourceType: "localhost",
|
|
@@ -1043,7 +1280,11 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1043
1280
|
return;
|
|
1044
1281
|
}
|
|
1045
1282
|
if (pathname === "/health") {
|
|
1046
|
-
sendJson(res, 200, {
|
|
1283
|
+
sendJson(res, 200, {
|
|
1284
|
+
ok: true,
|
|
1285
|
+
source: manifest.source,
|
|
1286
|
+
appFingerprint: designConnectAppFingerprint(manifest),
|
|
1287
|
+
});
|
|
1047
1288
|
return;
|
|
1048
1289
|
}
|
|
1049
1290
|
if (pathname === "/live-edit-bridge") {
|
|
@@ -1051,38 +1292,50 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1051
1292
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1052
1293
|
return;
|
|
1053
1294
|
}
|
|
1054
|
-
|
|
1055
|
-
const providedToken = typeof tokenHeader === "string" ? tokenHeader : "";
|
|
1056
|
-
let tokenValid = false;
|
|
1057
|
-
try {
|
|
1058
|
-
tokenValid =
|
|
1059
|
-
providedToken.length === bridgeToken.length &&
|
|
1060
|
-
crypto.timingSafeEqual(Buffer.from(providedToken, "utf8"), Buffer.from(bridgeToken, "utf8"));
|
|
1061
|
-
}
|
|
1062
|
-
catch {
|
|
1063
|
-
tokenValid = false;
|
|
1064
|
-
}
|
|
1065
|
-
if (!tokenValid) {
|
|
1066
|
-
sendJson(res, 401, {
|
|
1067
|
-
ok: false,
|
|
1068
|
-
error: "invalid or missing bridge token",
|
|
1069
|
-
});
|
|
1295
|
+
if (rejectInvalidPreviewToken())
|
|
1070
1296
|
return;
|
|
1071
|
-
}
|
|
1072
1297
|
void (async () => {
|
|
1073
1298
|
try {
|
|
1074
1299
|
const raw = await readRequestBody(req);
|
|
1075
1300
|
const body = JSON.parse(raw);
|
|
1076
1301
|
const script = typeof body["script"] === "string" ? body["script"] : "";
|
|
1077
|
-
|
|
1302
|
+
const bridgeKey = typeof body["bridgeKey"] === "string"
|
|
1303
|
+
? body["bridgeKey"].trim()
|
|
1304
|
+
: "";
|
|
1305
|
+
const installsSupportedDesignBridge = script.includes("agent-native:editor-chrome-ready") ||
|
|
1306
|
+
script.includes("embedded-canvas-pan");
|
|
1307
|
+
if (!installsSupportedDesignBridge) {
|
|
1078
1308
|
sendJson(res, 400, {
|
|
1079
1309
|
ok: false,
|
|
1080
|
-
error: "script must install
|
|
1310
|
+
error: "script must install an approved Agent Native editor or canvas-pan bridge",
|
|
1311
|
+
});
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (bridgeKey &&
|
|
1315
|
+
(!/^[a-zA-Z0-9:._-]+$/.test(bridgeKey) || bridgeKey.length > 128)) {
|
|
1316
|
+
sendJson(res, 400, {
|
|
1317
|
+
ok: false,
|
|
1318
|
+
error: "bridgeKey must be 1-128 safe identifier characters",
|
|
1081
1319
|
});
|
|
1082
1320
|
return;
|
|
1083
1321
|
}
|
|
1084
1322
|
liveEditBridgeScript = script;
|
|
1085
|
-
|
|
1323
|
+
if (bridgeKey) {
|
|
1324
|
+
liveEditBridgeScripts.delete(bridgeKey);
|
|
1325
|
+
liveEditBridgeScripts.set(bridgeKey, script);
|
|
1326
|
+
// Bound the in-memory cache. Normal editor usage has one key per
|
|
1327
|
+
// visible screen; 128 also leaves ample room for mode changes.
|
|
1328
|
+
while (liveEditBridgeScripts.size > 128) {
|
|
1329
|
+
const oldest = liveEditBridgeScripts.keys().next().value;
|
|
1330
|
+
if (typeof oldest !== "string")
|
|
1331
|
+
break;
|
|
1332
|
+
liveEditBridgeScripts.delete(oldest);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
sendJson(res, 200, {
|
|
1336
|
+
ok: true,
|
|
1337
|
+
...(bridgeKey ? { bridgeKey } : {}),
|
|
1338
|
+
});
|
|
1086
1339
|
}
|
|
1087
1340
|
catch (err) {
|
|
1088
1341
|
sendJson(res, 400, {
|
|
@@ -1098,19 +1351,33 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1098
1351
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1099
1352
|
return;
|
|
1100
1353
|
}
|
|
1354
|
+
if (rejectInvalidPreviewToken())
|
|
1355
|
+
return;
|
|
1101
1356
|
void (async () => {
|
|
1102
1357
|
try {
|
|
1103
|
-
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1104
1358
|
const targetUrl = resolvePreviewSnapshotUrl(manifest.devServerUrl, requestUrl.searchParams.get("url") ??
|
|
1105
1359
|
requestUrl.searchParams.get("path"));
|
|
1106
1360
|
const snapshot = await fetchPreviewSnapshot(manifest.devServerUrl, targetUrl);
|
|
1107
1361
|
const includeEditorBridge = requestUrl.searchParams.get("bridge") !== "0";
|
|
1362
|
+
const requestedBridgeKey = requestUrl.searchParams.get("bridgeKey")?.trim() ?? "";
|
|
1363
|
+
const editorBridgeScript = requestedBridgeKey
|
|
1364
|
+
? (liveEditBridgeScripts.get(requestedBridgeKey) ?? "")
|
|
1365
|
+
: liveEditBridgeScript;
|
|
1366
|
+
if (includeEditorBridge &&
|
|
1367
|
+
requestedBridgeKey &&
|
|
1368
|
+
!editorBridgeScript) {
|
|
1369
|
+
sendJson(res, 409, {
|
|
1370
|
+
ok: false,
|
|
1371
|
+
error: "The requested live-edit bridge script is not registered. Reload the Design frame to register it again.",
|
|
1372
|
+
});
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1108
1375
|
// The dev server route the SPA must boot on (e.g. "/todo"), taken
|
|
1109
1376
|
// from the resolved snapshot target rather than the bridge's own
|
|
1110
1377
|
// "/live-edit" request path.
|
|
1111
1378
|
const targetParsed = new URL(targetUrl);
|
|
1112
1379
|
const targetPath = `${targetParsed.pathname}${targetParsed.search}` || "/";
|
|
1113
|
-
const html = injectLiveEditBridge(snapshot.html, new URL("/", manifest.bridgeUrl).toString(), includeEditorBridge ?
|
|
1380
|
+
const html = injectLiveEditBridge(snapshot.html, new URL("/", manifest.bridgeUrl).toString(), includeEditorBridge ? editorBridgeScript : "", targetPath);
|
|
1114
1381
|
sendText(res, snapshot.status >= 400 ? snapshot.status : 200, html, snapshot.contentType.includes("html")
|
|
1115
1382
|
? snapshot.contentType
|
|
1116
1383
|
: "text/html; charset=utf-8");
|
|
@@ -1129,9 +1396,10 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1129
1396
|
sendJson(res, 405, { ok: false, error: "method not allowed" });
|
|
1130
1397
|
return;
|
|
1131
1398
|
}
|
|
1399
|
+
if (rejectInvalidPreviewToken())
|
|
1400
|
+
return;
|
|
1132
1401
|
void (async () => {
|
|
1133
1402
|
try {
|
|
1134
|
-
const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
|
|
1135
1403
|
const targetUrl = resolvePreviewSnapshotUrl(manifest.devServerUrl, requestUrl.searchParams.get("url") ??
|
|
1136
1404
|
requestUrl.searchParams.get("path"));
|
|
1137
1405
|
const snapshot = await fetchPreviewSnapshot(manifest.devServerUrl, targetUrl);
|
|
@@ -1163,17 +1431,8 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1163
1431
|
return;
|
|
1164
1432
|
}
|
|
1165
1433
|
// Authenticate with constant-time comparison to prevent timing attacks.
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
let tokenValid = false;
|
|
1169
|
-
try {
|
|
1170
|
-
tokenValid =
|
|
1171
|
-
providedToken.length === bridgeToken.length &&
|
|
1172
|
-
crypto.timingSafeEqual(Buffer.from(providedToken, "utf8"), Buffer.from(bridgeToken, "utf8"));
|
|
1173
|
-
}
|
|
1174
|
-
catch {
|
|
1175
|
-
tokenValid = false;
|
|
1176
|
-
}
|
|
1434
|
+
const providedToken = readHeader(req, "x-bridge-token");
|
|
1435
|
+
const tokenValid = constantTimeTokenMatches(providedToken, bridgeToken);
|
|
1177
1436
|
if (!tokenValid) {
|
|
1178
1437
|
sendJson(res, 401, {
|
|
1179
1438
|
ok: false,
|
|
@@ -1366,6 +1625,14 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1366
1625
|
return;
|
|
1367
1626
|
}
|
|
1368
1627
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
1628
|
+
const sameOriginPreviewSubresource = readHeader(req, "sec-fetch-site") === "same-origin";
|
|
1629
|
+
if (!previewTokenValid && !sameOriginPreviewSubresource) {
|
|
1630
|
+
sendJson(res, 401, {
|
|
1631
|
+
ok: false,
|
|
1632
|
+
error: "invalid or missing preview token",
|
|
1633
|
+
});
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1369
1636
|
void (async () => {
|
|
1370
1637
|
try {
|
|
1371
1638
|
const targetUrl = resolvePreviewProxyUrl(manifest.devServerUrl, req.url);
|
|
@@ -1390,7 +1657,7 @@ export async function startDesignConnectBridge(manifest, seedToken) {
|
|
|
1390
1657
|
resolve();
|
|
1391
1658
|
});
|
|
1392
1659
|
});
|
|
1393
|
-
return { server, manifest, bridgeToken };
|
|
1660
|
+
return { server, manifest, bridgeToken, previewToken };
|
|
1394
1661
|
}
|
|
1395
1662
|
/**
|
|
1396
1663
|
* Resolve the design app URL from an explicit value or environment variables.
|
|
@@ -1441,7 +1708,7 @@ function resolveAuthToken() {
|
|
|
1441
1708
|
*/
|
|
1442
1709
|
export async function registerConnectionWithServer(appUrl, bridge, authToken) {
|
|
1443
1710
|
const endpoint = `${appUrl}/_agent-native/actions/connect-localhost`;
|
|
1444
|
-
const { manifest, bridgeToken } = bridge;
|
|
1711
|
+
const { manifest, bridgeToken, previewToken } = bridge;
|
|
1445
1712
|
const payload = {
|
|
1446
1713
|
devServerUrl: manifest.devServerUrl,
|
|
1447
1714
|
bridgeUrl: manifest.bridgeUrl,
|
|
@@ -1459,6 +1726,7 @@ export async function registerConnectionWithServer(appUrl, bridge, authToken) {
|
|
|
1459
1726
|
// row. grant-localhost-write-consent then reads it from the row instead of
|
|
1460
1727
|
// minting its own unrelated token, which would always produce a 401.
|
|
1461
1728
|
bridgeToken,
|
|
1729
|
+
previewToken,
|
|
1462
1730
|
status: "connected",
|
|
1463
1731
|
};
|
|
1464
1732
|
const headers = {
|
|
@@ -1507,6 +1775,10 @@ Options:
|
|
|
1507
1775
|
/visual-edit flow so the bridge and the user's stored
|
|
1508
1776
|
connection token match with no self-registration.
|
|
1509
1777
|
(also reads AGENT_NATIVE_BRIDGE_TOKEN env)
|
|
1778
|
+
--preview-token <token> Adopt the paired read-only browser preview token.
|
|
1779
|
+
Optional when --bridge-token is present: compatible
|
|
1780
|
+
clients derive the same one-way token automatically.
|
|
1781
|
+
(also reads AGENT_NATIVE_PREVIEW_TOKEN env)
|
|
1510
1782
|
--daemon Start the bridge detached, wait for /health, then exit
|
|
1511
1783
|
--json Print the manifest JSON and exit
|
|
1512
1784
|
--once Prepare/scaffold the manifest and exit
|
|
@@ -1553,13 +1825,18 @@ function resolveCurrentCliInvocation(argv) {
|
|
|
1553
1825
|
args: [...cliPrefix, "design", ...removeDaemonFlag(argv)],
|
|
1554
1826
|
};
|
|
1555
1827
|
}
|
|
1556
|
-
async function startDetachedDesignBridge(argv, manifest) {
|
|
1828
|
+
async function startDetachedDesignBridge(argv, manifest, previewToken) {
|
|
1557
1829
|
if (await waitForBridgeHealth(manifest.bridgeUrl, 800)) {
|
|
1558
|
-
const runningManifest = await
|
|
1559
|
-
|
|
1560
|
-
|
|
1830
|
+
const [runningManifest, runningFingerprint] = await Promise.all([
|
|
1831
|
+
fetchRunningBridgeManifest(manifest.bridgeUrl, previewToken),
|
|
1832
|
+
fetchRunningBridgeFingerprint(manifest.bridgeUrl),
|
|
1833
|
+
]);
|
|
1834
|
+
const fingerprintMatches = runningFingerprint === designConnectAppFingerprint(manifest);
|
|
1835
|
+
if ((runningManifest &&
|
|
1836
|
+
designConnectManifestsTargetSameApp(runningManifest, manifest)) ||
|
|
1837
|
+
fingerprintMatches) {
|
|
1561
1838
|
console.error(`Design localhost bridge already running at ${manifest.bridgeUrl}`);
|
|
1562
|
-
console.log(JSON.stringify(runningManifest, null, 2));
|
|
1839
|
+
console.log(JSON.stringify(runningManifest ?? manifest, null, 2));
|
|
1563
1840
|
return 0;
|
|
1564
1841
|
}
|
|
1565
1842
|
console.error([
|
|
@@ -1607,21 +1884,29 @@ export async function runDesign(argv) {
|
|
|
1607
1884
|
return 0;
|
|
1608
1885
|
}
|
|
1609
1886
|
const manifest = await prepareDesignConnectManifest(parsed);
|
|
1887
|
+
const seedBridgeToken = parsed.bridgeToken || process.env["AGENT_NATIVE_BRIDGE_TOKEN"] || undefined;
|
|
1888
|
+
const seedPreviewToken = parsed.previewToken ||
|
|
1889
|
+
process.env["AGENT_NATIVE_PREVIEW_TOKEN"] ||
|
|
1890
|
+
(seedBridgeToken ? deriveDesignPreviewToken(seedBridgeToken) : undefined);
|
|
1891
|
+
const appUrl = resolveAppUrl(parsed.appUrl);
|
|
1610
1892
|
if (parsed.daemon) {
|
|
1611
|
-
return startDetachedDesignBridge(argv, manifest);
|
|
1893
|
+
return startDetachedDesignBridge(argv, manifest, seedPreviewToken);
|
|
1612
1894
|
}
|
|
1613
1895
|
if (parsed.json || parsed.once || parsed.dryRun) {
|
|
1614
1896
|
console.log(JSON.stringify(manifest, null, 2));
|
|
1615
1897
|
return 0;
|
|
1616
1898
|
}
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1899
|
+
const bridge = await startDesignConnectBridge(manifest, {
|
|
1900
|
+
bridgeToken: seedBridgeToken,
|
|
1901
|
+
previewToken: seedPreviewToken,
|
|
1902
|
+
allowedOrigins: appUrl ? [appUrl] : [],
|
|
1903
|
+
});
|
|
1619
1904
|
console.error("Design localhost bridge running");
|
|
1620
1905
|
console.error(`Bridge: ${manifest.bridgeUrl}`);
|
|
1621
1906
|
console.error(`Manifest: ${manifest.bridgeUrl}/manifest.json`);
|
|
1622
1907
|
console.error(`Routes: ${manifest.routeCount}`);
|
|
1623
1908
|
console.error(`Dev URL: ${manifest.devServerUrl}`);
|
|
1624
|
-
if (
|
|
1909
|
+
if (seedBridgeToken) {
|
|
1625
1910
|
// Server already stored this token on the row; bridge matches it, so no
|
|
1626
1911
|
// self-registration needed. Zero-config path for the remote-MCP flow.
|
|
1627
1912
|
console.error("[design connect] Using server-provided bridge token; skipping self-registration.");
|
|
@@ -1629,13 +1914,12 @@ export async function runDesign(argv) {
|
|
|
1629
1914
|
else {
|
|
1630
1915
|
// No seed: fall back to self-registration — POST the minted token to
|
|
1631
1916
|
// connect-localhost. Needs an auth token in env or it 401s (the old gap).
|
|
1632
|
-
const appUrl = resolveAppUrl(parsed.appUrl);
|
|
1633
1917
|
if (appUrl) {
|
|
1634
1918
|
void registerConnectionWithServer(appUrl, bridge, resolveAuthToken());
|
|
1635
1919
|
}
|
|
1636
1920
|
else {
|
|
1637
1921
|
// No token source at all — warn rather than 401 silently at edit time.
|
|
1638
|
-
console.error("[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.");
|
|
1922
|
+
console.error("[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.");
|
|
1639
1923
|
}
|
|
1640
1924
|
}
|
|
1641
1925
|
return await new Promise((resolve) => {
|