fix: default APNs delivery to production (#2381)

Issue: TestFlight iOS clients register production APNs device tokens, but notifications from a default OpenChamber server are not delivered.

Expected behavior: the released iOS app receives notifications without requiring users to configure an APNs environment, while development builds can explicitly select sandbox delivery.

Root cause: both relay and direct APNs delivery defaulted to the sandbox environment, so production TestFlight tokens were sent to the wrong APNs endpoint.

Fix: default both delivery modes to production, preserve OPENCHAMBER_APNS_ENVIRONMENT=sandbox as an explicit development override, and update tests and documentation.
This commit is contained in:
natheihei
2026-07-23 13:21:40 +03:00
committed by GitHub
parent a1784b8697
commit 12e2c55171
4 changed files with 31 additions and 6 deletions
@@ -69,6 +69,7 @@ afterEach(() => {
vi.unstubAllGlobals();
delete process.env.OPENCHAMBER_PUSH_RELAY_URL;
delete process.env.OPENCHAMBER_PUSH_RELAY_DISABLED;
delete process.env.OPENCHAMBER_APNS_ENVIRONMENT;
});
describe('apns runtime relay mode (default)', () => {
@@ -116,6 +117,7 @@ describe('apns runtime relay mode (default)', () => {
expect(sent.title).toBe('Agent response is ready');
expect(sent.body).toBe('My session');
expect(sent.badge).toBe(3);
expect(sent.env).toBe('production');
expect(sent.data).toEqual({ sessionId: 'sess1' });
expect(sent.publicKeyJwk).toMatchObject({ kty: 'EC', crv: 'P-256' });
const sendMessage = `${sent.ts}.${[...sent.tokens].sort().join(',')}.${sent.title}`;
@@ -144,6 +146,20 @@ describe('apns runtime relay mode (default)', () => {
expect(deps.writeSettingsToDisk).toHaveBeenCalledTimes(1);
});
it('honors an explicit sandbox environment', async () => {
const fetchMock = vi.fn(async () => jsonResponse({ ok: true, results: [] }));
vi.stubGlobal('fetch', fetchMock);
process.env.OPENCHAMBER_PUSH_RELAY_URL = 'https://relay.test/v1/push/send';
process.env.OPENCHAMBER_APNS_ENVIRONMENT = 'sandbox';
const runtime = createApnsRuntime(makeDeps());
await runtime.addOrUpdateApnsToken('s1', 'tokenA');
await runtime.sendApnsToAllUiSessions({ title: 't', body: 'b' });
const sent = JSON.parse(fetchMock.mock.calls.find(isSend)[1].body);
expect(sent.env).toBe('sandbox');
});
it('no-ops (no relay call) when no tokens are registered', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
@@ -154,6 +170,15 @@ describe('apns runtime relay mode (default)', () => {
});
describe('apns runtime direct fallback (relay disabled)', () => {
it('defaults direct APNs configuration to production', async () => {
const { environment: _environment, ...configWithoutEnvironment } = APNS_CONFIG;
const runtime = createApnsRuntime(
makeDeps({ readSettingsFromDiskMigrated: vi.fn(async () => ({ apnsConfig: configWithoutEnvironment })) }),
);
await expect(runtime.resolveApnsConfig()).resolves.toMatchObject({ environment: 'production' });
});
it('signs an ES256 JWT and sends over http2 when relay is disabled', async () => {
process.env.OPENCHAMBER_PUSH_RELAY_DISABLED = 'true';
const targeted = [];