field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

TeraRuntimeModels.swift (27686B)


      1 import Foundation
      2 
      3 enum TeraProtectedDataState: Sendable, Equatable {
      4   case available
      5   case unavailable
      6 }
      7 
      8 enum TeraRuntimeNetworkProfile: Sendable, Equatable {
      9   case publicNetwork
     10   case simulator
     11   case device
     12 }
     13 
     14 enum TeraBlossomHostKind: String, Codable, Sendable, Equatable {
     15   case native
     16   case simulator
     17   case physicalDevice = "physical_device"
     18 }
     19 
     20 enum TeraBlossomEndpointAuthority: String, Codable, Sendable, Equatable {
     21   case publicWebPKI = "public_web_pki"
     22   case loopbackDevelopment = "loopback_development"
     23   case privateNetworkDevelopment = "private_network_development"
     24 
     25   init?(runtimeValue: String) {
     26     switch runtimeValue {
     27     case "public_webpki": self = .publicWebPKI
     28     case "loopback_development": self = .loopbackDevelopment
     29     case "private_network_development": self = .privateNetworkDevelopment
     30     default: return nil
     31     }
     32   }
     33 }
     34 
     35 struct TeraBlossomEndpointConfiguration: Codable, Sendable, Equatable {
     36   let hostKind: TeraBlossomHostKind
     37   let endpointAuthority: TeraBlossomEndpointAuthority
     38   let primaryOrigin: String
     39   let fallbackOrigins: [String]
     40 }
     41 
     42 struct TeraBlossomConfigurationStatus: Sendable, Equatable {
     43   let schemaVersion: UInt16
     44   let hostKind: String
     45   let endpointAuthority: String
     46   let primaryOrigin: String
     47   let fallbackOrigins: [String]
     48   let configFingerprint: String
     49 }
     50 
     51 struct TeraBlossomEvidence: Sendable, Equatable {
     52   let schemaVersion: UInt16
     53   let origin: String
     54   let configFingerprint: String
     55   let state: String
     56   let lastSuccessfulState: String
     57   let transportSecurity: String
     58   let observedAtUnixMilliseconds: UInt64?
     59   let httpStatus: UInt16?
     60   let errorCode: String?
     61   let serverErrorCode: String?
     62   let errorPhase: String?
     63   let retryable: Bool
     64   let possibleOrphan: Bool
     65   let attempts: UInt8
     66 }
     67 
     68 struct TeraRuntimeAppMetadata: Sendable, Equatable {
     69   let bundleIdentifier: String
     70   let version: String
     71   let buildNumber: String
     72   let buildSHA: String?
     73 }
     74 
     75 enum TeraRuntimeSignerAvailability: Sendable, Equatable {
     76   case ready
     77   case busy
     78   case locked
     79   case unavailable
     80 }
     81 
     82 enum TeraRuntimeSigningPurpose: Sendable, Equatable {
     83   case nostrEvent
     84   case blossomUpload
     85 }
     86 
     87 struct TeraRuntimeSigningRequest: Sendable, Equatable {
     88   let operationID: String
     89   let signerRequestID: String
     90   let publicKeyHex: String
     91   let purpose: TeraRuntimeSigningPurpose
     92   let deadlineUnixMilliseconds: UInt64
     93   let digest: Data
     94 }
     95 
     96 enum TeraRuntimeSigningOutcome: Sendable, Equatable {
     97   case signed(signatureHex: String)
     98   case locked
     99   case cancelled
    100   case rejected
    101   case timedOut
    102   case unavailable
    103   case invalidated
    104   case failed
    105 }
    106 
    107 protocol TeraRuntimeSigner: Sendable {
    108   func availability() async -> TeraRuntimeSignerAvailability
    109   func sign(_ request: TeraRuntimeSigningRequest) async -> TeraRuntimeSigningOutcome
    110 }
    111 
    112 struct TeraRuntimeLaunchConfiguration: Sendable {
    113   let applicationSupportDirectory: String
    114   let publicKeyHex: String
    115   let sourceGenerationHex: String
    116   let sourceGenerationCreatedAtUnixMilliseconds: UInt64
    117   let protectedData: TeraProtectedDataState
    118   let networkProfile: TeraRuntimeNetworkProfile
    119   let writableRelays: [String]
    120   let blossom: TeraBlossomEndpointConfiguration?
    121   let app: TeraRuntimeAppMetadata
    122   let signerGeneration: String
    123   let signer: any TeraRuntimeSigner
    124   let adoptBootstrapSettings: Bool
    125 }
    126 
    127 extension TeraRuntimeLaunchConfiguration: Equatable {
    128   static func == (lhs: Self, rhs: Self) -> Bool {
    129     lhs.applicationSupportDirectory == rhs.applicationSupportDirectory
    130       && lhs.publicKeyHex == rhs.publicKeyHex
    131       && lhs.sourceGenerationHex == rhs.sourceGenerationHex
    132       && lhs.sourceGenerationCreatedAtUnixMilliseconds
    133         == rhs.sourceGenerationCreatedAtUnixMilliseconds
    134       && lhs.protectedData == rhs.protectedData
    135       && lhs.networkProfile == rhs.networkProfile
    136       && lhs.writableRelays == rhs.writableRelays
    137       && lhs.blossom == rhs.blossom
    138       && lhs.app == rhs.app
    139       && lhs.signerGeneration == rhs.signerGeneration
    140       && lhs.adoptBootstrapSettings == rhs.adoptBootstrapSettings
    141   }
    142 }
    143 
    144 struct TeraRuntimeIdentity: Sendable, Equatable {
    145   let publicKeyHex: String
    146   let hostSignerConfigured: Bool
    147 }
    148 
    149 enum TeraRelayAccess: Sendable, Equatable {
    150   case readOnly
    151   case readWrite
    152 
    153   var label: String {
    154     self == .readWrite ? "read write" : "read only"
    155   }
    156 }
    157 
    158 struct TeraRelayEndpointStatus: Sendable, Equatable {
    159   let url: String
    160   let access: TeraRelayAccess
    161   let readState: String
    162   let writeState: String
    163   let readLastAttemptUnixMilliseconds: UInt64?
    164   let writeLastAttemptUnixMilliseconds: UInt64?
    165   let readNextAttemptUnixMilliseconds: UInt64?
    166   let writeNextAttemptUnixMilliseconds: UInt64?
    167 }
    168 
    169 struct TeraRelayStatus: Sendable, Equatable {
    170   let profile: String
    171   let state: String
    172   let readAvailability: String
    173   let writeAvailability: String
    174   let relays: [TeraRelayEndpointStatus]
    175 }
    176 
    177 enum TeraSettingsIdentityLockState: Sendable, Equatable {
    178   case locked
    179   case unlocked
    180 }
    181 
    182 struct TeraSettingsIdentity: Sendable, Equatable, Identifiable {
    183   let id: String
    184   let publicKeyHex: String
    185 }
    186 
    187 struct TeraSettingsIdentityState: Sendable, Equatable {
    188   let identities: [TeraSettingsIdentity]
    189   let activeIdentityID: String?
    190   let lockState: TeraSettingsIdentityLockState
    191   let pendingImportOperationID: String?
    192 }
    193 
    194 enum TeraSettingsNetworkEnvironment: String, Sendable, Equatable, CaseIterable, Identifiable {
    195   case publicNetwork
    196   case simulator
    197   case physicalDevice
    198 
    199   var id: String {
    200     rawValue
    201   }
    202 }
    203 
    204 enum TeraRelayAccessPreference: String, Sendable, Equatable, CaseIterable, Identifiable {
    205   case readOnly
    206   case readWrite
    207 
    208   var id: String {
    209     rawValue
    210   }
    211 
    212   var label: String {
    213     self == .readWrite ? "Read and write" : "Read only"
    214   }
    215 }
    216 
    217 struct TeraRelayPreference: Sendable, Equatable, Identifiable {
    218   let id: UUID
    219   var url: String
    220   var access: TeraRelayAccessPreference
    221 
    222   init(id: UUID = UUID(), url: String, access: TeraRelayAccessPreference) {
    223     self.id = id
    224     self.url = url
    225     self.access = access
    226   }
    227 }
    228 
    229 enum TeraBlossomAuthorityPreference: String, Sendable, Equatable, CaseIterable, Identifiable {
    230   case publicWebPKI
    231   case loopbackDevelopment
    232   case privateNetworkDevelopment
    233 
    234   var id: String {
    235     rawValue
    236   }
    237 }
    238 
    239 struct TeraMobileSettings: Sendable, Equatable {
    240   let revision: UInt64
    241   let identity: TeraSettingsIdentityState
    242   var networkEnvironment: TeraSettingsNetworkEnvironment
    243   var relays: [TeraRelayPreference]
    244   var blossomAuthority: TeraBlossomAuthorityPreference
    245   var blossomPrimaryOrigin: String
    246   var blossomFallbackOrigins: [String]
    247   var allowCellularDownloads: Bool
    248   var allowCellularUploads: Bool
    249   var allowBackgroundTransfers: Bool
    250   var mediaCacheBytes: UInt64
    251   var mediaCacheArtifacts: UInt32
    252 }
    253 
    254 struct TeraReplaceSettings: Sendable, Equatable {
    255   let expectedRevision: UInt64
    256   let networkEnvironment: TeraSettingsNetworkEnvironment
    257   let relays: [TeraRelayPreference]
    258   let blossomAuthority: TeraBlossomAuthorityPreference
    259   let blossomPrimaryOrigin: String
    260   let blossomFallbackOrigins: [String]
    261   let allowCellularDownloads: Bool
    262   let allowCellularUploads: Bool
    263   let allowBackgroundTransfers: Bool
    264   let mediaCacheBytes: UInt64
    265   let mediaCacheArtifacts: UInt32
    266 }
    267 
    268 struct TeraSettingsTransition: Sendable, Equatable {
    269   let settings: TeraMobileSettings
    270   let runtimeRestartRequired: Bool
    271   let outboxRequeueRequired: Bool
    272   let mediaCacheInvalidationRequired: Bool
    273 }
    274 
    275 enum TeraIdentityCommandKind: Sendable, Equatable {
    276   case beginImport
    277   case completeImport
    278   case cancelImport
    279   case select
    280   case lock
    281   case unlock
    282   case recover
    283 }
    284 
    285 struct TeraIdentityCommand: Sendable, Equatable {
    286   let kind: TeraIdentityCommandKind
    287   let operationID: String?
    288   let identityID: String?
    289   let publicKeyHex: String?
    290 }
    291 
    292 struct TeraProfileMetadataInput: Sendable, Equatable {
    293   var name: String
    294   var displayName: String?
    295   var about: String?
    296   var picture: TeraPreparedMediaHandle?
    297   var banner: TeraPreparedMediaHandle?
    298   var nip05: String?
    299   var bot: Bool?
    300 }
    301 
    302 struct TeraProfileStatus: Sendable, Equatable, Identifiable {
    303   let id: String
    304   let revision: UInt64
    305   let authorPublicKey: String
    306   let state: TeraOutboxState
    307   let deliveryID: String?
    308   let createdAtUnixMilliseconds: UInt64
    309   let updatedAtUnixMilliseconds: UInt64
    310   let settlement: TeraOperationSettlement?
    311 
    312   var honestSummary: String {
    313     state.summary(settlement: settlement)
    314   }
    315 }
    316 
    317 struct TeraRuntimeSnapshot: Sendable, Equatable {
    318   let identity: TeraRuntimeIdentity
    319   let relay: TeraRelayStatus?
    320   let blossomConfiguration: TeraBlossomConfigurationStatus?
    321   let blossomEvidence: TeraBlossomEvidence?
    322   let crateName: String
    323   let crateVersion: String
    324   let isClosed: Bool
    325 }
    326 
    327 struct TeraRuntimeShutdownReceipt: Sendable, Equatable {
    328   let state: String
    329   let alreadyClosed: Bool
    330 
    331   static let alreadyStopped = Self(state: "closed", alreadyClosed: true)
    332 }
    333 
    334 enum TeraRuntimeOperationKind: String, Sendable, Equatable, Hashable {
    335   case startup
    336   case operation
    337   case reconfiguration
    338   case subscription
    339   case shutdown
    340 }
    341 
    342 struct TeraRuntimeDeadlinePolicy: Sendable, Equatable {
    343   let startupNanoseconds: UInt64
    344   let operationNanoseconds: UInt64
    345   let subscriptionNanoseconds: UInt64
    346   let shutdownNanoseconds: UInt64
    347 
    348   init(
    349     startupNanoseconds: UInt64 = 30_000_000_000,
    350     operationNanoseconds: UInt64 = 30_000_000_000,
    351     subscriptionNanoseconds: UInt64 = 10_000_000_000,
    352     shutdownNanoseconds: UInt64 = 10_000_000_000
    353   ) {
    354     precondition(startupNanoseconds > 0 && startupNanoseconds <= 120_000_000_000)
    355     precondition(operationNanoseconds > 0 && operationNanoseconds <= 120_000_000_000)
    356     precondition(subscriptionNanoseconds > 0 && subscriptionNanoseconds <= 60_000_000_000)
    357     precondition(shutdownNanoseconds > 0 && shutdownNanoseconds <= 60_000_000_000)
    358     self.startupNanoseconds = startupNanoseconds
    359     self.operationNanoseconds = operationNanoseconds
    360     self.subscriptionNanoseconds = subscriptionNanoseconds
    361     self.shutdownNanoseconds = shutdownNanoseconds
    362   }
    363 
    364   static let production = Self()
    365 }
    366 
    367 enum TeraRuntimeObservationState: Sendable, Equatable {
    368   case inactive
    369   case subscribing(attempt: UInt32)
    370   case active
    371   case retrying(attempt: UInt32, message: String)
    372   case stopped
    373 }
    374 
    375 enum TeraRuntimeObservationBackoff {
    376   static func sleep(attempt: UInt32) async throws {
    377     let exponent = min(attempt.saturatingSubtractingOne, 7)
    378     let delay = min(UInt64(250_000_000) << exponent, 30_000_000_000)
    379     try await Task.sleep(nanoseconds: delay)
    380   }
    381 }
    382 
    383 extension UInt32 {
    384   fileprivate var saturatingSubtractingOne: UInt32 {
    385     self == 0 ? 0 : self - 1
    386   }
    387 }
    388 
    389 struct TeraRuntimeFailure: Error, Sendable, Equatable {
    390   let schemaVersion: UInt16
    391   let code: String
    392   let category: String
    393   let retryable: Bool
    394   let recoveryActions: [String]
    395   let operationID: String?
    396   let capabilityID: String?
    397   let safeMessage: String
    398 
    399   static func local(operation: String, code: String, safeMessage: String) -> Self {
    400     Self(
    401       schemaVersion: 1,
    402       code: code,
    403       category: "runtime",
    404       retryable: false,
    405       recoveryActions: [],
    406       operationID: operation,
    407       capabilityID: nil,
    408       safeMessage: safeMessage
    409     )
    410   }
    411 }
    412 
    413 extension TeraRuntimeFailure: LocalizedError {
    414   var errorDescription: String? {
    415     TeraUserMessages.text(.runtimeOperationFailed)
    416   }
    417 }
    418 
    419 enum TeraRuntimeClientError: Error, Sendable, Equatable {
    420   case invalidBufferCapacity
    421   case notRunning
    422   case superseded
    423   case startup(TeraRuntimeFailure)
    424   case subscription(TeraRuntimeFailure)
    425   case status(TeraRuntimeFailure)
    426   case today(TeraRuntimeFailure)
    427   case add(TeraRuntimeFailure)
    428   case support(TeraRuntimeFailure)
    429   case shutdown(TeraRuntimeFailure)
    430 }
    431 
    432 extension TeraRuntimeClientError: LocalizedError {
    433   var errorDescription: String? {
    434     TeraUserMessages.text(for: self, fallback: .runtimeOperationFailed)
    435   }
    436 }
    437 
    438 struct TeraLocalNetwork: Sendable, Equatable, Hashable, Identifiable {
    439   let schemaVersion: UInt16
    440   let id: String
    441   let label: String
    442   let relayURLs: [String]
    443   let locality: String?
    444   let followedAuthors: [String]
    445   let generation: UInt64
    446 
    447   static func defaultContext(snapshot: TeraRuntimeSnapshot) -> Self {
    448     Self(
    449       schemaVersion: 1,
    450       id: "default",
    451       label: "Local network",
    452       relayURLs: snapshot.relay?.relays.map(\.url) ?? [],
    453       locality: nil,
    454       followedAuthors: [],
    455       generation: 1
    456     )
    457   }
    458 }
    459 
    460 enum TeraTodayCardType: String, CaseIterable, Sendable, Equatable, Hashable {
    461   case update
    462   case photoUpdate
    463   case ask
    464   case event
    465   case foodAvailability
    466 
    467   var label: String {
    468     switch self {
    469     case .update: "Update"
    470     case .photoUpdate: "Photo update"
    471     case .ask: "Ask"
    472     case .event: "Event"
    473     case .foodAvailability: "Food availability"
    474     }
    475   }
    476 
    477   var addCommandType: TeraAddCommandType {
    478     switch self {
    479     case .update: .createUpdate
    480     case .photoUpdate: .createPhotoUpdate
    481     case .ask: .createAsk
    482     case .event: .createEvent
    483     case .foodAvailability: .createFoodAvailability
    484     }
    485   }
    486 }
    487 
    488 enum TeraMediaVerificationState: String, Sendable, Equatable, Hashable {
    489   case pending
    490   case verified
    491   case failed
    492   case unavailable
    493 }
    494 
    495 struct TeraMediaReference: Sendable, Equatable, Hashable, Identifiable {
    496   let referenceFingerprint: String
    497   let url: String
    498   let sha256: String?
    499   let mediaType: String?
    500   let width: UInt32?
    501   let height: UInt32?
    502   let byteSize: UInt64?
    503   let alt: String?
    504   let verification: TeraMediaVerificationState
    505 
    506   var id: String {
    507     referenceFingerprint
    508   }
    509 
    510   var verifiedArtifactID: String? {
    511     guard verification == .verified,
    512       let sha256,
    513       sha256.count == 64,
    514       sha256.allSatisfy({ $0.isHexDigit && !$0.isUppercase })
    515     else { return nil }
    516     return sha256
    517   }
    518 }
    519 
    520 struct TeraVerifiedMediaArtifact: Sendable, Equatable {
    521   let artifactID: String
    522   let bytes: Data
    523   let byteSize: UInt64
    524   let mediaType: String
    525   let width: UInt32
    526   let height: UInt32
    527 
    528   init?(
    529     artifactID: String,
    530     bytes: Data,
    531     byteSize: UInt64,
    532     mediaType: String,
    533     width: UInt32,
    534     height: UInt32
    535   ) {
    536     guard artifactID.count == 64,
    537       artifactID.allSatisfy({ $0.isHexDigit && !$0.isUppercase }),
    538       !bytes.isEmpty,
    539       UInt64(bytes.count) == byteSize,
    540       ["image/gif", "image/jpeg", "image/png", "image/webp"].contains(mediaType),
    541       width > 0,
    542       height > 0
    543     else { return nil }
    544     self.artifactID = artifactID
    545     self.bytes = bytes
    546     self.byteSize = byteSize
    547     self.mediaType = mediaType
    548     self.width = width
    549     self.height = height
    550   }
    551 }
    552 
    553 struct TeraProfileSummary: Sendable, Equatable, Hashable {
    554   let authorPublicKey: String
    555   let name: String?
    556   let displayName: String?
    557   let about: String?
    558   let picture: TeraMediaReference?
    559   let banner: TeraMediaReference?
    560   let nip05: String?
    561   let website: String?
    562   let lightningAddress: String?
    563 
    564   var preferredName: String {
    565     for candidate in [displayName, name] {
    566       if let candidate, !candidate.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
    567         return candidate
    568       }
    569     }
    570     guard authorPublicKey.count > 16 else { return authorPublicKey }
    571     return "\(authorPublicKey.prefix(8))…\(authorPublicKey.suffix(8))"
    572   }
    573 }
    574 
    575 enum TeraThreadEntryType: String, Sendable, Equatable, Hashable {
    576   case profile
    577   case reply
    578   case comment
    579   case deletion
    580 }
    581 
    582 struct TeraThreadEntry: Sendable, Equatable, Hashable, Identifiable {
    583   let id: String
    584   let authorPublicKey: String
    585   let content: String
    586   let authoredAtUnixSeconds: UInt64
    587   let type: TeraThreadEntryType
    588   let root: String
    589   let parentEventID: String
    590   let authorProfile: TeraProfileSummary?
    591 }
    592 
    593 enum TeraCardLifecycleState: String, Sendable, Equatable, Hashable {
    594   case active
    595   case sold
    596   case past
    597 }
    598 
    599 struct TeraTodayCard: Sendable, Equatable, Hashable, Identifiable {
    600   let id: String
    601   let type: TeraTodayCardType
    602   let sourceEventID: String
    603   let sourceAddress: String?
    604   let authorPublicKey: String
    605   let contractID: String
    606   let title: String?
    607   let content: String
    608   let authoredAtUnixSeconds: UInt64
    609   let effectiveAtUnixSeconds: UInt64
    610   let calendarTiming: TeraCalendarTiming?
    611   let location: String?
    612   let priceAmount: String?
    613   let priceCurrency: String?
    614   let priceUnit: String?
    615   let quantity: String?
    616   let foodSummary: String?
    617   let foodPublishedAtUnixSeconds: UInt64?
    618   let foodStatus: String?
    619   let contextRank: UInt8
    620   let inclusionReason: String
    621   let media: [TeraMediaReference]
    622   let lifecycle: TeraCardLifecycleState
    623   let rankDigest: String?
    624   let authorProfile: TeraProfileSummary?
    625   let thread: [TeraThreadEntry]
    626   var localSourceDraftID: String?
    627   let localOperationID: String?
    628   let localOperationState: String?
    629 
    630   var authorName: String {
    631     authorProfile?.preferredName
    632       ?? TeraProfileSummary(
    633         authorPublicKey: authorPublicKey,
    634         name: nil,
    635         displayName: nil,
    636         about: nil,
    637         picture: nil,
    638         banner: nil,
    639         nip05: nil,
    640         website: nil,
    641         lightningAddress: nil
    642       ).preferredName
    643   }
    644 
    645   var priceSummary: String? {
    646     guard let priceAmount, let priceCurrency, let priceUnit else { return nil }
    647     return "\(priceAmount) \(priceCurrency)/\(priceUnit)"
    648   }
    649 
    650   var retractionTargetKind: UInt32? {
    651     switch type {
    652     case .update, .photoUpdate, .ask:
    653       guard sourceAddress == nil else { return nil }
    654       return 1
    655     case .event:
    656       guard let kind = sourceAddressKind, kind == 31922 || kind == 31923 else { return nil }
    657       return kind
    658     case .foodAvailability:
    659       guard sourceAddressKind == 30402 else { return nil }
    660       return 30402
    661     }
    662   }
    663 
    664   private var sourceAddressKind: UInt32? {
    665     guard let sourceAddress,
    666       let rawKind = sourceAddress.split(separator: ":", maxSplits: 1).first,
    667       let kind = UInt32(rawKind)
    668     else {
    669       return nil
    670     }
    671     return kind
    672   }
    673 }
    674 
    675 enum TeraTodayProjectionUpdate: Sendable, Equatable {
    676   case incremental
    677   case rebuild
    678 }
    679 
    680 enum TeraSearchResultType: Sendable, Equatable, Hashable {
    681   case card
    682   case profile
    683 }
    684 
    685 struct TeraSearchResult: Sendable, Equatable, Hashable, Identifiable {
    686   let type: TeraSearchResultType
    687   let id: String
    688   let card: TeraTodayCard?
    689   let profile: TeraProfileSummary?
    690 }
    691 
    692 struct TeraMeSnapshot: Sendable, Equatable {
    693   let publicKey: String
    694   let profile: TeraProfileSummary?
    695   let cards: [TeraTodayCard]
    696 }
    697 
    698 enum TeraAddCommandType: String, CaseIterable, Sendable, Equatable, Hashable, Identifiable {
    699   case createUpdate
    700   case createPhotoUpdate
    701   case createAsk
    702   case createEvent
    703   case createFoodAvailability
    704 
    705   var id: String {
    706     rawValue
    707   }
    708 
    709   var label: String {
    710     switch self {
    711     case .createUpdate: "Update"
    712     case .createPhotoUpdate: "Photo update"
    713     case .createAsk: "Ask"
    714     case .createEvent: "Event"
    715     case .createFoodAvailability: "Food availability"
    716     }
    717   }
    718 
    719   var todayCardType: TeraTodayCardType {
    720     switch self {
    721     case .createUpdate: .update
    722     case .createPhotoUpdate: .photoUpdate
    723     case .createAsk: .ask
    724     case .createEvent: .event
    725     case .createFoodAvailability: .foodAvailability
    726     }
    727   }
    728 }
    729 
    730 enum TeraAddFieldKind: Sendable, Equatable, Hashable {
    731   case text
    732   case multilineText
    733   case date
    734   case dateTime
    735   case decimal
    736   case choice
    737   case location
    738   case media
    739 }
    740 
    741 struct TeraAddField: Sendable, Equatable, Hashable, Identifiable {
    742   let schemaVersion: UInt16
    743   let id: String
    744   let label: String
    745   let kind: TeraAddFieldKind
    746   let required: Bool
    747   let choices: [String]
    748   let maxBytes: UInt64?
    749   let maxItems: UInt16?
    750 }
    751 
    752 struct TeraAddSchema: Sendable, Equatable, Hashable, Identifiable {
    753   let schemaVersion: UInt16
    754   let commandType: TeraAddCommandType
    755   let label: String
    756   let fields: [TeraAddField]
    757 
    758   var id: TeraAddCommandType {
    759     commandType
    760   }
    761 }
    762 
    763 enum TeraProductSurfaceContractError: LocalizedError, Sendable, Equatable {
    764   case invalidAddSchemaInventory
    765 
    766   var errorDescription: String? {
    767     "The Add product contract does not match this app."
    768   }
    769 }
    770 
    771 enum TeraProductSurfaceContract {
    772   private struct MediaProfile: Equatable {
    773     let required: Bool
    774     let maximum: UInt16
    775   }
    776 
    777   static let supportingSurfaceIDs = ["context_picker", "search", "me", "settings"]
    778 
    779   static var snapshot: String {
    780     let cards = TeraTodayCardType.allCases.map(\.rawValue).joined(separator: ",")
    781     let commands = TeraAddCommandType.allCases.map(\.rawValue).joined(separator: ",")
    782     let supporting = supportingSurfaceIDs.joined(separator: ",")
    783     return "today=\(cards)|add=\(commands)|support=\(supporting)"
    784   }
    785 
    786   static func validate(schemas: [TeraAddSchema]) throws -> [TeraAddSchema] {
    787     let expectedCommands = TeraAddCommandType.allCases
    788     guard schemas.map(\.commandType) == expectedCommands,
    789       schemas.allSatisfy({ $0.schemaVersion == 1 && $0.label == $0.commandType.label }),
    790       schemas.allSatisfy({ schema in
    791         schema.fields.allSatisfy { $0.schemaVersion == 1 }
    792           && Set(schema.fields.map(\.id)).count == schema.fields.count
    793       }),
    794       schemas.allSatisfy({ $0.fields.map(\.id) == expectedFieldIDs(for: $0.commandType) }),
    795       schemas.allSatisfy({ $0.fields.map(\.kind) == expectedFieldKinds(for: $0.commandType) }),
    796       schemas.allSatisfy({
    797         requiredFieldIDs(in: $0) == expectedRequiredFieldIDs(for: $0.commandType)
    798       }),
    799       schemas.allSatisfy({ mediaProfile(in: $0) == expectedMediaProfile(for: $0.commandType) }),
    800       schemas.first(where: { $0.commandType == .createFoodAvailability })?
    801         .fields.first(where: { $0.id == "unit" })?.choices
    802         == ["g", "kg", "lb", "oz", "each", "dozen", "bunch", "punnet", "bag", "basket"]
    803     else {
    804       throw TeraProductSurfaceContractError.invalidAddSchemaInventory
    805     }
    806     return schemas
    807   }
    808 
    809   private static func expectedFieldIDs(for command: TeraAddCommandType) -> [String] {
    810     switch command {
    811     case .createUpdate:
    812       ["content"]
    813     case .createPhotoUpdate, .createAsk:
    814       ["content", "media"]
    815     case .createEvent:
    816       ["identifier", "title", "content", "event_start", "event_end", "location", "media"]
    817     case .createFoodAvailability:
    818       [
    819         "identifier", "title", "summary", "content", "location", "price_amount", "currency",
    820         "unit", "quantity", "media",
    821       ]
    822     }
    823   }
    824 
    825   private static func expectedRequiredFieldIDs(for command: TeraAddCommandType) -> Set<String> {
    826     switch command {
    827     case .createUpdate:
    828       ["content"]
    829     case .createPhotoUpdate:
    830       ["content", "media"]
    831     case .createAsk:
    832       ["content"]
    833     case .createEvent:
    834       ["identifier", "title", "event_start"]
    835     case .createFoodAvailability:
    836       ["identifier", "title", "summary", "content", "location", "price_amount", "currency", "unit"]
    837     }
    838   }
    839 
    840   private static func expectedFieldKinds(
    841     for command: TeraAddCommandType
    842   ) -> [TeraAddFieldKind] {
    843     switch command {
    844     case .createUpdate:
    845       [.multilineText]
    846     case .createPhotoUpdate, .createAsk:
    847       [.multilineText, .media]
    848     case .createEvent:
    849       [.text, .text, .multilineText, .dateTime, .dateTime, .location, .media]
    850     case .createFoodAvailability:
    851       [
    852         .text, .text, .text, .multilineText, .location, .decimal, .choice, .choice, .decimal,
    853         .media,
    854       ]
    855     }
    856   }
    857 
    858   private static func requiredFieldIDs(in schema: TeraAddSchema) -> Set<String> {
    859     Set(schema.fields.lazy.filter(\.required).map(\.id))
    860   }
    861 
    862   private static func expectedMediaProfile(
    863     for command: TeraAddCommandType
    864   ) -> MediaProfile? {
    865     switch command {
    866     case .createUpdate:
    867       nil
    868     case .createPhotoUpdate:
    869       MediaProfile(required: true, maximum: 20)
    870     case .createAsk:
    871       MediaProfile(required: false, maximum: 20)
    872     case .createEvent:
    873       MediaProfile(required: false, maximum: 1)
    874     case .createFoodAvailability:
    875       MediaProfile(required: false, maximum: 20)
    876     }
    877   }
    878 
    879   private static func mediaProfile(
    880     in schema: TeraAddSchema
    881   ) -> MediaProfile? {
    882     let fields = schema.fields.filter { $0.kind == .media }
    883     guard fields.count == 1, let field = fields.first, let maximum = field.maxItems else {
    884       return nil
    885     }
    886     return MediaProfile(required: field.required, maximum: maximum)
    887   }
    888 }
    889 
    890 enum TeraEventTiming: String, Sendable, Equatable, Hashable, CaseIterable, Identifiable {
    891   case allDay
    892   case timed
    893 
    894   var id: String {
    895     rawValue
    896   }
    897 
    898   var label: String {
    899     self == .allDay ? "All day" : "Specific time"
    900   }
    901 }
    902 
    903 struct TeraPreparedMedia: Sendable, Equatable, Hashable, Identifiable {
    904   let opaqueReference: String
    905   let remoteURL: String?
    906   let sha256: String
    907   let mediaType: String
    908   let byteSize: UInt64
    909   let width: UInt32
    910   let height: UInt32
    911   var alt: String
    912   let preparedAtUnixSeconds: UInt64
    913 
    914   var id: String {
    915     opaqueReference
    916   }
    917 }
    918 
    919 struct TeraAddForm: Sendable, Equatable, Hashable {
    920   var commandType: TeraAddCommandType
    921   var content: String = ""
    922   var identifier: String?
    923   var title: String?
    924   var summary: String?
    925   var location: String?
    926   var eventTiming: TeraEventTiming?
    927   var eventStartDate: String?
    928   var eventEndDate: String?
    929   var eventStartUnixSeconds: UInt64?
    930   var eventEndUnixSeconds: UInt64?
    931   var eventTimezone: String?
    932   var priceAmount: String?
    933   var currency: String?
    934   var unit: String?
    935   var quantity: String?
    936   var foodPublishedAtUnixSeconds: UInt64?
    937   var foodStatus: String?
    938   var media: [TeraPreparedMedia] = []
    939 
    940   static func empty(_ type: TeraAddCommandType = .createUpdate) -> Self {
    941     var form = Self(commandType: type)
    942     if type == .createEvent {
    943       form.eventTiming = .timed
    944       form.eventTimezone = TimeZone.current.identifier
    945     }
    946     return form
    947   }
    948 }
    949 
    950 struct TeraAddRuntimeInput: Sendable, Equatable {
    951   let form: TeraAddForm
    952   let media: [TeraPreparedMediaHandle]
    953 }
    954 
    955 enum TeraDraftKind: String, Sendable, Equatable, Hashable {
    956   case add
    957   case retraction
    958 }
    959 
    960 enum TeraDraftMediaStage: String, Sendable, Equatable, Hashable {
    961   case pending
    962   case preparing
    963   case uploading
    964   case verified
    965   case failed
    966   case orphaned
    967 }
    968 
    969 struct TeraDraftMediaStatus: Sendable, Equatable, Hashable {
    970   let url: String
    971   let stage: TeraDraftMediaStage
    972   let uploadAttempts: UInt8
    973   let verifiedAtUnixMilliseconds: UInt64?
    974   let possibleOrphan: Bool
    975   let orphanReasonCode: String?
    976   let orphanRecordedAtUnixMilliseconds: UInt64?
    977   var uploadURL: String?
    978 }
    979 
    980 struct TeraOperationSettlement: Sendable, Equatable, Hashable {
    981   let artifacts: UInt16
    982   let signed: UInt16
    983   let admitted: UInt16
    984   let pending: UInt16
    985   let retryable: UInt16
    986   let indeterminate: UInt16
    987   let failedTerminal: UInt16
    988   let cancelled: UInt16
    989   let deliveryPlans: UInt16
    990   let deliverySatisfied: UInt16
    991   let deliveryPending: UInt16
    992   let deliveryRetryable: UInt16
    993   let deliveryExhausted: UInt16
    994   let deliveryFailedTerminal: UInt16
    995   let deliveryCancelled: UInt16
    996 }
    997 
    998 struct TeraRetractionDraftInput: Sendable, Equatable, Hashable {
    999   let commandType: TeraAddCommandType
   1000   let targetCardID: String
   1001   let targetEventID: String
   1002   let targetKind: UInt32
   1003   let targetAddress: String?
   1004   let reason: String
   1005 }
   1006 
   1007 struct TeraBlossomUploadIntent: Sendable, Equatable {
   1008   let draftID: String
   1009   let expectedRevision: UInt64
   1010   let media: TeraPreparedMediaHandle
   1011 }
   1012 
   1013 struct TeraNativeUploadCompletion: Sendable, Equatable {
   1014   let draftID: String
   1015   let expectedRevision: UInt64
   1016   let media: TeraPreparedMediaHandle
   1017   let statusCode: UInt16
   1018   let responseMediaType: String?
   1019   let responseContentEncoding: String?
   1020   let responseBody: Data
   1021 }
   1022 
   1023 enum TeraRuntimeLifecycle: Sendable, Equatable {
   1024   case stopped
   1025   case starting(generation: TeraSessionGeneration)
   1026   case running(generation: TeraSessionGeneration)
   1027   case stopping(generation: TeraSessionGeneration)
   1028   case failed(generation: TeraSessionGeneration, failure: TeraRuntimeFailure)
   1029 }