From b9ede753c48adeb7229bee77be8c4c87c71b2fbbf2dcb642ee14b03eec3c7604 Mon Sep 17 00:00:00 2001 From: victor Date: Wed, 22 Jul 2026 18:45:09 -0300 Subject: [PATCH] prompt inicial do projeto completo --- docs/01-ARQUITETURA.md | 2 +- docs/02-MODELO-DE-DADOS.md | 10 +++++++--- docs/03-FLUXOS-E-CONTRATOS.md | 26 ++++++++++++++++++++++---- docs/05-INFRA-DOCKER.md | 20 ++++++++++++++++---- docs/06-LGPD-E-SEGURANCA.md | 5 ++++- docs/07-REQUISITOS-DE-CAMPO.md | 3 +++ prompts/FASE-0-bootstrap.md | 2 +- prompts/FASE-1-dominio-e-dados.md | 4 +++- prompts/FASE-2-backend-api.md | 2 ++ prompts/FASE-6-painel-admin.md | 2 +- prompts/FASE-7-escalonamento.md | 2 ++ prompts/FASE-9-hardening.md | 4 ++-- 12 files changed, 64 insertions(+), 18 deletions(-) diff --git a/docs/01-ARQUITETURA.md b/docs/01-ARQUITETURA.md index 249403b..f16b34c 100644 --- a/docs/01-ARQUITETURA.md +++ b/docs/01-ARQUITETURA.md @@ -202,7 +202,7 @@ Três identidades distintas, três mecanismos: | **Morador / operador** | OIDC (Spring Security) + refresh token no keystore do dispositivo | Access 15 min, refresh 30 dias | | **Admin** | OIDC + MFA obrigatório | Sessão de 8h | -**Busca cega de unidade:** o endpoint de destino recebe bloco + unidade e **nunca** confirma se existe ou quem mora lá. Resposta idêntica para unidade válida e inválida; se não existir, a chamada simplesmente não é atendida. Isso impede que qualquer pessoa com o QR enumere quem mora onde — um problema de segurança física antes de ser de privacidade. Detalhes em `06-LGPD-E-SEGURANCA.md`. +**Busca cega de unidade:** o endpoint de destino recebe bloco + unidade e **nunca** confirma se existe ou quem mora lá. Resposta idêntica para unidade válida e inválida — **inclusive na linha do tempo de estados**: a visita-sombra percorre os mesmos estados nos mesmos tempos sem notificar ninguém (`03-FLUXOS-E-CONTRATOS.md` §2). Isso impede que qualquer pessoa com o QR enumere quem mora onde — um problema de segurança física antes de ser de privacidade. Detalhes em `06-LGPD-E-SEGURANCA.md`. ## 7. Multi-tenant preparado, não ativado diff --git a/docs/02-MODELO-DE-DADOS.md b/docs/02-MODELO-DE-DADOS.md index a3b8c5c..6b26442 100644 --- a/docs/02-MODELO-DE-DADOS.md +++ b/docs/02-MODELO-DE-DADOS.md @@ -56,7 +56,9 @@ CREATE TABLE condominiums ( name text NOT NULL, address text NOT NULL, timezone text NOT NULL DEFAULT 'America/Sao_Paulo', - quiet_hours int4range, -- janela de silêncio, ex.: [22,7) + -- janela de silêncio ("quiet_hours" nos demais docs); start > end cruza a meia-noite (22 → 7) + quiet_start smallint CHECK (quiet_start BETWEEN 0 AND 23), + quiet_end smallint CHECK (quiet_end BETWEEN 0 AND 23), created_at timestamptz NOT NULL DEFAULT now(), version integer NOT NULL DEFAULT 0 ); @@ -82,7 +84,7 @@ CREATE TABLE units ( ); ``` -`quiet_hours` alimenta a defesa contra "tocar em todos os apartamentos de madrugada": dentro da janela, visitas não-pré-autorizadas vão direto para a fila de operador em vez de acordar moradores. +`quiet_start`/`quiet_end` alimentam a defesa contra "tocar em todos os apartamentos de madrugada": dentro da janela, visitas não-pré-autorizadas vão direto para a fila de operador em vez de acordar moradores. Duas colunas em vez de `int4range` de propósito: a janela típica cruza a meia-noite (22 → 7), e `int4range(22, 7)` é um range inválido no Postgres. A verificação "está na janela?" vive em `shared/validation`, testada nos dois sentidos. ## 3. Pessoas e dispositivos @@ -258,7 +260,7 @@ CREATE TABLE access_grants ( visit_id uuid NOT NULL REFERENCES visits(id), gate_id uuid NOT NULL REFERENCES gates(id), granted_by uuid NOT NULL REFERENCES persons(id), - pin text, -- 6 dígitos, conferência humana + pin_hash text, -- hash do PIN de 6 dígitos; o PIN em claro só existe na tela e na notificação valid_until timestamptz NOT NULL, used_at timestamptz, device_result text, -- resultado do AccessControlDevice (v2) @@ -361,6 +363,8 @@ CREATE INDEX idx_audit_entity ON audit_log (tenant_id, entity_type, entity_id, c **Append-only.** Nenhum `UPDATE` ou `DELETE`, garantido por permissão do usuário de aplicação no banco. É a defesa jurídica em caso de autorização indevida. +**Papéis de banco separados — sem isso o revoke é decorativo.** Dono de tabela ignora `REVOKE` e RLS (salvo `FORCE`). Portanto: o Flyway conecta como `portaria` (dono do schema, roda migrations); a aplicação conecta como **`portaria_app`**, papel sem ownership, criado no init do Postgres (ver `05-INFRA-DOCKER.md` §2). Se o backend conectar como dono, nem o append-only do `audit_log` nem as policies de RLS valem nada — e nenhum teste funcional percebe. + ## 9. Row Level Security — preparada, desativada ```sql diff --git a/docs/03-FLUXOS-E-CONTRATOS.md b/docs/03-FLUXOS-E-CONTRATOS.md index c0a6a1b..217179e 100644 --- a/docs/03-FLUXOS-E-CONTRATOS.md +++ b/docs/03-FLUXOS-E-CONTRATOS.md @@ -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` diff --git a/docs/05-INFRA-DOCKER.md b/docs/05-INFRA-DOCKER.md index 8b4dd60..2d6c69d 100644 --- a/docs/05-INFRA-DOCKER.md +++ b/docs/05-INFRA-DOCKER.md @@ -43,8 +43,9 @@ services: image: postgis/postgis:17-3.5 environment: POSTGRES_DB: portaria - POSTGRES_USER: portaria + POSTGRES_USER: portaria # dono do schema — usado só pelo Flyway POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_APP_PASSWORD: ${POSTGRES_APP_PASSWORD} # lido por init/02-roles.sh → cria portaria_app volumes: - pgdata:/var/lib/postgresql/data - ./postgres/init:/docker-entrypoint-initdb.d:ro @@ -60,9 +61,13 @@ services: environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/portaria - SPRING_DATASOURCE_USERNAME: portaria - SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} - LIVEKIT_URL: ws://livekit:7880 + # aplicação NUNCA conecta como dona do schema — senão REVOKE e RLS são inócuos (02 §8) + SPRING_DATASOURCE_USERNAME: portaria_app + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_APP_PASSWORD} + SPRING_FLYWAY_USER: portaria + SPRING_FLYWAY_PASSWORD: ${POSTGRES_PASSWORD} + # livekit está em network_mode:host — o nome de serviço não resolve; alcançado via gateway do host + LIVEKIT_URL: ws://host.docker.internal:7880 LIVEKIT_API_KEY: ${LIVEKIT_API_KEY} LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET} STORAGE_ENDPOINT: http://minio:9000 @@ -73,6 +78,8 @@ services: OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317 volumes: - ./secrets:/secrets:ro + extra_hosts: + - host.docker.internal:host-gateway depends_on: postgres: { condition: service_healthy } minio: { condition: service_started } @@ -110,6 +117,8 @@ services: redis: # exclusivo do LiveKit e do rate limit image: redis:7-alpine # NUNCA para dados que não podem se perder command: redis-server --save "" --appendonly no + ports: + - 127.0.0.1:6379:6379 # livekit está em host network e alcança via localhost; nunca exponha fora do host restart: unless-stopped minio: @@ -185,6 +194,8 @@ room: **Redis não é opcional** com múltiplas instâncias do LiveKit: sem ele, o estado das salas se divide silenciosamente entre nós e dois participantes da mesma sala não se enxergam. +**Capacidade de relay do coturn:** a faixa 49160–49200 são 40 portas ≈ **20 chamadas simultâneas via TURN**. Suficiente para o piloto (TURN só entra quando o UDP direto falha, tipicamente 10–20% das chamadas), mas amplie a faixa em produção junto com o firewall. + ## 4. Dimensionamento de banda Uma chamada de portaria tem 2 participantes, vídeo em 480p a ~600kbps mais áudio. O SFU recebe e reenvia: @@ -218,6 +229,7 @@ GH_OWNER=sua-org TAG=latest POSTGRES_PASSWORD= +POSTGRES_APP_PASSWORD= LIVEKIT_API_KEY= LIVEKIT_API_SECRET= TURN_SECRET= diff --git a/docs/06-LGPD-E-SEGURANCA.md b/docs/06-LGPD-E-SEGURANCA.md index 976d3b4..67f9e56 100644 --- a/docs/06-LGPD-E-SEGURANCA.md +++ b/docs/06-LGPD-E-SEGURANCA.md @@ -93,6 +93,7 @@ Prazos são configuráveis por tenant, com **teto** definido pela plataforma. Um - Visita criada mesmo com `unit_id` nulo, preservando `unit_input` - Resposta idêntica em corpo, código e headers - **Tempo de resposta constante** (delay artificial iguala os caminhos — sem isso o ataque vira timing attack) +- **Linha do tempo de estados idêntica** — a visita-sombra percorre os mesmos estados nos mesmos tempos, sem notificar ninguém (`03-FLUXOS-E-CONTRATOS.md` §2). Sem isso, o atacante distingue os casos observando a sequência no WebSocket, e o tempo constante da resposta HTTP não protege nada - Nome do morador nunca aparece antes do atendimento - Rate limit por IP e por dispositivo, com bloqueio progressivo @@ -128,9 +129,11 @@ REVOKE UPDATE, DELETE ON audit_log FROM portaria_app; Nem a aplicação comprometida altera o log. Somado ao `sha256` da mídia, isso é o que dá valor probatório ao registro. +**Só funciona se a aplicação não for dona do schema** — dono ignora `REVOKE` e RLS. A aplicação conecta como `portaria_app`; o Flyway, como `portaria` (dono). Ver `02-MODELO-DE-DADOS.md` §8. + ### A.6 — Roubo de token -**Mitigação:** JWT de visitante com 15 min, escopo de uma visita, atado ao `gateId` e ao IP de origem. Refresh do morador no Keystore/Keychain com biometria de dispositivo. Rotação de refresh token e revogação por dispositivo no admin. +**Mitigação:** JWT de visitante com 15 min, escopo de uma visita, atado ao `gateId` e à sessão — **não ao IP de origem**: IP de celular muda no meio da sessão (troca de torre, CGNAT) e a amarração derrubaria visitante legítimo no meio do fluxo. A defesa é a posse do token somada à validade curta e ao escopo mínimo. Refresh do morador no Keystore/Keychain com biometria de dispositivo. Rotação de refresh token e revogação por dispositivo no admin. ### A.7 — QR vazado ou clonado diff --git a/docs/07-REQUISITOS-DE-CAMPO.md b/docs/07-REQUISITOS-DE-CAMPO.md index 52a527b..89d48b3 100644 --- a/docs/07-REQUISITOS-DE-CAMPO.md +++ b/docs/07-REQUISITOS-DE-CAMPO.md @@ -22,6 +22,7 @@ Um condomínio só entra em produção com todos os itens verificados **em campo - [ ] **Wi-Fi aberto de visitante** com cobertura medida em cada portaria - [ ] SSID óbvio: `Portaria-` - [ ] **Captive portal** que abre direto a página do visitante +- [ ] **Portal instrui a abrir no navegador padrão** — o mini-navegador do captive portal (CNA do iOS, e vários Androids) **bloqueia câmera e geolocalização**, as duas permissões de que o fluxo depende. O portal exibe botão/instrução "abrir no Safari/Chrome", e o teste de ativação percorre o fluxo completo **a partir do portal**, não de um navegador já aberto - [ ] Rede **isolada** da rede administrativa (VLAN separada, sem acesso à LAN interna) - [ ] Banda mínima 5 Mbps simétricos reservados para o visitante - [ ] Teste de sinal ≥ -70 dBm no ponto exato onde o visitante fica @@ -47,6 +48,8 @@ A rede de visitante isolada não é detalhe: uma rede aberta com acesso à LAN d O botão físico é o que atende quem não tem celular, está sem bateria ou não consegue usar o QR. **Sem ele, o produto exclui uma parcela real de visitantes** — e cria a situação em que alguém legítimo simplesmente não consegue entrar. +**Na v1, o botão é deliberadamente um interfone direto (GSM/4G ou SIP) para o responsável/central — fora da plataforma.** É proposital: ele precisa continuar funcionando exatamente quando a plataforma cai, então não pode depender dela. Integrá-lo como dispositivo da plataforma (disparando o fluxo de visita com operador) é v2, atrás de `AccessControlDevice`. + ### 2.4 Aviso legal — obrigatório - [ ] **Placa física** com o aviso de tratamento de dados, visível antes da coleta diff --git a/prompts/FASE-0-bootstrap.md b/prompts/FASE-0-bootstrap.md index a239fa3..7e574e2 100644 --- a/prompts/FASE-0-bootstrap.md +++ b/prompts/FASE-0-bootstrap.md @@ -40,7 +40,7 @@ Um controller `GET /api/v1/health` devolvendo `{"status":"UP"}`, só para provar ### 6. `infra/` -`docker-compose.yml` exatamente como em `docs/05-INFRA-DOCKER.md` §2, mais `docker-compose.observability.yml` e `.env.example`. Configs em `livekit/livekit.yaml`, `traefik/`, `postgres/init/01-extensions.sql` (`CREATE EXTENSION postgis; CREATE EXTENSION pgcrypto;`). +`docker-compose.yml` exatamente como em `docs/05-INFRA-DOCKER.md` §2, mais `docker-compose.observability.yml` e `.env.example`. Configs em `livekit/livekit.yaml`, `traefik/`, `postgres/init/01-extensions.sql` (`CREATE EXTENSION postgis; CREATE EXTENSION pgcrypto;`) e `postgres/init/02-roles.sh` (cria o papel `portaria_app`, sem ownership, com a senha de `POSTGRES_APP_PASSWORD` — ver `02-MODELO-DE-DADOS.md` §8). ### 7. CI — GitHub Actions diff --git a/prompts/FASE-1-dominio-e-dados.md b/prompts/FASE-1-dominio-e-dados.md index 7f3c979..244b58f 100644 --- a/prompts/FASE-1-dominio-e-dados.md +++ b/prompts/FASE-1-dominio-e-dados.md @@ -73,7 +73,9 @@ Filtro/interceptor que lê `Idempotency-Key`, consulta `idempotency_keys`, e: Spring Security com três cadeias de filtro conforme `06-LGPD-E-SEGURANCA.md` §6: `/api/v1/visitor/**` (JWT efêmero), `/api/v1/app/**` (OIDC), `/api/v1/admin/**` (OIDC + MFA). -Emissão e validação do JWT de visitante: 15 min, escopo de uma visita, atado a `gateId` e IP. +Emissão e validação do JWT de visitante: 15 min, escopo de uma visita, atado a `gateId` e à sessão — **não ao IP**: IP de celular muda no meio da sessão (CGNAT, troca de torre) e derrubaria visitante legítimo. + +**Conexões ao banco:** Flyway como `portaria` (dono), aplicação como `portaria_app` — sem essa separação, o revoke do `audit_log` e a RLS futura são decorativos (dono ignora ambos). Ver `02-MODELO-DE-DADOS.md` §8. ### 8. Auditoria diff --git a/prompts/FASE-2-backend-api.md b/prompts/FASE-2-backend-api.md index 22970e3..262d830 100644 --- a/prompts/FASE-2-backend-api.md +++ b/prompts/FASE-2-backend-api.md @@ -27,6 +27,7 @@ Localização ausente ou negada → `403 LOCATION_REQUIRED`. Fora do raio → `4 - Resolve `unit_id` a partir de bloco + unidade; **se não existir, cria a visita com `unit_id = NULL`** e preserva `unit_input` - Resposta **idêntica** — mesmo corpo, mesmo status, mesmos headers — nos dois casos - **Tempo de resposta constante:** meça o caminho mais lento e aplique delay artificial no mais rápido. Sem isso o ataque vira timing attack e a busca cega não serve para nada +- **Visita-sombra:** com `unit_id` NULL — ou unidade sem nenhum dispositivo ativo — os jobs percorrem a **mesma linha do tempo de estados**, nos mesmos tempos, sem notificar ninguém; com `operator_queue` ativo, entra na fila marcada "unidade não cadastrada" (`03-FLUXOS-E-CONTRATOS.md` §2). Sem isso, a sequência de estados no WebSocket entrega o que o tempo constante escondeu - Nome de morador nunca aparece na resposta - Rate limit por IP e por dispositivo, com bloqueio progressivo @@ -91,6 +92,7 @@ Importação CSV de unidades com **dry-run obrigatório** — devolve o que seri - [ ] Testes de integração com Testcontainers cobrindo os fluxos A, B e C ponta a ponta - [ ] **Teste de busca cega:** unidade existente e inexistente produzem resposta idêntica, e a diferença de tempo fica abaixo do ruído de medição +- [ ] **Teste de visita-sombra:** unidade inexistente percorre a mesma sequência de estados, nos mesmos tempos, que uma visita real não atendida — e ninguém é notificado - [ ] Teste: geofence rejeita fora do raio e aceita dentro - [ ] Teste: `quiet_hours` desvia do morador - [ ] Teste: dois `resolve` concorrentes — um vence, outro recebe `409` com o autor diff --git a/prompts/FASE-6-painel-admin.md b/prompts/FASE-6-painel-admin.md index 6d406c8..1a0846d 100644 --- a/prompts/FASE-6-painel-admin.md +++ b/prompts/FASE-6-painel-admin.md @@ -22,7 +22,7 @@ Login OIDC com **MFA obrigatório** — este perfil vê fotos de todos os visita ### 2. Auditoria de visitas — a tela mais importante -Tabela virtualizada com filtros persistidos na URL: período, condomínio, bloco, unidade, tipo, estado, **dentro/fora do geofence**, e quem resolveu. +Tabela virtualizada com filtros persistidos na URL: período, condomínio, bloco, unidade, tipo, estado, **dentro/fora do geofence**, quem resolveu, e **"unidade não cadastrada"** (visitas-sombra: erro de digitação legítimo ou tentativa de enumeração — recorrência do mesmo dispositivo em unidades diferentes é padrão de varredura e deve gerar destaque). Colunas: data/hora, tipo, visitante, unidade, estado, quem resolveu, duração, selo de geofence. diff --git a/prompts/FASE-7-escalonamento.md b/prompts/FASE-7-escalonamento.md index 8f0838b..9db742d 100644 --- a/prompts/FASE-7-escalonamento.md +++ b/prompts/FASE-7-escalonamento.md @@ -40,6 +40,8 @@ Mesmo binário Compose Multiplatform, perfil decidido por `GET /me`. Layout mais **Contexto** à direita, antes de assumir: histórico da unidade, visitas recentes, regra de entrega, nomes dos moradores, telefone. **O operador nunca atende sem contexto** — atender às cegas é o que faz a portaria remota parecer pior que a física. +**Visitas-sombra** (`unit_id` nulo — busca cega, `03-FLUXOS-E-CONTRATOS.md` §2) chegam à fila marcadas como **"unidade não cadastrada"**, visível só para o operador. Ele trata como um porteiro trataria quem errou o número: pergunta, corrige a unidade (`PATCH` que resolve o `unit_id` e registra em `audit_log`) e redireciona — ou nega. Para o visitante, nada distingue esse atendimento de um normal. + Presença: `DISPONIVEL` / `EM_ATENDIMENTO` / `OFFLINE`, com heartbeat. Operador que perde conexão volta para `OFFLINE` automaticamente e suas visitas retornam à fila. Ao resolver, o operador registra `resolution_reason` — obrigatório, e é o que alimenta a análise de por que o modelo autônomo falhou naquele caso. diff --git a/prompts/FASE-9-hardening.md b/prompts/FASE-9-hardening.md index 993be2e..3440a8d 100644 --- a/prompts/FASE-9-hardening.md +++ b/prompts/FASE-9-hardening.md @@ -18,7 +18,7 @@ Playwright para os webs, Maestro para os apps. Cenários obrigatórios: 2. Entrega em um toque, resolvida pela notificação, em menos de 10s 3. Escalonamento completo: ninguém atende → outros moradores → fila → operador → autorizado 4. Recado: ninguém atende, sem módulo de operador → grava → morador resolve depois -5. Unidade inexistente: visita criada, ninguém notificado, expira — **resposta indistinguível de unidade válida** +5. Unidade inexistente: visita criada, ninguém notificado, e a **sequência de estados vista pelo visitante é idêntica** à de uma visita real não atendida (visita-sombra) — incluindo os tempos das transições 6. Fora do geofence: bloqueado com mensagem acionável 7. Quiet hours: desvia sem tocar em ninguém 8. Dois moradores respondendo juntos: um vence, o outro vê quem resolveu @@ -56,7 +56,7 @@ Derrube cada peça, uma por vez, com uma visita em andamento, e verifique o comp Percorra o modelo de ameaças de `06-LGPD-E-SEGURANCA.md` §5 e prove cada mitigação: -- [ ] **A.1 Busca cega** — tempo de resposta constante medido estatisticamente, não no olho. Este é o teste mais importante da fase +- [ ] **A.1 Busca cega** — tempo de resposta constante medido estatisticamente, não no olho, **e** linha do tempo de estados idêntica entre unidade real não atendida e visita-sombra. Este é o teste mais importante da fase - [ ] **A.2 Geofence** — rejeita fora do raio; GPS falsificado é registrado - [ ] **A.3 DoS social** — rate limit e quiet hours funcionam; varredura gera alerta - [ ] **A.4 Mídia** — nenhuma URL pública; assinada expira; acesso auditado