funifier-mcp 0.2.25 → 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.
Files changed (211) hide show
  1. package/.cursor/rules/funifier.mdc +38 -41
  2. package/.github/copilot-instructions.md +38 -41
  3. package/AGENTS.md +56 -49
  4. package/README.md +40 -22
  5. package/datasource-funifier-docs/.coverage.json +326 -0
  6. package/datasource-funifier-docs/.validation.json +593 -0
  7. package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
  8. package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
  9. package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
  10. package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
  11. package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
  12. package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
  13. package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
  14. package/datasource-funifier-docs/knowledge/index.md +5 -2
  15. package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
  16. package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
  17. package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
  18. package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
  19. package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
  20. package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
  21. package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
  22. package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
  23. package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
  24. package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
  25. package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
  26. package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
  27. package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
  28. package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
  29. package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
  30. package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
  31. package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
  32. package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
  33. package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
  34. package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
  35. package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
  36. package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
  37. package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
  38. package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
  39. package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
  40. package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
  41. package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
  42. package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
  43. package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
  44. package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
  45. package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
  46. package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
  47. package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
  48. package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
  49. package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
  50. package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
  51. package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
  52. package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
  53. package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
  54. package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
  55. package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
  56. package/dist/cli/init.d.ts.map +1 -1
  57. package/dist/cli/init.js +42 -1
  58. package/dist/cli/init.js.map +1 -1
  59. package/dist/cli/init.test.js +74 -3
  60. package/dist/cli/init.test.js.map +1 -1
  61. package/dist/cli/persona.d.ts +3 -0
  62. package/dist/cli/persona.d.ts.map +1 -0
  63. package/dist/cli/persona.js +25 -0
  64. package/dist/cli/persona.js.map +1 -0
  65. package/dist/core/api-client.d.ts +21 -1
  66. package/dist/core/api-client.d.ts.map +1 -1
  67. package/dist/core/api-client.js +154 -1
  68. package/dist/core/api-client.js.map +1 -1
  69. package/dist/core/constants.d.ts +14 -0
  70. package/dist/core/constants.d.ts.map +1 -1
  71. package/dist/core/constants.js +14 -0
  72. package/dist/core/constants.js.map +1 -1
  73. package/dist/core/types/Folder.d.ts +16 -0
  74. package/dist/core/types/Folder.d.ts.map +1 -0
  75. package/dist/core/types/Folder.js +3 -0
  76. package/dist/core/types/Folder.js.map +1 -0
  77. package/dist/core/types/FolderContent.d.ts +10 -0
  78. package/dist/core/types/FolderContent.d.ts.map +1 -0
  79. package/dist/core/types/FolderContent.js +3 -0
  80. package/dist/core/types/FolderContent.js.map +1 -0
  81. package/dist/core/types/FolderContentType.d.ts +10 -0
  82. package/dist/core/types/FolderContentType.d.ts.map +1 -0
  83. package/dist/core/types/FolderContentType.js +3 -0
  84. package/dist/core/types/FolderContentType.js.map +1 -0
  85. package/dist/core/types/FolderLog.d.ts +11 -0
  86. package/dist/core/types/FolderLog.d.ts.map +1 -0
  87. package/dist/core/types/FolderLog.js +3 -0
  88. package/dist/core/types/FolderLog.js.map +1 -0
  89. package/dist/core/types/index.d.ts +4 -0
  90. package/dist/core/types/index.d.ts.map +1 -1
  91. package/dist/core/types/index.js +4 -0
  92. package/dist/core/types/index.js.map +1 -1
  93. package/dist/mcp/bundle.js +121 -87
  94. package/dist/mcp/check-update.d.ts +2 -0
  95. package/dist/mcp/check-update.d.ts.map +1 -0
  96. package/dist/mcp/check-update.js +44 -0
  97. package/dist/mcp/check-update.js.map +1 -0
  98. package/dist/mcp/index.js +5 -2
  99. package/dist/mcp/index.js.map +1 -1
  100. package/dist/mcp/resources/documentation.d.ts +1 -1
  101. package/dist/mcp/resources/documentation.d.ts.map +1 -1
  102. package/dist/mcp/resources/documentation.js +39 -3
  103. package/dist/mcp/resources/documentation.js.map +1 -1
  104. package/dist/mcp/tools/_char-guard.js +1 -1
  105. package/dist/mcp/tools/_char-guard.js.map +1 -1
  106. package/dist/mcp/tools/_fetch-current.d.ts +1 -1
  107. package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
  108. package/dist/mcp/tools/_fetch-current.js +12 -0
  109. package/dist/mcp/tools/_fetch-current.js.map +1 -1
  110. package/dist/mcp/tools/connect.d.ts.map +1 -1
  111. package/dist/mcp/tools/connect.js +18 -8
  112. package/dist/mcp/tools/connect.js.map +1 -1
  113. package/dist/mcp/tools/database.d.ts.map +1 -1
  114. package/dist/mcp/tools/database.js +59 -47
  115. package/dist/mcp/tools/database.js.map +1 -1
  116. package/dist/mcp/tools/database.test.js +2 -2
  117. package/dist/mcp/tools/database.test.js.map +1 -1
  118. package/dist/mcp/tools/delete.d.ts.map +1 -1
  119. package/dist/mcp/tools/delete.js +33 -3
  120. package/dist/mcp/tools/delete.js.map +1 -1
  121. package/dist/mcp/tools/execute.d.ts.map +1 -1
  122. package/dist/mcp/tools/execute.js +20 -9
  123. package/dist/mcp/tools/execute.js.map +1 -1
  124. package/dist/mcp/tools/folder.d.ts +4 -0
  125. package/dist/mcp/tools/folder.d.ts.map +1 -0
  126. package/dist/mcp/tools/folder.js +68 -0
  127. package/dist/mcp/tools/folder.js.map +1 -0
  128. package/dist/mcp/tools/get.d.ts.map +1 -1
  129. package/dist/mcp/tools/get.js +16 -6
  130. package/dist/mcp/tools/get.js.map +1 -1
  131. package/dist/mcp/tools/index.d.ts +1 -1
  132. package/dist/mcp/tools/index.d.ts.map +1 -1
  133. package/dist/mcp/tools/index.js +5 -1
  134. package/dist/mcp/tools/index.js.map +1 -1
  135. package/dist/mcp/tools/list.d.ts.map +1 -1
  136. package/dist/mcp/tools/list.js +38 -14
  137. package/dist/mcp/tools/list.js.map +1 -1
  138. package/dist/mcp/tools/logs.d.ts.map +1 -1
  139. package/dist/mcp/tools/logs.js +15 -5
  140. package/dist/mcp/tools/logs.js.map +1 -1
  141. package/dist/mcp/tools/save.d.ts.map +1 -1
  142. package/dist/mcp/tools/save.js +26 -4
  143. package/dist/mcp/tools/save.js.map +1 -1
  144. package/dist/mcp/tools/save.test.js +192 -1
  145. package/dist/mcp/tools/save.test.js.map +1 -1
  146. package/dist/mcp/tools/search-docs.d.ts +3 -0
  147. package/dist/mcp/tools/search-docs.d.ts.map +1 -0
  148. package/dist/mcp/tools/search-docs.js +102 -0
  149. package/dist/mcp/tools/search-docs.js.map +1 -0
  150. package/package.json +6 -2
  151. package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
  152. package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
  153. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
  154. package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
  155. package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
  156. package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
  157. package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
  158. package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
  159. package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
  160. package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
  161. package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
  162. package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
  163. package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
  164. package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
  165. package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
  166. package/skills/funifier/SKILL.md +88 -0
  167. package/skills/funifier/references/configure-security.md +96 -0
  168. package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
  169. package/skills/funifier/references/create-aggregate.md +144 -0
  170. package/skills/funifier/references/create-challenge.md +116 -0
  171. package/skills/funifier/references/create-competition.md +98 -0
  172. package/skills/funifier/references/create-crossword.md +574 -0
  173. package/skills/funifier/references/create-custom-object.md +91 -0
  174. package/skills/funifier/references/create-custom-page.md +135 -0
  175. package/skills/funifier/references/create-folder.md +104 -0
  176. package/skills/funifier/references/create-lastmile.md +643 -0
  177. package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
  178. package/skills/funifier/references/create-level.md +94 -0
  179. package/skills/funifier/references/create-lottery.md +913 -0
  180. package/skills/funifier/references/create-mystery.md +769 -0
  181. package/skills/funifier/references/create-notification.md +75 -0
  182. package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
  183. package/skills/funifier/references/create-quiz.md +98 -0
  184. package/skills/funifier/references/create-scheduler.md +141 -0
  185. package/skills/funifier/references/create-story.md +636 -0
  186. package/skills/funifier/references/create-swap.md +95 -0
  187. package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
  188. package/skills/funifier/references/create-virtual-good.md +96 -0
  189. package/skills/funifier/references/create-webhook.md +72 -0
  190. package/skills/funifier/references/create-websocket.md +71 -0
  191. package/skills/funifier/references/create-widget.md +76 -0
  192. package/skills/funifier/references/debug.md +87 -0
  193. package/skills/funifier/references/help.md +81 -0
  194. package/skills/funifier/references/implement-frontend.md +106 -0
  195. package/skills/funifier/references/import-csv.md +75 -0
  196. package/skills/funifier/references/manage-player.md +82 -0
  197. package/skills/funifier/references/manage-team.md +76 -0
  198. package/skills/funifier/references/upload-file.md +91 -0
  199. package/datasource-funifier-docs/.search-index.json +0 -17318
  200. package/datasource-funifier-docs/.skills-map.json +0 -73
  201. package/skills/funifier-create-aggregate/SKILL.md +0 -127
  202. package/skills/funifier-create-challenge/SKILL.md +0 -88
  203. package/skills/funifier-create-custom-page/SKILL.md +0 -127
  204. package/skills/funifier-create-level/SKILL.md +0 -87
  205. package/skills/funifier-create-quiz/SKILL.md +0 -87
  206. package/skills/funifier-create-scheduler/SKILL.md +0 -127
  207. package/skills/funifier-create-virtual-good/SKILL.md +0 -87
  208. package/skills/funifier-debug/SKILL.md +0 -92
  209. package/skills/funifier-help/SKILL.md +0 -86
  210. package/skills/funifier-implement-frontend/SKILL.md +0 -90
  211. package/skills/funifier-index/SKILL.md +0 -58
@@ -1,67 +1,197 @@
1
- # Public (Endpoints Públicos)
1
+ # `public`
2
2
 
3
3
  **Acesso Studio:** `/studio/public`
4
- **API Endpoint:** `/v3/public`
4
+ **API Endpoint (CRUD):** `/v3/public`
5
+ **API Endpoint (execução):** `/v3/pub/{apikey}/{slug}`
6
+ **Coleção MongoDB:** `public_endpoint`
5
7
 
6
- ## O que é
8
+ > Documentação de engenharia reversa baseada **exclusivamente** no código-fonte de `funifier-service`.
9
+ > Arquivos analisados:
10
+ > - `com/funifier/rest/v3/rest/PublicRest.java` (CRUD autenticado)
11
+ > - `com/funifier/rest/v3/rest/PubRest.java` (execução pública)
12
+ > - `com/funifier/engine/pub/PublicManager.java` (lógica de compilação/execução)
13
+ > - `com/funifier/engine/pub/PublicEndpoint.java` (entidade)
14
+ > - `com/funifier/engine/pub/PublicResponse.java` (DTO de resposta)
15
+ > - `com/funifier/engine/integration/trigger/TriggerExpressionChecker.java` (sandbox Groovy)
16
+ > - `com/funifier/engine/util/Entity.java` (`PUBLIC_ENDPOINT("public_endpoint", ...)`)
17
+ > - `com/funifier/engine/app/AppManager.java` (criação programática / instalação de app)
7
18
 
8
- Criação e gerenciamento de endpoints públicos personalizados. Esses endpoints são ideais para integração com plataformas externas (como Circle, Zapier, Typeform), pois não exigem autenticação via token. A autenticação é feita via `apikey` incluída na URL pública no formato: `/v3/pub/{apikey}/{slug}`.
19
+ ---
9
20
 
10
- ## Quando usar
21
+ ## 1. Visão Geral
11
22
 
12
- - Para receber dados de formulários externos (Typeform, Google Forms)
13
- - Para integrar com automações (Zapier, Make)
14
- - Para criar webhooks de recebimento
15
- - Para endpoints que não requerem autenticação por token
23
+ O módulo `public` permite definir **endpoints HTTP dinâmicos** cuja lógica é um **script Groovy** armazenado no banco. Cada endpoint é um documento na coleção `public_endpoint`. A invocação acontece por uma URL **sem token de autenticação**: `GET|POST /v3/pub/{apikey}/{slug}`, onde `apikey` é a chave pública da gamificação (resolve o tenant) e `slug` é o `_id` do documento.
16
24
 
17
- ## Checklist de Configuração no Studio
25
+ **Papel arquitetural:**
18
26
 
19
- - [ ] Definir _id (slug) do endpoint
20
- - [ ] Definir título e descrição
21
- - [ ] Definir método HTTP (GET, POST, etc.)
22
- - [ ] Escrever script Java com método `public Object handle(Object payload)`
23
- - [ ] Ativar endpoint (active: true)
24
- - [ ] Testar via URL pública
27
+ - É a superfície "serverless" da plataforma: o cliente envia um payload e o servidor executa um script Groovy arbitrário (dentro de um sandbox) que tem acesso ao `ManagerFactory` daquela gamificação — ou seja, a todos os managers (Player, Point, Achievement, Database, etc.).
28
+ - Resolve o problema de **integração com sistemas externos que não conseguem autenticar via token** (formulários, automações tipo Zapier/Make, webhooks de terceiros) — basta conhecer a `apikey` pública e o `slug`.
29
+ - A entidade carrega comentários internos chamando-a de *"prepared aggregate"* (ex.: `PublicEndpoint.java:40`, e o método comentado `// public String getScript(PreparedAggregate trigger)` em `PublicManager.java:109`). Isso é **legado**: o código foi adaptado da entidade `prepared_aggregate` (`Entity.PREPARED_AGGREGATE`). **Não existe nenhum pipeline de aggregate MongoDB no runtime do `public`** — o script é Groovy livre, não um aggregate preparado.
25
30
 
26
- ## API Endpoints
31
+ **Relação com outros módulos:**
27
32
 
28
- ### Listar Public Endpoints
29
- **Método:** GET
30
- **Endpoint:** `/v3/database/public_endpoint`
33
+ - Compartilha a mesma técnica de sandbox de script com `trigger` e `scheduler`: o checker `TriggerExpressionChecker` (pacote `engine.integration.trigger`) é reutilizado aqui.
34
+ - A criação pode ser disparada pela instalação de um **App/pacote** (`AppManager.funifierCreateCustomEndpoint`), além do CRUD direto.
35
+ - Em runtime, o script pode tocar qualquer coleção/manager via `manager.getXxxManager()`.
31
36
 
32
- **Exemplo de Resposta:**
33
- ```json
34
- [
35
- {
36
- "_id": "recebe_email",
37
- "title": "Recebe Email",
38
- "description": "Recebe email e registra na colecao email__c",
39
- "active": true,
40
- "method": "POST",
41
- "script": "public Object handle(Object payload) { payload._id = payload.email; manager.getJongoConnection().getCollection(\"email__c\").save(payload); Map<String, Object> response = new HashMap<>(); response.put(\"email\", payload.email); return response; }",
42
- "sample": "{ \"email\": \"user@email.com\" }",
43
- "extra": {}
44
- }
45
- ]
37
+ ---
38
+
39
+ ## 2. Arquitetura e Fluxos
40
+
41
+ ### 2.1 Pipeline principal — execução (`/v3/pub/{apikey}/{slug}`)
42
+
43
+ Sequência real (`PubRest.executePublicEndpoint` `PublicManager.run` `executor`):
44
+
45
+ ```
46
+ [Trigger HTTP] → PubRest.handleGet / handlePost
47
+ [Montagem payload]
48
+ GET → uriInfo.getQueryParameters() → payload.put(key, values.get(0)) // apenas o 1º valor de cada query param
49
+ POST → body (JSON desserializado em HashMap<String,Object>)
50
+ [Resolução tenant] → FrontController.getInstance(apikey).getManagerFactory()
51
+ [Lookup] → PublicManager.find(slug) // findOne({_id: slug})
52
+ [Validação 1] → endpoint == null → HTTP 404 (PublicResponse 404)
53
+ [Validação 2] → !endpoint.active → HTTP 403 (PublicResponse 403)
54
+ [Validação 3] → !method.equalsIgnoreCase(endpoint.method) → HTTP 406 (PublicResponse statusCode=405)
55
+ [Execução] → PublicManager.run(endpoint, payload, manager)
56
+ [Resposta] → Response.status(result.statusCode).entity(result.payload)
57
+ + header X-Response-Time: <ms>
58
+ ```
59
+
60
+ Lógica interna de `run()`:
61
+
62
+ ```
63
+ se endpoint.updated == null:
64
+ save(endpoint) # rede de segurança p/ docs inseridos fora do save()
65
+
66
+ lastCompilation = dates[endpoint._id]
67
+ clazz = compiled[endpoint._id]
68
+
69
+ se clazz == null OU lastCompilation == null OU lastCompilation.before(endpoint.updated):
70
+ script = getScript(endpoint.script) # injeta imports + wrapper class
71
+ clazz = compile(script) # SecureASTCustomizer + TriggerExpressionChecker
72
+ compiled[endpoint._id] = clazz
73
+ dates[endpoint._id] = agora
74
+
75
+ result = executor(clazz, endpoint, payload, manager) # roda handle(payload) com timeout
76
+ retorna PublicResponse(200, "OK", result)
77
+
78
+ # em qualquer exceção (compilação OU execução):
79
+ retorna PublicResponse(500, "Erro ao compilar ou executar o script", ex.getMessage())
46
80
  ```
47
81
 
48
- ### Criar Public Endpoint
49
- **Método:** POST
50
- **Endpoint:** `/v3/public`
82
+ Características:
83
+
84
+ - **Síncrono.** A requisição HTTP fica bloqueada até o script terminar (ou estourar timeout).
85
+ - **Sem transação.** Não há transação MongoDB envolvendo o que o script faz. Cada operação do script no banco é independente.
86
+ - **Dois timeouts** (ver seção 5): `@TimedInterrupt(5s)` no wrapper Groovy + `Future.get(timeout)` no executor (default 10s, ou `endpoint.timeout`).
87
+
88
+ ### Fluxo de execução — `/v3/pub`
89
+
90
+ ```mermaid
91
+ flowchart TD
92
+ A["Cliente: GET/POST /v3/pub/{apikey}/{slug}"] --> B[PubRest.executePublicEndpoint]
93
+ B --> C[FrontController.getInstance apikey]
94
+ C --> D["PublicManager.find(slug)"]
95
+ D --> E{endpoint == null?}
96
+ E -- sim --> E1["HTTP 404 — PublicResponse(404)"]
97
+ E -- não --> F{active?}
98
+ F -- não --> F1["HTTP 403 — PublicResponse(403)"]
99
+ F -- sim --> G{method confere?}
100
+ G -- não --> G1["HTTP 406 — PublicResponse statusCode=405"]
101
+ G -- sim --> H["PublicManager.run()"]
102
+ H --> I{cache de classe válido?}
103
+ I -- não --> J["getScript + compile + cacheia"]
104
+ I -- sim --> K[reusa classe compilada]
105
+ J --> L["executor: handle(payload) — single-thread + timeout"]
106
+ K --> L
107
+ L --> M{exceção / timeout?}
108
+ M -- sim --> M1["HTTP 500 — corpo = ex.getMessage()"]
109
+ M -- não --> N["HTTP 200 — corpo = retorno de handle()"]
110
+ ```
111
+
112
+ ### Interação entre componentes
113
+
114
+ ```mermaid
115
+ sequenceDiagram
116
+ participant C as Cliente
117
+ participant R as PubRest
118
+ participant M as PublicManager
119
+ participant G as Groovy (FunifierPublicEndpoint)
120
+ C->>R: POST /v3/pub/{apikey}/{slug} + payload
121
+ R->>M: find(slug)
122
+ M-->>R: PublicEndpoint (ou null)
123
+ R->>M: run(endpoint, payload, manager)
124
+ M->>M: compile + cache (se dates[_id] < updated)
125
+ M->>G: setManager(manager)
126
+ M->>G: handle(payload) [executor single-thread, timeout]
127
+ G-->>M: result
128
+ M-->>R: PublicResponse(200, "OK", result)
129
+ R-->>C: HTTP 200 + result (somente o payload)
130
+ ```
131
+
132
+ ### 2.2 Pipeline secundário — CRUD (`/v3/public`)
133
+
134
+ `PublicRest` expõe o gerenciamento autenticado (ver seção 4). É um CRUD fino sobre `PublicManager`:
135
+
136
+ ```
137
+ GET /v3/public/{id} → PublicManager.find(id) → 200 + JSON (campos null omitidos)
138
+ POST /v3/public → PublicManager.save(obj) → 201 + JSON do objeto salvo
139
+ DELETE /v3/public/{id} → PublicManager.delete(id) → 204 No Content
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 3. Estrutura dos Objetos
145
+
146
+ ### 3.1 `PublicEndpoint` — documento raiz (coleção `public_endpoint`)
147
+
148
+ Anotações: `@Data @NoArgsConstructor @AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown=true)`. **Todos os campos são `public`** (acesso direto, sem getters/setters obrigatórios no script).
149
+
150
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
151
+ | ------------- | --------------------- | ------------- | ----------- | --------- |
152
+ | `_id` | String | auto | não | Slug usado na URL pública (`/v3/pub/{apikey}/{_id}`). Se vazio/`null` no `save()`, gerado via `Guid.shortTimeMillis()`. |
153
+ | `apikey` | String | `null` | não | "apikey da gamificação". **Não é usado na execução** — o tenant vem da `apikey` da URL. Campo informativo/legado. |
154
+ | `title` | String | `null` | não | Nome amigável (uso interno/Studio). |
155
+ | `description` | String | `null` | não | Descrição interna do propósito. |
156
+ | `active` | boolean | `false` | não* | Se `false`, a execução retorna **403**. Como `boolean` primitivo, ausência no JSON ⇒ `false` ⇒ endpoint **desativado por padrão**. |
157
+ | `method` | String | `"POST"` | não | Método HTTP aceito. Comparado via `equalsIgnoreCase`. Valores usados: `"POST"` ou `"GET"`. |
158
+ | `script` | String | `null` | sim (de fato)| Corpo do script Groovy. Deve definir o método `handle(payload)`. |
159
+ | `timeout` | Long | `null` | não | Timeout de execução **em segundos**. `null` ou `<= 0` ⇒ usa **10s**. |
160
+ | `sample` | String | `null` | não | Exemplo de payload esperado (documentação; não usado em runtime). |
161
+ | `updated` | Date | auto | não | Setado para `new Date()` a cada `save()`. **Chave da invalidação de cache** de compilação. |
162
+ | `extra` | Map\<String,Object\> | `{}` | não | Campo livre. **Atenção:** é um campo literal chamado `extra`, **não** um catch-all do Jackson (não há `@JsonAnySetter`). Só é populado se o JSON tiver a chave `"extra"`. |
51
163
 
52
- ### Executar Public Endpoint
53
- **Método:** POST (ou conforme configurado)
54
- **Endpoint:** `/v3/pub/{apikey}/{slug}`
55
- **Descrição:** Executa o endpoint público. Não requer token de autenticação.
164
+ \* Tecnicamente nenhum campo tem validação de obrigatoriedade no código; `script` é obrigatório na prática (sem ele não há `handle` para invocar).
56
165
 
57
- ## Script Runtime Environment (Wrapper Class)
166
+ **Campos computados / não persistidos pelo cliente:**
167
+ - `updated` é sempre sobrescrito pelo servidor no `save()` — qualquer valor enviado pelo cliente é ignorado.
58
168
 
59
- O script que você escreve no Studio **não é standalone** ele é inserido dentro de uma classe Java wrapper gerada automaticamente pelo Funifier. Isso tem implicações importantes:
169
+ **Campos aceitos e silenciosamente ignorados:**
170
+ - `apikey` — aceito no schema, **não tem efeito** no fluxo de execução (`PubRest` resolve o tenant pela `apikey` da URL).
171
+ - `sample` — persistido, mas nunca lido pelo runtime.
172
+ - Qualquer chave JSON desconhecida — descartada por `@JsonIgnoreProperties(ignoreUnknown=true)` (**não** vai para `extra`).
60
173
 
61
- ### Estrutura do Wrapper
174
+ **Geração de `_id`:** `Guid.shortTimeMillis()` ⇒ `encode(System.currentTimeMillis(), Guid.range)` — string curta derivada do timestamp. Só ocorre quando `_id` é vazio/`null`.
62
175
 
63
- ```java
64
- // --- Imports automáticos (NÃO escreva import no seu script) ---
176
+ ### 3.2 `PublicResponse` — DTO de resposta
177
+
178
+ | Campo | Tipo | Descrição |
179
+ | ------------ | ------ | --------- |
180
+ | `statusCode` | int | Código lógico (200, 404, 403, 405, 500). **Nem sempre igual ao status HTTP** (ver seção 4). |
181
+ | `message` | String | Mensagem resumida. **Descartada no sucesso** (ver abaixo). |
182
+ | `payload` | Object | Dado de retorno. No sucesso, é o valor retornado por `handle()`. |
183
+
184
+ > **Assimetria crítica do corpo da resposta** (`PubRest`):
185
+ > - **Sucesso** → `entity(result.payload)`: o corpo é **apenas** o retorno de `handle()`. `statusCode` e `message` do `PublicResponse` são **descartados**.
186
+ > - **Erro de pré-checagem** (404/403/406) → `entity(new PublicResponse(...))`: o corpo é o **objeto `PublicResponse` completo** em JSON.
187
+ > - **Erro no script** (capturado dentro de `run()`) → `run()` devolve `PublicResponse(500, "Erro...", ex.getMessage())` e `PubRest` ainda faz `entity(result.payload)` ⇒ o corpo é a **string crua da mensagem de exceção**, com HTTP 500.
188
+
189
+ ### 3.3 Wrapper de execução (`FunifierPublicEndpoint`)
190
+
191
+ O `script` do usuário **não roda standalone**: `getScript()` o injeta dentro de uma classe Groovy gerada. Estrutura literal montada (`PublicManager.getScript`, linhas 110-207):
192
+
193
+ ```groovy
194
+ // imports automáticos (NÃO escreva 'import' no seu script)
65
195
  import groovyx.net.http.*
66
196
  import static groovyx.net.http.Method.*
67
197
  import static groovyx.net.http.ContentType.*
@@ -77,34 +207,49 @@ import org.apache.http.client.methods.HttpPost
77
207
  import org.apache.http.impl.client.HttpClientBuilder
78
208
  import org.apache.commons.io.IOUtils
79
209
  import java.lang.StringBuffer
80
- import java.io.*
210
+ // resize de imagens
211
+ import net.coobird.thumbnailator.Thumbnails
212
+ import java.io.InputStream
213
+ import java.io.ByteArrayInputStream
214
+ import java.io.ByteArrayOutputStream
215
+ import java.io.BufferedReader
216
+ import java.io.IOException
81
217
  import com.fasterxml.jackson.core.JsonProcessingException
82
- // Unirest (HTTP client)
218
+ // Unirest
83
219
  import com.mashape.unirest.http.HttpResponse
84
220
  import com.mashape.unirest.http.JsonNode
221
+ import com.mashape.unirest.http.ObjectMapper
85
222
  import com.mashape.unirest.http.Unirest
86
223
  import com.mashape.unirest.http.exceptions.UnirestException
87
224
  import com.mashape.unirest.request.GetRequest
88
225
  import com.mashape.unirest.request.HttpRequestWithBody
89
- // Mail (Simple Java Mail)
226
+ // Mail
90
227
  import org.simplejavamail.email.Email
91
228
  import org.simplejavamail.email.EmailBuilder
92
229
  import org.simplejavamail.mailer.MailerBuilder
93
- // Funifier entities
94
- import com.funifier.engine.action.*
95
- import com.funifier.engine.achievement.*
96
- import com.funifier.engine.lottery.*
97
- import com.funifier.engine.challenge.*
98
- import com.funifier.engine.constant.*
99
- import com.funifier.engine.guid.*
100
- import com.funifier.engine.level.*
230
+ // Entidades Funifier
231
+ import com.funifier.engine.action.Action
232
+ import com.funifier.engine.action.ActionLog
233
+ import com.funifier.engine.achievement.Achievement
234
+ import com.funifier.engine.lottery.Lottery
235
+ import com.funifier.engine.lottery.LotteryTicket
236
+ import com.funifier.engine.challenge.Challenge
237
+ import com.funifier.engine.challenge.ChallengeRule
238
+ import com.funifier.engine.challenge.ChallengeRuleFilter
239
+ import com.funifier.engine.challenge.Requirement
240
+ import com.funifier.engine.challenge.RewardPoint
241
+ import com.funifier.engine.constant.Operator
242
+ import com.funifier.engine.constant.TimeScale
243
+ import com.funifier.engine.guid.Guid
244
+ import com.funifier.engine.level.Level
101
245
  import com.funifier.engine.notify.*
102
- import com.funifier.engine.player.*
103
- import com.funifier.engine.point.*
104
- import com.funifier.engine.team.*
105
- import com.funifier.engine.catalog.*
106
- import com.funifier.engine.mail.*
107
- // Funifier utils
246
+ import com.funifier.engine.player.Player
247
+ import com.funifier.engine.point.Point
248
+ import com.funifier.engine.team.Team
249
+ import com.funifier.engine.catalog.Catalog
250
+ import com.funifier.engine.catalog.CatalogItem
251
+ import com.funifier.engine.mail.FunifierMail
252
+ // Utils Funifier
108
253
  import com.funifier.engine.util.DateUtil
109
254
  import com.funifier.engine.util.JsonUtil
110
255
  import com.funifier.engine.util.HttpUtil
@@ -112,142 +257,327 @@ import com.funifier.engine.util.HttpClientFunifier
112
257
  import com.funifier.engine.util.MustacheUtils
113
258
  import com.funifier.controller.ManagerFactory
114
259
 
115
- @TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)
116
- class FunifierPublicEndpoint {
260
+ @TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)class FunifierPublicEndpoint {
117
261
  ManagerFactory manager = null;
118
- void setManager(ManagerFactory c) { manager = c; }
262
+ void setManager(ManagerFactory c){ manager = c; };
119
263
 
120
- // --- SEU SCRIPT É INSERIDO AQUI ---
264
+ // <<< AQUI ENTRA O CONTEÚDO DE endpoint.script >>>
121
265
  }
122
266
  ```
123
267
 
124
- ### Regras para escrever scripts
125
-
126
- 1. **NÃO use `import`** todos os imports necessários estão no wrapper. Se precisar de uma classe não importada, use o nome completo (fully qualified): `com.example.MinhaClasse`
127
- 2. **O método principal é `public Object handle(Object payload)`**este é o entry point
128
- 3. **`manager` está disponível** como campo da classe (ManagerFactory)
129
- 4. **Timeout padrão de 10 segundos** — o executor limita a execução. **Pode ser customizado** via campo `timeout` (em segundos) no objeto do endpoint — ver seção "Campo timeout" abaixo. Nota: o `@TimedInterrupt(5s)` no wrapper Groovy é uma segunda camada de proteção
130
- 5. **Groovy, não Java puro** — o script roda em Groovy, então features como GString, closures e tipagem dinâmica funcionam
131
- 6. **Cuidado com `$` em GStrings** — operadores MongoDB (`$set`, `$inc`) e strings com `$` são interpretados como variáveis Groovy. Use `String.valueOf((char)0x24)` para escapar:
132
- ```groovy
133
- def d = String.valueOf((char)0x24) // = "$"
134
- def setCmd = '{"' + d + 'set": {"name": "Novo Nome"}}'
135
- ```
136
- 7. **Use apenas ASCII em comentários** — caracteres UTF-8 especiais (em dash `—`, acentos em comentários) podem causar erros de parse no Groovy
137
- 8. **`instanceof` é bloqueado** pelo SecureASTCustomizer — use `getClass().getName()` ou try/catch
138
- 9. **NÃO use `groovy.json.JsonSlurper`** para parsear JSON que será retornado na response — o `LazyMap` do JsonSlurper causa `ClassCastException: [B incompatible with [C` quando o Funifier tenta serializar. Use `JsonUtil.fromJsonToMap(jsonString)` em vez de `slurper.parseText(jsonString)`
139
- 10. **Player.extra é público** acesse direto com `player.extra`, não com `player.getExtra()` (método não existe)
140
- 11. **Para salvar player:** `manager.getPlayerManager().findById(id)` → modifique → `manager.getPlayerManager().insert(player)` (upsert)
141
-
142
- ### Campo `timeout` (Customizar timeout do script)
143
-
144
- O timeout padrão de **10 segundos** (confirmado no source: `PublicManager.java` linha 255) pode ser insuficiente para endpoints que fazem chamadas HTTP externas (ex: verificar token OAuth, chamar APIs de IA, download de imagens). O campo `timeout` permite aumentar esse limite.
145
-
146
- **Campo:** `timeout` (Long, em **segundos**)
147
- **Padrão:** `null` (usa 10 segundos source: `PublicManager.java:255`)
148
- **Nota:** Este campo **não aparece no formulário do Studio** — só pode ser configurado via API.
149
-
150
- ```bash
151
- # Exemplo: definir timeout de 30 segundos para o endpoint google_login
152
- curl -X POST "https://service2.funifier.com/v3/public" \
153
- -H "Authorization: Basic <token>" \
154
- -H "Content-Type: application/json" \
155
- -d '{"_id": "google_login", "timeout": 30}'
156
- ```
157
-
158
- **Este campo também existe em Triggers e Schedulers** — mesma estrutura, mesma API (`/v3/trigger`, `/v3/scheduler`).
159
-
160
- **Mecanismo de execução (Java):**
161
- ```java
162
- // O executor usa Executors.newSingleThreadExecutor() com Future.get(timeout, TimeUnit.SECONDS)
163
- long timeout = 10; // padrão
164
- if (endpoint.timeout != null && endpoint.timeout > 0) {
165
- timeout = endpoint.timeout;
166
- }
167
- Object result = future.get(timeout, TimeUnit.SECONDS);
168
- ```
169
-
170
- **Nota:** O `@TimedInterrupt(value = 5L, unit = TimeUnit.SECONDS)` no wrapper Groovy é uma segunda camada de proteção. O timeout do executor (campo `timeout`) é a camada principal e deve ser >= ao TimedInterrupt para funcionar corretamente.
171
-
172
- **Estrutura da classe PublicEndpoint (Java):**
173
- ```java
174
- public class PublicEndpoint {
175
- public String _id; // slug (URL identifier)
176
- public String apikey; // gamification apikey
177
- public String title; // friendly name
178
- public String description; // internal description
179
- public boolean active; // enabled/disabled
180
- public String method; // "POST" or "GET" (default: "POST")
181
- public String script; // Groovy script body
182
- public Long timeout; // timeout in SECONDS (null = default 10s)
183
- public String sample; // example payload
184
- public Date updated; // last update timestamp
185
- public Map<String, Object> extra; // extra metadata
268
+ Observações fiéis ao código:
269
+ - O método de entrada **deve se chamar `handle`** e receber 1 argumento: `executor()` chama `obj.invokeMethod("handle", new Object[]{ payload })`.
270
+ - O campo `manager` (`ManagerFactory`) é injetado via `setManager` antes do `handle`. É o acesso a toda a plataforma.
271
+ - A anotação `@TimedInterrupt(...)` é concatenada **sem espaço** antes de `class` (`...SECONDS)class FunifierPublicEndpoint`) — reproduzido aqui exatamente como o código gera.
272
+
273
+ ---
274
+
275
+ ## 4. Endpoints
276
+
277
+ ### `GET /v3/public/{id}` — buscar endpoint (CRUD)
278
+
279
+ | Aspecto | Detalhe |
280
+ | ------------ | ------- |
281
+ | Finalidade | Recuperar um `PublicEndpoint` por `_id`. |
282
+ | Autenticação | Header `Authorization` (resolvido por `AuthBean` Basic `apiKey:secretKey`, Bearer JWT, Studio, Account). |
283
+ | Resposta | `200` + JSON do endpoint, **com campos `null` omitidos** (`JsonUtil.toJsonRemoveNullFields`). |
284
+
285
+ **Comportamento real:** se o `_id` não existir, `findOne(...).as(PublicEndpoint.class)` retorna `null` e a resposta é **`200` com corpo literal `null`** (não há 404 neste endpoint de CRUD).
286
+
287
+ ### `POST /v3/public` — criar/atualizar endpoint (upsert)
288
+
289
+ | Aspecto | Detalhe |
290
+ | -------------------- | ------- |
291
+ | Finalidade | Criar ou atualizar um `PublicEndpoint`. |
292
+ | Autenticação | Header `Authorization` (idem acima). |
293
+ | Full replace ou patch| **Full replace.** `col.save(obj)` substitui o documento inteiro pelo objeto recebido. Campos omitidos voltam ao default da classe (ex.: `method` → `"POST"`, `active` → `false`). |
294
+
295
+ **Comportamento real:**
296
+ - `_id` vazio/`null` ⇒ gerado automaticamente (`Guid.shortTimeMillis`).
297
+ - `updated` sempre setado para o instante do save (valor do cliente ignorado).
298
+ - Efeito colateral: remove `compiled[_id]` do cache do nó atual (força recompilar no próximo `run`).
299
+ - Resposta `201 Created` + JSON do objeto salvo (campos null omitidos), incluindo `_id` e `updated` resultantes.
300
+
301
+ **Exemplo de request:**
302
+ ```json
303
+ {
304
+ "_id": "recebe-email",
305
+ "title": "Recebe email",
306
+ "active": true,
307
+ "method": "POST",
308
+ "timeout": 20,
309
+ "script": "def handle(payload){ payload._id = payload.email; manager.getJongoConnection().getCollection('email__c').save(payload); return ['ok': true] }"
186
310
  }
187
311
  ```
188
312
 
189
- **Quando usar timeout customizado:**
190
- - Endpoints que fazem chamadas HTTP externas (OAuth, APIs externas)
191
- - Endpoints com BCrypt hashing (custo computacional)
192
- - Endpoints que fazem múltiplas operações em sequência
193
- - **Recomendação:** 20-30s para endpoints com chamadas externas
313
+ ### `DELETE /v3/public/{id}` — remover endpoint (CRUD)
314
+
315
+ | Aspecto | Detalhe |
316
+ | ------------ | ------- |
317
+ | Finalidade | Remover o documento e limpar o cache de compilação. |
318
+ | Autenticação | Header `Authorization`. |
319
+ | Resposta | `204 No Content`. |
320
+
321
+ **Comportamento real:** `remove({_id:#}, id)` é idempotente (sem erro se não existir). Limpa `compiled[id]` **e** `dates[id]` do nó atual.
322
+
323
+ ### `GET /v3/pub/{apikey}/{slug}` — execução pública (GET)
324
+
325
+ | Aspecto | Detalhe |
326
+ | ------------ | ------- |
327
+ | Finalidade | Executar o script do endpoint cujo `_id == slug`. |
328
+ | Autenticação | **Nenhuma** (sem token/secret). A `apikey` da URL é a chave pública da gamificação e seleciona o tenant. |
329
+ | Payload | Query params ⇒ `Map`. **Apenas o primeiro valor** de cada parâmetro é mantido. |
330
+
331
+ ### `POST /v3/pub/{apikey}/{slug}` — execução pública (POST)
332
+
333
+ | Aspecto | Detalhe |
334
+ | ------------ | ------- |
335
+ | Finalidade | Idem GET, recebendo corpo JSON. |
336
+ | Autenticação | **Nenhuma** (apenas a `apikey` pública da URL). |
337
+ | Payload | Corpo JSON desserializado em `HashMap<String,Object>`. **Deve ser um objeto JSON** (array/escalar falha na desserialização). |
338
+
339
+ **Comportamento real (ambos):**
340
+
341
+ | Condição | Status HTTP | Corpo |
342
+ | ------------------------------------- | ----------- | ----- |
343
+ | `slug` não encontrado | `404` | `PublicResponse(404, "Endpoint '...' não encontrado", null)` |
344
+ | `active == false` | `403` | `PublicResponse(403, "Este endpoint está desativado", null)` |
345
+ | Método HTTP não confere | **`406`** | `PublicResponse(405, "Método HTTP '...' não permitido...", null)` |
346
+ | Sucesso | `200` | **somente** o retorno de `handle()` (serializado em JSON) |
347
+ | Exceção no script (compilar/executar) | `500` | **string** da mensagem de exceção |
348
+
349
+ > **Inconsistência confirmada no código:** na checagem de método, `Response.status(Response.Status.NOT_ACCEPTABLE)` ⇒ **HTTP 406**, mas o corpo `PublicResponse.statusCode = 405`. A intenção do desenvolvedor era `405 Method Not Allowed`; o status HTTP efetivo é `406`.
194
350
 
195
- ### Atualizar Public Endpoint via API
351
+ Todas as respostas de `/v3/pub` incluem o header **`X-Response-Time: <ms>`**.
196
352
 
353
+ ---
354
+
355
+ ## 5. Regras de Negócio
356
+
357
+ Regras presentes no código que **não** estão expressas no schema:
358
+
359
+ 1. **Endpoint desativado por padrão.** `active` é `boolean` primitivo (default `false`). Criar um endpoint sem `"active": true` resulta em **403** na execução.
360
+ 2. **Método default `POST`.** Omitir `method` no save grava `"POST"` (inicializador do campo). Chamar via GET nesse caso ⇒ **406**.
361
+ 3. **Comparação de método case-insensitive** (`equalsIgnoreCase`): `get`/`GET`/`Get` são equivalentes. Se `method` ficar `null` no documento (ex.: gravado fora do `save()` com `method:null`), a checagem `endpoint.method.equalsIgnoreCase(...)` lança **NPE** (não tratada → 500 do container).
362
+ 4. **Cache de compilação por nó + coerência via `updated`.** Cada nó do cluster mantém `compiled[_id]` e `dates[_id]` em memória. Recompila quando `clazz == null || dates[_id] == null || dates[_id].before(endpoint.updated)`. Como `save()` atualiza `updated`, qualquer nó que ainda tenha a classe antiga recompila no próximo `run` (o nó que salvou também já limpou o cache local).
363
+ 5. **Auto-save em `run()` se `updated == null`.** Endpoints inseridos por fora (ex.: via `/v3/database/public_endpoint` sem passar pelo `save()`) recebem `updated` na primeira execução.
364
+ 6. **Dois timeouts independentes:**
365
+ - `@TimedInterrupt(value = 5L, unit = SECONDS)` — transformação AST do Groovy; interrompe loops/métodos longos em **5s** (hardcoded no wrapper).
366
+ - `Future.get(timeout, SECONDS)` no `executor` — **10s** por padrão, ou `endpoint.timeout` se `> 0`. É a parede de wall-clock da chamada a `handle()`.
367
+ - **Inversão:** o `TimedInterrupt` (5s) é menor que o default do executor (10s). Aumentar `timeout` **não** afeta o limite de 5s do `TimedInterrupt` para código que cai sob suas verificações (loops/métodos). Os dois mecanismos coexistem.
368
+ 7. **Multi-tenant pela `apikey` da URL.** `FrontController.getInstance(apikey)` isola o `ManagerFactory` (e o MongoDB) daquela gamificação. O `slug` é buscado dentro desse tenant.
369
+ 8. **Sem transação / consistência eventual.** O que o script faz no banco é aplicado operação a operação; não há rollback automático em caso de timeout no meio da execução.
370
+
371
+ ---
372
+
373
+ ## 6. Comportamentos Automáticos
374
+
375
+ | Comportamento | Trigger | Impacto | Persistência |
376
+ | ------------- | ------- | ------- | ------------ |
377
+ | Geração de `_id` | `save()` com `_id` vazio/null | Cria slug via `Guid.shortTimeMillis()` | Sim (documento) |
378
+ | Carimbo de `updated` | Todo `save()` | `updated = new Date()` (sobrescreve cliente) | Sim |
379
+ | Invalidação de cache local | `save()` | `compiled.remove(_id)` (não remove `dates[_id]`) | Não (memória do nó) |
380
+ | Limpeza de cache | `delete()` | `compiled.remove(id)` + `dates.remove(id)` | Não (memória do nó) |
381
+ | Recompilação cross-cluster | `run()` em nó com classe defasada | Recompila se `dates[_id] < updated` | Não (memória do nó) |
382
+ | Auto-save | `run()` com `updated == null` | Persiste `updated` antes de executar | Sim |
383
+ | Criação via instalação de App | `AppManager.funifierCreateCustomEndpoint` | Cria/atualiza endpoint a partir do pacote | Sim |
384
+
385
+ ### Criação automática na instalação de um App/pacote
386
+
387
+ `AppManager` (linhas ~1131-1162) lê `funifierCreates.customEndpoints[]` na config do app:
388
+
389
+ ```mermaid
390
+ flowchart TD
391
+ A["App install: funifierCreates.customEndpoints[]"] --> B{elemento é JSONObject?}
392
+ B -- sim --> C["funifierCreateCustomEndpoint(doc)"]
393
+ C --> D["PublicManager.save() — upsert em public_endpoint"]
394
+ B -- "não, é String id" --> E{categoria do doc == backend?}
395
+ E -- sim --> F["[skip] sem payload"]
396
+ E -- não --> G["[skip] categoria não-java"]
197
397
  ```
198
- POST /v3/public
199
- Authorization: Basic <base64(apiKey + ":")>
200
- Content-Type: application/json
201
398
 
202
- {
203
- "_id": "slug_do_endpoint",
204
- "title": "Titulo",
205
- "description": "Descricao",
206
- "active": true,
207
- "method": "POST",
208
- "script": "public Object handle(Object payload) { ... }",
209
- "sample": "{}",
210
- "extra": {}
211
- }
399
+ `funifierCreateCustomEndpoint(JSONObject)` simplesmente faz `manager.getPublicManager().save(JsonUtil.fromJson(payload, PublicEndpoint.class))`. O bloco de código comentado logo abaixo (`AppManager.java:447-458`) mostra a **implementação anterior**, que fazia `Unirest.post(FUNIFIER_SERVER + "/v3/public")` — hoje substituída por chamada direta ao manager.
400
+
401
+ ---
402
+
403
+ ## 7. Suportado vs NÃO Suportado
404
+
405
+ ### Suportado
406
+
407
+ - CRUD autenticado de endpoints (`GET/POST/DELETE /v3/public`).
408
+ - Execução pública sem token (`GET/POST /v3/pub/{apikey}/{slug}`).
409
+ - Script Groovy com acesso ao `ManagerFactory` (todos os managers da gamificação).
410
+ - Imports pré-carregados de HTTP (Unirest, Apache HttpClient, groovyx.net.http), JSON (`groovy.json`, `JsonUtil`), e-mail (`simplejavamail`, `FunifierMail`), resize de imagem (Thumbnailator) e entidades Funifier.
411
+ - Timeout configurável por endpoint (`timeout`, em segundos).
412
+ - Cache de classe compilada por nó, com invalidação automática por `updated`.
413
+ - Criação programática e via instalação de App.
414
+
415
+ ### ❌ NÃO Suportado / Limitações confirmadas no código
416
+
417
+ - **Sem pipeline de aggregate.** Apesar dos comentários "prepared aggregate", **não há** montagem de aggregate MongoDB — é Groovy livre. Legado da entidade `prepared_aggregate`.
418
+ - **`apikey` (campo da entidade) ignorado na execução** — o tenant vem da URL.
419
+ - **`sample` ignorado em runtime** — apenas documentação.
420
+ - **`extra` não é catch-all** — só captura a chave JSON literal `"extra"`; chaves desconhecidas são descartadas.
421
+ - **GET multivalorado não suportado** — apenas o 1º valor de cada query param entra no payload.
422
+ - **Sem 404 no CRUD** — `GET /v3/public/{id}` inexistente retorna `200` com corpo `null`.
423
+ - **Status HTTP de método incorreto** — retorna `406` onde o corpo declara `405`.
424
+ - **Sem listagem nativa** — `PublicRest` não expõe `GET /v3/public` (lista). Para listar, usa-se `/v3/database/public_endpoint`.
425
+ - **Sem versionamento, sem histórico, sem rate limiting** no módulo.
426
+ - **`message` do `PublicResponse` perdida no sucesso** — o cliente nunca vê `"OK"`.
427
+
428
+ ### ⚠️ Bug confirmado — vazamento de threads no `executor()`
429
+
430
+ `PublicManager.executor()` cria `Executors.newSingleThreadExecutor()` a **cada** execução. O `executor.shutdownNow()` só é chamado no bloco `catch` (timeout/erro). **No caminho de sucesso o `shutdown` não é chamado explicitamente.** Como `newSingleThreadExecutor()` usa threads **não-daemon**, o worker permanece vivo até ser reclamado — o `FinalizableDelegatedExecutorService` retornado faz `shutdown()` apenas na finalização pelo GC. Na prática: o encerramento é **deferido e dependente do GC**, e threads/executors **acumulam sob carga** (reclamação lenta e frágil, presa a um hook do JVM). (Trecho relevante: `executor()`, linhas ~252-273.)
431
+
432
+ ---
433
+
434
+ ## 8. Segurança e Permissões
435
+
436
+ **Autenticação:**
437
+ - **CRUD (`/v3/public`):** exige header `Authorization`, resolvido por `AuthBean.getApiKey()` (suporta `Basic apiKey:secretKey`, `Bearer <jwt>`, `Studio <token>`, `Account ...`). O `PublicRest` em si **não faz checagem de role/permissão adicional** — o isolamento é dado pela `apikey` resolvida (tenant). Pela convenção Funifier, operações de escrita usam Basic com `apiKey:secretKey`.
438
+ - **Execução (`/v3/pub`):** **sem autenticação**. Conhecer a `apikey` pública + o `slug` é suficiente. Por isso, scripts públicos **não devem** expor dados sensíveis nem executar operações destrutivas sem validação própria dentro do `handle`.
439
+
440
+ **Isolamento por tenant:** `FrontController.getInstance(apikey)` garante que `find`/`run` operem apenas na gamificação da `apikey`.
441
+
442
+ **Sandbox do script** (`compile()` + `TriggerExpressionChecker`):
443
+
444
+ 1. `SecureASTCustomizer` com **whitelist de tokens** (operadores permitidos):
445
+ `=`, `+`, `-`, `*`, `/`, `%`, `**`, `++`, `+=`, `--`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `||`, `||=`, `&&`, `&&=`, `[`, `]`.
446
+ Operadores **fora dessa lista são rejeitados na compilação** — por exemplo `instanceof` (`KEYWORD_INSTANCEOF` não está na whitelist) não compila.
447
+ 2. `TriggerExpressionChecker` (blacklist):
448
+ - Bloqueia expressões cujo **tipo** seja `System`, `ProcessBuilder`, `File`, `GroovyShell` ou `GroovyObject`.
449
+ - Bloqueia expressões cujo **texto** contenha `.execute`, `.getDB`, `.getMongo` ou `.dropDatabase`.
450
+
451
+ **Superfícies de risco confirmadas no código:**
452
+ - O sandbox é **baseado em blacklist textual/por-tipo** — é contornável (ex.: reflexão, nomes construídos dinamicamente, classes não listadas). Não é uma sandbox forte.
453
+ - O script tem acesso total ao `ManagerFactory` ⇒ pode ler/escrever qualquer coleção do tenant. Combinado com a execução **sem token**, qualquer um que conheça `apikey` + `slug` aciona essa lógica.
454
+ - `endpoint.script` é compilado e executado — quem tem permissão de **escrita** no CRUD tem, na prática, **execução de código** no servidor (RCE dentro dos limites do sandbox).
455
+
456
+ ---
457
+
458
+ ## 9. Observabilidade e Troubleshooting
459
+
460
+ **Diagnóstico rápido:**
461
+ - O header `X-Response-Time` aparece em toda resposta de `/v3/pub` — útil para medir latência do script.
462
+ - Erros de instalação de App via pacote são impressos em stdout: `[funifier:public] criado: ...` / `Falha public: <msg>` (`AppManager`).
463
+
464
+ **Listar / inspecionar endpoints:**
465
+ ```
466
+ GET /v3/database/public_endpoint # lista todos (via database genérico)
467
+ GET /v3/public/{id} # busca um (200 + null se não existir)
212
468
  ```
213
469
 
214
- O mesmo endpoint serve para criar e atualizar (upsert por `_id`)
470
+ **Erros comuns e causas:**
215
471
 
216
- ### Bibliotecas HTTP disponíveis
472
+ | Sintoma | Causa provável |
473
+ | ------- | -------------- |
474
+ | `403 "Este endpoint está desativado"` | `active != true` no documento. |
475
+ | `406` ao chamar | `method` do endpoint difere do verbo usado (corpo dirá `405`). |
476
+ | `404 "Endpoint '...' não encontrado"` | `slug` ≠ `_id`, ou criado em outro tenant (`apikey` da URL errada). |
477
+ | `500` com string de exceção | Erro de **compilação** (token bloqueado, classe na blacklist) **ou** de **execução** do `handle`. A string é `ex.getMessage()`. |
478
+ | `500` por `TimeoutException` | `handle` excedeu `timeout` (default 10s) — note também o limite de 5s do `@TimedInterrupt`. |
479
+ | Mudança no script "não pega" | Cache de classe; só invalida via `save()` (que atualiza `updated`). Editar o doc direto no Mongo sem mudar `updated` mantém a classe antiga em cache. |
480
+ | `GET /v3/public/{id}` retornando `null` com 200 | Comportamento esperado — não há 404 no CRUD. |
217
481
 
218
- | Biblioteca | Uso recomendado |
219
- |-----------|----------------|
220
- | **Unirest** | HTTP client mais simples — `Unirest.post(url).header(...).body(...).asJson()` |
221
- | **Apache HttpClient** | Alternativa mais verbosa |
222
- | **groovyx.net.http** | HTTP Builder para Groovy |
223
- | **HttpUtil / HttpClientFunifier** | Utilitários Funifier |
482
+ **O que verificar quando "não funciona":**
483
+ 1. `active == true`?
484
+ 2. `method` bate com o verbo da chamada?
485
+ 3. `script` define `def handle(payload) { ... }`?
486
+ 4. A `apikey` da URL é a da gamificação correta?
487
+ 5. O erro 500 traz a mensagem de exceção no corpo — leia-a.
224
488
 
225
- ### Exemplo usando Unirest
489
+ ### Notas operacionais (observadas em runtime — **não** provadas pelo código deste módulo)
226
490
 
227
- ```groovy
228
- public Object handle(Object payload) {
229
- def response = Unirest.post("https://api.external.com/endpoint")
230
- .header("Content-Type", "application/json")
231
- .header("Authorization", "Bearer my-token")
232
- .body(JsonUtil.toJson(payload))
233
- .asString()
234
-
235
- def result = new groovy.json.JsonSlurper().parseText(response.getBody())
236
- return result
491
+ > As notas abaixo vêm de experiência operacional com o motor Groovy/Jackson e **não** são deriváveis dos arquivos do módulo `public`. Trate-as como heurísticas, não como contrato:
492
+ > - `groovy.json.JsonSlurper` pode produzir estruturas (`LazyMap`) que falham na serialização da resposta — preferir `JsonUtil.fromJsonToMap(...)`.
493
+ > - `$` dentro de GStrings é interpretado como variável Groovy; comandos Mongo (`$set`, `$inc`) precisam ser escapados/concatenados.
494
+ > - Caracteres não-ASCII em comentários do script podem causar erro de parse.
495
+ > - Acesso a propriedades de entidades (ex.: `player.extra`) depende da definição da classe correspondente — confirmar no código da entidade.
496
+
497
+ ---
498
+
499
+ ## 10. Exemplos Práticos
500
+
501
+ ### 10.1 Mínimo funcional — eco do payload
502
+
503
+ ```json
504
+ POST /v3/public
505
+ {
506
+ "_id": "echo",
507
+ "active": true,
508
+ "method": "POST",
509
+ "script": "def handle(payload){ return payload }"
237
510
  }
238
511
  ```
512
+ Chamada:
513
+ ```
514
+ POST /v3/pub/{apikey}/echo
515
+ { "nome": "Ana" }
516
+ → 200 { "nome": "Ana" } # corpo = retorno de handle()
517
+ ```
239
518
 
240
- > ⚠️ **Triggers e Schedulers usam a mesma estrutura de wrapper** com os mesmos imports pré-disponíveis. A diferença é apenas o método principal: triggers usam `void trigger(event, entity, player, database)` e schedulers usam `void execute()`.
241
-
242
- ## Boas Práticas
519
+ ### 10.2 Avançado gravar em coleção + usar managers + timeout
243
520
 
244
- - Não expor dados sensíveis em endpoints públicos
245
- - Não realizar operações críticas sem validação adicional
246
- - Validar payload recebido no script Java
521
+ ```json
522
+ POST /v3/public
523
+ {
524
+ "_id": "recebe-lead",
525
+ "title": "Recebe lead de formulário externo",
526
+ "active": true,
527
+ "method": "POST",
528
+ "timeout": 20,
529
+ "sample": "{ \"email\": \"user@x.com\", \"nome\": \"User\" }",
530
+ "script": "def handle(payload){ payload._id = payload.email; manager.getJongoConnection().getCollection('lead__c').save(payload); def resp = [:]; resp.ok = true; resp.id = payload.email; return resp }"
531
+ }
532
+ ```
533
+ Chamada (sem token):
534
+ ```
535
+ POST /v3/pub/{apikey}/recebe-lead
536
+ { "email": "ana@x.com", "nome": "Ana" }
537
+ → 200 { "ok": true, "id": "ana@x.com" }
538
+ ```
247
539
 
248
- ## Validações e Testes
540
+ ### 10.3 Anti-padrão — o que NÃO fazer
249
541
 
250
- - [ ] Endpoint aparece na lista
251
- - [ ] URL pública é acessível sem token
252
- - [ ] Script processa payload corretamente
253
- - [ ] Resposta é retornada conforme esperado
542
+ ```json
543
+ {
544
+ "_id": "ruim",
545
+ "script": "def handle(payload){ for(i in 0..1000000000){ }; return 'fim' }"
546
+ }
547
+ ```
548
+ Problemas:
549
+ - Sem `"active": true` ⇒ retorna **403** (esquecimento mais comum).
550
+ - Loop longo ⇒ interrompido pelo `@TimedInterrupt` (5s) ⇒ **500**.
551
+ - Usar `import` dentro do script ⇒ erro de compilação (imports já estão no wrapper).
552
+ - Nomear o método diferente de `handle` ⇒ `invokeMethod("handle", ...)` falha ⇒ **500**.
553
+ - Usar `instanceof`, `System.exit`, `.execute()`, `.getDB()` ⇒ bloqueado pelo sandbox ⇒ **500**.
554
+ - Retornar estrutura de `JsonSlurper` que não serializa ⇒ erro na resposta (ver Notas operacionais).
555
+
556
+ ---
557
+
558
+ ## Checklist de Configuração
559
+
560
+ - [ ] `_id` (slug) definido — ou deixe vazio para gerar automaticamente.
561
+ - [ ] `"active": true` — **sem isso, todas as chamadas retornam 403** (default é `false`).
562
+ - [ ] `method` bate com o verbo que o cliente vai usar (`"POST"` é o default).
563
+ - [ ] `script` define **`def handle(payload) { ... }`** (o nome `handle` é obrigatório).
564
+ - [ ] **Não** usar `import` no script — todos os imports já estão no wrapper.
565
+ - [ ] `timeout` (segundos) ajustado se o script faz I/O externo (default 10s; lembre do limite de 5s do `@TimedInterrupt`).
566
+ - [ ] Tenant correto: a `apikey` da URL `/v3/pub/{apikey}/{slug}` é a da gamificação onde o endpoint foi salvo.
567
+ - [ ] Não retornar dados sensíveis — a execução é **pública e sem token**.
568
+ - [ ] Validar o `payload` dentro do `handle` (não confie na entrada).
569
+ - [ ] Armadilha silenciosa: omitir `method`/`active` no `POST /v3/public` reseta esses campos (full replace) — `method` volta a `"POST"`, `active` volta a `false`.
570
+ - [ ] Após editar via Mongo direto, garanta que `updated` mude — senão o cache de classe não invalida.
571
+
572
+ ---
573
+
574
+ ## REGRAS DE OURO (resumo do comportamento real)
575
+
576
+ 1. `/v3/public` = CRUD autenticado; `/v3/pub/{apikey}/{slug}` = execução pública sem token.
577
+ 2. Execução é **síncrona**, com dois timeouts (5s TimedInterrupt + 10s/`timeout` executor).
578
+ 3. No **sucesso**, o corpo é só o retorno de `handle()`; em **erro de script**, é a string da exceção (500); em **pré-checagem**, é um `PublicResponse` JSON.
579
+ 4. Método errado ⇒ HTTP **406** (corpo diz 405). Inativo ⇒ 403. Inexistente ⇒ 404.
580
+ 5. Cache de classe por nó, invalidado por `updated`.
581
+ 6. Sandbox é blacklist (contornável); script tem acesso total ao tenant via `manager`.
582
+ 7. **Bug conhecido:** vazamento de thread no caminho de sucesso do `executor()`.
583
+ 8. Não existe aggregate pipeline — "prepared aggregate" é só legado de nomenclatura.