460 lines
17 KiB
Swift
460 lines
17 KiB
Swift
import Foundation
|
|
#if os(macOS)
|
|
import SweetCookieKit
|
|
#endif
|
|
|
|
#if os(macOS)
|
|
enum DevinSessionImporter {
|
|
nonisolated(unsafe) static var importSessionOverrideForTesting:
|
|
((BrowserDetection, String?, ((String) -> Void)?) -> SessionInfo?)?
|
|
|
|
private static let storageOrigin = "https://app.devin.ai"
|
|
private static let externalOrgPrefix = "last-internal-org-for-external-org-v1-"
|
|
|
|
struct SessionInfo: Equatable {
|
|
let accessToken: String
|
|
let organization: String?
|
|
let internalOrganizationID: String?
|
|
let sourceLabel: String
|
|
}
|
|
|
|
struct LocalStorageCandidate {
|
|
let label: String
|
|
let url: URL
|
|
}
|
|
|
|
static func importSession(
|
|
browserDetection: BrowserDetection,
|
|
organizationOverride: String? = nil,
|
|
logger: ((String) -> Void)? = nil) -> SessionInfo?
|
|
{
|
|
if let override = self.importSessionOverrideForTesting {
|
|
return override(browserDetection, organizationOverride, logger)
|
|
}
|
|
|
|
let sessions = self.importSessions(
|
|
browserDetection: browserDetection,
|
|
organizationOverride: organizationOverride,
|
|
logger: logger)
|
|
return sessions.first
|
|
}
|
|
|
|
static func importSessions(
|
|
browserDetection: BrowserDetection,
|
|
organizationOverride: String? = nil,
|
|
logger: ((String) -> Void)? = nil) -> [SessionInfo]
|
|
{
|
|
if let override = self.importSessionOverrideForTesting {
|
|
return override(browserDetection, organizationOverride, logger).map { [$0] } ?? []
|
|
}
|
|
|
|
let log: (String) -> Void = { msg in logger?("[devin-storage] \(msg)") }
|
|
let candidates = self.chromeLocalStorageCandidates(browserDetection: browserDetection)
|
|
if !candidates.isEmpty {
|
|
log("Chrome local storage candidates: \(candidates.count)")
|
|
}
|
|
|
|
var sessions: [SessionInfo] = []
|
|
for candidate in candidates {
|
|
let storage = self.readLocalStorage(from: candidate.url, logger: log)
|
|
guard let session = self.session(
|
|
from: storage,
|
|
organizationOverride: organizationOverride,
|
|
sourceLabel: candidate.label)
|
|
else {
|
|
continue
|
|
}
|
|
log(
|
|
"Found Devin session in \(candidate.label); " +
|
|
"organization=\(session.organization != nil), internalOrganizationID=" +
|
|
"\(session.internalOrganizationID != nil)")
|
|
sessions.append(session)
|
|
}
|
|
sessions = self.rankSessions(self.deduplicateSessions(sessions))
|
|
|
|
if sessions.isEmpty {
|
|
log("No Devin session found in browser local storage")
|
|
}
|
|
return sessions
|
|
}
|
|
|
|
static func session(
|
|
from storage: [String: String],
|
|
organizationOverride: String? = nil,
|
|
sourceLabel: String) -> SessionInfo?
|
|
{
|
|
guard let accessToken = self.accessToken(from: storage) else {
|
|
return nil
|
|
}
|
|
let organizationInfo = self.organizationInfo(from: storage, organizationOverride: organizationOverride)
|
|
return SessionInfo(
|
|
accessToken: accessToken,
|
|
organization: organizationInfo.organization,
|
|
internalOrganizationID: organizationInfo.internalOrganizationID,
|
|
sourceLabel: sourceLabel)
|
|
}
|
|
|
|
static func accessToken(from storage: [String: String]) -> String? {
|
|
for (key, value) in storage where self.isAuth1StorageKey(key) {
|
|
guard let json = self.jsonObject(from: value),
|
|
let token = self.findAuth1Token(in: json)
|
|
else {
|
|
continue
|
|
}
|
|
return token
|
|
}
|
|
|
|
for (key, value) in storage where self.isAuth0StorageKey(key) {
|
|
guard let json = self.jsonObject(from: value),
|
|
let token = self.findAccessToken(in: json)
|
|
else {
|
|
continue
|
|
}
|
|
return token
|
|
}
|
|
|
|
for value in storage.values {
|
|
guard let json = self.jsonObject(from: value),
|
|
let token = self.findAccessToken(in: json)
|
|
else {
|
|
continue
|
|
}
|
|
return token
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
static func deduplicateSessions(_ sessions: [SessionInfo]) -> [SessionInfo] {
|
|
var order: [String] = []
|
|
var bestByToken: [String: SessionInfo] = [:]
|
|
for session in sessions {
|
|
if let existing = bestByToken[session.accessToken] {
|
|
if self.organizationScore(session) > self.organizationScore(existing) {
|
|
bestByToken[session.accessToken] = session
|
|
}
|
|
} else {
|
|
order.append(session.accessToken)
|
|
bestByToken[session.accessToken] = session
|
|
}
|
|
}
|
|
return order.compactMap { bestByToken[$0] }
|
|
}
|
|
|
|
static func rankSessions(_ sessions: [SessionInfo]) -> [SessionInfo] {
|
|
sessions.enumerated()
|
|
.sorted { lhs, rhs in
|
|
let lhsScore = self.organizationScore(lhs.element)
|
|
let rhsScore = self.organizationScore(rhs.element)
|
|
return lhsScore == rhsScore ? lhs.offset < rhs.offset : lhsScore > rhsScore
|
|
}
|
|
.map(\.element)
|
|
}
|
|
|
|
private static func organizationScore(_ session: SessionInfo) -> Int {
|
|
(session.organization == nil ? 0 : 1) + (session.internalOrganizationID == nil ? 0 : 2)
|
|
}
|
|
|
|
static func organizationInfo(
|
|
from storage: [String: String],
|
|
organizationOverride: String?) -> (organization: String?, internalOrganizationID: String?)
|
|
{
|
|
let override = DevinUsageFetcher.normalizedOrganization(organizationOverride)
|
|
let overrideSlug = override.flatMap(self.slug(fromNormalizedOrganization:))
|
|
var firstInternalOrgID: String?
|
|
|
|
for (key, value) in storage where self.isExternalOrgStorageKey(key) {
|
|
let suffix = self.externalOrgSlug(from: key)
|
|
let orgID = self.cleanedOrgID(value)
|
|
if firstInternalOrgID == nil {
|
|
firstInternalOrgID = orgID
|
|
}
|
|
if let overrideSlug, suffix == overrideSlug {
|
|
return (override, orgID)
|
|
}
|
|
if override == nil, suffix != "null" {
|
|
return ("org/\(suffix)", orgID)
|
|
}
|
|
}
|
|
|
|
if let inferred = self.inferredOrganizationInfo(from: storage, override: override) {
|
|
return inferred
|
|
}
|
|
|
|
if let override {
|
|
return (override, firstInternalOrgID ?? self.orgID(fromNormalizedOrganization: override))
|
|
}
|
|
|
|
return (firstInternalOrgID.map { "organizations/\($0)" }, firstInternalOrgID)
|
|
}
|
|
|
|
static func decodedStorageValue(_ value: String) -> String {
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return trimmed }
|
|
if let data = trimmed.data(using: .utf8),
|
|
let decoded = try? JSONDecoder().decode(String.self, from: data)
|
|
{
|
|
return decoded.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
return trimmed.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
}
|
|
|
|
static func chromeLocalStorageCandidates(browserDetection: BrowserDetection) -> [LocalStorageCandidate] {
|
|
let installedBrowsers = self.localStorageBrowsers(browserDetection: browserDetection)
|
|
let roots = ChromiumProfileLocator
|
|
.roots(for: installedBrowsers, homeDirectories: BrowserCookieClient.defaultHomeDirectories())
|
|
.map { (url: $0.url, labelPrefix: $0.labelPrefix) }
|
|
|
|
var candidates: [LocalStorageCandidate] = []
|
|
for root in roots {
|
|
candidates.append(contentsOf: self.chromeProfileLocalStorageDirs(
|
|
root: root.url,
|
|
labelPrefix: root.labelPrefix))
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
static func localStorageBrowsers(browserDetection: BrowserDetection) -> [Browser] {
|
|
let order = ProviderDefaults.metadata[.devin]?.browserCookieOrder ?? [.chrome]
|
|
return order.browsersWithProfileData(using: browserDetection)
|
|
}
|
|
|
|
private static func chromeProfileLocalStorageDirs(root: URL, labelPrefix: String) -> [LocalStorageCandidate] {
|
|
guard let entries = try? FileManager.default.contentsOfDirectory(
|
|
at: root,
|
|
includingPropertiesForKeys: [.isDirectoryKey],
|
|
options: [.skipsHiddenFiles])
|
|
else { return [] }
|
|
|
|
return entries.filter { url in
|
|
guard let isDir = (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory), isDir else {
|
|
return false
|
|
}
|
|
let name = url.lastPathComponent
|
|
return name == "Default" || name.hasPrefix("Profile ") || name.hasPrefix("user-")
|
|
}
|
|
.sorted { $0.lastPathComponent < $1.lastPathComponent }
|
|
.compactMap { dir in
|
|
let levelDBURL = dir.appendingPathComponent("Local Storage").appendingPathComponent("leveldb")
|
|
guard FileManager.default.fileExists(atPath: levelDBURL.path) else { return nil }
|
|
return LocalStorageCandidate(label: "\(labelPrefix) \(dir.lastPathComponent)", url: levelDBURL)
|
|
}
|
|
}
|
|
|
|
private static func readLocalStorage(from levelDBURL: URL, logger: ((String) -> Void)?) -> [String: String] {
|
|
var storage: [String: String] = [:]
|
|
let entries = SweetCookieKit.ChromiumLocalStorageReader.readEntries(
|
|
for: self.storageOrigin,
|
|
in: levelDBURL,
|
|
logger: logger)
|
|
for entry in entries {
|
|
storage[entry.key] = self.decodedStorageValue(entry.value)
|
|
}
|
|
|
|
let textEntries = SweetCookieKit.ChromiumLocalStorageReader.readTextEntries(
|
|
in: levelDBURL,
|
|
logger: logger)
|
|
for entry in textEntries where storage[entry.key] == nil {
|
|
if self.isUsefulStorageKey(entry.key) {
|
|
storage[entry.key] = self.decodedStorageValue(entry.value)
|
|
}
|
|
}
|
|
|
|
return storage
|
|
}
|
|
|
|
private static func jsonObject(from raw: String) -> Any? {
|
|
guard let data = raw.data(using: .utf8) else { return nil }
|
|
return try? JSONSerialization.jsonObject(with: data)
|
|
}
|
|
|
|
private static func findAuth1Token(in object: Any) -> String? {
|
|
guard let dictionary = object as? [String: Any],
|
|
let token = dictionary["token"] as? String
|
|
else {
|
|
return nil
|
|
}
|
|
let value = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return value.hasPrefix("auth1_") && value.count > 20 ? value : nil
|
|
}
|
|
|
|
private static func findAccessToken(in object: Any) -> String? {
|
|
if let dictionary = object as? [String: Any] {
|
|
for key in ["access_token", "accessToken"] {
|
|
if let value = dictionary[key] as? String,
|
|
self.looksLikeToken(value)
|
|
{
|
|
return value
|
|
}
|
|
}
|
|
for value in dictionary.values {
|
|
if let found = self.findAccessToken(in: value) {
|
|
return found
|
|
}
|
|
}
|
|
}
|
|
|
|
if let array = object as? [Any] {
|
|
for value in array {
|
|
if let found = self.findAccessToken(in: value) {
|
|
return found
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private static func looksLikeToken(_ raw: String) -> Bool {
|
|
let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return value.count > 20 && (value.hasPrefix("eyJ") || value.contains("."))
|
|
}
|
|
|
|
private static func isAuth1StorageKey(_ key: String) -> Bool {
|
|
key.hasSuffix("auth1_session")
|
|
}
|
|
|
|
private static func isAuth0StorageKey(_ key: String) -> Bool {
|
|
key.contains("auth0spajs@@::")
|
|
}
|
|
|
|
private static func isExternalOrgStorageKey(_ key: String) -> Bool {
|
|
key.contains(self.externalOrgPrefix)
|
|
}
|
|
|
|
private static func isUsefulStorageKey(_ key: String) -> Bool {
|
|
self.isAuth1StorageKey(key) ||
|
|
self.isAuth0StorageKey(key) ||
|
|
self.isExternalOrgStorageKey(key) ||
|
|
key.contains("post-auth-v") ||
|
|
key.contains("member-info-v") ||
|
|
key.contains("feature-flags-cache:org-") ||
|
|
key.contains("feature-flags-cache:org_")
|
|
}
|
|
|
|
private static func inferredOrganizationInfo(
|
|
from storage: [String: String],
|
|
override: String?) -> (organization: String?, internalOrganizationID: String?)?
|
|
{
|
|
let overrideSlug = override.flatMap(self.slug(fromNormalizedOrganization:))
|
|
let overrideOrgID = override.flatMap(self.orgID(fromNormalizedOrganization:))
|
|
var fallbackSlug: String?
|
|
var fallbackInternalOrgID: String?
|
|
|
|
for (key, value) in storage {
|
|
let object = self.jsonObject(from: value)
|
|
let internalOrgID = self.cleanedOrgID(self.firstString(
|
|
in: object,
|
|
matching: ["internalOrgId", "internal_org_id", "org_id", "orgId"]))
|
|
?? self.internalOrgIDFromStorageKey(key)
|
|
let slug = self.cleanedSlug(
|
|
self.slugFromPostAuthKey(key) ??
|
|
self.firstString(in: object, matching: ["orgName", "org_name", "externalOrgId", "external_org_id"]))
|
|
|
|
if let overrideOrgID, internalOrgID == overrideOrgID {
|
|
return (override, internalOrgID)
|
|
}
|
|
if let overrideSlug, slug == overrideSlug {
|
|
return (override, internalOrgID)
|
|
}
|
|
|
|
if fallbackSlug == nil, let slug {
|
|
fallbackSlug = slug
|
|
}
|
|
if fallbackInternalOrgID == nil, let internalOrgID {
|
|
fallbackInternalOrgID = internalOrgID
|
|
}
|
|
}
|
|
|
|
if let override, fallbackInternalOrgID != nil {
|
|
return (override, fallbackInternalOrgID)
|
|
}
|
|
|
|
if let fallbackSlug {
|
|
return ("org/\(fallbackSlug)", fallbackInternalOrgID)
|
|
}
|
|
if let fallbackInternalOrgID {
|
|
return ("organizations/\(fallbackInternalOrgID)", fallbackInternalOrgID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private static func externalOrgSlug(from key: String) -> String {
|
|
guard let range = key.range(of: self.externalOrgPrefix) else { return key }
|
|
return String(key[range.upperBound...])
|
|
}
|
|
|
|
private static func cleanedOrgID(_ raw: String) -> String? {
|
|
let value = self.decodedStorageValue(raw)
|
|
guard DevinUsageFetcher.isInternalOrganizationID(value) else { return nil }
|
|
return value
|
|
}
|
|
|
|
private static func cleanedOrgID(_ raw: String?) -> String? {
|
|
guard let raw else { return nil }
|
|
return self.cleanedOrgID(raw)
|
|
}
|
|
|
|
private static func cleanedSlug(_ raw: String?) -> String? {
|
|
guard let raw else { return nil }
|
|
let value = self.decodedStorageValue(raw)
|
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !value.isEmpty, value != "null", !DevinUsageFetcher.isInternalOrganizationID(value) else {
|
|
return nil
|
|
}
|
|
if value.hasPrefix("org/") {
|
|
return String(value.dropFirst(4))
|
|
}
|
|
return value
|
|
}
|
|
|
|
private static func slugFromPostAuthKey(_ key: String) -> String? {
|
|
guard let range = key.range(of: "-org_name-") else { return nil }
|
|
return String(key[range.upperBound...])
|
|
}
|
|
|
|
private static func internalOrgIDFromStorageKey(_ key: String) -> String? {
|
|
guard let range = key.range(of: #"org[-_][A-Za-z0-9]{8,}"#, options: .regularExpression) else {
|
|
return nil
|
|
}
|
|
return self.cleanedOrgID(String(key[range]))
|
|
}
|
|
|
|
private static func firstString(in object: Any?, matching keys: Set<String>) -> String? {
|
|
if let dictionary = object as? [String: Any] {
|
|
for (key, value) in dictionary {
|
|
if keys.contains(key), let string = value as? String, !string.isEmpty {
|
|
return string
|
|
}
|
|
if let found = self.firstString(in: value, matching: keys) {
|
|
return found
|
|
}
|
|
}
|
|
}
|
|
|
|
if let array = object as? [Any] {
|
|
for value in array {
|
|
if let found = self.firstString(in: value, matching: keys) {
|
|
return found
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private static func slug(fromNormalizedOrganization organization: String) -> String? {
|
|
guard organization.hasPrefix("org/") else { return nil }
|
|
return String(organization.dropFirst(4))
|
|
}
|
|
|
|
private static func orgID(fromNormalizedOrganization organization: String) -> String? {
|
|
guard organization.hasPrefix("organizations/") else { return nil }
|
|
return String(organization.dropFirst("organizations/".count))
|
|
}
|
|
}
|
|
#endif
|