@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,1434 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import {
|
|
6
|
+
type AccountCatalog,
|
|
7
|
+
AccountCatalogSchema,
|
|
8
|
+
type AccountOrganization,
|
|
9
|
+
type AccountSnapshot,
|
|
10
|
+
AccountSnapshotSchema,
|
|
11
|
+
type AccountSpendPolicy,
|
|
12
|
+
AccountSpendPolicySchema,
|
|
13
|
+
type AccountUsageEntry,
|
|
14
|
+
AccountUsageEntrySchema,
|
|
15
|
+
type CodingInferenceSettings,
|
|
16
|
+
CodingInferenceSettingsSchema,
|
|
17
|
+
type EditorAccountSnapshot,
|
|
18
|
+
type GenerationExecutionRoute,
|
|
19
|
+
type ProviderCredentialId,
|
|
20
|
+
type ProviderCredentialTestResult,
|
|
21
|
+
} from '@volter/editor-sdk/account';
|
|
22
|
+
import type { ProviderExecutionMode } from '@volter/editor-sdk/tools/provider-execution';
|
|
23
|
+
import {
|
|
24
|
+
type AccountCredential,
|
|
25
|
+
type AccountCredentialStore,
|
|
26
|
+
createSystemAccountCredentialStore,
|
|
27
|
+
} from './account-credentials';
|
|
28
|
+
import {
|
|
29
|
+
createMockGenerativeControlPlane,
|
|
30
|
+
type MockGenerativeControlPlane,
|
|
31
|
+
type MockGenerativeControlPlaneState,
|
|
32
|
+
} from './mock-control-plane';
|
|
33
|
+
import {
|
|
34
|
+
createSystemProviderCredentialStore,
|
|
35
|
+
type ProviderCredentialMarker,
|
|
36
|
+
type ProviderCredentialMarkerStore,
|
|
37
|
+
ProviderCredentialService,
|
|
38
|
+
type ProviderCredentialStore,
|
|
39
|
+
} from './provider-credentials';
|
|
40
|
+
import { mintTwinSessionToken, signInToTwin } from './twin-auth';
|
|
41
|
+
|
|
42
|
+
interface PersistedAccount {
|
|
43
|
+
version: 1;
|
|
44
|
+
backend: 'mock' | 'live';
|
|
45
|
+
userId?: string;
|
|
46
|
+
email?: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Managed-twin identity (the placeholder-IdP path of the `live` backend, active when
|
|
50
|
+
* `VGAI_TWIN_URL` is set). The durable handle is the SESSION id, not a token: session
|
|
51
|
+
* tokens are ≈60s-lived and re-minted on demand. See `twin-auth.ts`.
|
|
52
|
+
*/
|
|
53
|
+
twinUserId?: string;
|
|
54
|
+
twinSessionId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Managed service endpoints captured at sign-in. Persisted so a STANDALONE `vgai deploy`
|
|
57
|
+
* (a separate process from the editor, with none of its env) can mint a fresh relay token
|
|
58
|
+
* from the stored session and reach the relay-token endpoint — see
|
|
59
|
+
* `packages/vgai-cli/src/managed-deploy-auth.ts`, the reader of these fields.
|
|
60
|
+
*/
|
|
61
|
+
twinUrl?: string;
|
|
62
|
+
accountUrl?: string;
|
|
63
|
+
oauthIssuerUrl?: string;
|
|
64
|
+
authUrl?: string;
|
|
65
|
+
/**
|
|
66
|
+
* NON-SECRET record of which provider keys are connected, and the mask the
|
|
67
|
+
* account panel renders for each. It exists so "is Fal connected?" is
|
|
68
|
+
* answerable without a credential-manager read — see
|
|
69
|
+
* `provider-credentials.ts`'s header for why that read may never happen on
|
|
70
|
+
* a boot or poll path.
|
|
71
|
+
*/
|
|
72
|
+
providerCredentials?: Partial<Record<ProviderCredentialId, ProviderCredentialMarker>>;
|
|
73
|
+
mockControlState?: MockGenerativeControlPlaneState;
|
|
74
|
+
/** v1 migration only; moved into the OS credential store on first read. */
|
|
75
|
+
accessToken?: string;
|
|
76
|
+
planId?: 'free' | 'creator' | 'max' | 'ultra';
|
|
77
|
+
planStatus?: 'active' | 'cancelling';
|
|
78
|
+
planRenewsAt?: string;
|
|
79
|
+
purchasedCredits?: number;
|
|
80
|
+
spendPolicy?: AccountSpendPolicy;
|
|
81
|
+
preferredRoute?: GenerationExecutionRoute;
|
|
82
|
+
codingInference?: CodingInferenceSettings;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResolvedCodingInference {
|
|
86
|
+
provider: 'openrouter';
|
|
87
|
+
route: 'managed' | 'byok';
|
|
88
|
+
/** The person's own stored account-panel choice, carried VERBATIM (never narrowed) so
|
|
89
|
+
* the launch seam can gate on it. A coding harness runs on the login the person
|
|
90
|
+
* already did unless this says `'managed'` or `'byok'`; `'auto'` is not a choice. */
|
|
91
|
+
preferredRoute: GenerationExecutionRoute;
|
|
92
|
+
model: string;
|
|
93
|
+
/** OpenAI-compatible API root, including `/v1`. */
|
|
94
|
+
baseUrl: string;
|
|
95
|
+
/** Trusted process-launch credential. Never serialize this into the editor. */
|
|
96
|
+
apiKey: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CodingInferenceQuote {
|
|
100
|
+
model: string;
|
|
101
|
+
promptTokens: number;
|
|
102
|
+
outputTokens: number;
|
|
103
|
+
providerAmountUsd: number;
|
|
104
|
+
estimatedCredits: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const DEFAULT_CODING_INFERENCE: CodingInferenceSettings = {
|
|
108
|
+
enabled: false,
|
|
109
|
+
provider: 'openrouter',
|
|
110
|
+
model: 'z-ai/glm-5.2',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
interface PendingBrowserAuthorization {
|
|
114
|
+
verifier: string;
|
|
115
|
+
redirectUri: string;
|
|
116
|
+
expiresAt: string;
|
|
117
|
+
status: 'pending' | 'complete' | 'failed';
|
|
118
|
+
error?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface OAuthServerMetadata {
|
|
122
|
+
authorizationEndpoint: string;
|
|
123
|
+
tokenEndpoint: string;
|
|
124
|
+
deviceAuthorizationEndpoint?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CollaborationAccountSession {
|
|
128
|
+
accessToken: string;
|
|
129
|
+
authUrl: string;
|
|
130
|
+
user: { id: string; email: string; name?: string; organizations?: AccountOrganization[] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function base64Url(value: Buffer): string {
|
|
134
|
+
return value.toString('base64url');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const ACCOUNT_PATH = process.env['VGAI_ACCOUNT_PATH']
|
|
138
|
+
? resolve(process.env['VGAI_ACCOUNT_PATH'])
|
|
139
|
+
: join(homedir(), '.vgai', 'account.json');
|
|
140
|
+
async function readPersisted(path: string): Promise<PersistedAccount> {
|
|
141
|
+
try {
|
|
142
|
+
const value = JSON.parse(await readFile(path, 'utf8')) as Partial<PersistedAccount> & {
|
|
143
|
+
mode?: unknown;
|
|
144
|
+
};
|
|
145
|
+
// Legacy migration: v1 files written before the backend rename used `mode`.
|
|
146
|
+
let migrated = false;
|
|
147
|
+
if (
|
|
148
|
+
value.version === 1 &&
|
|
149
|
+
(value as { mode?: unknown }).mode !== undefined &&
|
|
150
|
+
value.backend === undefined
|
|
151
|
+
) {
|
|
152
|
+
const legacyMode = (value as { mode?: unknown }).mode;
|
|
153
|
+
if (legacyMode === 'mock' || legacyMode === 'live') {
|
|
154
|
+
value.backend = legacyMode;
|
|
155
|
+
delete (value as { mode?: unknown }).mode;
|
|
156
|
+
migrated = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (value.version !== 1 || (value.backend !== 'mock' && value.backend !== 'live')) {
|
|
160
|
+
throw new Error('Unsupported account state.');
|
|
161
|
+
}
|
|
162
|
+
if ((value as { preferredRoute?: unknown }).preferredRoute === 'direct') {
|
|
163
|
+
value.preferredRoute = 'byok';
|
|
164
|
+
migrated = true;
|
|
165
|
+
}
|
|
166
|
+
// The shipped editor now has a managed account provider by default. A
|
|
167
|
+
// persisted local-mock selection from an older build must not silently
|
|
168
|
+
// keep production collaboration disabled forever. The explicit Clerk twin
|
|
169
|
+
// remains a development backend and therefore does not trigger this move.
|
|
170
|
+
if (
|
|
171
|
+
value.backend === 'mock' &&
|
|
172
|
+
process.env['VGAI_ACCOUNT_URL'] &&
|
|
173
|
+
!process.env['VGAI_TWIN_URL']
|
|
174
|
+
) {
|
|
175
|
+
value.backend = 'live';
|
|
176
|
+
delete value.userId;
|
|
177
|
+
delete value.email;
|
|
178
|
+
delete value.name;
|
|
179
|
+
delete value.planId;
|
|
180
|
+
delete value.planStatus;
|
|
181
|
+
delete value.planRenewsAt;
|
|
182
|
+
delete value.purchasedCredits;
|
|
183
|
+
delete value.spendPolicy;
|
|
184
|
+
migrated = true;
|
|
185
|
+
}
|
|
186
|
+
if (migrated) await writePersisted(path, value as PersistedAccount);
|
|
187
|
+
return value as PersistedAccount;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
190
|
+
return {
|
|
191
|
+
version: 1,
|
|
192
|
+
backend: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'live',
|
|
193
|
+
preferredRoute: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'auto',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function writePersisted(path: string, value: PersistedAccount): Promise<void> {
|
|
201
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
202
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
203
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
204
|
+
try {
|
|
205
|
+
await rename(temporary, path);
|
|
206
|
+
await chmod(path, 0o600);
|
|
207
|
+
} finally {
|
|
208
|
+
await rm(temporary, { force: true });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class EditorAccountService {
|
|
213
|
+
private statePromise: Promise<PersistedAccount>;
|
|
214
|
+
private credential: AccountCredential | null = null;
|
|
215
|
+
private readonly environmentAccessToken: string | undefined;
|
|
216
|
+
private readonly cloudInferenceGrant: string | undefined;
|
|
217
|
+
private readonly cloudInferenceModel: string | undefined;
|
|
218
|
+
private readonly cloudInferenceRoute: 'managed' | 'byok' | undefined;
|
|
219
|
+
private readonly cloudAccount: AccountSnapshot | undefined;
|
|
220
|
+
private readonly credentialStore: AccountCredentialStore;
|
|
221
|
+
private readonly providerCredentials: ProviderCredentialService;
|
|
222
|
+
private mockControl: MockGenerativeControlPlane | undefined;
|
|
223
|
+
private mockControlOwner: PersistedAccount | undefined;
|
|
224
|
+
private mockToken: string | undefined;
|
|
225
|
+
/** In-memory cache of the current twin session token; re-minted before it expires. */
|
|
226
|
+
private twinToken: { jwt: string; expiresAtMs: number } | undefined;
|
|
227
|
+
private readonly browserAuthorizations = new Map<string, PendingBrowserAuthorization>();
|
|
228
|
+
private oauthMetadataPromise: Promise<OAuthServerMetadata> | undefined;
|
|
229
|
+
|
|
230
|
+
constructor(
|
|
231
|
+
private readonly accountPath = ACCOUNT_PATH,
|
|
232
|
+
credentialStore?: AccountCredentialStore,
|
|
233
|
+
providerCredentialStore?: ProviderCredentialStore,
|
|
234
|
+
) {
|
|
235
|
+
this.statePromise = readPersisted(accountPath);
|
|
236
|
+
this.environmentAccessToken = process.env['VGAI_ACCESS_TOKEN'];
|
|
237
|
+
this.cloudInferenceGrant = process.env['VGAI_CLOUD_INFERENCE_GRANT'];
|
|
238
|
+
this.cloudInferenceModel = process.env['VGAI_CLOUD_INFERENCE_MODEL'];
|
|
239
|
+
this.cloudInferenceRoute =
|
|
240
|
+
process.env['VGAI_CLOUD_INFERENCE_ROUTE'] === 'byok' ? 'byok' : 'managed';
|
|
241
|
+
this.cloudAccount = parseCloudAccount(process.env['VGAI_CLOUD_ACCOUNT_JSON']);
|
|
242
|
+
delete process.env['VGAI_CLOUD_INFERENCE_GRANT'];
|
|
243
|
+
delete process.env['VGAI_CLOUD_ACCOUNT_JSON'];
|
|
244
|
+
this.credentialStore =
|
|
245
|
+
credentialStore ?? createSystemAccountCredentialStore(process.env['VGAI_ACCOUNT_URL']);
|
|
246
|
+
this.providerCredentials = new ProviderCredentialService(
|
|
247
|
+
providerCredentialStore ?? createSystemProviderCredentialStore(),
|
|
248
|
+
this.providerCredentialMarkers(),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Makes ONE provider's stored BYOK credential available to its native tool,
|
|
254
|
+
* reading the credential manager at that moment and no earlier. Callers are
|
|
255
|
+
* the job paths that know which provider they are about to run.
|
|
256
|
+
*/
|
|
257
|
+
async ensureProviderCredential(provider: string): Promise<void> {
|
|
258
|
+
await this.providerCredentials.ensure(provider);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The marker record lives in this service's own account file. */
|
|
262
|
+
private providerCredentialMarkers(): ProviderCredentialMarkerStore {
|
|
263
|
+
return {
|
|
264
|
+
read: async () => (await this.state()).providerCredentials ?? {},
|
|
265
|
+
write: async (provider, marker) => {
|
|
266
|
+
const state = await this.state();
|
|
267
|
+
const markers = { ...(state.providerCredentials ?? {}) };
|
|
268
|
+
if (marker) markers[provider] = marker;
|
|
269
|
+
else delete markers[provider];
|
|
270
|
+
if (Object.keys(markers).length > 0) state.providerCredentials = markers;
|
|
271
|
+
else delete state.providerCredentials;
|
|
272
|
+
await writePersisted(this.accountPath, state);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Resolve credentials and billing inside the trusted account boundary. */
|
|
278
|
+
async providerExecutionMode(
|
|
279
|
+
provider: string,
|
|
280
|
+
pinned?: ProviderExecutionMode,
|
|
281
|
+
): Promise<ProviderExecutionMode> {
|
|
282
|
+
// The job path's own read: this provider's secret, now, and no other's.
|
|
283
|
+
const configured = (await this.providerCredentials.ensure(provider)) !== undefined;
|
|
284
|
+
const state = await this.state();
|
|
285
|
+
const mock = state.backend === 'mock' && process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1';
|
|
286
|
+
if (pinned === 'mock' || (!pinned && mock && state.preferredRoute === 'mock')) {
|
|
287
|
+
if (!mock) throw new Error('This recorded mock job requires its development account.');
|
|
288
|
+
return 'mock';
|
|
289
|
+
}
|
|
290
|
+
if (pinned === 'direct' || (!pinned && configured && state.preferredRoute !== 'managed')) {
|
|
291
|
+
if (!configured) throw new Error(`Reconnect ${provider} in Account to continue this job.`);
|
|
292
|
+
return 'direct';
|
|
293
|
+
}
|
|
294
|
+
if (!pinned && state.preferredRoute === 'byok')
|
|
295
|
+
throw new Error(`Connect ${provider} in Account before generating with your own key.`);
|
|
296
|
+
const token = await this.accessToken();
|
|
297
|
+
if (process.env['VGAI_GENERATION_GATEWAY'] && token) return 'managed';
|
|
298
|
+
|
|
299
|
+
throw new Error(`Connect ${provider} or sign in to Volter Editor in Account before generating.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async state(): Promise<PersistedAccount> {
|
|
303
|
+
const state = await this.statePromise;
|
|
304
|
+
if (state.accessToken) {
|
|
305
|
+
await this.credentialStore.write({ accessToken: state.accessToken });
|
|
306
|
+
this.credential = { accessToken: state.accessToken };
|
|
307
|
+
delete state.accessToken;
|
|
308
|
+
await writePersisted(this.accountPath, state);
|
|
309
|
+
}
|
|
310
|
+
return state;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The managed twin base URL, or undefined when the twin path is not configured. When
|
|
315
|
+
* set, the `live` backend is twin-backed (identity is minted against the twin's Backend
|
|
316
|
+
* API) instead of the OAuth authorization-code flow. `VGAI_TWIN_SECRET` is optional and
|
|
317
|
+
* cosmetic — the twin accepts any Bearer.
|
|
318
|
+
*/
|
|
319
|
+
private twinBaseUrl(): string | undefined {
|
|
320
|
+
const url = process.env['VGAI_TWIN_URL'];
|
|
321
|
+
if (!url || url.trim().length === 0) return undefined;
|
|
322
|
+
const parsed = new URL(url);
|
|
323
|
+
if (
|
|
324
|
+
parsed.protocol !== 'http:' ||
|
|
325
|
+
!['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)
|
|
326
|
+
) {
|
|
327
|
+
throw new Error('VGAI_TWIN_URL must be an HTTP loopback Clerk Twin URL.');
|
|
328
|
+
}
|
|
329
|
+
return parsed.toString().replace(/\/$/, '');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private twinSecret(): string | undefined {
|
|
333
|
+
return process.env['VGAI_TWIN_SECRET'];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A fresh twin session token, minting on demand and caching it until it nears its ≈60s
|
|
338
|
+
* expiry. The token is never persisted — the durable handle is `state.twinSessionId`.
|
|
339
|
+
* Also mirrored onto `VGAI_ACCESS_TOKEN` so provider-native managed transports and a
|
|
340
|
+
* spawned deploy read the same conventional credential (same seam as the OAuth path).
|
|
341
|
+
*/
|
|
342
|
+
private async twinAccessToken(state: PersistedAccount): Promise<string | undefined> {
|
|
343
|
+
const baseUrl = this.twinBaseUrl();
|
|
344
|
+
if (!baseUrl || !state.twinSessionId) return undefined;
|
|
345
|
+
// Re-mint with a 10s safety margin against the ≈60s TTL.
|
|
346
|
+
if (!this.twinToken || this.twinToken.expiresAtMs <= Date.now() + 10_000) {
|
|
347
|
+
const jwt = await mintTwinSessionToken(baseUrl, state.twinSessionId, this.twinSecret());
|
|
348
|
+
this.twinToken = { jwt, expiresAtMs: Date.now() + 50_000 };
|
|
349
|
+
}
|
|
350
|
+
process.env['VGAI_ACCESS_TOKEN'] = this.twinToken.jwt;
|
|
351
|
+
return this.twinToken.jwt;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Sign in against the managed twin: get-or-create the user, open a durable session, and
|
|
356
|
+
* mint the first token. Identity flows from here to managed generation (`/verify`)
|
|
357
|
+
* via the shared `VGAI_ACCESS_TOKEN`.
|
|
358
|
+
*/
|
|
359
|
+
async signInTwin(email: string): Promise<EditorAccountSnapshot> {
|
|
360
|
+
const normalized = email.trim().toLowerCase();
|
|
361
|
+
if (!normalized.includes('@')) throw new Error('Enter a valid email address.');
|
|
362
|
+
const baseUrl = this.twinBaseUrl();
|
|
363
|
+
if (!baseUrl) throw new Error('No managed twin is configured (VGAI_TWIN_URL is unset).');
|
|
364
|
+
const identity = await signInToTwin(baseUrl, normalized, this.twinSecret());
|
|
365
|
+
// Capture the managed endpoints so a standalone `vgai deploy` can mint from the session
|
|
366
|
+
// (VGAI_AUTH_URL is the vgai-auth base; its `/relay/token` is the deploy's mint endpoint).
|
|
367
|
+
const authUrl = process.env['VGAI_AUTH_URL'];
|
|
368
|
+
const state: PersistedAccount = {
|
|
369
|
+
version: 1,
|
|
370
|
+
backend: 'live',
|
|
371
|
+
email: normalized,
|
|
372
|
+
twinUserId: identity.userId,
|
|
373
|
+
userId: identity.userId,
|
|
374
|
+
twinSessionId: identity.sessionId,
|
|
375
|
+
twinUrl: baseUrl,
|
|
376
|
+
...(authUrl && authUrl.trim().length > 0 ? { authUrl } : {}),
|
|
377
|
+
preferredRoute: 'managed',
|
|
378
|
+
};
|
|
379
|
+
await writePersisted(this.accountPath, state);
|
|
380
|
+
this.statePromise = Promise.resolve(state);
|
|
381
|
+
this.twinToken = undefined;
|
|
382
|
+
await this.credentialStore.delete();
|
|
383
|
+
this.credential = null;
|
|
384
|
+
await this.twinAccessToken(state);
|
|
385
|
+
return this.snapshot();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async accessToken(): Promise<string | undefined> {
|
|
389
|
+
if (this.environmentAccessToken) return this.environmentAccessToken;
|
|
390
|
+
const state = await this.statePromise;
|
|
391
|
+
if (this.twinBaseUrl() && state.twinSessionId) return this.twinAccessToken(state);
|
|
392
|
+
this.credential ??= await this.credentialStore.read();
|
|
393
|
+
if (!this.credential) return undefined;
|
|
394
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
395
|
+
const expiresSoon =
|
|
396
|
+
this.credential.expiresAt !== undefined &&
|
|
397
|
+
Date.parse(this.credential.expiresAt) <= Date.now() + 60_000;
|
|
398
|
+
if (expiresSoon && this.credential.refreshToken && accountUrl) {
|
|
399
|
+
this.credential = await this.refreshCredential(this.credential);
|
|
400
|
+
if (!this.credential) return undefined;
|
|
401
|
+
}
|
|
402
|
+
// Provider-native managed transports read this conventional process-local
|
|
403
|
+
// credential. Project tools already run as trusted Node code; the security
|
|
404
|
+
// boundary here is durable storage and browser/contribution exposure.
|
|
405
|
+
process.env['VGAI_ACCESS_TOKEN'] = this.credential.accessToken;
|
|
406
|
+
return this.credential.accessToken;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async refreshCredential(
|
|
410
|
+
credential: AccountCredential,
|
|
411
|
+
): Promise<AccountCredential | null> {
|
|
412
|
+
if (!credential.refreshToken) return credential;
|
|
413
|
+
const metadata = await this.oauthMetadata();
|
|
414
|
+
const response = await fetch(metadata.tokenEndpoint, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
417
|
+
body: new URLSearchParams({
|
|
418
|
+
grant_type: 'refresh_token',
|
|
419
|
+
refresh_token: credential.refreshToken,
|
|
420
|
+
client_id: this.oauthClientId(),
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
if (
|
|
426
|
+
response.status === 401 ||
|
|
427
|
+
body['error'] === 'invalid_grant' ||
|
|
428
|
+
body['error'] === 'invalid_token'
|
|
429
|
+
) {
|
|
430
|
+
await this.credentialStore.delete();
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`Account refresh failed (${response.status}).`);
|
|
434
|
+
}
|
|
435
|
+
if (typeof body['access_token'] !== 'string') {
|
|
436
|
+
throw new Error('Account refresh returned no access token.');
|
|
437
|
+
}
|
|
438
|
+
const refreshed: AccountCredential = {
|
|
439
|
+
accessToken: body['access_token'],
|
|
440
|
+
refreshToken:
|
|
441
|
+
typeof body['refresh_token'] === 'string' ? body['refresh_token'] : credential.refreshToken,
|
|
442
|
+
...(typeof body['expires_in'] === 'number'
|
|
443
|
+
? { expiresAt: new Date(Date.now() + body['expires_in'] * 1_000).toISOString() }
|
|
444
|
+
: {}),
|
|
445
|
+
};
|
|
446
|
+
await this.credentialStore.write(refreshed);
|
|
447
|
+
return refreshed;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private oauthClientId(): string {
|
|
451
|
+
return process.env['VGAI_OAUTH_CLIENT_ID'] ?? 'vgai-editor';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private oauthMetadata(): Promise<OAuthServerMetadata> {
|
|
455
|
+
this.oauthMetadataPromise ??= (async () => {
|
|
456
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
457
|
+
const issuerUrl = process.env['VGAI_OAUTH_ISSUER_URL'] ?? accountUrl;
|
|
458
|
+
if (!issuerUrl) throw new Error('No production Volter account service is configured.');
|
|
459
|
+
const response = await fetch(
|
|
460
|
+
`${issuerUrl.replace(/\/$/, '')}/.well-known/oauth-authorization-server`,
|
|
461
|
+
);
|
|
462
|
+
if (!response.ok) throw new Error(`OAuth discovery failed (${response.status}).`);
|
|
463
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
464
|
+
if (
|
|
465
|
+
typeof body['authorization_endpoint'] !== 'string' ||
|
|
466
|
+
typeof body['token_endpoint'] !== 'string'
|
|
467
|
+
) {
|
|
468
|
+
throw new Error('OAuth discovery returned invalid authorization endpoints.');
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
authorizationEndpoint: body['authorization_endpoint'],
|
|
472
|
+
tokenEndpoint: body['token_endpoint'],
|
|
473
|
+
...(typeof body['device_authorization_endpoint'] === 'string'
|
|
474
|
+
? { deviceAuthorizationEndpoint: body['device_authorization_endpoint'] }
|
|
475
|
+
: {}),
|
|
476
|
+
};
|
|
477
|
+
})();
|
|
478
|
+
return this.oauthMetadataPromise;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async authorizationMethods(): Promise<{ browser: true; device: boolean }> {
|
|
482
|
+
const metadata = await this.oauthMetadata();
|
|
483
|
+
return { browser: true, device: metadata.deviceAuthorizationEndpoint !== undefined };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private async editorProjection(
|
|
487
|
+
state: PersistedAccount,
|
|
488
|
+
hasAccessToken = false,
|
|
489
|
+
managedServicesAllowed = false,
|
|
490
|
+
) {
|
|
491
|
+
const providerCredentials = await this.providerCredentials.snapshot();
|
|
492
|
+
const byokProviders = providerCredentials
|
|
493
|
+
.filter((provider) => provider.configured)
|
|
494
|
+
.map((provider) => provider.provider);
|
|
495
|
+
const routes = {
|
|
496
|
+
mock: true as const,
|
|
497
|
+
managed: Boolean(
|
|
498
|
+
state.backend === 'live' &&
|
|
499
|
+
process.env['VGAI_GENERATION_GATEWAY'] &&
|
|
500
|
+
hasAccessToken &&
|
|
501
|
+
managedServicesAllowed,
|
|
502
|
+
),
|
|
503
|
+
byok: byokProviders.length > 0,
|
|
504
|
+
byokProviders,
|
|
505
|
+
};
|
|
506
|
+
let preferredRoute = state.preferredRoute ?? 'auto';
|
|
507
|
+
if (
|
|
508
|
+
preferredRoute !== 'auto' &&
|
|
509
|
+
(!routes[preferredRoute] || (preferredRoute === 'mock' && state.backend !== 'mock'))
|
|
510
|
+
) {
|
|
511
|
+
preferredRoute = 'auto';
|
|
512
|
+
state.preferredRoute = preferredRoute;
|
|
513
|
+
await writePersisted(this.accountPath, state);
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
accountEnvironment:
|
|
517
|
+
state.backend === 'mock'
|
|
518
|
+
? ('test-mock' as const)
|
|
519
|
+
: this.twinBaseUrl()
|
|
520
|
+
? ('development-twin' as const)
|
|
521
|
+
: ('production' as const),
|
|
522
|
+
routes,
|
|
523
|
+
providerCredentials,
|
|
524
|
+
preferredRoute,
|
|
525
|
+
codingInference: CodingInferenceSettingsSchema.parse(
|
|
526
|
+
state.codingInference ?? DEFAULT_CODING_INFERENCE,
|
|
527
|
+
),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private mock(state: PersistedAccount): MockGenerativeControlPlane {
|
|
532
|
+
if (this.mockControl && this.mockControlOwner === state) return this.mockControl;
|
|
533
|
+
this.mockControlOwner = state;
|
|
534
|
+
this.mockControl = createMockGenerativeControlPlane({
|
|
535
|
+
...(state.mockControlState ? { state: state.mockControlState } : {}),
|
|
536
|
+
onStateChange: async (controlState) => {
|
|
537
|
+
// Mock sessions are process-local just like live access tokens. The
|
|
538
|
+
// durable fixture ledger contains users/accounting, never bearer-like
|
|
539
|
+
// session strings; restart simply creates another zero-charge session.
|
|
540
|
+
state.mockControlState = { ...controlState, sessions: [] };
|
|
541
|
+
await writePersisted(this.accountPath, state);
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
return this.mockControl;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private async mockFetch(
|
|
548
|
+
state: PersistedAccount,
|
|
549
|
+
path: string,
|
|
550
|
+
init: RequestInit = {},
|
|
551
|
+
): Promise<Response> {
|
|
552
|
+
const headers = new Headers(init.headers);
|
|
553
|
+
if (this.mockToken) headers.set('Authorization', `Bearer ${this.mockToken}`);
|
|
554
|
+
return this.mock(state).fetch(new Request(`https://account.mock${path}`, { ...init, headers }));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private async ensureMockSession(state: PersistedAccount): Promise<void> {
|
|
558
|
+
if (this.mockToken || !state.email) return;
|
|
559
|
+
const response = await this.mockFetch(state, '/v1/sessions', {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: { 'Content-Type': 'application/json' },
|
|
562
|
+
body: JSON.stringify({ email: state.email }),
|
|
563
|
+
});
|
|
564
|
+
const body = (await response.json()) as { token?: unknown };
|
|
565
|
+
if (!response.ok || typeof body.token !== 'string') {
|
|
566
|
+
throw new Error('Could not create the local mock account session.');
|
|
567
|
+
}
|
|
568
|
+
this.mockToken = body.token;
|
|
569
|
+
|
|
570
|
+
// One-time migration from the pre-control-plane editor mock. The control
|
|
571
|
+
// plane becomes the only implementation of plan, policy, credit, and usage
|
|
572
|
+
// rules; the editor retains only its opaque serialized state and session.
|
|
573
|
+
if (state.planId && state.planId !== 'free') {
|
|
574
|
+
await this.mockFetch(state, '/v1/billing/checkout', {
|
|
575
|
+
method: 'POST',
|
|
576
|
+
headers: { 'Content-Type': 'application/json' },
|
|
577
|
+
body: JSON.stringify({ plan: state.planId }),
|
|
578
|
+
});
|
|
579
|
+
if (state.planStatus === 'cancelling') {
|
|
580
|
+
await this.mockFetch(state, '/v1/plan', {
|
|
581
|
+
method: 'PUT',
|
|
582
|
+
headers: { 'Content-Type': 'application/json' },
|
|
583
|
+
body: JSON.stringify({ action: 'cancel' }),
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (state.purchasedCredits && state.purchasedCredits > 0) {
|
|
588
|
+
await this.mockFetch(state, '/v1/credits/purchase', {
|
|
589
|
+
method: 'POST',
|
|
590
|
+
headers: { 'Content-Type': 'application/json' },
|
|
591
|
+
body: JSON.stringify({ credits: state.purchasedCredits }),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
if (state.spendPolicy) {
|
|
595
|
+
await this.mockFetch(state, '/v1/spend-policy', {
|
|
596
|
+
method: 'PUT',
|
|
597
|
+
headers: { 'Content-Type': 'application/json' },
|
|
598
|
+
body: JSON.stringify(state.spendPolicy),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
delete state.planId;
|
|
602
|
+
delete state.planStatus;
|
|
603
|
+
delete state.planRenewsAt;
|
|
604
|
+
delete state.purchasedCredits;
|
|
605
|
+
delete state.spendPolicy;
|
|
606
|
+
await writePersisted(this.accountPath, state);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private async mockSnapshot(state: PersistedAccount): Promise<EditorAccountSnapshot> {
|
|
610
|
+
const projection = await this.editorProjection(state);
|
|
611
|
+
if (!state.email) {
|
|
612
|
+
return {
|
|
613
|
+
authenticated: false,
|
|
614
|
+
backend: 'mock',
|
|
615
|
+
...projection,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
await this.ensureMockSession(state);
|
|
619
|
+
const response = await this.mockFetch(state, '/v1/account');
|
|
620
|
+
if (!response.ok) throw new Error(`Mock account request failed (${response.status}).`);
|
|
621
|
+
const account = AccountSnapshotSchema.parse(await response.json());
|
|
622
|
+
return { ...account, ...projection };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Development uses the same managed-account ledger contract as production;
|
|
626
|
+
* only its identity and payment providers are Clerk and Polar twins. */
|
|
627
|
+
private async twinSnapshot(state: PersistedAccount): Promise<EditorAccountSnapshot> {
|
|
628
|
+
const token = await this.twinAccessToken(state);
|
|
629
|
+
if (!state.email || !state.twinSessionId) {
|
|
630
|
+
return {
|
|
631
|
+
authenticated: false,
|
|
632
|
+
backend: 'live',
|
|
633
|
+
...(await this.editorProjection(state, Boolean(token))),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
637
|
+
if (!accountUrl || !token) {
|
|
638
|
+
throw new Error('Clerk Twin development requires the managed account service.');
|
|
639
|
+
}
|
|
640
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}/v1/account`, {
|
|
641
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
642
|
+
});
|
|
643
|
+
if (!response.ok) throw new Error(`Twin account request failed (${response.status}).`);
|
|
644
|
+
const account = AccountSnapshotSchema.parse(await response.json());
|
|
645
|
+
const projection = await this.editorProjection(
|
|
646
|
+
state,
|
|
647
|
+
Boolean(token),
|
|
648
|
+
account.authenticated && account.entitlements.generation,
|
|
649
|
+
);
|
|
650
|
+
return { ...account, ...projection };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async snapshot(): Promise<EditorAccountSnapshot> {
|
|
654
|
+
const state = await this.state();
|
|
655
|
+
if (this.cloudAccount) {
|
|
656
|
+
state.backend = 'live';
|
|
657
|
+
state.preferredRoute = 'managed';
|
|
658
|
+
state.codingInference = {
|
|
659
|
+
enabled: true,
|
|
660
|
+
provider: 'openrouter',
|
|
661
|
+
model: this.cloudInferenceModel ?? 'z-ai/glm-5.2',
|
|
662
|
+
};
|
|
663
|
+
const projection = await this.editorProjection(
|
|
664
|
+
state,
|
|
665
|
+
true,
|
|
666
|
+
this.cloudAccount.authenticated && this.cloudAccount.entitlements.generation,
|
|
667
|
+
);
|
|
668
|
+
const route = this.cloudInferenceRoute ?? 'managed';
|
|
669
|
+
return {
|
|
670
|
+
...this.cloudAccount,
|
|
671
|
+
...projection,
|
|
672
|
+
routes: {
|
|
673
|
+
...projection.routes,
|
|
674
|
+
managed: route === 'managed' && projection.routes.managed,
|
|
675
|
+
byok: route === 'byok',
|
|
676
|
+
byokProviders: route === 'byok' ? ['openrouter'] : [],
|
|
677
|
+
},
|
|
678
|
+
preferredRoute: route,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (state.backend === 'mock') return this.mockSnapshot(state);
|
|
682
|
+
// A configured twin makes the `live` backend twin-backed (identity minted against the
|
|
683
|
+
// twin's Backend API), superseding the OAuth authorization-code path.
|
|
684
|
+
if (this.twinBaseUrl()) return this.twinSnapshot(state);
|
|
685
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
686
|
+
const token = await this.accessToken();
|
|
687
|
+
if (!accountUrl || !token) {
|
|
688
|
+
return {
|
|
689
|
+
authenticated: false,
|
|
690
|
+
backend: 'live',
|
|
691
|
+
...(await this.editorProjection(state, Boolean(token))),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}/v1/account`, {
|
|
695
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
696
|
+
});
|
|
697
|
+
if (!response.ok) throw new Error(`Account request failed (${response.status}).`);
|
|
698
|
+
const account = AccountSnapshotSchema.parse(await response.json());
|
|
699
|
+
if (account.authenticated) {
|
|
700
|
+
const identityChanged =
|
|
701
|
+
state.userId !== account.user.id ||
|
|
702
|
+
state.email !== account.user.email ||
|
|
703
|
+
state.name !== account.user.name;
|
|
704
|
+
if (identityChanged) {
|
|
705
|
+
state.userId = account.user.id;
|
|
706
|
+
state.email = account.user.email;
|
|
707
|
+
if (account.user.name) state.name = account.user.name;
|
|
708
|
+
else delete state.name;
|
|
709
|
+
await writePersisted(this.accountPath, state);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...account,
|
|
714
|
+
...(await this.editorProjection(
|
|
715
|
+
state,
|
|
716
|
+
true,
|
|
717
|
+
account.authenticated && account.entitlements.generation,
|
|
718
|
+
)),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** Trusted Node-only account authority used by the session-owned share
|
|
723
|
+
* host. Tokens never cross an editor HTTP response or enter project state. */
|
|
724
|
+
async collaborationAccountSession(): Promise<CollaborationAccountSession> {
|
|
725
|
+
const state = await this.state();
|
|
726
|
+
if (state.backend !== 'live') {
|
|
727
|
+
throw new Error('Sign in to a live Volter account before sharing this editor.');
|
|
728
|
+
}
|
|
729
|
+
if (state.twinSessionId || state.twinUrl) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
'The development Clerk twin cannot host a public share. Sign in through the production Volter account provider.',
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const snapshot = await this.snapshot();
|
|
735
|
+
if (!snapshot.authenticated) throw new Error('Sign in to your Volter account before sharing.');
|
|
736
|
+
const accessToken = await this.accessToken();
|
|
737
|
+
const authUrl = process.env['VGAI_AUTH_URL']?.trim() || state.authUrl?.trim();
|
|
738
|
+
if (!accessToken || !authUrl) {
|
|
739
|
+
throw new Error('The signed-in Volter account is missing collaboration authorization config.');
|
|
740
|
+
}
|
|
741
|
+
// Identity-only, deliberately NOT `/verify`: that route also enforces
|
|
742
|
+
// managed-services admission (a hand-set billing flag), and hosting a
|
|
743
|
+
// share needs to know WHO the host is, never whether they bought managed
|
|
744
|
+
// generation. Routed through `/verify`, a freshly signed-up account could
|
|
745
|
+
// not host a share at all.
|
|
746
|
+
const response = await fetch(`${authUrl.replace(/\/$/, '')}/collaboration/verify`, {
|
|
747
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
748
|
+
});
|
|
749
|
+
const verified = (await response.json().catch(() => undefined)) as
|
|
750
|
+
| { userId?: unknown }
|
|
751
|
+
| undefined;
|
|
752
|
+
if (!response.ok || verified?.userId !== snapshot.user.id) {
|
|
753
|
+
throw new Error('The Volter account session expired or does not match the editor account.');
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
accessToken,
|
|
757
|
+
authUrl: authUrl.replace(/\/$/, ''),
|
|
758
|
+
user: {
|
|
759
|
+
id: snapshot.user.id,
|
|
760
|
+
email: snapshot.user.email,
|
|
761
|
+
...(snapshot.user.name ? { name: snapshot.user.name } : {}),
|
|
762
|
+
organizations: snapshot.organizations ?? [],
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async setProviderCredential(
|
|
768
|
+
provider: ProviderCredentialId,
|
|
769
|
+
key: string,
|
|
770
|
+
): Promise<EditorAccountSnapshot> {
|
|
771
|
+
await this.providerCredentials.set(provider, key);
|
|
772
|
+
return this.snapshot();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async deleteProviderCredential(provider: ProviderCredentialId): Promise<EditorAccountSnapshot> {
|
|
776
|
+
const credentials = await this.providerCredentials.delete(provider);
|
|
777
|
+
const state = await this.state();
|
|
778
|
+
if (state.preferredRoute === 'byok' && !credentials.some((item) => item.configured)) {
|
|
779
|
+
state.preferredRoute = 'auto';
|
|
780
|
+
await writePersisted(this.accountPath, state);
|
|
781
|
+
}
|
|
782
|
+
return this.snapshot();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
testProviderCredential(provider: ProviderCredentialId): Promise<ProviderCredentialTestResult> {
|
|
786
|
+
return this.providerCredentials.test(provider);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async signInMock(email: string): Promise<EditorAccountSnapshot> {
|
|
790
|
+
if (process.env['VGAI_TEST_ACCOUNT_MOCK'] !== '1') {
|
|
791
|
+
throw new Error('The in-process account mock is available only to isolated tests.');
|
|
792
|
+
}
|
|
793
|
+
const normalized = email.trim().toLowerCase();
|
|
794
|
+
if (!normalized.includes('@')) throw new Error('Enter a valid email address.');
|
|
795
|
+
const prior = await this.state();
|
|
796
|
+
const state: PersistedAccount = {
|
|
797
|
+
version: 1,
|
|
798
|
+
backend: 'mock',
|
|
799
|
+
email: normalized,
|
|
800
|
+
preferredRoute: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'auto',
|
|
801
|
+
...(prior.mockControlState ? { mockControlState: prior.mockControlState } : {}),
|
|
802
|
+
};
|
|
803
|
+
await writePersisted(this.accountPath, state);
|
|
804
|
+
this.statePromise = Promise.resolve(state);
|
|
805
|
+
await this.credentialStore.delete();
|
|
806
|
+
this.credential = null;
|
|
807
|
+
if (!this.environmentAccessToken) delete process.env['VGAI_ACCESS_TOKEN'];
|
|
808
|
+
this.mockControl = undefined;
|
|
809
|
+
this.mockControlOwner = undefined;
|
|
810
|
+
this.mockToken = undefined;
|
|
811
|
+
await this.ensureMockSession(state);
|
|
812
|
+
return this.snapshot();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async signOut(): Promise<EditorAccountSnapshot> {
|
|
816
|
+
const prior = await this.state();
|
|
817
|
+
const state: PersistedAccount = {
|
|
818
|
+
version: 1,
|
|
819
|
+
backend: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'live',
|
|
820
|
+
preferredRoute: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'auto',
|
|
821
|
+
...(prior.mockControlState ? { mockControlState: prior.mockControlState } : {}),
|
|
822
|
+
};
|
|
823
|
+
await writePersisted(this.accountPath, state);
|
|
824
|
+
this.statePromise = Promise.resolve(state);
|
|
825
|
+
this.mockToken = undefined;
|
|
826
|
+
this.twinToken = undefined;
|
|
827
|
+
await this.credentialStore.delete();
|
|
828
|
+
this.credential = null;
|
|
829
|
+
if (!this.environmentAccessToken) delete process.env['VGAI_ACCESS_TOKEN'];
|
|
830
|
+
return this.snapshot();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async updateSpendPolicy(input: unknown): Promise<EditorAccountSnapshot> {
|
|
834
|
+
const state = await this.state();
|
|
835
|
+
if (state.backend === 'live') return this.liveMutation('/v1/spend-policy', 'PUT', input);
|
|
836
|
+
const policy = AccountSpendPolicySchema.parse(input);
|
|
837
|
+
await this.ensureMockSession(state);
|
|
838
|
+
const response = await this.mockFetch(state, '/v1/spend-policy', {
|
|
839
|
+
method: 'PUT',
|
|
840
|
+
headers: { 'Content-Type': 'application/json' },
|
|
841
|
+
body: JSON.stringify(policy),
|
|
842
|
+
});
|
|
843
|
+
if (!response.ok) throw new Error(`Mock spend policy update failed (${response.status}).`);
|
|
844
|
+
return this.snapshot();
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async setPreferredRoute(route: GenerationExecutionRoute): Promise<EditorAccountSnapshot> {
|
|
848
|
+
const state = await this.state();
|
|
849
|
+
const { routes } = await this.snapshot();
|
|
850
|
+
if (route !== 'auto' && (!routes[route] || (route === 'mock' && state.backend !== 'mock'))) {
|
|
851
|
+
throw new Error(`${route} execution is not available.`);
|
|
852
|
+
}
|
|
853
|
+
state.preferredRoute = route;
|
|
854
|
+
await writePersisted(this.accountPath, state);
|
|
855
|
+
return this.snapshot();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async setCodingInference(input: unknown): Promise<EditorAccountSnapshot> {
|
|
859
|
+
const settings = CodingInferenceSettingsSchema.parse(input);
|
|
860
|
+
const state = await this.state();
|
|
861
|
+
state.codingInference = settings;
|
|
862
|
+
await writePersisted(this.accountPath, state);
|
|
863
|
+
return this.snapshot();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async quoteCodingInference(input: unknown): Promise<CodingInferenceQuote> {
|
|
867
|
+
const value = input as Record<string, unknown>;
|
|
868
|
+
const model = typeof value?.['model'] === 'string' ? value['model'].trim() : '';
|
|
869
|
+
const promptTokens = Number(value?.['promptTokens']);
|
|
870
|
+
const outputTokens = Number(value?.['outputTokens']);
|
|
871
|
+
if (
|
|
872
|
+
!/^[A-Za-z0-9._:/-]{1,200}$/.test(model) ||
|
|
873
|
+
!Number.isSafeInteger(promptTokens) ||
|
|
874
|
+
promptTokens < 0 ||
|
|
875
|
+
!Number.isSafeInteger(outputTokens) ||
|
|
876
|
+
outputTokens < 0
|
|
877
|
+
) {
|
|
878
|
+
throw new Error('A model and non-negative token estimates are required.');
|
|
879
|
+
}
|
|
880
|
+
const gateway = process.env['VGAI_GENERATION_GATEWAY']?.replace(/\/$/, '');
|
|
881
|
+
const token = this.cloudInferenceGrant ?? (await this.accessToken());
|
|
882
|
+
if (!gateway || !token) throw new Error('Managed coding pricing is unavailable.');
|
|
883
|
+
const modelPath = this.cloudInferenceGrant
|
|
884
|
+
? '/coding/openrouter/api/v1/models'
|
|
885
|
+
: '/openrouter/api/v1/models';
|
|
886
|
+
const modelsResponse = await fetch(`${gateway}${modelPath}`, {
|
|
887
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
888
|
+
});
|
|
889
|
+
const modelsBody = (await modelsResponse.json().catch(() => undefined)) as
|
|
890
|
+
| { data?: Array<{ id?: unknown; pricing?: { prompt?: unknown; completion?: unknown } }> }
|
|
891
|
+
| undefined;
|
|
892
|
+
if (!modelsResponse.ok)
|
|
893
|
+
throw new Error(`OpenRouter pricing failed (${modelsResponse.status}).`);
|
|
894
|
+
const pricing = modelsBody?.data?.find((entry) => entry.id === model)?.pricing;
|
|
895
|
+
const promptPrice = Number(pricing?.prompt);
|
|
896
|
+
const completionPrice = Number(pricing?.completion);
|
|
897
|
+
if (!Number.isFinite(promptPrice) || !Number.isFinite(completionPrice)) {
|
|
898
|
+
throw new Error(`OpenRouter returned no pricing for ${model}.`);
|
|
899
|
+
}
|
|
900
|
+
const providerAmountUsd = promptTokens * promptPrice + outputTokens * completionPrice;
|
|
901
|
+
const quoteResponse = await fetch(`${gateway}/billing/quote`, {
|
|
902
|
+
method: 'POST',
|
|
903
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
904
|
+
body: JSON.stringify({
|
|
905
|
+
kind: 'inference',
|
|
906
|
+
provider: 'openrouter',
|
|
907
|
+
operation: model,
|
|
908
|
+
workload: 'coding',
|
|
909
|
+
model,
|
|
910
|
+
providerPrice: { currency: 'USD', amount: providerAmountUsd },
|
|
911
|
+
}),
|
|
912
|
+
});
|
|
913
|
+
const quoteBody = (await quoteResponse.json().catch(() => undefined)) as
|
|
914
|
+
| { estimatedCredits?: unknown; error?: unknown }
|
|
915
|
+
| undefined;
|
|
916
|
+
if (!quoteResponse.ok || typeof quoteBody?.estimatedCredits !== 'number') {
|
|
917
|
+
throw new Error(
|
|
918
|
+
typeof quoteBody?.error === 'string'
|
|
919
|
+
? quoteBody.error
|
|
920
|
+
: `Managed coding quote failed (${quoteResponse.status}).`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
model,
|
|
925
|
+
promptTokens,
|
|
926
|
+
outputTokens,
|
|
927
|
+
providerAmountUsd,
|
|
928
|
+
estimatedCredits: quoteBody.estimatedCredits,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Resolve the account-owned coding route for a spawned harness — the OPT-IN only.
|
|
933
|
+
* A CODING HARNESS IS NOT A PROVIDER (owner ruling, 2026-09-21; ARCHITECTURE-CORE
|
|
934
|
+
* §Managed services): a harness is a program the person already signed into, so this
|
|
935
|
+
* answers `null` — inject nothing, run the stock CLI on their own login — for every
|
|
936
|
+
* route but the `'managed'`/`'byok'` they chose for themselves in the account panel.
|
|
937
|
+
* `'auto'` is NOT a choice here, which is the one semantic that differs from the
|
|
938
|
+
* generation lane, where fal/tripo/worldlabs really are providers. Managed processes
|
|
939
|
+
* receive only a short-lived grant scoped to the hashed workspace and exact model;
|
|
940
|
+
* the reusable account token stays in this trusted server. */
|
|
941
|
+
async resolvedCodingInference(workspace: string): Promise<ResolvedCodingInference | null> {
|
|
942
|
+
if (this.cloudInferenceGrant) {
|
|
943
|
+
const gateway = process.env['VGAI_GENERATION_GATEWAY']?.replace(/\/$/, '');
|
|
944
|
+
const model = this.cloudInferenceModel ?? 'z-ai/glm-5.2';
|
|
945
|
+
if (!gateway || !/^https:\/\//.test(gateway) || !/^[A-Za-z0-9._:/-]{1,200}$/.test(model)) {
|
|
946
|
+
throw new Error('Cloud managed coding is not configured securely.');
|
|
947
|
+
}
|
|
948
|
+
// A hosted container has no device login to fall back to, so the deployment's own
|
|
949
|
+
// `VGAI_CLOUD_INFERENCE_ROUTE` IS the explicit choice the gate reads.
|
|
950
|
+
const route = this.cloudInferenceRoute ?? 'managed';
|
|
951
|
+
return {
|
|
952
|
+
provider: 'openrouter',
|
|
953
|
+
route,
|
|
954
|
+
preferredRoute: route,
|
|
955
|
+
model,
|
|
956
|
+
baseUrl: `${gateway}/coding/openrouter/api/v1`,
|
|
957
|
+
apiKey: this.cloudInferenceGrant,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const state = await this.state();
|
|
961
|
+
const settings = CodingInferenceSettingsSchema.parse(
|
|
962
|
+
state.codingInference ?? DEFAULT_CODING_INFERENCE,
|
|
963
|
+
);
|
|
964
|
+
if (!settings.enabled) return null;
|
|
965
|
+
// THE GATE. The person's own stored choice, read straight off disk so the default
|
|
966
|
+
// path costs no account round-trip: `'auto'` and `'mock'` both mean "run the stock
|
|
967
|
+
// CLI on the login they already did". `editorProjection` demotes a route the
|
|
968
|
+
// account can no longer serve back to `'auto'` whenever the panel loads, so a stale
|
|
969
|
+
// opt-in self-heals into that default rather than stranding the launch.
|
|
970
|
+
const preferredRoute = state.preferredRoute ?? 'auto';
|
|
971
|
+
if (preferredRoute !== 'managed' && preferredRoute !== 'byok') return null;
|
|
972
|
+
if ((await this.providerExecutionMode('openrouter')) === 'direct') {
|
|
973
|
+
const apiKey = await this.providerCredentials.credential('openrouter');
|
|
974
|
+
if (!apiKey)
|
|
975
|
+
throw new Error('Add an OpenRouter credential before starting this coding agent.');
|
|
976
|
+
return {
|
|
977
|
+
provider: 'openrouter',
|
|
978
|
+
route: 'byok',
|
|
979
|
+
preferredRoute,
|
|
980
|
+
model: settings.model,
|
|
981
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
982
|
+
apiKey,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const gateway = process.env['VGAI_GENERATION_GATEWAY']?.replace(/\/$/, '');
|
|
986
|
+
const accessToken = await this.accessToken();
|
|
987
|
+
const authUrl = process.env['VGAI_AUTH_URL']?.trim() || state.authUrl?.trim();
|
|
988
|
+
if (!gateway || !accessToken || !authUrl) {
|
|
989
|
+
throw new Error('Managed OpenRouter coding is unavailable for this account session.');
|
|
990
|
+
}
|
|
991
|
+
const response = await fetch(`${authUrl.replace(/\/$/, '')}/inference/grant`, {
|
|
992
|
+
method: 'POST',
|
|
993
|
+
headers: {
|
|
994
|
+
Authorization: `Bearer ${accessToken}`,
|
|
995
|
+
'Content-Type': 'application/json',
|
|
996
|
+
},
|
|
997
|
+
body: JSON.stringify({
|
|
998
|
+
workspaceId: `workspace:${createHash('sha256').update(workspace).digest('hex')}`,
|
|
999
|
+
provider: 'openrouter',
|
|
1000
|
+
workload: 'coding',
|
|
1001
|
+
model: settings.model,
|
|
1002
|
+
}),
|
|
1003
|
+
});
|
|
1004
|
+
const body = (await response.json().catch(() => undefined)) as
|
|
1005
|
+
| { grant?: unknown; error?: unknown }
|
|
1006
|
+
| undefined;
|
|
1007
|
+
if (!response.ok || typeof body?.grant !== 'string' || body.grant.length < 32) {
|
|
1008
|
+
const detail = typeof body?.error === 'string' ? `: ${body.error}` : '';
|
|
1009
|
+
throw new Error(`Managed coding grant failed (${response.status})${detail}`);
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
provider: 'openrouter',
|
|
1013
|
+
route: 'managed',
|
|
1014
|
+
preferredRoute,
|
|
1015
|
+
model: settings.model,
|
|
1016
|
+
baseUrl: `${gateway}/coding/openrouter/api/v1`,
|
|
1017
|
+
apiKey: body.grant,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async checkout(
|
|
1022
|
+
plan: string,
|
|
1023
|
+
idempotencyKey: string,
|
|
1024
|
+
): Promise<{ account: EditorAccountSnapshot; checkoutUrl?: string; mock: boolean }> {
|
|
1025
|
+
const state = await this.state();
|
|
1026
|
+
if (state.backend === 'live') {
|
|
1027
|
+
const checkoutUrl = await this.hostedCheckout(
|
|
1028
|
+
'/v1/billing/checkout',
|
|
1029
|
+
{ plan },
|
|
1030
|
+
idempotencyKey,
|
|
1031
|
+
);
|
|
1032
|
+
return {
|
|
1033
|
+
account: await this.snapshot(),
|
|
1034
|
+
...(checkoutUrl ? { checkoutUrl } : {}),
|
|
1035
|
+
mock: false,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
if (!state.email) throw new Error('Sign in before changing plans.');
|
|
1039
|
+
await this.ensureMockSession(state);
|
|
1040
|
+
const response = await this.mockFetch(state, '/v1/billing/checkout', {
|
|
1041
|
+
method: 'POST',
|
|
1042
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1043
|
+
body: JSON.stringify({ plan }),
|
|
1044
|
+
});
|
|
1045
|
+
if (!response.ok) throw new Error(`Mock checkout failed (${response.status}).`);
|
|
1046
|
+
return { account: await this.snapshot(), mock: true };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async updatePlan(action: 'cancel' | 'resume'): Promise<EditorAccountSnapshot> {
|
|
1050
|
+
const state = await this.state();
|
|
1051
|
+
if (state.backend === 'live') {
|
|
1052
|
+
return this.liveMutation('/v1/billing/subscription', 'PATCH', {
|
|
1053
|
+
cancelAtPeriodEnd: action === 'cancel',
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
await this.ensureMockSession(state);
|
|
1057
|
+
const response = await this.mockFetch(state, '/v1/plan', {
|
|
1058
|
+
method: 'PUT',
|
|
1059
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1060
|
+
body: JSON.stringify({ action }),
|
|
1061
|
+
});
|
|
1062
|
+
if (!response.ok) {
|
|
1063
|
+
const body = (await response.json()) as { error?: unknown };
|
|
1064
|
+
throw new Error(typeof body.error === 'string' ? body.error : 'Mock plan update failed.');
|
|
1065
|
+
}
|
|
1066
|
+
return this.snapshot();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async purchaseCredits(
|
|
1070
|
+
packId: string,
|
|
1071
|
+
idempotencyKey: string,
|
|
1072
|
+
): Promise<{ account: EditorAccountSnapshot; checkoutUrl?: string; mock: boolean }> {
|
|
1073
|
+
const state = await this.state();
|
|
1074
|
+
if (state.backend === 'live') {
|
|
1075
|
+
const checkoutUrl = await this.hostedCheckout(
|
|
1076
|
+
'/v1/credits/checkout',
|
|
1077
|
+
{ packId },
|
|
1078
|
+
idempotencyKey,
|
|
1079
|
+
);
|
|
1080
|
+
return {
|
|
1081
|
+
account: await this.snapshot(),
|
|
1082
|
+
...(checkoutUrl ? { checkoutUrl } : {}),
|
|
1083
|
+
mock: false,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
await this.ensureMockSession(state);
|
|
1087
|
+
const response = await this.mockFetch(state, '/v1/credits/purchase', {
|
|
1088
|
+
method: 'POST',
|
|
1089
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1090
|
+
body: JSON.stringify({ packId }),
|
|
1091
|
+
});
|
|
1092
|
+
if (!response.ok) throw new Error(`Mock credit purchase failed (${response.status}).`);
|
|
1093
|
+
return { account: await this.snapshot(), mock: true };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async accountUsage(): Promise<AccountUsageEntry[]> {
|
|
1097
|
+
const state = await this.state();
|
|
1098
|
+
if (state.backend === 'mock') {
|
|
1099
|
+
if (!state.email) return [];
|
|
1100
|
+
await this.ensureMockSession(state);
|
|
1101
|
+
const response = await this.mockFetch(state, '/v1/usage');
|
|
1102
|
+
if (!response.ok) throw new Error(`Mock usage request failed (${response.status}).`);
|
|
1103
|
+
const body = (await response.json()) as { entries?: AccountUsageEntry[] };
|
|
1104
|
+
return AccountUsageEntrySchema.array().parse(body.entries ?? []);
|
|
1105
|
+
}
|
|
1106
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
1107
|
+
const token = await this.accessToken();
|
|
1108
|
+
if (!accountUrl || !token) return [];
|
|
1109
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}/v1/usage`, {
|
|
1110
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1111
|
+
});
|
|
1112
|
+
if (!response.ok) throw new Error(`Usage request failed (${response.status}).`);
|
|
1113
|
+
const body = (await response.json()) as { entries?: AccountUsageEntry[] };
|
|
1114
|
+
return AccountUsageEntrySchema.array().parse(body.entries ?? []);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async accountCatalog(): Promise<AccountCatalog> {
|
|
1118
|
+
const state = await this.state();
|
|
1119
|
+
if (state.backend === 'mock') {
|
|
1120
|
+
await this.ensureMockSession(state);
|
|
1121
|
+
const response = await this.mockFetch(state, '/v1/catalog');
|
|
1122
|
+
if (!response.ok) throw new Error(`Mock catalog request failed (${response.status}).`);
|
|
1123
|
+
return AccountCatalogSchema.parse(await response.json());
|
|
1124
|
+
}
|
|
1125
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
1126
|
+
const token = await this.accessToken();
|
|
1127
|
+
if (!accountUrl || !token) throw new Error('Sign in before loading the billing catalog.');
|
|
1128
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}/v1/catalog`, {
|
|
1129
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1130
|
+
});
|
|
1131
|
+
if (!response.ok) throw new Error(`Catalog request failed (${response.status}).`);
|
|
1132
|
+
return AccountCatalogSchema.parse(await response.json());
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async billingPortal(): Promise<{ url: string; mock: boolean }> {
|
|
1136
|
+
const state = await this.state();
|
|
1137
|
+
if (state.backend === 'mock')
|
|
1138
|
+
return { url: `mock://billing/${state.email ?? 'signed-out'}`, mock: true };
|
|
1139
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
1140
|
+
const token = await this.accessToken();
|
|
1141
|
+
if (!accountUrl || !token) throw new Error('Sign in before opening billing.');
|
|
1142
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}/v1/billing/portal`, {
|
|
1143
|
+
method: 'POST',
|
|
1144
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1145
|
+
});
|
|
1146
|
+
if (!response.ok) throw new Error(`Billing portal request failed (${response.status}).`);
|
|
1147
|
+
const body = (await response.json()) as { portal?: { url?: unknown } };
|
|
1148
|
+
if (typeof body.portal?.url !== 'string') throw new Error('Billing portal returned no URL.');
|
|
1149
|
+
return { url: body.portal.url, mock: false };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async confirmCheckout(checkoutId: string): Promise<EditorAccountSnapshot> {
|
|
1153
|
+
if (!/^[A-Za-z0-9_-]{1,200}$/.test(checkoutId)) throw new Error('Invalid checkout id.');
|
|
1154
|
+
const state = await this.state();
|
|
1155
|
+
if (state.backend === 'mock') return this.snapshot();
|
|
1156
|
+
return this.liveMutation('/v1/billing/confirm', 'POST', { checkoutId });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
async confirmTwinCheckout(checkoutId: string): Promise<EditorAccountSnapshot> {
|
|
1160
|
+
if (!this.twinBaseUrl()) throw new Error('Twin checkout is unavailable outside development.');
|
|
1161
|
+
if (!/^[A-Za-z0-9_-]{1,200}$/.test(checkoutId)) throw new Error('Invalid checkout id.');
|
|
1162
|
+
return this.liveMutation('/v1/billing/twin-confirm', 'POST', { checkoutId });
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async markAlertsRead(): Promise<EditorAccountSnapshot> {
|
|
1166
|
+
const state = await this.state();
|
|
1167
|
+
if (state.backend === 'mock') return this.snapshot();
|
|
1168
|
+
return this.liveMutation('/v1/alerts/read', 'POST', {});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async beginDeviceAuthorization(): Promise<{
|
|
1172
|
+
deviceCode: string;
|
|
1173
|
+
userCode: string;
|
|
1174
|
+
verificationUri: string;
|
|
1175
|
+
expiresAt: string;
|
|
1176
|
+
intervalSeconds: number;
|
|
1177
|
+
}> {
|
|
1178
|
+
const metadata = await this.oauthMetadata();
|
|
1179
|
+
if (!metadata.deviceAuthorizationEndpoint) {
|
|
1180
|
+
throw new Error('This account provider does not advertise OAuth device authorization.');
|
|
1181
|
+
}
|
|
1182
|
+
const response = await fetch(metadata.deviceAuthorizationEndpoint, {
|
|
1183
|
+
method: 'POST',
|
|
1184
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1185
|
+
body: new URLSearchParams({
|
|
1186
|
+
client_id: this.oauthClientId(),
|
|
1187
|
+
scope: 'openid profile email',
|
|
1188
|
+
}),
|
|
1189
|
+
});
|
|
1190
|
+
if (!response.ok) throw new Error(`Account authorization failed (${response.status}).`);
|
|
1191
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
1192
|
+
if (
|
|
1193
|
+
typeof body['device_code'] !== 'string' ||
|
|
1194
|
+
typeof body['user_code'] !== 'string' ||
|
|
1195
|
+
typeof body['verification_uri'] !== 'string' ||
|
|
1196
|
+
typeof body['expires_in'] !== 'number'
|
|
1197
|
+
) {
|
|
1198
|
+
throw new Error('Account authorization returned an invalid device flow.');
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
deviceCode: body['device_code'],
|
|
1202
|
+
userCode: body['user_code'],
|
|
1203
|
+
verificationUri: body['verification_uri'],
|
|
1204
|
+
expiresAt: new Date(Date.now() + body['expires_in'] * 1_000).toISOString(),
|
|
1205
|
+
intervalSeconds: typeof body['interval'] === 'number' ? body['interval'] : 5,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/** Primary desktop login: system browser + authorization code with PKCE.
|
|
1210
|
+
* The browser sees only the one-time authorization URL; token exchange and
|
|
1211
|
+
* durable credential storage remain in this trusted Node boundary. */
|
|
1212
|
+
async beginBrowserAuthorization(redirectOrigin: string): Promise<{
|
|
1213
|
+
authorizationId: string;
|
|
1214
|
+
authorizationUrl: string;
|
|
1215
|
+
expiresAt: string;
|
|
1216
|
+
}> {
|
|
1217
|
+
const metadata = await this.oauthMetadata();
|
|
1218
|
+
const origin = new URL(redirectOrigin).origin;
|
|
1219
|
+
if (!['http:', 'https:'].includes(new URL(origin).protocol)) {
|
|
1220
|
+
throw new Error('The editor origin cannot receive an account callback.');
|
|
1221
|
+
}
|
|
1222
|
+
const authorizationId = base64Url(randomBytes(24));
|
|
1223
|
+
const verifier = base64Url(randomBytes(48));
|
|
1224
|
+
const challenge = base64Url(createHash('sha256').update(verifier).digest());
|
|
1225
|
+
const expiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
|
|
1226
|
+
const redirectUri = `${origin}/__editor/account/browser-authorization/callback`;
|
|
1227
|
+
this.browserAuthorizations.set(authorizationId, {
|
|
1228
|
+
verifier,
|
|
1229
|
+
redirectUri,
|
|
1230
|
+
expiresAt,
|
|
1231
|
+
status: 'pending',
|
|
1232
|
+
});
|
|
1233
|
+
const authorizationUrl = new URL(metadata.authorizationEndpoint);
|
|
1234
|
+
authorizationUrl.searchParams.set('response_type', 'code');
|
|
1235
|
+
authorizationUrl.searchParams.set('client_id', this.oauthClientId());
|
|
1236
|
+
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
|
|
1237
|
+
authorizationUrl.searchParams.set('state', authorizationId);
|
|
1238
|
+
authorizationUrl.searchParams.set('code_challenge', challenge);
|
|
1239
|
+
authorizationUrl.searchParams.set('code_challenge_method', 'S256');
|
|
1240
|
+
authorizationUrl.searchParams.set('scope', 'openid profile email');
|
|
1241
|
+
return { authorizationId, authorizationUrl: authorizationUrl.toString(), expiresAt };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async completeBrowserAuthorization(state: string, code: string): Promise<void> {
|
|
1245
|
+
const pending = this.browserAuthorizations.get(state);
|
|
1246
|
+
if (!pending || pending.status !== 'pending') {
|
|
1247
|
+
throw new Error('This account authorization is missing, expired, or already used.');
|
|
1248
|
+
}
|
|
1249
|
+
if (Date.parse(pending.expiresAt) <= Date.now()) {
|
|
1250
|
+
this.browserAuthorizations.delete(state);
|
|
1251
|
+
throw new Error('This account authorization expired. Return to the editor and try again.');
|
|
1252
|
+
}
|
|
1253
|
+
const metadata = await this.oauthMetadata();
|
|
1254
|
+
try {
|
|
1255
|
+
const response = await fetch(metadata.tokenEndpoint, {
|
|
1256
|
+
method: 'POST',
|
|
1257
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1258
|
+
body: new URLSearchParams({
|
|
1259
|
+
grant_type: 'authorization_code',
|
|
1260
|
+
client_id: this.oauthClientId(),
|
|
1261
|
+
code,
|
|
1262
|
+
redirect_uri: pending.redirectUri,
|
|
1263
|
+
code_verifier: pending.verifier,
|
|
1264
|
+
}),
|
|
1265
|
+
});
|
|
1266
|
+
if (!response.ok) throw new Error(`Account token exchange failed (${response.status}).`);
|
|
1267
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
1268
|
+
const accessToken = body['access_token'];
|
|
1269
|
+
const refreshToken = body['refresh_token'];
|
|
1270
|
+
const expiresIn = body['expires_in'];
|
|
1271
|
+
if (typeof accessToken !== 'string') {
|
|
1272
|
+
throw new Error('Account token exchange returned no access token.');
|
|
1273
|
+
}
|
|
1274
|
+
this.credential = {
|
|
1275
|
+
accessToken,
|
|
1276
|
+
...(typeof refreshToken === 'string' ? { refreshToken } : {}),
|
|
1277
|
+
...(typeof expiresIn === 'number'
|
|
1278
|
+
? { expiresAt: new Date(Date.now() + expiresIn * 1_000).toISOString() }
|
|
1279
|
+
: {}),
|
|
1280
|
+
};
|
|
1281
|
+
await this.credentialStore.write(this.credential);
|
|
1282
|
+
const persisted: PersistedAccount = {
|
|
1283
|
+
version: 1,
|
|
1284
|
+
backend: 'live',
|
|
1285
|
+
preferredRoute: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'auto',
|
|
1286
|
+
...(process.env['VGAI_ACCOUNT_URL'] ? { accountUrl: process.env['VGAI_ACCOUNT_URL'] } : {}),
|
|
1287
|
+
...(process.env['VGAI_OAUTH_ISSUER_URL']
|
|
1288
|
+
? { oauthIssuerUrl: process.env['VGAI_OAUTH_ISSUER_URL'] }
|
|
1289
|
+
: {}),
|
|
1290
|
+
...(process.env['VGAI_AUTH_URL'] ? { authUrl: process.env['VGAI_AUTH_URL'] } : {}),
|
|
1291
|
+
};
|
|
1292
|
+
await writePersisted(this.accountPath, persisted);
|
|
1293
|
+
this.statePromise = Promise.resolve(persisted);
|
|
1294
|
+
process.env['VGAI_ACCESS_TOKEN'] = accessToken;
|
|
1295
|
+
pending.status = 'complete';
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
pending.status = 'failed';
|
|
1298
|
+
pending.error = error instanceof Error ? error.message : String(error);
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
failBrowserAuthorization(state: string, message: string): void {
|
|
1304
|
+
const pending = this.browserAuthorizations.get(state);
|
|
1305
|
+
if (!pending || pending.status !== 'pending') return;
|
|
1306
|
+
pending.status = 'failed';
|
|
1307
|
+
pending.error = message;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async pollBrowserAuthorization(
|
|
1311
|
+
authorizationId: string,
|
|
1312
|
+
): Promise<{ pending: true } | { pending: false; account: EditorAccountSnapshot }> {
|
|
1313
|
+
const pending = this.browserAuthorizations.get(authorizationId);
|
|
1314
|
+
if (!pending) throw new Error('This account authorization is no longer active.');
|
|
1315
|
+
if (Date.parse(pending.expiresAt) <= Date.now()) {
|
|
1316
|
+
this.browserAuthorizations.delete(authorizationId);
|
|
1317
|
+
throw new Error('This account authorization expired.');
|
|
1318
|
+
}
|
|
1319
|
+
if (pending.status === 'failed') {
|
|
1320
|
+
this.browserAuthorizations.delete(authorizationId);
|
|
1321
|
+
throw new Error(pending.error ?? 'Account authorization failed.');
|
|
1322
|
+
}
|
|
1323
|
+
if (pending.status === 'pending') return { pending: true };
|
|
1324
|
+
this.browserAuthorizations.delete(authorizationId);
|
|
1325
|
+
return { pending: false, account: await this.snapshot() };
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async pollDeviceAuthorization(
|
|
1329
|
+
deviceCode: string,
|
|
1330
|
+
): Promise<{ pending: true } | { pending: false; account: EditorAccountSnapshot }> {
|
|
1331
|
+
const metadata = await this.oauthMetadata();
|
|
1332
|
+
const response = await fetch(metadata.tokenEndpoint, {
|
|
1333
|
+
method: 'POST',
|
|
1334
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1335
|
+
body: new URLSearchParams({
|
|
1336
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
1337
|
+
device_code: deviceCode,
|
|
1338
|
+
client_id: this.oauthClientId(),
|
|
1339
|
+
}),
|
|
1340
|
+
});
|
|
1341
|
+
const body = (await response.json()) as Record<string, unknown>;
|
|
1342
|
+
if (!response.ok) {
|
|
1343
|
+
if (body['error'] === 'authorization_pending' || body['error'] === 'slow_down') {
|
|
1344
|
+
return { pending: true };
|
|
1345
|
+
}
|
|
1346
|
+
throw new Error(`Account authorization failed (${response.status}).`);
|
|
1347
|
+
}
|
|
1348
|
+
if (typeof body['access_token'] !== 'string') {
|
|
1349
|
+
throw new Error('Completed account authorization returned no access token.');
|
|
1350
|
+
}
|
|
1351
|
+
const state: PersistedAccount = {
|
|
1352
|
+
version: 1,
|
|
1353
|
+
backend: 'live',
|
|
1354
|
+
preferredRoute: process.env['VGAI_TEST_ACCOUNT_MOCK'] === '1' ? 'mock' : 'auto',
|
|
1355
|
+
...(process.env['VGAI_ACCOUNT_URL'] ? { accountUrl: process.env['VGAI_ACCOUNT_URL'] } : {}),
|
|
1356
|
+
...(process.env['VGAI_OAUTH_ISSUER_URL']
|
|
1357
|
+
? { oauthIssuerUrl: process.env['VGAI_OAUTH_ISSUER_URL'] }
|
|
1358
|
+
: {}),
|
|
1359
|
+
...(process.env['VGAI_AUTH_URL'] ? { authUrl: process.env['VGAI_AUTH_URL'] } : {}),
|
|
1360
|
+
};
|
|
1361
|
+
this.credential = {
|
|
1362
|
+
accessToken: body['access_token'],
|
|
1363
|
+
...(typeof body['refresh_token'] === 'string' ? { refreshToken: body['refresh_token'] } : {}),
|
|
1364
|
+
...(typeof body['expires_in'] === 'number'
|
|
1365
|
+
? { expiresAt: new Date(Date.now() + body['expires_in'] * 1_000).toISOString() }
|
|
1366
|
+
: {}),
|
|
1367
|
+
};
|
|
1368
|
+
await this.credentialStore.write(this.credential);
|
|
1369
|
+
await writePersisted(this.accountPath, state);
|
|
1370
|
+
this.statePromise = Promise.resolve(state);
|
|
1371
|
+
process.env['VGAI_ACCESS_TOKEN'] = this.credential.accessToken;
|
|
1372
|
+
return { pending: false, account: await this.snapshot() };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private async liveMutation(
|
|
1376
|
+
path: string,
|
|
1377
|
+
method: 'POST' | 'PUT' | 'PATCH',
|
|
1378
|
+
input: unknown,
|
|
1379
|
+
): Promise<EditorAccountSnapshot> {
|
|
1380
|
+
await this.state();
|
|
1381
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
1382
|
+
const token = await this.accessToken();
|
|
1383
|
+
if (!accountUrl || !token) throw new Error('Sign in before managing the account.');
|
|
1384
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}${path}`, {
|
|
1385
|
+
method,
|
|
1386
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
1387
|
+
body: JSON.stringify(input),
|
|
1388
|
+
});
|
|
1389
|
+
if (!response.ok) throw new Error(`Account update failed (${response.status}).`);
|
|
1390
|
+
return this.snapshot();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
private async hostedCheckout(
|
|
1394
|
+
path: string,
|
|
1395
|
+
input: unknown,
|
|
1396
|
+
idempotencyKey: string,
|
|
1397
|
+
): Promise<string | undefined> {
|
|
1398
|
+
await this.state();
|
|
1399
|
+
const accountUrl = process.env['VGAI_ACCOUNT_URL'];
|
|
1400
|
+
const token = await this.accessToken();
|
|
1401
|
+
if (!accountUrl || !token) throw new Error('Sign in before opening checkout.');
|
|
1402
|
+
const response = await fetch(`${accountUrl.replace(/\/$/, '')}${path}`, {
|
|
1403
|
+
method: 'POST',
|
|
1404
|
+
headers: {
|
|
1405
|
+
Authorization: `Bearer ${token}`,
|
|
1406
|
+
'Content-Type': 'application/json',
|
|
1407
|
+
'Idempotency-Key': idempotencyKey,
|
|
1408
|
+
},
|
|
1409
|
+
body: JSON.stringify(input),
|
|
1410
|
+
});
|
|
1411
|
+
if (!response.ok) throw new Error(`Checkout request failed (${response.status}).`);
|
|
1412
|
+
const body = (await response.json()) as { checkout?: { url?: unknown }; url?: unknown };
|
|
1413
|
+
const url = body.checkout?.url ?? body.url;
|
|
1414
|
+
if (typeof url === 'string') return url;
|
|
1415
|
+
const subscription = body as { subscription?: { id?: unknown } };
|
|
1416
|
+
if (typeof subscription.subscription?.id === 'string') return undefined;
|
|
1417
|
+
throw new Error('Checkout returned neither a hosted URL nor a subscription update.');
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function parseCloudAccount(value: string | undefined): AccountSnapshot | undefined {
|
|
1422
|
+
if (!value) return undefined;
|
|
1423
|
+
try {
|
|
1424
|
+
const account = AccountSnapshotSchema.parse(JSON.parse(value));
|
|
1425
|
+
if (!account.authenticated || account.backend !== 'live') {
|
|
1426
|
+
throw new Error('Cloud account projection is not an authenticated live account.');
|
|
1427
|
+
}
|
|
1428
|
+
return account;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
throw new Error(
|
|
1431
|
+
`Cloud account projection is invalid: ${error instanceof Error ? error.message : 'invalid JSON'}`,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
}
|