140 lines
5.0 KiB
Swift
140 lines
5.0 KiB
Swift
import CodexBarCore
|
|
import CryptoKit
|
|
import Foundation
|
|
|
|
enum CodexHistoryPersistedOwner: Equatable {
|
|
case canonical(String)
|
|
case legacyEmailHash(String)
|
|
case legacyOpaqueScoped(String)
|
|
case legacyUnscoped
|
|
}
|
|
|
|
enum CodexHistoryOwnership {
|
|
private static let providerAccountPrefix = "codex:v1:provider-account:"
|
|
private static let emailHashPrefix = "codex:v1:email-hash:"
|
|
|
|
static func canonicalKey(for identity: CodexIdentity) -> String? {
|
|
switch identity {
|
|
case let .providerAccount(id):
|
|
guard let normalized = Self.normalizeScopedValue(id) else { return nil }
|
|
return "\(Self.providerAccountPrefix)\(normalized)"
|
|
case let .emailOnly(normalizedEmail):
|
|
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return nil }
|
|
return self.canonicalEmailHashKey(for: normalized)
|
|
case .unresolved:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func canonicalEmailHashKey(for normalizedEmail: String) -> String {
|
|
"\(self.emailHashPrefix)\(self.legacyEmailHash(normalizedEmail: normalizedEmail))"
|
|
}
|
|
|
|
static func legacyEmailHash(normalizedEmail: String) -> String {
|
|
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return "" }
|
|
return self.sha256Hex(normalized)
|
|
}
|
|
|
|
static func classifyPersistedKey(
|
|
_ rawKey: String?,
|
|
legacyEmailHash: String? = nil) -> CodexHistoryPersistedOwner
|
|
{
|
|
guard let normalizedKey = normalizeScopedValue(rawKey) else {
|
|
return .legacyUnscoped
|
|
}
|
|
if self.isCanonicalKey(normalizedKey) {
|
|
return .canonical(normalizedKey)
|
|
}
|
|
if let legacyEmailHash, normalizedKey == legacyEmailHash {
|
|
return .legacyEmailHash(normalizedKey)
|
|
}
|
|
return .legacyOpaqueScoped(normalizedKey)
|
|
}
|
|
|
|
static func belongsToTargetContinuity(
|
|
_ owner: CodexHistoryPersistedOwner,
|
|
targetCanonicalKey: String,
|
|
canonicalEmailHashKey: String?) -> Bool
|
|
{
|
|
switch owner {
|
|
case let .canonical(key):
|
|
if key == targetCanonicalKey {
|
|
return true
|
|
}
|
|
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
|
|
return false
|
|
}
|
|
return key == canonicalEmailHashKey
|
|
case .legacyEmailHash:
|
|
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
|
|
return false
|
|
}
|
|
return canonicalEmailHashKey == targetCanonicalKey ||
|
|
targetCanonicalKey.hasPrefix(self.providerAccountPrefix)
|
|
case .legacyOpaqueScoped, .legacyUnscoped:
|
|
return false
|
|
}
|
|
}
|
|
|
|
static func hasStrictSingleAccountContinuity(
|
|
scopedRawKeys: [String],
|
|
targetCanonicalKey: String,
|
|
canonicalEmailHashKey: String?,
|
|
legacyEmailHash: String?,
|
|
hasAdjacentMultiAccountVeto: Bool) -> Bool
|
|
{
|
|
guard !hasAdjacentMultiAccountVeto else { return false }
|
|
|
|
let normalizedCandidates = Set(scopedRawKeys.compactMap { rawKey in
|
|
let owner = self.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash)
|
|
if self.belongsToTargetContinuity(
|
|
owner,
|
|
targetCanonicalKey: targetCanonicalKey,
|
|
canonicalEmailHashKey: canonicalEmailHashKey)
|
|
{
|
|
return targetCanonicalKey
|
|
}
|
|
|
|
switch owner {
|
|
case .legacyUnscoped:
|
|
return nil
|
|
case let .legacyOpaqueScoped(key):
|
|
return "legacy-opaque:\(key)"
|
|
case let .legacyEmailHash(hash):
|
|
return "legacy-email-hash:\(hash)"
|
|
case let .canonical(key):
|
|
return key
|
|
}
|
|
})
|
|
|
|
guard normalizedCandidates.count == 1, normalizedCandidates.first == targetCanonicalKey else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static func isCanonicalKey(_ rawKey: String) -> Bool {
|
|
self.isCanonicalProviderAccountKey(rawKey) || self.isCanonicalEmailHashKey(rawKey)
|
|
}
|
|
|
|
static func isCanonicalProviderAccountKey(_ rawKey: String) -> Bool {
|
|
rawKey.hasPrefix(self.providerAccountPrefix) && rawKey.count > self.providerAccountPrefix.count
|
|
}
|
|
|
|
private static func isCanonicalEmailHashKey(_ rawKey: String) -> Bool {
|
|
rawKey.hasPrefix(self.emailHashPrefix) && rawKey.count > self.emailHashPrefix.count
|
|
}
|
|
|
|
private static func normalizeScopedValue(_ value: String?) -> String? {
|
|
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
|
|
return nil
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
private static func sha256Hex(_ input: String) -> String {
|
|
let digest = SHA256.hash(data: Data(input.utf8))
|
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
}
|