@tinkerlandai/indiana-react-native 0.9.2 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/IndianaReactNative.podspec +19 -0
  2. package/README.md +62 -5
  3. package/THIRD_PARTY_NOTICES.md +74 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +3 -0
  6. package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgeModule.kt +237 -0
  7. package/android/src/main/java/ai/tinkerland/indiana/reactnative/IndianaBridgePackage.kt +11 -0
  8. package/dist/index.cjs +17 -1
  9. package/dist/index.d.cts +96889 -6007
  10. package/dist/index.d.ts +96889 -6007
  11. package/dist/index.js +17 -1
  12. package/ios/IndianaBridge.m +11 -0
  13. package/ios/IndianaBridge.swift +275 -0
  14. package/native-sdk/android/assets/indiana/index.html +268 -0
  15. package/native-sdk/android/java/ai/tinkerland/indiana/BillingAuthorizationTerms.kt +55 -0
  16. package/native-sdk/android/java/ai/tinkerland/indiana/BundledWebView.kt +90 -0
  17. package/native-sdk/android/java/ai/tinkerland/indiana/ContentSession.kt +45 -0
  18. package/native-sdk/android/java/ai/tinkerland/indiana/ContentView.kt +36 -0
  19. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerOperations.generated.kt +84 -0
  20. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerResponseSemantics.kt +197 -0
  21. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSemantics.kt +120 -0
  22. package/native-sdk/android/java/ai/tinkerland/indiana/CustomerSession.kt +106 -0
  23. package/native-sdk/android/java/ai/tinkerland/indiana/FeatureClient.kt +65 -0
  24. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelCommandStorage.kt +163 -0
  25. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelConfigurationRelay.kt +50 -0
  26. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelSession.kt +208 -0
  27. package/native-sdk/android/java/ai/tinkerland/indiana/FunnelView.kt +71 -0
  28. package/native-sdk/android/java/ai/tinkerland/indiana/GeneratedProtocol.kt +16 -0
  29. package/native-sdk/android/java/ai/tinkerland/indiana/GoogleStorefront.kt +165 -0
  30. package/native-sdk/android/java/ai/tinkerland/indiana/HTTPTransport.kt +43 -0
  31. package/native-sdk/android/java/ai/tinkerland/indiana/IndianaClient.kt +526 -0
  32. package/native-sdk/android/java/ai/tinkerland/indiana/MerchantCommerceSemantics.kt +38 -0
  33. package/native-sdk/android/java/ai/tinkerland/indiana/NativePendingKeystore.kt +81 -0
  34. package/native-sdk/android/java/ai/tinkerland/indiana/NativeReceiptRelay.kt +28 -0
  35. package/native-sdk/android/java/ai/tinkerland/indiana/NativeStoreOffer.kt +24 -0
  36. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallContent.kt +75 -0
  37. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallSession.kt +144 -0
  38. package/native-sdk/android/java/ai/tinkerland/indiana/PaywallView.kt +74 -0
  39. package/native-sdk/android/java/ai/tinkerland/indiana/Protocol.kt +28 -0
  40. package/native-sdk/android/java/ai/tinkerland/indiana/PushInstallation.kt +170 -0
  41. package/native-sdk/android/java/ai/tinkerland/indiana/RendererNavigation.kt +13 -0
  42. package/native-sdk/android/java/ai/tinkerland/indiana/SDKVersion.kt +6 -0
  43. package/native-sdk/android/java/ai/tinkerland/indiana/StoreSubscriptionManagement.kt +19 -0
  44. package/native-sdk/android/java/ai/tinkerland/indiana/WireSchema.kt +76 -0
  45. package/native-sdk/android/resources/funnel-wire-schemas.json +1 -0
  46. package/native-sdk/android/resources/indiana-third-party-notices.txt +101 -0
  47. package/native-sdk/android/resources/renderer-data-url.txt +1 -0
  48. package/native-sdk/ios/AnalyticsReceipt.swift +10 -0
  49. package/native-sdk/ios/AppleStorefront.swift +218 -0
  50. package/native-sdk/ios/BillingAuthorizationTerms.swift +57 -0
  51. package/native-sdk/ios/BundledWebView.swift +87 -0
  52. package/native-sdk/ios/ContentSession.swift +48 -0
  53. package/native-sdk/ios/ContentView.swift +51 -0
  54. package/native-sdk/ios/CustomerOperations.generated.swift +166 -0
  55. package/native-sdk/ios/CustomerResponseSemantics.swift +236 -0
  56. package/native-sdk/ios/CustomerSemantics.swift +145 -0
  57. package/native-sdk/ios/CustomerSession.swift +129 -0
  58. package/native-sdk/ios/FeatureClient.swift +86 -0
  59. package/native-sdk/ios/FunnelCommandStorage.swift +163 -0
  60. package/native-sdk/ios/FunnelConfigurationRelay.swift +48 -0
  61. package/native-sdk/ios/FunnelSession.swift +232 -0
  62. package/native-sdk/ios/FunnelView.swift +125 -0
  63. package/native-sdk/ios/GeneratedProtocol.swift +16 -0
  64. package/native-sdk/ios/HTTPTransport.swift +74 -0
  65. package/native-sdk/ios/IndianaClient.swift +601 -0
  66. package/native-sdk/ios/MerchantCommerceSemantics.swift +51 -0
  67. package/native-sdk/ios/NativePendingKeychain.swift +52 -0
  68. package/native-sdk/ios/NativeReceiptRelay.swift +28 -0
  69. package/native-sdk/ios/NativeStoreOffer.swift +34 -0
  70. package/native-sdk/ios/PaywallContent.swift +75 -0
  71. package/native-sdk/ios/PaywallSession.swift +124 -0
  72. package/native-sdk/ios/PaywallView.swift +87 -0
  73. package/native-sdk/ios/Protocol.swift +49 -0
  74. package/native-sdk/ios/PushInstallation.swift +154 -0
  75. package/native-sdk/ios/RendererNavigation.swift +8 -0
  76. package/native-sdk/ios/Resources.swift +15 -0
  77. package/native-sdk/ios/SDKVersion.swift +4 -0
  78. package/native-sdk/ios/StoreSubscriptionManagement.swift +11 -0
  79. package/native-sdk/ios/WebView/THIRD_PARTY_NOTICES.txt +101 -0
  80. package/native-sdk/ios/WebView/index.html +268 -0
  81. package/native-sdk/ios/WebView/renderer-data-url.txt +1 -0
  82. package/native-sdk/ios/WireSchema.swift +107 -0
  83. package/native-sdk/ios/funnel-wire-schemas.json +1 -0
  84. package/package.json +8 -3
  85. package/react-native.config.cjs +12 -0
@@ -0,0 +1,125 @@
1
+ #if canImport(UIKit)
2
+ import Foundation
3
+ import UIKit
4
+ @preconcurrency import WebKit
5
+ @preconcurrency import SafariServices
6
+ import SwiftUI
7
+
8
+ /// A native container for the single published Funnel renderer. Embed as a child or present over the app.
9
+ @MainActor public final class IndianaFunnelViewController: UIViewController, @preconcurrency SFSafariViewControllerDelegate {
10
+ public let session: IndianaFunnelSession
11
+ public var onPurchase: (@MainActor (IndianaJSON) async throws -> Void)?
12
+ public var onEvent: (@MainActor (String) async throws -> Void)?
13
+ /// Opens an external page or Paywall link (`openIn` is `same_window` or `new_window`). Return false when it could
14
+ /// not be opened; an external page then takes its failed route. Default: in-app browser for `same_window`,
15
+ /// the system browser for `new_window`.
16
+ public var onOpenURL: (@MainActor (URL, String) async throws -> Bool)?
17
+ public var onComplete: (@MainActor (IndianaFunnelRun) -> Void)?
18
+ public var onDismiss: (@MainActor () -> Void)?
19
+ public var onError: (@MainActor (Error) -> Void)?
20
+ private var host: IndianaBundledWebView!
21
+ private var configurationRelay: IndianaFunnelConfigurationRelay?
22
+ private var stopped = false; private var completed = false; private var dismissed = false
23
+ private var observer: IndianaForegroundObserver?
24
+ public init(session: IndianaFunnelSession) { self.session = session; super.init(nibName: nil, bundle: nil) }
25
+ required init?(coder: NSCoder) { fatalError("Use init(session:)") }
26
+ public override func loadView() {
27
+ host = IndianaBundledWebView(label:"Funnel", dispatch: { [weak self] method,payload in
28
+ guard let self, !self.stopped else { throw IndianaFunnelError.identityChanged }
29
+ return try await self.dispatch(method,payload:payload)
30
+ },onError: { [weak self] error in self?.onError?(error) })
31
+ view = host.webView
32
+ Task {
33
+ await session.onInvalidation { [weak self] in Task { @MainActor in self?.invalidate() } }
34
+ guard !stopped else { return }; host.start()
35
+ }
36
+ observer = IndianaForegroundObserver { [weak self] in
37
+ Task { @MainActor in self?.sendEvent("refresh") }
38
+ }
39
+ }
40
+ public func close() async {
41
+ guard !stopped else { return }
42
+ do { _ = try await session.close() } catch { onError?(error) }
43
+ invalidate()
44
+ }
45
+ private func invalidate() {
46
+ guard !stopped else { return }
47
+ host?.destroy(); stopped = true; configurationRelay = nil
48
+ presentedViewController?.dismiss(animated: false)
49
+ if !dismissed { dismissed = true; onDismiss?() }
50
+ }
51
+ private func dispatch(_ method: String, payload: IndianaJSON) async throws -> IndianaJSON {
52
+ switch method {
53
+ case "ready":
54
+ let config = await session.hostConfiguration()
55
+ // UUID's synthesized Codable form is uppercase; the wire scope is canonical lowercase.
56
+ let scope: IndianaJSON = .object([
57
+ "projectId": .string(config.scope.projectId.uuidString.lowercased()),
58
+ "environment": .string(config.scope.environment.rawValue),
59
+ ])
60
+ send(.object(["event":.string("init"),"scope":scope,"apiOrigin":.string(config.apiOrigin)])); return .null
61
+ case "pendingCommand": return await session.hostPendingCommand()
62
+ case "configuration":
63
+ if configurationRelay == nil { configurationRelay = IndianaFunnelConfigurationRelay(configuration: await session.hostConfiguration()) }
64
+ guard let relay = configurationRelay, !stopped else { throw IndianaFunnelError.identityChanged }
65
+ return try await relay.read(payload)
66
+ case "paywallContent":
67
+ return try await session.hostPaywallContent(payload)
68
+ case "command":
69
+ guard let command = payload.object else { throw IndianaFunnelError.invalidCommand }
70
+ return try JSONDecoder().decode(IndianaJSON.self, from: await session.hostCommand(command))
71
+ case "purchase":
72
+ guard let operation = payload.object?["operation"], let onPurchase else { throw IndianaClientError.unsupportedCapability }
73
+ let trusted = try await session.trustedPurchase(operation)
74
+ try await onPurchase(trusted);return .null
75
+ case "checkout":
76
+ guard let raw = payload.object?["url"]?.string else { throw IndianaFunnelError.invalidCommand }
77
+ let url = try await session.trustedCheckoutURL(raw)
78
+ let browser = SFSafariViewController(url: url);browser.delegate = self;present(browser, animated: true);return .null
79
+ case "open":
80
+ let url = try await session.trustedOpen(payload), openIn = payload.object?["openIn"]?.string ?? "same_window"
81
+ if let onOpenURL { return .bool(try await onOpenURL(url, openIn)) }
82
+ if openIn == "new_window" { return .bool(await UIApplication.shared.open(url)) }
83
+ let browser = SFSafariViewController(url: url);browser.delegate = self;present(browser, animated: true);return .bool(true)
84
+ case "event":
85
+ guard let name = payload.object?["event"]?.string, let onEvent else { throw IndianaClientError.unsupportedCapability }
86
+ try await onEvent(try await session.trustedEvent(name)); return .null
87
+ case "complete":
88
+ guard let run = await session.current, run.status == "completed", run.instruction == nil || run.instruction == .null else { throw IndianaFunnelError.invalidCommand }
89
+ if !completed { completed = true; onComplete?(run) }; return .null
90
+ case "dismiss":
91
+ if !dismissed { dismissed = true; onDismiss?() }; return .null
92
+ default: throw IndianaFunnelError.invalidCommand
93
+ }
94
+ }
95
+ private func send(_ value: IndianaJSON) { host?.send(value) }
96
+ private func sendEvent(_ event: String) { send(.object(["event":.string(event)])) }
97
+ public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { controller.dismiss(animated: true); sendEvent("refresh") }
98
+
99
+ }
100
+
101
+ final class IndianaForegroundObserver {
102
+ private let token: NSObjectProtocol
103
+ init(_ action: @escaping @Sendable () -> Void) {
104
+ token = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { _ in action() }
105
+ }
106
+ deinit { NotificationCenter.default.removeObserver(token) }
107
+ }
108
+
109
+ /// Supports embedding in an existing product screen; node presentation stays authored by the Funnel.
110
+ @MainActor public struct IndianaFunnelView: UIViewControllerRepresentable {
111
+ public let session: IndianaFunnelSession
112
+ public var onPurchase: (@MainActor (IndianaJSON) async throws -> Void)?
113
+ public var onEvent: (@MainActor (String) async throws -> Void)?
114
+ public var onComplete: (@MainActor (IndianaFunnelRun) -> Void)?
115
+ public var onDismiss: (@MainActor () -> Void)?
116
+ public var onError: (@MainActor (Error) -> Void)?
117
+ public var onOpenURL: (@MainActor (URL, String) async throws -> Bool)?
118
+ public init(session: IndianaFunnelSession, onPurchase: (@MainActor (IndianaJSON) async throws -> Void)? = nil, onEvent: (@MainActor (String) async throws -> Void)? = nil, onComplete: (@MainActor (IndianaFunnelRun) -> Void)? = nil, onDismiss: (@MainActor () -> Void)? = nil, onError: (@MainActor (Error) -> Void)? = nil, onOpenURL: (@MainActor (URL, String) async throws -> Bool)? = nil) {
119
+ self.session = session;self.onPurchase = onPurchase;self.onEvent = onEvent;self.onComplete = onComplete;self.onDismiss = onDismiss;self.onError = onError;self.onOpenURL = onOpenURL
120
+ }
121
+ public func makeUIViewController(context: Context) -> IndianaFunnelViewController { let controller = IndianaFunnelViewController(session: session); updateUIViewController(controller, context: context); return controller }
122
+ public func updateUIViewController(_ controller: IndianaFunnelViewController, context: Context) { controller.onPurchase = onPurchase; controller.onEvent = onEvent; controller.onComplete = onComplete; controller.onDismiss = onDismiss; controller.onError = onError; controller.onOpenURL = onOpenURL }
123
+ public static func dismantleUIViewController(_ controller: IndianaFunnelViewController, coordinator: ()) { Task { await controller.close() } }
124
+ }
125
+ #endif
@@ -0,0 +1,16 @@
1
+ // Generated by scripts/sdk/generate-native-protocol.mjs. Do not edit.
2
+ import Foundation
3
+ public enum IndianaEnvironment: String, Codable, Sendable { case sandbox, live }
4
+ enum IndianaProtocol {
5
+ static let wireProtocol = "0.10"
6
+ static let uuidPattern = "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
7
+ static let utcPattern = "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
8
+ static let scopeKeys: Set<String> = ["projectId", "environment"]
9
+ static let runKeys: Set<String> = ["id", "source", "revision", "visit", "nodeId", "nodeType", "status", "stopped", "canBack", "authenticated", "locale", "answers", "parameters", "variables", "output", "screen", "paywall", "instruction", "pendingRequest", "runExpiresAt", "credentialExpiresAt"]
10
+ static let runRequiredKeys: Set<String> = ["id", "source", "revision", "visit", "nodeId", "nodeType", "status", "stopped", "canBack", "authenticated", "locale", "answers", "parameters", "variables", "output", "screen", "paywall", "instruction", "pendingRequest", "runExpiresAt", "credentialExpiresAt"]
11
+ static let nodeTypes: Set<String> = ["entry", "screen", "condition", "split", "feature", "action", "external_page", "wait", "end", "offering", "coupon", "coupons", "output"]
12
+ static let runStatuses: Set<String> = ["running", "waiting", "completed", "closed", "ineligible", "failed"]
13
+ static let nonCanonicalLocaleLanguages: Set<String> = ["aam", "aar", "abk", "adp", "afr", "agp", "ais", "ajp", "ajt", "aju", "aka", "alb", "als", "amh", "ara", "arb", "arg", "arm", "asd", "asm", "aue", "ava", "ave", "aym", "ayr", "ayx", "aze", "azj", "bak", "bam", "baq", "baz", "bcc", "bcl", "bel", "ben", "bgm", "bhk", "bic", "bih", "bis", "bjd", "bjq", "bkb", "blg", "bod", "bos", "bre", "btb", "bul", "bur", "bxk", "bxr", "cat", "ccq", "ces", "cha", "che", "chi", "chu", "chv", "cjr", "cka", "cld", "cls", "cmk", "cmn", "cnr", "cor", "cos", "coy", "cqu", "cre", "cwd", "cym", "cze", "daf", "dan", "dap", "deu", "dgo", "dhd", "dik", "diq", "dit", "div", "djl", "dkl", "drh", "drr", "drw", "dud", "duj", "dut", "dwl", "dzo", "ekk", "ell", "elp", "emk", "eng", "epo", "esk", "est", "eus", "ewe", "fao", "fas", "fat", "fij", "fin", "fra", "fre", "fry", "fuc", "ful", "gav", "gaz", "gbc", "gbo", "geo", "ger", "gfx", "ggn", "ggo", "ggr", "gio", "gla", "gle", "glg", "gli", "glv", "gno", "gom", "gre", "grn", "gti", "gug", "guj", "guv", "gya", "hat", "hau", "hbs", "hdn", "hea", "heb", "her", "him", "hin", "hmo", "hrr", "hrv", "hun", "hye", "ibi", "ibo", "ice", "ido", "iii", "ike", "iku", "ile", "ill", "ilw", "in", "ina", "ind", "ipk", "isl", "ita", "iw", "izi", "jar", "jav", "jaw", "jeg", "ji", "jpn", "jw", "kal", "kan", "kas", "kat", "kau", "kaz", "kdv", "kgc", "kgd", "kgh", "kgm", "khk", "khm", "kik", "kin", "kir", "kmr", "knc", "kng", "koj", "kom", "kon", "kor", "kpp", "kpv", "krm", "ktr", "kua", "kur", "kvs", "kwq", "kxe", "kxl", "kzh", "kzj", "kzt", "lak", "lao", "lat", "lav", "lbk", "leg", "lii", "lim", "lin", "lit", "llo", "lmm", "ltz", "lub", "lug", "lvs", "mac", "mah", "mal", "mao", "mar", "may", "meg", "mgx", "mhr", "mkd", "mlg", "mlt", "mnk", "mnt", "mo", "mof", "mol", "mon", "mri", "msa", "mst", "mup", "mwd", "mwj", "mya", "myd", "myt", "nad", "nau", "nav", "nbf", "nbl", "nbx", "ncp", "nde", "ndo", "nep", "nld", "nln", "nlr", "nno", "nns", "nnx", "nob", "nom", "noo", "nor", "npi", "nts", "nxu", "nya", "oci", "ojg", "oji", "ori", "orm", "ory", "oss", "oun", "pan", "pat", "pbu", "pcr", "per", "pes", "pli", "plt", "pmc", "pmk", "pmu", "pnb", "pol", "por", "ppa", "ppr", "prp", "prs", "pry", "pus", "puz", "que", "quz", "rmr", "rmy", "roh", "ron", "rum", "run", "rus", "sag", "san", "sap", "sca", "scc", "scr", "sgl", "sh", "sin", "skk", "slk", "slo", "slv", "smd", "sme", "smo", "sna", "snb", "snd", "som", "sot", "spa", "spy", "sqi", "src", "srd", "srp", "ssw", "sul", "sum", "sun", "swa", "swc", "swe", "swh", "szd", "tah", "tam", "tat", "tdu", "tel", "tgg", "tgk", "tgl", "tha", "thc", "thw", "thx", "tib", "tid", "tie", "tir", "tkk", "tl", "tlw", "tmk", "tmp", "tne", "tnf", "ton", "tpw", "tsf", "tsn", "tso", "ttq", "tuk", "tur", "twi", "uig", "ukr", "umu", "unp", "uok", "urd", "uzb", "uzn", "ven", "vie", "vol", "wel", "wgw", "wit", "wiw", "wln", "wol", "xba", "xho", "xia", "xkh", "xpe", "xrq", "xsj", "xsl", "xss", "ybd", "ydd", "yen", "yid", "yiy", "yma", "ymt", "yor", "yos", "yuu", "zai", "zha", "zho", "zir", "zkb", "zsm", "zul", "zyb"]
14
+ static let nonCanonicalLocaleScripts: Set<String> = ["Qaai"]
15
+ static let nonCanonicalLocaleRegions: Set<String> = ["AN", "BU", "CS", "CT", "DD", "DY", "FQ", "FX", "HV", "JT", "MI", "NH", "NQ", "NT", "PC", "PU", "PZ", "QU", "RH", "SU", "TP", "UK", "VD", "WK", "YD", "YU", "ZR", "004", "008", "010", "012", "016", "020", "024", "028", "031", "032", "036", "040", "044", "048", "050", "051", "052", "056", "060", "062", "064", "068", "070", "072", "074", "076", "084", "086", "090", "092", "096", "100", "104", "108", "112", "116", "120", "124", "132", "136", "140", "144", "148", "152", "156", "158", "162", "166", "170", "172", "174", "175", "178", "180", "184", "188", "191", "192", "196", "200", "203", "204", "208", "212", "214", "218", "222", "226", "230", "231", "232", "233", "234", "238", "239", "242", "246", "248", "249", "250", "254", "258", "260", "262", "266", "268", "270", "275", "276", "278", "280", "288", "292", "296", "300", "304", "308", "312", "316", "320", "324", "328", "332", "334", "336", "340", "344", "348", "352", "356", "360", "364", "368", "372", "376", "380", "384", "388", "392", "398", "400", "404", "408", "410", "414", "417", "418", "422", "426", "428", "430", "434", "438", "440", "442", "446", "450", "454", "458", "462", "466", "470", "474", "478", "480", "484", "492", "496", "498", "499", "500", "504", "508", "512", "516", "520", "524", "528", "530", "531", "532", "533", "534", "535", "536", "540", "548", "554", "558", "562", "566", "570", "574", "578", "580", "581", "582", "583", "584", "585", "586", "591", "598", "600", "604", "608", "612", "616", "620", "624", "626", "630", "634", "638", "642", "643", "646", "652", "654", "659", "660", "662", "663", "666", "670", "674", "678", "682", "686", "688", "690", "694", "702", "703", "704", "705", "706", "710", "716", "720", "724", "728", "729", "732", "736", "740", "744", "748", "752", "756", "760", "762", "764", "768", "772", "776", "780", "784", "788", "792", "795", "796", "798", "800", "804", "807", "810", "818", "826", "830", "831", "832", "833", "834", "840", "850", "854", "858", "860", "862", "876", "882", "886", "887", "890", "891", "894", "958", "959", "960", "962", "963", "964", "965", "966", "967", "968", "969", "970", "971", "972", "973", "974", "975", "976", "977", "978", "979", "980", "981", "982", "983", "984", "985", "986", "987", "988", "989", "990", "991", "992", "993", "994", "995", "996", "997", "998", "999"]
16
+ }
@@ -0,0 +1,74 @@
1
+ import Foundation
2
+
3
+ public struct IndianaConfiguration: Sendable {
4
+ public let baseURL: URL
5
+ internal var apiOrigin: String {
6
+ var parts = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
7
+ parts.path = ""; parts.query = nil; parts.fragment = nil
8
+ parts.host = parts.host?.lowercased(); if parts.port == 443 { parts.port = nil }
9
+ return parts.string!
10
+ }
11
+ public let scope: IndianaScope
12
+ public let timeout: TimeInterval
13
+ /// Override baseURL explicitly for test services; Sandbox is a business scope, not a deployment selector.
14
+ public init(baseURL: URL = URL(string: "https://api.tinker.cash")!, scope: IndianaScope, timeout: TimeInterval = 15) throws {
15
+ guard baseURL.scheme == "https", baseURL.host != nil, baseURL.user == nil, baseURL.password == nil,
16
+ baseURL.query == nil, baseURL.fragment == nil, timeout > 0, timeout <= 120 else { throw IndianaClientError.invalidConfiguration }
17
+ self.baseURL = baseURL; self.scope = scope; self.timeout = timeout
18
+ }
19
+ }
20
+ protocol IndianaHTTP: Sendable {
21
+ func send(path: String, method: String, authorization: String, body: Data?) async throws -> Data
22
+ }
23
+ final class IndianaURLTransport: IndianaHTTP, Sendable {
24
+ static func requireCurrentProtocol(_ value: String?) throws {
25
+ guard value == IndianaProtocol.wireProtocol else { throw IndianaClientError.invalidResponse }
26
+ }
27
+ private let configuration: IndianaConfiguration
28
+ private let requestOrigin: URL
29
+ private let session: URLSession
30
+ // Internal test seam: the public configuration remains HTTPS-only. A wire fixture
31
+ // may use only a loopback HTTP origin; no public SDK caller can select it.
32
+ init(configuration: IndianaConfiguration, sessionConfiguration: URLSessionConfiguration = .ephemeral, loopbackFixtureOrigin: URL? = nil) {
33
+ self.configuration = configuration
34
+ if let origin = loopbackFixtureOrigin {
35
+ precondition(origin.scheme == "http" && origin.host == "127.0.0.1" && (origin.port ?? 0) > 1024 && origin.user == nil && origin.password == nil && origin.query == nil && origin.fragment == nil && origin.path.isEmpty)
36
+ }
37
+ self.requestOrigin = loopbackFixtureOrigin ?? configuration.baseURL
38
+ let config = sessionConfiguration.copy() as! URLSessionConfiguration
39
+ config.urlCache = nil; config.httpCookieStorage = nil; config.httpShouldSetCookies = false; config.urlCredentialStorage = nil; config.requestCachePolicy = .reloadIgnoringLocalCacheData
40
+ let ownedHeaders = Set(["authorization", "cookie", "x-indiana-project", "x-indiana-environment", "x-indiana-protocol", "x-indiana-sdk-version"])
41
+ config.httpAdditionalHeaders = config.httpAdditionalHeaders?.filter { !ownedHeaders.contains(String(describing: $0.key).lowercased()) }
42
+ config.timeoutIntervalForRequest = configuration.timeout; config.timeoutIntervalForResource = configuration.timeout
43
+ self.session = URLSession(configuration: config, delegate: NoRedirect(), delegateQueue: nil)
44
+ }
45
+ func send(path: String, method: String, authorization: String, body: Data?) async throws -> Data {
46
+ guard !path.contains(".."), !path.contains("?"), !path.contains("#"), !path.hasPrefix("/") else { throw IndianaClientError.invalidConfiguration }
47
+ var request = URLRequest(url: requestOrigin.appendingPathComponent("api/v1").appendingPathComponent(path))
48
+ request.httpMethod = method; request.httpBody = body
49
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
50
+ if !authorization.isEmpty { request.setValue("Bearer \(authorization)", forHTTPHeaderField: "Authorization") }
51
+ request.setValue(configuration.scope.projectId.uuidString.lowercased(), forHTTPHeaderField: "X-Indiana-Project")
52
+ request.setValue(configuration.scope.environment.rawValue, forHTTPHeaderField: "X-Indiana-Environment")
53
+ request.setValue(IndianaProtocol.wireProtocol, forHTTPHeaderField: "X-Indiana-Protocol")
54
+ request.setValue(IndianaSDK.version, forHTTPHeaderField: "X-Indiana-SDK-Version")
55
+ do {
56
+ let (bytes, response) = try await session.bytes(for: request)
57
+ guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
58
+ throw IndianaClientError.requestFailed((response as? HTTPURLResponse)?.statusCode ?? 0)
59
+ }
60
+ try Self.requireCurrentProtocol(http.value(forHTTPHeaderField: "X-Indiana-Protocol"))
61
+ if response.expectedContentLength > 2_097_152 { throw IndianaClientError.invalidResponse }
62
+ var data = Data()
63
+ for try await byte in bytes {
64
+ guard data.count < 2_097_152 else { throw IndianaClientError.invalidResponse }
65
+ data.append(byte)
66
+ }
67
+ return data
68
+ } catch let error as IndianaClientError { throw error }
69
+ catch { throw IndianaClientError.temporaryUnavailable }
70
+ }
71
+ private final class NoRedirect: NSObject, URLSessionTaskDelegate, Sendable {
72
+ func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) }
73
+ }
74
+ }