@luanpdd/kit-mcp 1.19.0 → 1.21.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/README.md +1 -1
- package/gates/dept-cycle-prevention.md +179 -0
- package/gates/multi-tenant-rls-coverage.md +102 -0
- package/gates/service-role-not-in-user-facing.md +113 -0
- package/kit/agents/audit-log-implementer.md +175 -0
- package/kit/agents/b2b-saas-architect.md +156 -0
- package/kit/agents/crm-pipeline-implementer.md +150 -0
- package/kit/agents/evolution-go-integrator.md +179 -0
- package/kit/agents/invite-flow-implementer.md +137 -0
- package/kit/agents/lgpd-compliance-auditor.md +206 -0
- package/kit/agents/multi-tenant-isolation-auditor.md +243 -0
- package/kit/agents/multi-tenant-rls-writer.md +262 -0
- package/kit/agents/org-onboarding-implementer.md +202 -0
- package/kit/agents/super-admin-implementer.md +182 -0
- package/kit/commands/burn-rate-status.md +237 -121
- package/kit/commands/multi-tenant.md +163 -0
- package/kit/file-manifest.json +31 -4
- package/kit/skills/_shared-multi-tenant/glossary.md +186 -0
- package/kit/skills/audit-log-multi-tenant/SKILL.md +334 -0
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -0
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +326 -0
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -0
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -0
- package/kit/skills/member-invite-flow/SKILL.md +305 -0
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -0
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +312 -0
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +338 -0
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -0
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -0
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -0
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +301 -0
- package/kit/skills/super-admin-platform-pattern/SKILL.md +322 -0
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -0
- package/package.json +6 -2
- package/src/mcp-server/index.js +34 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Inspired by [vinilana/dotcontext](https://github.com/vinilana/dotcontext) — se
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
<!-- AUTOGEN-COUNTS-START -->
|
|
27
|
-
**Bundled workflow:**
|
|
27
|
+
**Bundled workflow:** 57 agents · 88 commands · 60 skills · 23 gates
|
|
28
28
|
<!-- AUTOGEN-COUNTS-END -->
|
|
29
29
|
|
|
30
30
|
## What ships in the box
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dept-cycle-prevention
|
|
3
|
+
stage: pre-verify
|
|
4
|
+
blocking: true
|
|
5
|
+
description: Detecta tabela departments (ou similar) com parent_id FK self-referencial mas sem trigger anti-cycle. Loop circular em dept hierarchy esgota connection pool via WITH RECURSIVE infinito. Skip se projeto não tem supabase/migrations/.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Department Cycle Prevention gate
|
|
9
|
+
|
|
10
|
+
**When to run:** pre-verify (blocking — anti-pitfall P0 multi-tenant hierarchy).
|
|
11
|
+
|
|
12
|
+
## Check
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
# PT-BR: detecta departments com parent_id self-referencial sem trigger anti-cycle.
|
|
17
|
+
# Anti-pitfall #3 multi-tenant: dept hierarchy com loop circular (A.parent=B, B.parent=A) → WITH RECURSIVE infinito → connection pool exhaustion.
|
|
18
|
+
# Bash 3.2-portable (macOS default).
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
MIGRATIONS_DIR="supabase/migrations"
|
|
22
|
+
|
|
23
|
+
if [ ! -d "$MIGRATIONS_DIR" ]; then
|
|
24
|
+
echo "INFO: $MIGRATIONS_DIR não existe — projeto não usa Supabase migrations. Gate skipped."
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# PT-BR: nomes comuns de tabelas hierárquicas (departments + variantes)
|
|
29
|
+
HIERARCHY_TABLE_PATTERNS="departments|teams|groups|categories|nodes|tree"
|
|
30
|
+
|
|
31
|
+
VIOLATIONS=0
|
|
32
|
+
VIOLATIONS_DETAIL=""
|
|
33
|
+
|
|
34
|
+
# PT-BR: iterar migrations em ordem cronológica
|
|
35
|
+
MIGRATION_FILES=$(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort)
|
|
36
|
+
|
|
37
|
+
if [ -z "$MIGRATION_FILES" ]; then
|
|
38
|
+
echo "INFO: nenhum arquivo .sql em $MIGRATIONS_DIR — gate skipped."
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# PT-BR: encontrar tabelas com parent_id auto-referencial (heurística: parent_id + REFERENCES <self>)
|
|
43
|
+
SELF_REF_TABLES=""
|
|
44
|
+
|
|
45
|
+
for f in $MIGRATION_FILES; do
|
|
46
|
+
# PT-BR: detecta padrão "parent_id ... references public.<table>(id)" onde <table> bate com nome da tabela sendo criada
|
|
47
|
+
# (case-insensitive, multi-line awk porque DDL pode ter quebras de linha)
|
|
48
|
+
TABLES_IN_FILE=$(awk '
|
|
49
|
+
BEGIN { in_create = 0; current_table = "" }
|
|
50
|
+
/create[ \t]+table[ \t]+(if[ \t]+not[ \t]+exists[ \t]+)?[a-z_]+\.[a-z_]+/ {
|
|
51
|
+
in_create = 1
|
|
52
|
+
match($0, /create[ \t]+table[ \t]+(if[ \t]+not[ \t]+exists[ \t]+)?([a-z_]+\.[a-z_]+)/)
|
|
53
|
+
if (RSTART > 0) {
|
|
54
|
+
# extrair só o último match group (table name)
|
|
55
|
+
s = substr($0, RSTART, RLENGTH)
|
|
56
|
+
n = split(s, parts, /[ \t]+/)
|
|
57
|
+
current_table = parts[n]
|
|
58
|
+
}
|
|
59
|
+
next
|
|
60
|
+
}
|
|
61
|
+
in_create && /parent_id[ \t]+uuid[ \t]+references[ \t]+([a-z_]+\.[a-z_]+)/ {
|
|
62
|
+
match($0, /references[ \t]+([a-z_]+\.[a-z_]+)/)
|
|
63
|
+
if (RSTART > 0) {
|
|
64
|
+
ref_table = substr($0, RSTART + 11, RLENGTH - 11)
|
|
65
|
+
gsub(/[ \t]/, "", ref_table)
|
|
66
|
+
if (ref_table == current_table) {
|
|
67
|
+
print current_table
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/;[ \t]*$/ && in_create { in_create = 0; current_table = "" }
|
|
72
|
+
' "$f" 2>/dev/null)
|
|
73
|
+
|
|
74
|
+
if [ -n "$TABLES_IN_FILE" ]; then
|
|
75
|
+
SELF_REF_TABLES="$SELF_REF_TABLES
|
|
76
|
+
$TABLES_IN_FILE"
|
|
77
|
+
fi
|
|
78
|
+
done
|
|
79
|
+
|
|
80
|
+
# PT-BR: filtrar entries vazias
|
|
81
|
+
SELF_REF_TABLES=$(echo "$SELF_REF_TABLES" | grep -v "^$" | sort -u)
|
|
82
|
+
|
|
83
|
+
if [ -z "$SELF_REF_TABLES" ]; then
|
|
84
|
+
echo "INFO: nenhuma tabela com parent_id self-referencial detectada — gate skipped."
|
|
85
|
+
exit 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# PT-BR: para cada tabela self-ref, verificar se há trigger ou function anti-cycle
|
|
89
|
+
for table in $SELF_REF_TABLES; do
|
|
90
|
+
# PT-BR: extrair só nome da tabela (sem schema)
|
|
91
|
+
table_name=$(echo "$table" | awk -F. '{print $2}')
|
|
92
|
+
|
|
93
|
+
# PT-BR: heurística: procurar trigger com nome contendo cycle/loop/recursion + tabela
|
|
94
|
+
CYCLE_GUARD_FOUND=0
|
|
95
|
+
for f in $MIGRATION_FILES; do
|
|
96
|
+
if grep -iE "(create[ \t]+(or[ \t]+replace[ \t]+)?(function|trigger))[ \t]+[a-z_]*(cycle|loop|recurs|hierarchy_check|anti_cycle)" "$f" 2>/dev/null \
|
|
97
|
+
| grep -iqE "$table_name|$(echo "$table" | tr '.' '_')"; then
|
|
98
|
+
CYCLE_GUARD_FOUND=1
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# PT-BR: pattern alternativo: trigger genérico que menciona a tabela e WITH RECURSIVE em corpo
|
|
103
|
+
if grep -iqE "create[ \t]+trigger.*on[ \t]+$table" "$f" 2>/dev/null \
|
|
104
|
+
&& grep -iqE "with[ \t]+recursive.*parent_id" "$f" 2>/dev/null; then
|
|
105
|
+
CYCLE_GUARD_FOUND=1
|
|
106
|
+
break
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
if [ "$CYCLE_GUARD_FOUND" -eq 0 ]; then
|
|
111
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
112
|
+
VIOLATIONS_DETAIL="${VIOLATIONS_DETAIL}
|
|
113
|
+
Tabela '$table' tem parent_id self-referencial mas sem trigger anti-cycle detectado"
|
|
114
|
+
fi
|
|
115
|
+
done
|
|
116
|
+
|
|
117
|
+
if [ "$VIOLATIONS" -eq 0 ]; then
|
|
118
|
+
echo "PASS: todas as tabelas hierárquicas têm proteção anti-cycle."
|
|
119
|
+
exit 0
|
|
120
|
+
else
|
|
121
|
+
echo "FAIL: $VIOLATIONS tabela(s) hierárquica(s) sem trigger anti-cycle:$VIOLATIONS_DETAIL"
|
|
122
|
+
echo ""
|
|
123
|
+
echo "Fix: adicionar trigger BEFORE INSERT/UPDATE que detecta cycle via WITH RECURSIVE:"
|
|
124
|
+
cat << 'EOF'
|
|
125
|
+
|
|
126
|
+
create or replace function private.check_no_dept_cycle()
|
|
127
|
+
returns trigger
|
|
128
|
+
language plpgsql
|
|
129
|
+
security invoker
|
|
130
|
+
set search_path = ''
|
|
131
|
+
as $$
|
|
132
|
+
declare
|
|
133
|
+
cycle_detected boolean;
|
|
134
|
+
begin
|
|
135
|
+
if new.parent_id is null then return new; end if;
|
|
136
|
+
|
|
137
|
+
with recursive ancestors as (
|
|
138
|
+
select id, parent_id, 1 as depth from public.departments where id = new.parent_id
|
|
139
|
+
union all
|
|
140
|
+
select d.id, d.parent_id, a.depth + 1
|
|
141
|
+
from public.departments d
|
|
142
|
+
join ancestors a on d.id = a.parent_id
|
|
143
|
+
where a.depth < 10 -- max 10 níveis
|
|
144
|
+
)
|
|
145
|
+
select exists (select 1 from ancestors where id = new.id) into cycle_detected;
|
|
146
|
+
|
|
147
|
+
if cycle_detected then
|
|
148
|
+
raise exception 'department hierarchy cycle detected: % cannot be parent of %',
|
|
149
|
+
new.parent_id, new.id;
|
|
150
|
+
end if;
|
|
151
|
+
|
|
152
|
+
return new;
|
|
153
|
+
end;
|
|
154
|
+
$$;
|
|
155
|
+
|
|
156
|
+
create trigger check_no_dept_cycle_trigger
|
|
157
|
+
before insert or update of parent_id on public.departments
|
|
158
|
+
for each row execute function private.check_no_dept_cycle();
|
|
159
|
+
|
|
160
|
+
EOF
|
|
161
|
+
echo "Ref: kit/skills/_shared-multi-tenant/glossary.md (Department Hierarchy)"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Verdict
|
|
167
|
+
|
|
168
|
+
- **passed** — todas tabelas hierárquicas têm proteção anti-cycle → continuar
|
|
169
|
+
- **block** — apresentar tabelas violadoras + DDL pronto do trigger anti-cycle
|
|
170
|
+
|
|
171
|
+
## Notes
|
|
172
|
+
|
|
173
|
+
Detecção é heurística baseada em naming (`parent_id` + `references <self_table>`). Pode produzir:
|
|
174
|
+
- **Falso-negativo** se a coluna se chamar `parent` em vez de `parent_id`, ou referenciar tabela diferente (não self-ref)
|
|
175
|
+
- **Falso-positivo** se o nome do trigger não contém palavras-chave (`cycle`, `loop`, `recurs`, `anti_cycle`) — neste caso, renomear o trigger ou estender a allowlist do gate
|
|
176
|
+
|
|
177
|
+
Tabelas hierárquicas tipicamente afetadas: `departments`, `teams`, `groups`, `categories`, `nodes`, `tree`.
|
|
178
|
+
|
|
179
|
+
Limit recursivo padrão sugerido: 10 níveis. Hierarquia mais profunda que isso indica modelagem questionável (Linear/Notion suportam até 5 níveis).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: multi-tenant-rls-coverage
|
|
3
|
+
stage: pre-verify
|
|
4
|
+
blocking: true
|
|
5
|
+
description: Detecta CREATE TABLE em supabase/migrations/ sem ENABLE ROW LEVEL SECURITY no mesmo arquivo. Cross-tenant data leak silencioso é a falha #1 de apps multi-tenant Supabase. Skip se projeto não tem supabase/migrations/.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Multi-Tenant RLS Coverage gate
|
|
9
|
+
|
|
10
|
+
**When to run:** pre-verify (blocking — multi-tenant phase não verifica até cobertura completa).
|
|
11
|
+
|
|
12
|
+
## Check
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
# PT-BR: detecta CREATE TABLE em supabase/migrations/ sem ENABLE ROW LEVEL SECURITY no mesmo arquivo.
|
|
17
|
+
# Anti-pitfall #1 multi-tenant: tabela nova sem RLS = cross-tenant leak silencioso (Postgres não aplica policies automaticamente).
|
|
18
|
+
# Bash 3.2-portable (macOS default).
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
MIGRATIONS_DIR="supabase/migrations"
|
|
22
|
+
|
|
23
|
+
# PT-BR: skip gracioso se projeto não tem migrations Supabase
|
|
24
|
+
if [ ! -d "$MIGRATIONS_DIR" ]; then
|
|
25
|
+
echo "INFO: $MIGRATIONS_DIR não existe — projeto não usa Supabase migrations. Gate skipped."
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# PT-BR: tabelas em schemas system não exigem RLS (auth, storage, realtime, vault, supabase_*)
|
|
30
|
+
SYSTEM_SCHEMA_PREFIXES="auth\\.|storage\\.|realtime\\.|vault\\.|supabase_|extensions\\."
|
|
31
|
+
|
|
32
|
+
# PT-BR: allowlist de tabelas que conscientemente não têm RLS (ex: lookup tables públicas)
|
|
33
|
+
ALLOWLIST_TABLES=(
|
|
34
|
+
"public.permissions" # catálogo global de permissions, leitura pública por design
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
is_allowlisted() {
|
|
38
|
+
local table="$1"
|
|
39
|
+
for at in "${ALLOWLIST_TABLES[@]}"; do
|
|
40
|
+
[ "$table" = "$at" ] && return 0
|
|
41
|
+
done
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
VIOLATIONS=0
|
|
46
|
+
VIOLATIONS_DETAIL=""
|
|
47
|
+
|
|
48
|
+
# PT-BR: iterar migrations em ordem cronológica
|
|
49
|
+
MIGRATION_FILES=$(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort)
|
|
50
|
+
|
|
51
|
+
if [ -z "$MIGRATION_FILES" ]; then
|
|
52
|
+
echo "INFO: nenhum arquivo .sql em $MIGRATIONS_DIR — gate skipped."
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
for f in $MIGRATION_FILES; do
|
|
57
|
+
# PT-BR: extrair tabelas criadas via CREATE TABLE (case-insensitive, ignora IF NOT EXISTS)
|
|
58
|
+
CREATED_TABLES=$(grep -iE "^create\s+table\s+(if\s+not\s+exists\s+)?[a-z_]+\." "$f" 2>/dev/null \
|
|
59
|
+
| sed -E 's/.*create\s+table\s+(if\s+not\s+exists\s+)?([a-z_]+\.[a-z_]+).*/\2/i' \
|
|
60
|
+
| grep -viE "$SYSTEM_SCHEMA_PREFIXES" || true)
|
|
61
|
+
|
|
62
|
+
# PT-BR: extrair tabelas com RLS habilitada no MESMO arquivo
|
|
63
|
+
RLS_TABLES=$(grep -iE "alter\s+table\s+[a-z_]+\.[a-z_]+\s+enable\s+row\s+level\s+security" "$f" 2>/dev/null \
|
|
64
|
+
| sed -E 's/.*alter\s+table\s+([a-z_]+\.[a-z_]+)\s+enable.*/\1/i' || true)
|
|
65
|
+
|
|
66
|
+
# PT-BR: para cada tabela criada, checar se RLS foi habilitada
|
|
67
|
+
for table in $CREATED_TABLES; do
|
|
68
|
+
[ -z "$table" ] && continue
|
|
69
|
+
is_allowlisted "$table" && continue
|
|
70
|
+
|
|
71
|
+
if ! echo "$RLS_TABLES" | grep -qFx "$table"; then
|
|
72
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
73
|
+
VIOLATIONS_DETAIL="${VIOLATIONS_DETAIL}
|
|
74
|
+
$(basename "$f"): tabela '$table' criada sem ENABLE ROW LEVEL SECURITY"
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ "$VIOLATIONS" -eq 0 ]; then
|
|
80
|
+
echo "PASS: todas as tabelas em supabase/migrations/ têm RLS habilitada no mesmo arquivo de criação."
|
|
81
|
+
exit 0
|
|
82
|
+
else
|
|
83
|
+
echo "FAIL: $VIOLATIONS tabela(s) criada(s) sem ENABLE ROW LEVEL SECURITY:$VIOLATIONS_DETAIL"
|
|
84
|
+
echo ""
|
|
85
|
+
echo "Fix: adicione 'alter table <schema>.<table> enable row level security;' no MESMO arquivo de migration que criou a tabela."
|
|
86
|
+
echo "Ref: kit/skills/multi-tenant-rls-hierarchy/SKILL.md (REGRA #1)"
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Verdict
|
|
92
|
+
|
|
93
|
+
- **passed** — todas tabelas multi-tenant têm RLS habilitada → continuar
|
|
94
|
+
- **block** — apresentar tabela de violations + sugestão de fix; sem opção de skip (anti-pitfall P0 — cross-tenant leak)
|
|
95
|
+
|
|
96
|
+
## Notes
|
|
97
|
+
|
|
98
|
+
Este gate só checa **habilitação** de RLS — não checa se as policies cobrem todos os casos. Ver `multi-tenant-isolation-auditor` agent para análise completa de policies (requer MCP Supabase ativo para query a `pg_policies`).
|
|
99
|
+
|
|
100
|
+
Tabelas em schemas system (`auth.*`, `storage.*`, `realtime.*`, `vault.*`, `supabase_*`, `extensions.*`) são automaticamente skipped — Supabase já aplica RLS interno nelas.
|
|
101
|
+
|
|
102
|
+
Allowlist mínima: `public.permissions` (catálogo global de permissions, leitura pública por design — tem `to authenticated` em SELECT mas sem isolamento por tenant).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: service-role-not-in-user-facing
|
|
3
|
+
stage: pre-verify
|
|
4
|
+
blocking: true
|
|
5
|
+
description: Detecta uso de SUPABASE_SERVICE_ROLE_KEY em Edge Functions com verify_jwt:true (user-facing). Service role bypassa RLS — uso em rota acessível a usuário desliga toda autorização. Skip se projeto não tem supabase/functions/.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Service Role Not In User-Facing gate
|
|
9
|
+
|
|
10
|
+
**When to run:** pre-verify (blocking — anti-pitfall P0 multi-tenant).
|
|
11
|
+
|
|
12
|
+
## Check
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
# PT-BR: detecta uso de SUPABASE_SERVICE_ROLE_KEY em Edge Functions com verify_jwt:true.
|
|
17
|
+
# Anti-pitfall #2 multi-tenant: service role em rota user-facing = bypass total de RLS.
|
|
18
|
+
# Bash 3.2-portable (macOS default).
|
|
19
|
+
set -e
|
|
20
|
+
|
|
21
|
+
FUNCTIONS_DIR="supabase/functions"
|
|
22
|
+
CONFIG_FILE="supabase/config.toml"
|
|
23
|
+
|
|
24
|
+
if [ ! -d "$FUNCTIONS_DIR" ]; then
|
|
25
|
+
echo "INFO: $FUNCTIONS_DIR não existe — projeto não usa Supabase Edge Functions. Gate skipped."
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# PT-BR: env vars que indicam uso de service role
|
|
30
|
+
SERVICE_ROLE_PATTERNS="SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE_KEY|service_role_key|serviceRoleKey"
|
|
31
|
+
|
|
32
|
+
VIOLATIONS=0
|
|
33
|
+
VIOLATIONS_DETAIL=""
|
|
34
|
+
|
|
35
|
+
# PT-BR: iterar cada Edge Function (cada subdir em functions/)
|
|
36
|
+
FUNCTION_DIRS=$(find "$FUNCTIONS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
if [ -z "$FUNCTION_DIRS" ]; then
|
|
39
|
+
echo "INFO: nenhuma Edge Function em $FUNCTIONS_DIR — gate skipped."
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
for fn_dir in $FUNCTION_DIRS; do
|
|
44
|
+
fn_name=$(basename "$fn_dir")
|
|
45
|
+
|
|
46
|
+
# PT-BR: skip _shared (não é Edge Function deployable)
|
|
47
|
+
[ "$fn_name" = "_shared" ] && continue
|
|
48
|
+
|
|
49
|
+
# PT-BR: descobrir verify_jwt setting da função (default: true se ausente)
|
|
50
|
+
VERIFY_JWT="true" # default Supabase
|
|
51
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
52
|
+
# PT-BR: parsing leve — procurar [functions.<name>] block + verify_jwt
|
|
53
|
+
SECTION_VERIFY=$(awk -v fn="$fn_name" '
|
|
54
|
+
$0 ~ "^\\[functions\\." fn "\\]" { in_section = 1; next }
|
|
55
|
+
in_section && /^\[/ { in_section = 0 }
|
|
56
|
+
in_section && /verify_jwt/ {
|
|
57
|
+
gsub(/[ \t]/, "")
|
|
58
|
+
split($0, a, "=")
|
|
59
|
+
print a[2]
|
|
60
|
+
exit
|
|
61
|
+
}
|
|
62
|
+
' "$CONFIG_FILE" 2>/dev/null)
|
|
63
|
+
|
|
64
|
+
if [ -n "$SECTION_VERIFY" ]; then
|
|
65
|
+
VERIFY_JWT="$SECTION_VERIFY"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# PT-BR: se verify_jwt=false (webhook/internal), skip — service role é OK aqui
|
|
70
|
+
if [ "$VERIFY_JWT" = "false" ]; then
|
|
71
|
+
continue
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# PT-BR: buscar uso de service role em arquivos .ts/.js da function
|
|
75
|
+
SERVICE_ROLE_FILES=$(grep -rlE "$SERVICE_ROLE_PATTERNS" "$fn_dir" --include="*.ts" --include="*.js" --include="*.mjs" 2>/dev/null || true)
|
|
76
|
+
|
|
77
|
+
if [ -n "$SERVICE_ROLE_FILES" ]; then
|
|
78
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
79
|
+
VIOLATIONS_DETAIL="${VIOLATIONS_DETAIL}
|
|
80
|
+
Edge Function '$fn_name' (verify_jwt=$VERIFY_JWT) usa service role:
|
|
81
|
+
$(echo "$SERVICE_ROLE_FILES" | sed 's/^/ /')"
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
if [ "$VIOLATIONS" -eq 0 ]; then
|
|
86
|
+
echo "PASS: nenhuma Edge Function user-facing (verify_jwt=true) usa SERVICE_ROLE_KEY."
|
|
87
|
+
exit 0
|
|
88
|
+
else
|
|
89
|
+
echo "FAIL: $VIOLATIONS Edge Function(s) user-facing usam SERVICE_ROLE_KEY:$VIOLATIONS_DETAIL"
|
|
90
|
+
echo ""
|
|
91
|
+
echo "Fix: use ANON_KEY com JWT do user para preservar RLS. Se realmente precisa de service role:"
|
|
92
|
+
echo " - Mover lógica privilegiada para Edge Function separada com verify_jwt=false (webhook ou cron-only)"
|
|
93
|
+
echo " - OU validar manualmente quem está chamando antes de usar service role (anti-pattern, mas explícito)"
|
|
94
|
+
echo "Ref: kit/skills/_shared-multi-tenant/glossary.md (sub-seção super_admin) + kit/skills/supabase-rls-policies/SKILL.md"
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Verdict
|
|
100
|
+
|
|
101
|
+
- **passed** — nenhum service role em Edge Function user-facing → continuar
|
|
102
|
+
- **block** — apresentar lista de Edge Functions violadoras + fix recomendado
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
Edge Functions com `verify_jwt = false` (webhooks Evolution Go, Stripe, schedulers internos via pg_cron) podem usar service role legitimamente — gate skippa essas.
|
|
107
|
+
|
|
108
|
+
Detecção pode produzir falso-positivo se code referencia o nome da var em comentário ou string literal (ex: documentação dentro do code). Para suprimir, mover documentação para arquivo externo ou usar comentário JSDoc fora do regex match.
|
|
109
|
+
|
|
110
|
+
Para super-admin operations user-facing (impersonation, cross-tenant queries), pattern correto é:
|
|
111
|
+
1. Endpoint user-facing valida `super_admin: true` em JWT app_metadata
|
|
112
|
+
2. Endpoint chama segunda Edge Function interna (verify_jwt=false) que usa service role
|
|
113
|
+
3. Audit log obrigatório no caminho
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-log-implementer
|
|
3
|
+
description: Materializa audit log multi-tenant — tabela append-only (REVOKE DELETE/UPDATE), helper function private.audit_log com PII hashing, retention scheduler pg_cron 3 tiers (30d/90d/365d), legal_hold flag para LGPD. Cross-suite: usa skill supabase-cron-queues + delega para supabase-migration-writer.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Grep, Glob, Task, AskUserQuestion, mcp__supabase__execute_sql, mcp__supabase__list_tables
|
|
5
|
+
color: yellow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Você é o **audit-log-implementer**. Materializa o audit log canônico v1.21 — tabela append-only + helper function + retention scheduler. **Delega SQL final para `supabase-migration-writer`** (cross-suite). Lê skill [`audit-log-multi-tenant`](../skills/audit-log-multi-tenant/SKILL.md) como base.
|
|
9
|
+
|
|
10
|
+
**Compat:** Full em Claude Code + Cursor (com Supabase MCP); Partial em Codex + Gemini CLI.
|
|
11
|
+
|
|
12
|
+
## Por que existe
|
|
13
|
+
|
|
14
|
+
Audit log é **pré-requisito BLOCKER** para Phase 111 (super-admin) — sem ele, super_admin opera sem rastro. Este agent garante que o pattern canônico (append-only + PII sanitization + retention multi-tier + legal_hold) seja materializado consistentemente, sem improviso por phase.
|
|
15
|
+
|
|
16
|
+
## Inputs esperados (do caller)
|
|
17
|
+
|
|
18
|
+
- (Opcional) `default_tier`: `free` (30d) | `pro` (90d) | `enterprise` (365d) — se ausente, usa `free` como default + aplica per-org via `organizations.plan`
|
|
19
|
+
- (Opcional) `partitioning`: `true` | `false` — true só se app espera >50k events/org/ano. Default `false` (single table)
|
|
20
|
+
- (Opcional) `extra_event_types`: lista de custom event types (prefix `custom_`) além dos 7 canônicos
|
|
21
|
+
- (Opcional) `audit_super_admin_tables`: lista de tabelas que ganham trigger automático de audit super_admin
|
|
22
|
+
|
|
23
|
+
## Passos
|
|
24
|
+
|
|
25
|
+
### Step 0 — Preflight
|
|
26
|
+
|
|
27
|
+
Detectar MCP. Verificar se Phase 106 schema existe (organizations, organization_members).
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
select exists (select 1 from information_schema.tables where table_schema = 'public' and table_name = 'organizations') as ok;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Se não existe → ABORT: "Phase 106 não implementada — schema base faltando."
|
|
34
|
+
|
|
35
|
+
### Step 1 — Validar pg_cron extension
|
|
36
|
+
|
|
37
|
+
```sql
|
|
38
|
+
select extname from pg_extension where extname = 'pg_cron';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Se não habilitada:
|
|
42
|
+
```
|
|
43
|
+
⚠ pg_cron extension não habilitada — retention scheduler não vai funcionar.
|
|
44
|
+
Solução: na Supabase Dashboard → Database → Extensions → enable pg_cron.
|
|
45
|
+
Continuar mesmo assim? [yes/no]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Step 2 — Coletar tier preferences via AskUserQuestion (se default_tier ausente)
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
- "Free 30d (Recomendado para start)" — Org plan 'free' → 30 dias retention
|
|
52
|
+
- "Pro 90d" — Org plan 'pro' → 90 dias retention
|
|
53
|
+
- "Enterprise 365d" — Org plan 'enterprise' → 365 dias retention
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
(Default behavior: aplica os 3 tiers automaticamente baseado em `organizations.plan` — não precisa escolher um único)
|
|
57
|
+
|
|
58
|
+
### Step 3 — Decidir partitioning
|
|
59
|
+
|
|
60
|
+
Perguntar se app espera >50k events/org/ano:
|
|
61
|
+
- Sim → partitioning LIST por tenant_id (mais complexo)
|
|
62
|
+
- Não → tabela única (default)
|
|
63
|
+
|
|
64
|
+
### Step 4 — Gerar migration brief
|
|
65
|
+
|
|
66
|
+
Construir prompt para `supabase-migration-writer`:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
[Migration brief — gerada por audit-log-implementer]
|
|
70
|
+
|
|
71
|
+
Objetivo: materializar audit log canônico v1.21 baseado em:
|
|
72
|
+
- kit/skills/audit-log-multi-tenant/SKILL.md (regras + DDL)
|
|
73
|
+
- kit/skills/supabase-cron-queues/SKILL.md (pattern pg_cron)
|
|
74
|
+
|
|
75
|
+
Artefatos a produzir:
|
|
76
|
+
1. Tabela `public.audit_logs` (append-only, com 7 event types canônicos + custom prefix)
|
|
77
|
+
- REVOKE DELETE, UPDATE FROM authenticated, anon
|
|
78
|
+
- 3 indexes: (tenant_id, created_at desc) composite, (actor_id, created_at) where not null, (legal_hold, created_at) where legal_hold = false
|
|
79
|
+
- 3 RLS policies: SELECT com private.has_permission, INSERT com tenant_id check, super_admin PERMISSIVE bypass
|
|
80
|
+
|
|
81
|
+
2. Função `private.audit_log(event_type, tenant_id, target_id, target_type, target_email, payload)` SECURITY DEFINER
|
|
82
|
+
- Hash actor_email + target_email (SHA-256)
|
|
83
|
+
- GRANT EXECUTE TO authenticated
|
|
84
|
+
|
|
85
|
+
3. pg_cron schedule `audit-log-retention` (cron expr: '0 3 * * *')
|
|
86
|
+
- 3 DELETEs, um por tier (free 30d / pro 90d / enterprise 365d)
|
|
87
|
+
- Sempre `and legal_hold = false`
|
|
88
|
+
|
|
89
|
+
4. (Opcional se partitioning=true) Tabela particionada LIST + função private.create_audit_partition + trigger on_org_created
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Step 5 — Delegar para supabase-migration-writer
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
Task(
|
|
96
|
+
subagent_type='supabase-migration-writer',
|
|
97
|
+
prompt=<migration brief acima>
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Step 6 — Gerar audit triggers para super_admin (se audit_super_admin_tables fornecido)
|
|
102
|
+
|
|
103
|
+
Para cada tabela na lista, gerar trigger AFTER usando o template do agent `multi-tenant-rls-writer`:
|
|
104
|
+
|
|
105
|
+
```sql
|
|
106
|
+
create or replace function private.audit_super_admin_<table>()
|
|
107
|
+
...
|
|
108
|
+
create trigger audit_super_admin_<table>_trigger ...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Delegar para `supabase-migration-writer` em segunda invocação (ou batch na primeira).
|
|
112
|
+
|
|
113
|
+
### Step 7 — Output integrado
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
═══════════════════════════════════════════════════════════
|
|
117
|
+
AUDIT-LOG-IMPLEMENTER · output integrado
|
|
118
|
+
═══════════════════════════════════════════════════════════
|
|
119
|
+
|
|
120
|
+
## 1. Decisões tomadas
|
|
121
|
+
- Default tier: <chosen>
|
|
122
|
+
- Partitioning: <yes/no>
|
|
123
|
+
- Custom event types: <list>
|
|
124
|
+
- Tables com super_admin audit trigger: <list>
|
|
125
|
+
|
|
126
|
+
## 2. Migration entregue (via supabase-migration-writer)
|
|
127
|
+
<output>
|
|
128
|
+
|
|
129
|
+
## 3. Eventos canônicos disponíveis
|
|
130
|
+
- login
|
|
131
|
+
- member_invited
|
|
132
|
+
- role_changed
|
|
133
|
+
- data_exported
|
|
134
|
+
- member_removed
|
|
135
|
+
- settings_changed
|
|
136
|
+
- super_admin_action
|
|
137
|
+
- <custom_*>
|
|
138
|
+
|
|
139
|
+
## 4. Como emitir audit em Edge Functions / app code
|
|
140
|
+
- TypeScript example: supabase.rpc('audit_log', { p_event_type: 'login', p_tenant_id: orgId, p_payload: {} })
|
|
141
|
+
|
|
142
|
+
## 5. Próximos passos
|
|
143
|
+
- Aplicar migration: supabase db push
|
|
144
|
+
- Verificar pg_cron job: select * from cron.job where jobname = 'audit-log-retention'
|
|
145
|
+
- Phase 111 (super-admin) pode prosseguir — audit_logs disponível
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Anti-patterns prevenidos
|
|
149
|
+
|
|
150
|
+
- Tabela audit_logs sem REVOKE → ABORT no migration brief
|
|
151
|
+
- Raw PII em columns → hash SHA-256 obrigatório
|
|
152
|
+
- Retention sem legal_hold filter → mandatory no pg_cron schedule
|
|
153
|
+
- pg_cron disabled → warn explícito + opção de continuar
|
|
154
|
+
- super_admin tables sem trigger audit → opt-in via `audit_super_admin_tables`
|
|
155
|
+
|
|
156
|
+
## Quando NÃO invocar
|
|
157
|
+
|
|
158
|
+
- Phase 106 não implementada → ABORT
|
|
159
|
+
- App single-tenant sem requisito de audit → overhead
|
|
160
|
+
- Audit log já existe em outra tabela (legacy) → use Edit + migration de schema
|
|
161
|
+
|
|
162
|
+
## Observabilidade integrada
|
|
163
|
+
|
|
164
|
+
- Counter `audit.log.events.count{event_type, tenant_id}` por insert
|
|
165
|
+
- Histogram `audit.log.payload_size_bytes` (detectar payload bloat)
|
|
166
|
+
- Alarme se `audit.log.events.count{event_type=super_admin_action}` > baseline → suspeita de comprometimento
|
|
167
|
+
|
|
168
|
+
## Ver também
|
|
169
|
+
|
|
170
|
+
- [audit-log-multi-tenant](../skills/audit-log-multi-tenant/SKILL.md) — base de conhecimento (DDL + regras)
|
|
171
|
+
- [supabase-cron-queues](../skills/supabase-cron-queues/SKILL.md) — pattern pg_cron (cross-suite)
|
|
172
|
+
- [supabase-migration-writer](./supabase-migration-writer.md) — agent invocado para SQL final
|
|
173
|
+
- [super-admin-implementer](./super-admin-implementer.md) — Phase 111, **DEPENDE** deste agent (BLOCKER ADMIN-03)
|
|
174
|
+
- [lgpd-compliance-auditor](./lgpd-compliance-auditor.md) — Phase 114, gerencia legal_hold lifecycle
|
|
175
|
+
- [_shared-multi-tenant/glossary.md](../skills/_shared-multi-tenant/glossary.md) — termos `audit log`, `legal hold`, `event taxonomy`
|