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,169 +1,731 @@
1
- # Security (Segurança)
1
+ # `security`
2
2
 
3
- **Acesso Studio:** Gamification Settings"Mais" Security "Change your gamification security settings"
4
- **Armazenamento:** Coleção `security`, documento com `_id` = API Key da gamificação
3
+ **Acesso Studio:** Funifier StudioSecurity (cadastro de **Apps** e **Roles**). Não rota Studio dedicada confirmável no backend.
4
+ **API Endpoint:** `/v3/database/security/{_id}` gerenciado pelo `DatabaseRest` genérico. **Não existe `SecurityRest`.** Endpoints correlatos: `/v3/auth/token`, `/v3/auth/module`.
5
+ **Coleção MongoDB:** `security`
5
6
 
6
- ## Estrutura do Documento
7
+ > Engenharia reversa baseada no código de `funifier-service` (`com.funifier.engine.security.*`, `com.funifier.rest.v3.filter.SecurityFilter`, `com.funifier.engine.auth.*`, `com.funifier.rest.v3.rest.DatabaseRest`, `com.funifier.rest.v3.rest.AuthenticationRest`, `com.funifier.rest.v3.TokenUtil`, `com.funifier.rest.v3.AuthBean`). Onde a documentação anterior divergia do código, o código prevalece — as divergências estão sinalizadas ao longo do texto.
7
8
 
8
- ```json
9
- {
10
- "_id": "API_KEY_DA_GAMIFICACAO",
11
- "roles": [
12
- { "name": "public", "scope": "read_all", "timeout": "" },
13
- { "name": "player", "scope": "read_all, write_all, delete_all, database", "timeout": "" }
14
- ],
15
- "apps": [
16
- { "name": "Nome do App", "app_secret": "GUID_GERADO", "scope": "read_all, write_all, delete_all, database" }
17
- ]
18
- }
19
- ```
9
+ ---
10
+
11
+ ## 1. Visão Geral
20
12
 
21
- ## Roles
13
+ A coleção `security` guarda **um documento de configuração de segurança por gamificação**. No Funifier cada gamificação tem seu próprio banco MongoDB, então a coleção `security` daquele banco contém — **por convenção** — um único documento. Essa unicidade **não é imposta pelo código**: a leitura usa `findOne()` sem filtro (`SecurityDaoMongo.find()`) e a escrita é um `save()` por `_id` (ver seção 2.1). Documentos extras podem coexistir e causar comportamento ambíguo.
22
14
 
23
- Definem o nível de acesso dos tokens Bearer (jogadores logados) e Basic (acesso público).
15
+ O documento concentra:
24
16
 
25
- ### Campos
26
- - `name`Identificador da role (ex: `public`, `player`, `admin`)
27
- - `scope`Permissões separadas por vírgula
28
- - `timeout` — Tempo de expiração do token (ex: `7d`, `1h`). **⚠️ NÃO usar string vazia `""`** — causa NPE no auth. Omitir o campo ou usar `null` para timeout padrão (7 dias).
17
+ - **`apps`** — aplicações servidoras com `app_secret`, `scope` e `whitelist` (autenticação Basic / `client_credentials`).
18
+ - **`roles`**papéis de jogador com `scope`, `timeout` (validade do token) e `whitelist` (autenticação `password`).
19
+ - **`requirePassword`**, **`createPlayerIfDontExist`** comportamentos de login de jogador.
20
+ - **`timeZone`** — fuso horário consumido por challenges/bônus/conquistas.
21
+ - **`inlineEditable`** — edição inline no Studio.
29
22
 
30
- ### Scopes Disponíveis
31
- | Scope | Descrição |
32
- |-------|-----------|
33
- | `read_all` | Leitura em todas as coleções |
34
- | `write_all` | Escrita em todas as coleções |
35
- | `delete_all` | Exclusão em todas as coleções |
36
- | `database` | **Obrigatório** para acessar `/v3/database`. Sem ele, POST retorna 201 mas não persiste |
23
+ O documento é **lido em tempo de request** por:
37
24
 
38
- ### Roles Comuns
39
- - **`public`** — `read_all, write_database_signup__c` Para acesso não autenticado (signup, landing page). Usa token Basic. Precisa de `write_database_signup__c` para o signup pattern funcionar.
40
- - **`player`**`read_all, write_all, delete_all, database` Para usuários logados. Usa token Bearer.
25
+ 1. **`SecurityFilter`** — filtro de servlet que intercepta toda requisição `/v3/**` e decide passar ou retornar `401` (seção 2.2). Lê `roles` (role `public`) e `apps` (`scope`/`whitelist`).
26
+ 2. **`AuthBean.getScope()`** — recalcula o scope efetivo para `/v3/database` (seção 4).
27
+ 3. **`AuthenticationRest` (`/v3/auth/token`)** emite tokens Bearer JWT a partir de `apps`/`roles` (seção 2.4).
28
+ 4. **Managers de domínio** — `timeZone` (`ChallengeManager`, `LastMileManager`, `BonusManager`, `AchievementManager`), `inlineEditable` (`FrontController.isInline`, `InlineManager`), `createPlayerIfDontExist` (`AuthenticationManager.createIfDontExist`, usado na ingestão de action logs).
41
29
 
42
- ### ⚠️ Lição Crítica
43
- A palavra **`database`** deve estar literalmente no scope para que `/v3/database` funcione. Mesmo com `write_all` e `read_all`, sem `database` as operações falham silenciosamente.
30
+ > **Importante (divergência com a documentação anterior):** existe no código um método `AuthenticationManager.authenticate(auth_mode, player, app_secret, password, …)` com os modos `IMPLICIT`, `CREDENTIAL`, `LDAP`, `FACEBOOK`, `GOOGLE`, validação de `websites` e auto-criação de player no login. **Esse método não é chamado por nenhum endpoint** (código órfão — ver seção 2.6 e 7). A documentação anterior descrevia esse fluxo como se fosse o caminho vivo de autenticação de jogador; ele **não** é alcançável.
44
31
 
45
- ## Apps
32
+ ---
46
33
 
47
- Apps geram tokens Basic não-expirantes para uso em triggers, endpoints públicos e integrações server-side.
34
+ ## 2. Arquitetura e Fluxos
48
35
 
49
- ### Campos
50
- - `name` — Nome descritivo do app
51
- - `app_secret` GUID gerado pelo Funifier (via `/v3/util/guid/new`)
52
- - `scope` — Permissões (mesmo formato das roles)
36
+ ### 2.1 Persistência — via `DatabaseRest` genérico (NÃO há full-replace)
37
+
38
+ Não existe `SecurityRest`. A entidade está registrada no enum de roteamento como `Entity.SECURITY("security", Security.class)` e é gravada/lida pela coleção genérica do `DatabaseRest`.
39
+
40
+ `SecurityManager.update()` e `SecurityDaoMongo.update()` (que fazem `c.remove(); c.save(security);` — apagar **toda** a coleção e reinserir) **são código morto no caminho Java**: o único `.update(security)` no projeto é a própria definição/duplicata comentada, e nenhum endpoint o invoca. Ressalva: o `grep` cobre apenas o código Java compilado — um script Groovy (trigger/auth module) que chame `manager.getSecurityManager().update(...)` **dispararia** o `c.remove() + c.save()` destrutivo (apaga toda a coleção `security`). Evite expor essa chamada em scripts.
41
+
42
+ A escrita real é a do `DatabaseRest`:
53
43
 
54
- ### Token Basic do App
55
44
  ```
56
- Basic base64(API_KEY + ":" + APP_SECRET)
45
+ [PUT /v3/database/security] → DatabaseRest.update()
46
+ 1. valida scope contém "database" (Application.SCOPE_DATABASE) e collection.length > 3
47
+ 2. se body não tem _id → gera Guid.newShortGuid() ← cria documento avulso!
48
+ 3. TriggerManager.execute(..., "security", EVENT_BEFORE_UPDATE, player)
49
+ 4. jongo.getCollection("security").save(o) ← upsert por _id (Jongo)
50
+ 5. TriggerManager.execute(..., "security", EVENT_AFTER_UPDATE, player)
51
+ 6. AuditManager.log("security", EVENT_UPDATE, authBean, o)
57
52
  ```
58
53
 
59
- Exemplo:
60
- ```javascript
61
- var basicToken = 'Basic ' + btoa('69ab1a9a607db81962b92cd2:69ab3566607db81962b9686e');
54
+ Consequências reais do `save(o)`:
55
+
56
+ - **É um upsert do documento inteiro pelo `_id`.** Campos omitidos no PUT **somem daquele documento** (é substituição do documento, não merge/patch). A advertência "PUT parcial apaga campos" continua válida — porém **o motivo não é** "apaga a coleção inteira".
57
+ - **NÃO apaga os demais documentos da coleção.** Se você gravar com um `_id` diferente, cria um **segundo** documento `security`. Como `find()` usa `findOne()` sem filtro, qualquer documento pode "vencer".
58
+ - **POST e PUT geram `_id` automático** quando ausente (`Guid.newShortGuid()`), o que produz documentos `security` órfãos.
59
+
60
+ Leitura interna (`SecurityDaoMongo.find()`):
61
+
62
+ ```java
63
+ Security s = c.findOne().as(Security.class);
64
+ return s != null ? s : new Security(); // NUNCA retorna null
65
+ ```
66
+
67
+ > `find()` **nunca retorna null** — se a coleção estiver vazia retorna `new Security()`, cujos defaults de campo são `requirePassword=false`, `createPlayerIfDontExist=true`, `timeZone=null`. (A documentação anterior afirmava retorno `null` — incorreto.)
68
+
69
+ ### 2.2 Pipeline de autorização — `SecurityFilter.doFilter()`
70
+
71
+ Executado a cada requisição HTTP. Tipos de auth reconhecidos: `Basic`, `Account`, `Studio`, `Bearer`.
72
+
73
+ #### Fluxo de autorização — `SecurityFilter.doFilter`
74
+
75
+ ```mermaid
76
+ flowchart TD
77
+ A[Request /v3/**] --> B{method == OPTIONS?}
78
+ B -- sim --> PASS[doFilter -> segue]
79
+ B -- nao --> C[apikey = AuthBean.getApiKey]
80
+ C --> D{apikey != null?}
81
+ D -- sim --> E[StatisticManager.newApiRequest]
82
+ E --> F{limite diario ok?}
83
+ F -- nao --> R401A[401 exceeded daily api requests]
84
+ F -- sim --> G
85
+ D -- nao --> G{path em whitelist publica?}
86
+ G -- sim --> PASS
87
+ G -- nao --> H{GET widget/global/system.global?}
88
+ H -- sim --> PASS
89
+ H -- nao --> I{tipo de Authorization}
90
+ I -- Basic publico --> J[scope = role public.scope]
91
+ I -- Basic c/ secret --> K[authBasic; scope=app.scope; whitelist=app.whitelist]
92
+ I -- Basic invalido --> ERR[errorMessage]
93
+ I -- Account --> L[authAccount]
94
+ I -- Studio --> M[authStudio]
95
+ I -- Bearer --> N[authBearer; scope=token.scope; whitelist=token.whitelist]
96
+ I -- nenhum --> ERR
97
+ J --> O{whitelist != null?}
98
+ K --> O
99
+ N --> O
100
+ O -- sim --> P{IP ou URL na whitelist?}
101
+ P -- nao --> R401B[401 Domain or IP is not authorized]
102
+ P -- sim --> Q
103
+ O -- nao --> Q{scope != null?}
104
+ Q -- sim --> S[avalia hierarquia de scope]
105
+ Q -- nao --> PASS
106
+ S --> T{permitido?}
107
+ T -- sim --> PASS
108
+ T -- nao --> ERR
109
+ ERR --> U[newBadRequest + 401 errorMessage]
62
110
  ```
63
111
 
64
- ## Como Criar via API (sem Studio UI)
112
+ Sequência exata:
65
113
 
66
- ### 1. Obter token de autenticação do Studio
67
- O Studio armazena o token em `localStorage` com a chave `marketplace_authorization`:
68
- ```javascript
69
- var auth = localStorage.getItem('marketplace_authorization');
70
- // Retorna: "Bearer eyJ..."
114
+ ```
115
+ 1. OPTIONS doFilter (preflight CORS), sem validação
116
+ 2. apikey = AuthBean.getApiKey(Authorization)
117
+ 3. entity = path sem "/v3/"; corta no primeiro "/"
118
+ entity_full = path sem "/v3/", com "/" trocado por "_"
119
+ 4. Estatística: se apikey != null → SystemFactory.getStatisticManager(apikey).newApiRequest(method, entity)
120
+ → retorno false → 401 "You have exceeded your gamification daily api requests limits"
121
+ 5. Paths públicos (passa sem auth) — startsWith de:
122
+ /customer, /v3/pub, /v3/version, /v3/player/password,
123
+ /v3/system/user/password/code, /v3/auth/token, /v3/sso/saml,
124
+ /v3/package/marketplace/aggregate, /v3/package/marketplace/, /v3/package/aggregate,
125
+ /v3/system/template/request/aggregate, /v3/system/template/request_folder/aggregate,
126
+ /v3/system/ai/knowledge, /v3/system/package/aggregate, /v3/system/country/aggregate,
127
+ /v3/system/plan/aggregate, /v3/system/remote/ping, /v3/system/landing, /v3/system/blog,
128
+ /v3/technique, /v3/coredrive, /v3/inline,
129
+ /v3/crm/email/track/open, /v3/crm/email/oauth_callback, /v3/crm/calendar/oauth_callback,
130
+ /v3/crm/doc/oauth_callback, /v3/crm/doc/link, /v3/ad/proxy-image, /v3/account/register
131
+ 6. GET /v3/widget | /v3/global | /v3/system/global → passa sem auth
132
+ 7. Por tipo de token (Authorization "contém" a palavra):
133
+ - Basic público (só apiKey, sem secret) → authPublic() true → scope = role "public".scope (se a role existir)
134
+ - Basic com secret → authBasic() → se inválido errorMessage; se válido scope=app.scope, whitelist=app.whitelist
135
+ - Account → authAccount() (AccountManager.isAppSecretAllowed)
136
+ - Studio → authStudio() (TokenUtil.getValueStudio != null) — usa lib auth0 JWTVerifier
137
+ - Bearer → authBearer() (TokenUtil.getValue "apiKey" ou "account" != null); scope/whitelist do token
138
+ - nenhum → errorMessage "Need to inform a type of authentication. E.g. Basic, Studio or Bearer."
139
+ 8. Se whitelist != null → isRequestWhiteListed() (seção 5); se não → 401 "Domain or IP is not authorized..."
140
+ 9. Se scope != null → avalia hierarquia (2.3)
141
+ 10. errorMessage != null → newBadRequest + 401 errorMessage
142
+ 11. caso contrário → doFilter (segue)
71
143
  ```
72
144
 
73
- ### 2. Gerar GUID para App Secret
145
+ Notas:
146
+
147
+ - **Account** com Authorization não tem token JWT; `AuthBean.getApiKey()` retorna `null` para tipo `Account` na versão estática (linha 45) — daí a estatística pode não ser registrada para Account.
148
+ - O `try/catch` global converte qualquer exceção (inclusive NPE) em `401` com `e.getMessage()`.
149
+
150
+ ### 2.3 Avaliação hierárquica de scope (`SecurityFilter`)
151
+
74
152
  ```
75
- GET https://service2.funifier.com/v3/util/guid/new
76
- Authorization: Bearer <studio_token>
153
+ scope = scope.replaceAll(" ", "") // remove TODOS os espaços
154
+ list = scope.split(",")
155
+ m = GET→"read" | POST→"write" | PUT→"write" | (outro)→method.toLowerCase() // DELETE→"delete"
156
+
157
+ allow_all = scope.indexOf(m + "_all") != -1 // ex.: read_all
158
+ allow_full = list.contains(m + "_" + entity_full) // ex.: delete_swap_metodo_123
159
+ // fallback só 2 níveis (apesar do comentário "menos n"):
160
+ allow_full_m1 = list.contains(m + "_" + <entity_full sem último _segmento> + "_all") // delete_swap_metodo_all
161
+ allow_full_m2 = list.contains(m + "_" + <dois segmentos a menos> + "_all") // delete_swap_all
162
+
163
+ PASSA se (allow_all || allow_full || allow_full_m1 || allow_full_m2)
77
164
  ```
78
- Retorna: `{ "guid": "69ab3566607db81962b9686e" }`
79
165
 
80
- ### 3. Criar/Atualizar Security Document
166
+ #### Hierarquia de scope — exemplo `DELETE /v3/swap/metodo/123`
167
+
168
+ ```mermaid
169
+ flowchart LR
170
+ A["entity_full = swap_metodo_123<br/>m = delete"] --> B{delete_all?}
171
+ B -- sim --> P[passa]
172
+ B -- nao --> C{delete_swap_metodo_123?}
173
+ C -- sim --> P
174
+ C -- nao --> D{delete_swap_metodo_all?}
175
+ D -- sim --> P
176
+ D -- nao --> E{delete_swap_all?}
177
+ E -- sim --> P
178
+ E -- nao --> X[401 sem permissao]
81
179
  ```
82
- PUT https://service2.funifier.com/v3/database/security
83
- Authorization: Bearer <studio_token>
84
- Content-Type: application/json
85
180
 
86
- {
87
- "_id": "API_KEY",
88
- "roles": [...],
89
- "apps": [...]
90
- }
181
+ Exceções (avaliadas só para `POST`, depois da hierarquia):
182
+
183
+ - `POST /v3/action/log` com scope contendo `write_actionlog` → passa mesmo sem `write_all`.
184
+ - `POST /v3/mobile/device` → passa sem verificação de scope.
185
+
186
+ Mensagem de negação: `"You dont have permission to {m} in {entity} endpoint, you need {m}_{entity_full} or {m}_all access to do it"`.
187
+
188
+ > **Camada dupla para `/v3/database`:** o `SecurityFilter` valida `read_all`/`write_all`/`delete_all` (ou os hierárquicos `*_database_all`) usando `entity = "database"`. **Além disso**, o próprio `DatabaseRest` exige que o scope contenha `database`. Logo, um app de servidor típico usa `read_all,write_all,delete_all,database`.
189
+
190
+ ### 2.4 Emissão de token — `POST /v3/auth/token`
191
+
192
+ #### Emissão de token Bearer — `/v3/auth/token`
193
+
194
+ ```mermaid
195
+ sequenceDiagram
196
+ participant Client
197
+ participant AuthenticationRest
198
+ participant AuthenticationManager
199
+ participant SecurityDaoMongo
200
+ participant TokenUtil
201
+
202
+ Client->>AuthenticationRest: POST /v3/auth/token (grant_type)
203
+ alt client_credentials
204
+ AuthenticationRest->>AuthenticationManager: isAppSecretAllowed(client_secret)
205
+ AuthenticationManager->>SecurityDaoMongo: isAppAllowed (count == 1?)
206
+ SecurityDaoMongo-->>AuthenticationManager: true/false
207
+ AuthenticationRest->>AuthenticationManager: findApplication(secret)
208
+ AuthenticationManager-->>AuthenticationRest: Application{scope, whitelist}
209
+ AuthenticationRest->>TokenUtil: generateToken(apiKey, secret, scope, 7 dias, whitelist)
210
+ TokenUtil-->>AuthenticationRest: JWT HS512+GZIP
211
+ AuthenticationRest-->>Client: {access_token, refresh_token, token_type, expires_in}
212
+ else password
213
+ AuthenticationRest->>AuthenticationManager: authenticateModules(req) (Groovy/PAM)
214
+ AuthenticationManager-->>AuthenticationRest: status 0|1|2
215
+ AuthenticationRest->>AuthenticationManager: authenticatePassword(player, password) (se status 0)
216
+ AuthenticationRest->>AuthenticationManager: findRoleCalculatedToPlayer(player)
217
+ AuthenticationManager-->>AuthenticationRest: Role{scope, timeout, whitelist}
218
+ AuthenticationRest->>TokenUtil: generateToken(apiKey, player, scope, role.timeout||7d)
219
+ AuthenticationRest-->>Client: {access_token, refresh_token, token_type, expires_in}
220
+ else refresh_token
221
+ AuthenticationRest->>TokenUtil: accessTokenWithRefreshToken(refresh)
222
+ AuthenticationRest-->>Client: novo access_token + refresh_token
223
+ end
91
224
  ```
92
225
 
93
- **⚠️ PUT é upsert** substitui o documento inteiro. Sempre incluir roles E apps existentes.
226
+ Detalhes por `grant_type` (existem **dois** handlers `/v3/auth/token`: um `application/x-www-form-urlencoded`, outro `application/json`):
227
+
228
+ | grant_type | Validação | Expiração do access_token | Refresh token |
229
+ | --- | --- | --- | --- |
230
+ | `client_credentials` | `isAppSecretAllowed(secret)` → `findApplication` | **7 dias** (`TimeScale.DAY, 7`) | sim (form) / não (json) |
231
+ | `password` | auth modules (PAM) + `authenticatePassword` | `role.timeout` (ex.: `7d`) ou **7 dias** default | sim (form) / não (json) |
232
+ | `refresh_token` | re-emite a partir do refresh (1 ano) | reconstruída (app 7d / player role.timeout\|7d) | sim |
233
+
234
+ - O `expires_in` retornado é **timestamp Unix em milissegundos** (`expiration.getTime()`), e **não** segundos — diverge do padrão OAuth2.
235
+ - A variante `application/json` (`tokenJson`) **não** anexa `whitelist` ao token (usa overload sem whitelist) e **não** devolve `refresh_token`. A variante `application/x-www-form-urlencoded` anexa whitelist e devolve refresh token.
236
+ - `/v3/auth/token` está na whitelist de paths públicos do `SecurityFilter` (não exige autenticação prévia).
237
+
238
+ > **Atenção ao default de 30 dias:** o valor `60*60*24*30` (30 dias) existe **apenas** dentro do método órfão `authenticate(auth_mode, …)` (seção 2.6), que não é chamado. Os tokens emitidos de verdade por `/v3/auth/token` usam **7 dias** (app e player default) ou `role.timeout`.
239
+
240
+ ### 2.5 Cálculo de role do jogador — `findRoleCalculatedToPlayer(player)`
94
241
 
95
- ### 4. Verificar
96
242
  ```
97
- GET https://service2.funifier.com/v3/database/security/API_KEY?strict=true
98
- Authorization: Bearer <studio_token>
243
+ 1. principal = coleção "principal".findOne({_id: player})
244
+ 2. security = SecurityDaoMongo.find() // nunca null
245
+ 3. se principal == null OU principal.roles vazio → role = security.getRole("player")
246
+ 4. senão se security.getRole(player) != null → role individual por id do player
247
+ 5. senão se principal.roles.size() == 1 → role = security.getRole(roles[0])
248
+ 6. senão (size > 1) → MERGE:
249
+ - scope = concatenação de todos os scopes das roles, separados por ","
250
+ - timeout = o de MENOR data de expiração (sessão mais curta vence)
251
+ - whitelist: união sem duplicatas; se QUALQUER role tiver whitelist null → whitelist final = null (irrestrito)
252
+ - name = player
99
253
  ```
100
254
 
101
- ## Navegação no Studio (Browser Automation)
255
+ Observações de runtime:
256
+
257
+ - Se a role resolvida for `null` (ex.: passo 5 com nome de role inexistente em `security.roles`), o chamador (`TokenUtil`/`AuthenticationRest`) cai no default de scope `read_all,write_actionlog,write_upload` e expiração `+7d` — **mas** acessa `role.whitelist` em seguida, o que provoca **NPE** quando `role == null` no overload com whitelist. Edge case real.
258
+ - `DateUtil.fromKeyword("+" + role.timeout)`: um `timeout` igual a `""` (string vazia) gera `"+"`, que tende a produzir erro/`NPE` na geração do token. **Use `null`/omita o campo, nunca `""`.**
259
+
260
+ ### 2.6 Código órfão (presente, mas NÃO acessível)
261
+
262
+ `AuthenticationManager.authenticate(String auth_mode, String player, String app_secret, String password, int expiration, String language, String host, String oauth_access_token)`:
263
+
264
+ - Implementa os modos `IMPLICIT` (valida `websites` via `isWebsiteAllowed`), `PASSWORD`, `CREDENTIAL` (valida `app_secret`), `LDAP` (**bloco inteiro comentado** — o `else if (AUTH_MODE_LDAP…)` casa mas não faz nada → `error` permanece null → "autentica" sem checar nada), `FACEBOOK`/`GOOGLE` (com `// TODO`, sem validação), default de expiração de **30 dias**, e auto-criação de player no login.
265
+ - **Nenhum endpoint chama esse método** (`grep` confirma: somente a versão `authenticate(playerId)` de 1 argumento é usada, em `StudioRest`). Portanto:
266
+ - **`websites` / `isWebsiteAllowed` não têm caminho vivo** (o único caller Java está dentro desse método órfão).
267
+ - **Facebook/Google (`AuthenticationFacebook`/`AuthenticationGoogle`) não são acionáveis** (instanciados só aqui).
268
+ - **LDAP não é acionável** e, mesmo se fosse, o bloco está comentado.
269
+
270
+ `AuthenticationManager.authenticate(String playerId)` (deprecated, 1 argumento) **é** usado por `StudioRest` para gerar um `access_token` opaco (shortGuid) gravado na coleção `authentication` — um mecanismo de sessão legado, distinto do JWT Bearer.
271
+
272
+ ---
102
273
 
103
- 1. Abrir `https://my.funifier.com`
104
- 2. Selecionar a gamificação desejada (botão "Select")
105
- 3. Se gamificação nova, clicar "Começar do Zero" para inicializar
106
- 4. Clicar no nome da gamificação na sidebar (link para `/studio/gamification/me`)
107
- 5. Expandir seção "Mais"
108
- 6. Clicar "Change your gamification security settings"
109
- 7. Na página Security: botão "Apps" expande a seção de Apps, botão "Roles" expande Roles
274
+ ## 3. Estrutura dos Objetos
110
275
 
111
- **⚠️ SPA Redirect:** Se a gamificação não foi inicializada (wizard "Escolha o melhor caminho"), qualquer navegação para `/studio/security` redireciona para o wizard. Primeiro clicar "Começar do Zero".
276
+ ### 3.1 `Security` documento raiz (`com.funifier.engine.security.Security`)
277
+
278
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
279
+ | --- | --- | --- | --- | --- |
280
+ | `_id` | String | — | por convenção = apiKey | Mapeado por `@JsonProperty("_id")`. `find()` ignora o `_id` (usa `findOne()`). |
281
+ | `created_at` | Date | — | — | Data de criação (não preenchida pelo `DatabaseRest`). |
282
+ | `updated_at` | Date | — | — | Data de atualização (não preenchida pelo `DatabaseRest`). |
283
+ | `requirePassword` | boolean | `false` | — | `true` → valida BCrypt no login `password`. `false` → senha ignorada. |
284
+ | `createPlayerIfDontExist` | boolean | `true` | — | `true` → cria player automaticamente (ver 5). Default de campo é `true`. |
285
+ | `timeZone` | String | `null` | — | Fuso Java (ex.: `America/Sao_Paulo`). Lido por Challenge/LastMile/Bonus/Achievement managers. |
286
+ | `inlineEditable` | boolean | `false` | — | Habilita edição inline no Studio (`FrontController.isInline`, `InlineManager`). |
287
+ | `appIdFacebook` | String | — | — | Config OAuth Facebook. **Sem consumidor vivo de auth** (ver 2.6/7). |
288
+ | `appSecretFacebook` | String | — | — | idem. |
289
+ | `appIdTwitter` | String | — | — | Config Twitter. **Nunca referenciado no código.** |
290
+ | `appSecretTwitter` | String | — | — | idem. |
291
+ | `appIdGoogle` | String | — | — | Config OAuth Google. **Sem consumidor vivo de auth.** |
292
+ | `appSecretGoogle` | String | — | — | idem. |
293
+ | `apps` | List\<Application\> | `[]` | — | Aplicações registradas (autenticação Basic / client_credentials). |
294
+ | `roles` | List\<Role\> | `[]` | — | Papéis de jogador (autenticação password). |
295
+ | `websites` | List\<String\> | `[]` | — | Origens para IMPLICIT auth. **Sem consumidor vivo** — só usado no método órfão (2.6). |
296
+ | `runInSilentMode` | boolean | `false` | — | **Persistido, sem consumidor em runtime.** |
297
+ | `baselines` | List\<BaseLine\> | `[]` | — | **Persistido, sem consumidor em runtime.** |
298
+ | `homepage` | String | — | — | Página inicial da gamification no Studio (campo `public`; sem getter; uso pelo frontend). |
299
+ | `ldap` | LDAPConfig | — | — | Config LDAP. **Auth LDAP desabilitada/órfã** (2.6). |
300
+
301
+ **Diferença schema × runtime:**
302
+
303
+ - Campos `runInSilentMode`, `baselines`, `websites`, `ldap`, `appId*/appSecret*` (social) e `homepage` são **aceitos no PUT e devolvidos no GET** (round-trip via Jackson). A imprecisão da documentação anterior era dizer que são "removidos silenciosamente" — eles **persistem**; o que falta é **consumidor em runtime** (são inertes para a lógica do servidor).
304
+ - `created_at`/`updated_at` não são preenchidos automaticamente pelo caminho de escrita (`DatabaseRest`), apenas persistidos se vierem no body.
305
+ - `Role` tem `@JsonIgnoreProperties(ignoreUnknown=true)`; `Security` e `Application` **não** têm essa anotação.
306
+
307
+ ### 3.2 `Application` — aplicação registrada (`Application.java`)
308
+
309
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
310
+ | --- | --- | --- | --- | --- |
311
+ | `name` | String | — | — | Nome descritivo. |
312
+ | `app_secret` | String | — | sim | Chave secreta (use um GUID, ex.: `/v3/util/guid/new`). |
313
+ | `scope` | String | — | — | Permissões separadas por vírgula. |
314
+ | `whitelist` | List\<String\> | `null` | — | URLs e IPs autorizados (verifica `Referer`/`Origin` e IP remoto). |
315
+
316
+ **Scopes (constantes literais de `Application.java`):**
317
+
318
+ | Constante | Valor | Significado |
319
+ | --- | --- | --- |
320
+ | `SCOPE_READ_ALL` | `read_all` | Leitura em todas as coleções. |
321
+ | `SCOPE_WRITE_ALL` | `write_all` | Escrita em todas as coleções. |
322
+ | `SCOPE_DELETE_ALL` | `delete_all` | Exclusão em todas as coleções. |
323
+ | `SCOPE_DATABASE` | `database` | **Obrigatório** para qualquer operação em `/v3/database`. |
324
+ | `SCOPE_READ_CRYPTED` | `read_encrypted_field_values` | Ler campos criptografados. |
325
+ | `SCOPE_READ_PLAYER_PASSWORD` | `read_encrypted_player_password` | Ler senha criptografada do player. |
326
+ | `SCOPE_CROSSDOMAIN` | `cross_domain` | Operações cross-account. |
327
+ | `SCOPE_WRITE_ACTIONLOG` | `write_actionlog` | `POST /v3/action/log` sem `write_all`. |
328
+ | `SCOPE_WRITE_UPLOAD` | `write_upload` | Upload sem `write_all`. |
329
+
330
+ > `write_own_actionlog` aparece **comentado** no código (`//public static final String SCOPE_WRITE_OWN_ACTIONLOG`) — não existe. Scopes hierárquicos (`{m}_{entidade}_all`) são suportados pela avaliação da seção 2.3 (até 2 níveis de fallback).
331
+
332
+ ### 3.3 `Role` — papel de jogador (`Role.java`, `@JsonIgnoreProperties(ignoreUnknown=true)`)
333
+
334
+ | Campo | Tipo | Padrão | Obrigatório | Descrição |
335
+ | --- | --- | --- | --- | --- |
336
+ | `name` | String | — | — | Identificador (ex.: `public`, `player`, `admin`). |
337
+ | `timeout` | String | — | — | Validade do token (ex.: `7d`, `1h`, `30m`). Omitir → `+7d`. **Nunca `""`** (gera erro em `DateUtil.fromKeyword("+")`). |
338
+ | `scope` | String | — | — | Permissões separadas por vírgula. |
339
+ | `whitelist` | List\<String\> | `null` | — | URLs/IPs. `null` em alguma role no merge → irrestrito (2.5). |
340
+
341
+ Roles com semântica especial no código:
342
+
343
+ - **`public`** — usada pelo `SecurityFilter`/`AuthBean` quando o Basic traz só `apiKey` (sem secret). Se não existir, `scope` fica `null` → **nenhuma verificação de scope** é feita (passa em tudo nessa camada).
344
+ - **`player`** — fallback de `findRoleCalculatedToPlayer` quando o principal não tem roles.
345
+
346
+ ### 3.4 `BaseLine` — janela temporal (`BaseLine.java`)
347
+
348
+ | Campo | Tipo | Descrição |
349
+ | --- | --- | --- |
350
+ | `start` | Date | Início. |
351
+ | `end` | Date | Fim. |
352
+
353
+ Campos package-private, sem getters, **sem consumidor em runtime**.
354
+
355
+ ### 3.5 `LDAPConfig` — configuração LDAP (`auth/ldap/LDAPConfig.java`)
356
+
357
+ | Campo | Tipo | Descrição |
358
+ | --- | --- | --- |
359
+ | `active` | boolean | Ativar LDAP. |
360
+ | `host` | String | Host do servidor. |
361
+ | `port` | String | Porta (ex.: `389`). |
362
+ | `dc` | String | Domain Component. |
363
+ | `ou` | String | Organizational Unit. |
364
+
365
+ **LDAP está desabilitado/órfão.** O bloco que usaria `LDAPConfig.isActive()` está comentado dentro do método `authenticate()` órfão. Definir `ldap.active=true` não tem efeito em nenhum fluxo acessível.
366
+
367
+ ### 3.6 Coleções relacionadas (separadas de `security`)
368
+
369
+ Configuradas/usadas no domínio de autenticação, mas em **coleções próprias** (registradas em `Entity`):
370
+
371
+ - **`auth_module`** (`AuthModule`) — módulos de autenticação **customizados em Groovy** (PAM stacking). Campos: `_id`, `name`, `script`, `relevance` (`required`/`requisite`/`sufficient`/`optional`), `order` (int), `created`, `updated`, `timeout` (Long, s). Gerenciados por `/v3/auth/module`. Consumidos por `/v3/auth/token` (grant `password`). Observação: a relevância `optional` é declarada mas **ignorada** em `authenticateModules` (só `required`/`requisite`/`sufficient` têm efeito).
372
+ - **`sso_saml`** (`SsoSaml`) — configuração SSO SAML 2.0 (SP/IdP, certificados, flags de assinatura). Endpoint público `/v3/sso/saml`.
373
+ - **`two_factor`** (`TwoFactor`) — configuração de 2FA.
374
+
375
+ ---
376
+
377
+ ## 4. Endpoints
378
+
379
+ > Toda a gestão de `security` usa o `DatabaseRest` genérico (`@Path("v3/database")`). Requer **scope contendo `database`** (verificado dentro do `DatabaseRest`) **e** aprovação prévia do `SecurityFilter` (tipicamente `read_all`/`write_all`/`delete_all`).
380
+
381
+ ### `GET /v3/database/security/{_id}`
382
+
383
+ | Aspecto | Detalhe |
384
+ | --- | --- |
385
+ | Finalidade | Ler o documento `security` por `_id`. |
386
+ | Implementação | `DatabaseRest.find()` |
387
+ | Autenticação | Bearer/Studio com scope `database` (+ `read_all`). |
388
+ | Pré-condições | `collection.length > 3`, `id.length > 0`. |
389
+
390
+ **Query params:**
391
+
392
+ | Param | Tipo | Descrição |
393
+ | --- | --- | --- |
394
+ | `strict` | boolean | `true` → saída em BSON strict mode. |
395
+
396
+ **Comportamento real:** `c.findOne("{_id:#}", id).as(HashMap.class)`. Se não autorizado/parâmetros inválidos → retorna `null` com `200 OK`. `{_id}` igual a `me` resolve para o player do token.
112
397
 
113
- ## Token Basic da Gamificação (público)
114
- Para acesso público sem App (signup, leitura):
115
398
  ```
116
- Basic base64(API_KEY + ":")
399
+ GET /v3/database/security/540f14b00ffeeb8c2fe11767?strict=true
400
+ Authorization: Bearer <token com scope database,read_all>
117
401
  ```
118
- Nota: dois-pontos no final, sem app_secret. Usa a role `public` se existir.
119
402
 
120
- ## Auth Token (`/v3/auth/token`)
403
+ ### `GET /v3/database/security?q={...}`
404
+
405
+ `DatabaseRest.findAll()` — agregação `{$match: {...}}`, paginada (`Range`, `max_results` default 100, `strict`). Útil para inspecionar/contar documentos `security` (detectar duplicatas).
406
+
407
+ ### `PUT /v3/database/security`
408
+
409
+ | Aspecto | Detalhe |
410
+ | --- | --- |
411
+ | Finalidade | Criar/atualizar o documento `security`. |
412
+ | Implementação | `DatabaseRest.update()` → `jongo.save(o)` |
413
+ | Full replace ou patch | **Replace do documento inteiro pelo `_id`** (campos omitidos somem). **NÃO** apaga outros documentos. |
414
+ | Side effects | Dispara triggers `BEFORE/AFTER_UPDATE` na coleção `security` + registro no Audit. |
415
+
416
+ > Se o body **não** tiver `_id`, o servidor gera um `Guid.newShortGuid()` → cria documento `security` avulso. Sempre envie `_id`.
121
417
 
122
- ### Request (JSON body)
123
418
  ```json
419
+ PUT /v3/database/security
420
+ Authorization: Bearer <token com scope database,write_all>
421
+ Content-Type: application/json
422
+
124
423
  {
125
- "grant_type": "password",
126
- "apiKey": "API_KEY",
127
- "username": "player_email",
128
- "password": "plain_text_password"
424
+ "_id": "540f14b00ffeeb8c2fe11767",
425
+ "requirePassword": true,
426
+ "createPlayerIfDontExist": true,
427
+ "timeZone": "America/Sao_Paulo",
428
+ "apps": [
429
+ { "name": "Backend App", "app_secret": "69ab3566607db81962b9686e", "scope": "read_all,write_all,delete_all,database" }
430
+ ],
431
+ "roles": [
432
+ { "name": "public", "scope": "read_all", "timeout": "7d" },
433
+ { "name": "player", "scope": "read_all,write_actionlog,write_upload", "timeout": "30d" }
434
+ ]
129
435
  }
130
436
  ```
131
437
 
132
- **⚠️ Campos obrigatórios:**
133
- - `grant_type` — DEVE ser `"password"` (sem isso, retorna `invalid_grant`)
134
- - `username` — NÃO `login` (campo frontend pode usar `login` mas API espera `username`)
135
- - A senha do Player no banco DEVE ser BCrypt hash (`$2a$...`). Senha em texto plano causa `"Invalid salt version"`
438
+ ### `POST /v3/database/security`
439
+
440
+ `DatabaseRest.insert()` — `insert(o)` (não upsert). Gera `_id` se ausente. Dispara triggers `BEFORE/AFTER_CREATE` + Audit. Em coleção singleton, prefira `PUT`.
441
+
442
+ ### `DELETE /v3/database/security?q={...}`
443
+
444
+ `DatabaseRest.delete()` — exige `q.length >= 3`. Coleta `_id`s afetados (se total ≤ 20.000), dispara triggers `BEFORE/AFTER_DELETE` + Audit, e remove por `{q}`. Apagar o documento `security` deixa `find()` retornando `new Security()` (defaults).
445
+
446
+ ### `POST /v3/auth/token`
447
+
448
+ | Aspecto | Detalhe |
449
+ | --- | --- |
450
+ | Finalidade | Emitir access_token Bearer. |
451
+ | Autenticação | Pública (path liberado no `SecurityFilter`). |
452
+ | Content-Type | `application/x-www-form-urlencoded` **ou** `application/json`. |
453
+
454
+ Grant types e expirações: ver seção 2.4. Exemplo `client_credentials` (form):
455
+
456
+ ```
457
+ POST /v3/auth/token
458
+ Content-Type: application/x-www-form-urlencoded
459
+
460
+ client_id=<apiKey>&client_secret=<app_secret>&grant_type=client_credentials
461
+ ```
462
+
463
+ Resposta:
136
464
 
137
- ### Response
138
465
  ```json
139
466
  {
140
- "access_token": "eyJ...",
467
+ "access_token": "eyJhbGciOiJIUzUxMiIsImNhbGciOiJHWklQIn0...",
468
+ "refresh_token": "eyJhbGciOiJIUzUxMiIs...",
141
469
  "token_type": "Bearer",
142
- "expires_in": "1772..."
470
+ "expires_in": "1747862400000"
143
471
  }
144
472
  ```
145
473
 
146
- ### Campos da Security que afetam auth
147
- | Campo | Tipo | Descrição |
148
- |-------|------|-----------|
149
- | `requirePassword` | boolean | Se `true`, valida senha BCrypt. **NÃO** é `passwordRequired` |
150
- | `createPlayerIfDontExist` | boolean | Se `true`, cria player automaticamente no login. **NÃO** é `autoCreatePlayer` |
474
+ `expires_in` = timestamp Unix **em milissegundos**.
475
+
476
+ ### `/v3/auth/module` (módulos Groovy)
477
+
478
+ | Método | Caminho | Função |
479
+ | --- | --- | --- |
480
+ | `GET` | `/v3/auth/module/{id}` | Buscar módulo por id. |
481
+ | `GET` | `/v3/auth/module` | Listar/filtrar (`q`, `published_min/max`, `orderby`, `reverse`, `max_results`). |
482
+ | `POST` | `/v3/auth/module` | Criar/atualizar — **compila** o script antes; se falhar retorna `status: ERROR`. |
483
+ | `DELETE` | `/v3/auth/module/{id}` | Excluir. |
484
+
485
+ ---
486
+
487
+ ## 5. Regras de Negócio
488
+
489
+ Regras que **estão no código mas não no schema**:
490
+
491
+ - **Scope `database` + `*_all` (camada dupla).** `/v3/database/*` precisa passar pelo `SecurityFilter` (`read_all`/`write_all`/`delete_all` ou hierárquicos) **e** pelo `DatabaseRest` (`scope` contém `database`). Token sem `database` recebe resposta vazia/`200` no `DatabaseRest`; sem `*_all` recebe `401` no filtro.
492
+ - **Basic público sem role `public`.** Se a role `public` não existir, `scope` fica `null` → o `SecurityFilter` **não** verifica scope → passa em todos os endpoints daquela camada.
493
+ - **`requirePassword` (com nuance).** Em `authenticatePassword(player, password)`: erro de senha só ocorre se `password != null && requirePassword && (user.password == null || !BCrypt.checkpw(password, user.password))`. Logo:
494
+ - `requirePassword=false` → senha ignorada (qualquer login passa).
495
+ - `requirePassword=true` **mas `password` ausente/null** no request → a condição curto-circuita e **passa sem checar senha**.
496
+ - `requirePassword=true` com senha em texto plano (não-BCrypt) → `BCrypt.checkpw` falha → "password incorrect for player".
497
+ - **`createPlayerIfDontExist` (caminho vivo = `createIfDontExist`).** Usado na **ingestão de action logs** (`ActionManager`/`ActionRest`) e em `PlayerRest`/`InGrupo`. Se `true` e o player não existe: verifica `Account.players_allowed`; se o limite foi atingido, **loga no console e NÃO cria** (sem exceção); senão cria o `Player`. (No `authenticatePassword`, o mesmo limite **lança** exceção.)
498
+ - **`isAppAllowed` exige `count == 1`.** `db.security.count({"apps.app_secret": <secret>}) == 1`. Se `0` → falso; se `≥ 2` (secret duplicado entre apps/documentos) → **também falso**. Combine com o risco de documentos `security` duplicados.
499
+ - **Reuso de token (player).** Antes de emitir, o fluxo busca uma `authentication` válida do player (`expires_in >= now`); se existir, **reusa** o mesmo access_token.
500
+ - **Singleton só por convenção.** `find()` faz `findOne()` sem filtro. Vários documentos `security` → resultado não determinístico.
501
+ - **`find()` nunca retorna null** → consumidores sempre recebem ao menos `new Security()` (defaults).
502
+ - **Triggers e Audit em escrita de `security`.** PUT/POST/DELETE em `security` disparam Triggers (Groovy) e gravam Audit como qualquer coleção. Surpreendente para um documento de configuração — uma trigger mal escrita em `security` afeta a configuração de segurança.
503
+
504
+ ---
505
+
506
+ ## 6. Comportamentos Automáticos
507
+
508
+ | Comportamento | Trigger | Impacto | Persistência |
509
+ | --- | --- | --- | --- |
510
+ | `_id` automático | PUT/POST em `security` sem `_id` | Cria documento com `Guid.newShortGuid()` | Sim (documento avulso) |
511
+ | Triggers `BEFORE/AFTER_CREATE/UPDATE/DELETE` | Qualquer escrita em `security` via `DatabaseRest` | Executa scripts Groovy registrados na coleção `security` | Depende da trigger |
512
+ | Audit log | Qualquer escrita em `security` | Registro em Audit (`EVENT_CREATE/UPDATE/DELETE`) | Sim |
513
+ | Criação automática de player | `createIfDontExist` (ingestão de action log, operações de player) com `createPlayerIfDontExist=true` | Cria `Player` se não existir e dentro de `players_allowed` | Sim |
514
+ | Reuso de access_token | `/v3/auth/token` (password) com sessão válida | Devolve token existente em vez de novo | Não (já existia) |
515
+ | `read_all`/limite diário | Cada request via `StatisticManager.newApiRequest` | Conta requisições; estoura limite → `401` | Em memória/estatística |
516
+ | Índice `apps.app_secret` | `ConnectionPool.createIndexes()` na inicialização | `createIndex({apps.app_secret: 1})` na coleção `security` | Sim (índice Mongo) |
517
+
518
+ #### Efeitos colaterais de uma escrita em `security`
519
+
520
+ ```mermaid
521
+ flowchart LR
522
+ A[PUT/POST /v3/database/security] --> B{tem _id?}
523
+ B -- nao --> C[gera shortGuid]
524
+ B -- sim --> D[Trigger BEFORE_*]
525
+ C --> D
526
+ D --> E[jongo.save / insert]
527
+ E --> F[Trigger AFTER_*]
528
+ F --> G[Audit.log]
529
+ ```
530
+
531
+ ---
532
+
533
+ ## 7. Suportado vs NÃO Suportado
534
+
535
+ ### ✅ Suportado (caminho vivo no código)
536
+
537
+ - Autorização por `SecurityFilter` em toda req `/v3/**` (Basic, Account, Studio, Bearer).
538
+ - Basic com `app_secret` (validação `isAppAllowed`, `count == 1`).
539
+ - Basic público (`apiKey:` sem secret) → role `public`.
540
+ - Bearer JWT (HS512 + GZIP) com claims `apiKey`, `appSecret`, `player`, `scope`, `account`, `user`, `whitelist`.
541
+ - Studio token (lib auth0 `JWTVerifier`) e Account token.
542
+ - `/v3/auth/token`: `client_credentials`, `password`, `refresh_token`.
543
+ - Scope hierárquico (`{m}_{path}_all`, até 2 níveis de fallback) + exceções `write_actionlog`/`/v3/mobile/device`.
544
+ - Whitelist por IP/URL (App e Role) via `startsWith`.
545
+ - Merge de múltiplas roles e role individual por id de player.
546
+ - `requirePassword` (BCrypt) e `createPlayerIfDontExist` (via `createIfDontExist`).
547
+ - `timeZone` (challenges/bônus/conquistas) e `inlineEditable` (edição inline).
548
+ - Módulos de autenticação customizados Groovy (`/v3/auth/module`, PAM stacking) — relevâncias `required`/`requisite`/`sufficient`.
549
+ - Índice MongoDB em `apps.app_secret` criado na inicialização.
550
+ - SSO SAML (`/v3/sso/saml`, coleção `sso_saml`) e 2FA (coleção `two_factor`) — features próprias.
551
+
552
+ ### ❌ NÃO Suportado / inerte / órfão
553
+
554
+ - **`AuthenticationManager.authenticate(auth_mode, …)` (9 args)** — não chamado por nenhum endpoint. Logo, **estão órfãos**:
555
+ - **Auth LDAP** — bloco comentado **e** método órfão; `LDAPConfig.active=true` sem efeito.
556
+ - **Auth Facebook/Google** — `AuthenticationFacebook`/`AuthenticationGoogle` instanciados só no método órfão; o ramo de auth tem `// TODO`.
557
+ - **Validação de `websites` (IMPLICIT)** — `isWebsiteAllowed` só é chamado dentro do método órfão; **não há caminho vivo** que valide `websites`.
558
+ - **Default de expiração de 30 dias** — existe só no método órfão.
559
+ - **Twitter** — `appIdTwitter`/`appSecretTwitter` nunca referenciados em lugar nenhum.
560
+ - **`runInSilentMode`, `baselines`** — persistidos e devolvidos, **sem consumidor em runtime**.
561
+ - **`homepage`** — persistido (uso pelo frontend Studio); sem consumidor server-side de auth.
562
+ - **`SecurityRest` dedicado** — não existe; gestão via `DatabaseRest`.
563
+ - **Full-replace de coleção** — `SecurityDaoMongo.update()`/`SecurityManager.update()` (remove+save) são **código morto no caminho Java** (escrita real = `save()` por `_id`). Só seriam executados se um script Groovy os chamasse explicitamente (ver 2.1).
564
+ - **PATCH/merge parcial** — PUT substitui o documento inteiro pelo `_id` (não faz merge).
565
+ - **Unicidade do documento** — não imposta pelo código.
566
+ - **Rotação/revogação de token** — JWT não é rastreado no banco; sem blacklist; expira só por prazo.
567
+ - **`expires_in` em segundos (OAuth2)** — retorna timestamp Unix em milissegundos.
568
+ - **Relevância `optional` de auth module** — declarada mas ignorada em `authenticateModules`.
569
+
570
+ ---
571
+
572
+ ## 8. Segurança e Permissões
573
+
574
+ - **Isolamento por gamificação:** cada gamificação tem seu próprio banco; a coleção `security` é escopada via `manager.getJongoConnection()` (por apiKey).
575
+ - **Chave JWT hardcoded e global:** `TokenUtil.SECRET_KEY = "d61de14ad05f48c5283bad0f5924071d87766219"` — fixa, HS512 + GZIP, **compartilhada por todas as gamificações**. A separação entre gamificações depende da validação do claim `apiKey` contra o endpoint no `SecurityFilter`, não da chave de assinatura. Studio usa a mesma chave via auth0 `JWTVerifier`.
576
+ - **Refresh token de 1 ano** sem acesso (`generateRefreshToken`), reidratável em access tokens — janela longa de re-emissão.
577
+ - **Whitelist por `startsWith`, sem CIDR/wildcard:** `isRequestWhiteListed` testa `url.startsWith(ref) || ip.startsWith(ref)`. IP é lido de `X-Forwarded-For`, `Proxy-Client-IP`, `WL-Proxy-Client-IP`, `HTTP_CLIENT_IP`, `HTTP_X_FORWARDED_FOR`, `RemoteAddr` (nessa ordem), **sem sanitização** — spoofável e sujeito a match parcial (ex.: `"10.0.0."` casa `10.0.0.7` e `10.0.0.99`). Se não houver `Referer`/`Origin` e a whitelist estiver setada, `url` é `null` → `NPE` → vira `401`.
578
+ - **Scope sem sanitização forte:** `scope.replaceAll(" ", "")` remove espaços e compara via `indexOf`/`contains`. Scope malformado pode gerar falso negativo (negar acesso), não falso positivo.
579
+ - **Senha do player:** só validada se `requirePassword=true`. Default `false` → qualquer senha aceita. Mesmo com `true`, request sem `password` passa (seção 5).
580
+ - **Triggers em `security`:** escritas disparam Groovy — superfície para lógica não-intencional sobre a própria config de segurança.
581
+ - **`/v3/auth/token` público:** força bruta de `client_secret`/senha não é bloqueada pela camada de filtro (só pelo limite diário de API agregado por gamificação).
582
+
583
+ ---
584
+
585
+ ## 9. Observabilidade e Troubleshooting
586
+
587
+ ### Verificar a configuração atual
588
+
589
+ ```
590
+ GET /v3/database/security/<apiKey>?strict=true
591
+ Authorization: Bearer <token com scope database,read_all>
592
+ ```
593
+
594
+ ### Detectar documentos `security` duplicados (causa de comportamento errático)
595
+
596
+ ```
597
+ GET /v3/database/security/count # via DatabaseRest /count?collection=security
598
+ # ou
599
+ POST /v3/database/security/aggregate
600
+ [ { "$group": { "_id": null, "total": { "$sum": 1 } } } ]
601
+ ```
602
+
603
+ Espera-se `total = 1`. `≥ 2` explica `isAppAllowed` falhando (exige `count == 1`) e `find()` não determinístico.
604
+
605
+ ### Verificar app_secret cadastrado
606
+
607
+ ```
608
+ GET /v3/database/security/<apiKey>?strict=true
609
+ ```
610
+
611
+ Inspecione `apps[].app_secret`. Lembre: `isAppAllowed` exige **exatamente um** documento com aquele secret.
612
+
613
+ ### Testar emissão de token
614
+
615
+ ```
616
+ POST /v3/auth/token
617
+ Content-Type: application/x-www-form-urlencoded
618
+
619
+ client_id=<apiKey>&client_secret=<app_secret>&grant_type=client_credentials
620
+ ```
621
+
622
+ ### Auditoria de mudanças em `security`
623
+
624
+ Escritas geram Audit (`EVENT_CREATE/UPDATE/DELETE`) — consulte a coleção de auditoria para "quem mudou a security e quando".
625
+
626
+ ### Erros comuns
627
+
628
+ | Erro | Causa provável |
629
+ | --- | --- |
630
+ | `api_key or app_secret is not valid or app_secret is not enabled in Funifier Studio.` | Basic com secret não cadastrado em `apps`, ou secret duplicado (`count != 1`). |
631
+ | `Need to inform a type of authentication. E.g. Basic, Studio or Bearer.` | Header `Authorization` ausente/prefixo desconhecido. |
632
+ | `Authorization token bearer is not valid.` | Bearer expirado/malformado/assinado com chave errada (sem `apiKey` e sem `account`). |
633
+ | `Studio token is invalid.` | Studio token inválido (`getValueStudio` retornou null). |
634
+ | `Domain or IP is not authorized. Ask the admin to add this in the gamification whitelist.` | App/Role com `whitelist` e origem/IP fora dela (ou ausência de `Referer`/`Origin`). |
635
+ | `You have exceeded your gamification daily api requests limits` | Limite diário da Account atingido. |
636
+ | `You dont have permission to {m} in {entity} endpoint, you need {m}_{entity_full} or {m}_all access to do it` | Scope sem `*_all`/`*_{path}` adequado. |
637
+ | Resposta vazia (`null`/`200`) em `/v3/database/security` | Scope sem `database`, ou `collection.length <= 3`/`id` ausente. |
638
+ | `auth_module_failed` | Auth module Groovy retornou status 2 (falha) com output. |
639
+ | `password incorrect for player` | `requirePassword=true` e BCrypt não confere (ou senha não-BCrypt). |
640
+ | NPE/erro ao gerar token de player | `role.timeout = ""` (use `null`/omita), ou role resolvida `null` no overload com whitelist. |
641
+
642
+ ---
643
+
644
+ ## 10. Exemplos Práticos
645
+
646
+ ### Exemplo mínimo — um app de servidor + role player
151
647
 
152
- ### ⚠️ Role timeout NUNCA string vazia
153
648
  ```json
154
- // ❌ ERRADO — causa NPE no auth (DateUtil.fromKeyword("+") falha)
155
- { "name": "player", "scope": "...", "timeout": "" }
649
+ PUT /v3/database/security
650
+ Authorization: Bearer <token com scope database,write_all>
651
+ Content-Type: application/json
156
652
 
157
- // ✅ CORRETO — omitir campo para timeout padrão (7 dias)
158
- { "name": "player", "scope": "..." }
653
+ {
654
+ "_id": "540f14b00ffeeb8c2fe11767",
655
+ "createPlayerIfDontExist": true,
656
+ "requirePassword": false,
657
+ "apps": [
658
+ { "name": "Server App", "app_secret": "a1b2c3d4e5f6a7b8c9d0e1f2", "scope": "read_all,write_all,delete_all,database" }
659
+ ],
660
+ "roles": [
661
+ { "name": "player", "scope": "read_all,write_actionlog,write_upload", "timeout": "7d" }
662
+ ]
663
+ }
664
+ ```
159
665
 
160
- // CORRETOvalor explícito
161
- { "name": "player", "scope": "...", "timeout": "7d" }
666
+ ### Exemplo avançadoroles + whitelist + timezone + senha obrigatória
667
+
668
+ ```json
669
+ {
670
+ "_id": "540f14b00ffeeb8c2fe11767",
671
+ "createPlayerIfDontExist": true,
672
+ "requirePassword": true,
673
+ "timeZone": "America/Sao_Paulo",
674
+ "inlineEditable": false,
675
+ "apps": [
676
+ {
677
+ "name": "Backend Server",
678
+ "app_secret": "a1b2c3d4e5f6a7b8c9d0e1f2",
679
+ "scope": "read_all,write_all,delete_all,database",
680
+ "whitelist": ["10.0.0.", "192.168.1."]
681
+ },
682
+ {
683
+ "name": "Frontend Read Only",
684
+ "app_secret": "f9e8d7c6b5a4f3e2d1c0b9a8",
685
+ "scope": "read_all,write_actionlog",
686
+ "whitelist": ["https://app.empresa.com"]
687
+ }
688
+ ],
689
+ "roles": [
690
+ { "name": "public", "scope": "read_all", "timeout": "1d" },
691
+ { "name": "player", "scope": "read_all,write_actionlog,write_upload", "timeout": "30d" },
692
+ { "name": "admin", "scope": "read_all,write_all,delete_all,database", "timeout": "8h" }
693
+ ]
694
+ }
695
+ ```
696
+
697
+ > A `whitelist` usa `startsWith` (sem CIDR). `"10.0.0."` cobre toda a faixa `10.0.0.x` por prefixo de string — confirme que isso é o pretendido.
698
+
699
+ ### Anti-pattern — o que NÃO fazer
700
+
701
+ ```json
702
+ {
703
+ "roles": [
704
+ { "name": "player", "scope": "read_all,write_all", "timeout": "" }
705
+ ]
706
+ }
162
707
  ```
163
708
 
164
- ## Validações e Testes
165
- - [ ] Role `public` criada com scope `read_all`
166
- - [ ] Role `player` criada com scope incluindo `database`
167
- - [ ] App criada com secret gerado
168
- - [ ] Token Basic do App acessa `/v3/database/collections`
169
- - [ ] Token Basic público (sem app) acessa endpoints de leitura
709
+ Problemas:
710
+
711
+ 1. **Sem `_id`** o servidor gera um `Guid.newShortGuid()` e cria um **segundo** documento `security`. Como `find()` faz `findOne()` sem filtro, ele pode passar a "vencer" sobre o real, e `isAppAllowed` (que exige `count == 1`) pode quebrar.
712
+ 2. **`timeout: ""`** `DateUtil.fromKeyword("+")` erro/NPE na emissão do token do player.
713
+ 3. **PUT sem `apps`** (omitindo) como o PUT substitui o documento inteiro pelo `_id`, **todos os apps somem** daquele documento. Sempre reenvie o documento completo.
714
+ 4. **Sem scope `database` em nenhum app/role** `/v3/database` retorna vazio.
715
+
716
+ ---
717
+
718
+ ## Checklist de Configuração
719
+
720
+ - [ ] `_id` preenchido com a API Key da gamificação (nunca deixar o servidor gerar).
721
+ - [ ] Exatamente **um** documento na coleção `security` (cheque com `aggregate $group/count`).
722
+ - [ ] Pelo menos um `app` com `app_secret` único (GUID) — secret duplicado quebra `isAppAllowed` (`count == 1`).
723
+ - [ ] Scope dos apps inclui `database` **e** `read_all`/`write_all`/`delete_all` para usar `/v3/database`.
724
+ - [ ] Role `player` com `timeout` não-vazio e diferente de `""` (use `null`/omita ou `7d`).
725
+ - [ ] Role `public` definida se acesso Basic público for necessário (senão Basic público passa sem scope).
726
+ - [ ] `timeZone` preenchido se challenges/bônus por hora/dia forem usados.
727
+ - [ ] `requirePassword` consciente: `false` (default) = sem validação de senha; `true` ainda passa se o request não enviar `password`.
728
+ - [ ] PUT sempre com o documento completo (campos omitidos são apagados daquele `_id`).
729
+ - [ ] `whitelist` com prefixos corretos (lembre: `startsWith`, sem CIDR/wildcard).
730
+ - [ ] Não confiar em LDAP/Facebook/Google/Twitter/`websites`/`baselines`/`runInSilentMode` — inertes ou órfãos no código atual.
731
+ - [ ] Triggers cadastradas na coleção `security` revisadas (escritas disparam Groovy + Audit).