summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/push.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/push.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/push.py14
1 files changed, 11 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/push.py b/packages/meshbay-hub/src/meshbay_hub/api/push.py
index 0c4a411..dd694e5 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/push.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/push.py
@@ -16,8 +16,9 @@ module is reached, so the two switches the person sees are the only two there ar
The poll is authenticated by a secret issued with the row, not by a session. It
reads notification lines and nothing else, so a phone running in the background
-holds no token that could do anything more — and a sign-out, which deletes the
-row, ends it.
+holds no token that could do anything more — and turning it off, signing out
+everywhere, a passphrase change or reset, a revocation and an erasure all
+delete the row, which ends it.
Who pays (§13.5b): a member's chat costs every other member's phones a push.
That fan-out is already bounded where it starts — `chat_notify` is budgeted per
@@ -56,6 +57,8 @@ CHAT_COALESCE = 30.0
_COALESCE_ENTRIES = 10_000
POLL_MIN_INTERVAL = 60.0
POLL_LIMIT = 20
+# Whose phones may still fetch: not a revoked or deleted account.
+POLLING_STATUSES = ("active", "suspended")
# Strong references: asyncio holds a task weakly, and a collected one is a push
# that silently never went (CLAUDE.md, "a background task nobody holds").
@@ -167,12 +170,17 @@ async def poll(body: PollIn, db: AsyncSession = Depends(get_db)):
Authenticated by the row's secret rather than a session, so what a phone
keeps for running in the background reads notification lines and nothing
- else. A wrong secret and an unknown row answer the same 404.
+ else. A wrong secret, an unknown row and an account that is revoked or
+ deleted answer the same 404. A suspended account still reads: the line
+ telling it so is one of them.
"""
sub = await db.get(PushSubscription, body.id)
if (sub is None or sub.poll_hash is None
or not hmac.compare_digest(sub.poll_hash, _hash(body.secret))):
raise HTTPException(status_code=404, detail="Subscription not found")
+ owner = await db.get(User, sub.user_id)
+ if owner is None or owner.status not in POLLING_STATUSES:
+ raise HTTPException(status_code=404, detail="Subscription not found")
now = time.monotonic()
last = _last_poll.get(sub.id)
if last is not None and now - last < POLL_MIN_INTERVAL: