feat(cloud): redeem desktop login codes for web-to-desktop identity stitching (GTM-93)#13418
Conversation
…titching (GTM-93)
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a desktop-login one-time-code redemption feature: a new module handles per-code approval, redeem requests with Firebase ID tokens, terminal/transient error handling, retry scheduling, and an auth watcher; wired into router navigation with a preserved-query namespace and new localization strings. ChangesDesktop login redemption feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Router
participant desktopLoginRedemption
participant DialogService
participant AuthStore
participant Backend
Router->>desktopLoginRedemption: afterEach navigation
desktopLoginRedemption->>desktopLoginRedemption: read preserved desktop_login_code
desktopLoginRedemption->>AuthStore: wait for authenticated user
desktopLoginRedemption->>DialogService: confirmRedemption(code, uid)
DialogService-->>desktopLoginRedemption: approved/declined
alt approved
desktopLoginRedemption->>AuthStore: get Firebase ID token
AuthStore-->>desktopLoginRedemption: idToken
desktopLoginRedemption->>Backend: POST /auth/desktop-login-codes/redeem
Backend-->>desktopLoginRedemption: response (ok/terminal/transient)
alt success
desktopLoginRedemption-->>Router: clear preserved query, success toast
else terminal error
desktopLoginRedemption-->>Router: clear preserved query, expired/failed toast
else transient error
desktopLoginRedemption->>desktopLoginRedemption: schedule delayed retry
end
else declined
desktopLoginRedemption-->>Router: clear preserved query
end
Suggested labels: Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1691 passed, 0 failed · 3 flaky📊 Browser Reports
📦 Bundle: 7.78 MB gzip 🔴 +1.78 kBDetailsSummary
Category Glance App Entry Points — 53.2 kB (baseline 48 kB) • 🔴 +5.19 kBMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.27 MB (baseline 1.27 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 110 kB (baseline 110 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 11 added / 11 removed / 3 unchanged Panels & Settings — 550 kB (baseline 550 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 16 unchanged User & Accounts — 29.1 kB (baseline 29.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 117 kB (baseline 117 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed / 1 unchanged UI Components — 57.5 kB (baseline 57.5 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 270 kB (baseline 270 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 13 added / 13 removed / 3 unchanged Utilities & Hooks — 3.39 MB (baseline 3.39 MB) • 🔴 +63 BHelpers, composables, and utility bundles
Status: 19 added / 19 removed / 16 unchanged Vendor & Third-Party — 15.3 MB (baseline 15.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 11.7 MB (baseline 11.7 MB) • 🔴 +735 BBundles that do not match a named category
Status: 75 added / 75 removed / 90 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-10T02:05:20.748Z",
"gitSha": "cc3087253cad7e9ae558e64f0621871e392cf309",
"branch": "benceruleanlu/gtm-93-desktop-login-completion",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2070.2269999999885,
"styleRecalcs": 10,
"styleRecalcDurationMs": 12.307999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 420.86,
"heapDeltaBytes": -7373336,
"heapUsedBytes": 62339656,
"domNodes": -278,
"jsHeapTotalBytes": 19283968,
"scriptDurationMs": 16.833000000000002,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2008.5000000000264,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.319999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 346.07,
"heapDeltaBytes": 3892280,
"heapUsedBytes": 67283460,
"domNodes": 20,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 15.389,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1984.986000000049,
"styleRecalcs": 77,
"styleRecalcDurationMs": 53.077,
"layouts": 12,
"layoutDurationMs": 3.818,
"taskDurationMs": 940.134,
"heapDeltaBytes": -12362392,
"heapUsedBytes": 57388168,
"domNodes": -242,
"jsHeapTotalBytes": 18759680,
"scriptDurationMs": 129.43200000000002,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1802.5400000000218,
"styleRecalcs": 72,
"styleRecalcDurationMs": 35.192,
"layouts": 12,
"layoutDurationMs": 3.333,
"taskDurationMs": 765.473,
"heapDeltaBytes": -4755720,
"heapUsedBytes": 47316420,
"domNodes": -270,
"jsHeapTotalBytes": 17690624,
"scriptDurationMs": 119.39999999999999,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1783.350999999982,
"styleRecalcs": 30,
"styleRecalcDurationMs": 16.933999999999997,
"layouts": 6,
"layoutDurationMs": 0.559,
"taskDurationMs": 372.413,
"heapDeltaBytes": -3551068,
"heapUsedBytes": 66221712,
"domNodes": -224,
"jsHeapTotalBytes": 11943936,
"scriptDurationMs": 18.783,
"eventListeners": -184,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1757.8469999999697,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.575000000000003,
"layouts": 6,
"layoutDurationMs": 0.6019999999999999,
"taskDurationMs": 381.921,
"heapDeltaBytes": -3665768,
"heapUsedBytes": 66148488,
"domNodes": -230,
"jsHeapTotalBytes": 12730368,
"scriptDurationMs": 19.480999999999998,
"eventListeners": -184,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 619.8290000000384,
"styleRecalcs": 12,
"styleRecalcDurationMs": 9.765,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 389.942,
"heapDeltaBytes": -23738084,
"heapUsedBytes": 45752600,
"domNodes": -275,
"jsHeapTotalBytes": 6963200,
"scriptDurationMs": 56.44199999999999,
"eventListeners": -201,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 583.8449999999966,
"styleRecalcs": 12,
"styleRecalcDurationMs": 8.251,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.13699999999994,
"heapDeltaBytes": -12209572,
"heapUsedBytes": 52915656,
"domNodes": 20,
"jsHeapTotalBytes": 24076288,
"scriptDurationMs": 57.755,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666682,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2044.7540000000117,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.75,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 522.577,
"heapDeltaBytes": -8411500,
"heapUsedBytes": 59027852,
"domNodes": -268,
"jsHeapTotalBytes": -1323008,
"scriptDurationMs": 89.122,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2059.2980000000125,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.759999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 527.8209999999999,
"heapDeltaBytes": -1155796,
"heapUsedBytes": 59189016,
"domNodes": -271,
"jsHeapTotalBytes": -569344,
"scriptDurationMs": 91.423,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2091.1289999999667,
"styleRecalcs": 69,
"styleRecalcDurationMs": 18.826,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1025.331,
"heapDeltaBytes": 12453840,
"heapUsedBytes": 85345104,
"domNodes": 18,
"jsHeapTotalBytes": 12378112,
"scriptDurationMs": 376.005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2109.2480000000933,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.191,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1028.497,
"heapDeltaBytes": 20758084,
"heapUsedBytes": 82326324,
"domNodes": -278,
"jsHeapTotalBytes": -626688,
"scriptDurationMs": 371.551,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3115.0820000000294,
"styleRecalcs": 62,
"styleRecalcDurationMs": 15.067999999999998,
"layouts": 60,
"layoutDurationMs": 6.922999999999998,
"taskDurationMs": 1250.582,
"heapDeltaBytes": 14149680,
"heapUsedBytes": 70555516,
"domNodes": 6,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 468.325,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3096.3799999999537,
"styleRecalcs": 62,
"styleRecalcDurationMs": 15.294000000000002,
"layouts": 60,
"layoutDurationMs": 7.122,
"taskDurationMs": 1230.5030000000002,
"heapDeltaBytes": 12885768,
"heapUsedBytes": 66829108,
"domNodes": -284,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 470.698,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2020.0830000000565,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.532000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 494.243,
"heapDeltaBytes": -8990068,
"heapUsedBytes": 64435680,
"domNodes": 22,
"jsHeapTotalBytes": 10104832,
"scriptDurationMs": 91.859,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2019.8739999999589,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.759,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 494.59799999999996,
"heapDeltaBytes": -8914360,
"heapUsedBytes": 65028600,
"domNodes": 20,
"jsHeapTotalBytes": 8531968,
"scriptDurationMs": 90.484,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 580.8949999999982,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.549999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 396.33599999999996,
"heapDeltaBytes": -22145096,
"heapUsedBytes": 47535344,
"domNodes": -275,
"jsHeapTotalBytes": 5652480,
"scriptDurationMs": 115.836,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66666666666665,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 607.0609999999306,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.331,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 397.373,
"heapDeltaBytes": -23140132,
"heapUsedBytes": 46733696,
"domNodes": -273,
"jsHeapTotalBytes": 5652480,
"scriptDurationMs": 124.04700000000001,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2012.4649999999633,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.434000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 381.692,
"heapDeltaBytes": -3485588,
"heapUsedBytes": 48929684,
"domNodes": -285,
"jsHeapTotalBytes": 17166336,
"scriptDurationMs": 13.314,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 2034.084000000007,
"styleRecalcs": 9,
"styleRecalcDurationMs": 10.042,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 392.19399999999996,
"heapDeltaBytes": -2469596,
"heapUsedBytes": 49850708,
"domNodes": -290,
"jsHeapTotalBytes": 17952768,
"scriptDurationMs": 14.329999999999998,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1756.9780000000037,
"styleRecalcs": 77,
"styleRecalcDurationMs": 39.220000000000006,
"layouts": 16,
"layoutDurationMs": 4.1899999999999995,
"taskDurationMs": 723.428,
"heapDeltaBytes": -16536160,
"heapUsedBytes": 53163832,
"domNodes": -232,
"jsHeapTotalBytes": 18759680,
"scriptDurationMs": 90.532,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1708.2470000000285,
"styleRecalcs": 76,
"styleRecalcDurationMs": 36.223,
"layouts": 16,
"layoutDurationMs": 4.181000000000001,
"taskDurationMs": 666.404,
"heapDeltaBytes": -9575108,
"heapUsedBytes": 49380804,
"domNodes": 62,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 90.494,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 984.6099999999751,
"styleRecalcs": 18,
"styleRecalcDurationMs": 32.12800000000001,
"layouts": 14,
"layoutDurationMs": 15.925999999999998,
"taskDurationMs": 755.301,
"heapDeltaBytes": 29945288,
"heapUsedBytes": 97151624,
"domNodes": 13673,
"jsHeapTotalBytes": 15466496,
"scriptDurationMs": 28.569999999999997,
"eventListeners": 2533,
"totalBlockingTimeMs": 149,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8131.411000000015,
"styleRecalcs": 250,
"styleRecalcDurationMs": 50.939,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3606.746,
"heapDeltaBytes": -1501348,
"heapUsedBytes": 66061092,
"domNodes": -271,
"jsHeapTotalBytes": -69632,
"scriptDurationMs": 1196.696,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 8158.192999999983,
"styleRecalcs": 251,
"styleRecalcDurationMs": 55.118,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3768.0289999999995,
"heapDeltaBytes": 8920504,
"heapUsedBytes": 69157048,
"domNodes": -273,
"jsHeapTotalBytes": -364544,
"scriptDurationMs": 1237.947,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12108.855000000005,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12086.460000000001,
"heapDeltaBytes": -43955296,
"heapUsedBytes": 167138788,
"domNodes": -3300,
"jsHeapTotalBytes": 5476352,
"scriptDurationMs": 534.917,
"eventListeners": -16376,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12217.73399999995,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12186.124000000002,
"heapDeltaBytes": -13999040,
"heapUsedBytes": 169669204,
"domNodes": -3300,
"jsHeapTotalBytes": 17563648,
"scriptDurationMs": 566.0019999999998,
"eventListeners": -16370,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15217.557999999997,
"styleRecalcs": 75,
"styleRecalcDurationMs": 17.38500000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15173.73,
"heapDeltaBytes": -35410960,
"heapUsedBytes": 172258100,
"domNodes": -8314,
"jsHeapTotalBytes": 18059264,
"scriptDurationMs": 853.0409999999999,
"eventListeners": -16372,
"totalBlockingTimeMs": 12,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 15004.467999999975,
"styleRecalcs": 69,
"styleRecalcDurationMs": 17.217999999999954,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14980.99,
"heapDeltaBytes": -13135852,
"heapUsedBytes": 179687560,
"domNodes": -3300,
"jsHeapTotalBytes": 12496896,
"scriptDurationMs": 800.5980000000001,
"eventListeners": -16364,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 435.80500000007305,
"styleRecalcs": 11,
"styleRecalcDurationMs": 16.182000000000002,
"layouts": 3,
"layoutDurationMs": 0.6090000000000002,
"taskDurationMs": 94.10400000000001,
"heapDeltaBytes": 4877580,
"heapUsedBytes": 56941640,
"domNodes": 128,
"jsHeapTotalBytes": 524288,
"scriptDurationMs": 8.645999999999999,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66666666666665,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 112.2330000000602,
"styleRecalcs": 9,
"styleRecalcDurationMs": 15.938,
"layouts": 2,
"layoutDurationMs": 0.838,
"taskDurationMs": 75.80300000000001,
"heapDeltaBytes": 2980856,
"heapUsedBytes": 67584680,
"domNodes": 109,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 8.635,
"eventListeners": 33,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/composables/useUrlActionLoaders.ts`:
- Around line 21-36: `runUrlActionLoaders` currently awaits
`desktopLoginRedemption.redeemIfPresent()` before any other URL-action handling,
which can block unrelated loaders. Update `useUrlActionLoaders` so the desktop
login redemption and the other independent loaders are started together, for
example by collecting them into a `Promise.allSettled` (or equivalent parallel
flow) inside `runUrlActionLoaders`. Keep the existing error handling around
`desktopLoginRedemption` and ensure the invite/create_workspace/pricing paths
can proceed without waiting on the desktop login flow.
In `@src/platform/cloud/onboarding/CloudSignupView.vue`:
- Around line 166-169: The call to useDesktopLoginRedemption().redeemIfPresent()
is only being silenced with void, so any rejection can still become an unhandled
promise rejection. Update the CloudSignupView setup flow to attach explicit
error handling to redeemIfPresent(), matching the pattern used by
usePostAuthRedirect and useUrlActionLoaders, so failures are caught and the
pre-auth path still exits silently.
In `@src/platform/cloud/onboarding/composables/useDesktopLoginRedemption.ts`:
- Around line 144-152: The redeemOnce flow waits on authStore.isInitialized
without a bound and can hang forever, blocking later URL loaders. Update
redeemOnce in useDesktopLoginRedemption to add a timeout around until(() =>
authStore.isInitialized).toBe(true), and if it times out, handle it the same as
the “no session yet” path so the code stash is kept for a later retry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 493f6d38-4ded-4433-bb67-014f9ec4648a
📒 Files selected for processing (12)
src/composables/useUrlActionLoaders.test.tssrc/composables/useUrlActionLoaders.tssrc/locales/en/main.jsonsrc/platform/cloud/onboarding/CloudLoginView.vuesrc/platform/cloud/onboarding/CloudSignupView.vuesrc/platform/cloud/onboarding/CloudSurveyView.vuesrc/platform/cloud/onboarding/UserCheckView.vuesrc/platform/cloud/onboarding/composables/useDesktopLoginRedemption.test.tssrc/platform/cloud/onboarding/composables/useDesktopLoginRedemption.tssrc/platform/cloud/onboarding/composables/usePostAuthRedirect.tssrc/platform/navigation/preservedQueryNamespaces.tssrc/router.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #13418 +/- ##
==========================================
+ Coverage 77.82% 77.92% +0.09%
==========================================
Files 1648 1658 +10
Lines 92736 111903 +19167
Branches 31842 37651 +5809
==========================================
+ Hits 72176 87204 +15028
- Misses 19917 23803 +3886
- Partials 643 896 +253
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 429 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 24f6f398e8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- redeemIfPresent never rejects; all six triggers fire-and-forget safely - strip desktop_login_code from previousFullPath on the raw string so URLSearchParams re-encoding cannot alter other params under vue-router - bound the auth-init wait (16s, matching router.ts) and stop blocking the other URL action loaders behind the approval dialog
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/cloud/onboarding/composables/useDesktopLoginRedemption.ts`:
- Around line 59-62: The filtering in useDesktopLoginRedemption is only matching
the raw parameter text, so encoded variants of the desktop login code key can
survive the redirect. Update the logic around the params filter to decode just
the parameter name before comparing it against DESKTOP_LOGIN_CODE_KEY, while
still preserving the original value/encoding for unrelated parameters. Make sure
the check in useDesktopLoginRedemption catches both exact and encoded spellings
of the key before building kept.
- Around line 228-230: The visible-URL cleanup in useDesktopLoginRedemption’s
stripCodeFromUrl() still rebuilds the URL from route.query, which can re-encode
surviving params; update that helper to scrub the current URL using
stripDesktopLoginCodeFromPath(route.fullPath) instead, matching the same
raw-path preservation used for previousFullPath and keeping the code out of the
visible address bar without losing encoding fidelity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 96b08f6a-edbc-4ea6-bae7-503c92a56ed8
📒 Files selected for processing (5)
src/composables/useUrlActionLoaders.test.tssrc/composables/useUrlActionLoaders.tssrc/platform/cloud/onboarding/composables/useDesktopLoginRedemption.test.tssrc/platform/cloud/onboarding/composables/useDesktopLoginRedemption.tssrc/platform/cloud/onboarding/composables/usePostAuthRedirect.ts
- Strip the code from previousFullPath at the auth guard so it never rides along on the login URL (the preserved-query stash captures it first) - Match percent-encoded spellings of the query key when scrubbing paths - Scrub the visible URL via the raw-path scrubber so surviving params keep their original encoding - Tie redemption approval to the specific code so a new code always re-prompts
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/cloud/onboarding/desktopLoginRedemption.test.ts`:
- Around line 248-260: The desktopLoginRedemption test coverage is missing
regressions for identity changes while a redemption is still in flight. Add
tests around the existing setup/trigger flow to verify that after a transient
failure, switching users requires a fresh approval, and that seeding SECOND_CODE
while the first approval dialog is still pending does not allow the earlier
promise settlement to clear the newer code. Reuse the existing desktop login
redemption helpers and assertions in desktopLoginRedemption.test.ts so the new
cases exercise the same coalescing and stash behavior.
In `@src/platform/cloud/onboarding/desktopLoginRedemption.ts`:
- Around line 37-40: The approval state in desktopLoginRedemption should be tied
to the specific user being redeemed, not just the code, so update the
CodeRedemptionState and the redemption flow to verify the signed-in user
identity before reusing any approved code. In the main redemption path and the
dialog-confirmation logic, use currentUser as the user-specific guard, and
re-read currentUser immediately after the approval dialog returns before
consuming the token so an account switch cannot redeem with a stale user object.
Ensure the code path that handles retries and stashed codes only proceeds when
the approval still matches the same user.
- Around line 60-62: Make the redemption flow code-aware so one
desktop_login_code cannot clear or reuse another code’s state. Update the global
inFlight coalescing and the settle() cleanup in desktopLoginRedemption.ts so
they track the specific code value being processed, and only resolve/clear
matching preserved query state for that code. Also adjust the confirmation/fetch
paths that use inFlight and the stash/restore logic so a newer code is not
deleted by an older pending redemption.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 212f25b6-e78f-4f09-a810-38ad0ed3d11e
📒 Files selected for processing (8)
src/locales/en/main.jsonsrc/platform/cloud/onboarding/desktopLoginRedemption.test.tssrc/platform/cloud/onboarding/desktopLoginRedemption.tssrc/platform/navigation/preservedQueryManager.test.tssrc/platform/navigation/preservedQueryManager.tssrc/platform/navigation/preservedQueryTracker.test.tssrc/platform/navigation/preservedQueryTracker.tssrc/router.ts
…ker (Comfy-Org#13465) ## What - `installPreservedQueryTracker` definitions accept an opt-in `stripAfterCapture` flag: the marked keys are captured into the sessionStorage stash and removed from the URL before the navigation completes (single guard redirect at the decoded query-object level; push/replace semantics are inherited from the original navigation, and vue-router force-replaces the initial one). - `preservedQueryManager` now captures the first non-empty string element of repeated (array-valued) params instead of silently dropping them. - New real-router test suite for the tracker (createRouter + createMemoryHistory, no router mocks), including a history-depth test pinning the push/replace inheritance; manager tests extended for the array/junk-value cases. ## Why One-time secrets in query params (first consumer: desktop login codes, GTM-93) must not linger in the visible URL, browser history, `previousFullPath` redirects, or telemetry. Stripping after navigation — what each loader does ad hoc today — leaves a window and forces per-feature URL scrubbing; Comfy-Org#13418 originally needed a hand-rolled encoding-aware string parser in three places. Stripping at capture time, at the decoded query-object level, makes the stash the only carrier and lets vue-router round-trip the surviving params' encoding itself. Capability only — no existing namespace opts in; behavior is unchanged for all current definitions. `stripAfterCapture`'s contract is documented on the option: strip-marked keys must never be read from `route.query` by later guards or views; the stash is the only post-capture source. ## Landing order Independent of everything else; Comfy-Org#13418 stacks on this branch.
# Conflicts: # src/platform/navigation/preservedQueryManager.test.ts # src/platform/navigation/preservedQueryTracker.test.ts # src/platform/navigation/preservedQueryTracker.ts
…h-aware Approval is now recorded as the approving account's uid and re-verified after the dialog resolves, so an account change mid-dialog or between a transient failure and its retry can never redeem on a stale approval. Settlement clears the preserved-query stash only when it still holds the settling code, and the redemption runner drains the stash after each pass, so a newer code stashed mid-flight survives and is processed immediately.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/platform/cloud/onboarding/desktopLoginRedemption.ts (1)
149-159: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRe-check the active account after token retrieval.
Line 151 awaits
approvedUser.getIdToken()after the uid check. IfcurrentUserchanges while that promise is pending, the subsequent POST can still redeem with the old user’s token. Re-readcurrentUserafter token retrieval and beforefetch.Proposed fix
try { idToken = await approvedUser.getIdToken() } catch { handleTransientFailure(code, state, 'could not get id token') return } + + const tokenUser = useAuthStore().currentUser + if (!tokenUser || tokenUser.uid !== state.approvedUserUid) return let response: Response🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/platform/cloud/onboarding/desktopLoginRedemption.ts` around lines 149 - 159, The redeem flow in desktopLoginRedemption should re-check the active account after approvedUser.getIdToken() resolves and before calling fetch, because currentUser may have changed while the token request was pending. Update the logic around approvedUser and the redemption request so it verifies the current user still matches the original uid after token retrieval, and aborts via handleTransientFailure if not.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/platform/cloud/onboarding/desktopLoginRedemption.ts`:
- Around line 149-159: The redeem flow in desktopLoginRedemption should re-check
the active account after approvedUser.getIdToken() resolves and before calling
fetch, because currentUser may have changed while the token request was pending.
Update the logic around approvedUser and the redemption request so it verifies
the current user still matches the original uid after token retrieval, and
aborts via handleTransientFailure if not.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d19bdd88-bbc5-4443-871b-70e56324462c
📒 Files selected for processing (2)
src/platform/cloud/onboarding/desktopLoginRedemption.test.tssrc/platform/cloud/onboarding/desktopLoginRedemption.ts
…ter a 401 A trigger arriving while a drain is in progress (e.g. the auth watcher firing while the approval dialog is open) was dropped, leaving a mismatch-parked code stashed until an unrelated navigation; it now replays as one more drain pass. A 401 retry also minted the same cached id token; it now forces a refresh.
AustinMroz
left a comment
There was a problem hiding this comment.
This one was kinda rough to review, but I'm confident in the correctness of the code.
Co-authored-by: AustinMroz <austin@comfy.org>
|
@benceruleanlu Successfully backported to #13566 |
What
Browser half of GTM-93 macOS web→desktop identity stitching: when the desktop app opens the system browser at cloud login with
?desktop_login_code=dlc_…, the frontend redeems that code against the cloud backend once a Firebase session exists — after explicit user approval.Reworked on top of the preserved-query
stripAfterCapturecapability (#13465):DESKTOP_LOGINnamespace opts into strip-on-capture: the code is stashed and removed from the URL before any navigation completes, so it never reaches history,previousFullPath, later guards, or telemetry — the hand-rolled URL scrubbing this PR previously carried (raw-string parser + three strip sites) is gone.desktopLoginRedemption.tsis a plain module with a single export,installDesktopLoginRedemption(router), installed once inrouter.ts's cloud block (replaces six per-view/composable trigger sites). Redemption reads the code only from the stash: per-code state (approval + 2-attempt transient budget, so a second code gets its own approval and budget), approval dialog,POST /api/auth/desktop-login-codes/redeemwith the raw Firebase ID token (backend route is Firebase-JWT-only), 10s fetch timeout.router.afterEach(the cloud auth guard settles Firebase init before navigations complete) plus a lazy watcher onauthStore.currentUserfor sessions that appear without a navigation (OAuth-resume error branch, dialog sign-in). One bounded in-page retry (5s) guarantees an approved sign-in always ends in a success or failure toast.Why
Windows stitches web→desktop at download time via installer stamping; macOS DMGs can't be stamped, so we stitch at login. The browser is where both halves meet: the existing
posthog.identify(uid)merges the web anon person into the Firebase uid, and the backend emitscomfy.cloud.identity.login_attributed(uid ↔ installation_id) at redeem. The desktop app polls the backend and receives a one-time custom token — no auth material posted to a desktop loopback server (the concern that stalled #12983, which this supersedes).Security
Testing
Vitest, driven through a real router (createRouter/createMemoryHistory, no vue-router mocks) and the real preserved-query manager: capture/stash lifecycle, approval gate (no fetch before approve; decline/dismiss clears; per-code approval), Bearer/body shape, terminal-vs-transient statuses, timeout abort, bounded in-page retry + failure toast on budget exhaustion, per-code regressions (second code after success/decline/exhaustion redeems independently), auth-watcher trigger (session appearing without navigation), unauthenticated no-op, trigger coalescing. Typecheck/lint/format clean.
Types are hand-written with a
TODO(@comfyorg/ingest-types)— the generated types land automatically once the cloud PR merges and the type-gen workflow runs.Landing order
GTM-93 · Supersedes #12983