Skip to content

Commit 54e83c2

Browse files
steipetekiranmagic7
andcommitted
fix: read Windsurf sessions from Devin app origin
Co-authored-by: kiranmagic7 <[email protected]>
1 parent d39b4d8 commit 54e83c2

6 files changed

Lines changed: 269 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
### Fixed
1818
- Settings: switch tabs immediately before animated window resizing and reduce Providers sidebar work. Thanks @elijahfriedman!
19+
- Windsurf: import complete Devin sessions from the current app origin before legacy browser storage. Thanks @kiranmagic7!
1920
- Menu bar: show provider status markers only for the provider rendered in each icon. Thanks @Zihao-Qi!
2021
- Codex CLI: make automatic usage reads prefer OAuth and CLI sources instead of blocking on the optional web dashboard.
2122
- Provider probes: cap captured subprocess output at 1 MiB per stream without dropping valid text at a truncated UTF-8 boundary. Thanks @ProspectOre!

Sources/CodexBarCore/Providers/Windsurf/WindsurfDevinSessionImporter.swift

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,20 @@ enum WindsurfDevinSessionImporter {
148148
let url: URL
149149
}
150150

151+
struct LocalStorageSnapshot: Equatable {
152+
let storage: [String: String]
153+
let sourceSuffix: String?
154+
}
155+
156+
typealias LocalStorageOriginEntries = (
157+
origin: URL,
158+
entries: [SweetCookieKit.ChromiumLocalStorageEntry])
159+
160+
static let localStorageOrigins = [
161+
URL(string: "https://app.devin.ai")!,
162+
URL(string: "https://windsurf.com")!,
163+
]
164+
151165
private static func importSessions(
152166
browserDetection: BrowserDetection,
153167
browsers: [Browser],
@@ -162,10 +176,13 @@ enum WindsurfDevinSessionImporter {
162176
}
163177

164178
for candidate in candidates {
165-
let storage = self.readLocalStorage(from: candidate.url, logger: logger)
166-
guard let session = self.session(from: storage, sourceLabel: candidate.label) else { continue }
167-
logger("Found Windsurf devin session in \(candidate.label)")
168-
sessions.append(session)
179+
let snapshots = self.readLocalStorageSnapshots(from: candidate.url, logger: logger)
180+
for snapshot in snapshots {
181+
let sourceLabel = self.sourceLabel(candidate.label, suffix: snapshot.sourceSuffix)
182+
guard let session = self.session(from: snapshot.storage, sourceLabel: sourceLabel) else { continue }
183+
logger("Found Windsurf devin session in \(sourceLabel)")
184+
sessions.append(session)
185+
}
169186
}
170187

171188
return self.deduplicateSessions(sessions)
@@ -213,37 +230,74 @@ enum WindsurfDevinSessionImporter {
213230
}
214231
}
215232

216-
private static func readLocalStorage(
233+
private static func readLocalStorageSnapshots(
217234
from levelDBURL: URL,
218-
logger: ((String) -> Void)? = nil) -> [String: String]
235+
logger: ((String) -> Void)? = nil) -> [LocalStorageSnapshot]
219236
{
220-
var storage: [String: String] = [:]
237+
let originEntries = Self.localStorageOrigins.map { origin in
238+
let entries = SweetCookieKit.ChromiumLocalStorageReader.readEntries(
239+
for: origin.absoluteString,
240+
in: levelDBURL,
241+
logger: logger)
242+
return (origin: origin, entries: entries)
243+
}
221244

222-
let entries = SweetCookieKit.ChromiumLocalStorageReader.readEntries(
223-
for: "https://windsurf.com",
245+
let textEntries = SweetCookieKit.ChromiumLocalStorageReader.readTextEntries(
224246
in: levelDBURL,
225247
logger: logger)
248+
return self.localStorageSnapshots(from: originEntries, textEntries: textEntries)
249+
}
226250

227-
for entry in entries where Self.targetKeys.contains(entry.key) {
228-
storage[entry.key] = self.decodedStorageValue(entry.value)
251+
static func localStorageSnapshots(
252+
from originEntries: [LocalStorageOriginEntries],
253+
textEntries: [SweetCookieKit.ChromiumLevelDBTextEntry]) -> [LocalStorageSnapshot]
254+
{
255+
var snapshots = self.localStorageSnapshots(from: originEntries)
256+
let textStorage = self.storage(from: textEntries)
257+
if textStorage.count == Self.targetKeys.count {
258+
snapshots.append(LocalStorageSnapshot(storage: textStorage, sourceSuffix: nil))
229259
}
230260

231-
if storage.count == Self.targetKeys.count {
232-
return storage
261+
return snapshots
262+
}
263+
264+
static func localStorageSnapshots(from originEntries: [LocalStorageOriginEntries]) -> [LocalStorageSnapshot] {
265+
originEntries.compactMap { originEntry in
266+
let storage = self.storage(from: originEntry.entries)
267+
guard storage.count == Self.targetKeys.count else { return nil }
268+
return LocalStorageSnapshot(
269+
storage: storage,
270+
sourceSuffix: originEntry.origin.host ?? originEntry.origin.absoluteString)
233271
}
272+
}
234273

235-
let textEntries = SweetCookieKit.ChromiumLocalStorageReader.readTextEntries(
236-
in: levelDBURL,
237-
logger: logger)
274+
private static func storage(
275+
from entries: [SweetCookieKit.ChromiumLocalStorageEntry]) -> [String: String]
276+
{
277+
var storage: [String: String] = [:]
278+
for entry in entries where storage[entry.key] == nil && Self.targetKeys.contains(entry.key) {
279+
storage[entry.key] = self.decodedStorageValue(entry.value)
280+
}
281+
return storage
282+
}
238283

239-
for entry in textEntries {
284+
private static func storage(
285+
from entries: [SweetCookieKit.ChromiumLevelDBTextEntry]) -> [String: String]
286+
{
287+
var storage: [String: String] = [:]
288+
for entry in entries {
240289
guard storage[entry.key] == nil, Self.targetKeys.contains(entry.key) else { continue }
241290
storage[entry.key] = self.decodedStorageValue(entry.value)
242291
}
243292

244293
return storage
245294
}
246295

296+
private static func sourceLabel(_ label: String, suffix: String?) -> String {
297+
guard let suffix else { return label }
298+
return "\(label) (\(suffix))"
299+
}
300+
247301
private static let targetKeys: Set<String> = [
248302
"devin_session_token",
249303
"devin_auth1_token",

Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ public enum WindsurfWebFetcherError: LocalizedError, Sendable {
119119
public var errorDescription: String? {
120120
switch self {
121121
case .noSessionData:
122-
"No Windsurf web session found in Chromium localStorage. Sign in to windsurf.com in Chrome or Edge first."
122+
"No Windsurf web session found in Chromium localStorage. " +
123+
"Sign in to app.devin.ai or windsurf.com in Chrome first."
123124
case let .invalidManualSession(message):
124125
"Invalid Windsurf session payload: \(message)"
125126
case let .apiCallFailed(message):

Tests/CodexBarTests/WindsurfDevinSessionImporterTests.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SweetCookieKit
23
import Testing
34
@testable import CodexBarCore
45

@@ -11,6 +12,14 @@ struct WindsurfDevinSessionImporterTests {
1112
#expect(!WindsurfDevinSessionImporter.fallbackBrowsersExcluding([.chrome, .edge]).contains(.edge))
1213
}
1314

15+
@Test
16+
func `reads Devin app storage before legacy Windsurf origin`() {
17+
#expect(WindsurfDevinSessionImporter.localStorageOrigins.map(\.absoluteString) == [
18+
"https://app.devin.ai",
19+
"https://windsurf.com",
20+
])
21+
}
22+
1423
@Test
1524
func `decodes quoted local storage strings`() {
1625
#expect(WindsurfDevinSessionImporter
@@ -36,6 +45,82 @@ struct WindsurfDevinSessionImporterTests {
3645
#expect(session?.sourceLabel == "Chrome Default")
3746
}
3847

48+
@Test
49+
func `keeps partial app origin separate from complete legacy origin`() throws {
50+
let appOrigin = try #require(URL(string: "https://app.devin.ai"))
51+
let legacyOrigin = try #require(URL(string: "https://windsurf.com"))
52+
53+
let snapshots = WindsurfDevinSessionImporter.localStorageSnapshots(from: [
54+
(
55+
origin: appOrigin,
56+
entries: [
57+
Self.entry(origin: appOrigin, key: "devin_session_token", value: "app-session"),
58+
Self.entry(origin: appOrigin, key: "devin_auth1_token", value: "app-auth1"),
59+
]),
60+
(
61+
origin: legacyOrigin,
62+
entries: [
63+
Self.entry(origin: legacyOrigin, key: "devin_session_token", value: "legacy-session"),
64+
Self.entry(origin: legacyOrigin, key: "devin_auth1_token", value: "legacy-auth1"),
65+
Self.entry(origin: legacyOrigin, key: "devin_account_id", value: "legacy-account"),
66+
Self.entry(origin: legacyOrigin, key: "devin_primary_org_id", value: "legacy-org"),
67+
]),
68+
])
69+
70+
#expect(snapshots == [
71+
WindsurfDevinSessionImporter.LocalStorageSnapshot(
72+
storage: [
73+
"devin_session_token": "legacy-session",
74+
"devin_auth1_token": "legacy-auth1",
75+
"devin_account_id": "legacy-account",
76+
"devin_primary_org_id": "legacy-org",
77+
],
78+
sourceSuffix: "windsurf.com"),
79+
])
80+
}
81+
82+
@Test
83+
func `keeps text entry fallback after structured origin snapshots`() throws {
84+
let appOrigin = try #require(URL(string: "https://app.devin.ai"))
85+
86+
let snapshots = WindsurfDevinSessionImporter.localStorageSnapshots(
87+
from: [
88+
(
89+
origin: appOrigin,
90+
entries: [
91+
Self.entry(origin: appOrigin, key: "devin_session_token", value: "stale-app-session"),
92+
Self.entry(origin: appOrigin, key: "devin_auth1_token", value: "stale-app-auth1"),
93+
Self.entry(origin: appOrigin, key: "devin_account_id", value: "stale-app-account"),
94+
Self.entry(origin: appOrigin, key: "devin_primary_org_id", value: "stale-app-org"),
95+
]),
96+
],
97+
textEntries: [
98+
Self.textEntry(key: "devin_session_token", value: "legacy-text-session"),
99+
Self.textEntry(key: "devin_auth1_token", value: "legacy-text-auth1"),
100+
Self.textEntry(key: "devin_account_id", value: "legacy-text-account"),
101+
Self.textEntry(key: "devin_primary_org_id", value: "legacy-text-org"),
102+
])
103+
104+
#expect(snapshots == [
105+
WindsurfDevinSessionImporter.LocalStorageSnapshot(
106+
storage: [
107+
"devin_session_token": "stale-app-session",
108+
"devin_auth1_token": "stale-app-auth1",
109+
"devin_account_id": "stale-app-account",
110+
"devin_primary_org_id": "stale-app-org",
111+
],
112+
sourceSuffix: "app.devin.ai"),
113+
WindsurfDevinSessionImporter.LocalStorageSnapshot(
114+
storage: [
115+
"devin_session_token": "legacy-text-session",
116+
"devin_auth1_token": "legacy-text-auth1",
117+
"devin_account_id": "legacy-text-account",
118+
"devin_primary_org_id": "legacy-text-org",
119+
],
120+
sourceSuffix: nil),
121+
])
122+
}
123+
39124
@Test
40125
func `deduplicates repeated session tokens while preserving first source`() {
41126
let sessions = [
@@ -69,4 +154,16 @@ struct WindsurfDevinSessionImporterTests {
69154
#expect(deduplicated[0].session.sessionToken == "devin-session-token$abc")
70155
#expect(deduplicated[1].session.sessionToken == "devin-session-token$def")
71156
}
157+
158+
private static func entry(origin: URL, key: String, value: String) -> ChromiumLocalStorageEntry {
159+
ChromiumLocalStorageEntry(
160+
origin: origin.absoluteString,
161+
key: key,
162+
value: value,
163+
rawValueLength: value.utf8.count)
164+
}
165+
166+
private static func textEntry(key: String, value: String) -> ChromiumLevelDBTextEntry {
167+
ChromiumLevelDBTextEntry(key: key, value: value)
168+
}
72169
}

Tests/CodexBarTests/WindsurfWebFetcherTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import Foundation
2+
import SweetCookieKit
23
import Testing
34
@testable import CodexBarCore
45

56
@Suite(.serialized)
67
struct WindsurfWebFetcherTests {
8+
@Test
9+
func `missing session guidance names current and legacy origins`() {
10+
let message = WindsurfWebFetcherError.noSessionData.errorDescription
11+
12+
#expect(message?.contains("app.devin.ai") == true)
13+
#expect(message?.contains("windsurf.com") == true)
14+
}
15+
716
private struct ResponseFixture {
817
let planName: String
918
let dailyRemaining: Int
@@ -221,6 +230,80 @@ struct WindsurfWebFetcherTests {
221230
#expect(snapshot.secondary?.usedPercent == 20)
222231
}
223232

233+
@Test
234+
func `auto import uses complete legacy origin when app origin is partial`() async throws {
235+
defer {
236+
WindsurfDevinSessionImporter.importSessionsOverrideForTesting = nil
237+
WindsurfDevinSessionImporter.importPreferredSessionsOverrideForTesting = nil
238+
WindsurfDevinSessionImporter.importFallbackSessionsOverrideForTesting = nil
239+
WindsurfWebFetcherStubURLProtocol.requests = []
240+
WindsurfWebFetcherStubURLProtocol.handler = nil
241+
}
242+
243+
let appOrigin = try #require(URL(string: "https://app.devin.ai"))
244+
let legacyOrigin = try #require(URL(string: "https://windsurf.com"))
245+
let snapshots = WindsurfDevinSessionImporter.localStorageSnapshots(from: [
246+
(
247+
origin: appOrigin,
248+
entries: [
249+
Self.localStorageEntry(origin: appOrigin, key: "devin_session_token", value: "app-session"),
250+
Self.localStorageEntry(origin: appOrigin, key: "devin_auth1_token", value: "app-auth1"),
251+
]),
252+
(
253+
origin: legacyOrigin,
254+
entries: [
255+
Self.localStorageEntry(origin: legacyOrigin, key: "devin_session_token", value: "legacy-session"),
256+
Self.localStorageEntry(origin: legacyOrigin, key: "devin_auth1_token", value: "legacy-auth1"),
257+
Self.localStorageEntry(origin: legacyOrigin, key: "devin_account_id", value: "legacy-account"),
258+
Self.localStorageEntry(origin: legacyOrigin, key: "devin_primary_org_id", value: "legacy-org"),
259+
]),
260+
])
261+
262+
let sessionInfos = snapshots.compactMap { snapshot in
263+
WindsurfDevinSessionImporter.session(
264+
from: snapshot.storage,
265+
sourceLabel: "Chrome Default (\(snapshot.sourceSuffix ?? "unknown"))")
266+
}
267+
WindsurfDevinSessionImporter.importPreferredSessionsOverrideForTesting = { _, _ in sessionInfos }
268+
269+
WindsurfWebFetcherStubURLProtocol.requests = []
270+
WindsurfWebFetcherStubURLProtocol.handler = { request in
271+
let url = try #require(request.url)
272+
#expect(request.value(forHTTPHeaderField: "x-devin-session-token") == "legacy-session")
273+
#expect(request.value(forHTTPHeaderField: "x-devin-auth1-token") == "legacy-auth1")
274+
#expect(request.value(forHTTPHeaderField: "x-devin-account-id") == "legacy-account")
275+
#expect(request.value(forHTTPHeaderField: "x-devin-primary-org-id") == "legacy-org")
276+
277+
let body = try WindsurfPlanStatusProtoCodec.decodeRequest(Self.requestBodyData(from: request))
278+
#expect(body.authToken == "legacy-session")
279+
280+
return Self.makeResponse(
281+
url: url,
282+
body: Self.makePlanStatusResponse(ResponseFixture(
283+
planName: "Pro",
284+
dailyRemaining: 70,
285+
weeklyRemaining: 85,
286+
planEndUnix: 1_777_888_000,
287+
dailyResetUnix: 1_777_900_000,
288+
weeklyResetUnix: 1_778_000_000)),
289+
contentType: "application/proto",
290+
statusCode: 200)
291+
}
292+
293+
let snapshot = try await WindsurfWebFetcher.fetchUsage(
294+
browserDetection: BrowserDetection(cacheTTL: 0),
295+
cookieSource: .auto,
296+
timeout: 2,
297+
session: self.makeSession())
298+
299+
#expect(snapshots.count == 1)
300+
#expect(sessionInfos.map(\.sourceLabel) == ["Chrome Default (windsurf.com)"])
301+
#expect(WindsurfWebFetcherStubURLProtocol.requests.count == 1)
302+
#expect(snapshot.identity?.loginMethod == "Pro")
303+
#expect(snapshot.primary?.usedPercent == 30)
304+
#expect(snapshot.secondary?.usedPercent == 15)
305+
}
306+
224307
@Test
225308
func `manual mode with empty session does not fall back to imported session`() async {
226309
defer {
@@ -439,6 +522,14 @@ struct WindsurfWebFetcherTests {
439522
data.append(UInt8(remaining))
440523
return data
441524
}
525+
526+
private static func localStorageEntry(origin: URL, key: String, value: String) -> ChromiumLocalStorageEntry {
527+
ChromiumLocalStorageEntry(
528+
origin: origin.absoluteString,
529+
key: key,
530+
value: value,
531+
rawValueLength: value.utf8.count)
532+
}
442533
}
443534

444535
final class WindsurfWebFetcherStubURLProtocol: URLProtocol {

0 commit comments

Comments
 (0)