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 }