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:
@@ -76,7 +76,7 @@ device token of a server sees the same badge.
|
||||
|
||||
Server (`apns-runtime.js`):
|
||||
- `OPENCHAMBER_PUSH_RELAY_URL` (default the public relay), `OPENCHAMBER_APNS_ENVIRONMENT`
|
||||
(`sandbox` default / `production`). The signing keypair is auto-generated — nothing to set.
|
||||
(`production` default / `sandbox` for development builds). The signing keypair is auto-generated — nothing to set.
|
||||
- Direct fallback: `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8`
|
||||
(or `_P8_PATH`), `OPENCHAMBER_APNS_BUNDLE_ID`, `OPENCHAMBER_PUSH_RELAY_DISABLED=true`.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ This module provides notification message preparation utilities for the web serv
|
||||
- `removeApnsTokenFromAllSessions(deviceToken)`
|
||||
- `sendApnsToAllUiSessions(payload)` — signs + sends to all registered tokens (no UI-visibility gate; iOS suppresses the foreground banner). No-ops with a single warning when APNs is unconfigured. Drops tokens on `410` / `BadDeviceToken` / `Unregistered`.
|
||||
- `resolveApnsConfig()`
|
||||
- Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.openchamber.app`), `OPENCHAMBER_APNS_ENVIRONMENT` (`sandbox` default, or `production`).
|
||||
- Configuration (env first, then `settings.apnsConfig`): `OPENCHAMBER_APNS_KEY_ID`, `OPENCHAMBER_APNS_TEAM_ID`, `OPENCHAMBER_APNS_P8` (PEM contents; literal `\n` accepted) or `OPENCHAMBER_APNS_P8_PATH`, `OPENCHAMBER_APNS_BUNDLE_ID` (default `com.openchamber.app`), `OPENCHAMBER_APNS_ENVIRONMENT` (`production` default, or `sandbox` for development builds).
|
||||
|
||||
### Emitter runtime API (emitter-runtime.js)
|
||||
- `createNotificationEmitterRuntime(dependencies)`: creates runtime for unified notification emission channels.
|
||||
|
||||
@@ -256,7 +256,7 @@ export const createApnsRuntime = (deps) => {
|
||||
teamId,
|
||||
p8,
|
||||
bundleId: bundleId || DEFAULT_BUNDLE_ID,
|
||||
environment: environment === 'production' ? 'production' : 'sandbox',
|
||||
environment: environment === 'sandbox' ? 'sandbox' : 'production',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -374,9 +374,9 @@ export const createApnsRuntime = (deps) => {
|
||||
url,
|
||||
registerUrl: url.replace(/\/send$/, '/register-token'),
|
||||
environment:
|
||||
(trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || 'sandbox').toLowerCase() === 'production'
|
||||
? 'production'
|
||||
: 'sandbox',
|
||||
(trimmedEnv('OPENCHAMBER_APNS_ENVIRONMENT') || 'production').toLowerCase() === 'sandbox'
|
||||
? 'sandbox'
|
||||
: 'production',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user