feat(ios): default to hosted push relay

This commit is contained in:
Nimrod Gutman
2026-05-29 20:24:03 +03:00
parent 7aca070723
commit e2986f827f
8 changed files with 37 additions and 34 deletions

View File

@@ -73,9 +73,10 @@ Release behavior:
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
- See `apps/ios/VERSIONING.md` for the full workflow.
Required env for beta builds:
Relay behavior for beta builds:
- `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
- Beta builds default to `https://ios-push-relay.openclaw.ai`.
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
Archive without upload:
@@ -118,7 +119,7 @@ scripts/ios-asc-keychain-setup.sh \
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
3. Set the official/TestFlight relay URL for the build:
3. Optional: set a custom official/TestFlight relay URL for the build. If unset, the beta flow uses `https://ios-push-relay.openclaw.ai`.
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com

View File

@@ -337,9 +337,9 @@ candidate contains redacted secret placeholders such as `***`.
</Accordion>
<Accordion title="Enable relay-backed push for official iOS builds">
Relay-backed push is configured in `openclaw.json`.
Relay-backed push uses the hosted OpenClaw relay by default: `https://ios-push-relay.openclaw.ai`.
Set this in gateway config:
To use a custom relay, set this in gateway config:
```json5
{
@@ -373,8 +373,8 @@ candidate contains redacted secret placeholders such as `***`.
End-to-end flow:
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
1. Install an official/TestFlight iOS build.
2. Optional: configure `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
@@ -387,6 +387,7 @@ candidate contains redacted secret placeholders such as `***`.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
- Custom gateway relay URLs must match the relay base URL baked into the official/TestFlight iOS build.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.

View File

@@ -75,7 +75,9 @@ openclaw gateway call node.list --params "{}"
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
token to the gateway.
Gateway-side requirement:
By default, official/TestFlight builds and gateways use the hosted relay at `https://ios-push-relay.openclaw.ai`.
Custom relay deployments can override the gateway relay URL:
```json5
{
@@ -98,7 +100,7 @@ How the flow works:
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
- The gateway relay base URL must match the relay URL baked into the official/TestFlight iOS build.
- Custom gateway relay URLs must match the relay URL baked into the official/TestFlight iOS build.
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
What the gateway does **not** need for this path:
@@ -109,7 +111,7 @@ What the gateway does **not** need for this path:
Expected operator flow:
1. Install the official/TestFlight iOS build.
2. Set `gateway.push.apns.relay.baseUrl` on the gateway.
2. Optional: set `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
3. Pair the app to the gateway and let it finish connecting.
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
@@ -128,6 +130,7 @@ compatible but does not count as a durable last-seen update.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
- `OPENCLAW_PUSH_RELAY_BASE_URL` still works as a temporary env override for official/TestFlight iOS builds.
## Authentication and trust flow

View File

@@ -4,6 +4,9 @@ set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/ios-beta-prepare.sh --build-number 7 [--team-id TEAMID]
Optional custom relay:
OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com \
scripts/ios-beta-prepare.sh --build-number 7 [--team-id TEAMID]
@@ -26,7 +29,8 @@ VERSION_SYNC_HELPER="${ROOT_DIR}/scripts/ios-sync-versioning.ts"
BUILD_NUMBER=""
TEAM_ID="${IOS_DEVELOPMENT_TEAM:-}"
PUSH_RELAY_BASE_URL="${OPENCLAW_PUSH_RELAY_BASE_URL:-${IOS_PUSH_RELAY_BASE_URL:-}}"
DEFAULT_IOS_PUSH_RELAY_BASE_URL="https://ios-push-relay.openclaw.ai"
PUSH_RELAY_BASE_URL="${OPENCLAW_PUSH_RELAY_BASE_URL:-${IOS_PUSH_RELAY_BASE_URL:-${DEFAULT_IOS_PUSH_RELAY_BASE_URL}}}"
PUSH_RELAY_BASE_URL_XCCONFIG=""
IOS_VERSION=""
@@ -118,11 +122,6 @@ if [[ -z "${TEAM_ID}" ]]; then
exit 1
fi
if [[ -z "${PUSH_RELAY_BASE_URL}" ]]; then
echo "Missing OPENCLAW_PUSH_RELAY_BASE_URL (or IOS_PUSH_RELAY_BASE_URL) for beta relay registration." >&2
exit 1
fi
validate_push_relay_base_url "${PUSH_RELAY_BASE_URL}"
# `.xcconfig` treats `//` as a comment opener. Break the URL with a helper setting

View File

@@ -552,9 +552,9 @@ export const FIELD_HELP: Record<string, string> = {
"gateway.push.apns":
"APNs delivery settings for iOS devices paired to this gateway. Use relay settings for official/TestFlight builds that register through the external push relay.",
"gateway.push.apns.relay":
"External relay settings for relay-backed APNs sends. The gateway uses this relay for push.test, wake nudges, and reconnect wakes after a paired official iOS build publishes a relay-backed registration.",
"External relay settings for relay-backed APNs sends. The gateway uses the hosted OpenClaw relay by default, or this custom relay for push.test, wake nudges, and reconnect wakes after a paired official iOS build publishes a relay-backed registration.",
"gateway.push.apns.relay.baseUrl":
"Base HTTPS URL for the external APNs relay service used by official/TestFlight iOS builds. Keep this aligned with the relay URL baked into the iOS build so registration and send traffic hit the same deployment.",
"Optional custom base HTTPS URL for the external APNs relay service used by official/TestFlight iOS builds. Keep this aligned with the relay URL baked into the iOS build so registration and send traffic hit the same deployment.",
"gateway.push.apns.relay.timeoutMs":
"Timeout in milliseconds for relay send requests from the gateway to the APNs relay (default: 10000). Increase for slower relays or networks, or lower to fail wake attempts faster.",
"gateway.http.endpoints.chatCompletions.enabled":

View File

@@ -87,7 +87,7 @@ const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.controlUi.basePath": "/openclaw",
"gateway.controlUi.root": "dist/control-ui",
"gateway.controlUi.allowedOrigins": "https://control.example.com",
"gateway.push.apns.relay.baseUrl": "https://relay.example.com",
"gateway.push.apns.relay.baseUrl": "https://ios-push-relay.openclaw.ai",
"channels.mattermost.baseUrl": "https://chat.example.com",
"agents.list[].identity.avatar": "avatars/openclaw.png",
};

View File

@@ -5,7 +5,11 @@ import {
publicKeyRawBase64UrlFromPem,
verifyDeviceSignature,
} from "./device-identity.js";
import { resolveApnsRelayConfigFromEnv, sendApnsRelayPush } from "./push-apns.relay.js";
import {
DEFAULT_APNS_RELAY_BASE_URL,
resolveApnsRelayConfigFromEnv,
sendApnsRelayPush,
} from "./push-apns.relay.js";
const relayGatewayIdentity = (() => {
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
@@ -60,11 +64,10 @@ function firstMockCall<T extends unknown[]>(mock: { mock: { calls: T[] } }): T |
describe("push-apns.relay", () => {
describe("resolveApnsRelayConfigFromEnv", () => {
it("returns a missing-config error when no relay base URL is configured", () => {
expect(resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv)).toEqual({
ok: false,
error:
"APNs relay config missing: set gateway.push.apns.relay.baseUrl or OPENCLAW_APNS_RELAY_BASE_URL",
it("defaults to the hosted relay when no relay base URL is configured", () => {
expectRelayConfig(resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv), {
baseUrl: DEFAULT_APNS_RELAY_BASE_URL,
timeoutMs: 10_000,
});
});

View File

@@ -45,6 +45,7 @@ export type ApnsRelayRequestSender = (params: {
payload: object;
}) => Promise<ApnsRelayPushResponse>;
export const DEFAULT_APNS_RELAY_BASE_URL = "https://ios-push-relay.openclaw.ai";
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";
@@ -113,17 +114,12 @@ export function resolveApnsRelayConfigFromEnv(
const configuredRelay = gatewayConfig?.push?.apns?.relay;
const envBaseUrl = normalizeNonEmptyString(env.OPENCLAW_APNS_RELAY_BASE_URL);
const configBaseUrl = normalizeNonEmptyString(configuredRelay?.baseUrl);
const baseUrl = envBaseUrl ?? configBaseUrl;
const baseUrl = envBaseUrl ?? configBaseUrl ?? DEFAULT_APNS_RELAY_BASE_URL;
const baseUrlSource = envBaseUrl
? "OPENCLAW_APNS_RELAY_BASE_URL"
: "gateway.push.apns.relay.baseUrl";
if (!baseUrl) {
return {
ok: false,
error:
"APNs relay config missing: set gateway.push.apns.relay.baseUrl or OPENCLAW_APNS_RELAY_BASE_URL",
};
}
: configBaseUrl
? "gateway.push.apns.relay.baseUrl"
: "default APNs relay base URL";
try {
const parsed = new URL(baseUrl);