From 473c82d09d8d1b8b6c3508ae46f7c5294282e0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 23 Jul 2026 22:25:18 -0300 Subject: [PATCH 1/9] Lock down CMS/PII collection access and leader routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - users/signature/subscription/consent: all operations Payload-admin-only (Payload's default allowed ANY authenticated user, incl. campaign JWTs via /api — privilege escalation + PII exposure) - post/tag/media/petition: public read kept, writes admin-only, readVersions admin-only on drafted collections - leader lockdown: staff gates on /campanha/demandas* and /campanha/planos*; removed the dead leader branch of the demand detail page - fix createLeaderSupporter: scope now asserted via the leader's engaged leadership municipalities (canReadMunicipality always denied leaders) - candidate alignment: supporter area, invite creation, and escalated demand decisions now treat candidate as unrestricted staff consistently - tests: collectionAccessLockdown.int.spec pins the lockdown; leased-consent helpers generalized (supporter keys); fixtures purge deletes residue invites before leaderships; stale leader-phone tests updated to the lockdown model; 'sem Praça' copy renamed to 'sem município' --- .../campanha/(app)/apoiadores/[id]/page.tsx | 6 +- .../(app)/apoiadores/novo/formActions.ts | 2 +- .../campanha/(app)/apoiadores/novo/page.tsx | 6 +- .../campanha/(app)/apoiadores/page.tsx | 7 +- .../campanha/(app)/demandas/[slug]/page.tsx | 140 ++++++------ .../campanha/(app)/demandas/nova/page.tsx | 2 + .../campanha/(app)/demandas/page.tsx | 2 + .../campanha/(app)/planos/[slug]/page.tsx | 1 + .../(campaign)/campanha/(app)/planos/page.tsx | 1 + .../campanha/actions/leaderSupporter.ts | 24 +- .../(campaign)/campanha/actions/supporter.ts | 2 +- src/collections/Consent.ts | 10 + src/collections/Media.ts | 6 + src/collections/Petition.ts | 5 + src/collections/Post.ts | 5 + src/collections/Signature.ts | 9 + src/collections/Subscription.ts | 9 + src/collections/Tag.ts | 4 + src/collections/Users.ts | 12 + .../campaign/DemandWorkflowCard.tsx | 9 +- src/utilities/campaignAccess.ts | 39 ++++ src/utilities/campaignInviteCreation.ts | 5 +- src/utilities/supporterUi.ts | 3 +- tests/helpers/campaignFixtures.ts | 45 +++- tests/helpers/testDatabaseLease.ts | 84 ++++--- tests/int/campaignSupporter.int.spec.ts | 119 ++++++---- tests/int/campaignUserPhone.int.spec.ts | 30 ++- .../int/collectionAccessLockdown.int.spec.ts | 207 ++++++++++++++++++ 28 files changed, 593 insertions(+), 201 deletions(-) create mode 100644 tests/int/collectionAccessLockdown.int.spec.ts diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx index 3142cc6e..9460a666 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/[id]/page.tsx @@ -13,7 +13,7 @@ import { VoteIntentionControl } from '@/components/campaign/VoteIntentionControl import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { isCampaignCoordinator } from '@/utilities/campaignAccess' +import { campaignRoleLabels } from '@/utilities/campaignUserProfile' import { getCampaignUser } from '@/utilities/campaignAuth' import { loadSupporterDetailConsentData, @@ -69,9 +69,7 @@ export default async function SupporterDetailPage({ params }: SupporterDetailPag Voltar para apoiadores - - {isCampaignCoordinator(user) ? 'Coordenador Geral' : 'Assessor'} - + {campaignRoleLabels[user.role]}

{supporter.name}

{supporter.city ?? 'Município não informado'} diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts b/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts index 4a43e2c4..1fdfc67e 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts @@ -34,7 +34,7 @@ const safeActionMessages = [ 'Esta pessoa já está cadastrada como apoiador nesta Praça.', 'Existe mais de um contato com este celular. Resolva a duplicidade no admin antes de continuar.', 'Somente a coordenação e a assessoria podem gerenciar apoiadores.', - 'Somente o Coordenador Geral pode cadastrar apoiadores sem Praça.', + 'Somente o Coordenador Geral pode cadastrar apoiadores sem município.', 'Consentimento de cadastro de apoiador ainda não configurado.', 'Consentimento de intenção de voto ainda não configurado.', ] as const diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/novo/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/novo/page.tsx index baeeeb9f..73785b44 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/novo/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/novo/page.tsx @@ -7,7 +7,7 @@ import { createSupporterFormAction } from '@/app/(campaign)/campanha/(app)/apoia import { CampaignScopeBadge } from '@/components/campaign/CampaignScopeBadge' import { SupporterForm } from '@/components/campaign/SupporterForm' import { Button } from '@/components/ui/button' -import { isCampaignCoordinator } from '@/utilities/campaignAccess' +import { campaignRoleLabels } from '@/utilities/campaignUserProfile' import { getCampaignUser } from '@/utilities/campaignAuth' import { loadSupporterCreatePageData } from '@/utilities/supporterPageData' import { canAccessSupporterArea } from '@/utilities/supporterUi' @@ -26,9 +26,7 @@ export default async function NewSupporterPage() { - - {isCampaignCoordinator(user) ? 'Coordenador Geral' : 'Assessor'} - + {campaignRoleLabels[user.role]}

Novo apoiador

Cadastre um apoiador com telefone obrigatório e consentimento LGPD. diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx index 737119f4..d3de2acc 100644 --- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx +++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx @@ -19,8 +19,9 @@ import { EmptyMedia, EmptyTitle, } from '@/components/ui/Empty' -import { isCampaignCoordinator } from '@/utilities/campaignAccess' +import { isCampaignCoordinator, isCampaignUnrestricted } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' +import { campaignRoleLabels } from '@/utilities/campaignUserProfile' import { loadSupportersPageData } from '@/utilities/supporterPageData' import { buildSupporterFiltersKey, @@ -58,8 +59,8 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro Base nominal de apoio com intenção de voto e vínculo opcional a Praças.

- {isCampaignCoordinator(user) - ? 'Coordenador Geral · todos os apoiadores' + {isCampaignUnrestricted(user) + ? `${campaignRoleLabels[user.role]} · todos os apoiadores` : getSupporterScopeLabel(result.totalDocs)} diff --git a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx index 19c15e86..daf83c54 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx @@ -13,7 +13,7 @@ import { campaignDemandStatusLabels, type CampaignDemandStatus, } from '@/lib/schemas/campaignDemand' -import { isCampaignStaff } from '@/utilities/campaignAccess' +import { isCampaignStaff, isCampaignUnrestricted } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { loadDemandDetail } from '@/utilities/campaignDemandData' import { @@ -51,12 +51,11 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps const { slug } = await params const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) if (!user) redirect('/campanha/login') + if (!isCampaignStaff(user)) redirect('/campanha') const demand = await loadDemandDetail(payload, user, slug) if (!demand) notFound() - const isStaffView = isCampaignStaff(user) - return (
@@ -105,82 +104,67 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps ) : null} - {isStaffView ? ( - <> - + -
-

- Controle interno -

-

- Custo:{' '} - {demand.cost != null ? currencyFormatter.format(demand.cost) : 'não registrado'} -

- {demand.receipts.length ? ( - - ) : ( -

Nenhum comprovante anexado.

- )} - {demand.statusHistory.length ? ( -
-

Histórico

-
    - {demand.statusHistory.map((entry, index) => ( -
  • - {campaignDemandStatusLabels[entry.status]} - {entry.authorName ? ` — ${entry.authorName}` : ''} - {entry.createdAt - ? ` · ${dateTimeFormatter.format(new Date(entry.createdAt))}` - : ''} - {entry.note ? ` · ${entry.note}` : ''} -
  • - ))} -
-
- ) : null} -
- - ) : ( -
-

- {demand.status === 'aberta' - ? 'Sua demanda foi registrada e aguarda análise da assessoria.' - : demand.status === 'em_analise' || demand.status === 'escalada' - ? 'Sua demanda está em análise pela coordenação.' - : 'Sua demanda foi decidida — veja a nota acima.'} -

-
- )} +
+

+ Controle interno +

+

+ Custo: {demand.cost != null ? currencyFormatter.format(demand.cost) : 'não registrado'} +

+ {demand.receipts.length ? ( + + ) : ( +

Nenhum comprovante anexado.

+ )} + {demand.statusHistory.length ? ( +
+

Histórico

+
    + {demand.statusHistory.map((entry, index) => ( +
  • + {campaignDemandStatusLabels[entry.status]} + {entry.authorName ? ` — ${entry.authorName}` : ''} + {entry.createdAt + ? ` · ${dateTimeFormatter.format(new Date(entry.createdAt))}` + : ''} + {entry.note ? ` · ${entry.note}` : ''} +
  • + ))} +
+
+ ) : null} +
) } diff --git a/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx index 3821e9c7..435ea8ba 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx @@ -7,6 +7,7 @@ import { getPayload } from 'payload' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' import { DemandForm } from '@/components/campaign/DemandForm' import { Button } from '@/components/ui/button' +import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { loadMunicipalityOptions } from '@/utilities/campaignRelationOptions' import { createDemandFormAction } from './formActions' @@ -14,6 +15,7 @@ import { createDemandFormAction } from './formActions' export default async function NewDemandPage() { const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) if (!user) redirect('/campanha/login') + if (!isCampaignStaff(user)) redirect('/campanha') const municipalityOptions = await loadMunicipalityOptions(payload, user) diff --git a/src/app/(campaign)/campanha/(app)/demandas/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/page.tsx index a7fc2d5c..efaca7a3 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/page.tsx @@ -30,6 +30,7 @@ import { campaignDemandStatuses, type CampaignDemandStatus, } from '@/lib/schemas/campaignDemand' +import { isCampaignStaff } from '@/utilities/campaignAccess' import { getCampaignUser } from '@/utilities/campaignAuth' import { loadDemandListPageData, parseDemandListParams } from '@/utilities/campaignDemandData' @@ -54,6 +55,7 @@ export default async function DemandsPage({ searchParams }: DemandsPageProps) { const rawSearchParams = await searchParams const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) if (!user) redirect('/campanha/login') + if (!isCampaignStaff(user)) redirect('/campanha') const state = parseDemandListParams(rawSearchParams) const { rows, totalDocs, totalPages, openCount } = await loadDemandListPageData( diff --git a/src/app/(campaign)/campanha/(app)/planos/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/planos/[slug]/page.tsx index 223a989a..4a2f6447 100644 --- a/src/app/(campaign)/campanha/(app)/planos/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/[slug]/page.tsx @@ -48,6 +48,7 @@ export default async function ActionPlanDetailPage({ ]) if (!user) redirect('/campanha/login') + if (!isCampaignStaff(user)) redirect('/campanha') if (!slug) notFound() const activeTab = resolveActionPlanDetailTab(query) diff --git a/src/app/(campaign)/campanha/(app)/planos/page.tsx b/src/app/(campaign)/campanha/(app)/planos/page.tsx index 57e25cfc..6bfa09a4 100644 --- a/src/app/(campaign)/campanha/(app)/planos/page.tsx +++ b/src/app/(campaign)/campanha/(app)/planos/page.tsx @@ -40,6 +40,7 @@ export default async function ActionPlanListPage({ searchParams }: ActionPlanLis const [user, payload] = await Promise.all([getCampaignUser(), getPayload({ config })]) if (!user) return null + if (!isCampaignStaff(user)) redirect('/campanha') const now = new Date() const [{ result, state }, municipalityOptions] = await Promise.all([ diff --git a/src/app/(campaign)/campanha/actions/leaderSupporter.ts b/src/app/(campaign)/campanha/actions/leaderSupporter.ts index 1545ce43..d9d1e230 100644 --- a/src/app/(campaign)/campanha/actions/leaderSupporter.ts +++ b/src/app/(campaign)/campanha/actions/leaderSupporter.ts @@ -3,14 +3,11 @@ import { revalidatePath } from 'next/cache' import type { Payload } from 'payload' -import { - assertMunicipalityManagement, - upsertContactByPhone, -} from '@/app/(campaign)/campanha/actions/supporter' +import { upsertContactByPhone } from '@/app/(campaign)/campanha/actions/supporter' import { checkboxFormValue, nullableRelationshipFormValue, optionalFormText } from '@/lib/formData' import { leaderSupporterCreateSchema } from '@/lib/schemas/supporter' import type { CampaignUser, Supporter } from '@/payload-types' -import { isCampaignLeader } from '@/utilities/campaignAccess' +import { getEngagedLeaderMunicipalityIds, isCampaignLeader } from '@/utilities/campaignAccess' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { requireConsentByKey, @@ -38,11 +35,15 @@ export type LeaderSupporterFormValues = { municipality?: string } +const MUNICIPALITY_OUT_OF_SCOPE_MESSAGE = + 'Você só pode cadastrar contatos nos municípios da sua liderança.' + const safeActionMessages = [ 'Esta pessoa já está cadastrada como apoiador neste município.', 'Existe mais de um contato com este celular. Resolva a duplicidade no admin antes de continuar.', 'Somente lideranças podem cadastrar contatos por aqui.', 'Consentimento de cadastro de apoiador ainda não configurado.', + MUNICIPALITY_OUT_OF_SCOPE_MESSAGE, ] as const const getLeaderSupporterFormError = ( @@ -82,7 +83,18 @@ export const createLeaderSupporterRecord = async ( payload, async ({ req }) => { const currentActor = await getFreshLeaderActor(payload, actor, req) - await assertMunicipalityManagement(payload, currentActor, data.municipality, req) + + // Leaders cannot read the municipality collection, so scope is asserted + // against their own engaged leadership's municipalities (mirroring the + // leader branch of the collection-level `canCreateSupporter`). + const accessibleMunicipalityIDs = await getEngagedLeaderMunicipalityIds( + payload, + currentActor.id, + req, + ) + if (!accessibleMunicipalityIDs.includes(data.municipality)) { + throw new Error(MUNICIPALITY_OUT_OF_SCOPE_MESSAGE) + } const registrationConsent = await requireConsentByKey( payload, diff --git a/src/app/(campaign)/campanha/actions/supporter.ts b/src/app/(campaign)/campanha/actions/supporter.ts index 4a8f58ee..b097c3e2 100644 --- a/src/app/(campaign)/campanha/actions/supporter.ts +++ b/src/app/(campaign)/campanha/actions/supporter.ts @@ -193,7 +193,7 @@ const createValidatedSupporter = async (payload: Payload, actor: CampaignUser, i if (data.municipality) { await assertMunicipalityManagement(payload, currentActor, data.municipality, req) } else if (currentActor.role !== 'coordinator') { - throw new Error('Somente o Coordenador Geral pode cadastrar apoiadores sem Praça.') + throw new Error('Somente o Coordenador Geral pode cadastrar apoiadores sem município.') } const registrationConsent = await requireSupporterRegistrationConsent( diff --git a/src/collections/Consent.ts b/src/collections/Consent.ts index 69df3b24..6389ce35 100644 --- a/src/collections/Consent.ts +++ b/src/collections/Consent.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { CollectionConfig } from 'payload' export const Consent: CollectionConfig = { @@ -10,6 +11,15 @@ export const Consent: CollectionConfig = { group: 'Contatos', useAsTitle: 'text', }, + // Versioned legal texts referenced by signatures/subscriptions/supporters. + // Server flows resolve them via the Local API without a user; only admins + // may create or alter them. + access: { + create: payloadAdminOnly, + read: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, + }, fields: [ { name: 'key', diff --git a/src/collections/Media.ts b/src/collections/Media.ts index c02edab0..8bdb4ace 100644 --- a/src/collections/Media.ts +++ b/src/collections/Media.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { @@ -9,8 +10,13 @@ export const Media: CollectionConfig = { admin: { group: 'Coleções', }, + // Campaign flows (avatar, demand receipts) upload via server actions with an + // explicit overrideAccess: true after their own role checks. access: { read: () => true, + create: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, }, fields: [ { diff --git a/src/collections/Petition.ts b/src/collections/Petition.ts index 4723d539..5ebc81bf 100644 --- a/src/collections/Petition.ts +++ b/src/collections/Petition.ts @@ -1,4 +1,5 @@ import { normalizeFacebookPixelId } from '@/lib/facebookPixel' +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { revalidateDocumentById } from '@/utilities/documents' import type { CollectionConfig } from 'payload' @@ -25,6 +26,10 @@ export const Petition: CollectionConfig = { }, access: { read: () => true, + readVersions: payloadAdminOnly, + create: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, }, hooks: { beforeChange: [ diff --git a/src/collections/Post.ts b/src/collections/Post.ts index b285fa9a..6bacef48 100644 --- a/src/collections/Post.ts +++ b/src/collections/Post.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents' import { slugify } from '@/utilities/slug' import type { CollectionConfig } from 'payload' @@ -22,6 +23,10 @@ export const Post: CollectionConfig = { }, access: { read: () => true, + readVersions: payloadAdminOnly, + create: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, }, hooks: { afterChange: [ diff --git a/src/collections/Signature.ts b/src/collections/Signature.ts index 72864516..e5c89635 100644 --- a/src/collections/Signature.ts +++ b/src/collections/Signature.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { signatureContactToCSV, signaturePetitionToCSV } from '@/utilities/signatureExport' import { CollectionConfig } from 'payload' @@ -10,6 +11,14 @@ export const Signature: CollectionConfig = { admin: { group: 'Abaixo-assinados', }, + // Citizen PII. Public petition flows write via the Local API without a user + // (overrideAccess defaults to true), so admin-only access does not affect them. + access: { + create: payloadAdminOnly, + read: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, + }, fields: [ { type: 'relationship', diff --git a/src/collections/Subscription.ts b/src/collections/Subscription.ts index 3da3465e..d7ce4106 100644 --- a/src/collections/Subscription.ts +++ b/src/collections/Subscription.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { CollectionConfig } from 'payload' export const Subscription: CollectionConfig = { @@ -9,6 +10,14 @@ export const Subscription: CollectionConfig = { admin: { group: 'Contatos', }, + // Citizen PII. Public opt-in flows write via the Local API without a user + // (overrideAccess defaults to true), so admin-only access does not affect them. + access: { + create: payloadAdminOnly, + read: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, + }, fields: [ { type: 'relationship', diff --git a/src/collections/Tag.ts b/src/collections/Tag.ts index 06bb2be6..95a038f7 100644 --- a/src/collections/Tag.ts +++ b/src/collections/Tag.ts @@ -1,3 +1,4 @@ +import { payloadAdminOnly } from '@/utilities/campaignAccess' import { revalidateDocumentById, revalidatePostsListing } from '@/utilities/documents' import { slugify } from '@/utilities/slug' import type { CollectionConfig } from 'payload' @@ -16,6 +17,9 @@ export const Tag: CollectionConfig = { }, access: { read: () => true, + create: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, }, hooks: { afterChange: [ diff --git a/src/collections/Users.ts b/src/collections/Users.ts index eb4f2b7a..2998534f 100644 --- a/src/collections/Users.ts +++ b/src/collections/Users.ts @@ -1,3 +1,4 @@ +import { isPayloadAdmin, payloadAdminOnly } from '@/utilities/campaignAccess' import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { @@ -11,6 +12,17 @@ export const Users: CollectionConfig = { useAsTitle: 'email', }, auth: true, + // Admin accounts are managed only by admin accounts. Without this, Payload's + // "any authenticated user" default would let a campaignUser JWT manage admins + // through /api/users — a privilege-escalation path. + access: { + admin: ({ req }) => isPayloadAdmin(req.user), + create: payloadAdminOnly, + read: payloadAdminOnly, + update: payloadAdminOnly, + delete: payloadAdminOnly, + unlock: payloadAdminOnly, + }, fields: [ // Email added by default // Add more fields as needed diff --git a/src/components/campaign/DemandWorkflowCard.tsx b/src/components/campaign/DemandWorkflowCard.tsx index e627ffe6..9ab64098 100644 --- a/src/components/campaign/DemandWorkflowCard.tsx +++ b/src/components/campaign/DemandWorkflowCard.tsx @@ -24,7 +24,8 @@ type FormAction = ( type DemandWorkflowCardProps = { demandID: number status: CampaignDemandStatus - isCoordinator: boolean + /** Coordinator or candidate — the roles allowed to decide escalated demands. */ + canDecideEscalated: boolean currentCost: number | null transitionFormAction: FormAction costFormAction: FormAction @@ -38,7 +39,7 @@ const transitionVariant = (target: CampaignDemandStatus) => export const DemandWorkflowCard = ({ demandID, status, - isCoordinator, + canDecideEscalated, currentCost, transitionFormAction, costFormAction, @@ -53,7 +54,9 @@ export const DemandWorkflowCard = ({ const availableTransitions = campaignDemandTransitions[status].filter( (target) => - isCoordinator || status !== 'escalada' || (target !== 'aprovada' && target !== 'rejeitada'), + canDecideEscalated || + status !== 'escalada' || + (target !== 'aprovada' && target !== 'rejeitada'), ) return ( diff --git a/src/utilities/campaignAccess.ts b/src/utilities/campaignAccess.ts index 3aa25ad4..ba2cf32c 100644 --- a/src/utilities/campaignAccess.ts +++ b/src/utilities/campaignAccess.ts @@ -33,6 +33,14 @@ type DynamicFind = (args: { export const isPayloadAdmin = (user: CampaignActor): user is User => user?.collection === 'users' +/** + * Payload-admin-only collection access. Collections without explicit access fall + * back to Payload's "any authenticated user" default — which includes campaign + * users hitting `/api/*` with a `campaign-token` JWT — so every CMS/PII + * collection must set this (or something stricter) explicitly. + */ +export const payloadAdminOnly: Access = ({ req }) => isPayloadAdmin(req.user) + const isCampaignUser = (user: CampaignActor): user is CampaignUser => user?.collection === 'campaignUser' @@ -176,6 +184,37 @@ export const getAdvisorMunicipalityIds = async ( return result.docs.map((doc) => relationshipId(doc.id)).filter((id): id is number => id !== null) } +/** + * Municipality IDs linked to `userID`'s engaged leadership, without requiring a + * `PayloadRequest`. Transaction-safe counterpart of the leader branch in + * `getAccessibleMunicipalityIds`, for server actions running inside + * `withPayloadTransaction` (where only `{ transactionID }` is available). + */ +export const getEngagedLeaderMunicipalityIds = async ( + payload: Pick, + userID: number, + req?: CampaignTransactionRequest, +): Promise => { + const find = payload.find.bind(payload) as unknown as DynamicFind + const result = await find({ + collection: 'leadership', + where: { + and: [{ user: { equals: userID } }, { supportStatus: { equals: 'engajado' } }], + }, + depth: 0, + limit: 1, + pagination: false, + select: { municipalities: true }, + overrideAccess: true, + ...(req ? { req } : {}), + }) + + const doc = result.docs[0] + return (Array.isArray(doc?.municipalities) ? doc.municipalities : []) + .map(relationshipId) + .filter((id): id is number => id !== null) +} + type OwnLeadership = { id: number; municipalityIDs: number[]; organizationIDs: number[] } | null /** diff --git a/src/utilities/campaignInviteCreation.ts b/src/utilities/campaignInviteCreation.ts index d51c863a..a5ab73c0 100644 --- a/src/utilities/campaignInviteCreation.ts +++ b/src/utilities/campaignInviteCreation.ts @@ -7,6 +7,7 @@ import { type CampaignInviteCreateInput, } from '@/lib/schemas/invite' import type { CampaignUser } from '@/payload-types' +import { isCampaignStaff } from '@/utilities/campaignAccess' import { reloadCampaignActor } from '@/utilities/campaignActionContext' import { buildCampaignInviteWhatsAppLink, @@ -31,7 +32,9 @@ const getFreshInviteCreator = async ( req: PayloadTransactionRequest, ): Promise => { const currentActor = await reloadCampaignActor(payload, actor, req) - if (currentActor.role !== 'coordinator' && currentActor.role !== 'advisor') { + // Staff only (coordinator, advisor, candidate) — matches the collection-level + // `canCreateCampaignInvite` scope; the leadership read below narrows advisors. + if (!isCampaignStaff(currentActor)) { throw new Error('Somente a coordenação pode criar convites.') } return currentActor diff --git a/src/utilities/supporterUi.ts b/src/utilities/supporterUi.ts index 8f2633f6..aaa97024 100644 --- a/src/utilities/supporterUi.ts +++ b/src/utilities/supporterUi.ts @@ -120,5 +120,6 @@ export const resolveSupporterListUrl = ( export const getSupporterScopeLabel = (total: number): string => `${total} ${total === 1 ? 'apoiador nas suas Praças' : 'apoiadores nas suas Praças'}` +/** Staff roles with supporter-area access — matches collection-level supporter access. */ export const canAccessSupporterArea = (role: CampaignUser['role']): boolean => - role === 'coordinator' || role === 'advisor' + role === 'coordinator' || role === 'advisor' || role === 'candidate' diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index d4140e25..77d4d121 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -114,7 +114,17 @@ const emptyOwnedIDs = (): OwnedIDs => ({ consent: new Set(), }) -const inviteConsentKey = 'lideranca-autopreenchimento' +/** + * Consent rows shared across spec files (stable keys the app resolves at + * runtime). They are leased via `testDatabaseLease.ts`, never owned by a + * fixture instance — deleting them at cleanup would steal them from + * concurrently running spec files. + */ +const leasedConsentKeys = new Set([ + 'lideranca-autopreenchimento', + 'apoiador-cadastro', + 'apoiador-intencao-voto', +]) const defaultConsentText = (text: string): Consent['text'] => ({ root: { @@ -237,8 +247,24 @@ const purgeMunicipalityResidue = async ( }), ]) + // Residue invites reference residue leaderships (FK), so they must be + // found and deleted before the leaderships — otherwise the leadership + // delete aborts and the residue poisons every later claim of this municipality. + const leadershipIDs = leaderships.docs.map((doc) => doc.id) + const invites = + leadershipIDs.length > 0 + ? await payload.find({ + collection: 'campaignInvite', + where: { leadership: { in: leadershipIDs } }, + depth: 0, + pagination: false, + select: {}, + }) + : { docs: [] } + const deletions: Array<{ collection: + | 'campaignInvite' | 'votePledge' | 'municipalityUpdate' | 'campaignDemand' @@ -247,11 +273,12 @@ const purgeMunicipalityResidue = async ( | 'actionPlan' ids: number[] }> = [ + { collection: 'campaignInvite', ids: invites.docs.map((doc) => doc.id) }, { collection: 'votePledge', ids: pledges.docs.map((doc) => doc.id) }, { collection: 'municipalityUpdate', ids: updates.docs.map((doc) => doc.id) }, { collection: 'campaignDemand', ids: demands.docs.map((doc) => doc.id) }, { collection: 'actionPlan', ids: plans.docs.map((doc) => doc.id) }, - { collection: 'leadership', ids: leaderships.docs.map((doc) => doc.id) }, + { collection: 'leadership', ids: leadershipIDs }, { collection: 'supporter', ids: supporters.docs.map((doc) => doc.id) }, ] for (const { collection, ids } of deletions) { @@ -283,13 +310,14 @@ export class CampaignFixtures { const trackedCreate = async (args: Parameters[0]) => { const document = await rootPayload.create(args as never) const collection = args.collection - const isLeasedInviteConsent = + const isLeasedConsent = collection === 'consent' && typeof args.data === 'object' && args.data !== null && 'key' in args.data && - args.data.key === inviteConsentKey - if (collection in this.owned && typeof document.id === 'number' && !isLeasedInviteConsent) { + typeof args.data.key === 'string' && + leasedConsentKeys.has(args.data.key) + if (collection in this.owned && typeof document.id === 'number' && !isLeasedConsent) { this.own(collection as CampaignCollection, document.id) } return document @@ -519,6 +547,13 @@ export class CampaignFixtures { return user } + /** + * Creates an OWNED consent row (deleted at cleanup) — even for a leased + * shared key, since calling this helper is an explicit ownership request. + * For the shared stable keys, prefer `ensureLeasedConsent` / + * `withLeasedConsent` (testDatabaseLease.ts) so parallel spec files are not + * robbed of the row at cleanup. + */ async createConsent(input: ConsentInput = {}): Promise { const marker = this.value('consent') const consent = await this.rootPayload.create({ diff --git a/tests/helpers/testDatabaseLease.ts b/tests/helpers/testDatabaseLease.ts index 8fe7d017..1026c575 100644 --- a/tests/helpers/testDatabaseLease.ts +++ b/tests/helpers/testDatabaseLease.ts @@ -51,7 +51,15 @@ type ConsentSnapshot = { } export const CAMPAIGN_INVITE_CONSENT_LEASE_KEY = 'campaign-invite-consent' +export const SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY = 'supporter-registration-consent' +export const SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY = 'supporter-vote-intention-consent' const CAMPAIGN_INVITE_CONSENT_KEY = 'lideranca-autopreenchimento' + +/** A consent row shared across spec files, protected by an advisory-lock lease. */ +export type LeasedConsentFixture = { + consentKey: string + leaseKey: string +} const fixtureConsentText: Consent['text'] = { root: { type: 'root', @@ -88,11 +96,8 @@ const rowsFrom = (result: unknown): Row[] => { const combineErrors = (primary: unknown, cleanup: unknown, message: string): AggregateError => new AggregateError([primary, cleanup], message) -const addFailure = ( - primary: unknown | undefined, - secondary: unknown, - message: string, -): unknown => (primary === undefined ? secondary : combineErrors(primary, secondary, message)) +const addFailure = (primary: unknown | undefined, secondary: unknown, message: string): unknown => + primary === undefined ? secondary : combineErrors(primary, secondary, message) const rollbackWithFault = async ( lease: InternalTestDatabaseLease, @@ -236,19 +241,14 @@ export const startTestDatabaseLeaseAcquisition = ( resolveBackendPID = resolve rejectBackendPID = reject }) - const acquisition = beginTestDatabaseLease( - payload, - leaseKey, - mode, - async ({ transactionID }) => { - try { - resolveBackendPID(await getTestTransactionBackendPID(payload, transactionID)) - } catch (error) { - rejectBackendPID(error) - throw error - } - }, - ) + const acquisition = beginTestDatabaseLease(payload, leaseKey, mode, async ({ transactionID }) => { + try { + resolveBackendPID(await getTestTransactionBackendPID(payload, transactionID)) + } catch (error) { + rejectBackendPID(error) + throw error + } + }) return { acquisition, backendPID } } @@ -302,9 +302,7 @@ export const waitForAdvisoryLockWaiter = async ( const waiting = rowsFrom(result)[0] if (waiting) return waiting } - throw new Error( - `Timed out waiting for backend ${waiterPID} on the exact ${mode} advisory lock.`, - ) + throw new Error(`Timed out waiting for backend ${waiterPID} on the exact ${mode} advisory lock.`) } export const acquireTestDatabaseLease = async ( @@ -373,11 +371,14 @@ export const withSharedTestDatabaseLease = async ( } } -export const ensureInviteConsent = async (payload: Payload): Promise => - withTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, async () => { +export const ensureLeasedConsent = async ( + payload: Payload, + { consentKey, leaseKey }: LeasedConsentFixture, +): Promise => + withTestDatabaseLease(payload, leaseKey, async () => { const existing = await payload.find({ collection: 'consent', - where: { key: { equals: CAMPAIGN_INVITE_CONSENT_KEY } }, + where: { key: { equals: consentKey } }, limit: 1, depth: 0, }) @@ -386,27 +387,28 @@ export const ensureInviteConsent = async (payload: Payload): Promise => return payload.create({ collection: 'consent', data: { - key: CAMPAIGN_INVITE_CONSENT_KEY, + key: consentKey, text: fixtureConsentText, }, depth: 0, }) }) -export const withInviteConsent = async ( +export const withLeasedConsent = async ( payload: Payload, + { consentKey, leaseKey }: LeasedConsentFixture, operation: (consent: Consent) => Promise, ): Promise => { for (;;) { - await ensureInviteConsent(payload) - const lease = await beginTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY, 'shared') + await ensureLeasedConsent(payload, { consentKey, leaseKey }) + const lease = await beginTestDatabaseLease(payload, leaseKey, 'shared') let failure: unknown let result: Result | undefined let retry = false try { const configured = await payload.find({ collection: 'consent', - where: { key: { equals: CAMPAIGN_INVITE_CONSENT_KEY } }, + where: { key: { equals: consentKey } }, limit: 1, depth: 0, }) @@ -433,6 +435,22 @@ export const withInviteConsent = async ( } } +export const ensureInviteConsent = async (payload: Payload): Promise => + ensureLeasedConsent(payload, { + consentKey: CAMPAIGN_INVITE_CONSENT_KEY, + leaseKey: CAMPAIGN_INVITE_CONSENT_LEASE_KEY, + }) + +export const withInviteConsent = async ( + payload: Payload, + operation: (consent: Consent) => Promise, +): Promise => + withLeasedConsent( + payload, + { consentKey: CAMPAIGN_INVITE_CONSENT_KEY, leaseKey: CAMPAIGN_INVITE_CONSENT_LEASE_KEY }, + operation, + ) + export const withMissingInviteConsentFixture = async ( payload: Payload, operation: () => Promise, @@ -453,9 +471,7 @@ export const withMissingInviteConsentFixture = async ( LIMIT 1 `), )[0] - await setupLease.transaction.execute( - sql`DELETE FROM "consent" WHERE "key" = ${consentKey}`, - ) + await setupLease.transaction.execute(sql`DELETE FROM "consent" WHERE "key" = ${consentKey}`) } catch (error) { setupError = error } @@ -509,9 +525,7 @@ export const withMissingInviteConsentFixture = async ( } if (restoreLease) { try { - await restoreLease.transaction.execute( - sql`DELETE FROM "consent" WHERE "key" = ${consentKey}`, - ) + await restoreLease.transaction.execute(sql`DELETE FROM "consent" WHERE "key" = ${consentKey}`) if (snapshot) { await restoreLease.transaction.execute(sql` INSERT INTO "consent" ("id", "key", "text", "updated_at", "created_at") diff --git a/tests/int/campaignSupporter.int.spec.ts b/tests/int/campaignSupporter.int.spec.ts index f7df966a..5b977974 100644 --- a/tests/int/campaignSupporter.int.spec.ts +++ b/tests/int/campaignSupporter.int.spec.ts @@ -23,6 +23,12 @@ import { import { buildSupporterListWhere } from '@/utilities/supporterUi' import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { + ensureLeasedConsent, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, + withMissingInviteConsentFixture, +} from '../helpers/testDatabaseLease' let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -32,24 +38,23 @@ const campaignFixtures = installCampaignFixtures({ }, }) -const ensureConsentByKey = async (fixtures: ReturnType, key: string) => { - const existing = await payload.find({ - collection: 'consent', - where: { key: { equals: key } }, - depth: 0, - limit: 1, - pagination: false, - }) - if (existing.docs[0]) { - fixtures.own('consent', existing.docs[0].id) - return existing.docs[0] - } - return fixtures.createConsent({ key }) -} +/** + * Shared consent rows (stable keys) are leased fixtures: ensured under an + * advisory-lock lease and never owned/deleted, so parallel spec files can rely + * on them (see `leasedConsentKeys` in campaignFixtures.ts). + */ +const ensureConsentByKey = (key: string, leaseKey: string) => + ensureLeasedConsent(payload, { consentKey: key, leaseKey }) -const ensureSupporterConsents = async (fixtures: ReturnType) => { - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) - const voteIntention = await ensureConsentByKey(fixtures, SUPPORTER_VOTE_INTENTION_CONSENT_KEY) +const ensureSupporterConsents = async () => { + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) + const voteIntention = await ensureConsentByKey( + SUPPORTER_VOTE_INTENTION_CONSENT_KEY, + SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, + ) return { registration, voteIntention } } @@ -87,28 +92,27 @@ describe('campaign supporter domain', () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') - const existing = await payload.find({ - collection: 'consent', - where: { key: { equals: SUPPORTER_REGISTRATION_CONSENT_KEY } }, - depth: 0, - limit: 1, - }) - if (existing.docs[0]) { - await payload.delete({ collection: 'consent', id: existing.docs[0].id }) - } - - await expect( - createSupporterRecord(payload, coordinator, { - name: fixtures.value('Apoiador'), - phone: fixtures.phone(), - consentAccepted: true, - }), - ).rejects.toThrow(/Consentimento de cadastro de apoiador ainda não configurado/) + await withMissingInviteConsentFixture( + payload, + async () => { + await expect( + createSupporterRecord(payload, coordinator, { + name: fixtures.value('Apoiador'), + phone: fixtures.phone(), + consentAccepted: true, + }), + ).rejects.toThrow(/Consentimento de cadastro de apoiador ainda não configurado/) + }, + { + consentKey: SUPPORTER_REGISTRATION_CONSENT_KEY, + leaseKey: SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + }, + ) }) it('creates a supporter with contact upsert and blocks leadership coexistence', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const coordinator = await fixtures.createCampaignUser('coordinator') const municipality = await fixtures.getMunicipality() const phone = fixtures.phone() @@ -144,7 +148,7 @@ describe('campaign supporter domain', () => { it('scopes advisor create to administered municipalities and blocks municipality-less create', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const advisor = await fixtures.createCampaignUser('advisor') const municipality = await fixtures.getMunicipality() const otherMunicipality = await fixtures.getMunicipality() @@ -180,7 +184,10 @@ describe('campaign supporter domain', () => { it('requires highlighted vote-intention consent before setting intention', async () => { const fixtures = campaignFixtures() - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const coordinator = await fixtures.createCampaignUser('coordinator') const contact = await fixtures.createContact() const supporter = await fixtures.createSupporter({ @@ -212,7 +219,10 @@ describe('campaign supporter domain', () => { }), ).rejects.toThrow(/Consentimento de intenção de voto ainda não configurado/) - await ensureConsentByKey(fixtures, SUPPORTER_VOTE_INTENTION_CONSENT_KEY) + await ensureConsentByKey( + SUPPORTER_VOTE_INTENTION_CONSENT_KEY, + SUPPORTER_VOTE_INTENTION_CONSENT_LEASE_KEY, + ) const updated = await setSupporterVoteIntentionRecord(payload, coordinator, { id: supporter.id, @@ -225,7 +235,7 @@ describe('campaign supporter domain', () => { it('previews and confirms CSV import for the coordinator only', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const coordinator = await fixtures.createCampaignUser('coordinator') const advisor = await fixtures.createCampaignUser('advisor') const phoneOk = fixtures.phone() @@ -292,7 +302,7 @@ describe('campaign supporter domain', () => { it('rejects a tampered import token and a token issued to another actor', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const coordinator = await fixtures.createCampaignUser('coordinator') const otherCoordinator = await fixtures.createCampaignUser('coordinator') const phoneOk = fixtures.phone() @@ -343,7 +353,7 @@ describe('campaign supporter domain', () => { it('removes supporter and anonymizes contact when no other joins remain', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const coordinator = await fixtures.createCampaignUser('coordinator') const phone = fixtures.phone() const created = await createSupporterRecord(payload, coordinator, { @@ -370,7 +380,10 @@ describe('campaign supporter domain', () => { it('aggregates supporter overview KPIs in a single SQL query for the coordinator', async () => { const fixtures = campaignFixtures() - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const coordinator = await fixtures.createCampaignUser('coordinator') const intentions: Array<{ @@ -424,7 +437,10 @@ describe('campaign supporter domain', () => { it('scopes the overview aggregate to administered municipalities for an advisor and applies filters', async () => { const fixtures = campaignFixtures() - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const advisor = await fixtures.createCampaignUser('advisor') const assigned = await fixtures.getMunicipality() const other = await fixtures.getMunicipality() @@ -471,7 +487,10 @@ describe('campaign supporter domain', () => { it('ignores single-character search queries in list and overview filters', async () => { const fixtures = campaignFixtures() - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const coordinator = await fixtures.createCampaignUser('coordinator') const uniqueName = fixtures.value('ApoiadorUnico') @@ -509,7 +528,10 @@ describe('campaign supporter domain', () => { it('keeps list and overview totals aligned for multi-character search', async () => { const fixtures = campaignFixtures() - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const coordinator = await fixtures.createCampaignUser('coordinator') const uniqueName = fixtures.value('ApoiadorBusca') @@ -547,11 +569,14 @@ describe('campaign supporter domain', () => { it('loads supporters page data scoped to the administered municipalities of an advisor', async () => { const fixtures = campaignFixtures() - await ensureSupporterConsents(fixtures) + await ensureSupporterConsents() const advisor = await fixtures.createCampaignUser('advisor') const assigned = await fixtures.getMunicipality() await fixtures.assignMunicipalityAdvisors(assigned, [advisor]) - const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY) + const registration = await ensureConsentByKey( + SUPPORTER_REGISTRATION_CONSENT_KEY, + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + ) const contact = await fixtures.createContact() await fixtures.createSupporter({ diff --git a/tests/int/campaignUserPhone.int.spec.ts b/tests/int/campaignUserPhone.int.spec.ts index 5f4025cc..e30b525a 100644 --- a/tests/int/campaignUserPhone.int.spec.ts +++ b/tests/int/campaignUserPhone.int.spec.ts @@ -62,7 +62,7 @@ describe('campaign user contact phone', () => { }) }) - it('denies leaders from reading advisor contact phones', async () => { + it('omits advisor contact phones from leader reads (lockdown)', async () => { await withCampaignFixtures(payload, async (fixtures) => { const coordinator = await fixtures.createCampaignUser('coordinator') const advisorPhone = fixtures.phone() @@ -76,16 +76,19 @@ describe('campaign user contact phone', () => { await fixtures.assignMunicipalityAdvisors(municipality, [advisor]) await createEngagedPhoneAccessGraph(fixtures, coordinator, leader, municipality.id) - await expect( - payload.findByID({ - collection: 'campaignUser', - id: advisor.id, - depth: 0, - select: { name: true, phone: true }, - user: leader, - overrideAccess: false, - }), - ).rejects.toThrow(/permissão/i) + // Leaders are lockdown accounts: even for advisors of their own linked + // municipality, the phone field must be withheld. + const advisorRead = await payload.findByID({ + collection: 'campaignUser', + id: advisor.id, + depth: 0, + select: { name: true, phone: true }, + user: leader, + overrideAccess: false, + }) + + expect(advisorRead.phone).toBeUndefined() + expect(JSON.stringify(advisorRead)).not.toContain(advisorPhone) }) }) @@ -154,8 +157,11 @@ describe('campaign user contact phone', () => { }) const byId = new Map(result.docs.map((doc) => [doc.id, doc])) - expect(byId.get(advisor.id)?.phone).toBe(advisorPhone) + // Leaders are lockdown accounts: no staff phone is visible, linked + // municipality or not. + expect(byId.get(advisor.id)?.phone).toBeUndefined() expect(byId.get(foreignAdvisor.id)?.phone).toBeUndefined() + expect(JSON.stringify(result.docs)).not.toContain(advisorPhone) expect(JSON.stringify(result.docs)).not.toContain(foreignPhone) }) }) diff --git a/tests/int/collectionAccessLockdown.int.spec.ts b/tests/int/collectionAccessLockdown.int.spec.ts new file mode 100644 index 00000000..2a49f31f --- /dev/null +++ b/tests/int/collectionAccessLockdown.int.spec.ts @@ -0,0 +1,207 @@ +// @vitest-environment node + +import { getPayload, type Payload } from 'payload' +import { beforeAll, describe, expect, it } from 'vitest' + +import { createLeaderSupporterRecord } from '@/app/(campaign)/campanha/actions/leaderSupporter' +import config from '@/payload.config' +import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '@/utilities/campaignConsent' + +import { installCampaignFixtures } from '../helpers/campaignFixtures' +import { + SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + withLeasedConsent, +} from '../helpers/testDatabaseLease' + +let payload: Payload +const campaignFixtures = installCampaignFixtures({ + getPayload: () => payload, + setPayload: (nextPayload) => { + payload = nextPayload + }, +}) + +/** Runs `operation` with the shared supporter-registration consent guaranteed present. */ +const withSupporterRegistrationConsent = ( + operation: () => Promise, +): Promise => + withLeasedConsent( + payload, + { + consentKey: SUPPORTER_REGISTRATION_CONSENT_KEY, + leaseKey: SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY, + }, + operation, + ) + +/** + * Collections without explicit `access` fall back to Payload's "any + * authenticated user" default — which includes campaign users presenting the + * `campaign-token` JWT against `/api/*`. These tests pin the lockdown added to + * the CMS/PII collections: campaign users (even the coordinator) must be + * denied, admins keep working, and the public keeps read-only access where + * intended. + */ +describe('CMS/PII collection access lockdown', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + }) + + it('denies campaign users any access to admin accounts (users)', async () => { + const coordinator = await campaignFixtures().createCampaignUser('coordinator') + + await expect( + payload.count({ collection: 'users', where: {}, user: coordinator, overrideAccess: false }), + ).rejects.toThrow(/permissão|not allowed/i) + + await expect( + payload.create({ + collection: 'users', + data: { email: 'escalated@example.com', password: 'owned-by-attacker' }, + user: coordinator, + overrideAccess: false, + }), + ).rejects.toThrow(/permissão|not allowed/i) + }) + + it('lets admin users keep managing the users collection', async () => { + const fixtures = campaignFixtures() + const admin = await fixtures.createAdminUser() + + const result = await payload.count({ + collection: 'users', + where: { id: { equals: admin.id } }, + user: admin, + overrideAccess: false, + }) + expect(result.totalDocs).toBe(1) + }) + + it.each(['signature', 'subscription', 'consent'] as const)( + 'denies campaign users and anonymous requests access to %s (PII/legal)', + async (collection) => { + const coordinator = await campaignFixtures().createCampaignUser('coordinator') + + await expect( + payload.count({ collection, where: {}, user: coordinator, overrideAccess: false }), + ).rejects.toThrow(/permissão|not allowed/i) + + await expect(payload.count({ collection, where: {}, overrideAccess: false })).rejects.toThrow( + /permissão|not allowed/i, + ) + }, + ) + + it('lets admin users keep reading signatures and subscriptions', async () => { + const fixtures = campaignFixtures() + const admin = await fixtures.createAdminUser() + + await expect( + payload.count({ collection: 'signature', where: {}, user: admin, overrideAccess: false }), + ).resolves.toMatchObject({ totalDocs: expect.any(Number) }) + await expect( + payload.count({ collection: 'subscription', where: {}, user: admin, overrideAccess: false }), + ).resolves.toMatchObject({ totalDocs: expect.any(Number) }) + }) + + it.each(['post', 'tag', 'petition', 'media'] as const)( + 'denies campaign users create access to public-content collection %s', + async (collection) => { + const coordinator = await campaignFixtures().createCampaignUser('coordinator') + + await expect( + payload.create({ + collection, + data: {}, + user: coordinator, + overrideAccess: false, + } as never), + ).rejects.toThrow(/permissão|not allowed/i) + }, + ) + + it.each(['post', 'tag', 'petition', 'media'] as const)( + 'keeps anonymous read access to public-content collection %s', + async (collection) => { + await expect( + payload.count({ collection, where: {}, overrideAccess: false }), + ).resolves.toMatchObject({ totalDocs: expect.any(Number) }) + }, + ) +}) + +describe('leader supporter creation scope', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + }) + + it('lets an engaged leader register a supporter in a linked municipality', async () => { + const fixtures = campaignFixtures() + const leaderAccount = await fixtures.createCampaignUser('leader') + const leaderContact = await fixtures.createContact() + const linked = await fixtures.getMunicipality() + await fixtures.createLeadership({ + contact: leaderContact.id, + municipalities: [linked.id], + user: leaderAccount.id, + supportStatus: 'engajado', + }) + + const supporter = await withSupporterRegistrationConsent(() => + createLeaderSupporterRecord(payload, leaderAccount, { + name: fixtures.value('Apoiador'), + phone: fixtures.phone(), + city: 'Salvador', + municipality: linked.id, + consentAccepted: true, + }), + ) + fixtures.own('supporter', supporter.id) + + expect(supporter.source).toBe('lideranca') + expect(fixtures.id(supporter.createdBy as never)).toBe(leaderAccount.id) + expect(fixtures.id(supporter.municipality as never)).toBe(linked.id) + }) + + it('rejects a supporter outside the leadership municipalities', async () => { + const fixtures = campaignFixtures() + const leaderAccount = await fixtures.createCampaignUser('leader') + const leaderContact = await fixtures.createContact() + const linked = await fixtures.getMunicipality() + const outside = await fixtures.getMunicipality() + await fixtures.createLeadership({ + contact: leaderContact.id, + municipalities: [linked.id], + user: leaderAccount.id, + supportStatus: 'engajado', + }) + + await expect( + withSupporterRegistrationConsent(() => + createLeaderSupporterRecord(payload, leaderAccount, { + name: fixtures.value('Apoiador'), + phone: fixtures.phone(), + city: 'Salvador', + municipality: outside.id, + consentAccepted: true, + }), + ), + ).rejects.toThrow('Você só pode cadastrar contatos nos municípios da sua liderança.') + }) + + it('rejects staff accounts on the leader-only contact tool', async () => { + const fixtures = campaignFixtures() + const advisor = await fixtures.createCampaignUser('advisor') + const municipality = await fixtures.getMunicipality() + + await expect( + createLeaderSupporterRecord(payload, advisor, { + name: fixtures.value('Apoiador'), + phone: fixtures.phone(), + city: 'Salvador', + municipality: municipality.id, + consentAccepted: true, + }), + ).rejects.toThrow('Somente lideranças podem cadastrar contatos por aqui.') + }) +}) From 0e4e6855dcb55da16930cedfb8767f98917a83c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 23 Jul 2026 23:06:12 -0300 Subject: [PATCH 2/9] Add the tooling gate: knip, CI, zero-warning lint, as-never ban - knip (files/deps at error level; unused exports ledgered as warn) with config for Payload/Next entries; deleted the dead modules it confirmed: RecentlyVisited + UpcomingActionPlansCard + actionPlanUpcomingPreview, lazy-dialog helpers, tseZone, federalCandidateTotalsAggregate (test-only), unused ui kit files, one-shot rename script; pruned unused radix deps and typescript-eslint; added pg/@types/pg (pinned to drizzle's 8.10.2) - GitHub Actions CI on the mirror: lint, typecheck, knip, unit, migrate, int against a Postgres 17 service - eslint: unused-vars with ^_ ignore patterns (migrations args exempt), repo-wide ban on `as never` (no-restricted-syntax), lint gate at --max-warnings=0; fixed all 54 pre-existing warnings (dead imports, hooks deps, tracking-pixel img disable with impeccable config exception) - tsconfig: include consolidated from 219 lines to 5 (wildcard covers all .next dist variants), noUnusedLocals on - typed replacements for every as-never: hookFilledCreateData (actions create data completed by collection hooks), drizzle scopeColumn narrowing, tests' stub helper (property-level checking kept, completeness waived) - test infra: leased-consent generalization used by onda0 spec (shared leases over the stable consent keys; window race ledgered), purge fix - docs: IMPROVE-CODE-QUALITY-PLAN.md tracker, TECH-DEBT.md ledger, TESTING.md safety-net map (package.json also gains the Phase 2 build:election-aggregates script entry; the script lands next commit) --- .github/workflows/ci.yml | 68 ++ .impeccable/config.json | 9 + docs/IMPROVE-CODE-QUALITY-PLAN.md | 46 + docs/TECH-DEBT.md | 54 + docs/TESTING.md | 32 + eslint.config.mjs | 34 + knip.json | 31 + package.json | 13 +- pnpm-lock.yaml | 1009 ++++++++++++----- scripts/build-bahia-demographics.mjs | 2 +- scripts/rename-plaza-to-municipality.mjs | 214 ---- .../(app)/dobradinhas/nova/formActions.ts | 2 +- .../campanha/(app)/dobradinhas/nova/page.tsx | 1 - .../campanha/(app)/dobradinhas/page.tsx | 1 - .../(campaign)/campanha/actions/actionPlan.ts | 3 +- src/app/(campaign)/campanha/actions/demand.ts | 3 +- .../campanha/actions/municipalityUpdate.ts | 3 +- .../campanha/actions/organization.ts | 3 +- .../campanha/actions/stateDeputy.ts | 3 +- src/components/MetaPixel.tsx | 2 + src/components/WhatsappForm.tsx | 4 +- src/components/campaign/BahiaMap.tsx | 6 + .../campaign/CreateCampaignClientDynamic.tsx | 18 - .../campaign/LazyDialogModuleFallback.tsx | 59 - src/components/campaign/MunicipalityList.tsx | 1 - .../campaign/RecentVisitTracker.tsx | 8 +- src/components/campaign/RecentlyVisited.tsx | 32 - .../campaign/UpcomingActionPlansCard.tsx | 60 - .../campaign/useLazyDialogModule.ts | 67 -- src/components/ui/DropdownMenu.tsx | 234 ---- src/components/ui/Tabs.tsx | 78 -- src/components/ui/select.tsx | 185 --- src/utilities/actionPlanUpcomingPreview.ts | 98 -- src/utilities/electionResultsImport.ts | 18 +- .../federalCandidateTotalsAggregate.ts | 58 - src/utilities/hookFilledData.ts | 13 + src/utilities/tseZone.ts | 25 - tests/e2e/fixtures/campaignE2EFixtures.ts | 2 +- tests/helpers/campaignFixtures.ts | 8 +- tests/helpers/stub.ts | 7 + tests/int/campaignActionPlan.int.spec.ts | 51 +- tests/int/campaignAuth.int.spec.ts | 18 +- .../int/campaignFixturesRowCounts.int.spec.ts | 13 +- tests/int/campaignInvite.int.spec.ts | 42 +- tests/int/campaignLeadership.int.spec.ts | 10 +- ...ampaignMigrationReconciliation.int.spec.ts | 21 +- .../campaignMunicipalityAccess.int.spec.ts | 9 +- .../campaignMunicipalityUpdate.int.spec.ts | 39 +- tests/int/campaignUserPhone.int.spec.ts | 25 +- .../int/collectionAccessLockdown.int.spec.ts | 13 +- .../int/contactPhoneNormalization.int.spec.ts | 10 +- tests/int/onda0Provision.int.spec.ts | 41 +- tests/int/payloadTransaction.int.spec.ts | 32 +- .../int/postgresTransactionLocks.int.spec.ts | 44 +- tests/unit/campaignActionContext.unit.spec.ts | 4 +- tests/unit/campaignUserProfile.unit.spec.ts | 5 +- ...deralCandidateTotalsAggregate.unit.spec.ts | 94 -- tsconfig.json | 212 +--- 58 files changed, 1291 insertions(+), 1906 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/IMPROVE-CODE-QUALITY-PLAN.md create mode 100644 docs/TECH-DEBT.md create mode 100644 docs/TESTING.md create mode 100644 knip.json delete mode 100644 scripts/rename-plaza-to-municipality.mjs delete mode 100644 src/components/campaign/CreateCampaignClientDynamic.tsx delete mode 100644 src/components/campaign/LazyDialogModuleFallback.tsx delete mode 100644 src/components/campaign/RecentlyVisited.tsx delete mode 100644 src/components/campaign/UpcomingActionPlansCard.tsx delete mode 100644 src/components/campaign/useLazyDialogModule.ts delete mode 100644 src/components/ui/DropdownMenu.tsx delete mode 100644 src/components/ui/Tabs.tsx delete mode 100644 src/components/ui/select.tsx delete mode 100644 src/utilities/actionPlanUpcomingPreview.ts delete mode 100644 src/utilities/federalCandidateTotalsAggregate.ts create mode 100644 src/utilities/hookFilledData.ts delete mode 100644 src/utilities/tseZone.ts create mode 100644 tests/helpers/stub.ts delete mode 100644 tests/unit/federalCandidateTotalsAggregate.unit.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7e185a6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +# Continuous integration for the GitHub mirror (pushes go to Codeberg + GitHub). +# Runs the same gate as .husky/pre-push plus tests and dead-code analysis. +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + postgres: + # Matches production Neon major (17) and docker-compose.yml. + image: postgres:17-alpine + env: + POSTGRES_USER: teqo + POSTGRES_PASSWORD: teqo + POSTGRES_DB: teqo_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U teqo" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + env: + # Tests load .env.test themselves (database teqo_test on localhost:5432). + # These are only for the migrate step, which reads the process env. + DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test + PAYLOAD_SECRET: test-only-secret-not-used-in-production + NEXT_PUBLIC_SITE_URL: http://localhost:3000 + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint (zero warnings) + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Dead code (knip) + run: pnpm exec knip + + - name: Unit tests + run: pnpm test:unit + + - name: Migrate test database + run: pnpm migrate + + - name: Integration tests + run: pnpm test:int diff --git a/.impeccable/config.json b/.impeccable/config.json index 5fc7123b..8e40de01 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -11,6 +11,15 @@ ], "createdAt": "2026-07-21T01:54:11.928Z", "reason": "Pre-existing inline hex in the password-reset EMAIL HTML template; transactional email cannot use CSS tokens" + }, + { + "rule": "broken-image", + "value": "*", + "files": [ + "src/components/MetaPixel.tsx" + ], + "createdAt": "2026-07-23T22:40:00.000Z", + "reason": "Meta's documented noscript tracking pixel: a hidden 1x1 whose src is a templated facebook.com/tr URL — not a rendered image" } ] } diff --git a/docs/IMPROVE-CODE-QUALITY-PLAN.md b/docs/IMPROVE-CODE-QUALITY-PLAN.md new file mode 100644 index 00000000..3c1dc2ef --- /dev/null +++ b/docs/IMPROVE-CODE-QUALITY-PLAN.md @@ -0,0 +1,46 @@ +# Improve Code Quality Plan + +## Context + +- **Started:** 2026-07-23 +- **Trigger:** full-codebase engineering-standards review (five parallel audits: structure/dead code, RBAC, client boundary/state, loading UX, caching) of a rapidly built ("vibe coded") codebase. +- **Scope:** whole repo; production is live (Neon Postgres with real citizens' PII), so security ordered first. +- **Method:** `improve-code-quality` journey, compressed into the seven delivery phases below (mapping to the skill's phases noted per row). The audits replaced a separate intake; the existing test suite (238 unit + 345 int + 5 e2e specs) is the safety net. +- **Companion docs:** [TECH-DEBT.md](TECH-DEBT.md) (ledger), [TESTING.md](TESTING.md) (safety-net map), [roadmap.md](roadmap.md) (canonical product/engineering backlog — larger debts become roadmap items per the `capture-review-debts` convention). + +## Phase Status + +| Phase | Content | Skill phases | Status | Date | +| ----- | ------- | ------------ | ------ | ---- | +| 0 | Security lockdown: collection access defaults, leader route gates, broken leader-supporter create, candidate alignment | — (P0 pre-work) | done | 2026-07-23 | +| 1 | Tooling gate: knip + dead code, CI, tsconfig, zero lint warnings, `as never` ban, docs artifacts | 1 (net) + 2 (readability) + 6 (habits) | in-progress | 2026-07-23 | +| 2 | Caching: React `cache()` dedup, committed election-aggregate artifact, `unstable_cache`, select/depth trims | 8 (sizing) | pending | | +| 3 | Loading feedback: task-toggle revalidation bug, map compare transition, result-region pending, Suspense streaming | — (UX correctness) | pending | | +| 4 | Client boundary/state: map hover isolation, bundle slimming, provider narrowing, composition splits | 4 (deep modules, UI) | pending | | +| 5 | Structure: split campaignAccess + supporter actions, dedupe clones, utilities organization, finish rename | 3 (refactoring) + 4 (deep modules) | pending | | +| 6 | Documentation: engineering-standards rule, AGENTS.md drift fixes, debt registration | 6 (habits) | pending | | + +Statuses: pending · in-progress · awaiting-evidence · done · deferred: \ · skipped: \ + +## Key Decisions + +| Date | Phase | Decision | Rationale | +| ---- | ----- | -------- | --------- | +| 2026-07-23 | 0 | Collection access lockdown before anything else | Payload defaults allowed any authenticated user (incl. campaign JWTs) to CRUD `users`/`signature`/`subscription`/`consent` via `/api` — live PII | +| 2026-07-23 | 0 | Keep pinned test contracts; stale tests updated, not silently changed code | Two leader-phone tests encoded the pre-remodel model; code was right, tests were stale | +| 2026-07-23 | 1 | `as never` banned repo-wide (ESLint `no-restricted-syntax`) | It silences all type checking; replaced by typed helpers (`hookFilledCreateData`, tests' `stub`) that keep property-level checking | +| 2026-07-23 | 1 | Lint gate is zero warnings (`--max-warnings=0`) | Warnings that never fail teach everyone to ignore the linter | +| 2026-07-23 | 1 | knip: files/dependencies = error; unused exports = warn (ledgered) | 158 export findings are dominated by god modules Phase 5 splits; failing CI on them day one would freeze the repo | +| 2026-07-23 | 1 | CI on the GitHub mirror (`.github/workflows/ci.yml`) | Pushes go to Codeberg + GitHub; GitHub Actions runs on the mirror with a Postgres 17 service | +| 2026-07-23 | 2 | Immutable TSE aggregates become a committed artifact (script-generated), not build-time computation | Vercel builds must not depend on prod DB content (TSE seeds are local-only); provenance stays explicit like `build:geometries` | +| 2026-07-23 | 2 | Salvador is the only zone-split municipality in the artifact | 2026-07-23 remodel: Camaçari is a single municipality (ZE 170/171 aggregated) | + +## Next Actions + +- [x] Phase 0 shipped (commit `fe0fb1c`) +- [ ] Phase 1: finish tests `as never` conversion; full verify (lint/typecheck/test/build); commit +- [ ] Phase 2 — caching +- [ ] Phase 3 — loading feedback +- [ ] Phase 4 — client boundary +- [ ] Phase 5 — structure +- [ ] Phase 6 — documentation; register leftovers in TECH-DEBT.md Debt Ledger diff --git a/docs/TECH-DEBT.md b/docs/TECH-DEBT.md new file mode 100644 index 00000000..d0151d27 --- /dev/null +++ b/docs/TECH-DEBT.md @@ -0,0 +1,54 @@ +# Tech Debt + +Ledger of known engineering debt. Items large enough to be scheduled work become entries in [roadmap.md](roadmap.md) (linked in the Ref column); this file tracks the full inventory so nothing is lost. Sourced initially from the 2026-07-23 engineering-standards audits. + +## Debt Ledger + +| Item | Location | Type | Risk | Effort | Priority | Status | Ref | +| ---- | -------- | ---- | ---- | ------ | -------- | ------ | --- | +| 158 unused exports reported by knip (warn-level) | god modules, ui kit families | dead code | low | M | P2 | open — drive `exports` rule to error after splits | Phase 5 | +| knip loader noise: `Error loading src/payload.config.ts` (server-only guard) | knip run output | tooling noise | none (exit 0, static graph resolves) | S | P3 | open | | +| Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins | `tests/helpers/testDatabaseLease.ts` (`withMissingInviteConsentFixture`) | test-infra race | low (ms-wide window; shared leases added 2026-07-23 narrow it) | M | P2 | open | | +| `users` collection has no roles (all admins omnipotent) | `src/collections/Users.ts` | RBAC | medium (small trusted admin set) | M (schema migration) | P1 before widening `/admin` | open | AGENTS Known Gaps #1 | +| Any campaign user can enumerate staff names/roles (`canReadCampaignUsers`) | `src/utilities/campaignAccess.ts` | info disclosure | low | S | P2 | open | | +| `removeSupporterData` deletes via `overrideAccess: true` while `canDeleteSupporter` is admin-only | `src/app/(campaign)/campanha/actions/supporter.ts` | access asymmetry (likely intentional LGPD path) | low | S | P3 | open — document or align | | +| Legacy public consent flows hardcode document IDs (`consent: 2`) | `src/app/(frontend)/actions/submitWhatsapp.ts` | correctness | medium | S | P1 | open | AGENTS Known Gaps #2 | +| E2E suite thin (5 specs; most campaign CRUD int-only) | `tests/e2e/` | coverage | medium | L | P2 | open | | +| CI runs on GitHub mirror only (no Codeberg/Woodpecker pipeline) | `.github/workflows/ci.yml` | process | low | M | P3 | open | | +| Post listing cache holds all published posts, filters in memory | `src/utilities/posts.ts` | scale (fine at ~tens of posts) | low | M | P3 | open — watch corpus growth | | +| Petition `generateStaticParams` uses uncached `getDocuments` without `select`/`depth: 0` | `src/utilities/documents.ts` consumers | over-fetch (build-time) | low | S | P2 | planned (Phase 2) | | +| Compare mode (`?compare=`) still queries per candidate uncached cross-request | `src/utilities/municipalityMapData.ts` | perf | low | M | P3 | open — React `cache()` dedups in-request; add `unstable_cache` if profiling warrants | | + +## Smell Inventory + +| Smell | Location | Refactoring | Status | +| ----- | -------- | ----------- | ------ | +| God module: RBAC for every collection in one file (964 lines, ~80 exports) | `src/utilities/campaignAccess.ts` | split per domain under `src/utilities/access/` | planned (Phase 5) | +| God module: ~71 exports of electoral math | `src/lib/electionInsights.ts` | split by concern when next touched | ledgered | +| Multi-feature action bag: CRUD + CSV import pipeline + LGPD in one file (722 lines) | `src/app/(campaign)/campanha/actions/supporter.ts` | split CRUD vs import | planned (Phase 5) | +| Clone twins: create/update/staff-assert/unique-conflict skeleton | `actions/organization.ts` ↔ `actions/stateDeputy.ts` | extract shared factory | planned (Phase 5) | +| 13 near-identical `formActions.ts` try/catch→mapError→redirect ladders | `src/app/(campaign)/campanha/(app)/*/formActions.ts` | shared form-action wrapper | planned (Phase 5) | +| Flat 80+-file `src/utilities/` junk drawer; `lib/` vs `utilities/` boundary undocumented | `src/utilities/`, `src/lib/` | document convention + move misfiled modules | planned (Phase 5/6) | +| Praça→Município rename leftovers in identifiers (`PLAZA_MAP_YEARS`, `ACCESSIBLE_PLAZA_IDS_CONTEXT_KEY`, …) | map/list utilities | finish rename | planned (Phase 5) | +| Props→state `useEffect` mirrors | `MunicipalityListExpectedVotesControl`, comboboxes | key-remount or fully controlled | planned (Phase 4) | +| Detail-page RSCs mixing loading + tab UI + forms (300+ lines) | `municipios/[slug]/page.tsx`, `planos/[slug]/page.tsx` | extract tab loaders/sections | ledgered | + +## Sprout / Wrap Register + +| Host | Sprouted/Wrapped | Notes | +| ---- | ---------------- | ----- | +| (none yet) | | | + +## Adopted Conventions + +- **Zero lint warnings.** `pnpm lint` runs with `--max-warnings=0`; deliberate placeholders use a `_` prefix (`argsIgnorePattern`). +- **`as never` is banned** (ESLint `no-restricted-syntax`). Production code uses typed helpers (e.g. `hookFilledCreateData` for hook-completed create data); tests use `stub(partial)` which keeps property-level checking. A justified `eslint-disable-next-line` is the only escape hatch. +- **Dead code is deleted, not commented.** knip runs in CI (files/dependencies at error level). After any change: `pnpm exec knip` and remove what died. +- **Collections always declare `access` explicitly.** Payload's default ("any authenticated user") is treated as a bug — see `payloadAdminOnly`. +- **Generated migration signatures are exempt** from unused-args linting (`src/migrations/**` override); never hand-edit migration content for lint cosmetics. +- **Shared consent fixture rows are leased, never owned** in integration tests (`tests/helpers/testDatabaseLease.ts`); deleting them steals from parallel spec files. + +## Debt Budget & Broken-Windows Policy + +- New warnings/`as never`/unused exports fail or get ledgered the same day — no untracked `// TODO`s. +- Each delivery that runs `/simplify` registers surviving debts here or in `docs/plans/escala-dry-pos-*.md` (existing convention), whichever the item's size warrants. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 00000000..b036969b --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,32 @@ +# Testing + +## Test Strategy + +Three layers, all local-database-only (see `local-database` skill; tests refuse any database not ending in `_test`): + +- **Unit** (`tests/unit/**/*.unit.spec.ts`, 33+ specs): pure logic; runs with a deliberately invalid `DATABASE_URL` so nothing can touch a database. +- **Integration** (`tests/int/**/*.int.spec.ts`, 42+ specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned. +- **E2E** (`tests/e2e`, Playwright, 5 specs): smoke-level; most campaign behavior is pinned at the int layer. + +CI (`.github/workflows/ci.yml`) runs lint (zero warnings), typecheck, knip, unit, migrate, int against a Postgres 17 service. + +## Safety Net Map + +Modules the 2026-07 hardening phases touch, and where their behavior is pinned: + +| Module | Pinned behaviors | Test files | Gaps | +| ------ | ---------------- | ---------- | ---- | +| `campaignAccess.ts` (RBAC) | per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry | `campaignMunicipalityAccess`, `campaignVotePledge`, `campaignSupporter`, `campaignUserPhone`, `campaignActionPlan`, `campaignDemandWorkflow`, `collectionAccessLockdown` (int) | no dedicated per-export suite; covered through collection behavior | +| Collection access lockdown | campaign JWT denied on `users`/PII/CMS writes; anonymous read of public content; admin unaffected | `collectionAccessLockdown.int.spec.ts` | REST-transport variant untested (Local API with `overrideAccess: false` is equivalent) | +| `municipalityMapData.ts` | bundle scoping by URL filters + advisor access; `validVotesByYear` incl. 2026 reuse of 2022; null on empty scope | `municipalityMapData.int.spec.ts` | zone breakdown shape | +| `votePledgeData.ts` | `estimated[scenario] ?? declared` aggregation; role scoping; leader denial | `campaignVotePledge.int.spec.ts` | | +| Supporter import (token/bulk) | preview→confirm flow, dedup by phone, consent fail-closed | `campaignSupporter.int.spec.ts` | `supporterImportBulk`/`supporterImportToken` internals only via flow | +| Leader supporter create | linked-municipality scope; out-of-scope rejection; staff rejection | `collectionAccessLockdown.int.spec.ts` | | +| Posts visibility/caching | `isPostVisible` fail-closed; cached listing | (indirect via routes) | no dedicated `posts` spec — backlog | + +## Characterization Backlog + +- [ ] `src/utilities/posts.ts` — dedicated spec for `isPostVisible` fail-closed matrix and cache wrappers. +- [ ] `actionPlanViewModels.ts` + other `actionPlan*` utilities (~1k lines) — currently untested directly. +- [ ] `campaignDemandData.ts` / `leadershipData.ts` / `organizationData.ts` / `stateDeputyData.ts` list loaders. +- [ ] Election aggregate artifact (Phase 2): snapshot test pinning artifact ↔ `municipalityCatalog` key consistency. diff --git a/eslint.config.mjs b/eslint.config.mjs index 45960655..8f4c77c3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -141,6 +141,40 @@ const eslintConfig = [ 'local/use-server-async-exports': 'error', }, }, + { + // Underscore-prefixed bindings are deliberate placeholders (unused action + // state, ignored tuple slots); everything else unused is dead code. + files: ['**/*.{js,mjs,jsx,ts,tsx}'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + // `as never` silences every type check on the expression (worse than + // `any`). Fix the types instead; a justified eslint-disable comment is + // the only sanctioned escape hatch. + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSAsExpression > TSNeverKeyword', + message: + 'Do not cast with `as never` — it disables type checking entirely. Fix the types or use a narrowly-typed helper.', + }, + ], + }, + }, + { + // Payload generates migration signatures with unused destructured args. + files: ['src/migrations/**/*.ts'], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], + }, + }, prettier, // Override default ignores of eslint-config-next. globalIgnores([ diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..3eb8e869 --- /dev/null +++ b/knip.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "entry": [ + "src/payload.config.ts", + "src/migrations/index.ts", + "src/app/(payload)/admin/importMap.js", + "scripts/*.mjs", + "eslint-local-rules.mjs" + ], + "project": ["src/**/*.{ts,tsx,js}", "scripts/**/*.mjs"], + "ignore": ["src/payload-types.ts", "src/migrations/**"], + "paths": { + "server-only": ["tests/helpers/serverOnly.ts"] + }, + "ignoreBinaries": ["playwright"], + "ignoreDependencies": [ + "tsx", + "sharp", + "tailwindcss-animate", + "tw-animate-css", + "shadcn", + "eslint-config-next", + "prettier" + ], + "rules": { + "exports": "warn", + "types": "warn", + "enumMembers": "warn", + "duplicates": "warn" + } +} diff --git a/package.json b/package.json index 337881df..c807c32a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs", "db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs", "build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs", + "build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs", "build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs", "build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs", "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", @@ -38,7 +39,9 @@ "migrate": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate", "migrate:create": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate:create", "migrate:status": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload migrate:status", - "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint --cache --cache-location .next/cache/eslint/", + "knip": "knip", + "knip:production": "knip --production", + "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint --cache --cache-location .next/cache/eslint/ --max-warnings=0", "lint:fix": "cross-env NODE_OPTIONS=--no-deprecation eslint --fix", "payload": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload", "start": "cross-env NODE_OPTIONS=--no-deprecation next start", @@ -62,10 +65,6 @@ "@payloadcms/richtext-lexical": "3.82.0", "@payloadcms/storage-vercel-blob": "3.82.0", "@payloadcms/ui": "3.82.0", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", "@vercel/analytics": "^1.6.1", "@vercel/speed-insights": "^1.3.1", "class-variance-authority": "^0.7.1", @@ -101,6 +100,7 @@ "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/node": "22.19.9", + "@types/pg": "^8.10.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/topojson-client": "^3.1.5", @@ -117,6 +117,8 @@ "eslint-plugin-check-file": "^3.3.1", "husky": "^9.1.7", "jsdom": "28.0.0", + "knip": "^6.27.0", + "pg": "^8.22.0", "postcss": "^8.5.6", "prettier": "^3.4.2", "schema-dts": "^2.0.0", @@ -126,7 +128,6 @@ "tsx": "4.21.0", "typescript": "npm:@typescript/typescript6@6.0.2", "typescript-7": "npm:typescript@7.0.2", - "typescript-eslint": "^8.64.0", "vite-tsconfig-paths": "6.0.5", "vitest": "4.0.18", "yauzl": "^3.4.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b2b9a23..dac67e68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,18 +38,6 @@ importers: '@payloadcms/ui': specifier: 3.82.0 version: 3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-label': - specifier: ^2.1.8 - version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-select': - specifier: ^2.2.6 - version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-separator': - specifier: ^1.1.8 - version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': - specifier: ^1.2.4 - version: 1.2.4(@types/react@19.2.14)(react@19.2.4) '@vercel/analytics': specifier: ^1.6.1 version: 1.6.1(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(react@19.2.4) @@ -150,6 +138,9 @@ importers: '@types/node': specifier: 22.19.9 version: 22.19.9 + '@types/pg': + specifier: ^8.10.2 + version: 8.10.2 '@types/react': specifier: ^19.2.14 version: 19.2.14 @@ -176,28 +167,34 @@ importers: version: 2.3.1 '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) csv-parse: specifier: ^7.0.1 version: 7.0.1 eslint: specifier: ^9.39.2 - version: 9.39.2(jiti@2.6.1) + version: 9.39.2(jiti@2.7.0) eslint-config-next: specifier: 15.4.11 - version: 15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + version: 15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@9.39.2(jiti@2.7.0)) eslint-plugin-check-file: specifier: ^3.3.1 - version: 3.3.1(eslint@9.39.2(jiti@2.6.1)) + version: 3.3.1(eslint@9.39.2(jiti@2.7.0)) husky: specifier: ^9.1.7 version: 9.1.7 jsdom: specifier: 28.0.0 version: 28.0.0(@noble/hashes@1.8.0) + knip: + specifier: ^6.27.0 + version: 6.27.0 + pg: + specifier: ^8.22.0 + version: 8.22.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -225,15 +222,12 @@ importers: typescript-7: specifier: npm:typescript@7.0.2 version: typescript@7.0.2 - typescript-eslint: - specifier: ^8.64.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) vite-tsconfig-paths: specifier: 6.0.5 - version: 6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) vitest: specifier: 4.0.18 - version: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) yauzl: specifier: ^3.4.0 version: 3.4.0 @@ -518,15 +512,30 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1588,6 +1597,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@15.4.11': resolution: {integrity: sha512-mIYp/091eYfPFezKX7ZPTWqrmSXq+ih6+LcUyKvLmeLQGhlPtot33kuEOd4U+xAA7sFfj21+OtCpIZx0g5SpvQ==} @@ -1686,6 +1701,239 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.137.0': + resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.137.0': + resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.137.0': + resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.137.0': + resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.3': + resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.21.3': + resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} + cpu: [x64] + os: [win32] + '@payloadcms/db-postgres@3.82.0': resolution: {integrity: sha512-FzIRFpytQc8j6UjMpCYKN5Vz+sjw6EgOgsLzVtae6ihbJcHnEWeLXsV9ELd/r1349nNNqyFycRbJVpXbb+rhhw==} peerDependencies: @@ -2051,19 +2299,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-label@2.1.8': - resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-menu@2.1.16': resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: @@ -2285,19 +2520,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-separator@1.1.8': - resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-slider@1.3.6': resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} peerDependencies: @@ -2791,6 +3013,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -2920,14 +3145,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/eslint-plugin@8.64.0': - resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.64.0 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.55.0': resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2935,45 +3152,22 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.64.0': - resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.55.0': resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.64.0': - resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.55.0': resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.64.0': - resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.55.0': resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.64.0': - resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.55.0': resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2981,33 +3175,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.64.0': - resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.55.0': resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.64.0': - resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.55.0': resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.64.0': - resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.55.0': resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3015,21 +3192,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.64.0': - resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.55.0': resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.64.0': - resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -4223,10 +4389,6 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4338,6 +4500,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -4399,6 +4564,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + formatly@0.3.0: + resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} + engines: {node: '>=18.3.0'} + hasBin: true + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -4488,6 +4658,9 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -4898,6 +5071,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -4987,6 +5164,11 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + knip@6.27.0: + resolution: {integrity: sha512-CngYEYrD0n20N06FXA8n3u/0Wnnugoa+B9k14OP+iKIgkCHuzvIdsP3nfwjhByoc1WfogpxfiriMboAXFETDUw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -5453,6 +5635,13 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + oxc-parser@0.137.0: + resolution: {integrity: sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.21.3: + resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -5534,9 +5723,15 @@ packages: pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + pg-connection-string@2.11.0: resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -5550,9 +5745,17 @@ packages: peerDependencies: pg: '>=8.0' + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + pg-protocol@1.11.0: resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -5570,6 +5773,15 @@ packages: pg-native: optional: true + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} @@ -5584,6 +5796,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -6057,6 +6273,10 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} + engines: {node: '>= 18'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -6262,6 +6482,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -6323,12 +6547,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-api-utils@2.5.0: - resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-essentials@10.0.3: resolution: {integrity: sha512-/FrVAZ76JLTWxJOERk04fm8hYENDo0PWSP3YLQKxevLwWtxemGcl5JJEzN4iqfDlRve0ckyfFaOBu4xbNH/wZw==} peerDependencies: @@ -6396,13 +6614,6 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.64.0: - resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -6417,6 +6628,10 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + unbash@4.0.3: + resolution: {integrity: sha512-3cudTErfToSc4Ggv8XGXVNVli/xHKUtUZvaY5UVwhOcUPbQGz7PeaEnT/SAVgNziZtX67KEN9swMUYkLghxA1w==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -6626,6 +6841,10 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -6739,8 +6958,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true @@ -7124,12 +7343,34 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -7140,6 +7381,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.28.6 @@ -7436,9 +7682,9 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.7.0))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -8009,6 +8255,20 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@next/env@15.4.11': {} '@next/env@15.5.12': {} @@ -8072,6 +8332,133 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.137.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + optional: true + + '@oxc-project/types@0.137.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + optional: true + + '@oxc-resolver/binding-android-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + optional: true + '@payloadcms/db-postgres@3.82.0(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))': dependencies: '@payloadcms/drizzle': 3.82.0(@types/pg@8.10.2)(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(pg@8.16.3) @@ -8607,15 +8994,6 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -8897,15 +9275,6 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.14 - '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@radix-ui/number': 1.1.1 @@ -9326,6 +9695,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/acorn@4.0.6': dependencies: '@types/estree': 1.0.8 @@ -9405,7 +9779,7 @@ snapshots: '@types/pg@8.10.2': dependencies: '@types/node': 22.19.9 - pg-protocol: 1.11.0 + pg-protocol: 1.15.0 pg-types: 4.1.0 '@types/react-dom@19.2.3(@types/react@19.2.14)': @@ -9462,15 +9836,15 @@ snapshots: dependencies: '@types/node': 22.19.9 - '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.55.0 - '@typescript-eslint/type-utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/type-utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/visitor-keys': 8.55.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.4.0(@typescript/typescript6@6.0.2) @@ -9478,42 +9852,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 9.39.2(jiti@2.6.1) - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) '@typescript-eslint/visitor-keys': 8.55.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -9527,61 +9873,29 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(@typescript/typescript6@6.0.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - debug: 4.4.3 - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@8.55.0': dependencies: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/visitor-keys': 8.55.0 - '@typescript-eslint/scope-manager@8.64.0': - dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - '@typescript-eslint/tsconfig-utils@8.55.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': - dependencies: - typescript: '@typescript/typescript6@6.0.2' - - '@typescript-eslint/type-utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': + '@typescript-eslint/type-utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/utils': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) ts-api-utils: 2.4.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - '@typescript-eslint/types@8.55.0': {} - '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.55.0(@typescript/typescript6@6.0.2)': dependencies: '@typescript-eslint/project-service': 8.55.0(@typescript/typescript6@6.0.2) @@ -9597,39 +9911,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0))': dependencies: - '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 '@typescript-eslint/typescript-estree': 8.55.0(@typescript/typescript6@6.0.2) - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1))': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -9639,11 +9927,6 @@ snapshots: '@typescript-eslint/types': 8.55.0 eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.64.0': - dependencies: - '@typescript-eslint/types': 8.64.0 - eslint-visitor-keys: 5.0.1 - '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -9785,7 +10068,7 @@ snapshots: next: 15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4) react: 19.2.4 - '@vitejs/plugin-react@4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2))': + '@vitejs/plugin-react@4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -9793,7 +10076,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -9806,14 +10089,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2) - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -10647,19 +10930,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)): + eslint-config-next@15.4.11(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@next/eslint-plugin-next': 15.4.11 '@rushstack/eslint-patch': 1.16.1 - '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.7.0)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.7.0)) optionalDependencies: typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -10667,9 +10950,9 @@ snapshots: - eslint-plugin-import-x - supports-color - eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -10679,38 +10962,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - supports-color - eslint-plugin-check-file@3.3.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-check-file@3.3.1(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) is-glob: 4.0.3 micromatch: 4.0.8 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -10719,9 +11003,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -10733,13 +11017,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) + '@typescript-eslint/parser': 8.55.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.7.0)) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -10749,7 +11033,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -10758,11 +11042,11 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.7.0)): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.7.0)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -10770,7 +11054,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.2(jiti@2.7.0) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -10793,11 +11077,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - - eslint@9.39.2(jiti@2.6.1): + eslint@9.39.2(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 @@ -10832,7 +11114,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -10978,10 +11260,18 @@ snapshots: dependencies: reusify: 1.1.0 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -11053,6 +11343,10 @@ snapshots: dependencies: is-callable: 1.2.7 + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -11137,6 +11431,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -11504,6 +11802,8 @@ snapshots: jiti@2.6.1: {} + jiti@2.7.0: {} + jose@5.10.0: {} jose@6.2.1: {} @@ -11601,6 +11901,22 @@ snapshots: kleur@4.1.5: {} + knip@6.27.0: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + formatly: 0.3.0 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + oxc-parser: 0.137.0 + oxc-resolver: 11.21.3 + picomatch: 4.0.5 + smol-toml: 1.7.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.3 + yaml: 2.9.0 + zod: 4.3.6 + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -12186,6 +12502,53 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + oxc-parser@0.137.0: + dependencies: + '@oxc-project/types': 0.137.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.137.0 + '@oxc-parser/binding-android-arm64': 0.137.0 + '@oxc-parser/binding-darwin-arm64': 0.137.0 + '@oxc-parser/binding-darwin-x64': 0.137.0 + '@oxc-parser/binding-freebsd-x64': 0.137.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.137.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.137.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.137.0 + '@oxc-parser/binding-linux-arm64-musl': 0.137.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.137.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-musl': 0.137.0 + '@oxc-parser/binding-openharmony-arm64': 0.137.0 + '@oxc-parser/binding-wasm32-wasi': 0.137.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.137.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.137.0 + '@oxc-parser/binding-win32-x64-msvc': 0.137.0 + + oxc-resolver@11.21.3: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.21.3 + '@oxc-resolver/binding-android-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-x64': 11.21.3 + '@oxc-resolver/binding-freebsd-x64': 11.21.3 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.3 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.3 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-musl': 11.21.3 + '@oxc-resolver/binding-openharmony-arm64': 11.21.3 + '@oxc-resolver/binding-wasm32-wasi': 11.21.3 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -12290,8 +12653,13 @@ snapshots: pg-cloudflare@1.3.0: optional: true + pg-cloudflare@1.4.0: + optional: true + pg-connection-string@2.11.0: {} + pg-connection-string@2.14.0: {} + pg-int8@1.0.1: {} pg-numeric@1.0.2: {} @@ -12300,8 +12668,14 @@ snapshots: dependencies: pg: 8.16.3 + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + pg-protocol@1.11.0: {} + pg-protocol@1.15.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -12330,6 +12704,16 @@ snapshots: optionalDependencies: pg-cloudflare: 1.3.0 + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + pgpass@1.0.5: dependencies: split2: 4.2.0 @@ -12340,6 +12724,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.5: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -13024,6 +13410,8 @@ snapshots: sisteransi@1.0.5: {} + smol-toml@1.7.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -13216,6 +13604,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tinyrainbow@3.0.3: {} tldts-core@7.0.23: {} @@ -13275,10 +13668,6 @@ snapshots: dependencies: typescript: '@typescript/typescript6@6.0.2' - ts-api-utils@2.5.0(@typescript/typescript6@6.0.2): - dependencies: - typescript: '@typescript/typescript6@6.0.2' - ts-essentials@10.0.3(@typescript/typescript6@6.0.2): optionalDependencies: typescript: '@typescript/typescript6@6.0.2' @@ -13363,17 +13752,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)))(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@9.39.2(jiti@2.6.1)) - eslint: 9.39.2(jiti@2.6.1) - typescript: '@typescript/typescript6@6.0.2' - transitivePeerDependencies: - - supports-color - typescript@6.0.3: {} typescript@7.0.2: @@ -13401,6 +13779,8 @@ snapshots: uint8array-extras@1.5.0: {} + unbash@4.0.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -13528,17 +13908,17 @@ snapshots: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vite-tsconfig-paths@6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.5(@typescript/typescript6@6.0.2)(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13549,16 +13929,16 @@ snapshots: optionalDependencies: '@types/node': 22.19.9 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 lightningcss: 1.30.2 sass: 1.77.4 tsx: 4.21.0 - yaml: 2.8.2 + yaml: 2.9.0 - vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -13575,7 +13955,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.9 @@ -13598,6 +13978,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + walk-up-path@4.0.0: {} + web-streams-polyfill@3.3.3: {} webidl-conversions@8.0.1: {} @@ -13712,8 +14094,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.2: - optional: true + yaml@2.9.0: {} yargs-parser@21.1.1: {} diff --git a/scripts/build-bahia-demographics.mjs b/scripts/build-bahia-demographics.mjs index 7133abb3..2a03dfcb 100644 --- a/scripts/build-bahia-demographics.mjs +++ b/scripts/build-bahia-demographics.mjs @@ -130,7 +130,7 @@ const buildDemographicsByCode = ({ ageCategoryRows, femaleRows, medianRows }) => current.medianAge = Number.isFinite(median) ? median : null } - for (const [code, record] of byCode.entries()) { + for (const record of byCode.values()) { record.population = Object.values(record.ageBands).reduce((sum, value) => sum + value, 0) record.sexShareFemale = record.population > 0 ? record.femalePopulation / record.population : 0 diff --git a/scripts/rename-plaza-to-municipality.mjs b/scripts/rename-plaza-to-municipality.mjs deleted file mode 100644 index 1a937c48..00000000 --- a/scripts/rename-plaza-to-municipality.mjs +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env node -/** - * One-shot mechanical rename: plaza → municipality across src/ and tests/. - * Excludes frozen historical migration artifacts. - */ -import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs' -import { dirname, join, relative } from 'path' - -const ROOT = join(import.meta.dirname, '..') - -const EXCLUDE_PATH_FRAGMENTS = [ - 'src/migrations/data/plazaCatalog20260721.ts', - 'src/migrations/20260721_020109_remodel_plazas.ts', - 'src/migrations/20260721_020109_remodel_plazas.json', - 'src/migrations/20260721_133444_add_plaza_expected_votes.ts', - 'src/migrations/20260721_133444_add_plaza_expected_votes.json', - 'node_modules/', - '.git/', - 'scripts/rename-plaza-to-municipality.mjs', -] - -const shouldExclude = (relPath) => EXCLUDE_PATH_FRAGMENTS.some((frag) => relPath.includes(frag)) - -const renamePathSegment = (segment) => - segment - .replace(/PlazaUpdate/g, 'MunicipalityUpdate') - .replace(/plazaUpdate/g, 'municipalityUpdate') - .replace(/Plaza/g, 'Municipality') - .replace(/plaza/g, 'municipality') - .replace(/^pracas$/, 'municipios') - -function walk(dir, files = []) { - for (const entry of readdirSync(dir)) { - const full = join(dir, entry) - const rel = relative(ROOT, full) - if (shouldExclude(rel)) continue - const st = statSync(full) - if (st.isDirectory()) { - if (entry === 'node_modules' || entry === '.git') continue - walk(full, files) - } else if (/\.(ts|tsx|js|jsx|json|md|mdc)$/.test(entry)) { - files.push(full) - } - } - return files -} - -// Rename directories bottom-up -function collectDirs(dir, dirs = []) { - for (const entry of readdirSync(dir)) { - const full = join(dir, entry) - const rel = relative(ROOT, full) - if (shouldExclude(rel)) continue - if (statSync(full).isDirectory()) { - collectDirs(full, dirs) - if (/plaza|Plaza|pracas/i.test(entry)) dirs.push(full) - } - } - return dirs -} - -const dirRenames = collectDirs(join(ROOT, 'src')) - .concat(collectDirs(join(ROOT, 'tests'))) - .sort((a, b) => b.length - a.length) - -for (const dir of dirRenames) { - const parent = dirname(dir) - const base = renamePathSegment(dir.split('/').pop()) - const target = join(parent, base) - if (dir !== target) { - mkdirSync(dirname(target), { recursive: true }) - try { - renameSync(dir, target) - console.log('DIR', relative(ROOT, dir), '->', relative(ROOT, target)) - } catch (e) { - // already renamed - } - } -} - -// Rename files -const allFiles = walk(join(ROOT, 'src')).concat(walk(join(ROOT, 'tests'))) -const fileRenames = allFiles - .filter((f) => /plaza|Plaza|pracas/i.test(f.split('/').pop())) - .sort((a, b) => b.length - a.length) - -for (const file of fileRenames) { - const parent = dirname(file) - const base = renamePathSegment(file.split('/').pop()) - const target = join(parent, base) - if (file !== target) { - try { - renameSync(file, target) - console.log('FILE', relative(ROOT, file), '->', relative(ROOT, target)) - } catch { - /* noop */ - } - } -} - -const REPLACEMENTS = [ - ['/campanha/pracas', '/campanha/municipios'], - ["collection: 'plazaUpdate'", "collection: 'municipalityUpdate'"], - ["collection: 'plaza'", "collection: 'municipality'"], - ["relationTo: 'plazaUpdate'", "relationTo: 'municipalityUpdate'"], - ["relationTo: 'plaza'", "relationTo: 'municipality'"], - ['slug: plazaUpdate', 'slug: municipalityUpdate'], - ['slug: plaza', 'slug: municipality'], - ["'plazaUpdate'", "'municipalityUpdate'"], - ["'plaza'", "'municipality'"], - ['PlazaUpdate', 'MunicipalityUpdate'], - ['plazaUpdate', 'municipalityUpdate'], - ['PlazaEstimateScenario', 'MunicipalityEstimateScenario'], - ['plazaEstimateScenario', 'municipalityEstimateScenario'], - ['PlazaZoneNeighborhoods', 'MunicipalityZoneNeighborhoods'], - ['plazaZoneNeighborhoods', 'municipalityZoneNeighborhoods'], - ['plaza-zone-neighborhoods', 'municipality-zone-neighborhoods'], - ['plaza-catalog', 'municipality-catalog'], - ['plazaStaffFormActions', 'municipalityStaffFormActions'], - ['plazaStaffEditMessages', 'municipalityStaffEditMessages'], - ['plazaStaffVoteTotal', 'municipalityStaffVoteTotal'], - ['StaffPlazaVotes', 'StaffMunicipalityVotes'], - ['aggregatePledgesByPlaza', 'aggregatePledgesByMunicipality'], - ['loadPlazaListPageBundle', 'loadMunicipalityListPageBundle'], - ['loadPlazaMapBundle', 'loadMunicipalityMapBundle'], - ['buildPlazaMapBundleFromPlazas', 'buildMunicipalityMapBundleFromMunicipalities'], - ['buildPlazaMapBundle', 'buildMunicipalityMapBundle'], - ['buildPlazaListWhere', 'buildMunicipalityListWhere'], - ['shouldUpdatePlazaSearchUrl', 'shouldUpdateMunicipalitySearchUrl'], - ['buildPlazaFiltersKey', 'buildMunicipalityFiltersKey'], - ['getAccessiblePlazaIds', 'getAccessibleMunicipalityIds'], - ['canAssignPlazaAdvisors', 'canAssignMunicipalityAdvisors'], - ['canManagePlazaAdvisors', 'canManageMunicipalityAdvisors'], - ['canCreatePlaza', 'canCreateMunicipality'], - ['canDeletePlaza', 'canDeleteMunicipality'], - ['canReadPlaza', 'canReadMunicipality'], - ['canUpdatePlaza', 'canUpdateMunicipality'], - ['validatePlazaAdvisors', 'validateMunicipalityAdvisors'], - ['plazaCatalogEntriesForCity', 'municipalityCatalogEntriesForCity'], - ['getPlazaCatalogEntry', 'getMunicipalityCatalogEntry'], - ['plazaCatalog', 'municipalityCatalog'], - ['isPlazaSlug', 'isMunicipalitySlug'], - ['PlazaCatalogEntry', 'MunicipalityCatalogEntry'], - ['PlazaKind', 'MunicipalityKind'], - ['ZONE_PLAZA_CITIES', 'ZONE_MUNICIPALITY_CITIES'], - ['zonePlazaCitySet', 'zoneMunicipalityCitySet'], - ['plazasByIbgeCode', 'municipalitiesByIbgeCode'], - ['resolvePlazaMapNavigation', 'resolveMunicipalityMapNavigation'], - ['plazaMapNavigation', 'municipalityMapNavigation'], - ['plazaRevalidation', 'municipalityRevalidation'], - ['plazaSlug', 'municipalitySlug'], - ['plazaPageData', 'municipalityPageData'], - ['plazaViewModels', 'municipalityViewModels'], - ['plazaMapData', 'municipalityMapData'], - ['plazaUi', 'municipalityUi'], - ['plazaElectoralBaseline', 'municipalityElectoralBaseline'], - ['plazaElectionGeography', 'municipalityElectionGeography'], - ['plazaCandidateComparison', 'municipalityCandidateComparison'], - ['plazaDetailTabUi', 'municipalityDetailTabUi'], - ['campaignPlazaAccess', 'campaignMunicipalityAccess'], - ['campaignPlazas', 'campaignMunicipalities'], - ['campaignPlazaUpdate', 'campaignMunicipalityUpdate'], - ['updatePlazaStrategy', 'updateMunicipalityStrategy'], - ['PlazaMapBundle', 'MunicipalityMapBundle'], - ['plazaMapBundle', 'municipalityMapBundle'], - ['PlazaList', 'MunicipalityList'], - ['PlazaFilters', 'MunicipalityFilters'], - ['PlazaMap', 'MunicipalityMap'], - ['PlazaStrategy', 'MunicipalityStrategy'], - ['PlazaBaseline', 'MunicipalityBaseline'], - ['PlazaCandidate', 'MunicipalityCandidate'], - ['PlazaAdvisor', 'MunicipalityAdvisor'], - ['PlazaLeadership', 'MunicipalityLeadership'], - ['PlazaPledges', 'MunicipalityPledges'], - ['PlazaTab', 'MunicipalityTab'], - ['PlazaUpdate', 'MunicipalityUpdate'], - ['plazas', 'municipalities'], - ['Plaza', 'Municipality'], - ['plaza', 'municipality'], -] - -const contentFiles = walk(join(ROOT, 'src')) - .concat(walk(join(ROOT, 'tests'))) - .concat( - [ - join(ROOT, 'AGENTS.md'), - join(ROOT, 'PRODUCT.md'), - join(ROOT, 'docs/roadmap.md'), - join(ROOT, '.cursor/rules/projects/nucleos-eleitorais.mdc'), - ].filter((f) => { - try { - statSync(f) - return true - } catch { - return false - } - }), - ) - -for (const file of contentFiles) { - const rel = relative(ROOT, file) - if (shouldExclude(rel)) continue - let content = readFileSync(file, 'utf8') - const original = content - for (const [from, to] of REPLACEMENTS) { - content = content.split(from).join(to) - } - if (content !== original) { - writeFileSync(file, content) - console.log('CONTENT', rel) - } -} - -console.log('Done.') diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts index a47c540a..09189f55 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation' import { createStateDeputy } from '@/app/(campaign)/campanha/actions/stateDeputy' -import { nullableFormText, optionalFormText, requiredFormText } from '@/lib/formData' +import { optionalFormText, requiredFormText } from '@/lib/formData' import { mapCampaignFormActionError, type CampaignFormActionState, diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx index 29fac025..86df6742 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/page.tsx @@ -1,4 +1,3 @@ -import config from '@payload-config' import { ArrowLeftIcon } from 'lucide-react' import Link from 'next/link' import { redirect } from 'next/navigation' diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index da285424..070e8904 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -6,7 +6,6 @@ import { getPayload } from 'payload' import { CampaignListPagination } from '@/components/campaign/CampaignListPagination' import { CampaignPageShell } from '@/components/campaign/CampaignPageShell' -import { Badge } from '@/components/ui/Badge' import { Button } from '@/components/ui/button' import { Empty, diff --git a/src/app/(campaign)/campanha/actions/actionPlan.ts b/src/app/(campaign)/campanha/actions/actionPlan.ts index 8dfe082f..1f36ebaa 100644 --- a/src/app/(campaign)/campanha/actions/actionPlan.ts +++ b/src/app/(campaign)/campanha/actions/actionPlan.ts @@ -13,6 +13,7 @@ import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campa import { isCampaignStaff } from '@/utilities/campaignAccess' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const MAX_ACTION_PLAN_UPDATE_BODY_LENGTH = 4000 const MAX_ACTION_PLAN_RESULT_SUMMARY_LENGTH = 6000 @@ -26,7 +27,7 @@ export const createActionPlanRecord = async ( return payload.create({ collection: 'actionPlan', - data: data as never, + data: hookFilledCreateData<'actionPlan'>(data), depth: 0, user: actor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/demand.ts b/src/app/(campaign)/campanha/actions/demand.ts index 1df9bf26..68085ed3 100644 --- a/src/app/(campaign)/campanha/actions/demand.ts +++ b/src/app/(campaign)/campanha/actions/demand.ts @@ -21,6 +21,7 @@ import { } from '@/utilities/campaignActionContext' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { hookFilledCreateData } from '@/utilities/hookFilledData' export const createCampaignDemandRecord = async ( payload: Payload, @@ -38,7 +39,7 @@ export const createCampaignDemandRecord = async ( // links a leader's own leadership and enforces the initial status. return payload.create({ collection: 'campaignDemand', - data: data as never, + data: hookFilledCreateData<'campaignDemand'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts index 20024099..aaa8bfc1 100644 --- a/src/app/(campaign)/campanha/actions/municipalityUpdate.ts +++ b/src/app/(campaign)/campanha/actions/municipalityUpdate.ts @@ -6,6 +6,7 @@ import { municipalityUpdateCreateSchema, type MunicipalityUpdateCreateInput } fr import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { hookFilledCreateData } from '@/utilities/hookFilledData' export const createMunicipalityUpdateRecord = async ( payload: Payload, @@ -21,7 +22,7 @@ export const createMunicipalityUpdateRecord = async ( return payload.create({ collection: 'municipalityUpdate', - data: data as never, + data: hookFilledCreateData<'municipalityUpdate'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/organization.ts b/src/app/(campaign)/campanha/actions/organization.ts index e3d76a5b..d4bfc1e1 100644 --- a/src/app/(campaign)/campanha/actions/organization.ts +++ b/src/app/(campaign)/campanha/actions/organization.ts @@ -10,6 +10,7 @@ import { } from '@/lib/schemas/organization' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const assertStaffActor = (payload: Payload, actor: CampaignUser): Promise => reloadStaffActor(payload, actor, 'Somente a coordenação e a assessoria gerenciam organizações.') @@ -30,7 +31,7 @@ export const createOrganizationRecord = async ( try { return await payload.create({ collection: 'organization', - data: data as never, + data: hookFilledCreateData<'organization'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/app/(campaign)/campanha/actions/stateDeputy.ts b/src/app/(campaign)/campanha/actions/stateDeputy.ts index b2cb6a21..dfb92d78 100644 --- a/src/app/(campaign)/campanha/actions/stateDeputy.ts +++ b/src/app/(campaign)/campanha/actions/stateDeputy.ts @@ -10,6 +10,7 @@ import { } from '@/lib/schemas/stateDeputy' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' +import { hookFilledCreateData } from '@/utilities/hookFilledData' const assertStaffActor = (payload: Payload, actor: CampaignUser): Promise => reloadStaffActor(payload, actor, 'Somente a coordenação e a assessoria gerenciam dobradinhas.') @@ -30,7 +31,7 @@ export const createStateDeputyRecord = async ( try { return await payload.create({ collection: 'stateDeputy', - data: data as never, + data: hookFilledCreateData<'stateDeputy'>(data), depth: 0, user: currentActor, overrideAccess: false, diff --git a/src/components/MetaPixel.tsx b/src/components/MetaPixel.tsx index e2266943..d6009b5c 100644 --- a/src/components/MetaPixel.tsx +++ b/src/components/MetaPixel.tsx @@ -19,6 +19,8 @@ fbq('track', 'PageView'); `}
-
- + + query ? `/campanha/dobradinhas?q=${encodeURIComponent(query)}` : '/campanha/dobradinhas' + } /> - - - {rows.length ? ( - <> -
- - - - Nome - Partido - Municípios - Lideranças - - - - {rows.map((row) => ( - - - - {row.name} - - - {row.party ?? '—'} - {row.municipalityCount} - {row.leadershipCount} - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'} -

- -
- - ) : ( - - - - - Nenhuma dobradinha cadastrada - - Cadastre deputados estaduais parceiros para vincular a municípios e lideranças. - - - - - - - )} + + {rows.length ? ( + <> +
+ + + + Nome + Partido + Municípios + Lideranças + + + + {rows.map((row) => ( + + + + {row.name} + + + {row.party ?? '—'} + {row.municipalityCount} + {row.leadershipCount} + + ))} + +
+
+
+

+ {totalDocs} {totalDocs === 1 ? 'dobradinha' : 'dobradinhas'} +

+ +
+ + ) : ( + + + + + Nenhuma dobradinha cadastrada + + Cadastre deputados estaduais parceiros para vincular a municípios e lideranças. + + + + + + + )} +
+
) } diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx index 3dc13fa3..1b3c4d36 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx @@ -1,3 +1,8 @@ +import { + CampaignListPendingBoundary, + CampaignListResults, +} from '@/components/campaign/CampaignListPending' +import { CampaignSearchForm } from '@/components/campaign/CampaignSearchForm' import config from '@payload-config' import { PlusIcon, SearchXIcon } from 'lucide-react' import Link from 'next/link' @@ -67,96 +72,99 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP -
- + + query ? `/campanha/liderancas?q=${encodeURIComponent(query)}` : '/campanha/liderancas' + } /> - - - {rows.length ? ( - <> -
- - - - Nome - Status - Praças - Organizações - Acesso ao app - - - - {rows.map((row) => ( - - - - {row.name} - - - - {row.supportStatus ? : '—'} - - - {row.municipalityNames.join(', ') || '—'} - - - {row.organizationNames.join(', ') || '—'} - - - - {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} - - - - ))} - -
-
-
-

- {totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'} -

- -
- - ) : ( - - - - - Nenhuma liderança encontrada - - Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas - Praças. - - - - - - - )} + + {rows.length ? ( + <> +
+ + + + Nome + Status + Praças + Organizações + Acesso ao app + + + + {rows.map((row) => ( + + + + {row.name} + + + + {row.supportStatus ? ( + + ) : ( + '—' + )} + + + {row.municipalityNames.join(', ') || '—'} + + + {row.organizationNames.join(', ') || '—'} + + + + {row.hasAppAccess ? 'Com acesso' : 'Sem acesso'} + + + + ))} + +
+
+
+

+ {totalDocs} {totalDocs === 1 ? 'liderança' : 'lideranças'} +

+ +
+ + ) : ( + + + + + Nenhuma liderança encontrada + + Cadastre a primeira liderança ou ajuste a busca. Você só vê lideranças das suas + Praças. + + + + + + + )} +
+ ) } diff --git a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx index 6a64d454..1049b98e 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import config from '@payload-config' import Link from 'next/link' import { notFound, redirect } from 'next/navigation' @@ -111,24 +112,32 @@ export default async function MunicipalityDetailPage({ ) : null} {activeTab === 'elections' ? ( - + // Streams below the header/tab chrome — the TSE baselines and the + // comparison table are the slowest reads on this page. + }> + + ) : null} {activeTab === 'leaderships' ? ( - + }> + + ) : null} {activeTab === 'updates' ? ( - + }> + + ) : null} {activeTab === 'demands' ? ( @@ -193,6 +202,13 @@ const OverviewTab = async ({ ) } +const MunicipalityTabFallback = () => ( +