From c2ddbf6006c52094726042441a02a1728ff72c75 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 10 Aug 2026 00:01:46 -0300 Subject: [PATCH] =?UTF-8?q?refactor(D6):=20simplify=20=E2=80=94=20find=20e?= =?UTF-8?q?ndpoint-only=20no=20subscribe,=20sslmode=20verify-full,=20guard?= =?UTF-8?q?s=20no=20check-push-chain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check-push-chain.mjs | 48 +++++++++++-------- .../campanha/actions/notifications.ts | 12 ++--- src/app/(campaign)/campanha/sw.js/route.ts | 7 +-- tests/unit/campaignPwa.unit.spec.ts | 14 +++--- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/scripts/check-push-chain.mjs b/scripts/check-push-chain.mjs index 0a1f0263..0e88fc91 100644 --- a/scripts/check-push-chain.mjs +++ b/scripts/check-push-chain.mjs @@ -19,7 +19,7 @@ */ import { Client } from 'pg' -import { dieWithLabel, loadCliEnv } from './lib/cli.mjs' +import { dieWithLabel, loadCliEnv, LOCAL_HOSTS } from './lib/cli.mjs' const die = dieWithLabel('check:push-chain') @@ -34,9 +34,19 @@ if (!PROD_DATABASE_URL) { ) } -const siteArg = process.argv.find((arg) => arg.startsWith('--site=')) -const site = siteArg?.slice('--site='.length) || 'https://pt.jorgesolla.com.br' -const siteOrigin = new URL(site).origin +// Foot-gun guard: this script reports PRODUCTION facts; a local URL would +// produce verdicts about the wrong data that look like prod. +if (LOCAL_HOSTS.has(new URL(PROD_DATABASE_URL).hostname)) { + die(`PROD_DATABASE_URL points at a local host ("${new URL(PROD_DATABASE_URL).hostname}").`) +} + +let site +try { + const siteArg = process.argv.find((arg) => arg.startsWith('--site=')) + site = new URL(siteArg?.slice('--site='.length) || 'https://pt.jorgesolla.com.br').origin +} catch { + die('--site= deve ser uma URL válida (ex.: https://pt.jorgesolla.com.br).') +} const results = [] const report = (name, ok, detail) => { @@ -49,13 +59,15 @@ const fetchWithTimeout = async (url) => { return { response, body: await response.text() } } -console.log(`[check:push-chain] Site: ${siteOrigin}`) +console.log(`[check:push-chain] Site: ${site}`) // --- Elo 1: service worker served with a deploy-scoped build id --------------- +// `campanha-dev` = CAMPAIGN_CACHE_PREFIX ('campanha-') + fallback 'dev' of +// resolveCampaignPwaBuildId frozen at build time (the D6 defect). try { - const { response, body } = await fetchWithTimeout(`${siteOrigin}/campanha/sw.js`) + const { response, body } = await fetchWithTimeout(`${site}/campanha/sw.js`) const cacheName = body.match(/CACHE_NAME\s*=\s*"([^"]+)"/)?.[1] ?? null - const frozenBuild = cacheName === 'campanha-dev' || cacheName === 'campanha-dev"' + const frozenBuild = cacheName === 'campanha-dev' report( 'SW servido', response.ok && cacheName !== null && !frozenBuild, @@ -64,10 +76,12 @@ try { : `HTTP ${response.status}`, ) } catch (error) { - report('SW servido', false, `falha ao buscar ${siteOrigin}/campanha/sw.js: ${error.message}`) + report('SW servido', false, `falha ao buscar ${site}/campanha/sw.js: ${error.message}`) } // --- Elo 2: VAPID envs (server + public) -------------------------------------- +// Proves the DIAGNOSTIC environment, not the deployment — source them from +// `vercel env pull --environment=production` to mirror the deployed server. const vapidServer = process.env.VAPID_PUBLIC_KEY?.trim() const vapidPrivate = process.env.VAPID_PRIVATE_KEY?.trim() const vapidPublic = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY?.trim() @@ -77,7 +91,7 @@ const missingEnvs = [ !vapidPublic && 'NEXT_PUBLIC_VAPID_PUBLIC_KEY', ].filter(Boolean) report( - 'Envs VAPID', + 'Envs VAPID (ambiente de diagnóstico)', missingEnvs.length === 0, missingEnvs.length === 0 ? 'VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / NEXT_PUBLIC_VAPID_PUBLIC_KEY presentes' @@ -85,23 +99,17 @@ report( ) // --- Elos 3–5: DB (read-only) ------------------------------------------------- -// Neon URLs carry `sslmode=require`; hand TLS explicitly so pg-connection-string -// does not emit its v8 deprecation warning for that mode. -const hadSslMode = /sslmode=require/.test(PROD_DATABASE_URL) -const connectionString = hadSslMode - ? PROD_DATABASE_URL.replace(/sslmode=require/, '').replace(/[?&]$/, '') - : PROD_DATABASE_URL -const client = new Client({ - connectionString, - ...(hadSslMode ? { ssl: { rejectUnauthorized: false } } : {}), -}) +// Neon URLs carry `sslmode=require`; upgrade it to verify-full so pg keeps +// certificate verification without its v8 warning for `require` aliases. +const connectionString = PROD_DATABASE_URL.replace('sslmode=require', 'sslmode=verify-full') +const client = new Client({ connectionString }) try { await client.connect() await client.query('SET default_transaction_read_only = on') await client.query('SET statement_timeout = 15000') - // Elo 3: consent published + // Elo 3: consent published — stable key lives in src/lib/campaignConsentKeys.ts const consent = await client.query( `SELECT key, updated_at FROM consent WHERE key = 'campanha-notificacoes-push' LIMIT 1`, ) diff --git a/src/app/(campaign)/campanha/actions/notifications.ts b/src/app/(campaign)/campanha/actions/notifications.ts index 7deac685..3e26877f 100644 --- a/src/app/(campaign)/campanha/actions/notifications.ts +++ b/src/app/(campaign)/campanha/actions/notifications.ts @@ -125,18 +125,16 @@ export const subscribeCampaignPush = async (input: PushSubscribeInput) => throw new Error(CAMPAIGN_PUSH_SUBSCRIPTION_INVALID_MESSAGE) } - // Idempotent subscribe (D6): the browser returns the SAME subscription when - // the device is already registered, and `endpoint` is unique — a second - // create would throw and show a false failure with the device already active. + // Idempotent (D6): `endpoint` is unique and the browser returns the same + // subscription on re-subscribe — a second create would show a false failure. const existing = await payload.find({ collection: 'pushSubscription', - where: { - and: [{ user: { equals: user.id } }, { endpoint: { equals: endpoint } }], - }, + where: { endpoint: { equals: endpoint } }, depth: 0, limit: 1, pagination: false, - overrideAccess: true, + user, + overrideAccess: false, }) if (existing.docs.length > 0) { diff --git a/src/app/(campaign)/campanha/sw.js/route.ts b/src/app/(campaign)/campanha/sw.js/route.ts index 11bb059f..5c77b004 100644 --- a/src/app/(campaign)/campanha/sw.js/route.ts +++ b/src/app/(campaign)/campanha/sw.js/route.ts @@ -6,11 +6,8 @@ import { resolveCampaignPwaBuildId, } from '@/utilities/campaignPwa' -// Dynamic on purpose (D6): deploys are CLI `vercel build` + `--prebuilt`, so the -// Vercel build-time envs (`VERCEL_GIT_COMMIT_SHA`/`VERCEL_DEPLOYMENT_ID`) are not -// present when a `force-static` route is prerendered — prod was serving `dev` as -// the cache name, so SW caches never versioned per deploy. `VERCEL_DEPLOYMENT_ID` -// IS set at runtime, which makes the build id unique per deployment. +// Runtime env only: CLI `--prebuilt` builds have no VERCEL_* build-time envs +// (prod served `campanha-dev`); `VERCEL_DEPLOYMENT_ID` exists at runtime. export const dynamic = 'force-dynamic' export function GET(): NextResponse { diff --git a/tests/unit/campaignPwa.unit.spec.ts b/tests/unit/campaignPwa.unit.spec.ts index dc0214ec..d5d84d62 100644 --- a/tests/unit/campaignPwa.unit.spec.ts +++ b/tests/unit/campaignPwa.unit.spec.ts @@ -3,7 +3,10 @@ import { describe, expect, it } from 'vitest' import { GET as getManifest } from '@/app/(campaign)/campanha/manifest.webmanifest/route' -import { GET as getServiceWorker } from '@/app/(campaign)/campanha/sw.js/route' +import { + GET as getServiceWorker, + dynamic as serviceWorkerDynamic, +} from '@/app/(campaign)/campanha/sw.js/route' import { buildCampaignServiceWorkerScript, CAMPAIGN_CACHE_PREFIX, @@ -69,12 +72,7 @@ describe('campaign PWA route handlers', () => { expect(body).toContain('isRscRequest') }) - it('serves the service worker dynamically so the deploy-scoped build id resolves at runtime', async () => { - const route = await import('@/app/(campaign)/campanha/sw.js/route') - - // D6: CLI `--prebuilt` deploys lack the build-time Vercel envs that a - // force-static prerender would read; runtime VERCEL_DEPLOYMENT_ID is - // guaranteed on Vercel, so the cache name must not be frozen at build. - expect(route.dynamic).toBe('force-dynamic') + it('serves the service worker dynamically so the deploy-scoped build id resolves at runtime', () => { + expect(serviceWorkerDynamic).toBe('force-dynamic') }) })