import CodexBarCoreimport CryptoKitimport 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() }}
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()
}
}