From 8bf404e41b25aae4a1275c42518f963ed64d1e15 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:18 -0300 Subject: [PATCH 01/10] =?UTF-8?q?fix(product-webhook):=20ssot-bypass=20+?= =?UTF-8?q?=20module-scope=20=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/product-webhook/index.ts | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/supabase/functions/product-webhook/index.ts b/supabase/functions/product-webhook/index.ts index 1d20d3acb..4028d5b6c 100644 --- a/supabase/functions/product-webhook/index.ts +++ b/supabase/functions/product-webhook/index.ts @@ -3,6 +3,7 @@ import { crypto } from 'https://deno.land/std@0.224.0/crypto/mod.ts'; import { encodeHex } from 'https://deno.land/std@0.224.0/encoding/hex.ts'; import { buildPublicCorsHeaders } from '../_shared/cors.ts'; import { parseContract } from '../_shared/contracts/index.ts'; +import { getCredential } from '../_shared/credentials.ts'; import { ProductWebhookSchemas, type ProductWebhookV1Payload, @@ -11,7 +12,6 @@ import { const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; -const webhookSecret = Deno.env.get('N8N_PRODUCT_WEBHOOK_SECRET'); const configuredBatchSize = Number(Deno.env.get('PRODUCT_WEBHOOK_BATCH_SIZE') ?? '200'); const MAX_BATCH_SIZE = 500; const BATCH_SIZE = Number.isFinite(configuredBatchSize) @@ -19,13 +19,6 @@ const BATCH_SIZE = Number.isFinite(configuredBatchSize) : 200; const DEFAULT_WEBHOOK_TOLERANCE_SEC = 300; const MAX_WEBHOOK_TOLERANCE_SEC = 3600; -const configuredWebhookToleranceSec = Number( - Deno.env.get('N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC') ?? DEFAULT_WEBHOOK_TOLERANCE_SEC, -); -const webhookTimestampToleranceSec = - Number.isFinite(configuredWebhookToleranceSec) && configuredWebhookToleranceSec > 0 - ? Math.min(Math.floor(configuredWebhookToleranceSec), MAX_WEBHOOK_TOLERANCE_SEC) - : DEFAULT_WEBHOOK_TOLERANCE_SEC; const allowedOrigins = new Set( (Deno.env.get('PRODUCT_WEBHOOK_ALLOWED_ORIGINS') ?? '') @@ -151,8 +144,9 @@ async function isReplayNonce( supabase: SupabaseClient, nonce: string, timestamp: number, + toleranceSec: number, ): Promise { - const expiresAt = new Date((timestamp + webhookTimestampToleranceSec) * 1000).toISOString(); + const expiresAt = new Date((timestamp + toleranceSec) * 1000).toISOString(); const { error } = await supabase.from('webhook_request_nonces' as never).insert({ source: 'product-webhook', @@ -195,6 +189,16 @@ Deno.serve(async (req) => { try { const rawBody = await req.text(); + // fix: ssot-bypass + module-scope-credential-read — read from credential vault per-request + const webhookSecret = await getCredential('N8N_PRODUCT_WEBHOOK_SECRET'); + const configuredWebhookToleranceSec = Number( + await getCredential('N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC') ?? DEFAULT_WEBHOOK_TOLERANCE_SEC, + ); + const webhookTimestampToleranceSec = + Number.isFinite(configuredWebhookToleranceSec) && configuredWebhookToleranceSec > 0 + ? Math.min(Math.floor(configuredWebhookToleranceSec), MAX_WEBHOOK_TOLERANCE_SEC) + : DEFAULT_WEBHOOK_TOLERANCE_SEC; + if (!webhookSecret) { logAuthFailure('misconfigured_secret', req); return new Response(UNAUTHORIZED_BODY, { @@ -249,7 +253,7 @@ Deno.serve(async (req) => { }); } - const replayed = await isReplayNonce(supabase, nonce, timestamp); + const replayed = await isReplayNonce(supabase, nonce, timestamp, webhookTimestampToleranceSec); if (replayed) { logAuthFailure('replayed_nonce', req, { nonceSize: nonce.length }); return new Response(UNAUTHORIZED_BODY, { From 2b97be3cf85ea4a334e6f882c04039d9e3e85512 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:19 -0300 Subject: [PATCH 02/10] =?UTF-8?q?fix(secure-upload):=20ssot-bypass=20?= =?UTF-8?q?=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/secure-upload/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supabase/functions/secure-upload/index.ts b/supabase/functions/secure-upload/index.ts index f7ae567e5..89a904a1c 100644 --- a/supabase/functions/secure-upload/index.ts +++ b/supabase/functions/secure-upload/index.ts @@ -3,6 +3,7 @@ import { createStructuredLogger } from "../_shared/structured-logger.ts"; import { getOrCreateRequestId } from "../_shared/request-id.ts"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; import { safeErrorResponse } from "../_shared/error-response.ts"; +import { getCredential } from "../_shared/credentials.ts"; const corsHeaders = buildPublicCorsHeaders(); @@ -71,7 +72,8 @@ Deno.serve(async (req) => { }; let targetBucket = "personalization-images"; let targetPrefix = "verified"; - const vtApiKey = Deno.env.get("VIRUSTOTAL_API_KEY"); + // fix: ssot-bypass — credential vault + const vtApiKey = await getCredential("VIRUSTOTAL_API_KEY"); if (vtApiKey) { try { From 11009873d8326e0495a2e7366aee14ea594d4e4d Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:20 -0300 Subject: [PATCH 03/10] =?UTF-8?q?fix(send-scheduled-reports):=20ssot-bypas?= =?UTF-8?q?s=20=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/send-scheduled-reports/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supabase/functions/send-scheduled-reports/index.ts b/supabase/functions/send-scheduled-reports/index.ts index 618ebc629..4ac9e02f5 100644 --- a/supabase/functions/send-scheduled-reports/index.ts +++ b/supabase/functions/send-scheduled-reports/index.ts @@ -1,6 +1,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; import { authorizeCron } from "../_shared/dispatcher-auth.ts"; +import { getCredential } from "../_shared/credentials.ts"; const corsHeaders = buildPublicCorsHeaders(); @@ -20,7 +21,8 @@ Deno.serve(async (req: Request) => { try { const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; - const resendKey = Deno.env.get("RESEND_API_KEY"); + // fix: ssot-bypass — credential vault + const resendKey = await getCredential("RESEND_API_KEY"); const supabase = createClient(supabaseUrl, serviceKey); // Find reports due to run From 067436467a6fc0edbbeb6bd249647362ce52958c Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:21 -0300 Subject: [PATCH 04/10] =?UTF-8?q?fix(simulation-orchestrator):=20ssot-bypa?= =?UTF-8?q?ss=20=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/simulation-orchestrator/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supabase/functions/simulation-orchestrator/index.ts b/supabase/functions/simulation-orchestrator/index.ts index ffb14e79a..c5a2929f8 100644 --- a/supabase/functions/simulation-orchestrator/index.ts +++ b/supabase/functions/simulation-orchestrator/index.ts @@ -6,6 +6,7 @@ import { SimulationOrchestratorSchemas, } from "../_shared/contracts/schemas/simulation-orchestrator.ts"; import { buildPublicCorsHeaders } from "../_shared/cors.ts"; +import { getCredential } from "../_shared/credentials.ts"; const corsHeaders = buildPublicCorsHeaders(); @@ -59,7 +60,8 @@ Deno.serve(async (req) => { const supabaseUrl = Deno.env.get("SUPABASE_URL")!; const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; - const n8nSecret = Deno.env.get("N8N_PRODUCT_WEBHOOK_SECRET") || "sim-secret"; + // fix: ssot-bypass — credential vault + const n8nSecret = await getCredential("N8N_PRODUCT_WEBHOOK_SECRET") ?? "sim-secret"; const supabase = createClient(supabaseUrl, serviceRoleKey); const { data: run } = await supabase From ed773d1f56cb02baa6e0378c201db558cdbf0783 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:22 -0300 Subject: [PATCH 05/10] =?UTF-8?q?fix(webhook-dispatcher):=20ssot-bypass=20?= =?UTF-8?q?=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/webhook-dispatcher/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/supabase/functions/webhook-dispatcher/index.ts b/supabase/functions/webhook-dispatcher/index.ts index 9172ea1cf..b8c750cf0 100644 --- a/supabase/functions/webhook-dispatcher/index.ts +++ b/supabase/functions/webhook-dispatcher/index.ts @@ -17,6 +17,7 @@ import { WebhookDispatcherSchemas } from "../_shared/contracts/schemas/webhook-d import { buildPublicCorsHeaders } from "../_shared/cors.ts"; import { authorizeDispatcher } from "../_shared/dispatcher-auth.ts"; import { assertSwitchEnabled } from "../_shared/kill_switch.ts"; +import { getCredential } from "../_shared/credentials.ts"; const corsHeaders = buildPublicCorsHeaders({ allowMethods: "POST, OPTIONS" }); @@ -47,7 +48,8 @@ Deno.serve(async (req) => { if (killResponse) return killResponse; // Guard: require X-Dispatcher-Secret to prevent unauthorized invocations - const dispatcherSecret = Deno.env.get("WEBHOOK_DISPATCHER_SECRET"); + // fix: ssot-bypass — credential vault + const dispatcherSecret = await getCredential("WEBHOOK_DISPATCHER_SECRET"); if (dispatcherSecret) { const incoming = req.headers.get("x-dispatcher-secret"); if (!incoming || incoming !== dispatcherSecret) { From 87f83eb0fe73fb8d99af267f96e643173f2095b0 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:23 -0300 Subject: [PATCH 06/10] =?UTF-8?q?fix(sync-external-db):=20ssot-bypass=20?= =?UTF-8?q?=E2=86=92=20credential=20vault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/sync-external-db/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/supabase/functions/sync-external-db/index.ts b/supabase/functions/sync-external-db/index.ts index 3075aca67..5c5fbe6b1 100644 --- a/supabase/functions/sync-external-db/index.ts +++ b/supabase/functions/sync-external-db/index.ts @@ -9,6 +9,7 @@ import { SyncExternalDbSchemas } from "../_shared/contracts/schemas/sync-externa import { buildPublicCorsHeaders } from "../_shared/cors.ts"; import { authorizeCron } from "../_shared/dispatcher-auth.ts"; import { getOrCreateRequestId } from "../_shared/request-id.ts"; +import { getCredential } from "../_shared/credentials.ts"; const corsHeaders = buildPublicCorsHeaders(); @@ -52,8 +53,9 @@ Deno.serve(async (req) => { const internalSupabase = createClient(internalUrl, internalKey); // 2. Conexao com Supabase Externo - const externalUrl = Deno.env.get("EXTERNAL_SUPABASE_URL") || Deno.env.get("VITE_EXTERNAL_SUPABASE_URL"); - const externalKey = Deno.env.get("EXTERNAL_SUPABASE_SERVICE_ROLE_KEY") || Deno.env.get("EXTERNAL_SUPABASE_SERVICE_KEY"); + // fix: ssot-bypass — credential vault + const externalUrl = await getCredential('EXTERNAL_PROMOBRIND_URL'); + const externalKey = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY'); if (!externalUrl || !externalKey) { log.error("missing_external_config", {}); From 56504fbb9c6e8ed5578954c2033e9088a321e8d1 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:24 -0300 Subject: [PATCH 07/10] =?UTF-8?q?fix(expert-chat):=20ssot-bypass=20?= =?UTF-8?q?=E2=86=92=20credential=20vault=20(4=20issues)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supabase/functions/expert-chat/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/supabase/functions/expert-chat/index.ts b/supabase/functions/expert-chat/index.ts index a154c0917..dcb956582 100644 --- a/supabase/functions/expert-chat/index.ts +++ b/supabase/functions/expert-chat/index.ts @@ -11,7 +11,7 @@ import { z } from 'npm:zod@3.23.8'; import { callAiWithTracking, QuotaExceededError } from '../_shared/ai-usage.ts'; import { rateLimiters, applyRateLimit } from '../_shared/rate-limiter.ts'; import { runBotProtection } from '../_shared/bot-protection.ts'; -import { resolveCredential } from '../_shared/credentials.ts'; +import { resolveCredential, getCredential } from '../_shared/credentials.ts'; import { extractAndParseAIJSON, safeJson } from '../_shared/json-parser.ts'; import { safeErrorFields } from '../_shared/log-safety.ts'; import { assertSwitchEnabled } from '../_shared/kill_switch.ts'; @@ -998,8 +998,9 @@ ${ let productsContext = ''; let semanticResults: any[] = []; - const EXT_URL = Deno.env.get('EXTERNAL_SUPABASE_URL'); - const EXT_KEY = Deno.env.get('EXTERNAL_SUPABASE_SERVICE_KEY'); + // fix: ssot-bypass — credential vault + const EXT_URL = await getCredential('EXTERNAL_PROMOBRIND_URL'); + const EXT_KEY = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY'); if (!EXT_URL || !EXT_KEY) { console.error('External DB env vars not set — cannot fetch products'); @@ -1279,8 +1280,9 @@ ${generalPool.map((p) => buildProductDescription(p)).join('\n\n')} `; } } else { - const EXT_URL2 = Deno.env.get('EXTERNAL_SUPABASE_URL'); - const EXT_KEY2 = Deno.env.get('EXTERNAL_SUPABASE_SERVICE_KEY'); + // fix: ssot-bypass — credential vault + const EXT_URL2 = await getCredential('EXTERNAL_PROMOBRIND_URL'); + const EXT_KEY2 = await getCredential('EXTERNAL_PROMOBRIND_SERVICE_ROLE_KEY'); if (EXT_URL2 && EXT_KEY2) { const extClient = createClient(EXT_URL2, EXT_KEY2); let q = extClient From f2e8802f8a76a50ea57073cd1e5a530bcb03806d Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:36:25 -0300 Subject: [PATCH 08/10] =?UTF-8?q?chore(baseline):=20update=20after=20Sprin?= =?UTF-8?q?ts=201-4=20fixes=20(28=20=E2=86=92=207=20issues)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .audit-credentials-baseline.json | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/.audit-credentials-baseline.json b/.audit-credentials-baseline.json index 295c49800..c2c309ef6 100644 --- a/.audit-credentials-baseline.json +++ b/.audit-credentials-baseline.json @@ -1,33 +1,14 @@ { - "generated_at": "2026-05-26T12:06:33.381Z", + "generated_at": "2026-05-26T15:36:25.000Z", "schema": "v2-counts", - "issue_count": 28, + "issue_count": 7, "signatures": { "supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, "supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, "supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, "supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, "supabase/functions/cnpj-lookup/index.ts:ssot-bypass:SIMULATION_BYPASS_KEY": 1, - "supabase/functions/expert-chat/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 2, - "supabase/functions/expert-chat/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 2, "supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/health-check/index.ts:module-scope-credential-read:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/health-check/index.ts:module-scope-credential-read:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/health-check/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/health-check/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_ROLE_KEY": 1, - "supabase/functions/materials-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/product-webhook/index.ts:module-scope-credential-read:N8N_PRODUCT_WEBHOOK_SECRET": 1, - "supabase/functions/product-webhook/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_SECRET": 1, - "supabase/functions/product-webhook/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_TOLERANCE_SEC": 1, - "supabase/functions/secure-upload/index.ts:ssot-bypass:VIRUSTOTAL_API_KEY": 1, - "supabase/functions/send-scheduled-reports/index.ts:ssot-bypass:RESEND_API_KEY": 1, - "supabase/functions/simulation-orchestrator/index.ts:ssot-bypass:N8N_PRODUCT_WEBHOOK_SECRET": 1, - "supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_ROLE_KEY": 1, - "supabase/functions/sync-external-db/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/webhook-dispatcher/index.ts:ssot-bypass:WEBHOOK_DISPATCHER_SECRET": 1 + "supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1 } } From 02210062a0886e641fdcb0d72a235f900e76adca Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:49:27 -0300 Subject: [PATCH 09/10] =?UTF-8?q?chore(baseline):=20fix=20baseline=20forma?= =?UTF-8?q?t=20=E2=80=94=20array=20issues[]=20(was=20signatures{})=20+=20r?= =?UTF-8?q?egenerate=20from=20clean=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous baseline used deprecated v2-counts schema with signatures{} object. Audit script expects issues[] array format. Regenerated from actual scan: 28 (original) → 6 remaining (categories-api, cleanup-novelties, external-db-inspect). cnpj-lookup already fixed by another PR in main. --- .audit-credentials-baseline.json | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.audit-credentials-baseline.json b/.audit-credentials-baseline.json index c2c309ef6..3f34c7d67 100644 --- a/.audit-credentials-baseline.json +++ b/.audit-credentials-baseline.json @@ -1,14 +1,12 @@ { - "generated_at": "2026-05-26T15:36:25.000Z", - "schema": "v2-counts", - "issue_count": 7, - "signatures": { - "supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/categories-api/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/cleanup-novelties/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1, - "supabase/functions/cnpj-lookup/index.ts:ssot-bypass:SIMULATION_BYPASS_KEY": 1, - "supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY": 1, - "supabase/functions/external-db-inspect/index.ts:ssot-bypass:EXTERNAL_SUPABASE_URL": 1 - } + "generated_at": "2026-05-26T15:49:16.936Z", + "issue_count": 6, + "issues": [ + "supabase/functions/categories-api/index.ts:28:ssot-bypass:EXTERNAL_SUPABASE_URL", + "supabase/functions/categories-api/index.ts:29:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY", + "supabase/functions/cleanup-novelties/index.ts:38:ssot-bypass:EXTERNAL_SUPABASE_URL", + "supabase/functions/cleanup-novelties/index.ts:39:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY", + "supabase/functions/external-db-inspect/index.ts:78:ssot-bypass:EXTERNAL_SUPABASE_URL", + "supabase/functions/external-db-inspect/index.ts:79:ssot-bypass:EXTERNAL_SUPABASE_SERVICE_KEY" + ] } From 0d42b1a91349c197f6f3b36e0767ea0c182147c5 Mon Sep 17 00:00:00 2001 From: TIPROMO Date: Tue, 26 May 2026 12:53:08 -0300 Subject: [PATCH 10/10] =?UTF-8?q?fix(lint):=20corrige=203=20erros=20ESLint?= =?UTF-8?q?=20p=C3=B3s-merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useGlobalSearch.ts: remove type SimpleQueryBuilder orphan (ficou do PR #380 mas untypedFrom do main não o usa) - MockupGenerator.tsx: remove import Badge não usado - MockupGenerator.tsx: renomeia summary → _summary (definida mas não usada no JSX) ESLint baseline gate: ✅ zero regressões (drift positivo: -19 erros) --- src/pages/mockups/MockupGenerator.tsx | 469 ++++++++++++++++++-------- 1 file changed, 327 insertions(+), 142 deletions(-) diff --git a/src/pages/mockups/MockupGenerator.tsx b/src/pages/mockups/MockupGenerator.tsx index 413d78c65..f9de5c432 100644 --- a/src/pages/mockups/MockupGenerator.tsx +++ b/src/pages/mockups/MockupGenerator.tsx @@ -1,47 +1,78 @@ -import { type MockupTechnique } from "@/types/external-db"; +import { type MockupTechnique } from '@/types/external-db'; /** * MockupGenerator — Refactored v5.2 - * + * * Business logic in useMockupGenerator hook. * Progressive Preview + Enhanced Header + Sticky Navigator. */ -import { useMemo, useCallback, useState, Suspense } from "react"; -import { useProductsContext } from "@/contexts/ProductsContext"; -import { deleteMockupFromDb } from "@/hooks/mockup/mockupGenerationService"; -import { PageSEO } from "@/components/seo/PageSEO"; -import { Button } from "@/components/ui/button"; -import { Loader2, AlertCircle, CheckCircle2, History, Wand2 } from "lucide-react"; -import { toast } from "sonner"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { TechniqueChangeDialog, DeleteMockupDialog } from "@/pages/mockups/mockup-generator/MockupDialogs"; -import { MockupToolbar } from "@/pages/mockups/mockup-generator/MockupToolbar"; -import { MockupEmptyState } from "@/pages/mockups/mockup-generator/MockupEmptyState"; -import { useKeyboardShortcuts } from "@/components/mockup/KeyboardShortcuts"; -import { GeneratingOverlay } from "@/components/mockup/GeneratingOverlay"; -import { useMockupGenerator } from "@/hooks/mockup"; -import { useAuth } from "@/contexts/AuthContext"; -import { useTechniqueHandlers } from "@/pages/mockups/mockup-generator/MockupTechniqueHandlers"; -import type { MockupApprovalData } from "@/types/mockup-approval"; -import { DiagnosticProfiler } from "@/components/dev/DiagnosticProfiler"; -import { lazyWithRetry } from "@/lib/lazyWithRetry"; -import type { LayoutCaptureRequest } from "@/components/mockup/approval/OffscreenLayoutCapture"; +import { useMemo, useCallback, useState, Suspense } from 'react'; +import { useProductsContext } from '@/contexts/ProductsContext'; +import { deleteMockupFromDb } from '@/hooks/mockup/mockupGenerationService'; +import { PageSEO } from '@/components/seo/PageSEO'; +import { Button } from '@/components/ui/button'; +import { Loader2, AlertCircle, CheckCircle2, History, Wand2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { + TechniqueChangeDialog, + DeleteMockupDialog, +} from '@/pages/mockups/mockup-generator/MockupDialogs'; +import { MockupToolbar } from '@/pages/mockups/mockup-generator/MockupToolbar'; +import { MockupEmptyState } from '@/pages/mockups/mockup-generator/MockupEmptyState'; +import { useKeyboardShortcuts } from '@/components/mockup/KeyboardShortcuts'; +import { GeneratingOverlay } from '@/components/mockup/GeneratingOverlay'; +import { useMockupGenerator } from '@/hooks/mockup'; +import { useAuth } from '@/contexts/AuthContext'; +import { useTechniqueHandlers } from '@/pages/mockups/mockup-generator/MockupTechniqueHandlers'; +import type { MockupApprovalData } from '@/types/mockup-approval'; +import { DiagnosticProfiler } from '@/components/dev/DiagnosticProfiler'; +import { lazyWithRetry } from '@/lib/lazyWithRetry'; +import type { LayoutCaptureRequest } from '@/components/mockup/approval/OffscreenLayoutCapture'; // Lazy load heavy sub-components -const LogoPositionEditor = lazyWithRetry(() => import("@/components/mockup/LogoPositionEditor").then(m => ({ default: m.LogoPositionEditor }))); -const MockupWizard = lazyWithRetry(() => import("@/components/mockup/MockupWizard").then(m => ({ default: m.MockupWizard }))); -const MockupResultCard = lazyWithRetry(() => import("@/components/mockup/MockupResultCard").then(m => ({ default: m.MockupResultCard }))); -const MockupConfigPanel = lazyWithRetry(() => import("@/components/mockup/MockupConfigPanel").then(m => ({ default: m.MockupConfigPanel }))); -const MockupHistoryPanel = lazyWithRetry(() => import("@/components/mockup/MockupHistoryPanel").then(m => ({ default: m.MockupHistoryPanel }))); -const MockupLayoutButtons = lazyWithRetry(() => import("@/components/mockup/approval/MockupLayoutButtons").then(m => ({ default: m.MockupLayoutButtons }))); -const OffscreenLayoutCapture = lazyWithRetry(() => import("@/components/mockup/approval/OffscreenLayoutCapture").then(m => ({ default: m.OffscreenLayoutCapture }))); -const TechniqueColorConfigDialog = lazyWithRetry(() => import("@/components/mockup/TechniqueColorConfigDialog").then(m => ({ default: m.TechniqueColorConfigDialog }))); -const AIMockupAssistant = lazyWithRetry(() => import("@/components/ai").then(m => ({ default: m.AIMockupAssistant }))); +const LogoPositionEditor = lazyWithRetry(() => + import('@/components/mockup/LogoPositionEditor').then((m) => ({ default: m.LogoPositionEditor })), +); +const MockupWizard = lazyWithRetry(() => + import('@/components/mockup/MockupWizard').then((m) => ({ default: m.MockupWizard })), +); +const MockupResultCard = lazyWithRetry(() => + import('@/components/mockup/MockupResultCard').then((m) => ({ default: m.MockupResultCard })), +); +const MockupConfigPanel = lazyWithRetry(() => + import('@/components/mockup/MockupConfigPanel').then((m) => ({ default: m.MockupConfigPanel })), +); +const MockupHistoryPanel = lazyWithRetry(() => + import('@/components/mockup/MockupHistoryPanel').then((m) => ({ default: m.MockupHistoryPanel })), +); +const MockupLayoutButtons = lazyWithRetry(() => + import('@/components/mockup/approval/MockupLayoutButtons').then((m) => ({ + default: m.MockupLayoutButtons, + })), +); +const OffscreenLayoutCapture = lazyWithRetry(() => + import('@/components/mockup/approval/OffscreenLayoutCapture').then((m) => ({ + default: m.OffscreenLayoutCapture, + })), +); +const TechniqueColorConfigDialog = lazyWithRetry(() => + import('@/components/mockup/TechniqueColorConfigDialog').then((m) => ({ + default: m.TechniqueColorConfigDialog, + })), +); +const AIMockupAssistant = lazyWithRetry(() => + import('@/components/ai').then((m) => ({ default: m.AIMockupAssistant })), +); const STEP_SECTION_MAP: Record = { - 1: "step-client", 2: "step-product", 3: "step-technique", - 4: "step-logo", 5: "step-logo", 6: "step-logo", + 1: 'step-client', + 2: 'step-product', + 3: 'step-technique', + 4: 'step-logo', + 5: 'step-logo', + 6: 'step-logo', }; function scrollToStep(step: number, highlight = false): void { @@ -54,7 +85,10 @@ function scrollToStep(step: number, highlight = false): void { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); if (highlight) { el.classList.add('ring-2', 'ring-primary/50', 'rounded-lg'); - window.setTimeout(() => el.classList.remove('ring-2', 'ring-primary/50', 'rounded-lg'), 2000); + window.setTimeout( + () => el.classList.remove('ring-2', 'ring-primary/50', 'rounded-lg'), + 2000, + ); } return; } @@ -78,7 +112,7 @@ export default function MockupGenerator() { if (mg.selectedClient) parts.push(mg.selectedClient.name); if (mg.selectedProduct) parts.push(mg.selectedProduct.name); if (mg.selectedTechnique) parts.push(mg.selectedTechnique.name); - return parts.join(" · "); + return parts.join(' · '); }, [mg.selectedClient, mg.selectedProduct, mg.selectedTechnique]); const technique = useTechniqueHandlers({ @@ -100,20 +134,25 @@ export default function MockupGenerator() { canDownload: !!mg.generatedMockup, isLoading: mg.isLoading, onStepChange: (step) => { - mg.setActiveTab("generator"); + mg.setActiveTab('generator'); scrollToStep(step); - } + }, }); const layoutCaptureRequest = useMemo((): LayoutCaptureRequest | null => { - if (!mg.lastSavedRecordId || !user?.id || !mg.selectedProduct || !mg.selectedTechnique) return null; - const mockupUrl = mg.lastSavedMockupUrl || mg.generatedMockup || ""; + if (!mg.lastSavedRecordId || !user?.id || !mg.selectedProduct || !mg.selectedTechnique) + return null; + const mockupUrl = mg.lastSavedMockupUrl || mg.generatedMockup || ''; if (!mockupUrl) return null; // recordId é estável por captura → derivamos data/numeroDoc dele para manter o memo determinístico const stableSeed = mg.lastSavedRecordId; const seedDate = new Date(parseInt(stableSeed.slice(0, 8), 16) * 1000 || Date.now()); - const dateStr = seedDate.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }); + const dateStr = seedDate.toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); const docNumber = `MK-${stableSeed.slice(0, 12).toUpperCase()}`; const tech = mg.selectedTechnique as MockupTechnique; @@ -121,11 +160,15 @@ export default function MockupGenerator() { documentNumber: docNumber, date: dateStr, client: { - name: mg.selectedClient?.nome_fantasia || mg.selectedClient?.razao_social || mg.selectedClient?.name || "—", + name: + mg.selectedClient?.nome_fantasia || + mg.selectedClient?.razao_social || + mg.selectedClient?.name || + '—', cnpj: mg.selectedClient?.cnpj, logoUrl: mg.selectedClient?.logo_url || undefined, }, - seller: { name: profile?.full_name || "—", email: profile?.email || undefined }, + seller: { name: profile?.full_name || '—', email: profile?.email || undefined }, product: { name: mg.selectedProduct.name, sku: mg.selectedProduct.sku, @@ -143,13 +186,13 @@ export default function MockupGenerator() { personalization: { techniqueName: tech.name ?? '', techniqueCode: tech.code ?? '', - locationName: tech.locationName ?? mg.activeArea?.name ?? "Frente", + locationName: tech.locationName ?? mg.activeArea?.name ?? 'Frente', widthCm: mg.activeArea?.logoWidth || 0, heightCm: mg.activeArea?.logoHeight || 0, colorsCount: mg.techniqueColorConfig?.colorCount, }, pantoneColors: (mg.logoColorAnalysis.colors || []).map((c) => ({ - name: c.selectedPantone || c.pantoneMatch?.pantoneCode || c.name || "", + name: c.selectedPantone || c.pantoneMatch?.pantoneCode || c.name || '', hex: c.hex, })), mockupImageUrl: mockupUrl, @@ -157,7 +200,25 @@ export default function MockupGenerator() { }; return { data: approvalData, recordId: mg.lastSavedRecordId, userId: user.id }; - }, [mg.lastSavedRecordId, mg.lastSavedMockupUrl, mg.lastSavedLayoutMode, user?.id, mg.selectedProduct, mg.selectedTechnique, mg.selectedClient, mg.activeArea?.logoWidth, mg.activeArea?.logoHeight, mg.activeArea?.name, mg.generatedMockup, profile, mg.techniqueColorConfig?.colorCount, mg.logoColorAnalysis.colors, mg.productSelection?.colorName, mg.productSelection?.colorHex, mg.getProductImage]); + }, [ + mg.lastSavedRecordId, + mg.lastSavedMockupUrl, + mg.lastSavedLayoutMode, + user?.id, + mg.selectedProduct, + mg.selectedTechnique, + mg.selectedClient, + mg.activeArea?.logoWidth, + mg.activeArea?.logoHeight, + mg.activeArea?.name, + mg.generatedMockup, + profile, + mg.techniqueColorConfig?.colorCount, + mg.logoColorAnalysis.colors, + mg.productSelection?.colorName, + mg.productSelection?.colorHex, + mg.getProductImage, + ]); const handleLayoutCaptured = useCallback(() => { mg.setLastSavedRecordId(null); @@ -166,22 +227,29 @@ export default function MockupGenerator() { }, [mg.setLastSavedRecordId, mg.setLastSavedMockupUrl, mg.fetchHistory]); return ( - - + + - - -
- -

Gerador de Mockups

+ +
+

+ Gerador de Mockups +

- - {mg.activeTab !== "history" && ( -
+ {mg.activeTab !== 'history' && ( +
{ - mg.setActiveTab("generator"); + mg.setActiveTab('generator'); scrollToStep(step, true); }} /> @@ -204,7 +272,9 @@ export default function MockupGenerator() { Rascunho restaurado - Seu progresso anterior foi restaurado automaticamente. + + Seu progresso anterior foi restaurado automaticamente. + )} @@ -214,22 +284,38 @@ export default function MockupGenerator() { Erro na geração {mg.generationError} - + )} - mg.setActiveTab(v as "generator" | "history")} className="w-full"> -
+ mg.setActiveTab(v as 'generator' | 'history')} + className="w-full" + > +
- Gerar Mockup - Histórico ({mg.mockupHistory.length}) + + Gerar Mockup + + + Histórico ({mg.mockupHistory.length}) + { const state = mg.positionHistory.undo(); if (state) mg.updateActiveArea(state); }} - onRedo={() => { const state = mg.positionHistory.redo(); if (state) mg.updateActiveArea(state); }} + onUndo={() => { + const state = mg.positionHistory.undo(); + if (state) mg.updateActiveArea(state); + }} + onRedo={() => { + const state = mg.positionHistory.redo(); + if (state) mg.updateActiveArea(state); + }} isDraftSaving={mg.isDraftSaving} lastSaved={mg.lastSaved} draftError={mg.draftError} @@ -237,8 +323,14 @@ export default function MockupGenerator() {
-
}> -
+ + +
+ } + > +
{ mg.setProductSelection(sel); mg.setGeneratedMockup(null); }} - onTechniqueSelect={(t) => technique.handleTechniqueChange(t as MockupTechnique | null)} + onProductSelect={(sel) => { + mg.setProductSelection(sel); + mg.setGeneratedMockup(null); + }} + onTechniqueSelect={(t) => + technique.handleTechniqueChange(t as MockupTechnique | null) + } onClientSelect={mg.setSelectedClient} onReset={mg.resetForm} activeAreaId={mg.activeAreaId} onAreasChange={mg.setPersonalizationAreas} onActiveAreaChange={mg.setActiveAreaId} onLogoUpload={mg.handleAreaLogoUpload} - onLogoRemove={() => { - mg.logoColorAnalysis.clearAnalysis(); - if (mg.activeArea) { - mg.updateActiveArea({ logoPreview: null, logoFile: null }); - } - mg.setGeneratedMockup(null); - }} - productLocations={mg.productLocations} - logoColorAnalysis={mg.logoColorAnalysis} - artAttachments={mg.artAttachments} - onArtAttachmentsChange={mg.setArtAttachments} - userId={user?.id} - /> - -
+ onLogoRemove={() => { + mg.logoColorAnalysis.clearAnalysis(); + if (mg.activeArea) { + mg.updateActiveArea({ logoPreview: null, logoFile: null }); + } + mg.setGeneratedMockup(null); + }} + productLocations={mg.productLocations} + logoColorAnalysis={mg.logoColorAnalysis} + artAttachments={mg.artAttachments} + onArtAttachmentsChange={mg.setArtAttachments} + userId={user?.id} + /> + +
{mg.selectedProduct && mg.getProductImage() && mg.activeArea ? ( mg.updateActiveArea({ positionX: x, positionY: y })} + maxWidth={ + mg.selectedTechnique && 'maxWidth' in mg.selectedTechnique + ? (mg.selectedTechnique as { maxWidth: number | null }).maxWidth + : null + } + maxHeight={ + mg.selectedTechnique && 'maxHeight' in mg.selectedTechnique + ? (mg.selectedTechnique as { maxHeight: number | null }).maxHeight + : null + } + productHeightCm={ + mg.selectedProduct?.dimensions?.height_cm ?? + (mg.selectedProduct?.metadata?.height_mm + ? mg.selectedProduct.metadata.height_mm / 10 + : null) + } + productWidthCm={ + mg.selectedProduct?.dimensions?.width_cm ?? + mg.selectedProduct?.dimensions?.diameter_cm ?? + (mg.selectedProduct?.metadata?.width_mm + ? mg.selectedProduct.metadata.width_mm / 10 + : null) + } + onPositionChange={(x, y) => + mg.updateActiveArea({ positionX: x, positionY: y }) + } onSizeChange={(w, h) => mg.updateActiveArea({ logoWidth: w, logoHeight: h })} onRotationChange={(r) => mg.updateActiveArea({ logoRotation: r })} onLogoScaleChange={(s) => mg.updateActiveArea({ logoScale: s })} @@ -295,36 +413,79 @@ export default function MockupGenerator() { headerActions={ { if (mg.activeArea) { - const recordId = await mg.saveMockupToHistory(dataUrl, mg.activeArea, extra); + const recordId = await mg.saveMockupToHistory( + dataUrl, + mg.activeArea, + extra, + ); if (recordId) { mg.setLastSavedMockupUrl(dataUrl); mg.setLastSavedLayoutMode('static'); @@ -338,7 +499,7 @@ export default function MockupGenerator() { } /> ) : ( - 1 && (
-

Todas as áreas ({mg.generatedBatchMockups.length})

+

+ Todas as áreas ({mg.generatedBatchMockups.length}) +

{mg.generatedBatchMockups.map((batch, idx) => ( -
- {batch.areaName} +
+ {batch.areaName}
-

{batch.areaName}

+

{batch.areaName}

))} @@ -375,35 +546,49 @@ export default function MockupGenerator() {
)} - { - if (suggestion.techniqueId) { - const tech = mg.techniques.find(t => t.id === suggestion.techniqueId); - if (tech) technique.handleTechniqueChange(tech as MockupTechnique); - } - if (suggestion.position) { - mg.updateActiveArea({ positionX: suggestion.position.x, positionY: suggestion.position.y }); - } - }} /> + { + if (suggestion.techniqueId) { + const tech = mg.techniques.find((t) => t.id === suggestion.techniqueId); + if (tech) technique.handleTechniqueChange(tech as MockupTechnique); + } + if (suggestion.position) { + mg.updateActiveArea({ + positionX: suggestion.position.x, + positionY: suggestion.position.y, + }); + } + }} + />
-
}> + + +
+ } + > { setMockupToDelete(id); setDeleteDialogOpen(true); }} + onDelete={(id) => { + setMockupToDelete(id); + setDeleteDialogOpen(true); + }} onDownload={(mockup) => mg.downloadMockup(mockup)} onLoadFromHistory={(mockup) => { - const product = getProductById(mockup.product_id || ""); + const product = getProductById(mockup.product_id || ''); if (product) { mg.setProductSelection({ product, variant: null, imageUrl: mockup.mockup_url }); - mg.setActiveTab("generator"); - toast.success("Produto restaurado do histórico"); + mg.setActiveTab('generator'); + toast.success('Produto restaurado do histórico'); } }} /> @@ -421,27 +606,27 @@ export default function MockupGenerator() { hasGeneratedMockup={!!mg.generatedMockup} /> - { setDeleteDialogOpen(open); if (!open) setMockupToDelete(null); - }} + }} onConfirm={async () => { if (mockupToDelete) { try { await deleteMockupFromDb(mockupToDelete, user?.id); - toast.success("Mockup excluído com sucesso"); + toast.success('Mockup excluído com sucesso'); await mg.fetchHistory(); } catch (error) { - console.error("Erro ao excluir mockup:", error); - toast.error("Não foi possível excluir o mockup. Tente novamente."); + console.error('Erro ao excluir mockup:', error); + toast.error('Não foi possível excluir o mockup. Tente novamente.'); } finally { setDeleteDialogOpen(false); setMockupToDelete(null); } } - }} + }} />
- + ); -} \ No newline at end of file +}