crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 20c4340e40eccfdf90aad7fb83015c86e430104c
parent 115fa633ce404189fe70c3034ddf57d731d28eab
Author: Michael Camilleri <[email protected]>
Date:   Fri, 12 Jun 2026 08:24:36 +0900

Derive the APNs environment from a single build setting

The aps-environment entitlement was hardcoded to production for all
configurations while PushClient inferred sandbox/production from an #if
DEBUG guard, so a local device debug build could register an environment
with the push worker that disagreed with the environment its APNs token
was actually issued for, and pushes silently failed (audit Finding 2).

This commit introduces an APS_ENVIRONMENT build setting (development for
Debug, production for Release) and substitutes it into both the
entitlement and a new CrossmateAPSEnvironment Info.plist key. PushClient
now reads that key instead of #if DEBUG and disables push with a
diagnostic log if the value is missing or unrecognised. Entitlement and
worker registration now derive from the same variable, so they cannot
diverge.

Co-Authored-By: Claude Fable 5 <[email protected]>

Diffstat:
MCrossmate.xcodeproj/project.pbxproj | 2++
MCrossmate/Crossmate.entitlements | 2+-
MCrossmate/Info.plist | 2++
MCrossmate/Services/PushClient.swift | 21+++++++++++++++------
Mproject.yml | 8++++++++
5 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/Crossmate.xcodeproj/project.pbxproj b/Crossmate.xcodeproj/project.pbxproj @@ -1076,6 +1076,7 @@ isa = XCBuildConfiguration; buildSettings = { APP_ATTEST_ENVIRONMENT = production; + APS_ENVIRONMENT = production; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Crossmate/Crossmate.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -1098,6 +1099,7 @@ isa = XCBuildConfiguration; buildSettings = { APP_ATTEST_ENVIRONMENT = production; + APS_ENVIRONMENT = development; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = Crossmate/Crossmate.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; diff --git a/Crossmate/Crossmate.entitlements b/Crossmate/Crossmate.entitlements @@ -3,7 +3,7 @@ <plist version="1.0"> <dict> <key>aps-environment</key> - <string>production</string> + <string>$(APS_ENVIRONMENT)</string> <key>com.apple.developer.devicecheck.appattest-environment</key> <string>$(APP_ATTEST_ENVIRONMENT)</string> <key>com.apple.developer.icloud-container-identifiers</key> diff --git a/Crossmate/Info.plist b/Crossmate/Info.plist @@ -38,6 +38,8 @@ <string>$(CURRENT_PROJECT_VERSION)</string> <key>CKSharingSupported</key> <true/> + <key>CrossmateAPSEnvironment</key> + <string>$(APS_ENVIRONMENT)</string> <key>CrossmateEngagementSocketURL</key> <string>$(CROSSMATE_ENGAGEMENT_SOCKET_URL)</string> <key>CrossmatePushBaseURL</key> diff --git a/Crossmate/Services/PushClient.swift b/Crossmate/Services/PushClient.swift @@ -37,7 +37,8 @@ final class PushClient { } /// `nil` when the worker isn't configured (e.g. a fresh checkout without a - /// `Local.xcconfig`). The rest of the app treats a nil PushClient as "push + /// `Local.xcconfig`) or when the bundle's APNs environment is missing or + /// unrecognised. The rest of the app treats a nil PushClient as "push /// notifications are disabled" rather than crashing. init?( deviceID: String = RecordSerializer.localDeviceID, @@ -59,11 +60,19 @@ final class PushClient { deviceID: deviceID, session: session ) - #if DEBUG - self.environment = .sandbox - #else - self.environment = .production - #endif + // CrossmateAPSEnvironment carries the same APS_ENVIRONMENT build + // setting that fills the aps-environment entitlement, so the + // environment registered with the worker always matches the + // environment the APNs token was issued for. + switch Bundle.main.object(forInfoDictionaryKey: "CrossmateAPSEnvironment") as? String { + case "development": + self.environment = .sandbox + case "production": + self.environment = .production + case let other: + log("Push disabled: unrecognised APNs environment \(other ?? "<missing>")") + return nil + } } func updateAPNsToken(_ data: Data) { diff --git a/project.yml b/project.yml @@ -73,6 +73,7 @@ targets: CKSharingSupported: true CrossmateEngagementSocketURL: $(CROSSMATE_ENGAGEMENT_SOCKET_URL) CrossmatePushBaseURL: $(CROSSMATE_PUSH_BASE_URL) + CrossmateAPSEnvironment: $(APS_ENVIRONMENT) LSSupportsOpeningDocumentsInPlace: false UILaunchScreen: {} UISupportedInterfaceOrientations: @@ -91,11 +92,18 @@ targets: TARGETED_DEVICE_FAMILY: "1,2" CODE_SIGN_STYLE: Automatic configs: + # APS_ENVIRONMENT is the single source of truth for the APNs + # environment: it is substituted into the aps-environment entitlement + # (which decides the token's environment) and into Info.plist (which + # PushClient reports to the push worker), so the two cannot diverge. + Debug: + APS_ENVIRONMENT: development # Disable linker identical-code-folding in Release so TestFlight crash # reports name the real Swift async functions instead of collapsing # them to <deduplicated_symbol> (which forces dwarfdump --lookup # guesswork during symbolication). Release: + APS_ENVIRONMENT: production OTHER_LDFLAGS: $(inherited) -Wl,-no_deduplicate NotificationService: