@justmpm/firebase-audit 0.3.1 → 0.4.1

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.
@@ -0,0 +1,165 @@
1
+ import {
2
+ scan
3
+ } from "./chunk-3SEVYYCX.js";
4
+ import {
5
+ verify
6
+ } from "./chunk-PSSMYRRU.js";
7
+ import {
8
+ drift
9
+ } from "./chunk-NJBCSW7E.js";
10
+
11
+ // src/mcp.ts
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import { createRequire } from "module";
16
+ var require2 = createRequire(import.meta.url);
17
+ var pkg = require2("../package.json");
18
+ function createMcpServer() {
19
+ const server = new McpServer({
20
+ name: "firebase-audit-mcp-server",
21
+ version: pkg.version
22
+ });
23
+ server.registerTool(
24
+ "firebase_audit_scan",
25
+ {
26
+ title: "Firebase Audit Scan",
27
+ description: "Audita projeto Firebase (c\xF3digo + firestore.rules + \xEDndices + contrato). Static-first, nunca inventa certeza (CONFIRMED/PROBABLE/NOT_OBSERVED/UNKNOWN). Use para descobrir regras p\xFAblicas, Admin em cliente, permiss\xF5es sem contrato.",
28
+ inputSchema: {
29
+ cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase a auditar"),
30
+ adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can). Default: do contrato."),
31
+ strict: z.boolean().optional().describe("Exige contrato (firebase-audit.yaml). CI usa strict.")
32
+ }
33
+ },
34
+ async ({ cwd, adapter, strict }) => {
35
+ try {
36
+ const res = await scan(cwd, { adapterFn: adapter, strict });
37
+ return {
38
+ content: [
39
+ {
40
+ type: "text",
41
+ text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
42
+ }
43
+ ]
44
+ };
45
+ } catch (err) {
46
+ return {
47
+ content: [{ type: "text", text: `scan falhou: ${err instanceof Error ? err.message : err}` }],
48
+ isError: true
49
+ };
50
+ }
51
+ }
52
+ );
53
+ server.registerTool(
54
+ "firebase_audit_check",
55
+ {
56
+ title: "Firebase Audit Check (strict)",
57
+ description: "Alias de scan --strict: exige contrato e falha (exit 2 no CLI) se houver ERROR. Use no CI.",
58
+ inputSchema: {
59
+ cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase"),
60
+ adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can)")
61
+ }
62
+ },
63
+ async ({ cwd, adapter }) => {
64
+ try {
65
+ const res = await scan(cwd, { adapterFn: adapter, strict: true });
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
71
+ }
72
+ ]
73
+ };
74
+ } catch (err) {
75
+ return {
76
+ content: [{ type: "text", text: `check falhou: ${err instanceof Error ? err.message : err}` }],
77
+ isError: true
78
+ };
79
+ }
80
+ }
81
+ );
82
+ server.registerTool(
83
+ "firebase_audit_verify",
84
+ {
85
+ title: "Firebase Audit Verify (Emulator)",
86
+ description: "Gera matriz role\xD7opera\xE7\xE3o\xD7path e avalia coverage do :ruleCoverage. Requer Emulator (firebase emulators:exec). Nunca afirma cobertura sem teste real.",
87
+ inputSchema: {
88
+ cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
89
+ coverage: z.string().optional().describe("Path do coverage.json do :ruleCoverage (relativo ao cwd)")
90
+ }
91
+ },
92
+ async ({ cwd, coverage }) => {
93
+ try {
94
+ const { resolve } = await import("path");
95
+ const { model } = await scan(cwd);
96
+ const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: JSON.stringify(
102
+ { version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings },
103
+ null,
104
+ 2
105
+ )
106
+ }
107
+ ]
108
+ };
109
+ } catch (err) {
110
+ return {
111
+ content: [{ type: "text", text: `verify falhou: ${err instanceof Error ? err.message : err}` }],
112
+ isError: true
113
+ };
114
+ }
115
+ }
116
+ );
117
+ server.registerTool(
118
+ "firebase_audit_drift",
119
+ {
120
+ title: "Firebase Audit Drift (\xEDndices)",
121
+ description: "Compara \xEDndices locais vs implantados. Retorna MATCHED/LOCAL_ONLY/REMOTE_ONLY. Nunca diz 'unused', apenas NOT_OBSERVED.",
122
+ inputSchema: {
123
+ local: z.string().nullable().optional().describe("Path do firestore.indexes.json local (ou null). Relativo resolve contra cwd."),
124
+ remote: z.string().nullable().optional().describe("Path do JSON remoto (gcloud/REST) (ou null). Relativo resolve contra cwd."),
125
+ cwd: z.string().optional().describe("Diret\xF3rio base para paths relativos (default: cwd do servidor).")
126
+ }
127
+ },
128
+ async ({ local, remote, cwd }) => {
129
+ try {
130
+ const { resolve: resolveP, isAbsolute } = await import("path");
131
+ const base = cwd && cwd.length > 0 ? cwd : process.cwd();
132
+ const resPath = (p) => {
133
+ if (p === null || p === void 0) return null;
134
+ return isAbsolute(p) ? p : resolveP(base, p);
135
+ };
136
+ const res = drift(resPath(local), resPath(remote));
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify({ version: pkg.version, entries: res.entries, findings: res.findings }, null, 2)
142
+ }
143
+ ]
144
+ };
145
+ } catch (err) {
146
+ return {
147
+ content: [{ type: "text", text: `drift falhou: ${err instanceof Error ? err.message : err}` }],
148
+ isError: true
149
+ };
150
+ }
151
+ }
152
+ );
153
+ return server;
154
+ }
155
+ async function startMcpServer() {
156
+ const server = createMcpServer();
157
+ const transport = new StdioServerTransport();
158
+ await server.connect(transport);
159
+ console.error(`[firebase-audit] MCP server v${pkg.version} running via stdio`);
160
+ }
161
+
162
+ export {
163
+ createMcpServer,
164
+ startMcpServer
165
+ };
@@ -1,7 +1,7 @@
1
1
  // src/drift.ts
2
2
  import { readFileSync, existsSync } from "fs";
3
3
  function normalizeMode(f) {
4
- if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0 || f.fieldPath === "__name__") return null;
4
+ if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) return null;
5
5
  if (f.vectorConfig !== void 0 && f.vectorConfig !== null) return { fieldPath: f.fieldPath, mode: "VECTOR" };
6
6
  if (f.arrayConfig === "CONTAINS") return { fieldPath: f.fieldPath, mode: "ARRAY_CONTAINS" };
7
7
  if (typeof f.mode === "string" && (f.mode === "ASCENDING" || f.mode === "DESCENDING" || f.mode === "ARRAY_CONTAINS" || f.mode === "VECTOR")) {
@@ -19,6 +19,9 @@ function normalizeEntry(raw) {
19
19
  if (m) cg = m[1];
20
20
  }
21
21
  if (!cg) return null;
22
+ if (raw.queryScope !== void 0 && raw.queryScope !== "COLLECTION" && raw.queryScope !== "COLLECTION_GROUP") {
23
+ return null;
24
+ }
22
25
  const scope = raw.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION";
23
26
  const fields = [];
24
27
  if (Array.isArray(raw.fields)) {
@@ -27,6 +30,7 @@ function normalizeEntry(raw) {
27
30
  if (n) fields.push(n);
28
31
  }
29
32
  }
33
+ if (fields.length === 0) return null;
30
34
  return { collectionGroup: cg, queryScope: scope, fields };
31
35
  }
32
36
  function keyOf(i) {
@@ -42,6 +46,19 @@ function normalizeList(raw) {
42
46
  }
43
47
  function drift(localFile, remoteFile) {
44
48
  const findings = [];
49
+ const localMissing = !localFile || !existsSync(localFile);
50
+ const remoteMissing = !remoteFile || !existsSync(remoteFile);
51
+ if (localMissing && remoteMissing) {
52
+ findings.push({
53
+ rule: "DRIFT_NO_INPUT",
54
+ severity: "INFO",
55
+ confidence: "NOT_OBSERVED",
56
+ message: "Drift sem base local nem remota \u2014 nada a comparar.",
57
+ fingerprint: "DRIFT_NO_INPUT",
58
+ evidence: [{ id: "ev-drift-noinput", kind: "drift-parse", summary: "sem base", confidence: "NOT_OBSERVED" }]
59
+ });
60
+ return { entries: [], findings };
61
+ }
45
62
  let local = [];
46
63
  let remote = [];
47
64
  if (localFile && existsSync(localFile)) {
@@ -1,5 +1,21 @@
1
1
  // src/verify.ts
2
2
  import { readFileSync, existsSync } from "fs";
3
+ function collectVisitCounts(node, out) {
4
+ if (typeof node !== "object" || node === null) return;
5
+ if (Array.isArray(node)) {
6
+ for (const v of node) collectVisitCounts(v, out);
7
+ return;
8
+ }
9
+ const rec = node;
10
+ for (const k of ["visitCount", "visit_count"]) {
11
+ const v = rec[k];
12
+ if (typeof v === "number" && Number.isFinite(v)) {
13
+ out.push(v);
14
+ break;
15
+ }
16
+ }
17
+ for (const v of Object.values(rec)) collectVisitCounts(v, out);
18
+ }
3
19
  function planVerify(model) {
4
20
  const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
5
21
  const expand = (op) => {
@@ -10,6 +26,7 @@ function planVerify(model) {
10
26
  };
11
27
  const cases = [];
12
28
  for (const rule of model.rules) {
29
+ if (rule.path === "/(unknown)") continue;
13
30
  for (const role of roles) {
14
31
  for (const op of rule.operations) {
15
32
  for (const mapped of expand(op)) {
@@ -32,6 +49,7 @@ function verify(model, opts = {}) {
32
49
  const cases = planVerify(model);
33
50
  const findings = [];
34
51
  const uncovered = [];
52
+ let coveragePct = null;
35
53
  if (opts.coverageFile) {
36
54
  if (!existsSync(opts.coverageFile)) {
37
55
  findings.push({
@@ -46,15 +64,32 @@ function verify(model, opts = {}) {
46
64
  } else {
47
65
  try {
48
66
  const raw = JSON.parse(readFileSync(opts.coverageFile, "utf-8"));
49
- void raw;
50
- findings.push({
51
- rule: "COVERAGE_NOT_EVALUATED",
52
- severity: "WARNING",
53
- confidence: "NOT_OBSERVED",
54
- message: "Coverage recebido mas ainda n\xE3o avaliado nesta vers\xE3o (formato :ruleCoverage inst\xE1vel) \u2014 uncovered lista tudo por honestidade.",
55
- fingerprint: "COVERAGE_NOT_EVALUATED",
56
- evidence: [{ id: "ev-coverage-ne", kind: "coverage-parse", summary: "n\xE3o avaliado", confidence: "NOT_OBSERVED" }]
57
- });
67
+ const counts = [];
68
+ collectVisitCounts(raw, counts);
69
+ if (counts.length === 0) {
70
+ findings.push({
71
+ rule: "COVERAGE_NOT_EVALUATED",
72
+ severity: "WARNING",
73
+ confidence: "NOT_OBSERVED",
74
+ message: "Coverage sem visitCount reconhec\xEDvel nesta vers\xE3o \u2014 uncovered lista tudo por honestidade.",
75
+ fingerprint: "COVERAGE_NOT_EVALUATED",
76
+ evidence: [{ id: "ev-coverage-ne", kind: "coverage-parse", summary: "n\xE3o avaliado", confidence: "NOT_OBSERVED" }]
77
+ });
78
+ } else {
79
+ const covered = counts.filter((c) => c > 0).length;
80
+ coveragePct = Math.round(covered / counts.length * 100);
81
+ if (coveragePct < 100) {
82
+ findings.push({
83
+ rule: "COVERAGE_LOW",
84
+ severity: "WARNING",
85
+ confidence: "CONFIRMED",
86
+ message: `Cobertura de Rules em ${coveragePct}% (${covered}/${counts.length} express\xF5es visitadas) \u2014 ramos com visitCount 0 sem teste.`,
87
+ fingerprint: "COVERAGE_LOW",
88
+ evidence: [{ id: "ev-coverage-low", kind: "coverage-parse", summary: `${coveragePct}%`, confidence: "CONFIRMED" }],
89
+ fix: "Adicione casos anon/user/admin x get/list/create/update/delete at\xE9 100%."
90
+ });
91
+ }
92
+ }
58
93
  } catch {
59
94
  findings.push({
60
95
  rule: "COVERAGE_INVALID",
@@ -67,10 +102,14 @@ function verify(model, opts = {}) {
67
102
  }
68
103
  }
69
104
  }
105
+ if (coveragePct === 100) {
106
+ return { cases, uncovered: [], findings, coveragePct };
107
+ }
70
108
  for (const rule of model.rules) {
109
+ if (rule.path === "/(unknown)") continue;
71
110
  uncovered.push(rule.path);
72
111
  }
73
- return { cases, uncovered, findings };
112
+ return { cases, uncovered, findings, coveragePct };
74
113
  }
75
114
 
76
115
  export {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-UMQH7UDI.js";
4
+ } from "./chunk-3SEVYYCX.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { createRequire } from "module";
@@ -64,19 +64,20 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
64
64
  const { resolve: resolveRoot } = await import("path");
65
65
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
66
66
  if (cmd === "verify") {
67
- const { scan: scanForVerify } = await import("./scan-TLHI777J.js");
68
- const { verify } = await import("./verify-Y7BWAFPL.js");
67
+ const { scan: scanForVerify } = await import("./scan-VREY2FGO.js");
68
+ const { verify } = await import("./verify-O722ZZBV.js");
69
69
  const { resolve: resolveVerify } = await import("path");
70
70
  const coverageArg = getArg("coverage");
71
71
  const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
72
72
  const { model } = await scanForVerify(rootDir, { adapterFn: adapterArg });
73
73
  const res = verify(model, { coverageFile: coveragePath });
74
74
  if (asJson) {
75
- console.log(JSON.stringify({ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, findings: res.findings }, null, 2));
75
+ console.log(JSON.stringify({ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings }, null, 2));
76
76
  return;
77
77
  }
78
+ const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
78
79
  console.log(`
79
- FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem cobertura observada.`);
80
+ FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem mapeamento n\xF3\u2192path (${covStr}).`);
80
81
  for (const f of res.findings) {
81
82
  const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
82
83
  console.log(`
@@ -93,7 +94,7 @@ ${icon} ${f.rule} [${f.confidence}]
93
94
  return;
94
95
  }
95
96
  if (cmd === "drift") {
96
- const { drift } = await import("./drift-BEXSL2IX.js");
97
+ const { drift } = await import("./drift-KU7HWL7M.js");
97
98
  const { resolve: resolveDrift } = await import("path");
98
99
  const { existsSync: existsDrift } = await import("fs");
99
100
  const localArg = getArg("local");
@@ -106,6 +107,15 @@ ${icon} ${f.rule} [${f.confidence}]
106
107
  const remotePath = remoteArg ? resolveDrift(rootDir, remoteArg) : null;
107
108
  for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
108
109
  if (p && !existsDrift(p)) {
110
+ if (asJson) {
111
+ const { drift: driftMissing } = await import("./drift-KU7HWL7M.js");
112
+ const res2 = driftMissing(
113
+ localPath && existsDrift(localPath) ? localPath : null,
114
+ remotePath && existsDrift(remotePath) ? remotePath : null
115
+ );
116
+ console.log(JSON.stringify({ version: pkg.version, entries: res2.entries, findings: res2.findings }, null, 2));
117
+ process.exit(1);
118
+ }
109
119
  console.error(`Arquivo ${label} n\xE3o encontrado: ${p}`);
110
120
  process.exit(1);
111
121
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  drift
3
- } from "./chunk-GUSNXUAT.js";
3
+ } from "./chunk-NJBCSW7E.js";
4
4
  export {
5
5
  drift
6
6
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as z from 'zod';
2
2
  import { z as z$1 } from 'zod';
3
+ export { createMcpServer, startMcpServer } from './mcp.js';
4
+ import '@modelcontextprotocol/sdk/server/mcp.js';
3
5
 
4
6
  /**
5
7
  * firebase-audit — schemas centrais (Zod v4).
@@ -566,6 +568,7 @@ declare const FindingSchema: z.ZodObject<{
566
568
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
567
569
  }, z.core.$strict>;
568
570
  declare const AuditYamlSchema: z.ZodObject<{
571
+ $schema: z.ZodOptional<z.ZodString>;
569
572
  authorization: z.ZodObject<{
570
573
  adapter: z.ZodOptional<z.ZodObject<{
571
574
  function: z.ZodString;
@@ -592,6 +595,13 @@ declare const AuditYamlSchema: z.ZodObject<{
592
595
  }, z.core.$strict>>;
593
596
  }, z.core.$strict>;
594
597
  }, z.core.$strict>;
598
+ /** Chaves OIDC/Firebase reservadas — nunca em custom claims (1000 bytes, só acesso). */
599
+ declare const RESERVED_CLAIM_KEYS: readonly string[];
600
+ /** Valida samples de claims: tamanho 1000 bytes + chaves reservadas. */
601
+ declare function validateClaimSamples(samples: Record<string, Record<string, unknown>> | undefined): {
602
+ ok: boolean;
603
+ errors: string[];
604
+ };
595
605
  type ProjectModel = z.infer<typeof ProjectModelSchema>;
596
606
  type Finding = z.infer<typeof FindingSchema>;
597
607
  type AuditYaml = z.infer<typeof AuditYamlSchema>;
@@ -618,15 +628,25 @@ interface DiscoveryResult {
618
628
  serverFiles: string[];
619
629
  }
620
630
  declare function discover(rootDir: string): DiscoveryResult;
621
- /** Segmentos de servidor usados por classifyOrigin (documentados; ai-tool graph na V3). */
631
+ /** Segmentos de servidor (fail-closed: `api` genérico NÃO é server, ver classifyOrigin). */
622
632
  declare const SERVER_HINTS_SEGMENTS: string[];
623
- /** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo/voz do ai-tool na V2). */
633
+ /** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3). */
624
634
  declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
625
635
  declare function emptyModel(rootDir: string, d: DiscoveryResult): ProjectModel;
636
+ /**
637
+ * Fase 3 — enriquece Discovery com grafo do ai-tool (como lib, sem spawn/MCP).
638
+ * Preenche clientFiles/serverFiles a partir do mapa do projeto.
639
+ * Nunca joga: falha do ai-tool mantém listas vazias (honesto).
640
+ */
641
+ declare function enrichWithGraph(rootDir: string, d: DiscoveryResult): Promise<void>;
626
642
 
627
643
  /**
628
644
  * Rules extractor — estrutura + referências + locations (sem avaliar).
629
645
  * Parser leve e intencionalmente incompleto: avaliação real é do Emulator (V3).
646
+ *
647
+ * Limitação conhecida: `ALLOW_RE` corta a condição no primeiro `;` mesmo dentro
648
+ * de string literal (ex: `x != "a;b"`). Raríssimo em Rules reais; documentado aqui
649
+ * em vez de silêncio — Emulator acusa a regra de verdade.
630
650
  */
631
651
 
632
652
  type Rule = z$1.infer<typeof RuleSchema>;
@@ -636,23 +656,77 @@ declare function opsFrom(target: string): {
636
656
  ops: Rule["operations"];
637
657
  known: boolean;
638
658
  };
659
+ /** Normaliza target de allow para fingerprint estável (case/espaço/ordem). */
660
+ declare function normalizeTarget(target: string): string;
661
+ /** Bloco match com posição para concatenação de pais (subcoleções). */
662
+ interface MatchBlock {
663
+ path: string;
664
+ line: number;
665
+ blockStart: number;
666
+ blockEnd: number;
667
+ }
668
+ /** Núcleo comum: blocos match com fim por profundidade (fora de strings). */
669
+ declare function parseMatchBlocks(content: string): MatchBlock[];
670
+ /** Concatena paths ancestrais: /users/{u} + /orders/{o} = /users/{u}/orders/{o}. */
671
+ declare function buildFullPath(containingAsc: MatchBlock[]): string;
672
+ /** Mapa function NAME -> corpo para helpers (isAdmin, isSignedIn — padrão oficial). */
673
+ declare function parseRuleFunctions(content: string): Map<string, string>;
674
+ /** Versão com assinatura (params) para inline de helpers com argumentos. */
675
+ declare function parseRuleFunctionsDetailed(content: string): Map<string, {
676
+ params: string[];
677
+ body: string;
678
+ }>;
679
+ /** Inline textual de args nos params (1 nível, sem aninhamento complexo). */
680
+ declare function inlineHelperArgs(body: string, params: string[], args: string[]): string;
681
+ /** Inline de helpers dentro de expressões: `isAdmin() && x==1` vira `(corpo) && x==1`. */
682
+ declare function inlineHelpersInCondition(condition: string, detailed: Map<string, {
683
+ params: string[];
684
+ body: string;
685
+ }>): string;
686
+ /** Se condição é chamada única `nome(...)`, resolve corpo do helper (1 nível, com args). */
687
+ declare function resolveHelperCondition(condition: string, functions: Map<string, string>): string;
688
+ /** Resolução com substituição de args (para helpers parametrizados oficiais). */
689
+ declare function resolveHelperConditionDetailed(condition: string, detailed: Map<string, {
690
+ params: string[];
691
+ body: string;
692
+ }>): string;
639
693
  declare function extractRules(rulesFile: string, relFile: string): Rule[];
694
+ /** Extrai de conteúdo (evita triple-read + permite teste sem arquivo). */
695
+ declare function extractRulesContent(rawContent: string, relFile: string): Rule[];
696
+ /** Remove apenas parênteses externos balanceados que embrulham tudo. */
697
+ declare function stripOuterParens(s: string): string;
640
698
  /** Normaliza condição para detectar público: sem if, `true`, `(true)`, `|| true`, `auth == null`. */
641
699
  declare function isPublicCondition(condition: string | undefined): {
642
700
  isPublic: boolean;
643
701
  confidence: "CONFIRMED" | "PROBABLE";
644
702
  };
703
+ /** Hash curto e estável (8 hex) para fingerprint v2: imune a formatação/espaço/case. */
704
+ declare function hashShort(text: string): string;
705
+ /** Chave de condição para fingerprint: "uncond" ou hash da condição normalizada. */
706
+ declare function conditionKeyForFingerprint(condition: string | undefined): string;
645
707
  /** Detecta allows públicos preservando linha, path e confiança para FBA001/FBA002. */
646
708
  declare function findPublicAllows(rulesFile: string): {
647
709
  line: number;
648
710
  target: string;
649
711
  path: string;
650
712
  confidence: "CONFIRMED" | "PROBABLE";
713
+ conditionKey: string;
714
+ }[];
715
+ /** Extrai expressão de `return` do corpo do helper para avaliação pública. */
716
+ declare function helperBodyToCondition(body: string): string;
717
+ /** Núcleo com conteúdo (reuso em checks sem triple-read). */
718
+ declare function findPublicAllowsContent(rawContent: string): {
719
+ line: number;
720
+ target: string;
721
+ path: string;
722
+ confidence: "CONFIRMED" | "PROBABLE";
723
+ conditionKey: string;
651
724
  }[];
652
725
 
653
726
  /**
654
727
  * Checks V1+V2 (FBA001–FBA004, FBA009–FBA013, subconjunto intencional).
655
- * Fingerprints: FBA001/FBA002 por regra-lógica (sem linha, estáveis a formatação);
728
+ * Fingerprints v2: FBA001/FBA002 por regra-lógica + hash da condição
729
+ * (estáveis a formatação, únicos por regra duplicada);
656
730
  * FBA003/FBA004/FBA010/FBA012 com linha (únicos por ocorrência).
657
731
  */
658
732
 
@@ -668,17 +742,32 @@ declare function runChecks(model: ProjectModel, rootDir: string, opts?: {
668
742
  interface ScanOptions {
669
743
  adapterFn?: string;
670
744
  strict?: boolean;
745
+ /**
746
+ * Opt-in: enriquece Discovery com grafo do ai-tool (clientFiles/serverFiles).
747
+ * Default false (scan static-first rápido; nenhum check consome o grafo na 0.4.0).
748
+ */
749
+ graph?: boolean;
750
+ }
751
+ interface ScanSummary {
752
+ errors: number;
753
+ warnings: number;
754
+ infos: number;
755
+ /** Compat: mesmo que passedChecks. */
756
+ passed: number;
757
+ /** Compat: mesmo que totalChecks. */
758
+ total: number;
759
+ passedChecks: number;
760
+ totalChecks: number;
761
+ /** Findings de infra (fora dos checks FBA): rules/contrato/índices/model. */
762
+ infraSkipped: number;
763
+ /** Cobertura de Rules (0-100) quando verify com coverage avalia; null = não observada.
764
+ * Sempre null no scan() por desenho (cobertura só via verify coveragePct). */
765
+ coveragePct: number | null;
671
766
  }
672
767
  interface ScanResult {
673
768
  model: ProjectModel;
674
769
  findings: Finding[];
675
- summary: {
676
- errors: number;
677
- warnings: number;
678
- infos: number;
679
- passed: number;
680
- total: number;
681
- };
770
+ summary: ScanSummary;
682
771
  }
683
772
  declare function scan(rootDir: string, opts?: ScanOptions): Promise<ScanResult>;
684
773
 
@@ -704,6 +793,8 @@ interface VerifyResult {
704
793
  cases: VerifyCase[];
705
794
  uncovered: string[];
706
795
  findings: Finding[];
796
+ /** 0-100 quando o coverage tem visitCount avaliável; null = não observada. */
797
+ coveragePct: number | null;
707
798
  }
708
799
  /** Gera matriz role × operação × path a partir do modelo (sem executar nada).
709
800
  * Mapeamento: read≡get+list, write≡create+update+delete (mesma semântica do opsFrom).
@@ -740,4 +831,4 @@ declare function drift(localFile: string | null, remoteFile: string | null): Dri
740
831
 
741
832
  declare const VERSION: string;
742
833
 
743
- export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, classifyOrigin, discover, drift, emptyModel, extractRules, findPublicAllows, isPublicCondition, opsFrom, planVerify, runChecks, scan, stripRuleComments, verify };
834
+ export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RESERVED_CLAIM_KEYS, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, type ScanSummary, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, buildFullPath, classifyOrigin, conditionKeyForFingerprint, discover, drift, emptyModel, enrichWithGraph, extractRules, extractRulesContent, findPublicAllows, findPublicAllowsContent, hashShort, helperBodyToCondition, inlineHelperArgs, inlineHelpersInCondition, isPublicCondition, normalizeTarget, opsFrom, parseMatchBlocks, parseRuleFunctions, parseRuleFunctionsDetailed, planVerify, resolveHelperCondition, resolveHelperConditionDetailed, runChecks, scan, stripOuterParens, stripRuleComments, validateClaimSamples, verify };
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ createMcpServer,
3
+ startMcpServer
4
+ } from "./chunk-GOLWQBD5.js";
1
5
  import {
2
6
  AccessOriginSchema,
3
7
  AuditYamlSchema,
@@ -17,28 +21,46 @@ import {
17
21
  QueryFilterSchema,
18
22
  QueryOrderSchema,
19
23
  QueryShapeSchema,
24
+ RESERVED_CLAIM_KEYS,
20
25
  RoleSchema,
21
26
  RuleSchema,
22
27
  SERVER_HINTS_SEGMENTS,
23
28
  SeveritySchema,
29
+ buildFullPath,
24
30
  classifyOrigin,
31
+ conditionKeyForFingerprint,
25
32
  discover,
26
33
  emptyModel,
34
+ enrichWithGraph,
27
35
  extractRules,
36
+ extractRulesContent,
28
37
  findPublicAllows,
38
+ findPublicAllowsContent,
39
+ hashShort,
40
+ helperBodyToCondition,
41
+ inlineHelperArgs,
42
+ inlineHelpersInCondition,
29
43
  isPublicCondition,
44
+ normalizeTarget,
30
45
  opsFrom,
46
+ parseMatchBlocks,
47
+ parseRuleFunctions,
48
+ parseRuleFunctionsDetailed,
49
+ resolveHelperCondition,
50
+ resolveHelperConditionDetailed,
31
51
  runChecks,
32
52
  scan,
33
- stripRuleComments
34
- } from "./chunk-UMQH7UDI.js";
53
+ stripOuterParens,
54
+ stripRuleComments,
55
+ validateClaimSamples
56
+ } from "./chunk-3SEVYYCX.js";
35
57
  import {
36
58
  planVerify,
37
59
  verify
38
- } from "./chunk-CPEIPRCF.js";
60
+ } from "./chunk-PSSMYRRU.js";
39
61
  import {
40
62
  drift
41
- } from "./chunk-GUSNXUAT.js";
63
+ } from "./chunk-NJBCSW7E.js";
42
64
 
43
65
  // src/index.ts
44
66
  import { createRequire } from "module";
@@ -64,22 +86,42 @@ export {
64
86
  QueryFilterSchema,
65
87
  QueryOrderSchema,
66
88
  QueryShapeSchema,
89
+ RESERVED_CLAIM_KEYS,
67
90
  RoleSchema,
68
91
  RuleSchema,
69
92
  SERVER_HINTS_SEGMENTS,
70
93
  SeveritySchema,
71
94
  VERSION,
95
+ buildFullPath,
72
96
  classifyOrigin,
97
+ conditionKeyForFingerprint,
98
+ createMcpServer,
73
99
  discover,
74
100
  drift,
75
101
  emptyModel,
102
+ enrichWithGraph,
76
103
  extractRules,
104
+ extractRulesContent,
77
105
  findPublicAllows,
106
+ findPublicAllowsContent,
107
+ hashShort,
108
+ helperBodyToCondition,
109
+ inlineHelperArgs,
110
+ inlineHelpersInCondition,
78
111
  isPublicCondition,
112
+ normalizeTarget,
79
113
  opsFrom,
114
+ parseMatchBlocks,
115
+ parseRuleFunctions,
116
+ parseRuleFunctionsDetailed,
80
117
  planVerify,
118
+ resolveHelperCondition,
119
+ resolveHelperConditionDetailed,
81
120
  runChecks,
82
121
  scan,
122
+ startMcpServer,
123
+ stripOuterParens,
83
124
  stripRuleComments,
125
+ validateClaimSamples,
84
126
  verify
85
127
  };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node