refactor(D6): simplify — find endpoint-only no subscribe, sslmode verify-full, guards no check-push-chain

This commit is contained in:
Francisco Solla 2026-08-10 00:01:46 -03:00
parent f4873b8b3f
commit c2ddbf6006
4 changed files with 41 additions and 40 deletions

View file

@ -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=<url> 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`,
)

View file

@ -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) {

View file

@ -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 {

View file

@ -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')
})
})