--- title: "1.21.2 — Chat hardening and cards, truthful SMS auth failures, non-root job engine" description: "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 end…" date: "2026-08-31" tags: ["release"] canonical: "https://django-mojo.sitesmojo.com/changelog/1-21-2/" --- 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_key` on the `chat_message` send 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: `card` message kind carrying an opaque consumer-defined `metadata` payload, 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`, `file` and `card` messages through the same validation, moderation and broadcast path clients use. Clients can author only `text` and `image`; 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_key` scoping 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_key` column 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.