funifier-mcp 0.2.26 → 0.2.27
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.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,41 +1,388 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `webhook`
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Acesso Studio:** nenhuma tela de Studio localizada no `funifier-service` — o módulo é **API-only** (não há referência a `/studio/webhook` ou UI equivalente no código).
|
|
4
|
+
**API Endpoint:** `/v3/hook` (⚠️ **não** `/v3/webhook` — ver seção 4)
|
|
5
|
+
**Coleção MongoDB:** `webhook` (definida em `Entity.WEBHOOK("webhook", Webhook.class)`)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
---
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## 1. Visão Geral
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
O módulo `webhook` implementa um mecanismo de **subscription/notificação push**: um sistema externo registra uma `target_url` associada a um `event`, e a Funifier faz `HTTP POST` nessa URL quando o evento ocorre — evitando que o consumidor faça polling.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- Para notificar CRM quando jogador atinge uma meta
|
|
13
|
-
- Para integrar com ferramentas externas (Slack, Teams, etc.)
|
|
14
|
-
- Para sincronizar dados em tempo real com outros sistemas
|
|
13
|
+
Papel arquitetural e fatos confirmados no código:
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
- A entidade (`Webhook.java`) tem **apenas 3 campos**: `_id`, `target_url`, `event`.
|
|
16
|
+
- Existe **um único evento implementado**: `achievement_created` (constante `Webhook.EVENT_ACHIEVEMENT_CREATED`). Nenhum outro valor de `event` é disparado em qualquer ponto do código.
|
|
17
|
+
- O **único ponto de disparo** em todo o `funifier-service` é `AchievementManager.fireAction(...)`, linha 827. Não há scheduler, job, fila ou outro produtor.
|
|
18
|
+
- O módulo **depende do módulo Achievement** (único produtor de eventos) e é **consumido por sistemas externos**. Não há acoplamento com Trigger, mas a recomendação interna da própria plataforma (texto de prompt em `AIRest.buildZapier`) é usar **Triggers Groovy/Java com `Unirest.post`** para qualquer evento que não seja `achievement_created` — porque o módulo webhook nativo não os suporta.
|
|
19
|
+
|
|
20
|
+
> O módulo resolve o caso "notifique-me quando um jogador ganhar uma conquista". Para qualquer outra necessidade de integração em tempo real, o código não oferece cobertura nativa (ver seção 7).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Arquitetura e Fluxos
|
|
25
|
+
|
|
26
|
+
São dois fluxos independentes: **(A) gestão da inscrição** (REST, síncrono) e **(B) entrega do evento** (disparada pelo Achievement, síncrona).
|
|
27
|
+
|
|
28
|
+
### 2.1 Pipeline A — Gestão da inscrição (REST)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
[POST /v3/hook] → WebhookRest.insert() → WebhookManager.insert(hook)
|
|
32
|
+
[POST /v3/hook/unsub.] → WebhookRest.unsubscribe() → WebhookManager.deleteByURL(target_url)
|
|
33
|
+
[GET /v3/hook] → WebhookRest.findAll() → WebhookManager.findAll()
|
|
34
|
+
[GET /v3/hook/{id}] → WebhookRest.find() → WebhookManager.find(id)
|
|
35
|
+
[DELETE /v3/hook/{id}] → WebhookRest.delete() → WebhookManager.delete(id)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Detalhe de `WebhookManager.insert(hook)`:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
se hook != null e hook.id == null:
|
|
42
|
+
hook.id = Guid.newShortGuid() // = new ObjectId().toString() → hex de 24 chars
|
|
43
|
+
c = getJongoConnection().getCollection("webhook")
|
|
44
|
+
c.save(hook) // Jongo save = UPSERT por _id
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Consequência: se o `_id` for informado no corpo e já existir, `c.save()` **substitui** o documento (full replace) — POST funciona como upsert, não como create estrito (ver seção 4).
|
|
48
|
+
|
|
49
|
+
### 2.2 Pipeline B — Entrega do evento (`achievement_created`)
|
|
50
|
+
|
|
51
|
+
Sequência real, citando métodos:
|
|
52
|
+
|
|
53
|
+
1. Uma `ActionLog` é processada → `AchievementManager.fireAction(ActionLog trigger)` (linha 117) avalia desafios/pontos/níveis e acumula `List<Achievement> result`.
|
|
54
|
+
2. Etapa 5–6: `updatePlayerStatus(...)` adiciona status do jogador a `result`; conquistas de time são reavaliadas.
|
|
55
|
+
3. Etapa 7 (linha 825–828): **somente se `result.size() > 0`** →
|
|
56
|
+
`manager.getWebhookManager().execute(Webhook.EVENT_ACHIEVEMENT_CREATED, result)`.
|
|
57
|
+
4. `WebhookManager.execute(event, content)`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
hooks = getCollection("webhook").find("{event:#}", "achievement_created")
|
|
61
|
+
para cada webhook em hooks:
|
|
62
|
+
tente:
|
|
63
|
+
Unirest.post(webhook.target_url)
|
|
64
|
+
.header("Content-Type", "application/json")
|
|
65
|
+
.body(JsonUtil.toJsonRemoveNullFields(content)) // content = List<Achievement>
|
|
66
|
+
.asString()
|
|
67
|
+
capture UnirestException:
|
|
68
|
+
System.out.println("WEBHOOK ERROR (id - event) : URL " + target_url)
|
|
69
|
+
e.printStackTrace()
|
|
70
|
+
// NÃO interrompe o loop; NÃO repropaga
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Características confirmadas da entrega:
|
|
74
|
+
|
|
75
|
+
- **Síncrona e sequencial** — cada hook é chamado em sequência, dentro da thread de `fireAction`. Um `target_url` lento atrasa a etapa pós-avaliação.
|
|
76
|
+
- **Sem timeout configurado** no `Unirest.post` (usa o default da lib).
|
|
77
|
+
- **Sem retry, sem fila, sem DLQ, sem confirmação de entrega.**
|
|
78
|
+
- **Sem persistência de status** de envio.
|
|
79
|
+
- Erros são **engolidos por hook** (impressos em `stdout`/`stderr`, não em logger) e não afetam o resultado da ação.
|
|
80
|
+
- O corpo é `JsonUtil.toJsonRemoveNullFields(result)`: JSON indentado, campos `null` removidos. Esse helper **retorna `null`** se o Jackson lançar exceção — nesse caso o corpo do POST seria literalmente vazio/`null`.
|
|
81
|
+
|
|
82
|
+
#### Fluxo de entrega — `achievement_created`
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
sequenceDiagram
|
|
86
|
+
participant Ext as Cliente (ActionLog)
|
|
87
|
+
participant AM as AchievementManager.fireAction
|
|
88
|
+
participant WM as WebhookManager.execute
|
|
89
|
+
participant DB as MongoDB (webhook)
|
|
90
|
+
participant URL as target_url externa
|
|
91
|
+
|
|
92
|
+
Ext->>AM: processa ação do jogador
|
|
93
|
+
AM->>AM: avalia desafios/pontos/níveis → List<Achievement> result
|
|
94
|
+
alt result.size() > 0
|
|
95
|
+
AM->>WM: execute("achievement_created", result)
|
|
96
|
+
WM->>DB: find({event:"achievement_created"})
|
|
97
|
+
DB-->>WM: hooks correspondentes
|
|
98
|
+
loop para cada hook (sequencial, bloqueante)
|
|
99
|
+
WM->>URL: POST target_url (body = toJsonRemoveNullFields(result))
|
|
100
|
+
alt sucesso
|
|
101
|
+
URL-->>WM: 2xx (ignorado)
|
|
102
|
+
else UnirestException
|
|
103
|
+
URL-->>WM: erro → stdout + stackTrace, continua
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
else result vazio
|
|
107
|
+
AM->>AM: nenhum webhook é disparado
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 3. Estrutura dos Objetos
|
|
114
|
+
|
|
115
|
+
### 3.1 `Webhook` — documento raiz
|
|
116
|
+
|
|
117
|
+
Arquivo: `com/funifier/engine/webhook/Webhook.java`. Anotação de classe: `@JsonIgnoreProperties(ignoreUnknown=true)`.
|
|
118
|
+
|
|
119
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
120
|
+
| ------------ | ------ | ------------------------------- | ------------- | --------- |
|
|
121
|
+
| `_id` | String | `ObjectId` hex se omitido | não | Identificador. No Java é o campo `id` mapeado via `@JsonProperty("_id")`. Se não enviado, `insert()` gera `Guid.newShortGuid()` = `new ObjectId().toString()`. |
|
|
122
|
+
| `target_url` | String | — | de fato sim¹ | URL externa que recebe o `POST`. Não há validação de formato no código. |
|
|
123
|
+
| `event` | String | — | de fato sim¹ | Tipo de evento. **Único valor com efeito:** `achievement_created`. |
|
|
124
|
+
|
|
125
|
+
¹ **Não há `@NotNull` nem validação explícita.** A obrigatoriedade é de fato, derivada do comportamento: um hook sem `event` jamais casa o filtro `find({event:#})` e nunca dispara; um hook com `target_url` nula faria `Unirest.post(null)` → `UnirestException` capturada e ignorada.
|
|
126
|
+
|
|
127
|
+
#### Campos computados (não persistem)
|
|
128
|
+
Nenhum. A entidade é plana e totalmente persistida.
|
|
129
|
+
|
|
130
|
+
#### Campos removidos silenciosamente na resposta
|
|
131
|
+
A serialização de saída usa `JsonUtil.toJsonRemoveNullFields` (Jackson com `Include.NON_NULL`). Qualquer um dos 3 campos que esteja `null` é **omitido** do corpo de resposta do POST.
|
|
132
|
+
|
|
133
|
+
#### Campos aceitos e silenciosamente ignorados (entrada)
|
|
134
|
+
Por causa de `@JsonIgnoreProperties(ignoreUnknown=true)`, **qualquer campo extra no corpo é descartado sem erro**. Tentativas comuns (baseadas em outras plataformas de webhook) que **não existem** e são ignoradas:
|
|
135
|
+
|
|
136
|
+
- `headers`, `secret` / `signing_secret`, `description`, `enabled` / `active`, `method`, `retries`, `timeout`, `filter` / `criteria`, `player`, `item`.
|
|
137
|
+
|
|
138
|
+
Enviar qualquer um deles retorna 201 com sucesso, mas o campo **não é persistido nem tem efeito**.
|
|
139
|
+
|
|
140
|
+
#### Enum de eventos
|
|
141
|
+
Não existe um `enum` — `event` é `String` livre. A única constante declarada é:
|
|
142
|
+
|
|
143
|
+
| Constante | Valor | Significado operacional |
|
|
144
|
+
| --------- | ----- | ----------------------- |
|
|
145
|
+
| `Webhook.EVENT_ACHIEVEMENT_CREATED` | `"achievement_created"` | Disparado quando `fireAction` registra ≥1 nova conquista (progresso de desafio, level up, status do jogador). |
|
|
146
|
+
|
|
147
|
+
> Qualquer outro valor de `event` pode ser salvo, mas **nunca será disparado** (não há outro `execute(...)` no código).
|
|
148
|
+
|
|
149
|
+
### 3.2 Subentidades
|
|
150
|
+
**Não aplicável** — entidade plana, sem objetos aninhados.
|
|
151
|
+
|
|
152
|
+
### 3.3 Técnicas de jogo (`techniques`)
|
|
153
|
+
**Não aplicável** — o módulo não possui códigos GT nem qualquer relação com técnicas de jogo.
|
|
154
|
+
|
|
155
|
+
> **Ciclo de vida / `stateDiagram`:** não aplicável — a entidade não tem campo de status/estado. Um hook só existe ou não existe.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 4. Endpoints
|
|
160
|
+
|
|
161
|
+
Classe: `WebhookRest` — `@Path("v3/hook")`. **Esta é a única classe JAX-RS que registra o caminho** (confirmado: não existe `@Path` para `webhook` em nenhum outro lugar). O caminho documentado anteriormente como `/v3/webhook` está **incorreto**.
|
|
162
|
+
|
|
163
|
+
```mermaid
|
|
164
|
+
flowchart LR
|
|
165
|
+
A[POST /v3/hook] -->|insert/upsert| W[(coleção webhook)]
|
|
166
|
+
B[POST /v3/hook/unsubscribe] -->|deleteByURL| W
|
|
167
|
+
C[GET /v3/hook] -->|findAll| W
|
|
168
|
+
D[GET /v3/hook/:id] -->|find| W
|
|
169
|
+
E[DELETE /v3/hook/:id] -->|delete| W
|
|
170
|
+
```
|
|
17
171
|
|
|
18
|
-
|
|
19
|
-
- [ ] Definir eventos que disparam o webhook
|
|
20
|
-
- [ ] Definir formato do payload
|
|
21
|
-
- [ ] Testar conectividade com endpoint
|
|
172
|
+
### `POST /v3/hook` — Inscrever (subscribe)
|
|
22
173
|
|
|
23
|
-
|
|
174
|
+
| Aspecto | Detalhe |
|
|
175
|
+
| --------------------- | ------- |
|
|
176
|
+
| Finalidade | Registrar um webhook. |
|
|
177
|
+
| Autenticação | Credenciais via `AuthBean` (Bearer/Basic/Studio token, ou `api_key`/`X-Api-Key`). |
|
|
178
|
+
| Full replace ou patch | **Upsert (full replace por `_id`)** via `c.save(hook)`. Sem `_id` → cria com id gerado. Com `_id` existente → substitui o documento inteiro. |
|
|
179
|
+
| Status de retorno | `201 CREATED` com `toJsonRemoveNullFields(hook)`. |
|
|
24
180
|
|
|
25
|
-
|
|
26
|
-
**Método:** GET
|
|
27
|
-
**Endpoint:** `/v3/webhook`
|
|
181
|
+
**Comportamento real:** difere do REST convencional — POST não é create estrito; é upsert. O id gerado é um `ObjectId` hex.
|
|
28
182
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
183
|
+
**Exemplo request/response:**
|
|
184
|
+
```json
|
|
185
|
+
// POST /v3/hook
|
|
186
|
+
{ "target_url": "https://meusistema.com/funifier-hook", "event": "achievement_created" }
|
|
187
|
+
```
|
|
188
|
+
```json
|
|
189
|
+
// 201 CREATED
|
|
190
|
+
{ "_id": "665f0b2c9a1e4d0012ab34cd", "target_url": "https://meusistema.com/funifier-hook", "event": "achievement_created" }
|
|
191
|
+
```
|
|
32
192
|
|
|
33
|
-
###
|
|
34
|
-
**Método:** DELETE
|
|
35
|
-
**Endpoint:** `/v3/webhook/:id`
|
|
193
|
+
### `POST /v3/hook/unsubscribe` — Cancelar por URL
|
|
36
194
|
|
|
37
|
-
|
|
195
|
+
| Aspecto | Detalhe |
|
|
196
|
+
| --------------------- | ------- |
|
|
197
|
+
| Finalidade | Remover inscrição(ões) pela `target_url`. |
|
|
198
|
+
| Autenticação | `AuthBean`. |
|
|
199
|
+
| Corpo | `{ "target_url": "..." }` (só `target_url` é usado). |
|
|
200
|
+
| Status de retorno | `201 CREATED` (⚠️ retorna **CREATED** para uma operação de exclusão). |
|
|
201
|
+
|
|
202
|
+
**Comportamento real:** `WebhookManager.deleteByURL(target_url)` executa `remove("{target_url:#}", url)` — **remove TODOS os documentos** cuja `target_url` casa, não apenas um. **Nenhuma contagem é retornada**; o corpo de resposta é o próprio hook enviado, ecoado.
|
|
203
|
+
|
|
204
|
+
### `GET /v3/hook` — Listar todos
|
|
205
|
+
|
|
206
|
+
| Aspecto | Detalhe |
|
|
207
|
+
| ------- | ------- |
|
|
208
|
+
| Finalidade | Lista todos os webhooks da organização. |
|
|
209
|
+
| Autenticação | `AuthBean`. apidoc: OAuth 2.0 (client_credentials). |
|
|
210
|
+
| Retorno | `200 OK`, array JSON. |
|
|
211
|
+
|
|
212
|
+
### `GET /v3/hook/{id}` — Buscar por id
|
|
213
|
+
|
|
214
|
+
| Aspecto | Detalhe |
|
|
215
|
+
| ------- | ------- |
|
|
216
|
+
| Finalidade | Retorna um webhook pelo `_id`. |
|
|
217
|
+
| Autenticação | `AuthBean`. apidoc: OAuth 2.0 (client_credentials). |
|
|
218
|
+
| Retorno | `200 OK`. Se não encontrado, `find` retorna `null` → corpo `null`. |
|
|
219
|
+
|
|
220
|
+
### `DELETE /v3/hook/{id}` — Remover por id
|
|
221
|
+
|
|
222
|
+
| Aspecto | Detalhe |
|
|
223
|
+
| ------- | ------- |
|
|
224
|
+
| Finalidade | Remove um webhook pelo `_id` (unsubscribe). |
|
|
225
|
+
| Autenticação | `AuthBean`. apidoc: OAuth 2.0 (client_credentials). |
|
|
226
|
+
| Retorno | `200 OK` com corpo `null`. Idempotente: remover id inexistente também retorna 200. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 5. Regras de Negócio
|
|
231
|
+
|
|
232
|
+
Regras presentes no código e ausentes de qualquer schema:
|
|
233
|
+
|
|
234
|
+
- **`event` é o único filtro de entrega.** Não é possível filtrar por jogador, item, desafio ou critério. Todos os hooks de `achievement_created` recebem o mesmo payload completo.
|
|
235
|
+
- **POST é upsert.** Reenviar com `_id` existente substitui o documento.
|
|
236
|
+
- **`unsubscribe` é "delete-all-by-url".** Uma chamada remove todos os hooks com aquela `target_url`.
|
|
237
|
+
- **Campos obrigatórios são de fato, não validados.** O sistema aceita hooks malformados sem erro; eles simplesmente não funcionam.
|
|
238
|
+
- **Inscrição em evento desconhecido é no-op silencioso.** O documento é salvo, mas nunca dispara.
|
|
239
|
+
- **Multi-tenant por banco de dados.** `FrontController.getInstance(apiKey)` resolve a conexão Jongo da organização; a coleção `webhook` é isolada por org. Não há `organization`/`tenant` no documento — o isolamento vem do banco.
|
|
240
|
+
- **Entrega só ocorre quando `result.size() > 0`** em `fireAction` — ou seja, somente quando a ação efetivamente gerou conquistas.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 6. Comportamentos Automáticos
|
|
245
|
+
|
|
246
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
247
|
+
| ------------- | ------- | ------- | ------------ |
|
|
248
|
+
| Geração de `_id` | `insert()` com `id == null` | Atribui `ObjectId` hex ao hook | Persistido na coleção `webhook` |
|
|
249
|
+
| Entrega `achievement_created` | `fireAction` com `result.size() > 0` | POST síncrono em cada `target_url` inscrita | **Não persiste** resultado de envio |
|
|
250
|
+
| Log de erro de entrega | `UnirestException` durante o POST | `System.out.println` + `printStackTrace` | Apenas `stdout`/`stderr` |
|
|
251
|
+
| Remoção em massa por URL | `POST /v3/hook/unsubscribe` | Remove N documentos com a mesma URL | Persistido (remoção) |
|
|
252
|
+
|
|
253
|
+
```mermaid
|
|
254
|
+
flowchart LR
|
|
255
|
+
A[ActionLog processada] --> B[fireAction avalia conquistas]
|
|
256
|
+
B --> C{result.size > 0?}
|
|
257
|
+
C -- não --> Z[fim, sem webhook]
|
|
258
|
+
C -- sim --> D["execute('achievement_created', result)"]
|
|
259
|
+
D --> E[find hooks por event]
|
|
260
|
+
E --> F{para cada hook}
|
|
261
|
+
F --> G[POST target_url síncrono]
|
|
262
|
+
G -- erro --> H[stdout + stackTrace, continua]
|
|
263
|
+
G -- ok --> I[resposta ignorada]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 7. Suportado vs NÃO Suportado
|
|
269
|
+
|
|
270
|
+
### ✅ Suportado
|
|
271
|
+
- Inscrever webhook (`POST /v3/hook`) com upsert por `_id`.
|
|
272
|
+
- Listar (`GET /v3/hook`) e buscar por id (`GET /v3/hook/{id}`).
|
|
273
|
+
- Cancelar por id (`DELETE /v3/hook/{id}`) e por URL (`POST /v3/hook/unsubscribe`).
|
|
274
|
+
- Entrega push do evento `achievement_created` para todas as URLs inscritas.
|
|
275
|
+
- Isolamento por organização (banco de dados por `apiKey`).
|
|
276
|
+
|
|
277
|
+
### ❌ NÃO Suportado
|
|
278
|
+
- **Qualquer evento além de `achievement_created`.** A documentação anterior citava "desafio completado", "jogador atinge meta", "Slack/Teams" — **nenhum desses é um evento separado**. Não existe taxonomia de eventos no código.
|
|
279
|
+
- **Filtro/seleção de payload.** Sem filtro por jogador/item/critério; payload é sempre a `List<Achievement>` completa.
|
|
280
|
+
- **Customização do payload** (sem template, sem mapeamento de campos).
|
|
281
|
+
- **Retry, fila assíncrona, DLQ, confirmação de entrega ou histórico de envios.** Tudo síncrono e fire-and-forget.
|
|
282
|
+
- **Timeout configurável** no POST de saída.
|
|
283
|
+
- **Assinatura/HMAC, `secret` ou headers customizados** no POST de saída — o receptor não tem como verificar autenticidade.
|
|
284
|
+
- **Headers de autenticação no POST de saída** — só `Content-Type: application/json` é enviado.
|
|
285
|
+
- **Campos extras** no corpo de inscrição (`headers`, `secret`, `enabled`, etc.) — aceitos e silenciosamente descartados (`@JsonIgnoreProperties`).
|
|
286
|
+
- **UI de Studio** — nenhuma tela localizada no `funifier-service`.
|
|
287
|
+
- **Cobertura de testes** — nenhum arquivo de teste de webhook localizado em `src/test`.
|
|
288
|
+
- **Logging estruturado de entrega** — apenas `stdout` em caso de erro.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 8. Segurança e Permissões
|
|
293
|
+
|
|
294
|
+
- **Autenticação de entrada:** todos os endpoints passam por `AuthBean`, que resolve a `apiKey` a partir de `Authorization` (Bearer/Basic/Studio/Account), `X-Api-Key` ou query `api_key`. Sem credencial válida não há `apiKey` → sem conexão de organização. A apidoc declara OAuth 2.0 (client_credentials) para GET/DELETE.
|
|
295
|
+
- **Isolamento multi-tenant:** por banco de dados da organização (via `FrontController.getInstance(apiKey)`). Um cliente só enxerga/altera webhooks da própria org.
|
|
296
|
+
- **SSRF (superfície real):** `target_url` é uma string arbitrária fornecida pelo cliente, e o servidor faz `Unirest.post(target_url)` diretamente. Um cliente autenticado pode apontar a URL para endereços internos/metadados da infraestrutura. **Não há allowlist nem validação de destino no código.**
|
|
297
|
+
- **Ausência de autenticidade na saída:** o POST de saída não é assinado e não envia segredo/HMAC. O receptor não consegue distinguir um payload legítimo da Funifier de um forjado. Recomenda-se ao consumidor validar por outro meio (ex.: URL secreta longa).
|
|
298
|
+
- **Falha de serialização silenciosa:** `toJsonRemoveNullFields` retorna `null` em exceção do Jackson — nesse caso o corpo do POST seria `null`/vazio, sem erro registrado.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 9. Observabilidade e Troubleshooting
|
|
303
|
+
|
|
304
|
+
**Diagnóstico — o módulo está funcionando?**
|
|
305
|
+
- Liste as inscrições: `GET /v3/hook`.
|
|
306
|
+
- Confirme que o `event` é exatamente `achievement_created` (qualquer outro valor nunca dispara).
|
|
307
|
+
- Confirme que a ação realmente gera conquistas — webhooks só disparam quando `fireAction` produz `result.size() > 0`.
|
|
308
|
+
|
|
309
|
+
**Sinais no log (apenas em erro):**
|
|
310
|
+
```
|
|
311
|
+
WEBHOOK ERROR (<id> - <event>) : URL <target_url>
|
|
312
|
+
<stack trace da UnirestException>
|
|
313
|
+
```
|
|
314
|
+
Não há log de sucesso. Ausência de log não significa que a entrega ocorreu.
|
|
315
|
+
|
|
316
|
+
**Queries úteis (Mongo, banco da organização):**
|
|
317
|
+
```js
|
|
318
|
+
db.webhook.find({}) // todas as inscrições
|
|
319
|
+
db.webhook.find({ event: "achievement_created" })// as que efetivamente disparam
|
|
320
|
+
db.webhook.find({ target_url: "https://..." }) // localizar por destino antes de unsubscribe
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Comandos REST de investigação:**
|
|
324
|
+
```
|
|
325
|
+
GET /v3/hook
|
|
326
|
+
GET /v3/hook/<id>
|
|
327
|
+
DELETE /v3/hook/<id>
|
|
328
|
+
POST /v3/hook/unsubscribe { "target_url": "https://..." }
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Erros comuns e causas:**
|
|
332
|
+
- *"Cadastrei o webhook mas nada chega"* → `event` diferente de `achievement_created`; ou a ação não gerou conquista; ou `target_url` inacessível (erro foi só para stdout).
|
|
333
|
+
- *"Cadastrei `secret`/`headers` e não funcionam"* → campos descartados por `@JsonIgnoreProperties`.
|
|
334
|
+
- *"Processamento de ações ficou lento"* → `target_url` lenta; entrega é síncrona, sequencial e sem timeout.
|
|
335
|
+
- *"Deletei por URL e sumiram vários"* → `unsubscribe` remove todos com a mesma `target_url`.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 10. Exemplos Práticos
|
|
340
|
+
|
|
341
|
+
### Exemplo mínimo funcional
|
|
342
|
+
```json
|
|
343
|
+
// POST /v3/hook
|
|
344
|
+
{ "target_url": "https://meusistema.com/funifier", "event": "achievement_created" }
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Exemplo com `_id` controlado pelo cliente (upsert determinístico)
|
|
348
|
+
```json
|
|
349
|
+
// POST /v3/hook → cria/atualiza sempre o mesmo documento
|
|
350
|
+
{ "_id": "crm-prod-hook", "target_url": "https://crm.exemplo.com/funifier", "event": "achievement_created" }
|
|
351
|
+
```
|
|
352
|
+
Reenviar com o mesmo `_id` substitui a URL — útil para reconfiguração idempotente.
|
|
353
|
+
|
|
354
|
+
### Cancelamento
|
|
355
|
+
```json
|
|
356
|
+
// POST /v3/hook/unsubscribe → remove TODOS os hooks com esta URL
|
|
357
|
+
{ "target_url": "https://crm.exemplo.com/funifier" }
|
|
358
|
+
```
|
|
359
|
+
```
|
|
360
|
+
// ou, por id específico:
|
|
361
|
+
DELETE /v3/hook/crm-prod-hook
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Anti-pattern — o que NÃO fazer
|
|
365
|
+
```json
|
|
366
|
+
// ❌ Evento inexistente: salva com 201, mas NUNCA dispara
|
|
367
|
+
{ "target_url": "https://x.com/hook", "event": "challenge_completed" }
|
|
368
|
+
|
|
369
|
+
// ❌ Campos que não existem: aceitos e silenciosamente ignorados
|
|
370
|
+
{ "target_url": "https://x.com/hook", "event": "achievement_created",
|
|
371
|
+
"secret": "abc", "headers": { "X-Token": "..." }, "enabled": true }
|
|
372
|
+
|
|
373
|
+
// ❌ target_url lenta/instável: bloqueia a etapa pós-avaliação de fireAction (entrega síncrona, sem timeout)
|
|
374
|
+
```
|
|
375
|
+
Para eventos diferentes de `achievement_created`, **não use este módulo** — implemente um **Trigger** (Groovy/Java) com `Unirest.post(...)`, conforme a própria orientação interna da plataforma.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Checklist de Configuração
|
|
38
380
|
|
|
39
|
-
- [ ]
|
|
40
|
-
- [ ]
|
|
41
|
-
- [ ]
|
|
381
|
+
- [ ] `target_url` acessível **e rápida** — a entrega é síncrona, sequencial e sem timeout configurado.
|
|
382
|
+
- [ ] `event` é exatamente `achievement_created` — qualquer outro valor nunca dispara.
|
|
383
|
+
- [ ] O módulo Achievement está gerando conquistas para a ação esperada (`result.size() > 0`).
|
|
384
|
+
- [ ] Não confie em `secret`/`headers`/`enabled` no corpo — são silenciosamente ignorados.
|
|
385
|
+
- [ ] Ciente de que `POST /v3/hook` com `_id` existente **substitui** o documento (upsert).
|
|
386
|
+
- [ ] Ciente de que `unsubscribe` remove **todos** os hooks com a mesma `target_url`.
|
|
387
|
+
- [ ] O consumidor valida a autenticidade por conta própria — o POST de saída não é assinado.
|
|
388
|
+
- [ ] Caminho correto é `/v3/hook` (não `/v3/webhook`).
|