1.21.2 — Chat hardening and cards, truthful SMS auth failures, non-root job engine
Editorial identity incomplete
2026-08-31
django-mojo 1.21.2 hardens chat (join-time history cutoff on every non-channel room kind, banned members locked out of read state, a cross-tenant message-id oracle closed, idempotent sends and reactions) and adds the card message kind with a server-side send service; makes every SMS-sending auth endpoint report transport failures honestly with per-endpoint rate limits; repairs the job channel-health endpoints; and guarantees deployed job processes always run as the application account — a root-started engine now demotes itself and can no longer break the next deployment. Patch release; no consumer action required beyond running migrations.
Added
- Chat: optional
client_keyon thechat_messagesend frame — a client that loses an ack can resend the identical frame without creating a duplicate. The key is echoed on acks, error frames and broadcasts; history shows it only to its author. - Chat:
cardmessage kind carrying an opaque consumer-definedmetadatapayload, with structural validation and a byte cap (CHAT_METADATA_MAX_BYTES), optional per-kind validators (CHAT_KIND_VALIDATORS, fail-closed per kind), and room URL/phone rules applied to payload strings. - Chat: a server-side send service so backend code can author
system,fileandcardmessages through the same validation, moderation and broadcast path clients use. Clients can author onlytextandimage; unknown kinds are refused. - Chat: a message-deletion hook (
CHAT_MESSAGE_DELETED_HANDLER) fired on disappearing-message cleanup and room deletion.
Changed
- SMS-sending auth endpoints (phone change, magic login, MFA and phone registration) report transport failures honestly: 503 when the provider is unavailable (retry the same request), 400 when the number cannot receive texts — instead of returning success for a text nobody received. A failed phone change discards the pending change, so a session token is never reused across a retry.
- Chat reactions accept an explicit
action: "add" | "remove"(absent still toggles) and are idempotent — the reaction event is broadcast only on a real state change. - Chat read acks and the room read endpoint return the resolved
up_to_message_id— the newest message the caller was actually entitled to mark. - A deployment now restarts both the job engine and the scheduler, so the scheduler no longer keeps running pre-deploy code indefinitely.
- The dead "stale running" job-recovery method was removed — it republished to legacy queue keys nothing consumes. Recovering a stranded running job is now a documented manual operator recipe, with the stale count exposed in job stats.
Fixed
- Job channel health endpoints answered HTTP 400 on every call after the queue rewrite; they now report real queued and in-flight counts, and a Redis outage reports critical instead of an all-zero healthy.
- Chat history's join-time cutoff now applies to every non-channel room kind — a member removed from a direct (or custom-kind) room and later re-added could previously read everything sent while they were out. Unread counts use the same bound and now honor disappearing-message TTLs.
- Banned chat members can no longer stamp read receipts, and a read position beyond what the caller may see is clamped instead of erroring.
- The global chat DM endpoint prefers the personal (groupless) direct room and never silently creates a duplicate room when the pair already shares a workspace-scoped conversation.
- Magic login no longer errors on an account whose stored phone number is unparseable, and phone-change failures are attributed to the provider or the number correctly instead of always blaming the caller.
- Documentation corrected where it contradicted the code: the WebOTP autofill guidance that could never resolve, chat read-state and
client_keyscoping claims, and the phone-change notification claim.
Security
- Deployed job processes always run as the configured application account. A job-engine start invoked as root resolves the account from trusted configuration (the deployed jobs cron entry, then an explicit setting, then the checkout owner — never ambient sudo state), repairs its own file ownership, and demotes itself before starting anything; the deployment transaction refuses to run Git or checkout mutation as root. A root-started engine previously ran every queued job as root and broke the next deployment's Git fetch.
- Rendered cron configuration refuses to hand the job tick to root, including via a project override file.
- Chat reactions no longer reveal whether an arbitrary message id exists in another tenant's room — nonexistent, flagged, pre-join and foreign-room targets all answer identically.
- Account-enumeration differentials closed in the SMS auth paths: magic login's response no longer varies with the send outcome, and SMS login answers uniformly for accounts with no phone number.
Upgrade notes
- Chat gains two migrations (the
client_keycolumn and new message-kind choices); run migrations on upgrade. - Each SMS auth endpoint now has its own rate-limit bucket (10 per minute) instead of one shared oversized bucket; a client exceeding that per endpoint will see 429s where it previously did not.