From 3fb1604532ebabde04dd64592fb4befb5329631b Mon Sep 17 00:00:00 2001 From: Nimrod Gutman Date: Thu, 12 Mar 2026 17:11:50 +0200 Subject: [PATCH] feat(push): bind relay registration to gateway identity --- apps/ios/Sources/Model/NodeAppModel.swift | 29 ++++++++- .../Push/PushRegistrationManager.swift | 34 +++++++++-- apps/ios/Sources/Push/PushRelayClient.swift | 7 ++- .../Sources/Push/PushRelayKeychainStore.swift | 4 ++ src/gateway/method-scopes.ts | 1 + src/gateway/server-methods-list.ts | 1 + src/gateway/server-methods/system.ts | 15 +++++ src/gateway/server-node-events.test.ts | 31 ++++++++++ src/gateway/server-node-events.ts | 10 ++++ src/infra/push-apns.relay.ts | 60 +++++++++++++++++-- src/infra/push-apns.test.ts | 34 +++++++++++ src/infra/push-apns.ts | 7 +++ 12 files changed, 221 insertions(+), 12 deletions(-) diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index ee0dfba4a8e..6ae234fc3a0 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -12,6 +12,12 @@ import UserNotifications private struct NotificationCallError: Error, Sendable { let message: String } + +private struct GatewayRelayIdentityResponse: Decodable { + let deviceId: String + let publicKey: String +} + // Ensures notification requests return promptly even if the system prompt blocks. private final class NotificationInvokeLatch: @unchecked Sendable { private let lock = NSLock() @@ -2502,9 +2508,16 @@ extension NodeAppModel { } do { + let gatewayIdentity: PushRelayGatewayIdentity? + if await self.pushRegistrationManager.usesRelayTransport { + gatewayIdentity = try await self.fetchPushRelayGatewayIdentity() + } else { + gatewayIdentity = nil + } let payloadJSON = try await self.pushRegistrationManager.makeGatewayRegistrationPayload( apnsTokenHex: token, - topic: topic) + topic: topic, + gatewayIdentity: gatewayIdentity) await self.nodeGateway.sendEvent(event: "push.apns.register", payloadJSON: payloadJSON) self.apnsLastRegisteredTokenHex = token } catch { @@ -2513,6 +2526,20 @@ extension NodeAppModel { } } + private func fetchPushRelayGatewayIdentity() async throws -> PushRelayGatewayIdentity { + let response = try await self.operatorGateway.request( + method: "gateway.identity.get", + paramsJSON: "{}", + timeoutSeconds: 8) + let decoded = try JSONDecoder().decode(GatewayRelayIdentityResponse.self, from: response) + let deviceId = decoded.deviceId.trimmingCharacters(in: .whitespacesAndNewlines) + let publicKey = decoded.publicKey.trimmingCharacters(in: .whitespacesAndNewlines) + guard !deviceId.isEmpty, !publicKey.isEmpty else { + throw PushRelayError.relayMisconfigured("Gateway identity response missing required fields") + } + return PushRelayGatewayIdentity(deviceId: deviceId, publicKey: publicKey) + } + private static func isSilentPushPayload(_ userInfo: [AnyHashable: Any]) -> Bool { guard let apsAny = userInfo["aps"] else { return false } if let aps = apsAny as? [AnyHashable: Any] { diff --git a/apps/ios/Sources/Push/PushRegistrationManager.swift b/apps/ios/Sources/Push/PushRegistrationManager.swift index c22e142183c..39c9d0ede74 100644 --- a/apps/ios/Sources/Push/PushRegistrationManager.swift +++ b/apps/ios/Sources/Push/PushRegistrationManager.swift @@ -12,6 +12,7 @@ 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 @@ -19,10 +20,19 @@ private struct RelayGatewayPushRegistrationPayload: Encodable { 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) } @@ -30,7 +40,8 @@ actor PushRegistrationManager { func makeGatewayRegistrationPayload( apnsTokenHex: String, - topic: String) + topic: String, + gatewayIdentity: PushRelayGatewayIdentity?) async throws -> String { switch self.buildConfig.transport { case .direct: @@ -40,11 +51,21 @@ actor PushRegistrationManager { topic: topic, environment: self.buildConfig.apnsEnvironment.rawValue)) case .relay: - return try await self.makeRelayPayload(apnsTokenHex: apnsTokenHex, topic: topic) + 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) async throws -> String { + private func makeRelayPayload( + apnsTokenHex: String, + topic: String, + gatewayIdentity: PushRelayGatewayIdentity) + async throws -> String { guard self.buildConfig.distribution == .official else { throw PushRelayError.relayMisconfigured( "Relay transport requires OpenClawPushDistribution=official") @@ -71,6 +92,7 @@ actor PushRegistrationManager { let tokenHashHex = Self.sha256Hex(apnsTokenHex) if let stored = PushRelayRegistrationStore.loadRegistrationState(), stored.installationId == installationId, + stored.gatewayDeviceId == gatewayIdentity.deviceId, stored.lastAPNsTokenHashHex == tokenHashHex, !Self.isExpired(stored.relayHandleExpiresAtMs) { @@ -78,6 +100,7 @@ actor PushRegistrationManager { RelayGatewayPushRegistrationPayload( relayHandle: stored.relayHandle, sendGrant: stored.sendGrant, + gatewayDeviceId: gatewayIdentity.deviceId, installationId: installationId, topic: topic, environment: self.buildConfig.apnsEnvironment.rawValue, @@ -91,10 +114,12 @@ actor PushRegistrationManager { appVersion: DeviceInfoHelper.appVersion(), environment: self.buildConfig.apnsEnvironment, distribution: self.buildConfig.distribution, - apnsTokenHex: apnsTokenHex) + apnsTokenHex: apnsTokenHex, + gatewayIdentity: gatewayIdentity) let registrationState = PushRelayRegistrationStore.RegistrationState( relayHandle: response.relayHandle, sendGrant: response.sendGrant, + gatewayDeviceId: gatewayIdentity.deviceId, relayHandleExpiresAtMs: response.expiresAtMs, tokenDebugSuffix: Self.normalizeTokenSuffix(response.tokenSuffix), lastAPNsTokenHashHex: tokenHashHex, @@ -105,6 +130,7 @@ actor PushRegistrationManager { RelayGatewayPushRegistrationPayload( relayHandle: response.relayHandle, sendGrant: response.sendGrant, + gatewayDeviceId: gatewayIdentity.deviceId, installationId: installationId, topic: topic, environment: self.buildConfig.apnsEnvironment.rawValue, diff --git a/apps/ios/Sources/Push/PushRelayClient.swift b/apps/ios/Sources/Push/PushRelayClient.swift index e2e179feccc..fb869350add 100644 --- a/apps/ios/Sources/Push/PushRelayClient.swift +++ b/apps/ios/Sources/Push/PushRelayClient.swift @@ -41,6 +41,7 @@ private struct PushRelayRegisterSignedPayload: Encodable { var bundleId: String var environment: String var distribution: String + var gateway: PushRelayGatewayIdentity var appVersion: String var apnsToken: String } @@ -63,6 +64,7 @@ private struct PushRelayRegisterRequest: Encodable { var bundleId: String var environment: String var distribution: String + var gateway: PushRelayGatewayIdentity var appVersion: String var apnsToken: String var appAttest: PushRelayAppAttestPayload @@ -233,7 +235,8 @@ final class PushRelayClient: @unchecked Sendable { appVersion: String, environment: PushAPNsEnvironment, distribution: PushDistributionMode, - apnsTokenHex: String) + apnsTokenHex: String, + gatewayIdentity: PushRelayGatewayIdentity) async throws -> PushRelayRegisterResponse { let challenge = try await self.fetchChallenge() let signedPayload = PushRelayRegisterSignedPayload( @@ -242,6 +245,7 @@ final class PushRelayClient: @unchecked Sendable { bundleId: bundleId, environment: environment.rawValue, distribution: distribution.rawValue, + gateway: gatewayIdentity, appVersion: appVersion, apnsToken: apnsTokenHex) let signedPayloadData = try self.jsonEncoder.encode(signedPayload) @@ -255,6 +259,7 @@ final class PushRelayClient: @unchecked Sendable { bundleId: signedPayload.bundleId, environment: signedPayload.environment, distribution: signedPayload.distribution, + gateway: signedPayload.gateway, appVersion: signedPayload.appVersion, apnsToken: signedPayload.apnsToken, appAttest: PushRelayAppAttestPayload( diff --git a/apps/ios/Sources/Push/PushRelayKeychainStore.swift b/apps/ios/Sources/Push/PushRelayKeychainStore.swift index 02794c2aa21..b16790b31c1 100644 --- a/apps/ios/Sources/Push/PushRelayKeychainStore.swift +++ b/apps/ios/Sources/Push/PushRelayKeychainStore.swift @@ -3,6 +3,7 @@ import Foundation private struct StoredPushRelayRegistrationState: Codable { var relayHandle: String var sendGrant: String + var gatewayDeviceId: String var relayHandleExpiresAtMs: Int64? var tokenDebugSuffix: String? var lastAPNsTokenHashHex: String @@ -19,6 +20,7 @@ enum PushRelayRegistrationStore { struct RegistrationState: Codable { var relayHandle: String var sendGrant: String + var gatewayDeviceId: String var relayHandleExpiresAtMs: Int64? var tokenDebugSuffix: String? var lastAPNsTokenHashHex: String @@ -38,6 +40,7 @@ enum PushRelayRegistrationStore { return RegistrationState( relayHandle: decoded.relayHandle, sendGrant: decoded.sendGrant, + gatewayDeviceId: decoded.gatewayDeviceId, relayHandleExpiresAtMs: decoded.relayHandleExpiresAtMs, tokenDebugSuffix: decoded.tokenDebugSuffix, lastAPNsTokenHashHex: decoded.lastAPNsTokenHashHex, @@ -50,6 +53,7 @@ enum PushRelayRegistrationStore { let stored = StoredPushRelayRegistrationState( relayHandle: state.relayHandle, sendGrant: state.sendGrant, + gatewayDeviceId: state.gatewayDeviceId, relayHandleExpiresAtMs: state.relayHandleExpiresAtMs, tokenDebugSuffix: state.tokenDebugSuffix, lastAPNsTokenHashHex: state.lastAPNsTokenHashHex, diff --git a/src/gateway/method-scopes.ts b/src/gateway/method-scopes.ts index ec8279a1947..f4f57259212 100644 --- a/src/gateway/method-scopes.ts +++ b/src/gateway/method-scopes.ts @@ -75,6 +75,7 @@ const METHOD_SCOPE_GROUPS: Record = { "cron.list", "cron.status", "cron.runs", + "gateway.identity.get", "system-presence", "last-heartbeat", "node.list", diff --git a/src/gateway/server-methods-list.ts b/src/gateway/server-methods-list.ts index 2785eb7957e..205bb633e70 100644 --- a/src/gateway/server-methods-list.ts +++ b/src/gateway/server-methods-list.ts @@ -91,6 +91,7 @@ const BASE_METHODS = [ "cron.remove", "cron.run", "cron.runs", + "gateway.identity.get", "system-presence", "system-event", "send", diff --git a/src/gateway/server-methods/system.ts b/src/gateway/server-methods/system.ts index 7ee8ac35d7d..99853bcaecf 100644 --- a/src/gateway/server-methods/system.ts +++ b/src/gateway/server-methods/system.ts @@ -1,4 +1,8 @@ import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js"; +import { + loadOrCreateDeviceIdentity, + publicKeyRawBase64UrlFromPem, +} from "../../infra/device-identity.js"; import { getLastHeartbeatEvent } from "../../infra/heartbeat-events.js"; import { setHeartbeatsEnabled } from "../../infra/heartbeat-runner.js"; import { enqueueSystemEvent, isSystemEventContextChanged } from "../../infra/system-events.js"; @@ -8,6 +12,17 @@ import { broadcastPresenceSnapshot } from "../server/presence-events.js"; import type { GatewayRequestHandlers } from "./types.js"; export const systemHandlers: GatewayRequestHandlers = { + "gateway.identity.get": ({ respond }) => { + const identity = loadOrCreateDeviceIdentity(); + respond( + true, + { + deviceId: identity.deviceId, + publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem), + }, + undefined, + ); + }, "last-heartbeat": ({ respond }) => { respond(true, getLastHeartbeatEvent(), undefined); }, diff --git a/src/gateway/server-node-events.test.ts b/src/gateway/server-node-events.test.ts index 7025072469c..07425808cea 100644 --- a/src/gateway/server-node-events.test.ts +++ b/src/gateway/server-node-events.test.ts @@ -26,6 +26,13 @@ const buildSessionLookup = ( const ingressAgentCommandMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); const registerApnsRegistrationMock = vi.hoisted(() => vi.fn()); +const loadOrCreateDeviceIdentityMock = vi.hoisted(() => + vi.fn(() => ({ + deviceId: "gateway-device-1", + publicKeyPem: "public", + privateKeyPem: "private", + })), +); vi.mock("../infra/system-events.js", () => ({ enqueueSystemEvent: vi.fn(), @@ -47,6 +54,9 @@ vi.mock("../config/sessions.js", () => ({ vi.mock("../infra/push-apns.js", () => ({ registerApnsRegistration: registerApnsRegistrationMock, })); +vi.mock("../infra/device-identity.js", () => ({ + loadOrCreateDeviceIdentity: loadOrCreateDeviceIdentityMock, +})); vi.mock("./session-utils.js", () => ({ loadSessionEntry: vi.fn((sessionKey: string) => buildSessionLookup(sessionKey)), pruneLegacyStoreKeys: vi.fn(), @@ -104,6 +114,7 @@ describe("node exec events", () => { enqueueSystemEventMock.mockClear(); requestHeartbeatNowMock.mockClear(); registerApnsRegistrationVi.mockClear(); + loadOrCreateDeviceIdentityMock.mockClear(); }); it("enqueues exec.started events", async () => { @@ -291,6 +302,7 @@ describe("node exec events", () => { transport: "relay", relayHandle: "relay-handle-123", sendGrant: "send-grant-123", + gatewayDeviceId: "gateway-device-1", installationId: "install-123", topic: "ai.openclaw.ios", environment: "production", @@ -311,6 +323,25 @@ describe("node exec events", () => { tokenDebugSuffix: "abcd1234", }); }); + + it("rejects relay registrations bound to a different gateway identity", async () => { + const ctx = buildCtx(); + await handleNodeEvent(ctx, "node-relay", { + event: "push.apns.register", + payloadJSON: JSON.stringify({ + transport: "relay", + relayHandle: "relay-handle-123", + sendGrant: "send-grant-123", + gatewayDeviceId: "gateway-device-other", + installationId: "install-123", + topic: "ai.openclaw.ios", + environment: "production", + distribution: "official", + }), + }); + + expect(registerApnsRegistrationVi).not.toHaveBeenCalled(); + }); }); describe("voice transcript events", () => { diff --git a/src/gateway/server-node-events.ts b/src/gateway/server-node-events.ts index bb1fba16c2e..169b0040297 100644 --- a/src/gateway/server-node-events.ts +++ b/src/gateway/server-node-events.ts @@ -4,6 +4,7 @@ import { createOutboundSendDeps } from "../cli/outbound-send-deps.js"; import { agentCommandFromIngress } from "../commands/agent.js"; import { loadConfig } from "../config/config.js"; import { updateSessionStore } from "../config/sessions.js"; +import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js"; import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; import { deliverOutboundPayloads } from "../infra/outbound/deliver.js"; import { buildOutboundSessionContext } from "../infra/outbound/session-context.js"; @@ -594,6 +595,15 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt const environment = obj.environment; try { if (transport === "relay") { + const gatewayDeviceId = + typeof obj.gatewayDeviceId === "string" ? obj.gatewayDeviceId.trim() : ""; + const currentGatewayDeviceId = loadOrCreateDeviceIdentity().deviceId; + if (!gatewayDeviceId || gatewayDeviceId !== currentGatewayDeviceId) { + ctx.logGateway.warn( + `push relay register rejected node=${nodeId}: gateway identity mismatch`, + ); + return; + } await registerApnsRegistration({ nodeId, transport: "relay", diff --git a/src/infra/push-apns.relay.ts b/src/infra/push-apns.relay.ts index af851a1483b..1b3251e6713 100644 --- a/src/infra/push-apns.relay.ts +++ b/src/infra/push-apns.relay.ts @@ -1,5 +1,10 @@ import { URL } from "node:url"; import type { GatewayConfig } from "../config/types.gateway.js"; +import { + loadOrCreateDeviceIdentity, + signDevicePayload, + type DeviceIdentity, +} from "./device-identity.js"; export type ApnsRelayPushType = "alert" | "background"; @@ -25,12 +30,19 @@ export type ApnsRelayRequestSender = (params: { relayConfig: ApnsRelayConfig; sendGrant: string; relayHandle: string; + gatewayDeviceId: string; + signature: string; + signedAtMs: number; + bodyJson: string; pushType: ApnsRelayPushType; priority: "10" | "5"; payload: object; }) => Promise; const DEFAULT_APNS_RELAY_TIMEOUT_MS = 10_000; +const GATEWAY_DEVICE_ID_HEADER = "x-openclaw-gateway-device-id"; +const GATEWAY_SIGNATURE_HEADER = "x-openclaw-gateway-signature"; +const GATEWAY_SIGNED_AT_HEADER = "x-openclaw-gateway-signed-at-ms"; function normalizeNonEmptyString(value: string | undefined): string | null { const trimmed = value?.trim() ?? ""; @@ -69,6 +81,19 @@ function parseReason(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } +function buildRelayGatewaySignaturePayload(params: { + gatewayDeviceId: string; + signedAtMs: number; + bodyJson: string; +}): string { + return [ + "openclaw-relay-send-v1", + params.gatewayDeviceId.trim(), + String(Math.trunc(params.signedAtMs)), + params.bodyJson, + ].join("\n"); +} + export function resolveApnsRelayConfigFromEnv( env: NodeJS.ProcessEnv = process.env, gatewayConfig?: GatewayConfig, @@ -132,6 +157,10 @@ async function sendApnsRelayRequest(params: { relayConfig: ApnsRelayConfig; sendGrant: string; relayHandle: string; + gatewayDeviceId: string; + signature: string; + signedAtMs: number; + bodyJson: string; pushType: ApnsRelayPushType; priority: "10" | "5"; payload: object; @@ -142,13 +171,11 @@ async function sendApnsRelayRequest(params: { headers: { authorization: `Bearer ${params.sendGrant}`, "content-type": "application/json", + [GATEWAY_DEVICE_ID_HEADER]: params.gatewayDeviceId, + [GATEWAY_SIGNATURE_HEADER]: params.signature, + [GATEWAY_SIGNED_AT_HEADER]: String(params.signedAtMs), }, - body: JSON.stringify({ - relayHandle: params.relayHandle, - pushType: params.pushType, - priority: Number(params.priority), - payload: params.payload, - }), + body: params.bodyJson, signal: AbortSignal.timeout(params.relayConfig.timeoutMs), }); if (response.status >= 300 && response.status < 400) { @@ -192,13 +219,34 @@ export async function sendApnsRelayPush(params: { pushType: ApnsRelayPushType; priority: "10" | "5"; payload: object; + gatewayIdentity?: Pick; requestSender?: ApnsRelayRequestSender; }): Promise { const sender = params.requestSender ?? sendApnsRelayRequest; + const gatewayIdentity = params.gatewayIdentity ?? loadOrCreateDeviceIdentity(); + const signedAtMs = Date.now(); + const bodyJson = JSON.stringify({ + relayHandle: params.relayHandle, + pushType: params.pushType, + priority: Number(params.priority), + payload: params.payload, + }); + const signature = signDevicePayload( + gatewayIdentity.privateKeyPem, + buildRelayGatewaySignaturePayload({ + gatewayDeviceId: gatewayIdentity.deviceId, + signedAtMs, + bodyJson, + }), + ); return await sender({ relayConfig: params.relayConfig, sendGrant: params.sendGrant, relayHandle: params.relayHandle, + gatewayDeviceId: gatewayIdentity.deviceId, + signature, + signedAtMs, + bodyJson, pushType: params.pushType, priority: params.priority, payload: params.payload, diff --git a/src/infra/push-apns.test.ts b/src/infra/push-apns.test.ts index e85e134d01f..cd35e844398 100644 --- a/src/infra/push-apns.test.ts +++ b/src/infra/push-apns.test.ts @@ -3,6 +3,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { + deriveDeviceIdFromPublicKey, + publicKeyRawBase64UrlFromPem, + verifyDeviceSignature, +} from "./device-identity.js"; import { clearApnsRegistration, clearApnsRegistrationIfCurrent, @@ -23,6 +28,20 @@ const tempDirs: string[] = []; const testAuthPrivateKey = generateKeyPairSync("ec", { namedCurve: "prime256v1" }) .privateKey.export({ format: "pem", type: "pkcs8" }) .toString(); +const relayGatewayIdentity = (() => { + const { publicKey, privateKey } = generateKeyPairSync("ed25519"); + const publicKeyPem = publicKey.export({ format: "pem", type: "spki" }).toString(); + const publicKeyRaw = publicKeyRawBase64UrlFromPem(publicKeyPem); + const deviceId = deriveDeviceIdFromPublicKey(publicKeyRaw); + if (!deviceId) { + throw new Error("failed to derive test gateway device id"); + } + return { + deviceId, + publicKey: publicKeyRaw, + privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(), + }; +})(); async function makeTempDir(): Promise { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-push-apns-test-")); @@ -545,12 +564,14 @@ describe("push APNs send semantics", () => { nodeId: "ios-node-relay", title: "Wake", body: "Ping", + relayGatewayIdentity: relayGatewayIdentity, relayRequestSender: send, }); expect(send).toHaveBeenCalledTimes(1); expect(send.mock.calls[0]?.[0]).toMatchObject({ relayHandle: "relay-handle-123", + gatewayDeviceId: relayGatewayIdentity.deviceId, pushType: "alert", priority: "10", payload: { @@ -560,6 +581,18 @@ describe("push APNs send semantics", () => { }, }, }); + const sent = send.mock.calls[0]?.[0]; + expect(typeof sent?.signature).toBe("string"); + expect(typeof sent?.signedAtMs).toBe("number"); + const signedPayload = [ + "openclaw-relay-send-v1", + sent?.gatewayDeviceId, + String(sent?.signedAtMs), + sent?.bodyJson, + ].join("\n"); + expect( + verifyDeviceSignature(relayGatewayIdentity.publicKey, signedPayload, sent?.signature), + ).toBe(true); expect(result).toMatchObject({ ok: true, status: 200, @@ -587,6 +620,7 @@ describe("push APNs send semantics", () => { payload: { aps: { "content-available": 1 } }, pushType: "background", priority: "5", + gatewayIdentity: relayGatewayIdentity, }); expect(fetchMock).toHaveBeenCalledTimes(1); diff --git a/src/infra/push-apns.ts b/src/infra/push-apns.ts index 026ca398e8d..cd19f77c698 100644 --- a/src/infra/push-apns.ts +++ b/src/infra/push-apns.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import http2 from "node:http2"; import path from "node:path"; import { resolveStateDir } from "../config/paths.js"; +import type { DeviceIdentity } from "./device-identity.js"; import { createAsyncLock, readJsonFile, writeJsonAtomic } from "./json-files.js"; import { type ApnsRelayConfig, @@ -826,6 +827,7 @@ async function sendRelayApnsPush(params: { payload: object; pushType: ApnsPushType; priority: "10" | "5"; + gatewayIdentity?: Pick; requestSender?: ApnsRelayRequestSender; }): Promise { const response = await sendApnsRelayPush({ @@ -835,6 +837,7 @@ async function sendRelayApnsPush(params: { payload: params.payload, pushType: params.pushType, priority: params.priority, + gatewayIdentity: params.gatewayIdentity, requestSender: params.requestSender, }); return toPushResult({ registration: params.registration, response }); @@ -888,6 +891,7 @@ type RelayApnsAlertParams = ApnsAlertCommonParams & { registration: RelayApnsRegistration; relayConfig: ApnsRelayConfig; relayRequestSender?: ApnsRelayRequestSender; + relayGatewayIdentity?: Pick; auth?: never; requestSender?: never; }; @@ -910,6 +914,7 @@ type RelayApnsBackgroundWakeParams = ApnsBackgroundWakeCommonParams & { registration: RelayApnsRegistration; relayConfig: ApnsRelayConfig; relayRequestSender?: ApnsRelayRequestSender; + relayGatewayIdentity?: Pick; auth?: never; requestSender?: never; }; @@ -931,6 +936,7 @@ export async function sendApnsAlert( payload, pushType: "alert", priority: "10", + gatewayIdentity: relayParams.relayGatewayIdentity, requestSender: relayParams.relayRequestSender, }); } @@ -962,6 +968,7 @@ export async function sendApnsBackgroundWake( payload, pushType: "background", priority: "5", + gatewayIdentity: relayParams.relayGatewayIdentity, requestSender: relayParams.relayRequestSender, }); }