@seedgrid/ai-toolkit 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +3 -3
  2. package/dist/adapters/shared.js +12 -7
  3. package/dist/cli.js +1 -1
  4. package/dist/commands/review.js +2 -2
  5. package/dist/core/plan.js +5 -0
  6. package/docs/arquitetura.md +2 -2
  7. package/docs/compatibilidade.md +1 -1
  8. package/docs/formato.md +4 -4
  9. package/package.json +1 -1
  10. package/seedgridKB/SeedGrid/Extensions/Catalogo-de-propriedades.md +3 -2
  11. package/seedgridKB/SeedGrid/Extensions/Indice.md +1 -0
  12. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Configuracao.md +11 -18
  13. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Exemplos.md +2 -2
  14. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Visao-geral.md +2 -2
  15. package/seedgridKB/SeedGrid/Extensions/business-days/Configuracao.md +36 -0
  16. package/seedgridKB/SeedGrid/Extensions/business-days/Exemplos.md +37 -0
  17. package/seedgridKB/SeedGrid/Extensions/business-days/Visao-geral.md +48 -0
  18. package/seedgridKB/SeedGrid/Extensions/exchange/Configuracao.md +10 -16
  19. package/seedgridKB/SeedGrid/Extensions/exchange/Exemplos.md +2 -2
  20. package/seedgridKB/SeedGrid/Extensions/exchange/Visao-geral.md +2 -2
  21. package/seedgridKB/SeedGrid/Extensions/login-tenancy/Configuracao.md +4 -5
  22. package/seedgridKB/SeedGrid/Extensions/login-tenancy/Visao-geral.md +1 -1
  23. package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Configuracao.md +3 -2
  24. package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Visao-geral.md +2 -2
  25. package/seedgridKB/SeedGrid/Mapa-dos-repositorios.md +9 -0
  26. package/seedgridKB/SeedGrid/Produtos/Pulse/Autenticacao-e-chaves.md +89 -0
  27. package/seedgridKB/SeedGrid/Produtos/Pulse/Exemplos.md +44 -0
  28. package/seedgridKB/SeedGrid/Produtos/Pulse/Protocolo-e-API.md +191 -0
  29. package/seedgridKB/SeedGrid/Produtos/Pulse/SDKs.md +287 -0
  30. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Configuracao.md +128 -0
  31. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Instalacao.md +105 -0
  32. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Operacao.md +86 -0
  33. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Providers.md +121 -0
  34. package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Configuracao.md +61 -0
  35. package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Visao-geral.md +74 -0
  36. package/seedgridKB/SeedGrid/Produtos/Pulse/Visao-geral.md +96 -0
  37. package/seedgridKB/SeedGrid/Toolkit/agents/arquiteto.md +1 -1
  38. package/seedgridKB/SeedGrid/Toolkit/agents/backend-java.md +2 -2
  39. package/seedgridKB/SeedGrid/Toolkit/agents/backend-rust.md +1 -1
  40. package/seedgridKB/SeedGrid/Toolkit/agents/frontend-react.md +1 -1
  41. package/seedgridKB/SeedGrid/Toolkit/agents/qa.md +2 -2
  42. package/seedgridKB/SeedGrid/Toolkit/agents/revisor.md +2 -2
  43. package/seedgridKB/SeedGrid/Toolkit/references/fluxo-de-trabalho.md +3 -3
  44. package/seedgridKB/SeedGrid/Toolkit/rules/backend-java-seedgrid.md +27 -3
  45. package/seedgridKB/SeedGrid/Toolkit/rules/coordenacao.md +1 -1
  46. package/seedgridKB/SeedGrid/Toolkit/rules/documentos.md +26 -0
  47. package/seedgridKB/SeedGrid/Toolkit/rules/frontend-seedgrid.md +2 -2
  48. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-bump → sg-bump}/SKILL.md +1 -1
  49. package/seedgridKB/SeedGrid/Toolkit/skills/{revisar-codigo → sg-code-review}/SKILL.md +2 -2
  50. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-backend-crud → sg-crud-back}/SKILL.md +4 -4
  51. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud → sg-crud-front}/SKILL.md +5 -5
  52. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud → sg-crud-front}/agents/openai.yaml +1 -1
  53. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/routing.md → sg-crud-front/references/crud-front-routing.md} +1 -1
  54. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen → sg-new-screen}/SKILL.md +3 -3
  55. package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-screen/agents/openai.yaml +4 -0
  56. package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-skill/SKILL.md +50 -0
  57. package/seedgridKB/SeedGrid/Toolkit/skills/{planejar-mudanca → sg-plan-change}/SKILL.md +1 -1
  58. package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-review → sg-review}/SKILL.md +1 -1
  59. package/seedgridKB/SeedGrid/Toolkit/skills/{validar-mudanca → sg-validate-change}/SKILL.md +1 -1
  60. package/seedgridKB/SeedGrid/Visao-geral.md +7 -0
  61. package/seedgridKB/manifest.json +32 -23
  62. package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/agents/openai.yaml +0 -4
  63. /package/seedgridKB/SeedGrid/Toolkit/skills/{revisar-codigo/references/checklist.md → sg-code-review/references/code-review-checklist.md} +0 -0
  64. /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/checklist.md → sg-crud-front/references/crud-front-checklist.md} +0 -0
  65. /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/patterns.md → sg-crud-front/references/crud-front-patterns.md} +0 -0
  66. /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen/references/checklist.md → sg-new-screen/references/new-screen-checklist.md} +0 -0
  67. /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen/references/steps.md → sg-new-screen/references/new-screen-steps.md} +0 -0
@@ -72,8 +72,8 @@ flowchart LR
72
72
  - **Resolução** (Confirmado): `TenantResolutionFilter` lê `X-Forwarded-Host` (prioritário) ou `Host`, extrai o tenant com `TenantHostExtractor` e grava no `TenantContext` e no MDC (`tenantId`); sem tenant → `master`. O Javadoc do filtro deixa explícito que o default para `master` **não** é fronteira de segurança: o isolamento vem do JWT/API key serem resolvidos no banco do próprio tenant.
73
73
  - **JWT**: `SubdomainTenancyJwtTokenCustomizer` adiciona as claims `subdomain` e `audience` (`platform`/`client`); `SubdomainTenancyJwtTokenValidator` rejeita token cujo `subdomain` difere do host.
74
74
  - **Conexões**: `TenantConnectionResolverImpl` verifica no master se o tenant existe e está ativo (`TenantNotFoundException` caso contrário), cria `SimpleDriverDataSource` por tenant, aplica limite/evicção e circuit breaker. Detalhes dos parâmetros em [[SeedGrid/Extensions/subdomain-tenancy/Configuracao|Configuração]].
75
- - **Startup** (`MigrationService#onStart`, `@Priority(APPLICATION)`): no master roda, em ordem, security → audit (se `jpa`) → backup → billing → apikeys → ai-pricing/exchange (se habilitados) → subdomain-tenancy → `db/migration/master` (+ additional), depois `AdminUserBootstrapService#seedAdminUserIfNeeded`. Tenants não migram no boot por padrão.
76
- - **Signup → provisionamento**: `POST /public/tenants` grava o tenant pendente e envia OTP; `POST /public/tenants/validate-email` valida o código e chama `TenantProvisioningService#createTenantWithAdmin` (lock local + `pg_try_advisory_lock`, `CREATE DATABASE` idempotente, migrations security → tenant → audit → backup → apikeys, usuário root; rollback com `DROP DATABASE` se o banco foi criado no fluxo), depois `SubdomainBillingAccounts#ensureFor` (conta + trial no billing) e ativa o tenant.
75
+ - **Startup** (`MigrationService#onStart`, `@Priority(APPLICATION)`): no master roda, em ordem, security → audit (se `jpa`) → backup → billing → apikeys → ai-pricing → exchange → business-days (módulos de produto, `modules.<x>.enabled`, default `true`) → subdomain-tenancy → `db/migration/master` (+ additional), depois `AdminUserBootstrapService#seedAdminUserIfNeeded`. Tenants não migram no boot por padrão.
76
+ - **Signup → provisionamento**: `POST /public/tenants` grava o tenant pendente e envia OTP; `POST /public/tenants/validate-email` valida o código e chama `TenantProvisioningService#createTenantWithAdmin` (lock local + `pg_try_advisory_lock`, `CREATE DATABASE` idempotente, migrations security → tenant → audit → backup → apikeys → business-days, usuário root; rollback com `DROP DATABASE` se o banco foi criado no fluxo), depois `SubdomainBillingAccounts#ensureFor` (conta + trial no billing) e ativa o tenant.
77
77
  - **Migração por tenant**: `TenantMigrationCoordinator` com estado em `tenant_migration_state` (`PENDING`/`MIGRATING`/`READY`/`FAILED`), lock com fencing token e heartbeat. Gatilhos: login (gate devolve `503` + `Retry-After: 15`), endpoint manual, ou boot.
78
78
  - **Limpeza**: `TenantCleanupScheduler` remove do master os cadastros não validados mais velhos que o TTL (não remove o banco — ver Limitações).
79
79
 
@@ -25,6 +25,7 @@ Estado na análise (2026-10-14): todos na branch `main` e **sem alterações loc
25
25
  | `seedgrid-quarkus-ext-billing` | Extension (core + stripe) | 2026.10.14 | `ba38efc347` | 2026-10-14 | [[SeedGrid/Extensions/billing/Visao-geral\|billing]] |
26
26
  | `seedgrid-quarkus-ext-ai-pricing` | Extension | 2026.10.13 | `0608ca66b5` | 2026-10-13 | [[SeedGrid/Extensions/ai-pricing/Visao-geral\|ai-pricing]] |
27
27
  | `seedgrid-quarkus-ext-exchange` | Extension | 2026.10.13 | `30f1fd6005` | 2026-10-13 | [[SeedGrid/Extensions/exchange/Visao-geral\|exchange]] |
28
+ | `seedgrid-quarkus-ext-business-days` | Extension (repositório ainda não criado) | 2026.10.15 | — | 2026-10-16 | [[SeedGrid/Extensions/business-days/Visao-geral|business-days]] |
28
29
  | `seedgrid-quarkus-ext-audit` | Extension (core + postgresql + mongodb) | 2026.09.25 | `d5bf32e509` | 2026-09-25 | [[SeedGrid/Extensions/audit/Visao-geral\|audit]] |
29
30
  | `seedgrid-quarkus-ext-backup` | Extension | 2026.10.09 | `3bed4075ea` | 2026-10-10 | [[SeedGrid/Extensions/backup/Visao-geral\|backup]] |
30
31
  | `seedgrid-quarkus-ext-notification` | Extension (+ `api`) | 2026.10.15 | `3d00bee` | 2026-10-15 | [[SeedGrid/Extensions/notification/Visao-geral\|notification]] |
@@ -64,6 +65,14 @@ Documentação: [[SeedGrid/SeedGrid-components/Indice|Índice dos componentes]].
64
65
 
65
66
  `front/next-projects/` contém apenas documentação de padrões/decisões (sem código) e não foi analisado.
66
67
 
68
+ ### Produtos (`seedgrid-products/`)
69
+
70
+ Fora de `seedgrid-platform`; consomem as crates/extensions acima.
71
+
72
+ | Repositório | Tipo | Versão | Commit | Consome | Nota |
73
+ |---|---|---|---|---|---|
74
+ | `seedgrid-pulse` | Produto (servidor Rust + UI Next.js + SDKs) | 0.3.0 (servidor e UI) | `9dcbdad` | crates `security`, `login-tenancy`, `audit`, `apikeys`, `broker-*` (por path dependency) | [[SeedGrid/Produtos/Pulse/Visao-geral\|Pulse]] |
75
+
67
76
  ## Dependências entre módulos Java
68
77
 
69
78
  Extraído dos `pom.xml` (excluindo `deployment` e `integration-tests`). Escopo `compile` salvo indicação.
@@ -0,0 +1,89 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, autenticacao, chaves, seguranca]
3
+ fonte: seedgrid-products/seedgrid-pulse (backend/env.md, backend/PROTOCOL.md, migrations)
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse — autenticação, chaves e permissions
10
+
11
+ O Pulse tem **credenciais distintas para atores distintos**. Elas nunca se substituem; mandar a errada é o erro mais comum de integração.
12
+
13
+ Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]]
14
+
15
+ ## Quem usa o quê
16
+
17
+ | Ator | Credencial | Como envia | Serve para |
18
+ |---|---|---|---|
19
+ | **Backend de produto** (publica) | chave de conta **`project`** (`pulsek_…`) 🔒 | `x-api-key` ou `Authorization: Bearer` | publicar **só na conta da chave** e emitir token curto. **Não conecta** no WebSocket |
20
+ | **Navegador / app** (recebe) | **token curto** `pct_…` | `?access_token=` ou subprotocolo | conectar como **um usuário** (`subject`) da conta |
21
+ | **Agente instalado / serviço** (recebe) | chave de conta **`client`** 🔒 (com ou sem `subject`) | header, `?access_token=` ou subprotocolo | conectar como a conta (e, com `subject`, como um usuário). **Não publica** (403) |
22
+ | **Operação do Pulse** | `SEEDGRID_PULSE_SERVER_KEY` 🔒 | `x-api-key` | gerenciar contas/chaves e métricas. **Não publica** (403) |
23
+ | **Operador humano** (UI) | JWT do `security` | `Authorization: Bearer` | telas por **permission**; publica em qualquer conta com `PULSE_MESSAGE_PUBLISH` informando `accountId` |
24
+
25
+ Regras que mordem:
26
+
27
+ - O `kind` da chave (`project` \| `client`) é **obrigatório na emissão** e preservado na rotação. Chave `project` não tem `subject` (422).
28
+ - Quem publica é decidido pelo **`kind`**, não pelo `subject`.
29
+ - A chave de servidor **não vai para produto nenhum**.
30
+ - Chave `project` **nunca no navegador** nem em app distribuído.
31
+
32
+ ## Contas e chaves
33
+
34
+ - **Conta** = fronteira de isolamento (entrega e replay nunca cruzam contas). Um produto ↔ uma ou mais contas.
35
+ - Criar chave: UI **Contas → conta → Chaves → Nova chave**, ou `POST /api/v1/accounts/{id}/keys` com `{ "name", "kind", "subject"? }`.
36
+ - O **segredo aparece uma vez** (UI com botão de copiar; API no campo `secret`). Depois só resta o prefixo (`keyPrefix`). Perdeu → **rotacionar** (`POST /accounts/{id}/keys/{keyId}/rotate`).
37
+ - A chave vive no banco (por conta, revogável, rotacionável); o banco guarda hash + prefixo. Revogar/rotacionar uma chave `client` ou revogar a conta derruba as conexões dela **na hora**, em todas as instâncias (fecha com `4403`).
38
+ - **Revogar** é conceito da **chave**. A **conta** não se revoga: vai para a **lixeira de contas** e depois é apagada (ver [[SeedGrid/Produtos/Pulse/Servidor/Operacao|Operação]]).
39
+
40
+ ### O `subject` da chave
41
+
42
+ Definido na emissão e **imutável** (rotação preserva). É o `user` do protocolo. Use um id estável e não-PII (`usr_7f3a9c1e`, `agent:<uuid>`): nunca e-mail nem hostname. Sem `subject` = credencial de serviço (sem `user`, sem grupo implícito).
43
+
44
+ ## Token curto de conexão (cliente sem segredo)
45
+
46
+ O cliente nunca recebe chave. O **backend do produto**, com a chave `project`, pede um token para o usuário **da sessão** e devolve só o token:
47
+
48
+ ```http
49
+ POST /api/v1/connect-tokens
50
+ x-api-key: <chave project>
51
+
52
+ { "subject": "<publicId da sessão>", "ttlSeconds": 300, "channels": ["orders"] }
53
+ ```
54
+
55
+ ```json
56
+ { "token": "pct_…", "expiresAt": 1791205500, "connectUrl": "wss://…/api/v1/connect" }
57
+ ```
58
+
59
+ - `subject`: **da sessão, nunca de parâmetro do request** (senão um usuário pede token em nome de outro).
60
+ - `ttlSeconds` (padrão 300, teto 900): janela para **conectar**. A conexão aberta segue depois do vencimento até cair ou a credencial ser revogada.
61
+ - `channels` (opcional): restringe o que pode assinar (`channel_not_allowed` fora disso).
62
+ - `connectUrl` só vem se `SEEDGRID_PULSE_PUBLIC_WS_URL` estiver configurada.
63
+ - Erros: 401 chave inválida · 403 chave `client`/de servidor · 422 validação · 429 limite (com `Retry-After`) · 503 sem `SEEDGRID_PULSE_CONNECT_TOKEN_SECRET`.
64
+ - O token é assinado (HMAC-SHA256). Revogar a chave `project` que o emitiu ou a conta o invalida.
65
+ - Os SDKs subscriber pedem token a **cada (re)conexão** via `getToken`.
66
+
67
+ ## Operador humano: JWT e permissions
68
+
69
+ Login pelo `security` (JWT RS256, 2FA opcional). Cada endpoint administrativo exige uma **permission** (ou a chave de servidor, para automação). O papel `ADMIN` herda as da migration; outros perfis são configurados em **Segurança → Perfis**.
70
+
71
+ | Grupo | Permissions |
72
+ |---|---|
73
+ | Mensagens | `PULSE_MESSAGE_READ`, `PULSE_MESSAGE_PUBLISH`, `PULSE_MESSAGE_DELETE`, `PULSE_MESSAGE_PURGE` |
74
+ | Pendências / DLQ | `PULSE_BACKLOG_READ`, `PULSE_BACKLOG_DISCARD`, `PULSE_DLQ_READ`, `PULSE_DLQ_REPLAY`, `PULSE_DLQ_CLEAN` |
75
+ | Conexões / métricas / ajustes | `PULSE_CONNECTION_READ`, `PULSE_METRIC_READ`, `PULSE_SETTINGS_READ`, `PULSE_SETTINGS_UPDATE` |
76
+ | Contas | `PULSE_ACCOUNT_READ`, `_CREATE`, `_UPDATE`, `_REVOKE`, `PULSE_ACCOUNT_DELETE`, `PULSE_ACCOUNT_PURGE` |
77
+ | Chaves | `PULSE_KEY_READ`, `_CREATE`, `_ROTATE`, `_REVOKE`, `_PURGE` |
78
+ | Segurança | `PULSE_SECURITY_READ`, `_ADMIN`, `_USER_MANAGE`, `_ROLE_MANAGE` |
79
+
80
+ (Confirmado nas migrations `backend/migrations/tenant/`. As quatro de lixeira vêm da `0012`, concedidas só ao ADMIN; `DELETE` e `PURGE` são separadas de propósito.)
81
+
82
+ **Operações destrutivas**: exigem JWT humano (a chave de servidor recebe 401, pois toda exclusão precisa de autor) e gravam **audit** (quem, o quê, quantos).
83
+
84
+ ## Boas práticas
85
+
86
+ - Chave `project` em variável de ambiente/secret do backend; rotacione ao suspeitar de vazamento.
87
+ - Um `subject` por pessoa (id estável), nunca compartilhado entre pessoas.
88
+ - Cliente de navegador/mobile: **sempre** token curto, nunca chave.
89
+ - Exemplos de demo nos repositórios de exemplo usam chaves de demonstração; não as use fora de dev.
@@ -0,0 +1,44 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, exemplos]
3
+ fonte: seedgrid-products/seedgrid-pulse/examples
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse — exemplos executáveis
10
+
11
+ Quatro aplicações de exemplo com **a mesma tela e o mesmo roteiro**; só muda a linguagem. Em todas, publisher e subscriber são **independentes** (cada um com sua chave, conexão, SDK e painel): copie para o seu projeto só o lado que precisa. Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · SDKs: [[SeedGrid/Produtos/Pulse/SDKs|SDKs]].
12
+
13
+ | Exemplo | Pasta | Publisher | Subscriber | Como rodar |
14
+ |---|---|---|---|---|
15
+ | Delphi XE6 (VCL) | `examples/delphi-vcl` | `IPulsePublisher` (REST) | `IPulseSubscriber` Indy, auto-ack | abrir o projeto no Delphi |
16
+ | Flutter (Android) | `examples/flutter` | `seedgrid_pulse_publisher` | `seedgrid_pulse_subscriber` | `flutter run` (`--dart-define=PULSE_BASE_URL=…`) |
17
+ | React (Vite) | `examples/react` | `@seedgrid/pulse-publisher-next` via backend de apoio | `@seedgrid/pulse-subscriber-react` | `npm install` · `npm start` |
18
+ | Angular | `examples/angular` | `@seedgrid/pulse-publisher-next` via backend de apoio | `@seedgrid/pulse-subscriber-angular` | `npm install` · `npm start` (API de apoio em `8787` + `ng serve` em `4200`) · `npm test` |
19
+
20
+ ## O que a tela faz
21
+
22
+ - **Publisher:** Conectar (só prepara URL + chave; HTTP é stateless) · Desconectar · Publicar (canal, evento, destino conta/grupo/usuário, JSON).
23
+ - **Subscriber:** Conectar · Desconectar · Inscrever · Desinscrever, com a lista de inscrições ativas e o log de mensagens recebidas.
24
+ - Roteiro de ponta a ponta: conectar os dois, inscrever-se em `orders`, publicar em `orders` para a **conta inteira** — a mensagem aparece no log do subscriber.
25
+
26
+ ## Duas chaves, dois papéis
27
+
28
+ - Chave **`client`** conecta, assina e recebe; **não publica** (403).
29
+ - Chave **`project`** publica; **não conecta** (403 no handshake).
30
+ - Após conectar, `subscribe` e mensagens trafegam no mesmo socket, sem reenviar a chave.
31
+ - As telas trazem **chaves de demonstração** pré-preenchidas (sobrescrevíveis por variável de ambiente / `--dart-define`). A chave `project` é credencial de servidor: não a distribua em app de usuário final.
32
+
33
+ ## Receber só de um usuário
34
+
35
+ O **usuário vem da credencial**, não do `subscribe`. Com o campo **Usuário** preenchido (`usr_1`, `usr_2`), a tela conecta com um **token curto** emitido para esse usuário; `usr_2` não recebe o que foi publicado para `usr_1`. Vazio, conecta com a chave `client` como *serviço* (só mensagens da conta inteira ou de grupos). Para trocar de usuário, desconecte.
36
+
37
+ ## O "backend do produto" nos exemplos web
38
+
39
+ O SDK de publicação para Node é **server-only**, então React e Angular trazem um backend mínimo (`/api/publish` e `/api/connect-token`) que usa `createPulsePublisher` e `issueConnectToken`; o navegador só chama esse backend e o WebSocket vai direto ao Pulse com o token. **No seu produto** a chave fica em variável de ambiente no servidor e o `subject` vem da **sessão** do usuário, nunca do request.
40
+
41
+ ## Limitações conhecidas
42
+
43
+ - O SDK Angular não tem `unsubscribe`, lista de inscrições nem `autoAck`; o exemplo Angular faz isso na própria tela (envia a união das inscrições e, para desinscrever, reconecta só com o que sobrou). Os exemplos Delphi, Flutter e React usam os recursos nativos do SDK.
44
+ - O exemplo Flutter para repassar a outra pessoa pode ser empacotado com os SDKs copiados em `sdk/subscriber` e `sdk/publisher` (dependências `path:`), sem precisar do monorepo.
@@ -0,0 +1,191 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, protocolo, api, websocket]
3
+ fonte: seedgrid-products/seedgrid-pulse (backend/PROTOCOL.md, backend/README.md, src/openapi.rs)
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse — protocolo realtime e API HTTP
10
+
11
+ Resumo do contrato. Fonte completa: `backend/PROTOCOL.md` (protocolVersion **1**) e a spec OpenAPI gerada pelo servidor. Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · credenciais: [[SeedGrid/Produtos/Pulse/Autenticacao-e-chaves|Autenticação]].
12
+
13
+ ## Endereçamento
14
+
15
+ Uma mensagem tem: `accountId` (da chave), `channel` (obrigatório), `event` (concreto, obrigatório), `group` e `user` (opcionais, independentes), `data`.
16
+
17
+ | `group` | `user` | entrega para |
18
+ |---|---|---|
19
+ | — | — | todas as conexões **da conta** no canal |
20
+ | G | — | quem assinou G |
21
+ | — | U | as conexões do usuário U |
22
+ | G | U | as conexões de U **que também assinaram** G |
23
+
24
+ - `user` vem da **credencial** (subject), nunca do `subscribe`. `group` é **namespace, não autorização**: trate a mensagem como **sinal**, não como conteúdo sensível.
25
+ - `group`/`user` não podem ser vazios nem conter `|`. Use `publicId`, nunca e-mail.
26
+ - Fronteira = conta; sem destinatário a mensagem fica `pending` até alguém conectar (TTL expira).
27
+
28
+ ## Publicar (REST)
29
+
30
+ ```http
31
+ POST /api/v1/messages
32
+ x-api-key: <chave project>
33
+ ```
34
+
35
+ ```json
36
+ {
37
+ "channel": "orders",
38
+ "event": "order.updated",
39
+ "user": "usr_7f3a9c1e",
40
+ "deliveryMode": "notification",
41
+ "data": { "orderId": "o-1", "status": "paid" },
42
+ "deliveryPolicy": { "maxAttempts": 5, "ttlSeconds": 3600, "retryBaseMs": 250, "retryMaxMs": 5000, "retryStrategy": "exponential" }
43
+ }
44
+ ```
45
+
46
+ Resposta: `{ "messageId": "msg_…", "status": "accepted", "createdAt": "…" }`. `deliveryPolicy` é opcional e limitada pelos tetos `*_CAP` do servidor (campo a campo em [[SeedGrid/Produtos/Pulse/Protocolo-e-API#deliveryPolicy: campo a campo|deliveryPolicy]]). Erros: 400 requisição malformada · 422 `deliveryPolicy`/endereçamento inválido · 401 credencial · 403 `ACCOUNT_MISMATCH`/chave `client` · 404/422 conta inexistente ou revogada, endereçamento ou `event` não concreto.
47
+
48
+ ## Delivery modes
49
+
50
+ | Modo | Fim do ciclo | `ack` | `result` | DLQ |
51
+ |---|---|---|---|---|
52
+ | `notification` (padrão) | escrita no socket ⇒ `completed` | no-op | erro `result_not_applicable` | nunca |
53
+ | `acknowledged` | `ack` do cliente ⇒ `completed` | termina | erro `result_not_applicable` | nunca |
54
+ | `processed` | `result: success` ⇒ `completed` | ⇒ `delivered` | `success` / `retryable` / `permanent` | **sim** |
55
+
56
+ Só o `processed` consome tentativas e chega à DLQ. Offline ou falha de transporte **não** vão à DLQ (a mensagem fica `pending`).
57
+
58
+ ## deliveryPolicy: campo a campo
59
+
60
+ Opcional, por mensagem. Diz **como insistir**, não para quem entregar. Todo campo é opcional: ausente ⇒ vale o padrão do servidor. Uso nos SDKs: [[SeedGrid/Produtos/Pulse/SDKs|SDKs]].
61
+
62
+ | Campo | Tipo | Padrão do servidor | O que faz |
63
+ |---|---|---|---|
64
+ | `maxAttempts` | inteiro > 0 | `20` (`DELIVERY_MAX_ATTEMPTS`) | Quantas falhas de **execução** (`result: retryable`) a mensagem aguenta. Só vale em `processed`. A cada `retryable` o contador sobe; quando passa de `maxAttempts` (ou seja, na falha de número `maxAttempts + 1`) a mensagem vai à **DLQ**. Falha de transporte (cliente offline, escrita no socket) **não** consome tentativa. `result: permanent` vai à DLQ na hora, independentemente deste valor. |
65
+ | `ttlSeconds` | inteiro > 0 | `86400` = 1 dia (`DELIVERY_TTL_SECONDS`) | Validade da mensagem, contada a partir da publicação. Se ao fim do prazo ela ainda estiver `pending` (ninguém a recebeu), o estado vira `expired` e ela não é mais entregue. Vale para **todos** os modos. Expirar **não** manda para a DLQ nem consome tentativa. Veja [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Como o TTL é aplicado|Como o TTL é aplicado]]. |
66
+ | `resultTimeoutSeconds` | inteiro > 0 (s) | `0` = **sem prazo** (`DELIVERY_RESULT_TIMEOUT_SECONDS`) | **Prazo de execução**, só em `processed`: segundos entre o `ack` e o `result`. Estourado sem `result`, vale como um `result: retryable` com motivo `result_timeout` (consome tentativa; esgotadas, vai à **DLQ**). Detalhes em [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Prazo de execução (resultTimeoutSeconds)|Prazo de execução]]. |
67
+ | `retryBaseMs` | inteiro > 0 (ms) | `300` (`DELIVERY_RETRY_BASE_MS`) | Atraso antes da próxima tentativa. Na estratégia `exponential` é o atraso inicial; em `fixed` é o atraso de toda tentativa. |
68
+ | `retryMaxMs` | inteiro > 0 (ms), `>= retryBaseMs` | `5000` (`DELIVERY_RETRY_MAX_MS`) | Teto do atraso entre tentativas. |
69
+ | `retryStrategy` | `"fixed"` \| `"exponential"` | `exponential` (`DELIVERY_RETRY_STRATEGY`) | Como o atraso cresce. |
70
+
71
+ **Cálculo do atraso** (Confirmado, `DeliveryPolicy::next_delay` no servidor), com *n* = número da tentativa que falhou:
72
+
73
+ - `exponential`: `min(retryBaseMs × 2^(n−1), retryMaxMs)`. Com base 200 e teto 2000: 200, 400, 800, 1600, 2000, 2000…
74
+ - `fixed`: `min(retryBaseMs, retryMaxMs)`, sempre o mesmo atraso.
75
+ - No `result: retryable`, se o cliente mandar `retryAfterSecs`, **ele vale no lugar** do cálculo acima.
76
+ - O mesmo atraso espaça as tentativas de transporte quando o destinatário está offline ou a escrita no socket falha (a mensagem fica `pending` até o TTL).
77
+
78
+ **Validação** (rejeita com **422** `INVALID_MESSAGE_REQUEST`): qualquer campo numérico igual a `0` (inclusive `resultTimeoutSeconds`); `retryMaxMs < retryBaseMs` quando os dois vêm; `retryStrategy` diferente de `fixed`/`exponential` (minúsculas).
79
+
80
+ **Tetos do servidor.** O que o publisher manda **nunca passa** do teto configurado (`DELIVERY_MAX_ATTEMPTS_CAP`, `DELIVERY_TTL_SECONDS_CAP`, `DELIVERY_RETRY_BASE_MS_CAP`, `DELIVERY_RETRY_MAX_MS_CAP`, `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP`): acima dele o valor é **reduzido em silêncio**, sem erro. Como cada teto, quando não definido, é igual ao padrão, **por padrão uma mensagem só consegue pedir menos que o padrão**: `maxAttempts: 50` vira `20`, `ttlSeconds: 604800` vira `86400`. Para permitir mais, suba o `*_CAP` no servidor ([[SeedGrid/Produtos/Pulse/Servidor/Configuracao|Configuração do servidor]]). Dentro dos tetos, `retryMaxMs` nunca fica abaixo do `retryBaseMs` efetivo. Exceção: o teto do prazo de execução tem padrão `86400` (e não o valor do padrão, que é `0`), então o publisher consegue pedir até 1 dia mesmo com o recurso desligado por padrão; `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP=0` **proíbe** o prazo de execução.
81
+
82
+ **Quando usar.** Em `notification` e `acknowledged` só o `ttlSeconds` (e o espaçamento das tentativas de transporte) importa; `maxAttempts` e a DLQ só entram em `processed`. Exemplo para um job que pode ser reentregue algumas vezes e deve morrer em 5 minutos:
83
+
84
+ ```json
85
+ "deliveryMode": "processed",
86
+ "deliveryPolicy": { "maxAttempts": 5, "ttlSeconds": 300, "resultTimeoutSeconds": 120, "retryBaseMs": 200, "retryMaxMs": 2000, "retryStrategy": "exponential" }
87
+ ```
88
+
89
+ ### Como o TTL é aplicado
90
+
91
+ - **Quando é verificado.** A cada tentativa de entrega e por um verificador periódico que roda a cada `ceil(DELIVERY_RETRY_BASE_MS / 1000)` segundos, no mínimo 1 s (com o padrão de 300 ms, **a cada 1 s**). Na prática a mensagem expira com até ~1 s de atraso depois do prazo. Esse verificador usa o valor **do servidor**, não o `retryBaseMs` da mensagem. Ele **não** tem relação com o intervalo de limpeza (`DELIVERY_CLEANUP_INTERVAL_SECONDS`), que cuida só de retenção e lixeira.
92
+ - **Só vale para `pending`.** O TTL mede quanto tempo a mensagem espera por um destinatário. Depois que o `ack` chega, uma mensagem `processed` fica `delivered`, e a partir daí o TTL **não a expira mais**. Quem cobre essa fase é o prazo de execução (`resultTimeoutSeconds`, veja abaixo). Sem esse prazo (o padrão é `0` = sem prazo), uma tarefa que o consumidor recebeu e nunca respondeu fica `delivered` até o `result` chegar ou até um operador descartar.
93
+ - **Descarte manual antecipa o TTL.** Em Pendências, "descartar" (`POST /backlog/discard`) leva `pending` **e** `delivered` sem resultado para `expired` na hora. A mensagem continua visível em "Mensagens" e só é apagada pela retenção (`DELIVERY_CLEANUP_RETENTION_DAYS`).
94
+ - **Lixeira.** Mensagem na lixeira não expira por TTL.
95
+ - **Não confunda com o token.** `ttlSeconds` também existe em `issueConnectToken`, mas ali é a janela do token curto para **conectar** (padrão 300 s, teto 900 s). Os dois nomes são iguais e não têm relação entre si.
96
+ - **O publisher não é avisado.** O publish responde `202 accepted` e pronto. Que a mensagem expirou aparece na UI (Mensagens, filtro `expired`), na métrica `delivery_expired_total` e no log de tentativas da mensagem (`outcome: expired`, `message_ttl_exceeded`).
97
+
98
+ ### Prazo de execução (resultTimeoutSeconds)
99
+
100
+ Disponível a partir do Pulse **0.3.0** (Confirmado no commit `9dcbdad`). Cobre o caso da tarefa que o consumidor recebeu e travou.
101
+
102
+ - **Só `processed`.** Em `notification` e `acknowledged` não há `result`, então o campo não tem efeito.
103
+ - **Conta do `ack` até o `result`** (`delivered_at`). Cada nova entrega confirmada reinicia a contagem.
104
+ - **Estourou sem `result`:** o servidor aplica o mesmo tratamento de um `result: retryable`: soma 1 à tentativa, devolve a mensagem a `pending` com o backoff da `deliveryPolicy` e a reentrega. Se `attemptCount > maxAttempts`, vai à DLQ com `lastExecutionError = "result_timeout"`. No log de tentativas aparece como `execution_retryable` com detalhe `result_timeout` (não existe outcome separado).
105
+ - **Quando é verificado:** pelo mesmo verificador periódico do retry (a cada ~1 s com o padrão), então o estouro é detectado com até ~1 s de atraso.
106
+ - **Padrão `0` = sem prazo** (`DELIVERY_RESULT_TIMEOUT_SECONDS`), para não mudar o comportamento de quem já usa o Pulse. Peça o prazo por mensagem, ou o operador define um padrão em Configurações. `0` **enviado** no `deliveryPolicy` é inválido (422); para não ter prazo, omita o campo.
107
+ - **Teto:** `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP` (padrão `86400`; `0` proíbe). Acima do teto, o valor é reduzido em silêncio, como nos outros campos.
108
+ - **`result` tardio é ignorado.** Se o `result` da tentativa antiga chegar depois do estouro, enquanto a mensagem espera a nova tentativa (`pending` com `lastExecutionError = "result_timeout"`), o servidor o ignora (sem erro de protocolo e sem alterar o estado). Depois que a nova tentativa é entregue e confirmada, o `result` volta a valer. **Limitação:** não há identificador de tentativa; um `result` antigo que chegue *depois* da reentrega ser confirmada é aceito.
109
+ - **Mensagens antigas:** as `delivered` que já existiam antes da migration (`0013`) ficam **sem prazo**; o descarte manual em Pendências continua sendo a saída para elas.
110
+ - **Cuidado (a tarefa pode rodar duas vezes).** Um prazo curto reentrega uma tarefa que ainda está em execução. Escolha um prazo **maior que o pior caso** da tarefa e torne o consumidor **idempotente** (deduplicando por `messageId`).
111
+ - **Observabilidade:** motivo `result_timeout` na UI (tela de detalhes da mensagem: "Último erro de execução" e detalhe das tentativas; as telas de DLQ não mostram esse campo) e o campo opcional "Prazo de execução (s)" na tela Publicar e a métrica `seedgrid_pulse_delivery_result_timeout_total`. O endpoint `/meta` expõe o padrão e o teto (`deliveryPolicyDefaults.resultTimeoutSeconds` e `deliveryPolicyCaps.resultTimeoutSeconds`).
112
+
113
+ ## DLQ: quando uma mensagem vai para lá
114
+
115
+ A DLQ (fila de mensagens mortas) guarda **tarefas** que desistiram de executar. Só entra nela mensagem em `processed`, por um destes dois caminhos:
116
+
117
+ | Situação | Vai à DLQ? |
118
+ |---|---|
119
+ | Subscriber responde `result: permanent` | **Sim, na hora**, sem olhar as tentativas que sobram |
120
+ | Subscriber responde `result: retryable` e as tentativas esgotam (`attemptCount > maxAttempts`) | **Sim** |
121
+ | Subscriber responde `result: retryable` e ainda há tentativas | Não: volta a `pending` e é reentregue após o atraso do backoff (ou do `retryAfterSecs`) |
122
+ | Tarefa `delivered` e o prazo de execução (`resultTimeoutSeconds`) estoura sem `result` | Conta como `retryable` com motivo `result_timeout`: volta a `pending` com backoff; **se as tentativas esgotam, sim**, com `lastExecutionError = "result_timeout"` |
123
+ | Subscriber responde `result: success` | Não: `completed` |
124
+
125
+ **Nunca vai à DLQ:**
126
+
127
+ - `notification` e `acknowledged`, mesmo que o destinatário nunca receba (não há execução para falhar; `result` nesses modos é recusado com `result_not_applicable`).
128
+ - Destinatário offline, ou falha ao escrever no socket: a mensagem fica `pending`, tenta de novo e não consome tentativa.
129
+ - TTL vencido: vira `expired`.
130
+ - Tarefa que o consumidor recebeu (`delivered`) e nunca respondeu, **quando não há prazo de execução** (`resultTimeoutSeconds` ausente e padrão `0`): não cai sozinha na DLQ; só o descarte manual a tira dali.
131
+
132
+ **O que fazer com o que está na DLQ** (UI: Falhas, ou API `/dlq/*`, com as permissões `PULSE_DLQ_READ`, `PULSE_DLQ_REPLAY`, `PULSE_DLQ_CLEAN`):
133
+
134
+ - **Reprocessar** (`POST /dlq/requeue`): a mensagem volta a `pending` com o contador de tentativas **zerado**, o erro anterior apagado e a **data de publicação reiniciada**, então o TTL também recomeça.
135
+ - **Limpar** (`/dlq/clean`): vai para a lixeira da DLQ (`dlq_removed`); dá para restaurar (`/dlq/trash/restore`) ou apagar de vez (`/dlq/trash/purge`).
136
+ - O motivo da última falha fica registrado (`reason` do `result`).
137
+
138
+ ## Conectar (WebSocket)
139
+
140
+ ```text
141
+ GET /api/v1/connect?protocolVersion=1
142
+ ```
143
+
144
+ Credencial por um de três caminhos (browser não envia header em WebSocket): header `Authorization: Bearer`, query `?access_token=`, ou subprotocolo `bearer, <credencial>` / `pulse.bearer.<credencial>`. Credencial inválida/ausente ⇒ 401 no upgrade.
145
+
146
+ Fluxo:
147
+
148
+ ```text
149
+ servidor → {"type":"connected","protocolVersion":1,"connectionId":"…"}
150
+ cliente → {"type":"subscribe","channels":["orders"],"groups":["loja:15"],"events":["order.*"]}
151
+ servidor → {"type":"subscribed","accountId":"…","user":"…","channels":[…],"groups":[…,"user:<u>"],"events":[…]}
152
+ servidor → {"type":"message","messageId":"…","channel":"…","event":"…","data":{…},"requiresAck":true,"deliveryMode":"processed"}
153
+ cliente → {"type":"ack","messageId":"…"}
154
+ cliente → {"type":"result","messageId":"…","outcome":"success"|"retryable"|"permanent","reason":"…","retryAfterSecs":30}
155
+ ```
156
+
157
+ - `events`: filtro opcional; `*` só como último segmento e casa **um ou mais** segmentos (`order.*` casa `order.item.updated`, não `order`). Um `subscribe` novo **acumula** canais/grupos e **substitui** o filtro de eventos.
158
+ - O cliente **não declara identidade**: `userId`/`user` enviados são ignorados.
159
+ - Replay: ao assinar, o servidor entrega as pendências da conta que a conexão cobre.
160
+ - Entrega é **at-least-once**: o cliente deve **deduplicar por `messageId`**; `result` é idempotente.
161
+
162
+ Erros realtime: `unsupported_protocol_version`, `invalid_subscribe`, `invalid_ack`, `invalid_result`, `result_not_applicable`, `channel_not_allowed`, `unauthorized`, `internal_error`.
163
+
164
+ Fechamentos: **`4403`** `credential_revoked` (chave/conta revogada — **não reconectar** com a mesma credencial); `4401` legado (token expirado; os SDKs ainda tratam).
165
+
166
+ ## Estados de uma mensagem
167
+
168
+ `pending` → `delivered` → `completed` (terminal) · `dead_letter` (falha de execução) · `expired` (TTL) · `trashed` (na lixeira, só vinda de `pending`) · `dlq_removed` (lixeira da DLQ) · `acknowledged` (legado, lido como `delivered`).
169
+
170
+ ## Endpoints principais
171
+
172
+ | Área | Endpoints |
173
+ |---|---|
174
+ | Mensagens | `POST/GET /api/v1/messages`, `GET /messages/{id}`, `POST /messages/trash`, `/trash/restore`, `/trash/purge` |
175
+ | Pendências | `GET /backlog`, `POST /backlog/discard` |
176
+ | DLQ | `POST /dlq/requeue`, `/dlq/clean`, `/dlq/trash/restore`, `/dlq/trash/purge` |
177
+ | Conexões / presença / stats | `GET /connections`, `GET /presence/groups/{group}`, `GET /stats/connections`, `GET /stats/messages/aggregates` |
178
+ | Token | `POST /connect-tokens` |
179
+ | Contas | `GET/POST /accounts`, `/accounts/{id}`, `/activate`, `/revoke`, `/trash`, `/trash/restore`, `/purge`, `GET /accounts/{id}/deletion-summary`; chaves em `/accounts/{id}/keys` |
180
+ | Ajustes | `GET /api/v1/admin/settings`; alterar/resetar um ajuste em `/api/v1/admin/settings/{key}` (`POST` altera, `DELETE` volta ao padrão) |
181
+ | Operação | `GET /health`, `/ready`, `/meta`, `/metrics`, `/metrics/prometheus` |
182
+
183
+ Lotes de lixeira: máximo **1000** ids (`400 BATCH_TOO_LARGE`); resposta `{requested, affected, skipped}`. Erros de domínio seguem RFC 7807 (`application/problem+json`).
184
+
185
+ ## Observabilidade por destinatário
186
+
187
+ `GET /messages/{id}/recipients` e `GET /recipient-deliveries` (filtros `userId`, `groupId`, `messageId`, `connectionId`, `status`, `acknowledged`) mostram quem recebeu cada mensagem — o histórico serve de **prova de entrega**.
188
+
189
+ ## Compatibilidade
190
+
191
+ Mudanças aditivas (campos opcionais, novos eventos) não mudam `protocolVersion`; mudar semântica de endereçamento, remover `ack` ou alterar o shape de `message` exige incremento. Ordem de rollout quando o protocolo muda: publicar SDKs ⇒ reinstalar agentes ⇒ subir servidor ⇒ UI.