agent-engineering-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +249 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.js +223 -0
- package/docs/agent-integration.md +200 -0
- package/docs/philosophy.md +131 -0
- package/docs/reference-authoring.md +117 -0
- package/docs/skill-authoring.md +126 -0
- package/examples/authorization-bypass.md +191 -0
- package/examples/frontend-review.md +244 -0
- package/examples/race-condition.md +128 -0
- package/examples/xp-reward-loop.md +123 -0
- package/package.json +45 -0
- package/references/engineering.yaml +88 -0
- package/references/frontend.yaml +139 -0
- package/references/product.yaml +54 -0
- package/references/research.yaml +88 -0
- package/references/security.yaml +85 -0
- package/references/ux.yaml +37 -0
- package/scripts/validate.py +454 -0
- package/skills/audit/adversarial-review/SKILL.md +190 -0
- package/skills/audit/business-logic-audit/SKILL.md +182 -0
- package/skills/audit/edge-case-hunter/SKILL.md +159 -0
- package/skills/audit/error-flow-audit/SKILL.md +184 -0
- package/skills/audit/state-consistency-audit/SKILL.md +174 -0
- package/skills/audit/user-flow-audit/SKILL.md +161 -0
- package/skills/frontend/accessibility-review/SKILL.md +186 -0
- package/skills/frontend/animation-review/SKILL.md +171 -0
- package/skills/frontend/interaction-design/SKILL.md +162 -0
- package/skills/frontend/ux-review/SKILL.md +172 -0
- package/skills/frontend/visual-quality-review/SKILL.md +160 -0
- package/skills/meta/research-router/SKILL.md +184 -0
- package/skills/meta/skill-router/SKILL.md +206 -0
- package/skills/product/gamification-audit/SKILL.md +213 -0
- package/skills/reliability/data-integrity-audit/SKILL.md +187 -0
- package/skills/reliability/idempotency-audit/SKILL.md +191 -0
- package/skills/reliability/race-condition-hunter/SKILL.md +181 -0
- package/skills/research/github-reference-research/SKILL.md +197 -0
- package/skills/research/implementation-research/SKILL.md +181 -0
- package/skills/research/market-research/SKILL.md +202 -0
- package/skills/research/reference-research/SKILL.md +186 -0
- package/skills/security/api-abuse-audit/SKILL.md +178 -0
- package/skills/security/authorization-audit/SKILL.md +176 -0
- package/skills/security/input-trust-audit/SKILL.md +178 -0
- package/templates/audit-report.md +89 -0
- package/templates/bug-report.md +107 -0
- package/templates/design-review.md +122 -0
- package/templates/research-report.md +96 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Example — Authorization Bypass (IDOR + Vertical Escalation)
|
|
2
|
+
|
|
3
|
+
*Demonstração concreta de uma auditoria de autorização. Ilustra:
|
|
4
|
+
`authorization-audit` + `api-abuse-audit` + `input-trust-audit`. Modelo: autenticado ≠
|
|
5
|
+
autorizado (ver `plan.md` §20).*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Target
|
|
10
|
+
|
|
11
|
+
Um sistema com recursos de usuário (pedidos, documentos, perfil) e endpoints de
|
|
12
|
+
moderação (banir usuário, deletar post). Os endpoints exigem autenticação, mas a
|
|
13
|
+
autorização (ownership / role) é inconsistente.
|
|
14
|
+
|
|
15
|
+
## Hypotheses
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
GET /resource/123
|
|
19
|
+
|
|
20
|
+
authenticated ≠ authorized
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **IDOR (horizontal):** usuário A acessa pedido de usuário B apenas trocando o ID.
|
|
24
|
+
- **Vertical escalation:** usuário comum chama endpoint de admin/moderator.
|
|
25
|
+
- **Role from client:** o payload do request contém `role` que o servidor confia.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
## Finding 1 — IDOR: GET /order/{id}
|
|
29
|
+
|
|
30
|
+
### Reproduction
|
|
31
|
+
|
|
32
|
+
```http
|
|
33
|
+
POST /auth/login {email: "userA@example.com", password: "..."}
|
|
34
|
+
→ token: eyJ...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```http
|
|
38
|
+
GET /api/orders/1000 (pedido do userA)
|
|
39
|
+
Authorization: Bearer eyJ...
|
|
40
|
+
→ 200 { id: 1000, userId: "userA", items: [...], total: 250 }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```http
|
|
44
|
+
GET /api/orders/1001 (tentativa — pedido do userB)
|
|
45
|
+
Authorization: Bearer eyJ...
|
|
46
|
+
→ 200 { id: 1001, userId: "userB", items: [...], total: 500 }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Resultado:** servidor retornou o pedido `1001` de outro usuário. A única checagem é
|
|
50
|
+
autenticação (token válido). **Não há checagem de ownership.**
|
|
51
|
+
|
|
52
|
+
### Root cause
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// router.get('/api/orders/:id', auth, async (req, res) => {
|
|
56
|
+
// const order = await Order.findById(req.params.id); // ← sem .where({ userId: ... })
|
|
57
|
+
// res.json(order);
|
|
58
|
+
// });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
O middleware `auth` só valida o token (quem é). O handler não adiciona
|
|
62
|
+
`req.user.id` ao filtro.
|
|
63
|
+
|
|
64
|
+
### Recommendation
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
router.get('/api/orders/:id', auth, async (req, res) => {
|
|
68
|
+
const order = await Order.findOne({
|
|
69
|
+
_id: req.params.id,
|
|
70
|
+
userId: req.user.id, // ← ownership check
|
|
71
|
+
});
|
|
72
|
+
if (!order) return res.status(403).json({ error: 'Forbidden' });
|
|
73
|
+
res.json(order);
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
## Finding 2 — Vertical escalation: POST /admin/ban
|
|
79
|
+
|
|
80
|
+
### Reproduction
|
|
81
|
+
|
|
82
|
+
```http
|
|
83
|
+
POST /api/admin/ban
|
|
84
|
+
Authorization: Bearer eyJ... (token de user comum)
|
|
85
|
+
Content-Type: application/json
|
|
86
|
+
|
|
87
|
+
{ "userId": "userC", "reason": "spam" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```http
|
|
91
|
+
→ 200 { ok: true }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Resultado:** usuário comum banou outro usuário. Nenhuma checagem de `role` no
|
|
95
|
+
handler.
|
|
96
|
+
|
|
97
|
+
### Root cause
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
// router.post('/api/admin/ban', auth, async (req, res) => {
|
|
101
|
+
// await User.updateOne({ _id: req.body.userId }, { banned: true });
|
|
102
|
+
// res.json({ ok: true });
|
|
103
|
+
// });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
O middleware `auth` só valida o token. O prefixo `/admin` na rota é só convenção de
|
|
107
|
+
nomenclatura — não há middleware de RBAC separado.
|
|
108
|
+
|
|
109
|
+
### Recommendation
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
// middleware de role
|
|
113
|
+
const requireRole = (...roles) => (req, res, next) => {
|
|
114
|
+
if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'Forbidden' });
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
router.post('/api/admin/ban', auth, requireRole('admin'), async (req, res) => {
|
|
119
|
+
// ...
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
## Finding 3 — Role from client (mass assignment)
|
|
125
|
+
|
|
126
|
+
### Reproduction
|
|
127
|
+
|
|
128
|
+
```http
|
|
129
|
+
PUT /api/profile
|
|
130
|
+
Authorization: Bearer eyJ...
|
|
131
|
+
Content-Type: application/json
|
|
132
|
+
|
|
133
|
+
{ "bio": "new bio", "role": "admin" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```http
|
|
137
|
+
→ 200 { bio: "new bio", role: "admin" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Resultado:** o campo `role` foi aceito do body e gravado.
|
|
141
|
+
|
|
142
|
+
### Root cause
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
// router.put('/api/profile', auth, async (req, res) => {
|
|
146
|
+
// const user = await User.findByIdAndUpdate(req.user.id, req.body, { new: true });
|
|
147
|
+
// res.json(user);
|
|
148
|
+
// });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
O bind de `req.body` é genérico. O modelo `User` tem o campo `role`, então o `$set`
|
|
152
|
+
aceita qualquer valor.
|
|
153
|
+
|
|
154
|
+
### Recommendation
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
// Allowlist de campos atualizáveis
|
|
158
|
+
const ALLOWED_FIELDS = ['bio', 'displayName', 'avatar'];
|
|
159
|
+
|
|
160
|
+
router.put('/api/profile', auth, async (req, res) => {
|
|
161
|
+
const updates = {};
|
|
162
|
+
for (const field of ALLOWED_FIELDS) {
|
|
163
|
+
if (req.body[field] !== undefined) updates[field] = req.body[field];
|
|
164
|
+
}
|
|
165
|
+
const user = await User.findByIdAndUpdate(req.user.id, updates, { new: true });
|
|
166
|
+
res.json(user);
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
## Summary
|
|
172
|
+
|
|
173
|
+
| # | Finding | Type | Severity | Confidence |
|
|
174
|
+
|---|---|---|---|---|
|
|
175
|
+
| 1 | IDOR horizontal — `GET /order/{id}` sem ownership check | authorization | Critical | CONFIRMED |
|
|
176
|
+
| 2 | Vertical escalation — `POST /admin/ban` sem role check | authorization | Critical | CONFIRMED |
|
|
177
|
+
| 3 | Mass assignment — `role` aceito do body | input-trust | High | CONFIRMED |
|
|
178
|
+
|
|
179
|
+
## Matriz de autorização
|
|
180
|
+
|
|
181
|
+
| Recurso × Ação | Papel exigido | Onde imposto | ✓/✗ |
|
|
182
|
+
|---|---|---|---|
|
|
183
|
+
| order × read | owner | middleware `auth` só | ✗ |
|
|
184
|
+
| order × delete | owner | middleware `auth` só | ✗ |
|
|
185
|
+
| order × update | owner | middleware `auth` só | ✗ |
|
|
186
|
+
| /admin/ban | admin | handler não checa | ✗ |
|
|
187
|
+
| /admin/delete-post | moderator/admin | handler não checa | ✗ |
|
|
188
|
+
| profile × update | owner | handler não checa role | ✓ (userId do token) |
|
|
189
|
+
| profile × role | N/A (nunca do body) | `req.body` direto → | ✗ |
|
|
190
|
+
|
|
191
|
+
*Ver skills: `authorization-audit`, `api-abuse-audit`, `input-trust-audit`.*
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Example — Frontend Review of a Character Creation Screen
|
|
2
|
+
|
|
3
|
+
*Demonstração concreta de uma revisão de frontend integrada. Ilustra: `ux-review` +
|
|
4
|
+
`visual-quality-review` + `interaction-design` + `animation-review` +
|
|
5
|
+
`accessibility-review` + `reference-research` + `market-research` (ver `plan.md`
|
|
6
|
+
§20). Usa o template `design-review.md`.*
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Design Review — Character Creation Screen
|
|
11
|
+
|
|
12
|
+
**Date:** 2026-08-28
|
|
13
|
+
**Target:** Screen "Criação de Personagem" (multi-step wizard: class → attributes →
|
|
14
|
+
appearance → confirm)
|
|
15
|
+
**Skills used:** ux-review, visual-quality-review, interaction-design, animation-review,
|
|
16
|
+
accessibility-review, reference-research, market-research
|
|
17
|
+
**References consulted:** Laws of UX (methodology), Impeccable (heuristic), Interfaces
|
|
18
|
+
(heuristic), Dribbble (inspiration)
|
|
19
|
+
|
|
20
|
+
## Executive summary
|
|
21
|
+
|
|
22
|
+
O fluxo de 4 passos funciona, mas tem três problemas sérios: (1) não há feedback de
|
|
23
|
+
progresso nem caminho de volta claro no passo 3 (estado de confusão); (2) contraste
|
|
24
|
+
insuficiente em texto de helper e estado focado removido — falha WCAG; (3) animação de
|
|
25
|
+
transição entre passos é lenta (500ms) e não respeita reduced motion. Correção
|
|
26
|
+
prioritária: acessibilidade e feedback.
|
|
27
|
+
|
|
28
|
+
## Verdict
|
|
29
|
+
|
|
30
|
+
**Fix first**
|
|
31
|
+
|
|
32
|
+
## Dimensions
|
|
33
|
+
|
|
34
|
+
| Dimension | Verdict | Findings |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| UX | ⚠️ | #1, #2, #3 |
|
|
37
|
+
| Visual | ✅ | — |
|
|
38
|
+
| Interaction | ⚠️ | #4, #5 |
|
|
39
|
+
| Animation | ⚠️ | #6 |
|
|
40
|
+
| Accessibility | ❌ | #7, #8 |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Findings
|
|
45
|
+
|
|
46
|
+
### Finding 1 — Sem feedback de progresso
|
|
47
|
+
|
|
48
|
+
| Field | Value |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Severity** | Medium |
|
|
51
|
+
| **Confidence** | CONFIRMED |
|
|
52
|
+
| **Dimension** | UX |
|
|
53
|
+
| **Affected component** | Wizard — passo 1-4 |
|
|
54
|
+
| **Affected flow** | criação de personagem |
|
|
55
|
+
| **Reproduction** | No passo 2, não há indicação de "passo X de 4"; usuário não sabe se vai continuar |
|
|
56
|
+
| **Expected behavior** | Barra de progresso / stepper visível com estado atual |
|
|
57
|
+
| **Actual behavior** | Apenas título do passo, sem contexto da posição no fluxo |
|
|
58
|
+
| **Root cause** | Layout não inclui elemento de progresso (viola princípio de feedback/estado visível — Laws of UX, "Feedback") |
|
|
59
|
+
| **Impact** | Usuário abandona ou clica "next" sem entender a jornada; retorno ao passo anterior confuso |
|
|
60
|
+
| **Recommendation** | Adicionar stepper de 4 etapas com estado atual destacado |
|
|
61
|
+
|
|
62
|
+
### Finding 2 — Empty state do atributo não orienta
|
|
63
|
+
|
|
64
|
+
| Field | Value |
|
|
65
|
+
|---|---|
|
|
66
|
+
| **Severity** | Medium |
|
|
67
|
+
| **Confidence** | CONFIRMED |
|
|
68
|
+
| **Dimension** | UX |
|
|
69
|
+
| **Affected component** | Passo 2 (atributos) — quando nenhum ponto distribuído |
|
|
70
|
+
| **Reproduction** | 0 pontos distribuídos → área vazia sem instrução |
|
|
71
|
+
| **Expected behavior** | Estado vazio guiado ("Distribua seus 10 pontos") |
|
|
72
|
+
| **Actual behavior** | Área em branco |
|
|
73
|
+
| **Root cause** | Empty state não implementado (princípio de empty states) |
|
|
74
|
+
| **Impact** | Usuário não sabe o que fazer a seguir |
|
|
75
|
+
| **Recommendation** | Adicionar empty state com instrução + CTA |
|
|
76
|
+
|
|
77
|
+
### Finding 3 — Caminho de volta inconsistente
|
|
78
|
+
|
|
79
|
+
| Field | Value |
|
|
80
|
+
|---|---|
|
|
81
|
+
| **Severity** | Low |
|
|
82
|
+
| **Confidence** | HIGH CONFIDENCE |
|
|
83
|
+
| **Dimension** | UX |
|
|
84
|
+
| **Affected component** | Passo 3 (appearance) — sem "back" |
|
|
85
|
+
| **Reproduction** | No passo 3, não há botão voltar (presente nos outros passos) |
|
|
86
|
+
| **Expected behavior** | Navegação consistente entre passos |
|
|
87
|
+
| **Actual behavior** | Passo 3 órfão — usuário precisa fechar e recomeçar |
|
|
88
|
+
| **Root cause** | Botão back omitido no passo 3 (consistência de navegação violada) |
|
|
89
|
+
| **Impact** | Usuário preso em um estado intermediário |
|
|
90
|
+
| **Recommendation** | Adicionar back button consistente em todos os passos |
|
|
91
|
+
|
|
92
|
+
### Finding 4 — Pressed state ausente
|
|
93
|
+
|
|
94
|
+
| Field | Value |
|
|
95
|
+
|---|---|
|
|
96
|
+
| **Severity** | Low |
|
|
97
|
+
| **Confidence** | HIGH CONFIDENCE |
|
|
98
|
+
| **Dimension** | Interaction |
|
|
99
|
+
| **Affected component** | Botão "Next" |
|
|
100
|
+
| **Reproduction** | Clicar e segurar no botão — nenhuma mudança visual |
|
|
101
|
+
| **Expected behavior** | Escurecer/efeito de pressionamento |
|
|
102
|
+
| **Actual behavior** | Nada muda até o release |
|
|
103
|
+
| **Root cause** | `:active` não estilizado |
|
|
104
|
+
| **Impact** | Usuário não sabe se o clique foi registrado |
|
|
105
|
+
| **Recommendation** | Estilizar `:active` (scale/color) |
|
|
106
|
+
|
|
107
|
+
### Finding 5 — Loading de "save" congelado
|
|
108
|
+
|
|
109
|
+
| Field | Value |
|
|
110
|
+
|---|---|
|
|
111
|
+
| **Severity** | Medium |
|
|
112
|
+
| **Confidence** | POSSIBLE |
|
|
113
|
+
| **Dimension** | Interaction |
|
|
114
|
+
| **Affected component** | Confirmação (passo 4) — salvar personagem |
|
|
115
|
+
| **Reproduction** | Ao salvar, tela congela ~2s sem feedback (verificar com rede lenta) |
|
|
116
|
+
| **Expected behavior** | Skeleton/spinner + disable do botão |
|
|
117
|
+
| **Actual behavior** | Nada acontece visualmente durante o request |
|
|
118
|
+
| **Root cause** | Estado de loading não implementado |
|
|
119
|
+
| **Impact** | Duplo-submit ou abandono |
|
|
120
|
+
| **Recommendation** | Loading state no botão de salvar |
|
|
121
|
+
|
|
122
|
+
### Finding 6 — Transição lenta + sem reduced motion
|
|
123
|
+
|
|
124
|
+
| Field | Value |
|
|
125
|
+
|---|---|
|
|
126
|
+
| **Severity** | Medium |
|
|
127
|
+
| **Confidence** | CONFIRMED |
|
|
128
|
+
| **Dimension** | Animation |
|
|
129
|
+
| **Affected component** | Transição entre passos do wizard |
|
|
130
|
+
| **Reproduction** | Transição de 500ms com fade — com `prefers-reduced-motion: reduce`, continua animando |
|
|
131
|
+
| **Expected behavior** | ≤ 200-300ms; desligada com reduced motion |
|
|
132
|
+
| **Actual behavior** | 500ms sempre |
|
|
133
|
+
| **Root cause** | Timing alto + ausência de `@media (prefers-reduced-motion)` |
|
|
134
|
+
| **Impact** | Sensação de lentidão; desconforto vestibular |
|
|
135
|
+
| **Recommendation** | Reduzir para 250ms + desligar/curtar em reduced motion |
|
|
136
|
+
|
|
137
|
+
### Finding 7 — Contraste insuficiente (helper text)
|
|
138
|
+
|
|
139
|
+
| Field | Value |
|
|
140
|
+
|---|---|
|
|
141
|
+
| **Severity** | High |
|
|
142
|
+
| **Confidence** | CONFIRMED |
|
|
143
|
+
| **Dimension** | Accessibility |
|
|
144
|
+
| **Affected component** | Texto de ajuda sob campos (`#9CA3AF` em fundo branco) |
|
|
145
|
+
| **Reproduction** | Inspeção: contraste calculado = **2.9:1** |
|
|
146
|
+
| **Expected behavior** | ≥ 4.5:1 (WCAG AA) |
|
|
147
|
+
| **Actual behavior** | 2.9:1 |
|
|
148
|
+
| **Root cause** | Cor de texto gray-400 em branco |
|
|
149
|
+
| **Impact** | Ilegível para baixa visão |
|
|
150
|
+
| **Recommendation** | Usar `#6B7280` (4.6:1) ou escurecer mais |
|
|
151
|
+
|
|
152
|
+
### Finding 8 — Focus ring removido
|
|
153
|
+
|
|
154
|
+
| Field | Value |
|
|
155
|
+
|---|---|
|
|
156
|
+
| **Severity** | High |
|
|
157
|
+
| **Confidence** | CONFIRMED |
|
|
158
|
+
| **Dimension** | Accessibility |
|
|
159
|
+
| **Affected component** | Todos os inputs e botões |
|
|
160
|
+
| **Reproduction** | `*:focus { outline: none }` sem substituto — Tab não mostra posição |
|
|
161
|
+
| **Expected behavior** | Focus ring visível (WCAG 2.4.7) |
|
|
162
|
+
| **Actual behavior** | Nenhuma indicação de foco |
|
|
163
|
+
| **Root cause** | `outline: none` global sem fallback |
|
|
164
|
+
| **Impact** | Usuários de teclado/leitores de tela perdem a posição |
|
|
165
|
+
| **Recommendation** | Remover `outline: none` global; adicionar focus ring visível |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Research synthesis
|
|
170
|
+
|
|
171
|
+
### Reference
|
|
172
|
+
[Impeccable]
|
|
173
|
+
|
|
174
|
+
### Relevant Pattern
|
|
175
|
+
Contraste de texto legível, sistema de spacing consistente, sem decoração sem função.
|
|
176
|
+
|
|
177
|
+
### Why It Matters
|
|
178
|
+
A screen atual usa cores de baixo contraste e espaçamento irregular; Impeccable é o
|
|
179
|
+
bar de referência.
|
|
180
|
+
|
|
181
|
+
### Adaptation
|
|
182
|
+
Aplicar o sistema de 4px no spacing e revisar a paleta de texto (≥ 4.5:1).
|
|
183
|
+
|
|
184
|
+
### Trade-offs
|
|
185
|
+
Requer revisão de todos os componentes visuais existentes.
|
|
186
|
+
|
|
187
|
+
### Recommendation
|
|
188
|
+
Adotar o padrão de contraste do Impeccable + remover animação decorativa.
|
|
189
|
+
|
|
190
|
+
### Reference 2
|
|
191
|
+
[Laws of UX — Feedback / State visibility]
|
|
192
|
+
|
|
193
|
+
### Relevant Pattern
|
|
194
|
+
"Feedback: o sistema deve sempre informar o estado atual." "Visibility of system
|
|
195
|
+
status."
|
|
196
|
+
|
|
197
|
+
### Why It Matters
|
|
198
|
+
As findings #1 e #5 são exatamente a violação deste princípio (sem progresso, sem
|
|
199
|
+
loading).
|
|
200
|
+
|
|
201
|
+
### Adaptation
|
|
202
|
+
Adicionar stepper + loading states ao wizard.
|
|
203
|
+
|
|
204
|
+
### Trade-offs
|
|
205
|
+
Nenhum — é padrão esperado.
|
|
206
|
+
|
|
207
|
+
### Recommendation
|
|
208
|
+
Adotar os princípios de feedback/estado visível no fluxo inteiro.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Accessibility checklist
|
|
213
|
+
|
|
214
|
+
| Check | Pass | Fail | N/A |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| Keyboard reachable | | ✗ (focus ring removido) | |
|
|
217
|
+
| Focus visible | | ✗ (#8) | |
|
|
218
|
+
| Focus trap correct in modals | | | (sem modal no fluxo) |
|
|
219
|
+
| Alt text on informative images | ✓ | | |
|
|
220
|
+
| aria-label on semantic icons | ✓ | | |
|
|
221
|
+
| aria-live on dynamic content | | ✗ (sem aria-live em erros) | |
|
|
222
|
+
| Semantic HTML | ✓ | | |
|
|
223
|
+
| Text contrast ≥ 4.5:1 | | ✗ (#7) | |
|
|
224
|
+
| Touch targets ≥ 44×44px | ✓ | | |
|
|
225
|
+
| `<label>` on every form input | ✓ | | |
|
|
226
|
+
| Errors text + aria-live | | ✗ (erro só por cor) | |
|
|
227
|
+
| `prefers-reduced-motion` respected | | ✗ (#6) | |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Out of scope
|
|
232
|
+
|
|
233
|
+
- Mobile layout (não avaliado neste device).
|
|
234
|
+
- Validação server-side do personagem (ver skills de backend).
|
|
235
|
+
- Performance de assets.
|
|
236
|
+
|
|
237
|
+
## Next steps
|
|
238
|
+
|
|
239
|
+
1. **Fix first:** #8 (focus ring), #7 (contraste) — acessibilidade bloqueante.
|
|
240
|
+
2. **Fix first:** #1 (stepper), #5 (loading) — feedback essencial.
|
|
241
|
+
3. **Ship-with-fixes:** #6 (reduced motion), #4, #3, #2.
|
|
242
|
+
|
|
243
|
+
*Ver skills: `ux-review`, `visual-quality-review`, `interaction-design`,
|
|
244
|
+
`animation-review`, `accessibility-review`, `reference-research`, `market-research`.*
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Example — Race Condition in Balance Checkout
|
|
2
|
+
|
|
3
|
+
*Demonstração concreta de uma auditoria de race condition. Ilustra:
|
|
4
|
+
`race-condition-hunter` + `business-logic-audit` + `idempotency-audit` +
|
|
5
|
+
`data-integrity-audit`. Modelo: READ → DECISION → WRITE (ver `plan.md` §20).*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Target
|
|
10
|
+
|
|
11
|
+
Um sistema de checkout que verifica o saldo do usuário antes de debitar:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
READ balance (≥ price?)
|
|
15
|
+
DECIDE yes → proceed
|
|
16
|
+
WRITE balance = balance - price
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Hypothesis
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
check balance
|
|
23
|
+
→ two requests
|
|
24
|
+
→ both pass
|
|
25
|
+
→ both deduct
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Dois requests simultâneos lêem o mesmo saldo, ambos decidem "ok", e ambos debitam —
|
|
29
|
+
resultando em saldo final menor que o permitido (ou negativo).
|
|
30
|
+
|
|
31
|
+
## Mapeamento da race
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// src/services/checkout.js
|
|
35
|
+
async function checkout(userId, price) {
|
|
36
|
+
const user = await User.findById(userId); // ← READ
|
|
37
|
+
if (user.balance < price) throw new Error('...'); // ← DECISION
|
|
38
|
+
user.balance -= price; // ← WRITE
|
|
39
|
+
await user.save();
|
|
40
|
+
await createOrder(userId, price);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Janela:** entre o `findById` (READ) e o `save` (WRITE), outro request pode executar
|
|
45
|
+
o mesmo bloco, ler o mesmo saldo, e aprovar.
|
|
46
|
+
|
|
47
|
+
## Reprodução
|
|
48
|
+
|
|
49
|
+
Simular dois requests simultâneos contra `GET /checkout`:
|
|
50
|
+
|
|
51
|
+
| Request | Time | Ação |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| — | t0 | Saldo inicial: 100 |
|
|
54
|
+
| A | t1 | `findById` → balance=100 (≥ 50? yes) |
|
|
55
|
+
| B | t2 | `findById` → balance=100 (≥ 50? yes) ← mesma leitura, A ainda não salvou |
|
|
56
|
+
| A | t3 | `save` → balance=50 |
|
|
57
|
+
| B | t4 | `save` → balance=50 ← deveria ser 0, mas B usou a leitura de t2 |
|
|
58
|
+
|
|
59
|
+
**Resultado:** saldo final = 50 (em vez de 0). Duas compras de 50, mas o saldo só
|
|
60
|
+
reduziu uma vez. Se o preço fosse 60, o saldo ficaria -20 — estado impossível.
|
|
61
|
+
|
|
62
|
+
## Evidência
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Reprodução conceitual — dois requests paralelos
|
|
66
|
+
const results = await Promise.all([
|
|
67
|
+
checkout(userId, 50), // request A
|
|
68
|
+
checkout(userId, 50), // request B
|
|
69
|
+
]);
|
|
70
|
+
// user.balance → 50, 2 orders created → saldo deveria ser 0
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Causa raiz
|
|
74
|
+
|
|
75
|
+
O código executa um padrão **read-then-write sem atomicidade**. O MongoDB `.save()`
|
|
76
|
+
substitui o documento inteiro — não é um `$inc` atômico — e não há `where` condicional
|
|
77
|
+
que impeça a escrita se o saldo mudou.
|
|
78
|
+
|
|
79
|
+
## Correção recomendada
|
|
80
|
+
|
|
81
|
+
### Opção 1 — Atômico (preferida)
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// Usa $inc atômico + condicional
|
|
85
|
+
const result = await User.findOneAndUpdate(
|
|
86
|
+
{ _id: userId, balance: { $gte: price } }, // ← CAS: balance >= price
|
|
87
|
+
{ $inc: { balance: -price } }, // ← atômico
|
|
88
|
+
{ new: true }
|
|
89
|
+
);
|
|
90
|
+
if (!result) throw new Error('Insufficient balance');
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Opção 2 — Transação
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const session = await mongoose.startSession();
|
|
97
|
+
session.startTransaction();
|
|
98
|
+
try {
|
|
99
|
+
const user = await User.findById(userId).session(session);
|
|
100
|
+
if (user.balance < price) throw new Error('...');
|
|
101
|
+
user.balance -= price;
|
|
102
|
+
await user.save({ session });
|
|
103
|
+
await createOrder(userId, price, { session });
|
|
104
|
+
await session.commitTransaction();
|
|
105
|
+
} catch {
|
|
106
|
+
await session.abortTransaction();
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Nota:** transação sem `$inc` não fecha a janela de leitura se outro request lê antes
|
|
111
|
+
do commit. Opção 1 (CAS) é mais segura.
|
|
112
|
+
|
|
113
|
+
### Opção 3 — Unique constraint
|
|
114
|
+
|
|
115
|
+
Se o pedido (`orderId`) for a chave de idempotência, uma unique constraint impede
|
|
116
|
+
duplicação — mas não impede o *segundo* débito se o saldo foi lido antes do primeiro
|
|
117
|
+
commit. Opção 1 é a defesa correta.
|
|
118
|
+
|
|
119
|
+
## Checklist de aplicação
|
|
120
|
+
|
|
121
|
+
| Invariant | Defesa atual | Defesa recomendada | Prioridade |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| balance ≥ 0 | `save()` sem condicional | `$inc` + `$gte` (CAS) | Critical |
|
|
124
|
+
| saldo não negativo | nenhuma (sem CHECK) | `CHECK (balance >= 0)` no DB | High |
|
|
125
|
+
| order única | nenhuma | UNIQUE (order_id) | High |
|
|
126
|
+
|
|
127
|
+
*Ver skills: `race-condition-hunter`, `business-logic-audit`, `idempotency-audit`,
|
|
128
|
+
`data-integrity-audit`.*
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Example — XP Reward Loop Farming
|
|
2
|
+
|
|
3
|
+
*Demonstração concreta de uma auditoria de loop de recompensa. Ilustra:
|
|
4
|
+
`gamification-audit` + `business-logic-audit` + `idempotency-audit` +
|
|
5
|
+
`race-condition-hunter` + `api-abuse-audit`. Modelo: TRIGGER → CONDITION → REWARD →
|
|
6
|
+
REVERSAL (ver `plan.md` §20).*
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Target
|
|
11
|
+
|
|
12
|
+
Um sistema de reações sociais em que **reagir dá +10 XP** ao autor do conteúdo. O
|
|
13
|
+
usuário pode remover a reação (unreact).
|
|
14
|
+
|
|
15
|
+
## Hypothesis (gerada por adversarial-review)
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
reaction
|
|
19
|
+
→ XP
|
|
20
|
+
→ remove reaction
|
|
21
|
+
→ reaction
|
|
22
|
+
→ XP
|
|
23
|
+
→ infinite farming
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
O farming acontece se a reversão (unreact) **não remove o XP concedido** — porque então
|
|
27
|
+
reagir de novo re-concede XP sobre um saldo que nunca voltou.
|
|
28
|
+
|
|
29
|
+
## Loop mapeado
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
TRIGGER → usuário reage a um post
|
|
33
|
+
CONDITION → usuário não é o autor (self-reward bloqueado); usuário ainda não reagiu
|
|
34
|
+
REWARD → autor do post recebe +10 XP
|
|
35
|
+
REVERSAL → usuário remove a reação → autor perde os 10 XP?
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Investigação
|
|
39
|
+
|
|
40
|
+
### 1. O que acontece em `unreact`?
|
|
41
|
+
|
|
42
|
+
```http
|
|
43
|
+
POST /reactions/unreact {postId: 42}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Resposta:** `200 OK`. O registro da reação é removido.
|
|
47
|
+
|
|
48
|
+
**Pergunta:** o XP concedido na reação é *devolvido*?
|
|
49
|
+
|
|
50
|
+
Rastreando o handler de unreact:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
// src/routes/reactions.js
|
|
54
|
+
router.post('/unreact', auth, async (req, res) => {
|
|
55
|
+
const { postId } = req.body;
|
|
56
|
+
const exists = await Reaction.findOne({ postId, userId: req.user.id });
|
|
57
|
+
if (!exists) return res.status(404).json({ error: 'no reaction' });
|
|
58
|
+
|
|
59
|
+
await Reaction.deleteOne({ postId, userId: req.user.id });
|
|
60
|
+
// ⚠️ NENHUMA chamada para devolver XP ao autor
|
|
61
|
+
res.json({ ok: true });
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Evidência:** `unreact` remove a reação mas **não** reverte o `+10 XP` dado ao autor
|
|
66
|
+
em `react`.
|
|
67
|
+
|
|
68
|
+
### 2. Confirmação — testar ACTION → REWARD → REVERSE → ACTION
|
|
69
|
+
|
|
70
|
+
| Ação | XP do autor |
|
|
71
|
+
|---|---|
|
|
72
|
+
| (estado inicial) | 0 |
|
|
73
|
+
| `react` | +10 → 10 |
|
|
74
|
+
| `unreact` | **10** (deveria ser 0 — a reversão não remove) |
|
|
75
|
+
| `react` (de novo) | +10 → **20** |
|
|
76
|
+
|
|
77
|
+
Reproduzido: o autor acumula XP indefinidamente alternando react/unreact sobre o
|
|
78
|
+
próprio conteúdo (ou sobre conteúdo de um parceiro).
|
|
79
|
+
|
|
80
|
+
### 3. Vetores adicionais
|
|
81
|
+
|
|
82
|
+
- **self-reward:** `react` ao *próprio* post é permitido? O handler de `react` checa
|
|
83
|
+
`post.authorId !== req.user.id`? (Rastrear.)
|
|
84
|
+
- **concurrency:** dois `react` simultâneos — ambos passam na checagem "já reagiu?"
|
|
85
|
+
(read-then-write) e concedem 2× o XP? (sem unique constraint → `race-condition-hunter`.)
|
|
86
|
+
- **replay:** `react` com a mesma combinação duas vezes — a checagem de existência
|
|
87
|
+
impede? (idempotência.)
|
|
88
|
+
|
|
89
|
+
## Findings (resumo)
|
|
90
|
+
|
|
91
|
+
| # | Severity | Confidence | Finding |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| 1 | High | CONFIRMED | Farming infinito: `unreact` não reverte o XP; `react`+`unreact` repetidos acumulam XP sem limite |
|
|
94
|
+
| 2 | High | POSSIBLE | Self-reward possivelmente permitido (handler de `react` não checa `authorId` vs `userId`) |
|
|
95
|
+
| 3 | Medium | HIGH CONFIDENCE | Race no dedup de reação: sem unique constraint, dois `react` simultâneos concedem XP 2× |
|
|
96
|
+
|
|
97
|
+
## Causa raiz
|
|
98
|
+
|
|
99
|
+
A **REVERSAL do loop é incompleta**: a ação que concede XP (`react`) e a que deveria
|
|
100
|
+
removê-lo (`unreact`) não são transações acopladas. O XP é um efeito colateral do
|
|
101
|
+
`react` sem o inverso no `unreact`. Falta também a constraint que faz da reação
|
|
102
|
+
única por (user, post), e a checagem server-side de self-reward.
|
|
103
|
+
|
|
104
|
+
## Correção recomendada
|
|
105
|
+
|
|
106
|
+
1. **Reversão completa** — `unreact` deve executar a dedução dos 10 XP do autor, de
|
|
107
|
+
forma atômica (mesma transação que remove a reação).
|
|
108
|
+
2. **Unique constraint** — `UNIQUE (user_id, post_id)` na tabela de reações. Impede
|
|
109
|
+
reação duplicada (fecha race e replay).
|
|
110
|
+
3. **Self-reward server-side** — `react` rejeita quando `post.authorId === userId`.
|
|
111
|
+
Não confiar na UI.
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
-- migração
|
|
115
|
+
ALTER TABLE reactions ADD CONSTRAINT uq_reaction UNIQUE (user_id, post_id);
|
|
116
|
+
CREATE OR REPLACE FUNCTION grant_xp(author_id INT, delta INT)
|
|
117
|
+
RETURNS void AS $$
|
|
118
|
+
UPDATE users SET xp = GREATEST(xp + delta, 0) WHERE id = author_id;
|
|
119
|
+
$$ LANGUAGE sql;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
*Ver skills: `gamification-audit`, `business-logic-audit`, `idempotency-audit`,
|
|
123
|
+
`race-condition-hunter`, `api-abuse-audit`, `adversarial-review`.*
|