Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
212 lines
9.1 KiB
Swift
212 lines
9.1 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
|
|
private struct DirectGatewayPushRegistrationPayload: Encodable {
|
|
var transport: String = PushTransportMode.direct.rawValue
|
|
var token: String
|
|
var topic: String
|
|
var environment: String
|
|
}
|
|
|
|
private struct RelayGatewayPushRegistrationPayload: Encodable {
|
|
var transport: String = PushTransportMode.relay.rawValue
|
|
var relayHandle: String
|
|
var sendGrant: String
|
|
var gatewayDeviceId: String
|
|
var installationId: String
|
|
var topic: String
|
|
var environment: String
|
|
var distribution: String
|
|
var relayOrigin: String
|
|
var tokenDebugSuffix: String?
|
|
}
|
|
|
|
struct PushRelayGatewayIdentity: Codable {
|
|
var deviceId: String
|
|
var publicKey: String
|
|
}
|
|
|
|
actor PushRegistrationManager {
|
|
private let buildConfig: PushBuildConfig
|
|
private let relayClient: PushRelayClient?
|
|
|
|
var usesRelayTransport: Bool {
|
|
self.buildConfig.transport == .relay
|
|
}
|
|
|
|
init(buildConfig: PushBuildConfig = .current) {
|
|
self.buildConfig = buildConfig
|
|
self.relayClient = buildConfig.relayBaseURL.map { PushRelayClient(baseURL: $0) }
|
|
}
|
|
|
|
func makeGatewayRegistrationPayload(
|
|
apnsTokenHex: String,
|
|
topic: String,
|
|
gatewayIdentity: PushRelayGatewayIdentity?)
|
|
async throws -> String {
|
|
switch self.buildConfig.transport {
|
|
case .direct:
|
|
return try Self.encodePayload(
|
|
DirectGatewayPushRegistrationPayload(
|
|
token: apnsTokenHex,
|
|
topic: topic,
|
|
environment: self.buildConfig.apnsEnvironment.rawValue))
|
|
case .relay:
|
|
guard let gatewayIdentity else {
|
|
throw PushRelayError.relayMisconfigured("Missing gateway identity for relay registration")
|
|
}
|
|
return try await self.makeRelayPayload(
|
|
apnsTokenHex: apnsTokenHex,
|
|
topic: topic,
|
|
gatewayIdentity: gatewayIdentity)
|
|
}
|
|
}
|
|
|
|
private func makeRelayPayload(
|
|
apnsTokenHex: String,
|
|
topic: String,
|
|
gatewayIdentity: PushRelayGatewayIdentity)
|
|
async throws -> String {
|
|
guard self.buildConfig.distribution == .official else {
|
|
throw PushRelayError.relayMisconfigured(
|
|
"Relay transport requires an official push build mode")
|
|
}
|
|
try Self.validateRelayContract(
|
|
relayProfile: self.buildConfig.relayProfile,
|
|
apnsEnvironment: self.buildConfig.apnsEnvironment,
|
|
proofPolicy: self.buildConfig.proofPolicy)
|
|
GatewayDiagnostics.pushRelay.stage(
|
|
"contract validated apns=\(self.buildConfig.apnsEnvironment.rawValue) "
|
|
+ "profile=\(self.buildConfig.relayProfile.rawValue) "
|
|
+ "proof=\(self.buildConfig.proofPolicy.rawValue)")
|
|
guard let relayClient = self.relayClient else {
|
|
throw PushRelayError.relayBaseURLMissing
|
|
}
|
|
guard let bundleId = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!bundleId.isEmpty
|
|
else {
|
|
throw PushRelayError.relayMisconfigured("Missing bundle identifier for relay registration")
|
|
}
|
|
guard let installationId = GatewaySettingsStore.loadStableInstanceID()?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!installationId.isEmpty
|
|
else {
|
|
throw PushRelayError.relayMisconfigured("Missing stable installation ID for relay registration")
|
|
}
|
|
|
|
let tokenHashHex = Self.sha256Hex(apnsTokenHex)
|
|
let relayOrigin = relayClient.normalizedBaseURLString
|
|
if let stored = PushRelayRegistrationStore.loadRegistrationState(),
|
|
stored.installationId == installationId,
|
|
stored.gatewayDeviceId == gatewayIdentity.deviceId,
|
|
stored.relayOrigin == relayOrigin,
|
|
stored.apnsEnvironment == self.buildConfig.apnsEnvironment.rawValue,
|
|
stored.relayProfile == self.buildConfig.relayProfile.rawValue,
|
|
stored.proofPolicy == self.buildConfig.proofPolicy.rawValue,
|
|
stored.lastAPNsTokenHashHex == tokenHashHex,
|
|
!Self.isExpired(stored.relayHandleExpiresAtMs)
|
|
{
|
|
GatewayDiagnostics.pushRelay.stage("using cached relay registration")
|
|
return try Self.encodePayload(
|
|
RelayGatewayPushRegistrationPayload(
|
|
relayHandle: stored.relayHandle,
|
|
sendGrant: stored.sendGrant,
|
|
gatewayDeviceId: gatewayIdentity.deviceId,
|
|
installationId: installationId,
|
|
topic: topic,
|
|
environment: self.buildConfig.apnsEnvironment.rawValue,
|
|
distribution: self.buildConfig.distribution.rawValue,
|
|
relayOrigin: relayOrigin,
|
|
tokenDebugSuffix: stored.tokenDebugSuffix))
|
|
}
|
|
|
|
GatewayDiagnostics.pushRelay.stage("relay registration cache miss")
|
|
let response = try await relayClient.register(PushRelayRegistrationInput(
|
|
installationId: installationId,
|
|
bundleId: bundleId,
|
|
appVersion: DeviceInfoHelper.appVersion(),
|
|
environment: self.buildConfig.apnsEnvironment,
|
|
relayProfile: self.buildConfig.relayProfile,
|
|
proofPolicy: self.buildConfig.proofPolicy,
|
|
distribution: self.buildConfig.distribution,
|
|
apnsTokenHex: apnsTokenHex,
|
|
gatewayIdentity: gatewayIdentity))
|
|
let registrationState = PushRelayRegistrationStore.RegistrationState(
|
|
relayHandle: response.relayHandle,
|
|
sendGrant: response.sendGrant,
|
|
relayOrigin: relayOrigin,
|
|
gatewayDeviceId: gatewayIdentity.deviceId,
|
|
relayHandleExpiresAtMs: response.expiresAtMs,
|
|
tokenDebugSuffix: Self.normalizeTokenSuffix(response.tokenSuffix),
|
|
lastAPNsTokenHashHex: tokenHashHex,
|
|
installationId: installationId,
|
|
lastTransport: self.buildConfig.transport.rawValue,
|
|
apnsEnvironment: self.buildConfig.apnsEnvironment.rawValue,
|
|
relayProfile: self.buildConfig.relayProfile.rawValue,
|
|
proofPolicy: self.buildConfig.proofPolicy.rawValue)
|
|
_ = PushRelayRegistrationStore.saveRegistrationState(registrationState)
|
|
GatewayDiagnostics.pushRelay.stage("stored relay registration hasExpiry=\(response.expiresAtMs != nil)")
|
|
return try Self.encodePayload(
|
|
RelayGatewayPushRegistrationPayload(
|
|
relayHandle: response.relayHandle,
|
|
sendGrant: response.sendGrant,
|
|
gatewayDeviceId: gatewayIdentity.deviceId,
|
|
installationId: installationId,
|
|
topic: topic,
|
|
environment: self.buildConfig.apnsEnvironment.rawValue,
|
|
distribution: self.buildConfig.distribution.rawValue,
|
|
relayOrigin: relayOrigin,
|
|
tokenDebugSuffix: registrationState.tokenDebugSuffix))
|
|
}
|
|
|
|
private static func isExpired(_ expiresAtMs: Int64?) -> Bool {
|
|
guard let expiresAtMs else { return true }
|
|
let nowMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
// Refresh shortly before expiry so reconnect-path republishes a live handle.
|
|
return expiresAtMs <= nowMs + 60000
|
|
}
|
|
|
|
private static func validateRelayContract(
|
|
relayProfile: PushRelayProfile,
|
|
apnsEnvironment: PushAPNsEnvironment,
|
|
proofPolicy: PushProofPolicy)
|
|
throws {
|
|
switch relayProfile {
|
|
case .production:
|
|
guard apnsEnvironment == .production, proofPolicy == .appleStrict else {
|
|
throw PushRelayError.relayMisconfigured(
|
|
"production relay profile requires production APNs and appleStrict proof")
|
|
}
|
|
case .deviceSandbox:
|
|
guard apnsEnvironment == .sandbox, proofPolicy == .appleDevelopment else {
|
|
throw PushRelayError.relayMisconfigured(
|
|
"deviceSandbox relay profile requires sandbox APNs and appleDevelopment proof")
|
|
}
|
|
case .simulatorSandbox:
|
|
guard apnsEnvironment == .sandbox, proofPolicy == .internalSimulator else {
|
|
throw PushRelayError.relayMisconfigured(
|
|
"simulatorSandbox relay profile requires sandbox APNs and internalSimulator proof")
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func sha256Hex(_ value: String) -> String {
|
|
let digest = SHA256.hash(data: Data(value.utf8))
|
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
private static func normalizeTokenSuffix(_ value: String?) -> String? {
|
|
guard let value else { return nil }
|
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return trimmed.isEmpty ? nil : trimmed
|
|
}
|
|
|
|
private static func encodePayload(_ payload: some Encodable) throws -> String {
|
|
let data = try JSONEncoder().encode(payload)
|
|
guard let json = String(data: data, encoding: .utf8) else {
|
|
throw PushRelayError.relayMisconfigured("Failed to encode push registration payload as UTF-8")
|
|
}
|
|
return json
|
|
}
|
|
}
|