prompt inicial do projeto completo

This commit is contained in:
2026-07-22 18:45:09 -03:00
parent 6fb920d333
commit b9ede753c4
12 changed files with 64 additions and 18 deletions

View File

@@ -42,7 +42,7 @@ Implementada em `shared/state/VisitStateMachine.kt` — a mesma classe roda no a
| Job | Dispara | Ação |
|---|---|---|
| `VISIT_RING_TIMEOUT` | 20s após `TOCANDO` | → `ESCALONADA`, toca para os demais `unit_members` por `ring_order` |
| `VISIT_RING_TIMEOUT` | 20s após `TOCANDO` (kind `VISITA`) | → `ESCALONADA`, toca para os demais `unit_members` por `ring_order` |
| `VISIT_ESCALATE` | 15s após `ESCALONADA` | → `FILA_OPERADOR` se `operator_queue` ativo, senão → `RECADO_EM_VIDEO` |
| `VISIT_QUEUE_TIMEOUT` | 120s em `FILA_OPERADOR` | → `RECADO_EM_VIDEO` |
| `VISIT_EXPIRE` | `expires_at` (10 min) | → `EXPIRADA` |
@@ -66,7 +66,7 @@ VISITANTE BACKEND MORADOR
│ {kind, nome, bloco, │ BUSCA CEGA: │
│ unidade, foto} │ resolve unit_id ou NULL │
├───────────────────────────►│ resposta idêntica sempre │
│◄─── {visitId, roomToken} ──┤ │
│◄──────── {visitId} ────────┤ │
│ │ ── outbox: VisitaCriada ──┤
│ │ │
│ │ push FCM / VoIP+CallKit │
@@ -74,6 +74,8 @@ VISITANTE BACKEND MORADOR
│ │ │
│ 4. WS /ws/visitor/{id} │ │ 5. atende
│◄══ estado em tempo real ══►│◄═══════ WS /ws/app ═══════►│
│ │ sala criada em EM_CHAMADA │
│◄── ROOM_READY + token ─────┼──── ROOM_READY + token ───►│
│ │ │
│◄────── LiveKit: sala compartilhada ────────────────────►│
│ │ │
@@ -83,7 +85,23 @@ VISITANTE BACKEND MORADOR
│ ├── notifica responsável ────►
```
**Passo 3 é o ponto crítico de segurança.** A resposta é byte-a-byte idêntica para unidade existente e inexistente, com o mesmo tempo de resposta (comparação em tempo constante e delay artificial se necessário). Se a unidade não existe, a visita entra em `TOCANDO`, ninguém é notificado, e ela expira normalmente. Quem tem o QR não consegue mapear o prédio.
**Passo 3 é o ponto crítico de segurança.** A resposta é byte-a-byte idêntica para unidade existente e inexistente, com o mesmo tempo de resposta (comparação em tempo constante e delay artificial se necessário). Quem tem o QR não consegue mapear o prédio.
### Visita-sombra — a busca cega vale também para a linha do tempo
Não basta a resposta HTTP ser idêntica: se a visita de unidade inexistente ficasse parada em `TOCANDO` até expirar enquanto a real anda `TOCANDO → ESCALONADA → FILA_OPERADOR`, o atacante distinguiria os dois casos **observando a sequência de estados no WebSocket**. A busca cega tem que valer para a linha do tempo inteira.
Por isso, quando `unit_id` é `NULL` — ou quando a unidade existe mas não tem nenhum dispositivo ativo — a visita percorre **exatamente os mesmos estados, nos mesmos tempos**, sem notificar ninguém:
```
visita-sombra: TOCANDO (20s, sem push) → ESCALONADA (15s, sem push)
├─ operator_queue ativo → FILA_OPERADOR (de verdade,
│ marcada para o operador como "unidade não cadastrada")
└─ sem módulo → RECADO_EM_VIDEO
(listado no admin como "recado sem unidade")
```
Com o módulo de operador, a visita-sombra entra na fila **de verdade**: o operador vê a marcação privada "unidade não cadastrada" e trata como um porteiro humano trataria quem errou o número — pergunta, corrige a unidade e redireciona. Efeito colateral desejável: erro de digitação legítimo ("101A" em vez de "101-A") é **recuperado** em vez de perdido, e a indistinguibilidade fica perfeita — nos dois casos alguém atende.
## 3. Fluxo B — Entrega (funil rápido)
@@ -161,7 +179,7 @@ Na v1 a abertura é humana: quem abre confere o PIN na tela do visitante contra
| `GET` | `/visits/{id}` | Estado atual (fallback de polling se o WS cair) |
| `POST` | `/visits/{id}/message` | Envia recado em vídeo |
| `POST` | `/visits/{id}/cancel` | Visitante desiste |
| `GET` | `/visits/{id}/room-token` | Token LiveKit com escopo da sala |
| `GET` | `/visits/{id}/room-token` | Token LiveKit com escopo da sala — disponível a partir de `EM_CHAMADA`; antes disso, `409` |
### Morador e operador — `/api/v1/app`