@kinkai.cloud/vibecheck 0.1.6 → 0.2.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.
Files changed (2) hide show
  1. package/dist/cli.js +1545 -402
  2. package/package.json +12 -2
package/dist/cli.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
3
+ var __export = (target, all2) => {
4
+ for (var name in all2)
5
+ __defProp(target, name, { get: all2[name], enumerable: true });
6
6
  };
7
7
 
8
8
  // ../../packages/checks/src/types.ts
9
+ var SEVERITIES = ["fatal", "critical", "high", "medium", "low"];
9
10
  var NODE_KINDS = [
10
11
  "browser",
11
12
  "app",
@@ -17,6 +18,7 @@ var NODE_KINDS = [
17
18
  "payments",
18
19
  "secrets"
19
20
  ];
21
+ var CATEGORIES = ["secrets", "data", "storage", "auth", "ai", "config"];
20
22
 
21
23
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
22
24
  var external_exports = {};
@@ -14533,164 +14535,401 @@ function date4(params) {
14533
14535
  config(en_default());
14534
14536
 
14535
14537
  // ../../packages/graph/src/types.ts
14536
- var ARESTA_TIPOS = ["imports", "calls", "contains"];
14537
- var MAX_NOS = 400;
14538
- var MAX_ARESTAS = 800;
14539
- var MAX_COMUNIDADES = 9;
14540
- var MAX_ARQUIVOS_POR_NO = 12;
14541
- var MAX_ARQUIVOS_COMUNIDADE = 8;
14538
+ var EDGE_TYPES = ["imports", "calls", "contains"];
14539
+ var MAX_NODES = 400;
14540
+ var MAX_EDGES = 800;
14541
+ var MAX_COMMUNITIES = 9;
14542
+ var MAX_FILES_PER_NODE = 12;
14543
+ var MAX_COMMUNITY_FILES = 8;
14542
14544
  var MAX_BYTES = 256e3;
14543
- var MAX_ROTULO = 80;
14545
+ var MAX_LABEL = 80;
14544
14546
  var MAX_ID = 64;
14545
- var MAX_CAMINHO = 180;
14546
- var MAX_PACOTES = 32;
14547
- var caminho = external_exports.string().min(1).max(MAX_CAMINHO);
14548
- var noSchema = external_exports.object({
14547
+ var MAX_PATH = 180;
14548
+ var MAX_PACKAGES = 32;
14549
+ var MAX_HINT_ROUTES = 80;
14550
+ var MAX_HINT_TABLES = 40;
14551
+ var MAX_HINT_BUCKETS = 24;
14552
+ var MAX_HINT_COLLECTIONS = 24;
14553
+ var EMPTY_DATA = { tables: [], buckets: [], collections: [] };
14554
+ var pathSchema = external_exports.string().min(1).max(MAX_PATH);
14555
+ var nodeSchema = external_exports.object({
14549
14556
  id: external_exports.string().min(1).max(MAX_ID),
14550
14557
  kind: external_exports.enum(NODE_KINDS),
14551
- rotulo: external_exports.string().min(1).max(MAX_ROTULO),
14552
- arquivos: external_exports.array(caminho).max(MAX_ARQUIVOS_POR_NO)
14558
+ label: external_exports.string().min(1).max(MAX_LABEL),
14559
+ files: external_exports.array(pathSchema).max(MAX_FILES_PER_NODE)
14553
14560
  });
14554
- var arestaSchema = external_exports.object({
14555
- de: external_exports.string().min(1).max(MAX_ID),
14556
- para: external_exports.string().min(1).max(MAX_ID),
14557
- tipo: external_exports.enum(ARESTA_TIPOS)
14561
+ var edgeSchema = external_exports.object({
14562
+ from: external_exports.string().min(1).max(MAX_ID),
14563
+ to: external_exports.string().min(1).max(MAX_ID),
14564
+ type: external_exports.enum(EDGE_TYPES)
14558
14565
  });
14559
- var comunidadeSchema = external_exports.object({
14566
+ var communitySchema = external_exports.object({
14560
14567
  nodeKind: external_exports.enum(NODE_KINDS),
14561
- rotulo: external_exports.string().min(1).max(MAX_ROTULO),
14562
- arquivos: external_exports.array(caminho).max(MAX_ARQUIVOS_COMUNIDADE)
14568
+ label: external_exports.string().min(1).max(MAX_LABEL),
14569
+ files: external_exports.array(pathSchema).max(MAX_COMMUNITY_FILES)
14563
14570
  });
14564
- var grafoScanSchema = external_exports.object({
14565
- versao: external_exports.literal(1),
14566
- avancado: external_exports.boolean(),
14567
- nos: external_exports.array(noSchema).max(MAX_NOS),
14568
- arestas: external_exports.array(arestaSchema).max(MAX_ARESTAS),
14569
- comunidades: external_exports.array(comunidadeSchema).max(MAX_COMUNIDADES),
14570
- pacotes: external_exports.array(external_exports.string().min(1).max(MAX_ROTULO)).max(MAX_PACOTES)
14571
+ var tableName = external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,62}$/);
14572
+ var bucketName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/);
14573
+ var dataSchema = external_exports.object({
14574
+ tables: external_exports.array(tableName).max(MAX_HINT_TABLES),
14575
+ buckets: external_exports.array(bucketName).max(MAX_HINT_BUCKETS),
14576
+ collections: external_exports.array(tableName).max(MAX_HINT_COLLECTIONS)
14571
14577
  });
14578
+ var scanGraphSchema = external_exports.object({
14579
+ version: external_exports.literal(1),
14580
+ advanced: external_exports.boolean(),
14581
+ nodes: external_exports.array(nodeSchema).max(MAX_NODES),
14582
+ edges: external_exports.array(edgeSchema).max(MAX_EDGES),
14583
+ communities: external_exports.array(communitySchema).max(MAX_COMMUNITIES),
14584
+ packages: external_exports.array(external_exports.string().min(1).max(MAX_LABEL)).max(MAX_PACKAGES),
14585
+ data: dataSchema.optional()
14586
+ });
14587
+ var EMPTY_HINTS = {
14588
+ apiRoutes: [],
14589
+ tableNames: [],
14590
+ buckets: [],
14591
+ collections: []
14592
+ };
14572
14593
 
14573
- // ../../packages/graph/src/dicas.ts
14574
- function rotaDeArquivo(caminho2) {
14575
- const normal = caminho2.replaceAll("\\", "/");
14594
+ // ../../packages/graph/src/hints.ts
14595
+ function uniqueCapped(values, cap) {
14596
+ const seen = /* @__PURE__ */ new Set();
14597
+ const out = [];
14598
+ for (const value of values) {
14599
+ if (out.length >= cap) break;
14600
+ if (seen.has(value)) continue;
14601
+ seen.add(value);
14602
+ out.push(value);
14603
+ }
14604
+ return out;
14605
+ }
14606
+ function routeFromFile(path) {
14607
+ const normal = path.replaceAll("\\", "/");
14576
14608
  const app = /(?:^|\/)app\/(.+)\/route\.(t|j)sx?$/i.exec(normal);
14577
14609
  if (app?.[1] !== void 0) {
14578
- const segmentos = app[1].split("/").filter((s) => s.length > 0 && !s.startsWith("(") && !s.startsWith("@")).map((s) => s.replace(/^\[+\.\.\.(.+)\]+$/, "*").replace(/^\[+(.+)\]+$/, ":$1"));
14579
- if (segmentos.length === 0) return "/api";
14580
- return `/${segmentos.join("/")}`;
14610
+ const segments = app[1].split("/").filter((s) => s.length > 0 && !s.startsWith("(") && !s.startsWith("@")).map((s) => s.replace(/^\[+\.\.\.(.+)\]+$/, "*").replace(/^\[+(.+)\]+$/, ":$1"));
14611
+ if (segments.length === 0) return "/api";
14612
+ return `/${segments.join("/")}`;
14581
14613
  }
14582
14614
  const pages = /(?:^|\/)pages\/api\/(.+)\.(t|j)sx?$/i.exec(normal);
14583
14615
  if (pages?.[1] !== void 0) {
14584
- const semIndex = pages[1].replace(/\/index$/i, "");
14585
- return `/api/${semIndex}`;
14616
+ const withoutIndex = pages[1].replace(/\/index$/i, "");
14617
+ return `/api/${withoutIndex}`;
14586
14618
  }
14587
14619
  return null;
14588
14620
  }
14621
+ function filesFromGraph(graph) {
14622
+ return [
14623
+ ...graph.nodes.flatMap((n) => n.files),
14624
+ ...graph.communities.flatMap((c) => c.files)
14625
+ ];
14626
+ }
14627
+ function hintsFromGraph(graph) {
14628
+ if (graph === null || graph === void 0 || !graph.advanced) return EMPTY_HINTS;
14629
+ const routes = [];
14630
+ for (const file2 of filesFromGraph(graph)) {
14631
+ const route = routeFromFile(file2);
14632
+ if (route !== null) routes.push(route);
14633
+ }
14634
+ routes.sort((a, b) => a.localeCompare(b));
14635
+ const data = graph.data ?? EMPTY_DATA;
14636
+ return {
14637
+ apiRoutes: uniqueCapped(routes, MAX_HINT_ROUTES),
14638
+ tableNames: uniqueCapped(data.tables, MAX_HINT_TABLES),
14639
+ buckets: uniqueCapped(data.buckets, MAX_HINT_BUCKETS),
14640
+ collections: uniqueCapped(data.collections, MAX_HINT_COLLECTIONS)
14641
+ };
14642
+ }
14643
+
14644
+ // ../../packages/graph/src/metrics.ts
14645
+ function importCycles(graph) {
14646
+ const adjacencia = /* @__PURE__ */ new Map();
14647
+ for (const aresta of graph.edges) {
14648
+ if (aresta.type !== "imports") continue;
14649
+ const vizinhos = adjacencia.get(aresta.from) ?? [];
14650
+ vizinhos.push(aresta.to);
14651
+ adjacencia.set(aresta.from, vizinhos);
14652
+ }
14653
+ const indice = /* @__PURE__ */ new Map();
14654
+ const lowlink = /* @__PURE__ */ new Map();
14655
+ const naPilha = /* @__PURE__ */ new Set();
14656
+ const pilha = [];
14657
+ let proximo = 0;
14658
+ let ciclos = 0;
14659
+ for (const raiz of graph.nodes.map((n) => n.id)) {
14660
+ if (indice.has(raiz)) continue;
14661
+ const quadros = [{ no: raiz, vizinho: 0 }];
14662
+ indice.set(raiz, proximo);
14663
+ lowlink.set(raiz, proximo);
14664
+ proximo += 1;
14665
+ pilha.push(raiz);
14666
+ naPilha.add(raiz);
14667
+ while (quadros.length > 0) {
14668
+ const quadro = quadros[quadros.length - 1];
14669
+ if (quadro === void 0) break;
14670
+ const vizinhos = adjacencia.get(quadro.no) ?? [];
14671
+ if (quadro.vizinho < vizinhos.length) {
14672
+ const vizinho = vizinhos[quadro.vizinho];
14673
+ quadro.vizinho += 1;
14674
+ if (!indice.has(vizinho)) {
14675
+ indice.set(vizinho, proximo);
14676
+ lowlink.set(vizinho, proximo);
14677
+ proximo += 1;
14678
+ pilha.push(vizinho);
14679
+ naPilha.add(vizinho);
14680
+ quadros.push({ no: vizinho, vizinho: 0 });
14681
+ } else if (naPilha.has(vizinho)) {
14682
+ lowlink.set(
14683
+ quadro.no,
14684
+ Math.min(lowlink.get(quadro.no) ?? 0, indice.get(vizinho) ?? 0)
14685
+ );
14686
+ }
14687
+ continue;
14688
+ }
14689
+ quadros.pop();
14690
+ const pai = quadros[quadros.length - 1];
14691
+ if (pai !== void 0) {
14692
+ lowlink.set(
14693
+ pai.no,
14694
+ Math.min(lowlink.get(pai.no) ?? 0, lowlink.get(quadro.no) ?? 0)
14695
+ );
14696
+ }
14697
+ if (lowlink.get(quadro.no) === indice.get(quadro.no)) {
14698
+ let tamanho = 0;
14699
+ let atual;
14700
+ do {
14701
+ atual = pilha.pop();
14702
+ if (atual === void 0) break;
14703
+ naPilha.delete(atual);
14704
+ tamanho += 1;
14705
+ } while (atual !== quadro.no);
14706
+ if (tamanho > 1) ciclos += 1;
14707
+ }
14708
+ }
14709
+ }
14710
+ return ciclos;
14711
+ }
14712
+ function graphMetrics(graph) {
14713
+ return {
14714
+ nodes: graph.nodes.length,
14715
+ edges: graph.edges.length,
14716
+ routes: hintsFromGraph(graph).apiRoutes.length,
14717
+ packages: graph.packages.length,
14718
+ cycles: importCycles(graph)
14719
+ };
14720
+ }
14589
14721
 
14590
- // ../../packages/graph/src/mapear.ts
14591
- var ROTULO_KIND = {
14592
- browser: "navegador",
14593
- app: "aplica\xE7\xE3o",
14722
+ // ../../packages/graph/src/identifiers.ts
14723
+ var FROM_CALL = /([A-Za-z_$][\w$]*)\s*\.\s*from\(\s*["'`]([A-Za-z0-9._-]{1,63})["'`]\s*\)/g;
14724
+ var NOT_A_DATABASE = /* @__PURE__ */ new Set([
14725
+ "array",
14726
+ "buffer",
14727
+ "object",
14728
+ "string",
14729
+ "number",
14730
+ "date",
14731
+ "map",
14732
+ "set",
14733
+ "promise",
14734
+ "uint8array",
14735
+ "int8array",
14736
+ "float32array",
14737
+ "float64array",
14738
+ "blob",
14739
+ "json",
14740
+ "bigint",
14741
+ "proxy",
14742
+ "reflect"
14743
+ ]);
14744
+ var STORAGE_URL = /\/storage\/v1\/object\/(?:public|sign)\/([A-Za-z0-9._-]{1,63})\//g;
14745
+ var COLLECTION_DOT = /\.\s*collection\(\s*["'`]([A-Za-z][A-Za-z0-9_-]{0,62})["'`]\s*\)/g;
14746
+ var COLLECTION_MODULAR = /\bcollection\(\s*[^,)"'`]+,\s*["'`]([A-Za-z][A-Za-z0-9_-]{0,62})["'`]/g;
14747
+ var VALID_TABLE = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
14748
+ var VALID_BUCKET = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/;
14749
+ var SECRETISH = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|sb_publishable_|eyJ[A-Za-z0-9_-]{10,}|gsk_|sk-ant-|sk-proj-|sk-or-v1-|AIza)/i;
14750
+ function capture(content, pattern, group = 1) {
14751
+ const re = new RegExp(pattern.source, pattern.flags);
14752
+ const out = [];
14753
+ let m;
14754
+ while ((m = re.exec(content)) !== null) {
14755
+ const value = m[group];
14756
+ if (value !== void 0) out.push(value);
14757
+ if (m[0].length === 0) re.lastIndex += 1;
14758
+ }
14759
+ return out;
14760
+ }
14761
+ function identifiersFromCode(content) {
14762
+ const tables = [];
14763
+ const buckets = [];
14764
+ const re = new RegExp(FROM_CALL.source, FROM_CALL.flags);
14765
+ let m;
14766
+ while ((m = re.exec(content)) !== null) {
14767
+ const receiver = m[1];
14768
+ const name = m[2];
14769
+ if (receiver === void 0 || name === void 0) continue;
14770
+ if (NOT_A_DATABASE.has(receiver.toLowerCase())) continue;
14771
+ if (receiver.toLowerCase() === "storage") {
14772
+ if (VALID_BUCKET.test(name)) buckets.push(name);
14773
+ continue;
14774
+ }
14775
+ if (VALID_TABLE.test(name)) tables.push(name);
14776
+ }
14777
+ for (const name of capture(content, STORAGE_URL)) {
14778
+ if (VALID_BUCKET.test(name)) buckets.push(name);
14779
+ }
14780
+ const collections = [
14781
+ ...capture(content, COLLECTION_DOT),
14782
+ ...capture(content, COLLECTION_MODULAR)
14783
+ ].filter((name) => VALID_TABLE.test(name));
14784
+ return {
14785
+ tables: tables.filter((n) => !SECRETISH.test(n)),
14786
+ buckets: buckets.filter((n) => !SECRETISH.test(n)),
14787
+ collections: collections.filter((n) => !SECRETISH.test(n))
14788
+ };
14789
+ }
14790
+ function mergeIdentifiers(parts, cap) {
14791
+ const dedup = (values, max) => {
14792
+ const seen = /* @__PURE__ */ new Set();
14793
+ const out = [];
14794
+ for (const value of values) {
14795
+ const key = value.toLowerCase();
14796
+ if (seen.has(key)) continue;
14797
+ seen.add(key);
14798
+ out.push(value);
14799
+ if (out.length >= max) break;
14800
+ }
14801
+ return out;
14802
+ };
14803
+ return {
14804
+ tables: dedup(
14805
+ parts.flatMap((p) => p.tables),
14806
+ cap.tables
14807
+ ),
14808
+ buckets: dedup(
14809
+ parts.flatMap((p) => p.buckets),
14810
+ cap.buckets
14811
+ ),
14812
+ collections: dedup(
14813
+ parts.flatMap((p) => p.collections),
14814
+ cap.collections
14815
+ )
14816
+ };
14817
+ }
14818
+ function isValidDataName(raw, kind) {
14819
+ if (typeof raw !== "string") return null;
14820
+ const cleaned = raw.trim();
14821
+ if (cleaned.length === 0) return null;
14822
+ const pattern = kind === "bucket" ? VALID_BUCKET : VALID_TABLE;
14823
+ if (!pattern.test(cleaned)) return null;
14824
+ if (SECRETISH.test(cleaned)) return null;
14825
+ return cleaned;
14826
+ }
14827
+
14828
+ // ../../packages/graph/src/map.ts
14829
+ var KIND_LABEL = {
14830
+ browser: "browser",
14831
+ app: "application",
14594
14832
  api: "API",
14595
- db: "banco",
14596
- storage: "arquivos",
14597
- auth: "autentica\xE7\xE3o",
14598
- ai: "IA",
14599
- payments: "pagamentos",
14600
- secrets: "segredos"
14601
- };
14602
- var REGRAS = [
14833
+ db: "database",
14834
+ storage: "files",
14835
+ auth: "authentication",
14836
+ ai: "AI",
14837
+ payments: "payments",
14838
+ secrets: "security"
14839
+ };
14840
+ var RULES = [
14603
14841
  {
14604
14842
  kind: "api",
14605
- teste: /app\/.+\/route\.(t|j)sx?\b|pages\/api\//i
14843
+ test: /app\/.+\/route\.(t|j)sx?\b|pages\/api\//i
14606
14844
  },
14607
- { kind: "payments", teste: /\b(stripe|pagarme|mercadopago|paypal|adyen)\b/i },
14608
- { kind: "auth", teste: /\b(clerk|next-auth|auth0|lucia|kinde|supabase\/auth)\b/i },
14609
- { kind: "ai", teste: /\b(openai|anthropic|groq|ollama|\bllm\b|langchain)\b/i },
14610
- { kind: "storage", teste: /\b(storage|s3|r2|bucket|firebase\/storage)\b/i },
14845
+ { kind: "payments", test: /\b(stripe|pagarme|mercadopago|paypal|adyen)\b/i },
14846
+ { kind: "auth", test: /\b(clerk|next-auth|auth0|lucia|kinde|supabase\/auth)\b/i },
14847
+ { kind: "ai", test: /\b(openai|anthropic|groq|ollama|\bllm\b|langchain)\b/i },
14848
+ { kind: "storage", test: /\b(storage|s3|r2|bucket|firebase\/storage)\b/i },
14611
14849
  {
14612
14850
  kind: "db",
14613
- teste: /\b(supabase|prisma|drizzle|postgres|mongodb|firebase|firestore|kysely)\b/i
14851
+ test: /\b(supabase|prisma|drizzle|postgres|mongodb|firebase|firestore|kysely)\b/i
14614
14852
  },
14615
- { kind: "secrets", teste: /\b(secret|credentials|vault)\b/i },
14616
- { kind: "browser", teste: /\b(middleware\.(t|j)s|proxy\.(t|j)s)\b/i }
14853
+ { kind: "secrets", test: /\b(secret|credentials|vault)\b/i },
14854
+ { kind: "browser", test: /\b(middleware\.(t|j)s|proxy\.(t|j)s)\b/i }
14617
14855
  ];
14618
- function mapearKind(sinal) {
14619
- const texto = `${sinal.rotulo} ${sinal.arquivos.join(" ")}`;
14620
- for (const regra of REGRAS) {
14621
- if (regra.teste.test(texto)) return regra.kind;
14856
+ function mapKind(signal) {
14857
+ const text = `${signal.label} ${signal.files.join(" ")}`;
14858
+ for (const rule of RULES) {
14859
+ if (rule.test.test(text)) return rule.kind;
14622
14860
  }
14623
14861
  return "app";
14624
14862
  }
14625
14863
 
14626
- // ../../packages/graph/src/pacotes.ts
14627
- var CARA_DE_CHAVE = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
14628
- var REGRAS2 = [
14629
- { id: "next", teste: /^next(?:\/|$)/ },
14630
- { id: "react", teste: /^react(?:-dom)?(?:\/|$)/ },
14631
- { id: "vue", teste: /^vue(?:\/|$)/ },
14632
- { id: "svelte", teste: /^svelte(?:\/|$)/ },
14633
- { id: "angular", teste: /^@angular\// },
14634
- { id: "vite", teste: /^vite(?:\/|$)/ },
14635
- { id: "astro", teste: /^astro(?:\/|$)/ },
14636
- { id: "stripe", teste: /^(?:stripe|@stripe\/)/ },
14637
- { id: "supabase", teste: /^(?:@supabase\/|@supabase$)/ },
14638
- { id: "firebase", teste: /^(?:firebase|@firebase\/)/ },
14639
- { id: "clerk", teste: /^@clerk\// },
14640
- { id: "next-auth", teste: /^(?:next-auth|@auth\/)/ },
14641
- { id: "prisma", teste: /^(?:@prisma\/client|prisma)(?:\/|$)/ },
14642
- { id: "drizzle", teste: /^drizzle-orm(?:\/|$)/ },
14643
- { id: "kysely", teste: /^kysely(?:\/|$)/ },
14644
- { id: "postgres", teste: /^(?:postgres|pg|@neondatabase\/)/ },
14645
- { id: "mongodb", teste: /^(?:mongodb|mongoose)(?:\/|$)/ },
14646
- { id: "styled-components", teste: /^styled-components(?:\/|$)/ },
14647
- { id: "emotion", teste: /^@emotion\// },
14648
- { id: "tailwind", teste: /^tailwindcss(?:\/|$)/ },
14649
- { id: "openai", teste: /^openai(?:\/|$)/ },
14650
- { id: "anthropic", teste: /^@anthropic-ai\// },
14651
- { id: "langchain", teste: /^langchain(?:\/|$)/ },
14652
- { id: "trpc", teste: /^@trpc\// },
14653
- { id: "hono", teste: /^hono(?:\/|$)/ },
14654
- { id: "express", teste: /^express(?:\/|$)/ },
14655
- { id: "fastify", teste: /^fastify(?:\/|$)/ },
14656
- { id: "graphql", teste: /^(?:graphql|@apollo\/)/ },
14657
- { id: "redis", teste: /^(?:redis|ioredis)(?:\/|$)/ },
14658
- { id: "resend", teste: /^resend(?:\/|$)/ },
14659
- { id: "auth0", teste: /^(?:auth0|@auth0\/)/ },
14660
- { id: "lucia", teste: /^lucia(?:\/|$)/ },
14661
- { id: "kinde", teste: /^@kinde-oss\// }
14864
+ // ../../packages/graph/src/packages.ts
14865
+ var SECRETISH2 = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
14866
+ var RULES2 = [
14867
+ { id: "next", test: /^next(?:\/|$)/ },
14868
+ { id: "react", test: /^react(?:-dom)?(?:\/|$)/ },
14869
+ { id: "vue", test: /^vue(?:\/|$)/ },
14870
+ { id: "svelte", test: /^svelte(?:\/|$)/ },
14871
+ { id: "angular", test: /^@angular\// },
14872
+ { id: "vite", test: /^vite(?:\/|$)/ },
14873
+ { id: "astro", test: /^astro(?:\/|$)/ },
14874
+ { id: "stripe", test: /^(?:stripe|@stripe\/)/ },
14875
+ { id: "supabase", test: /^(?:@supabase\/|@supabase$)/ },
14876
+ { id: "firebase", test: /^(?:firebase|@firebase\/)/ },
14877
+ { id: "clerk", test: /^@clerk\// },
14878
+ { id: "next-auth", test: /^(?:next-auth|@auth\/)/ },
14879
+ { id: "prisma", test: /^(?:@prisma\/client|prisma)(?:\/|$)/ },
14880
+ { id: "drizzle", test: /^drizzle-orm(?:\/|$)/ },
14881
+ { id: "kysely", test: /^kysely(?:\/|$)/ },
14882
+ { id: "postgres", test: /^(?:postgres|pg|@neondatabase\/)/ },
14883
+ { id: "mongodb", test: /^(?:mongodb|mongoose)(?:\/|$)/ },
14884
+ { id: "styled-components", test: /^styled-components(?:\/|$)/ },
14885
+ { id: "emotion", test: /^@emotion\// },
14886
+ { id: "tailwind", test: /^tailwindcss(?:\/|$)/ },
14887
+ { id: "openai", test: /^openai(?:\/|$)/ },
14888
+ { id: "anthropic", test: /^@anthropic-ai\// },
14889
+ { id: "langchain", test: /^langchain(?:\/|$)/ },
14890
+ { id: "trpc", test: /^@trpc\// },
14891
+ { id: "hono", test: /^hono(?:\/|$)/ },
14892
+ { id: "express", test: /^express(?:\/|$)/ },
14893
+ { id: "fastify", test: /^fastify(?:\/|$)/ },
14894
+ { id: "graphql", test: /^(?:graphql|@apollo\/)/ },
14895
+ { id: "redis", test: /^(?:redis|ioredis)(?:\/|$)/ },
14896
+ { id: "resend", test: /^resend(?:\/|$)/ },
14897
+ { id: "auth0", test: /^(?:auth0|@auth0\/)/ },
14898
+ { id: "lucia", test: /^lucia(?:\/|$)/ },
14899
+ { id: "kinde", test: /^@kinde-oss\// }
14662
14900
  ];
14663
- function idDePacote(spec) {
14664
- const limpo = spec.trim().replace(/^npm:/, "");
14665
- if (limpo.length === 0 || limpo.startsWith(".") || limpo.startsWith("/")) return null;
14666
- if (limpo.startsWith("node:") || limpo.startsWith("#")) return null;
14667
- for (const regra of REGRAS2) {
14668
- if (regra.teste.test(limpo)) return regra.id;
14901
+ function packageId(spec) {
14902
+ const cleaned = spec.trim().replace(/^npm:/, "");
14903
+ if (cleaned.length === 0 || cleaned.startsWith(".") || cleaned.startsWith("/"))
14904
+ return null;
14905
+ if (cleaned.startsWith("node:") || cleaned.startsWith("#")) return null;
14906
+ for (const rule of RULES2) {
14907
+ if (rule.test.test(cleaned)) return rule.id;
14669
14908
  }
14670
14909
  return null;
14671
14910
  }
14672
- function sanitizarPacote(bruto) {
14673
- const limpo = bruto.trim().slice(0, MAX_ROTULO);
14674
- if (limpo.length === 0) return null;
14675
- if (CARA_DE_CHAVE.test(limpo)) return null;
14676
- if (!/^[@a-z0-9][a-z0-9._+/-]{0,60}$/i.test(limpo)) return null;
14677
- return limpo;
14911
+ function sanitizePackage(raw) {
14912
+ const cleaned = raw.trim().slice(0, MAX_LABEL);
14913
+ if (cleaned.length === 0) return null;
14914
+ if (SECRETISH2.test(cleaned)) return null;
14915
+ if (!/^[@a-z0-9][a-z0-9._+/-]{0,60}$/i.test(cleaned)) return null;
14916
+ return cleaned;
14678
14917
  }
14679
- function uniquePacotes(values) {
14918
+ function uniquePackages(values) {
14680
14919
  const seen = /* @__PURE__ */ new Set();
14681
14920
  const out = [];
14682
14921
  for (const value of values) {
14683
- if (out.length >= MAX_PACOTES) break;
14684
- const limpo = sanitizarPacote(value);
14685
- if (limpo === null || seen.has(limpo)) continue;
14686
- seen.add(limpo);
14687
- out.push(limpo);
14922
+ if (out.length >= MAX_PACKAGES) break;
14923
+ const cleaned = sanitizePackage(value);
14924
+ if (cleaned === null || seen.has(cleaned)) continue;
14925
+ seen.add(cleaned);
14926
+ out.push(cleaned);
14688
14927
  }
14689
14928
  return out;
14690
14929
  }
14691
14930
 
14692
14931
  // ../../packages/graph/src/sanitize.ts
14693
- var CAMPOS_CORPO = /* @__PURE__ */ new Set([
14932
+ var BODY_FIELDS = /* @__PURE__ */ new Set([
14694
14933
  "content",
14695
14934
  "snippet",
14696
14935
  "code",
@@ -14702,34 +14941,43 @@ var CAMPOS_CORPO = /* @__PURE__ */ new Set([
14702
14941
  "raw",
14703
14942
  "bytes"
14704
14943
  ]);
14705
- var EXTENSOES_SEGREDO = /\.(env|pem|key|p12|pfx|crt|cer|p8|keystore)(?:\.|$)/i;
14706
- var SEGMENTO_PROIBIDO = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|credentials|secrets?)(?:\/|$)/i;
14707
- var CARA_DE_CHAVE2 = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|sb_publishable_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
14708
- var MAX_ROTULO_ARQUIVO = MAX_CAMINHO;
14709
- function eRecord(value) {
14944
+ var SECRET_EXTENSIONS = /\.(env|pem|key|p12|pfx|crt|cer|p8|keystore)(?:\.|$)/i;
14945
+ var FORBIDDEN_SEGMENT = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|credentials|secrets?)(?:\/|$)/i;
14946
+ var SECRETISH3 = /(sk_live_|sk_test_|rk_live_|pk_live_|pk_test_|sb_secret_|sb_publishable_|eyJ[A-Za-z0-9_-]{20,}|gsk_[A-Za-z0-9]|sk-ant-|sk-proj-|sk-or-v1-)/i;
14947
+ var MAX_FILE_LABEL = MAX_PATH;
14948
+ function isRecord(value) {
14710
14949
  return typeof value === "object" && value !== null && !Array.isArray(value);
14711
14950
  }
14712
- function comoString(value) {
14951
+ function asString(value) {
14713
14952
  return typeof value === "string" && value.length > 0 ? value : null;
14714
14953
  }
14715
- function sanitizarCaminho(bruto) {
14716
- const cortado = bruto.trim().replaceAll("\\", "/");
14717
- if (cortado.length === 0 || cortado.length > MAX_ROTULO_ARQUIVO) return null;
14718
- if (cortado.includes("\0")) return null;
14719
- if (cortado.startsWith("/") || /^[a-zA-Z]:/.test(cortado)) return null;
14720
- if (cortado.split("/").some((p) => p === ".." || p === ".")) return null;
14721
- if (EXTENSOES_SEGREDO.test(cortado) || SEGMENTO_PROIBIDO.test(cortado)) return null;
14722
- if (/(?:^|\/)node_modules\//.test(cortado)) return null;
14723
- if (/(?:^|\/)\.git\//.test(cortado)) return null;
14724
- return cortado.slice(0, MAX_CAMINHO);
14725
- }
14726
- function redigirRotulo(bruto) {
14727
- const limpo = bruto.trim().slice(0, MAX_ROTULO);
14728
- if (limpo.length === 0) return "n\xF3";
14729
- if (CARA_DE_CHAVE2.test(limpo)) return "[redigido]";
14730
- return limpo;
14731
- }
14732
- function uniqueCapped(values, cap) {
14954
+ function sanitizePath(raw) {
14955
+ const trimmed = raw.trim().replaceAll("\\", "/");
14956
+ if (trimmed.length === 0 || trimmed.length > MAX_FILE_LABEL) return null;
14957
+ if (trimmed.includes("\0")) return null;
14958
+ if (trimmed.startsWith("/") || /^[a-zA-Z]:/.test(trimmed)) return null;
14959
+ if (trimmed.split("/").some((p) => p === ".." || p === ".")) return null;
14960
+ if (SECRET_EXTENSIONS.test(trimmed) || FORBIDDEN_SEGMENT.test(trimmed)) return null;
14961
+ if (/(?:^|\/)node_modules\//.test(trimmed)) return null;
14962
+ if (/(?:^|\/)\.git\//.test(trimmed)) return null;
14963
+ return trimmed.slice(0, MAX_PATH);
14964
+ }
14965
+ function redactLabel(raw) {
14966
+ const cleaned = raw.trim().slice(0, MAX_LABEL);
14967
+ if (cleaned.length === 0) return "node";
14968
+ if (SECRETISH3.test(cleaned)) return "[redacted]";
14969
+ if (looksLikePath(cleaned) && sanitizePath(cleaned) === null) return "[redacted]";
14970
+ return cleaned;
14971
+ }
14972
+ function looksLikePath(value) {
14973
+ return /[\\/]/.test(value) || /^[a-zA-Z]:/.test(value);
14974
+ }
14975
+ function opaque(value) {
14976
+ let h = 5381;
14977
+ for (let i = 0; i < value.length; i += 1) h = (h << 5) + h + value.charCodeAt(i) | 0;
14978
+ return `p${(h >>> 0).toString(36)}`;
14979
+ }
14980
+ function uniqueCapped2(values, cap) {
14733
14981
  const seen = /* @__PURE__ */ new Set();
14734
14982
  const out = [];
14735
14983
  for (const value of values) {
@@ -14740,252 +14988,417 @@ function uniqueCapped(values, cap) {
14740
14988
  }
14741
14989
  return out;
14742
14990
  }
14743
- function arquivosDe(value) {
14744
- const brutos = [];
14745
- if (typeof value === "string") brutos.push(value);
14991
+ function filesFrom(value) {
14992
+ const raw = [];
14993
+ if (typeof value === "string") raw.push(value);
14746
14994
  if (Array.isArray(value)) {
14747
14995
  for (const item of value) {
14748
- if (typeof item === "string") brutos.push(item);
14996
+ if (typeof item === "string") raw.push(item);
14749
14997
  }
14750
14998
  }
14751
- const limpos = [];
14752
- for (const bruto of brutos) {
14753
- const caminho2 = sanitizarCaminho(bruto);
14754
- if (caminho2 !== null) limpos.push(caminho2);
14999
+ const cleaned = [];
15000
+ for (const item of raw) {
15001
+ const path = sanitizePath(item);
15002
+ if (path !== null) cleaned.push(path);
14755
15003
  }
14756
- return uniqueCapped(limpos, MAX_ARQUIVOS_POR_NO);
15004
+ return uniqueCapped2(cleaned, MAX_FILES_PER_NODE);
14757
15005
  }
14758
- function idDe(value, fallback) {
14759
- const bruto = comoString(value) ?? fallback;
14760
- return bruto.trim().slice(0, MAX_ID).replace(/[^\w.:/-]/g, "_") || fallback;
15006
+ function idFrom(value, fallback) {
15007
+ const raw = (asString(value) ?? fallback).trim().slice(0, MAX_ID);
15008
+ const safe = raw.split("::").map(
15009
+ (part) => looksLikePath(part) && sanitizePath(part) === null ? opaque(part) : part
15010
+ ).join("::");
15011
+ return safe.replace(/[^\w.:/-]/g, "_") || fallback;
14761
15012
  }
14762
- function tipoAresta(value) {
14763
- const bruto = (comoString(value) ?? "contains").toLowerCase();
14764
- if (bruto.includes("import")) return "imports";
14765
- if (bruto.includes("call")) return "calls";
15013
+ function edgeType(value) {
15014
+ const raw = (asString(value) ?? "contains").toLowerCase();
15015
+ if (raw.includes("import")) return "imports";
15016
+ if (raw.includes("call")) return "calls";
14766
15017
  return "contains";
14767
15018
  }
14768
- function stripCorpo(value) {
14769
- if (Array.isArray(value)) return value.map(stripCorpo);
14770
- if (!eRecord(value)) return value;
15019
+ function stripBody(value) {
15020
+ if (Array.isArray(value)) return value.map(stripBody);
15021
+ if (!isRecord(value)) return value;
14771
15022
  const out = {};
14772
- for (const [chave, item] of Object.entries(value)) {
14773
- if (CAMPOS_CORPO.has(chave)) continue;
14774
- if (chave === "source" && typeof item === "string" && (item.includes("\n") || item.length > MAX_ID)) {
15023
+ for (const [key, item] of Object.entries(value)) {
15024
+ if (BODY_FIELDS.has(key)) continue;
15025
+ if (key === "source" && typeof item === "string" && (item.includes("\n") || item.length > MAX_ID)) {
14775
15026
  continue;
14776
15027
  }
14777
- out[chave] = stripCorpo(item);
15028
+ out[key] = stripBody(item);
14778
15029
  }
14779
15030
  return out;
14780
15031
  }
14781
- function primeiroExport(value) {
15032
+ function firstExport(value) {
14782
15033
  if (typeof value === "string" && value.length > 0) {
14783
- const primeiro = value.split(/[\s,]+/)[0];
14784
- return primeiro !== void 0 && primeiro.length > 0 ? primeiro : null;
15034
+ const first = value.split(/[\s,]+/)[0];
15035
+ return first !== void 0 && first.length > 0 ? first : null;
14785
15036
  }
14786
15037
  if (!Array.isArray(value)) return null;
14787
15038
  for (const item of value) {
14788
15039
  if (typeof item === "string" && item.length > 0) return item;
14789
- if (eRecord(item)) {
14790
- const nome = comoString(item["name"]) ?? comoString(item["identifier"]);
14791
- if (nome !== null) return nome;
15040
+ if (isRecord(item)) {
15041
+ const name = asString(item["name"]) ?? asString(item["identifier"]);
15042
+ if (name !== null) return name;
14792
15043
  }
14793
15044
  }
14794
15045
  return null;
14795
15046
  }
14796
- function nosDoDump(dump) {
14797
- const bruto = dump["nodes"] ?? dump["nos"];
14798
- const lista = Array.isArray(bruto) ? bruto : eRecord(bruto) ? Object.entries(bruto).map(
14799
- ([id, n]) => eRecord(n) ? { id, ...n } : { id }
14800
- ) : [];
14801
- const nos = [];
14802
- for (const [i, item] of lista.entries()) {
14803
- if (!eRecord(item)) continue;
14804
- const arquivos = arquivosDe(
14805
- item["arquivos"] ?? item["files"] ?? item["source_file"] ?? item["sourceFile"] ?? item["file"] ?? item["path"]
15047
+ function nodesFromDump(dump) {
15048
+ const raw = dump["nodes"] ?? dump["nos"];
15049
+ const list = Array.isArray(raw) ? raw : isRecord(raw) ? Object.entries(raw).map(([id, n]) => isRecord(n) ? { id, ...n } : { id }) : [];
15050
+ const nodes = [];
15051
+ for (const [i, item] of list.entries()) {
15052
+ if (!isRecord(item)) continue;
15053
+ const files = filesFrom(
15054
+ item["files"] ?? item["files"] ?? item["source_file"] ?? item["sourceFile"] ?? item["file"] ?? item["path"]
14806
15055
  );
14807
- const rotulo = redigirRotulo(
14808
- comoString(item["rotulo"]) ?? comoString(item["label"]) ?? comoString(item["name"]) ?? comoString(item["identifier"]) ?? comoString(item["symbol"]) ?? primeiroExport(item["exports"] ?? item["identificadores"]) ?? arquivos[0] ?? `n\xF3-${String(i)}`
15056
+ const label = redactLabel(
15057
+ asString(item["label"]) ?? asString(item["label"]) ?? asString(item["name"]) ?? asString(item["identifier"]) ?? asString(item["symbol"]) ?? firstExport(item["exports"] ?? item["identificadores"]) ?? files[0] ?? `node-${String(i)}`
14809
15058
  );
14810
- const kindBruto = comoString(item["kind"]) ?? comoString(item["nodeKind"]);
14811
- const kind = kindBruto !== null && NODE_KINDS.includes(kindBruto) ? kindBruto : mapearKind({ rotulo, arquivos });
14812
- nos.push({
14813
- id: idDe(item["id"], `n${String(i)}`),
15059
+ const kindRaw = asString(item["kind"]) ?? asString(item["nodeKind"]);
15060
+ const kind = kindRaw !== null && NODE_KINDS.includes(kindRaw) ? kindRaw : mapKind({ label, files });
15061
+ nodes.push({
15062
+ id: idFrom(item["id"], `n${String(i)}`),
14814
15063
  kind,
14815
- rotulo,
14816
- arquivos
15064
+ label,
15065
+ files
14817
15066
  });
14818
- if (nos.length >= MAX_NOS) break;
15067
+ if (nodes.length >= MAX_NODES) break;
14819
15068
  }
14820
- return nos;
15069
+ return nodes;
14821
15070
  }
14822
- function arestasDoDump(dump, ids) {
14823
- const bruto = dump["edges"] ?? dump["arestas"];
14824
- const lista = Array.isArray(bruto) ? bruto : [];
15071
+ function edgesFromDump(dump, ids) {
15072
+ const raw = dump["edges"] ?? dump["edges"];
15073
+ const list = Array.isArray(raw) ? raw : [];
14825
15074
  const out = [];
14826
15075
  const seen = /* @__PURE__ */ new Set();
14827
- for (const item of lista) {
14828
- if (!eRecord(item)) continue;
14829
- const de = idDe(item["de"] ?? item["source"] ?? item["from"], "");
14830
- const para = idDe(item["para"] ?? item["target"] ?? item["to"], "");
14831
- if (de.length === 0 || para.length === 0) continue;
14832
- if (!ids.has(de) || !ids.has(para)) continue;
14833
- const tipo = tipoAresta(item["tipo"] ?? item["relation"] ?? item["kind"]);
14834
- const chave = `${de}>${para}:${tipo}`;
14835
- if (seen.has(chave)) continue;
14836
- seen.add(chave);
14837
- out.push({ de, para, tipo });
14838
- if (out.length >= MAX_ARESTAS) break;
15076
+ for (const item of list) {
15077
+ if (!isRecord(item)) continue;
15078
+ const from = idFrom(item["from"] ?? item["de"] ?? item["source"], "");
15079
+ const to = idFrom(item["to"] ?? item["para"] ?? item["target"], "");
15080
+ if (from.length === 0 || to.length === 0) continue;
15081
+ if (!ids.has(from) || !ids.has(to)) continue;
15082
+ const type = edgeType(
15083
+ item["type"] ?? item["tipo"] ?? item["relation"] ?? item["kind"]
15084
+ );
15085
+ const key = `${from}>${to}:${type}`;
15086
+ if (seen.has(key)) continue;
15087
+ seen.add(key);
15088
+ out.push({ from, to, type });
15089
+ if (out.length >= MAX_EDGES) break;
14839
15090
  }
14840
15091
  return out;
14841
15092
  }
14842
- function comunidadesDoDump(dump, nos) {
14843
- const jaCurto = dump["comunidades"];
14844
- if (Array.isArray(jaCurto)) {
15093
+ function communitiesFromDump(dump, nodes) {
15094
+ const alreadyShort = dump["communities"] ?? dump["communities"];
15095
+ if (Array.isArray(alreadyShort)) {
14845
15096
  const out2 = [];
14846
15097
  const seen = /* @__PURE__ */ new Set();
14847
- for (const item of jaCurto) {
14848
- if (!eRecord(item)) continue;
14849
- const kindBruto = comoString(item["nodeKind"]) ?? comoString(item["kind"]);
14850
- if (kindBruto === null || !NODE_KINDS.includes(kindBruto)) {
15098
+ for (const item of alreadyShort) {
15099
+ if (!isRecord(item)) continue;
15100
+ const kindRaw = asString(item["nodeKind"]) ?? asString(item["kind"]);
15101
+ if (kindRaw === null || !NODE_KINDS.includes(kindRaw)) {
14851
15102
  continue;
14852
15103
  }
14853
- const nodeKind = kindBruto;
15104
+ const nodeKind = kindRaw;
14854
15105
  if (seen.has(nodeKind)) {
14855
- const atual = out2.find((c) => c.nodeKind === nodeKind);
14856
- if (atual === void 0) continue;
14857
- const extra = arquivosDe(item["arquivos"] ?? item["files"]);
14858
- const merged = uniqueCapped([...atual.arquivos, ...extra], MAX_ARQUIVOS_COMUNIDADE);
14859
- out2.splice(out2.indexOf(atual), 1, {
15106
+ const current = out2.find((c) => c.nodeKind === nodeKind);
15107
+ if (current === void 0) continue;
15108
+ const extra = filesFrom(item["files"] ?? item["files"]);
15109
+ const merged = uniqueCapped2([...current.files, ...extra], MAX_COMMUNITY_FILES);
15110
+ out2.splice(out2.indexOf(current), 1, {
14860
15111
  nodeKind,
14861
- rotulo: atual.rotulo,
14862
- arquivos: merged
15112
+ label: current.label,
15113
+ files: merged
14863
15114
  });
14864
15115
  continue;
14865
15116
  }
14866
15117
  seen.add(nodeKind);
14867
15118
  out2.push({
14868
15119
  nodeKind,
14869
- rotulo: ROTULO_KIND[nodeKind],
14870
- arquivos: uniqueCapped(
14871
- arquivosDe(item["arquivos"] ?? item["files"]),
14872
- MAX_ARQUIVOS_COMUNIDADE
15120
+ label: KIND_LABEL[nodeKind],
15121
+ files: uniqueCapped2(
15122
+ filesFrom(item["files"] ?? item["files"]),
15123
+ MAX_COMMUNITY_FILES
14873
15124
  )
14874
15125
  });
14875
- if (out2.length >= MAX_COMUNIDADES) break;
15126
+ if (out2.length >= MAX_COMMUNITIES) break;
14876
15127
  }
14877
15128
  return out2;
14878
15129
  }
14879
- const porKind = /* @__PURE__ */ new Map();
14880
- for (const no of nos) {
14881
- const atual = porKind.get(no.kind) ?? { rotulo: no.rotulo, arquivos: [] };
14882
- atual.arquivos.push(...no.arquivos);
14883
- porKind.set(no.kind, atual);
15130
+ const byKind = /* @__PURE__ */ new Map();
15131
+ for (const node of nodes) {
15132
+ const current = byKind.get(node.kind) ?? { label: node.label, files: [] };
15133
+ current.files.push(...node.files);
15134
+ byKind.set(node.kind, current);
14884
15135
  }
14885
15136
  const out = [];
14886
15137
  for (const kind of NODE_KINDS) {
14887
- const grupo = porKind.get(kind);
14888
- if (grupo === void 0) continue;
14889
- const arquivos = uniqueCapped(grupo.arquivos, MAX_ARQUIVOS_COMUNIDADE);
14890
- if (arquivos.length === 0 && grupo.rotulo.length === 0) continue;
14891
- out.push({ nodeKind: kind, rotulo: ROTULO_KIND[kind], arquivos });
14892
- if (out.length >= MAX_COMUNIDADES) break;
15138
+ const group = byKind.get(kind);
15139
+ if (group === void 0) continue;
15140
+ const files = uniqueCapped2(group.files, MAX_COMMUNITY_FILES);
15141
+ if (files.length === 0 && group.label.length === 0) continue;
15142
+ out.push({ nodeKind: kind, label: KIND_LABEL[kind], files });
15143
+ if (out.length >= MAX_COMMUNITIES) break;
14893
15144
  }
14894
15145
  return out;
14895
15146
  }
14896
- function sanitizarGrafo(entrada) {
14897
- if (entrada === null || entrada === void 0) return { ok: false, motivo: "invalido" };
15147
+ function sanitizeGraph(input) {
15148
+ if (input === null || input === void 0) return { ok: false, reason: "invalid" };
14898
15149
  let json2;
14899
15150
  try {
14900
- json2 = JSON.stringify(entrada);
15151
+ json2 = JSON.stringify(input);
14901
15152
  } catch {
14902
- return { ok: false, motivo: "invalido" };
14903
- }
14904
- if (json2.length > MAX_BYTES) return { ok: false, motivo: "grande" };
14905
- const limpo = stripCorpo(entrada);
14906
- if (!eRecord(limpo)) return { ok: false, motivo: "invalido" };
14907
- const nos = nosDoDump(limpo);
14908
- const ids = new Set(nos.map((n) => n.id));
14909
- const arestas = arestasDoDump(limpo, ids);
14910
- const comunidades = comunidadesDoDump(limpo, nos);
14911
- const pacotes = uniquePacotes(pacotesDoDump(limpo, nos));
14912
- const temArquivo = comunidades.some((c) => c.arquivos.length > 0) || nos.some((n) => n.arquivos.length > 0);
14913
- if (!temArquivo) return { ok: false, motivo: "vazio" };
14914
- const avancado = comunidades.length > 0;
15153
+ return { ok: false, reason: "invalid" };
15154
+ }
15155
+ if (json2.length > MAX_BYTES) return { ok: false, reason: "too-large" };
15156
+ const cleaned = stripBody(input);
15157
+ if (!isRecord(cleaned)) return { ok: false, reason: "invalid" };
15158
+ const nodes = nodesFromDump(cleaned);
15159
+ const ids = new Set(nodes.map((n) => n.id));
15160
+ const edges = edgesFromDump(cleaned, ids);
15161
+ const communities = communitiesFromDump(cleaned, nodes);
15162
+ const packages = uniquePackages(packagesFromDump(cleaned, nodes));
15163
+ const data = dataFromDump(cleaned);
15164
+ const hasFile = communities.some((c) => c.files.length > 0) || nodes.some((n) => n.files.length > 0);
15165
+ if (!hasFile) return { ok: false, reason: "empty" };
15166
+ const advanced = communities.length > 0;
14915
15167
  return {
14916
15168
  ok: true,
14917
- grafo: {
14918
- versao: 1,
14919
- avancado,
14920
- nos,
14921
- arestas,
14922
- comunidades,
14923
- pacotes
15169
+ graph: {
15170
+ version: 1,
15171
+ advanced,
15172
+ nodes,
15173
+ edges,
15174
+ communities,
15175
+ packages,
15176
+ ...data === void 0 ? {} : { data }
15177
+ }
15178
+ };
15179
+ }
15180
+ function dataFromDump(dump) {
15181
+ const raw = dump["data"] ?? dump["dados"];
15182
+ if (!isRecord(raw)) return void 0;
15183
+ const clean = (value, kind, cap) => {
15184
+ if (!Array.isArray(value)) return [];
15185
+ const cleaned = [];
15186
+ for (const item of value) {
15187
+ const name = isValidDataName(item, kind);
15188
+ if (name !== null) cleaned.push(name);
14924
15189
  }
15190
+ return uniqueCapped2(cleaned, cap);
15191
+ };
15192
+ const data = {
15193
+ tables: clean(raw["tables"] ?? raw["tables"], "table", MAX_HINT_TABLES),
15194
+ buckets: clean(raw["buckets"], "bucket", MAX_HINT_BUCKETS),
15195
+ collections: clean(
15196
+ raw["collections"] ?? raw["collections"],
15197
+ "table",
15198
+ MAX_HINT_COLLECTIONS
15199
+ )
14925
15200
  };
15201
+ if (data.tables.length === 0 && data.buckets.length === 0 && data.collections.length === 0) {
15202
+ return void 0;
15203
+ }
15204
+ return data;
14926
15205
  }
14927
- function pacotesDoDump(dump, nos) {
14928
- const brutos = [];
14929
- const campo = dump["pacotes"] ?? dump["packages"];
14930
- if (Array.isArray(campo)) {
14931
- for (const item of campo) {
14932
- if (typeof item === "string") brutos.push(item);
15206
+ function packagesFromDump(dump, nodes) {
15207
+ const raw = [];
15208
+ const field = dump["packages"] ?? dump["packages"];
15209
+ if (Array.isArray(field)) {
15210
+ for (const item of field) {
15211
+ if (typeof item === "string") raw.push(item);
14933
15212
  }
14934
15213
  }
14935
- for (const no of nos) {
14936
- if (no.id.startsWith("pkg:")) brutos.push(no.rotulo);
15214
+ for (const node of nodes) {
15215
+ if (node.id.startsWith("pkg:")) raw.push(node.label);
14937
15216
  }
14938
- return brutos;
15217
+ return raw;
14939
15218
  }
14940
15219
 
14941
- // src/graphify.ts
14942
- import { spawn } from "child_process";
14943
- import { readFile } from "fs/promises";
14944
- import { join } from "path";
14945
- async function tentarGraphify(raiz) {
14946
- const json2 = join(raiz, "graphify-out", "graph.json");
14947
- try {
14948
- return JSON.parse(await readFile(json2, "utf8"));
14949
- } catch {
14950
- }
14951
- const ok = await new Promise((resolve) => {
14952
- const child = spawn("graphify", [".", "--no-viz"], {
14953
- cwd: raiz,
14954
- stdio: "ignore"
14955
- });
14956
- const timer = setTimeout(() => {
14957
- child.kill();
14958
- resolve(false);
14959
- }, 2e4);
14960
- child.on("exit", (code) => {
14961
- clearTimeout(timer);
14962
- resolve(code === 0);
14963
- });
14964
- child.on("error", () => {
14965
- clearTimeout(timer);
14966
- resolve(false);
14967
- });
14968
- });
14969
- if (!ok) return null;
14970
- try {
14971
- return JSON.parse(await readFile(json2, "utf8"));
14972
- } catch {
14973
- return null;
14974
- }
14975
- }
15220
+ // ../../packages/schema/src/quality-gate.ts
15221
+ var QUALITY_GATE_POLICY = "2026-code-gate-v1";
15222
+ var GATE_STATUSES = ["passed", "failed", "inconclusive"];
15223
+ var HTTP_SCAN_OUTCOMES = [
15224
+ "ran",
15225
+ "skipped-quota",
15226
+ "skipped-flag",
15227
+ "failed"
15228
+ ];
15229
+ var ISSUE_SOURCES = ["code", "http"];
15230
+ var REPROVA = ["fatal", "critical", "high"];
15231
+ function reprova(severity) {
15232
+ return REPROVA.includes(severity);
15233
+ }
15234
+ function contarPorSeveridade(issues) {
15235
+ const counts = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
15236
+ for (const issue2 of issues) counts[issue2.severity] += 1;
15237
+ return counts;
15238
+ }
15239
+ function analysed(report) {
15240
+ return report.codeFiles > 0 || report.httpScan === "ran";
15241
+ }
15242
+ function statusFromIssues(issues, httpScan, analisou) {
15243
+ if (!analisou) return "inconclusive";
15244
+ if (issues.some((i) => reprova(i.severity))) return "failed";
15245
+ return httpScan === "failed" ? "inconclusive" : "passed";
15246
+ }
15247
+ var GATE_EXIT = {
15248
+ passed: 0,
15249
+ failed: 1,
15250
+ inconclusive: 2,
15251
+ /** Erro operacional: autenticação, transporte, persistência. */
15252
+ operational: 3
15253
+ };
15254
+ function exitCodeFromStatus(status) {
15255
+ return GATE_EXIT[status];
15256
+ }
15257
+ var MAX_ISSUES = 200;
15258
+ var MAX_REASON = 200;
15259
+ var MAX_PATH2 = 180;
15260
+ var issueSchema = external_exports.object({
15261
+ checkId: external_exports.string().min(1).max(80),
15262
+ severity: external_exports.enum(SEVERITIES),
15263
+ path: external_exports.string().min(1).max(MAX_PATH2),
15264
+ reason: external_exports.string().min(1).max(MAX_REASON),
15265
+ source: external_exports.enum(ISSUE_SOURCES)
15266
+ });
15267
+ var metricsSchema = external_exports.object({
15268
+ nodes: external_exports.number().int().nonnegative(),
15269
+ edges: external_exports.number().int().nonnegative(),
15270
+ routes: external_exports.number().int().nonnegative(),
15271
+ packages: external_exports.number().int().nonnegative(),
15272
+ cycles: external_exports.number().int().nonnegative()
15273
+ });
15274
+ var qualityGateSchema = external_exports.object({
15275
+ policy: external_exports.literal(QUALITY_GATE_POLICY),
15276
+ status: external_exports.enum(GATE_STATUSES),
15277
+ httpScan: external_exports.enum(HTTP_SCAN_OUTCOMES),
15278
+ issues: external_exports.array(issueSchema).max(MAX_ISSUES),
15279
+ counts: external_exports.record(external_exports.enum(SEVERITIES), external_exports.number().int().nonnegative()),
15280
+ metrics: metricsSchema.nullable(),
15281
+ codeFiles: external_exports.number().int().nonnegative(),
15282
+ ranAt: external_exports.string().min(1)
15283
+ });
14976
15284
 
14977
- // src/login.ts
14978
- import { spawn as spawn2 } from "child_process";
15285
+ // ../../packages/schema/src/stages.ts
15286
+ var SCAN_STAGES = [
15287
+ "connecting",
15288
+ "reading-app",
15289
+ "secrets",
15290
+ "data-files",
15291
+ "api-ai",
15292
+ "config",
15293
+ "verdict"
15294
+ ];
15295
+ var CHECK_OUTCOMES = ["passed", "found", "skipped", "error"];
15296
+ var SKIP_REASONS = ["requires-verified", "requires-consent"];
15297
+
15298
+ // ../../packages/schema/src/events.ts
15299
+ var VERDICTS = ["sealed", "exposed"];
15300
+ var nodeId = external_exports.enum(NODE_KINDS);
15301
+ var scanEventSchema = external_exports.discriminatedUnion("type", [
15302
+ external_exports.object({
15303
+ type: external_exports.literal("node.discovered"),
15304
+ nodeId,
15305
+ kind: external_exports.enum(NODE_KINDS),
15306
+ label: external_exports.string().min(1),
15307
+ parentId: nodeId.optional()
15308
+ }),
15309
+ external_exports.object({ type: external_exports.literal("node.probing"), nodeId }),
15310
+ external_exports.object({ type: external_exports.literal("node.sealed"), nodeId }),
15311
+ external_exports.object({
15312
+ type: external_exports.literal("finding.found"),
15313
+ checkId: external_exports.string().min(1),
15314
+ severity: external_exports.enum(SEVERITIES),
15315
+ nodeId,
15316
+ proof: external_exports.string().min(1),
15317
+ subject: external_exports.string().optional()
15318
+ }),
15319
+ external_exports.object({
15320
+ type: external_exports.literal("scan.done"),
15321
+ verdict: external_exports.enum(VERDICTS),
15322
+ exposureCount: external_exports.number().int().nonnegative(),
15323
+ exposedNodes: external_exports.number().int().nonnegative(),
15324
+ stack: external_exports.object({
15325
+ language: external_exports.array(external_exports.string()),
15326
+ framework: external_exports.array(external_exports.string()),
15327
+ styling: external_exports.array(external_exports.string())
15328
+ }).optional(),
15329
+ coverage: external_exports.object({
15330
+ bundles: external_exports.number().int().nonnegative(),
15331
+ routes: external_exports.number().int().nonnegative(),
15332
+ tables: external_exports.number().int().nonnegative(),
15333
+ buckets: external_exports.number().int().nonnegative(),
15334
+ withGraph: external_exports.boolean()
15335
+ }).optional()
15336
+ }),
15337
+ external_exports.object({ type: external_exports.literal("scan.failed"), reason: external_exports.string().min(1) }),
15338
+ external_exports.object({
15339
+ type: external_exports.literal("graph.ready"),
15340
+ communities: external_exports.array(
15341
+ external_exports.object({
15342
+ nodeKind: external_exports.enum(NODE_KINDS),
15343
+ label: external_exports.string().min(1),
15344
+ files: external_exports.array(external_exports.string())
15345
+ })
15346
+ ),
15347
+ nodes: external_exports.array(
15348
+ external_exports.object({
15349
+ id: external_exports.string().min(1),
15350
+ kind: external_exports.enum(NODE_KINDS),
15351
+ label: external_exports.string().min(1),
15352
+ files: external_exports.array(external_exports.string())
15353
+ })
15354
+ ).optional(),
15355
+ edges: external_exports.array(
15356
+ external_exports.object({
15357
+ from: external_exports.string().min(1),
15358
+ to: external_exports.string().min(1),
15359
+ type: external_exports.enum(["imports", "calls", "contains"])
15360
+ })
15361
+ ).optional(),
15362
+ packages: external_exports.array(external_exports.string()).optional(),
15363
+ advanced: external_exports.boolean()
15364
+ }),
15365
+ external_exports.object({
15366
+ type: external_exports.literal("stage.started"),
15367
+ stage: external_exports.enum(SCAN_STAGES),
15368
+ index: external_exports.number().int().nonnegative(),
15369
+ total: external_exports.number().int().positive(),
15370
+ checks: external_exports.array(external_exports.object({ checkId: external_exports.string().min(1), title: external_exports.string().min(1) }))
15371
+ }),
15372
+ external_exports.object({ type: external_exports.literal("stage.done"), stage: external_exports.enum(SCAN_STAGES) }),
15373
+ external_exports.object({
15374
+ type: external_exports.literal("check.done"),
15375
+ checkId: external_exports.string().min(1),
15376
+ title: external_exports.string().min(1),
15377
+ category: external_exports.enum(CATEGORIES),
15378
+ nodeId,
15379
+ outcome: external_exports.enum(CHECK_OUTCOMES),
15380
+ reason: external_exports.enum(SKIP_REASONS).optional()
15381
+ })
15382
+ ]);
15383
+ var sequencedScanEventSchema = external_exports.intersection(
15384
+ scanEventSchema,
15385
+ external_exports.object({ seq: external_exports.number().int().nonnegative() })
15386
+ );
15387
+
15388
+ // src/consent.ts
15389
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
15390
+ import { createInterface } from "readline/promises";
15391
+ import { join as join2, resolve } from "path";
14979
15392
 
14980
15393
  // src/token.ts
14981
15394
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
14982
15395
  import { homedir } from "os";
14983
- import { join as join2 } from "path";
15396
+ import { join } from "path";
14984
15397
  function dirConfig() {
14985
- return join2(homedir(), ".config", "vibecheck");
15398
+ return join(homedir(), ".config", "vibecheck");
14986
15399
  }
14987
15400
  function caminhoToken() {
14988
- return join2(dirConfig(), "token");
15401
+ return join(dirConfig(), "token");
14989
15402
  }
14990
15403
  function lerToken() {
14991
15404
  const env = process.env["VIBECHECK_TOKEN"];
@@ -15012,7 +15425,515 @@ ${host}
15012
15425
  chmodSync(destino, 384);
15013
15426
  }
15014
15427
 
15428
+ // src/consent.ts
15429
+ var ARQUIVO = "code-consent.json";
15430
+ var ENV_CI_ALLOW = "VIBECHECK_ALLOW_CODE_SCAN";
15431
+ function caminho(deps) {
15432
+ return join2((deps.configDir ?? dirConfig)(), ARQUIVO);
15433
+ }
15434
+ function repoKey(dir) {
15435
+ return resolve(dir);
15436
+ }
15437
+ function listarConsentimentos(deps = {}) {
15438
+ try {
15439
+ const bruto = JSON.parse(readFileSync2(caminho(deps), "utf8"));
15440
+ if (!Array.isArray(bruto)) return [];
15441
+ return bruto.filter(
15442
+ (e) => e !== null && typeof e === "object" && typeof e.repo === "string" && typeof e.acceptedAt === "string"
15443
+ );
15444
+ } catch {
15445
+ return [];
15446
+ }
15447
+ }
15448
+ function gravar(entradas, deps) {
15449
+ const dir = (deps.configDir ?? dirConfig)();
15450
+ mkdirSync2(dir, { recursive: true, mode: 448 });
15451
+ const arquivo = join2(dir, ARQUIVO);
15452
+ writeFileSync2(arquivo, `${JSON.stringify(entradas, null, 2)}
15453
+ `, { mode: 384 });
15454
+ chmodSync2(arquivo, 384);
15455
+ }
15456
+ function temConsentimento(dir, deps = {}) {
15457
+ const chave = repoKey(dir);
15458
+ return listarConsentimentos(deps).some((e) => e.repo === chave);
15459
+ }
15460
+ function registrarConsentimento(dir, deps = {}) {
15461
+ const chave = repoKey(dir);
15462
+ const atuais = listarConsentimentos(deps).filter((e) => e.repo !== chave);
15463
+ const quando = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
15464
+ gravar([...atuais, { repo: chave, acceptedAt: quando }], deps);
15465
+ }
15466
+ function revogarConsentimento(dir, deps = {}) {
15467
+ const chave = repoKey(dir);
15468
+ const atuais = listarConsentimentos(deps);
15469
+ const restantes = atuais.filter((e) => e.repo !== chave);
15470
+ if (restantes.length === atuais.length) return false;
15471
+ gravar(restantes, deps);
15472
+ return true;
15473
+ }
15474
+ var TEXTO_DO_PEDIDO = [
15475
+ "VibeCheck is about to read the source files in this repository, on this machine,",
15476
+ "to run the code checks.",
15477
+ "",
15478
+ "What leaves your machine: the check id, its severity, the file path and the",
15479
+ "catalog sentence. Never a line of code, never a snippet, never a value.",
15480
+ ""
15481
+ ].join("\n");
15482
+ async function resolverConsentimento(dir, opcoes, deps = {}) {
15483
+ if (temConsentimento(dir, deps)) return "already";
15484
+ const env = deps.env ?? process.env;
15485
+ if (opcoes.ci) {
15486
+ if (env[ENV_CI_ALLOW] !== "1") return "needs-ci-env";
15487
+ registrarConsentimento(dir, deps);
15488
+ return "granted";
15489
+ }
15490
+ const perguntar = deps.ask ?? (async (pergunta) => {
15491
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
15492
+ try {
15493
+ return await rl.question(pergunta);
15494
+ } finally {
15495
+ rl.close();
15496
+ }
15497
+ });
15498
+ const resposta = await perguntar(`${TEXTO_DO_PEDIDO}Read this repository? [y/N] `);
15499
+ if (!/^y(es)?$/i.test(resposta.trim())) return "denied";
15500
+ registrarConsentimento(dir, deps);
15501
+ return "granted";
15502
+ }
15503
+
15504
+ // src/follow.ts
15505
+ function eventsUrl(base, scanId, shareToken) {
15506
+ return `${base}/api/scan/${scanId}/events?t=${encodeURIComponent(shareToken)}`;
15507
+ }
15508
+ async function followScan(base, scanId, shareToken, options = {}) {
15509
+ const fetchFn = options.fetch ?? fetch;
15510
+ const events = [];
15511
+ let exit = 0;
15512
+ const resposta = await fetchFn(eventsUrl(base, scanId, shareToken));
15513
+ if (!resposta.ok || resposta.body === null) {
15514
+ return { events, final: null, exit: 0, opened: false };
15515
+ }
15516
+ const leitor = resposta.body.getReader();
15517
+ const decoder = new TextDecoder();
15518
+ let buf = "";
15519
+ while (true) {
15520
+ const { done, value } = await leitor.read();
15521
+ if (done) break;
15522
+ buf += decoder.decode(value, { stream: true });
15523
+ const blocos = buf.split("\n\n");
15524
+ buf = blocos.pop() ?? "";
15525
+ for (const bloco of blocos) {
15526
+ const linha = bloco.split("\n").find((l) => l.startsWith("data: "));
15527
+ if (linha === void 0) continue;
15528
+ let evento;
15529
+ try {
15530
+ evento = JSON.parse(linha.slice(6));
15531
+ } catch {
15532
+ continue;
15533
+ }
15534
+ if (typeof evento !== "object" || evento === null || !("type" in evento)) continue;
15535
+ events.push(evento);
15536
+ options.onEvent?.(evento);
15537
+ if (evento.type === "finding.found" && (evento.severity === "fatal" || evento.severity === "critical")) {
15538
+ exit = 1;
15539
+ }
15540
+ if (evento.type === "scan.done" || evento.type === "scan.failed") {
15541
+ if (evento.type === "scan.failed") exit = 1;
15542
+ return { events, final: evento, exit, opened: true };
15543
+ }
15544
+ }
15545
+ }
15546
+ return { events, final: null, exit, opened: true };
15547
+ }
15548
+
15549
+ // src/gate.ts
15550
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
15551
+ import { join as join3 } from "path";
15552
+
15553
+ // ../../packages/checks/src/code/types.ts
15554
+ var API_ROUTE_FILE = /(^|\/)(app\/.*\/route\.(ts|js|tsx|jsx)|pages\/api\/.+\.(ts|js))$/;
15555
+ function isApiRoute(path) {
15556
+ return API_ROUTE_FILE.test(path);
15557
+ }
15558
+
15559
+ // ../../packages/checks/src/code/checks.ts
15560
+ var MIGRATION_FILE = /(^|\/)supabase\/migrations\/[^/]+\.sql$/;
15561
+ var CREATE_TABLE = /create\s+table\s+(?:if\s+not\s+exists\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?/gi;
15562
+ var ENABLE_RLS = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:"?[a-z_][a-z0-9_]*"?\.)?"?([a-z_][a-z0-9_]*)"?\s+enable\s+row\s+level\s+security/gi;
15563
+ function all(pattern, text) {
15564
+ const out = [];
15565
+ for (const m of text.matchAll(pattern)) {
15566
+ const nome = m[1];
15567
+ if (nome !== void 0) out.push(nome.toLowerCase());
15568
+ }
15569
+ return out;
15570
+ }
15571
+ function isClientFile(file2) {
15572
+ if (/^\s*['"]use client['"]/.test(file2.content)) return true;
15573
+ if (/\.client\.(tsx|jsx|ts|js)$/.test(file2.path)) return true;
15574
+ return /(^|\/)pages\/(?!api\/).+\.(tsx|jsx|ts|js)$/.test(file2.path);
15575
+ }
15576
+ var TOUCHES_DB = /\b(supabase\.(?:from|rpc|storage)\b|prisma\.\w+\.\w+\(|drizzle\(|\bdb\.(?:select|insert|update|delete|query|execute|from)\(|\.from\(['"]|sql`|pool\.query\(|knex\(|mongoose\.|\.collection\(['"])/;
15577
+ var HAS_AUTH = /\b(getSession|getUser|auth\(\)|currentUser|requireAuth|verifyToken|verifyJwt|jwtVerify|authorization|Bearer|withAuth|clerk|next-auth|getServerSession|session)\b/i;
15578
+ var PUBLIC_ROUTE = /(^|\/)(webhooks?|health|healthz|ping|status|robots|sitemap|og|opengraph|cron)(\/|\.|$)/i;
15579
+ var AI_SDK = /from\s+['"](openai|@anthropic-ai\/sdk|ai|@ai-sdk\/[^'"]+|@google\/generative-ai|groq-sdk|cohere-ai|@mistralai\/mistralai)['"]/;
15580
+ var HAS_LIMITER = /\b(ratelimit|rateLimit|Ratelimit|upstash|limiter|throttle|slidingWindow|tokenBucket)\b/;
15581
+ var STRIPE_IMPORT = /from\s+['"]stripe['"]/;
15582
+ var WEBHOOK_PATH = /webhook/i;
15583
+ var SIGNATURE_CHECK = /constructEvent(?:Async)?\(|stripe-signature/;
15584
+ var SERVICE_ROLE_REF = /\b(SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE_KEY|sb_secret_[A-Za-z0-9_]+|NEXT_PUBLIC_[A-Z0-9_]*(?:SECRET|SERVICE_ROLE|PRIVATE)[A-Z0-9_]*)\b/;
15585
+ var ENV_FILES = [
15586
+ ".env",
15587
+ ".env.local",
15588
+ ".env.production",
15589
+ ".env.development",
15590
+ ".env.staging"
15591
+ ];
15592
+ var codeRlsMissing = {
15593
+ id: "code-rls-missing",
15594
+ source: "code",
15595
+ category: "data",
15596
+ severity: "critical",
15597
+ title: "Table created without row level security",
15598
+ impact: "A migration creates a table and never turns on row level security for it. Once it has rows, anyone with the public key can read them \u2014 this is the hole the RLS check lights up from outside.",
15599
+ howToDetect: (ctx) => {
15600
+ const migrations = ctx.files.filter((f) => MIGRATION_FILE.test(f.path));
15601
+ if (migrations.length === 0) return [];
15602
+ const comRls = /* @__PURE__ */ new Set();
15603
+ for (const m of migrations) for (const t of all(ENABLE_RLS, m.content)) comRls.add(t);
15604
+ const out = [];
15605
+ const vistas = /* @__PURE__ */ new Set();
15606
+ for (const m of migrations) {
15607
+ for (const tabela of all(CREATE_TABLE, m.content)) {
15608
+ if (comRls.has(tabela) || vistas.has(tabela)) continue;
15609
+ vistas.add(tabela);
15610
+ out.push({
15611
+ path: m.path,
15612
+ reason: `table \`${tabela}\` is created without enable row level security`
15613
+ });
15614
+ }
15615
+ }
15616
+ return out;
15617
+ },
15618
+ fixPrompt: (a) => `In your code, ${a.reason} (${a.path}).
15619
+
15620
+ We do not know the columns or who should read what. We cannot write the policy from here.
15621
+
15622
+ Good practice: every table starts with row level security on, in the same migration that creates it. Then one policy per real access: a person reads their own rows; a public catalog, if it exists, is a deliberate read policy; writes always go through a session. Do this in a new migration, never by editing one that already ran.
15623
+
15624
+ After, deploy and read the table with the public key and no session \u2014 it must answer empty or denied.`
15625
+ };
15626
+ var codeServiceRoleInClient = {
15627
+ id: "code-service-role-in-client",
15628
+ source: "code",
15629
+ category: "secrets",
15630
+ severity: "fatal",
15631
+ title: "Service key referenced in client code",
15632
+ impact: "A file that runs in the browser mentions the Supabase service key or a NEXT_PUBLIC_ secret. Whatever the bundler inlines there ships to every visitor \u2014 this is the fatal finding from outside, one build away.",
15633
+ howToDetect: (ctx) => ctx.files.filter(isClientFile).filter((f) => SERVICE_ROLE_REF.test(f.content)).map((f) => ({
15634
+ path: f.path,
15635
+ reason: "client-side file references a service or secret key"
15636
+ })),
15637
+ fixPrompt: (a) => `In your code, a client-side file references a service or secret key (${a.path}).
15638
+
15639
+ We do not have the value and we do not want it. The problem is the place, not the string.
15640
+
15641
+ Good practice: the service key lives only in server code \u2014 a route handler, a server action, an edge function \u2014 read from the environment without the NEXT_PUBLIC_ prefix. The browser talks to your API; your API talks to the database. Remove the reference from the client file and, if the key was ever bundled, rotate it in the dashboard.
15642
+
15643
+ After, search the built bundle for the key prefix and confirm it is gone.`
15644
+ };
15645
+ var codeRouteWithoutAuth = {
15646
+ id: "code-route-without-auth",
15647
+ source: "code",
15648
+ category: "auth",
15649
+ severity: "high",
15650
+ title: "API route touches data without a session check",
15651
+ impact: "An API route reads or writes the database and nothing in it checks who is asking. If the middleware does not cover it, anyone can call it \u2014 this is what api-route-noauth lights up from outside.",
15652
+ howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path) && !PUBLIC_ROUTE.test(f.path)).filter((f) => TOUCHES_DB.test(f.content) && !HAS_AUTH.test(f.content)).map((f) => ({
15653
+ path: f.path,
15654
+ reason: "route reads or writes data with no session check in the file"
15655
+ })),
15656
+ fixPrompt: (a) => `In your code, a route reads or writes data with no session check in the file (${a.path}).
15657
+
15658
+ We cannot see your middleware. If it already protects this path, mark the finding as known.
15659
+
15660
+ Good practice: the route resolves the session first and answers 401 before touching data; what it reads or writes is scoped to that user. A public route is public on purpose, with a comment saying so, and never exposes rows that belong to someone.
15661
+
15662
+ After, call the route from a logged-out client and confirm it answers 401 or 403.`
15663
+ };
15664
+ var codeEnvCommitted = {
15665
+ id: "code-env-committed",
15666
+ source: "code",
15667
+ category: "secrets",
15668
+ severity: "critical",
15669
+ title: ".env file tracked by git",
15670
+ impact: "An environment file is committed to the repository. Every clone, fork, CI log and AI tool that reads the repo has the secrets in it. Even after deleting it, the history keeps the values.",
15671
+ // Só o nome. O conteúdo nunca é lido — é o único check que não recebe
15672
+ // `content`, e é assim de propósito.
15673
+ howToDetect: (ctx) => ENV_FILES.filter((nome) => ctx.tracked(nome)).map((nome) => ({
15674
+ path: nome,
15675
+ reason: `${nome} is tracked by git`
15676
+ })),
15677
+ fixPrompt: (a) => `In your code, ${a.reason}.
15678
+
15679
+ We did not read it. We only saw that git tracks it.
15680
+
15681
+ Good practice: environment files stay out of version control \u2014 add them to .gitignore, remove them from the index, and keep a committed .env.example with the variable names and no values. Every secret that was ever in the file is compromised: rotate each one in its provider.
15682
+
15683
+ After, run git ls-files and confirm no .env file is listed.`
15684
+ };
15685
+ var codeAiRouteWithoutLimit = {
15686
+ id: "code-ai-route-without-limit",
15687
+ source: "code",
15688
+ category: "ai",
15689
+ severity: "high",
15690
+ title: "AI route with no rate limit",
15691
+ impact: "A route calls an LLM provider and nothing in it limits how often. A loop from one client turns your provider account into an open bill \u2014 this is ai-proxy-open, seen from inside.",
15692
+ howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path)).filter((f) => AI_SDK.test(f.content) && !HAS_LIMITER.test(f.content)).map((f) => ({
15693
+ path: f.path,
15694
+ reason: "route calls an AI provider with no rate limit in the file"
15695
+ })),
15696
+ fixPrompt: (a) => `In your code, a route calls an AI provider with no rate limit in the file (${a.path}).
15697
+
15698
+ We cannot see limits applied elsewhere. If your gateway already throttles this path, mark the finding as known.
15699
+
15700
+ Good practice: the route requires a session, then applies a per-user limit (so many calls per minute, so many tokens per day) before calling the provider, and answers 429 when it is hit. Cost caps at the provider are the second net, not the first.
15701
+
15702
+ After, call the route in a loop from one client and confirm it starts answering 429.`
15703
+ };
15704
+ var codeWebhookUnsigned = {
15705
+ id: "code-webhook-unsigned",
15706
+ source: "code",
15707
+ category: "config",
15708
+ severity: "high",
15709
+ title: "Stripe webhook without signature verification",
15710
+ impact: 'A webhook route imports Stripe and never verifies the event signature. Anyone can POST a fake "payment succeeded" and your app will believe it \u2014 the one check we refuse to run from outside, because proving it would mean doing exactly that.',
15711
+ howToDetect: (ctx) => ctx.files.filter((f) => isApiRoute(f.path) && WEBHOOK_PATH.test(f.path)).filter((f) => STRIPE_IMPORT.test(f.content) && !SIGNATURE_CHECK.test(f.content)).map((f) => ({
15712
+ path: f.path,
15713
+ reason: "Stripe webhook route never verifies the event signature"
15714
+ })),
15715
+ fixPrompt: (a) => `In your code, a Stripe webhook route never verifies the event signature (${a.path}).
15716
+
15717
+ We cannot see the secret and do not want it.
15718
+
15719
+ Good practice: the route reads the raw body and the stripe-signature header, builds the event with the webhook secret from the environment, and rejects anything that fails. Only the verified event drives state. Each event id is processed once.
15720
+
15721
+ After, POST a fabricated event to the route and confirm it answers 400.`
15722
+ };
15723
+ var CODE_CHECKS = [
15724
+ codeRlsMissing,
15725
+ codeServiceRoleInClient,
15726
+ codeRouteWithoutAuth,
15727
+ codeEnvCommitted,
15728
+ codeAiRouteWithoutLimit,
15729
+ codeWebhookUnsigned
15730
+ ];
15731
+ function runCodeChecks(ctx, catalog = CODE_CHECKS) {
15732
+ const out = [];
15733
+ for (const check2 of catalog) {
15734
+ let advisories = [];
15735
+ try {
15736
+ advisories = check2.howToDetect(ctx);
15737
+ } catch {
15738
+ advisories = [];
15739
+ }
15740
+ for (const a of advisories) {
15741
+ out.push({
15742
+ checkId: check2.id,
15743
+ severity: check2.severity,
15744
+ path: a.path,
15745
+ reason: a.reason
15746
+ });
15747
+ }
15748
+ }
15749
+ return out;
15750
+ }
15751
+
15752
+ // src/gate.ts
15753
+ var GATE_DIR = ".vibecheck";
15754
+ var GATE_JSON = "quality-gate.json";
15755
+ var GATE_SARIF = "quality-gate.sarif";
15756
+ function escreverArtefatos(dir, report, sarif, deps) {
15757
+ deps.writeArtifact(
15758
+ join3(dir, GATE_DIR, GATE_JSON),
15759
+ `${JSON.stringify(report, null, 2)}
15760
+ `
15761
+ );
15762
+ deps.writeArtifact(
15763
+ join3(dir, GATE_DIR, GATE_SARIF),
15764
+ `${JSON.stringify(sarif, null, 2)}
15765
+ `
15766
+ );
15767
+ }
15768
+ function gravarNoDisco(caminho2, conteudo) {
15769
+ mkdirSync3(join3(caminho2, ".."), { recursive: true });
15770
+ writeFileSync3(caminho2, conteudo);
15771
+ }
15772
+ async function rodarHttp(input, grafo, deps) {
15773
+ const headers = {
15774
+ authorization: `Bearer ${input.token}`,
15775
+ "content-type": "application/json"
15776
+ };
15777
+ const criado = await deps.fetch(`${deps.base}/api/cli/scan`, {
15778
+ method: "POST",
15779
+ headers,
15780
+ body: JSON.stringify({
15781
+ url: input.url,
15782
+ ...grafo === null ? {} : { graph: grafo }
15783
+ })
15784
+ });
15785
+ const corpo = await criado.json().catch(() => ({}));
15786
+ if (criado.status === 429) {
15787
+ deps.log(`quota: ${corpo.error ?? "this domain was scanned recently"}`);
15788
+ return { outcome: "skipped-quota", issues: [] };
15789
+ }
15790
+ if (!criado.ok || corpo.scanId === void 0) {
15791
+ deps.log(`scan not opened: ${corpo.error ?? `HTTP ${String(criado.status)}`}`);
15792
+ return { outcome: "failed", issues: [] };
15793
+ }
15794
+ const scan = {
15795
+ scanId: corpo.scanId,
15796
+ shareToken: corpo.shareToken ?? "",
15797
+ liveUrl: corpo.liveUrl ?? `${deps.base}/advanced/${corpo.scanId}`
15798
+ };
15799
+ if (grafo !== null && grafo.advanced) {
15800
+ const put = await deps.fetch(`${deps.base}/api/cli/scan/${scan.scanId}/graph`, {
15801
+ method: "PUT",
15802
+ headers,
15803
+ body: JSON.stringify(grafo)
15804
+ });
15805
+ if (!put.ok) deps.log("graph rejected by the server \u2014 scan continues HTTP-only");
15806
+ }
15807
+ const seguido = await followScan(deps.base, scan.scanId, scan.shareToken, {
15808
+ fetch: deps.fetch
15809
+ });
15810
+ if (!seguido.opened) {
15811
+ deps.log(`could not follow the scan \u2014 see ${scan.liveUrl}`);
15812
+ return { outcome: "failed", issues: [], scan };
15813
+ }
15814
+ const issues = [];
15815
+ for (const evento of seguido.events) {
15816
+ if (evento.type !== "finding.found") continue;
15817
+ issues.push({
15818
+ checkId: evento.checkId,
15819
+ severity: evento.severity,
15820
+ // No achado HTTP o "path" é o assunto (tabela, bucket, rota) ou o
15821
+ // nó que acendeu — não há arquivo do lado de fora.
15822
+ path: evento.subject ?? evento.nodeId,
15823
+ reason: evento.proof.length > 0 ? evento.proof : evento.checkId,
15824
+ source: "http"
15825
+ });
15826
+ }
15827
+ return {
15828
+ outcome: seguido.final?.type === "scan.failed" ? "failed" : "ran",
15829
+ issues,
15830
+ scan
15831
+ };
15832
+ }
15833
+ async function runGate(input, deps) {
15834
+ const decisao = await resolverConsentimento(input.dir, { ci: input.ci }, deps.consent);
15835
+ if (decisao === "denied") {
15836
+ deps.log("code scan declined \u2014 nothing was read, nothing was sent");
15837
+ return { exit: GATE_EXIT.inconclusive, report: null };
15838
+ }
15839
+ if (decisao === "needs-ci-env") {
15840
+ deps.log(
15841
+ "CI needs VIBECHECK_ALLOW_CODE_SCAN=1 to read the repository \u2014 refusing to guess"
15842
+ );
15843
+ return { exit: GATE_EXIT.inconclusive, report: null };
15844
+ }
15845
+ const grafo = await deps.extractGraph(input.dir);
15846
+ const metrics = grafo === null ? null : graphMetrics(grafo);
15847
+ const repo = deps.readRepo(input.dir);
15848
+ const codeIssues = runCodeChecks(repo).map((i) => ({
15849
+ checkId: i.checkId,
15850
+ severity: i.severity,
15851
+ path: i.path,
15852
+ reason: i.reason,
15853
+ source: "code"
15854
+ }));
15855
+ let httpScan = "skipped-flag";
15856
+ let httpIssues = [];
15857
+ let scan;
15858
+ if (!input.codeOnly) {
15859
+ const resultado = await rodarHttp(input, grafo, deps);
15860
+ httpScan = resultado.outcome;
15861
+ httpIssues = resultado.issues;
15862
+ scan = resultado.scan;
15863
+ }
15864
+ const issues = [...codeIssues, ...httpIssues];
15865
+ const codeFiles = repo.files.length;
15866
+ const report = {
15867
+ policy: QUALITY_GATE_POLICY,
15868
+ status: statusFromIssues(issues, httpScan, analysed({ codeFiles, httpScan })),
15869
+ httpScan,
15870
+ issues,
15871
+ counts: contarPorSeveridade(issues),
15872
+ metrics,
15873
+ codeFiles,
15874
+ ranAt: deps.now().toISOString()
15875
+ };
15876
+ return {
15877
+ exit: exitCodeFromStatus(report.status),
15878
+ report,
15879
+ ...scan === void 0 ? {} : { scanId: scan.scanId, liveUrl: scan.liveUrl }
15880
+ };
15881
+ }
15882
+ async function enviarGate(scanId, report, input, deps) {
15883
+ const res = await deps.fetch(`${deps.base}/api/cli/scan/${scanId}/quality-gate`, {
15884
+ method: "PUT",
15885
+ headers: {
15886
+ authorization: `Bearer ${input.token}`,
15887
+ "content-type": "application/json"
15888
+ },
15889
+ body: JSON.stringify(report)
15890
+ });
15891
+ if (!res.ok) {
15892
+ const corpo = await res.json().catch(() => ({}));
15893
+ deps.log(`gate not saved: ${corpo.error ?? `HTTP ${String(res.status)}`}`);
15894
+ return false;
15895
+ }
15896
+ return true;
15897
+ }
15898
+
15899
+ // src/graphify.ts
15900
+ import { spawn } from "child_process";
15901
+ import { readFile } from "fs/promises";
15902
+ import { join as join4 } from "path";
15903
+ async function tentarGraphify(raiz) {
15904
+ const json2 = join4(raiz, "graphify-out", "graph.json");
15905
+ try {
15906
+ return JSON.parse(await readFile(json2, "utf8"));
15907
+ } catch {
15908
+ }
15909
+ const ok = await new Promise((resolve2) => {
15910
+ const child = spawn("graphify", [".", "--no-viz"], {
15911
+ cwd: raiz,
15912
+ stdio: "ignore"
15913
+ });
15914
+ const timer = setTimeout(() => {
15915
+ child.kill();
15916
+ resolve2(false);
15917
+ }, 2e4);
15918
+ child.on("exit", (code) => {
15919
+ clearTimeout(timer);
15920
+ resolve2(code === 0);
15921
+ });
15922
+ child.on("error", () => {
15923
+ clearTimeout(timer);
15924
+ resolve2(false);
15925
+ });
15926
+ });
15927
+ if (!ok) return null;
15928
+ try {
15929
+ return JSON.parse(await readFile(json2, "utf8"));
15930
+ } catch {
15931
+ return null;
15932
+ }
15933
+ }
15934
+
15015
15935
  // src/login.ts
15936
+ import { spawn as spawn2 } from "child_process";
15016
15937
  function urlBase() {
15017
15938
  return (process.env["VIBECHECK_URL"] ?? "https://vibecheck.kinkai.cloud").replace(
15018
15939
  /\/$/,
@@ -15032,12 +15953,12 @@ function comandosAbrirBrowser(url2, platform = process.platform, env = process.e
15032
15953
  return [["xdg-open", [url2]]];
15033
15954
  }
15034
15955
  function tentarSpawn(cmd, args) {
15035
- return new Promise((resolve) => {
15956
+ return new Promise((resolve2) => {
15036
15957
  const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
15037
- child.once("error", () => resolve(false));
15958
+ child.once("error", () => resolve2(false));
15038
15959
  child.once("spawn", () => {
15039
15960
  child.unref();
15040
- resolve(true);
15961
+ resolve2(true);
15041
15962
  });
15042
15963
  });
15043
15964
  }
@@ -15058,22 +15979,23 @@ async function detalheHttp(resposta) {
15058
15979
  }
15059
15980
  }
15060
15981
  if (resposta.status === 404) {
15061
- return " \u2014 a API do CLI ainda n\xE3o est\xE1 neste deploy";
15982
+ return " \u2014 the CLI API is not in this deploy yet";
15062
15983
  }
15063
15984
  return "";
15064
15985
  }
15065
- async function loginDevice(ci) {
15986
+ async function loginDevice(ci, options = {}) {
15066
15987
  const base = urlBase();
15067
15988
  const aberto = await fetch(`${base}/api/cli/device`, { method: "POST" });
15068
15989
  if (!aberto.ok) {
15069
15990
  const extra = await detalheHttp(aberto);
15070
15991
  throw new Error(
15071
- `n\xE3o foi poss\xEDvel abrir o device-code em ${base} (HTTP ${aberto.status})${extra}`
15992
+ `could not open the device-code at ${base} (HTTP ${aberto.status})${extra}`
15072
15993
  );
15073
15994
  }
15074
15995
  const corpo = await aberto.json();
15075
- console.warn(`No navegador, entre na conta e autorize com o c\xF3digo ${corpo.user_code}`);
15996
+ console.warn(`In the browser, sign in and authorize with code ${corpo.user_code}`);
15076
15997
  console.warn(corpo.verification_url);
15998
+ options.onCode?.({ userCode: corpo.user_code, verificationUrl: corpo.verification_url });
15077
15999
  if (!ci) await abrirBrowser(corpo.verification_url);
15078
16000
  const intervalo = Math.max(3, corpo.interval ?? 3) * 1e3;
15079
16001
  const limite = Date.now() + 10 * 6e4;
@@ -15088,15 +16010,133 @@ async function loginDevice(ci) {
15088
16010
  return { token: dados.token, host: dados.host };
15089
16011
  }
15090
16012
  if (dados.status === "expired" || dados.error === "expired") {
15091
- throw new Error("c\xF3digo expirado \u2014 rode de novo");
16013
+ throw new Error("code expired \u2014 run again");
16014
+ }
16015
+ }
16016
+ throw new Error("timed out without authorization");
16017
+ }
16018
+
16019
+ // src/repo.ts
16020
+ import { execFileSync } from "child_process";
16021
+ import { lstatSync, readdirSync, readFileSync as readFileSync3 } from "fs";
16022
+ import { join as join5, relative } from "path";
16023
+ var MAX_FILES = 800;
16024
+ var MAX_BYTES2 = 64e3;
16025
+ var MAX_SQL_BYTES = 512e3;
16026
+ var SOURCE = /\.(ts|tsx|js|jsx|mjs|cjs|sql)$/;
16027
+ var SKIP_DIR = /* @__PURE__ */ new Set([
16028
+ "node_modules",
16029
+ ".git",
16030
+ "dist",
16031
+ ".next",
16032
+ "coverage",
16033
+ "graphify-out",
16034
+ ".turbo",
16035
+ "out",
16036
+ "build",
16037
+ ".vercel"
16038
+ ]);
16039
+ function gitTracked(dir) {
16040
+ try {
16041
+ const saida = execFileSync("git", ["ls-files", "-z"], {
16042
+ cwd: dir,
16043
+ encoding: "utf8",
16044
+ stdio: ["ignore", "pipe", "ignore"],
16045
+ maxBuffer: 16 * 1024 * 1024
16046
+ });
16047
+ return new Set(saida.split("\0").filter((p) => p.length > 0));
16048
+ } catch {
16049
+ return null;
16050
+ }
16051
+ }
16052
+ function walk(dir, raiz, out) {
16053
+ if (out.length >= MAX_FILES) return;
16054
+ let entradas;
16055
+ try {
16056
+ entradas = readdirSync(dir);
16057
+ } catch {
16058
+ return;
16059
+ }
16060
+ for (const nome of entradas) {
16061
+ if (out.length >= MAX_FILES) return;
16062
+ if (SKIP_DIR.has(nome) || nome.startsWith(".") && nome !== ".env") continue;
16063
+ const caminho2 = join5(dir, nome);
16064
+ let st;
16065
+ try {
16066
+ st = lstatSync(caminho2);
16067
+ } catch {
16068
+ continue;
16069
+ }
16070
+ if (st.isSymbolicLink()) continue;
16071
+ if (st.isDirectory()) walk(caminho2, raiz, out);
16072
+ else if (SOURCE.test(nome)) out.push(relative(raiz, caminho2).split("\\").join("/"));
16073
+ }
16074
+ }
16075
+ function readCodeContext(dir) {
16076
+ const tracked = gitTracked(dir);
16077
+ const caminhos = [];
16078
+ walk(dir, dir, caminhos);
16079
+ const files = [];
16080
+ for (const path of caminhos) {
16081
+ try {
16082
+ const st = lstatSync(join5(dir, path));
16083
+ if (st.size > (path.endsWith(".sql") ? MAX_SQL_BYTES : MAX_BYTES2)) continue;
16084
+ files.push({ path, content: readFileSync3(join5(dir, path), "utf8") });
16085
+ } catch {
15092
16086
  }
15093
16087
  }
15094
- throw new Error("tempo esgotado sem autoriza\xE7\xE3o");
16088
+ return {
16089
+ files,
16090
+ // Sem git não há "commitado": um `.env` só presente no disco era
16091
+ // reportado como "tracked by git" (critical) em qualquer pasta sem
16092
+ // `.git` — tarball, contexto de build, runner sem git no PATH.
16093
+ tracked: (path) => tracked !== null && tracked.has(path)
16094
+ };
16095
+ }
16096
+
16097
+ // src/sarif.ts
16098
+ var NIVEL = {
16099
+ fatal: "error",
16100
+ critical: "error",
16101
+ high: "error",
16102
+ medium: "warning",
16103
+ low: "note"
16104
+ };
16105
+ function regras(issues) {
16106
+ const vistos = /* @__PURE__ */ new Map();
16107
+ for (const issue2 of issues) {
16108
+ if (!vistos.has(issue2.checkId)) vistos.set(issue2.checkId, issue2.reason);
16109
+ }
16110
+ return [...vistos].map(([id, text]) => ({ id, shortDescription: { text } }));
16111
+ }
16112
+ function toSarif(report, version2) {
16113
+ return {
16114
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
16115
+ version: "2.1.0",
16116
+ runs: [
16117
+ {
16118
+ tool: {
16119
+ driver: {
16120
+ name: "VibeCheck",
16121
+ informationUri: "https://vibecheck.kinkai.cloud",
16122
+ version: version2,
16123
+ rules: regras(report.issues)
16124
+ }
16125
+ },
16126
+ results: report.issues.map((issue2) => ({
16127
+ ruleId: issue2.checkId,
16128
+ level: NIVEL[issue2.severity],
16129
+ message: { text: issue2.reason },
16130
+ locations: [{ physicalLocation: { artifactLocation: { uri: issue2.path } } }]
16131
+ }))
16132
+ }
16133
+ ]
16134
+ };
15095
16135
  }
15096
16136
 
15097
16137
  // src/walker.ts
15098
- import { lstatSync, readdirSync, readFileSync as readFileSync2 } from "fs";
15099
- import { join as join3, relative } from "path";
16138
+ import { lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
16139
+ import { join as join6, relative as relative2 } from "path";
15100
16140
  var IGNORAR = /* @__PURE__ */ new Set([
15101
16141
  "node_modules",
15102
16142
  ".git",
@@ -15111,7 +16151,19 @@ var IGNORAR = /* @__PURE__ */ new Set([
15111
16151
  var CODIGO = /\.(m?[jt]sx?)$/;
15112
16152
  var MAX_ARQUIVOS = 800;
15113
16153
  var MAX_NOS_WALKER = 120;
15114
- var EXT = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", "/index.ts", "/index.tsx", "/index.js"];
16154
+ var MAX_BYTES_ARQUIVO = 32e3;
16155
+ var EXT = [
16156
+ "",
16157
+ ".ts",
16158
+ ".tsx",
16159
+ ".js",
16160
+ ".jsx",
16161
+ ".mjs",
16162
+ ".cjs",
16163
+ "/index.ts",
16164
+ "/index.tsx",
16165
+ "/index.js"
16166
+ ];
15115
16167
  var EXPORT_RE = /export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|enum)\s+([A-Za-z_$][\w$]*)/g;
15116
16168
  var EXPORT_LISTA_RE = /export\s+\{([^}]+)\}/g;
15117
16169
  var FROM_RE = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"]([^'"]+)['"]/g;
@@ -15119,16 +16171,16 @@ var HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE",
15119
16171
  function listar(raiz, dir, acc) {
15120
16172
  let nomes;
15121
16173
  try {
15122
- nomes = readdirSync(dir);
16174
+ nomes = readdirSync2(dir);
15123
16175
  } catch {
15124
16176
  return;
15125
16177
  }
15126
16178
  for (const nome of nomes) {
15127
16179
  if (IGNORAR.has(nome) || nome.startsWith(".")) continue;
15128
- const cheio = join3(dir, nome);
16180
+ const cheio = join6(dir, nome);
15129
16181
  let stat;
15130
16182
  try {
15131
- stat = lstatSync(cheio);
16183
+ stat = lstatSync2(cheio);
15132
16184
  } catch {
15133
16185
  continue;
15134
16186
  }
@@ -15142,7 +16194,7 @@ function listar(raiz, dir, acc) {
15142
16194
  }
15143
16195
  }
15144
16196
  function posixRel(raiz, cheio) {
15145
- return relative(raiz, cheio).replaceAll("\\", "/");
16197
+ return relative2(raiz, cheio).replaceAll("\\", "/");
15146
16198
  }
15147
16199
  function dirnamePosix(caminho2) {
15148
16200
  const i = caminho2.lastIndexOf("/");
@@ -15215,7 +16267,7 @@ function resolverSpec(de, spec, indice) {
15215
16267
  return null;
15216
16268
  }
15217
16269
  function rotuloDe(rel, exports) {
15218
- const rota = rotaDeArquivo(rel);
16270
+ const rota = routeFromFile(rel);
15219
16271
  const handlers = exports.filter((e) => HANDLERS.has(e));
15220
16272
  if (rota !== null && handlers.length > 0) return `${handlers[0]} ${rota}`;
15221
16273
  if (rota !== null) return rota;
@@ -15228,7 +16280,7 @@ function lerArquivo(raiz, cheio) {
15228
16280
  const rel = posixRel(raiz, cheio);
15229
16281
  let conteudo = "";
15230
16282
  try {
15231
- conteudo = readFileSync2(cheio, "utf8").slice(0, 12e3);
16283
+ conteudo = readFileSync4(cheio, "utf8").slice(0, MAX_BYTES_ARQUIVO);
15232
16284
  } catch {
15233
16285
  return null;
15234
16286
  }
@@ -15236,21 +16288,25 @@ function lerArquivo(raiz, cheio) {
15236
16288
  const specs = specsDe(conteudo);
15237
16289
  return {
15238
16290
  rel,
15239
- rotulo: rotuloDe(rel, exports),
16291
+ label: rotuloDe(rel, exports),
15240
16292
  exports,
15241
- importsRel: specs.filter((s) => s.startsWith(".") || s.startsWith("@/") || s.startsWith("~/")),
15242
- importsPkg: specs
16293
+ importsRel: specs.filter(
16294
+ (s) => s.startsWith(".") || s.startsWith("@/") || s.startsWith("~/")
16295
+ ),
16296
+ importsPkg: specs,
16297
+ data: identifiersFromCode(conteudo)
15243
16298
  };
15244
16299
  }
15245
16300
  function prioridade(info) {
15246
- const kind = mapearKind({ rotulo: info.rotulo, arquivos: [info.rel] });
16301
+ const kind = mapKind({ label: info.label, files: [info.rel] });
15247
16302
  if (kind === "api") return 0;
15248
16303
  if (kind === "payments" || kind === "auth" || kind === "db") return 1;
15249
- if (kind === "storage" || kind === "secrets" || kind === "browser" || kind === "ai") return 2;
15250
- if (info.importsPkg.some((s) => idDePacote(s) !== null)) return 3;
16304
+ if (kind === "storage" || kind === "secrets" || kind === "browser" || kind === "ai")
16305
+ return 2;
16306
+ if (info.importsPkg.some((s) => packageId(s) !== null)) return 3;
15251
16307
  return 8;
15252
16308
  }
15253
- function extrairDoRepo(raiz) {
16309
+ function extractFromRepo(raiz) {
15254
16310
  const caminhos = [];
15255
16311
  listar(raiz, raiz, caminhos);
15256
16312
  if (caminhos.length === 0) return null;
@@ -15265,7 +16321,9 @@ function extrairDoRepo(raiz) {
15265
16321
  if (infos.length === 0) return null;
15266
16322
  const porRel = new Map(infos.map((i) => [i.rel, i]));
15267
16323
  const escolhidos = /* @__PURE__ */ new Set();
15268
- const ordenados = [...infos].sort((a, b) => prioridade(a) - prioridade(b) || a.rel.localeCompare(b.rel));
16324
+ const ordenados = [...infos].sort(
16325
+ (a, b) => prioridade(a) - prioridade(b) || a.rel.localeCompare(b.rel)
16326
+ );
15269
16327
  for (const info of ordenados) {
15270
16328
  if (prioridade(info) <= 3) escolhidos.add(info.rel);
15271
16329
  }
@@ -15295,31 +16353,31 @@ function extrairDoRepo(raiz) {
15295
16353
  const files = [...escolhidos].map((rel) => porRel.get(rel)).filter((i) => i !== void 0).sort((a, b) => prioridade(a) - prioridade(b) || a.rel.localeCompare(b.rel)).slice(0, MAX_NOS_WALKER);
15296
16354
  const nodes = [];
15297
16355
  const edges = [];
15298
- const pacotes = /* @__PURE__ */ new Set();
15299
- const idDe2 = /* @__PURE__ */ new Map();
16356
+ const packages = /* @__PURE__ */ new Set();
16357
+ const idDe = /* @__PURE__ */ new Map();
15300
16358
  for (const [i, info] of files.entries()) {
15301
16359
  const id = `f${String(i)}`;
15302
- idDe2.set(info.rel, id);
16360
+ idDe.set(info.rel, id);
15303
16361
  nodes.push({
15304
16362
  id,
15305
- label: info.rotulo,
15306
- identifier: info.exports[0] ?? info.rotulo,
16363
+ label: info.label,
16364
+ identifier: info.exports[0] ?? info.label,
15307
16365
  source_file: info.rel
15308
16366
  });
15309
16367
  }
15310
16368
  for (const info of files) {
15311
- const de = idDe2.get(info.rel);
16369
+ const de = idDe.get(info.rel);
15312
16370
  if (de === void 0) continue;
15313
16371
  for (const spec of info.importsRel) {
15314
16372
  const alvo = resolverSpec(info.rel, spec, indice);
15315
- const para = alvo === null ? void 0 : idDe2.get(alvo);
16373
+ const para = alvo === null ? void 0 : idDe.get(alvo);
15316
16374
  if (para === void 0) continue;
15317
16375
  edges.push({ source: de, target: para, relation: "imports" });
15318
16376
  }
15319
16377
  for (const spec of info.importsPkg) {
15320
- const pkg = idDePacote(spec);
16378
+ const pkg = packageId(spec);
15321
16379
  if (pkg === null) continue;
15322
- pacotes.add(pkg);
16380
+ packages.add(pkg);
15323
16381
  const pkgId = `pkg:${pkg}`;
15324
16382
  if (!nodes.some((n) => n["id"] === pkgId)) {
15325
16383
  nodes.push({ id: pkgId, label: pkg });
@@ -15327,15 +16385,44 @@ function extrairDoRepo(raiz) {
15327
16385
  edges.push({ source: de, target: pkgId, relation: "imports" });
15328
16386
  }
15329
16387
  }
15330
- const sanitizado = sanitizarGrafo({ nodes, edges, pacotes: [...pacotes] });
15331
- if (!sanitizado.ok || !sanitizado.grafo.avancado) return null;
15332
- return sanitizado.grafo;
16388
+ const data = mergeIdentifiers(
16389
+ infos.map((i) => i.data),
16390
+ {
16391
+ tables: MAX_HINT_TABLES,
16392
+ buckets: MAX_HINT_BUCKETS,
16393
+ collections: MAX_HINT_COLLECTIONS
16394
+ }
16395
+ );
16396
+ const sanitizado = sanitizeGraph({ nodes, edges, packages: [...packages], data });
16397
+ if (!sanitizado.ok || !sanitizado.graph.advanced) return null;
16398
+ return sanitizado.graph;
15333
16399
  }
15334
- function scanEhAvancado(grafo) {
15335
- return grafo !== null && grafo.avancado === true;
16400
+ function isAdvancedScan(grafo) {
16401
+ return grafo !== null && grafo.advanced === true;
15336
16402
  }
15337
16403
 
15338
16404
  // src/cli.ts
16405
+ var VERSION = "0.2.0";
16406
+ var AJUDA = `vibecheck \u2014 security check for AI-built apps
16407
+
16408
+ npx @kinkai.cloud/vibecheck [options]
16409
+
16410
+ Options
16411
+ --url <url> target; defaults to https://<verified host of the token>
16412
+ --dir <path> repository root (default: current directory)
16413
+ --gate CI mode: HTTP checks + code checks + graph signals,
16414
+ writes ${GATE_DIR}/${GATE_JSON} (+ .sarif) and exits
16415
+ 0 passed \xB7 1 failed \xB7 2 inconclusive \xB7 3 error
16416
+ --code-only with --gate: skip the HTTP scan (no quota, no history)
16417
+ --ci never open a browser; requires VIBECHECK_TOKEN
16418
+ --no-wait do not follow the scan; exit as soon as it is queued
16419
+ --gate-consent <list|revoke> manage local code-scan consent
16420
+ --help, --version
16421
+
16422
+ Environment
16423
+ VIBECHECK_TOKEN, VIBECHECK_HOST, VIBECHECK_URL
16424
+ VIBECHECK_ALLOW_CODE_SCAN=1 required by --gate in CI
16425
+ `;
15339
16426
  function arg(nome, argv) {
15340
16427
  const i = argv.indexOf(nome);
15341
16428
  if (i < 0) return null;
@@ -15347,87 +16434,134 @@ function temFlag(nome, argv) {
15347
16434
  async function extrairGrafo(raiz) {
15348
16435
  const dump = await tentarGraphify(raiz);
15349
16436
  if (dump !== null) {
15350
- const sanitizado = sanitizarGrafo(dump);
15351
- if (sanitizado.ok && sanitizado.grafo.avancado) return sanitizado.grafo;
16437
+ const sanitizado = sanitizeGraph(dump);
16438
+ if (sanitizado.ok && sanitizado.graph.advanced) return sanitizado.graph;
15352
16439
  }
15353
- return extrairDoRepo(raiz);
16440
+ return extractFromRepo(raiz);
15354
16441
  }
15355
16442
  async function acompanhar(liveUrl, scanId, shareToken) {
15356
- const base = urlBase();
15357
- const streamUrl = `${base}/api/scan/${scanId}/events?t=${encodeURIComponent(shareToken)}`;
15358
- const resposta = await fetch(streamUrl);
15359
- if (!resposta.ok || resposta.body === null) {
15360
- console.warn(`Acompanhe em ${liveUrl}`);
15361
- return 0;
15362
- }
15363
- const leitor = resposta.body.getReader();
15364
- const decoder = new TextDecoder();
15365
- let buf = "";
15366
- let exit = 0;
15367
16443
  let gravidades = "";
15368
- while (true) {
15369
- const { done, value } = await leitor.read();
15370
- if (done) break;
15371
- buf += decoder.decode(value, { stream: true });
15372
- const blocos = buf.split("\n\n");
15373
- buf = blocos.pop() ?? "";
15374
- for (const bloco of blocos) {
15375
- const linha = bloco.split("\n").find((l) => l.startsWith("data: "));
15376
- if (linha === void 0) continue;
15377
- let evento;
15378
- try {
15379
- evento = JSON.parse(linha.slice(6));
15380
- } catch {
15381
- continue;
15382
- }
16444
+ const resultado = await followScan(urlBase(), scanId, shareToken, {
16445
+ onEvent: (evento) => {
15383
16446
  if (evento.type === "finding.found" && (evento.severity === "fatal" || evento.severity === "critical")) {
15384
- exit = 1;
15385
16447
  gravidades = evento.severity;
15386
16448
  }
15387
- if (evento.type === "scan.done" || evento.type === "scan.failed") {
15388
- if (evento.type === "scan.failed") exit = 1;
15389
- console.warn(`scan ${evento.type}${gravidades.length > 0 ? ` \xB7 ${gravidades}` : ""}`);
15390
- return exit;
15391
- }
15392
16449
  }
16450
+ });
16451
+ if (!resultado.opened) {
16452
+ console.warn(`Follow along at ${liveUrl}`);
16453
+ return 0;
15393
16454
  }
15394
- return exit;
16455
+ if (resultado.final !== null) {
16456
+ console.warn(
16457
+ `scan ${resultado.final.type}${gravidades.length > 0 ? ` \xB7 ${gravidades}` : ""}`
16458
+ );
16459
+ }
16460
+ return resultado.exit;
16461
+ }
16462
+ async function comandoConsentimento(acao, raiz) {
16463
+ if (acao === "list") {
16464
+ const entradas = listarConsentimentos();
16465
+ if (entradas.length === 0) console.warn("no repository consented to code scan");
16466
+ for (const e of entradas) console.warn(`${e.repo} ${e.acceptedAt}`);
16467
+ return 0;
16468
+ }
16469
+ if (acao === "revoke") {
16470
+ console.warn(
16471
+ revogarConsentimento(raiz) ? `revoked for ${raiz}` : `nothing to revoke for ${raiz}`
16472
+ );
16473
+ return 0;
16474
+ }
16475
+ console.error("usage: --gate-consent <list|revoke>");
16476
+ return GATE_EXIT.operational;
15395
16477
  }
15396
16478
  async function main(argv) {
16479
+ if (temFlag("--help", argv) || temFlag("-h", argv)) {
16480
+ console.warn(AJUDA);
16481
+ return 0;
16482
+ }
16483
+ if (temFlag("--version", argv)) {
16484
+ console.warn(VERSION);
16485
+ return 0;
16486
+ }
15397
16487
  const ci = temFlag("--ci", argv) || process.env["CI"] === "true" || process.env["VIBECHECK_TOKEN"] !== void 0 && process.env["VIBECHECK_TOKEN"].length > 0;
15398
16488
  const noWait = temFlag("--no-wait", argv);
15399
16489
  const raiz = arg("--dir", argv) ?? process.cwd();
15400
- let url2 = arg("--url", argv) ?? argv.find((a) => a.startsWith("http")) ?? null;
15401
- let cred = lerToken();
16490
+ const posicional = argv.find(
16491
+ (a, i) => /^https?:\/\//.test(a) && !(argv[i - 1] ?? "").startsWith("--")
16492
+ );
16493
+ let url2 = arg("--url", argv) ?? posicional ?? null;
16494
+ const consentimento = arg("--gate-consent", argv);
16495
+ if (consentimento !== null || temFlag("--gate-consent", argv)) {
16496
+ return comandoConsentimento(consentimento, raiz);
16497
+ }
16498
+ const gate = temFlag("--gate", argv);
16499
+ const codeOnly = gate && temFlag("--code-only", argv);
16500
+ let cred = codeOnly ? { token: "", host: "" } : lerToken();
15402
16501
  if (cred === null) {
15403
16502
  if (ci) {
15404
- console.error("CI exige VIBECHECK_TOKEN");
15405
- return 1;
16503
+ console.error("CI requires VIBECHECK_TOKEN");
16504
+ return gate ? GATE_EXIT.operational : 1;
15406
16505
  }
15407
16506
  cred = await loginDevice(false);
15408
16507
  }
15409
16508
  if (url2 === null) {
15410
- if (cred.host.length === 0) {
15411
- console.error("informe --url https://seu-app.com");
15412
- return 1;
16509
+ if (codeOnly) {
16510
+ url2 = "https://localhost";
16511
+ } else if (cred.host.length === 0) {
16512
+ console.error("pass --url https://your-app.com");
16513
+ return gate ? GATE_EXIT.operational : 1;
16514
+ } else {
16515
+ url2 = `https://${cred.host}`;
15413
16516
  }
15414
- url2 = `https://${cred.host}`;
15415
16517
  }
15416
16518
  const base = urlBase();
16519
+ if (gate) {
16520
+ const resultado = await runGate(
16521
+ { dir: raiz, url: url2, token: cred.token, ci, codeOnly },
16522
+ {
16523
+ base,
16524
+ fetch,
16525
+ readRepo: readCodeContext,
16526
+ extractGraph: extrairGrafo,
16527
+ writeArtifact: gravarNoDisco,
16528
+ log: (m) => console.warn(m),
16529
+ now: () => /* @__PURE__ */ new Date()
16530
+ }
16531
+ );
16532
+ if (resultado.report !== null) {
16533
+ escreverArtefatos(raiz, resultado.report, toSarif(resultado.report, VERSION), {
16534
+ writeArtifact: gravarNoDisco
16535
+ });
16536
+ const { status, httpScan, counts } = resultado.report;
16537
+ console.warn(
16538
+ `gate ${status} \xB7 http ${httpScan} \xB7 ${String(counts.fatal + counts.critical + counts.high)} blocking \xB7 ${String(resultado.report.issues.length)} total`
16539
+ );
16540
+ if (resultado.scanId !== void 0) {
16541
+ await enviarGate(resultado.scanId, resultado.report, cred, {
16542
+ base,
16543
+ fetch,
16544
+ log: (m) => console.warn(m)
16545
+ });
16546
+ console.warn(resultado.liveUrl ?? `${base}/report/${resultado.scanId}`);
16547
+ }
16548
+ }
16549
+ return resultado.exit;
16550
+ }
15417
16551
  const headers = {
15418
16552
  authorization: `Bearer ${cred.token}`,
15419
16553
  "content-type": "application/json"
15420
16554
  };
15421
- console.warn("Gerando GrafoScan\u2026");
16555
+ console.warn("Building ScanGraph\u2026");
15422
16556
  const grafo = await extrairGrafo(raiz);
15423
- const avancado = scanEhAvancado(grafo);
15424
- if (!avancado) {
16557
+ const advanced = isAdvancedScan(grafo);
16558
+ if (!advanced) {
15425
16559
  console.warn(
15426
- "Sem grafo extra\xEDvel (nem graphify, nem JS/TS). O HTTP roda, mas isto n\xE3o \xE9 scan avan\xE7ado."
16560
+ "No extractable graph (neither graphify nor JS/TS). HTTP still runs, but this is not an advanced scan."
15427
16561
  );
15428
16562
  } else {
15429
16563
  console.warn(
15430
- `GrafoScan pronto \xB7 ${String(grafo?.nos.length ?? 0)} n\xF3s \xB7 ${String(grafo?.arestas.length ?? 0)} arestas`
16564
+ `ScanGraph ready \xB7 ${String(grafo?.nodes.length ?? 0)} nodes \xB7 ${String(grafo?.edges.length ?? 0)} edges`
15431
16565
  );
15432
16566
  }
15433
16567
  const criado = await fetch(`${base}/api/cli/scan`, {
@@ -15440,22 +16574,31 @@ async function main(argv) {
15440
16574
  });
15441
16575
  const corpo = await criado.json();
15442
16576
  if (!criado.ok || corpo.scanId === void 0) {
15443
- console.error(corpo.error ?? "n\xE3o foi poss\xEDvel abrir o scan");
16577
+ console.error(corpo.error ?? "could not open the scan");
15444
16578
  return 1;
15445
16579
  }
15446
- if (avancado && grafo !== null) {
16580
+ if (advanced && grafo !== null) {
15447
16581
  const put = await fetch(`${base}/api/cli/scan/${corpo.scanId}/graph`, {
15448
16582
  method: "PUT",
15449
16583
  headers,
15450
16584
  body: JSON.stringify(grafo)
15451
16585
  });
15452
- if (!put.ok) console.warn("grafo rejeitado pelo servidor \u2014 scan segue s\xF3 HTTP");
16586
+ if (!put.ok) console.warn("graph rejected by the server \u2014 scan continues HTTP-only");
15453
16587
  }
15454
- const live = corpo.liveUrl ?? `${base}/avancado/${corpo.scanId}`;
16588
+ const live = corpo.liveUrl ?? `${base}/advanced/${corpo.scanId}`;
15455
16589
  console.warn(live);
15456
16590
  if (deveAbrirNavegador({ ci, noWait })) await abrirBrowser(live);
15457
16591
  if (noWait) return 0;
15458
16592
  return acompanhar(live, corpo.scanId, corpo.shareToken ?? "");
15459
16593
  }
15460
- var codigo = await main(process.argv.slice(2));
16594
+ var codigo;
16595
+ try {
16596
+ codigo = await main(process.argv.slice(2));
16597
+ } catch (erro) {
16598
+ console.error(erro instanceof Error ? erro.message : String(erro));
16599
+ codigo = GATE_EXIT.operational;
16600
+ }
15461
16601
  process.exit(codigo);
16602
+ export {
16603
+ VERSION
16604
+ };