HTTP Paths can change their absolute path based on IAM enablement (#7422)

* ci: announce modifications in the SDK API feed (#7451)

An attribute-only modification is neither an addition nor a break, so it reached the feed as a headline and a link, with the headline claiming new API.

* HTTP Paths can change their absolute path based on IAM enablement

* missed one

* fix failures

* fix crashing test

* address feedback

* unit tests

* slightly more graceful

* fix signing test

* fix other test

* log on invalid path

* throwAssertion is unavailable on watchOS

* fix backend integration tests

* fix import

---------

Co-authored-by: Álvaro Brey <alvaro.brey@revenuecat.com>
This commit is contained in:
Dave DeLongandÁlvaro Brey authored and GitHub committed 2026-08-21 14:04:21 -06:00
1 parent 4524b91148
commit 3eb01c32bc
27 files changed
+775 -61

No files matched your search

@@ -43,6 +43,9 @@ protocol HTTPRequestPath {
/// The full relative path for this endpoint.
var relativePath: String { get }
/// The full relative path for this endpoint when using IAM tokens.
var relativeIAMPath: String { get }
/// The fallback relative path for this endpoint, if any.
var fallbackRelativePath: String? { get }
@@ -62,6 +65,9 @@ protocol HTTPRequestPath {
/// Provides endpoint-specific inputs for response signature verification.
var responseSignatureContextProvider: ResponseSignatureContextProvider { get }
/// Whether this path corresponds to an IAM request
var isIAMPath: Bool { get }
}
/// Provides endpoint-specific inputs for backend response signature verification.
@@ -106,6 +112,10 @@ extension HTTPRequestPath {
return nil
}
var relativeIAMPath: String {
return self.relativePath
}
var usesAPISources: Bool {
return false
}
@@ -122,9 +132,14 @@ extension HTTPRequestPath {
return DefaultResponseSignatureContextProvider()
}
var url: URL? { return self.url(proxyURL: nil) }
var isIAMPath: Bool {
return false
}
func url(proxyURL: URL? = nil, apiSourceURL: URL? = nil, fallbackUrlIndex: Int? = nil) -> URL? {
func url(proxyURL: URL? = nil,
apiSourceURL: URL? = nil,
fallbackUrlIndex: Int? = nil,
preferIAMPath: Bool) -> URL? {
let baseURL: URL
if let proxyURL {
// When a Proxy URL is set, we don't support API sources or fallback URLs
@@ -141,7 +156,9 @@ extension HTTPRequestPath {
} else {
baseURL = Self.serverHostURL
}
return URL(string: self.relativePath, relativeTo: baseURL)
let path = preferIAMPath ? self.relativeIAMPath : self.relativePath
return URL(string: path, relativeTo: baseURL)
}
}
@@ -368,7 +385,15 @@ extension HTTPRequest.Path: HTTPRequestPath {
}
var relativePath: String {
return "/v1/\(self.pathComponent)"
let component = self.pathComponent
if component.hasPrefix("/") { return component }
return "/v1/\(component)"
}
var relativeIAMPath: String {
let component = self.iamPathComponent
if component.hasPrefix("/") { return component }
return "/v1/\(component)"
}
var pathComponent: String {
@@ -434,6 +459,56 @@ extension HTTPRequest.Path: HTTPRequestPath {
}
}
var iamPathComponent: String {
// NOTE: cases below that "return self.pathComponent" are indicating
// that the corresponding server paths have not yet been migrated to
// support access token authorization
switch self {
case .getCustomerInfo:
return "customer"
case .getOfferings:
return "customer/offerings"
case .getIntroEligibility:
return "customer/intro_eligibility"
case .logIn:
assertionFailure("The .login endpoint is not allowed when IAM is enabled")
Logger.error("The .login endpoint is not allowed when IAM is enabled")
return self.pathComponent
case .postAttributionData:
return "customer/attribution"
case .postOfferForSigning:
return "offers"
case .postReceiptData:
return "receipts"
case .postSubscriberAttributes:
return "customer/attributes"
case .postAdServicesToken:
return "customer/adservices_attribution"
case .health:
return self.pathComponent
case .appHealthReport:
return "customer/health_report"
case .appHealthReportAvailability:
return self.pathComponent
case .getProductEntitlementMapping:
return "product_entitlement_mapping"
case .getCustomerCenterConfig:
return "customer/customercenter"
case .getVirtualCurrencies:
return "customer/virtual_currencies"
case .postRedeemWebPurchase:
return self.pathComponent
case .postCreateTicket:
return self.pathComponent
case .isPurchaseAllowedByRestoreBehavior:
return "customer/restore/eligibility"
case .rewardVerificationStatus:
return self.pathComponent
case .remoteConfig:
return self.pathComponent
}
}
var name: String {
switch self {
case .getCustomerInfo: