mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 04:20:44 +00:00
feat(push): configure ios relay through gateway config
This commit is contained in:
@@ -1183,7 +1183,15 @@ final class NodeAppModel {
|
||||
_ = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
return await self.notificationAuthorizationStatus()
|
||||
let updatedStatus = await self.notificationAuthorizationStatus()
|
||||
if Self.isNotificationAuthorizationAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return updatedStatus
|
||||
}
|
||||
|
||||
private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus {
|
||||
@@ -1198,6 +1206,17 @@ final class NodeAppModel {
|
||||
}
|
||||
}
|
||||
|
||||
private static func isNotificationAuthorizationAllowed(
|
||||
_ status: NotificationAuthorizationStatus
|
||||
) -> Bool {
|
||||
switch status {
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
true
|
||||
case .denied, .notDetermined:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private func runNotificationCall<T: Sendable>(
|
||||
timeoutSeconds: Double,
|
||||
operation: @escaping @Sendable () async throws -> T
|
||||
|
||||
@@ -407,6 +407,13 @@ enum WatchPromptNotificationBridge {
|
||||
let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
if !granted { return false }
|
||||
let updatedStatus = await self.notificationAuthorizationStatus(center: center)
|
||||
if self.isAuthorizationStatusAllowed(updatedStatus) {
|
||||
// Refresh APNs registration immediately after the first permission grant so the
|
||||
// gateway can receive a push registration without requiring an app relaunch.
|
||||
await MainActor.run {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
return self.isAuthorizationStatusAllowed(updatedStatus)
|
||||
case .denied:
|
||||
return false
|
||||
|
||||
@@ -2445,6 +2445,14 @@ See [Plugins](/tools/plugin).
|
||||
// Remove tools from the default HTTP deny list
|
||||
allow: ["gateway"],
|
||||
},
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -2470,6 +2478,10 @@ See [Plugins](/tools/plugin).
|
||||
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
|
||||
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side break-glass override that allows plaintext `ws://` to trusted private-network IPs; default remains loopback-only for plaintext.
|
||||
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
|
||||
- `gateway.push.apns.relay.baseUrl`: base HTTPS URL for the external APNs relay used by official/TestFlight iOS builds after they publish relay-backed registrations to the gateway.
|
||||
- `gateway.push.apns.relay.timeoutMs`: gateway-to-relay send timeout in milliseconds. Defaults to `10000`.
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above.
|
||||
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true`: development-only escape hatch for loopback HTTP relay URLs. Production relay URLs should stay on HTTPS.
|
||||
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
|
||||
|
||||
@@ -225,6 +225,49 @@ When validation fails:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Enable relay-backed push for official iOS builds">
|
||||
Relay-backed push is configured in `openclaw.json`.
|
||||
|
||||
Set this in gateway config:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
// Optional. Default: 10000
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
CLI equivalent:
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.push.apns.relay.baseUrl https://relay.example.com
|
||||
```
|
||||
|
||||
What this does:
|
||||
|
||||
- Lets the gateway send `push.test`, wake nudges, and reconnect wakes through the external relay.
|
||||
- Uses a registration-scoped send grant forwarded by the paired iOS app. The gateway does not need a deployment-wide relay token.
|
||||
- Keeps local/manual iOS builds on direct APNs. Relay-backed sends apply only to official distributed builds that registered through the relay.
|
||||
- Must match the relay base URL baked into the official/TestFlight iOS build, so registration and send traffic reach the same relay deployment.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
|
||||
- `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.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Set up heartbeat (periodic check-ins)">
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -49,6 +49,53 @@ openclaw nodes status
|
||||
openclaw gateway call node.list --params "{}"
|
||||
```
|
||||
|
||||
## Relay-backed push for official builds
|
||||
|
||||
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
|
||||
token to the gateway.
|
||||
|
||||
Gateway-side requirement:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
How the flow works:
|
||||
|
||||
- The iOS app registers with the relay using App Attest and the app receipt.
|
||||
- The relay returns an opaque relay handle plus a registration-scoped send grant.
|
||||
- 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.
|
||||
|
||||
What the gateway does **not** need for this path:
|
||||
|
||||
- No deployment-wide relay token.
|
||||
- No direct APNs key for official/TestFlight relay-backed sends.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
|
||||
|
||||
Local/manual builds remain on direct APNs. If you are testing those builds without the relay, the
|
||||
gateway still needs direct APNs credentials:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_APNS_TEAM_ID="TEAMID"
|
||||
export OPENCLAW_APNS_KEY_ID="KEYID"
|
||||
export OPENCLAW_APNS_PRIVATE_KEY_P8='-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
|
||||
```
|
||||
|
||||
## Discovery paths
|
||||
|
||||
### Bonjour (LAN)
|
||||
|
||||
@@ -386,6 +386,16 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Loosens strict browser auth checks for Control UI when you must run a non-standard setup. Keep this off unless you trust your network and proxy path, because impersonation risk is higher.",
|
||||
"gateway.controlUi.dangerouslyDisableDeviceAuth":
|
||||
"Disables Control UI device identity checks and relies on token/password only. Use only for short-lived debugging on trusted networks, then turn it off immediately.",
|
||||
"gateway.push":
|
||||
"Push-delivery settings used by the gateway when it needs to wake or notify paired devices. Configure relay-backed APNs here for official iOS builds; direct APNs auth remains env-based for local/manual builds.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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":
|
||||
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
||||
"gateway.http.endpoints.chatCompletions.maxBodyBytes":
|
||||
|
||||
@@ -75,6 +75,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",
|
||||
"channels.mattermost.baseUrl": "https://chat.example.com",
|
||||
"agents.list[].identity.avatar": "avatars/openclaw.png",
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ const TAG_PRIORITY: Record<ConfigTag, number> = {
|
||||
const TAG_OVERRIDES: Record<string, ConfigTag[]> = {
|
||||
"gateway.auth.token": ["security", "auth", "access", "network"],
|
||||
"gateway.auth.password": ["security", "auth", "access", "network"],
|
||||
"gateway.push.apns.relay.baseUrl": ["network", "advanced"],
|
||||
"gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback": [
|
||||
"security",
|
||||
"access",
|
||||
|
||||
@@ -345,6 +345,21 @@ export type GatewayHttpConfig = {
|
||||
securityHeaders?: GatewayHttpSecurityHeadersConfig;
|
||||
};
|
||||
|
||||
export type GatewayPushApnsRelayConfig = {
|
||||
/** Base HTTPS URL for the external iOS APNs relay service. */
|
||||
baseUrl?: string;
|
||||
/** Timeout in milliseconds for relay send requests (default: 10000). */
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
export type GatewayPushApnsConfig = {
|
||||
relay?: GatewayPushApnsRelayConfig;
|
||||
};
|
||||
|
||||
export type GatewayPushConfig = {
|
||||
apns?: GatewayPushApnsConfig;
|
||||
};
|
||||
|
||||
export type GatewayNodesConfig = {
|
||||
/** Browser routing policy for node-hosted browser proxies. */
|
||||
browser?: {
|
||||
@@ -393,6 +408,7 @@ export type GatewayConfig = {
|
||||
reload?: GatewayReloadConfig;
|
||||
tls?: GatewayTlsConfig;
|
||||
http?: GatewayHttpConfig;
|
||||
push?: GatewayPushConfig;
|
||||
nodes?: GatewayNodesConfig;
|
||||
/**
|
||||
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
|
||||
|
||||
@@ -789,6 +789,23 @@ export const OpenClawSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
push: z
|
||||
.object({
|
||||
apns: z
|
||||
.object({
|
||||
relay: z
|
||||
.object({
|
||||
baseUrl: z.string().optional(),
|
||||
timeoutMs: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
nodes: z
|
||||
.object({
|
||||
browser: z
|
||||
|
||||
@@ -325,6 +325,18 @@ describe("node.invoke APNs wake path", () => {
|
||||
});
|
||||
|
||||
it("does not clear relay registrations from wake failures", async () => {
|
||||
mocks.loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.loadApnsRegistration.mockResolvedValue({
|
||||
nodeId: "ios-node-relay",
|
||||
transport: "relay",
|
||||
@@ -368,6 +380,16 @@ describe("node.invoke APNs wake path", () => {
|
||||
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||
expect(call?.[0]).toBe(false);
|
||||
expect(call?.[2]?.message).toBe("node not connected");
|
||||
expect(mocks.resolveApnsRelayConfigFromEnv).toHaveBeenCalledWith(process.env, {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mocks.shouldClearStoredApnsRegistration).toHaveBeenCalledWith({
|
||||
registration: {
|
||||
nodeId: "ios-node-relay",
|
||||
|
||||
@@ -103,7 +103,7 @@ async function resolveDirectNodePushConfig() {
|
||||
}
|
||||
|
||||
function resolveRelayNodePushConfig() {
|
||||
const relay = resolveApnsRelayConfigFromEnv(process.env);
|
||||
const relay = resolveApnsRelayConfigFromEnv(process.env, loadConfig().gateway);
|
||||
return relay.ok
|
||||
? { ok: true as const, relayConfig: relay.value }
|
||||
: { ok: false as const, error: relay.error };
|
||||
|
||||
@@ -2,6 +2,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ErrorCodes } from "../protocol/index.js";
|
||||
import { pushHandlers } from "./push.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig: mocks.loadConfig,
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/push-apns.js", () => ({
|
||||
clearApnsRegistrationIfCurrent: vi.fn(),
|
||||
loadApnsRegistration: vi.fn(),
|
||||
@@ -52,6 +60,8 @@ function expectInvalidRequestResponse(
|
||||
|
||||
describe("push.test handler", () => {
|
||||
beforeEach(() => {
|
||||
mocks.loadConfig.mockClear();
|
||||
mocks.loadConfig.mockReturnValue({});
|
||||
vi.mocked(loadApnsRegistration).mockClear();
|
||||
vi.mocked(normalizeApnsEnvironment).mockClear();
|
||||
vi.mocked(resolveApnsAuthConfigFromEnv).mockClear();
|
||||
@@ -115,6 +125,18 @@ describe("push.test handler", () => {
|
||||
});
|
||||
|
||||
it("sends push test through relay registrations", async () => {
|
||||
mocks.loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.mocked(loadApnsRegistration).mockResolvedValue({
|
||||
nodeId: "ios-node-1",
|
||||
transport: "relay",
|
||||
@@ -153,6 +175,16 @@ 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(sendApnsAlert).toHaveBeenCalledTimes(1);
|
||||
const call = respond.mock.calls[0] as RespondCall | undefined;
|
||||
expect(call?.[0]).toBe(true);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
clearApnsRegistrationIfCurrent,
|
||||
loadApnsRegistration,
|
||||
@@ -74,7 +75,7 @@ export const pushHandlers: GatewayRequestHandlers = {
|
||||
});
|
||||
})()
|
||||
: await (async () => {
|
||||
const relay = resolveApnsRelayConfigFromEnv(process.env);
|
||||
const relay = resolveApnsRelayConfigFromEnv(process.env, loadConfig().gateway);
|
||||
if (!relay.ok) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, relay.error));
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { URL } from "node:url";
|
||||
import type { GatewayConfig } from "../config/types.gateway.js";
|
||||
|
||||
export type ApnsRelayPushType = "alert" | "background";
|
||||
|
||||
@@ -36,9 +37,10 @@ function normalizeNonEmptyString(value: string | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeTimeoutMs(value: string | undefined): number {
|
||||
const raw = value?.trim();
|
||||
if (!raw) {
|
||||
function normalizeTimeoutMs(value: string | number | undefined): number {
|
||||
const raw =
|
||||
typeof value === "number" ? value : typeof value === "string" ? value.trim() : undefined;
|
||||
if (raw === undefined || raw === "") {
|
||||
return DEFAULT_APNS_RELAY_TIMEOUT_MS;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
@@ -69,12 +71,20 @@ function parseReason(value: unknown): string | undefined {
|
||||
|
||||
export function resolveApnsRelayConfigFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
gatewayConfig?: GatewayConfig,
|
||||
): ApnsRelayConfigResolution {
|
||||
const baseUrl = normalizeNonEmptyString(env.OPENCLAW_APNS_RELAY_BASE_URL);
|
||||
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 OPENCLAW_APNS_RELAY_BASE_URL",
|
||||
error:
|
||||
"APNs relay config missing: set gateway.push.apns.relay.baseUrl or OPENCLAW_APNS_RELAY_BASE_URL",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,14 +114,16 @@ export function resolveApnsRelayConfigFromEnv(
|
||||
ok: true,
|
||||
value: {
|
||||
baseUrl: parsed.toString().replace(/\/+$/, ""),
|
||||
timeoutMs: normalizeTimeoutMs(env.OPENCLAW_APNS_RELAY_TIMEOUT_MS),
|
||||
timeoutMs: normalizeTimeoutMs(
|
||||
env.OPENCLAW_APNS_RELAY_TIMEOUT_MS ?? configuredRelay?.timeoutMs,
|
||||
),
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
ok: false,
|
||||
error: `invalid OPENCLAW_APNS_RELAY_BASE_URL (${baseUrl}): ${message}`,
|
||||
error: `invalid ${baseUrlSource} (${baseUrl}): ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,52 @@ describe("push APNs env config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves APNs relay config from gateway config", () => {
|
||||
const resolved = resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv, {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com/base/",
|
||||
timeoutMs: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(resolved).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
baseUrl: "https://relay.example.com/base",
|
||||
timeoutMs: 2500,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("lets relay env overrides win over gateway config", () => {
|
||||
const resolved = resolveApnsRelayConfigFromEnv(
|
||||
{
|
||||
OPENCLAW_APNS_RELAY_BASE_URL: "https://relay-override.example.com",
|
||||
OPENCLAW_APNS_RELAY_TIMEOUT_MS: "3000",
|
||||
} as NodeJS.ProcessEnv,
|
||||
{
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(resolved).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
baseUrl: "https://relay-override.example.com",
|
||||
timeoutMs: 3000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects insecure APNs relay http URLs by default", () => {
|
||||
const resolved = resolveApnsRelayConfigFromEnv({
|
||||
OPENCLAW_APNS_RELAY_BASE_URL: "http://relay.example.com",
|
||||
@@ -321,6 +367,22 @@ describe("push APNs env config", () => {
|
||||
expect(withUserinfo.error).toContain("userinfo is not allowed");
|
||||
}
|
||||
});
|
||||
|
||||
it("reports the config key name for invalid gateway relay URLs", () => {
|
||||
const resolved = resolveApnsRelayConfigFromEnv({} as NodeJS.ProcessEnv, {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com/path?debug=1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(resolved.ok).toBe(false);
|
||||
if (!resolved.ok) {
|
||||
expect(resolved.error).toContain("gateway.push.apns.relay.baseUrl");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("push APNs send semantics", () => {
|
||||
|
||||
Reference in New Issue
Block a user