notify: only "the app is gone" deletes a device row
reported from the field on the first real push: it failed with BadDeviceToken and the server deleted the registration. the build was production-signed and the row said sandbox — a configuration mismatch, with the device and the app both perfectly fine. pruning there destroys the evidence. the phone re-registers on next launch, the row comes back looking healthy, and the next send fails identically; you can lose a day to that loop without ever seeing what is actually wrong. so the one set becomes three. Unregistered/UNREGISTERED/NOT_FOUND prune — apple and google are saying the app is uninstalled. BadDeviceToken, DeviceTokenNotForTopic, TopicDisallowed, INVALID_ARGUMENT and SENDER_ID_MISMATCH are OURS: the row is kept and the log says what to check. everything else is transient and still counts toward three strikes. INVALID_ARGUMENT was the sharpest one. it is how fcm reports a malformed MESSAGE — including a non-string in `data`, which is the mistake this codebase is most likely to make and already has a test for. deleting on it meant one bug in the payload builder would wipe every android device from the registry on a single send, presenting as "android push stopped working" with nothing left to explain why. misconfigured is also not counted as a strike. not deleting is not enough on its own — a persistent mismatch would reach three failures and delete the row anyway, which is the same bug with a slower fuse. the apns log now names the mismatch rather than the reason code, since the code alone sends people to look at the device: it prints the row's environment and topic and says to check the SHIPPED artefact rather than the intermediate build, which is exactly where this one hid.
This commit is contained in:
@@ -22,7 +22,7 @@ import { assertPermissionTotality, isExemptApiPath } from './totality';
|
||||
// switched off 2026-08-13. It was listed here until 2026-08-18 and made this file's own totality tests
|
||||
// fail for five days — the mirror claiming a provider the server does not serve, which is precisely the
|
||||
// drift the comment above warns about, caught by the check and then ignored because it was "the baseline".
|
||||
const REAL_WS = ['terminal', 'chat', 'task-runner', 'pipeline', 'sidecar'];
|
||||
const REAL_WS = ['terminal', 'chat', 'chat-notify', 'task-runner', 'pipeline', 'sidecar'];
|
||||
const realApi = () => [...new Set(PERMISSIONS.flatMap((c) => c.api))];
|
||||
const surface = () => ({
|
||||
apiPrefixes: [...realApi(), '/auth', '/landing-page-data', '/waitlist', '/sidecar'],
|
||||
|
||||
@@ -252,7 +252,11 @@ const CORE_REGISTRY: Permission[] = [
|
||||
// two booleans about the caller's own home and nothing about anyone else, which is what lets a member be
|
||||
// told "run `claude` once to sign in" instead of being shown a tile that 403s.
|
||||
api: ['/chat', '/agent-status'],
|
||||
ws: ['chat'],
|
||||
// 'chat-notify' is a session-independent socket (the bell feature): it carries no sessionId and no
|
||||
// transcript, just a ping that a chat this account can already reach has finished a turn or needs
|
||||
// input. Riding this permission rather than a new one, because it is meaningless without `chat`
|
||||
// already granted.
|
||||
ws: ['chat', 'chat-notify'],
|
||||
routes: ['/chat'],
|
||||
},
|
||||
// Confined rather than execution since 2026-08-11. Every path under `/file-browser` resolves its root
|
||||
|
||||
Reference in New Issue
Block a user