feat(ios): default to hosted push relay (#88096)

Merged via squash.

Prepared head SHA: 75f939af5c
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
This commit is contained in:
Nimrod Gutman
2026-05-29 22:05:25 +03:00
committed by GitHub
parent 11e82bdef2
commit 0167f0a6df
16 changed files with 218 additions and 91 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

@@ -17,6 +17,7 @@ private struct RelayGatewayPushRegistrationPayload: Encodable {
var topic: String
var environment: String
var distribution: String
var relayOrigin: String
var tokenDebugSuffix: String?
}
@@ -107,6 +108,7 @@ actor PushRegistrationManager {
topic: topic,
environment: self.buildConfig.apnsEnvironment.rawValue,
distribution: self.buildConfig.distribution.rawValue,
relayOrigin: relayOrigin,
tokenDebugSuffix: stored.tokenDebugSuffix))
}
@@ -138,6 +140,7 @@ actor PushRegistrationManager {
topic: topic,
environment: self.buildConfig.apnsEnvironment.rawValue,
distribution: self.buildConfig.distribution.rawValue,
relayOrigin: relayOrigin,
tokenDebugSuffix: registrationState.tokenDebugSuffix))
}

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

@@ -157,19 +157,30 @@ async function resolveDeliveryPlan(params: {
}
}
let relayConfig: ApnsRelayConfig | undefined;
const relayConfigByNodeId = new Map<string, ApnsRelayConfig>();
if (needsRelay) {
const relay = resolveApnsRelayConfigFromEnv(process.env, getRuntimeConfig().gateway);
if (relay.ok) {
relayConfig = relay.value;
} else {
params.log.warn?.(`exec approvals: iOS relay APNs config unavailable: ${relay.error}`);
for (const target of targets) {
if (target.registration.transport !== "relay") {
continue;
}
const relay = resolveApnsRelayConfigFromEnv(process.env, getRuntimeConfig().gateway, {
registrationRelayOrigin: target.registration.relayOrigin,
});
if (relay.ok) {
relayConfigByNodeId.set(target.nodeId, relay.value);
} else {
params.log.warn?.(`exec approvals: iOS relay APNs config unavailable: ${relay.error}`);
}
}
}
const relayConfig = relayConfigByNodeId.values().next().value;
return {
targets: targets.filter((target) =>
target.registration.transport === "direct" ? Boolean(directAuth) : Boolean(relayConfig),
target.registration.transport === "direct"
? Boolean(directAuth)
: relayConfigByNodeId.has(target.nodeId) &&
relayConfigByNodeId.get(target.nodeId)?.baseUrl === relayConfig?.baseUrl,
),
directAuth,
relayConfig,

View File

@@ -729,13 +729,17 @@ describe("node.invoke APNs wake path", () => {
apnsReason: "Unregistered",
apnsStatus: 410,
});
expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(process.env, {
push: {
apns: {
relay: DEFAULT_RELAY_CONFIG,
expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(
process.env,
{
push: {
apns: {
relay: DEFAULT_RELAY_CONFIG,
},
},
},
});
{ registrationRelayOrigin: undefined },
);
expect(mocks.shouldClearStoredApnsRegistration).toHaveBeenCalledWith({
registration,
result: {

View File

@@ -199,8 +199,16 @@ async function resolveDirectNodePushConfig() {
: { ok: false as const, error: auth.error };
}
function resolveRelayNodePushConfig(cfg: OpenClawConfig) {
const relay = resolveApnsRelayConfigFromEnv(process.env, cfg.gateway);
function resolveRelayNodePushConfig(
cfg: OpenClawConfig,
registration: Extract<
NonNullable<Awaited<ReturnType<typeof loadApnsRegistration>>>,
{ transport: "relay" }
>,
) {
const relay = resolveApnsRelayConfigFromEnv(process.env, cfg.gateway, {
registrationRelayOrigin: registration.relayOrigin,
});
return relay.ok
? { ok: true as const, relayConfig: relay.value }
: { ok: false as const, error: relay.error };
@@ -493,7 +501,7 @@ export async function maybeWakeNodeWithApns(
let wakeResult;
if (registration.transport === "relay") {
const relay = resolveRelayNodePushConfig(opts?.cfg ?? getRuntimeConfig());
const relay = resolveRelayNodePushConfig(opts?.cfg ?? getRuntimeConfig(), registration);
if (!relay.ok) {
return withDuration({
available: false,
@@ -595,7 +603,7 @@ export async function maybeSendNodeWakeNudge(
try {
let result;
if (registration.transport === "relay") {
const relay = resolveRelayNodePushConfig(opts?.cfg ?? getRuntimeConfig());
const relay = resolveRelayNodePushConfig(opts?.cfg ?? getRuntimeConfig(), registration);
if (!relay.ok) {
return withDuration({
sent: false,

View File

@@ -209,16 +209,20 @@ describe("push.test handler", () => {
expect(resolveApnsAuthConfigFromEnv).not.toHaveBeenCalled();
expect(resolveApnsRelayConfigFromEnv).toHaveBeenCalledTimes(1);
expect(resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(process.env, {
push: {
apns: {
relay: {
baseUrl: "https://relay.example.com",
timeoutMs: 1000,
expect(resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(
process.env,
{
push: {
apns: {
relay: {
baseUrl: "https://relay.example.com",
timeoutMs: 1000,
},
},
},
},
});
{ registrationRelayOrigin: undefined },
);
expect(sendApnsAlert).toHaveBeenCalledTimes(1);
const call = firstRespondCall(respond);
expect(call?.[0]).toBe(true);

View File

@@ -85,6 +85,7 @@ export const pushHandlers: GatewayRequestHandlers = {
const relay = resolveApnsRelayConfigFromEnv(
process.env,
context.getRuntimeConfig().gateway,
{ registrationRelayOrigin: registration.relayOrigin },
);
if (!relay.ok) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, relay.error));

View File

@@ -828,6 +828,7 @@ export const handleNodeEvent = async (
topic,
environment,
distribution: obj.distribution,
relayOrigin: obj.relayOrigin,
tokenDebugSuffix: obj.tokenDebugSuffix,
});
} else {

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,12 +64,38 @@ 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 the registration was minted by the hosted relay", () => {
expectRelayConfig(
resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv, undefined, {
registrationRelayOrigin: `${DEFAULT_APNS_RELAY_BASE_URL}/`,
}),
{
baseUrl: DEFAULT_APNS_RELAY_BASE_URL,
timeoutMs: 10_000,
},
);
});
it("fails closed when relay registration origin is unknown and no relay URL is configured", () => {
const resolved = resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv);
expect(resolved.ok).toBe(false);
if (!resolved.ok) {
expect(resolved.error).toContain("relay registrations without the hosted relay origin");
}
});
it("rejects config that does not match the registration relay origin", () => {
const resolved = resolveApnsRelayConfigFromEnv(
{} as NodeJS.ProcessEnv,
{ push: { apns: { relay: { baseUrl: DEFAULT_APNS_RELAY_BASE_URL } } } },
{ registrationRelayOrigin: "https://relay.example.com" },
);
expect(resolved.ok).toBe(false);
if (!resolved.ok) {
expect(resolved.error).toContain("origin mismatch");
}
});
it("lets env overrides win and clamps tiny timeout values", () => {

View File

@@ -23,6 +23,10 @@ type ApnsRelayConfigResolution =
| { ok: true; value: ApnsRelayConfig }
| { ok: false; error: string };
type ApnsRelayConfigResolutionOptions = {
registrationRelayOrigin?: string;
};
export type ApnsRelayPushResponse = {
ok: boolean;
status: number;
@@ -45,6 +49,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";
@@ -93,38 +98,10 @@ function parseReason(value: unknown): string | undefined {
return typeof value === "string" ? normalizeOptionalString(value) : 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(
export function normalizeApnsRelayBaseUrl(
baseUrl: string,
env: NodeJS.ProcessEnv = process.env,
gatewayConfig?: GatewayConfig,
): ApnsRelayConfigResolution {
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 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",
};
}
): { ok: true; value: string } | { ok: false; error: string } {
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
@@ -147,22 +124,87 @@ export function resolveApnsRelayConfigFromEnv(
if (parsed.search || parsed.hash) {
throw new Error("query and fragment are not allowed");
}
return {
ok: true,
value: {
baseUrl: parsed.toString().replace(/\/+$/, ""),
timeoutMs: normalizeTimeoutMs(
env.OPENCLAW_APNS_RELAY_TIMEOUT_MS ?? configuredRelay?.timeoutMs,
),
},
};
return { ok: true, value: parsed.toString().replace(/\/+$/, "") };
} catch (err) {
const message = formatErrorMessage(err);
return { ok: false, error: formatErrorMessage(err) };
}
}
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,
options: ApnsRelayConfigResolutionOptions = {},
): ApnsRelayConfigResolution {
const configuredRelay = gatewayConfig?.push?.apns?.relay;
const envBaseUrl = normalizeNonEmptyString(env.OPENCLAW_APNS_RELAY_BASE_URL);
const configBaseUrl = normalizeNonEmptyString(configuredRelay?.baseUrl);
const explicitBaseUrl = envBaseUrl ?? configBaseUrl;
const normalizedRegistrationOrigin = options.registrationRelayOrigin
? normalizeApnsRelayBaseUrl(options.registrationRelayOrigin, env)
: undefined;
if (normalizedRegistrationOrigin && !normalizedRegistrationOrigin.ok) {
return {
ok: false,
error: `invalid ${baseUrlSource} (${baseUrl}): ${message}`,
error: `invalid relay registration origin (${options.registrationRelayOrigin}): ${normalizedRegistrationOrigin.error}`,
};
}
const baseUrl =
explicitBaseUrl ??
(normalizedRegistrationOrigin?.value === DEFAULT_APNS_RELAY_BASE_URL
? DEFAULT_APNS_RELAY_BASE_URL
: undefined);
const baseUrlSource = envBaseUrl
? "OPENCLAW_APNS_RELAY_BASE_URL"
: configBaseUrl
? "gateway.push.apns.relay.baseUrl"
: "default APNs relay base URL";
if (!baseUrl) {
return {
ok: false,
error:
"APNs relay config missing: set gateway.push.apns.relay.baseUrl or OPENCLAW_APNS_RELAY_BASE_URL for relay registrations without the hosted relay origin",
};
}
const normalizedBaseUrl = normalizeApnsRelayBaseUrl(baseUrl, env);
if (!normalizedBaseUrl.ok) {
return {
ok: false,
error: `invalid ${baseUrlSource} (${baseUrl}): ${normalizedBaseUrl.error}`,
};
}
if (
normalizedRegistrationOrigin &&
normalizedRegistrationOrigin.value !== normalizedBaseUrl.value
) {
return {
ok: false,
error: `APNs relay config origin mismatch: registration uses ${normalizedRegistrationOrigin.value} but ${baseUrlSource} is ${normalizedBaseUrl.value}`,
};
}
return {
ok: true,
value: {
baseUrl: normalizedBaseUrl.value,
timeoutMs: normalizeTimeoutMs(
env.OPENCLAW_APNS_RELAY_TIMEOUT_MS ?? configuredRelay?.timeoutMs,
),
},
};
}
async function sendApnsRelayRequest(params: {

View File

@@ -14,6 +14,7 @@ import {
type ApnsRelayConfig,
type ApnsRelayPushResponse,
type ApnsRelayRequestSender,
normalizeApnsRelayBaseUrl,
resolveApnsRelayConfigFromEnv,
sendApnsRelayPush,
} from "./push-apns.relay.js";
@@ -40,6 +41,7 @@ type RelayApnsRegistration = {
environment: "production";
distribution: "official";
updatedAtMs: number;
relayOrigin?: string;
tokenDebugSuffix?: string;
};
@@ -109,6 +111,7 @@ type RegisterRelayApnsParams = {
topic: string;
environment?: unknown;
distribution?: unknown;
relayOrigin?: unknown;
tokenDebugSuffix?: unknown;
baseDir?: string;
};
@@ -263,6 +266,18 @@ function normalizeDistribution(value: unknown): "official" | null {
return normalized === "official" ? "official" : null;
}
function normalizeRelayOrigin(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
return undefined;
}
const normalized = normalizeApnsRelayBaseUrl(trimmed, process.env);
return normalized.ok ? normalized.value : undefined;
}
function normalizeDirectRegistration(
record: Partial<DirectApnsRegistration> & { nodeId?: unknown; token?: unknown },
): DirectApnsRegistration | null {
@@ -312,6 +327,7 @@ function normalizeRelayRegistration(
const topic = normalizeTopic(typeof record.topic === "string" ? record.topic : "");
const environment = normalizeApnsEnvironment(record.environment);
const distribution = normalizeDistribution(record.distribution);
const relayOrigin = normalizeRelayOrigin(record.relayOrigin);
const updatedAtMs =
typeof record.updatedAtMs === "number" && Number.isFinite(record.updatedAtMs)
? Math.trunc(record.updatedAtMs)
@@ -337,6 +353,7 @@ function normalizeRelayRegistration(
environment,
distribution,
updatedAtMs,
...(relayOrigin ? { relayOrigin } : {}),
tokenDebugSuffix: normalizeTokenDebugSuffix(record.tokenDebugSuffix),
};
}
@@ -433,6 +450,7 @@ export async function registerApnsRegistration(
);
const environment = normalizeApnsEnvironment(params.environment);
const distribution = normalizeDistribution(params.distribution);
const relayOrigin = normalizeRelayOrigin(params.relayOrigin);
if (environment !== "production") {
throw new Error("relay registrations must use production environment");
}
@@ -449,6 +467,7 @@ export async function registerApnsRegistration(
environment,
distribution,
updatedAtMs,
...(relayOrigin ? { relayOrigin } : {}),
tokenDebugSuffix: normalizeTokenDebugSuffix(params.tokenDebugSuffix),
};
} else {