@volter/editor-core 0.5.57
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/BUNDLED_NOTICES +1527 -0
- package/LICENSE +686 -0
- package/LICENSE-APACHE +202 -0
- package/NOTICE +18 -0
- package/README.md +24 -0
- package/dist/build/chunk-JS4D4XZ3.js +175 -0
- package/dist/build/chunk-JS4D4XZ3.js.map +7 -0
- package/dist/build/vite-plugin-product-contributions.js +100 -0
- package/dist/build/vite-plugin-product-contributions.js.map +7 -0
- package/dist/build/vite-plugin-shared-react.js +25 -0
- package/dist/build/vite-plugin-shared-react.js.map +7 -0
- package/dist/build/vite-plugin-shared-three.js +86 -0
- package/dist/build/vite-plugin-shared-three.js.map +7 -0
- package/dist/server/chunk-KMJC7AGO.js +98 -0
- package/dist/server/chunk-KMJC7AGO.js.map +7 -0
- package/dist/server/chunk-PSILUMGS.js +46 -0
- package/dist/server/chunk-PSILUMGS.js.map +7 -0
- package/dist/server/frame-proxy.js +255 -0
- package/dist/server/frame-proxy.js.map +7 -0
- package/dist/server/open-browser.js +53 -0
- package/dist/server/open-browser.js.map +7 -0
- package/dist/server/process-shutdown.js +127 -0
- package/dist/server/process-shutdown.js.map +7 -0
- package/dist/server/session-registry.js +141 -0
- package/dist/server/session-registry.js.map +7 -0
- package/dist/server/spawn-opener.js +11 -0
- package/dist/server/spawn-opener.js.map +7 -0
- package/dist/server/worktree-identity.js +11 -0
- package/dist/server/worktree-identity.js.map +7 -0
- package/dist-server/packaged.mjs +88816 -0
- package/package.json +118 -0
- package/scripts/build-plugins.mjs +14 -0
- package/scripts/build-server.mjs +26 -0
- package/scripts/build-session.mjs +9 -0
- package/server/account-credentials.ts +60 -0
- package/server/account-service.ts +1434 -0
- package/server/adapter-region-includes.ts +47 -0
- package/server/asset-catalog-v2.ts +175 -0
- package/server/asset-history-snapshots.ts +196 -0
- package/server/asset-ledger-store.ts +195 -0
- package/server/asset-library-routes.ts +1595 -0
- package/server/blender-wasm-artifact.ts +430 -0
- package/server/boot-timings.ts +106 -0
- package/server/canonical-path.ts +50 -0
- package/server/checkout-workspace-preflight.ts +349 -0
- package/server/cloud-asset-catalog.ts +158 -0
- package/server/coding-inference-launch.ts +237 -0
- package/server/collaboration-account-client.ts +202 -0
- package/server/collaboration-attribution.ts +122 -0
- package/server/collaboration-session.ts +934 -0
- package/server/comfyui-bridge.ts +248 -0
- package/server/console-ledger.ts +499 -0
- package/server/creation-site-transform.ts +304 -0
- package/server/creation-site-write.ts +648 -0
- package/server/data-file-serialize.ts +158 -0
- package/server/editor-brand-html.ts +72 -0
- package/server/editor-control-socket.ts +366 -0
- package/server/editor-server-options.ts +190 -0
- package/server/editor-server.ts +1483 -0
- package/server/editor-sse.ts +352 -0
- package/server/engine-provenance.ts +169 -0
- package/server/engine-source-restart.ts +117 -0
- package/server/frame-bridge.ts +115 -0
- package/server/frame-proxy.ts +490 -0
- package/server/frame-workbench.ts +259 -0
- package/server/frontend-handoff.ts +229 -0
- package/server/game-globals-shadow.ts +113 -0
- package/server/gameplay-session-retention.ts +149 -0
- package/server/gameplay-sessions.ts +123 -0
- package/server/generation-jobs.ts +201 -0
- package/server/generation-reconciler.ts +137 -0
- package/server/generative-execution-context.ts +59 -0
- package/server/git-workflow.ts +612 -0
- package/server/harness-chat-caller.ts +36 -0
- package/server/harness-chat-service.ts +1681 -0
- package/server/idle-shutdown.ts +170 -0
- package/server/js-profiling-policy.ts +52 -0
- package/server/launcher-settings.ts +84 -0
- package/server/local-asset-catalog.ts +410 -0
- package/server/managed-account-defaults.ts +22 -0
- package/server/mock-control-plane.ts +540 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/native-credential-store.ts +162 -0
- package/server/open-browser.ts +118 -0
- package/server/packaged.ts +1192 -0
- package/server/play-stall.ts +126 -0
- package/server/process-shutdown.ts +218 -0
- package/server/product-presets.ts +55 -0
- package/server/project-build-artifact.ts +95 -0
- package/server/project-components.ts +100 -0
- package/server/project-dependency-invalidation.ts +114 -0
- package/server/project-file-scan.ts +235 -0
- package/server/project-hmr-files.ts +215 -0
- package/server/project-install-roots.ts +210 -0
- package/server/project-kinds.ts +71 -0
- package/server/project-mcp-servers.ts +82 -0
- package/server/project-module-freshness.ts +109 -0
- package/server/project-module-instance.ts +375 -0
- package/server/project-optimize-deps-entries.ts +827 -0
- package/server/project-output-writer.ts +679 -0
- package/server/project-package-origin.ts +63 -0
- package/server/project-root-surface.ts +831 -0
- package/server/project-scratch-path.ts +47 -0
- package/server/project-script-hmr.ts +619 -0
- package/server/project-serving-plugins.ts +236 -0
- package/server/project-tools.ts +653 -0
- package/server/project-validation.ts +250 -0
- package/server/project-verbs.ts +127 -0
- package/server/project-view.ts +52 -0
- package/server/project-watch.ts +1134 -0
- package/server/project-work-coordinator.ts +288 -0
- package/server/provider-credentials.ts +331 -0
- package/server/recent-projects-store.ts +98 -0
- package/server/redact-secrets.ts +71 -0
- package/server/repository-presence.ts +267 -0
- package/server/routes/account.ts +357 -0
- package/server/routes/agents.ts +60 -0
- package/server/routes/assets.ts +154 -0
- package/server/routes/build.ts +13 -0
- package/server/routes/collaboration.ts +595 -0
- package/server/routes/configurations.ts +419 -0
- package/server/routes/context.ts +264 -0
- package/server/routes/control-plane.ts +1811 -0
- package/server/routes/logs.ts +272 -0
- package/server/routes/project-identity.ts +302 -0
- package/server/routes/project-open.ts +429 -0
- package/server/routes/project-source.ts +686 -0
- package/server/routes/project-state.ts +383 -0
- package/server/routes/relay.ts +1029 -0
- package/server/routes/route-helpers.ts +29 -0
- package/server/routes/served-modules.ts +138 -0
- package/server/routes/session-tabs.ts +196 -0
- package/server/routes/settings.ts +121 -0
- package/server/routes/share-control.ts +486 -0
- package/server/routes/themes.ts +116 -0
- package/server/routes/tools.ts +230 -0
- package/server/routes/worktrees.ts +595 -0
- package/server/scoped-game-css.ts +461 -0
- package/server/server-utils.ts +1194 -0
- package/server/session-product.ts +112 -0
- package/server/session-registry.ts +230 -0
- package/server/share-claims.ts +138 -0
- package/server/share-host.ts +526 -0
- package/server/share-session-gateway.ts +1362 -0
- package/server/share-tunnel.ts +123 -0
- package/server/spawn-opener.ts +74 -0
- package/server/support/play/log-format.ts +76 -0
- package/server/support/play/session-record.ts +199 -0
- package/server/support/project/build-discipline.ts +718 -0
- package/server/support/project/inspection-node.ts +119 -0
- package/server/support/project/log-naming.ts +54 -0
- package/server/support/project/provenance.ts +290 -0
- package/server/support/project/run-name.ts +18 -0
- package/server/support/project/session-journal.ts +830 -0
- package/server/support/project/shared.ts +327 -0
- package/server/tab-bootstrap.ts +62 -0
- package/server/tab-heartbeat.ts +477 -0
- package/server/tab-lifecycle.ts +550 -0
- package/server/tab-presence.ts +1508 -0
- package/server/team-agent-mirror.ts +95 -0
- package/server/twin-auth.ts +138 -0
- package/server/vendored-lock-recorder.ts +968 -0
- package/server/worktree-identity.ts +121 -0
- package/server/worktree-management.ts +398 -0
- package/server/worktree-retention.d.mts +13 -0
- package/server/worktree-retention.mjs +205 -0
- package/src/EditorContext.tsx +169 -0
- package/src/account.ts +333 -0
- package/src/action-registry.ts +532 -0
- package/src/active-product.ts +74 -0
- package/src/active-project.ts +155 -0
- package/src/adapter-editor-config.ts +25 -0
- package/src/adapter-observation.ts +49 -0
- package/src/animation/stage-transport.ts +379 -0
- package/src/animation/three-clips-subject.ts +172 -0
- package/src/api/asset-library-wire.ts +45 -0
- package/src/api/assets.ts +365 -0
- package/src/api/base.ts +10 -0
- package/src/api/build.ts +99 -0
- package/src/api/git-wire.ts +56 -0
- package/src/api/logs.ts +92 -0
- package/src/api/project-identity.ts +74 -0
- package/src/api/project-open.ts +355 -0
- package/src/api/project-source.ts +162 -0
- package/src/api/project-state.ts +107 -0
- package/src/api/relay.ts +268 -0
- package/src/api/settings.ts +36 -0
- package/src/api/themes.ts +45 -0
- package/src/api/worktrees.ts +257 -0
- package/src/asset-compare-core.ts +171 -0
- package/src/asset-compare.ts +294 -0
- package/src/asset-editor-context.tsx +100 -0
- package/src/asset-events.ts +97 -0
- package/src/asset-inspector-actions.ts +87 -0
- package/src/asset-preview-framing.ts +357 -0
- package/src/asset-preview.ts +2802 -0
- package/src/asset-selection-viewer-registry.ts +113 -0
- package/src/asset-selection.ts +133 -0
- package/src/asset-workflow/asset-capabilities.ts +344 -0
- package/src/asset-workflow/asset-import-jobs.ts +106 -0
- package/src/asset-workflow/asset-ledger-backend.ts +126 -0
- package/src/asset-workflow/asset-ledger.ts +156 -0
- package/src/asset-workflow/asset-materialization-report.ts +140 -0
- package/src/asset-workflow/asset-pack-manifest.ts +320 -0
- package/src/asset-workflow/asset-types.ts +142 -0
- package/src/asset-workflow/asset-workflow-quality.ts +61 -0
- package/src/asset-workflow/audio-preview-player.ts +193 -0
- package/src/asset-workflow/audio-waveform.ts +22 -0
- package/src/asset-workflow/cloud-asset-client.ts +263 -0
- package/src/asset-workflow/folder-preview.ts +345 -0
- package/src/asset-workflow/hosted-asset-materialization.ts +236 -0
- package/src/asset-workflow/image-view-scale.ts +32 -0
- package/src/asset-workflow/import-contract.ts +124 -0
- package/src/asset-workflow/ledger-write-lock.ts +244 -0
- package/src/asset-workflow/model-inspection.ts +818 -0
- package/src/asset-workflow/pixi-spritesheet.ts +197 -0
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +139 -0
- package/src/asset-workflow/project-asset-operations.ts +154 -0
- package/src/asset-workflow/project-asset-roots.ts +68 -0
- package/src/asset-workflow/project-content.ts +288 -0
- package/src/asset-workflow/project-source-index.ts +550 -0
- package/src/asset-workflow/thumbnail-system.ts +256 -0
- package/src/authoring/active-adapter.ts +199 -0
- package/src/authoring/active-systems.ts +422 -0
- package/src/authoring/adapter-key.ts +18 -0
- package/src/authoring/authoring-asset-url.ts +27 -0
- package/src/authoring/bootstrap-state.ts +49 -0
- package/src/authoring/boundary-authoring-adapter.ts +184 -0
- package/src/authoring/breakpoint-state.ts +43 -0
- package/src/authoring/canvas-scene-guides.ts +76 -0
- package/src/authoring/component-instance-root.ts +171 -0
- package/src/authoring/composite-authoring-adapter.ts +2110 -0
- package/src/authoring/consumer-actions.ts +520 -0
- package/src/authoring/css-numeric-style.ts +97 -0
- package/src/authoring/design-time-layers.ts +849 -0
- package/src/authoring/design-time-mount-registry.ts +235 -0
- package/src/authoring/design-time-settle.ts +343 -0
- package/src/authoring/edit-mode-authoring.ts +619 -0
- package/src/authoring/eyedropper-session.ts +60 -0
- package/src/authoring/instance-source-menu-register.ts +13 -0
- package/src/authoring/instance-source-menu.ts +135 -0
- package/src/authoring/layered-pick.ts +183 -0
- package/src/authoring/live-gesture-lock.ts +50 -0
- package/src/authoring/live-object-transform.ts +62 -0
- package/src/authoring/mount-failure-report.ts +154 -0
- package/src/authoring/mounted-root-subjects.ts +144 -0
- package/src/authoring/no-authoring-adapter.ts +55 -0
- package/src/authoring/null-inspection-subjects.tsx +75 -0
- package/src/authoring/object3d-document-persistence.ts +106 -0
- package/src/authoring/object3d-document-session-registry.ts +120 -0
- package/src/authoring/object3d-document-session.ts +1326 -0
- package/src/authoring/object3d-gesture-controller.ts +113 -0
- package/src/authoring/panel-authoring.ts +121 -0
- package/src/authoring/prefab-instance-inspector-section.tsx +237 -0
- package/src/authoring/prefab-instance-section-model.ts +55 -0
- package/src/authoring/project-authoring-session.ts +105 -0
- package/src/authoring/provenance.ts +99 -0
- package/src/authoring/quarks-particle-systems.ts +19 -0
- package/src/authoring/react-canvas-navigation.ts +255 -0
- package/src/authoring/react-design-canvas-style.ts +20 -0
- package/src/authoring/react-story-board.ts +937 -0
- package/src/authoring/selection-scope.ts +195 -0
- package/src/authoring/shell-document-ops.ts +169 -0
- package/src/authoring/shell-object3d-document-write-policy.ts +108 -0
- package/src/authoring/shell-viewport-policy.ts +48 -0
- package/src/authoring/source-object3d-authoring-adapter.ts +526 -0
- package/src/authoring/stories-scope.ts +35 -0
- package/src/authoring/story-board-chrome-fit.ts +107 -0
- package/src/authoring/story-board-presentation.ts +111 -0
- package/src/authoring/three-projection-core.ts +226 -0
- package/src/authoring/viewport-pick-context.ts +40 -0
- package/src/authoring/viewport-raycast.ts +240 -0
- package/src/authoring/viewport-tool-context.ts +73 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +152 -0
- package/src/authoring/world-pan-state.ts +198 -0
- package/src/authoring/world-session-state.ts +101 -0
- package/src/authoring/write-pipe.ts +173 -0
- package/src/availability-tick.ts +66 -0
- package/src/bitmap-label.ts +120 -0
- package/src/blender-tab-metrics.ts +161 -0
- package/src/board-open-actions.ts +20 -0
- package/src/boot-routing.ts +391 -0
- package/src/build-identity.ts +15 -0
- package/src/bytes-codec.ts +62 -0
- package/src/camera-authoring.ts +175 -0
- package/src/cancellation-reason.ts +58 -0
- package/src/canvas-preview-frames.ts +482 -0
- package/src/capture-camera-pose.ts +77 -0
- package/src/chrome-registry.ts +159 -0
- package/src/chrome-slot-registry.ts +91 -0
- package/src/collaboration-client.ts +264 -0
- package/src/collaboration-presence.ts +41 -0
- package/src/command-dispatch.ts +19 -0
- package/src/command-listener.ts +2743 -0
- package/src/command-registry.ts +70 -0
- package/src/component-board-registry.ts +205 -0
- package/src/component-states-registry.ts +200 -0
- package/src/components/AgentPresentationNotice.tsx +75 -0
- package/src/components/AlignToolbar.tsx +204 -0
- package/src/components/AppRoot.tsx +394 -0
- package/src/components/ApplicationMenus.tsx +415 -0
- package/src/components/AssetBrowser.tsx +2843 -0
- package/src/components/AssetEditorShell.tsx +216 -0
- package/src/components/AssetInspectorToolSection.tsx +124 -0
- package/src/components/BoardRulers.tsx +354 -0
- package/src/components/CameraInfo.tsx +81 -0
- package/src/components/CanvasSceneViewport.tsx +556 -0
- package/src/components/CapabilityCoverageSection.tsx +185 -0
- package/src/components/CenterDocuments.tsx +22 -0
- package/src/components/ChromeSlot.tsx +20 -0
- package/src/components/CodeView.tsx +470 -0
- package/src/components/CollaborationHeaderControl.css +74 -0
- package/src/components/CompactInspectorCard.tsx +190 -0
- package/src/components/CompactInspectorShell.tsx +39 -0
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +270 -0
- package/src/components/DefaultEditorLayout.tsx +106 -0
- package/src/components/DocumentHeaderStrip.tsx +96 -0
- package/src/components/DocumentShelfRail.tsx +45 -0
- package/src/components/DocumentThumbnail.tsx +345 -0
- package/src/components/EditorLeaseGuard.tsx +372 -0
- package/src/components/ErrorBanner.tsx +123 -0
- package/src/components/FolderPreviewTile.tsx +159 -0
- package/src/components/GameHierarchy.tsx +3842 -0
- package/src/components/GameplaySessionTimeline.tsx +295 -0
- package/src/components/InspectionProjection.tsx +968 -0
- package/src/components/Inspector.tsx +233 -0
- package/src/components/InspectorCanvasPreview.tsx +35 -0
- package/src/components/InspectorFieldsSection.tsx +290 -0
- package/src/components/InspectorObjectPreview.tsx +57 -0
- package/src/components/InspectorStoriesSection.tsx +92 -0
- package/src/components/InspectorToolSection.tsx +96 -0
- package/src/components/InspectorTransformSection.tsx +245 -0
- package/src/components/LightExplorerPanel.tsx +433 -0
- package/src/components/MediaProperties.tsx +145 -0
- package/src/components/NonThreeAuthoringBootstrap.tsx +19 -0
- package/src/components/Object3DDocumentToolbar.css +100 -0
- package/src/components/Object3DDocumentToolbar.tsx +454 -0
- package/src/components/Object3DDocumentViewport.tsx +58 -0
- package/src/components/ProjectHeader.css +617 -0
- package/src/components/ProjectHeader.tsx +328 -0
- package/src/components/ProjectLayout.tsx +249 -0
- package/src/components/ReactCanvasControls.tsx +284 -0
- package/src/components/RootSelectionOverlay.tsx +3162 -0
- package/src/components/RootTextEditor.tsx +79 -0
- package/src/components/SaveStatus.tsx +70 -0
- package/src/components/StageHost.tsx +1954 -0
- package/src/components/StageOverlays.tsx +21 -0
- package/src/components/StartupErrorScreen.tsx +76 -0
- package/src/components/StartupLoadingScreen.tsx +65 -0
- package/src/components/StatsOverlay.tsx +78 -0
- package/src/components/SurfaceStateOverlay.tsx +24 -0
- package/src/components/ToolContributionSurfaces.tsx +65 -0
- package/src/components/ToolHost.tsx +370 -0
- package/src/components/ToolObject3DPreview.tsx +39 -0
- package/src/components/Toolbar.tsx +595 -0
- package/src/components/TransientHint.tsx +44 -0
- package/src/components/TransportStrip.tsx +174 -0
- package/src/components/VersionControlSection.tsx +470 -0
- package/src/components/VgaiLogo.css +83 -0
- package/src/components/VgaiLogo.tsx +35 -0
- package/src/components/ViewportControlsHint.tsx +60 -0
- package/src/components/ViewportFurniture.tsx +401 -0
- package/src/components/ViewportOverlay.tsx +145 -0
- package/src/components/ViewportOverlaysMenu.tsx +153 -0
- package/src/components/ViewportShadingMenu.tsx +300 -0
- package/src/components/ViewportViewMenu.tsx +101 -0
- package/src/components/WorkspaceDocumentSurface.tsx +204 -0
- package/src/components/WorkspaceUtilitySurface.tsx +22 -0
- package/src/components/WorktreeSwitcher.css +239 -0
- package/src/components/WorktreeSwitcher.tsx +890 -0
- package/src/components/account-documents.tsx +1162 -0
- package/src/components/asset-documents.tsx +812 -0
- package/src/components/asset-editor-persistence.ts +216 -0
- package/src/components/asset-selection-section.tsx +535 -0
- package/src/components/asset-thumbnails.tsx +312 -0
- package/src/components/asset-viewers/AudioViewer.tsx +201 -0
- package/src/components/asset-viewers/EntityModelDocument.tsx +122 -0
- package/src/components/asset-viewers/EnvironmentAssetDocument.tsx +440 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +102 -0
- package/src/components/asset-viewers/ImageViewer.tsx +300 -0
- package/src/components/asset-viewers/JsonAssetDocument.tsx +93 -0
- package/src/components/asset-viewers/LiveModuleDocument.tsx +417 -0
- package/src/components/asset-viewers/LutAssetDocument.tsx +444 -0
- package/src/components/asset-viewers/ModelAssetDocument.tsx +105 -0
- package/src/components/asset-viewers/Object3DPreview.tsx +359 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +421 -0
- package/src/components/asset-viewers/PasteboardModuleDocument.tsx +67 -0
- package/src/components/asset-viewers/QuarksAssetDocument.tsx +526 -0
- package/src/components/asset-viewers/ShaderAssetDocument.tsx +743 -0
- package/src/components/asset-viewers/SourceAssetViewer.tsx +281 -0
- package/src/components/asset-viewers/SpritesheetSpriteView.tsx +102 -0
- package/src/components/asset-viewers/VideoViewer.tsx +101 -0
- package/src/components/asset-viewers/shader-source.ts +144 -0
- package/src/components/asset-workflow.css +731 -0
- package/src/components/board-guides.ts +150 -0
- package/src/components/compact-inspector.css +539 -0
- package/src/components/core-utilities.tsx +90 -0
- package/src/components/editor-notifications.css +40 -0
- package/src/components/engine-workspace.css +274 -0
- package/src/components/inspector-preview-section.tsx +238 -0
- package/src/components/inspector-property-grouping.ts +64 -0
- package/src/components/inspector-revert-label.ts +20 -0
- package/src/components/inspector-selection.ts +42 -0
- package/src/components/inspector-stories-gating.ts +171 -0
- package/src/components/inspector-transform-subject.ts +11 -0
- package/src/components/inspector-transform.ts +75 -0
- package/src/components/kind-documents.tsx +500 -0
- package/src/components/palette-action-publisher.tsx +140 -0
- package/src/components/primitives/DraftColorInput.tsx +74 -0
- package/src/components/product-shell.css +1829 -0
- package/src/components/project-tool-documents.tsx +410 -0
- package/src/components/scene-documents.tsx +223 -0
- package/src/components/stage-keyboard.tsx +37 -0
- package/src/components/stage-overlay-set.tsx +107 -0
- package/src/components/stage-presence-markers.ts +478 -0
- package/src/components/standard-viewport-dressing.ts +417 -0
- package/src/components/status-contributions.tsx +403 -0
- package/src/components/tool-documents.tsx +309 -0
- package/src/components/tool-schema-form.tsx +262 -0
- package/src/components/use-after-paint.ts +41 -0
- package/src/components/use-project-image-assets.ts +86 -0
- package/src/components/viewport-header-controls.css +90 -0
- package/src/components/viewport-surface-status.tsx +55 -0
- package/src/components/workspace-history.ts +32 -0
- package/src/components/workspace-static-panel-registry.tsx +152 -0
- package/src/components/workspace-surfaces.css +621 -0
- package/src/components/world-documents.tsx +586 -0
- package/src/components/world-overlay-gestures.ts +1694 -0
- package/src/composite-screenshot.ts +1641 -0
- package/src/console-sync.ts +131 -0
- package/src/constraint-helper.ts +338 -0
- package/src/content-entry-source-registry.ts +184 -0
- package/src/coverage/authoring-seam-evidence.ts +300 -0
- package/src/coverage/canvas-reveal.ts +192 -0
- package/src/coverage/design-time-surfaces.ts +101 -0
- package/src/coverage/live-seam-evidence.ts +11 -0
- package/src/coverage/ontology-invariants.ts +466 -0
- package/src/coverage/session-vitals.ts +501 -0
- package/src/coverage/system-seam-evidence.ts +72 -0
- package/src/crash-null-boundary.ts +36 -0
- package/src/creation-site-edit.ts +1479 -0
- package/src/creation-site-registry.ts +160 -0
- package/src/delegate-harness-registry.ts +143 -0
- package/src/document-context-registry.ts +177 -0
- package/src/document-open-registry.ts +200 -0
- package/src/document-preview-source.ts +20 -0
- package/src/document-renderer-session.ts +138 -0
- package/src/editor-api.ts +46 -0
- package/src/editor-chrome-capture.ts +139 -0
- package/src/editor-commands.ts +164 -0
- package/src/editor-console.ts +563 -0
- package/src/editor-current-view.ts +82 -0
- package/src/editor-document-probe.ts +881 -0
- package/src/editor-git-client.ts +115 -0
- package/src/editor-host-door.ts +462 -0
- package/src/editor-hotkeys.ts +842 -0
- package/src/editor-lease-view.ts +39 -0
- package/src/editor-lease.ts +415 -0
- package/src/editor-mode.ts +19 -0
- package/src/editor-notifications.ts +140 -0
- package/src/editor-presence.ts +563 -0
- package/src/editor-presentation-activity.ts +50 -0
- package/src/editor-presentation-notice.ts +42 -0
- package/src/editor-runtime.tsx +145 -0
- package/src/editor-server-response.ts +86 -0
- package/src/editor-session-attribution.ts +75 -0
- package/src/editor-session-mode.ts +54 -0
- package/src/editor-shell-store.ts +1304 -0
- package/src/editor-state-facets.ts +74 -0
- package/src/editor-styles.css +33 -0
- package/src/editor-view-presentation.ts +810 -0
- package/src/editor-viewport.ts +5302 -0
- package/src/entity-lod.ts +31 -0
- package/src/entity-object.ts +91 -0
- package/src/files/file-provider.ts +62 -0
- package/src/files/project-files.ts +264 -0
- package/src/finders/index.ts +136 -0
- package/src/finders/scenes-from-entrypoint-selection.ts +387 -0
- package/src/frame/bridge.tsx +1408 -0
- package/src/frame/product.ts +77 -0
- package/src/gameplay-dom-recording.ts +319 -0
- package/src/gameplay-export-state.ts +14 -0
- package/src/gameplay-replay.ts +417 -0
- package/src/gameplay-session-time.ts +9 -0
- package/src/gameplay-sessions.ts +204 -0
- package/src/harness-chat-types.ts +253 -0
- package/src/hierarchy-component-marks.ts +298 -0
- package/src/hierarchy-drop.ts +91 -0
- package/src/hierarchy-expansion-state.ts +80 -0
- package/src/hierarchy-header-slot.ts +52 -0
- package/src/hierarchy-internals.ts +197 -0
- package/src/hierarchy-kind-icon.ts +217 -0
- package/src/hierarchy-mark-reader.ts +73 -0
- package/src/hierarchy-menu-registry.ts +67 -0
- package/src/hierarchy-node-rows.ts +307 -0
- package/src/hierarchy-panel-view.ts +280 -0
- package/src/hierarchy-projection.ts +76 -0
- package/src/hierarchy-row-cache.ts +243 -0
- package/src/hierarchy-row-model.ts +308 -0
- package/src/hierarchy-rows.ts +11 -0
- package/src/hierarchy-walk.ts +86 -0
- package/src/history/editor-session.ts +25 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-delegate.ts +163 -0
- package/src/history/history-limit-notices.ts +43 -0
- package/src/history/history-service.ts +1173 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +386 -0
- package/src/history/project-root-history-backends.ts +139 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +103 -0
- package/src/history/source-history-backend.ts +546 -0
- package/src/history/types.ts +124 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hotkeys.ts +188 -0
- package/src/inference-diagnostics.ts +69 -0
- package/src/initial-project.ts +80 -0
- package/src/inspection/active-subject.ts +578 -0
- package/src/inspection/active-surface.ts +142 -0
- package/src/inspection/compose.ts +1064 -0
- package/src/inspection/display.ts +170 -0
- package/src/inspection/document-subject.ts +109 -0
- package/src/inspection/game-subject.ts +85 -0
- package/src/inspection/model.ts +542 -0
- package/src/inspection/null-subject.ts +115 -0
- package/src/inspection/serialize.ts +357 -0
- package/src/inspection/use-active-inspection.ts +180 -0
- package/src/inspector-presentation.ts +201 -0
- package/src/inspector-section-registry.ts +221 -0
- package/src/instance-source-actions.ts +163 -0
- package/src/instanced-presentation.ts +164 -0
- package/src/js-heap.ts +71 -0
- package/src/key-actions.ts +91 -0
- package/src/keymap-presets.ts +401 -0
- package/src/layout-policy.ts +31 -0
- package/src/learn-links.ts +73 -0
- package/src/light-explorer-model.ts +134 -0
- package/src/live-canvas-frame.ts +55 -0
- package/src/live-document.ts +279 -0
- package/src/live-module-source.ts +230 -0
- package/src/live-session-registry.ts +220 -0
- package/src/live-transition.ts +633 -0
- package/src/manifest-project.ts +107 -0
- package/src/model-thumbnail.ts +523 -0
- package/src/native-selection-style.ts +202 -0
- package/src/object3d-document-write-policy.ts +137 -0
- package/src/packaged-runtime.ts +108 -0
- package/src/palettes/maya.palette.json +57 -0
- package/src/palettes/substance.palette.json +57 -0
- package/src/pasteboard-module.ts +81 -0
- package/src/performance-profiler.ts +367 -0
- package/src/performance-sources.ts +69 -0
- package/src/play-boot-phase.ts +145 -0
- package/src/presentation-surface.ts +248 -0
- package/src/project-adapter.ts +1143 -0
- package/src/project-asset-refresh.ts +26 -0
- package/src/project-declaration-refresh.ts +31 -0
- package/src/project-local-state.ts +118 -0
- package/src/project-manager.ts +243 -0
- package/src/project-module-changes.ts +163 -0
- package/src/project-module-split.ts +266 -0
- package/src/project-provenance.ts +115 -0
- package/src/project-ready.ts +42 -0
- package/src/project-session-reset.ts +67 -0
- package/src/project-shape.ts +68 -0
- package/src/project-tool-discovery.ts +66 -0
- package/src/project-tools.ts +107 -0
- package/src/project-work-types.ts +149 -0
- package/src/projection/three.ts +898 -0
- package/src/projection/types.ts +44 -0
- package/src/readiness.ts +113 -0
- package/src/reflection-probe-helper.ts +142 -0
- package/src/reported-play-state.ts +90 -0
- package/src/resolve-relative-specifier.ts +33 -0
- package/src/scene-document-plan.ts +315 -0
- package/src/scene-framing.ts +315 -0
- package/src/scene-live-open.ts +210 -0
- package/src/scene-view-fog.ts +89 -0
- package/src/scoped-game-css.ts +152 -0
- package/src/session-orphan-record.ts +193 -0
- package/src/session-tombstone.ts +126 -0
- package/src/settings/settings-provider.ts +82 -0
- package/src/settings-store.ts +345 -0
- package/src/shared-view-restore.ts +42 -0
- package/src/shell-store-door.ts +45 -0
- package/src/source-conflict.ts +122 -0
- package/src/spatial-handle-visuals.ts +332 -0
- package/src/stage-context.ts +341 -0
- package/src/stage-store-registry.ts +63 -0
- package/src/stale-chunk-recovery.ts +34 -0
- package/src/state-report-deferral.ts +73 -0
- package/src/storage/host-files-storage.ts +97 -0
- package/src/storage/http-storage.ts +174 -0
- package/src/storage/index.ts +75 -0
- package/src/storage/mem-storage.ts +149 -0
- package/src/storage/path-lock.ts +44 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/types.ts +116 -0
- package/src/stories/StoryComponentThumbnail.tsx +184 -0
- package/src/stories/StoryPreviewMount.tsx +306 -0
- package/src/stories/component-content-source.tsx +106 -0
- package/src/stories/component-states-source.ts +78 -0
- package/src/stories/compose-project-stories.ts +255 -0
- package/src/stories/pixi-story-model.ts +30 -0
- package/src/stories/prefabs-finder.ts +54 -0
- package/src/stories/prefabs-from-stories.ts +182 -0
- package/src/stories/project-story-discovery.ts +78 -0
- package/src/stories/project-story-regions.ts +24 -0
- package/src/stories/story-actions.ts +54 -0
- package/src/stories/story-capture-command.ts +124 -0
- package/src/stories/story-capture.ts +584 -0
- package/src/stories/story-declared-medium.ts +126 -0
- package/src/stories/story-discovery.ts +176 -0
- package/src/stories/story-document-openers.ts +36 -0
- package/src/stories/story-dom-runtime.ts +78 -0
- package/src/stories/story-grouping.ts +111 -0
- package/src/stories/story-lane.ts +88 -0
- package/src/stories/story-mount-turn.ts +27 -0
- package/src/stories/story-opener.ts +260 -0
- package/src/stories/story-pixi-preview.ts +408 -0
- package/src/stories/story-presentation.ts +215 -0
- package/src/stories/story-registry.ts +517 -0
- package/src/stories/story-three-preview.ts +807 -0
- package/src/stories/three-story-model.ts +96 -0
- package/src/story-three-preview-runtime.ts +56 -0
- package/src/surface-keyboard.ts +112 -0
- package/src/surface-state.ts +124 -0
- package/src/tab-bootstrap.js +628 -0
- package/src/tab-census.ts +198 -0
- package/src/tab-lifecycle-client.ts +196 -0
- package/src/theme-library.ts +844 -0
- package/src/theme-preference.ts +378 -0
- package/src/theme.css +3437 -0
- package/src/three-viewport/bone-selection-highlight.ts +119 -0
- package/src/three-viewport/camera-fit.ts +41 -0
- package/src/three-viewport/interactive-renderer.ts +132 -0
- package/src/three-viewport/selection-brackets.ts +310 -0
- package/src/three-viewport/selection-outline.ts +183 -0
- package/src/three-viewport/skeleton-helper.ts +61 -0
- package/src/three-viewport/source-color.ts +167 -0
- package/src/three-viewport/studio-environment.ts +96 -0
- package/src/three-viewport-presentation.ts +23 -0
- package/src/tool-contribution-play.ts +74 -0
- package/src/tool-loader.ts +1798 -0
- package/src/transform-mode-request.ts +57 -0
- package/src/transient-hint.ts +78 -0
- package/src/trigger-volume-helper.ts +116 -0
- package/src/ui-source/adapter-region-includes.ts +241 -0
- package/src/ui-source/ensure-import.ts +132 -0
- package/src/ui-source/file-region-resolver.ts +302 -0
- package/src/ui-source/inspect.ts +775 -0
- package/src/ui-source/oid-transform.ts +1845 -0
- package/src/ui-source/plan-csf-story.ts +196 -0
- package/src/ui-source/plan-extract-component.ts +421 -0
- package/src/ui-source/plan-fork-component.ts +621 -0
- package/src/ui-source/plan-named-style.ts +95 -0
- package/src/ui-source/plan-source-edit.ts +353 -0
- package/src/ui-source/r3f-contract-resolver.ts +327 -0
- package/src/ui-source/r3f-diagnostic-index.ts +165 -0
- package/src/ui-source/r3f-environment-binding.ts +133 -0
- package/src/ui-source/r3f-joint-binding.ts +251 -0
- package/src/ui-source/r3f-lod-binding.ts +91 -0
- package/src/ui-source/r3f-particle-binding.ts +235 -0
- package/src/ui-source/r3f-physics-binding.ts +483 -0
- package/src/ui-source/r3f-project-contracts.ts +444 -0
- package/src/ui-source/relative-import-specifier.ts +34 -0
- package/src/ui-source/reparent-guard.ts +544 -0
- package/src/ui-source/source-edit-request.ts +97 -0
- package/src/ui-source/source-write-backend.ts +618 -0
- package/src/ui-source/syntactic-prop-specs.ts +296 -0
- package/src/ui-source/tier-source-write-backend.ts +279 -0
- package/src/ui-source/ts-ast.ts +171 -0
- package/src/ui-source/utility-class-support.ts +169 -0
- package/src/ui-source/write-component-default.ts +133 -0
- package/src/ui-source/writer.ts +1989 -0
- package/src/viewport-activation-timings.ts +840 -0
- package/src/viewport-authoring-policy.ts +186 -0
- package/src/viewport-controls-hint.ts +57 -0
- package/src/viewport-door.ts +237 -0
- package/src/viewport-shading-boundary.ts +12 -0
- package/src/vite-error-surface.ts +141 -0
- package/src/wait-until.ts +37 -0
- package/src/workspace-areas.ts +156 -0
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-available-documents.ts +127 -0
- package/src/workspace-core-utilities.ts +31 -0
- package/src/workspace-document-ids.ts +59 -0
- package/src/workspace-document-registry.ts +612 -0
- package/src/workspace-document-restore.ts +145 -0
- package/src/workspace-host-commands.ts +141 -0
- package/src/workspace-persistence-gate.ts +40 -0
- package/src/workspace-play-utilities.ts +44 -0
- package/src/workspace-presets.ts +425 -0
- package/src/workspace-regions.ts +294 -0
- package/src/workspace-state-persistence.ts +536 -0
- package/src/workspace-static-panels.ts +73 -0
- package/src/workspace-status-registry.ts +121 -0
- package/src/workspace-style.ts +239 -0
- package/src/workspace-utility-commands.ts +74 -0
- package/src/workspace-utility-registry.ts +263 -0
- package/src/workspace-viewport-rect.ts +97 -0
- package/src/world-adoption.ts +115 -0
- package/src/world-document-routing.ts +104 -0
- package/vite-plugin-creation-site-write.ts +184 -0
- package/vite-plugin-creation-site.ts +80 -0
- package/vite-plugin-game-static.ts +303 -0
- package/vite-plugin-module-doorways.ts +336 -0
- package/vite-plugin-product-contributions.ts +197 -0
- package/vite-plugin-project-game-static.ts +125 -0
- package/vite-plugin-project-jsx-js.ts +65 -0
- package/vite-plugin-project-root-absolute-assets.ts +359 -0
- package/vite-plugin-shared-react.ts +420 -0
- package/vite-plugin-shared-three.ts +203 -0
- package/vite-plugin-ui-oid.ts +2106 -0
|
@@ -0,0 +1,1362 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer, request as httpRequest, type Server } from 'node:http';
|
|
3
|
+
import { EDITOR_BRAND, editorAppIconSvg } from '@volter/editor-sdk/session/editor-brand';
|
|
4
|
+
import type {
|
|
5
|
+
ShareAccount,
|
|
6
|
+
ShareAuditEvent,
|
|
7
|
+
ShareCapability,
|
|
8
|
+
ShareParticipant,
|
|
9
|
+
ShareRole,
|
|
10
|
+
} from '@volter/editor-sdk/share';
|
|
11
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
12
|
+
import { editorHmrPort } from '@volter/editor-project/manifest/editor-port';
|
|
13
|
+
import {
|
|
14
|
+
isServableFsExtension,
|
|
15
|
+
isServableFsPath,
|
|
16
|
+
PROJECT_RESOURCE_EXTENSIONS,
|
|
17
|
+
SERVABLE_FS_EXTENSIONS,
|
|
18
|
+
} from './server-utils';
|
|
19
|
+
import { signedShareClaimHeaders } from './share-claims';
|
|
20
|
+
|
|
21
|
+
const COOKIE = 'vgai_share';
|
|
22
|
+
const AUTH_PATH = '/__vgai_share/auth';
|
|
23
|
+
const CALLBACK_PATH = '/__vgai_share/callback';
|
|
24
|
+
const COMPLETE_PATH = '/__vgai_share/complete';
|
|
25
|
+
const HEALTH_PATH = '/__vgai_share/health';
|
|
26
|
+
|
|
27
|
+
export type { ShareCapability, ShareRole } from '@volter/editor-sdk/share';
|
|
28
|
+
|
|
29
|
+
const ROLE_CAPABILITIES: Record<ShareRole, readonly ShareCapability[]> = {
|
|
30
|
+
viewer: ['view'],
|
|
31
|
+
commenter: ['view', 'comment'],
|
|
32
|
+
tester: ['view', 'comment', 'test'],
|
|
33
|
+
editor: ['view', 'comment', 'test', 'edit'],
|
|
34
|
+
terminal: ['view', 'comment', 'test', 'edit', 'terminal'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Least authority first. Derived from a `satisfies Record<ShareRole, …>` rank
|
|
39
|
+
* table rather than written as an array annotated `readonly ShareRole[]`, for
|
|
40
|
+
* the same reason `COLLABORATION_ROLE_ORDER` is: the array form ACCEPTS an
|
|
41
|
+
* incomplete list, so adding a role and forgetting it here compiles, and
|
|
42
|
+
* `roleAllowsRole` then reads the new role as over every ceiling. A missing key
|
|
43
|
+
* here is a compile error.
|
|
44
|
+
*/
|
|
45
|
+
const ROLE_RANK = {
|
|
46
|
+
viewer: 0,
|
|
47
|
+
commenter: 1,
|
|
48
|
+
tester: 2,
|
|
49
|
+
editor: 3,
|
|
50
|
+
terminal: 4,
|
|
51
|
+
} as const satisfies Record<ShareRole, number>;
|
|
52
|
+
|
|
53
|
+
const ROLE_ORDER: readonly ShareRole[] = (Object.keys(ROLE_RANK) as ShareRole[]).sort(
|
|
54
|
+
(a, b) => ROLE_RANK[a] - ROLE_RANK[b],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/** A share invitation's ceiling stops at `terminal`. The collaboration
|
|
58
|
+
* session's `maintainer` has no gateway meaning, so it is not a share role. */
|
|
59
|
+
export function isShareRole(value: string): value is ShareRole {
|
|
60
|
+
return (ROLE_ORDER as readonly string[]).includes(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The account as the GATEWAY holds it: the wire's {@link ShareAccount} plus the
|
|
65
|
+
* organization list it needs to resolve a team invitation. The extension is
|
|
66
|
+
* server-local by construction — `ShareHost.status()` projects a participant
|
|
67
|
+
* down to `ShareParticipant`, so the org list never rides the status wire.
|
|
68
|
+
*/
|
|
69
|
+
export interface ShareAccountProjection extends ShareAccount {
|
|
70
|
+
organizations?: Array<{
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
slug?: string;
|
|
74
|
+
role: string;
|
|
75
|
+
domains: Array<{
|
|
76
|
+
name: string;
|
|
77
|
+
verified: boolean;
|
|
78
|
+
enrollmentMode:
|
|
79
|
+
| 'manual_invitation'
|
|
80
|
+
| 'automatic_invitation'
|
|
81
|
+
| 'automatic_suggestion'
|
|
82
|
+
| 'enterprise_sso';
|
|
83
|
+
}>;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ShareGrantRedemption {
|
|
88
|
+
jti: string;
|
|
89
|
+
participant: ShareAccountProjection;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A redemption that failed because the VGAI account service could not be
|
|
94
|
+
* reached or answered 5xx — NOT because the grant belongs to a different
|
|
95
|
+
* invitation. The gateway must keep the two apart: an unreachable service is a
|
|
96
|
+
* 502 the participant can retry, while "this invitation does not own that
|
|
97
|
+
* grant" is the ordinary 403 that makes probing every live invitation safe.
|
|
98
|
+
*/
|
|
99
|
+
export class ShareGrantServiceError extends Error {}
|
|
100
|
+
|
|
101
|
+
export interface GatewayInvitation {
|
|
102
|
+
id: string;
|
|
103
|
+
token: string;
|
|
104
|
+
role: ShareRole;
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
authorize(): Promise<string>;
|
|
107
|
+
recipientAccountId?: string;
|
|
108
|
+
recipientEmail?: string;
|
|
109
|
+
recipientOrganizationId?: string;
|
|
110
|
+
recipientOrganizationDomain?: string;
|
|
111
|
+
usePolicy: 'person' | 'team';
|
|
112
|
+
redeem(grant: string): Promise<ShareGrantRedemption>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A wire {@link ShareParticipant} carrying the gateway's own richer account. */
|
|
116
|
+
export interface GatewayParticipant extends ShareParticipant {
|
|
117
|
+
account: ShareAccountProjection;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The audit row IS the wire's — the gateway adds nothing to it. */
|
|
121
|
+
export type GatewayAuditEvent = ShareAuditEvent;
|
|
122
|
+
|
|
123
|
+
export interface SessionShareGateway {
|
|
124
|
+
readonly port: number;
|
|
125
|
+
addInvitation(invitation: GatewayInvitation): void;
|
|
126
|
+
revokeInvitation(invitationId: string): number;
|
|
127
|
+
kickParticipant(participantId: string): number;
|
|
128
|
+
setParticipantRole(participantId: string, role: ShareRole): void;
|
|
129
|
+
/** Whether this id names a credentialed tunnel participant. The one question
|
|
130
|
+
* the editor's local routes ask before treating an id as remote. */
|
|
131
|
+
hasParticipant(participantId: string): boolean;
|
|
132
|
+
participants(): readonly GatewayParticipant[];
|
|
133
|
+
audit(): readonly GatewayAuditEvent[];
|
|
134
|
+
close(): Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface AuthenticatedShare {
|
|
138
|
+
credentialId: string;
|
|
139
|
+
participantId: string;
|
|
140
|
+
account: ShareAccountProjection;
|
|
141
|
+
invitationId: string;
|
|
142
|
+
role: ShareRole;
|
|
143
|
+
roleCeiling: ShareRole;
|
|
144
|
+
joined: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sameSecret(actual: string, expected: string): boolean {
|
|
148
|
+
const a = Buffer.from(actual);
|
|
149
|
+
const b = Buffer.from(expected);
|
|
150
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cookieValue(header: string | undefined, name: string): string | undefined {
|
|
154
|
+
return header
|
|
155
|
+
?.split(';')
|
|
156
|
+
.map((part) => part.trim().split('='))
|
|
157
|
+
.find(([key]) => key === name)?.[1];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const GATEWAY_PARTICIPANT_PREFIX = 'account-';
|
|
161
|
+
|
|
162
|
+
function participantIdFor(accountId: string): string {
|
|
163
|
+
return `${GATEWAY_PARTICIPANT_PREFIX}${createHash('sha256').update(accountId).digest('base64url').slice(0, 22)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether an id is one the gateway MINTS for a verified account, as opposed
|
|
167
|
+
* to one a local editor page minted for itself. The two id spaces are separate
|
|
168
|
+
* so a loopback caller cannot claim a remote person's identity after that
|
|
169
|
+
* person's credential is gone but their session record lingers. */
|
|
170
|
+
export function isGatewayParticipantId(value: string): boolean {
|
|
171
|
+
return value.startsWith(GATEWAY_PARTICIPANT_PREFIX);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A role outside `ROLE_ORDER` (the collaboration session's `maintainer`, say)
|
|
175
|
+
* is over EVERY ceiling: gateway roles stop at `terminal`. Answering with an
|
|
176
|
+
* `indexOf` of -1 would read as "below every ceiling" and let an unmodelled
|
|
177
|
+
* role be stored, where `ROLE_CAPABILITIES[role]` is undefined and the next
|
|
178
|
+
* proxied request throws inside the capability gate. */
|
|
179
|
+
function roleAllowsRole(ceiling: ShareRole, role: ShareRole): boolean {
|
|
180
|
+
const requested = ROLE_ORDER.indexOf(role);
|
|
181
|
+
return requested >= 0 && requested <= ROLE_ORDER.indexOf(ceiling);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const NEVER_SHARE_PATHS = new Set([
|
|
185
|
+
'/__editor/adapt-project',
|
|
186
|
+
'/__editor/browse-folder',
|
|
187
|
+
// The ComfyUI bridge reaches a LOCAL machine's install and its provider
|
|
188
|
+
// credentials; `never-share` is the conservative default this list exists to
|
|
189
|
+
// make explicit, and the route's own workstream can widen it deliberately.
|
|
190
|
+
// (Undeclared until now — the literal-route guard below has been red on
|
|
191
|
+
// main since these landed.)
|
|
192
|
+
'/__editor/comfyui/bridge/connect',
|
|
193
|
+
'/__editor/comfyui/bridge/install',
|
|
194
|
+
'/__editor/comfyui/provider-operation',
|
|
195
|
+
'/__editor/create-project',
|
|
196
|
+
'/__editor/download',
|
|
197
|
+
// Seven more that were never declared — the literal-route guard below has
|
|
198
|
+
// been red on main since each landed, and `requestedShareCapability`'s
|
|
199
|
+
// fallback has been answering `never-share` for all of them the whole time.
|
|
200
|
+
// Writing them down changes NO behavior; it only stops the guard reporting a
|
|
201
|
+
// backlog instead of a regression. Two of them deserve their own
|
|
202
|
+
// workstream's second look rather than a refactor's: a shared game page
|
|
203
|
+
// plausibly wants `scoped-game-css` and `play-phase`, and today it is
|
|
204
|
+
// refused both.
|
|
205
|
+
'/__editor/play-phase',
|
|
206
|
+
'/__editor/project-verbs',
|
|
207
|
+
'/__editor/recording/abort',
|
|
208
|
+
'/__editor/recording/asset',
|
|
209
|
+
'/__editor/recording/chunk',
|
|
210
|
+
'/__editor/recording/dom',
|
|
211
|
+
'/__editor/recording/finish',
|
|
212
|
+
'/__editor/recording/replay-file',
|
|
213
|
+
'/__editor/recording/start',
|
|
214
|
+
'/__editor/scoped-game-css',
|
|
215
|
+
'/__editor/editor-state',
|
|
216
|
+
// The USER settings layer is the host's own ~/.vgai/settings.json.
|
|
217
|
+
'/__editor/settings/user',
|
|
218
|
+
'/__editor/themes/user',
|
|
219
|
+
'/__editor/git/status',
|
|
220
|
+
'/__editor/inspect-project',
|
|
221
|
+
'/__editor/launcher-settings',
|
|
222
|
+
'/__editor/open-project',
|
|
223
|
+
'/__editor/recent-projects',
|
|
224
|
+
'/__editor/reveal',
|
|
225
|
+
'/__editor/save-thumbnail',
|
|
226
|
+
// The tab heartbeat is LOCAL presence. A remote participant's tab is seen
|
|
227
|
+
// through its bridged event stream (tab-presence.ts's union rule covers a
|
|
228
|
+
// client that cannot beat); letting it beat would put a foreign tab in the
|
|
229
|
+
// host session's table, where the bijection would then try to manage it.
|
|
230
|
+
// The worker script is refused for the same reason and in the same breath:
|
|
231
|
+
// handing a tunnelled tab a worker whose every beat gets a 403 would just
|
|
232
|
+
// be a 403 once per second forever.
|
|
233
|
+
'/__editor/heartbeat',
|
|
234
|
+
'/__editor/tab-heartbeat.js',
|
|
235
|
+
'/__editor/tab/ensure',
|
|
236
|
+
'/__editor/tab/expect-restart',
|
|
237
|
+
'/__editor/worktrees',
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const VIEW_API_PATHS = new Set([
|
|
241
|
+
'/__editor/assets',
|
|
242
|
+
'/__editor/collaboration',
|
|
243
|
+
'/__editor/collaboration/messages',
|
|
244
|
+
'/__editor/collaboration/revisions',
|
|
245
|
+
// Reading the session's unresolved errors/warnings. A guest's own tab is one
|
|
246
|
+
// of the things reporting into that set, and a guest who cannot see what
|
|
247
|
+
// their page just broke is a guest who reports it to nobody.
|
|
248
|
+
'/__editor/console',
|
|
249
|
+
'/__editor/collaboration/audit',
|
|
250
|
+
'/__editor/compatibility',
|
|
251
|
+
'/__editor/events',
|
|
252
|
+
'/__editor/examples',
|
|
253
|
+
'/__editor/export',
|
|
254
|
+
'/__editor/learn-thumbnail',
|
|
255
|
+
'/__editor/manifest',
|
|
256
|
+
'/__editor/project',
|
|
257
|
+
'/__editor/project-attribution',
|
|
258
|
+
'/__editor/project-components',
|
|
259
|
+
'/__editor/project-thumbnail',
|
|
260
|
+
'/__editor/project-resource',
|
|
261
|
+
'/__editor/repository-presence',
|
|
262
|
+
// The PROJECT settings layer is committed project data: read as view.
|
|
263
|
+
'/__editor/settings/project',
|
|
264
|
+
'/__editor/themes/project',
|
|
265
|
+
'/__editor/story-files',
|
|
266
|
+
'/__editor/source-conflict',
|
|
267
|
+
'/__editor/templates',
|
|
268
|
+
'/__editor/tab-yielded',
|
|
269
|
+
'/__editor/validation-log',
|
|
270
|
+
'/__editor/vgai-file',
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
const VIEW_MUTATION_PATHS = new Set([
|
|
274
|
+
'/__editor/collaboration/join',
|
|
275
|
+
'/__editor/collaboration/leave',
|
|
276
|
+
'/__editor/collaboration/presence',
|
|
277
|
+
// A page reporting an error IN ITSELF, like `tab/route` below: it grants the
|
|
278
|
+
// reporter nothing and claims nothing about anyone else, and a guest whose
|
|
279
|
+
// tab dies during boot needs the same honest answer the host gets.
|
|
280
|
+
'/__editor/page-error',
|
|
281
|
+
// Same reasoning as `page-error` directly above, and the same shape: a page
|
|
282
|
+
// saying what IT logged. It grants the reporter nothing.
|
|
283
|
+
'/__editor/console-entries',
|
|
284
|
+
// Same shape again: a page saying a condition IT raised is gone.
|
|
285
|
+
'/__editor/console-resolved',
|
|
286
|
+
'/__editor/tab-yielded',
|
|
287
|
+
'/__editor/tab/claim',
|
|
288
|
+
// A page saying IT is going away (`pagehide` beacon). Same shape as
|
|
289
|
+
// `tab/route` below: a statement about the sender and nobody else, and the
|
|
290
|
+
// difference between `closed` and `crashed` for a guest's tab too.
|
|
291
|
+
'/__editor/tab/close',
|
|
292
|
+
'/__editor/tab/route',
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const TEST_PATHS = new Set([
|
|
296
|
+
'/__editor/collaboration/team-test',
|
|
297
|
+
'/__editor/collaboration/team-test/stop',
|
|
298
|
+
'/__editor/log-entries',
|
|
299
|
+
'/__editor/log-session',
|
|
300
|
+
'/__editor/server-log',
|
|
301
|
+
'/__editor/state',
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
const EDIT_PATHS = new Set([
|
|
305
|
+
'/__editor/data-file',
|
|
306
|
+
'/__editor/manifest',
|
|
307
|
+
'/__editor/project-resource',
|
|
308
|
+
'/__editor/save-file',
|
|
309
|
+
'/__editor/settings/project',
|
|
310
|
+
'/__editor/themes/project',
|
|
311
|
+
'/__editor/source-conflict/resolve',
|
|
312
|
+
'/__editor/vgai-file',
|
|
313
|
+
]);
|
|
314
|
+
|
|
315
|
+
const TERMINAL_PATHS = new Set([
|
|
316
|
+
'/__editor/command',
|
|
317
|
+
// Reporting a command listener is what makes a tab ELIGIBLE to receive the
|
|
318
|
+
// owner's commands, so it belongs with its two siblings rather than with
|
|
319
|
+
// ordinary presence: a remote page that could claim eligibility could become
|
|
320
|
+
// the target of a `vgai eval` the local owner ran. (Undeclared until now —
|
|
321
|
+
// the literal-route guard has been red on main since this route landed.)
|
|
322
|
+
'/__editor/command-listener',
|
|
323
|
+
'/__editor/command-received',
|
|
324
|
+
// Acknowledging a console error CHANGES WHAT THE OWNER IS TOLD — it is the
|
|
325
|
+
// one door that takes something out of the set every `vgai` command shouts
|
|
326
|
+
// about. A guest who could reach it could silence the host's errors, so it
|
|
327
|
+
// sits with the owner-only verbs and not with the reporting route above.
|
|
328
|
+
'/__editor/console/ack',
|
|
329
|
+
'/__editor/command-result',
|
|
330
|
+
'/__editor/harness-chat',
|
|
331
|
+
'/__editor/harness-chat/host-action',
|
|
332
|
+
'/__editor/harness-chat/intent',
|
|
333
|
+
'/__editor/harness-chat/caller',
|
|
334
|
+
'/__editor/harness-terminal',
|
|
335
|
+
'/__editor/harness-terminal/action',
|
|
336
|
+
'/__editor/generations',
|
|
337
|
+
'/__editor/project-tools',
|
|
338
|
+
'/__editor/project-tools/run',
|
|
339
|
+
'/__vgai',
|
|
340
|
+
'/__vgai/screenshot',
|
|
341
|
+
'/__vgai/state',
|
|
342
|
+
'/__vgai/validation',
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
/** Types a shared browser legitimately fetches from a project that neither the
|
|
346
|
+
* `/@fs` module allowlist nor the writable project-resource allowlist names —
|
|
347
|
+
* page shells, source maps, fonts, icons, PNG textures, authored markdown. */
|
|
348
|
+
const SHARE_VIEW_EXTRA_EXTENSIONS: ReadonlySet<string> = new Set([
|
|
349
|
+
'html',
|
|
350
|
+
'ico',
|
|
351
|
+
'map',
|
|
352
|
+
'md',
|
|
353
|
+
'otf',
|
|
354
|
+
'png',
|
|
355
|
+
'ttf',
|
|
356
|
+
'webmanifest',
|
|
357
|
+
'woff',
|
|
358
|
+
'woff2',
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
/** YAML is an authored project resource the ASSET routes may carry, but it is
|
|
362
|
+
* not fetched by anything in a running project — and it is the shape a
|
|
363
|
+
* credential file most often takes (`secrets.yaml`, a service-account export).
|
|
364
|
+
* A remote participant reads project-owned YAML through
|
|
365
|
+
* `/__editor/project-resource`, where the project-root confinement applies. */
|
|
366
|
+
const SHARE_VIEW_DENIED_EXTENSIONS: ReadonlySet<string> = new Set(['yaml', 'yml']);
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Which raw (non-`/__`) project files a share VIEWER may fetch. This is an
|
|
370
|
+
* ALLOWLIST on purpose: the reverse — "serve anything that is not `.git`,
|
|
371
|
+
* `.vgai` or `.env*`" — hands the lowest remote role every `id_rsa`,
|
|
372
|
+
* `credentials.pem` and `backup.sqlite` that happens to sit in the project.
|
|
373
|
+
*
|
|
374
|
+
* Extensionless paths are the SPA root and Vite's virtual-module namespace
|
|
375
|
+
* (`/@vite/client`, `/@react-refresh`, `/@id/…`, `/@fs/…`). A bare
|
|
376
|
+
* extensionless path elsewhere is a file, not a route, so it is denied.
|
|
377
|
+
*/
|
|
378
|
+
function isShareViewableFile(path: string): boolean {
|
|
379
|
+
if (!isServableFsPath(path)) return false;
|
|
380
|
+
if (path === '/' || path.startsWith('/@')) return true;
|
|
381
|
+
const filename = path.split('/').pop() ?? '';
|
|
382
|
+
if (!filename.includes('.')) return false;
|
|
383
|
+
const extension = (filename.split('.').pop() ?? '').toLowerCase();
|
|
384
|
+
if (SHARE_VIEW_DENIED_EXTENSIONS.has(extension)) return false;
|
|
385
|
+
return (
|
|
386
|
+
SERVABLE_FS_EXTENSIONS.has(extension) ||
|
|
387
|
+
PROJECT_RESOURCE_EXTENSIONS.has(extension) ||
|
|
388
|
+
SHARE_VIEW_EXTRA_EXTENSIONS.has(extension)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function requestedShareCapability(
|
|
393
|
+
method: string | undefined,
|
|
394
|
+
rawUrl: string | undefined,
|
|
395
|
+
): ShareCapability {
|
|
396
|
+
return declaredShareCapability(method, rawUrl) ?? 'never-share';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Null means the route has no public-share declaration and is denied by the
|
|
400
|
+
* caller. Returning `never-share` is an explicit private-route declaration. */
|
|
401
|
+
export function declaredShareCapability(
|
|
402
|
+
method: string | undefined,
|
|
403
|
+
rawUrl: string | undefined,
|
|
404
|
+
): ShareCapability | null {
|
|
405
|
+
const path = decodedSharePath(rawUrl);
|
|
406
|
+
if (!path) return 'never-share';
|
|
407
|
+
const mutation = method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS';
|
|
408
|
+
if (path.startsWith('/@fs/')) {
|
|
409
|
+
const extension = path.split('.').pop() ?? '';
|
|
410
|
+
if (!isServableFsExtension(extension)) return 'never-share';
|
|
411
|
+
}
|
|
412
|
+
if (!path.startsWith('/__') && !isShareViewableFile(path)) return 'never-share';
|
|
413
|
+
if (
|
|
414
|
+
path.startsWith('/__editor/account') ||
|
|
415
|
+
path.startsWith('/__editor/share-control') ||
|
|
416
|
+
path.startsWith('/__editor/worktrees')
|
|
417
|
+
)
|
|
418
|
+
return 'never-share';
|
|
419
|
+
if (NEVER_SHARE_PATHS.has(path)) return 'never-share';
|
|
420
|
+
// Project Work is a read-only projection. Its POST action only resolves a
|
|
421
|
+
// work item into prompt context; invoking a harness remains guarded by the
|
|
422
|
+
// collaboration message/terminal capability path.
|
|
423
|
+
if (path === '/__editor/project-work' || path.startsWith('/__editor/project-work/')) {
|
|
424
|
+
return 'view';
|
|
425
|
+
}
|
|
426
|
+
if (path === '/__editor/collaboration/message') return 'comment';
|
|
427
|
+
if (path.startsWith('/__editor/collaboration/messages/')) return 'comment';
|
|
428
|
+
if (path === '/__editor/collaboration/role') return 'maintain';
|
|
429
|
+
// Project Work joins durable tracker data to coding-session context and can
|
|
430
|
+
// serve project-authored visualizer code. Keep the whole surface behind the
|
|
431
|
+
// same highest remote role as agent chat rather than exposing it to viewers.
|
|
432
|
+
if (path.startsWith('/__editor/project-work')) return 'terminal';
|
|
433
|
+
// The project's configurations (ARCHITECTURE-CORE §The project model):
|
|
434
|
+
// listing and reading a log is view-level; starting, stopping or building
|
|
435
|
+
// one runs on the HOST machine and is the host's alone.
|
|
436
|
+
if (path === '/__editor/configurations' || path.startsWith('/__editor/configurations/')) {
|
|
437
|
+
return mutation ? 'never-share' : 'view';
|
|
438
|
+
}
|
|
439
|
+
if (path === '/__editor/gameplay-sessions' || path.startsWith('/__editor/gameplay-sessions/')) {
|
|
440
|
+
// Gameplay Sessions expose the same complete Play log as `log-entries`,
|
|
441
|
+
// plus an optional recording. They are tester evidence, not the public
|
|
442
|
+
// read-only project projection, so keep both routes on one authority tier.
|
|
443
|
+
return mutation ? 'never-share' : 'test';
|
|
444
|
+
}
|
|
445
|
+
if (TEST_PATHS.has(path)) return 'test';
|
|
446
|
+
if (TERMINAL_PATHS.has(path) || path.startsWith('/__editor/generations/')) return 'terminal';
|
|
447
|
+
if (path.startsWith('/__vgai/state/')) return 'terminal';
|
|
448
|
+
// Synthetic Vite module routes (`/__vgai-react-world-runtime`,
|
|
449
|
+
// `/__vgai-game-provider`, `/__vgai-story-runtime`, …). These are read-only
|
|
450
|
+
// JS the editor imports to MOUNT THE GAME WORLD — without them a remote
|
|
451
|
+
// guest's viewport fails to mount ("world failed to mount") and they see the
|
|
452
|
+
// presence overlay over a blank scene. Served like any other module a viewer
|
|
453
|
+
// already receives; a mutation to this synthetic namespace has no meaning and
|
|
454
|
+
// stays refused. NOTE the HYPHEN: `/__vgai/` (slash) is the terminal/state
|
|
455
|
+
// surface above and is deliberately untouched.
|
|
456
|
+
if (path.startsWith('/__vgai-')) return mutation ? 'never-share' : 'view';
|
|
457
|
+
if (path.startsWith('/__ui-source/')) {
|
|
458
|
+
return !mutation && (path === '/__ui-source/read' || path === '/__ui-source/prepare')
|
|
459
|
+
? 'view'
|
|
460
|
+
: 'edit';
|
|
461
|
+
}
|
|
462
|
+
if (mutation && VIEW_MUTATION_PATHS.has(path)) return 'view';
|
|
463
|
+
if (mutation && EDIT_PATHS.has(path)) return 'edit';
|
|
464
|
+
if (path === '/__editor/asset-library' || path.startsWith('/__editor/asset-library/')) {
|
|
465
|
+
return mutation ? 'edit' : 'view';
|
|
466
|
+
}
|
|
467
|
+
if (VIEW_API_PATHS.has(path) || path === '/__editor/data-files') {
|
|
468
|
+
return mutation ? 'never-share' : 'view';
|
|
469
|
+
}
|
|
470
|
+
// Reaching here means the path already passed the viewer file allowlist above.
|
|
471
|
+
if (!path.startsWith('/__')) return mutation ? 'never-share' : 'view';
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function decodedSharePath(rawUrl: string | undefined): string | null {
|
|
476
|
+
try {
|
|
477
|
+
let path = new URL(rawUrl ?? '/', 'http://vgai.local').pathname;
|
|
478
|
+
for (let pass = 0; pass < 4; pass += 1) {
|
|
479
|
+
const decoded = decodeURIComponent(path);
|
|
480
|
+
if (decoded === path) return path;
|
|
481
|
+
path = decoded;
|
|
482
|
+
}
|
|
483
|
+
// A residual escape after four passes has no legitimate editor-module use
|
|
484
|
+
// and could be decoded again by a downstream URL/filesystem layer.
|
|
485
|
+
return path.includes('%') ? null : path;
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isRepositoryPresenceRead(method: string | undefined, rawUrl: string | undefined): boolean {
|
|
492
|
+
return (
|
|
493
|
+
(method === 'GET' || method === 'HEAD') &&
|
|
494
|
+
decodedSharePath(rawUrl) === '/__editor/repository-presence'
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function shareRoleAllows(
|
|
499
|
+
role: ShareRole,
|
|
500
|
+
method: string | undefined,
|
|
501
|
+
rawUrl: string | undefined,
|
|
502
|
+
): boolean {
|
|
503
|
+
const capability = requestedShareCapability(method, rawUrl);
|
|
504
|
+
return capability !== 'never-share' && ROLE_CAPABILITIES[role].includes(capability);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function upstreamHeaders(
|
|
508
|
+
headers: Record<string, string | string[] | undefined>,
|
|
509
|
+
targetPort: number,
|
|
510
|
+
share: AuthenticatedShare,
|
|
511
|
+
method: string,
|
|
512
|
+
rawUrl: string,
|
|
513
|
+
claimSecret: string,
|
|
514
|
+
): Record<string, string | string[] | undefined> {
|
|
515
|
+
const next = { ...headers };
|
|
516
|
+
delete next['cookie'];
|
|
517
|
+
for (const header of Object.keys(next)) {
|
|
518
|
+
if (header.toLowerCase().startsWith('x-vgai-share-')) delete next[header];
|
|
519
|
+
}
|
|
520
|
+
next['host'] = `127.0.0.1:${targetPort}`;
|
|
521
|
+
if (next['origin']) next['origin'] = `http://127.0.0.1:${targetPort}`;
|
|
522
|
+
Object.assign(
|
|
523
|
+
next,
|
|
524
|
+
signedShareClaimHeaders(claimSecret, method, rawUrl, {
|
|
525
|
+
participantId: share.participantId,
|
|
526
|
+
invitationId: share.invitationId,
|
|
527
|
+
credentialId: share.credentialId,
|
|
528
|
+
role: share.role,
|
|
529
|
+
account: share.account,
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
return next;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The invitee's first impression of VGAI, and it must survive the gateway's
|
|
536
|
+
* `default-src 'none'` CSP: no external asset, no framework, one inline style
|
|
537
|
+
* block shared by both pages. */
|
|
538
|
+
const SHARE_PAGE_STYLES = `:root{color-scheme:dark}
|
|
539
|
+
body{margin:0;display:grid;place-items:center;min-height:100vh;background:#111418;color:#c9d1d9;font:14px/1.5 system-ui,sans-serif}
|
|
540
|
+
main{max-width:32rem;padding:28px 32px;border:1px solid #232a31;border-radius:12px;background:#171b20;text-align:center}
|
|
541
|
+
.brand-mark{width:64px;height:64px;margin:0 auto 18px}.brand-mark svg{display:block;width:100%;height:100%}
|
|
542
|
+
h1{margin:0 0 8px;font-size:18px;font-weight:600;color:#e6edf3}
|
|
543
|
+
p{margin:6px 0}
|
|
544
|
+
.subject{color:#8b949e}
|
|
545
|
+
.role{color:#e6edf3}
|
|
546
|
+
.role b{color:#58a6ff}
|
|
547
|
+
.status{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:16px}
|
|
548
|
+
.error{color:#f0883e}
|
|
549
|
+
.spinner{width:14px;height:14px;border:2px solid #30363d;border-top-color:#58a6ff;border-radius:50%;animation:spin 900ms linear infinite}
|
|
550
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
551
|
+
button{margin-top:14px;padding:7px 14px;border:1px solid #30363d;border-radius:7px;background:#21262d;color:#e6edf3;font:inherit;cursor:pointer}
|
|
552
|
+
button:hover{border-color:#58a6ff}
|
|
553
|
+
[hidden]{display:none}`;
|
|
554
|
+
|
|
555
|
+
/** These values are host/project supplied, so they are escaped, never trusted. */
|
|
556
|
+
function escapeHtml(value: string): string {
|
|
557
|
+
return value.replace(
|
|
558
|
+
/[&<>"']/g,
|
|
559
|
+
(character) =>
|
|
560
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] ??
|
|
561
|
+
character,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export interface ShareSessionDisplay {
|
|
566
|
+
projectName?: string;
|
|
567
|
+
hostName?: string;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function invitationSubject(display: ShareSessionDisplay | undefined): string {
|
|
571
|
+
const project = display?.projectName ? escapeHtml(display.projectName) : '';
|
|
572
|
+
const host = display?.hostName ? escapeHtml(display.hostName) : '';
|
|
573
|
+
if (project && host) return `${host} shared the project <b>${project}</b> with you.`;
|
|
574
|
+
if (project) return `Project <b>${project}</b>.`;
|
|
575
|
+
if (host) return `Shared by ${host}.`;
|
|
576
|
+
return '';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function sharePage(title: string, body: string, script: string): string {
|
|
580
|
+
const fullTitle = `${title} — ${EDITOR_BRAND.name}`;
|
|
581
|
+
const description = 'Open a shared Volter Editor session.';
|
|
582
|
+
const favicon = `data:image/svg+xml,${encodeURIComponent(editorAppIconSvg())}`;
|
|
583
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="${EDITOR_BRAND.themeColor}"><meta name="robots" content="noindex,nofollow"><meta name="description" content="${description}"><link rel="icon" type="image/svg+xml" href="${favicon}"><meta property="og:type" content="website"><meta property="og:site_name" content="${EDITOR_BRAND.name}"><meta property="og:title" content="${fullTitle}"><meta property="og:description" content="${description}"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="${fullTitle}"><meta name="twitter:description" content="${description}"><title>${fullTitle}</title><style>${SHARE_PAGE_STYLES}</style></head>
|
|
584
|
+
<body><main><div class="brand-mark">${editorAppIconSvg()}</div>${body}</main><script>${script}</script></body></html>`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function landing(display?: ShareSessionDisplay): string {
|
|
588
|
+
const subject = invitationSubject(display);
|
|
589
|
+
return sharePage(
|
|
590
|
+
'Join Volter Editor session',
|
|
591
|
+
`<h1>You've been invited to a Volter Editor live session</h1>
|
|
592
|
+
${subject ? `<p class="subject">${subject}</p>` : ''}
|
|
593
|
+
<p class="role" id="role" hidden></p>
|
|
594
|
+
<p class="subject">Joining takes a Volter account — sign in, or create one right there (Google or email; it's free).</p>
|
|
595
|
+
<p class="status" id="status"><span class="spinner"></span><span id="statusText">Checking your invitation…</span></p>
|
|
596
|
+
<button id="retry" type="button" hidden>Retry</button>`,
|
|
597
|
+
`const statusRow=document.getElementById('status');
|
|
598
|
+
const statusText=document.getElementById('statusText');
|
|
599
|
+
const spinner=statusRow.querySelector('.spinner');
|
|
600
|
+
const retry=document.getElementById('retry');
|
|
601
|
+
const roleLine=document.getElementById('role');
|
|
602
|
+
const token=new URLSearchParams(location.hash.slice(1)).get('token');
|
|
603
|
+
function fail(message){statusText.textContent=message;statusRow.classList.add('error');spinner.hidden=true;retry.hidden=!token}
|
|
604
|
+
function working(message){statusText.textContent=message;statusRow.classList.remove('error');spinner.hidden=false;retry.hidden=true}
|
|
605
|
+
function join(){
|
|
606
|
+
if(!token){fail('This link is incomplete — ask for a fresh invitation link.');return}
|
|
607
|
+
working('Checking your invitation…');
|
|
608
|
+
fetch('${AUTH_PATH}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token})}).then(async r=>{
|
|
609
|
+
if(r.status===429){fail('Too many attempts — wait a minute.');return}
|
|
610
|
+
if(!r.ok){fail('This invitation is invalid, expired, or was revoked — ask the host for a new link.');return}
|
|
611
|
+
const body=await r.json();
|
|
612
|
+
if(body.role){roleLine.hidden=false;roleLine.innerHTML='Your access: <b>'+String(body.role).replace(/[^a-z]/gi,'')+'</b>'}
|
|
613
|
+
working('Taking you to Volter Editor — sign in or create your account…');
|
|
614
|
+
history.replaceState(null,'',location.pathname);
|
|
615
|
+
location.replace(body.authorizationUrl);
|
|
616
|
+
}).catch(()=>{fail('This invitation is invalid, expired, or was revoked — ask the host for a new link.')});
|
|
617
|
+
}
|
|
618
|
+
retry.addEventListener('click',join);
|
|
619
|
+
join();`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function callbackLanding(): string {
|
|
624
|
+
return sharePage(
|
|
625
|
+
'Join Volter Editor session',
|
|
626
|
+
`<h1>Finishing your Volter Editor sign-in</h1>
|
|
627
|
+
<p class="status" id="status"><span class="spinner"></span><span id="statusText">Confirming your account…</span></p>`,
|
|
628
|
+
`const statusRow=document.getElementById('status');
|
|
629
|
+
const statusText=document.getElementById('statusText');
|
|
630
|
+
const spinner=statusRow.querySelector('.spinner');
|
|
631
|
+
function fail(message){statusText.textContent=message;statusRow.classList.add('error');spinner.hidden=true}
|
|
632
|
+
const params=new URLSearchParams(location.hash.slice(1));
|
|
633
|
+
const grant=params.get('grant');
|
|
634
|
+
const invitation=params.get('invitation');
|
|
635
|
+
history.replaceState(null,'',location.pathname);
|
|
636
|
+
if(!grant){fail('This link is incomplete — ask the host for a new invitation link.')}
|
|
637
|
+
else fetch('${COMPLETE_PATH}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(invitation?{grant,invitationId:invitation}:{grant})}).then(async r=>{
|
|
638
|
+
if(r.status===429){fail('Too many attempts — wait a minute.');return}
|
|
639
|
+
if(!r.ok){fail('This invitation is invalid, expired, or was revoked — ask the host for a new link.');return}
|
|
640
|
+
const body=await r.json();
|
|
641
|
+
try{localStorage.setItem('vgai.collaboration.participant.v1',body.participantId);localStorage.setItem('vgai.collaboration.remote-share.v1','1')}
|
|
642
|
+
catch{fail('This browser blocks site storage; the shared editor cannot keep your identity. Enable storage/cookies for this site and reopen the link.');return}
|
|
643
|
+
statusText.textContent='Loading the editor — the first load can take a while.';
|
|
644
|
+
setTimeout(()=>location.replace('/'),600);
|
|
645
|
+
}).catch(()=>{fail('This invitation is invalid, expired, or was revoked — ask the host for a new link.')});`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function readJsonBody(
|
|
650
|
+
request: import('node:http').IncomingMessage,
|
|
651
|
+
maximumBytes: number,
|
|
652
|
+
): Promise<Record<string, unknown>> {
|
|
653
|
+
const chunks: Buffer[] = [];
|
|
654
|
+
let size = 0;
|
|
655
|
+
for await (const chunk of request) {
|
|
656
|
+
const buffer = Buffer.from(chunk);
|
|
657
|
+
size += buffer.length;
|
|
658
|
+
if (size > maximumBytes) throw new Error('Authentication body is too large.');
|
|
659
|
+
chunks.push(buffer);
|
|
660
|
+
}
|
|
661
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function stringField(body: Record<string, unknown>, key: string, maximumLength: number): string {
|
|
665
|
+
const value = body[key];
|
|
666
|
+
return typeof value === 'string' && value.length <= maximumLength ? value : '';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function html(response: import('node:http').ServerResponse, content: string): void {
|
|
670
|
+
response.writeHead(200, {
|
|
671
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
672
|
+
'Cache-Control': 'no-store',
|
|
673
|
+
'Referrer-Policy': 'no-referrer',
|
|
674
|
+
'X-Content-Type-Options': 'nosniff',
|
|
675
|
+
'Content-Security-Policy':
|
|
676
|
+
"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'",
|
|
677
|
+
});
|
|
678
|
+
response.end(content);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function participantFromEventsUrl(rawUrl: string | undefined): string | null {
|
|
682
|
+
const url = new URL(rawUrl ?? '/', 'http://vgai.local');
|
|
683
|
+
if (url.pathname !== '/__editor/events') return null;
|
|
684
|
+
const participantId = url.searchParams.get('participantId')?.trim();
|
|
685
|
+
return participantId && participantId.length <= 200 ? participantId : null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function forwardEventStreamToWebSocket(
|
|
689
|
+
client: WebSocket,
|
|
690
|
+
upstream: import('node:http').ClientRequest,
|
|
691
|
+
): void {
|
|
692
|
+
upstream.once('response', (response) => {
|
|
693
|
+
if (response.statusCode !== 200) {
|
|
694
|
+
client.close(1011, 'Editor event stream refused the connection.');
|
|
695
|
+
response.resume();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
let buffered = '';
|
|
699
|
+
response.setEncoding('utf8');
|
|
700
|
+
response.on('data', (chunk: string) => {
|
|
701
|
+
buffered += chunk;
|
|
702
|
+
const frames = buffered.split(/\r?\n\r?\n/);
|
|
703
|
+
buffered = frames.pop() ?? '';
|
|
704
|
+
for (const frame of frames) {
|
|
705
|
+
let event = 'message';
|
|
706
|
+
let id = '';
|
|
707
|
+
const data: string[] = [];
|
|
708
|
+
for (const line of frame.split(/\r?\n/)) {
|
|
709
|
+
if (line.startsWith('event:')) event = line.slice(6).trimStart();
|
|
710
|
+
else if (line.startsWith('id:')) id = line.slice(3).trimStart();
|
|
711
|
+
else if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
|
712
|
+
}
|
|
713
|
+
if (data.length === 0 || client.readyState !== WebSocket.OPEN) continue;
|
|
714
|
+
client.send(JSON.stringify({ event, data: data.join('\n'), ...(id ? { id } : {}) }));
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
response.once('end', () => {
|
|
718
|
+
if (client.readyState === WebSocket.OPEN) client.close(1012, 'Editor event stream closed.');
|
|
719
|
+
});
|
|
720
|
+
response.once('error', () => client.terminate());
|
|
721
|
+
client.once('close', () => response.destroy());
|
|
722
|
+
});
|
|
723
|
+
upstream.once('error', () => client.terminate());
|
|
724
|
+
upstream.end();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Every gate reads ONE normalization. Classifying capability from the decoded
|
|
728
|
+
* path while gating CSRF/rate/joined from the raw one let `/__editor%2Fsave-file`
|
|
729
|
+
* be routed as an edit and skip the mutation-only gates entirely. */
|
|
730
|
+
function isEditorMutation(method: string | undefined, rawUrl: string | undefined): boolean {
|
|
731
|
+
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return false;
|
|
732
|
+
const path = decodedSharePath(rawUrl);
|
|
733
|
+
return path === null || path.startsWith('/__editor/') || path.startsWith('/__ui-source/');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function sameOrigin(request: import('node:http').IncomingMessage): boolean {
|
|
737
|
+
const origin = request.headers.origin;
|
|
738
|
+
const host = request.headers.host;
|
|
739
|
+
if (!origin || !host) return false;
|
|
740
|
+
try {
|
|
741
|
+
return new URL(origin).host === host;
|
|
742
|
+
} catch {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function isViteClientRequest(method: string | undefined, rawUrl: string | undefined): boolean {
|
|
748
|
+
return (
|
|
749
|
+
method === 'GET' && new URL(rawUrl ?? '/', 'http://vgai.local').pathname === '/@vite/client'
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function isViteHmrUpgrade(request: import('node:http').IncomingMessage): boolean {
|
|
754
|
+
const value = request.headers['sec-websocket-protocol'];
|
|
755
|
+
const protocols = Array.isArray(value) ? value : [value ?? ''];
|
|
756
|
+
return protocols.flatMap((item) => item.split(',')).some((item) => item.trim() === 'vite-hmr');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** The local editor uses its paired private HMR port. A public tunnel has one
|
|
760
|
+
* HTTPS authority, so only its copy of Vite's client points the socket back at
|
|
761
|
+
* standard WSS; the gateway then relays the `vite-hmr` upgrade to that private
|
|
762
|
+
* port. Local tabs continue to receive Vite's unmodified client. */
|
|
763
|
+
export function publicViteClient(source: string): string {
|
|
764
|
+
const portLine = /^const hmrPort = .*;$/m;
|
|
765
|
+
const directLine = /^const directSocketHost = .*;$/m;
|
|
766
|
+
if (!portLine.test(source) || !directLine.test(source)) {
|
|
767
|
+
throw new Error('The Vite HMR client shape is not supported by the share gateway.');
|
|
768
|
+
}
|
|
769
|
+
return source
|
|
770
|
+
.replace(portLine, 'const hmrPort = 443;')
|
|
771
|
+
.replace(directLine, 'const directSocketHost = socketHost;');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export async function createSessionShareGateway(options: {
|
|
775
|
+
targetPort: number;
|
|
776
|
+
hmrPort?: number;
|
|
777
|
+
claimSecret: string;
|
|
778
|
+
now?: () => number;
|
|
779
|
+
/** Named on the invitee's join card so the link is not anonymous. */
|
|
780
|
+
display?: ShareSessionDisplay;
|
|
781
|
+
}): Promise<SessionShareGateway> {
|
|
782
|
+
const now = options.now ?? Date.now;
|
|
783
|
+
const hmrPort = options.hmrPort ?? editorHmrPort(options.targetPort);
|
|
784
|
+
const invitations = new Map<string, GatewayInvitation>();
|
|
785
|
+
const credentials = new Map<string, AuthenticatedShare>();
|
|
786
|
+
/** jti → the instant the record may be forgotten. A grant lives ≤2 minutes,
|
|
787
|
+
* so five minutes of retention outlives every grant it must refuse twice. */
|
|
788
|
+
const usedGrantIds = new Map<string, number>();
|
|
789
|
+
const GRANT_REPLAY_RETENTION_MS = 5 * 60_000;
|
|
790
|
+
const assignedRoles = new Map<string, ShareRole>();
|
|
791
|
+
const kickedParticipants = new Set<string>();
|
|
792
|
+
const personInvitationAccounts = new Map<string, string>();
|
|
793
|
+
const auditEvents: GatewayAuditEvent[] = [];
|
|
794
|
+
/** Keyed partly by remote address, so an unauthenticated caller decides how
|
|
795
|
+
* many entries exist. Both this and {@link usedGrantIds} sweep their expired
|
|
796
|
+
* records once past a ceiling rather than growing until the session closes. */
|
|
797
|
+
const rateWindows = new Map<string, { startedAt: number; count: number; windowMs: number }>();
|
|
798
|
+
const MAP_SWEEP_THRESHOLD = 1_024;
|
|
799
|
+
const audit = (event: Omit<GatewayAuditEvent, 'at'>): void => {
|
|
800
|
+
auditEvents.push({ at: new Date(now()).toISOString(), ...event });
|
|
801
|
+
if (auditEvents.length > 500) auditEvents.splice(0, auditEvents.length - 500);
|
|
802
|
+
};
|
|
803
|
+
const rateAllowed = (key: string, limit: number, windowMs: number): boolean => {
|
|
804
|
+
if (rateWindows.size > MAP_SWEEP_THRESHOLD) {
|
|
805
|
+
for (const [candidate, window] of rateWindows) {
|
|
806
|
+
if (now() - window.startedAt >= window.windowMs) rateWindows.delete(candidate);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const current = rateWindows.get(key);
|
|
810
|
+
if (!current || now() - current.startedAt >= windowMs) {
|
|
811
|
+
rateWindows.set(key, { startedAt: now(), count: 1, windowMs });
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
current.count += 1;
|
|
815
|
+
return current.count <= limit;
|
|
816
|
+
};
|
|
817
|
+
const rememberGrantId = (jti: string): void => {
|
|
818
|
+
if (usedGrantIds.size > MAP_SWEEP_THRESHOLD) {
|
|
819
|
+
for (const [candidate, expiresAt] of usedGrantIds) {
|
|
820
|
+
if (expiresAt <= now()) usedGrantIds.delete(candidate);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
usedGrantIds.set(jti, now() + GRANT_REPLAY_RETENTION_MS);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Credential → the terminators for every connection that credential is
|
|
828
|
+
* currently holding open: bridged event WebSockets, proxied upgrade socket
|
|
829
|
+
* pairs, and the long-lived SSE responses that reach the HTTP branch.
|
|
830
|
+
*
|
|
831
|
+
* Deleting a credential only stops the NEXT request. A kicked participant's
|
|
832
|
+
* already-open event stream keeps delivering the whole session until they
|
|
833
|
+
* choose to reconnect, so removal must reach the sockets too. A role change
|
|
834
|
+
* deliberately does not: it applies on the participant's next request.
|
|
835
|
+
*/
|
|
836
|
+
const liveConnections = new Map<string, Set<() => void>>();
|
|
837
|
+
const trackConnection = (credentialId: string, terminate: () => void): (() => void) => {
|
|
838
|
+
const held = liveConnections.get(credentialId) ?? new Set<() => void>();
|
|
839
|
+
liveConnections.set(credentialId, held);
|
|
840
|
+
held.add(terminate);
|
|
841
|
+
return () => {
|
|
842
|
+
held.delete(terminate);
|
|
843
|
+
if (held.size === 0) liveConnections.delete(credentialId);
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
const severConnections = (credentialIds: Iterable<string>): void => {
|
|
847
|
+
for (const credentialId of credentialIds) {
|
|
848
|
+
const held = liveConnections.get(credentialId);
|
|
849
|
+
if (!held) continue;
|
|
850
|
+
liveConnections.delete(credentialId);
|
|
851
|
+
for (const terminate of held) terminate();
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const invitationForToken = (token: string): GatewayInvitation | null => {
|
|
856
|
+
for (const invitation of invitations.values()) {
|
|
857
|
+
if (invitation.expiresAt > now() && sameSecret(token, invitation.token)) return invitation;
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
};
|
|
861
|
+
const activeShare = (request: import('node:http').IncomingMessage): AuthenticatedShare | null => {
|
|
862
|
+
const credential = cookieValue(request.headers.cookie, COOKIE);
|
|
863
|
+
const share = credential ? credentials.get(credential) : undefined;
|
|
864
|
+
if (!share || kickedParticipants.has(share.participantId)) return null;
|
|
865
|
+
const invitation = invitations.get(share.invitationId);
|
|
866
|
+
return invitation && invitation.expiresAt > now() ? share : null;
|
|
867
|
+
};
|
|
868
|
+
const eventWebSockets = new WebSocketServer({ noServer: true });
|
|
869
|
+
|
|
870
|
+
const server: Server = createServer(async (request, response) => {
|
|
871
|
+
if ((request.method === 'GET' || request.method === 'HEAD') && request.url === HEALTH_PATH) {
|
|
872
|
+
response.writeHead(200, {
|
|
873
|
+
'Content-Type': 'application/json',
|
|
874
|
+
'Cache-Control': 'no-store',
|
|
875
|
+
'X-VGAI-Share-Gateway': 'ready',
|
|
876
|
+
});
|
|
877
|
+
response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ ready: true }));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (request.url === AUTH_PATH && request.method === 'POST') {
|
|
881
|
+
if (!sameOrigin(request)) {
|
|
882
|
+
response.writeHead(403, { 'Cache-Control': 'no-store' }).end('Invalid request origin.');
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// Throttle BEFORE the token is read: a limiter that only runs after a
|
|
886
|
+
// token validates leaves guessing unbounded, and every guess costs one
|
|
887
|
+
// timing-safe compare per live invitation.
|
|
888
|
+
if (!rateAllowed(`auth:${request.socket.remoteAddress ?? 'unknown'}`, 20, 60_000)) {
|
|
889
|
+
response.writeHead(429).end('Too many invitation authentication attempts.');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
const invitation = invitationForToken(
|
|
894
|
+
stringField(await readJsonBody(request, 4_096), 'token', 4_096),
|
|
895
|
+
);
|
|
896
|
+
if (!invitation) throw new Error('Invalid or expired share token.');
|
|
897
|
+
audit({ type: 'auth-started', invitationId: invitation.id });
|
|
898
|
+
response.writeHead(200, {
|
|
899
|
+
'Content-Type': 'application/json',
|
|
900
|
+
'Cache-Control': 'no-store',
|
|
901
|
+
});
|
|
902
|
+
// The role is per-invitation, so the landing card can only name it from
|
|
903
|
+
// here — it has nothing but a token.
|
|
904
|
+
response.end(
|
|
905
|
+
JSON.stringify({ authorizationUrl: await invitation.authorize(), role: invitation.role }),
|
|
906
|
+
);
|
|
907
|
+
} catch {
|
|
908
|
+
audit({ type: 'auth-failed' });
|
|
909
|
+
response.writeHead(403).end('Invalid authentication request.');
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (request.url === COMPLETE_PATH && request.method === 'POST') {
|
|
914
|
+
if (!sameOrigin(request)) {
|
|
915
|
+
response.writeHead(403, { 'Cache-Control': 'no-store' }).end('Invalid request origin.');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
if (!rateAllowed(`complete:${request.socket.remoteAddress ?? 'unknown'}`, 40, 60_000)) {
|
|
920
|
+
response.writeHead(429).end('Too many authorization attempts.');
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const body = await readJsonBody(request, 32_768);
|
|
924
|
+
const grant = stringField(body, 'grant', 32_768);
|
|
925
|
+
// The callback page names the invitation its grant was issued for, so
|
|
926
|
+
// the common case is ONE worker round-trip instead of one per live
|
|
927
|
+
// invitation. A wrong or stale hint simply fails redemption (the worker
|
|
928
|
+
// binds every grant to an invitation), so the loop stays the fallback.
|
|
929
|
+
const hinted = invitations.get(stringField(body, 'invitationId', 200));
|
|
930
|
+
const candidates =
|
|
931
|
+
hinted && hinted.expiresAt > now() ? [hinted] : [...invitations.values()];
|
|
932
|
+
let matched: { invitation: GatewayInvitation; redeemed: ShareGrantRedemption } | null =
|
|
933
|
+
null;
|
|
934
|
+
let serviceUnavailable = false;
|
|
935
|
+
for (const invitation of candidates) {
|
|
936
|
+
if (invitation.expiresAt <= now()) continue;
|
|
937
|
+
try {
|
|
938
|
+
matched = { invitation, redeemed: await invitation.redeem(grant) };
|
|
939
|
+
break;
|
|
940
|
+
} catch (error) {
|
|
941
|
+
// A grant is bound to one invitation; try the remaining live
|
|
942
|
+
// records. An account service that never answered is a different
|
|
943
|
+
// story and must not read as a rejected grant.
|
|
944
|
+
if (error instanceof ShareGrantServiceError) serviceUnavailable = true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (!matched && serviceUnavailable) {
|
|
948
|
+
audit({ type: 'auth-failed' });
|
|
949
|
+
response.writeHead(502, { 'Cache-Control': 'no-store' });
|
|
950
|
+
response.end('Volter account service unavailable.');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (!matched || usedGrantIds.has(matched.redeemed.jti)) throw new Error('Invalid grant.');
|
|
954
|
+
const { invitation, redeemed } = matched;
|
|
955
|
+
const normalizedEmail = redeemed.participant.email.trim().toLowerCase();
|
|
956
|
+
if (
|
|
957
|
+
(invitation.recipientAccountId &&
|
|
958
|
+
invitation.recipientAccountId !== redeemed.participant.id) ||
|
|
959
|
+
(invitation.recipientEmail &&
|
|
960
|
+
invitation.recipientEmail.trim().toLowerCase() !== normalizedEmail)
|
|
961
|
+
)
|
|
962
|
+
throw new Error('This invitation belongs to another account.');
|
|
963
|
+
const organization = invitation.recipientOrganizationId
|
|
964
|
+
? redeemed.participant.organizations?.find(
|
|
965
|
+
(candidate) => candidate.id === invitation.recipientOrganizationId,
|
|
966
|
+
)
|
|
967
|
+
: undefined;
|
|
968
|
+
if (invitation.recipientOrganizationId && !organization) {
|
|
969
|
+
throw new Error('This invitation belongs to another organization.');
|
|
970
|
+
}
|
|
971
|
+
if (
|
|
972
|
+
invitation.recipientOrganizationDomain &&
|
|
973
|
+
!organization?.domains.some(
|
|
974
|
+
(domain) =>
|
|
975
|
+
domain.verified &&
|
|
976
|
+
domain.name.toLowerCase() === invitation.recipientOrganizationDomain?.toLowerCase(),
|
|
977
|
+
)
|
|
978
|
+
) {
|
|
979
|
+
throw new Error('This invitation requires a Clerk-verified organization domain.');
|
|
980
|
+
}
|
|
981
|
+
const claimedPerson = personInvitationAccounts.get(invitation.id);
|
|
982
|
+
if (
|
|
983
|
+
invitation.usePolicy === 'person' &&
|
|
984
|
+
claimedPerson &&
|
|
985
|
+
claimedPerson !== redeemed.participant.id
|
|
986
|
+
)
|
|
987
|
+
throw new Error('This one-person invitation has already been claimed.');
|
|
988
|
+
const participantId = participantIdFor(redeemed.participant.id);
|
|
989
|
+
if (kickedParticipants.has(participantId)) throw new Error('This participant was removed.');
|
|
990
|
+
const invitationCredentials = [...credentials.values()].filter(
|
|
991
|
+
(share) => share.invitationId === invitation.id,
|
|
992
|
+
);
|
|
993
|
+
if (invitationCredentials.length >= 16)
|
|
994
|
+
throw new Error('Invitation credential limit reached.');
|
|
995
|
+
const accounts = new Set([...credentials.values()].map((share) => share.account.id));
|
|
996
|
+
if (!accounts.has(redeemed.participant.id) && accounts.size >= 32) {
|
|
997
|
+
throw new Error('Participant limit reached.');
|
|
998
|
+
}
|
|
999
|
+
// An account's stored assignment above THIS invitation's ceiling used
|
|
1000
|
+
// to refuse the redemption outright, so a participant demoted on an
|
|
1001
|
+
// earlier, wider invitation could not join through a narrower new one
|
|
1002
|
+
// at all — the invitation names a ceiling, and a ceiling clamps. The
|
|
1003
|
+
// assignment itself is left alone: it is the account's standing role,
|
|
1004
|
+
// and a later invitation that admits it should still restore it.
|
|
1005
|
+
const assigned = assignedRoles.get(redeemed.participant.id);
|
|
1006
|
+
const role =
|
|
1007
|
+
assigned && roleAllowsRole(invitation.role, assigned) ? assigned : invitation.role;
|
|
1008
|
+
if (assigned && assigned !== role) {
|
|
1009
|
+
audit({
|
|
1010
|
+
type: 'role-changed',
|
|
1011
|
+
invitationId: invitation.id,
|
|
1012
|
+
participantId,
|
|
1013
|
+
accountId: redeemed.participant.id,
|
|
1014
|
+
role,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
rememberGrantId(redeemed.jti);
|
|
1018
|
+
if (invitation.usePolicy === 'person' && !claimedPerson) {
|
|
1019
|
+
personInvitationAccounts.set(invitation.id, redeemed.participant.id);
|
|
1020
|
+
}
|
|
1021
|
+
const credential = randomBytes(24).toString('base64url');
|
|
1022
|
+
credentials.set(credential, {
|
|
1023
|
+
credentialId: createHash('sha256').update(credential).digest('base64url'),
|
|
1024
|
+
participantId,
|
|
1025
|
+
account: redeemed.participant,
|
|
1026
|
+
invitationId: invitation.id,
|
|
1027
|
+
role,
|
|
1028
|
+
roleCeiling: invitation.role,
|
|
1029
|
+
joined: false,
|
|
1030
|
+
});
|
|
1031
|
+
audit({
|
|
1032
|
+
type: 'invite-redeemed',
|
|
1033
|
+
invitationId: invitation.id,
|
|
1034
|
+
participantId,
|
|
1035
|
+
accountId: redeemed.participant.id,
|
|
1036
|
+
});
|
|
1037
|
+
response.setHeader(
|
|
1038
|
+
'Set-Cookie',
|
|
1039
|
+
`${COOKIE}=${credential}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${Math.max(1, Math.ceil((invitation.expiresAt - now()) / 1_000))}`,
|
|
1040
|
+
);
|
|
1041
|
+
response.writeHead(200, {
|
|
1042
|
+
'Content-Type': 'application/json',
|
|
1043
|
+
'Cache-Control': 'no-store',
|
|
1044
|
+
});
|
|
1045
|
+
response.end(JSON.stringify({ participantId, participant: redeemed.participant, role }));
|
|
1046
|
+
} catch {
|
|
1047
|
+
audit({ type: 'auth-failed' });
|
|
1048
|
+
response.writeHead(403).end('Invalid or expired Volter account authorization.');
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const share = activeShare(request);
|
|
1053
|
+
if (!share) {
|
|
1054
|
+
if (request.method === 'GET' && request.url === CALLBACK_PATH)
|
|
1055
|
+
html(response, callbackLanding());
|
|
1056
|
+
else if (request.method === 'GET' && (request.url === '/' || request.url === '/index.html'))
|
|
1057
|
+
html(response, landing(options.display));
|
|
1058
|
+
else response.writeHead(401).end('Share authorization required.');
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const participantId = participantFromEventsUrl(request.url);
|
|
1062
|
+
if (new URL(request.url ?? '/', 'http://vgai.local').pathname === '/__editor/events') {
|
|
1063
|
+
if (participantId && share.participantId !== participantId) {
|
|
1064
|
+
response.writeHead(403).end('This share login is already bound to another participant.');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
share.joined = true;
|
|
1068
|
+
}
|
|
1069
|
+
if (!shareRoleAllows(share.role, request.method, request.url)) {
|
|
1070
|
+
audit({
|
|
1071
|
+
type: 'capability-denied',
|
|
1072
|
+
invitationId: share.invitationId,
|
|
1073
|
+
participantId: share.participantId,
|
|
1074
|
+
accountId: share.account.id,
|
|
1075
|
+
capability: requestedShareCapability(request.method, request.url),
|
|
1076
|
+
});
|
|
1077
|
+
response
|
|
1078
|
+
.writeHead(403)
|
|
1079
|
+
.end(`The ${share.role} share role cannot access this editor capability.`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (
|
|
1083
|
+
isRepositoryPresenceRead(request.method, request.url) &&
|
|
1084
|
+
!rateAllowed(`repository-presence:${share.credentialId}`, 60, 60_000)
|
|
1085
|
+
) {
|
|
1086
|
+
response.writeHead(429, { 'Retry-After': '60' }).end('Too many repository presence reads.');
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (isEditorMutation(request.method, request.url) && !sameOrigin(request)) {
|
|
1090
|
+
response.writeHead(403).end('The share request origin does not match this tunnel.');
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (
|
|
1094
|
+
isEditorMutation(request.method, request.url) &&
|
|
1095
|
+
!rateAllowed(`mutation:${share.credentialId}`, 240, 60_000)
|
|
1096
|
+
) {
|
|
1097
|
+
response.writeHead(429).end('Too many shared editor mutations.');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (!share.joined && isEditorMutation(request.method, request.url)) {
|
|
1101
|
+
response.writeHead(409).end('Open the shared editor before mutating its session.');
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (requestedShareCapability(request.method, request.url) === 'terminal') {
|
|
1105
|
+
audit({
|
|
1106
|
+
type: 'terminal-invoked',
|
|
1107
|
+
invitationId: share.invitationId,
|
|
1108
|
+
participantId: share.participantId,
|
|
1109
|
+
accountId: share.account.id,
|
|
1110
|
+
capability: 'terminal',
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const viteClient = isViteClientRequest(request.method, request.url);
|
|
1114
|
+
const headers = upstreamHeaders(
|
|
1115
|
+
request.headers,
|
|
1116
|
+
options.targetPort,
|
|
1117
|
+
share,
|
|
1118
|
+
request.method ?? 'GET',
|
|
1119
|
+
request.url ?? '/',
|
|
1120
|
+
options.claimSecret,
|
|
1121
|
+
);
|
|
1122
|
+
if (viteClient) delete headers['accept-encoding'];
|
|
1123
|
+
const upstream = httpRequest(
|
|
1124
|
+
{
|
|
1125
|
+
hostname: '127.0.0.1',
|
|
1126
|
+
port: options.targetPort,
|
|
1127
|
+
path: request.url,
|
|
1128
|
+
method: request.method,
|
|
1129
|
+
headers,
|
|
1130
|
+
},
|
|
1131
|
+
(upstreamResponse) => {
|
|
1132
|
+
if (!viteClient || upstreamResponse.statusCode !== 200) {
|
|
1133
|
+
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
1134
|
+
upstreamResponse.pipe(response);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const chunks: Buffer[] = [];
|
|
1138
|
+
upstreamResponse.on('data', (chunk: Buffer | string) => chunks.push(Buffer.from(chunk)));
|
|
1139
|
+
upstreamResponse.once('end', () => {
|
|
1140
|
+
try {
|
|
1141
|
+
const body = Buffer.from(publicViteClient(Buffer.concat(chunks).toString('utf8')));
|
|
1142
|
+
const responseHeaders = { ...upstreamResponse.headers };
|
|
1143
|
+
delete responseHeaders['content-length'];
|
|
1144
|
+
delete responseHeaders['content-encoding'];
|
|
1145
|
+
delete responseHeaders['etag'];
|
|
1146
|
+
responseHeaders['content-length'] = String(body.length);
|
|
1147
|
+
response.writeHead(200, responseHeaders).end(body);
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
response
|
|
1150
|
+
.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1151
|
+
.end(error instanceof Error ? error.message : 'Vite HMR relay failed.');
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
},
|
|
1155
|
+
);
|
|
1156
|
+
upstream.on('error', () => response.writeHead(502).end('Editor session is unavailable.'));
|
|
1157
|
+
if (decodedSharePath(request.url) === '/__editor/events') {
|
|
1158
|
+
const release = trackConnection(share.credentialId, () => {
|
|
1159
|
+
upstream.destroy();
|
|
1160
|
+
response.destroy();
|
|
1161
|
+
});
|
|
1162
|
+
response.once('close', release);
|
|
1163
|
+
}
|
|
1164
|
+
request.pipe(upstream);
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
server.on('upgrade', (request, socket, head) => {
|
|
1168
|
+
const share = activeShare(request);
|
|
1169
|
+
if (
|
|
1170
|
+
!share ||
|
|
1171
|
+
!shareRoleAllows(share.role, request.method, request.url) ||
|
|
1172
|
+
!sameOrigin(request)
|
|
1173
|
+
) {
|
|
1174
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const eventUrl = new URL(request.url ?? '/', 'http://vgai.local');
|
|
1178
|
+
if (eventUrl.pathname === '/__editor/events') {
|
|
1179
|
+
const participantId = participantFromEventsUrl(request.url);
|
|
1180
|
+
if (participantId && share.participantId !== participantId) {
|
|
1181
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
share.joined = true;
|
|
1185
|
+
eventWebSockets.handleUpgrade(request, socket, head, (client) => {
|
|
1186
|
+
const release = trackConnection(share.credentialId, () => client.terminate());
|
|
1187
|
+
client.once('close', release);
|
|
1188
|
+
const headers = upstreamHeaders(
|
|
1189
|
+
request.headers,
|
|
1190
|
+
options.targetPort,
|
|
1191
|
+
share,
|
|
1192
|
+
'GET',
|
|
1193
|
+
request.url ?? '/',
|
|
1194
|
+
options.claimSecret,
|
|
1195
|
+
);
|
|
1196
|
+
for (const header of Object.keys(headers)) {
|
|
1197
|
+
if (
|
|
1198
|
+
header.toLowerCase() === 'connection' ||
|
|
1199
|
+
header.toLowerCase() === 'upgrade' ||
|
|
1200
|
+
header.toLowerCase().startsWith('sec-websocket-')
|
|
1201
|
+
) {
|
|
1202
|
+
delete headers[header];
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
headers['accept'] = 'text/event-stream';
|
|
1206
|
+
const lastEventId = eventUrl.searchParams.get('lastEventId');
|
|
1207
|
+
if (lastEventId) headers['last-event-id'] = lastEventId;
|
|
1208
|
+
forwardEventStreamToWebSocket(
|
|
1209
|
+
client,
|
|
1210
|
+
httpRequest({
|
|
1211
|
+
hostname: '127.0.0.1',
|
|
1212
|
+
port: options.targetPort,
|
|
1213
|
+
path: request.url,
|
|
1214
|
+
method: 'GET',
|
|
1215
|
+
headers,
|
|
1216
|
+
}),
|
|
1217
|
+
);
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const targetPort = isViteHmrUpgrade(request) ? hmrPort : options.targetPort;
|
|
1222
|
+
const upstream = httpRequest({
|
|
1223
|
+
hostname: '127.0.0.1',
|
|
1224
|
+
port: targetPort,
|
|
1225
|
+
path: request.url,
|
|
1226
|
+
method: request.method,
|
|
1227
|
+
headers: upstreamHeaders(
|
|
1228
|
+
request.headers,
|
|
1229
|
+
targetPort,
|
|
1230
|
+
share,
|
|
1231
|
+
request.method ?? 'GET',
|
|
1232
|
+
request.url ?? '/',
|
|
1233
|
+
options.claimSecret,
|
|
1234
|
+
),
|
|
1235
|
+
});
|
|
1236
|
+
upstream.on('upgrade', (upstreamResponse, upstreamSocket, upstreamHead) => {
|
|
1237
|
+
const release = trackConnection(share.credentialId, () => {
|
|
1238
|
+
upstreamSocket.destroy();
|
|
1239
|
+
socket.destroy();
|
|
1240
|
+
});
|
|
1241
|
+
socket.once('close', release);
|
|
1242
|
+
socket.write(
|
|
1243
|
+
`HTTP/1.1 ${upstreamResponse.statusCode ?? 101} ${upstreamResponse.statusMessage ?? 'Switching Protocols'}\r\n${Object.entries(
|
|
1244
|
+
upstreamResponse.headers,
|
|
1245
|
+
)
|
|
1246
|
+
.flatMap(([key, value]) =>
|
|
1247
|
+
Array.isArray(value)
|
|
1248
|
+
? value.map((entry) => `${key}: ${entry}\r\n`)
|
|
1249
|
+
: `${key}: ${value}\r\n`,
|
|
1250
|
+
)
|
|
1251
|
+
.join('')}\r\n`,
|
|
1252
|
+
);
|
|
1253
|
+
if (upstreamHead.length) socket.write(upstreamHead);
|
|
1254
|
+
if (head.length) upstreamSocket.write(head);
|
|
1255
|
+
upstreamSocket.pipe(socket).pipe(upstreamSocket);
|
|
1256
|
+
});
|
|
1257
|
+
upstream.on('error', () => socket.destroy());
|
|
1258
|
+
upstream.end();
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
await new Promise<void>((resolveListen, rejectListen) => {
|
|
1262
|
+
server.once('error', rejectListen);
|
|
1263
|
+
server.listen(0, '127.0.0.1', () => {
|
|
1264
|
+
server.off('error', rejectListen);
|
|
1265
|
+
resolveListen();
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
const address = server.address();
|
|
1269
|
+
if (!address || typeof address === 'string') throw new Error('Share gateway did not bind.');
|
|
1270
|
+
|
|
1271
|
+
return {
|
|
1272
|
+
port: address.port,
|
|
1273
|
+
addInvitation(invitation) {
|
|
1274
|
+
if (invitations.has(invitation.id)) throw new Error(`Duplicate invitation: ${invitation.id}`);
|
|
1275
|
+
invitations.set(invitation.id, invitation);
|
|
1276
|
+
},
|
|
1277
|
+
revokeInvitation(invitationId) {
|
|
1278
|
+
invitations.delete(invitationId);
|
|
1279
|
+
personInvitationAccounts.delete(invitationId);
|
|
1280
|
+
const severed: string[] = [];
|
|
1281
|
+
for (const [credential, share] of credentials) {
|
|
1282
|
+
if (share.invitationId !== invitationId) continue;
|
|
1283
|
+
credentials.delete(credential);
|
|
1284
|
+
severed.push(share.credentialId);
|
|
1285
|
+
}
|
|
1286
|
+
severConnections(severed);
|
|
1287
|
+
audit({ type: 'invitation-revoked', invitationId });
|
|
1288
|
+
return severed.length;
|
|
1289
|
+
},
|
|
1290
|
+
kickParticipant(participantId) {
|
|
1291
|
+
kickedParticipants.add(participantId);
|
|
1292
|
+
const severed: string[] = [];
|
|
1293
|
+
for (const [credential, share] of credentials) {
|
|
1294
|
+
if (share.participantId !== participantId) continue;
|
|
1295
|
+
credentials.delete(credential);
|
|
1296
|
+
severed.push(share.credentialId);
|
|
1297
|
+
}
|
|
1298
|
+
severConnections(severed);
|
|
1299
|
+
audit({ type: 'participant-kicked', participantId });
|
|
1300
|
+
return severed.length;
|
|
1301
|
+
},
|
|
1302
|
+
setParticipantRole(participantId, role) {
|
|
1303
|
+
const matches = [...credentials.values()].filter(
|
|
1304
|
+
(share) => share.participantId === participantId,
|
|
1305
|
+
);
|
|
1306
|
+
if (matches.length === 0) throw new Error(`Unknown shared participant: ${participantId}`);
|
|
1307
|
+
if (matches.some((share) => !roleAllowsRole(share.roleCeiling, role))) {
|
|
1308
|
+
throw new Error('The requested role exceeds this participant invitation ceiling.');
|
|
1309
|
+
}
|
|
1310
|
+
assignedRoles.set(matches[0]!.account.id, role);
|
|
1311
|
+
for (const share of matches) share.role = role;
|
|
1312
|
+
audit({
|
|
1313
|
+
type: 'role-changed',
|
|
1314
|
+
participantId,
|
|
1315
|
+
accountId: matches[0]!.account.id,
|
|
1316
|
+
role,
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
hasParticipant(participantId) {
|
|
1320
|
+
for (const share of credentials.values()) {
|
|
1321
|
+
if (share.participantId === participantId) return true;
|
|
1322
|
+
}
|
|
1323
|
+
return false;
|
|
1324
|
+
},
|
|
1325
|
+
participants() {
|
|
1326
|
+
const byParticipant = new Map<string, GatewayParticipant>();
|
|
1327
|
+
for (const share of credentials.values()) {
|
|
1328
|
+
const prior = byParticipant.get(share.participantId);
|
|
1329
|
+
if (prior) {
|
|
1330
|
+
prior.credentials += 1;
|
|
1331
|
+
prior.joined ||= share.joined;
|
|
1332
|
+
} else {
|
|
1333
|
+
byParticipant.set(share.participantId, {
|
|
1334
|
+
participantId: share.participantId,
|
|
1335
|
+
account: { ...share.account },
|
|
1336
|
+
invitationId: share.invitationId,
|
|
1337
|
+
role: share.role,
|
|
1338
|
+
roleCeiling: share.roleCeiling,
|
|
1339
|
+
credentials: 1,
|
|
1340
|
+
joined: share.joined,
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return [...byParticipant.values()];
|
|
1345
|
+
},
|
|
1346
|
+
audit() {
|
|
1347
|
+
return auditEvents.map((event) => ({ ...event }));
|
|
1348
|
+
},
|
|
1349
|
+
close: () =>
|
|
1350
|
+
new Promise<void>((resolveClose) => {
|
|
1351
|
+
credentials.clear();
|
|
1352
|
+
invitations.clear();
|
|
1353
|
+
personInvitationAccounts.clear();
|
|
1354
|
+
rateWindows.clear();
|
|
1355
|
+
usedGrantIds.clear();
|
|
1356
|
+
severConnections([...liveConnections.keys()]);
|
|
1357
|
+
for (const client of eventWebSockets.clients) client.terminate();
|
|
1358
|
+
server.closeAllConnections();
|
|
1359
|
+
server.close(() => resolveClose());
|
|
1360
|
+
}),
|
|
1361
|
+
};
|
|
1362
|
+
}
|